From 30dce1118d5c0794fef0cb69644911f86aab3e78 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Thu, 15 Sep 2016 18:47:57 -0400 Subject: [PATCH 01/39] ENH: adding first draft for new reporting module (issue #74) - Segment Editor and measurement table still needs to be added --- Py/CMakeLists.txt | 5 ++ Py/Reporting.py | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 Py/Reporting.py 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/Reporting.py b/Py/Reporting.py new file mode 100644 index 0000000..a818f30 --- /dev/null +++ b/Py/Reporting.py @@ -0,0 +1,201 @@ +import getpass + +from slicer.ScriptedLoadableModule import * + +from SlicerProstateUtils.mixins import * +from SlicerProstateUtils.helpers import WatchBoxAttribute, DICOMBasedInformationWatchBox +from SlicerProstateUtils.constants import DICOMTAGS + +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" # TODO make this more human readable by adding spaces + self.parent.categories = ["Examples"] + self.parent.dependencies = ["SlicerProstate"] + self.parent.contributors = ["Andrey Fedorov (SPL, BWH), Nicole Aucoin (SPL, BWH), " + "Steve Pieper (Isomics), Christian Herz (SPL)"] + 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. + """ + 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. +""" # 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) + + def cleanup(self): + pass + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + self.setupWatchbox() + self.setupSelectionArea() + self.setupViewSettingsArea() + self.setupSegmentationsArea() + self.setupMeasurementsArea() + self.setupActionButtons() + self.setupConnections() + self.layout.addStretch(1) + + def setupWatchbox(self): + self.watchBoxInformation = [ + WatchBoxAttribute('StudyID', 'Study ID: ', DICOMTAGS.PATIENT_BIRTH_DATE), + 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 setupSelectionArea(self): + self.imageVolumeSelector = self.createComboBox(nodeTypes=["vtkMRMLScalarVolumeNode", ""], showChildNodeTypes=False, + selectNodeUponCreation=True, toolTip="Select image volume to annotate") + self.measurementReportSelector = self.createComboBox(nodeTypes=["vtkMRMLScriptedModuleNode", ""], showChildNodeTypes=False, + selectNodeUponCreation=True, toolTip="Select measurement report") + self.layout.addWidget(self.createHLayout([qt.QLabel("Image volume to annotate"), self.imageVolumeSelector])) + self.layout.addWidget(self.createHLayout([qt.QLabel("Measurement report"), self.measurementReportSelector])) + + def setupViewSettingsArea(self): + pass + + def setupSegmentationsArea(self): + pass + + def setupMeasurementsArea(self): + pass + + def setupActionButtons(self): + self.saveReportButton = self.createButton("Save Report") + self.completeReportButton = self.createButton("Complete Report") + self.layout.addWidget(self.createHLayout([self.saveReportButton, self.completeReportButton])) + + def setupConnections(self): + + def setupSelectorConnections(): + self.imageVolumeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onImageVolumeSelectorChanged) + + def setupButtonConnections(): + self.saveReportButton.clicked.connect(self.onSaveReportButtonClicked) + self.completeReportButton.clicked.connect(self.onCompleteReportButtonClicked) + + setupSelectorConnections() + setupButtonConnections() + + def onImageVolumeSelectorChanged(self, node): + # TODO: save, cleanup open sessions + try: + dicomFileName = node.GetStorageNode().GetFileName() + self.watchBox.sourceFile = dicomFileName if os.path.exists(dicomFileName) else None + except AttributeError: + self.watchBox.sourceFile = None + + def onSaveReportButtonClicked(self): + print "on save report button clicked" + + def onCompleteReportButtonClicked(self): + print "on complete report button clicked" + + +class ReportingLogic(ScriptedLoadableModuleLogic): + """This class should implement all the actual + computation done by your module. The interface + should be such that other python code can import + this class and make use of the functionality without + requiring an instance of the Widget. + Uses ScriptedLoadableModuleLogic base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def hasImageData(self, volumeNode): + """This is an example logic method that + returns true if the passed in volume + node has valid image data + """ + if not volumeNode: + logging.debug('hasImageData failed: no volume node') + return False + if volumeNode.GetImageData() is None: + logging.debug('hasImageData failed: no image data in volume node') + return False + return True + + def isValidInputOutputData(self, inputVolumeNode, outputVolumeNode): + """Validates if the output is not the same as input + """ + if not inputVolumeNode: + logging.debug('isValidInputOutputData failed: no input volume node defined') + return False + if not outputVolumeNode: + logging.debug('isValidInputOutputData failed: no output volume node defined') + return False + if inputVolumeNode.GetID()==outputVolumeNode.GetID(): + logging.debug('isValidInputOutputData failed: input and output volume is the same. Create a new volume for output to avoid this error.') + return False + return True + + +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") + logic = ReportingLogic() + self.assertIsNotNone( logic.hasImageData(volumeNode) ) + self.delayDisplay('Test passed!') From 9cdbf857bb4d683554276355d5cb0e9d537ce828 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Fri, 16 Sep 2016 11:15:21 -0400 Subject: [PATCH 02/39] ENH: added segmentation editor (issue #74) - when image volume is selected, a new segmentation is created and image volume is set as master for the segmentation - when another image volume is selected and then going back to the previous one, Reporting module knows referenced segmentation so no new one is created --- Py/Reporting.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index a818f30..b090e4b 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -6,6 +6,8 @@ from SlicerProstateUtils.helpers import WatchBoxAttribute, DICOMBasedInformationWatchBox from SlicerProstateUtils.constants import DICOMTAGS +from SegmentEditor import SegmentEditorWidget + class Reporting(ScriptedLoadableModule): """Uses ScriptedLoadableModule base class, available at: https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py @@ -36,11 +38,16 @@ class ReportingWidget(ModuleWidgetMixin, ScriptedLoadableModuleWidget): def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) + def initializeMembers(self): + self.tNode = None + self.segReferencedMasterVolume = {} + def cleanup(self): pass def setup(self): ScriptedLoadableModuleWidget.setup(self) + self.initializeMembers() self.setupWatchbox() self.setupSelectionArea() self.setupViewSettingsArea() @@ -71,7 +78,25 @@ def setupViewSettingsArea(self): pass def setupSegmentationsArea(self): - pass + self.segmentationWidget = qt.QGroupBox("Segmentations") + self.segmentationWidgetLayout = qt.QFormLayout() + self.segmentationWidget.setLayout(self.segmentationWidgetLayout) + self.editorWidget = SegmentEditorWidget(parent=self.segmentationWidget) + self.editorWidget.setup() + # self.segmentationWidget.children()[1].hide() + self.editorWidget.editor.segmentationNodeSelectorVisible = False + self.editorWidget.editor.masterVolumeNodeSelectorVisible = False + self.clearSegmentationEditorSelectors() + self.layout.addWidget(self.segmentationWidget) + + def clearSegmentationEditorSelectors(self): + self.editorWidget.editor.setSegmentationNode(None) + self.editorWidget.editor.setMasterVolumeNode(None) + + def hideUnwantedEditorUIElements(self): + for widgetName in ['MRMLNodeComboBox_MasterVolume']: + widget = slicer.util.findChildren(self.editorWidget.volumes, widgetName)[0] + widget.hide() def setupMeasurementsArea(self): pass @@ -95,11 +120,23 @@ def setupButtonConnections(): def onImageVolumeSelectorChanged(self, node): # TODO: save, cleanup open sessions + if not node: + self.clearSegmentationEditorSelectors() try: dicomFileName = node.GetStorageNode().GetFileName() self.watchBox.sourceFile = dicomFileName if os.path.exists(dicomFileName) else None except AttributeError: self.watchBox.sourceFile = None + if node: + # TODO: check if there is a segmentation Node for the selected image volume available instead of creating a new one each time + if node in self.segReferencedMasterVolume.keys(): + self.editorWidget.editor.setSegmentationNode(self.segReferencedMasterVolume[node]) + else: + segNode = slicer.vtkMRMLSegmentationNode() + slicer.mrmlScene.AddNode(segNode) + self.editorWidget.editor.setSegmentationNode(segNode) + self.editorWidget.editor.setMasterVolumeNode(node) + self.segReferencedMasterVolume[node] = segNode def onSaveReportButtonClicked(self): print "on save report button clicked" @@ -107,6 +144,13 @@ def onSaveReportButtonClicked(self): def onCompleteReportButtonClicked(self): print "on complete report button clicked" + def onAnnotationReady(self): + #TODO: calc measurements (logic) and set table node + pass + + def updateTableNode(self): + # TODO: update table node for annotations + pass class ReportingLogic(ScriptedLoadableModuleLogic): """This class should implement all the actual From 133d6633a23d09af5b61a8b2fdfd308e59062407 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Fri, 16 Sep 2016 16:38:23 -0400 Subject: [PATCH 03/39] ENH: Using LabelStatistics module widget for the first draft. (issue #74) - later moving that into the background (logic) only for statistics calculation and using vtkMRLMTableNode instead --- Py/Reporting.py | 115 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 39 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index b090e4b..831bf28 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -3,10 +3,12 @@ from slicer.ScriptedLoadableModule import * from SlicerProstateUtils.mixins import * +from SlicerProstateUtils.decorators import logmethod from SlicerProstateUtils.helpers import WatchBoxAttribute, DICOMBasedInformationWatchBox from SlicerProstateUtils.constants import DICOMTAGS from SegmentEditor import SegmentEditorWidget +from LabelStatistics import LabelStatisticsWidget class Reporting(ScriptedLoadableModule): """Uses ScriptedLoadableModule base class, available at: @@ -37,13 +39,24 @@ class ReportingWidget(ModuleWidgetMixin, ScriptedLoadableModuleWidget): def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) + self.logic = ReportingLogic() def initializeMembers(self): self.tNode = None - self.segReferencedMasterVolume = {} + self.tableNode = None + self.segNode = None + self.segmentObservers = {} + self.segNodeObserverTag = None + self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? + + def onReload(self): + super(ReportingWidget, self).onReload() + self.cleanup() def cleanup(self): - pass + self.removeSegmentationObserver() + self.removeAllSegmentObservers() + self.initializeMembers() def setup(self): ScriptedLoadableModuleWidget.setup(self) @@ -69,8 +82,9 @@ def setupWatchbox(self): def setupSelectionArea(self): self.imageVolumeSelector = self.createComboBox(nodeTypes=["vtkMRMLScalarVolumeNode", ""], showChildNodeTypes=False, selectNodeUponCreation=True, toolTip="Select image volume to annotate") - self.measurementReportSelector = self.createComboBox(nodeTypes=["vtkMRMLScriptedModuleNode", ""], showChildNodeTypes=False, - selectNodeUponCreation=True, toolTip="Select measurement report") + self.measurementReportSelector = self.createComboBox(nodeTypes=["vtkMRMLTableNode", ""], showChildNodeTypes=False, + selectNodeUponCreation=True, toolTip="Select measurement report", + addEnabled=True) self.layout.addWidget(self.createHLayout([qt.QLabel("Image volume to annotate"), self.imageVolumeSelector])) self.layout.addWidget(self.createHLayout([qt.QLabel("Measurement report"), self.measurementReportSelector])) @@ -83,7 +97,7 @@ def setupSegmentationsArea(self): self.segmentationWidget.setLayout(self.segmentationWidgetLayout) self.editorWidget = SegmentEditorWidget(parent=self.segmentationWidget) self.editorWidget.setup() - # self.segmentationWidget.children()[1].hide() + self.segmentationWidget.children()[1].hide() self.editorWidget.editor.segmentationNodeSelectorVisible = False self.editorWidget.editor.masterVolumeNodeSelectorVisible = False self.clearSegmentationEditorSelectors() @@ -99,7 +113,16 @@ def hideUnwantedEditorUIElements(self): widget.hide() def setupMeasurementsArea(self): - pass + self.measurementsWidget = qt.QGroupBox("Measurements") + self.measurementsWidgetLayout = qt.QVBoxLayout() + self.measurementsWidget.setLayout(self.measurementsWidgetLayout) + self.labelStatisticsWidget = LabelStatisticsWidget(parent=self.measurementsWidget) + self.labelStatisticsWidget.setup() + self.measurementsWidget.children()[1].hide() + self.labelStatisticsWidget.grayscaleSelectorFrame.hide() + self.labelStatisticsWidget.labelSelectorFrame.hide() + self.labelStatisticsWidget.applyButton.hide() + self.layout.addWidget(self.measurementsWidget) def setupActionButtons(self): self.saveReportButton = self.createButton("Save Report") @@ -118,7 +141,15 @@ def setupButtonConnections(): setupSelectorConnections() setupButtonConnections() + def removeSegmentationObserver(self): + if self.segNode and self.segNodeObserverTag: + self.segNode.removeObserver(self.segNodeObserverTag) + self.segNode = None + self.segNodeObserverTag = None + self.removeAllSegmentObservers() + def onImageVolumeSelectorChanged(self, node): + self.removeSegmentationObserver() # TODO: save, cleanup open sessions if not node: self.clearSegmentationEditorSelectors() @@ -130,13 +161,36 @@ def onImageVolumeSelectorChanged(self, node): if node: # TODO: check if there is a segmentation Node for the selected image volume available instead of creating a new one each time if node in self.segReferencedMasterVolume.keys(): - self.editorWidget.editor.setSegmentationNode(self.segReferencedMasterVolume[node]) + self.segNode = self.segReferencedMasterVolume[node] + self.editorWidget.editor.setSegmentationNode(self.segNode) else: - segNode = slicer.vtkMRMLSegmentationNode() - slicer.mrmlScene.AddNode(segNode) - self.editorWidget.editor.setSegmentationNode(segNode) + self.segNode = slicer.vtkMRMLSegmentationNode() + slicer.mrmlScene.AddNode(self.segNode) + self.editorWidget.editor.setSegmentationNode(self.segNode) self.editorWidget.editor.setMasterVolumeNode(node) - self.segReferencedMasterVolume[node] = segNode + self.segReferencedMasterVolume[node] = self.segNode + self.labelStatisticsWidget.labelSelector.setCurrentNode(self.segNode) + self.labelStatisticsWidget.grayscaleSelector.setCurrentNode(node) + self.segNode.AddObserver(self.segNode.GetSegmentation().SegmentAdded, self.onSegmentCountChanged) + self.segNode.AddObserver(self.segNode.GetSegmentation().SegmentRemoved, self.onSegmentCountChanged) + self.segNode.AddObserver(self.segNode.GetSegmentation().SegmentModified, self.onSegmentationNodeChanged) + + @logmethod() + def onSegmentCountChanged(self, observer=None, caller=None): + segmentIDs = vtk.vtkStringArray() + self.removeAllSegmentObservers() + for idx in range(segmentIDs.GetNumberOfValues()): + segmentID = segmentIDs.GetValue(idx) + segment = self.segNode.GetSegment(segmentID) + segment.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onSegmentationNodeChanged) + + def onSegmentationNodeChanged(self, observer=None, caller=None): + self.labelStatisticsWidget.applyButton.click() + + def removeAllSegmentObservers(self): + for segment, tag in self.segmentObservers.iteritems(): + segment.RemoveObserver(tag) + self.segmentObservers = {} def onSaveReportButtonClicked(self): print "on save report button clicked" @@ -149,8 +203,13 @@ def onAnnotationReady(self): pass def updateTableNode(self): - # TODO: update table node for annotations - pass + data = self.logic.calculateLabelStatistics(self.editorWidget.editor.segmentationNode()) + # TODO: apply data to tableNode + if not self.tableNode: + self.tableNode = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(self.tableNode) + if data is not None: + pass class ReportingLogic(ScriptedLoadableModuleLogic): """This class should implement all the actual @@ -162,32 +221,10 @@ class ReportingLogic(ScriptedLoadableModuleLogic): https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - def hasImageData(self, volumeNode): - """This is an example logic method that - returns true if the passed in volume - node has valid image data - """ - if not volumeNode: - logging.debug('hasImageData failed: no volume node') - return False - if volumeNode.GetImageData() is None: - logging.debug('hasImageData failed: no image data in volume node') - return False - return True - - def isValidInputOutputData(self, inputVolumeNode, outputVolumeNode): - """Validates if the output is not the same as input - """ - if not inputVolumeNode: - logging.debug('isValidInputOutputData failed: no input volume node defined') - return False - if not outputVolumeNode: - logging.debug('isValidInputOutputData failed: no output volume node defined') - return False - if inputVolumeNode.GetID()==outputVolumeNode.GetID(): - logging.debug('isValidInputOutputData failed: input and output volume is the same. Create a new volume for output to avoid this error.') - return False - return True + def calculateLabelStatistics(self, segmentationNode): + # TODO: need to think about what to deliver as parameters here + + return None class ReportingTest(ScriptedLoadableModuleTest): From c3a06fbbe4f0fd690b660df284a73473f7adce2d Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Mon, 19 Sep 2016 13:58:09 -0400 Subject: [PATCH 04/39] ENH: displaying only volumes coming from DICOM in image volume selector(issue #74) --- Py/Reporting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Py/Reporting.py b/Py/Reporting.py index 831bf28..f8b44ac 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -82,6 +82,7 @@ def setupWatchbox(self): def setupSelectionArea(self): self.imageVolumeSelector = self.createComboBox(nodeTypes=["vtkMRMLScalarVolumeNode", ""], showChildNodeTypes=False, selectNodeUponCreation=True, toolTip="Select image volume to annotate") + self.imageVolumeSelector.addAttribute("vtkMRMLScalarVolumeNode", "DICOM.instanceUIDs", None) self.measurementReportSelector = self.createComboBox(nodeTypes=["vtkMRMLTableNode", ""], showChildNodeTypes=False, selectNodeUponCreation=True, toolTip="Select measurement report", addEnabled=True) From 13b35b1cfde8c0a8f7a2634f2686c4f076361026 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Mon, 19 Sep 2016 15:12:14 -0400 Subject: [PATCH 05/39] STYLE: Hiding unneeded editor components and reorganized buttons for a better style (issue #74) --- Py/Reporting.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index f8b44ac..2e56ee5 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -10,6 +10,7 @@ from SegmentEditor import SegmentEditorWidget from LabelStatistics import LabelStatisticsWidget + class Reporting(ScriptedLoadableModule): """Uses ScriptedLoadableModule base class, available at: https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py @@ -72,7 +73,7 @@ def setup(self): def setupWatchbox(self): self.watchBoxInformation = [ - WatchBoxAttribute('StudyID', 'Study ID: ', DICOMTAGS.PATIENT_BIRTH_DATE), + 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)] @@ -99,8 +100,8 @@ def setupSegmentationsArea(self): self.editorWidget = SegmentEditorWidget(parent=self.segmentationWidget) self.editorWidget.setup() self.segmentationWidget.children()[1].hide() - self.editorWidget.editor.segmentationNodeSelectorVisible = False - self.editorWidget.editor.masterVolumeNodeSelectorVisible = False + self.hideUnwantedEditorUIElements() + self.reorganizeEffectButtons() self.clearSegmentationEditorSelectors() self.layout.addWidget(self.segmentationWidget) @@ -109,8 +110,17 @@ def clearSegmentationEditorSelectors(self): self.editorWidget.editor.setMasterVolumeNode(None) def hideUnwantedEditorUIElements(self): - for widgetName in ['MRMLNodeComboBox_MasterVolume']: - widget = slicer.util.findChildren(self.editorWidget.volumes, widgetName)[0] + self.editorWidget.editor.segmentationNodeSelectorVisible = False + self.editorWidget.editor.masterVolumeNodeSelectorVisible = False + for widgetName in ["OptionsGroupBox","MaskingGroupBox"]: + widget = slicer.util.findChildren(self.editorWidget.editor, widgetName)[0] + widget.hide() + + def reorganizeEffectButtons(self): + widget = slicer.util.findChildren(self.editorWidget.editor, "EffectsGroupBox")[0] + if widget: + buttons = [b for b in widget.children() if isinstance(b, qt.QPushButton)] + self.segmentationWidgetLayout.addWidget(self.createHLayout(buttons)) widget.hide() def setupMeasurementsArea(self): From c0857ea8b889277bbf4d0b51717317f268b88e61 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Mon, 19 Sep 2016 16:56:59 -0400 Subject: [PATCH 06/39] ENH: added observers for segment added/removed and changes in image data of segments (issue #74) - needed for recalculating statistics --- Py/Reporting.py | 61 ++++++++++++++++++++++++++------------------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 2e56ee5..7c75379 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -1,6 +1,7 @@ import getpass from slicer.ScriptedLoadableModule import * +import vtkSegmentationCorePython as vtkCoreSeg from SlicerProstateUtils.mixins import * from SlicerProstateUtils.decorators import logmethod @@ -45,9 +46,8 @@ def __init__(self, parent=None): def initializeMembers(self): self.tNode = None self.tableNode = None - self.segNode = None - self.segmentObservers = {} - self.segNodeObserverTag = None + self.segmentation = None + self.segmentationObservers = [] self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? def onReload(self): @@ -56,7 +56,6 @@ def onReload(self): def cleanup(self): self.removeSegmentationObserver() - self.removeAllSegmentObservers() self.initializeMembers() def setup(self): @@ -122,6 +121,12 @@ def reorganizeEffectButtons(self): buttons = [b for b in widget.children() if isinstance(b, qt.QPushButton)] self.segmentationWidgetLayout.addWidget(self.createHLayout(buttons)) widget.hide() + undo = slicer.util.findChildren(self.editorWidget.editor, "UndoButton")[0] + redo = slicer.util.findChildren(self.editorWidget.editor, "RedoButton")[0] + if undo and redo: + undo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + redo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + self.segmentationWidgetLayout.addWidget(self.createHLayout([undo, redo])) def setupMeasurementsArea(self): self.measurementsWidget = qt.QGroupBox("Measurements") @@ -153,11 +158,11 @@ def setupButtonConnections(): setupButtonConnections() def removeSegmentationObserver(self): - if self.segNode and self.segNodeObserverTag: - self.segNode.removeObserver(self.segNodeObserverTag) - self.segNode = None - self.segNodeObserverTag = None - self.removeAllSegmentObservers() + if self.segmentation and len(self.segmentationObservers): + for observer in self.segmentationObservers: + self.segmentation.RemoveObserver(observer) + self.segmentationObservers = [] + self.segmentation = None def onImageVolumeSelectorChanged(self, node): self.removeSegmentationObserver() @@ -172,37 +177,27 @@ def onImageVolumeSelectorChanged(self, node): if node: # TODO: check if there is a segmentation Node for the selected image volume available instead of creating a new one each time if node in self.segReferencedMasterVolume.keys(): - self.segNode = self.segReferencedMasterVolume[node] - self.editorWidget.editor.setSegmentationNode(self.segNode) + segNode = self.segReferencedMasterVolume[node] + self.editorWidget.editor.setSegmentationNode(segNode) else: - self.segNode = slicer.vtkMRMLSegmentationNode() - slicer.mrmlScene.AddNode(self.segNode) - self.editorWidget.editor.setSegmentationNode(self.segNode) + segNode = slicer.vtkMRMLSegmentationNode() + slicer.mrmlScene.AddNode(segNode) + self.editorWidget.editor.setSegmentationNode(segNode) self.editorWidget.editor.setMasterVolumeNode(node) - self.segReferencedMasterVolume[node] = self.segNode - self.labelStatisticsWidget.labelSelector.setCurrentNode(self.segNode) + self.segReferencedMasterVolume[node] = segNode + self.labelStatisticsWidget.labelSelector.setCurrentNode(segNode) self.labelStatisticsWidget.grayscaleSelector.setCurrentNode(node) - self.segNode.AddObserver(self.segNode.GetSegmentation().SegmentAdded, self.onSegmentCountChanged) - self.segNode.AddObserver(self.segNode.GetSegmentation().SegmentRemoved, self.onSegmentCountChanged) - self.segNode.AddObserver(self.segNode.GetSegmentation().SegmentModified, self.onSegmentationNodeChanged) - - @logmethod() - def onSegmentCountChanged(self, observer=None, caller=None): - segmentIDs = vtk.vtkStringArray() - self.removeAllSegmentObservers() - for idx in range(segmentIDs.GetNumberOfValues()): - segmentID = segmentIDs.GetValue(idx) - segment = self.segNode.GetSegment(segmentID) - segment.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onSegmentationNodeChanged) + self.segmentation = segNode.GetSegmentation() + self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentAdded, + self.onSegmentationNodeChanged)) + self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentRemoved, + self.onSegmentationNodeChanged)) + self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.MasterRepresentationModified, + self.onSegmentationNodeChanged)) def onSegmentationNodeChanged(self, observer=None, caller=None): self.labelStatisticsWidget.applyButton.click() - def removeAllSegmentObservers(self): - for segment, tag in self.segmentObservers.iteritems(): - segment.RemoveObserver(tag) - self.segmentObservers = {} - def onSaveReportButtonClicked(self): print "on save report button clicked" From 247f055904de25cd324e6b1395437711f8fe35fc Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Mon, 19 Sep 2016 23:37:17 -0400 Subject: [PATCH 07/39] ENH: exporting from label statistics to table node right away once segments change (issue #74) --- Py/Reporting.py | 50 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 7c75379..55d2bf8 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -46,9 +46,12 @@ def __init__(self, parent=None): def initializeMembers(self): self.tNode = None self.tableNode = None + self.segNode = None self.segmentation = None self.segmentationObservers = [] self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? + self.segmentationsLogic = slicer.modules.segmentations.logic() + self.segmentationLabelMapDummy = None def onReload(self): super(ReportingWidget, self).onReload() @@ -56,6 +59,8 @@ def onReload(self): def cleanup(self): self.removeSegmentationObserver() + if self.tableNode: + slicer.mrmlScene.RemoveNode(self.tableNode) self.initializeMembers() def setup(self): @@ -84,8 +89,8 @@ def setupSelectionArea(self): selectNodeUponCreation=True, toolTip="Select image volume to annotate") self.imageVolumeSelector.addAttribute("vtkMRMLScalarVolumeNode", "DICOM.instanceUIDs", None) self.measurementReportSelector = self.createComboBox(nodeTypes=["vtkMRMLTableNode", ""], showChildNodeTypes=False, - selectNodeUponCreation=True, toolTip="Select measurement report", - addEnabled=True) + selectNodeUponCreation=True, toolTip="Select measurement report") + self.imageVolumeSelector.addAttribute("vtkMRMLTableNode", "Reporting", None) self.layout.addWidget(self.createHLayout([qt.QLabel("Image volume to annotate"), self.imageVolumeSelector])) self.layout.addWidget(self.createHLayout([qt.QLabel("Measurement report"), self.measurementReportSelector])) @@ -135,10 +140,10 @@ def setupMeasurementsArea(self): self.labelStatisticsWidget = LabelStatisticsWidget(parent=self.measurementsWidget) self.labelStatisticsWidget.setup() self.measurementsWidget.children()[1].hide() - self.labelStatisticsWidget.grayscaleSelectorFrame.hide() - self.labelStatisticsWidget.labelSelectorFrame.hide() - self.labelStatisticsWidget.applyButton.hide() - self.layout.addWidget(self.measurementsWidget) + # self.labelStatisticsWidget.grayscaleSelectorFrame.hide() + # self.labelStatisticsWidget.labelSelectorFrame.hide() + # self.labelStatisticsWidget.applyButton.hide() + # self.layout.addWidget(self.measurementsWidget) def setupActionButtons(self): self.saveReportButton = self.createButton("Save Report") @@ -163,6 +168,7 @@ def removeSegmentationObserver(self): self.segmentation.RemoveObserver(observer) self.segmentationObservers = [] self.segmentation = None + self.segNode = None def onImageVolumeSelectorChanged(self, node): self.removeSegmentationObserver() @@ -177,17 +183,16 @@ def onImageVolumeSelectorChanged(self, node): if node: # TODO: check if there is a segmentation Node for the selected image volume available instead of creating a new one each time if node in self.segReferencedMasterVolume.keys(): - segNode = self.segReferencedMasterVolume[node] - self.editorWidget.editor.setSegmentationNode(segNode) + self.segNode = self.segReferencedMasterVolume[node] + self.editorWidget.editor.setSegmentationNode(self.segNode) else: - segNode = slicer.vtkMRMLSegmentationNode() - slicer.mrmlScene.AddNode(segNode) - self.editorWidget.editor.setSegmentationNode(segNode) + self.segNode = slicer.vtkMRMLSegmentationNode() + slicer.mrmlScene.AddNode(self.segNode) + self.editorWidget.editor.setSegmentationNode(self.segNode) self.editorWidget.editor.setMasterVolumeNode(node) - self.segReferencedMasterVolume[node] = segNode - self.labelStatisticsWidget.labelSelector.setCurrentNode(segNode) + self.segReferencedMasterVolume[node] = self.segNode self.labelStatisticsWidget.grayscaleSelector.setCurrentNode(node) - self.segmentation = segNode.GetSegmentation() + self.segmentation = self.segNode.GetSegmentation() self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentAdded, self.onSegmentationNodeChanged)) self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentRemoved, @@ -196,7 +201,21 @@ def onImageVolumeSelectorChanged(self, node): self.onSegmentationNodeChanged)) def onSegmentationNodeChanged(self, observer=None, caller=None): - self.labelStatisticsWidget.applyButton.click() + if self.segmentationLabelMapDummy: + slicer.mrmlScene.RemoveNode(self.segmentationLabelMapDummy) + self.segmentationLabelMapDummy = slicer.vtkMRMLLabelMapVolumeNode() + slicer.mrmlScene.AddNode(self.segmentationLabelMapDummy) + if self.tableNode and self.tableNode.GetID() == self.getActiveSlicerTableID(): + slicer.mrmlScene.RemoveNode(self.tableNode) + if self.segmentationsLogic.ExportAllSegmentsToLabelmapNode(self.segNode, self.segmentationLabelMapDummy): + self.labelStatisticsWidget.labelSelector.setCurrentNode(self.segmentationLabelMapDummy) + self.labelStatisticsWidget.applyButton.click() + self.labelStatisticsWidget.logic.exportToTable() + self.tableNode = slicer.mrmlScene.GetNodeByID(self.getActiveSlicerTableID()) + self.tableNode.SetAttribute("Reporting", "Yes") + + def getActiveSlicerTableID(self): + return slicer.app.applicationLogic().GetSelectionNode().GetActiveTableID() def onSaveReportButtonClicked(self): print "on save report button clicked" @@ -209,6 +228,7 @@ def onAnnotationReady(self): pass def updateTableNode(self): + # TODO: maybe remove this method and or move some code to logic. label statistics widget is not needed data = self.logic.calculateLabelStatistics(self.editorWidget.editor.segmentationNode()) # TODO: apply data to tableNode if not self.tableNode: From 9b1db631187b52bbaeeaaf7d8d3f5610875ea3eb Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 20 Sep 2016 14:44:37 -0400 Subject: [PATCH 08/39] ENH: Using LabelStatisticsLogic only instead of widget for calculating --- Py/Reporting.py | 70 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 55d2bf8..a71ea04 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -1,4 +1,5 @@ import getpass +import json from slicer.ScriptedLoadableModule import * import vtkSegmentationCorePython as vtkCoreSeg @@ -9,7 +10,7 @@ from SlicerProstateUtils.constants import DICOMTAGS from SegmentEditor import SegmentEditorWidget -from LabelStatistics import LabelStatisticsWidget +from LabelStatistics import LabelStatisticsLogic class Reporting(ScriptedLoadableModule): @@ -134,16 +135,11 @@ def reorganizeEffectButtons(self): self.segmentationWidgetLayout.addWidget(self.createHLayout([undo, redo])) def setupMeasurementsArea(self): + # TODO: add tables here self.measurementsWidget = qt.QGroupBox("Measurements") self.measurementsWidgetLayout = qt.QVBoxLayout() self.measurementsWidget.setLayout(self.measurementsWidgetLayout) - self.labelStatisticsWidget = LabelStatisticsWidget(parent=self.measurementsWidget) - self.labelStatisticsWidget.setup() - self.measurementsWidget.children()[1].hide() - # self.labelStatisticsWidget.grayscaleSelectorFrame.hide() - # self.labelStatisticsWidget.labelSelectorFrame.hide() - # self.labelStatisticsWidget.applyButton.hide() - # self.layout.addWidget(self.measurementsWidget) + self.layout.addWidget(self.measurementsWidget) def setupActionButtons(self): self.saveReportButton = self.createButton("Save Report") @@ -191,7 +187,6 @@ def onImageVolumeSelectorChanged(self, node): self.editorWidget.editor.setSegmentationNode(self.segNode) self.editorWidget.editor.setMasterVolumeNode(node) self.segReferencedMasterVolume[node] = self.segNode - self.labelStatisticsWidget.grayscaleSelector.setCurrentNode(node) self.segmentation = self.segNode.GetSegmentation() self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentAdded, self.onSegmentationNodeChanged)) @@ -205,17 +200,14 @@ def onSegmentationNodeChanged(self, observer=None, caller=None): slicer.mrmlScene.RemoveNode(self.segmentationLabelMapDummy) self.segmentationLabelMapDummy = slicer.vtkMRMLLabelMapVolumeNode() slicer.mrmlScene.AddNode(self.segmentationLabelMapDummy) - if self.tableNode and self.tableNode.GetID() == self.getActiveSlicerTableID(): + if self.tableNode and self.tableNode.GetID() == self.logic.getActiveSlicerTableID(): slicer.mrmlScene.RemoveNode(self.tableNode) if self.segmentationsLogic.ExportAllSegmentsToLabelmapNode(self.segNode, self.segmentationLabelMapDummy): - self.labelStatisticsWidget.labelSelector.setCurrentNode(self.segmentationLabelMapDummy) - self.labelStatisticsWidget.applyButton.click() - self.labelStatisticsWidget.logic.exportToTable() - self.tableNode = slicer.mrmlScene.GetNodeByID(self.getActiveSlicerTableID()) - self.tableNode.SetAttribute("Reporting", "Yes") - - def getActiveSlicerTableID(self): - return slicer.app.applicationLogic().GetSelectionNode().GetActiveTableID() + grayscaleNode = self.segReferencedMasterVolume.keys()[self.segReferencedMasterVolume.values().index(self.segNode)] + try: + self.tableNode = self.logic.calculateLabelStatistics(self.segmentationLabelMapDummy, grayscaleNode) + except ValueError as exc: + slicer.util.warnDisplay(exc.message, windowTitle="Label Statistics") def onSaveReportButtonClicked(self): print "on save report button clicked" @@ -227,15 +219,6 @@ def onAnnotationReady(self): #TODO: calc measurements (logic) and set table node pass - def updateTableNode(self): - # TODO: maybe remove this method and or move some code to logic. label statistics widget is not needed - data = self.logic.calculateLabelStatistics(self.editorWidget.editor.segmentationNode()) - # TODO: apply data to tableNode - if not self.tableNode: - self.tableNode = slicer.vtkMRMLTableNode() - slicer.mrmlScene.AddNode(self.tableNode) - if data is not None: - pass class ReportingLogic(ScriptedLoadableModuleLogic): """This class should implement all the actual @@ -247,10 +230,37 @@ class ReportingLogic(ScriptedLoadableModuleLogic): https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - def calculateLabelStatistics(self, segmentationNode): - # TODO: need to think about what to deliver as parameters here + def __init__(self, parent=None): + ScriptedLoadableModuleLogic.__init__(self, parent) + self.volumesLogic = slicer.modules.volumes.logic() + + def calculateLabelStatistics(self, labelNode, grayscaleNode): + warnings = self.volumesLogic.CheckForLabelVolumeValidity(grayscaleNode, labelNode) + if warnings != "": + if 'mismatch' in warnings: + resampledLabelNode = self.volumesLogic.ResampleVolumeToReferenceVolume(labelNode, grayscaleNode) + # resampledLabelNode does not have a display node, therefore the colorNode has to be passed to it + labelStatisticsLogic = LabelStatisticsLogic(grayscaleNode, resampledLabelNode, + colorNode=labelNode.GetDisplayNode().GetColorNode(), + nodeBaseName=labelNode.GetName()) + else: + raise ValueError("Volumes do not have the same geometry.\n%s" % warnings) + else: + labelStatisticsLogic = LabelStatisticsLogic(grayscaleNode, labelNode) + + labelStatisticsLogic.exportToTable() + tableNode = slicer.mrmlScene.GetNodeByID(self.getActiveSlicerTableID()) + # TODO: uncommnent that once the new Slicer build is out + # self.tableNode = labelStatisticsLogic.logic.exportToTable() + # slicer.mrmlScene.AddNode(self.tableNode) + # slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpTableView) + # slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveTableID(self.tableNode.GetID()) + # slicer.app.applicationLogic().PropagateTableSelection() + tableNode.SetAttribute("Reporting", "Yes") + return tableNode - return None + def getActiveSlicerTableID(self): + return slicer.app.applicationLogic().GetSelectionNode().GetActiveTableID() class ReportingTest(ScriptedLoadableModuleTest): From 084a853f141dc8f2c17b12d6879e35ea5fa13192 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Thu, 22 Sep 2016 11:21:11 -0400 Subject: [PATCH 09/39] ENH: Aligning selectors, inherited class ReportingSegmentEditorWidget from SegmentEditorWidget(issue #74) - better structure - TODO: implement method for centering segmentation when selection changes in table view --- Py/Reporting.py | 200 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 132 insertions(+), 68 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index a71ea04..0a0e8e4 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -40,18 +40,25 @@ class ReportingWidget(ModuleWidgetMixin, ScriptedLoadableModuleWidget): https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ + @property + def segmentation(self): + try: + return self.segNode.GetSegmentation() + except AttributeError: + return None + def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) self.logic = ReportingLogic() + self.segmentationsLogic = slicer.modules.segmentations.logic() + self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? def initializeMembers(self): self.tNode = None self.tableNode = None self.segNode = None - self.segmentation = None + self.displayTableInSliceView = False self.segmentationObservers = [] - self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? - self.segmentationsLogic = slicer.modules.segmentations.logic() self.segmentationLabelMapDummy = None def onReload(self): @@ -60,12 +67,14 @@ def onReload(self): def cleanup(self): self.removeSegmentationObserver() + self.removeConnections() if self.tableNode: slicer.mrmlScene.RemoveNode(self.tableNode) self.initializeMembers() def setup(self): ScriptedLoadableModuleWidget.setup(self) + self.initializeMembers() self.setupWatchbox() self.setupSelectionArea() @@ -92,8 +101,15 @@ def setupSelectionArea(self): self.measurementReportSelector = self.createComboBox(nodeTypes=["vtkMRMLTableNode", ""], showChildNodeTypes=False, selectNodeUponCreation=True, toolTip="Select measurement report") self.imageVolumeSelector.addAttribute("vtkMRMLTableNode", "Reporting", None) - self.layout.addWidget(self.createHLayout([qt.QLabel("Image volume to annotate"), self.imageVolumeSelector])) - self.layout.addWidget(self.createHLayout([qt.QLabel("Measurement report"), self.measurementReportSelector])) + self.selectionAreaWidget = qt.QWidget() + self.selectionAreaWidgetLayout = qt.QGridLayout() + self.selectionAreaWidget.setLayout(self.selectionAreaWidgetLayout) + + self.selectionAreaWidgetLayout.addWidget(qt.QLabel("Image volume to annotate"), 0, 0) + self.selectionAreaWidgetLayout.addWidget(self.imageVolumeSelector, 0, 1) + self.selectionAreaWidgetLayout.addWidget(qt.QLabel("Measurement report"), 1, 0) + self.selectionAreaWidgetLayout.addWidget(self.measurementReportSelector, 1, 1) + self.layout.addWidget(self.selectionAreaWidget) def setupViewSettingsArea(self): pass @@ -102,43 +118,17 @@ def setupSegmentationsArea(self): self.segmentationWidget = qt.QGroupBox("Segmentations") self.segmentationWidgetLayout = qt.QFormLayout() self.segmentationWidget.setLayout(self.segmentationWidgetLayout) - self.editorWidget = SegmentEditorWidget(parent=self.segmentationWidget) + self.editorWidget = ReportingSegmentEditorWidget(parent=self.segmentationWidget) self.editorWidget.setup() - self.segmentationWidget.children()[1].hide() - self.hideUnwantedEditorUIElements() - self.reorganizeEffectButtons() - self.clearSegmentationEditorSelectors() self.layout.addWidget(self.segmentationWidget) - def clearSegmentationEditorSelectors(self): - self.editorWidget.editor.setSegmentationNode(None) - self.editorWidget.editor.setMasterVolumeNode(None) - - def hideUnwantedEditorUIElements(self): - self.editorWidget.editor.segmentationNodeSelectorVisible = False - self.editorWidget.editor.masterVolumeNodeSelectorVisible = False - for widgetName in ["OptionsGroupBox","MaskingGroupBox"]: - widget = slicer.util.findChildren(self.editorWidget.editor, widgetName)[0] - widget.hide() - - def reorganizeEffectButtons(self): - widget = slicer.util.findChildren(self.editorWidget.editor, "EffectsGroupBox")[0] - if widget: - buttons = [b for b in widget.children() if isinstance(b, qt.QPushButton)] - self.segmentationWidgetLayout.addWidget(self.createHLayout(buttons)) - widget.hide() - undo = slicer.util.findChildren(self.editorWidget.editor, "UndoButton")[0] - redo = slicer.util.findChildren(self.editorWidget.editor, "RedoButton")[0] - if undo and redo: - undo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) - redo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) - self.segmentationWidgetLayout.addWidget(self.createHLayout([undo, redo])) - def setupMeasurementsArea(self): - # TODO: add tables here self.measurementsWidget = qt.QGroupBox("Measurements") self.measurementsWidgetLayout = qt.QVBoxLayout() self.measurementsWidget.setLayout(self.measurementsWidgetLayout) + self.tableView = slicer.qMRMLTableView() + self.tableView.minimumHeight = 150 + self.measurementsWidgetLayout.addWidget(self.tableView) self.layout.addWidget(self.measurementsWidget) def setupActionButtons(self): @@ -146,55 +136,66 @@ def setupActionButtons(self): self.completeReportButton = self.createButton("Complete Report") self.layout.addWidget(self.createHLayout([self.saveReportButton, self.completeReportButton])) - def setupConnections(self): + def setupConnections(self, funcName="connect"): def setupSelectorConnections(): - self.imageVolumeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onImageVolumeSelectorChanged) + getattr(self.imageVolumeSelector, funcName)('currentNodeChanged(vtkMRMLNode*)', self.onImageVolumeSelectorChanged) def setupButtonConnections(): - self.saveReportButton.clicked.connect(self.onSaveReportButtonClicked) - self.completeReportButton.clicked.connect(self.onCompleteReportButtonClicked) + getattr(self.saveReportButton.clicked, funcName)(self.onSaveReportButtonClicked) + getattr(self.completeReportButton.clicked, funcName)(self.onCompleteReportButtonClicked) setupSelectorConnections() setupButtonConnections() + def removeConnections(self): + self.setupConnections(funcName="disconnect") + + def setupSegmentationObservers(self): + if self.segmentation: + self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentAdded, + self.onSegmentationNodeChanged)) + self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentRemoved, + self.onSegmentationNodeChanged)) + self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.MasterRepresentationModified, + self.onSegmentationNodeChanged)) + def removeSegmentationObserver(self): if self.segmentation and len(self.segmentationObservers): for observer in self.segmentationObservers: self.segmentation.RemoveObserver(observer) self.segmentationObservers = [] - self.segmentation = None self.segNode = None def onImageVolumeSelectorChanged(self, node): - self.removeSegmentationObserver() # TODO: save, cleanup open sessions - if not node: + self.removeSegmentationObserver() + self.initializeWatchBox(node) + if node: + if node in self.segReferencedMasterVolume.keys(): + self.editorWidget.editor.setSegmentationNode(self.segNode) + else: + self.segReferencedMasterVolume[node] = self.createNewSegmentation(node) + self.segNode = self.segReferencedMasterVolume[node] + self.setupSegmentationObservers() + else: self.clearSegmentationEditorSelectors() + + 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 - if node: - # TODO: check if there is a segmentation Node for the selected image volume available instead of creating a new one each time - if node in self.segReferencedMasterVolume.keys(): - self.segNode = self.segReferencedMasterVolume[node] - self.editorWidget.editor.setSegmentationNode(self.segNode) - else: - self.segNode = slicer.vtkMRMLSegmentationNode() - slicer.mrmlScene.AddNode(self.segNode) - self.editorWidget.editor.setSegmentationNode(self.segNode) - self.editorWidget.editor.setMasterVolumeNode(node) - self.segReferencedMasterVolume[node] = self.segNode - self.segmentation = self.segNode.GetSegmentation() - self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentAdded, - self.onSegmentationNodeChanged)) - self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentRemoved, - self.onSegmentationNodeChanged)) - self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.MasterRepresentationModified, - self.onSegmentationNodeChanged)) + def createNewSegmentation(self, masterNode): + segNode = slicer.vtkMRMLSegmentationNode() + slicer.mrmlScene.AddNode(segNode) + self.editorWidget.editor.setSegmentationNode(segNode) + self.editorWidget.editor.setMasterVolumeNode(masterNode) + return segNode + + @logmethod() def onSegmentationNodeChanged(self, observer=None, caller=None): if self.segmentationLabelMapDummy: slicer.mrmlScene.RemoveNode(self.segmentationLabelMapDummy) @@ -205,9 +206,18 @@ def onSegmentationNodeChanged(self, observer=None, caller=None): if self.segmentationsLogic.ExportAllSegmentsToLabelmapNode(self.segNode, self.segmentationLabelMapDummy): grayscaleNode = self.segReferencedMasterVolume.keys()[self.segReferencedMasterVolume.values().index(self.segNode)] try: + if self.tableNode: + slicer.mrmlScene.RemoveNode(self.tableNode) self.tableNode = self.logic.calculateLabelStatistics(self.segmentationLabelMapDummy, grayscaleNode) + self.tableView.setMRMLTableNode(self.tableNode) + if self.displayTableInSliceView: + slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpTableView) + slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveTableID(self.tableNode.GetID()) + slicer.app.applicationLogic().PropagateTableSelection() except ValueError as exc: slicer.util.warnDisplay(exc.message, windowTitle="Label Statistics") + else: + self.tableView.setMRMLTableNode(None) def onSaveReportButtonClicked(self): print "on save report button clicked" @@ -248,15 +258,11 @@ def calculateLabelStatistics(self, labelNode, grayscaleNode): else: labelStatisticsLogic = LabelStatisticsLogic(grayscaleNode, labelNode) - labelStatisticsLogic.exportToTable() - tableNode = slicer.mrmlScene.GetNodeByID(self.getActiveSlicerTableID()) - # TODO: uncommnent that once the new Slicer build is out - # self.tableNode = labelStatisticsLogic.logic.exportToTable() - # slicer.mrmlScene.AddNode(self.tableNode) - # slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpTableView) - # slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveTableID(self.tableNode.GetID()) - # slicer.app.applicationLogic().PropagateTableSelection() + # TODO: manually pick information from labelStatisticsLogic.labelStats + + tableNode = labelStatisticsLogic.exportToTable() tableNode.SetAttribute("Reporting", "Yes") + slicer.mrmlScene.AddNode(tableNode) return tableNode def getActiveSlicerTableID(self): @@ -316,3 +322,61 @@ def test_Reporting1(self): logic = ReportingLogic() self.assertIsNotNone( logic.hasImageData(volumeNode) ) self.delayDisplay('Test passed!') + + +class ReportingSegmentEditorWidget(SegmentEditorWidget, ModuleWidgetMixin): + + def __init__(self, parent): + super(ReportingSegmentEditorWidget, self).__init__(parent) + + def setup(self): + super(ReportingSegmentEditorWidget, self).setup() + self.reloadCollapsibleButton.hide() + self.hideUnwantedEditorUIElements() + self.reorganizeEffectButtons() + self.clearSegmentationEditorSelectors() + self.setupConnections() + + def setupConnections(self): + segmentsTableView = slicer.util.findChildren(self.editor, "SegmentsTableView")[0] + segmentsTableView.selectionChanged.connect(self.onSegmentSelected) + + def onSegmentSelected(self, item): + try: + # TODO: center on the segmentation + print item.indexes()[0] + except IndexError: + pass + + def clearSegmentationEditorSelectors(self): + self.editor.setSegmentationNode(None) + self.editor.setMasterVolumeNode(None) + + def hideUnwantedEditorUIElements(self): + self.editor.segmentationNodeSelectorVisible = False + self.editor.masterVolumeNodeSelectorVisible = False + for widgetName in ["OptionsGroupBox", "MaskingGroupBox"]: + widget = slicer.util.findChildren(self.editor, widgetName)[0] + widget.hide() + + def reorganizeEffectButtons(self): + widget = slicer.util.findChildren(self.editor, "EffectsGroupBox")[0] + if widget: + buttons = [b for b in widget.children() if isinstance(b, qt.QPushButton)] + self.layout.addWidget(self.createHLayout(buttons)) + widget.hide() + undo = slicer.util.findChildren(self.editor, "UndoButton")[0] + redo = slicer.util.findChildren(self.editor, "RedoButton")[0] + 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() \ No newline at end of file From 8a19dca3fc0b744c9a7e01ad24715f18161f94de Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Thu, 22 Sep 2016 17:38:29 -0400 Subject: [PATCH 10/39] ENH: On click into segment table jump to segment centroid as far as pixel data is already available(issue #74) --- Py/Reporting.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 0a0e8e4..a2202b7 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -1,4 +1,6 @@ import getpass +import SimpleITK as sitk +import sitkUtils import json from slicer.ScriptedLoadableModule import * @@ -118,8 +120,8 @@ def setupSegmentationsArea(self): self.segmentationWidget = qt.QGroupBox("Segmentations") self.segmentationWidgetLayout = qt.QFormLayout() self.segmentationWidget.setLayout(self.segmentationWidgetLayout) - self.editorWidget = ReportingSegmentEditorWidget(parent=self.segmentationWidget) - self.editorWidget.setup() + self.segmentEditorWidget = ReportingSegmentEditorWidget(parent=self.segmentationWidget) + self.segmentEditorWidget.setup() self.layout.addWidget(self.segmentationWidget) def setupMeasurementsArea(self): @@ -173,13 +175,13 @@ def onImageVolumeSelectorChanged(self, node): self.initializeWatchBox(node) if node: if node in self.segReferencedMasterVolume.keys(): - self.editorWidget.editor.setSegmentationNode(self.segNode) + self.segmentEditorWidget.editor.setSegmentationNode(self.segNode) else: self.segReferencedMasterVolume[node] = self.createNewSegmentation(node) self.segNode = self.segReferencedMasterVolume[node] self.setupSegmentationObservers() else: - self.clearSegmentationEditorSelectors() + self.segmentEditorWidget.clearSegmentationEditorSelectors() def initializeWatchBox(self, node): try: @@ -191,16 +193,15 @@ def initializeWatchBox(self, node): def createNewSegmentation(self, masterNode): segNode = slicer.vtkMRMLSegmentationNode() slicer.mrmlScene.AddNode(segNode) - self.editorWidget.editor.setSegmentationNode(segNode) - self.editorWidget.editor.setMasterVolumeNode(masterNode) + self.segmentEditorWidget.editor.setSegmentationNode(segNode) + self.segmentEditorWidget.editor.setMasterVolumeNode(masterNode) return segNode @logmethod() def onSegmentationNodeChanged(self, observer=None, caller=None): - if self.segmentationLabelMapDummy: - slicer.mrmlScene.RemoveNode(self.segmentationLabelMapDummy) - self.segmentationLabelMapDummy = slicer.vtkMRMLLabelMapVolumeNode() - slicer.mrmlScene.AddNode(self.segmentationLabelMapDummy) + if not self.segmentationLabelMapDummy: + self.segmentationLabelMapDummy = slicer.vtkMRMLLabelMapVolumeNode() + slicer.mrmlScene.AddNode(self.segmentationLabelMapDummy) if self.tableNode and self.tableNode.GetID() == self.logic.getActiveSlicerTableID(): slicer.mrmlScene.RemoveNode(self.tableNode) if self.segmentationsLogic.ExportAllSegmentsToLabelmapNode(self.segNode, self.segmentationLabelMapDummy): @@ -253,6 +254,7 @@ def calculateLabelStatistics(self, labelNode, grayscaleNode): labelStatisticsLogic = LabelStatisticsLogic(grayscaleNode, resampledLabelNode, colorNode=labelNode.GetDisplayNode().GetColorNode(), nodeBaseName=labelNode.GetName()) + slicer.mrmlScene.RemoveNode(resampledLabelNode) else: raise ValueError("Volumes do not have the same geometry.\n%s" % warnings) else: @@ -326,6 +328,17 @@ def test_Reporting1(self): class ReportingSegmentEditorWidget(SegmentEditorWidget, ModuleWidgetMixin): + @property + def segNode(self): + return self.editor.segmentationNode() + + @property + def table(self): + try: + return slicer.util.findChildren(self.editor, "SegmentsTableView")[0] + except IndexError: + return None + def __init__(self, parent): super(ReportingSegmentEditorWidget, self).__init__(parent) @@ -338,13 +351,29 @@ def setup(self): self.setupConnections() def setupConnections(self): - segmentsTableView = slicer.util.findChildren(self.editor, "SegmentsTableView")[0] - segmentsTableView.selectionChanged.connect(self.onSegmentSelected) + self.table.selectionChanged.connect(self.onSegmentSelected) def onSegmentSelected(self, item): try: # TODO: center on the segmentation - print item.indexes()[0] + segmentation = self.segNode.GetSegmentation() + segmentIDs = vtk.vtkStringArray() + segmentation.GetSegmentIDs(segmentIDs) + segmentID = segmentIDs.GetValue(item.indexes()[0].row()) # row + segment = segmentation.GetSegment(segmentID) + segmentationsLogic = slicer.modules.segmentations.logic() + tempLabel=slicer.vtkMRMLLabelMapVolumeNode() + slicer.mrmlScene.AddNode(tempLabel) + tempLabel.SetName(segment.GetName()+"CentroidHelper") + binData = segment.GetRepresentation("Binary labelmap") + extent = binData.GetExtent() + if extent[1] != -1 and extent[3] != -1 and extent[5] != -1: + segmentationsLogic.CreateLabelmapVolumeFromOrientedImageData(binData, tempLabel) + centroid = self.getCentroidForLabel(tempLabel) + if centroid: + markupsLogic = slicer.modules.markups.logic() + markupsLogic.JumpSlicesToLocation(centroid[0], centroid[1], centroid[2], False) + slicer.mrmlScene.RemoveNode(tempLabel) except IndexError: pass @@ -379,4 +408,24 @@ def enter(self): # Set parameter set node if absent self.selectParameterNode() - self.editor.updateWidgetFromMRML() \ No newline at end of file + self.editor.updateWidgetFromMRML() + + def getCentroidForLabel(self, label, index=1): + ls = sitk.LabelShapeStatisticsImageFilter() + dstLabelAddress = sitkUtils.GetSlicerITKReadWriteAddress(label.GetName()) + try: + dstLabelImage = sitk.ReadImage(dstLabelAddress) + except RuntimeError: + return None + ls.Execute(dstLabelImage) + centroid = ls.GetCentroid(index) + IJKtoRAS = vtk.vtkMatrix4x4() + label.GetIJKToRASMatrix(IJKtoRAS) + order = label.ComputeScanOrderFromIJKToRAS(IJKtoRAS) + if order == 'IS': + centroid = [-centroid[0], -centroid[1], centroid[2]] + elif order == 'AP': + centroid = [-centroid[0], -centroid[2], -centroid[1]] + elif order == 'LR': + centroid = [centroid[0], -centroid[2], -centroid[1]] + return centroid From 3f8dfa73b8ecc7a1a7a1bc85e3f20a97c4a629f7 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Thu, 22 Sep 2016 18:01:37 -0400 Subject: [PATCH 11/39] ENH: moved getCentroidForLabel to SlicerProstate logic mixin class for now (issue #74) --- Py/Reporting.py | 54 +++++++++++++++++------------------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index a2202b7..bfcea51 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -1,6 +1,4 @@ import getpass -import SimpleITK as sitk -import sitkUtils import json from slicer.ScriptedLoadableModule import * @@ -355,28 +353,30 @@ def setupConnections(self): def onSegmentSelected(self, item): try: - # TODO: center on the segmentation segmentation = self.segNode.GetSegmentation() segmentIDs = vtk.vtkStringArray() segmentation.GetSegmentIDs(segmentIDs) segmentID = segmentIDs.GetValue(item.indexes()[0].row()) # row segment = segmentation.GetSegment(segmentID) - segmentationsLogic = slicer.modules.segmentations.logic() - tempLabel=slicer.vtkMRMLLabelMapVolumeNode() - slicer.mrmlScene.AddNode(tempLabel) - tempLabel.SetName(segment.GetName()+"CentroidHelper") - binData = segment.GetRepresentation("Binary labelmap") - extent = binData.GetExtent() - if extent[1] != -1 and extent[3] != -1 and extent[5] != -1: - segmentationsLogic.CreateLabelmapVolumeFromOrientedImageData(binData, tempLabel) - centroid = self.getCentroidForLabel(tempLabel) - if centroid: - markupsLogic = slicer.modules.markups.logic() - markupsLogic.JumpSlicesToLocation(centroid[0], centroid[1], centroid[2], False) - slicer.mrmlScene.RemoveNode(tempLabel) + self.jumpToSegmentCenter(segment) except IndexError: pass + def jumpToSegmentCenter(self, segment): + segmentationsLogic = slicer.modules.segmentations.logic() + 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") + segmentationsLogic.CreateLabelmapVolumeFromOrientedImageData(binData, tempLabel) + centroid = ModuleLogicMixin.getCentroidForLabel(tempLabel, 1) + if centroid: + markupsLogic = slicer.modules.markups.logic() + markupsLogic.JumpSlicesToLocation(centroid[0], centroid[1], centroid[2], False) + slicer.mrmlScene.RemoveNode(tempLabel) + def clearSegmentationEditorSelectors(self): self.editor.setSegmentationNode(None) self.editor.setMasterVolumeNode(None) @@ -408,24 +408,4 @@ def enter(self): # Set parameter set node if absent self.selectParameterNode() - self.editor.updateWidgetFromMRML() - - def getCentroidForLabel(self, label, index=1): - ls = sitk.LabelShapeStatisticsImageFilter() - dstLabelAddress = sitkUtils.GetSlicerITKReadWriteAddress(label.GetName()) - try: - dstLabelImage = sitk.ReadImage(dstLabelAddress) - except RuntimeError: - return None - ls.Execute(dstLabelImage) - centroid = ls.GetCentroid(index) - IJKtoRAS = vtk.vtkMatrix4x4() - label.GetIJKToRASMatrix(IJKtoRAS) - order = label.ComputeScanOrderFromIJKToRAS(IJKtoRAS) - if order == 'IS': - centroid = [-centroid[0], -centroid[1], centroid[2]] - elif order == 'AP': - centroid = [-centroid[0], -centroid[2], -centroid[1]] - elif order == 'LR': - centroid = [centroid[0], -centroid[2], -centroid[1]] - return centroid + self.editor.updateWidgetFromMRML() \ No newline at end of file From cba95981eab21eba3c8ebdd0089a5567bc157c1c Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Fri, 23 Sep 2016 21:37:05 -0400 Subject: [PATCH 12/39] ENH: added view setting buttons for layout, window level and crosshair (issue #74) --- Py/Reporting.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index bfcea51..b43beb0 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -8,6 +8,7 @@ from SlicerProstateUtils.decorators import logmethod from SlicerProstateUtils.helpers import WatchBoxAttribute, DICOMBasedInformationWatchBox from SlicerProstateUtils.constants import DICOMTAGS +from SlicerProstateUtils.buttons import * from SegmentEditor import SegmentEditorWidget from LabelStatistics import LabelStatisticsLogic @@ -62,16 +63,24 @@ def initializeMembers(self): self.segmentationLabelMapDummy = None def onReload(self): + self.cleanupUIElements() + self.removeAllUIElements() super(ReportingWidget, self).onReload() - self.cleanup() - def cleanup(self): + def cleanupUIElements(self): self.removeSegmentationObserver() self.removeConnections() if self.tableNode: slicer.mrmlScene.RemoveNode(self.tableNode) 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 setup(self): ScriptedLoadableModuleWidget.setup(self) @@ -112,7 +121,20 @@ def setupSelectionArea(self): self.layout.addWidget(self.selectionAreaWidget) def setupViewSettingsArea(self): - pass + self.redSliceLayoutButton = RedSliceLayoutButton() + self.fourUpSliceLayoutButton = FourUpLayoutButton() + self.crosshairButton = CrosshairButton() + self.windowLevelEffectsButton = WindowLevelEffectsButton() + + self.layoutButtonGroup = qt.QButtonGroup() + self.layoutButtonGroup.addButton(self.redSliceLayoutButton, self.redSliceLayoutButton.LAYOUT) + self.layoutButtonGroup.addButton(self.fourUpSliceLayoutButton, self.fourUpSliceLayoutButton.LAYOUT) + self.layoutButtonGroup.setExclusive(False) + + hbox = self.createHLayout([self.redSliceLayoutButton, self.fourUpSliceLayoutButton, self.crosshairButton, + self.windowLevelEffectsButton]) + hbox.layout().addStretch(1) + self.layout.addWidget(hbox) def setupSegmentationsArea(self): self.segmentationWidget = qt.QGroupBox("Segmentations") From c687129dfbde49de43c1ffc130960ad1804a722b Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Mon, 26 Sep 2016 14:41:11 -0400 Subject: [PATCH 13/39] BUG: adding dependency to SlicerProstate --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) 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) From 286b4636a947258954d4335042894496d1e8e640 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Mon, 26 Sep 2016 17:31:59 -0400 Subject: [PATCH 14/39] ENH: Reorganized SegExportSelfTest for being able to use sample data in Reporting widget (issue #74) - added button for loading data rather than doing it manually for development purposes --- Py/Reporting.py | 37 ++++++++++++++++++++++++++++++ Py/SEGExporterSelfTest.py | 58 +++++++++++++++++++++++++++++++---------------- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index b43beb0..7c03b25 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -86,6 +86,7 @@ def setup(self): self.initializeMembers() self.setupWatchbox() + self.setupTestArea() self.setupSelectionArea() self.setupViewSettingsArea() self.setupSegmentationsArea() @@ -103,6 +104,30 @@ def setupWatchbox(self): self.watchBox = DICOMBasedInformationWatchBox(self.watchBoxInformation) self.layout.addWidget(self.watchBox) + def setupTestArea(self): + + def loadTestData(): + from SEGExporterSelfTest import SEGExporterSelfTestLogic + sampleData = SEGExporterSelfTestLogic.downloadSampleData() + unzipped = SEGExporterSelfTestLogic.unzipSampleData(sampleData) + SEGExporterSelfTestLogic.importIntoDICOMDatabase(unzipped) + dicomWidget = slicer.modules.dicom.widgetRepresentation().self() + mrHeadSeriesUID = "2.16.840.1.113662.4.4168496325.1025306170.548651188813145058" + dicomWidget.detailsPopup.offerLoadables(mrHeadSeriesUID, 'Series') + dicomWidget.detailsPopup.examineForLoading() + print 'Loading Selection' + dicomWidget.detailsPopup.loadCheckedLoadables() + masterNode = slicer.util.getNode('2: SAG*') + 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 setupSelectionArea(self): self.imageVolumeSelector = self.createComboBox(nodeTypes=["vtkMRMLScalarVolumeNode", ""], showChildNodeTypes=False, selectNodeUponCreation=True, toolTip="Select image volume to annotate") @@ -290,6 +315,18 @@ def calculateLabelStatistics(self, labelNode, grayscaleNode): def getActiveSlicerTableID(self): return slicer.app.applicationLogic().GetSelectionNode().GetActiveTableID() + def createJSON(self): + data = {} + + print json.dumps(data, indent = 4, separators = (',', ': ')) + + return "" + + def loadFromJSON(self, data): + # TODO: think about what and how to load the data + + pass + class ReportingTest(ScriptedLoadableModuleTest): """ 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 From 9b7bc2117b07f6c6fb186fa5f5c7c69534d9e08c Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Mon, 26 Sep 2016 17:48:17 -0400 Subject: [PATCH 15/39] BUG: fixed Slicer crashing when all segments out of the segments list got deleted (issue #142) --- Py/Reporting.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 7c03b25..9def543 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -415,9 +415,10 @@ def onSegmentSelected(self, item): segmentation = self.segNode.GetSegmentation() segmentIDs = vtk.vtkStringArray() segmentation.GetSegmentIDs(segmentIDs) - segmentID = segmentIDs.GetValue(item.indexes()[0].row()) # row - segment = segmentation.GetSegment(segmentID) - self.jumpToSegmentCenter(segment) + if segmentIDs.GetNumberOfValues(): + segmentID = segmentIDs.GetValue(item.indexes()[0].row()) # row + segment = segmentation.GetSegment(segmentID) + self.jumpToSegmentCenter(segment) except IndexError: pass From 85df41f73163ea16f8fbb1a88e7503f97f8652b6 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 28 Sep 2016 22:17:29 -0400 Subject: [PATCH 16/39] ENH: Reorganized Selectors, started with json implementation for segmentation meta information (issue #74) - based on layout showing measurements table in slice view or module UI --- Py/Reporting.py | 113 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 39 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 9def543..23c852f 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -5,7 +5,7 @@ import vtkSegmentationCorePython as vtkCoreSeg from SlicerProstateUtils.mixins import * -from SlicerProstateUtils.decorators import logmethod +from SlicerProstateUtils.decorators import * from SlicerProstateUtils.helpers import WatchBoxAttribute, DICOMBasedInformationWatchBox from SlicerProstateUtils.constants import DICOMTAGS from SlicerProstateUtils.buttons import * @@ -52,13 +52,12 @@ def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) self.logic = ReportingLogic() self.segmentationsLogic = slicer.modules.segmentations.logic() - self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? def initializeMembers(self): + self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? self.tNode = None self.tableNode = None self.segNode = None - self.displayTableInSliceView = False self.segmentationObservers = [] self.segmentationLabelMapDummy = None @@ -81,6 +80,14 @@ def removeAllUIElements(self): except AttributeError: pass + def exit(self): + # TODO: export SEG and SR + # TODO: disconnect from segment editor events + self.removeSegmentationObserver() + + def enter(self): + self.setupSegmentationObservers() + def setup(self): ScriptedLoadableModuleWidget.setup(self) @@ -94,6 +101,10 @@ def setup(self): self.setupActionButtons() self.setupConnections() self.layout.addStretch(1) + self.fourUpSliceLayoutButton.checked = True + + # TEST MODE + # self.retrieveTestDataButton.click() def setupWatchbox(self): self.watchBoxInformation = [ @@ -139,24 +150,21 @@ def setupSelectionArea(self): self.selectionAreaWidgetLayout = qt.QGridLayout() self.selectionAreaWidget.setLayout(self.selectionAreaWidgetLayout) - self.selectionAreaWidgetLayout.addWidget(qt.QLabel("Image volume to annotate"), 0, 0) - self.selectionAreaWidgetLayout.addWidget(self.imageVolumeSelector, 0, 1) - self.selectionAreaWidgetLayout.addWidget(qt.QLabel("Measurement report"), 1, 0) - self.selectionAreaWidgetLayout.addWidget(self.measurementReportSelector, 1, 1) + 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) def setupViewSettingsArea(self): self.redSliceLayoutButton = RedSliceLayoutButton() self.fourUpSliceLayoutButton = FourUpLayoutButton() + self.fourUpSliceTableViewLayoutButton = FourUpTableViewLayoutButton() self.crosshairButton = CrosshairButton() self.windowLevelEffectsButton = WindowLevelEffectsButton() - self.layoutButtonGroup = qt.QButtonGroup() - self.layoutButtonGroup.addButton(self.redSliceLayoutButton, self.redSliceLayoutButton.LAYOUT) - self.layoutButtonGroup.addButton(self.fourUpSliceLayoutButton, self.fourUpSliceLayoutButton.LAYOUT) - self.layoutButtonGroup.setExclusive(False) - - hbox = self.createHLayout([self.redSliceLayoutButton, self.fourUpSliceLayoutButton, self.crosshairButton, + hbox = self.createHLayout([self.redSliceLayoutButton, self.fourUpSliceLayoutButton, + self.fourUpSliceTableViewLayoutButton, self.crosshairButton, self.windowLevelEffectsButton]) hbox.layout().addStretch(1) self.layout.addWidget(hbox) @@ -192,21 +200,14 @@ def setupButtonConnections(): getattr(self.saveReportButton.clicked, funcName)(self.onSaveReportButtonClicked) getattr(self.completeReportButton.clicked, funcName)(self.onCompleteReportButtonClicked) + getattr(self.layoutManager.layoutChanged, funcName)(self.onLayoutChanged) + setupSelectorConnections() setupButtonConnections() def removeConnections(self): self.setupConnections(funcName="disconnect") - def setupSegmentationObservers(self): - if self.segmentation: - self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentAdded, - self.onSegmentationNodeChanged)) - self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentRemoved, - self.onSegmentationNodeChanged)) - self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.MasterRepresentationModified, - self.onSegmentationNodeChanged)) - def removeSegmentationObserver(self): if self.segmentation and len(self.segmentationObservers): for observer in self.segmentationObservers: @@ -214,6 +215,9 @@ def removeSegmentationObserver(self): self.segmentationObservers = [] self.segNode = None + def onLayoutChanged(self, layout): + self.onDisplayMeasurementsTable() + def onImageVolumeSelectorChanged(self, node): # TODO: save, cleanup open sessions self.removeSegmentationObserver() @@ -228,6 +232,15 @@ def onImageVolumeSelectorChanged(self, node): else: self.segmentEditorWidget.clearSegmentationEditorSelectors() + def setupSegmentationObservers(self): + if self.segmentation: + self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentAdded, + self.onSegmentationNodeChanged)) + self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentRemoved, + self.onSegmentationNodeChanged)) + self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.MasterRepresentationModified, + self.onSegmentationNodeChanged)) + def initializeWatchBox(self, node): try: dicomFileName = node.GetStorageNode().GetFileName() @@ -255,17 +268,23 @@ def onSegmentationNodeChanged(self, observer=None, caller=None): if self.tableNode: slicer.mrmlScene.RemoveNode(self.tableNode) self.tableNode = self.logic.calculateLabelStatistics(self.segmentationLabelMapDummy, grayscaleNode) + self.tableNode.SetLocked(True) self.tableView.setMRMLTableNode(self.tableNode) - if self.displayTableInSliceView: - slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpTableView) - slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveTableID(self.tableNode.GetID()) - slicer.app.applicationLogic().PropagateTableSelection() + self.onDisplayMeasurementsTable() except ValueError as exc: slicer.util.warnDisplay(exc.message, windowTitle="Label Statistics") else: self.tableView.setMRMLTableNode(None) + def onDisplayMeasurementsTable(self): + self.measurementsWidget.visible = not self.layoutManager.layout == self.fourUpSliceTableViewLayoutButton.LAYOUT + if self.layoutManager.layout == self.fourUpSliceTableViewLayoutButton.LAYOUT: + if self.tableNode: + slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveTableID(self.tableNode.GetID()) + slicer.app.applicationLogic().PropagateTableSelection() + def onSaveReportButtonClicked(self): + self.createJSON() print "on save report button clicked" def onCompleteReportButtonClicked(self): @@ -275,6 +294,20 @@ def onAnnotationReady(self): #TODO: calc measurements (logic) and set table node pass + def createJSON(self): + + data = {} + data["seriesAttributes"] = self._getSeriesAttributes() + + print json.dumps(data, indent = 2, separators = (',', ': ')) + + return "" + + def _getSeriesAttributes(self): + data = {} + data["ContentCreatorName"] = self.watchBox.getAttribute("Reader").value + return data + class ReportingLogic(ScriptedLoadableModuleLogic): """This class should implement all the actual @@ -315,13 +348,6 @@ def calculateLabelStatistics(self, labelNode, grayscaleNode): def getActiveSlicerTableID(self): return slicer.app.applicationLogic().GetSelectionNode().GetActiveTableID() - def createJSON(self): - data = {} - - print json.dumps(data, indent = 4, separators = (',', ': ')) - - return "" - def loadFromJSON(self, data): # TODO: think about what and how to load the data @@ -391,14 +417,23 @@ def segNode(self): @property def table(self): - try: - return slicer.util.findChildren(self.editor, "SegmentsTableView")[0] - except IndexError: - return None + return self.find("SegmentsTableView") + + @property + @onExceptionReturnNone + def tableWidget(self): + return self.table.tableWidget() def __init__(self, parent): super(ReportingSegmentEditorWidget, self).__init__(parent) + @onExceptionReturnNone + def find(self, objectName): + return self.findAll(objectName)[0] + + def findAll(self, objectName): + return slicer.util.findChildren(self.editor, objectName) + def setup(self): super(ReportingSegmentEditorWidget, self).setup() self.reloadCollapsibleButton.hide() @@ -408,7 +443,7 @@ def setup(self): self.setupConnections() def setupConnections(self): - self.table.selectionChanged.connect(self.onSegmentSelected) + self.tableWidget.itemClicked.connect(self.onSegmentSelected) def onSegmentSelected(self, item): try: @@ -416,7 +451,7 @@ def onSegmentSelected(self, item): segmentIDs = vtk.vtkStringArray() segmentation.GetSegmentIDs(segmentIDs) if segmentIDs.GetNumberOfValues(): - segmentID = segmentIDs.GetValue(item.indexes()[0].row()) # row + segmentID = segmentIDs.GetValue(item.row()) # row segment = segmentation.GetSegment(segmentID) self.jumpToSegmentCenter(segment) except IndexError: From 195c7cf7e6fa16c14c9a3f54b2f04ec06bd3cae9 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Sat, 8 Oct 2016 12:12:01 -0400 Subject: [PATCH 17/39] ENH: disabling UIElements when no measurement report is selected (issue #74) - setting image volume to None as well - removed WindowLevelEffectButton --- Py/Reporting.py | 90 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 23c852f..f422065 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -88,6 +88,12 @@ def exit(self): def enter(self): self.setupSegmentationObservers() + def refreshUIElementsAvailability(self): + self.imageVolumeSelector.enabled = self.measurementReportSelector.currentNode() is not None + self.segmentationGroupBox.enabled = self.imageVolumeSelector.currentNode() is not None + self.measurementsGroupBox.enabled = False # TODO: enabled if has measurements + + @postCall(refreshUIElementsAvailability) def setup(self): ScriptedLoadableModuleWidget.setup(self) @@ -129,6 +135,9 @@ def loadTestData(): print 'Loading Selection' dicomWidget.detailsPopup.loadCheckedLoadables() masterNode = slicer.util.getNode('2: SAG*') + tableNode = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(tableNode) + self.measurementReportSelector.setCurrentNode(tableNode) self.imageVolumeSelector.setCurrentNode(masterNode) self.retrieveTestDataButton.enabled = False @@ -141,9 +150,11 @@ def loadTestData(): def setupSelectionArea(self): self.imageVolumeSelector = self.createComboBox(nodeTypes=["vtkMRMLScalarVolumeNode", ""], showChildNodeTypes=False, - selectNodeUponCreation=True, toolTip="Select image volume to annotate") + selectNodeUponCreation=True, enabled=False, + toolTip="Select image volume to annotate") self.imageVolumeSelector.addAttribute("vtkMRMLScalarVolumeNode", "DICOM.instanceUIDs", None) self.measurementReportSelector = self.createComboBox(nodeTypes=["vtkMRMLTableNode", ""], showChildNodeTypes=False, + addEnabled=True, removeEnabled=True, noneEnabled=True, selectNodeUponCreation=True, toolTip="Select measurement report") self.imageVolumeSelector.addAttribute("vtkMRMLTableNode", "Reporting", None) self.selectionAreaWidget = qt.QWidget() @@ -161,30 +172,28 @@ def setupViewSettingsArea(self): self.fourUpSliceLayoutButton = FourUpLayoutButton() self.fourUpSliceTableViewLayoutButton = FourUpTableViewLayoutButton() self.crosshairButton = CrosshairButton() - self.windowLevelEffectsButton = WindowLevelEffectsButton() hbox = self.createHLayout([self.redSliceLayoutButton, self.fourUpSliceLayoutButton, - self.fourUpSliceTableViewLayoutButton, self.crosshairButton, - self.windowLevelEffectsButton]) + self.fourUpSliceTableViewLayoutButton, self.crosshairButton]) hbox.layout().addStretch(1) self.layout.addWidget(hbox) def setupSegmentationsArea(self): - self.segmentationWidget = qt.QGroupBox("Segmentations") - self.segmentationWidgetLayout = qt.QFormLayout() - self.segmentationWidget.setLayout(self.segmentationWidgetLayout) - self.segmentEditorWidget = ReportingSegmentEditorWidget(parent=self.segmentationWidget) + self.segmentationGroupBox = qt.QGroupBox("Segmentations") + self.segmentationGroupBoxLayout = qt.QFormLayout() + self.segmentationGroupBox.setLayout(self.segmentationGroupBoxLayout) + self.segmentEditorWidget = ReportingSegmentEditorWidget(parent=self.segmentationGroupBox) self.segmentEditorWidget.setup() - self.layout.addWidget(self.segmentationWidget) + self.layout.addWidget(self.segmentationGroupBox) def setupMeasurementsArea(self): - self.measurementsWidget = qt.QGroupBox("Measurements") - self.measurementsWidgetLayout = qt.QVBoxLayout() - self.measurementsWidget.setLayout(self.measurementsWidgetLayout) + self.measurementsGroupBox = qt.QGroupBox("Measurements") + self.measurementsGroupBoxLayout = qt.QVBoxLayout() + self.measurementsGroupBox.setLayout(self.measurementsGroupBoxLayout) self.tableView = slicer.qMRMLTableView() self.tableView.minimumHeight = 150 - self.measurementsWidgetLayout.addWidget(self.tableView) - self.layout.addWidget(self.measurementsWidget) + self.measurementsGroupBoxLayout.addWidget(self.tableView) + self.layout.addWidget(self.measurementsGroupBox) def setupActionButtons(self): self.saveReportButton = self.createButton("Save Report") @@ -194,7 +203,8 @@ def setupActionButtons(self): def setupConnections(self, funcName="connect"): def setupSelectorConnections(): - getattr(self.imageVolumeSelector, funcName)('currentNodeChanged(vtkMRMLNode*)', self.onImageVolumeSelectorChanged) + 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) @@ -210,36 +220,44 @@ def removeConnections(self): def removeSegmentationObserver(self): if self.segmentation and len(self.segmentationObservers): - for observer in self.segmentationObservers: + while len(self.segmentationObservers): + observer = self.segmentationObservers.pop() self.segmentation.RemoveObserver(observer) - self.segmentationObservers = [] self.segNode = None def onLayoutChanged(self, layout): self.onDisplayMeasurementsTable() - def onImageVolumeSelectorChanged(self, node): + @priorCall(refreshUIElementsAvailability) + def onImageVolumeSelected(self, node): # TODO: save, cleanup open sessions self.removeSegmentationObserver() self.initializeWatchBox(node) - if node: - if node in self.segReferencedMasterVolume.keys(): - self.segmentEditorWidget.editor.setSegmentationNode(self.segNode) - else: - self.segReferencedMasterVolume[node] = self.createNewSegmentation(node) - self.segNode = self.segReferencedMasterVolume[node] - self.setupSegmentationObservers() - else: + if not node: self.segmentEditorWidget.clearSegmentationEditorSelectors() + return + if node in self.segReferencedMasterVolume.keys(): + self.segmentEditorWidget.editor.setSegmentationNode(self.segNode) + else: + self.segReferencedMasterVolume[node] = self.createNewSegmentation(node) + self.segNode = self.segReferencedMasterVolume[node] + self.setupSegmentationObservers() + + @priorCall(refreshUIElementsAvailability) + def onMeasurementReportSelected(self, node): + if node is None: + self.imageVolumeSelector.setCurrentNode(None) + # TODO: create reference to segmentation node + # TODO: segmentationNode holds references to volume + pass def setupSegmentationObservers(self): - if self.segmentation: - self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentAdded, - self.onSegmentationNodeChanged)) - self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.SegmentRemoved, - self.onSegmentationNodeChanged)) - self.segmentationObservers.append(self.segmentation.AddObserver(vtkCoreSeg.vtkSegmentation.MasterRepresentationModified, - self.onSegmentationNodeChanged)) + if not self.segmentation: + return + segmentationEvents = [vtkCoreSeg.vtkSegmentation.SegmentAdded, vtkCoreSeg.vtkSegmentation.SegmentRemoved, + vtkCoreSeg.vtkSegmentation.MasterRepresentationModified] + for event in segmentationEvents: + self.segmentationObservers.append(self.segmentation.AddObserver(event, self.onSegmentationNodeChanged)) def initializeWatchBox(self, node): try: @@ -255,7 +273,6 @@ def createNewSegmentation(self, masterNode): self.segmentEditorWidget.editor.setMasterVolumeNode(masterNode) return segNode - @logmethod() def onSegmentationNodeChanged(self, observer=None, caller=None): if not self.segmentationLabelMapDummy: self.segmentationLabelMapDummy = slicer.vtkMRMLLabelMapVolumeNode() @@ -277,7 +294,7 @@ def onSegmentationNodeChanged(self, observer=None, caller=None): self.tableView.setMRMLTableNode(None) def onDisplayMeasurementsTable(self): - self.measurementsWidget.visible = not self.layoutManager.layout == self.fourUpSliceTableViewLayoutButton.LAYOUT + self.measurementsGroupBox.visible = not self.layoutManager.layout == self.fourUpSliceTableViewLayoutButton.LAYOUT if self.layoutManager.layout == self.fourUpSliceTableViewLayoutButton.LAYOUT: if self.tableNode: slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveTableID(self.tableNode.GetID()) @@ -479,9 +496,6 @@ def clearSegmentationEditorSelectors(self): def hideUnwantedEditorUIElements(self): self.editor.segmentationNodeSelectorVisible = False self.editor.masterVolumeNodeSelectorVisible = False - for widgetName in ["OptionsGroupBox", "MaskingGroupBox"]: - widget = slicer.util.findChildren(self.editor, widgetName)[0] - widget.hide() def reorganizeEffectButtons(self): widget = slicer.util.findChildren(self.editor, "EffectsGroupBox")[0] From 1f572b77398e9096746f15ce3b3c052b2809f03b Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 11 Oct 2016 13:04:22 -0700 Subject: [PATCH 18/39] ENH: moved editor effect buttons right below segments table --- Py/Reporting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Py/Reporting.py b/Py/Reporting.py index f422065..d3456b6 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -509,6 +509,9 @@ def reorganizeEffectButtons(self): undo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) redo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) self.layout.addWidget(self.createHLayout([undo, redo])) + for widgetName in ["OptionsGroupBox", "MaskingGroupBox"]: + widget = slicer.util.findChildren(self.editor, widgetName)[0] + self.layout.addWidget(widget) def enter(self): # overridden because SegmentEditorWidget automatically creates a new Segmentation upon instantiation From 769ac016c3830577a42056e9daf87a2de9983820 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 11 Oct 2016 15:01:27 -0700 Subject: [PATCH 19/39] ENH: hiding "Black" label and "Index" column from measurements table - added "Segment Name": - it would make sense to access segments from the label statistics logic in order to retrieve the segment names - deriving from LabelStatisticsLogic in order to customize table easier --- Py/Reporting.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index d3456b6..2e4f769 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -91,7 +91,7 @@ def enter(self): def refreshUIElementsAvailability(self): self.imageVolumeSelector.enabled = self.measurementReportSelector.currentNode() is not None self.segmentationGroupBox.enabled = self.imageVolumeSelector.currentNode() is not None - self.measurementsGroupBox.enabled = False # TODO: enabled if has measurements + self.measurementsGroupBox.enabled = self.imageVolumeSelector.currentNode() is not None # TODO: enabled if has measurements @postCall(refreshUIElementsAvailability) def setup(self): @@ -346,9 +346,9 @@ def calculateLabelStatistics(self, labelNode, grayscaleNode): if 'mismatch' in warnings: resampledLabelNode = self.volumesLogic.ResampleVolumeToReferenceVolume(labelNode, grayscaleNode) # resampledLabelNode does not have a display node, therefore the colorNode has to be passed to it - labelStatisticsLogic = LabelStatisticsLogic(grayscaleNode, resampledLabelNode, - colorNode=labelNode.GetDisplayNode().GetColorNode(), - nodeBaseName=labelNode.GetName()) + labelStatisticsLogic = CustomLabelStatisticsLogic(grayscaleNode, resampledLabelNode, + colorNode=labelNode.GetDisplayNode().GetColorNode(), + nodeBaseName=labelNode.GetName()) slicer.mrmlScene.RemoveNode(resampledLabelNode) else: raise ValueError("Volumes do not have the same geometry.\n%s" % warnings) @@ -422,7 +422,7 @@ def test_Reporting1(self): volumeNode = slicer.util.getNode(pattern="FA") logic = ReportingLogic() - self.assertIsNotNone( logic.hasImageData(volumeNode) ) + self.assertIsNotNone(logic.hasImageData(volumeNode)) self.delayDisplay('Test passed!') @@ -520,4 +520,46 @@ def enter(self): # Set parameter set node if absent self.selectParameterNode() - self.editor.updateWidgetFromMRML() \ No newline at end of file + self.editor.updateWidgetFromMRML() + + +class CustomLabelStatisticsLogic(LabelStatisticsLogic): + + def __init__(self, grayscaleNode, labelNode, colorNode=None, nodeBaseName=None, fileName=None): + LabelStatisticsLogic.__init__(self, grayscaleNode, labelNode, colorNode, nodeBaseName, fileName) + # TODO: maybe provide segments here in order to directly set the segment names for the output table + + def exportToTable(self): + table = slicer.vtkMRMLTableNode() + table.SetUseColumnNameAsColumnHeader(True) + tableWasModified = table.StartModify() + + table.SetName(slicer.mrmlScene.GenerateUniqueName(self.nodeBaseName + ' statistics')) + + keys, labelStats = self.customizeLabelStats() + for k in keys: + col = table.AddColumn() + col.SetName(k) + for labelValue in labelStats["Labels"]: + if labelValue == 0: + continue + rowIndex = table.AddEmptyRow() + for columnIndex, k in enumerate(keys): + table.SetCellText(rowIndex, columnIndex, str(labelStats[labelValue, k])) + + table.EndModify(tableWasModified) + return table + + def customizeLabelStats(self): + colorNode = self.getColorNode() + if not colorNode: + return self.keys, self.labelStats + + labelStats = self.labelStats.copy() + keys = ["Segment Name"] + list(self.keys) + keys.remove("Index") + + for labelValue in labelStats["Labels"]: + # TODO: get segment name directly from segments + labelStats[labelValue, "Segment Name"] = self.colorNode.GetColorName(labelValue) + return keys, labelStats \ No newline at end of file From 15fa353979eca98ab1afe1838a12f58f34ff6011 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 11 Oct 2016 21:53:08 -0700 Subject: [PATCH 20/39] ENH: delivering segments into label statistics in order to retrieve segment names - TODO: it might make sense to move code for converting segments into labelmaps to CustomLabelStatisticsLogic --- Py/Reporting.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 2e4f769..306ee80 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -284,7 +284,7 @@ def onSegmentationNodeChanged(self, observer=None, caller=None): try: if self.tableNode: slicer.mrmlScene.RemoveNode(self.tableNode) - self.tableNode = self.logic.calculateLabelStatistics(self.segmentationLabelMapDummy, grayscaleNode) + self.tableNode = self.logic.calculateLabelStatistics(self.segNode, self.segmentationLabelMapDummy, grayscaleNode) self.tableNode.SetLocked(True) self.tableView.setMRMLTableNode(self.tableNode) self.onDisplayMeasurementsTable() @@ -340,28 +340,35 @@ def __init__(self, parent=None): ScriptedLoadableModuleLogic.__init__(self, parent) self.volumesLogic = slicer.modules.volumes.logic() - def calculateLabelStatistics(self, labelNode, grayscaleNode): + def calculateLabelStatistics(self, segNode, labelNode, grayscaleNode): warnings = self.volumesLogic.CheckForLabelVolumeValidity(grayscaleNode, labelNode) if warnings != "": if 'mismatch' in warnings: resampledLabelNode = self.volumesLogic.ResampleVolumeToReferenceVolume(labelNode, grayscaleNode) # resampledLabelNode does not have a display node, therefore the colorNode has to be passed to it - labelStatisticsLogic = CustomLabelStatisticsLogic(grayscaleNode, resampledLabelNode, + segments = self.getSegments(segNode) + self.labelStatisticsLogic = CustomLabelStatisticsLogic(segments, grayscaleNode, resampledLabelNode, colorNode=labelNode.GetDisplayNode().GetColorNode(), nodeBaseName=labelNode.GetName()) slicer.mrmlScene.RemoveNode(resampledLabelNode) else: raise ValueError("Volumes do not have the same geometry.\n%s" % warnings) else: - labelStatisticsLogic = LabelStatisticsLogic(grayscaleNode, labelNode) + self.labelStatisticsLogic = LabelStatisticsLogic(grayscaleNode, labelNode) # TODO: manually pick information from labelStatisticsLogic.labelStats - tableNode = labelStatisticsLogic.exportToTable() + tableNode = self.labelStatisticsLogic.exportToTable() tableNode.SetAttribute("Reporting", "Yes") slicer.mrmlScene.AddNode(tableNode) return tableNode + def getSegments(self, segNode): + segmentation = segNode.GetSegmentation() + segmentIDs = vtk.vtkStringArray() + segmentation.GetSegmentIDs(segmentIDs) + return [segmentation.GetSegment(segmentIDs.GetValue(idx)) for idx in range(segmentIDs.GetNumberOfValues())] + def getActiveSlicerTableID(self): return slicer.app.applicationLogic().GetSelectionNode().GetActiveTableID() @@ -525,8 +532,9 @@ def enter(self): class CustomLabelStatisticsLogic(LabelStatisticsLogic): - def __init__(self, grayscaleNode, labelNode, colorNode=None, nodeBaseName=None, fileName=None): + def __init__(self, segments, grayscaleNode, labelNode, colorNode=None, nodeBaseName=None, fileName=None): LabelStatisticsLogic.__init__(self, grayscaleNode, labelNode, colorNode, nodeBaseName, fileName) + self.segments = segments # TODO: maybe provide segments here in order to directly set the segment names for the output table def exportToTable(self): @@ -541,8 +549,6 @@ def exportToTable(self): col = table.AddColumn() col.SetName(k) for labelValue in labelStats["Labels"]: - if labelValue == 0: - continue rowIndex = table.AddEmptyRow() for columnIndex, k in enumerate(keys): table.SetCellText(rowIndex, columnIndex, str(labelStats[labelValue, k])) @@ -559,7 +565,11 @@ def customizeLabelStats(self): keys = ["Segment Name"] + list(self.keys) keys.remove("Index") - for labelValue in labelStats["Labels"]: - # TODO: get segment name directly from segments - labelStats[labelValue, "Segment Name"] = self.colorNode.GetColorName(labelValue) + try: + del labelStats["Labels"][0] # Black label + except KeyError: + pass + + for segment, labelValue in zip(self.segments, labelStats["Labels"]): + labelStats[labelValue, "Segment Name"] = segment.GetName() return keys, labelStats \ No newline at end of file From a6f52e4174633eb8f916ddc863596fc42b74e0a4 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 12 Oct 2016 10:36:08 -0700 Subject: [PATCH 21/39] ENH: adding checkbox for "auto update" and button for triggering calculation of segment statistics --- Py/Reporting.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 306ee80..9da9399 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -91,7 +91,14 @@ def enter(self): def refreshUIElementsAvailability(self): self.imageVolumeSelector.enabled = self.measurementReportSelector.currentNode() is not None self.segmentationGroupBox.enabled = self.imageVolumeSelector.currentNode() is not None - self.measurementsGroupBox.enabled = self.imageVolumeSelector.currentNode() is not None # TODO: enabled if has measurements + self.measurementsGroupBox.enabled = self.hasSegments() + + def hasSegments(self): + if not self.segNode: + return False + segmentIDs = vtk.vtkStringArray() + self.segNode.GetSegmentation().GetSegmentIDs(segmentIDs) + return segmentIDs.GetNumberOfValues() > 0 @postCall(refreshUIElementsAvailability) def setup(self): @@ -196,8 +203,12 @@ def setupMeasurementsArea(self): 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"): @@ -211,10 +222,15 @@ def setupButtonConnections(): getattr(self.completeReportButton.clicked, funcName)(self.onCompleteReportButtonClicked) getattr(self.layoutManager.layoutChanged, funcName)(self.onLayoutChanged) + getattr(self.calculateAutomaticallyCheckbox.toggled, funcName)(self.onCalcAutomaticallyToggled) setupSelectorConnections() setupButtonConnections() + def onCalcAutomaticallyToggled(self, checked): + if checked: + self.onSegmentationNodeChanged() + def removeConnections(self): self.setupConnections(funcName="disconnect") @@ -255,7 +271,8 @@ def setupSegmentationObservers(self): if not self.segmentation: return segmentationEvents = [vtkCoreSeg.vtkSegmentation.SegmentAdded, vtkCoreSeg.vtkSegmentation.SegmentRemoved, - vtkCoreSeg.vtkSegmentation.MasterRepresentationModified] + vtkCoreSeg.vtkSegmentation.SegmentModified, + vtkCoreSeg.vtkSegmentation.RepresentationModified] for event in segmentationEvents: self.segmentationObservers.append(self.segmentation.AddObserver(event, self.onSegmentationNodeChanged)) @@ -273,12 +290,18 @@ def createNewSegmentation(self, masterNode): self.segmentEditorWidget.editor.setMasterVolumeNode(masterNode) return segNode + @postCall(refreshUIElementsAvailability) def onSegmentationNodeChanged(self, observer=None, caller=None): if not self.segmentationLabelMapDummy: self.segmentationLabelMapDummy = slicer.vtkMRMLLabelMapVolumeNode() slicer.mrmlScene.AddNode(self.segmentationLabelMapDummy) if self.tableNode and self.tableNode.GetID() == self.logic.getActiveSlicerTableID(): slicer.mrmlScene.RemoveNode(self.tableNode) + + if not self.calculateAutomaticallyCheckbox.checked: + # TODO: mark table as old (maybe with styling border red) + return + if self.segmentationsLogic.ExportAllSegmentsToLabelmapNode(self.segNode, self.segmentationLabelMapDummy): grayscaleNode = self.segReferencedMasterVolume.keys()[self.segReferencedMasterVolume.values().index(self.segNode)] try: From 84060c7f83234fa3ce1a194291be945215bb3fee Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Thu, 13 Oct 2016 15:22:56 -0700 Subject: [PATCH 22/39] ENH: Reorganizing and moving functionality to ReportingSegmentEditorWidget and ReportingSegmentEditorLogic --- Py/Reporting.py | 284 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 140 insertions(+), 144 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 9da9399..1f27002 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -41,25 +41,14 @@ class ReportingWidget(ModuleWidgetMixin, ScriptedLoadableModuleWidget): https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - @property - def segmentation(self): - try: - return self.segNode.GetSegmentation() - except AttributeError: - return None - def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) - self.logic = ReportingLogic() self.segmentationsLogic = slicer.modules.segmentations.logic() def initializeMembers(self): self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? - self.tNode = None self.tableNode = None - self.segNode = None self.segmentationObservers = [] - self.segmentationLabelMapDummy = None def onReload(self): self.cleanupUIElements() @@ -91,14 +80,7 @@ def enter(self): def refreshUIElementsAvailability(self): self.imageVolumeSelector.enabled = self.measurementReportSelector.currentNode() is not None self.segmentationGroupBox.enabled = self.imageVolumeSelector.currentNode() is not None - self.measurementsGroupBox.enabled = self.hasSegments() - - def hasSegments(self): - if not self.segNode: - return False - segmentIDs = vtk.vtkStringArray() - self.segNode.GetSegmentation().GetSegmentIDs(segmentIDs) - return segmentIDs.GetNumberOfValues() > 0 + self.measurementsGroupBox.enabled = len(self.segmentEditorWidget.segments) @postCall(refreshUIElementsAvailability) def setup(self): @@ -227,18 +209,18 @@ def setupButtonConnections(): setupSelectorConnections() setupButtonConnections() + def removeConnections(self): + self.setupConnections(funcName="disconnect") + def onCalcAutomaticallyToggled(self, checked): if checked: self.onSegmentationNodeChanged() - def removeConnections(self): - self.setupConnections(funcName="disconnect") - def removeSegmentationObserver(self): - if self.segmentation and len(self.segmentationObservers): + if self.segmentEditorWidget.segmentation and len(self.segmentationObservers): while len(self.segmentationObservers): observer = self.segmentationObservers.pop() - self.segmentation.RemoveObserver(observer) + self.segmentEditorWidget.segmentation.RemoveObserver(observer) self.segNode = None def onLayoutChanged(self, layout): @@ -246,17 +228,15 @@ def onLayoutChanged(self, layout): @priorCall(refreshUIElementsAvailability) def onImageVolumeSelected(self, node): - # TODO: save, cleanup open sessions self.removeSegmentationObserver() + self.segmentEditorWidget.clearSegmentationEditorSelectors() self.initializeWatchBox(node) - if not node: - self.segmentEditorWidget.clearSegmentationEditorSelectors() - return if node in self.segReferencedMasterVolume.keys(): - self.segmentEditorWidget.editor.setSegmentationNode(self.segNode) + self.segmentEditorWidget.segmentationNode = self.segReferencedMasterVolume[node] else: - self.segReferencedMasterVolume[node] = self.createNewSegmentation(node) - self.segNode = self.segReferencedMasterVolume[node] + self.segReferencedMasterVolume[node] = self.createNewSegmentation() + self.segmentEditorWidget.segmentationNode = self.segReferencedMasterVolume[node] + self.segmentEditorWidget.masterVolumeNode = node self.setupSegmentationObservers() @priorCall(refreshUIElementsAvailability) @@ -268,13 +248,14 @@ def onMeasurementReportSelected(self, node): pass def setupSegmentationObservers(self): - if not self.segmentation: + 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(self.segmentation.AddObserver(event, self.onSegmentationNodeChanged)) + self.segmentationObservers.append(segNode.AddObserver(event, self.onSegmentationNodeChanged)) def initializeWatchBox(self, node): try: @@ -283,45 +264,32 @@ def initializeWatchBox(self, node): except AttributeError: self.watchBox.sourceFile = None - def createNewSegmentation(self, masterNode): + def createNewSegmentation(self): segNode = slicer.vtkMRMLSegmentationNode() slicer.mrmlScene.AddNode(segNode) - self.segmentEditorWidget.editor.setSegmentationNode(segNode) - self.segmentEditorWidget.editor.setMasterVolumeNode(masterNode) return segNode @postCall(refreshUIElementsAvailability) def onSegmentationNodeChanged(self, observer=None, caller=None): - if not self.segmentationLabelMapDummy: - self.segmentationLabelMapDummy = slicer.vtkMRMLLabelMapVolumeNode() - slicer.mrmlScene.AddNode(self.segmentationLabelMapDummy) - if self.tableNode and self.tableNode.GetID() == self.logic.getActiveSlicerTableID(): - slicer.mrmlScene.RemoveNode(self.tableNode) - if not self.calculateAutomaticallyCheckbox.checked: # TODO: mark table as old (maybe with styling border red) return - if self.segmentationsLogic.ExportAllSegmentsToLabelmapNode(self.segNode, self.segmentationLabelMapDummy): - grayscaleNode = self.segReferencedMasterVolume.keys()[self.segReferencedMasterVolume.values().index(self.segNode)] - try: - if self.tableNode: - slicer.mrmlScene.RemoveNode(self.tableNode) - self.tableNode = self.logic.calculateLabelStatistics(self.segNode, self.segmentationLabelMapDummy, grayscaleNode) - self.tableNode.SetLocked(True) - self.tableView.setMRMLTableNode(self.tableNode) - self.onDisplayMeasurementsTable() - except ValueError as exc: - slicer.util.warnDisplay(exc.message, windowTitle="Label Statistics") - else: - self.tableView.setMRMLTableNode(None) + table = self.segmentEditorWidget.calculateLabelStatistics(self.tableNode) + if table: + self.tableNode = table + self.tableNode.SetLocked(True) + 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: - if self.tableNode: - slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveTableID(self.tableNode.GetID()) - slicer.app.applicationLogic().PropagateTableSelection() + 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): self.createJSON() @@ -349,58 +317,6 @@ def _getSeriesAttributes(self): return data -class ReportingLogic(ScriptedLoadableModuleLogic): - """This class should implement all the actual - computation done by your module. The interface - should be such that other python code can import - this class and make use of the functionality without - requiring an instance of the Widget. - Uses ScriptedLoadableModuleLogic base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def __init__(self, parent=None): - ScriptedLoadableModuleLogic.__init__(self, parent) - self.volumesLogic = slicer.modules.volumes.logic() - - def calculateLabelStatistics(self, segNode, labelNode, grayscaleNode): - warnings = self.volumesLogic.CheckForLabelVolumeValidity(grayscaleNode, labelNode) - if warnings != "": - if 'mismatch' in warnings: - resampledLabelNode = self.volumesLogic.ResampleVolumeToReferenceVolume(labelNode, grayscaleNode) - # resampledLabelNode does not have a display node, therefore the colorNode has to be passed to it - segments = self.getSegments(segNode) - self.labelStatisticsLogic = CustomLabelStatisticsLogic(segments, grayscaleNode, resampledLabelNode, - colorNode=labelNode.GetDisplayNode().GetColorNode(), - nodeBaseName=labelNode.GetName()) - slicer.mrmlScene.RemoveNode(resampledLabelNode) - else: - raise ValueError("Volumes do not have the same geometry.\n%s" % warnings) - else: - self.labelStatisticsLogic = LabelStatisticsLogic(grayscaleNode, labelNode) - - # TODO: manually pick information from labelStatisticsLogic.labelStats - - tableNode = self.labelStatisticsLogic.exportToTable() - tableNode.SetAttribute("Reporting", "Yes") - slicer.mrmlScene.AddNode(tableNode) - return tableNode - - def getSegments(self, segNode): - segmentation = segNode.GetSegmentation() - segmentIDs = vtk.vtkStringArray() - segmentation.GetSegmentIDs(segmentIDs) - return [segmentation.GetSegment(segmentIDs.GetValue(idx)) for idx in range(segmentIDs.GetNumberOfValues())] - - def getActiveSlicerTableID(self): - return slicer.app.applicationLogic().GetSelectionNode().GetActiveTableID() - - def loadFromJSON(self, data): - # TODO: think about what and how to load the data - - pass - - class ReportingTest(ScriptedLoadableModuleTest): """ This is the test case for your scripted module. @@ -451,16 +367,35 @@ def test_Reporting1(self): self.delayDisplay('Finished with download and loading') volumeNode = slicer.util.getNode(pattern="FA") - logic = ReportingLogic() - self.assertIsNotNone(logic.hasImageData(volumeNode)) self.delayDisplay('Test passed!') class ReportingSegmentEditorWidget(SegmentEditorWidget, ModuleWidgetMixin): @property - def segNode(self): - return self.editor.segmentationNode() + def segmentationNode(self): + return self.editor.segmentationNode() + + @segmentationNode.setter + def segmentationNode(self, value): + self.editor.setSegmentationNode(value) + + @property + def masterVolumeNode(self): + return self.editor.masterVolumeNode() + + @masterVolumeNode.setter + def masterVolumeNode(self, value): + self.editor.setMasterVolumeNode(value) + + @property + @onExceptionReturnNone + def segmentation(self): + return self.segmentationNode.GetSegmentation() + + @property + def segments(self): + return self.logic.getSegments(self.segmentation) @property def table(self): @@ -471,9 +406,6 @@ def table(self): def tableWidget(self): return self.table.tableWidget() - def __init__(self, parent): - super(ReportingSegmentEditorWidget, self).__init__(parent) - @onExceptionReturnNone def find(self, objectName): return self.findAll(objectName)[0] @@ -481,11 +413,17 @@ def find(self, objectName): 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() @@ -494,30 +432,16 @@ def setupConnections(self): def onSegmentSelected(self, item): try: - segmentation = self.segNode.GetSegmentation() - segmentIDs = vtk.vtkStringArray() - segmentation.GetSegmentIDs(segmentIDs) - if segmentIDs.GetNumberOfValues(): - segmentID = segmentIDs.GetValue(item.row()) # row - segment = segmentation.GetSegment(segmentID) - self.jumpToSegmentCenter(segment) + segment = self.segments[item.row()] + self.jumpToSegmentCenter(segment) except IndexError: pass def jumpToSegmentCenter(self, segment): - segmentationsLogic = slicer.modules.segmentations.logic() - 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") - segmentationsLogic.CreateLabelmapVolumeFromOrientedImageData(binData, tempLabel) - centroid = ModuleLogicMixin.getCentroidForLabel(tempLabel, 1) - if centroid: - markupsLogic = slicer.modules.markups.logic() - markupsLogic.JumpSlicesToLocation(centroid[0], centroid[1], centroid[2], False) - slicer.mrmlScene.RemoveNode(tempLabel) + 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) @@ -528,20 +452,24 @@ def hideUnwantedEditorUIElements(self): self.editor.masterVolumeNodeSelectorVisible = False def reorganizeEffectButtons(self): - widget = slicer.util.findChildren(self.editor, "EffectsGroupBox")[0] + 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() - undo = slicer.util.findChildren(self.editor, "UndoButton")[0] - redo = slicer.util.findChildren(self.editor, "RedoButton")[0] + + 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])) - for widgetName in ["OptionsGroupBox", "MaskingGroupBox"]: - widget = slicer.util.findChildren(self.editor, widgetName)[0] - self.layout.addWidget(widget) def enter(self): # overridden because SegmentEditorWidget automatically creates a new Segmentation upon instantiation @@ -552,6 +480,74 @@ def enter(self): self.selectParameterNode() self.editor.updateWidgetFromMRML() + def calculateLabelStatistics(self, tableNode): + return self.logic.calculateLabelStatistics(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() + + 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 labelMapFromSegmentationNode(self, segNode): + labelNode = slicer.vtkMRMLLabelMapVolumeNode() + slicer.mrmlScene.AddNode(labelNode) + if not self.segmentationsLogic.ExportAllSegmentsToLabelmapNode(segNode, labelNode): + slicer.mrmlScene.RemoveNode(labelNode) + return None + return labelNode + + def calculateLabelStatistics(self, segNode, grayscaleNode, tableNode=None): + labelNode = self.labelMapFromSegmentationNode(segNode) + if not labelNode: + return None + segments = self.getSegments(segNode.GetSegmentation()) + warnings = self.volumesLogic.CheckForLabelVolumeValidity(grayscaleNode, labelNode) + if warnings != "": + if 'mismatch' in warnings: + resampledLabelNode = self.volumesLogic.ResampleVolumeToReferenceVolume(labelNode, grayscaleNode) + labelStatisticsLogic = CustomLabelStatisticsLogic(segments, grayscaleNode, resampledLabelNode, + colorNode=labelNode.GetDisplayNode().GetColorNode(), + nodeBaseName=labelNode.GetName()) + slicer.mrmlScene.RemoveNode(resampledLabelNode) + else: + raise ValueError("Volumes do not have the same geometry.\n%s" % warnings) + else: + labelStatisticsLogic = LabelStatisticsLogic(segments, grayscaleNode, labelNode) + + tNode = labelStatisticsLogic.exportToTable() + tNode.SetAttribute("Reporting", "Yes") + if not tableNode: + slicer.mrmlScene.AddNode(tableNode) + tableNode = tNode + else: + tableNode.Copy(tNode) + slicer.mrmlScene.RemoveNode(labelNode) + return tableNode + class CustomLabelStatisticsLogic(LabelStatisticsLogic): From e0d9bd6e9d46ef310496394a45e7e364b8f91b8b Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 18 Oct 2016 17:12:32 -0400 Subject: [PATCH 23/39] ENH: Generating JSON for DCMSegmentation for segments including terminology - also checking if segments are available and if actual pixel data is available TODO: get CodeValue and Designators TODO: replace seriesAttributes by real values --- Py/Reporting.py | 118 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 29 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 1f27002..b5dee9e 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -58,8 +58,6 @@ def onReload(self): def cleanupUIElements(self): self.removeSegmentationObserver() self.removeConnections() - if self.tableNode: - slicer.mrmlScene.RemoveNode(self.tableNode) self.initializeMembers() def removeAllUIElements(self): @@ -72,7 +70,8 @@ def removeAllUIElements(self): def exit(self): # TODO: export SEG and SR # TODO: disconnect from segment editor events - self.removeSegmentationObserver() + # self.removeSegmentationObserver() + pass def enter(self): self.setupSegmentationObservers() @@ -298,22 +297,31 @@ def onSaveReportButtonClicked(self): def onCompleteReportButtonClicked(self): print "on complete report button clicked" - def onAnnotationReady(self): - #TODO: calc measurements (logic) and set table node - pass - def createJSON(self): + # TODO: Create Json + # TODO: Save Segmentation to DICOMDatabase + # TODO: Save Structured Report to DICOMDatabase + data = dict() + try: + data["seriesAttributes"] = self._getSeriesAttributes() + data["segmentAttributes"] = self.segmentEditorWidget.logic.labelStatisticsLogic.generateJSON4SEG() + except (ValueError, AttributeError) as exc: + slicer.util.warningDisplay(exc.message if isinstance(exc, ValueError) else "No segments found") + return - data = {} - data["seriesAttributes"] = self._getSeriesAttributes() - - print json.dumps(data, indent = 2, separators = (',', ': ')) - - return "" + print json.dumps(data, indent=2, separators=(',', ': ')) def _getSeriesAttributes(self): - data = {} + # TODO: populate + data = dict() data["ContentCreatorName"] = self.watchBox.getAttribute("Reader").value + data["ClinicalTrialSeriesID"] = "Session1" + data["ClinicalTrialTimePointID"] = "1" + data["ClinicalTrialCoordinatingCenterName"] = "BWH" + data["SeriesDescription"] = "Segmentation" + data["SeriesNumber"] = ModuleLogicMixin.getDICOMValue(self.watchBox.sourceFile, DICOMTAGS.SERIES_NUMBER) + data["InstanceNumber"] = ModuleLogicMixin.getDICOMValue(self.watchBox.sourceFile, DICOMTAGS.INSTANCE_NUMBER) + return data @@ -491,6 +499,7 @@ def __init__(self, parent=None): self.parent = parent self.volumesLogic = slicer.modules.volumes.logic() self.segmentationsLogic = slicer.modules.segmentations.logic() + self.labelStatisticsLogic = None def getSegments(self, segmentation): if not segmentation: @@ -529,16 +538,16 @@ def calculateLabelStatistics(self, segNode, grayscaleNode, tableNode=None): if warnings != "": if 'mismatch' in warnings: resampledLabelNode = self.volumesLogic.ResampleVolumeToReferenceVolume(labelNode, grayscaleNode) - labelStatisticsLogic = CustomLabelStatisticsLogic(segments, grayscaleNode, resampledLabelNode, + self.labelStatisticsLogic = CustomLabelStatisticsLogic(segments, grayscaleNode, resampledLabelNode, colorNode=labelNode.GetDisplayNode().GetColorNode(), nodeBaseName=labelNode.GetName()) slicer.mrmlScene.RemoveNode(resampledLabelNode) else: raise ValueError("Volumes do not have the same geometry.\n%s" % warnings) else: - labelStatisticsLogic = LabelStatisticsLogic(segments, grayscaleNode, labelNode) + self.labelStatisticsLogic = CustomLabelStatisticsLogic(segments, grayscaleNode, labelNode) - tNode = labelStatisticsLogic.exportToTable() + tNode = self.labelStatisticsLogic.exportToTable() tNode.SetAttribute("Reporting", "Yes") if not tableNode: slicer.mrmlScene.AddNode(tableNode) @@ -563,32 +572,83 @@ def exportToTable(self): table.SetName(slicer.mrmlScene.GenerateUniqueName(self.nodeBaseName + ' statistics')) - keys, labelStats = self.customizeLabelStats() - for k in keys: + self.customizeLabelStats() + for k in self.keys: col = table.AddColumn() col.SetName(k) - for labelValue in labelStats["Labels"]: + for labelValue in self.labelStats["Labels"]: rowIndex = table.AddEmptyRow() - for columnIndex, k in enumerate(keys): - table.SetCellText(rowIndex, columnIndex, str(labelStats[labelValue, k])) + for columnIndex, k in enumerate(self.keys): + table.SetCellText(rowIndex, columnIndex, str(self.labelStats[labelValue, k])) table.EndModify(tableWasModified) return table + def filterEmptySegments(self): + return [s for s in self.segments if not self.isSegmentEmpty(s)] + def customizeLabelStats(self): colorNode = self.getColorNode() if not colorNode: return self.keys, self.labelStats - labelStats = self.labelStats.copy() - keys = ["Segment Name"] + list(self.keys) - keys.remove("Index") + self.keys = ["Segment Name"] + list(self.keys) + self.keys.remove("Index") try: - del labelStats["Labels"][0] # Black label + del self.labelStats["Labels"][0] except KeyError: pass - for segment, labelValue in zip(self.segments, labelStats["Labels"]): - labelStats[labelValue, "Segment Name"] = segment.GetName() - return keys, labelStats \ No newline at end of file + segments = self.filterEmptySegments() + + for segment, labelValue in zip(segments, self.labelStats["Labels"]): + self.labelStats[labelValue, "Segment Name"] = segment.GetName() + + def generateJSON4SEG(self): + self.validateSegments() + segmentsData = [] + segments = self.filterEmptySegments() + if not len(segments): + raise ValueError("No segments with pixel data found.") + for segment, labelValue in zip(segments, self.labelStats["Labels"]): + segmentData = dict() + segmentData["LabelID"] = labelValue + category = self.getTagValue(segment, segment.GetTerminologyCategoryTagName()) + segmentData["SegmentDescription"] = category if category != "" else segment.GetName() + segmentData["SegmentAlgorithmType"] = "MANUAL" + segmentData["recommendedDisplayRGBValue"] = segment.GetDefaultColor() + if category != "": + segmentData["SegmentedPropertyCategoryCodeSequence"] = { + "CodeValue": "T-D0050", # where to get that code from???? + "CodingSchemeDesignator": "SRT", + "CodeMeaning": category} + propType = self.getTagValue(segment, segment.GetTerminologyTypeTagName()) + if propType: + segmentData["SegmentedPropertyTypeCodeSequence"] = { + "CodeValue": "T-D0050", + "CodingSchemeDesignator": "SRT", + "CodeMeaning": propType} + segmentsData.append(segmentData) + return [segmentsData] + + def generateJSON4SR(self): + pass + + def validateSegments(self): + segments = self.filterEmptySegments() + for segment, labelValue in zip(segments, self.labelStats["Labels"]): + 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 From e785ea669f57a0346208185a5d5029d6cdd546ec Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 18 Oct 2016 23:21:13 -0400 Subject: [PATCH 24/39] ENH: Creation of JSON for Segmentation object by means of vtkSlicerTerminologiesModuleLogic --- Py/Reporting.py | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index b5dee9e..7eb2722 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -562,6 +562,7 @@ class CustomLabelStatisticsLogic(LabelStatisticsLogic): def __init__(self, segments, grayscaleNode, labelNode, colorNode=None, nodeBaseName=None, fileName=None): LabelStatisticsLogic.__init__(self, grayscaleNode, labelNode, colorNode, nodeBaseName, fileName) + self.terminologyLogic = slicer.modules.terminologies.logic() self.segments = segments # TODO: maybe provide segments here in order to directly set the segment names for the output table @@ -618,20 +619,40 @@ def generateJSON4SEG(self): segmentData["SegmentDescription"] = category if category != "" else segment.GetName() segmentData["SegmentAlgorithmType"] = "MANUAL" segmentData["recommendedDisplayRGBValue"] = segment.GetDefaultColor() - if category != "": - segmentData["SegmentedPropertyCategoryCodeSequence"] = { - "CodeValue": "T-D0050", # where to get that code from???? - "CodingSchemeDesignator": "SRT", - "CodeMeaning": category} - propType = self.getTagValue(segment, segment.GetTerminologyTypeTagName()) - if propType: - segmentData["SegmentedPropertyTypeCodeSequence"] = { - "CodeValue": "T-D0050", - "CodingSchemeDesignator": "SRT", - "CodeMeaning": propType} - segmentsData.append(segmentData) + segmentData.update(self.createJSONFromTerminology(segment)) + segmentsData.append(segmentData) return [segmentsData] + def createJSONFromTerminology(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()) + print typeName + 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 getJSONFromVtkSlicerCodeSequence(self, codeSequence): + return {"CodeValue": codeSequence.GetCodeValue(), + "CodingSchemeDesignator": codeSequence.GetCodingScheme(), + "CodeMeaning": codeSequence.GetCodeMeaning()} + def generateJSON4SR(self): pass From 301405a3323a2619a89e62d31a5ec220b2894340 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 19 Oct 2016 10:55:24 -0400 Subject: [PATCH 25/39] ENH: adding anatomical context to json if available --- Py/Reporting.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 7eb2722..f211455 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -619,11 +619,12 @@ def generateJSON4SEG(self): segmentData["SegmentDescription"] = category if category != "" else segment.GetName() segmentData["SegmentAlgorithmType"] = "MANUAL" segmentData["recommendedDisplayRGBValue"] = segment.GetDefaultColor() - segmentData.update(self.createJSONFromTerminology(segment)) + segmentData.update(self.createJSONFromTerminologyContext(segment)) + segmentData.update(self.createJSONFromAnatomicContext(segment)) segmentsData.append(segmentData) return [segmentsData] - def createJSONFromTerminology(self, segment): + def createJSONFromTerminologyContext(self, segment): segmentData = dict() contextName = self.getTagValue(segment, segment.GetTerminologyContextTagName()) categoryName = self.getTagValue(segment, segment.GetTerminologyCategoryTagName()) @@ -634,7 +635,6 @@ def createJSONFromTerminology(self, segment): segmentData["SegmentedPropertyCategoryCodeSequence"] = self.getJSONFromVtkSlicerCodeSequence(category) typeName = self.getTagValue(segment, segment.GetTerminologyTypeTagName()) - print typeName pType = slicer.vtkSlicerTerminologyType() if not self.terminologyLogic.GetTypeInTerminologyCategory(contextName, categoryName, typeName, pType): raise ValueError("Error: Cannot get type from terminology") @@ -648,6 +648,27 @@ def createJSONFromTerminology(self, segment): 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 {"CodeValue": codeSequence.GetCodeValue(), "CodingSchemeDesignator": codeSequence.GetCodingScheme(), From 5b4be264edce68c5c732de951444ab72ded9a090 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 25 Oct 2016 15:16:24 -0400 Subject: [PATCH 26/39] ENH: Creation of DICOM Segmentation works as long as labelmap extent has the same size as source images - creating metadata for segmentation, - using Slicer temporary directory for storing data needed for using cli itkimage2segimage - resulting DICOM segmentation is saved to Slicer temp directory and then added to SlicerDICOMDatabase - TODO: use segmentation for creation of DICOM SR --- Py/Reporting.py | 98 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 18 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index f211455..cd0e161 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -1,5 +1,6 @@ import getpass import json +import logging, os from slicer.ScriptedLoadableModule import * import vtkSegmentationCorePython as vtkCoreSeg @@ -291,38 +292,100 @@ def onDisplayMeasurementsTable(self): slicer.app.applicationLogic().PropagateTableSelection() def onSaveReportButtonClicked(self): - self.createJSON() + dcmSegmentation = self.createSEG() # TODO: save to DICOM database and retrieve UID + # TODO: Save Structured Report to DICOMDatabase + # self.createDICOMSR(dcmSegmentation) print "on save report button clicked" def onCompleteReportButtonClicked(self): print "on complete report button clicked" - def createJSON(self): + def createSEG(self): # TODO: Create Json - # TODO: Save Segmentation to DICOMDatabase - # TODO: Save Structured Report to DICOMDatabase data = dict() try: - data["seriesAttributes"] = self._getSeriesAttributes() - data["segmentAttributes"] = self.segmentEditorWidget.logic.labelStatisticsLogic.generateJSON4SEG() + data.update(self._getSeriesAttributes()) + data.update(self._getAdditionalSeriesAttributes()) + data["segmentAttributes"] = self.segmentEditorWidget.logic.labelStatisticsLogic.generateJSON4DcmSEGExport() except (ValueError, AttributeError) as exc: slicer.util.warningDisplay(exc.message if isinstance(exc, ValueError) else "No segments found") return + logging.debug("DICOM SEG Metadata output:") + logging.debug(data) + + segmentationVolume = slicer.vtkMRMLScalarVolumeNode() + + segmentationVolume.SetName("Segmentation") + + tempDir = slicer.util.tempDirectory() + + labelNode = self.segmentEditorWidget.logic.labelMapFromSegmentationNode(self.segmentEditorWidget.segmentationNode) + slicer.mrmlScene.AddNode(labelNode) + slicer.util.saveNode(labelNode, os.path.join(tempDir, "labelmap.nrrd")) + + outputSegmentatonPath = os.path.join(tempDir, "segmentation.dcm") + + params = {"dicomImageFiles": ', '.join(self.getDICOMFileList(self.segmentEditorWidget.masterVolumeNode, + absolutePaths=True)).replace(', ', ","), + "segImageFiles": labelNode.GetStorageNode().GetFileName(), + "metaDataFileName": self.saveJSON(data, os.path.join(tempDir, "meta.json")), + "outputSEGFileName": outputSegmentatonPath} + + logging.debug(params) + slicer.cli.run(slicer.modules.itkimage2segimage, None, params, wait_for_completion=True) + + if not os.path.exists(outputSegmentatonPath): + raise RuntimeError("DICOM Segmentation was not created. Check Error Log for further information.") + indexer = ctk.ctkDICOMIndexer() + indexer.addFile(slicer.dicomDatabase, outputSegmentatonPath) + return segmentationVolume + + def createDICOMSR(self, referencedSegmentation): # TODO: we might have several segmentations + data = dict() + data.update(self._getSeriesAttributes()) + # compositeContextDataDir, data["compositeContext"] = self.getDICOMFileList(referencedSegmentation) + imageLibraryDataDir, data["imageLibrary"] = self.getDICOMFileList(self.segmentEditorWidget.masterVolumeNode) + + print "DICOM SR Metadata output:" print json.dumps(data, indent=2, separators=(',', ': ')) def _getSeriesAttributes(self): - # TODO: populate - data = dict() - data["ContentCreatorName"] = self.watchBox.getAttribute("Reader").value - data["ClinicalTrialSeriesID"] = "Session1" - data["ClinicalTrialTimePointID"] = "1" - data["ClinicalTrialCoordinatingCenterName"] = "BWH" - data["SeriesDescription"] = "Segmentation" - data["SeriesNumber"] = ModuleLogicMixin.getDICOMValue(self.watchBox.sourceFile, DICOMTAGS.SERIES_NUMBER) - data["InstanceNumber"] = ModuleLogicMixin.getDICOMValue(self.watchBox.sourceFile, DICOMTAGS.INSTANCE_NUMBER) + return {"SeriesDescription": "Segmentation", + "SeriesNumber": ModuleLogicMixin.getDICOMValue(self.watchBox.sourceFile, DICOMTAGS.SERIES_NUMBER), + "InstanceNumber": ModuleLogicMixin.getDICOMValue(self.watchBox.sourceFile, DICOMTAGS.INSTANCE_NUMBER)} - return data + def _getAdditionalSeriesAttributes(self): + # TODO: populate + return {"ContentCreatorName": self.watchBox.getAttribute("Reader").value, + "ClinicalTrialSeriesID": "Session1", + "ClinicalTrialTimePointID": "1", + "ClinicalTrialCoordinatingCenterName": "BWH"} + + 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): @@ -606,7 +669,7 @@ def customizeLabelStats(self): for segment, labelValue in zip(segments, self.labelStats["Labels"]): self.labelStats[labelValue, "Segment Name"] = segment.GetName() - def generateJSON4SEG(self): + def generateJSON4DcmSEGExport(self): self.validateSegments() segmentsData = [] segments = self.filterEmptySegments() @@ -668,7 +731,6 @@ def createJSONFromAnatomicContext(self, segment): return segmentData - def getJSONFromVtkSlicerCodeSequence(self, codeSequence): return {"CodeValue": codeSequence.GetCodeValue(), "CodingSchemeDesignator": codeSequence.GetCodingScheme(), From d52362af33276f26dbbcb71c747c7b2a53243951 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 25 Oct 2016 17:20:40 -0400 Subject: [PATCH 27/39] BUG: Using CreateLabelmapVolumeFromOrientedImageData for exporting segmentation into labelmap with same extent - generating temp files with timestamp in name --- Py/Reporting.py | 66 +++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index cd0e161..b2a5fb2 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -1,6 +1,7 @@ import getpass import json import logging, os +import datetime from slicer.ScriptedLoadableModule import * import vtkSegmentationCorePython as vtkCoreSeg @@ -292,16 +293,13 @@ def onDisplayMeasurementsTable(self): slicer.app.applicationLogic().PropagateTableSelection() def onSaveReportButtonClicked(self): - dcmSegmentation = self.createSEG() # TODO: save to DICOM database and retrieve UID - # TODO: Save Structured Report to DICOMDatabase - # self.createDICOMSR(dcmSegmentation) - print "on save report button clicked" + dcmSegmentation = self.createSEG() + self.createDICOMSR(dcmSegmentation) def onCompleteReportButtonClicked(self): print "on complete report button clicked" def createSEG(self): - # TODO: Create Json data = dict() try: data.update(self._getSeriesAttributes()) @@ -314,41 +312,64 @@ def createSEG(self): logging.debug("DICOM SEG Metadata output:") logging.debug(data) - segmentationVolume = slicer.vtkMRMLScalarVolumeNode() - - segmentationVolume.SetName("Segmentation") - tempDir = slicer.util.tempDirectory() labelNode = self.segmentEditorWidget.logic.labelMapFromSegmentationNode(self.segmentEditorWidget.segmentationNode) slicer.mrmlScene.AddNode(labelNode) slicer.util.saveNode(labelNode, os.path.join(tempDir, "labelmap.nrrd")) - outputSegmentatonPath = os.path.join(tempDir, "segmentation.dcm") + currentDateTime = datetime.date.today().strftime("%Y%m%d") + + metafilePath = self.saveJSON(data, os.path.join(tempDir, "meta.json")) + outputSegmentatonPath = os.path.join(tempDir, "seg_{}.dcm".format(currentDateTime)) params = {"dicomImageFiles": ', '.join(self.getDICOMFileList(self.segmentEditorWidget.masterVolumeNode, absolutePaths=True)).replace(', ', ","), "segImageFiles": labelNode.GetStorageNode().GetFileName(), - "metaDataFileName": self.saveJSON(data, os.path.join(tempDir, "meta.json")), + "metaDataFileName": metafilePath, "outputSEGFileName": outputSegmentatonPath} logging.debug(params) - slicer.cli.run(slicer.modules.itkimage2segimage, None, params, wait_for_completion=True) + + 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 Exception("encodeSEG CLI did not complete cleanly") if not os.path.exists(outputSegmentatonPath): raise RuntimeError("DICOM Segmentation was not created. Check Error Log for further information.") - indexer = ctk.ctkDICOMIndexer() - indexer.addFile(slicer.dicomDatabase, outputSegmentatonPath) - return segmentationVolume + slicer.dicomDatabase.insert(outputSegmentatonPath) + return slicer.dicomDatabase - def createDICOMSR(self, referencedSegmentation): # TODO: we might have several segmentations - data = dict() - data.update(self._getSeriesAttributes()) - # compositeContextDataDir, data["compositeContext"] = self.getDICOMFileList(referencedSegmentation) + def createDICOMSR(self, referencedSegmentation): + return + + data = self._getSeriesAttributes() + compositeContextDataDir, data["compositeContext"] = self.getDICOMFileList(referencedSegmentation) imageLibraryDataDir, data["imageLibrary"] = self.getDICOMFileList(self.segmentEditorWidget.masterVolumeNode) print "DICOM SR Metadata output:" - print json.dumps(data, indent=2, separators=(',', ': ')) + logging.debug(data) + + json.dumps(data, indent=2, separators=(',', ': ')) + + 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("encodeSEG CLI did not complete cleanly") + # TODO: Save Structured Report to DICOMDatabase + def _getSeriesAttributes(self): return {"SeriesDescription": "Segmentation", @@ -587,7 +608,10 @@ def getSegmentCentroid(self, segment): def labelMapFromSegmentationNode(self, segNode): labelNode = slicer.vtkMRMLLabelMapVolumeNode() slicer.mrmlScene.AddNode(labelNode) - if not self.segmentationsLogic.ExportAllSegmentsToLabelmapNode(segNode, labelNode): + + mergedImageData = vtkCoreSeg.vtkOrientedImageData() + segNode.GenerateMergedLabelmapForAllSegments(mergedImageData, 0) + if not self.segmentationsLogic.CreateLabelmapVolumeFromOrientedImageData(mergedImageData, labelNode): slicer.mrmlScene.RemoveNode(labelNode) return None return labelNode From 7d1889951178b54acdf99dc38c898694de770d5d Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 25 Oct 2016 17:39:08 -0400 Subject: [PATCH 28/39] ENH: Loading DICOM segmentation after its creation into mrmlScene for further processing --- Py/Reporting.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index b2a5fb2..ea45d16 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -114,16 +114,13 @@ def setupWatchbox(self): def setupTestArea(self): def loadTestData(): - from SEGExporterSelfTest import SEGExporterSelfTestLogic - sampleData = SEGExporterSelfTestLogic.downloadSampleData() - unzipped = SEGExporterSelfTestLogic.unzipSampleData(sampleData) - SEGExporterSelfTestLogic.importIntoDICOMDatabase(unzipped) - dicomWidget = slicer.modules.dicom.widgetRepresentation().self() mrHeadSeriesUID = "2.16.840.1.113662.4.4168496325.1025306170.548651188813145058" - dicomWidget.detailsPopup.offerLoadables(mrHeadSeriesUID, 'Series') - dicomWidget.detailsPopup.examineForLoading() - print 'Loading Selection' - dicomWidget.detailsPopup.loadCheckedLoadables() + if not len(slicer.dicomDatabase.filesForSeries()): + 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() slicer.mrmlScene.AddNode(tableNode) @@ -138,6 +135,16 @@ def loadTestData(): 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.createComboBox(nodeTypes=["vtkMRMLScalarVolumeNode", ""], showChildNodeTypes=False, selectNodeUponCreation=True, enabled=False, From b699d86abe1a57796eb76a6d7362bbbb94cb6c0e Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 26 Oct 2016 02:43:52 -0400 Subject: [PATCH 29/39] ENH: Generating metadata for SR: - TODO: Finding, FindingSite and figuring out which values to use in case of MR --- Py/Reporting.py | 141 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 115 insertions(+), 26 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index ea45d16..c43820e 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -46,6 +46,7 @@ class ReportingWidget(ModuleWidgetMixin, ScriptedLoadableModuleWidget): def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) self.segmentationsLogic = slicer.modules.segmentations.logic() + self.tempDir = slicer.util.tempDirectory() def initializeMembers(self): self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? @@ -115,7 +116,7 @@ def setupTestArea(self): def loadTestData(): mrHeadSeriesUID = "2.16.840.1.113662.4.4168496325.1025306170.548651188813145058" - if not len(slicer.dicomDatabase.filesForSeries()): + if not len(slicer.dicomDatabase.filesForSeries(mrHeadSeriesUID)): from SEGExporterSelfTest import SEGExporterSelfTestLogic sampleData = SEGExporterSelfTestLogic.downloadSampleData() unzipped = SEGExporterSelfTestLogic.unzipSampleData(sampleData) @@ -300,8 +301,9 @@ def onDisplayMeasurementsTable(self): slicer.app.applicationLogic().PropagateTableSelection() def onSaveReportButtonClicked(self): - dcmSegmentation = self.createSEG() - self.createDICOMSR(dcmSegmentation) + dcmSegmentationPath = self.createSEG() + self.loadSeriesByFileName(dcmSegmentationPath) + self.createDICOMSR(dcmSegmentationPath) def onCompleteReportButtonClicked(self): print "on complete report button clicked" @@ -319,22 +321,20 @@ def createSEG(self): logging.debug("DICOM SEG Metadata output:") logging.debug(data) - tempDir = slicer.util.tempDirectory() - labelNode = self.segmentEditorWidget.logic.labelMapFromSegmentationNode(self.segmentEditorWidget.segmentationNode) slicer.mrmlScene.AddNode(labelNode) - slicer.util.saveNode(labelNode, os.path.join(tempDir, "labelmap.nrrd")) + slicer.util.saveNode(labelNode, os.path.join(self.tempDir, "labelmap.nrrd")) - currentDateTime = datetime.date.today().strftime("%Y%m%d") + self.currentDateTime = datetime.date.today().strftime("%Y%m%d") - metafilePath = self.saveJSON(data, os.path.join(tempDir, "meta.json")) - outputSegmentatonPath = os.path.join(tempDir, "seg_{}.dcm".format(currentDateTime)) + metaFilePath = self.saveJSON(data, os.path.join(self.tempDir, "seg_meta_{}.json".format(self.currentDateTime))) + outputSegmentationPath = os.path.join(self.tempDir, "seg_{}.dcm".format(self.currentDateTime)) params = {"dicomImageFiles": ', '.join(self.getDICOMFileList(self.segmentEditorWidget.masterVolumeNode, absolutePaths=True)).replace(', ', ","), "segImageFiles": labelNode.GetStorageNode().GetFileName(), - "metaDataFileName": metafilePath, - "outputSEGFileName": outputSegmentatonPath} + "metaDataFileName": metaFilePath, + "outputSEGFileName": outputSegmentationPath} logging.debug(params) @@ -348,22 +348,34 @@ def createSEG(self): if cliNode.GetStatusString() != 'Completed': raise Exception("encodeSEG CLI did not complete cleanly") - if not os.path.exists(outputSegmentatonPath): + if not os.path.exists(outputSegmentationPath): raise RuntimeError("DICOM Segmentation was not created. Check Error Log for further information.") - slicer.dicomDatabase.insert(outputSegmentatonPath) - return slicer.dicomDatabase + indexer = ctk.ctkDICOMIndexer() + indexer.addFile(slicer.dicomDatabase, outputSegmentationPath) + return outputSegmentationPath def createDICOMSR(self, referencedSegmentation): - return - data = self._getSeriesAttributes() - compositeContextDataDir, data["compositeContext"] = self.getDICOMFileList(referencedSegmentation) + + 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.labelStatisticsLogic.generateJSON4DcmSR(referencedSegmentation, + self.segmentEditorWidget.masterVolumeNode) + + print json.dumps(data, indent=2, separators=(',', ': ')) # TODO: remove - print "DICOM SR Metadata output:" + logging.debug("DICOM SR Metadata output:") logging.debug(data) - json.dumps(data, indent=2, separators=(',', ': ')) + metaFilePath = self.saveJSON(data, os.path.join(self.tempDir, "sr_meta_{}.json".format(self.currentDateTime))) + outputSRPath = os.path.join(self.tempDir, "sr_{}.dcm".format(self.currentDateTime)) + + params = {"metaDataFileName": metaFilePath, + "compositeContextDataDir": compositeContextDataDir, + "imageLibraryDataDir": imageLibraryDataDir, + "outputFileName": outputSRPath} logging.debug(params) cliNode = None @@ -375,8 +387,7 @@ def createDICOMSR(self, referencedSegmentation): if cliNode.GetStatusString() != 'Completed': raise Exception("encodeSEG CLI did not complete cleanly") - # TODO: Save Structured Report to DICOMDatabase - + # # TODO: Save Structured Report to DICOMDatabase def _getSeriesAttributes(self): return {"SeriesDescription": "Segmentation", @@ -390,6 +401,16 @@ def _getAdditionalSeriesAttributes(self): "ClinicalTrialTimePointID": "1", "ClinicalTrialCoordinatingCenterName": "BWH"} + def _getAdditionalSRInformation(self): + data = dict() + data["observerContext"] = {"ObserverType": "PERSON", + "PersonObserverName": "Reader1"} + 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) @@ -763,12 +784,80 @@ def createJSONFromAnatomicContext(self, segment): return segmentData def getJSONFromVtkSlicerCodeSequence(self, codeSequence): - return {"CodeValue": codeSequence.GetCodeValue(), - "CodingSchemeDesignator": codeSequence.GetCodingScheme(), - "CodeMeaning": codeSequence.GetCodeMeaning()} + return self.createCodeSequence(codeSequence.GetCodeValue(), codeSequence.GetCodingScheme(), codeSequence.GetCodeMeaning()) - def generateJSON4SR(self): - pass + def generateJSON4DcmSR(self, dcmSegmentationFile, sourceVolumeNode): + measurements = [] + segments = self.filterEmptySegments() + + modality = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0008,0060") + + sourceImageSOPInstanceUID = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0008,00018") + segmentationImageInstanceUID = ModuleLogicMixin.getDICOMValue(dcmSegmentationFile, "0008,00018") + + for segment, labelValue in zip(segments, self.labelStats["Labels"]): + data = dict() + data["TrackingIdentifier"] = segment.GetName() + data["ReferencedSegment"] = labelValue + data["SourceSeriesForImageSegmentation"] = sourceImageSOPInstanceUID + data["segmentationSOPInstanceUID"] = segmentationImageInstanceUID # TODO: for now the same for all + # data["Finding"] = None # TODO + # data["FindingSite"] = None # TODO + measurementItems = [] + for key in [k for k in self.keys if k not in ["Index", "Segment Name", "Count"]]: + item = dict() + item["value"] = str(self.labelStats[labelValue, key]) + item["quantity"] = self.getQuantityCSforKey(key) + item["units"] = self.getUnitsCSForKey(key, modality) + derivationModifier = self.getDerivatinModifierCSForKey(key) + if derivationModifier: + item["derivationModifier"] = derivationModifier + measurementItems.append(item) + data["measurementItems"] = measurementItems + measurements.append(data) + return measurements + + def getQuantityCSforKey(self, key, modality="CT"): + if key in ["Min", "Max", "Mean", "StdDev"]: + if modality == "CT": + return self.createCodeSequence("122713", "DCM", "Attenuation Coefficient") + elif modality == "MR": + return self.createCodeSequence("value", "designator", "meaning") # TODO: find out how to adapt for MR + elif key in ["Volume mm^3", "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 = ["Min", "Max", "Mean", "StdDev"] + if key in keys: + if modality == "CT": + return self.createCodeSequence("[hnsf'U]", "UCUM", "Hounsfield unit") + elif modality == "MR": + return self.createCodeSequence("value", "designator", "meaning") # TODO: find out how to adapt for MR + raise ValueError("No matching units code sequence found for key {}".format(key)) + elif key == "Volume cc": + return self.createCodeSequence("mm3", "UCUM", "cubic millimeter") + elif key == "Volume mm^3": + return self.createCodeSequence("cm3", "UCUM", "cubic centimeter") + return None + + def getDerivatinModifierCSForKey(self, key): + keys = ["Min", "Max", "Mean", "StdDev"] + 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): segments = self.filterEmptySegments() From d9535b16ddae4ee32db50025e9458f005a5ad0ca Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 26 Oct 2016 15:47:02 -0400 Subject: [PATCH 30/39] ENH: Generating DICOM SR works (issue #74) - TODO: Populate hardcoded attributes with real ones - TODO: Refactoring --- Py/Reporting.py | 60 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index c43820e..379d27c 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -1,7 +1,7 @@ import getpass import json import logging, os -import datetime +from datetime import datetime from slicer.ScriptedLoadableModule import * import vtkSegmentationCorePython as vtkCoreSeg @@ -302,7 +302,6 @@ def onDisplayMeasurementsTable(self): def onSaveReportButtonClicked(self): dcmSegmentationPath = self.createSEG() - self.loadSeriesByFileName(dcmSegmentationPath) self.createDICOMSR(dcmSegmentationPath) def onCompleteReportButtonClicked(self): @@ -325,7 +324,7 @@ def createSEG(self): slicer.mrmlScene.AddNode(labelNode) slicer.util.saveNode(labelNode, os.path.join(self.tempDir, "labelmap.nrrd")) - self.currentDateTime = datetime.date.today().strftime("%Y%m%d") + self.currentDateTime = datetime.now().strftime('%Y-%m-%d_%H%M%S') metaFilePath = self.saveJSON(data, os.path.join(self.tempDir, "seg_meta_{}.json".format(self.currentDateTime))) outputSegmentationPath = os.path.join(self.tempDir, "seg_{}.dcm".format(self.currentDateTime)) @@ -346,12 +345,14 @@ def createSEG(self): waitCount += 1 if cliNode.GetStatusString() != 'Completed': - raise Exception("encodeSEG CLI did not complete cleanly") + raise Exception("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): @@ -386,7 +387,7 @@ def createDICOMSR(self, referencedSegmentation): waitCount += 1 if cliNode.GetStatusString() != 'Completed': - raise Exception("encodeSEG CLI did not complete cleanly") + raise Exception("tid1500writer CLI did not complete cleanly") # # TODO: Save Structured Report to DICOMDatabase def _getSeriesAttributes(self): @@ -404,7 +405,7 @@ def _getAdditionalSeriesAttributes(self): def _getAdditionalSRInformation(self): data = dict() data["observerContext"] = {"ObserverType": "PERSON", - "PersonObserverName": "Reader1"} + "PersonObserverName": self.watchBox.getAttribute("Reader").value} data["VerificationFlag"] = "VERIFIED" data["CompletionFlag"] = "COMPLETE" data["activitySession"] = "1" @@ -792,37 +793,44 @@ def generateJSON4DcmSR(self, dcmSegmentationFile, sourceVolumeNode): modality = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0008,0060") - sourceImageSOPInstanceUID = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0008,00018") - segmentationImageInstanceUID = ModuleLogicMixin.getDICOMValue(dcmSegmentationFile, "0008,00018") + sourceImageSeriesUID = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0020,000E") + logging.debug("SourceImageSeriesUID: {}".format(sourceImageSeriesUID)) + segmentationSOPInstanceUID = ModuleLogicMixin.getDICOMValue(dcmSegmentationFile, "0008,00018") + logging.debug("SegmentationSOPInstanceUID: {}".format(segmentationSOPInstanceUID)) for segment, labelValue in zip(segments, self.labelStats["Labels"]): data = dict() data["TrackingIdentifier"] = segment.GetName() data["ReferencedSegment"] = labelValue - data["SourceSeriesForImageSegmentation"] = sourceImageSOPInstanceUID - data["segmentationSOPInstanceUID"] = segmentationImageInstanceUID # TODO: for now the same for all - # data["Finding"] = None # TODO - # data["FindingSite"] = None # TODO - measurementItems = [] - for key in [k for k in self.keys if k not in ["Index", "Segment Name", "Count"]]: - item = dict() - item["value"] = str(self.labelStats[labelValue, key]) - item["quantity"] = self.getQuantityCSforKey(key) - item["units"] = self.getUnitsCSForKey(key, modality) - derivationModifier = self.getDerivatinModifierCSForKey(key) - if derivationModifier: - item["derivationModifier"] = derivationModifier - measurementItems.append(item) - data["measurementItems"] = measurementItems - measurements.append(data) + data["SourceSeriesForImageSegmentation"] = sourceImageSeriesUID + data["segmentationSOPInstanceUID"] = segmentationSOPInstanceUID + data["Finding"] = self.createJSONFromTerminologyContext(segment)["SegmentedPropertyTypeCodeSequence"] + anatomicContext = self.createJSONFromAnatomicContext(segment) + if anatomicContext.has_key("AnatomicRegionSequence"): + data["FindingSite"] = anatomicContext["AnatomicRegionSequence"] + data["measurementItems"] = self.createMeasurementItemsForLabelValue(labelValue, modality) + measurements.append(data) return measurements + def createMeasurementItemsForLabelValue(self, labelValue, modality): + measurementItems = [] + for key in [k for k in self.keys if k not in ["Index", "Segment Name", "Count"]]: + item = dict() + item["value"] = str(self.labelStats[labelValue, 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 ["Min", "Max", "Mean", "StdDev"]: if modality == "CT": return self.createCodeSequence("122713", "DCM", "Attenuation Coefficient") elif modality == "MR": - return self.createCodeSequence("value", "designator", "meaning") # TODO: find out how to adapt for MR + return self.createCodeSequence("110852", "DCM", "MR signal intensity") elif key in ["Volume mm^3", "Volume cc"]: return self.createCodeSequence("G-D705", "SRT", "Volume") raise ValueError("No matching quantity code sequence found for key {}".format(key)) @@ -833,7 +841,7 @@ def getUnitsCSForKey(self, key, modality="CT"): if modality == "CT": return self.createCodeSequence("[hnsf'U]", "UCUM", "Hounsfield unit") elif modality == "MR": - return self.createCodeSequence("value", "designator", "meaning") # TODO: find out how to adapt for MR + return self.createCodeSequence("1", "UCUM", "no units") raise ValueError("No matching units code sequence found for key {}".format(key)) elif key == "Volume cc": return self.createCodeSequence("mm3", "UCUM", "cubic millimeter") From d9871447fe38338cc40060a8f67502de134c7746 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 26 Oct 2016 16:10:30 -0400 Subject: [PATCH 31/39] ENH: Using some hardcoded values for now --- Py/Reporting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 379d27c..0e92aea 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -398,9 +398,9 @@ def _getSeriesAttributes(self): def _getAdditionalSeriesAttributes(self): # TODO: populate return {"ContentCreatorName": self.watchBox.getAttribute("Reader").value, - "ClinicalTrialSeriesID": "Session1", + "ClinicalTrialSeriesID": "1", "ClinicalTrialTimePointID": "1", - "ClinicalTrialCoordinatingCenterName": "BWH"} + "ClinicalTrialCoordinatingCenterName": "QIICR"} def _getAdditionalSRInformation(self): data = dict() From da4ca21aa0c86975884e7ef234d43e9bd3c02354 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Thu, 27 Oct 2016 13:29:19 -0400 Subject: [PATCH 32/39] ENH: Holding reference in vtkMRMLTableNode to created segmentation, in order to switch between tables (issue #74) - upon vtkMRMLTableNode selection is checked if there is a segmentation referenced, if not, create a new one and add the reference - once a image volume has been selected, it is bound to the segmentation and cannot be changed anymore (image selector disabled) --- Py/Reporting.py | 107 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 56 insertions(+), 51 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 0e92aea..a1a4e9d 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -23,19 +23,19 @@ class Reporting(ScriptedLoadableModule): def __init__(self, parent): ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "Reporting" # TODO make this more human readable by adding spaces + self.parent.title = "Reporting" self.parent.categories = ["Examples"] self.parent.dependencies = ["SlicerProstate"] - self.parent.contributors = ["Andrey Fedorov (SPL, BWH), Nicole Aucoin (SPL, BWH), " - "Steve Pieper (Isomics), Christian Herz (SPL)"] + 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. -""" # replace with organization, grant and thanks. + """ # TODO: replace with organization, grant and thanks. class ReportingWidget(ModuleWidgetMixin, ScriptedLoadableModuleWidget): @@ -49,7 +49,6 @@ def __init__(self, parent=None): self.tempDir = slicer.util.tempDirectory() def initializeMembers(self): - self.segReferencedMasterVolume = {} # TODO: maybe also add created table so that there is no need to recalculate everything? self.tableNode = None self.segmentationObservers = [] @@ -80,7 +79,8 @@ def enter(self): self.setupSegmentationObservers() def refreshUIElementsAvailability(self): - self.imageVolumeSelector.enabled = self.measurementReportSelector.currentNode() is not None + 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) @@ -91,18 +91,15 @@ def setup(self): self.initializeMembers() self.setupWatchbox() self.setupTestArea() - self.setupSelectionArea() self.setupViewSettingsArea() self.setupSegmentationsArea() + self.setupSelectionArea() self.setupMeasurementsArea() self.setupActionButtons() self.setupConnections() self.layout.addStretch(1) self.fourUpSliceLayoutButton.checked = True - # TEST MODE - # self.retrieveTestDataButton.click() - def setupWatchbox(self): self.watchBoxInformation = [ WatchBoxAttribute('StudyID', 'Study ID: ', DICOMTAGS.STUDY_ID), @@ -124,6 +121,7 @@ def loadTestData(): 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) @@ -147,14 +145,15 @@ def loadSeries(self, seriesUID): dicomWidget.detailsPopup.loadCheckedLoadables() def setupSelectionArea(self): - self.imageVolumeSelector = self.createComboBox(nodeTypes=["vtkMRMLScalarVolumeNode", ""], showChildNodeTypes=False, - selectNodeUponCreation=True, enabled=False, - toolTip="Select image volume to annotate") + + 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.imageVolumeSelector.addAttribute("vtkMRMLTableNode", "Reporting", None) + self.measurementReportSelector.addAttribute("vtkMRMLTableNode", "Reporting", "Yes") + self.selectionAreaWidget = qt.QWidget() self.selectionAreaWidgetLayout = qt.QGridLayout() self.selectionAreaWidget.setLayout(self.selectionAreaWidgetLayout) @@ -164,6 +163,7 @@ def setupSelectionArea(self): 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() @@ -182,7 +182,6 @@ def setupSegmentationsArea(self): self.segmentationGroupBox.setLayout(self.segmentationGroupBoxLayout) self.segmentEditorWidget = ReportingSegmentEditorWidget(parent=self.segmentationGroupBox) self.segmentEditorWidget.setup() - self.layout.addWidget(self.segmentationGroupBox) def setupMeasurementsArea(self): self.measurementsGroupBox = qt.QGroupBox("Measurements") @@ -211,6 +210,7 @@ def setupSelectorConnections(): 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) @@ -235,26 +235,32 @@ def removeSegmentationObserver(self): def onLayoutChanged(self, layout): self.onDisplayMeasurementsTable() - @priorCall(refreshUIElementsAvailability) + @postCall(refreshUIElementsAvailability) def onImageVolumeSelected(self, node): - self.removeSegmentationObserver() - self.segmentEditorWidget.clearSegmentationEditorSelectors() self.initializeWatchBox(node) - if node in self.segReferencedMasterVolume.keys(): - self.segmentEditorWidget.segmentationNode = self.segReferencedMasterVolume[node] - else: - self.segReferencedMasterVolume[node] = self.createNewSegmentation() - self.segmentEditorWidget.segmentationNode = self.segReferencedMasterVolume[node] - self.segmentEditorWidget.masterVolumeNode = node - self.setupSegmentationObservers() @priorCall(refreshUIElementsAvailability) def onMeasurementReportSelected(self, node): + self.removeSegmentationObserver() + self.imageVolumeSelector.setCurrentNode(None) + self.tableNode = node if node is None: - self.imageVolumeSelector.setCurrentNode(None) - # TODO: create reference to segmentation node - # TODO: segmentationNode holds references to volume - pass + 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() + + def getReferencedVolumeFromSegmentationNode(self, segmentationNode): + if not segmentationNode: + return None + return segmentationNode.GetNodeReference(segmentationNode.GetReferenceImageGeometryReferenceRole()) def setupSegmentationObservers(self): segNode = self.segmentEditorWidget.segmentation @@ -273,7 +279,7 @@ def initializeWatchBox(self, node): except AttributeError: self.watchBox.sourceFile = None - def createNewSegmentation(self): + def createNewSegmentationNode(self): segNode = slicer.vtkMRMLSegmentationNode() slicer.mrmlScene.AddNode(segNode) return segNode @@ -283,7 +289,9 @@ def onSegmentationNodeChanged(self, observer=None, caller=None): if not self.calculateAutomaticallyCheckbox.checked: # TODO: mark table as old (maybe with styling border red) return + self.updateMeasurementsTable() + def updateMeasurementsTable(self): table = self.segmentEditorWidget.calculateLabelStatistics(self.tableNode) if table: self.tableNode = table @@ -497,17 +505,17 @@ class ReportingSegmentEditorWidget(SegmentEditorWidget, ModuleWidgetMixin): def segmentationNode(self): return self.editor.segmentationNode() - @segmentationNode.setter - def segmentationNode(self, value): - self.editor.setSegmentationNode(value) + @property + def segmentationNodeSelector(self): + return self.find("MRMLNodeComboBox_Segmentation") @property def masterVolumeNode(self): return self.editor.masterVolumeNode() - @masterVolumeNode.setter - def masterVolumeNode(self, value): - self.editor.setMasterVolumeNode(value) + @property + def masterVolumeNodeSelector(self): + return self.find("MRMLNodeComboBox_MasterVolume") @property @onExceptionReturnNone @@ -570,7 +578,7 @@ def clearSegmentationEditorSelectors(self): def hideUnwantedEditorUIElements(self): self.editor.segmentationNodeSelectorVisible = False - self.editor.masterVolumeNodeSelectorVisible = False + # self.editor.masterVolumeNodeSelectorVisible = False def reorganizeEffectButtons(self): widget = self.find("EffectsGroupBox") @@ -663,13 +671,8 @@ def calculateLabelStatistics(self, segNode, grayscaleNode, tableNode=None): else: self.labelStatisticsLogic = CustomLabelStatisticsLogic(segments, grayscaleNode, labelNode) - tNode = self.labelStatisticsLogic.exportToTable() - tNode.SetAttribute("Reporting", "Yes") - if not tableNode: - slicer.mrmlScene.AddNode(tableNode) - tableNode = tNode - else: - tableNode.Copy(tNode) + tableNode = self.labelStatisticsLogic.exportToTable(tableNode) + slicer.mrmlScene.AddNode(tableNode) slicer.mrmlScene.RemoveNode(labelNode) return tableNode @@ -680,16 +683,18 @@ def __init__(self, segments, grayscaleNode, labelNode, colorNode=None, nodeBaseN LabelStatisticsLogic.__init__(self, grayscaleNode, labelNode, colorNode, nodeBaseName, fileName) self.terminologyLogic = slicer.modules.terminologies.logic() self.segments = segments - # TODO: maybe provide segments here in order to directly set the segment names for the output table - def exportToTable(self): - table = slicer.vtkMRMLTableNode() - table.SetUseColumnNameAsColumnHeader(True) - tableWasModified = table.StartModify() + def exportToTable(self, table=None): + if not table: + table = slicer.vtkMRMLTableNode() + table.SetName(slicer.mrmlScene.GenerateUniqueName(self.nodeBaseName + ' statistics')) - table.SetName(slicer.mrmlScene.GenerateUniqueName(self.nodeBaseName + ' statistics')) + table.RemoveAllColumns() + table.SetUseColumnNameAsColumnHeader(True) self.customizeLabelStats() + + tableWasModified = table.StartModify() for k in self.keys: col = table.AddColumn() col.SetName(k) From c42135792fd46b715eef65c462367d8012e35d34 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Thu, 27 Oct 2016 15:33:05 -0400 Subject: [PATCH 33/39] ENH: Added indicator for outdated table view when segmentation changed and "Auto Update" is unchecked - red border around table view in module widget (TODO: need to think about FourUp Table layout) --- Py/Reporting.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index a1a4e9d..75f7654 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -69,15 +69,6 @@ def removeAllUIElements(self): except AttributeError: pass - def exit(self): - # TODO: export SEG and SR - # TODO: disconnect from segment editor events - # self.removeSegmentationObserver() - pass - - def enter(self): - self.setupSegmentationObservers() - def refreshUIElementsAvailability(self): self.imageVolumeSelector.enabled = self.measurementReportSelector.currentNode() is not None \ and not self.getReferencedVolumeFromSegmentationNode(self.segmentEditorWidget.segmentationNode) @@ -256,6 +247,7 @@ def onMeasurementReportSelected(self, node): self.tableNode.SetAttribute('ReferencedSegmentationNodeID', segmentationNode.GetID()) self.segmentionNodeSelector.setCurrentNode(segmentationNode) self.setupSegmentationObservers() + self.onSegmentationNodeChanged() def getReferencedVolumeFromSegmentationNode(self, segmentationNode): if not segmentationNode: @@ -287,7 +279,7 @@ def createNewSegmentationNode(self): @postCall(refreshUIElementsAvailability) def onSegmentationNodeChanged(self, observer=None, caller=None): if not self.calculateAutomaticallyCheckbox.checked: - # TODO: mark table as old (maybe with styling border red) + self.tableView.setStyleSheet("QTableView{border:2px solid red;};") return self.updateMeasurementsTable() @@ -297,6 +289,7 @@ def updateMeasurementsTable(self): self.tableNode = table self.tableNode.SetLocked(True) self.tableView.setMRMLTableNode(self.tableNode) + self.tableView.setStyleSheet("QTableView{border:none};") self.onDisplayMeasurementsTable() def getActiveSlicerTableID(self): From e0d60770473f6080c81466a112264afa6bc281c7 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Thu, 27 Oct 2016 15:38:48 -0400 Subject: [PATCH 34/39] ENH: Improved error message when DICOM SEG creation failed and preventing further DICOM SR creation in that case --- Py/Reporting.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 75f7654..5aca3d5 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -302,7 +302,11 @@ def onDisplayMeasurementsTable(self): slicer.app.applicationLogic().PropagateTableSelection() def onSaveReportButtonClicked(self): - dcmSegmentationPath = self.createSEG() + try: + dcmSegmentationPath = self.createSEG() + except (RuntimeError, ValueError, AttributeError) as exc: + slicer.util.warningDisplay(exc.message if isinstance(exc, ValueError) else "No segments found") + return self.createDICOMSR(dcmSegmentationPath) def onCompleteReportButtonClicked(self): @@ -310,13 +314,9 @@ def onCompleteReportButtonClicked(self): def createSEG(self): data = dict() - try: - data.update(self._getSeriesAttributes()) - data.update(self._getAdditionalSeriesAttributes()) - data["segmentAttributes"] = self.segmentEditorWidget.logic.labelStatisticsLogic.generateJSON4DcmSEGExport() - except (ValueError, AttributeError) as exc: - slicer.util.warningDisplay(exc.message if isinstance(exc, ValueError) else "No segments found") - return + data.update(self._getSeriesAttributes()) + data.update(self._getAdditionalSeriesAttributes()) + data["segmentAttributes"] = self.segmentEditorWidget.logic.labelStatisticsLogic.generateJSON4DcmSEGExport() logging.debug("DICOM SEG Metadata output:") logging.debug(data) @@ -346,7 +346,7 @@ def createSEG(self): waitCount += 1 if cliNode.GetStatusString() != 'Completed': - raise Exception("itkimage2segimage CLI did not complete cleanly") + 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.") From 08efecf907ebdfb118040ba83a8b549d39f6339d Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Thu, 27 Oct 2016 16:36:49 -0400 Subject: [PATCH 35/39] ENH: Adding DICOM SR after creation to DICOMDatabase and fixed typo --- Py/Reporting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 5aca3d5..fa035be 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -389,7 +389,8 @@ def createDICOMSR(self, referencedSegmentation): if cliNode.GetStatusString() != 'Completed': raise Exception("tid1500writer CLI did not complete cleanly") - # # TODO: Save Structured Report to DICOMDatabase + indexer = ctk.ctkDICOMIndexer() + indexer.addFile(slicer.dicomDatabase, outputSRPath) def _getSeriesAttributes(self): return {"SeriesDescription": "Segmentation", @@ -793,7 +794,7 @@ def generateJSON4DcmSR(self, dcmSegmentationFile, sourceVolumeNode): sourceImageSeriesUID = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0020,000E") logging.debug("SourceImageSeriesUID: {}".format(sourceImageSeriesUID)) - segmentationSOPInstanceUID = ModuleLogicMixin.getDICOMValue(dcmSegmentationFile, "0008,00018") + segmentationSOPInstanceUID = ModuleLogicMixin.getDICOMValue(dcmSegmentationFile, "0008,0018") logging.debug("SegmentationSOPInstanceUID: {}".format(segmentationSOPInstanceUID)) for segment, labelValue in zip(segments, self.labelStats["Labels"]): From 5923d0d5603e553ad801e1ddc35d66d49f8b7c54 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Fri, 4 Nov 2016 10:10:40 -0400 Subject: [PATCH 36/39] BUG: LabelStatisticsCalculation was executed even when no segments were available --- Py/Reporting.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Py/Reporting.py b/Py/Reporting.py index fa035be..065f77b 100644 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -290,6 +290,10 @@ def updateMeasurementsTable(self): 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): @@ -648,6 +652,8 @@ def labelMapFromSegmentationNode(self, segNode): return labelNode def calculateLabelStatistics(self, segNode, grayscaleNode, tableNode=None): + if not len(self.getSegments(segNode.GetSegmentation())): + return None labelNode = self.labelMapFromSegmentationNode(segNode) if not labelNode: return None From bf008f6ec40f1ece373be16e7f33ed32a0a4de9e Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 9 Nov 2016 11:32:33 -0500 Subject: [PATCH 37/39] ENH: Loading SR from DICOM into json and its referenced segmentation and images (issue #74) --- Py/DICOMTID1500Plugin.py | 216 +++++++++++++++++++++++++++++++++++++++++++++++ Py/Reporting.py | 4 +- 2 files changed, 219 insertions(+), 1 deletion(-) create mode 100755 Py/DICOMTID1500Plugin.py mode change 100644 => 100755 Py/Reporting.py 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 old mode 100644 new mode 100755 index 065f77b..8093b23 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -309,7 +309,8 @@ def onSaveReportButtonClicked(self): try: dcmSegmentationPath = self.createSEG() except (RuntimeError, ValueError, AttributeError) as exc: - slicer.util.warningDisplay(exc.message if isinstance(exc, ValueError) else "No segments found") + slicer.util.warningDisplay(exc.message) + # slicer.util.warningDisplay(exc.message if isinstance(exc, ValueError) else "No segments found") return self.createDICOMSR(dcmSegmentationPath) @@ -716,6 +717,7 @@ def customizeLabelStats(self): self.keys = ["Segment Name"] + list(self.keys) self.keys.remove("Index") + self.keys.remove("Count") try: del self.labelStats["Labels"][0] From 774e5a320e066150d3713b9be3cda10fc39ac615 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 11 Nov 2016 18:28:49 -0500 Subject: [PATCH 38/39] ENH: Replaced usage of LabelStatistics module with SegmentStatistics module: - creation of temp directory in slicer temp directory with timestamp - saving labelmap for each segment separately TODO: investigate into itkimage2segimage with several itk segmentation input images --- Py/Reporting.py | 234 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 119 insertions(+), 115 deletions(-) diff --git a/Py/Reporting.py b/Py/Reporting.py index 8093b23..e47d350 100755 --- a/Py/Reporting.py +++ b/Py/Reporting.py @@ -13,7 +13,7 @@ from SlicerProstateUtils.buttons import * from SegmentEditor import SegmentEditorWidget -from LabelStatistics import LabelStatisticsLogic +from SegmentStatistics import SegmentStatisticsLogic class Reporting(ScriptedLoadableModule): @@ -46,7 +46,7 @@ class ReportingWidget(ModuleWidgetMixin, ScriptedLoadableModuleWidget): def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) self.segmentationsLogic = slicer.modules.segmentations.logic() - self.tempDir = slicer.util.tempDirectory() + self.slicerTempDir = slicer.util.tempDirectory() def initializeMembers(self): self.tableNode = None @@ -310,7 +310,6 @@ def onSaveReportButtonClicked(self): dcmSegmentationPath = self.createSEG() except (RuntimeError, ValueError, AttributeError) as exc: slicer.util.warningDisplay(exc.message) - # slicer.util.warningDisplay(exc.message if isinstance(exc, ValueError) else "No segments found") return self.createDICOMSR(dcmSegmentationPath) @@ -318,26 +317,36 @@ 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"] = self.segmentEditorWidget.logic.labelStatisticsLogic.generateJSON4DcmSEGExport() + data["segmentAttributes"] = segmentStatisticsLogic.generateJSON4DcmSEGExport() logging.debug("DICOM SEG Metadata output:") logging.debug(data) - labelNode = self.segmentEditorWidget.logic.labelMapFromSegmentationNode(self.segmentEditorWidget.segmentationNode) - slicer.mrmlScene.AddNode(labelNode) - slicer.util.saveNode(labelNode, os.path.join(self.tempDir, "labelmap.nrrd")) - self.currentDateTime = datetime.now().strftime('%Y-%m-%d_%H%M%S') - - metaFilePath = self.saveJSON(data, os.path.join(self.tempDir, "seg_meta_{}.json".format(self.currentDateTime))) - outputSegmentationPath = os.path.join(self.tempDir, "seg_{}.dcm".format(self.currentDateTime)) + 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": labelNode.GetStorageNode().GetFileName(), + "segImageFiles": ', '.join(segmentFiles).replace(', ', ","), "metaDataFileName": metaFilePath, "outputSEGFileName": outputSegmentationPath} @@ -368,16 +377,16 @@ def createDICOMSR(self, referencedSegmentation): imageLibraryDataDir, data["imageLibrary"] = self.getDICOMFileList(self.segmentEditorWidget.masterVolumeNode) data.update(self._getAdditionalSRInformation()) - data["Measurements"] = self.segmentEditorWidget.logic.labelStatisticsLogic.generateJSON4DcmSR(referencedSegmentation, - self.segmentEditorWidget.masterVolumeNode) + 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".format(self.currentDateTime))) - outputSRPath = os.path.join(self.tempDir, "sr_{}.dcm".format(self.currentDateTime)) + 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, @@ -609,7 +618,7 @@ def enter(self): self.editor.updateWidgetFromMRML() def calculateLabelStatistics(self, tableNode): - return self.logic.calculateLabelStatistics(self.segmentationNode, self.masterVolumeNode, tableNode) + return self.logic.calculateSegmentStatistics(self.segmentationNode, self.masterVolumeNode, tableNode) class ReportingSegmentEditorLogic(ScriptedLoadableModuleLogic): @@ -619,7 +628,8 @@ def __init__(self, parent=None): self.parent = parent self.volumesLogic = slicer.modules.volumes.logic() self.segmentationsLogic = slicer.modules.segmentations.logic() - self.labelStatisticsLogic = None + self.segmentStatisticsLogic = None + self.segmentStatisticsLogic = CustomSegmentStatisticsLogic() def getSegments(self, segmentation): if not segmentation: @@ -641,110 +651,65 @@ def getSegmentCentroid(self, segment): return centroid return None - def labelMapFromSegmentationNode(self, segNode): - labelNode = slicer.vtkMRMLLabelMapVolumeNode() - slicer.mrmlScene.AddNode(labelNode) - - mergedImageData = vtkCoreSeg.vtkOrientedImageData() - segNode.GenerateMergedLabelmapForAllSegments(mergedImageData, 0) - if not self.segmentationsLogic.CreateLabelmapVolumeFromOrientedImageData(mergedImageData, labelNode): - slicer.mrmlScene.RemoveNode(labelNode) - return None - return labelNode - - def calculateLabelStatistics(self, segNode, grayscaleNode, tableNode=None): - if not len(self.getSegments(segNode.GetSegmentation())): - return None - labelNode = self.labelMapFromSegmentationNode(segNode) - if not labelNode: - return None - segments = self.getSegments(segNode.GetSegmentation()) - warnings = self.volumesLogic.CheckForLabelVolumeValidity(grayscaleNode, labelNode) - if warnings != "": - if 'mismatch' in warnings: - resampledLabelNode = self.volumesLogic.ResampleVolumeToReferenceVolume(labelNode, grayscaleNode) - self.labelStatisticsLogic = CustomLabelStatisticsLogic(segments, grayscaleNode, resampledLabelNode, - colorNode=labelNode.GetDisplayNode().GetColorNode(), - nodeBaseName=labelNode.GetName()) - slicer.mrmlScene.RemoveNode(resampledLabelNode) - else: - raise ValueError("Volumes do not have the same geometry.\n%s" % warnings) - else: - self.labelStatisticsLogic = CustomLabelStatisticsLogic(segments, grayscaleNode, labelNode) + def calculateSegmentStatistics(self, segNode, grayscaleNode, tableNode=None): + self.segmentStatisticsLogic.computeStatistics(segNode, grayscaleNode) - tableNode = self.labelStatisticsLogic.exportToTable(tableNode) + tableNode = self.segmentStatisticsLogic.exportToTable(tableNode) slicer.mrmlScene.AddNode(tableNode) - slicer.mrmlScene.RemoveNode(labelNode) + # slicer.mrmlScene.RemoveNode(labelNode) return tableNode -class CustomLabelStatisticsLogic(LabelStatisticsLogic): - def __init__(self, segments, grayscaleNode, labelNode, colorNode=None, nodeBaseName=None, fileName=None): - LabelStatisticsLogic.__init__(self, grayscaleNode, labelNode, colorNode, nodeBaseName, fileName) + +class CustomSegmentStatisticsLogic(SegmentStatisticsLogic): + + def __init__(self): + SegmentStatisticsLogic.__init__(self) self.terminologyLogic = slicer.modules.terminologies.logic() - self.segments = segments + self.segmentationsLogic = slicer.modules.segmentations.logic() - def exportToTable(self, table=None): + 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.nodeBaseName + ' statistics')) - - table.RemoveAllColumns() - table.SetUseColumnNameAsColumnHeader(True) - - self.customizeLabelStats() - - tableWasModified = table.StartModify() - for k in self.keys: - col = table.AddColumn() - col.SetName(k) - for labelValue in self.labelStats["Labels"]: - rowIndex = table.AddEmptyRow() - for columnIndex, k in enumerate(self.keys): - table.SetCellText(rowIndex, columnIndex, str(self.labelStats[labelValue, k])) - - table.EndModify(tableWasModified) + 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 [s for s in self.segments if not self.isSegmentEmpty(s)] - - def customizeLabelStats(self): - colorNode = self.getColorNode() - if not colorNode: - return self.keys, self.labelStats - - self.keys = ["Segment Name"] + list(self.keys) - self.keys.remove("Index") - self.keys.remove("Count") - - try: - del self.labelStats["Labels"][0] - except KeyError: - pass - - segments = self.filterEmptySegments() - - for segment, labelValue in zip(segments, self.labelStats["Labels"]): - self.labelStats[labelValue, "Segment Name"] = segment.GetName() + 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 = [] - segments = self.filterEmptySegments() - if not len(segments): - raise ValueError("No segments with pixel data found.") - for segment, labelValue in zip(segments, self.labelStats["Labels"]): + for segmentID in self.statistics["SegmentIDs"]: + if self.statistics[segmentID, "GS voxel count"] == 0: + continue segmentData = dict() - segmentData["LabelID"] = labelValue + 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 segment.GetName() + 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): @@ -794,10 +759,48 @@ def createJSONFromAnatomicContext(self, segment): 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 = [] - segments = self.filterEmptySegments() - modality = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0008,0060") sourceImageSeriesUID = ModuleLogicMixin.getDICOMValue(sourceVolumeNode, "0020,000E") @@ -805,25 +808,26 @@ def generateJSON4DcmSR(self, dcmSegmentationFile, sourceVolumeNode): segmentationSOPInstanceUID = ModuleLogicMixin.getDICOMValue(dcmSegmentationFile, "0008,0018") logging.debug("SegmentationSOPInstanceUID: {}".format(segmentationSOPInstanceUID)) - for segment, labelValue in zip(segments, self.labelStats["Labels"]): + for segmentID in self.statistics["SegmentIDs"]: data = dict() - data["TrackingIdentifier"] = segment.GetName() - data["ReferencedSegment"] = labelValue + 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(labelValue, modality) + data["measurementItems"] = self.createMeasurementItemsForLabelValue(segmentID, modality) measurements.append(data) return measurements - def createMeasurementItemsForLabelValue(self, labelValue, modality): + def createMeasurementItemsForLabelValue(self, segmentValue, modality): measurementItems = [] - for key in [k for k in self.keys if k not in ["Index", "Segment Name", "Count"]]: + for key in [k for k in self.keys if k not in ["Segment", "GS voxel count"]]: item = dict() - item["value"] = str(self.labelStats[labelValue, key]) + item["value"] = str(self.statistics[segmentValue, key]) item["quantity"] = self.getQuantityCSforKey(key) item["units"] = self.getUnitsCSForKey(key, modality) derivationModifier = self.getDerivatinModifierCSForKey(key) @@ -833,31 +837,31 @@ def createMeasurementItemsForLabelValue(self, labelValue, modality): return measurementItems def getQuantityCSforKey(self, key, modality="CT"): - if key in ["Min", "Max", "Mean", "StdDev"]: + 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 ["Volume mm^3", "Volume cc"]: + 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 = ["Min", "Max", "Mean", "StdDev"] + 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 == "Volume cc": + elif key == "GS volume cc": return self.createCodeSequence("mm3", "UCUM", "cubic millimeter") - elif key == "Volume mm^3": + elif key == "GS volume mm3": return self.createCodeSequence("cm3", "UCUM", "cubic centimeter") return None def getDerivatinModifierCSForKey(self, key): - keys = ["Min", "Max", "Mean", "StdDev"] + keys = ["GS min", "GS max", "GS mean", "GS stdev"] if key in keys: if key == keys[0]: return self.createCodeSequence("R-404FB", "SRT", "Minimum") @@ -875,8 +879,8 @@ def createCodeSequence(self, value, designator, meaning): "CodeMeaning": meaning} def validateSegments(self): - segments = self.filterEmptySegments() - for segment, labelValue in zip(segments, self.labelStats["Labels"]): + 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]): From e322f47d9ae4f8b9f9fdfeb904324e217687a8a5 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 15 Nov 2016 11:22:35 -0500 Subject: [PATCH 39/39] ENH: fixed typos, leading semicolon and unused variables in DICOMSegmentationPlugin --- Py/DICOMSegmentationPlugin.py | 51 +++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 24 deletions(-) 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])