def execute(self, imageFilepath, maskFilepath, label=None, voxelBased=False): """ Compute radiomics signature for provide image and mask combination. It comprises of the following steps: 1. Image and mask are loaded and normalized/resampled if necessary. 2. Validity of ROI is checked using :py:func:`~imageoperations.checkMask`, which also computes and returns the bounding box. 3. If enabled, provenance information is calculated and stored as part of the result. (Not available in voxel-based extraction) 4. Shape features are calculated on a cropped (no padding) version of the original image. (Not available in voxel-based extraction) 5. If enabled, resegment the mask based upon the range specified in ``resegmentRange`` (default None: resegmentation disabled). 6. Other enabled feature classes are calculated using all specified image types in ``_enabledImageTypes``. Images are cropped to tumor mask (no padding) after application of any filter and before being passed to the feature class. 7. The calculated features is returned as ``collections.OrderedDict``. :param imageFilepath: SimpleITK Image, or string pointing to image file location :param maskFilepath: SimpleITK Image, or string pointing to labelmap file location :param label: Integer, value of the label for which to extract features. If not specified, last specified label is used. Default label is 1. :returns: dictionary containing calculated signature ("<imageType>_<featureClass>_<featureName>":value). """ geometryTolerance = self.settings.get('geometryTolerance') additionalInfo = self.settings.get('additionalInfo', False) resegmentShape = self.settings.get('resegmentShape', False) if label is not None: self.settings['label'] = label else: label = self.settings.get('label', 1) if self.geometryTolerance != geometryTolerance: self._setTolerance() if additionalInfo: self.generalInfo = generalinfo.GeneralInfo() self.generalInfo.addGeneralSettings(self.settings) self.generalInfo.addEnabledImageTypes(self._enabledImagetypes) else: self.generalInfo = None if voxelBased: self.settings['voxelBased'] = True self.logger.info('Starting voxel based extraction') self.logger.info('Calculating features with label: %d', label) self.logger.debug('Enabled images types: %s', self._enabledImagetypes) self.logger.debug('Enabled features: %s', self._enabledFeatures) self.logger.debug('Current settings: %s', self.settings) if self.settings.get('binCount', None) is not None: self.logger.warning( 'Fixed bin Count enabled! However, we recommend using a fixed bin Width. See ' 'http://pyradiomics.readthedocs.io/en/latest/faq.html#radiomics-fixed-bin-width for more ' 'details') # 1. Load the image and mask featureVector = collections.OrderedDict() image, mask = self.loadImage(imageFilepath, maskFilepath) # 2. Check whether loaded mask contains a valid ROI for feature extraction and get bounding box # Raises a ValueError if the ROI is invalid boundingBox, correctedMask = imageoperations.checkMask( image, mask, **self.settings) # Update the mask if it had to be resampled if correctedMask is not None: if self.generalInfo is not None: self.generalInfo.addMaskElements(image, correctedMask, label, 'corrected') mask = correctedMask self.logger.debug( 'Image and Mask loaded and valid, starting extraction') # 5. Resegment the mask if enabled (parameter regsegmentMask is not None) resegmentedMask = None if self.settings.get('resegmentRange', None) is not None: resegmentedMask = imageoperations.resegmentMask( image, mask, **self.settings) # Recheck to see if the mask is still valid, raises a ValueError if not boundingBox, correctedMask = imageoperations.checkMask( image, resegmentedMask, **self.settings) if self.generalInfo is not None: self.generalInfo.addMaskElements(image, resegmentedMask, label, 'resegmented') # if resegmentShape is True and resegmentation has been enabled, update the mask here to also use the # resegmented mask for shape calculation (e.g. PET resegmentation) if resegmentShape and resegmentedMask is not None: mask = resegmentedMask if not voxelBased: # 3. Add the additional information if enabled if self.generalInfo is not None: featureVector.update(self.generalInfo.getGeneralInfo()) # 4. If shape descriptors should be calculated, handle it separately here if 'shape' in self._enabledFeatures.keys(): featureVector.update( self.computeShape(image, mask, boundingBox)) if 'shape2D' in self._enabledFeatures.keys(): force2D = self.settings.get('force2D', False) force2Ddimension = self.settings.get('force2Ddimension', 0) if not force2D: self.logger.warning( 'parameter force2D must be set to True to enable shape2D extraction' ) elif not (boundingBox[1::2] - boundingBox[0::2] + 1)[force2Ddimension] > 1: self.logger.warning( 'Size in specified 2D dimension (%i) is greater than 1, cannot calculate 2D shape', force2Ddimension) else: featureVector.update( self.computeShape(image, mask, boundingBox, 'shape2D')) # (Default) Only use resegemented mask for feature classes other than shape # can be overridden by specifying `resegmentShape` = True if not resegmentShape and resegmentedMask is not None: mask = resegmentedMask # 6. Calculate other enabled feature classes using enabled image types # Make generators for all enabled image types self.logger.debug('Creating image type iterator') imageGenerators = [] for imageType, customKwargs in six.iteritems(self._enabledImagetypes): args = self.settings.copy() args.update(customKwargs) self.logger.info( 'Adding image type "%s" with custom settings: %s' % (imageType, str(customKwargs))) imageGenerators = chain( imageGenerators, getattr(imageoperations, 'get%sImage' % imageType)(image, mask, **args)) self.logger.debug('Extracting features') # Calculate features for all (filtered) images in the generator for inputImage, imageTypeName, inputKwargs in imageGenerators: self.logger.info('Calculating features for %s image', imageTypeName) inputImage, inputMask = imageoperations.cropToTumorMask( inputImage, mask, boundingBox) featureVector.update( self.computeFeatures(inputImage, inputMask, imageTypeName, **inputKwargs)) self.logger.debug('Features extracted') return featureVector
def execute(self, imageFilepath, maskFilepath, label=None, voxelBased=False): """ Compute radiomics signature for provide image and mask combination. It comprises of the following steps: 1. Image and mask are loaded and normalized/resampled if necessary. 2. Validity of ROI is checked using :py:func:`~imageoperations.checkMask`, which also computes and returns the bounding box. 3. If enabled, provenance information is calculated and stored as part of the result. (Not available in voxel-based extraction) 4. Shape features are calculated on a cropped (no padding) version of the original image. (Not available in voxel-based extraction) 5. If enabled, resegment the mask based upon the range specified in ``resegmentRange`` (default None: resegmentation disabled). 6. Other enabled feature classes are calculated using all specified image types in ``_enabledImageTypes``. Images are cropped to tumor mask (no padding) after application of any filter and before being passed to the feature class. 7. The calculated features is returned as ``collections.OrderedDict``. :param imageFilepath: SimpleITK Image, or string pointing to image file location :param maskFilepath: SimpleITK Image, or string pointing to labelmap file location :param label: Integer, value of the label for which to extract features. If not specified, last specified label is used. Default label is 1. :returns: dictionary containing calculated signature ("<imageType>_<featureClass>_<featureName>":value). """ if self.geometryTolerance != self.settings.get('geometryTolerance'): self._setTolerance() if label is not None: self.settings['label'] = label self.settings['voxelBased'] = voxelBased if voxelBased: self.logger.info('Starting voxel based extraction') self.logger.info('Calculating features with label: %d', self.settings['label']) self.logger.debug('Enabled images types: %s', self._enabledImagetypes) self.logger.debug('Enabled features: %s', self._enabledFeatures) self.logger.debug('Current settings: %s', self.settings) # 1. Load the image and mask featureVector = collections.OrderedDict() image, mask = self.loadImage(imageFilepath, maskFilepath) if image is None or mask is None: # No features can be extracted, return the empty featureVector return featureVector # 2. Check whether loaded mask contains a valid ROI for feature extraction and get bounding box boundingBox, correctedMask = imageoperations.checkMask( image, mask, **self.settings) # Update the mask if it had to be resampled if correctedMask is not None: mask = correctedMask if boundingBox is None: # Mask checks failed, do not extract features and return the empty featureVector return featureVector self.logger.debug( 'Image and Mask loaded and valid, starting extraction') if not voxelBased: # 3. Add the additional information if enabled if self.settings['additionalInfo']: featureVector.update( self.getProvenance(imageFilepath, maskFilepath, mask)) # 4. If shape descriptors should be calculated, handle it separately here if 'shape' in self._enabledFeatures.keys(): croppedImage, croppedMask = imageoperations.cropToTumorMask( image, mask, boundingBox) enabledFeatures = self._enabledFeatures['shape'] self.logger.info('Computing shape') shapeClass = self.featureClasses['shape'](croppedImage, croppedMask, **self.settings) if enabledFeatures is None or len(enabledFeatures) == 0: shapeClass.enableAllFeatures() else: for feature in enabledFeatures: shapeClass.enableFeatureByName(feature) shapeClass.calculateFeatures() for (featureName, featureValue) in six.iteritems(shapeClass.featureValues): newFeatureName = 'original_shape_%s' % featureName featureVector[newFeatureName] = featureValue # 5. Resegment the mask if enabled (parameter regsegmentMask is not None) resegmentRange = self.settings.get('resegmentRange', None) if resegmentRange is not None: resegmentedMask = imageoperations.resegmentMask( image, mask, resegmentRange, self.settings['label']) # Recheck to see if the mask is still valid boundingBox, correctedMask = imageoperations.checkMask( image, resegmentedMask, **self.settings) # Update the mask if it had to be resampled if correctedMask is not None: resegmentedMask = correctedMask if boundingBox is None: # Mask checks failed, do not extract features and return the empty featureVector return featureVector # Resegmentation successful mask = resegmentedMask # 6. Calculate other enabled feature classes using enabled image types # Make generators for all enabled image types self.logger.debug('Creating image type iterator') imageGenerators = [] for imageType, customKwargs in six.iteritems(self._enabledImagetypes): args = self.settings.copy() args.update(customKwargs) self.logger.info( 'Adding image type "%s" with custom settings: %s' % (imageType, str(customKwargs))) imageGenerators = chain( imageGenerators, getattr(imageoperations, 'get%sImage' % imageType)(image, mask, **args)) self.logger.debug('Extracting features') # Calculate features for all (filtered) images in the generator for inputImage, imageTypeName, inputKwargs in imageGenerators: self.logger.info('Calculating features for %s image', imageTypeName) inputImage, inputMask = imageoperations.cropToTumorMask( inputImage, mask, boundingBox) featureVector.update( self.computeFeatures(inputImage, inputMask, imageTypeName, **inputKwargs)) self.logger.debug('Features extracted') return featureVector
def execute(self, imageFilepath, maskFilepath, otherImageFilePath=None, label=None, label_channel=None, voxelBased=False): """ Compute radiomics signature for provide image and mask combination. It comprises of the following steps: 1. Image and mask are loaded and normalized/resampled if necessary. 2. Validity of ROI is checked using :py:func:`~imageoperations.checkMask`, which also computes and returns the bounding box. 3. If enabled, provenance information is calculated and stored as part of the result. (Not available in voxel-based extraction) 4. Shape features are calculated on a cropped (no padding) version of the original image. (Not available in voxel-based extraction) 5. If enabled, resegment the mask based upon the range specified in ``resegmentRange`` (default None: resegmentation disabled). 6. Other enabled feature classes are calculated using all specified image types in ``_enabledImageTypes``. Images are cropped to tumor mask (no padding) after application of any filter and before being passed to the feature class. 7. The calculated features is returned as ``collections.OrderedDict``. :param imageFilepath: SimpleITK Image, or string pointing to image file location :param maskFilepath: SimpleITK Image, or string pointing to labelmap file location :param label: Integer, value of the label for which to extract features. If not specified, last specified label is used. Default label is 1. :param label_channel: Integer, index of the channel to use when maskFilepath yields a SimpleITK.Image with a vector pixel type. Default index is 0. :param voxelBased: Boolean, default False. If set to true, a voxel-based extraction is performed, segment-based otherwise. :returns: dictionary containing calculated signature ("<imageType>_<featureClass>_<featureName>":value). In case of segment-based extraction, value type for features is float, if voxel-based, type is SimpleITK.Image. Type of diagnostic features differs, but can always be represented as a string. """ global geometryTolerance, logger _settings = self.settings.copy() tolerance = _settings.get('geometryTolerance') additionalInfo = _settings.get('additionalInfo', False) resegmentShape = _settings.get('resegmentShape', False) if label is not None: _settings['label'] = label else: label = _settings.get('label', 1) if label_channel is not None: _settings['label_channel'] = label_channel if geometryTolerance != tolerance: self._setTolerance() if additionalInfo: generalInfo = generalinfo.GeneralInfo() generalInfo.addGeneralSettings(_settings) generalInfo.addEnabledImageTypes(self.enabledImagetypes) else: generalInfo = None if voxelBased: _settings['voxelBased'] = True kernelRadius = _settings.get('kernelRadius', 1) logger.info('Starting voxel based extraction') else: kernelRadius = 0 logger.info('Calculating features with label: %d', label) logger.debug('Enabled images types: %s', self.enabledImagetypes) logger.debug('Enabled features: %s', self.enabledFeatures) logger.debug('Current settings: %s', _settings) # 1. Load the image and mask featureVector = collections.OrderedDict() image, mask = self.loadImage(imageFilepath, maskFilepath, otherImageFilePath, generalInfo, **_settings) # 2. Check whether loaded mask contains a valid ROI for feature extraction and get bounding box # Raises a ValueError if the ROI is invalid boundingBox, correctedMask = imageoperations.checkMask( image, mask, **_settings) # Update the mask if it had to be resampled if correctedMask is not None: if generalInfo is not None: generalInfo.addMaskElements(image, correctedMask, label, 'corrected') mask = correctedMask logger.debug('Image and Mask loaded and valid, starting extraction') # 5. Resegment the mask if enabled (parameter regsegmentMask is not None) resegmentedMask = None if _settings.get('resegmentRange', None) is not None: resegmentedMask = imageoperations.resegmentMask( image, mask, **_settings) # Recheck to see if the mask is still valid, raises a ValueError if not boundingBox, correctedMask = imageoperations.checkMask( image, resegmentedMask, **_settings) if generalInfo is not None: generalInfo.addMaskElements(image, resegmentedMask, label, 'resegmented') # 3. Add the additional information if enabled if generalInfo is not None: featureVector.update(generalInfo.getGeneralInfo()) # if resegmentShape is True and resegmentation has been enabled, update the mask here to also use the # resegmented mask for shape calculation (e.g. PET resegmentation) if resegmentShape and resegmentedMask is not None: mask = resegmentedMask if not voxelBased: # 4. If shape descriptors should be calculated, handle it separately here featureVector.update( self.computeShape(image, mask, boundingBox, **_settings)) # (Default) Only use resegemented mask for feature classes other than shape # can be overridden by specifying `resegmentShape` = True if not resegmentShape and resegmentedMask is not None: mask = resegmentedMask # 6. Calculate other enabled feature classes using enabled image types # Make generators for all enabled image types logger.debug('Creating image type iterator') imageGenerators = [] for imageType, customKwargs in six.iteritems(self.enabledImagetypes): args = _settings.copy() args.update(customKwargs) logger.info('Adding image type "%s" with custom settings: %s' % (imageType, str(customKwargs))) imageGenerators = chain( imageGenerators, getattr(imageoperations, 'get%sImage' % imageType)(image, mask, **args)) logger.debug('Extracting features') # Calculate features for all (filtered) images in the generator for inputImage, imageTypeName, inputKwargs in imageGenerators: logger.info('Calculating features for %s image', imageTypeName) inputImage, inputMask = imageoperations.cropToTumorMask( inputImage, mask, boundingBox, padDistance=kernelRadius) featureVector.update( self.computeFeatures(inputImage, inputMask, imageTypeName, **inputKwargs)) logger.debug('Features extracted') return featureVector