From 5b0f0cf40b81f0fcb46163a0283b3198aa6ca608 Mon Sep 17 00:00:00 2001 From: Soumya Snigdha Kundu Date: Thu, 25 Jun 2026 18:01:03 +0100 Subject: [PATCH 1/3] Fix FROC num_targets miscount when labels_to_exclude is set compute_fp_tp_probs_nd derived the target count as max_label minus the length of labels_to_exclude, which is wrong whenever the exclusion list contains a label that is absent from the mask, is greater than max_label, or is duplicated. In those cases num_targets is too small and inflates sensitivity downstream in compute_froc_curve_data, where total_tps is divided by num_targets. Count instead the labels in [1, max_label] that are not excluded, reusing the existing loop so an excluded or out-of-range entry can no longer subtract a target that was never counted. Signed-off-by: Soumya Snigdha Kundu --- monai/metrics/froc.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/monai/metrics/froc.py b/monai/metrics/froc.py index 81a890aa68..af9a04b945 100644 --- a/monai/metrics/froc.py +++ b/monai/metrics/froc.py @@ -67,11 +67,13 @@ def compute_fp_tp_probs_nd( hittedlabel = evaluation_mask[tuple(coords.T)] fp_probs = probs[np.where(hittedlabel == 0)] + num_targets = 0 for i in range(1, max_label + 1): - if i not in labels_to_exclude and i in hittedlabel: - tp_probs[i - 1] = probs[np.where(hittedlabel == i)].max() + if i not in labels_to_exclude: + num_targets += 1 + if i in hittedlabel: + tp_probs[i - 1] = probs[np.where(hittedlabel == i)].max() - num_targets = max_label - len(labels_to_exclude) return fp_probs, tp_probs, cast(int, num_targets) From 679e5342b50baf682d239a0a9db8f4741164d6ea Mon Sep 17 00:00:00 2001 From: Soumya Snigdha Kundu Date: Thu, 25 Jun 2026 18:01:03 +0100 Subject: [PATCH 2/3] Add regression tests for FROC num_targets with excluded labels Cover an out-of-range exclusion and a duplicated exclusion; both previously reduced num_targets below the true target count and fail before the fix. Signed-off-by: Soumya Snigdha Kundu --- tests/metrics/test_compute_froc.py | 32 +++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/metrics/test_compute_froc.py b/tests/metrics/test_compute_froc.py index 4dc0507366..aa889ddb07 100644 --- a/tests/metrics/test_compute_froc.py +++ b/tests/metrics/test_compute_froc.py @@ -60,6 +60,34 @@ 3, ] +TEST_CASE_EXCLUDE_ABSENT = [ + { + "probs": torch.tensor([1, 0.6, 0.8]), + "y_coord": torch.tensor([0, 2, 3]), + "x_coord": torch.tensor([3, 0, 1]), + "evaluation_mask": np.array([[0, 0, 1, 1], [2, 2, 0, 0], [0, 3, 3, 0], [0, 3, 3, 3]]), + "labels_to_exclude": [5], + "resolution_level": 0, + }, + np.array([0.6]), + np.array([1, 0, 0.8]), + 3, +] + +TEST_CASE_EXCLUDE_DUPLICATE = [ + { + "probs": torch.tensor([1, 0.6, 0.8]), + "y_coord": torch.tensor([0, 2, 3]), + "x_coord": torch.tensor([3, 0, 1]), + "evaluation_mask": np.array([[0, 0, 1, 1], [2, 2, 0, 0], [0, 3, 3, 0], [0, 3, 3, 3]]), + "labels_to_exclude": [2, 2], + "resolution_level": 0, + }, + np.array([0.6]), + np.array([1, 0, 0.8]), + 2, +] + TEST_CASE_4 = [ { "fp_probs": np.array([0.8, 0.6]), @@ -112,7 +140,9 @@ class TestComputeFpTp(unittest.TestCase): - @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) + @parameterized.expand( + [TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_EXCLUDE_ABSENT, TEST_CASE_EXCLUDE_DUPLICATE] + ) def test_value(self, input_data, expected_fp, expected_tp, expected_num): fp_probs, tp_probs, num_tumors = compute_fp_tp_probs(**input_data) np.testing.assert_allclose(fp_probs, expected_fp, rtol=1e-5) From b0b76c9d612fc470fb3fbee8a9697c2bf0889863 Mon Sep 17 00:00:00 2001 From: Soumya Snigdha Kundu Date: Thu, 25 Jun 2026 18:49:52 +0100 Subject: [PATCH 3/3] Remove redundant cast to int for num_targets num_targets is now a plain Python int counter, so the cast(int, ...) wrapper is flagged by mypy as a redundant-cast. Signed-off-by: Soumya Snigdha Kundu --- monai/metrics/froc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/metrics/froc.py b/monai/metrics/froc.py index af9a04b945..3faef84917 100644 --- a/monai/metrics/froc.py +++ b/monai/metrics/froc.py @@ -11,7 +11,7 @@ from __future__ import annotations -from typing import Any, cast +from typing import Any import numpy as np import torch @@ -74,7 +74,7 @@ def compute_fp_tp_probs_nd( if i in hittedlabel: tp_probs[i - 1] = probs[np.where(hittedlabel == i)].max() - return fp_probs, tp_probs, cast(int, num_targets) + return fp_probs, tp_probs, num_targets def compute_fp_tp_probs(