def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ self.detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool) supportedSOPClassUIDs = [ '1.2.840.10008.5.1.4.1.1.6.2', # Enhanced US Volume Storage ] # The only sample data set that we received from GE LOGIQE10 (software version R1.5.1). # It added all volumes into a single series, even though they were acquired minutes apart. # Therefore, instead of loading the volumes into a sequence, we load each as a separate volume. loadables = [] for filePath in files: # Quick check of SOP class UID without parsing the file... sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID']) if not (sopClassUID in supportedSOPClassUIDs): # Unsupported class continue instanceNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['instanceNumber']) modality = slicer.dicomDatabase.fileValue(filePath, self.tags['modality']) seriesNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesNumber']) seriesDescription = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesDescription']) photometricInterpretation = slicer.dicomDatabase.fileValue(filePath, self.tags['photometricInterpretation']) name = '' if seriesNumber: name = f'{seriesNumber}:' if modality: name = f'{name} {modality}' if seriesDescription: name = f'{name} {seriesDescription}' else: name = f'{name} volume' if instanceNumber: name = f'{name} [{instanceNumber}]' loadable = DICOMLoadable() loadable.singleSequence = False # put each instance in a separate sequence loadable.files = [filePath] loadable.name = name.strip() # remove leading and trailing spaces, if any loadable.warning = "Loading of this image type is experimental. Please verify image geometry and report any problem is found." loadable.tooltip = f"Ultrasound volume" loadable.selected = True # Confidence is slightly larger than default scalar volume plugin's (0.5) # and DICOMVolumeSequencePlugin (0.7) # but still leaving room for more specialized plugins. loadable.confidence = 0.8 loadable.grayscale = ('MONOCHROME' in photometricInterpretation) loadables.append(loadable) return loadables
def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ self.detailedLogging = slicer.util.settingsValue( 'DICOM/detailedLogging', False, converter=slicer.util.toBool) supportedSOPClassUIDs = [ '1.2.840.10008.5.1.4.1.1.12.1', # X-Ray Angiographic Image Storage '1.2.840.10008.5.1.4.1.1.12.2', # X-Ray Fluoroscopy Image Storage '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage '1.2.840.10008.5.1.4.1.1.6.1', # Ultrasound Image Storage '1.2.840.10008.5.1.4.1.1.7', # Secondary Capture Image Storage (only accepted for modalities that typically acquire 2D image sequences) '1.2.840.10008.5.1.4.1.1.4', # MR Image Storage (will be only accepted if cine-MRI) ] # Modalities that typically acquire 2D image sequences: suppportedSecondaryCaptureModalities = ['US', 'XA', 'RF', 'ES'] # Each instance will be a loadable, that will result in one sequence browser node # and usually one sequence (except simultaneous biplane acquisition, which will # result in two sequences). # Each pedal press on the XA/RF acquisition device creates a new instance number, # but if the device has two imaging planes (biplane) then two sequences # will be acquired, which have the same instance number. These two sequences # are synchronized in time, therefore they have to be assigned to the same # browser node. instanceNumberToLoadableIndex = {} loadables = [] canBeCineMri = True cineMriTriggerTimes = set() cineMriInstanceNumberToFilenameIndex = {} for filePath in files: # Quick check of SOP class UID without parsing the file... try: sopClassUID = slicer.dicomDatabase.fileValue( filePath, self.tags['sopClassUID']) if not (sopClassUID in supportedSOPClassUIDs): # Unsupported class continue # Only accept MRI if it looks like cine-MRI if sopClassUID != '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage (will be only accepted if cine-MRI) canBeCineMri = False if not canBeCineMri and sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage continue except Exception as e: # Quick check could not be completed (probably Slicer DICOM database is not initialized). # No problem, we'll try to parse the file and check the SOP class UID then. pass instanceNumber = slicer.dicomDatabase.fileValue( filePath, self.tags['instanceNumber']) if sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage if not instanceNumber: # no instance number, probably not cine-MRI canBeCineMri = False if self.detailedLogging: logging.debug( "No instance number attribute found, the series will not be considered as a cine MRI" ) continue cineMriInstanceNumberToFilenameIndex[int( instanceNumber)] = filePath cineMriTriggerTimes.add( slicer.dicomDatabase.fileValue(filePath, self.tags['triggerTime'])) else: modality = slicer.dicomDatabase.fileValue( filePath, self.tags['modality']) if sopClassUID == '1.2.840.10008.5.1.4.1.1.7': # Secondary Capture Image Storage if modality not in suppportedSecondaryCaptureModalities: # practice of dumping secondary capture images into the same series # is only prevalent in US and XA/RF modalities continue if not (instanceNumber in instanceNumberToLoadableIndex.keys()): # new instance number seriesNumber = slicer.dicomDatabase.fileValue( filePath, self.tags['seriesNumber']) seriesDescription = slicer.dicomDatabase.fileValue( filePath, self.tags['seriesDescription']) photometricInterpretation = slicer.dicomDatabase.fileValue( filePath, self.tags['photometricInterpretation']) name = '' if seriesNumber: name = f'{seriesNumber}:' if modality: name = f'{name} {modality}' if seriesDescription: name = f'{name} {seriesDescription}' if instanceNumber: name = f'{name} [{instanceNumber}]' loadable = DICOMLoadable() loadable.singleSequence = False # put each instance in a separate sequence loadable.files = [filePath] loadable.name = name.strip( ) # remove leading and trailing spaces, if any loadable.warning = "Image spacing may need to be calibrated for accurate size measurements." loadable.tooltip = f"{modality} image sequence" loadable.selected = True # Confidence is slightly larger than default scalar volume plugin's (0.5) # but still leaving room for more specialized plugins. loadable.confidence = 0.7 loadable.grayscale = ('MONOCHROME' in photometricInterpretation) # Add to loadables list loadables.append(loadable) instanceNumberToLoadableIndex[instanceNumber] = len( loadables) - 1 else: # existing instance number, add this file loadableIndex = instanceNumberToLoadableIndex[ instanceNumber] loadables[loadableIndex].files.append(filePath) loadable.tooltip = f"{modality} image sequence ({len(loadables[loadableIndex].files)} planes)" if canBeCineMri and len(cineMriInstanceNumberToFilenameIndex) > 1: # Get description from first ds = dicom.read_file(cineMriInstanceNumberToFilenameIndex[next( iter(cineMriInstanceNumberToFilenameIndex))], stop_before_pixels=True) name = '' if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber: name = f'{ds.SeriesNumber}:' if hasattr(ds, 'Modality') and ds.Modality: name = f'{name} {ds.Modality}' if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription: name = f'{name} {ds.SeriesDescription}' loadable = DICOMLoadable() loadable.singleSequence = True # put all instances in a single sequence loadable.instanceNumbers = sorted( cineMriInstanceNumberToFilenameIndex) loadable.files = [ cineMriInstanceNumberToFilenameIndex[instanceNumber] for instanceNumber in loadable.instanceNumbers ] loadable.name = name.strip( ) # remove leading and trailing spaces, if any loadable.tooltip = f"{ds.Modality} image sequence" loadable.selected = True if len(cineMriTriggerTimes) > 3: if self.detailedLogging: logging.debug("Several different trigger times found (" + repr(cineMriTriggerTimes) + ") - assuming this series is a cine MRI") # This is likely a cardiac cine acquisition. # Multivolume importer sets confidence=1.0, so we need to set a bit higher confidence to be selected by default loadable.confidence = 1.05 else: # This may be a 3D acquisition,so set lower confidence than scalar volume's default (0.5) if self.detailedLogging: logging.debug( "Only one or few different trigger times found (" + repr(cineMriTriggerTimes) + ") - assuming this series is not a cine MRI") loadable.confidence = 0.4 loadable.grayscale = ('MONOCHROME' in ds.PhotometricInterpretation) # Add to loadables list loadables.append(loadable) return loadables
def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ seriesUID = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesUID']) seriesName = self.defaultSeriesNodeName(seriesUID) # default loadable includes all files for series allFilesLoadable = DICOMLoadable() allFilesLoadable.files = files allFilesLoadable.name = self.cleanNodeName(seriesName) allFilesLoadable.tooltip = "%d files, first file: %s" % (len( allFilesLoadable.files), allFilesLoadable.files[0]) allFilesLoadable.selected = True # add it to the list of loadables later, if pixel data is available in at least one file # make subseries volumes based on tag differences subseriesTags = [ "seriesInstanceUID", "acquisitionNumber", # GE volume viewer and Siemens Axiom CBCT systems put an overview (localizer) slice and all the reconstructed slices # in one series, using two different image types. Splitting based on image type allows loading of these volumes # (loading the series without localizer). "imageType", "imageOrientationPatient", "diffusionGradientOrientation", ] if self.allowLoadingByTime(): subseriesTags.append("contentTime") subseriesTags.append("triggerTime") # Values for these tags will only be enumerated (value itself will not be part of the loadable name) # because the vale itself is usually too long and complicated to be displayed to users subseriesTagsToEnumerateValues = [ "seriesInstanceUID", "imageOrientationPatient", "diffusionGradientOrientation", ] # # first, look for subseries within this series # - build a list of files for each unique value # of each tag # subseriesFiles = {} subseriesValues = {} for file in allFilesLoadable.files: # check for subseries values for tag in subseriesTags: value = slicer.dicomDatabase.fileValue(file, self.tags[tag]) value = value.replace( ",", "_") # remove commas so it can be used as an index if tag not in subseriesValues: subseriesValues[tag] = [] if not subseriesValues[tag].__contains__(value): subseriesValues[tag].append(value) if (tag, value) not in subseriesFiles: subseriesFiles[tag, value] = [] subseriesFiles[tag, value].append(file) loadables = [] # Pixel data is available, so add the default loadable to the output loadables.append(allFilesLoadable) # # second, for any tags that have more than one value, create a new # virtual series # subseriesCount = 0 # List of loadables that look like subseries that contain the full series except a single frame probableLocalizerFreeLoadables = [] for tag in subseriesTags: if len(subseriesValues[tag]) > 1: subseriesCount += 1 for valueIndex, value in enumerate(subseriesValues[tag]): # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = subseriesFiles[tag, value] # value can be a long string (and it will be used for generating node name) # therefore use just an index instead if tag in subseriesTagsToEnumerateValues: loadable.name = seriesName + " - %s %d" % ( tag, valueIndex + 1) else: loadable.name = seriesName + " - %s %s" % (tag, value) loadable.name = self.cleanNodeName(loadable.name) loadable.tooltip = "%d files, grouped by %s = %s. First file: %s. %s = %s" % ( len(loadable.files), tag, value, loadable.files[0], tag, value) loadable.selected = False loadables.append(loadable) if len(subseriesValues[tag]) == 2: otherValue = subseriesValues[tag][1 - valueIndex] if len(subseriesFiles[tag, value]) > 1 and len( subseriesFiles[tag, otherValue]) == 1: # this looks like a subseries without a localizer image probableLocalizerFreeLoadables.append(loadable) # remove any files from loadables that don't have pixel data (no point sending them to ITK for reading) # also remove DICOM SEG, since it is not handled by ITK readers newLoadables = [] for loadable in loadables: newFiles = [] excludedLoadable = False for file in loadable.files: if slicer.dicomDatabase.fileValueExists( file, self.tags['pixelData']): newFiles.append(file) if slicer.dicomDatabase.fileValue( file, self.tags['sopClassUID'] ) == '1.2.840.10008.5.1.4.1.1.66.4': excludedLoadable = True if 'DICOMSegmentationPlugin' not in slicer.modules.dicomPlugins: logging.warning( 'Please install Quantitative Reporting extension to enable loading of DICOM Segmentation objects' ) elif slicer.dicomDatabase.fileValue( file, self.tags['sopClassUID'] ) == '1.2.840.10008.5.1.4.1.1.481.3': excludedLoadable = True if 'DicomRtImportExportPlugin' not in slicer.modules.dicomPlugins: logging.warning( 'Please install SlicerRT extension to enable loading of DICOM RT Structure Set objects' ) if len(newFiles) > 0 and not excludedLoadable: loadable.files = newFiles loadable.grayscale = ( 'MONOCHROME' in slicer.dicomDatabase.fileValue( newFiles[0], self.tags['photometricInterpretation'])) newLoadables.append(loadable) elif excludedLoadable: continue else: # here all files in have no pixel data, so they might be # secondary capture images which will read, so let's pass # them through with a warning and low confidence loadable.warning += "There is no pixel data attribute for the DICOM objects, but they might be readable as secondary capture images. " loadable.confidence = 0.2 loadable.grayscale = ( 'MONOCHROME' in slicer.dicomDatabase.fileValue( loadable.files[0], self.tags['photometricInterpretation'])) newLoadables.append(loadable) loadables = newLoadables # # now for each series and subseries, sort the images # by position and check for consistency # then adjust confidence values based on warnings # for loadable in loadables: loadable.files, distances, loadable.warning = DICOMUtils.getSortedImageFiles( loadable.files, self.epsilon) loadablesBetterThanAllFiles = [] if allFilesLoadable.warning != "": for probableLocalizerFreeLoadable in probableLocalizerFreeLoadables: if probableLocalizerFreeLoadable.warning == "": # localizer-free loadables are better then all files, if they don't have warning loadablesBetterThanAllFiles.append( probableLocalizerFreeLoadable) if not loadablesBetterThanAllFiles and subseriesCount == 1: # there was a sorting warning and # only one kind of subseries, so it's probably correct # to have lower confidence in the default all-files version. for loadable in loadables: if loadable != allFilesLoadable and loadable.warning == "": loadablesBetterThanAllFiles.append(loadable) # if there are loadables that are clearly better then all files, then use those (otherwise use all files loadable) preferredLoadables = loadablesBetterThanAllFiles if loadablesBetterThanAllFiles else [ allFilesLoadable ] # reduce confidence and deselect all non-preferred loadables for loadable in loadables: if loadable in preferredLoadables: loadable.selected = True else: loadable.selected = False if loadable.confidence > .45: loadable.confidence = .45 return loadables
def examineFiles(self,files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ supportedSOPClassUIDs = [ '1.2.840.10008.5.1.4.1.1.12.1', # X-Ray Angiographic Image Storage '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage ] # Each instance will be a loadable, that will result in one sequence browser node # and usually one sequence (except simultaneous biplane acquisition, which will # result in two sequences). # Each pedal press on the XA acquisition device creates a new instance number, # but if the device has two imaging planes (biplane) then two sequences # will be acquired, which have the same instance number. These two sequences # are synchronized in time, therefore they have to be assigned to the same # browser node. instanceNumberToLoadableIndex = {} loadables = [] # Confidence is slightly larger than default scalar volume plugin's (0.5) # but still leaving room for more specialized plugins. confidence = 0.7 for filePath in files: # Quick check of SOP class UID without parsing the file... try: sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID']) if not (sopClassUID in supportedSOPClassUIDs): # Unsupported class return [] except Exception as e: # Quick check could not be completed (probably Slicer DICOM database is not initialized). # No problem, we'll try to parse the file and check the SOP class UID then. pass try: ds = dicom.read_file(filePath, stop_before_pixels=True) except Exception as e: logging.debug("Failed to parse DICOM file: {0}".format(str(e))) return [] if not (ds.SOPClassUID in supportedSOPClassUIDs): # Unsupported class return [] if not (ds.InstanceNumber in instanceNumberToLoadableIndex.keys()): # new instance number name = '' if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber: name = '{0}:'.format(ds.SeriesNumber) if hasattr(ds, 'Modality') and ds.Modality: name = '{0} {1}'.format(name, ds.Modality) if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription: name = '{0} {1}'.format(name, ds.SeriesDescription) if hasattr(ds, 'InstanceNumber') and ds.InstanceNumber: name = '{0} [{1}]'.format(name, ds.InstanceNumber) loadable = DICOMLoadable() loadable.files = [filePath] loadable.name = name.strip() # remove leading and trailing spaces, if any loadable.warning = "Image spacing may need to be calibrated for accurate size measurements." loadable.tooltip = "{0} image sequence".format(ds.Modality) loadable.selected = True loadable.confidence = confidence loadable.grayscale = ('MONOCHROME' in ds.PhotometricInterpretation) # Add to loadables list loadables.append(loadable) instanceNumberToLoadableIndex[ds.InstanceNumber] = len(loadables)-1 else: # existing instance number, add this file loadableIndex = instanceNumberToLoadableIndex[ds.InstanceNumber] loadables[loadableIndex].files.append(filePath) loadable.tooltip = "{0} image sequence ({1} planes)".format(ds.Modality, len(loadables[loadableIndex].files)) return loadables