# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Functional tests for morphological filtering operations."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import numpy as np

from tensorflow.python.framework import constant_op
from tensorflow.python.ops import gradient_checker
from tensorflow.python.ops import nn_ops
import tensorflow.python.ops.nn_grad  # pylint: disable=unused-import
from tensorflow.python.platform import test


class DilationTest(test.TestCase):

  def _VerifyValues(self, image, kernel, strides, rates, padding, out, use_gpu):
    """Verifies the output values of the dilation function.

    Args:
      image: Input tensor with shape: [batch, in_height, in_width, channels].
      kernel: Filter tensor with shape: [filter_height, filter_width, channels].
      strides: Output strides, specified as [stride_height, stride_width].
      rates: Atrous rates, specified as [rate_height, rate_width].
      padding: Padding type.
      out: Expected output.
      use_gpu: Whether we are running on GPU.
    """
    strides = [1] + strides + [1]
    rates = [1] + rates + [1]

    with self.test_session(use_gpu=use_gpu):
      out_tensor = nn_ops.dilation2d(
          constant_op.constant(image),
          constant_op.constant(kernel),
          strides=strides,
          rates=rates,
          padding=padding,
          name="dilation2d")
      self.assertAllClose(out, out_tensor.eval())

  def _testDilationValidPadding(self, use_gpu):
    # [1, 2, 2, 1]
    image = [[[[.1], [.2]], [[.3], [.4]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.0]]]
    # [1, 1, 1, 1]
    out = [[[[.5]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="VALID",
        out=out,
        use_gpu=use_gpu)

  def _testDilationSamePadding(self, use_gpu):
    # [1, 2, 2, 1]
    image = [[[[.1], [.2]], [[.3], [.4]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.0]]]
    # [1, 2, 2, 1]
    out = [[[[.5], [.6]], [[.7], [.8]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        out=out,
        use_gpu=use_gpu)

  def _testDilationSamePaddingDepth(self, use_gpu):
    # [1, 2, 2, 3]
    image = [[[[.1, .2, .0], [.2, .3, .1]], [[.3, .4, .2], [.4, .5, .3]]]]
    # [2, 2, 3]
    kernel = [[[.4, .5, .3], [.3, .4, .2]], [[.1, .2, .0], [.0, .1, -.1]]]
    # [1, 2, 2, 3]
    out = [[[[.5, .7, .3], [.6, .8, .4]], [[.7, .9, .5], [.8, 1., .6]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        out=out,
        use_gpu=use_gpu)

  def _testDilationSamePaddingBatch(self, use_gpu):
    # [2, 2, 2, 1]
    image = [[[[.1], [.2]], [[.3], [.4]]], [[[.2], [.3]], [[.4], [.5]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.0]]]
    # [2, 2, 2, 1]
    out = [[[[.5], [.6]], [[.7], [.8]]], [[[.6], [.7]], [[.8], [.9]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        out=out,
        use_gpu=use_gpu)

  def _testDilationValidPaddingNonSquareWindow(self, use_gpu):
    # [1, 2, 2, 1]
    image = [[[[.1], [.2]], [[.3], [.4]]]]
    # [1, 2, 1]
    kernel = [[[.4], [.3]]]
    # [1, 2, 1, 1]
    out = [[[[.5]], [[.7]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="VALID",
        out=out,
        use_gpu=use_gpu)

  def _testDilationSamePaddingRate(self, use_gpu):
    # [1, 3, 3, 1]
    image = [[[[.1], [.2], [.3]], [[.4], [.5], [.6]], [[.7], [.8], [.9]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.2]]]
    # Because rate = 2, the effective kernel is [3, 3, 1]:
    # kernel_eff = [[[.4], [.0], [.3]],
    #               [[.0], [.0], [.0]],
    #               [[.1], [.0], [.2]]]
    # [1, 3, 3, 1]
    out = [[[[.7], [.8], [.6]], [[1.0], [1.1], [.9]], [[.8], [.9], [.9]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[2, 2],
        padding="SAME",
        out=out,
        use_gpu=use_gpu)

  def _testDilationValidPaddingUnevenStride(self, use_gpu):
    # [1, 3, 3, 1]
    image = [[[[.1], [.2], [.3], [.4]], [[.5], [.6], [.7], [.8]],
              [[.9], [1.0], [1.1], [1.2]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.2]]]
    # [1, 2, 2, 1]
    out = [[[[.8], [1.0]], [[1.2], [1.4]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 2],
        rates=[1, 1],
        padding="VALID",
        out=out,
        use_gpu=use_gpu)

  def testDilation(self):
    for use_gpu in True, False:
      self._testDilationValidPadding(use_gpu)
      self._testDilationSamePadding(use_gpu)
      self._testDilationSamePaddingDepth(use_gpu)
      self._testDilationSamePaddingBatch(use_gpu)
      self._testDilationValidPaddingNonSquareWindow(use_gpu)
      self._testDilationSamePaddingRate(use_gpu)
      self._testDilationValidPaddingUnevenStride(use_gpu)

  def _ConstructAndTestGradient(self, image_shape, kernel_shape, strides, rates,
                                padding, use_gpu):
    """Verifies the gradients of the dilation function.

    Args:
      image_shape: Input shape, [batch, in_height, in_width, channels].
      kernel_shape: Filter shape, [filter_height, filter_width, channels].
      strides: Output strides, specified as [stride_height, stride_width].
      rates: Atrous rates, specified as [rate_height, rate_width].
      padding: Padding type.
      use_gpu: Whether we are running on GPU.
    """
    assert image_shape[3] == kernel_shape[2]

    np.random.seed(1)  # Make it reproducible.
    image = np.random.random_sample(image_shape).astype(np.float32)
    kernel = np.random.random_sample(kernel_shape).astype(np.float32)
    image_init = np.random.random_sample(image_shape).astype(np.float32)
    kernel_init = np.random.random_sample(kernel_shape).astype(np.float32)

    strides = [1] + strides + [1]
    rates = [1] + rates + [1]

    with self.test_session(use_gpu=use_gpu):
      image_tensor = constant_op.constant(
          image, shape=image_shape, name="input")
      kernel_tensor = constant_op.constant(
          kernel, shape=kernel_shape, name="filter")
      out_tensor = nn_ops.dilation2d(
          image_tensor,
          kernel_tensor,
          strides=strides,
          rates=rates,
          padding=padding,
          name="dilation2d")
      out_shape = out_tensor.eval().shape

      # Small delta is necessary for argmax to remain the same.
      err = gradient_checker.compute_gradient_error(
          [image_tensor, kernel_tensor], [image_shape, kernel_shape],
          out_tensor,
          out_shape, [image_init, kernel_init],
          delta=1e-3)

    print("Dilation gradient error = %f" % err)
    self.assertLess(err, 1e-4)

  def _testDilationGradValidPadding_1x1x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 1],
        kernel_shape=[1, 1, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="VALID",
        use_gpu=use_gpu)

  def _testDilationGradSamePadding_1x1x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 1],
        kernel_shape=[1, 1, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def _testDilationGradSamePadding_1x1x2(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 2],
        kernel_shape=[1, 1, 2],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def _testDilationGradValidPadding_2x2x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 1],
        kernel_shape=[2, 2, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="VALID",
        use_gpu=use_gpu)

  def _testDilationGradSamePadding_2x2x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 1],
        kernel_shape=[2, 2, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def _testDilationGradSamePaddingBatch_2x2x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[4, 3, 3, 1],
        kernel_shape=[2, 2, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def _testDilationGradSamePadding_2x2x4(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 4],
        kernel_shape=[2, 2, 4],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def testDilationGrad(self):
    for use_gpu in True, False:
      self._testDilationGradValidPadding_1x1x1(use_gpu)
      self._testDilationGradSamePadding_1x1x1(use_gpu)
      self._testDilationGradSamePadding_1x1x2(use_gpu)
      self._testDilationGradValidPadding_2x2x1(use_gpu)
      self._testDilationGradSamePadding_2x2x1(use_gpu)
      self._testDilationGradSamePaddingBatch_2x2x1(use_gpu)
      self._testDilationGradSamePadding_2x2x4(use_gpu)


class ErosionTest(test.TestCase):

  def _VerifyValues(self, image, kernel, strides, rates, padding, out, use_gpu):
    """Verifies the output values of the erosion function.

    Args:
      image: Input tensor with shape: [batch, in_height, in_width, channels].
      kernel: Filter tensor with shape: [filter_height, filter_width, channels].
      strides: Output strides, specified as [stride_height, stride_width].
      rates: Atrous rates, specified as [rate_height, rate_width].
      padding: Padding type.
      out: Expected output.
      use_gpu: Whether we are running on GPU.
    """
    strides = [1] + strides + [1]
    rates = [1] + rates + [1]

    with self.test_session(use_gpu=use_gpu):
      out_tensor = nn_ops.erosion2d(
          constant_op.constant(image),
          constant_op.constant(kernel),
          strides=strides,
          rates=rates,
          padding=padding,
          name="erosion2d")
      self.assertAllClose(out, out_tensor.eval())

  def _testErosionValidPadding(self, use_gpu):
    # [1, 2, 2, 1]
    image = [[[[.1], [.2]], [[.3], [.4]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.0]]]
    # [1, 1, 1, 1]
    out = [[[[.0]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="VALID",
        out=out,
        use_gpu=use_gpu)

  def _testErosionSamePadding(self, use_gpu):
    # [1, 2, 2, 1]
    image = [[[[.1], [.2]], [[.3], [.4]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.0]]]
    # [1, 2, 2, 1]
    out = [[[[.0], [.1]], [[.3], [.4]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        out=out,
        use_gpu=use_gpu)

  def _testErosionSamePaddingDepth(self, use_gpu):
    # [1, 2, 2, 3]
    image = [[[[.1, .2, .0], [.2, .3, .1]], [[.3, .4, .2], [.4, .5, .3]]]]
    # [2, 2, 3]
    kernel = [[[.4, .5, .3], [.3, .4, .2]], [[.1, .2, .0], [.0, .1, -.1]]]
    # [1, 2, 2, 3]
    out = [[[[.0, .0, .0], [.1, .1, .1]], [[.3, .3, .3], [.4, .4, .4]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        out=out,
        use_gpu=use_gpu)

  def _testErosionSamePaddingBatch(self, use_gpu):
    # [2, 2, 2, 1]
    image = [[[[.1], [.2]], [[.3], [.4]]], [[[.2], [.3]], [[.4], [.5]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.0]]]
    # [2, 2, 2, 1]
    out = [[[[.0], [.1]], [[.3], [.4]]], [[[.1], [.2]], [[.4], [.5]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        out=out,
        use_gpu=use_gpu)

  def _testErosionValidPaddingNonSquareWindow(self, use_gpu):
    # [1, 2, 2, 1]
    image = [[[[.1], [.2]], [[.3], [.4]]]]
    # [1, 2, 1]
    kernel = [[[.4], [.3]]]
    # [1, 2, 1, 1]
    out = [[[[-.2]], [[.0]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[1, 1],
        padding="VALID",
        out=out,
        use_gpu=use_gpu)

  def _testErosionSamePaddingRate(self, use_gpu):
    # [1, 3, 3, 1]
    image = [[[[.1], [.2], [.3]], [[.4], [.5], [.6]], [[.7], [.8], [.9]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.2]]]
    # Because rate = 2, the effective kernel is [3, 3, 1]:
    # kernel_eff = [[[.4], [.0], [.3]],
    #               [[.0], [.0], [.0]],
    #               [[.1], [.0], [.2]]]
    # [1, 3, 3, 1]
    out = [[[[.1], [.1], [.2]], [[0.1], [-.1], [.0]], [[.4], [.2], [.3]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 1],
        rates=[2, 2],
        padding="SAME",
        out=out,
        use_gpu=use_gpu)

  def _testErosionValidPaddingUnevenStride(self, use_gpu):
    # [1, 3, 3, 1]
    image = [[[[.1], [.2], [.3], [.4]], [[.5], [.6], [.7], [.8]],
              [[.9], [1.0], [1.1], [1.2]]]]
    # [2, 2, 1]
    kernel = [[[.4], [.3]], [[.1], [.2]]]
    # [1, 2, 2, 1]
    out = [[[[-.1], [.1]], [[.3], [.5]]]]
    self._VerifyValues(
        image,
        kernel,
        strides=[1, 2],
        rates=[1, 1],
        padding="VALID",
        out=out,
        use_gpu=use_gpu)

  def testErosion(self):
    for use_gpu in True, False:
      self._testErosionValidPadding(use_gpu)
      self._testErosionSamePadding(use_gpu)
      self._testErosionSamePaddingDepth(use_gpu)
      self._testErosionSamePaddingBatch(use_gpu)
      self._testErosionValidPaddingNonSquareWindow(use_gpu)
      self._testErosionSamePaddingRate(use_gpu)
      self._testErosionValidPaddingUnevenStride(use_gpu)

  def _ConstructAndTestGradient(self, image_shape, kernel_shape, strides, rates,
                                padding, use_gpu):
    """Verifies the gradients of the erosion function.

    Args:
      image_shape: Input shape, [batch, in_height, in_width, channels].
      kernel_shape: Filter shape, [filter_height, filter_width, channels].
      strides: Output strides, specified as [stride_height, stride_width].
      rates: Atrous rates, specified as [rate_height, rate_width].
      padding: Padding type.
      use_gpu: Whether we are running on GPU.
    """
    assert image_shape[3] == kernel_shape[2]

    np.random.seed(1)  # Make it reproducible.
    image = np.random.random_sample(image_shape).astype(np.float32)
    kernel = np.random.random_sample(kernel_shape).astype(np.float32)
    image_init = np.random.random_sample(image_shape).astype(np.float32)
    kernel_init = np.random.random_sample(kernel_shape).astype(np.float32)

    strides = [1] + strides + [1]
    rates = [1] + rates + [1]

    with self.test_session(use_gpu=use_gpu):
      image_tensor = constant_op.constant(
          image, shape=image_shape, name="input")
      kernel_tensor = constant_op.constant(
          kernel, shape=kernel_shape, name="filter")
      out_tensor = nn_ops.erosion2d(
          image_tensor,
          kernel_tensor,
          strides=strides,
          rates=rates,
          padding=padding,
          name="erosion2d")
      out_shape = out_tensor.eval().shape

      # Small delta is necessary for argmax to remain the same.
      err = gradient_checker.compute_gradient_error(
          [image_tensor, kernel_tensor], [image_shape, kernel_shape],
          out_tensor,
          out_shape, [image_init, kernel_init],
          delta=1e-3)

    print("Erosion gradient error = %f" % err)
    self.assertLess(err, 1e-4)

  def _testErosionGradValidPadding_1x1x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 1],
        kernel_shape=[1, 1, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="VALID",
        use_gpu=use_gpu)

  def _testErosionGradSamePadding_1x1x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 1],
        kernel_shape=[1, 1, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def _testErosionGradSamePadding_1x1x2(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 2],
        kernel_shape=[1, 1, 2],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def _testErosionGradValidPadding_2x2x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 1],
        kernel_shape=[2, 2, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="VALID",
        use_gpu=use_gpu)

  def _testErosionGradSamePadding_2x2x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 1],
        kernel_shape=[2, 2, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def _testErosionGradSamePaddingBatch_2x2x1(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[4, 3, 3, 1],
        kernel_shape=[2, 2, 1],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def _testErosionGradSamePadding_2x2x4(self, use_gpu):
    self._ConstructAndTestGradient(
        image_shape=[1, 3, 3, 4],
        kernel_shape=[2, 2, 4],
        strides=[1, 1],
        rates=[1, 1],
        padding="SAME",
        use_gpu=use_gpu)

  def testErosionGrad(self):
    for use_gpu in True, False:
      self._testErosionGradValidPadding_1x1x1(use_gpu)
      self._testErosionGradSamePadding_1x1x1(use_gpu)
      self._testErosionGradSamePadding_1x1x2(use_gpu)
      self._testErosionGradValidPadding_2x2x1(use_gpu)
      self._testErosionGradSamePadding_2x2x1(use_gpu)
      self._testErosionGradSamePaddingBatch_2x2x1(use_gpu)
      self._testErosionGradSamePadding_2x2x4(use_gpu)


if __name__ == "__main__":
  test.main()
