From 34b22078f7f92c55d46e09aa55fe3f47b6fd9d48 Mon Sep 17 00:00:00 2001 From: tritsystem Date: Fri, 16 Oct 2026 14:34:14 -0700 Subject: [PATCH 1/2] fix(metrics): count true positives for bool inputs in get_confusion_matrix get_confusion_matrix finds true positives with (y_pred + y) == 2. For bool tensors that sum is a logical OR, which is never 2, so bool masks such as pred > 0.5 lost every true positive: a perfect prediction scored sensitivity 0 and F1 0. Cast bool inputs to float before counting; float and integer inputs are unchanged. Assisted-by: Claude Opus 5.5 Signed-off-by: tritsystem --- monai/metrics/confusion_matrix.py | 6 ++++++ tests/metrics/test_compute_confusion_matrix.py | 12 ++++++++++++ 2 files changed, 18 insertions(+) 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/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() From 67a70aedbc436884d6a93be9f6c18e622610f906 Mon Sep 17 00:00:00 2001 From: tritsystem Date: Fri, 16 Oct 2026 15:35:28 -0700 Subject: [PATCH 2/2] fix(metrics): count true positives for bool inputs in get_f_beta_score get_f_beta_score has the same (y_pred + y) == 2 true-positive test as get_confusion_matrix; both lost their float casts in #7187. For bool masks the sum is a logical OR, so every true positive was dropped and a perfect prediction scored F-beta 0. Cast bool inputs to float before counting, as in the previous commit; float and integer inputs are unchanged. Assisted-by: Claude Opus 5.5 Signed-off-by: tritsystem --- monai/metrics/f_beta_score.py | 7 +++++++ tests/metrics/test_compute_f_beta.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) 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_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()