Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4502-implement-pydicomreader #4550

Merged
merged 13 commits into from
Jun 26, 2022
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ pyyaml
fire
jsonschema
pynrrd
pydicom
4 changes: 2 additions & 2 deletions docs/source/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,9 @@ Since MONAI v0.2.0, the extras syntax such as `pip install 'monai[nibabel]'` is

- The options are
```
[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, einops, transformers, mlflow, matplotlib, tensorboardX, tifffile, imagecodecs, pyyaml, fire, jsonschema, pynrrd]
[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, einops, transformers, mlflow, matplotlib, tensorboardX, tifffile, imagecodecs, pyyaml, fire, jsonschema, pynrrd, pydicom]
```
which correspond to `nibabel`, `scikit-image`, `pillow`, `tensorboard`,
`gdown`, `pytorch-ignite`, `torchvision`, `itk`, `tqdm`, `lmdb`, `psutil`, `cucim`, `openslide-python`, `pandas`, `einops`, `transformers`, `mlflow`, `matplotlib`, `tensorboardX`, `tifffile`, `imagecodecs`, `pyyaml`, `fire`, `jsonschema`, `pynrrd`, respectively.
`gdown`, `pytorch-ignite`, `torchvision`, `itk`, `tqdm`, `lmdb`, `psutil`, `cucim`, `openslide-python`, `pandas`, `einops`, `transformers`, `mlflow`, `matplotlib`, `tensorboardX`, `tifffile`, `imagecodecs`, `pyyaml`, `fire`, `jsonschema`, `pynrrd`, `pydicom`, respectively.

- `pip install 'monai[all]'` installs all the optional dependencies.
1 change: 1 addition & 0 deletions environment-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies:
- fire
- jsonschema
- pynrrd
- pydicom
- pip
- pip:
# pip for itk as conda-forge version only up to v5.1
Expand Down
2 changes: 1 addition & 1 deletion monai/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
from .folder_layout import FolderLayout
from .grid_dataset import GridPatchDataset, PatchDataset, PatchIter, PatchIterd
from .image_dataset import ImageDataset
from .image_reader import ImageReader, ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader
from .image_reader import ImageReader, ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader, PydicomReader
from .image_writer import (
SUPPORTED_WRITERS,
ImageWriter,
Expand Down
335 changes: 332 additions & 3 deletions monai/data/image_reader.py

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions monai/transforms/io/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@
from monai.config import DtypeLike, NdarrayOrTensor, PathLike
from monai.data import image_writer
from monai.data.folder_layout import FolderLayout
from monai.data.image_reader import ImageReader, ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader
from monai.data.image_reader import (
ImageReader,
ITKReader,
NibabelReader,
NrrdReader,
NumpyReader,
PILReader,
PydicomReader,
)
from monai.transforms.transform import Transform
from monai.transforms.utility.array import EnsureChannelFirst
from monai.utils import GridSampleMode, GridSamplePadMode
Expand All @@ -42,6 +50,7 @@
__all__ = ["LoadImage", "SaveImage", "SUPPORTED_READERS"]

SUPPORTED_READERS = {
"pydicomreader": PydicomReader,
"itkreader": ITKReader,
"nrrdreader": NrrdReader,
"numpyreader": NumpyReader,
Expand Down Expand Up @@ -110,7 +119,7 @@ def __init__(
- if `reader` is None, a default set of `SUPPORTED_READERS` will be used.
- if `reader` is a string, it's treated as a class name or dotted path
(such as ``"monai.data.ITKReader"``), the supported built-in reader classes are
``"ITKReader"``, ``"NibabelReader"``, ``"NumpyReader"``.
``"ITKReader"``, ``"NibabelReader"``, ``"NumpyReader"``, ``"PydicomReader"``.
a reader instance will be constructed with the `*args` and `**kwargs` parameters.
- if `reader` is a reader class/instance, it will be registered to this loader accordingly.
image_only: if True return only the image volume, otherwise return image data array and header dict.
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ fire
jsonschema
pynrrd
pre-commit
pydicom
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ all =
fire
jsonschema
pynrrd
pydicom
nibabel =
nibabel
skimage =
Expand Down Expand Up @@ -104,6 +105,8 @@ jsonschema =
jsonschema
pynrrd =
pynrrd
pydicom =
pydicom

[flake8]
select = B,C,E,F,N,P,T4,W,B9
Expand Down
8 changes: 6 additions & 2 deletions tests/test_init_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import unittest

from monai.data import ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader
from monai.data import ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader, PydicomReader
from monai.transforms import LoadImage, LoadImaged
from tests.utils import SkipIfNoModule

Expand All @@ -23,14 +23,15 @@ def test_load_image(self):
self.assertIsInstance(instance1, LoadImage)
self.assertIsInstance(instance2, LoadImage)

for r in ["NibabelReader", "PILReader", "ITKReader", "NumpyReader", "NrrdReader", None]:
for r in ["NibabelReader", "PILReader", "ITKReader", "NumpyReader", "NrrdReader", "PydicomReader", None]:
inst = LoadImaged("image", reader=r)
self.assertIsInstance(inst, LoadImaged)

@SkipIfNoModule("itk")
@SkipIfNoModule("nibabel")
@SkipIfNoModule("PIL")
@SkipIfNoModule("nrrd")
@SkipIfNoModule("Pydicom")
def test_readers(self):
inst = ITKReader()
self.assertIsInstance(inst, ITKReader)
Expand All @@ -40,6 +41,9 @@ def test_readers(self):
inst = NibabelReader(as_closest_canonical=True)
self.assertIsInstance(inst, NibabelReader)

inst = PydicomReader()
self.assertIsInstance(inst, PydicomReader)

inst = NumpyReader()
self.assertIsInstance(inst, NumpyReader)
inst = NumpyReader(npz_keys="test")
Expand Down
41 changes: 39 additions & 2 deletions tests/test_load_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from parameterized import parameterized
from PIL import Image

from monai.data import ITKReader, NibabelReader
from monai.data import ITKReader, NibabelReader, PydicomReader
from monai.transforms import LoadImage


Expand Down Expand Up @@ -134,6 +134,31 @@ def get_data(self, _obj):
(128, 128, 3, 128),
]

# test same dicom data with PydicomReader
TEST_CASE_19 = [
{"image_only": False, "reader": PydicomReader()},
"tests/testing_data/CT_DICOM",
(16, 16, 4),
(16, 16, 4),
]

TEST_CASE_20 = [
{"image_only": False, "reader": "PydicomReader", "ensure_channel_first": True},
"tests/testing_data/CT_DICOM",
(16, 16, 4),
(1, 16, 16, 4),
]

TEST_CASE_21 = [
{"image_only": False, "reader": "PydicomReader", "affine_lps_to_ras": True, "defer_size": "2 MB"},
"tests/testing_data/CT_DICOM",
(16, 16, 4),
(16, 16, 4),
]

# test reader consistency between PydicomReader and ITKReader on dicom data
TEST_CASE_22 = ["tests/testing_data/CT_DICOM"]


class TestLoadImage(unittest.TestCase):
@parameterized.expand(
Expand Down Expand Up @@ -174,7 +199,7 @@ def test_itk_reader(self, input_param, filenames, expected_shape):
np.testing.assert_allclose(header["original_affine"], np_diag)
self.assertTupleEqual(result.shape, expected_shape)

@parameterized.expand([TEST_CASE_10, TEST_CASE_11, TEST_CASE_12])
@parameterized.expand([TEST_CASE_10, TEST_CASE_11, TEST_CASE_12, TEST_CASE_19, TEST_CASE_20, TEST_CASE_21])
def test_itk_dicom_series_reader(self, input_param, filenames, expected_shape, expected_np_shape):
result, header = LoadImage(**input_param)(filenames)
self.assertTrue("affine" in header)
Expand Down Expand Up @@ -208,6 +233,18 @@ def test_itk_reader_multichannel(self):
np.testing.assert_allclose(result[:, :, 1], test_image[:, :, 1])
np.testing.assert_allclose(result[:, :, 2], test_image[:, :, 2])

@parameterized.expand([TEST_CASE_22])
def test_dicom_reader_consistency(self, filenames):
itk_param = {"reader": "ITKReader"}
pydicom_param = {"reader": "PydicomReader"}
for affine_flag in [True, False]:
itk_param["affine_lps_to_ras"] = affine_flag
pydicom_param["affine_lps_to_ras"] = affine_flag
itk_result, itk_header = LoadImage(**itk_param)(filenames)
pydicom_result, pydicom_header = LoadImage(**pydicom_param)(filenames)
np.testing.assert_allclose(pydicom_result, itk_result)
np.testing.assert_allclose(itk_header["affine"], pydicom_header["affine"])

def test_load_nifti_multichannel(self):
test_image = np.random.randint(0, 256, size=(31, 64, 16, 2)).astype(np.float32)
with tempfile.TemporaryDirectory() as tempdir:
Expand Down