def examineFiles(self,files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. Look for the special private creator tags that indicate a slicer data bundle Note that each data bundle is in a unique series, so if 'files' is a list of more than one element, then this is not a data bundle. """ loadables = [] if len(files) == 1: f = files[0] # get the series description to use as base for volume name name = slicer.dicomDatabase.fileValue(f, self.tags['seriesDescription']) if name == "": name = "Unknown" candygramValue = slicer.dicomDatabase.fileValue(f, self.tags['candygram']) if candygramValue: # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = [f] loadable.name = name + ' - as Slicer Scene' loadable.selected = True loadable.tooltip = 'Appears to contain a slicer scene' loadables.append(loadable) return loadables
def examineFiles(self, files): loadables = [] for currentFile in files: dataset = pydicom.read_file(currentFile) uid = self.getDICOMValue(dataset, "SOPInstanceUID") if uid == "": return [] seriesDescription = self.getDICOMValue(dataset, "SeriesDescription", "Unknown") if self.isQIICRX(dataset): loadable = DICOMLoadable() loadable.selected = True loadable.confidence = 0.95 loadable.files = [currentFile] loadable.name = '{} - as a DICOM {} object'.format( seriesDescription, self.TEMPLATE_ID) loadable.tooltip = loadable.name loadable.selected = True loadable.confidence = 0.95 loadable.uids = [uid] loadables.append(loadable) logging.debug('DICOM SR {} modality found'.format( self.TEMPLATE_ID)) 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.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. Process is to look for 'known' private tags corresponding to the types of diffusion datasets that the DWIConvert utility should be able to process. We look at the first three headers because some vendors don't write gradient information in the B0. For testing: dv = slicer.modules.dicomPlugins['DICOMDiffusionVolumePlugin']() dv.examineForImport([['/media/extra650/data/DWI-examples/SiemensTrioTimB17-DWI/63000-000025-000001.dcm']]) """ # get the series description to use as base for volume name name = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesDescription']) if name == "": name = "Unknown" validDWI = False loadables = [] # The only valid modality is MR modality = slicer.dicomDatabase.fileValue(files[0], self.tags['modality']) if (modality.upper() != "MR"): return loadables vendorName = "" matchingVendors = [] for vendor in self.diffusionTags: for tag in self.diffusionTags[vendor]: # Check the first three files because some tags may # not be present in the b0 image (e.g. for Siemens) for i in range(0, 3 if (len(files) > 2) else len(files)): value = slicer.dicomDatabase.fileValue(files[i], tag) hasTag = value != "" if hasTag and not vendor in matchingVendors: matchingVendors.append(vendor) if len(matchingVendors) > 0: validDWI = True if validDWI: # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = files loadable.name = name + ' - as DWI Volume' loadable.selected = True loadable.tooltip = "Matches DICOM tags for the following vendor(s): %s" % ( ", ".join(matchingVendors)) loadable.confidence = 0.6 loadables = [loadable] return loadables
def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ loadables = [] # just read the modality type; need to go to reporting logic, since DCMTK # is not wrapped ... for file in files: uid = slicer.dicomDatabase.fileValue(file, self.tags['instanceUID']) if uid == '': return [] desc = slicer.dicomDatabase.fileValue( file, self.tags['seriesDescription']) if desc == "": name = "Unknown" number = slicer.dicomDatabase.fileValue(file, self.tags['seriesNumber']) if number == '': number = "Unknown" reportingLogic = None reportingLogic = slicer.modules.reporting.logic() isDicomSeg = (slicer.dicomDatabase.fileValue( file, self.tags['modality']) == 'SEG') if isDicomSeg: loadable = DICOMLoadable() loadable.files = [file] loadable.name = desc + ' - as a DICOM SEG object' loadable.tooltip = loadable.name loadable.selected = True loadable.confidence = 0.95 loadable.uid = uid self.addReferences(loadable) refName = self.referencedSeriesName(loadable) if refName != "": loadable.name = refName + " " + desc + " - Segmentations" loadables.append(loadable) print('DICOM SEG modality found') return loadables
def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. Process is to look for 'known' private tags corresponding to the types of diffusion datasets that the DicomToNrrd utility should be able to process. Only need to look at one header in the series since all should be the same with respect to this check. For testing: dv = slicer.modules.dicomPlugins['DICOMDiffusionVolumePlugin']() dv.examineForImport([['/media/extra650/data/DWI-examples/SiemensTrioTimB17-DWI/63000-000025-000001.dcm']]) """ # get the series description to use as base for volume name name = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesDescription']) if name == "": name = "Unknown" validDWI = False vendorName = "" for vendor in self.diffusionTags: matchesVendor = True for tag in self.diffusionTags[vendor]: # Check the first three files because some tags may # not be present in the b0 image (e.g. for Siemens) for i in xrange(0, 3 if (len(files) > 2) else len(files)): value = slicer.dicomDatabase.fileValue(files[i], tag) hasTag = value != "" if hasTag: matchesVendor &= hasTag if matchesVendor: validDWI = True vendorName = vendor loadables = [] if validDWI: # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = files loadable.name = name + ' - as DWI Volume' loadable.selected = True loadable.tooltip = "Appears to be DWI from vendor %s" % vendorName loadable.confidence = 0.4 loadables = [loadable] return loadables
def examineFiles(self,files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. Process is to look for 'known' private tags corresponding to the types of diffusion datasets that the DicomToNrrd utility should be able to process. Only need to look at one header in the series since all should be the same with respect to this check. For testing: dv = slicer.modules.dicomPlugins['DICOMDiffusionVolumePlugin']() dv.examineForImport([['/media/extra650/data/DWI-examples/SiemensTrioTimB17-DWI/63000-000025-000001.dcm']]) """ # get the series description to use as base for volume name name = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesDescription']) if name == "": name = "Unknown" validDWI = False vendorName = "" for vendor in self.diffusionTags: matchesVendor = True for tag in self.diffusionTags[vendor]: # Check the first three files because some tags may # not be present in the b0 image (e.g. for Siemens) for i in xrange(0, 3 if (len(files) > 2) else len(files)): value = slicer.dicomDatabase.fileValue(files[i], tag) hasTag = value != "" if hasTag: matchesVendor &= hasTag if matchesVendor: validDWI = True vendorName = vendor loadables = [] if validDWI: # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = files loadable.name = name + ' - as DWI Volume' loadable.selected = True loadable.tooltip = "Appears to be DWI from vendor %s" % vendorName loadable.confidence = 0.4 loadables = [loadable] return loadables
def examineFiles(self,files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ loadables = [] # just read the modality type; need to go to reporting logic, since DCMTK # is not wrapped ... for file in files: uid = slicer.dicomDatabase.fileValue(file, self.tags['instanceUID']) if uid == '': return [] desc = slicer.dicomDatabase.fileValue(file, self.tags['seriesDescription']) if desc == "": name = "Unknown" number = slicer.dicomDatabase.fileValue(file, self.tags['seriesNumber']) if number == '': number = "Unknown" isDicomSeg = (slicer.dicomDatabase.fileValue(file, self.tags['modality']) == 'SEG') if isDicomSeg: loadable = DICOMLoadable() loadable.files = [file] loadable.name = desc + ' - as a DICOM SEG object' loadable.tooltip = loadable.name loadable.selected = True loadable.confidence = 0.95 loadable.uid = uid self.addReferences(loadable) refName = self.referencedSeriesName(loadable) if refName != "": loadable.name = refName + " " + desc + " - Segmentations" loadables.append(loadable) print('DICOM SEG modality found') return loadables
def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ loadables = [] # just read the modality type; need to go to reporting logic, since DCMTK # is not wrapped ... for cFile in files: uid = slicer.dicomDatabase.fileValue(cFile, self.tags['instanceUID']) if uid == '': return [] desc = slicer.dicomDatabase.fileValue( cFile, self.tags['seriesDescription']) if desc == '': desc = "Unknown" isDicomPM = (slicer.dicomDatabase.fileValue( cFile, self.tags['classUID']) == '1.2.840.10008.5.1.4.1.1.30') if isDicomPM: loadable = DICOMLoadable() loadable.files = [cFile] loadable.name = desc + ' - as a DICOM Parametric Map object' loadable.tooltip = loadable.name loadable.selected = True loadable.confidence = 0.95 loadable.uid = uid self.addReferences(loadable) refName = self.referencedSeriesName(loadable) if refName != "": loadable.name = refName + " " + desc + " - ParametricMap" loadables.append(loadable) logging.debug('DICOM Parametric Map found') return loadables
def examineFiles(self,files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ loadables = [] # just read the modality type; need to go to reporting logic, since DCMTK # is not wrapped ... for cFile in files: uid = slicer.dicomDatabase.fileValue(cFile, self.tags['instanceUID']) if uid == '': return [] desc = slicer.dicomDatabase.fileValue(cFile, self.tags['seriesDescription']) if desc == '': desc = "Unknown" isDicomPM = (slicer.dicomDatabase.fileValue(cFile, self.tags['classUID']) == '1.2.840.10008.5.1.4.1.1.30') if isDicomPM: loadable = DICOMLoadable() loadable.files = [cFile] loadable.name = desc + ' - as a DICOM Parametric Map object' loadable.tooltip = loadable.name loadable.selected = True loadable.confidence = 0.95 loadable.uid = uid self.addReferences(loadable) refName = self.referencedSeriesName(loadable) if refName != "": loadable.name = refName + " " + desc + " - ParametricMap" loadables.append(loadable) logging.debug('DICOM Parametric Map found') return loadables
def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ loadables = [] # just read the modality type; need to go to reporting logic, since DCMTK # is not wrapped ... for cFile in files: uid = slicer.dicomDatabase.fileValue(cFile, self.tags['instanceUID']) if uid == '': return [] desc = slicer.dicomDatabase.fileValue( cFile, self.tags['seriesDescription']) if desc == '': desc = "Unknown" isDicomSeg = (slicer.dicomDatabase.fileValue( cFile, self.tags['modality']) == 'SEG') if isDicomSeg: loadable = DICOMLoadable() loadable.files = [cFile] loadable.name = desc loadable.tooltip = loadable.name + ' - as a DICOM SEG object' loadable.selected = True loadable.confidence = 0.95 loadable.uid = uid self.addReferences(loadable) loadables.append(loadable) logging.debug('DICOM SEG modality found') return loadables
def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. Process is to look for 'known' private tags corresponding to the types of diffusion datasets that the DicomToNrrd utility should be able to process. Only need to look at one header in the series since all should be the same with respect to this check. For testing: dv = slicer.modules.dicomPlugins['DICOMTractographyPlugin']() dv.examineForImport([['/media/extra650/data/DWI-examples/SiemensTrioTimB17-DWI/63000-000025-000001.dcm']]) """ # get the series description to use as base for volume name name = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesDescription']) if name == "": name = "Unknown" validTractObject = False for tag in self.tags.keys(): value = slicer.dicomDatabase.fileValue(files[0], tag) hasTag = (value != "") validTractObject = True vendorName = slicer.dicomDatabase.fileValue(files[0], "0008,0070") loadables = [] if validTractObject: # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = files loadable.name = name + ' - as DWI Volume' loadable.selected = False loadable.tooltip = "Appears to be DWI from vendor %s" % vendorName loadable.confidence = 0.75 loadables = [loadable] return loadables
def examineFiles(self,files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. Process is to look for 'known' private tags corresponding to the types of diffusion datasets that the DicomToNrrd utility should be able to process. Only need to look at one header in the series since all should be the same with respect to this check. For testing: dv = slicer.modules.dicomPlugins['DICOMTractographyPlugin']() dv.examineForImport([['/media/extra650/data/DWI-examples/SiemensTrioTimB17-DWI/63000-000025-000001.dcm']]) """ # get the series description to use as base for volume name name = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesDescription']) if name == "": name = "Unknown" validTractObject = False for tag in self.tags.keys(): value = slicer.dicomDatabase.fileValue(files[0], tag) hasTag = (value != "") validTractObject = True vendorName = slicer.dicomDatabase.fileValue(files[0], "0008,0070") loadables = [] if validTractObject: # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = files loadable.name = name + ' - as DWI Volume' loadable.selected = False loadable.tooltip = "Appears to be DWI from vendor %s" % vendorName loadable.confidence = 0.75 loadables = [loadable] return loadables
def convertUltrasoundDicomToNrrd(self, inputDicomFilePath, outputNrrdFilePath): # Load from DICOM from DICOMLib import DICOMLoadable loadable = DICOMLoadable() loadable.files = [inputDicomFilePath] loadable.name = os.path.basename(inputDicomFilePath) loadable.tooltip = '' loadable.selected = True loadable.confidence = 1. loadable.createBrowserNode = False # don't create browser nodes (it would clutter the scene when batch processing) dicomPlugin = slicer.modules.dicomPlugins['DicomUltrasoundPlugin']() sequenceNode = dicomPlugin.load(loadable) # Write to NRRD storageNode = sequenceNode.GetStorageNode() storageNode.SetFileName(outputNrrdFilePath) success = storageNode.WriteData(sequenceNode) # Clean up slicer.mrmlScene.RemoveNode(sequenceNode) return success
def examineFiles(self,files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ loadables = [] # just read the modality type; need to go to reporting logic, since DCMTK # is not wrapped ... for cFile in files: uid = slicer.dicomDatabase.fileValue(cFile, self.tags['instanceUID']) if uid == '': return [] desc = slicer.dicomDatabase.fileValue(cFile, self.tags['seriesDescription']) if desc == '': desc = "Unknown" isDicomSeg = (slicer.dicomDatabase.fileValue(cFile, self.tags['modality']) == 'SEG') if isDicomSeg: loadable = DICOMLoadable() loadable.files = [cFile] loadable.name = desc loadable.tooltip = loadable.name + ' - as a DICOM SEG object' loadable.selected = True loadable.confidence = 0.95 loadable.uid = uid self.addReferences(loadable) loadables.append(loadable) logging.debug('DICOM SEG modality found') 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 loadable = DICOMLoadable() loadable.files = files loadable.name = seriesName loadable.tooltip = "%d files, first file: %s" % (len( loadable.files), loadable.files[0]) loadable.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", "imageOrientationPatient", "diffusionGradientOrientation", ] if self.allowLoadingByTime(): subseriesTags.append("contentTime") subseriesTags.append("triggerTime") # # first, look for subseries within this series # - build a list of files for each unique value # of each tag # subseriesFiles = {} subseriesValues = {} for file in loadable.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(loadable) # # second, for any tags that have more than one value, create a new # virtual series # for tag in subseriesTags: if len(subseriesValues[tag]) > 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 loadable.name = seriesName + " - %s %d" % (tag, valueIndex + 1) loadable.tooltip = "%d files, grouped by %s = %s. First file: %s" % ( len(loadable.files), tag, value, loadable.files[0]) loadable.selected = False loadables.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.fileValue( 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 logging.error( '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 logging.error( 'Please install SlicerRT extension to enable loading of DICOM RT Structure Set objects' ) if len(newFiles) > 0 and not excludedLoadable: loadable.files = newFiles 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 newLoadables.append(loadable) loadables = newLoadables # # now for each series and subseries, sort the images # by position and check for consistency # for loadable in loadables: loadable.files, distances, loadable.warning = DICOMUtils.getSortedImageFiles( loadable.files, self.epsilon) 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 loadable = DICOMLoadable() loadable.files = files loadable.name = seriesName loadable.tooltip = "%d files, first file: %s" % (len(loadable.files), loadable.files[0]) loadable.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", "imageOrientationPatient", "diffusionGradientOrientation", ] if self.allowLoadingByTime(): subseriesTags.append("contentTime") subseriesTags.append("triggerTime") # # first, look for subseries within this series # - build a list of files for each unique value # of each tag # subseriesFiles = {} subseriesValues = {} for file in loadable.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(loadable) # # second, for any tags that have more than one value, create a new # virtual series # for tag in subseriesTags: if len(subseriesValues[tag]) > 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 loadable.name = seriesName + " - %s %d" % (tag,valueIndex+1) loadable.tooltip = "%d files, grouped by %s = %s. First file: %s" % (len(loadable.files), tag, value, loadable.files[0]) loadable.selected = False loadables.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.fileValue(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 logging.error('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 logging.error('Please install SlicerRT extension to enable loading of DICOM RT Structure Set objects') if len(newFiles) > 0 and not excludedLoadable: loadable.files = newFiles 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 newLoadables.append(loadable) loadables = newLoadables # # now for each series and subseries, sort the images # by position and check for consistency # for loadable in loadables: loadable.files, distances, loadable.warning = DICOMUtils.getSortedImageFiles(loadable.files, self.epsilon) return loadables
def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ # get the series description to use as base for volume name name = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesDescription']) if name == "": name = "Unknown" num = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesNumber']) if num != "": name = num + ": " + name # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = files loadable.name = name loadable.tooltip = "%d files, first file: %s" % (len( loadable.files), loadable.files[0]) loadable.selected = True # add it to the list of loadables later, if pixel data is available in at least one file # while looping through files, keep track of their # position and orientation for later use positions = {} orientations = {} # make subseries volumes based on tag differences subseriesTags = [ "seriesInstanceUID", "contentTime", "triggerTime", "diffusionGradientOrientation", "imageOrientationPatient", ] # it will be set to true if pixel data is found in any of the files pixelDataAvailable = False # # first, look for subseries within this series # - build a list of files for each unique value # of each tag # subseriesFiles = {} subseriesValues = {} for file in loadable.files: # save position and orientation positions[file] = slicer.dicomDatabase.fileValue( file, self.tags['position']) if positions[file] == "": positions[file] = None orientations[file] = slicer.dicomDatabase.fileValue( file, self.tags['orientation']) if orientations[file] == "": orientations[file] = None # 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 not subseriesValues.has_key(tag): subseriesValues[tag] = [] if not subseriesValues[tag].__contains__(value): subseriesValues[tag].append(value) if not subseriesFiles.has_key((tag, value)): subseriesFiles[tag, value] = [] subseriesFiles[tag, value].append(file) loadables = [] # Pixel data is available, so add the default loadable to the output loadables.append(loadable) # # second, for any tags that have more than one value, create a new # virtual series # for tag in subseriesTags: if len(subseriesValues[tag]) > 1: for value in subseriesValues[tag]: # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = subseriesFiles[tag, value] loadable.name = name + " for %s of %s" % (tag, value) loadable.tooltip = "%d files, first file: %s" % (len( loadable.files), loadable.files[0]) loadable.selected = False loadables.append(loadable) # remove any files from loadables that don't have pixel data (no point sending them to ITK for reading) newLoadables = [] for loadable in loadables: newFiles = [] for file in loadable.files: if slicer.dicomDatabase.fileValue( file, self.tags['pixelData']) != '': newFiles.append(file) if len(newFiles) > 0: loadable.files = newFiles newLoadables.append(loadable) 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 newLoadables.append(loadable) loadables = newLoadables # # now for each series and subseries, sort the images # by position and check for consistency # # TODO: more consistency checks: # - is there gantry tilt? # - are the orientations the same for all slices? for loadable in loadables: # # use the first file to get the ImageOrientationPatient for the # series and calculate the scan direction (assumed to be perpendicular # to the acquisition plane) # value = slicer.dicomDatabase.fileValue(loadable.files[0], self.tags['numberOfFrames']) if value != "": loadable.warning += "Multi-frame image. If slice orientation or spacing is non-uniform then the image may be displayed incorrectly. Use with caution. " validGeometry = True ref = {} for tag in [self.tags['position'], self.tags['orientation']]: value = slicer.dicomDatabase.fileValue(loadable.files[0], tag) if not value or value == "": loadable.warning += "Reference image in series does not contain geometry information. Please use caution. " validGeometry = False loadable.confidence = 0.2 break ref[tag] = value if not validGeometry: continue # get the geometry of the scan # with respect to an arbitrary slice sliceAxes = [ float(zz) for zz in ref[self.tags['orientation']].split('\\') ] x = sliceAxes[:3] y = sliceAxes[3:] scanAxis = self.cross(x, y) scanOrigin = [ float(zz) for zz in ref[self.tags['position']].split('\\') ] # # for each file in series, calculate the distance along # the scan axis, sort files by this # sortList = [] missingGeometry = False for file in loadable.files: if not positions[file]: missingGeometry = True break position = [float(zz) for zz in positions[file].split('\\')] vec = self.difference(position, scanOrigin) dist = self.dot(vec, scanAxis) sortList.append((file, dist)) if missingGeometry: loadable.warning += "One or more images is missing geometry information. " else: sortedFiles = sorted(sortList, key=lambda x: x[1]) distances = {} loadable.files = [] for file, dist in sortedFiles: loadable.files.append(file) distances[file] = dist # # confirm equal spacing between slices # - use variable 'epsilon' to determine the tolerance # spaceWarnings = 0 if len(loadable.files) > 1: file0 = loadable.files[0] file1 = loadable.files[1] dist0 = distances[file0] dist1 = distances[file1] spacing0 = dist1 - dist0 n = 1 for fileN in loadable.files[1:]: fileNminus1 = loadable.files[n - 1] distN = distances[fileN] distNminus1 = distances[fileNminus1] spacingN = distN - distNminus1 spaceError = spacingN - spacing0 if abs(spaceError) > self.epsilon: spaceWarnings += 1 loadable.warning += "Images are not equally spaced (a difference of %g in spacings was detected). Slicer will load this series as if it had a spacing of %g. Please use caution. " % ( spaceError, spacing0) break n += 1 if spaceWarnings != 0: logging.warning( "Geometric issues were found with %d of the series. Please use caution." % spaceWarnings) 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. """ detailedLogging = self.isDetailedLogging() supportedSOPClassUIDs = [ '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage ] loadables = [] 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 manufacturerModelName = slicer.dicomDatabase.fileValue( filePath, self.tags['manufacturerModelName']) if manufacturerModelName != "Invenia": if detailedLogging: logging.debug( "ManufacturerModelName is not Invenia, the series will not be considered as an ABUS image" ) 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 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))) continue # check if probeCurvatureRadius is available probeCurvatureRadiusFound = False for privateCreator in self.privateCreators: if self.findPrivateTag(ds, 0x0021, 0x40, privateCreator): probeCurvatureRadiusFound = True break if not probeCurvatureRadiusFound: if detailedLogging: logging.debug( "Probe curvature radius is not found, the series will not be considered as an ABUS image" ) continue 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.tooltip = "GE Invenia ABUS" loadable.warning = "Loading of this image type is experimental. Please verify image size and orientation and report any problem is found." loadable.selected = True loadable.confidence = 0.9 # this has to be higher than 0.7 (ultrasound sequence) # 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. """ 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
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 loadable = DICOMLoadable() loadable.files = files loadable.name = seriesName loadable.tooltip = "%d files, first file: %s" % (len(loadable.files), loadable.files[0]) loadable.selected = True # add it to the list of loadables later, if pixel data is available in at least one file # while looping through files, keep track of their # position and orientation for later use positions = {} orientations = {} # make subseries volumes based on tag differences subseriesTags = [ "seriesInstanceUID", "contentTime", "triggerTime", "diffusionGradientOrientation", "imageOrientationPatient", ] # # first, look for subseries within this series # - build a list of files for each unique value # of each tag # subseriesFiles = {} subseriesValues = {} for file in loadable.files: # save position and orientation positions[file] = slicer.dicomDatabase.fileValue(file,self.tags['position']) if positions[file] == "": positions[file] = None orientations[file] = slicer.dicomDatabase.fileValue(file,self.tags['orientation']) if orientations[file] == "": orientations[file] = None # 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 not subseriesValues.has_key(tag): subseriesValues[tag] = [] if not subseriesValues[tag].__contains__(value): subseriesValues[tag].append(value) if not subseriesFiles.has_key((tag,value)): subseriesFiles[tag,value] = [] subseriesFiles[tag,value].append(file) loadables = [] # Pixel data is available, so add the default loadable to the output loadables.append(loadable) # # second, for any tags that have more than one value, create a new # virtual series # for tag in subseriesTags: if len(subseriesValues[tag]) > 1: for value in subseriesValues[tag]: # default loadable includes all files for series loadable = DICOMLoadable() loadable.files = subseriesFiles[tag,value] loadable.name = seriesName + " for %s of %s" % (tag,value) loadable.tooltip = "%d files, first file: %s" % (len(loadable.files), loadable.files[0]) loadable.selected = False loadables.append(loadable) # remove any files from loadables that don't have pixel data (no point sending them to ITK for reading) newLoadables = [] for loadable in loadables: newFiles = [] for file in loadable.files: if slicer.dicomDatabase.fileValue(file,self.tags['pixelData'])!='': newFiles.append(file) if len(newFiles) > 0: loadable.files = newFiles newLoadables.append(loadable) 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 newLoadables.append(loadable) loadables = newLoadables # # now for each series and subseries, sort the images # by position and check for consistency # # TODO: more consistency checks: # - is there gantry tilt? # - are the orientations the same for all slices? for loadable in loadables: # # use the first file to get the ImageOrientationPatient for the # series and calculate the scan direction (assumed to be perpendicular # to the acquisition plane) # value = slicer.dicomDatabase.fileValue(loadable.files[0], self.tags['numberOfFrames']) if value != "": loadable.warning += "Multi-frame image. If slice orientation or spacing is non-uniform then the image may be displayed incorrectly. Use with caution. " validGeometry = True ref = {} for tag in [self.tags['position'], self.tags['orientation']]: value = slicer.dicomDatabase.fileValue(loadable.files[0], tag) if not value or value == "": loadable.warning += "Reference image in series does not contain geometry information. Please use caution. " validGeometry = False loadable.confidence = 0.2 break ref[tag] = value if not validGeometry: continue # get the geometry of the scan # with respect to an arbitrary slice sliceAxes = [float(zz) for zz in ref[self.tags['orientation']].split('\\')] x = sliceAxes[:3] y = sliceAxes[3:] scanAxis = self.cross(x,y) scanOrigin = [float(zz) for zz in ref[self.tags['position']].split('\\')] # # for each file in series, calculate the distance along # the scan axis, sort files by this # sortList = [] missingGeometry = False for file in loadable.files: if not positions[file]: missingGeometry = True break position = [float(zz) for zz in positions[file].split('\\')] vec = self.difference(position, scanOrigin) dist = self.dot(vec, scanAxis) sortList.append((file, dist)) if missingGeometry: loadable.warning += "One or more images is missing geometry information. " else: sortedFiles = sorted(sortList, key=lambda x: x[1]) distances = {} loadable.files = [] for file,dist in sortedFiles: loadable.files.append(file) distances[file] = dist # # confirm equal spacing between slices # - use variable 'epsilon' to determine the tolerance # spaceWarnings = 0 if len(loadable.files) > 1: file0 = loadable.files[0] file1 = loadable.files[1] dist0 = distances[file0] dist1 = distances[file1] spacing0 = dist1 - dist0 n = 1 for fileN in loadable.files[1:]: fileNminus1 = loadable.files[n-1] distN = distances[fileN] distNminus1 = distances[fileNminus1] spacingN = distN - distNminus1 spaceError = spacingN - spacing0 if abs(spaceError) > self.epsilon: spaceWarnings += 1 loadable.warning += "Images are not equally spaced (a difference of %g vs %g in spacings was detected). Slicer will try to load this series with a transform to compensate. Please use caution. " % (spaceError, spacing0) break n += 1 if spaceWarnings != 0: logging.warning("Geometric issues were found with %d of the series. Please use caution." % spaceWarnings) return loadables
def examinePhilips4DUS(self, filePath): # currently only this one (bogus, non-standard) Philips 4D US format is supported supportedSOPClassUID = '1.2.840.113543.6.6.1.3.10002' # Quick check of SOP class UID without parsing the file... try: sopClassUID = slicer.dicomDatabase.fileValue( filePath, self.tags['sopClassUID']) if sopClassUID != supportedSOPClassUID: # 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(e.msg)) return [] if ds.SOPClassUID != supportedSOPClassUID: # Unsupported class return [] confidence = 0.9 if ds.PhotometricInterpretation != 'MONOCHROME2': logging.warning('Warning: unsupported PhotometricInterpretation') confidence = .4 if ds.BitsAllocated != 8 or ds.BitsStored != 8 or ds.HighBit != 7: logging.warning('Warning: Bad scalar type (not unsigned byte)') confidence = .4 if ds.PhysicalUnitsXDirection != 3 or ds.PhysicalUnitsYDirection != 3: logging.warning('Warning: Units not in centimeters') confidence = .4 if ds.SamplesPerPixel != 1: logging.warning('Warning: multiple samples per pixel') confidence = .4 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.tooltip = "Philips 4D Ultrasound" loadable.selected = True loadable.confidence = confidence return [loadable]
def examineGeUSMovie(self, filePath): supportedSOPClassUIDs = [ '1.2.840.10008.5.1.4.1.1.6.1' # UltrasoundImageStorage ] # 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, defer_size=30) # use defer_size to not load large fields except Exception as e: logging.debug("Failed to parse DICOM file: {0}".format(e.msg)) return [] if not ds.SOPClassUID in supportedSOPClassUIDs: # Unsupported class return [] geUsMovieGroupRootTag = dicom.tag.Tag('0x7fe1', '0x0010') geUsMovieGroupRootItem = ds.get(geUsMovieGroupRootTag) if not geUsMovieGroupRootItem: return [] if geUsMovieGroupRootItem.name != 'Private Creator': return [] if geUsMovieGroupRootItem.value != 'GEMS_Ultrasound_MovieGroup_001': return [] confidence = 0.8 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.tooltip = "GE ultrasound image sequence" loadable.warning = "Importing of this file format is experimental: images may be distorted, size measurements may be inaccurate." loadable.selected = True loadable.confidence = confidence return [loadable]
def examineGeKretzUS(self, filePath): # E Kretz uses 'Ultrasound Image Storage' SOP class UID supportedSOPClassUID = '1.2.840.10008.5.1.4.1.1.6.1' # Quick check of SOP class UID without parsing the file... try: sopClassUID = slicer.dicomDatabase.fileValue( filePath, self.tags['sopClassUID']) if sopClassUID != supportedSOPClassUID: # 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, defer_size=50) # use defer_size to not load large fields except Exception as e: logging.debug("Failed to parse DICOM file: {0}".format(e.msg)) return [] if ds.SOPClassUID != supportedSOPClassUID: # Unsupported class return [] # Check if these expected DICOM tags are available: # (7fe1,0011) LO [KRETZ_US] # 8, 1 PrivateCreator # (7fe1,1101) OB 4b\52\45\54\5a\46\49\4c\45\20\31\2e\30\20\20\20\00\00\01\00\02\00... # 3471038, 1 Unknown Tag & Data kretzUsTag = dicom.tag.Tag('0x7fe1', '0x11') kretzUsDataTag = dicom.tag.Tag('0x7fe1', '0x1101') if kretzUsTag not in ds.keys(): return [] if kretzUsDataTag not in ds.keys(): return [] confidence = 0.9 # These manufacturers values have been found in successfully loadable files: # - Kretztechnik # - GE Healthcare # - GE Healthcare Austria GmbH & Co OG if (ds.Manufacturer != 'Kretztechnik') and ( ds.Manufacturer.find('GE Healthcare') < 0): logging.warning('Warning: unknown manufacturer: ' + ds.Manufacturer) confidence = .4 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) name = name.strip() # remove leading and trailing spaces, if any loadable = DICOMLoadable() loadable.files = [filePath] loadable.name = name loadable.tooltip = "GE Kretz 3D Ultrasound" loadable.warning = "Importing of this file format is experimental: images may be distorted, size measurements may be inaccurate." loadable.selected = True loadable.confidence = confidence loadableHighRes1 = DICOMLoadable() loadableHighRes1.files = loadable.files loadableHighRes1.name = loadable.name + " (LR)" loadableHighRes1.tooltip = loadable.tooltip + " (low-resolution)" loadableHighRes1.warning = loadable.warning loadableHighRes1.selected = False loadableHighRes1.confidence = confidence loadableHighRes2 = DICOMLoadable() loadableHighRes2.files = loadable.files loadableHighRes2.name = loadable.name + " (HR)" loadableHighRes2.tooltip = loadable.tooltip + " (high-resolution)" loadableHighRes2.warning = loadable.warning loadableHighRes2.selected = False loadableHighRes2.confidence = confidence return [loadable, loadableHighRes1, loadableHighRes2]
def examinePhilipsAffinity3DUS(self, filePath): supportedSOPClassUID = '1.2.840.10008.5.1.4.1.1.3.1' # UltrasoundMultiframeImageStorage # Quick check of SOP class UID without parsing the file... try: sopClassUID = slicer.dicomDatabase.fileValue( filePath, self.tags['sopClassUID']) if sopClassUID != supportedSOPClassUID: # Unsupported class logging.error("sopClassUID " + sopClassUID + " != supportedSOPClassUID " + supportedSOPClassUID) 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(e.msg)) return [] if ds.SOPClassUID != supportedSOPClassUID: # Unsupported class logging.error("sopClassUID " + ds.SOPClassUID + " != supportedSOPClassUID " + supportedSOPClassUID) return [] voxelSpacingTag = findPrivateTag(ds, 0x200d, 0x03, "Philips US Imaging DD 036") if not voxelSpacingTag: # this is most likely not a PhilipsAffinity image return [] if voxelSpacingTag not in ds.keys(): return [] confidence = 0.9 if ds.PhotometricInterpretation != 'MONOCHROME2': logging.warning('Warning: unsupported PhotometricInterpretation') confidence = .4 if ds.BitsAllocated != 8 or ds.BitsStored != 8 or ds.HighBit != 7: logging.warning('Warning: Bad scalar type (not unsigned byte)') confidence = .4 if ds.SamplesPerPixel != 1: logging.warning('Warning: multiple samples per pixel') confidence = .4 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.tooltip = "Philips Affinity 3D ultrasound" loadable.selected = True loadable.confidence = confidence return [loadable]
def examineEigenArtemis3DUS(self, filePath): supportedSOPClassUID = '1.2.840.10008.5.1.4.1.1.3.1' # UltrasoundMultiframeImageStorage # Quick check of SOP class UID without parsing the file... try: sopClassUID = slicer.dicomDatabase.fileValue( filePath, self.tags['sopClassUID']) if sopClassUID != supportedSOPClassUID: # Unsupported class #logging.debug("Not EigenArtemis3DUS: sopClassUID "+sopClassUID+" != supportedSOPClassUID "+supportedSOPClassUID) 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 ds.SOPClassUID != supportedSOPClassUID: # Unsupported class logging.debug("Not EigenArtemis3DUS: sopClassUID " + ds.SOPClassUID + " != supportedSOPClassUID " + supportedSOPClassUID) return [] if not (ds.Manufacturer == "Eigen" and ds.ManufacturerModelName == "Artemis"): return [] confidence = 0.9 if ds.PhotometricInterpretation != 'MONOCHROME2': logging.warning('Warning: unsupported PhotometricInterpretation') confidence = .4 if ds.BitsAllocated != 8 or ds.BitsStored != 8 or ds.HighBit != 7: logging.warning('Warning: Bad scalar type (not unsigned byte)') confidence = .4 if ds.SamplesPerPixel != 1: logging.warning('Warning: multiple samples per pixel') confidence = .4 """Use manufacturer-specific conventions, per communication from Rajesh Venkateraman (email to Andrey Fedorov et al on Feb 10, 2020): > please take the PixelAspectRatio tag divide by 1000 and that would be your isotropic resolution for display in all 3 dimensions. > > Image Patient Orientation would be [1 0 0; 0 1 0] for each frame > > The origin of the volume is the center of the 3D cube and the span would be > > For X: [-0.5*Rows*PixelAspectRatio[0]/1000, -0.5*Rows*PixelAspectRatio[0]/1000 ] > > For Y: [-0.5*Columns*PixelAspectRatio[0]/1000, -0.5*Columns*PixelAspectRatio[0]/1000 ] > > For Z: [-0.5*NumberOfSlices*PixelAspectRatio[0]/1000, -0.5* NumberOfSlices *PixelAspectRatio[0]/1000 ] Also, cross-check with the private attributes, if those are available. """ pixelSpacingPrivate = None pixelSpacingPublic = None try: pixelSpacingPrivateTag = findPrivateTag(ds, 0x1129, 0x16, "Eigen, Inc") if pixelSpacingPrivateTag is None: pixelSpacingPrivateTag = findPrivateTag( ds, 0x1129, 0x16, "Eigen Artemis") if pixelSpacingPrivateTag is not None: pixelSpacingPrivate = float(ds[pixelSpacingPrivateTag].value) except KeyError: logging.warning( "examineEigenArtemis3DUS: spacing not available in private tag" ) if hasattr(ds, "PixelAspectRatio"): if ds.PixelAspectRatio[0] != ds.PixelAspectRatio[1]: logging.warning( "examineEigenArtemis3DUS: PixelAspectRatio items are not equal!" ) pixelSpacingPublic = float(ds.PixelAspectRatio[0]) / 1000. if pixelSpacingPrivate is not None and pixelSpacingPrivate != pixelSpacingPublic: logging.warning( "examineEigenArtemis3DUS: private tag based spacing does not match computed spacing" ) else: if pixelSpacingPrivate is None: logging.debug( "examineEigenArtemis3DUS: unable to find spacing information" ) return [] # prefer private, if available outputSpacing = pixelSpacingPrivate if pixelSpacingPrivate is not None else pixelSpacingPublic outputOrigin = [ 0.5 * float(ds.Rows) * outputSpacing, 0.5 * float(ds.Columns) * outputSpacing, -0.5 * float(ds.NumberOfSlices) * outputSpacing ] logging.debug("examineEigenArtemis3DUS: assumed pixel spacing: %s" % str(outputSpacing)) 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) else: name = name + " Eigen Artemis 3D US" 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.tooltip = "Eigen Artemis 3D ultrasound" loadable.selected = True loadable.confidence = confidence loadable.spacing = outputSpacing loadable.origin = outputOrigin return [loadable]
def examineFiles(self, files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ loadables = [] import tempfile from subprocess import Popen, PIPE, CalledProcessError with tempfile.TemporaryDirectory( dir=slicer.app.temporaryPath) as tempDir: # write DICOM file list to text file inputDicomFileListFilename = os.path.join(tempDir, "input-dicom-files.txt") with open(inputDicomFileListFilename, 'w') as fp: for filename in files: fp.write("{0}\n".format(filename)) # examine using dcm2niix cmdPath = os.path.join( os.path.dirname(slicer.modules.dcm2niixgui.path), "Resources", "bin", "dcm2niix") command_line = [ cmdPath, "-n", "-1", # examine only, do not write output files "-s", "y", # input is a single filelist "-f", "%p_%t_%s", # output file name (difference compared to the default is that folder name is removed) "-i", "y", # ignore derived, localizer and 2D images "-o", str(tempDir), # set output folder str(inputDicomFileListFilename) ] logging.debug(repr(command_line)) try: # launch dcm2niix proc = slicer.util.launchConsoleProcess(command_line) # process output loadableInitialized = False for line in proc.stdout: line = line.rstrip() if not loadableInitialized: # Default scalar volume reader has confidence of 0.5, so this value will not # make dcm2niix used by default but the user has to select it confidence = 0.45 warningMessages = [] infoMessages = [] loadableInitialized = True logging.debug(line) #slicer.app.processEvents() # give a chance the application to refresh GUI if (line.startswith("Compression will be faster with pigz") or line.startswith("Chris Rorden's dcm2niiX") or line.startswith("Conversion required")): # general information, ignore pass elif (line.startswith(" ") or line.startswith("dx=") or line.startswith("instance=")): # debug message, ignore pass elif (line.startswith("Warning:") or line.startswith("Unsupported transfer syntax") or line.startswith("Unable to determine")): if line not in warningMessages: warningMessages.append(line) # Reduce confidence if there was a warning. # This value is higher than scalar volume's confidence of 0.2 when no pixel data is found. confidence = 0.3 elif line.startswith("\t"): # found a loadable file [dummy, seriesNumber, filepath] = line.split('\t') loadable = DICOMLoadable() loadable.files = files seriesUID = slicer.dicomDatabase.fileValue( files[0], self.tags['seriesUID']) seriesName = self.defaultSeriesNodeName(seriesUID) loadable.name = seriesName loadable.warning = ('\n').join(warningMessages) dcm2niixGeneratedName = os.path.basename(filepath) loadable.tooltip = ( '\n').join([dcm2niixGeneratedName] + infoMessages) loadable.selected = True loadable.confidence = confidence loadable.seriesNumber = seriesNumber loadables.append(loadable) loadableInitialized = False else: infoMessages.append(line) proc.wait() retcode = proc.returncode if retcode != 0: raise CalledProcessError(retcode, proc.args, output=proc.stdout, stderr=proc.stderr) except CalledProcessError as e: logging.debug( "Failed to examine files using dcm2niix: {0}".format( e.message)) 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 examineGeImage3dApi(self, filePath): if not hasattr(slicer.modules, 'ultrasoundimage3dreader'): return [] supportedSOPClassUIDs = [ '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multi-frame Image IOD '1.2.840.10008.5.1.4.1.1.6.1', # Ultrasound Image IOD ] # 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, defer_size=30) # use defer_size to not load large fields 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 [] try: # Check if '3D' data type is present (for example "Trace+3D") # # (7fe1,0010) LO [GEMS_Ultrasound_MovieGroup_001] # 30, 1 PrivateCreator # (7fe1,1001) SQ (Sequence with explicit length #=1) # 76006288, 1 Unknown Tag & Data # (fffe,e000) na (Item with explicit length #=6) # 76006280, 1 Item # (7fe1,0010) LO [GEMS_Ultrasound_MovieGroup_001] # 30, 1 PrivateCreator # (7fe1,1002) LO [Trace+3D] # 8, 1 Unknown Tag & Data # Get private tags movieGroupTag = findPrivateTag(ds, 0x7fe1, 0x01, 'GEMS_Ultrasound_MovieGroup_001') imageTypeTag = findPrivateTag(ds, 0x7fe1, 0x02, 'GEMS_Ultrasound_MovieGroup_001') contains3D = False movieGroup = ds[movieGroupTag].value for movieGroupItem in movieGroup: if imageTypeTag in movieGroupItem: if '3D' in movieGroupItem[imageTypeTag].value: contains3D = True break if not contains3D: # Probably 2D file return [] except: # Not a GE MovieGroup file return [] # It looks like a GE 3D ultrasound file import UltrasoundImage3dReader reader = UltrasoundImage3dReader.UltrasoundImage3dReaderFileReader( None) try: reader.getLoader(filePath) except Exception as e: logging.debug("3D ultrasound loader not found error: {0}".format( str(e))) logging.info( "File {0} looks like a GE 3D ultrasound file. Installing Image3dAPI reader may make the file loadable (https://github.com/MedicalUltrasound/Image3dAPI)." ) return [] # GE generic moviegroup reader has confidence=0.8 # this one is much better than that, so use much higher value confidence = 0.90 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.tooltip = "GE 3D ultrasound image sequence (using Image3dAPI)" loadable.selected = True loadable.confidence = confidence return [loadable]
def examineFiles(self,files): """ Returns a list of DICOMLoadable instances corresponding to ways of interpreting the files parameter. """ loadables = [] if len(files) > 1: # there should only be one instance per 4D volume return [] filePath = files[0] # currently only this one (bogus, non-standard) Philips 4D US format is supported supportedSOPClassUID = '1.2.840.113543.6.6.1.3.10002' # Quick check of SOP class UID without parsing the file... try: sopClassUID = slicer.dicomDatabase.fileValue(filePath,self.tags['sopClassUID']) if sopClassUID != supportedSOPClassUID: # 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(e.message)) return [] if ds.SOPClassUID != supportedSOPClassUID: # Unsupported class return [] if ds.PhotometricInterpretation != 'MONOCHROME2': logging.warning('Warning: unsupported PhotometricInterpretation') loadable.confidence = .4 if ds.BitsAllocated != 8 or ds.BitsStored != 8 or ds.HighBit != 7: logging.warning('Warning: Bad scalar type (not unsigned byte)') loadable.confidence = .4 if ds.PhysicalUnitsXDirection != 3 or ds.PhysicalUnitsYDirection != 3: logging.warning('Warning: Units not in centimeters') loadable.confidence = .4 if ds.SamplesPerPixel != 1: logging.warning('Warning: multiple samples per pixel') loadable.confidence = .4 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 = files loadable.name = name.strip() # remove leading and trailing spaces, if any loadable.tooltip = "Philips 4D Ultrasound" loadable.selected = True loadable.confidence = 1. loadables.append(loadable) return loadables