diff --git a/monai/metrics/hausdorff_distance.py b/monai/metrics/hausdorff_distance.py index 1b83c93e5b..6150fa5271 100644 --- a/monai/metrics/hausdorff_distance.py +++ b/monai/metrics/hausdorff_distance.py @@ -204,9 +204,12 @@ def _compute_percentile_hausdorff_distance( if surface_distance.shape == (0,): return torch.tensor(np.nan, dtype=torch.float, device=surface_distance.device) - if not percentile: + if percentile is None: return surface_distance.max() + if percentile == 0: + return surface_distance.min() + if 0 <= percentile <= 100: return torch.quantile(surface_distance, percentile / 100) raise ValueError(f"percentile should be a value between 0 and 100, get {percentile}.") diff --git a/tests/metrics/test_hausdorff_distance.py b/tests/metrics/test_hausdorff_distance.py index 20276a1832..57361dd82c 100644 --- a/tests/metrics/test_hausdorff_distance.py +++ b/tests/metrics/test_hausdorff_distance.py @@ -19,6 +19,7 @@ from parameterized import parameterized from monai.metrics import HausdorffDistanceMetric +from monai.metrics.hausdorff_distance import _compute_percentile_hausdorff_distance _devices = ["cpu"] if torch.cuda.is_available(): @@ -116,6 +117,25 @@ def create_spherical_seg_3d( ], [19.924858845171276, 20.09975124224178, 14, 18, 22, 33], ], + [ + [ + # percentile=0 is the 0th-percentile (minimum) surface distance, not the max + create_spherical_seg_3d(radius=20, centre=(20, 20, 20)), + create_spherical_seg_3d(radius=20, centre=(19, 19, 19)), + None, + 0, + ], + [0, 0, 0, 0, 0, 0], + ], + [ + [ + create_spherical_seg_3d(radius=15, centre=(20, 33, 22), im_spacing=test_spacing), + create_spherical_seg_3d(radius=30, centre=(20, 33, 22), im_spacing=test_spacing), + test_spacing, + 0, + ], + [5.099999904632568, 5.099999904632568, 6, 6, 6, 6], + ], [ [ create_spherical_seg_3d(radius=20, centre=(20, 20, 20), im_spacing=test_spacing), @@ -167,6 +187,15 @@ def _describe_test_case(test_func, test_number, params): return f"device: {_device} metric: {metric} directed:{directed} expected: {test_output}" +TEST_CASES_PERCENTILE = [ + [[0.0, 3.0], None, 3.0], + [[0.0, 3.0], 0, 0.0], + [[np.inf, np.inf, np.inf], 0, np.inf], + [[1.0, 2.0, 3.0], 50, 2.0], + [[], 0, np.nan], +] + + class TestHausdorffDistance(unittest.TestCase): @parameterized.expand(TEST_CASES_EXPANDED, doc_func=_describe_test_case) @@ -204,6 +233,18 @@ def test_nans(self, input_data): np.testing.assert_allclose(0, result, rtol=1e-7) np.testing.assert_allclose(0, not_nans, rtol=1e-7) + @parameterized.expand(TEST_CASES_PERCENTILE) + def test_percentile(self, surface_distances, percentile, expected_value): + surface_distance = torch.tensor(surface_distances, dtype=torch.float) + result = _compute_percentile_hausdorff_distance(surface_distance, percentile) + np.testing.assert_allclose(expected_value, result, rtol=1e-7) + + def test_percentile_out_of_range(self): + for percentile in [-1, 101]: + with self.subTest(percentile=percentile): + with self.assertRaises(ValueError): + _compute_percentile_hausdorff_distance(torch.tensor([1.0, 2.0, 3.0]), percentile) + if __name__ == "__main__": unittest.main()