Repository: Project-MONAI/MONAI

Issue:
LoadImage.__call__ kwargs passthrough for reader.read()
**Is your feature request related to a problem? Please describe.**
I am trying to load a DICOM image series stored in a pixeltype that is not included in the default ITK library. To load the file, I call `itk.imread(filepath, itk.D)` to cast the pixeltype to a library supported format. This is not possible in MONAI with the current `ImageLoader` and `ITKReader()`.

**Describe the solution you'd like**
When I call `ImageLoader`, I should be able to declare kwargs that are passed into the internal reader's read function.

`LoadImaged(keys=["image"], load_kwargs={"pixel_type": itk.D}, reader=ITKReader(), dtype=np.float64)`

Something like above to support adjusting the pixel_type param in itk.imread(). 

**Describe alternatives you've considered**
Extend `ITKReader()` for my project to include this functionality.



Discussion:

Resolving patch:
diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py
--- a/monai/data/image_reader.py
+++ b/monai/data/image_reader.py
@@ -52,15 +52,13 @@ def verify_suffix(self, filename: Union[Sequence[str], str]) -> bool:
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
 
     @abstractmethod
-    def read(self, data: Union[Sequence[str], str, Any], **kwargs) -> Union[Sequence[Any], Any]:
+    def read(self, data: Union[Sequence[str], str, Any]) -> Union[Sequence[Any], Any]:
         """
         Read image data from specified file or files, or set a loaded image.
         Note that it returns the raw data, so different readers return different image data type.
 
         Args:
             data: file name or a list of file names to read, or a loaded image object.
-            kwargs: additional args for 3rd party reader API.
-
 
         """
         raise NotImplementedError(f"Subclass {self.__class__.__name__} must implement this method.")
@@ -85,13 +83,16 @@ class ITKReader(ImageReader):
         c_order_axis_indexing: if `True`, the numpy array will have C-order indexing.
             this is the reverse of how indices are specified in ITK, i.e. k,j,i versus i,j,k.
             however C-order indexing is expected by most algorithms in numpy / scipy.
+        kwargs: additional args for `itk.imread` API. more details about available args:
+            https://github.com/InsightSoftwareConsortium/ITK/blob/master/Wrapping/Generators/Python/itkExtras.py
 
     """
 
-    def __init__(self, c_order_axis_indexing: bool = False):
+    def __init__(self, c_order_axis_indexing: bool = False, **kwargs):
         super().__init__()
         self._img: Optional[Sequence[Image]] = None
         self.c_order_axis_indexing = c_order_axis_indexing
+        self.kwargs = kwargs
 
     def verify_suffix(self, filename: Union[Sequence[str], str]) -> bool:
         """
@@ -104,7 +105,7 @@ def verify_suffix(self, filename: Union[Sequence[str], str]) -> bool:
         """
         return True
 
-    def read(self, data: Union[Sequence[str], str, Image], **kwargs):
+    def read(self, data: Union[Sequence[str], str, Image]):
         """
         Read image data from specified file or files, or set a `itk.Image` object.
         Note that the returned object is ITK image object or list of ITK image objects.
@@ -113,8 +114,6 @@ def read(self, data: Union[Sequence[str], str, Image], **kwargs):
         Args:
             data: file name or a list of file names to read, or a `itk.Image` object for the usage that
                 the image data is already in memory and no need to read from file again.
-            kwargs: additional args for `itk.imread` API. more details about available args:
-                https://github.com/InsightSoftwareConsortium/ITK/blob/master/Wrapping/Generators/Python/itkExtras.py
 
         """
         self._img = list()
@@ -124,7 +123,7 @@ def read(self, data: Union[Sequence[str], str, Image], **kwargs):
 
         filenames: Sequence[str] = ensure_tuple(data)
         for name in filenames:
-            self._img.append(itk.imread(name, **kwargs))
+            self._img.append(itk.imread(name, **self.kwargs))
         return self._img if len(filenames) > 1 else self._img[0]
 
     def get_data(self):
@@ -227,13 +226,16 @@ class NibabelReader(ImageReader):
 
     Args:
         as_closest_canonical: if True, load the image as closest to canonical axis format.
+        kwargs: additional args for `nibabel.load` API. more details about available args:
+            https://github.com/nipy/nibabel/blob/master/nibabel/loadsave.py
 
     """
 
-    def __init__(self, as_closest_canonical: bool = False):
+    def __init__(self, as_closest_canonical: bool = False, **kwargs):
         super().__init__()
         self._img: Optional[Sequence[Nifti1Image]] = None
         self.as_closest_canonical = as_closest_canonical
+        self.kwargs = kwargs
 
     def verify_suffix(self, filename: Union[Sequence[str], str]) -> bool:
         """
@@ -247,7 +249,7 @@ def verify_suffix(self, filename: Union[Sequence[str], str]) -> bool:
         suffixes: Sequence[str] = ["nii", "nii.gz"]
         return is_supported_format(filename, suffixes)
 
-    def read(self, data: Union[Sequence[str], str, Nifti1Image], **kwargs):
+    def read(self, data: Union[Sequence[str], str, Nifti1Image]):
         """
         Read image data from specified file or files, or set a Nibabel Image object.
         Note that the returned object is Nibabel image object or list of Nibabel image objects.
@@ -255,8 +257,6 @@ def read(self, data: Union[Sequence[str], str, Nifti1Image], **kwargs):
 
         Args:
             data: file name or a list of file names to read.
-            kwargs: additional args for `nibabel.load` API. more details about available args:
-                https://github.com/nipy/nibabel/blob/master/nibabel/loadsave.py
 
         """
         self._img = list()
@@ -266,7 +266,7 @@ def read(self, data: Union[Sequence[str], str, Nifti1Image], **kwargs):
 
         filenames: Sequence[str] = ensure_tuple(data)
         for name in filenames:
-            img = nib.load(name, **kwargs)
+            img = nib.load(name, **self.kwargs)
             img = correct_nifti_header_if_necessary(img)
             self._img.append(img)
         return self._img if len(filenames) > 1 else self._img[0]
@@ -360,15 +360,18 @@ class NumpyReader(ImageReader):
     Args:
         npz_keys: if loading npz file, only load the specified keys, if None, load all the items.
             stack the loaded items together to construct a new first dimension.
+        kwargs: additional args for `numpy.load` API except `allow_pickle`. more details about available args:
+            https://numpy.org/doc/stable/reference/generated/numpy.load.html
 
     """
 
-    def __init__(self, npz_keys: Optional[KeysCollection] = None):
+    def __init__(self, npz_keys: Optional[KeysCollection] = None, **kwargs):
         super().__init__()
         self._img: Optional[Sequence[Nifti1Image]] = None
         if npz_keys is not None:
             npz_keys = ensure_tuple(npz_keys)
         self.npz_keys = npz_keys
+        self.kwargs = kwargs
 
     def verify_suffix(self, filename: Union[Sequence[str], str]) -> bool:
         """
@@ -382,7 +385,7 @@ def verify_suffix(self, filename: Union[Sequence[str], str]) -> bool:
         suffixes: Sequence[str] = ["npz", "npy"]
         return is_supported_format(filename, suffixes)
 
-    def read(self, data: Union[Sequence[str], str, np.ndarray], **kwargs):
+    def read(self, data: Union[Sequence[str], str, np.ndarray]):
         """
         Read image data from specified file or files, or set a Numpy array.
         Note that the returned object is Numpy array or list of Numpy arrays.
@@ -390,8 +393,6 @@ def read(self, data: Union[Sequence[str], str, np.ndarray], **kwargs):
 
         Args:
             data: file name or a list of file names to read.
-            kwargs: additional args for `numpy.load` API except `allow_pickle`. more details about available args:
-                https://numpy.org/doc/stable/reference/generated/numpy.load.html
 
         """
         self._img = list()
@@ -401,7 +402,7 @@ def read(self, data: Union[Sequence[str], str, np.ndarray], **kwargs):
 
         filenames: Sequence[str] = ensure_tuple(data)
         for name in filenames:
-            img = np.load(name, allow_pickle=True, **kwargs)
+            img = np.load(name, allow_pickle=True, **self.kwargs)
             if name.endswith(".npz"):
                 # load expected items from NPZ file
                 npz_keys = [f"arr_{i}" for i in range(len(img))] if self.npz_keys is None else self.npz_keys


Test patch:
diff --git a/tests/test_load_image.py b/tests/test_load_image.py
--- a/tests/test_load_image.py
+++ b/tests/test_load_image.py
@@ -46,17 +46,23 @@
     (3, 128, 128, 128),
 ]
 
-TEST_CASE_5 = [{"image_only": True}, ["test_image.nii.gz"], (128, 128, 128)]
+TEST_CASE_5 = [
+    {"reader": NibabelReader(mmap=False), "image_only": False},
+    ["test_image.nii.gz"],
+    (128, 128, 128),
+]
 
-TEST_CASE_6 = [{"image_only": False}, ["test_image.nii.gz"], (128, 128, 128)]
+TEST_CASE_6 = [{"image_only": True}, ["test_image.nii.gz"], (128, 128, 128)]
 
-TEST_CASE_7 = [
+TEST_CASE_7 = [{"image_only": False}, ["test_image.nii.gz"], (128, 128, 128)]
+
+TEST_CASE_8 = [
     {"image_only": True},
     ["test_image.nii.gz", "test_image2.nii.gz", "test_image3.nii.gz"],
     (3, 128, 128, 128),
 ]
 
-TEST_CASE_8 = [
+TEST_CASE_9 = [
     {"image_only": False},
     ["test_image.nii.gz", "test_image2.nii.gz", "test_image3.nii.gz"],
     (3, 128, 128, 128),
@@ -64,7 +70,7 @@
 
 
 class TestLoadImage(unittest.TestCase):
-    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4])
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5])
     def test_nibabel_reader(self, input_param, filenames, expected_shape):
         test_image = np.random.rand(128, 128, 128)
         with tempfile.TemporaryDirectory() as tempdir:
@@ -81,7 +87,7 @@ def test_nibabel_reader(self, input_param, filenames, expected_shape):
                 np.testing.assert_allclose(header["original_affine"], np.eye(4))
             self.assertTupleEqual(result.shape, expected_shape)
 
-    @parameterized.expand([TEST_CASE_5, TEST_CASE_6, TEST_CASE_7, TEST_CASE_8])
+    @parameterized.expand([TEST_CASE_6, TEST_CASE_7, TEST_CASE_8, TEST_CASE_9])
     def test_itk_reader(self, input_param, filenames, expected_shape):
         test_image = np.random.rand(128, 128, 128)
         with tempfile.TemporaryDirectory() as tempdir: