diff --git a/monai/metrics/confusion_matrix.py b/monai/metrics/confusion_matrix.py index 67eff3451e..9157f9c3b9 100644 --- a/monai/metrics/confusion_matrix.py +++ b/monai/metrics/confusion_matrix.py @@ -178,6 +178,12 @@ def get_confusion_matrix( ValueError: when `y_pred` and `y` have different shapes. """ + # bool + bool is a logical OR, so the `(y_pred + y) == 2` test below never matches and loses every true positive. + if y_pred.dtype == torch.bool: + y_pred = y_pred.float() + if y.dtype == torch.bool: + y = y.float() + original_y = y if not include_background: y_pred, y = ignore_background(y_pred=y_pred, y=y) diff --git a/monai/metrics/f_beta_score.py b/monai/metrics/f_beta_score.py index bb9371c8bf..5b40322323 100644 --- a/monai/metrics/f_beta_score.py +++ b/monai/metrics/f_beta_score.py @@ -61,6 +61,13 @@ def aggregate( def get_f_beta_score(y_pred: torch.Tensor, y: torch.Tensor, include_background: bool = True) -> torch.Tensor: + # The true positive/negative counts below test `y_pred + y`; for bool tensors that sum is a logical OR, + # which never equals 2, so every true positive would be lost. Count on float values instead. + if y_pred.dtype == torch.bool: + y_pred = y_pred.float() + if y.dtype == torch.bool: + y = y.float() + if not include_background: y_pred, y = ignore_background(y_pred=y_pred, y=y) diff --git a/tests/metrics/test_compute_confusion_matrix.py b/tests/metrics/test_compute_confusion_matrix.py index 3b7f793323..406338685a 100644 --- a/tests/metrics/test_compute_confusion_matrix.py +++ b/tests/metrics/test_compute_confusion_matrix.py @@ -502,6 +502,18 @@ def test_unknown_metric_name(self): with self.assertRaises(NotImplementedError): compute_confusion_matrix_metric("diagnostic likelihood ratio", TEST_CASE_LR[0]) + def test_bool_inputs_match_float_inputs(self): + """Bool masks (e.g. ``pred > 0.5``) must give the same confusion matrix as their float equivalents.""" + generator = torch.Generator().manual_seed(0) + y_pred = torch.rand(2, 3, 8, 8, generator=generator) > 0.5 + y = torch.rand(2, 3, 8, 8, generator=generator) > 0.5 + + for include_background in (True, False): + expected = get_confusion_matrix(y_pred.float(), y.float(), include_background=include_background) + result = get_confusion_matrix(y_pred, y, include_background=include_background) + assert_allclose(result, expected, type_test=False) + self.assertTrue(bool((result[..., 0] > 0).all())) # true positives are counted + if __name__ == "__main__": unittest.main() diff --git a/tests/metrics/test_compute_f_beta.py b/tests/metrics/test_compute_f_beta.py index 21a4e3c22c..b207b0227c 100644 --- a/tests/metrics/test_compute_f_beta.py +++ b/tests/metrics/test_compute_f_beta.py @@ -18,6 +18,7 @@ from parameterized import parameterized from monai.metrics import FBetaScore +from monai.metrics.f_beta_score import get_f_beta_score from tests.test_utils import assert_allclose _device = "cuda:0" if torch.cuda.is_available() else "cpu" @@ -75,6 +76,22 @@ def test_prediction_and_result_have_different_shape(self): with self.assertRaises(ValueError): metric(y_pred=torch.Tensor([[1, 1, 1], [1, 1, 1]]), y=torch.Tensor([1, 1, 1])) + def test_bool_inputs_match_float_inputs(self): + """Bool masks (e.g. ``pred > 0.5``) must give the same counts and score as their float equivalents.""" + generator = torch.Generator().manual_seed(0) + y_pred = torch.rand(2, 3, 8, 8, generator=generator) > 0.5 + y = torch.rand(2, 3, 8, 8, generator=generator) > 0.5 + + for include_background in (True, False): + expected = get_f_beta_score(y_pred.float(), y.float(), include_background=include_background) + result = get_f_beta_score(y_pred, y, include_background=include_background) + assert_allclose(result, expected, type_test=False) + self.assertTrue(bool((result[..., 0] > 0).all())) # true positives are counted + + metric = FBetaScore() + metric(y_pred=y, y=y) + assert_allclose(metric.aggregate()[0], torch.Tensor([1.0]), atol=1e-7, rtol=1e-7) + if __name__ == "__main__": unittest.main()