Skip to content

Commit 1551cb8

Browse files
committed
perf: speed up FROC curve computation
1 parent e0cee14 commit 1551cb8

2 files changed

Lines changed: 45 additions & 11 deletions

File tree

‎monai/metrics/froc.py‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,15 +145,18 @@ def compute_froc_curve_data(
145145
if isinstance(tp_probs, torch.Tensor):
146146
tp_probs = tp_probs.detach().cpu().numpy()
147147

148-
total_fps, total_tps = [], []
148+
# Keep the threshold definition (unique scores from both arrays, excluding the
149+
# smallest score) while avoiding a full scan of both arrays at every threshold.
149150
all_probs = sorted(set(list(fp_probs) + list(tp_probs)))
150-
for thresh in all_probs[1:]:
151-
total_fps.append((fp_probs >= thresh).sum())
152-
total_tps.append((tp_probs >= thresh).sum())
153-
total_fps.append(0)
154-
total_tps.append(0)
155-
fps_per_image = np.asarray(total_fps) / float(num_images)
156-
total_sensitivity = np.asarray(total_tps) / float(num_targets)
151+
thresholds = all_probs[1:]
152+
sorted_fp_probs = np.sort(fp_probs)
153+
sorted_tp_probs = np.sort(tp_probs)
154+
total_fps = len(sorted_fp_probs) - np.searchsorted(sorted_fp_probs, thresholds, side="left")
155+
total_tps = len(sorted_tp_probs) - np.searchsorted(sorted_tp_probs, thresholds, side="left")
156+
total_fps = np.append(total_fps, 0)
157+
total_tps = np.append(total_tps, 0)
158+
fps_per_image = total_fps / float(num_images)
159+
total_sensitivity = total_tps / float(num_targets)
157160
return fps_per_image, total_sensitivity
158161

159162

‎tests/metrics/test_compute_froc.py‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@
139139

140140

141141
class TestComputeFpTp(unittest.TestCase):
142-
143142
@parameterized.expand(
144143
[TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_EXCLUDE_ABSENT, TEST_CASE_EXCLUDE_DUPLICATE]
145144
)
@@ -151,7 +150,6 @@ def test_value(self, input_data, expected_fp, expected_tp, expected_num):
151150

152151

153152
class TestComputeFpTpNd(unittest.TestCase):
154-
155153
@parameterized.expand([TEST_CASE_ND_1, TEST_CASE_ND_2])
156154
def test_value(self, input_data, expected_fp, expected_tp, expected_num):
157155
fp_probs, tp_probs, num_tumors = compute_fp_tp_probs_nd(**input_data)
@@ -161,13 +159,46 @@ def test_value(self, input_data, expected_fp, expected_tp, expected_num):
161159

162160

163161
class TestComputeFrocScore(unittest.TestCase):
164-
165162
@parameterized.expand([TEST_CASE_4, TEST_CASE_5])
166163
def test_value(self, input_data, thresholds, expected_score):
167164
fps_per_image, total_sensitivity = compute_froc_curve_data(**input_data)
168165
score = compute_froc_score(fps_per_image, total_sensitivity, thresholds)
169166
np.testing.assert_allclose(score, expected_score, rtol=1e-5)
170167

171168

169+
def _reference_compute_froc_curve_data(fp_probs, tp_probs, num_targets, num_images):
170+
"""Original threshold-by-threshold implementation, retained for regression comparisons."""
171+
total_fps, total_tps = [], []
172+
all_probs = sorted(set(list(fp_probs) + list(tp_probs)))
173+
for thresh in all_probs[1:]:
174+
total_fps.append((fp_probs >= thresh).sum())
175+
total_tps.append((tp_probs >= thresh).sum())
176+
total_fps.append(0)
177+
total_tps.append(0)
178+
return np.asarray(total_fps) / float(num_images), np.asarray(total_tps) / float(num_targets)
179+
180+
181+
class TestComputeFrocCurveData(unittest.TestCase):
182+
@parameterized.expand(
183+
[
184+
("duplicates", [0.2, 0.5, 0.5, 0.9], [0.1, 0.5, 0.5, 0.8]),
185+
("mostly_unique", [0.12, 0.34, 0.56, 0.78], [0.23, 0.45, 0.67, 0.89]),
186+
("all_equal", [0.4, 0.4], [0.4, 0.4, 0.4]),
187+
("empty", [], []),
188+
("empty_fp", [], [0.2, 0.6, 0.6]),
189+
("empty_tp", [0.2, 0.6, 0.6], []),
190+
("threshold_equality", [0.2, 0.5, 0.8], [0.5, 0.5, 0.9]),
191+
]
192+
)
193+
def test_matches_reference(self, _, fp_probs, tp_probs):
194+
fp_probs = np.asarray(fp_probs)
195+
tp_probs = np.asarray(tp_probs)
196+
actual = compute_froc_curve_data(fp_probs, tp_probs, num_targets=3, num_images=2)
197+
expected = _reference_compute_froc_curve_data(fp_probs, tp_probs, num_targets=3, num_images=2)
198+
for actual_array, expected_array in zip(actual, expected):
199+
np.testing.assert_array_equal(actual_array, expected_array)
200+
self.assertEqual(actual_array.dtype, expected_array.dtype)
201+
202+
172203
if __name__ == "__main__":
173204
unittest.main()

0 commit comments

Comments
 (0)