Repository: Project-MONAI/MONAI

Issue:
Promote Child Items to Top Level
The MONAO dict-based transforms acts on the top level keys, for instance `data={"image": ..., "pred": ...}`; however, when the output is nested (like in the hovernet model), the actual data will lie one level below like `data={"image": ..., "pred": {"a":..., "b":...}}`.

Currently, if user want to apply transform on "a" or "b", it can use `Lambdad` and apply that transfrom on `data["pred"]` instead and use "a" and "b" as keys. However, if the user want to apply a transform on "image" and "a" for instance, I didn't find any transform that can enable it.

Here a transform that can promote these children items ("a" and "b") to the same level of "image" can solve the problem.

premerge flake8 config error
**Describe the bug**
```
flake8
Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/__main__.py", line 7, in <module>
    raise SystemExit(main())
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/main/cli.py", line 23, in main
    app.run(argv)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/main/application.py", line 198, in run
    self._run(argv)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/main/application.py", line 186, in _run
    self.initialize(argv)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/main/application.py", line 165, in initialize
    self.plugins, self.options = parse_args(argv)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/options/parse_args.py", line 53, in parse_args
    opts = aggregator.aggregate_options(option_manager, cfg, cfg_dir, rest)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/options/aggregator.py", line 30, in aggregate_options
    parsed_config = config.parse_config(manager, cfg, cfg_dir)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/options/config.py", line 131, in parse_config
    raise ValueError(
ValueError: Error code '#' supplied to 'ignore' option does not match '^[A-Z]{1,3}[0-9]{0,3}$'
Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/__main__.py", line 7, in <module>
    raise SystemExit(main())
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/main/cli.py", line 23, in main
    app.run(argv)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/main/application.py", line 198, in run
    self._run(argv)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/main/application.py", line 186, in _run
    self.initialize(argv)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/main/application.py", line 165, in initialize
    self.plugins, self.options = parse_args(argv)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/options/parse_args.py", line 53, in parse_args
    opts = aggregator.aggregate_options(option_manager, cfg, cfg_dir, rest)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/options/aggregator.py", line 30, in aggregate_options
    parsed_config = config.parse_config(manager, cfg, cfg_dir)
  File "/opt/hostedtoolcache/Python/3.8.14/x64/lib/python3.8/site-packages/flake8/options/config.py", line 131, in parse_config
    raise ValueError(
ValueError: Error code '#' supplied to 'ignore' option does not match '^[A-Z]{1,3}[0-9]{0,3}$'
```
from

https://github.com/Project-MONAI/MONAI/blob/86f2573de3b6bf9dff5f4654320bdd88c7bb50e7/setup.cfg#L131-L132



Discussion:
Hi, I'm not familiar with this model, but i'm just curious  at which place do we get the output in this form `"pred": {"a":..., "b":...}}` during loading pipeline or post-processing after prediction? I don't think any loading, augmentation transforms produce such format.   or if the model  produces such output, can we simple reformat the output of the model? thank you
Hi @myron, we got this in the output of HoVerNet model.
```
{"pred": {"horizontal_vertical": ..., "nucleus_prediction": ..., "type_prediction": ...}
```
https://github.com/Project-MONAI/MONAI/blob/77651add61b50f2a31fcc3c8b510d3179daa078c/monai/networks/nets/hovernet.py#L581-L586


Changing that will cause backward incompatibilities internally and externally.

I see, thank you.  then if you have access to model output in the python code,  you could directly add those keys "horizontal_vertical", 'nucleus_prediction".. to you main dict 
e.g. `batch_data.update(output["pred])`
this would save you from writing another monai transform. 
but, I guess you don't have access to it in python code, then yes, you'd need a new transfor . I think there is a similar in-spirit transform  **splitdim** https://docs.monai.io/en/stable/transforms.html#splitdim, but that only split a tensor dimension to create new key, value pairs. 



Resolving patch:
diff --git a/monai/handlers/probability_maps.py b/monai/handlers/probability_maps.py
--- a/monai/handlers/probability_maps.py
+++ b/monai/handlers/probability_maps.py
@@ -10,13 +10,13 @@
 # limitations under the License.
 
 import logging
-import os
 import threading
 from typing import TYPE_CHECKING, Dict, Optional
 
 import numpy as np
 
 from monai.config import DtypeLike, IgniteInfo
+from monai.data.folder_layout import FolderLayout
 from monai.utils import ProbMapKeys, min_version, optional_import
 from monai.utils.enums import CommonKeys
 
@@ -52,10 +52,17 @@ def __init__(
             name: identifier of logging.logger to use, defaulting to `engine.logger`.
 
         """
+        self.folder_layout = FolderLayout(
+            output_dir=output_dir,
+            postfix=output_postfix,
+            extension=".npy",
+            parent=False,
+            makedirs=True,
+            data_root_dir="",
+        )
+
         self.logger = logging.getLogger(name)
         self._name = name
-        self.output_dir = output_dir
-        self.output_postfix = output_postfix
         self.prob_key = prob_key
         self.dtype = dtype
         self.prob_map: Dict[str, np.ndarray] = {}
@@ -113,8 +120,8 @@ def save_prob_map(self, name: str) -> None:
         Args:
             name: the name of image to be saved.
         """
-        file_path = os.path.join(self.output_dir, name)
-        np.save(file_path + self.output_postfix + ".npy", self.prob_map[name])
+        file_path = self.folder_layout.filename(name)
+        np.save(file_path, self.prob_map[name])
 
         self.num_done_images += 1
         self.logger.info(f"Inference of '{name}' is done [{self.num_done_images}/{self.num_images}]!")
diff --git a/monai/metrics/metric.py b/monai/metrics/metric.py
--- a/monai/metrics/metric.py
+++ b/monai/metrics/metric.py
@@ -62,7 +62,6 @@ def __call__(self, y_pred: TensorOrList, y: Optional[TensorOrList] = None):
             When it's a list of tensors, each item in the list can represent a specific type of metric.
 
         """
-        ret: TensorOrList
         # handling a list of channel-first data
         if isinstance(y_pred, (list, tuple)) or isinstance(y, (list, tuple)):
             return self._compute_list(y_pred, y)
diff --git a/monai/networks/nets/milmodel.py b/monai/networks/nets/milmodel.py
--- a/monai/networks/nets/milmodel.py
+++ b/monai/networks/nets/milmodel.py
@@ -78,7 +78,7 @@ def __init__(
             nfc = net.fc.in_features  # save the number of final features
             net.fc = torch.nn.Identity()  # remove final linear layer
 
-            self.extra_outputs = {}  # type: Dict[str, torch.Tensor]
+            self.extra_outputs: Dict[str, torch.Tensor] = {}
 
             if mil_mode == "att_trans_pyramid":
                 # register hooks to capture outputs of intermediate layers


Test patch: