Repository: Project-MONAI/MONAI

Issue:
Make HoVerNet  work SuperviseTrainer
Using `SupervisedTrainer` for HoVerNet pipeline training requires a `prepare_batch` to prepare data differently than how it is being prepared in `default_prepare_batch` since HoVerNet target includes three different tensors.


Discussion:
Pending on @KumoLiu to verify whether the existing `ExtraPrepareBatch` can satisfy the requirement.
If not, I will try to implement a new subclass.

Thanks.

Hi @Nic-Ma @KumoLiu, may I know what is the status of this? Is it in progress or you have found another way to train? Thanks
Hi @drbeh ,

The draft PR is here: https://github.com/Project-MONAI/tutorials/pull/999.

Thanks.

@KumoLiu has already implemented it in the tutorial and will port it to the monai core, maybe monai/apps/pathology.

Resolving patch:
diff --git a/monai/apps/pathology/handlers/utils.py b/monai/apps/pathology/handlers/utils.py
new file mode 100644
--- /dev/null
+++ b/monai/apps/pathology/handlers/utils.py
@@ -0,0 +1,50 @@
+# Copyright (c) MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from monai.config import KeysCollection
+from monai.utils import ensure_tuple
+
+
+def from_engine_hovernet(keys: KeysCollection, nested_key: str):
+    """
+    Since the output of HoVerNet is a dictionary, this function is to extend `monai.handlers.from_engine`
+    to work with HoVerNet.
+
+    If data is a list of nestes dictionaries after decollating, extract nested value with expected keys and
+    construct lists respectively, for example,
+    if data is `[{"A": {"C": 1, "D": 2}, "B": {"C": 2, "D": 2}}, {"A":  {"C": 3, "D": 2}, "B":  {"C": 4, "D": 2}}]`,
+    from_engine_hovernet(["A", "B"], "C"): `([1, 3], [2, 4])`.
+
+    Here is a simple example::
+
+        from monai.handlers import MeanDice, from_engine_hovernet
+
+        metric = MeanDice(
+            include_background=False,
+            output_transform=from_engine_hovernet(keys=["pred", "label"], nested_key=HoVerNetBranch.NP.value)
+        )
+
+    Args:
+        keys: specified keys to extract data from dictionary or decollated list of dictionaries.
+        nested_key: specified key to extract nested data from dictionary or decollated list of dictionaries.
+
+    """
+    keys = ensure_tuple(keys)
+
+    def _wrapper(data):
+        if isinstance(data, dict):
+            return tuple(data[k][nested_key] for k in keys)
+        if isinstance(data, list) and isinstance(data[0], dict):
+            # if data is a list of dictionaries, extract expected keys and construct lists,
+            ret = [[i[k][nested_key] for i in data] for k in keys]
+            return tuple(ret) if len(ret) > 1 else ret[0]
+
+    return _wrapper


Test patch:
diff --git a/tests/test_from_engine_hovernet.py b/tests/test_from_engine_hovernet.py
new file mode 100644
--- /dev/null
+++ b/tests/test_from_engine_hovernet.py
@@ -0,0 +1,36 @@
+# Copyright (c) MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+from parameterized import parameterized
+
+from monai.apps.pathology.handlers.utils import from_engine_hovernet
+from tests.utils import assert_allclose
+
+TEST_CASE_0 = [
+    [{"A": {"C": 1, "D": 2}, "B": {"C": 2, "D": 2}}, {"A": {"C": 3, "D": 2}, "B": {"C": 4, "D": 2}}],
+    ([1, 3], [2, 4]),
+]
+TEST_CASE_1 = [{"A": {"C": 1, "D": 2}, "B": {"C": 2, "D": 2}}, (1, 2)]
+
+CASES = [TEST_CASE_0, TEST_CASE_1]
+
+
+class TestFromEngineHovernet(unittest.TestCase):
+    @parameterized.expand(CASES)
+    def test_results(self, input, expected):
+        output = from_engine_hovernet(keys=["A", "B"], nested_key="C")(input)
+        assert_allclose(output, expected, type_test=False)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_sampler_dist.py b/tests/test_sampler_dist.py
--- a/tests/test_sampler_dist.py
+++ b/tests/test_sampler_dist.py
@@ -17,7 +17,7 @@
 
 from monai.data import CacheDataset, DataLoader, DistributedSampler
 from monai.transforms import ToTensor
-from tests.utils import DistCall, DistTestCase
+from tests.utils import DistCall, DistTestCase, assert_allclose
 
 
 class DistributedSamplerTest(DistTestCase):
@@ -45,26 +45,26 @@ def test_uneven(self):
         if dist.get_rank() == 1:
             np.testing.assert_allclose(samples, np.array([2, 4]))
 
-    @DistCall(nnodes=1, nproc_per_node=2)
+    @DistCall(nnodes=1, nproc_per_node=2, timeout=120)
     def test_cachedataset(self):
         data = [1, 2, 3, 4, 5]
         dataset = CacheDataset(data=data, transform=ToTensor(track_meta=False), cache_rate=1.0, runtime_cache=True)
         sampler = DistributedSampler(dataset=dataset, shuffle=False, even_divisible=False)
         dataloader = DataLoader(dataset=dataset, sampler=sampler, batch_size=1, num_workers=2)
         for i in range(3):
-            dist.barrier()
             if i > 0:
                 # verify the runtime cache content is completed after first epoch
                 for j, c in enumerate(dataset._cache):
-                    self.assertTrue(isinstance(c, torch.Tensor))
-                    torch.testing.assert_allclose(c, j + 1)
+                    self.assertIsInstance(c, torch.Tensor)
+                    assert_allclose(c, j + 1, type_test=False)
             for k, d in enumerate(dataloader):
-                self.assertTrue(isinstance(d, torch.Tensor))
+                self.assertIsInstance(d, torch.Tensor)
                 if dist.get_rank() == 0:
-                    torch.testing.assert_allclose(d[0], k * 2 + 1)
+                    assert_allclose(d[0], k * 2 + 1, type_test=False)
 
                 if dist.get_rank() == 1:
-                    torch.testing.assert_allclose(d[0], (k + 1) * 2)
+                    assert_allclose(d[0], (k + 1) * 2, type_test=False)
+            dist.barrier()
 
 
 if __name__ == "__main__":