|
| 1 | +# Copyright (c) MONAI Consortium |
| 2 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 3 | +# you may not use this file except in compliance with the License. |
| 4 | +# You may obtain a copy of the License at |
| 5 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 6 | +# Unless required by applicable law or agreed to in writing, software |
| 7 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 8 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 9 | +# See the License for the specific language governing permissions and |
| 10 | +# limitations under the License. |
| 11 | + |
| 12 | +from __future__ import annotations |
| 13 | + |
| 14 | +import unittest |
| 15 | + |
| 16 | +import torch |
| 17 | +from parameterized import parameterized |
| 18 | + |
| 19 | +from monai.metrics.utils import remap_instance_id |
| 20 | + |
| 21 | +_device = "cuda:0" if torch.cuda.is_available() else "cpu" |
| 22 | + |
| 23 | +TEST_CASES = [ |
| 24 | + # (description, input, by_size, expected) |
| 25 | + ["non_contiguous_ids", [[0, 2, 2], [5, 5, 0]], False, [[0, 1, 1], [2, 2, 0]]], |
| 26 | + ["already_contiguous", [[0, 1], [2, 2]], False, [[0, 1], [2, 2]]], |
| 27 | + # id 7 covers 3 pixels, id 3 covers 2, id 9 covers 1 -> sizes decide new ids |
| 28 | + ["by_size_largest_first", [[7, 7, 7, 0], [3, 3, 9, 0]], True, [[1, 1, 1, 0], [2, 2, 3, 0]]], |
| 29 | + # equal sizes: ascending original id order wins (stable tie-breaking) |
| 30 | + ["by_size_ties_stable", [[5, 5, 0], [2, 2, 0]], True, [[2, 2, 0], [1, 1, 0]]], |
| 31 | + ["no_background", [[4, 4], [6, 6]], False, [[1, 1], [2, 2]]], |
| 32 | + ["single_instance", [[0, 0], [3, 3]], True, [[0, 0], [1, 1]]], |
| 33 | +] |
| 34 | + |
| 35 | + |
| 36 | +def _reference_remap(pred: torch.Tensor, by_size: bool = False) -> torch.Tensor: |
| 37 | + """The original per-instance-loop implementation, kept as the behavioral reference.""" |
| 38 | + pred_id = [i for i in pred.unique() if i != 0] |
| 39 | + if not pred_id: |
| 40 | + return pred |
| 41 | + if by_size: |
| 42 | + instance_size = [(pred == instance_id).sum() for instance_id in pred_id] |
| 43 | + pair_list = sorted(zip(pred_id, instance_size), key=lambda x: x[1], reverse=True) |
| 44 | + pred_id = [p[0] for p in pair_list] |
| 45 | + new_pred = torch.zeros_like(pred, dtype=torch.int) |
| 46 | + for idx, instance_id in enumerate(pred_id): |
| 47 | + new_pred[pred == instance_id] = idx + 1 |
| 48 | + return new_pred |
| 49 | + |
| 50 | + |
| 51 | +class TestRemapInstanceId(unittest.TestCase): |
| 52 | + |
| 53 | + @parameterized.expand(TEST_CASES) |
| 54 | + def test_expected_value(self, _, pred, by_size, expected): |
| 55 | + result = remap_instance_id(torch.as_tensor(pred, device=_device), by_size=by_size) |
| 56 | + torch.testing.assert_close(result.cpu(), torch.as_tensor(expected, dtype=torch.int), check_dtype=False) |
| 57 | + |
| 58 | + @parameterized.expand([["all_background_2d", (4, 4)], ["all_background_3d", (2, 3, 4)], ["empty", (0,)]]) |
| 59 | + def test_passthrough(self, _, shape): |
| 60 | + pred = torch.zeros(shape, dtype=torch.int64, device=_device) |
| 61 | + result = remap_instance_id(pred, by_size=True) |
| 62 | + self.assertEqual(result.dtype, pred.dtype) |
| 63 | + torch.testing.assert_close(result, pred) |
| 64 | + |
| 65 | + def test_output_dtype(self): |
| 66 | + pred = torch.as_tensor([[0, 9]], dtype=torch.int64, device=_device) |
| 67 | + self.assertEqual(remap_instance_id(pred).dtype, torch.int) |
| 68 | + |
| 69 | + @parameterized.expand( |
| 70 | + [ |
| 71 | + ["2d", (64, 64), 20, False], |
| 72 | + ["2d_by_size", (64, 64), 20, True], |
| 73 | + ["3d_by_size", (16, 16, 16), 12, True], |
| 74 | + ["sparse_ids_by_size", (48, 48), 7, True], |
| 75 | + ] |
| 76 | + ) |
| 77 | + def test_matches_reference(self, name, shape, n_inst, by_size): |
| 78 | + g = torch.Generator().manual_seed(0) |
| 79 | + pred = torch.randint(0, n_inst + 1, shape, generator=g).to(_device) |
| 80 | + if name.startswith("sparse"): |
| 81 | + pred = pred * 1000 + 17 # large, non-contiguous, no-background ids |
| 82 | + result = remap_instance_id(pred, by_size=by_size) |
| 83 | + expected = _reference_remap(pred, by_size=by_size) |
| 84 | + torch.testing.assert_close(result, expected) |
| 85 | + |
| 86 | + |
| 87 | +if __name__ == "__main__": |
| 88 | + unittest.main() |
0 commit comments