Repository: Project-MONAI/MONAI

Issue:
Add utility to simplify metric handlers API
**Is your feature request related to a problem? Please describe.**
Currently, users must set `output_transform = lambda output: output["pred"], output["label"]` for metrics.
We have received much feedback from users that the `lambda` function is hard to understand.
Thanks for the discussion with @vfdev-5 in ignite ticket:
https://github.com/pytorch/ignite/pull/2055#issuecomment-861380281
I will try to add utility to simplify the usage of the metrics with the dictionary.




Discussion:

Resolving patch:
diff --git a/monai/handlers/__init__.py b/monai/handlers/__init__.py
--- a/monai/handlers/__init__.py
+++ b/monai/handlers/__init__.py
@@ -33,6 +33,7 @@
 from .transform_inverter import TransformInverter
 from .utils import (
     evenly_divisible_all_gather,
+    from_engine,
     stopping_fn_from_loss,
     stopping_fn_from_metric,
     string_list_all_gather,
diff --git a/monai/handlers/utils.py b/monai/handlers/utils.py
--- a/monai/handlers/utils.py
+++ b/monai/handlers/utils.py
@@ -17,6 +17,7 @@
 import numpy as np
 import torch
 
+from monai.config import KeysCollection
 from monai.utils import ensure_tuple, exact_version, get_torch_version_tuple, optional_import
 
 idist, _ = optional_import("ignite", "0.4.4", exact_version, "distributed")
@@ -31,6 +32,7 @@
     "evenly_divisible_all_gather",
     "string_list_all_gather",
     "write_metrics_reports",
+    "from_engine",
 ]
 
 
@@ -228,3 +230,28 @@ def _compute_op(op: str, d: np.ndarray):
                     f.write(f"class{deli}{deli.join(ops)}\n")
                     for i, c in enumerate(np.transpose(v)):
                         f.write(f"{class_labels[i]}{deli}{deli.join([f'{_compute_op(k, c):.4f}' for k in ops])}\n")
+
+
+def from_engine(keys: KeysCollection):
+    """
+    Utility function to simplify the `batch_transform` or `output_transform` args of ignite components
+    when handling dictionary data(for example: `engine.state.batch` or `engine.state.output`).
+    Users only need to set the expected keys, then it will return a callable function to extract data from
+    dictionary and construct a tuple respectively.
+    It can help avoid a complicated `lambda` function and make the arg of metrics more straight-forward.
+    For example, set the first key as the prediction and the second key as label to get the expected data
+    from `engine.state.output` for a metric::
+
+        from monai.handlers import MeanDice, from_engine
+
+        metric = MeanDice(
+            include_background=False,
+            output_transform=from_engine(["pred", "label"])
+        )
+
+    """
+
+    def _wrapper(output: Dict):
+        return tuple(output[k] for k in ensure_tuple(keys))
+
+    return _wrapper


Test patch:
diff --git a/tests/test_handler_mean_dice.py b/tests/test_handler_mean_dice.py
--- a/tests/test_handler_mean_dice.py
+++ b/tests/test_handler_mean_dice.py
@@ -12,13 +12,13 @@
 import unittest
 
 import torch
-from ignite.engine import Engine
+from ignite.engine import Engine, Events
 from parameterized import parameterized
 
-from monai.handlers import MeanDice
+from monai.handlers import MeanDice, from_engine
 
-TEST_CASE_1 = [{"include_background": True}, 0.75, (4, 2)]
-TEST_CASE_2 = [{"include_background": False}, 0.66666, (4, 1)]
+TEST_CASE_1 = [{"include_background": True, "output_transform": from_engine(["pred", "label"])}, 0.75, (4, 2)]
+TEST_CASE_2 = [{"include_background": False, "output_transform": from_engine(["pred", "label"])}, 0.66666, (4, 1)]
 
 
 class TestHandlerMeanDice(unittest.TestCase):
@@ -37,14 +37,17 @@ def _val_func(engine, batch):
         # test input a list of channel-first tensor
         y_pred = [torch.Tensor([[0], [1]]), torch.Tensor([[1], [0]])]
         y = torch.Tensor([[[0], [1]], [[0], [1]]])
-        dice_metric.update([y_pred, y])
+        engine.state.output = {"pred": y_pred, "label": y}
+        engine.fire_event(Events.ITERATION_COMPLETED)
 
         y_pred = [torch.Tensor([[0], [1]]), torch.Tensor([[1], [0]])]
         y = torch.Tensor([[[0], [1]], [[1], [0]]])
-        dice_metric.update([y_pred, y])
+        engine.state.output = {"pred": y_pred, "label": y}
+        engine.fire_event(Events.ITERATION_COMPLETED)
 
-        avg_dice = dice_metric.compute()
-        self.assertAlmostEqual(avg_dice, expected_avg, places=4)
+        engine.fire_event(Events.EPOCH_COMPLETED)
+
+        self.assertAlmostEqual(engine.state.metrics["mean_dice"], expected_avg, places=4)
         self.assertTupleEqual(tuple(engine.state.metric_details["mean_dice"].shape), details_shape)
 
     @parameterized.expand([TEST_CASE_1, TEST_CASE_2])