"""Tests for google3.third_party.tensorflow_models.object_detection.meta_architectures.deepmac_meta_arch."""

import functools
import math
import random
import unittest

from absl.testing import parameterized
import numpy as np
import tensorflow as tf

from google.protobuf import text_format
from object_detection.core import losses
from object_detection.core import preprocessor
from object_detection.meta_architectures import center_net_meta_arch
from object_detection.meta_architectures import deepmac_meta_arch
from object_detection.protos import center_net_pb2
from object_detection.utils import tf_version


def _logit(probability):
  return math.log(probability / (1. - probability))


LOGIT_HALF = _logit(0.5)
LOGIT_QUARTER = _logit(0.25)


class DummyFeatureExtractor(center_net_meta_arch.CenterNetFeatureExtractor):

  def __init__(self,
               channel_means,
               channel_stds,
               bgr_ordering,
               num_feature_outputs,
               stride):
    self._num_feature_outputs = num_feature_outputs
    self._stride = stride
    super(DummyFeatureExtractor, self).__init__(
        channel_means=channel_means, channel_stds=channel_stds,
        bgr_ordering=bgr_ordering)

  def predict(self):
    pass

  def loss(self):
    pass

  def postprocess(self):
    pass

  def call(self, inputs):
    batch_size, input_height, input_width, _ = inputs.shape
    fake_output = tf.ones([
        batch_size, input_height // self._stride, input_width // self._stride,
        64
    ], dtype=tf.float32)
    return [fake_output] * self._num_feature_outputs

  @property
  def out_stride(self):
    return self._stride

  @property
  def num_feature_outputs(self):
    return self._num_feature_outputs


class MockMaskNet(tf.keras.layers.Layer):

  def __call__(self, instance_embedding, pixel_embedding, training):
    return tf.zeros_like(pixel_embedding[:, :, :, 0]) + 0.9


def build_meta_arch(**override_params):
  """Builds the DeepMAC meta architecture."""

  params = dict(
      predict_full_resolution_masks=False,
      use_instance_embedding=True,
      mask_num_subsamples=-1,
      network_type='hourglass10',
      use_xy=True,
      pixel_embedding_dim=2,
      dice_loss_prediction_probability=False,
      feature_consistency_threshold=0.5,
      use_dice_loss=False,
      box_consistency_loss_normalize='normalize_auto',
      box_consistency_tightness=False,
      task_loss_weight=1.0,
      feature_consistency_loss_weight=1.0,
      box_consistency_loss_weight=1.0,
      num_init_channels=8,
      dim=8,
      allowed_masked_classes_ids=[],
      mask_size=16,
      postprocess_crop_size=128,
      max_roi_jitter_ratio=0.0,
      roi_jitter_mode='default',
      feature_consistency_dilation=2,
      feature_consistency_warmup_steps=0,
      feature_consistency_warmup_start=0,
      use_only_last_stage=True,
      augmented_self_supervision_max_translation=0.0,
      augmented_self_supervision_loss_weight=0.0,
      augmented_self_supervision_flip_probability=0.0,
      augmented_self_supervision_warmup_start=0,
      augmented_self_supervision_warmup_steps=0,
      augmented_self_supervision_loss='loss_dice',
      augmented_self_supervision_scale_min=1.0,
      augmented_self_supervision_scale_max=1.0,
      pointly_supervised_keypoint_loss_weight=1.0,
      ignore_per_class_box_overlap=False,
      feature_consistency_type='consistency_default_lab',
      feature_consistency_comparison='comparison_default_gaussian')

  params.update(override_params)

  feature_extractor = DummyFeatureExtractor(
      channel_means=(1.0, 2.0, 3.0),
      channel_stds=(10., 20., 30.),
      bgr_ordering=False,
      num_feature_outputs=2,
      stride=4)
  image_resizer_fn = functools.partial(
      preprocessor.resize_to_range,
      min_dimension=128,
      max_dimension=128,
      pad_to_max_dimesnion=True)

  object_center_params = center_net_meta_arch.ObjectCenterParams(
      classification_loss=losses.WeightedSigmoidClassificationLoss(),
      object_center_loss_weight=1.0,
      min_box_overlap_iou=1.0,
      max_box_predictions=5,
      use_labeled_classes=False)

  use_dice_loss = params.pop('use_dice_loss')
  dice_loss_prediction_prob = params.pop('dice_loss_prediction_probability')
  if use_dice_loss:
    classification_loss = losses.WeightedDiceClassificationLoss(
        squared_normalization=False,
        is_prediction_probability=dice_loss_prediction_prob)
  else:
    classification_loss = losses.WeightedSigmoidClassificationLoss()

  deepmac_params = deepmac_meta_arch.DeepMACParams(
      classification_loss=classification_loss,
      **params
  )

  object_detection_params = center_net_meta_arch.ObjectDetectionParams(
      localization_loss=losses.L1LocalizationLoss(),
      offset_loss_weight=1.0,
      scale_loss_weight=0.1
  )

  return deepmac_meta_arch.DeepMACMetaArch(
      is_training=True,
      add_summaries=False,
      num_classes=6,
      feature_extractor=feature_extractor,
      object_center_params=object_center_params,
      deepmac_params=deepmac_params,
      object_detection_params=object_detection_params,
      image_resizer_fn=image_resizer_fn)


DEEPMAC_PROTO_TEXT = """
  dim: 153
  task_loss_weight: 5.0
  pixel_embedding_dim: 8
  use_xy: false
  use_instance_embedding: false
  network_type: "cond_inst3"

  classification_loss {
    weighted_dice_classification_loss {
      squared_normalization: false
      is_prediction_probability: false
    }
  }
  jitter_mode: EXPAND_SYMMETRIC_XY
  max_roi_jitter_ratio: 0.0
  predict_full_resolution_masks: true
  allowed_masked_classes_ids: [99]
  box_consistency_loss_weight: 1.0
  feature_consistency_loss_weight: 1.0
  feature_consistency_threshold: 0.1

  box_consistency_tightness: false
  box_consistency_loss_normalize: NORMALIZE_AUTO
  feature_consistency_warmup_steps: 20
  feature_consistency_warmup_start: 10
  use_only_last_stage: false
  augmented_self_supervision_warmup_start: 13
  augmented_self_supervision_warmup_steps: 14
  augmented_self_supervision_loss: LOSS_MSE
  augmented_self_supervision_loss_weight: 11.0
  augmented_self_supervision_max_translation: 2.5
  augmented_self_supervision_flip_probability: 0.9
  augmented_self_supervision_scale_min: 0.42
  augmented_self_supervision_scale_max: 1.42
  pointly_supervised_keypoint_loss_weight: 0.13
  ignore_per_class_box_overlap: true
  feature_consistency_type: CONSISTENCY_FEATURE_MAP
  feature_consistency_comparison: COMPARISON_NORMALIZED_DOTPROD

"""


@unittest.skipIf(tf_version.is_tf1(), 'Skipping TF2.X only test.')
class DeepMACUtilsTest(tf.test.TestCase, parameterized.TestCase):

  def test_proto_parse(self):

    proto = center_net_pb2.CenterNet().DeepMACMaskEstimation()
    text_format.Parse(DEEPMAC_PROTO_TEXT, proto)
    params = deepmac_meta_arch.deepmac_proto_to_params(proto)
    self.assertIsInstance(params, deepmac_meta_arch.DeepMACParams)
    self.assertEqual(params.num_init_channels, 64)
    self.assertEqual(params.dim, 153)
    self.assertEqual(params.box_consistency_loss_normalize, 'normalize_auto')
    self.assertFalse(params.use_only_last_stage)
    self.assertEqual(params.augmented_self_supervision_warmup_start, 13)
    self.assertEqual(params.augmented_self_supervision_warmup_steps, 14)
    self.assertEqual(params.augmented_self_supervision_loss, 'loss_mse')
    self.assertEqual(params.augmented_self_supervision_loss_weight, 11.0)
    self.assertEqual(params.augmented_self_supervision_max_translation, 2.5)
    self.assertAlmostEqual(
        params.augmented_self_supervision_flip_probability, 0.9)
    self.assertAlmostEqual(
        params.augmented_self_supervision_scale_min, 0.42)
    self.assertAlmostEqual(
        params.augmented_self_supervision_scale_max, 1.42)
    self.assertAlmostEqual(
        params.pointly_supervised_keypoint_loss_weight, 0.13)
    self.assertTrue(params.ignore_per_class_box_overlap)
    self.assertEqual(params.feature_consistency_type, 'consistency_feature_map')
    self.assertEqual(
        params.feature_consistency_comparison, 'comparison_normalized_dotprod')

  def test_subsample_trivial(self):
    """Test subsampling masks."""

    boxes = np.arange(4).reshape(4, 1) * np.ones((4, 4))
    masks = np.arange(4).reshape(4, 1, 1) * np.ones((4, 32, 32))
    weights = np.ones(4)
    classes = tf.one_hot(tf.range(4), depth=4)

    result = deepmac_meta_arch.subsample_instances(
        classes, weights, boxes, masks, 4)
    self.assertAllClose(result[0], classes)
    self.assertAllClose(result[1], weights)
    self.assertAllClose(result[2], boxes)
    self.assertAllClose(result[3], masks)

  def test_filter_masked_classes(self):

    classes = np.zeros((2, 3, 5), dtype=np.float32)
    classes[0, 0] = [1.0, 0.0, 0.0, 0.0, 0.0]
    classes[0, 1] = [0.0, 1.0, 0.0, 0.0, 0.0]
    classes[0, 2] = [0.0, 0.0, 1.0, 0.0, 0.0]
    classes[1, 0] = [0.0, 0.0, 0.0, 1.0, 0.0]
    classes[1, 1] = [0.0, 0.0, 0.0, 0.0, 1.0]
    classes[1, 2] = [0.0, 0.0, 0.0, 0.0, 1.0]
    classes = tf.constant(classes)

    weights = tf.constant([[1.0, 1.0, 1.0], [1.0, 1.0, 0.0]])
    masks = tf.ones((2, 3, 32, 32), dtype=tf.float32)

    classes, weights, masks = deepmac_meta_arch.filter_masked_classes(
        [3, 4], classes, weights, masks)
    expected_classes = np.zeros((2, 3, 5))
    expected_classes[0, 0] = [0.0, 0.0, 0.0, 0.0, 0.0]
    expected_classes[0, 1] = [0.0, 0.0, 0.0, 0.0, 0.0]
    expected_classes[0, 2] = [0.0, 0.0, 1.0, 0.0, 0.0]
    expected_classes[1, 0] = [0.0, 0.0, 0.0, 1.0, 0.0]
    expected_classes[1, 1] = [0.0, 0.0, 0.0, 0.0, 0.0]
    expected_classes[1, 2] = [0.0, 0.0, 0.0, 0.0, 0.0]

    self.assertAllClose(expected_classes, classes.numpy())
    self.assertAllClose(np.array(([0.0, 0.0, 1.0], [1.0, 0.0, 0.0])), weights)

    self.assertAllClose(masks[0, 0], np.zeros((32, 32)))
    self.assertAllClose(masks[0, 1], np.zeros((32, 32)))
    self.assertAllClose(masks[0, 2], np.ones((32, 32)))
    self.assertAllClose(masks[1, 0], np.ones((32, 32)))
    self.assertAllClose(masks[1, 1], np.zeros((32, 32)))

  def test_fill_boxes(self):

    boxes = tf.constant([[[0., 0., 0.5, 0.5], [0.5, 0.5, 1.0, 1.0]],
                         [[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0]]])

    filled_boxes = deepmac_meta_arch.fill_boxes(boxes, 32, 32)
    expected = np.zeros((2, 2, 32, 32))
    expected[0, 0, :17, :17] = 1.0
    expected[0, 1, 16:, 16:] = 1.0
    expected[1, 0, :, :] = 1.0

    filled_boxes = filled_boxes.numpy()
    self.assertAllClose(expected[0, 0], filled_boxes[0, 0], rtol=1e-3)
    self.assertAllClose(expected[0, 1], filled_boxes[0, 1], rtol=1e-3)
    self.assertAllClose(expected[1, 0], filled_boxes[1, 0], rtol=1e-3)

  def test_flatten_and_unpack(self):

    t = tf.random.uniform((2, 3, 4, 5, 6))
    flatten = tf.function(deepmac_meta_arch.flatten_first2_dims)
    unpack = tf.function(deepmac_meta_arch.unpack_first2_dims)
    result, d1, d2 = flatten(t)
    result = unpack(result, d1, d2)
    self.assertAllClose(result.numpy(), t)

  def test_crop_and_resize_instance_masks(self):

    boxes = tf.zeros((8, 5, 4))
    masks = tf.zeros((8, 5, 128, 128))
    output = deepmac_meta_arch.crop_and_resize_instance_masks(
        masks, boxes, 32)
    self.assertEqual(output.shape, (8, 5, 32, 32))

  def test_embedding_projection_prob_shape(self):
    dist = deepmac_meta_arch.embedding_projection(
        tf.ones((4, 32, 32, 8)), tf.zeros((4, 32, 32, 8)))
    self.assertEqual(dist.shape, (4, 32, 32, 1))

  @parameterized.parameters([1e-20, 1e20])
  def test_embedding_projection_value(self, value):
    dist = deepmac_meta_arch.embedding_projection(
        tf.zeros((1, 1, 1, 8)), value + tf.zeros((1, 1, 1, 8))).numpy()
    max_float = np.finfo(dist.dtype).max
    self.assertLess(dist.max(), max_float)
    self.assertGreater(dist.max(), -max_float)

  @parameterized.named_parameters(
      [('no_conv_shortcut', (False,)),
       ('conv_shortcut', (True,))]
      )
  def test_res_dense_block(self, conv_shortcut):

    net = deepmac_meta_arch.DenseResidualBlock(32, conv_shortcut)
    out = net(tf.zeros((2, 32)))
    self.assertEqual(out.shape, (2, 32))

  @parameterized.parameters(
      [4, 8, 20]
  )
  def test_dense_resnet(self, num_layers):

    net = deepmac_meta_arch.DenseResNet(num_layers, 16, 8)
    out = net(tf.zeros((2, 24)))
    self.assertEqual(out.shape, (2, 8))

  def test_generate_2d_neighbors_shape(self):

    inp = tf.zeros((5, 13, 14, 3))
    out = deepmac_meta_arch.generate_2d_neighbors(inp)
    self.assertEqual((8, 5, 13, 14, 3), out.shape)

  def test_generate_2d_neighbors(self):
    inp = np.arange(16).reshape(4, 4).astype(np.float32)
    inp = tf.stack([inp, inp * 2], axis=2)
    inp = tf.reshape(inp, (1, 4, 4, 2))
    out = deepmac_meta_arch.generate_2d_neighbors(inp, dilation=1)
    self.assertEqual((8, 1, 4, 4, 2), out.shape)

    for i in range(2):
      expected = np.array([0, 1, 2, 4, 6, 8, 9, 10]) * (i + 1)
      self.assertAllEqual(out[:, 0, 1, 1, i], expected)

      expected = np.array([1, 2, 3, 5, 7, 9, 10, 11]) * (i + 1)
      self.assertAllEqual(out[:, 0, 1, 2, i], expected)

      expected = np.array([4, 5, 6, 8, 10, 12, 13, 14]) * (i + 1)
      self.assertAllEqual(out[:, 0, 2, 1, i], expected)

      expected = np.array([5, 6, 7, 9, 11, 13, 14, 15]) * (i + 1)
      self.assertAllEqual(out[:, 0, 2, 2, i], expected)

  def test_generate_2d_neighbors_dilation2(self):
    inp = np.arange(16).reshape(1, 4, 4, 1).astype(np.float32)
    out = deepmac_meta_arch.generate_2d_neighbors(inp, dilation=2)
    self.assertEqual((8, 1, 4, 4, 1), out.shape)

    expected = np.array([0, 0, 0, 0, 2, 0, 8, 10])
    self.assertAllEqual(out[:, 0, 0, 0, 0], expected)

  def test_dilated_similarity_shape(self):
    fmap = tf.zeros((5, 32, 32, 9))
    similarity = deepmac_meta_arch.dilated_cross_pixel_similarity(
        fmap)
    self.assertEqual((8, 5, 32, 32), similarity.shape)

  def test_dilated_similarity(self):

    fmap = np.zeros((1, 5, 5, 2), dtype=np.float32)

    fmap[0, 0, 0, :] = 1.0
    fmap[0, 4, 4, :] = 1.0

    similarity = deepmac_meta_arch.dilated_cross_pixel_similarity(
        fmap, theta=1.0, dilation=2)
    self.assertAlmostEqual(similarity.numpy()[0, 0, 2, 2],
                           np.exp(-np.sqrt(2)))

  def test_dilated_same_instance_mask_shape(self):
    instances = tf.zeros((2, 5, 32, 32))
    output = deepmac_meta_arch.dilated_cross_same_mask_label(instances)
    self.assertEqual((8, 2, 5, 32, 32), output.shape)

  def test_dilated_same_instance_mask(self):
    instances = np.zeros((3, 2, 5, 5), dtype=np.float32)
    instances[0, 0, 0, 0] = 1.0
    instances[0, 0, 2, 2] = 1.0
    instances[0, 0, 4, 4] = 1.0

    instances[2, 0, 0, 0] = 1.0
    instances[2, 0, 2, 2] = 1.0
    instances[2, 0, 4, 4] = 0.0

    output = deepmac_meta_arch.dilated_cross_same_mask_label(instances).numpy()
    self.assertAllClose(np.ones((8, 2, 5, 5)), output[:, 1, :, :])
    self.assertAllClose([1, 0, 0, 0, 0, 0, 0, 1], output[:, 0, 0, 2, 2])
    self.assertAllClose([1, 0, 0, 0, 0, 0, 0, 0], output[:, 2, 0, 2, 2])

  def test_per_pixel_single_conv_multiple_instance(self):

    inp = tf.zeros((5, 32, 32, 7))
    params = tf.zeros((5, 7*8 + 8))

    out = deepmac_meta_arch._per_pixel_single_conv(inp, params, 8)
    self.assertEqual(out.shape, (5, 32, 32, 8))

  def test_per_pixel_conditional_conv_error(self):

    with self.assertRaises(ValueError):
      deepmac_meta_arch.per_pixel_conditional_conv(
          tf.zeros((10, 32, 32, 8)), tf.zeros((10, 2)), 8, 3)

  def test_per_pixel_conditional_conv_error_tf_func(self):

    with self.assertRaises(ValueError):
      func = tf.function(deepmac_meta_arch.per_pixel_conditional_conv)
      func(tf.zeros((10, 32, 32, 8)), tf.zeros((10, 2)), 8, 3)

  def test_per_pixel_conditional_conv_depth1_error(self):

    with self.assertRaises(ValueError):
      _ = deepmac_meta_arch.per_pixel_conditional_conv(
          tf.zeros((10, 32, 32, 7)), tf.zeros((10, 8)), 99, 1)

  @parameterized.parameters([
      {
          'num_input_channels': 7,
          'instance_embedding_dim': 8,
          'channels': 7,
          'depth': 1
      },
      {
          'num_input_channels': 7,
          'instance_embedding_dim': 82,
          'channels': 9,
          'depth': 2
      },
      {  # From https://arxiv.org/abs/2003.05664
          'num_input_channels': 10,
          'instance_embedding_dim': 169,
          'channels': 8,
          'depth': 3
      },
      {
          'num_input_channels': 8,
          'instance_embedding_dim': 433,
          'channels': 16,
          'depth': 3
      },
      {
          'num_input_channels': 8,
          'instance_embedding_dim': 1377,
          'channels': 32,
          'depth': 3
      },
      {
          'num_input_channels': 8,
          'instance_embedding_dim': 4801,
          'channels': 64,
          'depth': 3
      },
  ])
  def test_per_pixel_conditional_conv_shape(
      self, num_input_channels, instance_embedding_dim, channels, depth):

    out = deepmac_meta_arch.per_pixel_conditional_conv(
        tf.zeros((10, 32, 32, num_input_channels)),
        tf.zeros((10, instance_embedding_dim)), channels, depth)

    self.assertEqual(out.shape, (10, 32, 32, 1))

  def test_per_pixel_conditional_conv_value_depth1(self):

    input_tensor = tf.constant(np.array([1, 2, 3]))
    input_tensor = tf.reshape(input_tensor, (1, 1, 1, 3))
    instance_embedding = tf.constant(
        np.array([1, 10, 100, 1000]))
    instance_embedding = tf.reshape(instance_embedding, (1, 4))
    out = deepmac_meta_arch.per_pixel_conditional_conv(
        input_tensor, instance_embedding, channels=3, depth=1)

    expected_output = np.array([1321])
    expected_output = np.reshape(expected_output, (1, 1, 1, 1))
    self.assertAllClose(expected_output, out)

  def test_per_pixel_conditional_conv_value_depth2_single(self):

    input_tensor = tf.constant(np.array([2]))
    input_tensor = tf.reshape(input_tensor, (1, 1, 1, 1))
    instance_embedding = tf.constant(
        np.array([-2, 3, 100, 5]))
    instance_embedding = tf.reshape(instance_embedding, (1, 4))
    out = deepmac_meta_arch.per_pixel_conditional_conv(
        input_tensor, instance_embedding, channels=1, depth=2)

    expected_output = np.array([5])
    expected_output = np.reshape(expected_output, (1, 1, 1, 1))
    self.assertAllClose(expected_output, out)

  def test_per_pixel_conditional_conv_value_depth2_identity(self):

    input_tensor = tf.constant(np.array([1, 2]))
    input_tensor = tf.reshape(input_tensor, (1, 1, 1, 2))
    instance_embedding = tf.constant(
        np.array([1, 0, 0, 1, 1, -3, 5, 100, -9]))
    instance_embedding = tf.reshape(
        instance_embedding, (1, 9))
    out = deepmac_meta_arch.per_pixel_conditional_conv(
        input_tensor, instance_embedding, channels=2, depth=2)

    expected_output = np.array([1])
    expected_output = np.reshape(expected_output, (1, 1, 1, 1))
    self.assertAllClose(expected_output, out)

  def test_per_instance_no_class_overlap(self):
    boxes = tf.constant([[[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 0.4, 0.4]],
                         [[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0]]],
                        dtype=tf.float32)
    classes = tf.constant([[[0, 1, 0], [0, 1, 0]], [[0, 1, 0], [1, 0, 0]]],
                          dtype=tf.float32)
    output = deepmac_meta_arch.per_instance_no_class_overlap(
        classes, boxes, 2, 2)
    self.assertEqual(output.shape, (2, 2, 2, 2))
    self.assertAllClose(output[1], np.ones((2, 2, 2)))
    self.assertAllClose(output[0, 1], [[0., 1.0], [1.0, 1.0]])


@unittest.skipIf(tf_version.is_tf1(), 'Skipping TF2.X only test.')
class DeepMACMaskHeadTest(tf.test.TestCase, parameterized.TestCase):

  def test_mask_network_params_resnet4(self):
    net = deepmac_meta_arch.MaskHeadNetwork('resnet4', num_init_channels=8)
    _ = net(tf.zeros((2, 16)), tf.zeros((2, 32, 32, 16)), training=True)

    trainable_params = tf.reduce_sum([tf.reduce_prod(tf.shape(w)) for w in
                                      net.trainable_weights])

    self.assertEqual(trainable_params.numpy(), 8665)

  def test_mask_network_embedding_projection_small(self):

    net = deepmac_meta_arch.MaskHeadNetwork(
        'embedding_projection', num_init_channels=-1,
        use_instance_embedding=False)
    call_func = tf.function(net.__call__)

    out = call_func(1e6 + tf.zeros((2, 7)),
                    tf.zeros((2, 32, 32, 7)), training=True)
    self.assertEqual(out.shape, (2, 32, 32))
    self.assertAllGreater(out.numpy(), -np.inf)
    self.assertAllLess(out.numpy(), np.inf)

  @parameterized.parameters([
      {
          'mask_net': 'resnet4',
          'mask_net_channels': 8,
          'instance_embedding_dim': 4,
          'input_channels': 16,
          'use_instance_embedding': False
      },
      {
          'mask_net': 'hourglass10',
          'mask_net_channels': 8,
          'instance_embedding_dim': 4,
          'input_channels': 16,
          'use_instance_embedding': False
      },
      {
          'mask_net': 'hourglass20',
          'mask_net_channels': 8,
          'instance_embedding_dim': 4,
          'input_channels': 16,
          'use_instance_embedding': False
      },
      {
          'mask_net': 'cond_inst3',
          'mask_net_channels': 8,
          'instance_embedding_dim': 153,
          'input_channels': 8,
          'use_instance_embedding': False
      },
      {
          'mask_net': 'cond_inst3',
          'mask_net_channels': 8,
          'instance_embedding_dim': 169,
          'input_channels': 10,
          'use_instance_embedding': False
      },
      {
          'mask_net': 'cond_inst1',
          'mask_net_channels': 8,
          'instance_embedding_dim': 9,
          'input_channels': 8,
          'use_instance_embedding': False
      },
      {
          'mask_net': 'cond_inst2',
          'mask_net_channels': 8,
          'instance_embedding_dim': 81,
          'input_channels': 8,
          'use_instance_embedding': False
      },
  ])
  def test_mask_network(self, mask_net, mask_net_channels,
                        instance_embedding_dim, input_channels,
                        use_instance_embedding):

    net = deepmac_meta_arch.MaskHeadNetwork(
        mask_net, num_init_channels=mask_net_channels,
        use_instance_embedding=use_instance_embedding)
    call_func = tf.function(net.__call__)

    out = call_func(tf.zeros((2, instance_embedding_dim)),
                    tf.zeros((2, 32, 32, input_channels)), training=True)
    self.assertEqual(out.shape, (2, 32, 32))
    self.assertAllGreater(out.numpy(), -np.inf)
    self.assertAllLess(out.numpy(), np.inf)

    out = call_func(tf.zeros((2, instance_embedding_dim)),
                    tf.zeros((2, 32, 32, input_channels)), training=True)
    self.assertEqual(out.shape, (2, 32, 32))

    out = call_func(tf.zeros((0, instance_embedding_dim)),
                    tf.zeros((0, 32, 32, input_channels)), training=True)
    self.assertEqual(out.shape, (0, 32, 32))

  @parameterized.parameters(
      [
          dict(x=4, y=4, height=4, width=4),
          dict(x=1, y=2, height=3, width=4),
          dict(x=14, y=14, height=5, width=5),
      ]
  )
  def test_transform_images_and_boxes_identity(self, x, y, height, width):
    images = np.zeros((1, 32, 32, 3), dtype=np.float32)
    images[:, y:y + height, x:x + width, :] = 1.0
    boxes = tf.constant([[[y / 32., x / 32.,
                           y / 32. + height/32, x/32. + width / 32]]])

    zeros = tf.zeros(1)
    ones = tf.ones(1)
    falses = tf.zeros(1, dtype=tf.bool)
    images = tf.constant(images)
    images_out, boxes_out = deepmac_meta_arch.transform_images_and_boxes(
        images, boxes, zeros, zeros, ones, ones, falses)
    self.assertAllClose(images, images_out)
    self.assertAllClose(boxes, boxes_out)

    coords = np.argwhere(images_out.numpy()[0, :, :, 0] > 0.5)
    self.assertEqual(np.min(coords[:, 0]), y)
    self.assertEqual(np.min(coords[:, 1]), x)
    self.assertEqual(np.max(coords[:, 0]), y + height - 1)
    self.assertEqual(np.max(coords[:, 1]), x + width - 1)

  def test_transform_images_and_boxes(self):
    images = np.zeros((2, 32, 32, 3), dtype=np.float32)
    images[:, 14:19, 14:19, :] = 1.0
    boxes = tf.constant(
        [[[14.0 / 32, 14.0 / 32, 18.0 / 32, 18.0 / 32]] * 2] * 2)
    flip = tf.constant([False, False])

    scale_y0 = 2.0
    translate_y0 = 1.0
    scale_x0 = 4.0
    translate_x0 = 4.0

    scale_y1 = 3.0
    translate_y1 = 3.0
    scale_x1 = 0.5
    translate_x1 = 2.0
    ty = tf.constant([translate_y0/32, translate_y1/32])
    sy = tf.constant([1./scale_y0, 1.0 / scale_y1])

    tx = tf.constant([translate_x0/32, translate_x1/32])
    sx = tf.constant([1 / scale_x0, 1.0 / scale_x1])

    images = tf.constant(images)
    images_out, boxes_out = deepmac_meta_arch.transform_images_and_boxes(
        images, boxes, tx=tx, ty=ty, sx=sx, sy=sy, flip=flip)

    boxes_out = boxes_out.numpy() * 32
    coords = np.argwhere(images_out[0, :, :, 0] >= 0.9)
    ymin = np.min(coords[:, 0])
    ymax = np.max(coords[:, 0])
    xmin = np.min(coords[:, 1])
    xmax = np.max(coords[:, 1])

    self.assertAlmostEqual(
        ymin, 16 - 2*scale_y0 + translate_y0, delta=1)
    self.assertAlmostEqual(
        ymax, 16 + 2*scale_y0 + translate_y0, delta=1)
    self.assertAlmostEqual(
        xmin, 16 - 2*scale_x0 + translate_x0, delta=1)
    self.assertAlmostEqual(
        xmax, 16 + 2*scale_x0 + translate_x0, delta=1)
    self.assertAlmostEqual(ymin, boxes_out[0, 0, 0], delta=1)
    self.assertAlmostEqual(xmin, boxes_out[0, 0, 1], delta=1)
    self.assertAlmostEqual(ymax, boxes_out[0, 0, 2], delta=1)
    self.assertAlmostEqual(xmax, boxes_out[0, 0, 3], delta=1)

    coords = np.argwhere(images_out[1, :, :, 0] >= 0.9)
    ymin = np.min(coords[:, 0])
    ymax = np.max(coords[:, 0])
    xmin = np.min(coords[:, 1])
    xmax = np.max(coords[:, 1])

    self.assertAlmostEqual(
        ymin, 16 - 2*scale_y1 + translate_y1, delta=1)
    self.assertAlmostEqual(
        ymax, 16 + 2*scale_y1 + translate_y1, delta=1)
    self.assertAlmostEqual(
        xmin, 16 - 2*scale_x1 + translate_x1, delta=1)
    self.assertAlmostEqual(
        xmax, 16 + 2*scale_x1 + translate_x1, delta=1)
    self.assertAlmostEqual(ymin, boxes_out[1, 0, 0], delta=1)
    self.assertAlmostEqual(xmin, boxes_out[1, 0, 1], delta=1)
    self.assertAlmostEqual(ymax, boxes_out[1, 0, 2], delta=1)
    self.assertAlmostEqual(xmax, boxes_out[1, 0, 3], delta=1)

  def test_transform_images_and_boxes_flip(self):
    images = np.zeros((2, 2, 2, 1), dtype=np.float32)
    images[0, :, :, 0] = [[1, 2], [3, 4]]
    images[1, :, :, 0] = [[1, 2], [3, 4]]
    images = tf.constant(images)

    boxes = tf.constant(
        [[[0.1, 0.2, 0.3, 0.4]], [[0.1, 0.2, 0.3, 0.4]]], dtype=tf.float32)

    tx = ty = tf.zeros([2], dtype=tf.float32)
    sx = sy = tf.ones([2], dtype=tf.float32)
    flip = tf.constant([True, False])

    output_images, output_boxes = deepmac_meta_arch.transform_images_and_boxes(
        images, boxes, tx, ty, sx, sy, flip)

    expected_images = np.zeros((2, 2, 2, 1), dtype=np.float32)
    expected_images[0, :, :, 0] = [[2, 1], [4, 3]]
    expected_images[1, :, :, 0] = [[1, 2], [3, 4]]
    self.assertAllClose(output_boxes,
                        [[[0.1, 0.6, 0.3, 0.8]], [[0.1, 0.2, 0.3, 0.4]]])
    self.assertAllClose(expected_images, output_images)

  def test_transform_images_and_boxes_tf_function(self):
    func = tf.function(deepmac_meta_arch.transform_images_and_boxes)

    output, _ = func(images=tf.zeros((2, 32, 32, 3)), boxes=tf.zeros((2, 5, 4)),
                     tx=tf.zeros(2), ty=tf.zeros(2),
                     sx=tf.ones(2), sy=tf.ones(2),
                     flip=tf.zeros(2, dtype=tf.bool))
    self.assertEqual(output.shape, (2, 32, 32, 3))

  def test_transform_instance_masks(self):
    instance_masks = np.zeros((2, 10, 32, 32), dtype=np.float32)
    instance_masks[0, 0, 1, 1] = 1
    instance_masks[0, 1, 1, 1] = 1

    instance_masks[1, 0, 2, 2] = 1
    instance_masks[1, 1, 2, 2] = 1

    tx = ty = tf.constant([1., 2.]) / 32.0
    sx = sy = tf.ones(2, dtype=tf.float32)
    flip = tf.zeros(2, dtype=tf.bool)

    instance_masks = deepmac_meta_arch.transform_instance_masks(
        instance_masks, tx, ty, sx, sy, flip=flip)
    self.assertEqual(instance_masks.shape, (2, 10, 32, 32))
    self.assertAlmostEqual(
        instance_masks[0].numpy().sum(), 2.0)
    self.assertGreater(
        instance_masks[0, 0, 2, 2].numpy(), 0.5)
    self.assertGreater(
        instance_masks[0, 1, 2, 2].numpy(), 0.5)

    self.assertAlmostEqual(
        instance_masks[1].numpy().sum(), 2.0)
    self.assertGreater(
        instance_masks[1, 0, 4, 4].numpy(), 0.5)
    self.assertGreater(
        instance_masks[1, 1, 4, 4].numpy(), 0.5)

  def test_augment_image_and_deaugment_mask(self):

    img = np.zeros((1, 32, 32, 3), dtype=np.float32)

    img[0, 10:12, 10:12, :] = 1.0

    tx = ty = tf.constant([1.]) / 32.0
    sx = sy = tf.constant([1.0 / 2.0])
    flip = tf.constant([False])

    img = tf.constant(img)
    img_t, _ = deepmac_meta_arch.transform_images_and_boxes(
        images=img, boxes=None, tx=tx, ty=ty, sx=sx, sy=sy, flip=flip)
    self.assertAlmostEqual(img_t.numpy().sum(), 16 * 3)

    # Converting channels of the image to instances.
    masks = tf.transpose(img_t, (0, 3, 1, 2))

    masks_t = deepmac_meta_arch.transform_instance_masks(
        masks, tx=-tx, ty=-ty, sx=1.0/sx, sy=1.0/sy, flip=flip)

    self.assertAlmostEqual(masks_t.numpy().sum(), 4 * 3)

    coords = np.argwhere(masks_t[0, 0, :, :] >= 0.5)

    self.assertAlmostEqual(np.min(coords[:, 0]), 10, delta=1)
    self.assertAlmostEqual(np.max(coords[:, 0]), 12, delta=1)
    self.assertAlmostEqual(np.min(coords[:, 1]), 10, delta=1)
    self.assertAlmostEqual(np.max(coords[:, 1]), 12, delta=1)


@unittest.skipIf(tf_version.is_tf1(), 'Skipping TF2.X only test.')
class DeepMACMetaArchTest(tf.test.TestCase, parameterized.TestCase):

  # TODO(vighneshb): Add batch_size > 1 tests for loss functions.

  def setUp(self):  # pylint:disable=g-missing-super-call
    self.model = build_meta_arch()

  def test_get_mask_head_input(self):

    boxes = tf.constant([[[0., 0., 0.25, 0.25], [0.75, 0.75, 1.0, 1.0]],
                         [[0., 0., 0.25, 0.25], [0.75, 0.75, 1.0, 1.0]]],
                        dtype=tf.float32)

    pixel_embedding = np.zeros((2, 32, 32, 4), dtype=np.float32)
    pixel_embedding[0, :16, :16] = 1.0
    pixel_embedding[0, 16:, 16:] = 2.0
    pixel_embedding[1, :16, :16] = 3.0
    pixel_embedding[1, 16:, 16:] = 4.0

    pixel_embedding = tf.constant(pixel_embedding)

    mask_inputs = self.model._get_mask_head_input(boxes, pixel_embedding)
    self.assertEqual(mask_inputs.shape, (2, 2, 16, 16, 6))

    y_grid, x_grid = tf.meshgrid(np.linspace(-1.0, 1.0, 16),
                                 np.linspace(-1.0, 1.0, 16), indexing='ij')

    for i, j in ([0, 0], [0, 1], [1, 0], [1, 1]):
      self.assertAllClose(y_grid, mask_inputs[i, j, :, :, 0])
      self.assertAllClose(x_grid, mask_inputs[i, j, :, :, 1])

    zeros = np.zeros((16, 16, 4))
    self.assertAllClose(zeros + 1, mask_inputs[0, 0, :, :, 2:])
    self.assertAllClose(zeros + 2, mask_inputs[0, 1, :, :, 2:])
    self.assertAllClose(zeros + 3, mask_inputs[1, 0, :, :, 2:])
    self.assertAllClose(zeros + 4, mask_inputs[1, 1, :, :, 2:])

  def test_get_mask_head_input_no_crop_resize(self):

    model = build_meta_arch(predict_full_resolution_masks=True)
    boxes = tf.constant([[[0., 0., 1.0, 1.0], [0.0, 0.0, 0.5, 1.0]],
                         [[0.5, 0.5, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0]]])

    pixel_embedding_np = np.random.randn(2, 32, 32, 4).astype(np.float32)
    pixel_embedding = tf.constant(pixel_embedding_np)

    mask_inputs = model._get_mask_head_input(boxes, pixel_embedding)
    self.assertEqual(mask_inputs.shape, (2, 2, 32, 32, 6))

    y_grid, x_grid = tf.meshgrid(np.linspace(.0, 1.0, 32),
                                 np.linspace(.0, 1.0, 32), indexing='ij')

    self.assertAllClose(y_grid - 0.5, mask_inputs[0, 0, :, :, 0])
    self.assertAllClose(x_grid - 0.5, mask_inputs[0, 0, :, :, 1])

    self.assertAllClose(y_grid - 0.25, mask_inputs[0, 1, :, :, 0])
    self.assertAllClose(x_grid - 0.5, mask_inputs[0, 1, :, :, 1])

    self.assertAllClose(y_grid - 0.75, mask_inputs[1, 0, :, :, 0])
    self.assertAllClose(x_grid - 0.75, mask_inputs[1, 0, :, :, 1])

    self.assertAllClose(y_grid, mask_inputs[1, 1, :, :, 0])
    self.assertAllClose(x_grid, mask_inputs[1, 1, :, :, 1])

  def test_get_instance_embeddings(self):

    embeddings = np.zeros((2, 32, 32, 2))
    embeddings[0, 8, 8] = 1.0
    embeddings[0, 24, 16] = 2.0
    embeddings[1, 8, 16] = 3.0
    embeddings = tf.constant(embeddings)

    boxes = np.zeros((2, 2, 4), dtype=np.float32)
    boxes[0, 0] = [0.0, 0.0, 0.5, 0.5]
    boxes[0, 1] = [0.5, 0.0, 1.0, 1.0]
    boxes[1, 0] = [0.0, 0.0, 0.5, 1.0]

    boxes = tf.constant(boxes)

    center_embeddings = self.model._get_instance_embeddings(boxes, embeddings)

    self.assertAllClose(center_embeddings[0, 0], [1.0, 1.0])
    self.assertAllClose(center_embeddings[0, 1], [2.0, 2.0])
    self.assertAllClose(center_embeddings[1, 0], [3.0, 3.0])

  def test_get_groundtruth_mask_output(self):

    boxes = np.zeros((2, 2, 4))
    masks = np.zeros((2, 2, 32, 32))

    boxes[0, 0] = [0.0, 0.0, 0.25, 0.25]
    boxes[0, 1] = [0.75, 0.75, 1.0, 1.0]
    boxes[1, 0] = [0.0, 0.0, 0.5, 1.0]
    masks = np.zeros((2, 2, 32, 32), dtype=np.float32)
    masks[0, 0, :16, :16] = 0.5
    masks[0, 1, 16:, 16:] = 0.1
    masks[1, 0, :17, :] = 0.3
    masks = self.model._get_groundtruth_mask_output(boxes, masks)
    self.assertEqual(masks.shape, (2, 2, 16, 16))

    self.assertAllClose(masks[0, 0], np.zeros((16, 16)) + 0.5)
    self.assertAllClose(masks[0, 1], np.zeros((16, 16)) + 0.1)
    self.assertAllClose(masks[1, 0], np.zeros((16, 16)) + 0.3)

  def test_get_groundtruth_mask_output_no_crop_resize(self):

    model = build_meta_arch(predict_full_resolution_masks=True)
    boxes = tf.zeros((2, 5, 4))
    masks = tf.ones((2, 5, 32, 32))
    masks = model._get_groundtruth_mask_output(boxes, masks)
    self.assertAllClose(masks, np.ones((2, 5, 32, 32)))

  def test_predict(self):

    tf.keras.backend.set_learning_phase(True)
    self.model.provide_groundtruth(
        groundtruth_boxes_list=[tf.convert_to_tensor([[0., 0., 1., 1.]] * 5)],
        groundtruth_classes_list=[tf.one_hot([1, 0, 1, 1, 1], depth=6)],
        groundtruth_weights_list=[tf.ones(5)],
        groundtruth_masks_list=[tf.ones((5, 32, 32))])
    prediction = self.model.predict(tf.zeros((1, 32, 32, 3)), None)
    self.assertEqual(prediction['MASK_LOGITS_GT_BOXES'][0].shape,
                     (1, 5, 16, 16))

  def test_predict_self_supervised_deaugmented_mask_logits(self):

    tf.keras.backend.set_learning_phase(True)
    model = build_meta_arch(
        augmented_self_supervision_loss_weight=1.0,
        predict_full_resolution_masks=True)

    model.provide_groundtruth(
        groundtruth_boxes_list=[tf.convert_to_tensor([[0., 0., 1., 1.]] * 5)],
        groundtruth_classes_list=[tf.one_hot([1, 0, 1, 1, 1], depth=6)],
        groundtruth_weights_list=[tf.ones(5)],
        groundtruth_masks_list=[tf.ones((5, 32, 32))])
    prediction = model.predict(tf.zeros((1, 32, 32, 3)), None)
    self.assertEqual(prediction['MASK_LOGITS_GT_BOXES'][0].shape,
                     (1, 5, 8, 8))
    self.assertEqual(
        prediction['SELF_SUPERVISED_DEAUGMENTED_MASK_LOGITS'][0].shape,
        (1, 5, 8, 8))

  def test_loss(self):

    model = build_meta_arch()
    boxes = tf.constant([[[0.0, 0.0, 0.25, 0.25], [0.75, 0.75, 1.0, 1.0]]])
    masks = np.zeros((1, 2, 32, 32), dtype=np.float32)
    masks[0, 0, :16, :16] = 1.0
    masks[0, 1, 16:, 16:] = 1.0
    masks_pred = tf.fill((1, 2, 32, 32), 0.9)
    classes = tf.zeros((1, 2, 5))

    loss_dict = model._compute_deepmac_losses(
        boxes, masks_pred, masks, classes, tf.zeros((1, 16, 16, 3)))
    self.assertAllClose(
        loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION],
        np.zeros((1, 2)) - tf.math.log(tf.nn.sigmoid(0.9)))

  def test_loss_no_crop_resize(self):

    model = build_meta_arch(predict_full_resolution_masks=True)
    boxes = tf.constant([[[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0]]])
    masks = tf.ones((1, 2, 128, 128), dtype=tf.float32)
    masks_pred = tf.fill((1, 2, 32, 32), 0.9)
    classes = tf.zeros((1, 2, 5))

    loss_dict = model._compute_deepmac_losses(
        boxes, masks_pred, masks, classes, tf.zeros((1, 32, 32, 3)))
    self.assertAllClose(
        loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION],
        np.zeros((1, 2)) - tf.math.log(tf.nn.sigmoid(0.9)))

  def test_loss_no_crop_resize_dice(self):

    model = build_meta_arch(predict_full_resolution_masks=True,
                            use_dice_loss=True)
    boxes = tf.constant([[[0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0]]])
    masks = np.ones((1, 2, 128, 128), dtype=np.float32)
    masks = tf.constant(masks)
    masks_pred = tf.fill((1, 2, 32, 32), 0.9)
    classes = tf.zeros((1, 2, 5))

    loss_dict = model._compute_deepmac_losses(
        boxes, masks_pred, masks, classes, tf.zeros((1, 32, 32, 3)))
    pred = tf.nn.sigmoid(0.9)
    expected = (1.0 - ((2.0 * pred) / (1.0 + pred)))
    self.assertAllClose(loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION],
                        [[expected, expected]], rtol=1e-3)

  def test_empty_masks(self):

    boxes = tf.zeros([1, 0, 4])
    masks = tf.zeros([1, 0, 128, 128])
    classes = tf.zeros((1, 2, 5))

    loss_dict = self.model._compute_deepmac_losses(
        boxes, masks, masks, classes,
        tf.zeros((1, 16, 16, 3)))
    self.assertEqual(loss_dict[deepmac_meta_arch.DEEP_MASK_ESTIMATION].shape,
                     (1, 0))

  def test_postprocess(self):
    model = build_meta_arch()
    model._mask_net = MockMaskNet()
    boxes = np.zeros((2, 3, 4), dtype=np.float32)
    boxes[:, :, [0, 2]] = 0.0
    boxes[:, :, [1, 3]] = 8.0
    boxes = tf.constant(boxes)

    masks = model._postprocess_masks(
        boxes, tf.zeros((2, 32, 32, 2)), tf.zeros((2, 32, 32, 2)))
    prob = tf.nn.sigmoid(0.9).numpy()
    self.assertAllClose(masks, prob * np.ones((2, 3, 16, 16)))

  def test_postprocess_emb_proj(self):
    model = build_meta_arch(network_type='embedding_projection',
                            use_instance_embedding=False,
                            use_xy=False, pixel_embedding_dim=8,
                            use_dice_loss=True,
                            dice_loss_prediction_probability=True)
    boxes = np.zeros((2, 3, 4), dtype=np.float32)
    boxes[:, :, [0, 2]] = 0.0
    boxes[:, :, [1, 3]] = 8.0
    boxes = tf.constant(boxes)

    masks = model._postprocess_masks(
        boxes, tf.zeros((2, 32, 32, 2)), tf.zeros((2, 32, 32, 2)))
    self.assertEqual(masks.shape, (2, 3, 16, 16))

  def test_postprocess_emb_proj_fullres(self):
    model = build_meta_arch(network_type='embedding_projection',
                            predict_full_resolution_masks=True,
                            use_instance_embedding=False,
                            pixel_embedding_dim=8, use_xy=False,
                            use_dice_loss=True)
    boxes = np.zeros((2, 3, 4), dtype=np.float32)
    boxes = tf.constant(boxes)

    masks = model._postprocess_masks(
        boxes, tf.zeros((2, 32, 32, 2)), tf.zeros((2, 32, 32, 2)))
    self.assertEqual(masks.shape, (2, 3, 128, 128))

  def test_postprocess_no_crop_resize_shape(self):

    model = build_meta_arch(predict_full_resolution_masks=True)
    model._mask_net = MockMaskNet()
    boxes = np.zeros((2, 3, 4), dtype=np.float32)
    boxes[:, :, [0, 2]] = 0.0
    boxes[:, :, [1, 3]] = 8.0
    boxes = tf.constant(boxes)

    masks = model._postprocess_masks(
        boxes, tf.zeros((2, 32, 32, 2)), tf.zeros((2, 32, 32, 2)))
    prob = tf.nn.sigmoid(0.9).numpy()
    self.assertAllClose(masks, prob * np.ones((2, 3, 128, 128)))

  def test_transform_boxes_to_feature_coordinates(self):
    batch_size = 2
    model = build_meta_arch()
    model._mask_net = MockMaskNet()
    boxes = np.zeros((batch_size, 3, 4), dtype=np.float32)
    boxes[:, :, [0, 2]] = 0.1
    boxes[:, :, [1, 3]] = 0.5
    boxes = tf.constant(boxes)
    true_image_shapes = tf.constant([
        [64, 32, 3],  # Image 1 is padded during resizing.
        [64, 64, 3],  # Image 2 is not padded.
    ])
    resized_image_height = 64
    resized_image_width = 64
    resized_image_shape = [
        batch_size, resized_image_height, resized_image_width, 3
    ]

    feature_map_height = 32
    feature_map_width = 32
    instance_embedding = tf.zeros(
        (batch_size, feature_map_height, feature_map_width, 2))

    expected_boxes = np.array([
        [  # Image 1
            # 0.1 * (64 / resized_image_height) * feature_map_height -> 3.2
            # 0.5 * (32 / resized_image_width) * feature_map_width -> 8.0
            [3.2, 8., 3.2, 8.],
            [3.2, 8., 3.2, 8.],
            [3.2, 8., 3.2, 8.],
        ],
        [  # Image 2
            # 0.1 * (64 / resized_image_height) * feature_map_height -> 3.2
            # 0.5 * (64 / resized_image_width) * feature_map_width -> 16
            [3.2, 16., 3.2, 16.],
            [3.2, 16., 3.2, 16.],
            [3.2, 16., 3.2, 16.],
        ],
    ])

    box_strided = model._transform_boxes_to_feature_coordinates(
        boxes, true_image_shapes, resized_image_shape, instance_embedding)
    self.assertAllClose(box_strided, expected_boxes)

  def test_fc_tf_function(self):

    net = deepmac_meta_arch.MaskHeadNetwork('fully_connected', 8, mask_size=32)
    call_func = tf.function(net.__call__)

    out = call_func(tf.zeros((2, 4)), tf.zeros((2, 32, 32, 8)), training=True)
    self.assertEqual(out.shape, (2, 32, 32))

  def test_box_consistency_loss(self):

    boxes_gt = tf.constant([[[0., 0., 0.49, 1.0]]])
    boxes_jittered = tf.constant([[[0.0, 0.0, 1.0, 1.0]]])

    mask_prediction = np.zeros((1, 1, 32, 32)).astype(np.float32)
    mask_prediction[0, 0, :24, :24] = 1.0

    loss = self.model._compute_box_consistency_loss(
        boxes_gt, boxes_jittered, tf.constant(mask_prediction))

    yloss = tf.nn.sigmoid_cross_entropy_with_logits(
        labels=tf.constant([1.0] * 8 + [0.0] * 8),
        logits=[1.0] * 12 + [0.0] * 4)
    xloss = tf.nn.sigmoid_cross_entropy_with_logits(
        labels=tf.constant([1.0] * 16),
        logits=[1.0] * 12 + [0.0] * 4)
    yloss_mean = tf.reduce_mean(yloss)
    xloss_mean = tf.reduce_mean(xloss)

    self.assertAllClose(loss[0], [yloss_mean + xloss_mean])

  def test_box_consistency_loss_with_tightness(self):

    boxes_gt = tf.constant([[[0., 0., 0.49, 0.49]]])
    boxes_jittered = None

    mask_prediction = np.zeros((1, 1, 8, 8)).astype(np.float32) - 1e10
    mask_prediction[0, 0, :4, :4] = 1e10

    model = build_meta_arch(box_consistency_tightness=True,
                            predict_full_resolution_masks=True)
    loss = model._compute_box_consistency_loss(
        boxes_gt, boxes_jittered, tf.constant(mask_prediction))

    self.assertAllClose(loss[0], [0.0])

  def test_box_consistency_loss_gt_count(self):

    boxes_gt = tf.constant([[
        [0., 0., 1.0, 1.0],
        [0., 0., 0.49, 0.49]]])
    boxes_jittered = None

    mask_prediction = np.zeros((1, 2, 32, 32)).astype(np.float32)
    mask_prediction[0, 0, :16, :16] = 1.0
    mask_prediction[0, 1, :8, :8] = 1.0

    model = build_meta_arch(
        box_consistency_loss_normalize='normalize_groundtruth_count',
        predict_full_resolution_masks=True)
    loss_func = (
        model._compute_box_consistency_loss)
    loss = loss_func(
        boxes_gt, boxes_jittered, tf.constant(mask_prediction))

    yloss = tf.nn.sigmoid_cross_entropy_with_logits(
        labels=tf.constant([1.0] * 32),
        logits=[1.0] * 16 + [0.0] * 16) / 32.0
    yloss_mean = tf.reduce_sum(yloss)
    xloss = yloss
    xloss_mean = tf.reduce_sum(xloss)

    self.assertAllClose(loss[0, 0], yloss_mean + xloss_mean)

    yloss = tf.nn.sigmoid_cross_entropy_with_logits(
        labels=tf.constant([1.0] * 16 + [0.0] * 16),
        logits=[1.0] * 8 + [0.0] * 24) / 16.0
    yloss_mean = tf.reduce_sum(yloss)
    xloss = yloss
    xloss_mean = tf.reduce_sum(xloss)
    self.assertAllClose(loss[0, 1], yloss_mean + xloss_mean)

  def test_box_consistency_loss_balanced(self):
    boxes_gt = tf.constant([[
        [0., 0., 0.49, 0.49]]])
    boxes_jittered = None

    mask_prediction = np.zeros((1, 1, 32, 32)).astype(np.float32)
    mask_prediction[0, 0] = 1.0

    model = build_meta_arch(box_consistency_loss_normalize='normalize_balanced',
                            predict_full_resolution_masks=True)
    loss_func = tf.function(
        model._compute_box_consistency_loss)
    loss = loss_func(
        boxes_gt, boxes_jittered, tf.constant(mask_prediction))

    yloss = tf.nn.sigmoid_cross_entropy_with_logits(
        labels=[0.] * 16 + [1.0] * 16,
        logits=[1.0] * 32)
    yloss_mean = tf.reduce_sum(yloss) / 16.0
    xloss_mean = yloss_mean

    self.assertAllClose(loss[0, 0], yloss_mean + xloss_mean)

  def test_box_consistency_dice_loss(self):

    model = build_meta_arch(use_dice_loss=True)
    boxes_gt = tf.constant([[[0., 0., 0.49, 1.0]]])
    boxes_jittered = tf.constant([[[0.0, 0.0, 1.0, 1.0]]])

    almost_inf = 1e10
    mask_prediction = np.full((1, 1, 32, 32), -almost_inf, dtype=np.float32)
    mask_prediction[0, 0, :24, :24] = almost_inf

    loss = model._compute_box_consistency_loss(
        boxes_gt, boxes_jittered, tf.constant(mask_prediction))

    yloss = 1 - 6.0 / 7
    xloss = 0.2

    self.assertAllClose(loss, [[yloss + xloss]])

  def test_feature_consistency_loss_full_res_shape(self):

    model = build_meta_arch(use_dice_loss=True,
                            predict_full_resolution_masks=True)
    boxes = tf.zeros((5, 3, 4))
    img = tf.zeros((5, 32, 32, 3))
    mask_logits = tf.zeros((5, 3, 32, 32))

    loss = model._compute_feature_consistency_loss(
        boxes, img, mask_logits)
    self.assertEqual([5, 3], loss.shape)

  def test_feature_consistency_1_threshold(self):
    model = build_meta_arch(predict_full_resolution_masks=True,
                            feature_consistency_threshold=0.99)
    boxes = tf.zeros((5, 3, 4))
    img = tf.zeros((5, 32, 32, 3))
    mask_logits = tf.zeros((5, 3, 32, 32)) - 1e4

    loss = model._compute_feature_consistency_loss(
        boxes, img, mask_logits)
    self.assertAllClose(loss, np.zeros((5, 3)))

  def test_box_consistency_dice_loss_full_res(self):

    model = build_meta_arch(use_dice_loss=True,
                            predict_full_resolution_masks=True)
    boxes_gt = tf.constant([[[0., 0., 1.0, 1.0]]])
    boxes_jittered = None

    size = 32
    almost_inf = 1e10
    mask_prediction = np.full((1, 1, size, size), -almost_inf, dtype=np.float32)
    mask_prediction[0, 0, :(size // 2), :] = almost_inf

    loss = model._compute_box_consistency_loss(
        boxes_gt, boxes_jittered, tf.constant(mask_prediction))
    self.assertAlmostEqual(loss[0, 0].numpy(), 1 / 3)

  def test_get_lab_image_shape(self):

    output = self.model._get_lab_image(tf.zeros((2, 4, 4, 3)))
    self.assertEqual(output.shape, (2, 4, 4, 3))

  def test_self_supervised_augmented_loss_identity(self):
    model = build_meta_arch(predict_full_resolution_masks=True,
                            augmented_self_supervision_max_translation=0.0)

    x = tf.random.uniform((2, 3, 32, 32), 0, 1)
    boxes = tf.constant([[0., 0., 1., 1.]] * 6)
    boxes = tf.reshape(boxes, [2, 3, 4])
    x = tf.cast(x > 0, tf.float32)
    x = (x - 0.5) * 2e40  # x is a tensor or large +ve or -ve values.
    loss = model._compute_self_supervised_augmented_loss(x, x, boxes)

    self.assertAlmostEqual(loss.numpy().sum(), 0.0)

  def test_self_supervised_mse_augmented_loss_0(self):
    model = build_meta_arch(predict_full_resolution_masks=True,
                            augmented_self_supervision_max_translation=0.0,
                            augmented_self_supervision_loss='loss_mse')

    x = tf.random.uniform((2, 3, 32, 32), 0, 1)
    boxes = tf.constant([[0., 0., 1., 1.]] * 6)
    boxes = tf.reshape(boxes, [2, 3, 4])
    loss = model._compute_self_supervised_augmented_loss(x, x, boxes)

    self.assertAlmostEqual(loss.numpy().min(), 0.0)
    self.assertAlmostEqual(loss.numpy().max(), 0.0)

  def test_self_supervised_mse_loss_scale_equivalent(self):
    model = build_meta_arch(predict_full_resolution_masks=True,
                            augmented_self_supervision_max_translation=0.0,
                            augmented_self_supervision_loss='loss_mse')

    x = np.zeros((1, 3, 32, 32), dtype=np.float32) + 100.0
    y = 0.0 * x.copy()

    x[0, 0, :8, :8] = 0.0
    y[0, 0, :8, :8] = 1.0
    x[0, 1, :16, :16] = 0.0
    y[0, 1, :16, :16] = 1.0
    x[0, 2, :16, :16] = 0.0
    x[0, 2, :8, :8] = 1.0
    y[0, 2, :16, :16] = 0.0

    boxes = np.array([[0., 0., 0.22, 0.22], [0., 0., 0.47, 0.47],
                      [0., 0., 0.47, 0.47]],
                     dtype=np.float32)

    boxes = tf.reshape(tf.constant(boxes), [1, 3, 4])
    loss = model._compute_self_supervised_augmented_loss(x, y, boxes)

    self.assertEqual(loss.shape, (1, 3))
    mse_1_minus_0 = (tf.nn.sigmoid(1.0) - tf.nn.sigmoid(0.0)).numpy()**2
    self.assertAlmostEqual(loss.numpy()[0, 0], mse_1_minus_0)
    self.assertAlmostEqual(loss.numpy()[0, 1], mse_1_minus_0)
    self.assertAlmostEqual(loss.numpy()[0, 2], mse_1_minus_0 / 4.0)

  def test_self_supervised_kldiv_augmented_loss_0(self):
    model = build_meta_arch(predict_full_resolution_masks=True,
                            augmented_self_supervision_max_translation=0.0,
                            augmented_self_supervision_loss='loss_kl_div')

    x = tf.random.uniform((2, 3, 32, 32), 0, 1)
    boxes = tf.constant([[0., 0., 1., 1.]] * 6)
    boxes = tf.reshape(boxes, [2, 3, 4])
    loss = model._compute_self_supervised_augmented_loss(x, x, boxes)

    self.assertAlmostEqual(loss.numpy().min(), 0.0)
    self.assertAlmostEqual(loss.numpy().max(), 0.0)

  def test_self_supervised_kldiv_scale_equivalent(self):
    model = build_meta_arch(predict_full_resolution_masks=True,
                            augmented_self_supervision_max_translation=0.0,
                            augmented_self_supervision_loss='loss_kl_div')

    pred = np.zeros((1, 2, 32, 32), dtype=np.float32) + 100.0
    true = 0.0 * pred.copy()

    pred[0, 0, :8, :8] = LOGIT_HALF
    true[0, 0, :8, :8] = LOGIT_QUARTER
    pred[0, 1, :16, :16] = LOGIT_HALF
    true[0, 1, :16, :16] = LOGIT_QUARTER

    boxes = np.array([[0., 0., 0.22, 0.22], [0., 0., 0.47, 0.47]],
                     dtype=np.float32)

    boxes = tf.reshape(tf.constant(boxes), [1, 2, 4])
    loss = model._compute_self_supervised_augmented_loss(
        original_logits=pred, deaugmented_logits=true, boxes=boxes)

    self.assertEqual(loss.shape, (1, 2))
    expected = (3 * math.log(3) - 4 * math.log(2)) / 4.0
    self.assertAlmostEqual(loss.numpy()[0, 0], expected, places=4)
    self.assertAlmostEqual(loss.numpy()[0, 1], expected, places=4)

  def test_self_supervision_warmup(self):
    tf.keras.backend.set_learning_phase(True)
    model = build_meta_arch(
        use_dice_loss=True,
        predict_full_resolution_masks=True,
        network_type='cond_inst1',
        dim=9,
        pixel_embedding_dim=8,
        use_instance_embedding=False,
        use_xy=False,
        augmented_self_supervision_loss_weight=1.0,
        augmented_self_supervision_max_translation=0.5,
        augmented_self_supervision_warmup_start=10,
        augmented_self_supervision_warmup_steps=40)
    num_stages = 1
    prediction = {
        'preprocessed_inputs': tf.random.normal((1, 32, 32, 3)),
        'MASK_LOGITS_GT_BOXES': [tf.random.normal((1, 5, 8, 8))] * num_stages,
        'SELF_SUPERVISED_DEAUGMENTED_MASK_LOGITS':
            [tf.random.normal((1, 5, 8, 8))] * num_stages,
        'object_center': [tf.random.normal((1, 8, 8, 6))] * num_stages,
        'box/offset': [tf.random.normal((1, 8, 8, 2))] * num_stages,
        'box/scale': [tf.random.normal((1, 8, 8, 2))] * num_stages,
        'extracted_features': [tf.random.normal((3, 32, 32, 7))] * num_stages
    }

    boxes = [tf.convert_to_tensor([[0., 0., 1., 1.]] * 5)]
    classes = [tf.one_hot([1, 0, 1, 1, 1], depth=6)]
    weights = [tf.ones(5)]
    masks = [tf.ones((5, 32, 32))]

    model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        training_step=5)
    loss_at_5 = model.loss(prediction, tf.constant([[32, 32, 3.0]]))

    model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        training_step=20)
    loss_at_20 = model.loss(prediction, tf.constant([[32, 32, 3.0]]))

    model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        training_step=50)
    loss_at_50 = model.loss(prediction, tf.constant([[32, 32, 3.0]]))

    model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        training_step=100)
    loss_at_100 = model.loss(prediction, tf.constant([[32, 32, 3.0]]))

    loss_key = 'Loss/' + deepmac_meta_arch.DEEP_MASK_AUGMENTED_SELF_SUPERVISION
    self.assertAlmostEqual(loss_at_5[loss_key].numpy(), 0.0)
    self.assertGreater(loss_at_20[loss_key], 0.0)

    self.assertAlmostEqual(loss_at_20[loss_key].numpy(),
                           loss_at_50[loss_key].numpy() / 4.0)
    self.assertAlmostEqual(loss_at_50[loss_key].numpy(),
                           loss_at_100[loss_key].numpy())

  def test_loss_keys(self):
    model = build_meta_arch(
        use_dice_loss=True,
        augmented_self_supervision_loss_weight=1.0,
        augmented_self_supervision_max_translation=0.5,
        predict_full_resolution_masks=True)

    prediction = {
        'preprocessed_inputs': tf.random.normal((3, 32, 32, 3)),
        'MASK_LOGITS_GT_BOXES': [tf.random.normal((3, 5, 8, 8))] * 2,
        'object_center': [tf.random.normal((3, 8, 8, 6))] * 2,
        'box/offset': [tf.random.normal((3, 8, 8, 2))] * 2,
        'box/scale': [tf.random.normal((3, 8, 8, 2))] * 2,
        'SELF_SUPERVISED_DEAUGMENTED_MASK_LOGITS': (
            [tf.random.normal((3, 5, 8, 8))] * 2),
        'extracted_features': [tf.random.normal((3, 32, 32, 7))] * 2
    }
    model.provide_groundtruth(
        groundtruth_boxes_list=[
            tf.convert_to_tensor([[0., 0., 1., 1.]] * 5)] * 3,
        groundtruth_classes_list=[tf.one_hot([1, 0, 1, 1, 1], depth=6)] * 3,
        groundtruth_weights_list=[tf.ones(5)] * 3,
        groundtruth_masks_list=[tf.ones((5, 32, 32))] * 3,
        groundtruth_keypoints_list=[tf.zeros((5, 10, 2))] * 3,
        groundtruth_keypoint_depths_list=[tf.zeros((5, 10))] * 3)
    loss = model.loss(prediction, tf.constant([[32, 32, 3.0]]))
    self.assertGreater(loss['Loss/deep_mask_estimation'], 0.0)

    for weak_loss in deepmac_meta_arch.MASK_LOSSES:
      if weak_loss == deepmac_meta_arch.DEEP_MASK_FEATURE_CONSISTENCY:
        continue
      self.assertGreater(loss['Loss/' + weak_loss], 0.0,
                         '{} was <= 0'.format(weak_loss))

  def test_eval_loss_and_postprocess_keys(self):

    model = build_meta_arch(
        use_dice_loss=True,
        augmented_self_supervision_loss_weight=1.0,
        augmented_self_supervision_max_translation=0.5,
        predict_full_resolution_masks=True)

    true_image_shapes = tf.constant([[32, 32, 3]], dtype=tf.int32)
    prediction_dict = model.predict(
        tf.zeros((1, 32, 32, 3)), true_image_shapes)
    output = model.postprocess(prediction_dict, true_image_shapes)
    self.assertEqual(output['detection_boxes'].shape, (1, 5, 4))
    self.assertEqual(output['detection_masks'].shape, (1, 5, 128, 128))

    model.provide_groundtruth(
        groundtruth_boxes_list=[
            tf.convert_to_tensor([[0., 0., 1., 1.]] * 5)] * 1,
        groundtruth_classes_list=[tf.one_hot([1, 0, 1, 1, 1], depth=6)] * 1,
        groundtruth_weights_list=[tf.ones(5)] * 1,
        groundtruth_masks_list=[tf.ones((5, 32, 32))] * 1,
        groundtruth_keypoints_list=[tf.zeros((5, 10, 2))] * 1,
        groundtruth_keypoint_depths_list=[tf.zeros((5, 10))] * 1)
    prediction_dict = model.predict(
        tf.zeros((1, 32, 32, 3)), true_image_shapes)
    model.loss(prediction_dict, true_image_shapes)

  def test_loss_weight_response(self):
    tf.random.set_seed(12)
    model = build_meta_arch(
        use_dice_loss=True,
        predict_full_resolution_masks=True,
        network_type='cond_inst1',
        dim=9,
        pixel_embedding_dim=8,
        use_instance_embedding=False,
        use_xy=False,
        augmented_self_supervision_loss_weight=1.0,
        augmented_self_supervision_max_translation=0.5,
        )
    num_stages = 1
    prediction = {
        'preprocessed_inputs': tf.random.normal((1, 32, 32, 3)),
        'MASK_LOGITS_GT_BOXES': [tf.random.normal((1, 5, 8, 8))] * num_stages,
        'object_center': [tf.random.normal((1, 8, 8, 6))] * num_stages,
        'box/offset': [tf.random.normal((1, 8, 8, 2))] * num_stages,
        'box/scale': [tf.random.normal((1, 8, 8, 2))] * num_stages,
        'SELF_SUPERVISED_DEAUGMENTED_MASK_LOGITS': (
            [tf.random.normal((1, 5, 8, 8))] * num_stages),
        'extracted_features': [tf.random.normal((3, 32, 32, 7))] * num_stages
    }

    boxes = [tf.convert_to_tensor([[0., 0., 1., 1.]] * 5)]
    classes = [tf.one_hot([1, 0, 1, 1, 1], depth=6)]
    weights = [tf.ones(5)]
    masks = [tf.ones((5, 32, 32))]
    keypoints = [tf.zeros((5, 10, 2))]
    keypoint_depths = [tf.ones((5, 10))]
    model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        groundtruth_keypoints_list=keypoints,
        groundtruth_keypoint_depths_list=keypoint_depths)
    loss = model.loss(prediction, tf.constant([[32, 32, 3.0]]))
    self.assertGreater(loss['Loss/deep_mask_estimation'], 0.0)

    for mask_loss in deepmac_meta_arch.MASK_LOSSES:
      self.assertGreater(loss['Loss/' + mask_loss], 0.0,
                         '{} was <= 0'.format(mask_loss))

    rng = random.Random(0)
    loss_weights = {
        deepmac_meta_arch.DEEP_MASK_ESTIMATION: rng.uniform(1, 5),
        deepmac_meta_arch.DEEP_MASK_BOX_CONSISTENCY: rng.uniform(1, 5),
        deepmac_meta_arch.DEEP_MASK_FEATURE_CONSISTENCY: rng.uniform(1, 5),
        deepmac_meta_arch.DEEP_MASK_AUGMENTED_SELF_SUPERVISION: (
            rng.uniform(1, 5)),
        deepmac_meta_arch.DEEP_MASK_POINTLY_SUPERVISED: rng.uniform(1, 5)
    }

    weighted_model = build_meta_arch(
        use_dice_loss=True,
        predict_full_resolution_masks=True,
        network_type='cond_inst1',
        dim=9,
        pixel_embedding_dim=8,
        use_instance_embedding=False,
        use_xy=False,
        task_loss_weight=loss_weights[deepmac_meta_arch.DEEP_MASK_ESTIMATION],
        box_consistency_loss_weight=(
            loss_weights[deepmac_meta_arch.DEEP_MASK_BOX_CONSISTENCY]),
        feature_consistency_loss_weight=(
            loss_weights[deepmac_meta_arch.DEEP_MASK_FEATURE_CONSISTENCY]),
        augmented_self_supervision_loss_weight=(
            loss_weights[deepmac_meta_arch.DEEP_MASK_AUGMENTED_SELF_SUPERVISION]
            ),
        pointly_supervised_keypoint_loss_weight=(
            loss_weights[deepmac_meta_arch.DEEP_MASK_POINTLY_SUPERVISED])
        )

    weighted_model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        groundtruth_keypoints_list=keypoints,
        groundtruth_keypoint_depths_list=keypoint_depths)

    weighted_loss = weighted_model.loss(prediction, tf.constant([[32, 32, 3]]))
    for mask_loss in deepmac_meta_arch.MASK_LOSSES:
      loss_key = 'Loss/' + mask_loss
      self.assertAllEqual(
          weighted_loss[loss_key], loss[loss_key] * loss_weights[mask_loss],
          f'{mask_loss} did not respond to change in weight.')

  @parameterized.parameters(
      [dict(feature_consistency_type='consistency_default_lab',
            feature_consistency_comparison='comparison_default_gaussian'),
       dict(feature_consistency_type='consistency_feature_map',
            feature_consistency_comparison='comparison_normalized_dotprod')],
  )
  def test_feature_consistency_warmup(
      self, feature_consistency_type, feature_consistency_comparison):
    tf.keras.backend.set_learning_phase(True)
    model = build_meta_arch(
        use_dice_loss=True,
        predict_full_resolution_masks=True,
        network_type='cond_inst1',
        dim=9,
        pixel_embedding_dim=8,
        use_instance_embedding=False,
        use_xy=False,
        feature_consistency_warmup_steps=10,
        feature_consistency_warmup_start=10,
        feature_consistency_type=feature_consistency_type,
        feature_consistency_comparison=feature_consistency_comparison)

    num_stages = 1
    prediction = {
        'preprocessed_inputs': tf.random.normal((1, 32, 32, 3)),
        'MASK_LOGITS_GT_BOXES': [tf.random.normal((1, 5, 8, 8))] * num_stages,
        'object_center': [tf.random.normal((1, 8, 8, 6))] * num_stages,
        'box/offset': [tf.random.normal((1, 8, 8, 2))] * num_stages,
        'box/scale': [tf.random.normal((1, 8, 8, 2))] * num_stages,
        'extracted_features': [tf.random.normal((3, 32, 32, 7))] * num_stages
    }

    boxes = [tf.convert_to_tensor([[0., 0., 1., 1.]] * 5)]
    classes = [tf.one_hot([1, 0, 1, 1, 1], depth=6)]
    weights = [tf.ones(5)]
    masks = [tf.ones((5, 32, 32))]

    model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        training_step=5)
    loss_at_5 = model.loss(prediction, tf.constant([[32, 32, 3.0]]))

    model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        training_step=15)
    loss_at_15 = model.loss(prediction, tf.constant([[32, 32, 3.0]]))

    model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        training_step=20)
    loss_at_20 = model.loss(prediction, tf.constant([[32, 32, 3.0]]))

    model.provide_groundtruth(
        groundtruth_boxes_list=boxes,
        groundtruth_classes_list=classes,
        groundtruth_weights_list=weights,
        groundtruth_masks_list=masks,
        training_step=100)
    loss_at_100 = model.loss(prediction, tf.constant([[32, 32, 3.0]]))

    loss_key = 'Loss/' + deepmac_meta_arch.DEEP_MASK_FEATURE_CONSISTENCY
    self.assertAlmostEqual(loss_at_5[loss_key].numpy(), 0.0)
    self.assertGreater(loss_at_15[loss_key], 0.0)
    self.assertAlmostEqual(loss_at_15[loss_key].numpy(),
                           loss_at_20[loss_key].numpy() / 2.0)
    self.assertAlmostEqual(loss_at_20[loss_key].numpy(),
                           loss_at_100[loss_key].numpy())

  def test_pointly_supervised_loss(self):
    tf.keras.backend.set_learning_phase(True)
    model = build_meta_arch(
        use_dice_loss=False,
        predict_full_resolution_masks=True,
        network_type='cond_inst1',
        dim=9,
        pixel_embedding_dim=8,
        use_instance_embedding=False,
        use_xy=False,
        pointly_supervised_keypoint_loss_weight=1.0)

    mask_logits = np.zeros((1, 1, 32, 32), dtype=np.float32)
    keypoints = np.zeros((1, 1, 1, 2), dtype=np.float32)
    keypoint_depths = np.zeros((1, 1, 1), dtype=np.float32)

    keypoints[..., 0] = 0.5
    keypoints[..., 1] = 0.5
    keypoint_depths[..., 0] = 1.0
    mask_logits[:, :, 16, 16] = 1.0

    expected_loss = tf.nn.sigmoid_cross_entropy_with_logits(
        logits=[[1.0]], labels=[[1.0]]
    ).numpy()
    loss = model._compute_pointly_supervised_loss_from_keypoints(
        mask_logits, keypoints, keypoint_depths)

    self.assertEqual(loss.shape, (1, 1))
    self.assertAllClose(expected_loss, loss)

  def test_ignore_per_class_box_overlap(self):
    tf.keras.backend.set_learning_phase(True)
    model = build_meta_arch(
        use_dice_loss=False,
        predict_full_resolution_masks=True,
        network_type='cond_inst1',
        dim=9,
        pixel_embedding_dim=8,
        use_instance_embedding=False,
        use_xy=False,
        pointly_supervised_keypoint_loss_weight=1.0,
        ignore_per_class_box_overlap=True)

    self.assertTrue(model._deepmac_params.ignore_per_class_box_overlap)
    mask_logits = tf.zeros((2, 3, 16, 16))
    mask_gt = tf.zeros((2, 3, 32, 32))
    boxes = tf.zeros((2, 3, 4))
    classes = tf.zeros((2, 3, 5))

    loss = model._compute_mask_prediction_loss(
        boxes, mask_logits, mask_gt, classes)

    self.assertEqual(loss.shape, (2, 3))


@unittest.skipIf(tf_version.is_tf1(), 'Skipping TF2.X only test.')
class FullyConnectedMaskHeadTest(tf.test.TestCase):

  def test_fc_mask_head(self):
    head = deepmac_meta_arch.FullyConnectedMaskHead(512, 16)
    inputs = tf.random.uniform([100, 16, 16, 512])
    output = head(inputs)
    self.assertAllEqual([100, 16, 16, 1], output.numpy().shape)


@unittest.skipIf(tf_version.is_tf1(), 'Skipping TF2.X only test.')
class ResNetMaskHeadTest(tf.test.TestCase, parameterized.TestCase):

  @parameterized.parameters(['resnet4', 'resnet8', 'resnet20'])
  def test_forward(self, name):
    net = deepmac_meta_arch.ResNetMaskNetwork(name, 8)
    out = net(tf.zeros((3, 32, 32, 16)))
    self.assertEqual(out.shape[:3], (3, 32, 32))


if __name__ == '__main__':
  tf.test.main()
