예제 #1
0
    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
예제 #2
0
    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
예제 #3
0
    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
예제 #4
0
  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