From 9f4176b4aa20120f428d104da6629eca6d01826d 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 | 7 +++++++ tests/metrics/test_compute_confusion_matrix.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/monai/metrics/confusion_matrix.py b/monai/metrics/confusion_matrix.py index c471200069b..a12a1db8f47 100644 --- a/monai/metrics/confusion_matrix.py +++ b/monai/metrics/confusion_matrix.py @@ -163,6 +163,13 @@ class range, ignored regions are inferred from spatial locations where all label ValueError: when `y_pred` and `y` have different shapes. """ + # 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() + 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 2486049d79f..76ac4ce3f46 100644 --- a/tests/metrics/test_compute_confusion_matrix.py +++ b/tests/metrics/test_compute_confusion_matrix.py @@ -289,6 +289,18 @@ def test_precision(self, input_data, expected_value): assert_allclose(result, expected_value, atol=1e-4, rtol=1e-4) np.testing.assert_equal(result.device, input_data["y_pred"].device) + 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 ba59ee79427383ce7e08d76f57dfc47ac3b6d6db 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 bb9371c8bf2..5b40322323b 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 21a4e3c22c1..b207b0227cb 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()