diff --git a/NEWS.md b/NEWS.md index 23fdd081..6592780e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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`. diff --git a/mirp/_data_import/generic_file.py b/mirp/_data_import/generic_file.py index 8a951f32..17473606 100644 --- a/mirp/_data_import/generic_file.py +++ b/mirp/_data_import/generic_file.py @@ -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 + )] diff --git a/mirp/data_import/import_image_and_mask.py b/mirp/data_import/import_image_and_mask.py index 4c88e677..720cdd5c 100644 --- a/mirp/data_import/import_image_and_mask.py +++ b/mirp/data_import/import_image_and_mask.py @@ -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 @@ -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. @@ -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. diff --git a/mirp/settings/generic.py b/mirp/settings/generic.py index b8a3c8d4..4907d9d2 100644 --- a/mirp/settings/generic.py +++ b/mirp/settings/generic.py @@ -1,4 +1,5 @@ import copy +from typing import Unpack from mirp.settings.feature_parameters import FeatureExtractionSettingsClass from mirp.settings.general_parameters import GeneralSettingsClass @@ -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 @@ -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) diff --git a/test/dl_preprocessing_test.py b/test/dl_preprocessing_test.py index 612beb9c..be77f20a 100644 --- a/test/dl_preprocessing_test.py +++ b/test/dl_preprocessing_test.py @@ -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, @@ -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,