def process_testcase(self, test, featureClassName): self.logger.debug('processing testCase = %s, featureClassName = %s', test, featureClassName) self.testUtils.setFeatureClassAndTestCase(featureClassName, test) testImage = self.testUtils.getImage('original') testMask = self.testUtils.getMask('original') featureClass = self.featureClasses[featureClassName]( testImage, testMask, **self.testUtils.getSettings()) featureClass.enableAllFeatures() featureClass.execute() if "_calculateMatrix" in dir(featureClass): cMat = getattr(featureClass, 'P_%s' % featureClassName) if cMat is not None: numpy.save( os.path.join(self.baselineDir, '%s_%s.npy' % (test, featureClassName)), cMat) imageTypeName = 'original' # Update versions to reflect which configuration generated the baseline self.generalInfo = generalinfo.GeneralInfo() versions = self.generalInfo.getGeneralInfo() self.new_baselines[featureClassName].configuration[test].update( versions) self.new_baselines[featureClassName].baseline[test] = { '%s_%s_%s' % (imageTypeName, featureClassName, key): val for key, val in six.iteritems(featureClass.featureValues) }
def getProvenance(self, imageFilepath, maskFilepath, mask): """ Generates provenance information for reproducibility. Takes the original image & mask filepath, as well as the resampled mask which is passed to the feature classes. Returns a dictionary with keynames coded as "general_info_<item>". For more information on generated items, see :ref:`generalinfo<radiomics-generalinfo-label>` """ self.logger.info('Adding additional extraction information') provenanceVector = collections.OrderedDict() generalinfoClass = generalinfo.GeneralInfo(imageFilepath, maskFilepath, mask, self.settings, self.inputImages) for k, v in six.iteritems(generalinfoClass.execute()): provenanceVector['general_info_%s' % k] = v return provenanceVector
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, 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