diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a7851d..d8c978f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ set(EXTENSION_DESCRIPTION "The purpose of the Reporting module is to provide Sli set(EXTENSION_ICONURL "http://wiki.slicer.org/slicerWiki/images/3/31/ReportingLogo.png") set(EXTENSION_SCREENSHOTURLS "http://wiki.slicer.org/slicerWiki/images/d/d6/Reporting-Prostate.png") set(EXTENSION_STATUS "Work in progress") +set(EXTENSION_DEPENDS "SlicerProstate") #----------------------------------------------------------------------------- find_package(Slicer REQUIRED) diff --git a/Py/CMakeLists.txt b/Py/CMakeLists.txt index 5680a92..7483fe0 100644 --- a/Py/CMakeLists.txt +++ b/Py/CMakeLists.txt @@ -12,3 +12,8 @@ slicerMacroBuildScriptedModule( NAME SEGExporterSelfTest SCRIPTS SEGExporterSelfTest.py ) + +slicerMacroBuildScriptedModule( + NAME Reporting + SCRIPTS Reporting.py + ) \ No newline at end of file diff --git a/Py/DICOMSegmentationPlugin.py b/Py/DICOMSegmentationPlugin.py index 03c0b8c..10b3f9d 100644 --- a/Py/DICOMSegmentationPlugin.py +++ b/Py/DICOMSegmentationPlugin.py @@ -41,25 +41,25 @@ def examineFiles(self,files): # just read the modality type; need to go to reporting logic, since DCMTK # is not wrapped ... - for file in files: + for cFile in files: - uid = slicer.dicomDatabase.fileValue(file, self.tags['instanceUID']) + uid = slicer.dicomDatabase.fileValue(cFile, self.tags['instanceUID']) if uid == '': return [] - desc = slicer.dicomDatabase.fileValue(file, self.tags['seriesDescription']) - if desc == "": - name = "Unknown" + desc = slicer.dicomDatabase.fileValue(cFile, self.tags['seriesDescription']) + if desc == '': + desc = "Unknown" - number = slicer.dicomDatabase.fileValue(file, self.tags['seriesNumber']) + number = slicer.dicomDatabase.fileValue(cFile, self.tags['seriesNumber']) if number == '': number = "Unknown" - isDicomSeg = (slicer.dicomDatabase.fileValue(file, self.tags['modality']) == 'SEG') + isDicomSeg = (slicer.dicomDatabase.fileValue(cFile, self.tags['modality']) == 'SEG') if isDicomSeg: loadable = DICOMLoadable() - loadable.files = [file] + loadable.files = [cFile] loadable.name = desc + ' - as a DICOM SEG object' loadable.tooltip = loadable.name loadable.selected = True @@ -84,7 +84,7 @@ def referencedSeriesName(self,loadable): return referencedName def addReferences(self,loadable): - """Puts a list of the referened UID into the loadable for use + """Puts a list of the referenced UID into the loadable for use in the node if this is loaded.""" import dicom dcm = dicom.read_file(loadable.files[0]) @@ -96,7 +96,7 @@ def addReferences(self,loadable): loadable.referencedInstanceUIDs = [] for f in slicer.dicomDatabase.filesForSeries(dcm.ReferencedSeriesSequence[0].SeriesInstanceUID): refDCM = dicom.read_file(f) - # this is a hack that should probablybe fixed in Slicer core - not all + # this is a hack that should probably fixed in Slicer core - not all # of those instances are truly referenced! loadable.referencedInstanceUIDs.append(refDCM.SOPInstanceUID) loadable.referencedSeriesUID = dcm.ReferencedSeriesSequence[0].SeriesInstanceUID @@ -105,22 +105,17 @@ def load(self,loadable): """ Load the DICOM SEG object """ print('DICOM SEG load()') - labelNodes = vtk.vtkCollection() - - uid = None - try: uid = loadable.uid print ('in load(): uid = ', uid) except AttributeError: return False - res = False # make the output directory outputDir = os.path.join(slicer.app.temporaryPath,"QIICR","SEG",loadable.uid) try: os.makedirs(outputDir) - except: + except OSError: pass # produces output label map files, one per segment, and information files with @@ -134,7 +129,6 @@ def load(self,loadable): "inputSEGFileName": segFileName, "outputDirName": outputDir, } - seg2nrrd = None try: seg2nrrd = slicer.modules.seg2nrrd except AttributeError: @@ -152,7 +146,7 @@ def load(self,loadable): colorLogic = slicer.modules.colors.logic() segmentationColorNode = slicer.vtkMRMLColorTableNode() segmentationColorNode.SetName(loadable.name) - segmentationColorNode.SetTypeToUser(); + segmentationColorNode.SetTypeToUser() segmentationColorNode.SetHideFromEditors(0) segmentationColorNode.SetAttribute("Category", "File") segmentationColorNode.NamesInitialisedOff() @@ -171,6 +165,7 @@ def load(self,loadable): seriesName = self.referencedSeriesName(loadable) segmentNodes = [] + labelNode = None for segmentId in range(numberOfSegments): # load each of the segments' segmentations # Initialize color and terminology from .info file @@ -266,18 +261,27 @@ def load(self,loadable): # point the label node to the color node we're creating labelDisplayNode = labelNode.GetDisplayNode() - if labelDisplayNode == None: + if labelDisplayNode is None: print ('Warning: no label map display node for segment ',segmentId,', creating!') labelNode.CreateDefaultDisplayNodes() labelDisplayNode = labelNode.GetDisplayNode() labelDisplayNode.SetAndObserveColorNodeID(segmentationColorNode.GetID()) # TODO: initialize referenced UID (and segment number?) attribute(s) + # dataset = dicom.read_file(segFileName) + # referencedSeries = dict() + # for refSeriesItem in dataset.ReferencedSeriesSequence: + # refSOPInstanceUIDs = [] + # for refSOPInstanceItem in refSeriesItem.ReferencedInstanceSequence: + # refSOPInstanceUIDs.append(refSOPInstanceItem.ReferencedSOPInstanceUID) + # referencedSeries[refSeriesItem.SeriesInstanceUID] = refSOPInstanceUIDs + # segmentationNode.SetAttribute("DICOM.referencedInstanceUIDs", str(referencedSeries)) # create Subject hierarchy nodes for the loaded series self.addSeriesInSubjectHierarchy(loadable, labelNode) # create a combined (merge) label volume node (only if a segment was created) + mergeNode = None if labelNode: volumeLogic = slicer.modules.volumes.logic() mergeNode = volumeLogic.CloneVolume(labelNode, seriesName + "-label") @@ -364,7 +368,7 @@ def examineForExport(self, node): exportable.confidence = 1.0 exportable.setTag('Modality', 'SEG') - if exportable != None: + if exportable is not None: exportable.name = self.loadType exportable.tooltip = "Create DICOM files from segmentation" exportable.nodeID = node.GetID() @@ -403,7 +407,7 @@ def exportAsDICOMSEG(self, exportablesCollection): instanceUIDs = subjectHierarchyNode.GetAttribute("DICOM.ReferencedInstanceUIDs").split() if instanceUIDs == "": - raise Exception("Editor master node does not have DICOM information") + raise Exception("Editor master node does not have DICOM information") # get the list of source DICOM files inputDICOMImageFileNames = "" @@ -423,7 +427,6 @@ def exportAsDICOMSEG(self, exportablesCollection): import vtkSlicerSegmentationsModuleLogic logic = vtkSlicerSegmentationsModuleLogic.vtkSlicerSegmentationsModuleLogic() - segmentationTransform = vtk.vtkMatrix4x4() segmentationNode = subjectHierarchyNode.GetAssociatedNode() mergedSegmentationImageData = segmentationNode.GetImageData() @@ -489,7 +492,7 @@ def exportAsDICOMSEG(self, exportablesCollection): colorNode.GetColor(labelIndex, rgbColor) rgbColor = map(lambda e: e*255., rgbColor) - # get the attributes and conver to format CodeValue,CodeMeaning,CodingSchemeDesignator + # get the attributes and convert to format CodeValue,CodeMeaning,CodingSchemeDesignator # or empty strings if not defined propertyCategoryWithColons = colorLogic.GetSegmentedPropertyCategory(labelIndex, terminologyName) if propertyCategoryWithColons == '': @@ -525,7 +528,7 @@ def exportAsDICOMSEG(self, exportablesCollection): if anatomicRegion != "": attributes += ";AnatomicRegion:" + anatomicRegion if anatomicRegionModifier != "": - attributes += ";AnatomicRegionModifer:" + anatomicRegionModifier + attributes += ";AnatomicRegionModifier:" + anatomicRegionModifier attributes += ";SegmentAlgorithmType:AUTOMATIC" attributes += ";SegmentAlgorithmName:SlicerSelfTest" attributes += ";RecommendedDisplayRGBValue:%g,%g,%g" % tuple(rgbColor[:-1]) diff --git a/Py/DICOMTID1500Plugin.py b/Py/DICOMTID1500Plugin.py new file mode 100755 index 0000000..48e02c6 --- /dev/null +++ b/Py/DICOMTID1500Plugin.py @@ -0,0 +1,216 @@ +import os, json +import slicer +import dicom +from DICOMLib import DICOMPlugin +from DICOMLib import DICOMLoadable + + +class DICOMTID1500PluginClass(DICOMPlugin): + + UID_EnhancedSRStorage = "1.2.840.10008.5.1.4.1.1.88.22" + UID_SegmentationStorage = "1.2.840.10008.5.1.4.1.1.66.4" + + def __init__(self, epsilon=0.01): + super(DICOMTID1500PluginClass, self).__init__() + self.loadType = "DICOM Structured Report TID1500" + + def examine(self, fileLists): + loadables = [] + for files in fileLists: + loadables += self.examineFiles(files) + return loadables + + def getDICOMValue(self, dataset, tagName, default=""): + try: + value = getattr(dataset, tagName) + except AttributeError: + value = default + return value + + def examineFiles(self, files): + loadables = [] + + for file in files: + dataset = dicom.read_file(file) + + uid = self.getDICOMValue(dataset, "SOPInstanceUID", default=None) + if not uid: + return [] + + seriesDescription = self.getDICOMValue(dataset, "SeriesDescription", "Unknown") + + try: + isDicomTID1500 = self.getDICOMValue(dataset, "Modality") == 'SR' and \ + self.getDICOMValue(dataset, "SOPClassUID") == self.UID_EnhancedSRStorage and \ + self.getDICOMValue(dataset, "ContentTemplateSequence")[0].TemplateIdentifier == '1500' + except (AttributeError, IndexError): + isDicomTID1500 = False + + if isDicomTID1500: + loadable = self.createLoadableAndAddReferences(dataset) + loadable.files = [file] + loadable.name = seriesDescription + ' - as a DICOM SR TID1500 object' + loadable.tooltip = loadable.name + loadable.selected = True + loadable.confidence = 0.95 + loadable.uid = uid + refName = self.referencedSeriesName(loadable) + if refName != "": + loadable.name = refName + " " + seriesDescription + " - SR TID1500" + + loadables.append(loadable) + + print('DICOM SR TID1500 modality found') + return loadables + + def referencedSeriesName(self, loadable): + """Returns the default series name for the given loadable""" + referencedName = "Unnamed Reference" + if hasattr(loadable, "referencedSOPInstanceUID"): + referencedName = self.defaultSeriesNodeName(loadable.referencedSOPInstanceUID) + return referencedName + + def createLoadableAndAddReferences(self, dataset): + loadable = DICOMLoadable() + loadable.selected = True + loadable.confidence = 0.95 + + if hasattr(dataset, "CurrentRequestedProcedureEvidenceSequence"): + # dataset.CurrentRequestedProcedureEvidenceSequence[0].ReferencedSeriesSequence[0].ReferencedSOPSequence[0] + loadable.referencedSeriesInstanceUIDs = [] + loadable.referencedSOPInstanceUIDs = [] + for refSeriesSequence in dataset.CurrentRequestedProcedureEvidenceSequence: + for referencedSeriesSequence in refSeriesSequence.ReferencedSeriesSequence: + for refSOPSequence in referencedSeriesSequence.ReferencedSOPSequence: + if refSOPSequence.ReferencedSOPClassUID == self.UID_SegmentationStorage: # TODO: differentiate between SR, SEG and other volumes + print "Found referenced segmentation" + loadable.referencedSeriesInstanceUIDs.append(referencedSeriesSequence.SeriesInstanceUID) + else: + # print "Found other reference" + for sopInstanceUID in slicer.dicomDatabase.fileForInstance(refSOPSequence.ReferencedSOPInstanceUID): + loadable.referencedSOPInstanceUIDs.append(sopInstanceUID) + # loadable.referencedSOPInstanceUID = refSOPSequence.ReferencedSOPInstanceUID + return loadable + + def loadSeries(self, seriesUID): + dicomWidget = slicer.modules.dicom.widgetRepresentation().self() + dicomWidget.detailsPopup.offerLoadables(seriesUID, 'Series') + dicomWidget.detailsPopup.examineForLoading() + dicomWidget.detailsPopup.loadCheckedLoadables() + + def load(self, loadable): + print('DICOM SR TID1500 load()') + + segPlugin = slicer.modules.dicomPlugins["DICOMSegmentationPlugin"]() + for seriesInstanceUID in loadable.referencedSeriesInstanceUIDs: + # segLoadables = segPlugin.examine([slicer.dicomDatabase.filesForSeries(seriesInstanceUID)]) + # for segLoadable in segLoadables: + # segPlugin.load(segLoadable) + self.loadSeries(seriesInstanceUID) + + try: + uid = loadable.uid + print ('in load(): uid = ', uid) + except AttributeError: + return False + + outputDir = os.path.join(slicer.app.temporaryPath, "QIICR", "SR", loadable.uid) + try: + os.makedirs(outputDir) + except: + pass + + outputFile = os.path.join(outputDir, loadable.uid+".json") + + srFileName = slicer.dicomDatabase.fileForInstance(uid) + if srFileName is None: + print 'Failed to get the filename from the DICOM database for ', uid + return False + + param = { + "inputSRFileName": srFileName, + "metaDataFileName": outputFile, + } + + try: + tid1500reader = slicer.modules.tid1500reader + except AttributeError: + print 'Unable to find CLI module tid1500reader, unable to load SR TID1500 object' + return False + + cliNode = None + cliNode = slicer.cli.run(tid1500reader, cliNode, param, wait_for_completion=True) + if cliNode.GetStatusString() != 'Completed': + print 'tid1500reader did not complete successfully, unable to load DICOM SR TID1500' + return False + + return self.metadata2vtkTableNode(outputFile) + + def metadata2vtkTableNode(self, metafile): + tableNode = None + with open(metafile) as datafile: + table = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(table) + table.SetAttribute("Reporting", "Yes") + table.SetAttribute("readonly", "Yes") + table.SetUseColumnNameAsColumnHeader(True) + + data = json.load(datafile) + + tableWasModified = table.StartModify() + for measurement in data["Measurements"]: + col = table.AddColumn() + col.SetName("Segment Name") + name = measurement["TrackingIdentifier"] + value = measurement["ReferencedSegment"] + rowIndex = table.AddEmptyRow() + # table.SetCellText(rowIndex, 1, name) + table.SetCellText(rowIndex, 0, name) + + # segmentationSOPInstanceUID = measurement["segmentationSOPInstanceUID"] + # ReportingWidget.loadSeries() + + for measurementItem in measurement["measurementItems"]: + col = table.AddColumn() + if "derivationModifier" in measurementItem.keys(): + col.SetName(measurementItem["derivationModifier"]["CodeMeaning"]) + else: + col.SetName(measurementItem["quantity"]["CodeMeaning"]+" "+measurementItem["units"]["CodeValue"]) + for columnIndex, measurementItem in enumerate(measurement["measurementItems"]): + table.SetCellText(rowIndex, columnIndex+1, measurementItem["value"]) + + table.EndModify(tableWasModified) + slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveTableID(table.GetID()) + slicer.app.applicationLogic().PropagateTableSelection() + + return table is not None + + +class DICOMTID1500Plugin: + """ + This class is the 'hook' for slicer to detect and recognize the plugin + as a loadable scripted module + """ + def __init__(self, parent): + parent.title = "DICOM SR TID1500 Object Import Plugin" + parent.categories = ["Developer Tools.DICOM Plugins"] + parent.contributors = ["Christian Herz (BWH), Andrey Fedorov (BWH)"] + parent.helpText = """ + Plugin to the DICOM Module to parse and load DICOM SR TID1500 modality. + No module interface here, only in the DICOM module + """ + parent.dependencies = ['DICOM', 'Colors'] # TODO: Colors needed??? + parent.acknowledgementText = """ + This DICOM Plugin was developed by + Christian Herz, BWH. + and was partially funded by NIH grant U01CA151261. + """ + + # Add this extension to the DICOM module's list for discovery when the module + # is created. Since this module may be discovered before DICOM itself, + # create the list if it doesn't already exist. + try: + slicer.modules.dicomPlugins + except AttributeError: + slicer.modules.dicomPlugins = {} + slicer.modules.dicomPlugins['DICOMTID1500Plugin'] = DICOMTID1500PluginClass diff --git a/Py/Reporting.py b/Py/Reporting.py new file mode 100755 index 0000000..e47d350 --- /dev/null +++ b/Py/Reporting.py @@ -0,0 +1,897 @@ +import getpass +import json +import logging, os +from datetime import datetime + +from slicer.ScriptedLoadableModule import * +import vtkSegmentationCorePython as vtkCoreSeg + +from SlicerProstateUtils.mixins import * +from SlicerProstateUtils.decorators import * +from SlicerProstateUtils.helpers import WatchBoxAttribute, DICOMBasedInformationWatchBox +from SlicerProstateUtils.constants import DICOMTAGS +from SlicerProstateUtils.buttons import * + +from SegmentEditor import SegmentEditorWidget +from SegmentStatistics import SegmentStatisticsLogic + + +class Reporting(ScriptedLoadableModule): + """Uses ScriptedLoadableModule base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "Reporting" + self.parent.categories = ["Examples"] + self.parent.dependencies = ["SlicerProstate"] + self.parent.contributors = ["Christian Herz (SPL), Andrey Fedorov (SPL, BWH), Nicole Aucoin (SPL, BWH), " + "Steve Pieper (Isomics)"] + self.parent.helpText = """ + This is an example of scripted loadable module bundled in an extension. + It performs a simple thresholding on the input volume and optionally captures a screenshot. + """ # TODO: + self.parent.acknowledgementText = """ + This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. + and Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. + """ # TODO: replace with organization, grant and thanks. + + +class ReportingWidget(ModuleWidgetMixin, ScriptedLoadableModuleWidget): + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def __init__(self, parent=None): + ScriptedLoadableModuleWidget.__init__(self, parent) + self.segmentationsLogic = slicer.modules.segmentations.logic() + self.slicerTempDir = slicer.util.tempDirectory() + + def initializeMembers(self): + self.tableNode = None + self.segmentationObservers = [] + + def onReload(self): + self.cleanupUIElements() + self.removeAllUIElements() + super(ReportingWidget, self).onReload() + + def cleanupUIElements(self): + self.removeSegmentationObserver() + self.removeConnections() + self.initializeMembers() + + def removeAllUIElements(self): + for child in [c for c in self.parent.children() if not isinstance(c, qt.QVBoxLayout)]: + try: + child.delete() + except AttributeError: + pass + + def refreshUIElementsAvailability(self): + self.imageVolumeSelector.enabled = self.measurementReportSelector.currentNode() is not None \ + and not self.getReferencedVolumeFromSegmentationNode(self.segmentEditorWidget.segmentationNode) + self.segmentationGroupBox.enabled = self.imageVolumeSelector.currentNode() is not None + self.measurementsGroupBox.enabled = len(self.segmentEditorWidget.segments) + + @postCall(refreshUIElementsAvailability) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + self.initializeMembers() + self.setupWatchbox() + self.setupTestArea() + self.setupViewSettingsArea() + self.setupSegmentationsArea() + self.setupSelectionArea() + self.setupMeasurementsArea() + self.setupActionButtons() + self.setupConnections() + self.layout.addStretch(1) + self.fourUpSliceLayoutButton.checked = True + + def setupWatchbox(self): + self.watchBoxInformation = [ + WatchBoxAttribute('StudyID', 'Study ID: ', DICOMTAGS.STUDY_ID), + WatchBoxAttribute('PatientName', 'Patient Name: ', DICOMTAGS.PATIENT_NAME), + WatchBoxAttribute('DOB', 'Date of Birth: ', DICOMTAGS.PATIENT_BIRTH_DATE), + WatchBoxAttribute('Reader', 'Reader Name: ', callback=getpass.getuser)] + self.watchBox = DICOMBasedInformationWatchBox(self.watchBoxInformation) + self.layout.addWidget(self.watchBox) + + def setupTestArea(self): + + def loadTestData(): + mrHeadSeriesUID = "2.16.840.1.113662.4.4168496325.1025306170.548651188813145058" + if not len(slicer.dicomDatabase.filesForSeries(mrHeadSeriesUID)): + from SEGExporterSelfTest import SEGExporterSelfTestLogic + sampleData = SEGExporterSelfTestLogic.downloadSampleData() + unzipped = SEGExporterSelfTestLogic.unzipSampleData(sampleData) + SEGExporterSelfTestLogic.importIntoDICOMDatabase(unzipped) + self.loadSeries(mrHeadSeriesUID) + masterNode = slicer.util.getNode('2: SAG*') + tableNode = slicer.vtkMRMLTableNode() + tableNode.SetAttribute("Reporting", "Yes") + slicer.mrmlScene.AddNode(tableNode) + self.measurementReportSelector.setCurrentNode(tableNode) + self.imageVolumeSelector.setCurrentNode(masterNode) + self.retrieveTestDataButton.enabled = False + + self.testArea = qt.QGroupBox("Test Area") + self.testAreaLayout = qt.QFormLayout(self.testArea) + self.retrieveTestDataButton = self.createButton("Retrieve and load test data") + self.testAreaLayout.addWidget(self.retrieveTestDataButton) + self.retrieveTestDataButton.clicked.connect(loadTestData) + self.layout.addWidget(self.testArea) + + def loadSeriesByFileName(self, filename): + seriesUID = slicer.dicomDatabase.seriesForFile(filename) + self.loadSeries(seriesUID) + + def loadSeries(self, seriesUID): + dicomWidget = slicer.modules.dicom.widgetRepresentation().self() + dicomWidget.detailsPopup.offerLoadables(seriesUID, 'Series') + dicomWidget.detailsPopup.examineForLoading() + dicomWidget.detailsPopup.loadCheckedLoadables() + + def setupSelectionArea(self): + + self.imageVolumeSelector = self.segmentEditorWidget.masterVolumeNodeSelector + self.imageVolumeSelector.addAttribute("vtkMRMLScalarVolumeNode", "DICOM.instanceUIDs", None) + self.segmentionNodeSelector = self.segmentEditorWidget.segmentationNodeSelector + self.measurementReportSelector = self.createComboBox(nodeTypes=["vtkMRMLTableNode", ""], showChildNodeTypes=False, + addEnabled=True, removeEnabled=True, noneEnabled=True, + selectNodeUponCreation=True, toolTip="Select measurement report") + self.measurementReportSelector.addAttribute("vtkMRMLTableNode", "Reporting", "Yes") + + self.selectionAreaWidget = qt.QWidget() + self.selectionAreaWidgetLayout = qt.QGridLayout() + self.selectionAreaWidget.setLayout(self.selectionAreaWidgetLayout) + + self.selectionAreaWidgetLayout.addWidget(qt.QLabel("Measurement report"), 0, 0) + self.selectionAreaWidgetLayout.addWidget(self.measurementReportSelector, 0, 1) + self.selectionAreaWidgetLayout.addWidget(qt.QLabel("Image volume to annotate"), 1, 0) + self.selectionAreaWidgetLayout.addWidget(self.imageVolumeSelector, 1, 1) + self.layout.addWidget(self.selectionAreaWidget) + self.layout.addWidget(self.segmentationGroupBox) + + def setupViewSettingsArea(self): + self.redSliceLayoutButton = RedSliceLayoutButton() + self.fourUpSliceLayoutButton = FourUpLayoutButton() + self.fourUpSliceTableViewLayoutButton = FourUpTableViewLayoutButton() + self.crosshairButton = CrosshairButton() + + hbox = self.createHLayout([self.redSliceLayoutButton, self.fourUpSliceLayoutButton, + self.fourUpSliceTableViewLayoutButton, self.crosshairButton]) + hbox.layout().addStretch(1) + self.layout.addWidget(hbox) + + def setupSegmentationsArea(self): + self.segmentationGroupBox = qt.QGroupBox("Segmentations") + self.segmentationGroupBoxLayout = qt.QFormLayout() + self.segmentationGroupBox.setLayout(self.segmentationGroupBoxLayout) + self.segmentEditorWidget = ReportingSegmentEditorWidget(parent=self.segmentationGroupBox) + self.segmentEditorWidget.setup() + + def setupMeasurementsArea(self): + self.measurementsGroupBox = qt.QGroupBox("Measurements") + self.measurementsGroupBoxLayout = qt.QVBoxLayout() + self.measurementsGroupBox.setLayout(self.measurementsGroupBoxLayout) + self.tableView = slicer.qMRMLTableView() + self.tableView.minimumHeight = 150 + self.measurementsGroupBoxLayout.addWidget(self.tableView) + self.layout.addWidget(self.measurementsGroupBox) + + def setupActionButtons(self): + self.calculateMeasurementsButton = self.createButton("Calculate Measurements") + self.calculateAutomaticallyCheckbox = qt.QCheckBox("Auto Update") + self.calculateAutomaticallyCheckbox.checked = True + self.saveReportButton = self.createButton("Save Report") + self.completeReportButton = self.createButton("Complete Report") + self.layout.addWidget(self.createHLayout([self.calculateMeasurementsButton, self.calculateAutomaticallyCheckbox])) + self.layout.addWidget(self.createHLayout([self.saveReportButton, self.completeReportButton])) + + def setupConnections(self, funcName="connect"): + + def setupSelectorConnections(): + getattr(self.imageVolumeSelector, funcName)('currentNodeChanged(vtkMRMLNode*)', self.onImageVolumeSelected) + getattr(self.measurementReportSelector, funcName)('currentNodeChanged(vtkMRMLNode*)', self.onMeasurementReportSelected) + + def setupButtonConnections(): + getattr(self.saveReportButton.clicked, funcName)(self.onSaveReportButtonClicked) + getattr(self.completeReportButton.clicked, funcName)(self.onCompleteReportButtonClicked) + getattr(self.calculateMeasurementsButton.clicked, funcName)(self.updateMeasurementsTable) + + getattr(self.layoutManager.layoutChanged, funcName)(self.onLayoutChanged) + getattr(self.calculateAutomaticallyCheckbox.toggled, funcName)(self.onCalcAutomaticallyToggled) + + setupSelectorConnections() + setupButtonConnections() + + def removeConnections(self): + self.setupConnections(funcName="disconnect") + + def onCalcAutomaticallyToggled(self, checked): + if checked: + self.onSegmentationNodeChanged() + + def removeSegmentationObserver(self): + if self.segmentEditorWidget.segmentation and len(self.segmentationObservers): + while len(self.segmentationObservers): + observer = self.segmentationObservers.pop() + self.segmentEditorWidget.segmentation.RemoveObserver(observer) + self.segNode = None + + def onLayoutChanged(self, layout): + self.onDisplayMeasurementsTable() + + @postCall(refreshUIElementsAvailability) + def onImageVolumeSelected(self, node): + self.initializeWatchBox(node) + + @priorCall(refreshUIElementsAvailability) + def onMeasurementReportSelected(self, node): + self.removeSegmentationObserver() + self.imageVolumeSelector.setCurrentNode(None) + self.tableNode = node + if node is None: + self.segmentionNodeSelector.setCurrentNode(None) + return + + segmentationNodeID = self.tableNode.GetAttribute('ReferencedSegmentationNodeID') + if segmentationNodeID: + segmentationNode = slicer.mrmlScene.GetNodeByID(segmentationNodeID) + else: + segmentationNode = self.createNewSegmentationNode() + self.tableNode.SetAttribute('ReferencedSegmentationNodeID', segmentationNode.GetID()) + self.segmentionNodeSelector.setCurrentNode(segmentationNode) + self.setupSegmentationObservers() + self.onSegmentationNodeChanged() + + def getReferencedVolumeFromSegmentationNode(self, segmentationNode): + if not segmentationNode: + return None + return segmentationNode.GetNodeReference(segmentationNode.GetReferenceImageGeometryReferenceRole()) + + def setupSegmentationObservers(self): + segNode = self.segmentEditorWidget.segmentation + if not segNode: + return + segmentationEvents = [vtkCoreSeg.vtkSegmentation.SegmentAdded, vtkCoreSeg.vtkSegmentation.SegmentRemoved, + vtkCoreSeg.vtkSegmentation.SegmentModified, + vtkCoreSeg.vtkSegmentation.RepresentationModified] + for event in segmentationEvents: + self.segmentationObservers.append(segNode.AddObserver(event, self.onSegmentationNodeChanged)) + + def initializeWatchBox(self, node): + try: + dicomFileName = node.GetStorageNode().GetFileName() + self.watchBox.sourceFile = dicomFileName if os.path.exists(dicomFileName) else None + except AttributeError: + self.watchBox.sourceFile = None + + def createNewSegmentationNode(self): + segNode = slicer.vtkMRMLSegmentationNode() + slicer.mrmlScene.AddNode(segNode) + return segNode + + @postCall(refreshUIElementsAvailability) + def onSegmentationNodeChanged(self, observer=None, caller=None): + if not self.calculateAutomaticallyCheckbox.checked: + self.tableView.setStyleSheet("QTableView{border:2px solid red;};") + return + self.updateMeasurementsTable() + + def updateMeasurementsTable(self): + table = self.segmentEditorWidget.calculateLabelStatistics(self.tableNode) + if table: + self.tableNode = table + self.tableNode.SetLocked(True) + self.tableView.setMRMLTableNode(self.tableNode) + self.tableView.setStyleSheet("QTableView{border:none};") + else: + if self.tableNode: + self.tableNode.RemoveAllColumns() + self.tableView.setMRMLTableNode(self.tableNode) + self.onDisplayMeasurementsTable() + + def getActiveSlicerTableID(self): + return slicer.app.applicationLogic().GetSelectionNode().GetActiveTableID() + + def onDisplayMeasurementsTable(self): + self.measurementsGroupBox.visible = not self.layoutManager.layout == self.fourUpSliceTableViewLayoutButton.LAYOUT + if self.layoutManager.layout == self.fourUpSliceTableViewLayoutButton.LAYOUT and self.tableNode: + slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveTableID(self.tableNode.GetID()) + slicer.app.applicationLogic().PropagateTableSelection() + + def onSaveReportButtonClicked(self): + try: + dcmSegmentationPath = self.createSEG() + except (RuntimeError, ValueError, AttributeError) as exc: + slicer.util.warningDisplay(exc.message) + return + self.createDICOMSR(dcmSegmentationPath) + + def onCompleteReportButtonClicked(self): + print "on complete report button clicked" + + def createSEG(self): + import vtkSegmentationCorePython as vtkSegmentationCore + segmentStatisticsLogic = self.segmentEditorWidget.logic.segmentStatisticsLogic + data = dict() + data.update(self._getSeriesAttributes()) + data.update(self._getAdditionalSeriesAttributes()) + data["segmentAttributes"] = segmentStatisticsLogic.generateJSON4DcmSEGExport() + + logging.debug("DICOM SEG Metadata output:") + logging.debug(data) + + self.currentDateTime = datetime.now().strftime('%Y-%m-%d_%H%M%S') + self.tempDir = os.path.join(self.slicerTempDir, self.currentDateTime) + os.mkdir(self.tempDir) + + # TODO: delete from temp afterwards + segmentFiles = [] + for segmentID in segmentStatisticsLogic.statistics["SegmentIDs"]: + if not segmentStatisticsLogic.statistics[segmentID, "GS voxel count"] > 0: + continue + segmentLabelmap = segmentStatisticsLogic.statistics[segmentID, "LM label map"] + filename = os.path.join(self.tempDir, "{}.nrrd".format(segmentLabelmap.GetName())) + slicer.util.saveNode(segmentLabelmap, filename) + segmentFiles.append(filename) + + metaFilePath = self.saveJSON(data, os.path.join(self.tempDir, "seg_meta.json")) + outputSegmentationPath = os.path.join(self.tempDir, "seg.dcm") + + params = {"dicomImageFiles": ', '.join(self.getDICOMFileList(self.segmentEditorWidget.masterVolumeNode, + absolutePaths=True)).replace(', ', ","), + "segImageFiles": ', '.join(segmentFiles).replace(', ', ","), + "metaDataFileName": metaFilePath, + "outputSEGFileName": outputSegmentationPath} + + logging.debug(params) + + cliNode = None + cliNode = slicer.cli.run(slicer.modules.itkimage2segimage, cliNode, params, wait_for_completion=True) + waitCount = 0 + while cliNode.IsBusy() and waitCount < 20: + slicer.util.delayDisplay("Running SEG Encoding... %d" % waitCount, 1000) + waitCount += 1 + + if cliNode.GetStatusString() != 'Completed': + raise RuntimeError("itkimage2segimage CLI did not complete cleanly") + + if not os.path.exists(outputSegmentationPath): + raise RuntimeError("DICOM Segmentation was not created. Check Error Log for further information.") + indexer = ctk.ctkDICOMIndexer() + indexer.addFile(slicer.dicomDatabase, outputSegmentationPath) + + logging.debug("Saved DICOM Segmentation to {}".format(outputSegmentationPath)) + return outputSegmentationPath + + def createDICOMSR(self, referencedSegmentation): + data = self._getSeriesAttributes() + + compositeContextDataDir, data["compositeContext"] = os.path.dirname(referencedSegmentation), [os.path.basename(referencedSegmentation)] + imageLibraryDataDir, data["imageLibrary"] = self.getDICOMFileList(self.segmentEditorWidget.masterVolumeNode) + data.update(self._getAdditionalSRInformation()) + + data["Measurements"] = self.segmentEditorWidget.logic.segmentStatisticsLogic.generateJSON4DcmSR(referencedSegmentation, + self.segmentEditorWidget.masterVolumeNode) + + print json.dumps(data, indent=2, separators=(',', ': ')) # TODO: remove + + logging.debug("DICOM SR Metadata output:") + logging.debug(data) + + metaFilePath = self.saveJSON(data, os.path.join(self.tempDir, "sr_meta.json")) + outputSRPath = os.path.join(self.tempDir, "sr.dcm") + + params = {"metaDataFileName": metaFilePath, + "compositeContextDataDir": compositeContextDataDir, + "imageLibraryDataDir": imageLibraryDataDir, + "outputFileName": outputSRPath} + + logging.debug(params) + cliNode = None + cliNode = slicer.cli.run(slicer.modules.tid1500writer, cliNode, params, wait_for_completion=True) + waitCount = 0 + while cliNode.IsBusy() and waitCount < 20: + slicer.util.delayDisplay("Running SR Encoding... %d" % waitCount, 1000) + waitCount += 1 + + if cliNode.GetStatusString() != 'Completed': + raise Exception("tid1500writer CLI did not complete cleanly") + indexer = ctk.ctkDICOMIndexer() + indexer.addFile(slicer.dicomDatabase, outputSRPath) + + def _getSeriesAttributes(self): + return {"SeriesDescription": "Segmentation", + "SeriesNumber": ModuleLogicMixin.getDICOMValue(self.watchBox.sourceFile, DICOMTAGS.SERIES_NUMBER), + "InstanceNumber": ModuleLogicMixin.getDICOMValue(self.watchBox.sourceFile, DICOMTAGS.INSTANCE_NUMBER)} + + def _getAdditionalSeriesAttributes(self): + # TODO: populate + return {"ContentCreatorName": self.watchBox.getAttribute("Reader").value, + "ClinicalTrialSeriesID": "1", + "ClinicalTrialTimePointID": "1", + "ClinicalTrialCoordinatingCenterName": "QIICR"} + + def _getAdditionalSRInformation(self): + data = dict() + data["observerContext"] = {"ObserverType": "PERSON", + "PersonObserverName": self.watchBox.getAttribute("Reader").value} + data["VerificationFlag"] = "VERIFIED" + data["CompletionFlag"] = "COMPLETE" + data["activitySession"] = "1" + data["timePoint"] = "1" + return data + + def saveJSON(self, data, destination): + with open(os.path.join(destination), 'w') as outfile: + json.dump(data, outfile, indent=2) + return destination + + def getDICOMFileList(self, volumeNode, absolutePaths=False): + # TODO: move to general class + attributeName = "DICOM.instanceUIDs" + instanceUIDs = volumeNode.GetAttribute(attributeName) + if not instanceUIDs: + raise ValueError("VolumeNode {0} has no attribute {1}".format(volumeNode.GetName(), attributeName)) + fileList = [] + rootDir = None + for uid in instanceUIDs.split(): + rootDir, filename = self.getInstanceUIDDirectoryAndFileName(uid) + fileList.append(str(filename if not absolutePaths else os.path.join(rootDir, filename))) + if not absolutePaths: + return rootDir, fileList + return fileList + + def getInstanceUIDDirectoryAndFileName(self, uid): + # TODO: move this method to a general class + path = slicer.dicomDatabase.fileForInstance(uid) + return os.path.dirname(path), os.path.basename(path) + + +class ReportingTest(ScriptedLoadableModuleTest): + """ + This is the test case for your scripted module. + Uses ScriptedLoadableModuleTest base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_Reporting1() + + def test_Reporting1(self): + """ Ideally you should have several levels of tests. At the lowest level + tests should exercise the functionality of the logic with different inputs + (both valid and invalid). At higher levels your tests should emulate the + way the user would interact with your code and confirm that it still works + the way you intended. + One of the most important features of the tests is that it should alert other + developers when their changes will have an impact on the behavior of your + module. For example, if a developer removes a feature that you depend on, + your test should break so they know that the feature is needed. + """ + + self.delayDisplay("Starting the test") + # + # first, get some data + # + import urllib + downloads = ( + ('http://slicer.kitware.com/midas3/download?items=5767', 'FA.nrrd', slicer.util.loadVolume), + ) + + for url,name,loader in downloads: + filePath = slicer.app.temporaryPath + '/' + name + if not os.path.exists(filePath) or os.stat(filePath).st_size == 0: + logging.info('Requesting download %s from %s...\n' % (name, url)) + urllib.urlretrieve(url, filePath) + if loader: + logging.info('Loading %s...' % (name,)) + loader(filePath) + self.delayDisplay('Finished with download and loading') + + volumeNode = slicer.util.getNode(pattern="FA") + self.delayDisplay('Test passed!') + + +class ReportingSegmentEditorWidget(SegmentEditorWidget, ModuleWidgetMixin): + + @property + def segmentationNode(self): + return self.editor.segmentationNode() + + @property + def segmentationNodeSelector(self): + return self.find("MRMLNodeComboBox_Segmentation") + + @property + def masterVolumeNode(self): + return self.editor.masterVolumeNode() + + @property + def masterVolumeNodeSelector(self): + return self.find("MRMLNodeComboBox_MasterVolume") + + @property + @onExceptionReturnNone + def segmentation(self): + return self.segmentationNode.GetSegmentation() + + @property + def segments(self): + return self.logic.getSegments(self.segmentation) + + @property + def table(self): + return self.find("SegmentsTableView") + + @property + @onExceptionReturnNone + def tableWidget(self): + return self.table.tableWidget() + + @onExceptionReturnNone + def find(self, objectName): + return self.findAll(objectName)[0] + + def findAll(self, objectName): + return slicer.util.findChildren(self.editor, objectName) + + def __init__(self, parent): + SegmentEditorWidget.__init__(self, parent) + self.logic = ReportingSegmentEditorLogic() + + def setup(self): + super(ReportingSegmentEditorWidget, self).setup() + self.reloadCollapsibleButton.hide() + self.hideUnwantedEditorUIElements() + self.reorganizeEffectButtons() + self.changeUndoRedoSizePolicies() + self.appendOptionsAndMaskingGroupBoxAtTheEnd() + self.clearSegmentationEditorSelectors() + self.setupConnections() + + def setupConnections(self): + self.tableWidget.itemClicked.connect(self.onSegmentSelected) + + def onSegmentSelected(self, item): + try: + segment = self.segments[item.row()] + self.jumpToSegmentCenter(segment) + except IndexError: + pass + + def jumpToSegmentCenter(self, segment): + centroid = self.logic.getSegmentCentroid(segment) + if centroid: + markupsLogic = slicer.modules.markups.logic() + markupsLogic.JumpSlicesToLocation(centroid[0], centroid[1], centroid[2], False) + + def clearSegmentationEditorSelectors(self): + self.editor.setSegmentationNode(None) + self.editor.setMasterVolumeNode(None) + + def hideUnwantedEditorUIElements(self): + self.editor.segmentationNodeSelectorVisible = False + # self.editor.masterVolumeNodeSelectorVisible = False + + def reorganizeEffectButtons(self): + widget = self.find("EffectsGroupBox") + if widget: + buttons = [b for b in widget.children() if isinstance(b, qt.QPushButton)] + self.layout.addWidget(self.createHLayout(buttons)) + widget.hide() + + def appendOptionsAndMaskingGroupBoxAtTheEnd(self): + for widgetName in ["OptionsGroupBox", "MaskingGroupBox"]: + widget = self.find(widgetName) + self.layout.addWidget(widget) + + def changeUndoRedoSizePolicies(self): + undo = self.find("UndoButton") + redo = self.find("RedoButton") + if undo and redo: + undo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + redo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + self.layout.addWidget(self.createHLayout([undo, redo])) + + def enter(self): + # overridden because SegmentEditorWidget automatically creates a new Segmentation upon instantiation + self.turnOffLightboxes() + self.installShortcutKeys() + + # Set parameter set node if absent + self.selectParameterNode() + self.editor.updateWidgetFromMRML() + + def calculateLabelStatistics(self, tableNode): + return self.logic.calculateSegmentStatistics(self.segmentationNode, self.masterVolumeNode, tableNode) + + +class ReportingSegmentEditorLogic(ScriptedLoadableModuleLogic): + + def __init__(self, parent=None): + ScriptedLoadableModuleLogic.__init__(self, parent) + self.parent = parent + self.volumesLogic = slicer.modules.volumes.logic() + self.segmentationsLogic = slicer.modules.segmentations.logic() + self.segmentStatisticsLogic = None + self.segmentStatisticsLogic = CustomSegmentStatisticsLogic() + + def getSegments(self, segmentation): + if not segmentation: + return [] + segmentIDs = vtk.vtkStringArray() + segmentation.GetSegmentIDs(segmentIDs) + return [segmentation.GetSegment(segmentIDs.GetValue(idx)) for idx in range(segmentIDs.GetNumberOfValues())] + + def getSegmentCentroid(self, segment): + binData = segment.GetRepresentation("Binary labelmap") + extent = binData.GetExtent() + if extent[1] != -1 and extent[3] != -1 and extent[5] != -1: + tempLabel = slicer.vtkMRMLLabelMapVolumeNode() + slicer.mrmlScene.AddNode(tempLabel) + tempLabel.SetName(segment.GetName() + "CentroidHelper") + self.segmentationsLogic.CreateLabelmapVolumeFromOrientedImageData(binData, tempLabel) + centroid = ModuleLogicMixin.getCentroidForLabel(tempLabel, 1) + slicer.mrmlScene.RemoveNode(tempLabel) + return centroid + return None + + def calculateSegmentStatistics(self, segNode, grayscaleNode, tableNode=None): + self.segmentStatisticsLogic.computeStatistics(segNode, grayscaleNode) + + tableNode = self.segmentStatisticsLogic.exportToTable(tableNode) + slicer.mrmlScene.AddNode(tableNode) + # slicer.mrmlScene.RemoveNode(labelNode) + return tableNode + + + + +class CustomSegmentStatisticsLogic(SegmentStatisticsLogic): + + def __init__(self): + SegmentStatisticsLogic.__init__(self) + self.terminologyLogic = slicer.modules.terminologies.logic() + self.segmentationsLogic = slicer.modules.segmentations.logic() + + def reset(self): + if hasattr(self, "statistics"): + for segmentID in self.statistics["SegmentIDs"]: + try: + labelmap = self.statistics[segmentID, "LM label map"] + slicer.mrmlScene.RemoveNode(labelmap) + except Exception: + continue + SegmentStatisticsLogic.reset(self) + + def exportToTable(self, table=None, nonEmptyKeysOnly = True): + if not table: + table = slicer.vtkMRMLTableNode() + table.SetName(slicer.mrmlScene.GenerateUniqueName(self.grayscaleNode.GetName() + ' statistics')) + self.keys = ("Segment", "GS voxel count", "GS volume mm3", "GS volume cc", + "GS min", "GS max", "GS mean", "GS stdev") + SegmentStatisticsLogic.exportToTable(self, table, nonEmptyKeysOnly) + return table + + def filterEmptySegments(self): + return [self.segmentationNode.GetSegmentation().GetSegment(s) for s in self.statistics["SegmentIDs"] + if self.statistics[s, "GS voxel count"] > 0] + + def generateJSON4DcmSEGExport(self): + self.validateSegments() + segmentsData = [] + for segmentID in self.statistics["SegmentIDs"]: + if self.statistics[segmentID, "GS voxel count"] == 0: + continue + segmentData = dict() + segmentData["LabelID"] = self.statistics[segmentID, "LM pixel value"] + segment = self.segmentationNode.GetSegmentation().GetSegment(segmentID) + category = self.getTagValue(segment, segment.GetTerminologyCategoryTagName()) + segmentData["SegmentDescription"] = category if category != "" else self.statistics[segmentID, "Segment"] + segmentData["SegmentAlgorithmType"] = "MANUAL" + segmentData["recommendedDisplayRGBValue"] = segment.GetDefaultColor() + segmentData.update(self.createJSONFromTerminologyContext(segment)) + segmentData.update(self.createJSONFromAnatomicContext(segment)) + segmentsData.append(segmentData) + if not len(segmentsData): + raise ValueError("No segments with pixel data found.") + return [segmentsData] + + def createJSONFromTerminologyContext(self, segment): + segmentData = dict() + contextName = self.getTagValue(segment, segment.GetTerminologyContextTagName()) + categoryName = self.getTagValue(segment, segment.GetTerminologyCategoryTagName()) + + category = slicer.vtkSlicerTerminologyCategory() + if not self.terminologyLogic.GetCategoryInTerminology(contextName, categoryName, category): + raise ValueError("Error: Cannot get category from terminology") + segmentData["SegmentedPropertyCategoryCodeSequence"] = self.getJSONFromVtkSlicerCodeSequence(category) + + typeName = self.getTagValue(segment, segment.GetTerminologyTypeTagName()) + pType = slicer.vtkSlicerTerminologyType() + if not self.terminologyLogic.GetTypeInTerminologyCategory(contextName, categoryName, typeName, pType): + raise ValueError("Error: Cannot get type from terminology") + segmentData["SegmentedPropertyTypeCodeSequence"] = self.getJSONFromVtkSlicerCodeSequence(pType) + + modifierName = self.getTagValue(segment, segment.GetTerminologyTypeModifierTagName()) + if modifierName != "": + modifier = slicer.vtkSlicerTerminologyType() + if self.terminologyLogic.GetTypeModifierInTerminologyType(contextName, categoryName, typeName, modifierName, modifier): + segmentData["SegmentedPropertyTypeModifierCodeSequence"] = self.getJSONFromVtkSlicerCodeSequence(modifier) + + return segmentData + + def createJSONFromAnatomicContext(self, segment): + segmentData = dict() + anatomicContextName = self.getTagValue(segment, segment.GetAnatomicContextTagName()) + regionName = self.getTagValue(segment, segment.GetAnatomicRegionTagName()) + + if regionName == "": + return {} + + region = slicer.vtkSlicerTerminologyType() + self.terminologyLogic.GetRegionInAnatomicContext(anatomicContextName, regionName, region) + segmentData["AnatomicRegionSequence"] = self.getJSONFromVtkSlicerCodeSequence(region) + + modifierName = self.getTagValue(segment, segment.GetAnatomicRegionModifierTagName()) + if modifierName != "": + modifier = slicer.vtkSlicerTerminologyType() + self.terminologyLogic.GetRegionModifierInAnatomicRegion(anatomicContextName, regionName, modifierName, modifier) + segmentData["AnatomicRegionModifierSequence"] = self.getJSONFromVtkSlicerCodeSequence(modifier) + + return segmentData + + def getJSONFromVtkSlicerCodeSequence(self, codeSequence): + return self.createCodeSequence(codeSequence.GetCodeValue(), codeSequence.GetCodingScheme(), codeSequence.GetCodeMeaning()) + + def createLabelNodeFromSegment(self, segmentID): + labelNode = slicer.vtkMRMLLabelMapVolumeNode() + slicer.mrmlScene.AddNode(labelNode) + + mergedImageData = vtkCoreSeg.vtkOrientedImageData() + self.segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImageData, 0, None, self.vtkStringArrayFromList([segmentID])) + if not self.segmentationsLogic.CreateLabelmapVolumeFromOrientedImageData(mergedImageData, labelNode): + slicer.mrmlScene.RemoveNode(labelNode) + return None + labelNode.SetName("{}_label".format(segmentID)) + return labelNode + + def vtkStringArrayFromList(self, listToConvert): + stringArray = vtk.vtkStringArray() + for listElement in listToConvert: + stringArray.InsertNextValue(listElement) + return stringArray + + def changePixelValue(self, labelNode, outValue): + imageData = labelNode.GetImageData() + backgroundValue = 0 + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(imageData) + thresh.ThresholdByLower(0) + thresh.SetInValue(backgroundValue) + thresh.SetOutValue(outValue) + thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) + thresh.Update() + labelNode.SetAndObserveImageData(thresh.GetOutput()) + + def addSegmentLabelmapStatistics(self): + SegmentStatisticsLogic.addSegmentLabelmapStatistics(self) + for pixelValue, segmentID in enumerate(self.statistics["SegmentIDs"], 1): + if not self.statistics[segmentID,"LM voxel count"] > 0: + continue + segmentLabelmap = self.createLabelNodeFromSegment(segmentID) + self.changePixelValue(segmentLabelmap, pixelValue) + self.statistics[segmentID, "LM pixel value"] = pixelValue + self.statistics[segmentID, "LM label map"] = segmentLabelmap + + def generateJSON4DcmSR(self, dcmSegmentationFile, sourceVolumeNode): + measurements = [] + modality = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0008,0060") + + sourceImageSeriesUID = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0020,000E") + logging.debug("SourceImageSeriesUID: {}".format(sourceImageSeriesUID)) + segmentationSOPInstanceUID = ModuleLogicMixin.getDICOMValue(dcmSegmentationFile, "0008,0018") + logging.debug("SegmentationSOPInstanceUID: {}".format(segmentationSOPInstanceUID)) + + for segmentID in self.statistics["SegmentIDs"]: + data = dict() + data["TrackingIdentifier"] = self.statistics[segmentID, "Segment"] + data["ReferencedSegment"] = self.statistics[segmentID, "LM pixel value"] + data["SourceSeriesForImageSegmentation"] = sourceImageSeriesUID + data["segmentationSOPInstanceUID"] = segmentationSOPInstanceUID + segment = self.segmentationNode.GetSegmentation().GetSegment(segmentID) + data["Finding"] = self.createJSONFromTerminologyContext(segment)["SegmentedPropertyTypeCodeSequence"] + anatomicContext = self.createJSONFromAnatomicContext(segment) + if anatomicContext.has_key("AnatomicRegionSequence"): + data["FindingSite"] = anatomicContext["AnatomicRegionSequence"] + data["measurementItems"] = self.createMeasurementItemsForLabelValue(segmentID, modality) + measurements.append(data) + return measurements + + def createMeasurementItemsForLabelValue(self, segmentValue, modality): + measurementItems = [] + for key in [k for k in self.keys if k not in ["Segment", "GS voxel count"]]: + item = dict() + item["value"] = str(self.statistics[segmentValue, key]) + item["quantity"] = self.getQuantityCSforKey(key) + item["units"] = self.getUnitsCSForKey(key, modality) + derivationModifier = self.getDerivatinModifierCSForKey(key) + if derivationModifier: + item["derivationModifier"] = derivationModifier + measurementItems.append(item) + return measurementItems + + def getQuantityCSforKey(self, key, modality="CT"): + if key in ["GS min", "GS max", "GS mean", "GS stdev"]: + if modality == "CT": + return self.createCodeSequence("122713", "DCM", "Attenuation Coefficient") + elif modality == "MR": + return self.createCodeSequence("110852", "DCM", "MR signal intensity") + elif key in ["GS volume mm3", "GS volume cc"]: + return self.createCodeSequence("G-D705", "SRT", "Volume") + raise ValueError("No matching quantity code sequence found for key {}".format(key)) + + def getUnitsCSForKey(self, key, modality="CT"): + keys = ["GS min", "GS max", "GS mean", "GS stdev"] + if key in keys: + if modality == "CT": + return self.createCodeSequence("[hnsf'U]", "UCUM", "Hounsfield unit") + elif modality == "MR": + return self.createCodeSequence("1", "UCUM", "no units") + raise ValueError("No matching units code sequence found for key {}".format(key)) + elif key == "GS volume cc": + return self.createCodeSequence("mm3", "UCUM", "cubic millimeter") + elif key == "GS volume mm3": + return self.createCodeSequence("cm3", "UCUM", "cubic centimeter") + return None + + def getDerivatinModifierCSForKey(self, key): + keys = ["GS min", "GS max", "GS mean", "GS stdev"] + if key in keys: + if key == keys[0]: + return self.createCodeSequence("R-404FB", "SRT", "Minimum") + elif key == keys[1]: + return self.createCodeSequence("G-A437", "SRT", "Maximum") + elif key == keys[2]: + return self.createCodeSequence("R-00317", "SRT", "Mean") + else: + return self.createCodeSequence("R-10047", "SRT", "Standard Deviation") + return None + + def createCodeSequence(self, value, designator, meaning): + return {"CodeValue": value, + "CodingSchemeDesignator": designator, + "CodeMeaning": meaning} + + def validateSegments(self): + for segmentID in self.statistics["SegmentIDs"]: + segment = self.segmentationNode.GetSegmentation().GetSegment(segmentID) + category = self.getTagValue(segment, segment.GetTerminologyCategoryTagName()) + propType = self.getTagValue(segment, segment.GetTerminologyTypeTagName()) + if any(v == "" for v in [category, propType]): + raise ValueError("Segment {} has missing attributes. Make sure to set terminology.".format(segment.GetName())) + + def isSegmentEmpty(self, segment): + bounds = [0.0,0.0,0.0,0.0,0.0,0.0] + segment.GetBounds(bounds) + return bounds[1] < 0 and bounds[3] < 0 and bounds[5] < 0 + + def getTagValue(self, segment, tagName): + value = vtk.mutable("") + segment.GetTag(tagName, value) + return value.get() \ No newline at end of file diff --git a/Py/SEGExporterSelfTest.py b/Py/SEGExporterSelfTest.py index 963a463..6468ff6 100644 --- a/Py/SEGExporterSelfTest.py +++ b/Py/SEGExporterSelfTest.py @@ -28,6 +28,7 @@ def __init__(self, parent): QIICR, NIH National Cancer Institute, award U24 CA180918. """ # replace with organization, grant and thanks. + class SEGExporterSelfTestWidget(ScriptedLoadableModuleWidget): """Uses ScriptedLoadableModuleWidget base class, available at: https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py @@ -37,8 +38,33 @@ def setup(self): ScriptedLoadableModuleWidget.setup(self) class SEGExporterSelfTestLogic(ScriptedLoadableModuleLogic): + @staticmethod + def importIntoDICOMDatabase(dicomFilesDirectory): + indexer = ctk.ctkDICOMIndexer() + indexer.addDirectory(slicer.dicomDatabase, dicomFilesDirectory, None) + indexer.waitForImportFinished() + + @staticmethod + def unzipSampleData(filePath): + dicomFilesDirectory = slicer.app.temporaryPath + '/dicomFiles' + qt.QDir().mkpath(dicomFilesDirectory) + slicer.app.applicationLogic().Unzip(filePath, dicomFilesDirectory) + return dicomFilesDirectory + + @staticmethod + def downloadSampleData(): + import urllib + downloads = ( + ('http://slicer.kitware.com/midas3/download/item/220834/PieperMRHead.zip', 'PieperMRHead.zip'), + ) + slicer.util.delayDisplay("Downloading", 1000) + for url, name in downloads: + filePath = slicer.app.temporaryPath + '/' + name + if not os.path.exists(filePath) or os.stat(filePath).st_size == 0: + slicer.util.delayDisplay('Requesting download %s from %s...\n' % (name, url), 1000) + urllib.urlretrieve(url, filePath) + return filePath - pass class SEGExporterSelfTestTest(ScriptedLoadableModuleTest): """ @@ -49,6 +75,7 @@ def setUp(self): """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ slicer.mrmlScene.Clear(0) + self.logic = SEGExporterSelfTestLogic def runTest(self): """Run as few or as many tests as needed here. @@ -67,28 +94,20 @@ def test_SEGExporterSelfTest1(self): # # first, get the data - a zip file of dicom data # - import urllib - downloads = ( - ('http://slicer.kitware.com/midas3/download/item/220834/PieperMRHead.zip', 'PieperMRHead.zip'), - ) - self.delayDisplay("Downloading") - for url,name in downloads: - filePath = slicer.app.temporaryPath + '/' + name - if not os.path.exists(filePath) or os.stat(filePath).st_size == 0: - self.delayDisplay('Requesting download %s from %s...\n' % (name, url)) - urllib.urlretrieve(url, filePath) + filePath = self.logic.downloadSampleData() self.delayDisplay('Finished with download\n') self.delayDisplay("Unzipping") - dicomFilesDirectory = slicer.app.temporaryPath + '/dicomFiles' - qt.QDir().mkpath(dicomFilesDirectory) - slicer.app.applicationLogic().Unzip(filePath, dicomFilesDirectory) + dicomFilesDirectory = self.logic.unzipSampleData(filePath) try: self.delayDisplay("Switching to temp database directory") tempDatabaseDirectory = slicer.app.temporaryPath + '/tempDICOMDatabase' import shutil - shutil.rmtree(tempDatabaseDirectory) + try: + shutil.rmtree(tempDatabaseDirectory) + except OSError: + pass qt.QDir().mkpath(tempDatabaseDirectory) if slicer.dicomDatabase: originalDatabaseDirectory = os.path.split(slicer.dicomDatabase.databaseFilename)[0] @@ -103,9 +122,7 @@ def test_SEGExporterSelfTest1(self): mainWindow = slicer.util.mainWindow() mainWindow.moduleSelector().selectModule('DICOM') - indexer = ctk.ctkDICOMIndexer() - indexer.addDirectory(slicer.dicomDatabase, dicomFilesDirectory, None) - indexer.waitForImportFinished() + self.logic.importIntoDICOMDatabase(dicomFilesDirectory) dicomWidget.detailsPopup.open() @@ -141,7 +158,7 @@ def test_SEGExporterSelfTest1(self): parameterNode.SetParameter("LabelEffect,paintThresholdMax", "279.75") parameterNode.SetParameter("PaintEffect,radius", "40") parameterNode.SetParameter("PaintEffect,sphere", "1") - + self.delayDisplay("Paint some things") parameterNode = EditUtil.getParameterNode() lm = slicer.app.layoutManager() @@ -178,6 +195,7 @@ def test_SEGExporterSelfTest1(self): # close scene re-load the input data and SEG slicer.mrmlScene.Clear(0) + indexer = ctk.ctkDICOMIndexer() indexer.addDirectory(slicer.dicomDatabase, tempSEGDirectory, None) indexer.waitForImportFinished() @@ -215,4 +233,4 @@ def test_SEGExporterSelfTest1(self): if originalDatabaseDirectory: dicomWidget.onDatabaseDirectoryChanged(originalDatabaseDirectory) - self.delayDisplay("Finished!") + self.delayDisplay("Finished!") \ No newline at end of file