Skip to content

Commit ed76cd5

Browse files
aymuos15ericspod
andauthored
Fix class_labels mutation across multi-metric write_metrics_reports (#8902)
### Description `write_metrics_reports` rebinds and appends to its `class_labels` parameter inside the `metric_details` loop. After the first metric's CSV is written, `class_labels` is no longer `None`. It holds `["class0", ..., "classN", "mean"]`. On every subsequent metric the `else` branch runs and appends another `"mean"`, producing headers like `class0,class1,mean,mean,mean`. This hits any evaluation with two or more metrics in `metric_details`, for example Dice + IoU + Hausdorff. The only real caller `MetricsSaver` always passes `class_labels=None`. The first metric's CSV is correct, but every one after is structurally corrupt with mismatched header and data columns. The fix uses a local `labels` variable per iteration so the original `class_labels` parameter is never modified. The existing test only verified existence of subsequent metric files, not their headers. A regression test is included. ### Types of changes - [x] Non-breaking change (fix or new feature that would not break existing functionality). - [x] New tests added to cover the changes. - [ ] Integration tests passed locally by running `./runtests.sh -f -u --net --coverage`. - [ ] Quick tests passed locally by running `./runtests.sh --quick --unittests --disttests`. - [ ] In-line docstrings updated. --------- Signed-off-by: Soumya Snigdha Kundu <soumya_snigdha.kundu@kcl.ac.uk> Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
1 parent 3ee058b commit ed76cd5

2 files changed

Lines changed: 27 additions & 5 deletions

File tree

monai/handlers/utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,15 @@ class mean median max 5percentile 95percentile notnans
122122

123123
# add the average value of all classes to v
124124
if class_labels is None:
125-
class_labels = ["class" + str(i) for i in range(v.shape[1])]
125+
labels = ["class" + str(i) for i in range(v.shape[1])]
126126
else:
127-
class_labels = [str(i) for i in class_labels] # ensure to have a list of str
127+
labels = [str(i) for i in class_labels] # ensure to have a list of str
128128

129-
class_labels += ["mean"]
129+
labels += ["mean"]
130130
v = np.concatenate([v, np.nanmean(v, axis=1, keepdims=True)], axis=1)
131131

132132
with open(os.path.join(save_dir, f"{k}_raw.csv"), "w") as f:
133-
f.write(f"filename{deli}{deli.join(class_labels)}\n")
133+
f.write(f"filename{deli}{deli.join(labels)}\n")
134134
for i, b in enumerate(v):
135135
f.write(
136136
f"{images[i] if images is not None else str(i)}{deli}"
@@ -164,7 +164,7 @@ def _compute_op(op: str, d: np.ndarray) -> Any:
164164
with open(os.path.join(save_dir, f"{k}_summary.csv"), "w") as f:
165165
f.write(f"class{deli}{deli.join(ops)}\n")
166166
for i, c in enumerate(np.transpose(v)):
167-
f.write(f"{class_labels[i]}{deli}{deli.join([f'{_compute_op(k, c):.4f}' for k in ops])}\n")
167+
f.write(f"{labels[i]}{deli}{deli.join([f'{_compute_op(k, c):.4f}' for k in ops])}\n")
168168

169169

170170
def from_engine(keys: KeysCollection, first: bool = False) -> Callable:

tests/handlers/test_write_metrics_reports.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ def test_content(self):
6363
self.assertTrue(os.path.exists(os.path.join(tempdir, "metric4_raw.csv")))
6464
self.assertTrue(os.path.exists(os.path.join(tempdir, "metric4_summary.csv")))
6565

66+
def test_multi_metric_details_headers(self):
67+
with tempfile.TemporaryDirectory() as tempdir:
68+
write_metrics_reports(
69+
save_dir=Path(tempdir),
70+
images=["img1", "img2"],
71+
metrics=None,
72+
metric_details={
73+
"m1": torch.tensor([[1, 2, 3], [4, 5, 6]]),
74+
"m2": torch.tensor([[7, 8], [9, 10]]),
75+
"m3": torch.tensor([[11, 12, 13, 14], [15, 16, 17, 18]]),
76+
},
77+
summary_ops=None,
78+
deli=",",
79+
output_type="csv",
80+
)
81+
for name, nclass in [("m1", 3), ("m2", 2), ("m3", 4)]:
82+
path = os.path.join(tempdir, f"{name}_raw.csv")
83+
self.assertTrue(os.path.exists(path))
84+
with open(path) as f:
85+
header = f.readline().strip().split(",")
86+
self.assertEqual(header, ["filename"] + [f"class{i}" for i in range(nclass)] + ["mean"])
87+
6688

6789
if __name__ == "__main__":
6890
unittest.main()

0 commit comments

Comments
 (0)