Skip to content

Commit

Permalink
Images can now be processed without providing a mask.
Browse files Browse the repository at this point in the history
  • Loading branch information
alexzwanenburg committed Mar 27, 2024
1 parent 651f03a commit ce8396b
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 63 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

# Minor changes

- If mask-related parameters are not provided for computing features or processing of images for deep learning, a
mask is generated that covers the entire image.

- Add fall-back methods for missing installation of the `ray` package for parallel processing. This can happen when
a python version is not supported by the `ray` package. `ray` is now a conditional dependency, until that package
is released for python `3.12`.
Expand Down
32 changes: 32 additions & 0 deletions mirp/_data_import/generic_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -1097,3 +1097,35 @@ def export_roi_labels(self) -> dict[str, Any]:
"file_path": [self.file_name] * n_labels,
"roi_label": labels
}


class MaskFullImage(MaskFile):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.roi_name = "full_image_mask"

def to_object(
self,
image: None | ImageFile,
**kwargs
) -> None | list[BaseMask]:
if image is None:
raise TypeError(
f"Creation of a full image mask requires that the corresponding image is set. "
f"No image was provided ({self.file_path})."
)
else:
image.complete()

self._complete_modality()

return [BaseMask(
roi_name=self.roi_name,
sample_name=image.sample_name,
image_modality=self.modality,
image_data=np.ones(image.image_dimension, dtype=bool),
image_spacing=image.image_spacing,
image_origin=image.image_origin,
image_orientation=image.image_orientation,
image_dimensions=image.image_dimension
)]
138 changes: 77 additions & 61 deletions mirp/data_import/import_image_and_mask.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from mirp.data_import.import_image import import_image
from mirp.data_import.import_mask import import_mask
from mirp._data_import.generic_file import ImageFile, MaskFile
from mirp._data_import.generic_file import ImageFile, MaskFile, MaskFullImage
from mirp._data_import.dicom_file import ImageDicomFile, MaskDicomFile
from mirp._data_import.dicom_file_stack import ImageDicomFileStack
from mirp.utilities.utilities import random_string
Expand Down Expand Up @@ -109,7 +109,16 @@ def import_image_and_mask(
list[ImageFile]
The functions returns a list of ImageFile objects, if any were found with the specified filters.
"""
if mask is None:

# If mask = None, this can mean several things. Here we check that mask should not be interpreted as having the
# same meaning as image.
if mask is None and (
mask_name is not None or mask_sub_folder is not None or mask_modality is not None or
mask_file_type is not None or roi_name is not None
):
mask = image

elif mask is None and isinstance(image, str) and image.endswith(".xml"):
mask = image

# Generate list of images.
Expand All @@ -123,75 +132,82 @@ def import_image_and_mask(
stack_images=stack_images
)

# Generate list of images.
mask_list = import_mask(
mask,
sample_name=sample_name,
mask_name=mask_name,
mask_file_type=mask_file_type,
mask_modality=mask_modality,
mask_sub_folder=mask_sub_folder,
stack_masks=stack_masks,
roi_name=roi_name
)

if len(image_list) == 0:
raise ValueError(f"No images were found. Possible reasons are lack of images with the preferred modality.")
if len(mask_list) == 0:
raise ValueError(f"No masks were found. Possible reasons are lack of masks with the preferred modality.")

# Determine association strategy, if this is unset.
possible_association_strategy = set_association_strategy(
image_list=image_list,
mask_list=mask_list
)

if association_strategy is None:
association_strategy = possible_association_strategy
elif isinstance(association_strategy, str):
association_strategy = [association_strategy]

if not isinstance(association_strategy, set):
association_strategy = set(association_strategy)

# Test association strategy.
unavailable_strategy = association_strategy - possible_association_strategy
if len(unavailable_strategy) > 0:
raise ValueError(
f"One or more strategies for associating images and masks are not available for the provided image and "
f"mask set: {', '.join(list(unavailable_strategy))}. Only the following strategies are available: "
f"{'. '.join(list(possible_association_strategy))}"
if mask is not None:
# Generate list of masks from mask.
mask_list = import_mask(
mask,
sample_name=sample_name,
mask_name=mask_name,
mask_file_type=mask_file_type,
mask_modality=mask_modality,
mask_sub_folder=mask_sub_folder,
stack_masks=stack_masks,
roi_name=roi_name
)

if len(possible_association_strategy) == 0:
raise ValueError(
f"No strategies for associating images and masks are available, indicating that there is no clear way to "
f"establish an association."
)
if len(mask_list) == 0:
raise ValueError(f"No masks were found. Possible reasons are lack of masks with the preferred modality.")

# Start association.
if association_strategy == {"list_order"}:
# If only the list_order strategy is available, use this.
for ii, image in enumerate(image_list):
image.associated_masks = [mask_list[ii]]
# Determine association strategy, if this is unset.
possible_association_strategy = set_association_strategy(
image_list=image_list,
mask_list=mask_list
)

elif association_strategy == {"single_image"}:
# If single_image is the only strategy, use this.
image_list[0].associated_masks = mask_list
if association_strategy is None:
association_strategy = possible_association_strategy
elif isinstance(association_strategy, str):
association_strategy = [association_strategy]

if not isinstance(association_strategy, set):
association_strategy = set(association_strategy)

# Test association strategy.
unavailable_strategy = association_strategy - possible_association_strategy
if len(unavailable_strategy) > 0:
raise ValueError(
f"One or more strategies for associating images and masks are not available for the provided image and "
f"mask set: {', '.join(list(unavailable_strategy))}. Only the following strategies are available: "
f"{'. '.join(list(possible_association_strategy))}"
)

else:
for ii, image in enumerate(image_list):
image.associate_with_mask(
mask_list=mask_list,
association_strategy=association_strategy
if len(possible_association_strategy) == 0:
raise ValueError(
f"No strategies for associating images and masks are available, indicating that there is no clear way to "
f"establish an association."
)

if all(image.associated_masks is None for image in image_list):
if "single_image" in association_strategy:
image_list[0].associated_masks = mask_list
elif "list_order" in association_strategy:
for ii, image in enumerate(image_list):
image.associated_masks = [mask_list[ii]]
# Start association.
if association_strategy == {"list_order"}:
# If only the list_order strategy is available, use this.
for ii, image in enumerate(image_list):
image.associated_masks = [mask_list[ii]]

elif association_strategy == {"single_image"}:
# If single_image is the only strategy, use this.
image_list[0].associated_masks = mask_list

else:
for ii, image in enumerate(image_list):
image.associate_with_mask(
mask_list=mask_list,
association_strategy=association_strategy
)

if all(image.associated_masks is None for image in image_list):
if "single_image" in association_strategy:
image_list[0].associated_masks = mask_list
elif "list_order" in association_strategy:
for ii, image in enumerate(image_list):
image.associated_masks = [mask_list[ii]]

else:
# Generate full masks.
for image in image_list:
image.associated_masks = [MaskFullImage()]

# Ensure that we are working with deep copies from this point - we don't want to propagate changes to masks,
# images by reference.
Expand Down
9 changes: 7 additions & 2 deletions mirp/settings/generic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import copy
from typing import Unpack

from mirp.settings.feature_parameters import FeatureExtractionSettingsClass
from mirp.settings.general_parameters import GeneralSettingsClass
Expand Down Expand Up @@ -52,7 +53,7 @@ class SettingsClass:
Configuration object for parameters related to image transformation. See
:class:`~mirp.settings.transformation_parameters.ImageTransformationSettingsClass`.
**kwargs: dict, optional
**kwargs: Any, optional
Keyword arguments for initialising configuration objects stored in this container object.
See Also
Expand All @@ -79,7 +80,11 @@ def __init__(
roi_resegment_settings: None | ResegmentationSettingsClass = None,
feature_extr_settings: None | FeatureExtractionSettingsClass = None,
img_transform_settings: None | ImageTransformationSettingsClass = None,
**kwargs
**kwargs: Unpack[
None | GeneralSettingsClass | ImagePostProcessingClass | ImagePostProcessingClass |
ImageInterpolationSettingsClass | MaskInterpolationSettingsClass | ResegmentationSettingsClass |
FeatureExtractionSettingsClass | ImageTransformationSettingsClass
]
):
kwargs = copy.deepcopy(kwargs)

Expand Down
31 changes: 31 additions & 0 deletions test/dl_preprocessing_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@


def test_extract_image_crop():

# Without specifying a mask and without cropping -- this means the entire image is masked..
data = deep_learning_preprocessing(
output_slices=False,
crop_size=None,
export_images=True,
write_images=False,
image=os.path.join(CURRENT_DIR, "data", "ibsi_1_ct_radiomics_phantom", "dicom", "image")
)

image = data[0][0][0]
mask = data[0][1][0]

assert image.shape == (60, 201, 204)
assert mask.shape == (60, 201, 204)

# No cropping.
data = deep_learning_preprocessing(
output_slices=False,
Expand Down Expand Up @@ -43,6 +59,21 @@ def test_extract_image_crop():
assert len(masks) == 60
assert all(mask.shape == (1, 201, 204) for mask in masks)

# Crop to size without specifying a mask -- this means that the entire image is masked.
data = deep_learning_preprocessing(
output_slices=False,
crop_size=[20, 50, 50],
export_images=True,
write_images=False,
image=os.path.join(CURRENT_DIR, "data", "ibsi_1_ct_radiomics_phantom", "dicom", "image")
)

image = data[0][0][0]
mask = data[0][1][0]

assert image.shape == (20, 50, 50)
assert mask.shape == (20, 50, 50)

# Crop to size.
data = deep_learning_preprocessing(
output_slices=False,
Expand Down

0 comments on commit ce8396b

Please sign in to comment.