# Copyright 2025 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.

"""Tests for metrics.py."""

from absl.testing import parameterized
import tensorflow as tf, tf_keras

from official.vision.evaluation import instance_metrics


class InstanceMetricsTest(tf.test.TestCase, parameterized.TestCase):

  def test_compute_coco_ap(self):
    precisions = [1.0, 1.0, 0.5, 0.8, 0.4, 0.5, 0.2, 0.3]
    recalls = [0.0, 0.1, 0.1, 0.5, 0.5, 0.7, 0.7, 1.0]
    self.assertAllClose(
        instance_metrics.COCOAveragePrecision(recalls_desc=False)(
            precisions, recalls
        ),
        0.613861,
        atol=1e-4,
    )

    precisions.reverse()
    recalls.reverse()
    self.assertAllClose(
        instance_metrics.COCOAveragePrecision(recalls_desc=True)(
            precisions, recalls
        ),
        0.613861,
        atol=1e-4,
    )

  def test_compute_voc10_ap(self):
    precisions = [1.0, 1.0, 0.5, 0.8, 0.4, 0.5, 0.2, 0.3]
    recalls = [0.0, 0.1, 0.1, 0.5, 0.5, 0.7, 0.7, 1.0]
    self.assertAllClose(
        instance_metrics.VOC2010AveragePrecision(recalls_desc=False)(
            precisions, recalls
        ),
        0.61,
        atol=1e-4,
    )

    precisions.reverse()
    recalls.reverse()
    self.assertAllClose(
        instance_metrics.VOC2010AveragePrecision(recalls_desc=True)(
            precisions, recalls
        ),
        0.61,
        atol=1e-4,
    )

  def test_match_detections_to_gts(self):
    coco_matching_algorithm = instance_metrics.COCOMatchingAlgorithm(
        iou_thresholds=(0.5, 0.85)
    )

    detection_is_tp, gt_is_tp = coco_matching_algorithm(
        detection_to_gt_ious=tf.constant([[[0.8, 0.7, 0.95], [0.9, 0.6, 0.3]]]),
        detection_classes=tf.constant([[1, 1]]),
        detection_scores=tf.constant([[0.6, 0.8]]),
        gt_classes=tf.constant([[1, 1, 2]]),
    )
    self.assertAllEqual(detection_is_tp, [[[True, False], [True, True]]])
    self.assertAllEqual(
        gt_is_tp, [[[True, True], [True, False], [False, False]]]
    )

  def test_shift_and_rescale_boxes(self):
    self.assertAllClose(
        instance_metrics._shift_and_rescale_boxes(
            boxes=[[[2, 3, 4, 9], [15, 17, 18, 23]]], output_boundary=(20, 20)
        ),
        [[[0.0, 0.0, 2.0, 6.0], [13.0, 14.0, 16.0, 20.0]]],
        atol=1e-4,
    )
    self.assertAllClose(
        instance_metrics._shift_and_rescale_boxes(
            boxes=[[[-2, -1, 0, 5], [11, 13, 14, 19]]], output_boundary=(20, 20)
        ),
        [[[0.0, 0.0, 2.0, 6.0], [13.0, 14.0, 16.0, 20.0]]],
        atol=1e-4,
    )
    self.assertAllClose(
        instance_metrics._shift_and_rescale_boxes(
            boxes=[[[2, 3, 4, 9], [15, 17, 18, 23]]], output_boundary=(10, 10)
        ),
        [[[0.0, 0.0, 1.0, 3.0], [6.5, 7.0, 8.0, 10.0]]],
        atol=1e-4,
    )
    self.assertAllClose(
        instance_metrics._shift_and_rescale_boxes(
            boxes=[[[-2, -1, 0, 5], [11, 13, 14, 19]]], output_boundary=(10, 10)
        ),
        [[[0.0, 0.0, 1.0, 3.0], [6.5, 7.0, 8.0, 10.0]]],
        atol=1e-4,
    )
    self.assertAllClose(
        instance_metrics._shift_and_rescale_boxes(
            boxes=[[[2, 3, 4, 9], [-1, -1, -1, -1]]], output_boundary=(10, 10)
        ),
        [[[0.0, 0.0, 2.0, 6.0], [0.0, 0.0, 0.0, 0.0]]],
        atol=1e-4,
    )

  def test_count_detection_type(self):
    result = instance_metrics._count_detection_type(
        detection_type_mask=tf.constant(
            [[[True], [True], [False]], [[True], [True], [False]]]
        ),
        detection_classes=tf.constant([[1, 2, 3], [2, 3, 4]]),
        flattened_binned_confidence_one_hot=tf.constant([
            [False, True, False],
            [True, False, False],
            [False, True, False],
            [True, False, False],
            [False, False, True],
            [False, False, True],
        ]),
        num_classes=5,
    )
    self.assertAllClose(
        result,
        [[
            [0.0, 0.0, 0.0],
            [0.0, 1.0, 0.0],
            [2.0, 0.0, 0.0],
            [0.0, 0.0, 1.0],
            [0.0, 0.0, 0.0],
        ]],
        atol=1e-4,
    )

  @parameterized.parameters(True, False)
  def test_instance_metrics(self, use_mask):
    metrics = instance_metrics.InstanceMetrics(
        name='per_class_ap',
        num_classes=3,
        use_masks=use_mask,
        iou_thresholds=(0.1, 0.5),
        confidence_thresholds=(0.2, 0.7),
        mask_output_boundary=(32, 32),
        average_precision_algorithms={
            'ap_coco': instance_metrics.COCOAveragePrecision(),
            'ap_voc10': instance_metrics.VOC2010AveragePrecision(),
        },
    )
    y_true = {
        'boxes': [[
            [12, 12, 15, 15],
            [16, 16, 20, 20],
            [0, 0, 5, 5],
            [6, 6, 10, 10],
        ]],
        # 1x1 mask
        'masks': [[[[1.0]], [[0.9]], [[0.8]], [[0.7]]]],
        'classes': [[2, 1, 1, 1]],
        'image_info': tf.constant(
            [[[32, 32], [32, 32], [1, 1], [0, 0]]], dtype=tf.float32
        ),
    }
    y_pred = {
        'detection_boxes': [[
            [12, 12, 15, 15],
            # The duplicate detection with lower score won't be counted as TP.
            [12, 12, 15, 15],
            [16, 19, 20, 20],
            [1, 1, 6, 6],
            [6, 6, 11, 11],
        ]],
        # 1x1 mask
        'detection_masks': [[[[1.0]], [[0.9]], [[0.8]], [[0.7]], [[0.6]]]],
        'detection_classes': [[1, 1, 1, 2, 1]],
        'detection_scores': [[0.3, 0.25, 0.4, 0.6, 0.8]],
    }
    metrics.update_state(y_true, y_pred)
    result = metrics.result()
    self.assertAllClose(
        result['ap_coco'],
        [[0.0, 0.663366, 0.0], [0.0, 0.336634, 0.0]],
        atol=1e-4,
    )
    self.assertAllClose(
        result['ap_voc10'],
        [[0.0, 2.0 / 3.0, 0.0], [0.0, 1.0 / 3.0, 0.0]],
        atol=1e-4,
    )
    self.assertAllClose(
        result['precisions'],
        [
            [[0.0, 0.5, 0.0], [0.0, 0.25, 0.0]],
            [[0.0, 1.0, 0.0], [0.0, 1.0, 0.0]],
        ],
        atol=1e-4,
    )
    self.assertAllClose(
        result['recalls'],
        [
            [[0.0, 2.0 / 3.0, 0.0], [0.0, 1.0 / 3.0, 0.0]],
            [[0.0, 1.0 / 3, 0.0], [0.0, 1.0 / 3, 0.0]],
        ],
        atol=1e-4,
    )
    self.assertAllEqual(result['valid_classes'], [False, True, True])

  def test_mask_metrics_with_instance_rescaled(self):
    metrics = instance_metrics.InstanceMetrics(
        name='per_class_ap',
        use_masks=True,
        num_classes=3,
        iou_thresholds=(0.5,),
        confidence_thresholds=(0.5,),
        mask_output_boundary=(10, 10),
        average_precision_algorithms={
            'ap_coco': instance_metrics.COCOAveragePrecision(),
            'ap_voc10': instance_metrics.VOC2010AveragePrecision(),
        },
    )
    y_true = {
        # Instances are rescaled to (10, 10) boundary.
        'boxes': [[[0, 0, 8, 8], [10, 10, 20, 20]]],
        'masks': [[
            [
                [1, 1, 1, 1],
                [1, 1, 1, 1],
                [1, 1, 1, 0],
                [1, 1, 1, 0],
            ],
            [
                [1, 1, 0, 0],
                [1, 1, 0, 0],
                [1, 1, 0, 0],
                [1, 1, 0, 0],
            ],
        ]],
        'classes': [[1, 2]],
        'image_info': tf.constant(
            [[[20, 20], [20, 20], [1, 1], [0, 0]]], dtype=tf.float32
        ),
    }
    y_pred = {
        # Instances are rescaled to (10, 10) boundary.
        'detection_boxes': [[[0, 1, 8, 9], [10, 10, 20, 20]]],
        'detection_masks': [[
            [
                [1, 1, 1, 0],
                [1, 1, 1, 0],
                [1, 1, 1, 0],
                [1, 1, 1, 0],
            ],
            [
                [0, 0, 0, 0],
                [0, 0, 0, 0],
                [1, 1, 1, 1],
                [1, 1, 1, 1],
            ],
        ]],
        'detection_classes': [[1, 2]],
        'detection_scores': [[0.9, 0.8]],
    }
    metrics.update_state(y_true, y_pred)
    result = metrics.result()
    self.assertAllClose(
        result['precisions'],
        [[[0.0, 1.0, 0.0]]],
        atol=1e-4,
    )
    self.assertAllClose(
        result['recalls'],
        [[[0.0, 1.0, 0.0]]],
        atol=1e-4,
    )
    self.assertAllClose(
        result['ap_coco'],
        [[0.0, 1.0, 0.0]],
        atol=1e-4,
    )
    self.assertAllClose(
        result['ap_voc10'],
        [[0.0, 1.0, 0.0]],
        atol=1e-4,
    )
    self.assertAllEqual(result['valid_classes'], [False, True, True])

  @parameterized.parameters(True, False)
  def test_instance_metrics_with_crowd(self, use_mask):
    metrics = instance_metrics.InstanceMetrics(
        name='per_class_ap',
        use_masks=use_mask,
        num_classes=2,
        iou_thresholds=(0.5,),
        confidence_thresholds=(0.5,),
        mask_output_boundary=(20, 20),
        average_precision_algorithms={
            'ap_coco': instance_metrics.COCOAveragePrecision(),
            'ap_voc10': instance_metrics.VOC2010AveragePrecision(),
        },
    )
    y_true = {
        'boxes': [[[0, 1, 4, 10], [0, 5, 4, 11]]],
        'masks': [[[[1]], [[1]]]],
        'classes': [[1, 1]],
        'image_info': tf.constant(
            [[[20, 20], [20, 20], [1, 1], [0, 0]]], dtype=tf.float32
        ),
        'is_crowds': [[True, False]],
    }
    y_pred = {
        # Over 50% of first box [0, 0, 4, 4] matches the crowd instance
        # [0, 1, 4, 10], so it's excluded from the false positives.
        'detection_boxes': [[[0, 0, 4, 4], [1, 5, 5, 11]]],
        'detection_masks': [[[[1]], [[1]]]],
        'detection_classes': [[1, 1]],
        'detection_scores': [[0.9, 0.8]],
    }
    metrics.update_state(y_true, y_pred)
    result = metrics.result()
    self.assertAllClose(
        result['precisions'],
        [[[0.0, 1.0]]],
        atol=1e-4,
    )
    self.assertAllClose(
        result['recalls'],
        [[[0.0, 1.0]]],
        atol=1e-4,
    )
    self.assertAllClose(
        result['ap_coco'],
        [[0.0, 1.0]],
        atol=1e-4,
    )
    self.assertAllClose(
        result['ap_voc10'],
        [[0.0, 1.0]],
        atol=1e-4,
    )
    self.assertAllEqual(result['valid_classes'], [False, True])


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