Repository: Project-MONAI/MONAI

Issue:
Refactor loaders for common logic
**Is your feature request related to a problem? Please describe.**
Currently, most of the logic in `LoadNiftid`, `LoadPNGd` and `LoadNumpyd` are similar with duplicated code.
Need to define base class for them.



Discussion:

Resolving patch:
diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py
--- a/monai/transforms/io/dictionary.py
+++ b/monai/transforms/io/dictionary.py
@@ -15,7 +15,7 @@
 Class names are ended with 'd' to denote dictionary-based transforms.
 """
 
-from typing import Optional
+from typing import Optional, Callable
 
 import numpy as np
 
@@ -24,10 +24,10 @@
 from monai.transforms.io.array import LoadNifti, LoadPNG, LoadNumpy
 
 
-class LoadNiftid(MapTransform):
+class LoadDatad(MapTransform):
     """
-    Dictionary-based wrapper of :py:class:`monai.transforms.LoadNifti`,
-    must load image and metadata together. If loading a list of files in one key,
+    Base class for dictionary-based wrapper of IO loader transforms.
+    It must load image and metadata together. If loading a list of files in one key,
     stack them together and add a new dimension as the first dimension, and use the
     meta data of the first image to represent the stacked result. Note that the affine
     transform of all the stacked images should be same. The output metadata field will
@@ -35,22 +35,18 @@ class LoadNiftid(MapTransform):
     """
 
     def __init__(
-        self,
-        keys: KeysCollection,
-        as_closest_canonical: bool = False,
-        dtype: Optional[np.dtype] = np.float32,
-        meta_key_postfix: str = "meta_dict",
-        overwriting: bool = False,
+        self, keys: KeysCollection, loader: Callable, meta_key_postfix: str = "meta_dict", overwriting: bool = False,
     ) -> None:
         """
         Args:
             keys: keys of the corresponding items to be transformed.
                 See also: :py:class:`monai.transforms.compose.MapTransform`
-            as_closest_canonical: if True, load the image as closest to canonical axis format.
-            dtype: if not None convert the loaded image data to this data type.
-            meta_key_postfix: use `key_{postfix}` to store the metadata of the nifti image,
+            loader: callable function to load data from expected source.
+                typically, it's array level transform, for example: `LoadNifti`,
+                `LoadPNG` and `LoadNumpy`, etc.
+            meta_key_postfix: use `key_{postfix}` to store the metadata of the loaded data,
                 default is `meta_dict`. The meta data is a dictionary object.
-                For example, load nifti file for `image`, store the metadata into `image_meta_dict`.
+                For example, load Nifti file for `image`, store the metadata into `image_meta_dict`.
             overwriting: whether allow to overwrite existing meta data of same key.
                 default is False, which will raise exception if encountering existing key.
 
@@ -59,7 +55,9 @@ def __init__(
 
         """
         super().__init__(keys)
-        self.loader = LoadNifti(as_closest_canonical, False, dtype)
+        if not callable(loader):
+            raise ValueError("loader must be a Callable function or object.")
+        self.loader = loader
         if not isinstance(meta_key_postfix, str):
             raise ValueError("meta_key_postfix must be a string.")
         self.meta_key_postfix = meta_key_postfix
@@ -79,7 +77,45 @@ def __call__(self, data):
         return d
 
 
-class LoadPNGd(MapTransform):
+class LoadNiftid(LoadDatad):
+    """
+    Dictionary-based wrapper of :py:class:`monai.transforms.LoadNifti`,
+    must load image and metadata together. If loading a list of files in one key,
+    stack them together and add a new dimension as the first dimension, and use the
+    meta data of the first image to represent the stacked result. Note that the affine
+    transform of all the stacked images should be same. The output metadata field will
+    be created as ``key_{meta_key_postfix}``.
+    """
+
+    def __init__(
+        self,
+        keys: KeysCollection,
+        as_closest_canonical: bool = False,
+        dtype: Optional[np.dtype] = np.float32,
+        meta_key_postfix: str = "meta_dict",
+        overwriting: bool = False,
+    ) -> None:
+        """
+        Args:
+            keys: keys of the corresponding items to be transformed.
+                See also: :py:class:`monai.transforms.compose.MapTransform`
+            as_closest_canonical: if True, load the image as closest to canonical axis format.
+            dtype: if not None convert the loaded image data to this data type.
+            meta_key_postfix: use `key_{postfix}` to store the metadata of the nifti image,
+                default is `meta_dict`. The meta data is a dictionary object.
+                For example, load nifti file for `image`, store the metadata into `image_meta_dict`.
+            overwriting: whether allow to overwrite existing meta data of same key.
+                default is False, which will raise exception if encountering existing key.
+
+        Raises:
+            ValueError: meta_key_postfix must be a string.
+
+        """
+        loader = LoadNifti(as_closest_canonical, False, dtype)
+        super().__init__(keys, loader, meta_key_postfix, overwriting)
+
+
+class LoadPNGd(LoadDatad):
     """
     Dictionary-based wrapper of :py:class:`monai.transforms.LoadPNG`.
     """
@@ -106,28 +142,11 @@ def __init__(
             ValueError: meta_key_postfix must be a string.
 
         """
-        super().__init__(keys)
-        self.loader = LoadPNG(False, dtype)
-        if not isinstance(meta_key_postfix, str):
-            raise ValueError("meta_key_postfix must be a string.")
-        self.meta_key_postfix = meta_key_postfix
-        self.overwriting = overwriting
-
-    def __call__(self, data):
-        d = dict(data)
-        for key in self.keys:
-            data = self.loader(d[key])
-            assert isinstance(data, (tuple, list)), "loader must return a tuple or list."
-            d[key] = data[0]
-            assert isinstance(data[1], dict), "metadata must be a dict."
-            key_to_add = f"{key}_{self.meta_key_postfix}"
-            if key_to_add in d and not self.overwriting:
-                raise KeyError(f"meta data with key {key_to_add} already exists.")
-            d[key_to_add] = data[1]
-        return d
+        loader = LoadPNG(False, dtype)
+        super().__init__(keys, loader, meta_key_postfix, overwriting)
 
 
-class LoadNumpyd(MapTransform):
+class LoadNumpyd(LoadDatad):
     """
     Dictionary-based wrapper of :py:class:`monai.transforms.LoadNumpy`.
     """
@@ -157,25 +176,8 @@ def __init__(
             ValueError: meta_key_postfix must be a string.
 
         """
-        super().__init__(keys)
-        self.loader = LoadNumpy(data_only=False, dtype=dtype, npz_keys=npz_keys)
-        if not isinstance(meta_key_postfix, str):
-            raise ValueError("meta_key_postfix must be a string.")
-        self.meta_key_postfix = meta_key_postfix
-        self.overwriting = overwriting
-
-    def __call__(self, data):
-        d = dict(data)
-        for key in self.keys:
-            data = self.loader(d[key])
-            assert isinstance(data, (tuple, list)), "loader must return a tuple or list."
-            d[key] = data[0]
-            assert isinstance(data[1], dict), "metadata must be a dict."
-            key_to_add = f"{key}_{self.meta_key_postfix}"
-            if key_to_add in d and not self.overwriting:
-                raise KeyError(f"meta data with key {key_to_add} already exists.")
-            d[key_to_add] = data[1]
-        return d
+        loader = LoadNumpy(data_only=False, dtype=dtype, npz_keys=npz_keys)
+        super().__init__(keys, loader, meta_key_postfix, overwriting)
 
 
 LoadNiftiD = LoadNiftiDict = LoadNiftid


Test patch: