Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
torch>=1.5
pytorch-ignite==0.4.5
numpy>=1.17
itk>=5.0, <=5.1.2
itk>=5.2
nibabel
parameterized
scikit-image>=0.14.2
Expand Down
9 changes: 0 additions & 9 deletions monai/config/deviceconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@
import monai
from monai.utils.module import OptionalImportError, get_package_version, optional_import

try:
import itk # type: ignore

itk_version = itk.Version.GetITKVersion()
del itk
except (ImportError, AttributeError):
itk_version = "NOT INSTALLED or UNKNOWN VERSION."

try:
_, HAS_EXT = optional_import("monai._C")
USE_COMPILED = HAS_EXT and os.getenv("BUILD_MONAI", "0") == "1"
Expand Down Expand Up @@ -76,7 +68,6 @@ def get_optional_config_values():
output["Tensorboard"] = get_package_version("tensorboard")
output["gdown"] = get_package_version("gdown")
output["TorchVision"] = get_package_version("torchvision")
output["ITK"] = itk_version
output["tqdm"] = get_package_version("tqdm")
output["lmdb"] = get_package_version("lmdb")
output["psutil"] = psutil_version
Expand Down
166 changes: 78 additions & 88 deletions monai/data/image_reader.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions monai/data/png_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def write_png(
if data.dtype not in (np.uint8, np.uint16): # type: ignore
data = data.astype(np.uint8)

data = np.moveaxis(data, 0, 1)
img = Image.fromarray(data)
img.save(file_name, "PNG")
return
2 changes: 1 addition & 1 deletion monai/transforms/inverse.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def check_transforms_match(self, transform: dict) -> None:
return
# basic check if multiprocessing uses 'spawn' (objects get recreated so don't have same ID)
if (
torch.multiprocessing.get_start_method(allow_none=False) == "spawn"
torch.multiprocessing.get_start_method() in ("spawn", None)
and transform[InverseKeys.CLASS_NAME] == self.__class__.__name__
):
return
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pytorch-ignite==0.4.5
gdown>=3.6.4
scipy
itk>=5.0, <=5.1.2
itk>=5.2
nibabel
pillow!=8.3.0 # https://github.com/python-pillow/Pillow/issues/5571
tensorboard
Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ all =
gdown>=3.6.4
pytorch-ignite==0.4.5
torchvision
itk>=5.0, <=5.1.2
itk>=5.2
tqdm>=4.47.0
lmdb
psutil
Expand All @@ -59,7 +59,7 @@ ignite =
torchvision =
torchvision
itk =
itk>=5.0, <=5.1.2
itk>=5.2
tqdm =
tqdm>=4.47.0
lmdb =
Expand Down
6 changes: 5 additions & 1 deletion tests/test_integration_classification_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
RandZoom,
ScaleIntensity,
ToTensor,
Transpose,
)
from monai.utils import set_determinism
from tests.testing_data.integration_answers import test_integration_value
Expand Down Expand Up @@ -66,6 +67,7 @@ def run_training_test(root_dir, train_x, train_y, val_x, val_y, device="cuda:0",
[
LoadImage(image_only=True),
AddChannel(),
Transpose(indices=[0, 2, 1]),
ScaleIntensity(),
RandRotate(range_x=np.pi / 12, prob=0.5, keep_size=True),
RandFlip(spatial_axis=0, prob=0.5),
Expand All @@ -74,7 +76,9 @@ def run_training_test(root_dir, train_x, train_y, val_x, val_y, device="cuda:0",
]
)
train_transforms.set_random_state(1234)
val_transforms = Compose([LoadImage(image_only=True), AddChannel(), ScaleIntensity(), ToTensor()])
val_transforms = Compose(
[LoadImage(image_only=True), AddChannel(), Transpose(indices=[0, 2, 1]), ScaleIntensity(), ToTensor()]
)
y_pred_trans = Compose([ToTensor(), Activations(softmax=True)])
y_trans = Compose([ToTensor(), AsDiscrete(to_onehot=True, n_classes=len(np.unique(train_y)))])
auc_metric = ROCAUCMetric()
Expand Down
29 changes: 18 additions & 11 deletions tests/test_integration_segmentation_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
from monai.networks.nets import UNet
from monai.transforms import (
Activations,
AsChannelFirstd,
AsDiscrete,
Compose,
EnsureChannelFirstd,
LoadImaged,
RandCropByPosNegLabeld,
RandRotate90d,
Expand All @@ -47,7 +47,7 @@
TASK = "integration_segmentation_3d"


def run_training_test(root_dir, device="cuda:0", cachedataset=0):
def run_training_test(root_dir, device="cuda:0", cachedataset=0, readers=(None, None)):
monai.config.print_config()
images = sorted(glob(os.path.join(root_dir, "img*.nii.gz")))
segs = sorted(glob(os.path.join(root_dir, "seg*.nii.gz")))
Expand All @@ -57,8 +57,8 @@ def run_training_test(root_dir, device="cuda:0", cachedataset=0):
# define transforms for image and segmentation
train_transforms = Compose(
[
LoadImaged(keys=["img", "seg"]),
AsChannelFirstd(keys=["img", "seg"], channel_dim=-1),
LoadImaged(keys=["img", "seg"], reader=readers[0]),
EnsureChannelFirstd(keys=["img", "seg"]),
# resampling with align_corners=True or dtype=float64 will generate
# slight different results between PyTorch 1.5 an 1.6
Spacingd(keys=["img", "seg"], pixdim=[1.2, 0.8, 0.7], mode=["bilinear", "nearest"], dtype=np.float32),
Expand All @@ -73,8 +73,8 @@ def run_training_test(root_dir, device="cuda:0", cachedataset=0):
train_transforms.set_random_state(1234)
val_transforms = Compose(
[
LoadImaged(keys=["img", "seg"]),
AsChannelFirstd(keys=["img", "seg"], channel_dim=-1),
LoadImaged(keys=["img", "seg"], reader=readers[1]),
EnsureChannelFirstd(keys=["img", "seg"]),
# resampling with align_corners=True or dtype=float64 will generate
# slight different results between PyTorch 1.5 an 1.6
Spacingd(keys=["img", "seg"], pixdim=[1.2, 0.8, 0.7], mode=["bilinear", "nearest"], dtype=np.float32),
Expand Down Expand Up @@ -184,7 +184,7 @@ def run_inference_test(root_dir, device="cuda:0"):
val_transforms = Compose(
[
LoadImaged(keys=["img", "seg"]),
AsChannelFirstd(keys=["img", "seg"], channel_dim=-1),
EnsureChannelFirstd(keys=["img", "seg"]),
# resampling with align_corners=True or dtype=float64 will generate
# slight different results between PyTorch 1.5 an 1.6
Spacingd(keys=["img", "seg"], pixdim=[1.2, 0.8, 0.7], mode=["bilinear", "nearest"], dtype=np.float32),
Expand Down Expand Up @@ -249,21 +249,25 @@ def tearDown(self):
def train_and_infer(self, idx=0):
results = []
set_determinism(0)
losses, best_metric, best_metric_epoch = run_training_test(self.data_dir, device=self.device, cachedataset=idx)
_readers = (None, None)
if idx == 1:
_readers = ("itkreader", "itkreader")
elif idx == 2:
_readers = ("itkreader", "nibabelreader")
losses, best_metric, best_metric_epoch = run_training_test(
self.data_dir, device=self.device, cachedataset=idx, readers=_readers
)
infer_metric = run_inference_test(self.data_dir, device=self.device)

# check training properties
print("losses", losses)
print("best metric", best_metric)
print("infer metric", infer_metric)
self.assertTrue(test_integration_value(TASK, key="losses", data=losses, rtol=1e-3))
self.assertTrue(test_integration_value(TASK, key="best_metric", data=best_metric, rtol=1e-2))
self.assertTrue(len(glob(os.path.join(self.data_dir, "runs"))) > 0)
model_file = os.path.join(self.data_dir, "best_metric_model.pth")
self.assertTrue(os.path.exists(model_file))

# check inference properties
self.assertTrue(test_integration_value(TASK, key="infer_metric", data=infer_metric, rtol=1e-2))
output_files = sorted(glob(os.path.join(self.data_dir, "output", "img*", "*.nii.gz")))
print([np.mean(nib.load(output).get_fdata()) for output in output_files])
results.extend(losses)
Expand All @@ -272,6 +276,9 @@ def train_and_infer(self, idx=0):
for output in output_files:
ave = np.mean(nib.load(output).get_fdata())
results.append(ave)
self.assertTrue(test_integration_value(TASK, key="losses", data=results[:6], rtol=1e-3))
self.assertTrue(test_integration_value(TASK, key="best_metric", data=results[6], rtol=1e-2))
self.assertTrue(test_integration_value(TASK, key="infer_metric", data=results[7], rtol=1e-2))
self.assertTrue(test_integration_value(TASK, key="output_sums", data=results[8:], rtol=1e-2))
return results

Expand Down
2 changes: 1 addition & 1 deletion tests/test_inverse.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ def test_inverse(self, _, data_name, acceptable_diff, *transforms):
self.check_inverse(name, data.keys(), forwards[-i - 2], fwd_bck, forwards[-1], acceptable_diff)

# skip this test if multiprocessing uses 'spawn', as the check is only basic anyway
@skipUnless(torch.multiprocessing.get_start_method(allow_none=False) == "spawn", "requires spawn")
@skipUnless(torch.multiprocessing.get_start_method() == "spawn", "requires spawn")
def test_fail(self):

t1 = SpatialPadd("image", [10, 5])
Expand Down
35 changes: 18 additions & 17 deletions tests/test_load_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@
TEST_CASE_10 = [
{"image_only": False, "reader": ITKReader(pixel_type=itk.UC)},
"tests/testing_data/CT_DICOM",
(4, 16, 16),
(16, 16, 4),
]

TEST_CASE_11 = [
{"image_only": False, "reader": "ITKReader", "pixel_type": itk.UC},
"tests/testing_data/CT_DICOM",
(4, 16, 16),
(16, 16, 4),
]


Expand Down Expand Up @@ -105,8 +105,9 @@ def test_itk_reader(self, input_param, filenames, expected_shape):
result, header = result
self.assertTrue("affine" in header)
self.assertEqual(header["filename_or_obj"], os.path.join(tempdir, "test_image.nii.gz"))
np.testing.assert_allclose(header["affine"], np.eye(4))
np.testing.assert_allclose(header["original_affine"], np.eye(4))
np_diag = np.diag([-1, -1, 1, 1])
np.testing.assert_allclose(header["affine"], np_diag)
np.testing.assert_allclose(header["original_affine"], np_diag)
self.assertTupleEqual(result.shape, expected_shape)

@parameterized.expand([TEST_CASE_10, TEST_CASE_11])
Expand All @@ -118,8 +119,8 @@ def test_itk_dicom_series_reader(self, input_param, filenames, expected_shape):
header["affine"],
np.array(
[
[0.488281, 0.0, 0.0, -125.0],
[0.0, 0.488281, 0.0, -128.100006],
[-0.488281, 0.0, 0.0, 125.0],
[0.0, -0.488281, 0.0, 128.100006],
[0.0, 0.0, 68.33333333, -99.480003],
[0.0, 0.0, 0.0, 1.0],
]
Expand All @@ -129,28 +130,28 @@ def test_itk_dicom_series_reader(self, input_param, filenames, expected_shape):
self.assertTupleEqual(tuple(header["spatial_shape"]), expected_shape)

def test_itk_reader_multichannel(self):
test_image = np.random.randint(0, 256, size=(256, 256, 3)).astype("uint8")
test_image = np.random.randint(0, 256, size=(256, 224, 3)).astype("uint8")
with tempfile.TemporaryDirectory() as tempdir:
filename = os.path.join(tempdir, "test_image.png")
itk_np_view = itk.image_view_from_array(test_image, is_vector=True)
itk.imwrite(itk_np_view, filename)
result, header = LoadImage(reader=ITKReader())(filename)

self.assertTupleEqual(tuple(header["spatial_shape"]), (256, 256))
np.testing.assert_allclose(result[0, :, :], test_image[:, :, 0])
np.testing.assert_allclose(result[1, :, :], test_image[:, :, 1])
np.testing.assert_allclose(result[2, :, :], test_image[:, :, 2])
self.assertTupleEqual(tuple(header["spatial_shape"]), (224, 256))
np.testing.assert_allclose(result[:, :, 0], test_image[:, :, 0].T)
np.testing.assert_allclose(result[:, :, 1], test_image[:, :, 1].T)
np.testing.assert_allclose(result[:, :, 2], test_image[:, :, 2].T)

def test_load_png(self):
spatial_size = (256, 256)
spatial_size = (256, 224)
test_image = np.random.randint(0, 256, size=spatial_size)
with tempfile.TemporaryDirectory() as tempdir:
filename = os.path.join(tempdir, "test_image.png")
Image.fromarray(test_image.astype("uint8")).save(filename)
result, header = LoadImage(image_only=False)(filename)
self.assertTupleEqual(tuple(header["spatial_shape"]), spatial_size)
self.assertTupleEqual(result.shape, spatial_size)
np.testing.assert_allclose(result, test_image)
self.assertTupleEqual(tuple(header["spatial_shape"]), spatial_size[::-1])
self.assertTupleEqual(result.shape, spatial_size[::-1])
np.testing.assert_allclose(result.T, test_image)

def test_register(self):
spatial_size = (32, 64, 128)
Expand All @@ -163,8 +164,8 @@ def test_register(self):
loader = LoadImage(image_only=False)
loader.register(ITKReader())
result, header = loader(filename)
self.assertTupleEqual(tuple(header["spatial_shape"]), spatial_size)
self.assertTupleEqual(result.shape, spatial_size)
self.assertTupleEqual(tuple(header["spatial_shape"]), spatial_size[::-1])
self.assertTupleEqual(result.shape, spatial_size[::-1])

def test_kwargs(self):
spatial_size = (32, 64, 128)
Expand Down
88 changes: 85 additions & 3 deletions tests/test_load_imaged.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from parameterized import parameterized

from monai.data import ITKReader
from monai.transforms import LoadImaged
from monai.transforms import Compose, EnsureChannelFirstD, LoadImaged, SaveImageD

KEYS = ["image", "label", "extra"]

Expand Down Expand Up @@ -53,8 +53,90 @@ def test_register(self):
loader = LoadImaged(keys="img")
loader.register(ITKReader())
result = loader({"img": filename})
self.assertTupleEqual(tuple(result["img_meta_dict"]["spatial_shape"]), spatial_size)
self.assertTupleEqual(result["img"].shape, spatial_size)
self.assertTupleEqual(tuple(result["img_meta_dict"]["spatial_shape"]), spatial_size[::-1])
self.assertTupleEqual(result["img"].shape, spatial_size[::-1])

def test_channel_dim(self):
spatial_size = (32, 64, 3, 128)
test_image = np.random.rand(*spatial_size)
with tempfile.TemporaryDirectory() as tempdir:
filename = os.path.join(tempdir, "test_image.nii.gz")
nib.save(nib.Nifti1Image(test_image, affine=np.eye(4)), filename)

loader = LoadImaged(keys="img")
loader.register(ITKReader(channel_dim=2))
result = EnsureChannelFirstD("img")(loader({"img": filename}))
self.assertTupleEqual(tuple(result["img_meta_dict"]["spatial_shape"]), (32, 64, 128))
self.assertTupleEqual(result["img"].shape, (3, 32, 64, 128))


class TestConsistency(unittest.TestCase):
def _cmp(self, filename, shape, ch_shape, reader_1, reader_2, outname, ext):
data_dict = {"img": filename}
keys = data_dict.keys()
xforms = Compose(
[
LoadImaged(keys, reader=reader_1),
EnsureChannelFirstD(keys),
]
)
img_dict = xforms(data_dict) # load dicom with itk
self.assertTupleEqual(img_dict["img"].shape, ch_shape)
self.assertTupleEqual(tuple(img_dict["img_meta_dict"]["spatial_shape"]), shape)

with tempfile.TemporaryDirectory() as tempdir:
save_xform = SaveImageD(
keys, meta_keys="img_meta_dict", output_dir=tempdir, squeeze_end_dims=False, output_ext=ext
)
save_xform(img_dict) # save to nifti

new_xforms = Compose(
[
LoadImaged(keys, reader=reader_2),
EnsureChannelFirstD(keys),
]
)
out = new_xforms({"img": os.path.join(tempdir, outname)}) # load nifti with itk
self.assertTupleEqual(out["img"].shape, ch_shape)
self.assertTupleEqual(tuple(out["img_meta_dict"]["spatial_shape"]), shape)
if "affine" in img_dict["img_meta_dict"] and "affine" in out["img_meta_dict"]:
np.testing.assert_allclose(
img_dict["img_meta_dict"]["affine"], out["img_meta_dict"]["affine"], rtol=1e-3
)
np.testing.assert_allclose(out["img"], img_dict["img"], rtol=1e-3)

def test_dicom(self):
img_dir = "tests/testing_data/CT_DICOM"
self._cmp(
img_dir, (16, 16, 4), (1, 16, 16, 4), "itkreader", "itkreader", "CT_DICOM/CT_DICOM_trans.nii.gz", ".nii.gz"
)
output_name = "CT_DICOM/CT_DICOM_trans.nii.gz"
self._cmp(img_dir, (16, 16, 4), (1, 16, 16, 4), "nibabelreader", "itkreader", output_name, ".nii.gz")
self._cmp(img_dir, (16, 16, 4), (1, 16, 16, 4), "itkreader", "nibabelreader", output_name, ".nii.gz")

def test_multi_dicom(self):
"""multichannel dicom reading, saving to nifti, then load with itk or nibabel"""

img_dir = ["tests/testing_data/CT_DICOM", "tests/testing_data/CT_DICOM"]
self._cmp(
img_dir, (16, 16, 4), (2, 16, 16, 4), "itkreader", "itkreader", "CT_DICOM/CT_DICOM_trans.nii.gz", ".nii.gz"
)
output_name = "CT_DICOM/CT_DICOM_trans.nii.gz"
self._cmp(img_dir, (16, 16, 4), (2, 16, 16, 4), "nibabelreader", "itkreader", output_name, ".nii.gz")
self._cmp(img_dir, (16, 16, 4), (2, 16, 16, 4), "itkreader", "nibabelreader", output_name, ".nii.gz")

def test_png(self):
"""png reading with itk, saving to nifti, then load with itk or nibabel or PIL"""

test_image = np.random.randint(0, 256, size=(256, 224, 3)).astype("uint8")
with tempfile.TemporaryDirectory() as tempdir:
filename = os.path.join(tempdir, "test_image.png")
itk_np_view = itk.image_view_from_array(test_image, is_vector=True)
itk.imwrite(itk_np_view, filename)
output_name = "test_image/test_image_trans.png"
self._cmp(filename, (224, 256), (3, 224, 256), "itkreader", "itkreader", output_name, ".png")
self._cmp(filename, (224, 256), (3, 224, 256), "itkreader", "PILReader", output_name, ".png")
self._cmp(filename, (224, 256), (3, 224, 256), "itkreader", "nibabelreader", output_name, ".png")


if __name__ == "__main__":
Expand Down
Loading