|
291 | 291 | ] |
292 | 292 |
|
293 | 293 |
|
| 294 | +# single-channel y (class indices) with multi-channel y_pred (one-hot) |
| 295 | +TEST_CASE_MIXED_1 = [ |
| 296 | + { |
| 297 | + "y_pred": torch.tensor( |
| 298 | + [[[[0.0, 1.0], [0.0, 0.0]], [[0.0, 0.0], [0.0, 1.0]], [[1.0, 0.0], [1.0, 0.0]]]] |
| 299 | + ), # (1, 3, 2, 2) one-hot |
| 300 | + "y": torch.tensor([[[[0.0, 1.0], [2.0, 1.0]]]]), # (1, 1, 2, 2) class indices |
| 301 | + "include_background": True, |
| 302 | + }, |
| 303 | + # class 0: y_gt=[[1,0],[0,0]], y_pred=[[0,1],[0,0]] -> dice=0.0 |
| 304 | + # class 1: y_gt=[[0,1],[0,1]], y_pred=[[0,0],[0,1]] -> dice=2/3 |
| 305 | + # class 2: y_gt=[[0,0],[1,0]], y_pred=[[1,0],[1,0]] -> dice=2/3 |
| 306 | + [[0.0000, 0.6667, 0.6667]], |
| 307 | +] |
| 308 | + |
| 309 | +# single-channel y_pred (argmaxed, with num_classes) with multi-channel y (one-hot) |
| 310 | +TEST_CASE_MIXED_2 = [ |
| 311 | + { |
| 312 | + "y_pred": torch.tensor([[[[2.0, 2.0], [2.0, 2.0]]]]), # (1, 1, 2, 2) all class 2 |
| 313 | + "y": torch.tensor( |
| 314 | + [[[[1.0, 1.0], [1.0, 1.0]], [[0.0, 0.0], [0.0, 0.0]], [[0.0, 0.0], [0.0, 0.0]]]] |
| 315 | + ), # (1, 3, 2, 2) one-hot, all background |
| 316 | + "include_background": True, |
| 317 | + "num_classes": 3, |
| 318 | + }, |
| 319 | + # class 0: y_gt=[1,1,1,1](4), y_pred=[0,0,0,0](0) -> dice=0.0 |
| 320 | + # class 1: y_gt=[0,0,0,0](0), y_pred=[0,0,0,0](0) -> dice=nan (ignore_empty default) |
| 321 | + # class 2: y_gt=[0,0,0,0](0), y_pred=[1,1,1,1](4) -> dice=nan (ignore_empty default) |
| 322 | + [[False, True, True]], # False=not-nan, True=nan |
| 323 | +] |
| 324 | + |
| 325 | +# single-channel y (class indices) with multi-channel y_pred, exclude background |
| 326 | +TEST_CASE_MIXED_3 = [ |
| 327 | + { |
| 328 | + "y_pred": torch.tensor( |
| 329 | + [ |
| 330 | + [[[0.0, 1.0], [0.0, 0.0]], [[0.0, 0.0], [1.0, 1.0]], [[1.0, 0.0], [0.0, 0.0]]], |
| 331 | + [[[0.0, 0.0], [0.0, 1.0]], [[1.0, 0.0], [0.0, 0.0]], [[0.0, 1.0], [1.0, 0.0]]], |
| 332 | + ] |
| 333 | + ), # (2, 3, 2, 2) one-hot |
| 334 | + "y": torch.tensor([[[[0.0, 0.0], [0.0, 1.0]]], [[[0.0, 0.0], [0.0, 1.0]]]]), # (2, 1, 2, 2) class indices |
| 335 | + "include_background": False, |
| 336 | + }, |
| 337 | + # batch 0: class 1 y_gt=[[0,0],[0,1]], y_pred=[[0,0],[1,1]] -> dice=2/3 |
| 338 | + # class 2 y_gt=[[0,0],[0,0]], y_pred=[[1,0],[0,0]] -> dice=nan |
| 339 | + # batch 1: class 1 y_gt=[[0,0],[0,1]], y_pred=[[1,0],[0,0]] -> dice=0.0 |
| 340 | + # class 2 y_gt=[[0,0],[0,0]], y_pred=[[0,1],[1,0]] -> dice=nan |
| 341 | + [[False, True], [False, True]], # nan pattern |
| 342 | +] |
| 343 | + |
| 344 | + |
294 | 345 | class TestComputeMeanDice(unittest.TestCase): |
295 | 346 |
|
296 | | - @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_9, TEST_CASE_11, TEST_CASE_12]) |
| 347 | + @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_9, TEST_CASE_11, TEST_CASE_12, TEST_CASE_MIXED_1]) |
297 | 348 | def test_value(self, input_data, expected_value): |
298 | 349 | result = compute_dice(**input_data) |
299 | 350 | np.testing.assert_allclose(result.cpu().numpy(), expected_value, atol=1e-4) |
300 | 351 | np.testing.assert_equal(result.device, input_data["y_pred"].device) |
301 | 352 |
|
302 | | - @parameterized.expand([TEST_CASE_3]) |
| 353 | + @parameterized.expand([TEST_CASE_3, TEST_CASE_MIXED_2, TEST_CASE_MIXED_3]) |
303 | 354 | def test_nans(self, input_data, expected_value): |
304 | 355 | result = compute_dice(**input_data) |
305 | 356 | self.assertTrue(np.allclose(np.isnan(result.cpu().numpy()), expected_value)) |
@@ -359,6 +410,32 @@ def test_channel_dimensions(self): |
359 | 410 | with self.assertRaises(ValueError): |
360 | 411 | DiceMetric(per_component=True)(torch.ones([3, 3, 144, 144]), torch.ones([3, 3, 144, 144])) |
361 | 412 |
|
| 413 | + def test_label_map_fast_path(self): |
| 414 | + torch.manual_seed(0) |
| 415 | + for n in (3, 8): |
| 416 | + for include_background in (True, False): |
| 417 | + for ignore_empty in (True, False): |
| 418 | + kwargs = dict( |
| 419 | + num_classes=n, |
| 420 | + include_background=include_background, |
| 421 | + ignore_empty=ignore_empty, |
| 422 | + get_not_nans=False, |
| 423 | + ) |
| 424 | + y_pred = torch.randint(0, n, (2, 1, 8, 8, 8)) |
| 425 | + y = torch.randint(0, n, (2, 1, 8, 8, 8)) |
| 426 | + fast = DiceHelper(**kwargs)(y_pred, y) |
| 427 | + ref = DiceHelper(**kwargs)(torch.nn.functional.one_hot(y_pred[:, 0], n).movedim(-1, 1), y) |
| 428 | + np.testing.assert_array_equal(torch.isnan(fast).cpu().numpy(), torch.isnan(ref).cpu().numpy()) |
| 429 | + np.testing.assert_allclose( |
| 430 | + torch.nan_to_num(fast).cpu().numpy(), torch.nan_to_num(ref).cpu().numpy(), atol=1e-4 |
| 431 | + ) |
| 432 | + |
| 433 | + def test_label_map_out_of_range_fallback(self): |
| 434 | + y_pred = torch.tensor([[[[0, 1, 2, 3]]]]) |
| 435 | + y = torch.tensor([[[[0, 1, 2, 0]]]]) |
| 436 | + result = DiceHelper(num_classes=3, get_not_nans=False)(y_pred, y) |
| 437 | + np.testing.assert_allclose(result.cpu().numpy(), [1.0, 1.0], atol=1e-4) |
| 438 | + |
362 | 439 |
|
363 | 440 | if __name__ == "__main__": |
364 | 441 | unittest.main() |
0 commit comments