diff --git a/MEDimage/MEDscan.py b/MEDimage/MEDscan.py index 865fe0d..857a185 100644 --- a/MEDimage/MEDscan.py +++ b/MEDimage/MEDscan.py @@ -60,15 +60,9 @@ def __init__(self, medscan=None) -> None: self.data = medscan.data except: self.data = self.data() - try: - self.params = medscan.params - except: - self.params = self.Params() - try: - self.radiomics = medscan.radiomics - except: - self.radiomics = self.Radiomics() + self.params = self.Params() + self.radiomics = self.Radiomics() self.skip = False def __init_process_params(self, im_params: Dict) -> None: @@ -90,6 +84,8 @@ def __init_process_params(self, im_params: Dict) -> None: raise ValueError(f"The given parameters dict is not valid, no params found for {self.type} modality") # re-segmentation range processing + if(im_params['reSeg']['range'] and (im_params['reSeg']['range'][0] == "inf" or im_params['reSeg']['range'][0] == "-inf")): + im_params['reSeg']['range'][0] = -np.inf if(im_params['reSeg']['range'] and im_params['reSeg']['range'][1] == "inf"): im_params['reSeg']['range'][1] = np.inf @@ -218,6 +214,22 @@ def __init_extraction_params(self, im_params: Dict): self.params.radiomics.ngtdm.dist_correction = False else: self.params.radiomics.ngtdm.dist_correction = False + + # Features to extract + features = [ + "Morph", "LocalIntensity", "Stats", "IntensityHistogram", "IntensityVolumeHistogram", + "GLCM", "GLRLM", "GLSZM", "GLDZM", "NGTDM", "NGLDM" + ] + if "extract" in im_params.keys(): + self.params.radiomics.extract = im_params['extract'] + for key in self.params.radiomics.extract: + if key not in features: + raise ValueError(f"Invalid key in 'extract' parameter: {key} (Modality {self.type}).") + + # Ensure each feature is in the extract dictionary with a default value of True + for feature in features: + if feature not in self.params.radiomics.extract: + self.params.radiomics.extract[feature] = True def __init_filter_params(self, filter_params: Dict) -> None: """Initializes the filtering params from a given Dict. @@ -351,54 +363,41 @@ def init_ntf_calculation(self, vol_obj: image_volume_obj) -> None: '_' + ih_val_name + min_val_name # IVH name - if not self.params.process.ivh: # CT case + if self.params.process.im_range: # The im_range defines the computation. + min_val_name = ((str(self.params.process.im_range[0])).replace('.', 'dot')).replace('-', 'M') + max_val_name = ((str(self.params.process.im_range[1])).replace('.', 'dot')).replace('-', 'M') + if max_val_name == 'inf': + # In this case, the maximum value of the ROI is used, + # so no need to report it. + range_name = '_min' + min_val_name + elif min_val_name == '-inf' or min_val_name == 'inf': + # In this case, the minimum value of the ROI is used, + # so no need to report it. + range_name = '_max' + max_val_name + else: + range_name = '_min' + min_val_name + '_max' + max_val_name + else: + # min-max of ROI will be used, no need to report it. + range_name = '' + if not self.params.process.ivh: # CT case for example ivh_algo_name = 'algoNone' ivh_val_name = 'bin1' - if self.params.process.im_range: # The im_range defines the computation. - min_val_name = ((str(self.params.process.im_range[0])).replace( - '.', 'dot')).replace('-', 'M') - max_val_name = ((str(self.params.process.im_range[1])).replace( - '.', 'dot')).replace('-', 'M') - range_name = '_min' + min_val_name + '_max' + max_val_name - else: - range_name = '' else: - ivh_algo_name = 'algo' + self.params.process.ivh['type'] - if 'val' in self.params.process.ivh: + ivh_algo_name = 'algo' + self.params.process.ivh['type'] if 'type' in self.params.process.ivh else 'algoNone' + if 'val' in self.params.process.ivh and self.params.process.ivh['val']: ivh_val_name = 'bin' + (str(self.params.process.ivh['val'])).replace('.', 'dot') else: ivh_val_name = 'binNone' - # The im_range defines the computation. - if 'type' in self.params.process.ivh and self.params.process.ivh['type'].find('FBS') >=0: - if self.params.process.im_range: - min_val_name = ((str(self.params.process.im_range[0])).replace( - '.', 'dot')).replace('-', 'M') - max_val_name = ((str(self.params.process.im_range[1])).replace( - '.', 'dot')).replace('-', 'M') - if max_val_name == 'inf': - # In this case, the maximum value of the ROI is used, - # so no need to report it. - range_name = '_min' + min_val_name - elif min_val_name == '-inf': - # In this case, the minimum value of the ROI is used, - # so no need to report it. - range_name = '_max' + max_val_name - else: - range_name = '_min' + min_val_name + '_max' + max_val_name - else: # min-max of ROI will be used, no need to report it. - range_name = '' - else: # min-max of ROI will be used, no need to report it. - range_name = '' self.params.radiomics.ivh_name = self.params.radiomics.scale_name + '_' + ivh_algo_name + '_' + ivh_val_name + range_name # Now initialize the attribute that will hold the computation results self.radiomics.image.update({ - 'morph_3D': {self.params.radiomics.scale_name: {}}, - 'locInt_3D': {self.params.radiomics.scale_name: {}}, - 'stats_3D': {self.params.radiomics.scale_name: {}}, - 'intHist_3D': {self.params.radiomics.ih_name: {}}, - 'intVolHist_3D': {self.params.radiomics.ivh_name: {}} - }) + 'morph_3D': {self.params.radiomics.scale_name: {}}, + 'locInt_3D': {self.params.radiomics.scale_name: {}}, + 'stats_3D': {self.params.radiomics.scale_name: {}}, + 'intHist_3D': {self.params.radiomics.ih_name: {}}, + 'intVolHist_3D': {self.params.radiomics.ivh_name: {}} + }) except Exception as e: message = f"\n PROBLEM WITH PRE-PROCESSING OF FEATURES IN init_ntf_calculation(): \n {e}" @@ -1037,7 +1036,7 @@ def __init__(self, **kwargs) -> None: self.name_text_types = kwargs['name_text_types'] if 'name_text_types' in kwargs else None self.processing_name = kwargs['processing_name'] if 'processing_name' in kwargs else None self.scale_name = kwargs['scale_name'] if 'scale_name' in kwargs else None - + self.extract = kwargs['extract'] if 'extract' in kwargs else {} class GLCM: """Organizes the GLCM features extraction parameters""" diff --git a/MEDimage/__init__.py b/MEDimage/__init__.py index 647477d..6ed02d7 100644 --- a/MEDimage/__init__.py +++ b/MEDimage/__init__.py @@ -14,7 +14,7 @@ logging.getLogger(__name__).addHandler(stream_handler) __author__ = "MEDomicsLab consortium" -__version__ = "0.9.7" +__version__ = "0.9.8" __copyright__ = "Copyright (C) MEDomicsLab consortium" __license__ = "GNU General Public License 3.0" __maintainer__ = "MAHDI AIT LHAJ LOUTFI" diff --git a/MEDimage/biomarkers/BatchExtractor.py b/MEDimage/biomarkers/BatchExtractor.py index daf2954..f5d3c77 100644 --- a/MEDimage/biomarkers/BatchExtractor.py +++ b/MEDimage/biomarkers/BatchExtractor.py @@ -29,7 +29,8 @@ def __init__( path_csv: Union[str, Path], path_params: Union[str, Path], path_save: Union[str, Path], - n_batch: int = 4 + n_batch: int = 4, + skip_existing: bool = False ) -> None: """ constructor of the BatchExtractor class @@ -41,6 +42,7 @@ def __init__( self.roi_types = [] self.roi_type_labels = [] self.n_bacth = n_batch + self.skip_existing = skip_existing def __load_and_process_params(self) -> Dict: """Load and process the computing & batch parameters from JSON file""" @@ -82,6 +84,14 @@ def __compute_radiomics_one_patient( # Setting up logging settings logging.basicConfig(filename=log_file, level=logging.DEBUG, force=True) + # Check if features are already computed for the current scan + if self.skip_existing: + modality = name_patient.split('.')[1] + name_save = name_patient.split('.')[0] + f'({roi_type_label})' + f'.{modality}.json' + if Path(self._path_save / f'features({roi_type})' / name_save).exists(): + logging.info("Skipping existing features for scan: {name_patient}") + return log_file + # start timer t_start = time() @@ -94,7 +104,7 @@ def __compute_radiomics_one_patient( with open(self._path_read / name_patient, 'rb') as f: medscan = pickle.load(f) medscan = MEDimage.MEDscan(medscan) except Exception as e: - logging.error(f"\n ERROR LOADING PATIENT {name_patient}:\n {e}") + print(f"\n ERROR LOADING PATIENT {name_patient}:\n {e}") return None # Init processing & computation parameters @@ -197,35 +207,44 @@ def __compute_radiomics_one_patient( # Morphological features extraction try: - morph = MEDimage.biomarkers.morph.extract_all( - vol=vol_obj.data, - mask_int=roi_obj_int.data, - mask_morph=roi_obj_morph.data, - res=medscan.params.process.scale_non_text, - intensity_type=medscan.params.process.intensity_type - ) + if medscan.params.radiomics.extract['Morph']: + morph = MEDimage.biomarkers.morph.extract_all( + vol=vol_obj.data, + mask_int=roi_obj_int.data, + mask_morph=roi_obj_morph.data, + res=medscan.params.process.scale_non_text, + intensity_type=medscan.params.process.intensity_type + ) + else: + morph = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF MORPHOLOGICAL FEATURES {e}') morph = None # Local intensity features extraction try: - local_intensity = MEDimage.biomarkers.local_intensity.extract_all( - img_obj=vol_obj.data, - roi_obj=roi_obj_int.data, - res=medscan.params.process.scale_non_text, - intensity_type=medscan.params.process.intensity_type - ) + if medscan.params.radiomics.extract['LocalIntensity']: + local_intensity = MEDimage.biomarkers.local_intensity.extract_all( + img_obj=vol_obj.data, + roi_obj=roi_obj_int.data, + res=medscan.params.process.scale_non_text, + intensity_type=medscan.params.process.intensity_type + ) + else: + local_intensity = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF LOCAL INTENSITY FEATURES {e}') local_intensity = None # statistical features extraction try: - stats = MEDimage.biomarkers.stats.extract_all( - vol=vol_int_re, - intensity_type=medscan.params.process.intensity_type - ) + if medscan.params.radiomics.extract['Stats']: + stats = MEDimage.biomarkers.stats.extract_all( + vol=vol_int_re, + intensity_type=medscan.params.process.intensity_type + ) + else: + stats = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF STATISTICAL FEATURES {e}') stats = None @@ -240,9 +259,12 @@ def __compute_radiomics_one_patient( # Intensity histogram features extraction try: - int_hist = MEDimage.biomarkers.intensity_histogram.extract_all( - vol=vol_quant_re - ) + if medscan.params.radiomics.extract['IntensityHistogram']: + int_hist = MEDimage.biomarkers.intensity_histogram.extract_all( + vol=vol_quant_re + ) + else: + int_hist = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF INTENSITY HISTOGRAM FEATURES {e}') int_hist = None @@ -262,12 +284,15 @@ def __compute_radiomics_one_patient( wd = 1 # Intensity volume histogram features extraction - int_vol_hist = MEDimage.biomarkers.int_vol_hist.extract_all( - medscan=medscan, - vol=vol_quant_re, - vol_int_re=vol_int_re, - wd=wd - ) + if medscan.params.radiomics.extract['IntensityVolumeHistogram']: + int_vol_hist = MEDimage.biomarkers.int_vol_hist.extract_all( + medscan=medscan, + vol=vol_quant_re, + vol_int_re=vol_int_re, + wd=wd + ) + else: + int_vol_hist = None # End of Non-Texture features extraction logging.info(f"End of non-texture features extraction: {time() - start}\n") @@ -372,52 +397,70 @@ def __compute_radiomics_one_patient( # GLCM features extraction try: - glcm = MEDimage.biomarkers.glcm.extract_all( - vol=vol_quant_re, - dist_correction=medscan.params.radiomics.glcm.dist_correction) + if medscan.params.radiomics.extract['GLCM']: + glcm = MEDimage.biomarkers.glcm.extract_all( + vol=vol_quant_re, + dist_correction=medscan.params.radiomics.glcm.dist_correction) + else: + glcm = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF GLCM FEATURES {e}') glcm = None # GLRLM features extraction try: - glrlm = MEDimage.biomarkers.glrlm.extract_all( - vol=vol_quant_re, - dist_correction=medscan.params.radiomics.glrlm.dist_correction) + if medscan.params.radiomics.extract['GLRLM']: + glrlm = MEDimage.biomarkers.glrlm.extract_all( + vol=vol_quant_re, + dist_correction=medscan.params.radiomics.glrlm.dist_correction) + else: + glrlm = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF GLRLM FEATURES {e}') glrlm = None # GLSZM features extraction try: - glszm = MEDimage.biomarkers.glszm.extract_all( - vol=vol_quant_re) + if medscan.params.radiomics.extract['GLSZM']: + glszm = MEDimage.biomarkers.glszm.extract_all( + vol=vol_quant_re) + else: + glszm = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF GLSZM FEATURES {e}') glszm = None # GLDZM features extraction try: - gldzm = MEDimage.biomarkers.gldzm.extract_all( - vol_int=vol_quant_re, - mask_morph=roi_obj_morph.data) + if medscan.params.radiomics.extract['GLDZM']: + gldzm = MEDimage.biomarkers.gldzm.extract_all( + vol_int=vol_quant_re, + mask_morph=roi_obj_morph.data) + else: + gldzm = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF GLDZM FEATURES {e}') gldzm = None # NGTDM features extraction try: - ngtdm = MEDimage.biomarkers.ngtdm.extract_all( - vol=vol_quant_re, - dist_correction=medscan.params.radiomics.ngtdm.dist_correction) + if medscan.params.radiomics.extract['NGTDM']: + ngtdm = MEDimage.biomarkers.ngtdm.extract_all( + vol=vol_quant_re, + dist_correction=medscan.params.radiomics.ngtdm.dist_correction) + else: + ngtdm = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF NGTDM FEATURES {e}') ngtdm = None # NGLDM features extraction try: - ngldm = MEDimage.biomarkers.ngldm.extract_all( - vol=vol_quant_re) + if medscan.params.radiomics.extract['NGLDM']: + ngldm = MEDimage.biomarkers.ngldm.extract_all( + vol=vol_quant_re) + else: + ngldm = None except Exception as e: logging.error(f'PROBLEM WITH COMPUTATION OF NGLDM FEATURES {e}') ngldm = None diff --git a/MEDimage/wrangling/DataManager.py b/MEDimage/wrangling/DataManager.py index 348bb7e..6105012 100644 --- a/MEDimage/wrangling/DataManager.py +++ b/MEDimage/wrangling/DataManager.py @@ -3,7 +3,6 @@ import os import pickle import re -import warnings from dataclasses import dataclass from pathlib import Path from time import time @@ -23,7 +22,6 @@ from ..MEDscan import MEDscan from ..processing.compute_suv_map import compute_suv_map -from ..processing.interpolation import interp_volume from ..processing.segmentation import get_roi_from_indexes from ..utils.get_file_paths import get_file_paths from ..utils.get_patient_names import get_patient_names @@ -782,6 +780,10 @@ def __pre_radiomics_checks_dimensions( print("Wildcard is empty, the pre-checks will be aborted") return + # Updating plotting params + plt.rcParams["figure.figsize"] = (20,20) + plt.rcParams.update({'font.size': 22}) + # TODO: seperate by studies and scan type (MRscan, CTscan...) # TODO: Two summaries (df, list of names saves) -> # name_save = name_save(ROI) : Glioma-Huashan-001__T1.MRscan.npy({GTV}) @@ -846,7 +848,11 @@ def __pre_radiomics_checks_dimensions( x.grid(False) plt.title(f"Voxels xy-spacing checks for {wildcard}") plt.legend() - plt.show() + # Save the plot + if save: + plt.savefig(self.paths._path_save_checks / ('Voxels_xy_check.png')) + else: + plt.show() # Plotting z-spacing data histogram df_z = pd.DataFrame(z_dim["data"], columns=['data']) @@ -860,7 +866,11 @@ def __pre_radiomics_checks_dimensions( x.grid(False) plt.title(f"Voxels z-spacing checks for {wildcard}") plt.legend() - plt.show() + # Save the plot + if save: + plt.savefig(self.paths._path_save_checks / ('Voxels_z_check.png')) + else: + plt.show() # Saving files using wildcard for name if save: @@ -902,6 +912,10 @@ def __pre_radiomics_checks_window( Returns: None. """ + # Updating plotting params + plt.rcParams["figure.figsize"] = (20,20) + plt.rcParams.update({'font.size': 22}) + if type(wildcards_window) is str: wildcards_window = [wildcards_window] @@ -1032,7 +1046,11 @@ def __pre_radiomics_checks_window( x.xaxis.set_tick_params(pad=15) plt.title(f"Intensity range checks for {wildcard}, bw={bin_width}") plt.legend() - plt.show() + # Save the plot + if save: + plt.savefig(self.paths._path_save_checks / ('Intensity_range_check_' + f'bw_{bin_width}.png')) + else: + plt.show() # save final checks if save: diff --git a/README.md b/README.md index 8a1bbee..74e5d25 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI - Python Version](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10-blue)](https://www.python.org/downloads/release/python-380/) -[![PyPI - version](https://img.shields.io/badge/pypi-v0.9.7-blue)](https://pypi.org/project/medimage-pkg/) +[![PyPI - version](https://img.shields.io/badge/pypi-v0.9.8-blue)](https://pypi.org/project/medimage-pkg/) [![Continuous Integration](https://github.com/MahdiAll99/MEDimage/actions/workflows/python-app.yml/badge.svg)](https://github.com/MahdiAll99/MEDimage/actions/workflows/python-app.yml) [![Documentation Status](https://readthedocs.org/projects/medimage/badge/?version=latest)](https://medimage.readthedocs.io/en/latest/?badge=latest) [![License: GPL-3](https://img.shields.io/badge/license-GPLv3-blue)](LICENSE) diff --git a/pyproject.toml b/pyproject.toml index 0b62d96..8fcb5f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [tool.poetry] name = "medimage-pkg" -version = "0.9.7" +version = "0.9.8" description = "MEDimage is a Python package for processing and extracting features from medical images" authors = ["MEDomics Consortium "] license = "GPL-3.0" readme = "README.md" -homepage = "https://github.com/medomics/MEDomicsLab-develop" -repository = "https://github.com/medomics/MEDomicsLab-develop" +homepage = "https://medimage.app/" +repository = "https://github.com/MEDomics-UdeS/MEDimage/" keywords = ["python", "ibsi", "medical-imaging", "cancer-imaging-research", "radiomics", "medical-image-analysis", "features-extraction", diff --git a/setup.py b/setup.py index 4aa657d..a9502f4 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name="MEDimage", - version="0.9.7", + version="0.9.8", author="MEDomics consortium", author_email="medomics.info@gmail.com", description="Python Open-source package for medical images processing and radiomic features extraction",