diff --git a/.flake8 b/.flake8 index c30584c18db..abaf16fb25a 100644 --- a/.flake8 +++ b/.flake8 @@ -40,94 +40,16 @@ ignore = \ B009, \ # B010] Do not call setattr with a constant attribute value, it is not any safer than normal property access. B010, \ - # Indentation is not a multiple of four - E111, \ - # Indentation is not a multiple of four (comment) - E114, \ - # expected an indented block (comment) - E115, \ - # unexpected indentation (comment) - E116, \ - # over-indented - E117, \ - # continuation line under-indented for hanging indent - E121, \ - # continuation line missing indentation or outdented - E122, \ - # closing bracket does not match visual indentation - E124, \ - # closing bracket does not match indentation of opening bracket's line - E123, \ - # continuation line with same indent as next logical line - E125, \ - # continuation line over-indented for hanging indent - E126, \ - # continuation line over-indented for visual indent - E127, \ - # continuation line under-indented for visual indent - E128, \ - # visually indented line with same indent as next logical line - E129, \ - # continuation line unaligned for hanging indent - E131, \ - # whitespace after '(' - E201, \ - # whitespace before ')' - E202, \ - # whitespace before ',' - E203, \ - # whitespace before '(' - E211, \ - # multiple spaces before operator - E221, \ - # multiple spaces after operator - E222, \ - # missing whitespace around operator - E225, \ - # missing whitespace around arithmetic operator - E226, \ - # missing whitespace around bitwise or shift operator - E227, \ - # missing whitespace around modulo operator - E228, \ - # missing whitespace after ',' - E231, \ - # multiple spaces after ',' - E241, \ - # unexpected spaces around keyword / parameter equals - E251, \ - # at least two spaces before inline comment - E261, \ - # inline comment should start with '# ' - E262, \ - # block comment should start with '# ' - E265, \ - # too many leading '#' for block comment - E266, \ - # multiple spaces after keyword - E271, \ # multiple imports on one line E401, \ # module level import not at top of file E402, \ # the backslash is redundant between bracket E502, \ - # multiple statements on one line (colon) - E701, \ - # statement ends with a semicolon - E703, \ - # comparison to True should be 'if cond is True:' or 'if cond:' - E712, \ - # test for membership should be 'not in' - E713, \ - # test for object identity should be 'is not' - E714, \ # do not use bare 'except' E722, \ # do not assign a lambda expression, use a def E731, \ - # ambiguous variable name 'l' - E741, \ # 'from module import *' used; unable to detect undefined names F403, \ # Name may be undefined, or defined from star import diff --git a/Applications/SlicerApp/Testing/Python/AtlasTests.py b/Applications/SlicerApp/Testing/Python/AtlasTests.py index f19e0a4765d..a7e59cbf1c7 100644 --- a/Applications/SlicerApp/Testing/Python/AtlasTests.py +++ b/Applications/SlicerApp/Testing/Python/AtlasTests.py @@ -11,17 +11,17 @@ # class AtlasTests(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) - parent.title = "AtlasTests" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + """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) + parent.title = "AtlasTests" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ This is a self test that downloads and displays volumetric atlases from the NA-MIC publication database. For more information: @@ -31,9 +31,9 @@ def __init__(self, parent): Knee Atlas: https://www.slicer.org/publications/item/view/1953 """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -41,54 +41,54 @@ def __init__(self, parent): # class AtlasTestsWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... - # Collapsible button - atlasTests = ctk.ctkCollapsibleButton() - atlasTests.text = "Atlas Tests" - self.layout.addWidget(atlasTests) + # Collapsible button + atlasTests = ctk.ctkCollapsibleButton() + atlasTests.text = "Atlas Tests" + self.layout.addWidget(atlasTests) - # Layout within the dummy collapsible button - dummyFormLayout = qt.QFormLayout(atlasTests) + # Layout within the dummy collapsible button + dummyFormLayout = qt.QFormLayout(atlasTests) - # run Abdominal Test - self.abdominalAtlas = qt.QPushButton("Run Abdominal Test") - self.abdominalAtlas.toolTip = "Downloads abdominal atlas and loads it." - dummyFormLayout.addWidget(self.abdominalAtlas) - self.abdominalAtlas.connect('clicked(bool)', self.onAbdominalAtlas) + # run Abdominal Test + self.abdominalAtlas = qt.QPushButton("Run Abdominal Test") + self.abdominalAtlas.toolTip = "Downloads abdominal atlas and loads it." + dummyFormLayout.addWidget(self.abdominalAtlas) + self.abdominalAtlas.connect('clicked(bool)', self.onAbdominalAtlas) - # run brain Test - self.brainAtlas = qt.QPushButton("Run Brain Test") - self.brainAtlas.toolTip = "Downloads brain atlas and loads it." - dummyFormLayout.addWidget(self.brainAtlas) - self.brainAtlas.connect('clicked(bool)', self.onBrainAtlas) + # run brain Test + self.brainAtlas = qt.QPushButton("Run Brain Test") + self.brainAtlas.toolTip = "Downloads brain atlas and loads it." + dummyFormLayout.addWidget(self.brainAtlas) + self.brainAtlas.connect('clicked(bool)', self.onBrainAtlas) - # run knee Test - self.kneeAtlas = qt.QPushButton("Run Knee Test") - self.kneeAtlas.toolTip = "Downloads knee atlas and loads it." - dummyFormLayout.addWidget(self.kneeAtlas) - self.kneeAtlas.connect('clicked(bool)', self.onKneeAtlas) + # run knee Test + self.kneeAtlas = qt.QPushButton("Run Knee Test") + self.kneeAtlas.toolTip = "Downloads knee atlas and loads it." + dummyFormLayout.addWidget(self.kneeAtlas) + self.kneeAtlas.connect('clicked(bool)', self.onKneeAtlas) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - def onAbdominalAtlas(self): - tester = AtlasTestsTest() - tester.runAbdominalTest() + def onAbdominalAtlas(self): + tester = AtlasTestsTest() + tester.runAbdominalTest() - def onBrainAtlas(self): - tester = AtlasTestsTest() - tester.runBrainTest() + def onBrainAtlas(self): + tester = AtlasTestsTest() + tester.runBrainTest() - def onKneeAtlas(self): - tester = AtlasTestsTest() - tester.runKneeTest() + def onKneeAtlas(self): + tester = AtlasTestsTest() + tester.runKneeTest() # @@ -96,168 +96,168 @@ def onKneeAtlas(self): # class AtlasTestsLogic(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 a dummy logic method that - returns true if the passed in volume - node has valid image data + """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 """ - if not volumeNode: - print('no volume node') - return False - if volumeNode.GetImageData() is None: - print('no image data') - return False - return True + def hasImageData(self, volumeNode): + """This is a dummy logic method that + returns true if the passed in volume + node has valid image data + """ + if not volumeNode: + print('no volume node') + return False + if volumeNode.GetImageData() is None: + print('no image data') + return False + return True -class AtlasTestsTest(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. +class AtlasTestsTest(ScriptedLoadableModuleTest): """ - self.setUp() - self.test_AbdominalAtlasTest() - self.setUp() - self.test_BrainAtlasTest() - self.setUp() - self.test_KneeAtlasTest() - - def runAbdominalTest(self): - self.setUp() - self.test_AbdominalAtlasTest() - - def runBrainTest(self): - self.setUp() - self.test_BrainAtlasTest() - - def runKneeTest(self): - self.setUp() - self.test_KneeAtlasTest() - - def test_AbdominalAtlasTest(self): - self.delayDisplay('Running Abdominal Atlas Test') - downloads = { - 'fileNames': 'Abdominal_Atlas_2012.mrb', - 'loadFiles': True, - 'uris': TESTING_DATA_URL + 'SHA256/5d315abf7d303326669c6075f9eea927eeda2e531a5b1662cfa505806cb498ea', - 'checksums': 'SHA256:5d315abf7d303326669c6075f9eea927eeda2e531a5b1662cfa505806cb498ea', - } - self.perform_AtlasTest(downloads,'I') - - def test_BrainAtlasTest(self): - self.delayDisplay('Running Brain Atlas Test') - downloads = { - 'fileNames': 'BrainAtlas2012.mrb', - 'loadFiles': True, - 'uris': TESTING_DATA_URL + 'SHA256/688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1', - 'checksums': 'SHA256:688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1', - } - self.perform_AtlasTest(downloads,'A1_grayT1') - - def test_KneeAtlasTest(self): - self.delayDisplay('Running Knee Atlas Test') - downloads = { - 'fileNames': 'KneeAtlas2012.mrb', - 'loadFiles': True, - 'uris': TESTING_DATA_URL + 'SHA256/5d5506c07c238918d0c892e7b04c26ad7f43684d89580780bb207d1d860b0b33', - 'checksums': 'SHA256:5d5506c07c238918d0c892e7b04c26ad7f43684d89580780bb207d1d860b0b33', - } - self.perform_AtlasTest(downloads,'I') - - def perform_AtlasTest(self, downloads, testVolumePattern): - """ Perform the actual atlas test. - This includes: download and load the given data, touch all - model hierarchies, and restore all scene views. - downloads : dictionary of URIs and fileNames - testVolumePattern : volume name/id that is tested for valid load + 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 """ - self.delayDisplay("Starting the test") - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL(**downloads) - self.delayDisplay('Finished with download and loading\n') - - volumeNode = slicer.util.getNode(pattern=testVolumePattern) - logic = AtlasTestsLogic() - self.assertIsNotNone( logic.hasImageData(volumeNode) ) - - m = slicer.util.mainWindow() - - # go to the models module - m.moduleSelector().selectModule('Models') - self.delayDisplay("Entered Models module") - - # get model hierarchy nodes that have children hierarchies - numModelHierarchies = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLModelHierarchyNode") - # get the number that we'll be manipulating - numModelHierarchiesToManipulate = 0 - for h in range(numModelHierarchies): - mh = slicer.mrmlScene.GetNthNodeByClass(h, "vtkMRMLModelHierarchyNode") - if mh.GetNumberOfChildrenNodes() > 0 and mh.GetDisplayNode() is not None: - numModelHierarchiesToManipulate += 1 - # iterate over all the hierarchies - hierarchyManipulating = 0 - for h in range(numModelHierarchies): - mh = slicer.mrmlScene.GetNthNodeByClass(h, "vtkMRMLModelHierarchyNode") - numChildren = mh.GetNumberOfChildrenNodes() - if numChildren > 0: - mhd = mh.GetDisplayNode() - # manually added hierarchies may not have display nodes, skip - if mhd is None: - self.delayDisplay("Skipping model hierarchy with no display node " + mh.GetName()) - else: - hierarchyManipulating += 1 - self.delayDisplay("Manipulating model hierarchy " + mh.GetName() + " (" + str(hierarchyManipulating) + "/" + str(numModelHierarchiesToManipulate) + ")") - hierarchyOriginalColor = mhd.GetColor() - hierarchyOriginalVisibility = mhd.GetVisibility() - hierarchyOriginalExpanded = mh.GetExpanded() - # collapse and change the color on the hierarchy to full red - mh.SetExpanded(0) - self.delayDisplay("Model hierarchy " + mh.GetName() + ": expanded = false") - mhd.SetColor(1,0,0) - self.delayDisplay("Model hierarchy " + mh.GetName() + ": color = red") - # set the collapsed visibility to 0 - mhd.SetVisibility(0) - self.delayDisplay("Model hierarchy " + mh.GetName() + ": visibility = off") - # expand, should see all models in correct color - mh.SetExpanded(1) - self.delayDisplay("Model hierarchy " + mh.GetName() + ": expanded = true") - # reset the hierarchy - mhd.SetVisibility(hierarchyOriginalVisibility) - mhd.SetColor(hierarchyOriginalColor) - mh.SetExpanded(hierarchyOriginalExpanded) - - # go to the scene views module - m.moduleSelector().selectModule('SceneViews') - self.delayDisplay("Entered Scene Views module") - - # iterate over the scene views and restore them - numSceneViews = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLSceneViewNode") - for s in range(numSceneViews): - sv = slicer.mrmlScene.GetNthNodeByClass(s, "vtkMRMLSceneViewNode") - self.delayDisplay("Restoring scene " + sv.GetName() + " (" + str(s+1) + "/" + str(numSceneViews) + ")") - sv.RestoreScene() - - self.delayDisplay('Test passed!') + 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_AbdominalAtlasTest() + self.setUp() + self.test_BrainAtlasTest() + self.setUp() + self.test_KneeAtlasTest() + + def runAbdominalTest(self): + self.setUp() + self.test_AbdominalAtlasTest() + + def runBrainTest(self): + self.setUp() + self.test_BrainAtlasTest() + + def runKneeTest(self): + self.setUp() + self.test_KneeAtlasTest() + + def test_AbdominalAtlasTest(self): + self.delayDisplay('Running Abdominal Atlas Test') + downloads = { + 'fileNames': 'Abdominal_Atlas_2012.mrb', + 'loadFiles': True, + 'uris': TESTING_DATA_URL + 'SHA256/5d315abf7d303326669c6075f9eea927eeda2e531a5b1662cfa505806cb498ea', + 'checksums': 'SHA256:5d315abf7d303326669c6075f9eea927eeda2e531a5b1662cfa505806cb498ea', + } + self.perform_AtlasTest(downloads, 'I') + + def test_BrainAtlasTest(self): + self.delayDisplay('Running Brain Atlas Test') + downloads = { + 'fileNames': 'BrainAtlas2012.mrb', + 'loadFiles': True, + 'uris': TESTING_DATA_URL + 'SHA256/688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1', + 'checksums': 'SHA256:688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1', + } + self.perform_AtlasTest(downloads, 'A1_grayT1') + + def test_KneeAtlasTest(self): + self.delayDisplay('Running Knee Atlas Test') + downloads = { + 'fileNames': 'KneeAtlas2012.mrb', + 'loadFiles': True, + 'uris': TESTING_DATA_URL + 'SHA256/5d5506c07c238918d0c892e7b04c26ad7f43684d89580780bb207d1d860b0b33', + 'checksums': 'SHA256:5d5506c07c238918d0c892e7b04c26ad7f43684d89580780bb207d1d860b0b33', + } + self.perform_AtlasTest(downloads, 'I') + + def perform_AtlasTest(self, downloads, testVolumePattern): + """ Perform the actual atlas test. + This includes: download and load the given data, touch all + model hierarchies, and restore all scene views. + downloads : dictionary of URIs and fileNames + testVolumePattern : volume name/id that is tested for valid load + """ + + self.delayDisplay("Starting the test") + # + # first, get some data + # + import SampleData + SampleData.downloadFromURL(**downloads) + self.delayDisplay('Finished with download and loading\n') + + volumeNode = slicer.util.getNode(pattern=testVolumePattern) + logic = AtlasTestsLogic() + self.assertIsNotNone(logic.hasImageData(volumeNode)) + + m = slicer.util.mainWindow() + + # go to the models module + m.moduleSelector().selectModule('Models') + self.delayDisplay("Entered Models module") + + # get model hierarchy nodes that have children hierarchies + numModelHierarchies = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLModelHierarchyNode") + # get the number that we'll be manipulating + numModelHierarchiesToManipulate = 0 + for h in range(numModelHierarchies): + mh = slicer.mrmlScene.GetNthNodeByClass(h, "vtkMRMLModelHierarchyNode") + if mh.GetNumberOfChildrenNodes() > 0 and mh.GetDisplayNode() is not None: + numModelHierarchiesToManipulate += 1 + # iterate over all the hierarchies + hierarchyManipulating = 0 + for h in range(numModelHierarchies): + mh = slicer.mrmlScene.GetNthNodeByClass(h, "vtkMRMLModelHierarchyNode") + numChildren = mh.GetNumberOfChildrenNodes() + if numChildren > 0: + mhd = mh.GetDisplayNode() + # manually added hierarchies may not have display nodes, skip + if mhd is None: + self.delayDisplay("Skipping model hierarchy with no display node " + mh.GetName()) + else: + hierarchyManipulating += 1 + self.delayDisplay("Manipulating model hierarchy " + mh.GetName() + " (" + str(hierarchyManipulating) + "/" + str(numModelHierarchiesToManipulate) + ")") + hierarchyOriginalColor = mhd.GetColor() + hierarchyOriginalVisibility = mhd.GetVisibility() + hierarchyOriginalExpanded = mh.GetExpanded() + # collapse and change the color on the hierarchy to full red + mh.SetExpanded(0) + self.delayDisplay("Model hierarchy " + mh.GetName() + ": expanded = false") + mhd.SetColor(1, 0, 0) + self.delayDisplay("Model hierarchy " + mh.GetName() + ": color = red") + # set the collapsed visibility to 0 + mhd.SetVisibility(0) + self.delayDisplay("Model hierarchy " + mh.GetName() + ": visibility = off") + # expand, should see all models in correct color + mh.SetExpanded(1) + self.delayDisplay("Model hierarchy " + mh.GetName() + ": expanded = true") + # reset the hierarchy + mhd.SetVisibility(hierarchyOriginalVisibility) + mhd.SetColor(hierarchyOriginalColor) + mh.SetExpanded(hierarchyOriginalExpanded) + + # go to the scene views module + m.moduleSelector().selectModule('SceneViews') + self.delayDisplay("Entered Scene Views module") + + # iterate over the scene views and restore them + numSceneViews = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLSceneViewNode") + for s in range(numSceneViews): + sv = slicer.mrmlScene.GetNthNodeByClass(s, "vtkMRMLSceneViewNode") + self.delayDisplay("Restoring scene " + sv.GetName() + " (" + str(s + 1) + "/" + str(numSceneViews) + ")") + sv.RestoreScene() + + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/BRAINSFitRigidRegistrationCrashIssue4139.py b/Applications/SlicerApp/Testing/Python/BRAINSFitRigidRegistrationCrashIssue4139.py index 32cb80b8c1e..7dd62626232 100644 --- a/Applications/SlicerApp/Testing/Python/BRAINSFitRigidRegistrationCrashIssue4139.py +++ b/Applications/SlicerApp/Testing/Python/BRAINSFitRigidRegistrationCrashIssue4139.py @@ -9,26 +9,26 @@ # class BRAINSFitRigidRegistrationCrashIssue4139(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 = "BRAINSFit Rigid Registration vtkITKTransformConverter crash (Issue 4139)" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = [] - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"] # replace with "Firstname Lastname (Organization)" - self.parent.helpText = """This test has been added to check that + """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 = "BRAINSFit Rigid Registration vtkITKTransformConverter crash (Issue 4139)" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = [] + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"] # replace with "Firstname Lastname (Organization)" + self.parent.helpText = """This test has been added to check that Slicer does not crash in vtkITKTransformConverter after completing BRAINSFit rigid registration. Problem has been documented in issue #4139. Commit r24901 fixes the problem by updating vtkITKTransformConverter class. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. and was partially funded by NIH grant 1U24CA194354-01. - """ # replace with organization, grant and thanks. + """ # replace with organization, grant and thanks. # @@ -36,12 +36,12 @@ def __init__(self, parent): # class BRAINSFitRigidRegistrationCrashIssue4139Widget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -49,89 +49,89 @@ def setup(self): # class BRAINSFitRigidRegistrationCrashIssue4139Logic(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 + """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 """ - 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 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 -class BRAINSFitRigidRegistrationCrashIssue4139Test(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. +class BRAINSFitRigidRegistrationCrashIssue4139Test(ScriptedLoadableModuleTest): """ - self.setUp() - self.test_BRAINSFitRigidRegistrationCrashIssue4139() - - def test_BRAINSFitRigidRegistrationCrashIssue4139(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. + 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 """ - self.delayDisplay("Starting the test") + 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_BRAINSFitRigidRegistrationCrashIssue4139() + + def test_BRAINSFitRigidRegistrationCrashIssue4139(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") - logic = BRAINSFitRigidRegistrationCrashIssue4139Logic() + logic = BRAINSFitRigidRegistrationCrashIssue4139Logic() - import SampleData + import SampleData - fixed = SampleData.downloadSample('MRBrainTumor1') - self.assertIsNotNone(logic.hasImageData(fixed)) + fixed = SampleData.downloadSample('MRBrainTumor1') + self.assertIsNotNone(logic.hasImageData(fixed)) - moving = SampleData.downloadSample('MRBrainTumor2') - self.assertIsNotNone(logic.hasImageData(moving)) + moving = SampleData.downloadSample('MRBrainTumor2') + self.assertIsNotNone(logic.hasImageData(moving)) - self.delayDisplay('Finished with download and loading') + self.delayDisplay('Finished with download and loading') - outputTransform = slicer.vtkMRMLLinearTransformNode() - slicer.mrmlScene.AddNode(outputTransform) + outputTransform = slicer.vtkMRMLLinearTransformNode() + slicer.mrmlScene.AddNode(outputTransform) - outputVolume = slicer.vtkMRMLScalarVolumeNode() - slicer.mrmlScene.AddNode(outputVolume) + outputVolume = slicer.vtkMRMLScalarVolumeNode() + slicer.mrmlScene.AddNode(outputVolume) - parameters = { - 'fixedVolume' : fixed, - 'movingVolume' : moving, - 'linearTransform' : outputTransform, - 'outputVolume' : outputVolume, - 'useRigid' : True - } - cmdLineNode = slicer.cli.runSync(slicer.modules.brainsfit, parameters=parameters) - self.assertIsNotNone(cmdLineNode) + parameters = { + 'fixedVolume': fixed, + 'movingVolume': moving, + 'linearTransform': outputTransform, + 'outputVolume': outputVolume, + 'useRigid': True + } + cmdLineNode = slicer.cli.runSync(slicer.modules.brainsfit, parameters=parameters) + self.assertIsNotNone(cmdLineNode) - # If test reach this point without crashing it is a success + # If test reach this point without crashing it is a success - self.delayDisplay('Test passed!') + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/CLIEventTest.py b/Applications/SlicerApp/Testing/Python/CLIEventTest.py index 1cc15e5aa3f..272250a17dd 100644 --- a/Applications/SlicerApp/Testing/Python/CLIEventTest.py +++ b/Applications/SlicerApp/Testing/Python/CLIEventTest.py @@ -10,29 +10,29 @@ # class CLIEventTest(ScriptedLoadableModule): - def __init__(self, parent): - parent.title = "CLIEventTest" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = ["CLI4Test"] - parent.contributors = ["Johan Andruejol (Kitware)"] - parent.helpText = """ + def __init__(self, parent): + parent.title = "CLIEventTest" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = ["CLI4Test"] + parent.contributors = ["Johan Andruejol (Kitware)"] + parent.helpText = """ This is a self test that tests that CLI send all the event properly. """ - parent.acknowledgementText = """""" # replace with organization, grant and thanks. - self.parent = parent + parent.acknowledgementText = """""" # replace with organization, grant and thanks. + self.parent = parent - # Add this test to the SelfTest module's list for discovery when the module - # is created. Since this module may be discovered before SelfTests itself, - # create the list if it doesn't already exist. - try: - slicer.selfTests - except AttributeError: - slicer.selfTests = {} - slicer.selfTests['CLIEventTest'] = self.runTest + # Add this test to the SelfTest module's list for discovery when the module + # is created. Since this module may be discovered before SelfTests itself, + # create the list if it doesn't already exist. + try: + slicer.selfTests + except AttributeError: + slicer.selfTests = {} + slicer.selfTests['CLIEventTest'] = self.runTest - def runTest(self): - tester = CLIEventTestTest() - tester.runTest() + def runTest(self): + tester = CLIEventTestTest() + tester.runTest() # @@ -41,8 +41,8 @@ def runTest(self): class CLIEventTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -50,30 +50,30 @@ def setup(self): # class CLIEventTestLogic(VTKObservationMixin): - def __init__(self): - VTKObservationMixin.__init__(self) + def __init__(self): + VTKObservationMixin.__init__(self) - cli = slicer.vtkMRMLCommandLineModuleNode() - self.StatusModifiedEvent = cli.StatusModifiedEvent - self.StatusEvents = [] + cli = slicer.vtkMRMLCommandLineModuleNode() + self.StatusModifiedEvent = cli.StatusModifiedEvent + self.StatusEvents = [] - self.ExecutionFinished = False - self.StatusEventCallback = None + self.ExecutionFinished = False + self.StatusEventCallback = None - def runCLI(self, cliModule, cliNode, parameters, wait_for_completion): - self.addObserver(cliNode, self.StatusModifiedEvent, self.onCLIModified) - cliNode = slicer.cli.run(cliModule, cliNode, parameters, wait_for_completion) + def runCLI(self, cliModule, cliNode, parameters, wait_for_completion): + self.addObserver(cliNode, self.StatusModifiedEvent, self.onCLIModified) + cliNode = slicer.cli.run(cliModule, cliNode, parameters, wait_for_completion) - def onCLIModified(self, cliNode, event): - print ("-- " + cliNode.GetStatusString() + ":" + cliNode.GetName()) - self.StatusEvents.append(cliNode.GetStatus()) + def onCLIModified(self, cliNode, event): + print("-- " + cliNode.GetStatusString() + ":" + cliNode.GetName()) + self.StatusEvents.append(cliNode.GetStatus()) - if not cliNode.IsBusy(): - self.removeObserver(cliNode, self.StatusModifiedEvent, self.onCLIModified) - self.ExecutionFinished = True + if not cliNode.IsBusy(): + self.removeObserver(cliNode, self.StatusModifiedEvent, self.onCLIModified) + self.ExecutionFinished = True - if self.StatusEventCallback: - self.StatusEventCallback(cliNode) + if self.StatusEventCallback: + self.StatusEventCallback(cliNode) # @@ -82,167 +82,167 @@ def onCLIModified(self, cliNode, event): class CLIEventTestTest(ScriptedLoadableModuleTest): - def setUp(self): - """ Reset the state for testing. - """ + def setUp(self): + """ Reset the state for testing. + """ - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_CLIStatusEventTestSynchronous() - self.test_CLIStatusEventTestAsynchronous() - self.test_CLIStatusEventTestCancel() - self.test_CLIStatusEventOnErrorTestSynchronous() - self.test_CLIStatusEventOnErrorTestAsynchronous() - self.test_SubjectHierarchyReference() - - # Testing a the status event on a normal execution - def test_CLIStatusEventTestSynchronous(self): - self._testCLIStatusEventTest(True) - - def test_CLIStatusEventTestAsynchronous(self): - self._testCLIStatusEventTest(False) + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_CLIStatusEventTestSynchronous() + self.test_CLIStatusEventTestAsynchronous() + self.test_CLIStatusEventTestCancel() + self.test_CLIStatusEventOnErrorTestSynchronous() + self.test_CLIStatusEventOnErrorTestAsynchronous() + self.test_SubjectHierarchyReference() + + # Testing a the status event on a normal execution + def test_CLIStatusEventTestSynchronous(self): + self._testCLIStatusEventTest(True) + + def test_CLIStatusEventTestAsynchronous(self): + self._testCLIStatusEventTest(False) - def _testCLIStatusEventTest(self, wait_for_completion): - self.delayDisplay('Testing status events for a normal execution of a CLI') + def _testCLIStatusEventTest(self, wait_for_completion): + self.delayDisplay('Testing status events for a normal execution of a CLI') - tempFile = qt.QTemporaryFile("CLIEventTest-outputFile-XXXXXX") - self.assertTrue(tempFile.open()) - - logic = CLIEventTestLogic() - parameters = {} - parameters["InputValue1"] = 1 - parameters["InputValue2"] = 2 - parameters["OperationType"] = 'Addition' - parameters["OutputFile"] = tempFile.fileName() - - cliModule = slicer.modules.cli4test - cli = slicer.cli.createNode(cliModule) - self.assertEqual(cli.GetStatus(), cli.Idle) + tempFile = qt.QTemporaryFile("CLIEventTest-outputFile-XXXXXX") + self.assertTrue(tempFile.open()) + + logic = CLIEventTestLogic() + parameters = {} + parameters["InputValue1"] = 1 + parameters["InputValue2"] = 2 + parameters["OperationType"] = 'Addition' + parameters["OutputFile"] = tempFile.fileName() + + cliModule = slicer.modules.cli4test + cli = slicer.cli.createNode(cliModule) + self.assertEqual(cli.GetStatus(), cli.Idle) - logic.runCLI(cliModule, cli, parameters, wait_for_completion) + logic.runCLI(cliModule, cli, parameters, wait_for_completion) - while not logic.ExecutionFinished: - self.delayDisplay('Waiting for module to complete...') + while not logic.ExecutionFinished: + self.delayDisplay('Waiting for module to complete...') - cli = slicer.vtkMRMLCommandLineModuleNode() - expectedEvents = [] - if not wait_for_completion: - expectedEvents.append(cli.Scheduled) - expectedEvents.append(cli.Completed) + cli = slicer.vtkMRMLCommandLineModuleNode() + expectedEvents = [] + if not wait_for_completion: + expectedEvents.append(cli.Scheduled) + expectedEvents.append(cli.Completed) - # Ignore cli.Running event (it may or may not be fired) - if cli.Running in logic.StatusEvents: - logic.StatusEvents.remove(cli.Running) + # Ignore cli.Running event (it may or may not be fired) + if cli.Running in logic.StatusEvents: + logic.StatusEvents.remove(cli.Running) - self.assertEqual(logic.StatusEvents, expectedEvents) - self.delayDisplay('Testing normal execution Passed') - - # Testing the status event on a bad execution - def test_CLIStatusEventOnErrorTestSynchronous(self): - self._testCLIStatusEventOnErrorTest(True) + self.assertEqual(logic.StatusEvents, expectedEvents) + self.delayDisplay('Testing normal execution Passed') + + # Testing the status event on a bad execution + def test_CLIStatusEventOnErrorTestSynchronous(self): + self._testCLIStatusEventOnErrorTest(True) - def test_CLIStatusEventOnErrorTestAsynchronous(self): - self._testCLIStatusEventOnErrorTest(False) - - def _testCLIStatusEventOnErrorTest(self, wait_for_completion): - self.delayDisplay('Testing status events for a bad execution of a CLI') - - tempFile = qt.QTemporaryFile("CLIEventTest-outputFile-XXXXXX") - self.assertTrue(tempFile.open()) - - logic = CLIEventTestLogic() - parameters = {} - parameters["InputValue1"] = 1 - parameters["InputValue2"] = 2 - parameters["OperationType"] = 'Fail' - parameters["OutputFile"] = tempFile.fileName() - - cliModule = slicer.modules.cli4test - cli = slicer.cli.createNode(cliModule) - self.assertEqual(cli.GetStatus(), cli.Idle) - - logic.runCLI(cliModule, cli, parameters, wait_for_completion) - - while not logic.ExecutionFinished: - self.delayDisplay('Waiting for module to complete...') - - cli = slicer.vtkMRMLCommandLineModuleNode() - expectedEvents = [] - if not wait_for_completion: - expectedEvents.append(cli.Scheduled) - expectedEvents.append(cli.CompletedWithErrors) - - # Ignore cli.Running event (it may or may not be fired) - if cli.Running in logic.StatusEvents: - logic.StatusEvents.remove(cli.Running) - - self.assertEqual(logic.StatusEvents, expectedEvents) - self.delayDisplay('Testing bad execution Passed') - - # Testing a the status event when canceling - def test_CLIStatusEventTestCancel(self): - self.delayDisplay('Testing status events when cancelling the execution of a CLI') - - tempFile = qt.QTemporaryFile("CLIEventTest-outputFile-XXXXXX") - self.assertTrue(tempFile.open()) - - logic = CLIEventTestLogic() - parameters = {} - parameters["InputValue1"] = 1 - parameters["InputValue2"] = 2 - parameters["OperationType"] = 'Addition' - parameters["OutputFile"] = tempFile.fileName() + def test_CLIStatusEventOnErrorTestAsynchronous(self): + self._testCLIStatusEventOnErrorTest(False) + + def _testCLIStatusEventOnErrorTest(self, wait_for_completion): + self.delayDisplay('Testing status events for a bad execution of a CLI') + + tempFile = qt.QTemporaryFile("CLIEventTest-outputFile-XXXXXX") + self.assertTrue(tempFile.open()) + + logic = CLIEventTestLogic() + parameters = {} + parameters["InputValue1"] = 1 + parameters["InputValue2"] = 2 + parameters["OperationType"] = 'Fail' + parameters["OutputFile"] = tempFile.fileName() + + cliModule = slicer.modules.cli4test + cli = slicer.cli.createNode(cliModule) + self.assertEqual(cli.GetStatus(), cli.Idle) + + logic.runCLI(cliModule, cli, parameters, wait_for_completion) + + while not logic.ExecutionFinished: + self.delayDisplay('Waiting for module to complete...') + + cli = slicer.vtkMRMLCommandLineModuleNode() + expectedEvents = [] + if not wait_for_completion: + expectedEvents.append(cli.Scheduled) + expectedEvents.append(cli.CompletedWithErrors) + + # Ignore cli.Running event (it may or may not be fired) + if cli.Running in logic.StatusEvents: + logic.StatusEvents.remove(cli.Running) + + self.assertEqual(logic.StatusEvents, expectedEvents) + self.delayDisplay('Testing bad execution Passed') + + # Testing a the status event when canceling + def test_CLIStatusEventTestCancel(self): + self.delayDisplay('Testing status events when cancelling the execution of a CLI') + + tempFile = qt.QTemporaryFile("CLIEventTest-outputFile-XXXXXX") + self.assertTrue(tempFile.open()) + + logic = CLIEventTestLogic() + parameters = {} + parameters["InputValue1"] = 1 + parameters["InputValue2"] = 2 + parameters["OperationType"] = 'Addition' + parameters["OutputFile"] = tempFile.fileName() - cliModule = slicer.modules.cli4test - cli = slicer.cli.createNode(cliModule) - self.assertEqual(cli.GetStatus(), cli.Idle) + cliModule = slicer.modules.cli4test + cli = slicer.cli.createNode(cliModule) + self.assertEqual(cli.GetStatus(), cli.Idle) - logic.runCLI(cliModule, cli, parameters, False) - cli.Cancel() + logic.runCLI(cliModule, cli, parameters, False) + cli.Cancel() - while not logic.ExecutionFinished: - self.delayDisplay('Waiting for module to complete...') + while not logic.ExecutionFinished: + self.delayDisplay('Waiting for module to complete...') - expectedEvents = [ - cli.Scheduled, - cli.Cancelling, - cli.Cancelled, - ] - - # Ignore cli.Running event (it may or may not be fired) - if cli.Running in logic.StatusEvents: - logic.StatusEvents.remove(cli.Running) + expectedEvents = [ + cli.Scheduled, + cli.Cancelling, + cli.Cancelled, + ] + + # Ignore cli.Running event (it may or may not be fired) + if cli.Running in logic.StatusEvents: + logic.StatusEvents.remove(cli.Running) - self.assertEqual(logic.StatusEvents, expectedEvents) - self.delayDisplay('Testing cancelled execution Passed') + self.assertEqual(logic.StatusEvents, expectedEvents) + self.delayDisplay('Testing cancelled execution Passed') - def test_SubjectHierarchyReference(self): - self.delayDisplay('Test that output node moved to referenced node location in subject hierarchy') + def test_SubjectHierarchyReference(self): + self.delayDisplay('Test that output node moved to referenced node location in subject hierarchy') - self.delayDisplay('Load input volume') - import SampleData - inputVolume = SampleData.downloadSample("MRHead") + self.delayDisplay('Load input volume') + import SampleData + inputVolume = SampleData.downloadSample("MRHead") - self.delayDisplay('Create subject hierarchy of input volume') - shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - self.patientItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), "John Doe") - self.studyItemID = shNode.CreateStudyItem(self.patientItemID, "Some study") - self.folderItemID = shNode.CreateFolderItem(self.studyItemID, "Some group") - shNode.SetItemParent(shNode.GetItemByDataNode(inputVolume), self.folderItemID) + self.delayDisplay('Create subject hierarchy of input volume') + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + self.patientItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), "John Doe") + self.studyItemID = shNode.CreateStudyItem(self.patientItemID, "Some study") + self.folderItemID = shNode.CreateFolderItem(self.studyItemID, "Some group") + shNode.SetItemParent(shNode.GetItemByDataNode(inputVolume), self.folderItemID) - outputVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") - # New node is expected to be created in the subject hierarchy root - self.assertEqual(shNode.GetItemParent(shNode.GetItemByDataNode(outputVolume)), shNode.GetSceneItemID()) + outputVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") + # New node is expected to be created in the subject hierarchy root + self.assertEqual(shNode.GetItemParent(shNode.GetItemByDataNode(outputVolume)), shNode.GetSceneItemID()) - self.delayDisplay('Run CLI module') - cliParams = {'InputVolume': inputVolume.GetID(), 'OutputVolume': outputVolume.GetID(), 'ThresholdValue' : 100, 'ThresholdType' : 'Above'} - cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True) + self.delayDisplay('Run CLI module') + cliParams = {'InputVolume': inputVolume.GetID(), 'OutputVolume': outputVolume.GetID(), 'ThresholdValue': 100, 'ThresholdType': 'Above'} + cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True) - # After CLI execution is completed, output volume must be in the same folder as the referenced node - self.assertEqual(shNode.GetItemParent(shNode.GetItemByDataNode(outputVolume)), shNode.GetItemParent(shNode.GetItemByDataNode(inputVolume))) + # After CLI execution is completed, output volume must be in the same folder as the referenced node + self.assertEqual(shNode.GetItemParent(shNode.GetItemByDataNode(outputVolume)), shNode.GetItemParent(shNode.GetItemByDataNode(inputVolume))) - # After pending events are processed, the output volume must be in the same subject hierarchy folder - slicer.app.processEvents() - self.assertEqual(shNode.GetItemParent(shNode.GetItemByDataNode(outputVolume)), shNode.GetItemParent(shNode.GetItemByDataNode(inputVolume))) + # After pending events are processed, the output volume must be in the same subject hierarchy folder + slicer.app.processEvents() + self.assertEqual(shNode.GetItemParent(shNode.GetItemByDataNode(outputVolume)), shNode.GetItemParent(shNode.GetItemByDataNode(inputVolume))) diff --git a/Applications/SlicerApp/Testing/Python/CLISerializationTest.py b/Applications/SlicerApp/Testing/Python/CLISerializationTest.py index bc2e0ea3296..20d0f83e25d 100644 --- a/Applications/SlicerApp/Testing/Python/CLISerializationTest.py +++ b/Applications/SlicerApp/Testing/Python/CLISerializationTest.py @@ -33,217 +33,217 @@ class CLISerializationTest: - def __init__(self): - self.SlicerExecutable = None + def __init__(self): + self.SlicerExecutable = None - def _runCLI(self, cli_name, option, json_file_path, parameters=[]): - args = ['--launch', - cli_name, - option, json_file_path, - ] - args.extend(parameters) - return run(self.SlicerExecutable, args) + def _runCLI(self, cli_name, option, json_file_path, parameters=[]): + args = ['--launch', + cli_name, + option, json_file_path, + ] + args.extend(parameters) + return run(self.SlicerExecutable, args) - def serializeCLI(self, cli_name, json_file_path, parameters=[]): - return self._runCLI(cli_name, '--serialize', json_file_path, parameters) + def serializeCLI(self, cli_name, json_file_path, parameters=[]): + return self._runCLI(cli_name, '--serialize', json_file_path, parameters) - def deserializeCLI(self, cli_name, json_file_path, parameters=[]): - return self._runCLI(cli_name, '--deserialize', json_file_path, parameters) + def deserializeCLI(self, cli_name, json_file_path, parameters=[]): + return self._runCLI(cli_name, '--deserialize', json_file_path, parameters) if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Test command line CLIs serialization/deserialization.') - # Common options - parser.add_argument("/path/to/Slicer") - parser.add_argument("/path/to/data_dir") - parser.add_argument("/path/to/MRHeadResampled.nhdr") - parser.add_argument("/path/to/CTHeadAxial.nhdr") - parser.add_argument("/path/to/temp_dir") - args = parser.parse_args() - - # Get testing parameters - slicer_executable = os.path.expanduser(getattr(args, "/path/to/Slicer")) - data_dir = os.path.expanduser(getattr(args, "/path/to/data_dir")) - mrHeadResampled = os.path.expanduser(getattr(args, "/path/to/MRHeadResampled.nhdr")) - ctHeadAxial = os.path.expanduser(getattr(args, "/path/to/CTHeadAxial.nhdr")) - temp_dir = os.path.expanduser(getattr(args, "/path/to/temp_dir")) - - # Create input/output - serializeSeedsOutFile = '%s/%s.acsv' %(temp_dir, 'SeedsSerialized') - deserializeSeedsOutFile = '%s/%s.acsv' %(temp_dir, 'SeedsDeSerialized') - json_file = '%s/%s.json' %(temp_dir, 'ExecutionModelTourSerialized') - - #Copy .mrml file to prevent modification of source tree - mrml_source_path = os.path.join(data_dir, 'ExecutionModelTourTest.mrml') - mrml_dest_path = os.path.join(temp_dir, 'ExecutionModelTourTestPython.mrml') - shutil.copyfile(mrml_source_path, mrml_dest_path) - - # -- Test ExecutionModelTour -- - EMTSerializer = CLISerializationTest() - EMTSerializer.SlicerExecutable = slicer_executable - CLIName = 'ExecutionModelTour' - required_inputs = [ - '--transform1', '%s/ExecutionModelTourTestPython.mrml#vtkMRMLLinearTransformNode1'%(temp_dir), - '--transform2', '%s/ExecutionModelTourTestPython.mrml#vtkMRMLLinearTransformNode2'%(temp_dir), - mrHeadResampled, - ctHeadAxial, + parser = argparse.ArgumentParser(description='Test command line CLIs serialization/deserialization.') + # Common options + parser.add_argument("/path/to/Slicer") + parser.add_argument("/path/to/data_dir") + parser.add_argument("/path/to/MRHeadResampled.nhdr") + parser.add_argument("/path/to/CTHeadAxial.nhdr") + parser.add_argument("/path/to/temp_dir") + args = parser.parse_args() + + # Get testing parameters + slicer_executable = os.path.expanduser(getattr(args, "/path/to/Slicer")) + data_dir = os.path.expanduser(getattr(args, "/path/to/data_dir")) + mrHeadResampled = os.path.expanduser(getattr(args, "/path/to/MRHeadResampled.nhdr")) + ctHeadAxial = os.path.expanduser(getattr(args, "/path/to/CTHeadAxial.nhdr")) + temp_dir = os.path.expanduser(getattr(args, "/path/to/temp_dir")) + + # Create input/output + serializeSeedsOutFile = '%s/%s.acsv' % (temp_dir, 'SeedsSerialized') + deserializeSeedsOutFile = '%s/%s.acsv' % (temp_dir, 'SeedsDeSerialized') + json_file = '%s/%s.json' % (temp_dir, 'ExecutionModelTourSerialized') + + # Copy .mrml file to prevent modification of source tree + mrml_source_path = os.path.join(data_dir, 'ExecutionModelTourTest.mrml') + mrml_dest_path = os.path.join(temp_dir, 'ExecutionModelTourTestPython.mrml') + shutil.copyfile(mrml_source_path, mrml_dest_path) + + # -- Test ExecutionModelTour -- + EMTSerializer = CLISerializationTest() + EMTSerializer.SlicerExecutable = slicer_executable + CLIName = 'ExecutionModelTour' + required_inputs = [ + '--transform1', '%s/ExecutionModelTourTestPython.mrml#vtkMRMLLinearTransformNode1' % (temp_dir), + '--transform2', '%s/ExecutionModelTourTestPython.mrml#vtkMRMLLinearTransformNode2' % (temp_dir), + mrHeadResampled, + ctHeadAxial, ] - serialize_options = [ - '--integer', '30', - '--double', '30', - '-f', '1.3,2,-14', - '--files', '1.does,2.not,3.matter', - '--string_vector', 'foo,bar,foobar', - '--enumeration', 'Bill', - '--boolean1', - '--seed', '1.0,0.0,-1.0', - '--seedsOutFile', serializeSeedsOutFile + serialize_options = [ + '--integer', '30', + '--double', '30', + '-f', '1.3,2,-14', + '--files', '1.does,2.not,3.matter', + '--string_vector', 'foo,bar,foobar', + '--enumeration', 'Bill', + '--boolean1', + '--seed', '1.0,0.0,-1.0', + '--seedsOutFile', serializeSeedsOutFile ] - parameters = serialize_options - parameters.extend(required_inputs) - - # Serialize the CLI - (returncode, serializeErr, serializeOut) = EMTSerializer.serializeCLI(CLIName, json_file, parameters) - if returncode != EXIT_SUCCESS: - print("Problem while serializing the CLI: %s" %serializeErr) - exit(EXIT_FAILURE) - - # Make sure the Json is generated correctly - expected_json = { - "Parameters" : - { - "Boolean Parameters" : - { - "boolean1" : True, - "boolean2" : False, - "boolean3" : False - }, - "Enumeration Parameters" : - { - "stringChoice" : "Bill" - }, - "File, Directory and Image Parameters" : - { - "directory1" : "", - "file1" : "", - "files" : [ "1.does", "2.not", "3.matter" ], - "image1" : "", - "image2" : "", - "outputFile1" : "" - }, - "Generic Tables" : - { - "inputDT" : "", - "outputDT" : "" - }, - "Geometry Parameters" : - { - "InputModel" : "", - "ModelSceneFile" : [], - "OutputModel" : "" - }, - "Index Parameters" : - { - "arg0" : mrHeadResampled, - "arg1" : ctHeadAxial - }, - "Measurements" : - { - "inputFA" : "", - "outputFA" : "" - }, - "Point Parameters" : - { - "seed" : [[1.0,0.0,-1.0]], - "seedsFile" : "", - "seedsOutFile" : serializeSeedsOutFile, - }, - "Regions of interest" : - { - "regions" : [] - }, - "Scalar Parameters (\u00e1rv\u00edzt\u0171r\u0151 t\u00fck\u00f6rf\u00far\u00f3g\u00e9p)" : - { - "doubleVariable" : 30, - "integerVariable" : 30 - }, - "Simple return types" : - { - "abooleanreturn" : False, - "adoublereturn" : 14, - "afloatreturn" : 7, - "anintegerreturn" : 5, - "anintegervectorreturn" : [], - "astringchoicereturn" : "Bill", - "astringreturn" : "Hello" - }, - "Transform Parameters" : - { - "transform1" : "%s/ExecutionModelTourTestPython.mrml#vtkMRMLLinearTransformNode1"%(temp_dir), - "transform2" : "%s/ExecutionModelTourTestPython.mrml#vtkMRMLLinearTransformNode2"%(temp_dir), - "transformInput" : "", - "transformInputBspline" : "", - "transformInputNonlinear" : "", - "transformOutput" : "", - "transformOutputBspline" : "", - "transformOutputNonlinear" : "" - }, - "Vector Parameters" : + parameters = serialize_options + parameters.extend(required_inputs) + + # Serialize the CLI + (returncode, serializeErr, serializeOut) = EMTSerializer.serializeCLI(CLIName, json_file, parameters) + if returncode != EXIT_SUCCESS: + print("Problem while serializing the CLI: %s" % serializeErr) + exit(EXIT_FAILURE) + + # Make sure the Json is generated correctly + expected_json = { + "Parameters": { - "floatVector" : [ 1.2999999523162842, 2, -14 ], - "stringVector" : [ "foo", "bar", "foobar" ] + "Boolean Parameters": + { + "boolean1": True, + "boolean2": False, + "boolean3": False + }, + "Enumeration Parameters": + { + "stringChoice": "Bill" + }, + "File, Directory and Image Parameters": + { + "directory1": "", + "file1": "", + "files": ["1.does", "2.not", "3.matter"], + "image1": "", + "image2": "", + "outputFile1": "" + }, + "Generic Tables": + { + "inputDT": "", + "outputDT": "" + }, + "Geometry Parameters": + { + "InputModel": "", + "ModelSceneFile": [], + "OutputModel": "" + }, + "Index Parameters": + { + "arg0": mrHeadResampled, + "arg1": ctHeadAxial + }, + "Measurements": + { + "inputFA": "", + "outputFA": "" + }, + "Point Parameters": + { + "seed": [[1.0, 0.0, -1.0]], + "seedsFile": "", + "seedsOutFile": serializeSeedsOutFile, + }, + "Regions of interest": + { + "regions": [] + }, + "Scalar Parameters (\u00e1rv\u00edzt\u0171r\u0151 t\u00fck\u00f6rf\u00far\u00f3g\u00e9p)": + { + "doubleVariable": 30, + "integerVariable": 30 + }, + "Simple return types": + { + "abooleanreturn": False, + "adoublereturn": 14, + "afloatreturn": 7, + "anintegerreturn": 5, + "anintegervectorreturn": [], + "astringchoicereturn": "Bill", + "astringreturn": "Hello" + }, + "Transform Parameters": + { + "transform1": "%s/ExecutionModelTourTestPython.mrml#vtkMRMLLinearTransformNode1" % (temp_dir), + "transform2": "%s/ExecutionModelTourTestPython.mrml#vtkMRMLLinearTransformNode2" % (temp_dir), + "transformInput": "", + "transformInputBspline": "", + "transformInputNonlinear": "", + "transformOutput": "", + "transformOutputBspline": "", + "transformOutputNonlinear": "" + }, + "Vector Parameters": + { + "floatVector": [1.2999999523162842, 2, -14], + "stringVector": ["foo", "bar", "foobar"] + } } - } } - with open(json_file, encoding='utf8') as file: - data = json.load(file) - if data != expected_json: - print('Json comparison failed !') - expected_json_filename = temp_dir+'/ExecutionModelTourSerializedBaseline.json' - print("Expected json: " + expected_json_filename) - with open(expected_json_filename, 'w', encoding='utf8') as outfile: - json.dump(expected_json, outfile, indent="\t", ensure_ascii=False) - actual_json_filename = temp_dir+'/ExecutionModelTourSerializedActual.json' - print("Actual json: " + actual_json_filename) - with open(actual_json_filename, 'w', encoding='utf8') as outfile: - json.dump(data, outfile, indent="\t", ensure_ascii=False) - exit(EXIT_FAILURE) - - # Now try to deserialize the CLI. - parameters = [ - '--seedsOutFile', deserializeSeedsOutFile + with open(json_file, encoding='utf8') as file: + data = json.load(file) + if data != expected_json: + print('Json comparison failed !') + expected_json_filename = temp_dir + '/ExecutionModelTourSerializedBaseline.json' + print("Expected json: " + expected_json_filename) + with open(expected_json_filename, 'w', encoding='utf8') as outfile: + json.dump(expected_json, outfile, indent="\t", ensure_ascii=False) + actual_json_filename = temp_dir + '/ExecutionModelTourSerializedActual.json' + print("Actual json: " + actual_json_filename) + with open(actual_json_filename, 'w', encoding='utf8') as outfile: + json.dump(data, outfile, indent="\t", ensure_ascii=False) + exit(EXIT_FAILURE) + + # Now try to deserialize the CLI. + parameters = [ + '--seedsOutFile', deserializeSeedsOutFile ] - (returncode, deserializeErr, deserializeOut) = EMTSerializer.deserializeCLI(CLIName, json_file, parameters) - if returncode != EXIT_SUCCESS: - print("Problem while deserializing the CLI: %s" %deserializeErr) - exit(EXIT_FAILURE) - - # Finally compare seeds file - with open(serializeSeedsOutFile) as in_file, open(deserializeSeedsOutFile) as out_file: - in_reader = csv.reader(in_file) - out_reader = csv.reader(out_file) - - serializedRows = list(in_reader) - deserializedRows = list(out_reader) - if len(serializedRows) != len(deserializedRows): - print('Seeds comparison failed, files have different number of rows !') - exit(EXIT_FAILURE) - - for i in range(len(serializedRows)): - if serializedRows[i] != deserializedRows[i]: - print('Row #%s comparison failed:' %i) - print('Serialize row: %s' %serializedRows[i]) - print('Deserialize row: %s' %deserializedRows[i]) + (returncode, deserializeErr, deserializeOut) = EMTSerializer.deserializeCLI(CLIName, json_file, parameters) + if returncode != EXIT_SUCCESS: + print("Problem while deserializing the CLI: %s" % deserializeErr) exit(EXIT_FAILURE) - try: - os.remove(serializeSeedsOutFile) - os.remove(deserializeSeedsOutFile) - os.remove(json_file) - except AttributeError as OSError: - pass - - print("\n=> ok") - exit(EXIT_SUCCESS) + # Finally compare seeds file + with open(serializeSeedsOutFile) as in_file, open(deserializeSeedsOutFile) as out_file: + in_reader = csv.reader(in_file) + out_reader = csv.reader(out_file) + + serializedRows = list(in_reader) + deserializedRows = list(out_reader) + if len(serializedRows) != len(deserializedRows): + print('Seeds comparison failed, files have different number of rows !') + exit(EXIT_FAILURE) + + for i in range(len(serializedRows)): + if serializedRows[i] != deserializedRows[i]: + print('Row #%s comparison failed:' % i) + print('Serialize row: %s' % serializedRows[i]) + print('Deserialize row: %s' % deserializedRows[i]) + exit(EXIT_FAILURE) + + try: + os.remove(serializeSeedsOutFile) + os.remove(deserializeSeedsOutFile) + os.remove(json_file) + except AttributeError as OSError: + pass + + print("\n=> ok") + exit(EXIT_SUCCESS) diff --git a/Applications/SlicerApp/Testing/Python/DCMTKPrivateDictTest.py b/Applications/SlicerApp/Testing/Python/DCMTKPrivateDictTest.py index fb8c2f9f8f9..9cfcc0938c6 100644 --- a/Applications/SlicerApp/Testing/Python/DCMTKPrivateDictTest.py +++ b/Applications/SlicerApp/Testing/Python/DCMTKPrivateDictTest.py @@ -4,17 +4,17 @@ dcmfile = sys.argv[1] -dcmdump=DICOMLib.DICOMCommand('dcmdump',[dcmfile]) -dump=str(dcmdump.start()).replace("\\r","\r").replace("\\n","\n").splitlines() +dcmdump = DICOMLib.DICOMCommand('dcmdump', [dcmfile]) +dump = str(dcmdump.start()).replace("\\r", "\r").replace("\\n", "\n").splitlines() found_private_tag = False for line in dump: - line = line.split(' ') - if line[0] == '(2001,1003)': - if line[-1] == "DiffusionBFactor": - found_private_tag = True - break + line = line.split(' ') + if line[0] == '(2001,1003)': + if line[-1] == "DiffusionBFactor": + found_private_tag = True + break if not found_private_tag: - raise Exception("Could not find 'DiffusionBFactor' " - "private tag reading file '%s' using 'dcmdump' !" % dcmfile) + raise Exception("Could not find 'DiffusionBFactor' " + "private tag reading file '%s' using 'dcmdump' !" % dcmfile) diff --git a/Applications/SlicerApp/Testing/Python/DICOMReaders.py b/Applications/SlicerApp/Testing/Python/DICOMReaders.py index ea5015d727a..45eca2f555c 100644 --- a/Applications/SlicerApp/Testing/Python/DICOMReaders.py +++ b/Applications/SlicerApp/Testing/Python/DICOMReaders.py @@ -15,18 +15,18 @@ # class DICOMReaders(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "DICOMReaders" - parent.categories = ["DICOMReaders"] - parent.dependencies = [] - parent.contributors = ["Steve Pieper (Isomics)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "DICOMReaders" + parent.categories = ["DICOMReaders"] + parent.dependencies = [] + parent.contributors = ["Steve Pieper (Isomics)"] + parent.helpText = """ This module was developed to confirm that different DICOM reading approaches result in the same volumes loaded in Slicer (or that old readers fail but fixed readers succeed). """ - parent.acknowledgementText = """This work is supported primarily by the National Institutes of Health, National Cancer Institute, Informatics Technology for Cancer Research (ITCR) program, grant Quantitative Image Informatics for Cancer Research (QIICR) (U24 CA180918, PIs Kikinis and Fedorov). We also acknowledge support of the following grants: Neuroimage Analysis Center (NAC) (P41 EB015902, PI Kikinis) and National Center for Image Guided Therapy (NCIGT) (P41 EB015898, PI Tempany). + parent.acknowledgementText = """This work is supported primarily by the National Institutes of Health, National Cancer Institute, Informatics Technology for Cancer Research (ITCR) program, grant Quantitative Image Informatics for Cancer Research (QIICR) (U24 CA180918, PIs Kikinis and Fedorov). We also acknowledge support of the following grants: Neuroimage Analysis Center (NAC) (P41 EB015902, PI Kikinis) and National Center for Image Guided Therapy (NCIGT) (P41 EB015898, PI Tempany). This file was originally developed by Steve Pieper, Isomics, Inc. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -35,280 +35,280 @@ def __init__(self, parent): class DICOMReadersWidget(ScriptedLoadableModuleWidget): - def setup(self): - # Instantiate and connect widgets ... - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + # Instantiate and connect widgets ... + ScriptedLoadableModuleWidget.setup(self) - self.layout.addStretch(1) + self.layout.addStretch(1) class DICOMReadersTest(ScriptedLoadableModuleTest): - """ - This is the test case - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. - """ - self.delayDisplay("Closing the scene") - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - slicer.mrmlScene.Clear(0) - - def runTest(self): - """Run as few or as many tests as needed here. """ - self.setUp() - self.test_AlternateReaders() - self.setUp() - self.test_MissingSlices() - - def test_AlternateReaders(self): - """ Test the DICOM loading of sample testing data + This is the test case """ - testPass = True - - self.delayDisplay("Starting the DICOM test") - - referenceData = [ - { "url": TESTING_DATA_URL + "SHA256/3450ef9372a3460a2f181c8d3bb35a74b4f0acb10c6e18cfcf7804e1d99bf843", - "checksum": "SHA256:3450ef9372a3460a2f181c8d3bb35a74b4f0acb10c6e18cfcf7804e1d99bf843", - "fileName": "Mouse-MR-example-where-GDCM_fails.zip", - "name": "Mouse-MR-example-where-GDCM_fails", - "seriesUID": "1.3.6.1.4.1.9590.100.1.2.366426457713813178933224342280246227461", - # GDCM rejects loading. - # DCMTK reads it but then ITK rejects loading the image with 0 spacing. - "expectedFailures": ["GDCM", "Archetype", "DCMTK", "GDCM with DCMTK fallback"], - "voxelValueQuantity": "(110852, DCM, \"MR signal intensity\")", - "voxelValueUnits": "(1, UCUM, \"no units\")" - }, - { "url": TESTING_DATA_URL + "SHA256/899f3f8617ca53bad7dca0b2908478319e708b48ff41dfa64b6bac1d76529928", - "checksum": "SHA256:899f3f8617ca53bad7dca0b2908478319e708b48ff41dfa64b6bac1d76529928", - "fileName": "deidentifiedMRHead-dcm-one-series.zip", - "name": "deidentifiedMRHead-dcm-one-series", - "seriesUID": "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.270.0", - "expectedFailures": [], - "voxelValueQuantity": "(110852, DCM, \"MR signal intensity\")", - "voxelValueUnits": "(1, UCUM, \"no units\")" - } - ] - - # another dataset that could be added in the future - currently fails for all readers - # due to invalid format - see https://issues.slicer.org/view.php?id=3569 - #{ "url": TESTING_DATA_URL + "SHA256/4cbd051dc249ea47d0f7b4147ea8340ba11a4a18a1771d37c387e40538374cab", - #"fileName": "RIDER_bug.zip", - #"name": "RIDER_bug", - #"seriesUID": "1.3.6.1.4.1.9328.50.7.261772317324041365541450388603508531852", - #"expectedFailures": [] - #} - - loadingResult = {} - # - # first, get the data - a zip file of dicom data - # - self.delayDisplay("Downloading") - for dataset in referenceData: - try: - import SampleData - dicomFilesDirectory = SampleData.downloadFromURL( - fileNames=dataset['fileName'], uris=dataset['url'], checksums=dataset['checksum'])[0] - self.delayDisplay('Finished with download') - - # - # insert the data into the database - # - self.delayDisplay("Switching to temp database directory") - originalDatabaseDirectory = DICOMUtils.openTemporaryDatabase('tempDICOMDatabase') - - self.delayDisplay('Importing DICOM') - slicer.util.selectModule("DICOM") - - browserWidget = slicer.modules.DICOMWidget.browserWidget - dicomBrowser = browserWidget.dicomBrowser - dicomBrowser.importDirectory(dicomFilesDirectory, dicomBrowser.ImportDirectoryAddLink) - dicomBrowser.waitForImportFinished() - - # - # select the series - # - - browserWidget.onSeriesSelected([dataset['seriesUID']]) - # load the data by series UID - browserWidget.examineForLoading() - # Get first selected loadable - for plugin in browserWidget.loadablesByPlugin: - for loadable in browserWidget.loadablesByPlugin[plugin]: - if loadable.selected: - # found the first selected loadable - break + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + self.delayDisplay("Closing the scene") + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + slicer.mrmlScene.Clear(0) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_AlternateReaders() + self.setUp() + self.test_MissingSlices() + + def test_AlternateReaders(self): + """ Test the DICOM loading of sample testing data + """ + testPass = True + + self.delayDisplay("Starting the DICOM test") + + referenceData = [ + {"url": TESTING_DATA_URL + "SHA256/3450ef9372a3460a2f181c8d3bb35a74b4f0acb10c6e18cfcf7804e1d99bf843", + "checksum": "SHA256:3450ef9372a3460a2f181c8d3bb35a74b4f0acb10c6e18cfcf7804e1d99bf843", + "fileName": "Mouse-MR-example-where-GDCM_fails.zip", + "name": "Mouse-MR-example-where-GDCM_fails", + "seriesUID": "1.3.6.1.4.1.9590.100.1.2.366426457713813178933224342280246227461", + # GDCM rejects loading. + # DCMTK reads it but then ITK rejects loading the image with 0 spacing. + "expectedFailures": ["GDCM", "Archetype", "DCMTK", "GDCM with DCMTK fallback"], + "voxelValueQuantity": "(110852, DCM, \"MR signal intensity\")", + "voxelValueUnits": "(1, UCUM, \"no units\")" + }, + {"url": TESTING_DATA_URL + "SHA256/899f3f8617ca53bad7dca0b2908478319e708b48ff41dfa64b6bac1d76529928", + "checksum": "SHA256:899f3f8617ca53bad7dca0b2908478319e708b48ff41dfa64b6bac1d76529928", + "fileName": "deidentifiedMRHead-dcm-one-series.zip", + "name": "deidentifiedMRHead-dcm-one-series", + "seriesUID": "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.270.0", + "expectedFailures": [], + "voxelValueQuantity": "(110852, DCM, \"MR signal intensity\")", + "voxelValueUnits": "(1, UCUM, \"no units\")" + } + ] + + # another dataset that could be added in the future - currently fails for all readers + # due to invalid format - see https://issues.slicer.org/view.php?id=3569 + # { "url": TESTING_DATA_URL + "SHA256/4cbd051dc249ea47d0f7b4147ea8340ba11a4a18a1771d37c387e40538374cab", + # "fileName": "RIDER_bug.zip", + # "name": "RIDER_bug", + # "seriesUID": "1.3.6.1.4.1.9328.50.7.261772317324041365541450388603508531852", + # "expectedFailures": [] + # } + + loadingResult = {} # - # try loading using each of the selected readers, fail - # on enexpected load issue + # first, get the data - a zip file of dicom data # - scalarVolumePlugin = slicer.modules.dicomPlugins['DICOMScalarVolumePlugin']() - readerApproaches = scalarVolumePlugin.readerApproaches() - basename = loadable.name - volumesByApproach = {} - for readerApproach in readerApproaches: - self.delayDisplay('Loading Selection with approach: %s' % readerApproach) - loadable.name = basename + "-" + readerApproach - volumeNode = scalarVolumePlugin.load(loadable,readerApproach) - if not volumeNode and readerApproach not in dataset['expectedFailures']: - raise Exception("Expected to be able to read with %s, but couldn't" % readerApproach) - if volumeNode and readerApproach in dataset['expectedFailures']: - raise Exception("Expected to NOT be able to read with %s, but could!" % readerApproach) - if volumeNode: - volumesByApproach[readerApproach] = volumeNode - - self.delayDisplay('Test quantity and unit') - if 'voxelValueQuantity' in dataset.keys(): - self.assertEqual(volumeNode.GetVoxelValueQuantity().GetAsPrintableString(), dataset['voxelValueQuantity']) - if 'voxelValueUnits' in dataset.keys(): - self.assertEqual(volumeNode.GetVoxelValueUnits().GetAsPrintableString(), dataset['voxelValueUnits']) + self.delayDisplay("Downloading") + for dataset in referenceData: + try: + import SampleData + dicomFilesDirectory = SampleData.downloadFromURL( + fileNames=dataset['fileName'], uris=dataset['url'], checksums=dataset['checksum'])[0] + self.delayDisplay('Finished with download') + + # + # insert the data into the database + # + self.delayDisplay("Switching to temp database directory") + originalDatabaseDirectory = DICOMUtils.openTemporaryDatabase('tempDICOMDatabase') + + self.delayDisplay('Importing DICOM') + slicer.util.selectModule("DICOM") + + browserWidget = slicer.modules.DICOMWidget.browserWidget + dicomBrowser = browserWidget.dicomBrowser + dicomBrowser.importDirectory(dicomFilesDirectory, dicomBrowser.ImportDirectoryAddLink) + dicomBrowser.waitForImportFinished() + + # + # select the series + # + + browserWidget.onSeriesSelected([dataset['seriesUID']]) + # load the data by series UID + browserWidget.examineForLoading() + # Get first selected loadable + for plugin in browserWidget.loadablesByPlugin: + for loadable in browserWidget.loadablesByPlugin[plugin]: + if loadable.selected: + # found the first selected loadable + break + + # + # try loading using each of the selected readers, fail + # on enexpected load issue + # + scalarVolumePlugin = slicer.modules.dicomPlugins['DICOMScalarVolumePlugin']() + readerApproaches = scalarVolumePlugin.readerApproaches() + basename = loadable.name + volumesByApproach = {} + for readerApproach in readerApproaches: + self.delayDisplay('Loading Selection with approach: %s' % readerApproach) + loadable.name = basename + "-" + readerApproach + volumeNode = scalarVolumePlugin.load(loadable, readerApproach) + if not volumeNode and readerApproach not in dataset['expectedFailures']: + raise Exception("Expected to be able to read with %s, but couldn't" % readerApproach) + if volumeNode and readerApproach in dataset['expectedFailures']: + raise Exception("Expected to NOT be able to read with %s, but could!" % readerApproach) + if volumeNode: + volumesByApproach[readerApproach] = volumeNode + + self.delayDisplay('Test quantity and unit') + if 'voxelValueQuantity' in dataset.keys(): + self.assertEqual(volumeNode.GetVoxelValueQuantity().GetAsPrintableString(), dataset['voxelValueQuantity']) + if 'voxelValueUnits' in dataset.keys(): + self.assertEqual(volumeNode.GetVoxelValueUnits().GetAsPrintableString(), dataset['voxelValueUnits']) + + # + # for each approach that loaded as expected, compare the volumes + # to ensure they match in terms of pixel data and metadata + # + failedComparisons = {} + approachesThatLoaded = list(volumesByApproach.keys()) + print('approachesThatLoaded %s' % approachesThatLoaded) + for approachIndex in range(len(approachesThatLoaded)): + firstApproach = approachesThatLoaded[approachIndex] + firstVolume = volumesByApproach[firstApproach] + for secondApproachIndex in range(approachIndex + 1, len(approachesThatLoaded)): + secondApproach = approachesThatLoaded[secondApproachIndex] + secondVolume = volumesByApproach[secondApproach] + print(f'comparing {firstApproach},{secondApproach}') + comparison = slicer.modules.dicomPlugins['DICOMScalarVolumePlugin'].compareVolumeNodes(firstVolume, secondVolume) + if comparison != "": + print(('failed: %s', comparison)) + failedComparisons[firstApproach, secondApproach] = comparison + + if len(failedComparisons.keys()) > 0: + raise Exception("Loaded volumes don't match: %s" % failedComparisons) + + self.delayDisplay('%s Test passed!' % dataset['name']) + + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('%s Test caused exception!\n' % dataset['name'] + str(e)) + testPass = False + + self.delayDisplay("Restoring original database directory") + DICOMUtils.closeTemporaryDatabase(originalDatabaseDirectory) + slicer.util.selectModule('DICOMReaders') + + logging.info(loadingResult) + + return testPass + + def test_MissingSlices(self): + """ Test behavior of the readers when slices are missing + + To edit and run this test from the python console, paste this below: + + reloadScriptedModule('DICOMReaders'); import DICOMReaders; tester = DICOMReaders.DICOMReadersTest(); tester.setUp(); tester.test_MissingSlices() + + """ + testPass = True + + self.delayDisplay("Starting the DICOM test") + + settings = qt.QSettings() + settings.setValue("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "transform") - # - # for each approach that loaded as expected, compare the volumes - # to ensure they match in terms of pixel data and metadata - # - failedComparisons = {} - approachesThatLoaded = list(volumesByApproach.keys()) - print('approachesThatLoaded %s' % approachesThatLoaded) - for approachIndex in range(len(approachesThatLoaded)): - firstApproach = approachesThatLoaded[approachIndex] - firstVolume = volumesByApproach[firstApproach] - for secondApproachIndex in range(approachIndex+1,len(approachesThatLoaded)): - secondApproach = approachesThatLoaded[secondApproachIndex] - secondVolume = volumesByApproach[secondApproach] - print(f'comparing {firstApproach},{secondApproach}') - comparison = slicer.modules.dicomPlugins['DICOMScalarVolumePlugin'].compareVolumeNodes(firstVolume,secondVolume) - if comparison != "": - print(('failed: %s', comparison)) - failedComparisons[firstApproach,secondApproach] = comparison - - if len(failedComparisons.keys()) > 0: - raise Exception("Loaded volumes don't match: %s" % failedComparisons) - - self.delayDisplay('%s Test passed!' % dataset['name']) - - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('%s Test caused exception!\n' % dataset['name'] + str(e)) - testPass = False - - self.delayDisplay("Restoring original database directory") - DICOMUtils.closeTemporaryDatabase(originalDatabaseDirectory) - slicer.util.selectModule('DICOMReaders') - - logging.info(loadingResult) - - return testPass - - def test_MissingSlices(self): - """ Test behavior of the readers when slices are missing - - To edit and run this test from the python console, paste this below: - -reloadScriptedModule('DICOMReaders'); import DICOMReaders; tester = DICOMReaders.DICOMReadersTest(); tester.setUp(); tester.test_MissingSlices() - - """ - testPass = True - - self.delayDisplay("Starting the DICOM test") - - settings = qt.QSettings() - settings.setValue("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "transform") - - import SampleData - dicomFilesDirectory = SampleData.downloadFromURL( - fileNames='deidentifiedMRHead-dcm-one-series.zip', - uris=TESTING_DATA_URL + 'SHA256/899f3f8617ca53bad7dca0b2908478319e708b48ff41dfa64b6bac1d76529928', - checksums='SHA256:899f3f8617ca53bad7dca0b2908478319e708b48ff41dfa64b6bac1d76529928')[0] - self.delayDisplay('Finished with download\n') - - seriesUID = "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.270.0" - seriesRASBounds = [-87.29489517211913, 81.70450973510744, - -121.57139587402344, 134.42860412597656, - -138.71430206298828, 117.28569793701172] - seriesDirectory = "Series 004 [MR - SAG RF FAST VOL FLIP 20]" - lastSliceCorners = [[[81.05451202, 133.92860413, 116.78569794], [81.05451202, -122.07139587, 116.78569794]], - [[81.05451202, 133.92860413, -139.21429443], [81.05451202, -122.07139587, -139.21429443]]] - filesToRemove = [ - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.361.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.362.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.363.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.364.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.365.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.366.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.367.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.368.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.369.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.370.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.371.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.372.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.373.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.374.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.375.0.dcm", - "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.376.0.dcm", - ] - - try: - - print('Removing %d files from the middle of the series' % len(filesToRemove)) - for file in filesToRemove: - filePath = os.path.join(dicomFilesDirectory, seriesDirectory, file) - os.remove(filePath) - - # - # insert the data into the database - # - self.delayDisplay("Switching to temp database directory") - originalDatabaseDirectory = DICOMUtils.openTemporaryDatabase('tempDICOMDatabase') - - self.delayDisplay('Importing DICOM') - slicer.util.selectModule("DICOM") - - browserWidget = slicer.modules.DICOMWidget.browserWidget - dicomBrowser = browserWidget.dicomBrowser - dicomBrowser.importDirectory(dicomFilesDirectory, dicomBrowser.ImportDirectoryAddLink) - dicomBrowser.waitForImportFinished() - - # - # select the series - # - browserWidget.onSeriesSelected([seriesUID]) - # load the data by series UID - browserWidget.examineForLoading() - # Get first selected loadable - for plugin in browserWidget.loadablesByPlugin: - for loadable in browserWidget.loadablesByPlugin[plugin]: - if loadable.selected: - # found the first selected loadable - break - - if len(loadable.warning) == 0: - raise Exception("Expected warning about geometry issues due to missing slices!") - - # - # load and correct for acquisition then check the geometry - # - scalarVolumePlugin = slicer.modules.dicomPlugins['DICOMScalarVolumePlugin']() - volumeNode = scalarVolumePlugin.load(loadable) - - if not numpy.allclose(scalarVolumePlugin.acquisitionModeling.fixedCorners[-1], lastSliceCorners): - raise Exception("Acquisition transform didn't fix slice corners!") - - self.delayDisplay('test_MissingSlices passed!') - - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Missing Slices Test caused exception!\n' + str(e)) - testPass = False - - self.delayDisplay("Restoring original database directory") - DICOMUtils.closeTemporaryDatabase(originalDatabaseDirectory) - slicer.util.selectModule('') - - return testPass + import SampleData + dicomFilesDirectory = SampleData.downloadFromURL( + fileNames='deidentifiedMRHead-dcm-one-series.zip', + uris=TESTING_DATA_URL + 'SHA256/899f3f8617ca53bad7dca0b2908478319e708b48ff41dfa64b6bac1d76529928', + checksums='SHA256:899f3f8617ca53bad7dca0b2908478319e708b48ff41dfa64b6bac1d76529928')[0] + self.delayDisplay('Finished with download\n') + + seriesUID = "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.270.0" + seriesRASBounds = [-87.29489517211913, 81.70450973510744, + -121.57139587402344, 134.42860412597656, + -138.71430206298828, 117.28569793701172] + seriesDirectory = "Series 004 [MR - SAG RF FAST VOL FLIP 20]" + lastSliceCorners = [[[81.05451202, 133.92860413, 116.78569794], [81.05451202, -122.07139587, 116.78569794]], + [[81.05451202, 133.92860413, -139.21429443], [81.05451202, -122.07139587, -139.21429443]]] + filesToRemove = [ + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.361.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.362.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.363.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.364.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.365.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.366.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.367.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.368.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.369.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.370.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.371.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.372.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.373.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.374.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.375.0.dcm", + "1.3.6.1.4.1.5962.99.1.3814087073.479799962.1489872804257.376.0.dcm", + ] + + try: + + print('Removing %d files from the middle of the series' % len(filesToRemove)) + for file in filesToRemove: + filePath = os.path.join(dicomFilesDirectory, seriesDirectory, file) + os.remove(filePath) + + # + # insert the data into the database + # + self.delayDisplay("Switching to temp database directory") + originalDatabaseDirectory = DICOMUtils.openTemporaryDatabase('tempDICOMDatabase') + + self.delayDisplay('Importing DICOM') + slicer.util.selectModule("DICOM") + + browserWidget = slicer.modules.DICOMWidget.browserWidget + dicomBrowser = browserWidget.dicomBrowser + dicomBrowser.importDirectory(dicomFilesDirectory, dicomBrowser.ImportDirectoryAddLink) + dicomBrowser.waitForImportFinished() + + # + # select the series + # + browserWidget.onSeriesSelected([seriesUID]) + # load the data by series UID + browserWidget.examineForLoading() + # Get first selected loadable + for plugin in browserWidget.loadablesByPlugin: + for loadable in browserWidget.loadablesByPlugin[plugin]: + if loadable.selected: + # found the first selected loadable + break + + if len(loadable.warning) == 0: + raise Exception("Expected warning about geometry issues due to missing slices!") + + # + # load and correct for acquisition then check the geometry + # + scalarVolumePlugin = slicer.modules.dicomPlugins['DICOMScalarVolumePlugin']() + volumeNode = scalarVolumePlugin.load(loadable) + + if not numpy.allclose(scalarVolumePlugin.acquisitionModeling.fixedCorners[-1], lastSliceCorners): + raise Exception("Acquisition transform didn't fix slice corners!") + + self.delayDisplay('test_MissingSlices passed!') + + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Missing Slices Test caused exception!\n' + str(e)) + testPass = False + + self.delayDisplay("Restoring original database directory") + DICOMUtils.closeTemporaryDatabase(originalDatabaseDirectory) + slicer.util.selectModule('') + + return testPass diff --git a/Applications/SlicerApp/Testing/Python/DWMRIMultishellIOTests.py b/Applications/SlicerApp/Testing/Python/DWMRIMultishellIOTests.py index 5e0c2178ed5..d691bc44ddc 100644 --- a/Applications/SlicerApp/Testing/Python/DWMRIMultishellIOTests.py +++ b/Applications/SlicerApp/Testing/Python/DWMRIMultishellIOTests.py @@ -9,21 +9,21 @@ import slicer -#=============================================================================== +# =============================================================================== mrmlcore_testdata_path = "Libs/MRML/Core/Testing/TestData/" multishell_dwi_451 = os.path.join(mrmlcore_testdata_path, "multishell-DWI-451dir.nhdr") -#================================================================================ +# ================================================================================ NRRD = namedtuple('NRRD', ['header', 'bvalue', 'gradients']) def parse_nhdr(path): - dwmri_bval_key = "DWMRI_b-value" - dwmri_grad_keybase = "DWMRI_gradient_" - dwmri_grad_key_n = "DWMRI_gradient_{:04d}" + dwmri_bval_key = "DWMRI_b-value" + dwmri_grad_keybase = "DWMRI_gradient_" + dwmri_grad_key_n = "DWMRI_gradient_{:04d}" kvdict = {} grad_count = 0 @@ -44,9 +44,9 @@ def parse_nhdr(path): kvdict[key] = val if key.startswith(dwmri_grad_keybase): - _gn = int(key[ len(dwmri_grad_keybase):None ]) + _gn = int(key[len(dwmri_grad_keybase):None]) # monotonic keys - assert( _gn == grad_count ) # offset + assert(_gn == grad_count) # offset grad_count += 1 bvalue = float(kvdict[dwmri_bval_key]) @@ -60,13 +60,13 @@ def parse_nhdr(path): return NRRD(header=kvdict, bvalue=bvalue, gradients=grads) -#================================================================================ +# ================================================================================ def normalize(vec): norm = np.linalg.norm(vec) if norm == 0.0: return vec else: - return vec * 1/norm + return vec * 1 / norm def test_nrrd_dwi_load(first_file, second_file=None): @@ -94,7 +94,7 @@ def test_nrrd_dwi_load(first_file, second_file=None): ################################## # 1) check the number of gradients - assert( len(parsed_nrrd.gradients) == slicer_numgrads ) + assert(len(parsed_nrrd.gradients) == slicer_numgrads) ################################## # 2) check the node b values and gradients are correct @@ -126,7 +126,7 @@ def test_nrrd_dwi_load(first_file, second_file=None): for i in range(0, slicer_numgrads): grad_key = f"DWMRI_gradient_{i:04d}" parsed_gradient = np.fromstring(parsed_nrrd.header[grad_key], count=3, sep=' ', dtype=np.float64) - attr_gradient = np.fromstring(dw_node.GetAttribute(grad_key), count=3, sep=' ', dtype=np.float64) + attr_gradient = np.fromstring(dw_node.GetAttribute(grad_key), count=3, sep=' ', dtype=np.float64) np.testing.assert_array_almost_equal(parsed_gradient, attr_gradient, decimal=12, err_msg="NHDR gradient does not match gradient in node attribute dictionary") diff --git a/Applications/SlicerApp/Testing/Python/FiducialLayoutSwitchBug1914.py b/Applications/SlicerApp/Testing/Python/FiducialLayoutSwitchBug1914.py index 99cf4f50a63..c3a90d63e5b 100644 --- a/Applications/SlicerApp/Testing/Python/FiducialLayoutSwitchBug1914.py +++ b/Applications/SlicerApp/Testing/Python/FiducialLayoutSwitchBug1914.py @@ -12,16 +12,16 @@ # class FiducialLayoutSwitchBug1914(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "FiducialLayoutSwitchBug1914" - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Nicole Aucoin (BWH)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "FiducialLayoutSwitchBug1914" + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Nicole Aucoin (BWH)"] + parent.helpText = """ Test for bug 1914, misplaced control point after switching layouts. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218-12S1. """ @@ -32,8 +32,8 @@ def __init__(self, parent): class FiducialLayoutSwitchBug1914Widget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -41,161 +41,161 @@ def setup(self): # class FiducialLayoutSwitchBug1914Logic(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 - """ - - def __init__(self): - ScriptedLoadableModuleLogic.__init__(self) - - def getPointSliceDisplayableManagerHelper(self,sliceName='Red'): - sliceWidget = slicer.app.layoutManager().sliceWidget(sliceName) - sliceView = sliceWidget.sliceView() - collection = vtk.vtkCollection() - sliceView.getDisplayableManagers(collection) - for i in range(collection.GetNumberOfItems()): - m = collection.GetItemAsObject(i) - if m.GetClassName() == "vtkMRMLMarkupsFiducialDisplayableManager2D": - return m.GetHelper() - return None + """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 + """ + def __init__(self): + ScriptedLoadableModuleLogic.__init__(self) -class FiducialLayoutSwitchBug1914Test(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ + def getPointSliceDisplayableManagerHelper(self, sliceName='Red'): + sliceWidget = slicer.app.layoutManager().sliceWidget(sliceName) + sliceView = sliceWidget.sliceView() + collection = vtk.vtkCollection() + sliceView.getDisplayableManagers(collection) + for i in range(collection.GetNumberOfItems()): + m = collection.GetItemAsObject(i) + if m.GetClassName() == "vtkMRMLMarkupsFiducialDisplayableManager2D": + return m.GetHelper() + return None - 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. +class FiducialLayoutSwitchBug1914Test(ScriptedLoadableModuleTest): + """ + This is the test case for your scripted module. """ - self.setUp() - self.test_FiducialLayoutSwitchBug1914() - - def test_FiducialLayoutSwitchBug1914(self): - # test difference in display location and then in RAS if this is too fine - maximumDisplayDifference = 1.0 - # for future testing: take into account the volume voxel size - maximumRASDifference = 1.0 - - enableScreenshots = 0 - screenshotScaleFactor = 1 - - logic = FiducialLayoutSwitchBug1914Logic() - logging.info("ctest, please don't truncate my output: CTEST_FULL_OUTPUT") - - self.delayDisplay('Running the algorithm') - # Start in conventional layout - lm = slicer.app.layoutManager() - lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - # without this delayed display, when running from the cmd line Slicer starts - # up in a different layout and the seed won't get rendered in the right spot - self.delayDisplay("Conventional view") - - # Download MRHead from sample data - import SampleData - mrHeadVolume = SampleData.downloadSample("MRHead") - - # Place a point on the red slice - eye = [33.4975, 79.4042, -10.2143] - markupNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") - markupNode.AddControlPoint(eye) - self.delayDisplay(f"Placed a point at {eye[0]:g}, {eye[1]:g}, {eye[2]:g}") - - # Pan and zoom - sliceWidget = slicer.app.layoutManager().sliceWidget('Red') - sliceLogic = sliceWidget.sliceLogic() - compositeNode = sliceLogic.GetSliceCompositeNode() - sliceNode = sliceLogic.GetSliceNode() - sliceNode.SetXYZOrigin(-71.7, 129.7, 0.0) - sliceNode.SetFieldOfView(98.3, 130.5, 1.0) - self.delayDisplay("Panned and zoomed") - - # Get the seed widget seed location - startingSeedDisplayCoords = [0.0, 0.0, 0.0] - helper = logic.getPointSliceDisplayableManagerHelper('Red') - if helper is not None: - seedWidget = helper.GetWidget(markupNode) - seedRepresentation = seedWidget.GetSeedRepresentation() - handleRep = seedRepresentation.GetHandleRepresentation(fidIndex) - startingSeedDisplayCoords = handleRep.GetDisplayPosition() - print('Starting seed display coords = %d, %d, %d' % (startingSeedDisplayCoords[0], startingSeedDisplayCoords[1], startingSeedDisplayCoords[2])) - self.takeScreenshot('FiducialLayoutSwitchBug1914-StartingPosition','Point starting position',slicer.qMRMLScreenShotDialog.Red) - - # Switch to red slice only - lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView) - self.delayDisplay("Red Slice only") - - # Switch to conventional layout - print('Calling set layout back to conventional') - lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - print('Done calling set layout back to conventional') - self.delayDisplay("Conventional layout") - - # Get the current seed widget seed location - endingSeedDisplayCoords = [0.0, 0.0, 0.0] - helper = logic.getPointSliceDisplayableManagerHelper('Red') - if helper is not None: - seedWidget = helper.GetWidget(markupNode) - seedRepresentation = seedWidget.GetSeedRepresentation() - handleRep = seedRepresentation.GetHandleRepresentation(fidIndex) - endingSeedDisplayCoords = handleRep.GetDisplayPosition() - print('Ending seed display coords = %d, %d, %d' % (endingSeedDisplayCoords[0], endingSeedDisplayCoords[1], endingSeedDisplayCoords[2])) - self.takeScreenshot('FiducialLayoutSwitchBug1914-EndingPosition','Point ending position',slicer.qMRMLScreenShotDialog.Red) - - # Compare to original seed widget location - diff = math.pow((startingSeedDisplayCoords[0] - endingSeedDisplayCoords[0]),2) + math.pow((startingSeedDisplayCoords[1] - endingSeedDisplayCoords[1]),2) + math.pow((startingSeedDisplayCoords[2] - endingSeedDisplayCoords[2]),2) - if diff != 0.0: - diff = math.sqrt(diff) - self.delayDisplay("Difference between starting and ending seed display coordinates = %g" % diff) - - if diff > maximumDisplayDifference: - # double check against the RAS coordinates of the underlying volume since the display could have changed with a FOV adjustment. - sliceView = sliceWidget.sliceView() - volumeRAS = sliceView.convertXYZToRAS(endingSeedDisplayCoords) - seedRAS = [0,0,0] - markupNode.GetNthControlPointPosition(0,seedRAS) - rasDiff = math.pow((seedRAS[0] - volumeRAS[0]),2) + math.pow((seedRAS[1] - volumeRAS[1]),2) + math.pow((seedRAS[2] - volumeRAS[2]),2) - if rasDiff != 0.0: - rasDiff = math.sqrt(rasDiff) - print('Checking the difference between point RAS position',seedRAS, - 'and volume RAS as derived from the point display position',volumeRAS,': ',rasDiff) - if rasDiff > maximumRASDifference: - raise Exception(f"RAS coordinate difference is too large as well!\nExpected < {maximumRASDifference:g} but got {rasDiff:g}") - else: - self.delayDisplay(f"RAS coordinate difference is {rasDiff:g} which is < {maximumRASDifference:g}, test passes.") - - if enableScreenshots == 1: - # compare the screen snapshots - startView = slicer.mrmlScene.GetFirstNodeByName('FiducialLayoutSwitchBug1914-StartingPosition') - startShot = startView.GetScreenShot() - endView = slicer.mrmlScene.GetFirstNodeByName('FiducialLayoutSwitchBug1914-EndingPosition') - endShot = endView.GetScreenShot() - imageMath = vtk.vtkImageMathematics() - imageMath.SetOperationToSubtract() - imageMath.SetInput1(startShot) - imageMath.SetInput2(endShot) - imageMath.Update() - shotDiff = imageMath.GetOutput() - # save it as a scene view - annotationLogic = slicer.modules.annotations.logic() - annotationLogic.CreateSnapShot("FiducialLayoutSwitchBug1914-Diff", "Difference between starting and ending point positions", - slicer.qMRMLScreenShotDialog.Red, screenshotScaleFactor, shotDiff) - # calculate the image difference - imageStats = vtk.vtkImageHistogramStatistics() - imageStats.SetInput(shotDiff) - imageStats.GenerateHistogramImageOff() - imageStats.Update() - meanVal = imageStats.GetMean() - self.delayDisplay("Mean of image difference = %g" % meanVal) - if meanVal > 5.0: - raise Exception("Image difference is too great!\nExpected <= 5.0, but got %g" % (meanVal)) - - self.delayDisplay('Test passed!') + + 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_FiducialLayoutSwitchBug1914() + + def test_FiducialLayoutSwitchBug1914(self): + # test difference in display location and then in RAS if this is too fine + maximumDisplayDifference = 1.0 + # for future testing: take into account the volume voxel size + maximumRASDifference = 1.0 + + enableScreenshots = 0 + screenshotScaleFactor = 1 + + logic = FiducialLayoutSwitchBug1914Logic() + logging.info("ctest, please don't truncate my output: CTEST_FULL_OUTPUT") + + self.delayDisplay('Running the algorithm') + # Start in conventional layout + lm = slicer.app.layoutManager() + lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + # without this delayed display, when running from the cmd line Slicer starts + # up in a different layout and the seed won't get rendered in the right spot + self.delayDisplay("Conventional view") + + # Download MRHead from sample data + import SampleData + mrHeadVolume = SampleData.downloadSample("MRHead") + + # Place a point on the red slice + eye = [33.4975, 79.4042, -10.2143] + markupNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") + markupNode.AddControlPoint(eye) + self.delayDisplay(f"Placed a point at {eye[0]:g}, {eye[1]:g}, {eye[2]:g}") + + # Pan and zoom + sliceWidget = slicer.app.layoutManager().sliceWidget('Red') + sliceLogic = sliceWidget.sliceLogic() + compositeNode = sliceLogic.GetSliceCompositeNode() + sliceNode = sliceLogic.GetSliceNode() + sliceNode.SetXYZOrigin(-71.7, 129.7, 0.0) + sliceNode.SetFieldOfView(98.3, 130.5, 1.0) + self.delayDisplay("Panned and zoomed") + + # Get the seed widget seed location + startingSeedDisplayCoords = [0.0, 0.0, 0.0] + helper = logic.getPointSliceDisplayableManagerHelper('Red') + if helper is not None: + seedWidget = helper.GetWidget(markupNode) + seedRepresentation = seedWidget.GetSeedRepresentation() + handleRep = seedRepresentation.GetHandleRepresentation(fidIndex) + startingSeedDisplayCoords = handleRep.GetDisplayPosition() + print('Starting seed display coords = %d, %d, %d' % (startingSeedDisplayCoords[0], startingSeedDisplayCoords[1], startingSeedDisplayCoords[2])) + self.takeScreenshot('FiducialLayoutSwitchBug1914-StartingPosition', 'Point starting position', slicer.qMRMLScreenShotDialog.Red) + + # Switch to red slice only + lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView) + self.delayDisplay("Red Slice only") + + # Switch to conventional layout + print('Calling set layout back to conventional') + lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + print('Done calling set layout back to conventional') + self.delayDisplay("Conventional layout") + + # Get the current seed widget seed location + endingSeedDisplayCoords = [0.0, 0.0, 0.0] + helper = logic.getPointSliceDisplayableManagerHelper('Red') + if helper is not None: + seedWidget = helper.GetWidget(markupNode) + seedRepresentation = seedWidget.GetSeedRepresentation() + handleRep = seedRepresentation.GetHandleRepresentation(fidIndex) + endingSeedDisplayCoords = handleRep.GetDisplayPosition() + print('Ending seed display coords = %d, %d, %d' % (endingSeedDisplayCoords[0], endingSeedDisplayCoords[1], endingSeedDisplayCoords[2])) + self.takeScreenshot('FiducialLayoutSwitchBug1914-EndingPosition', 'Point ending position', slicer.qMRMLScreenShotDialog.Red) + + # Compare to original seed widget location + diff = math.pow((startingSeedDisplayCoords[0] - endingSeedDisplayCoords[0]), 2) + math.pow((startingSeedDisplayCoords[1] - endingSeedDisplayCoords[1]), 2) + math.pow((startingSeedDisplayCoords[2] - endingSeedDisplayCoords[2]), 2) + if diff != 0.0: + diff = math.sqrt(diff) + self.delayDisplay("Difference between starting and ending seed display coordinates = %g" % diff) + + if diff > maximumDisplayDifference: + # double check against the RAS coordinates of the underlying volume since the display could have changed with a FOV adjustment. + sliceView = sliceWidget.sliceView() + volumeRAS = sliceView.convertXYZToRAS(endingSeedDisplayCoords) + seedRAS = [0, 0, 0] + markupNode.GetNthControlPointPosition(0, seedRAS) + rasDiff = math.pow((seedRAS[0] - volumeRAS[0]), 2) + math.pow((seedRAS[1] - volumeRAS[1]), 2) + math.pow((seedRAS[2] - volumeRAS[2]), 2) + if rasDiff != 0.0: + rasDiff = math.sqrt(rasDiff) + print('Checking the difference between point RAS position', seedRAS, + 'and volume RAS as derived from the point display position', volumeRAS, ': ', rasDiff) + if rasDiff > maximumRASDifference: + raise Exception(f"RAS coordinate difference is too large as well!\nExpected < {maximumRASDifference:g} but got {rasDiff:g}") + else: + self.delayDisplay(f"RAS coordinate difference is {rasDiff:g} which is < {maximumRASDifference:g}, test passes.") + + if enableScreenshots == 1: + # compare the screen snapshots + startView = slicer.mrmlScene.GetFirstNodeByName('FiducialLayoutSwitchBug1914-StartingPosition') + startShot = startView.GetScreenShot() + endView = slicer.mrmlScene.GetFirstNodeByName('FiducialLayoutSwitchBug1914-EndingPosition') + endShot = endView.GetScreenShot() + imageMath = vtk.vtkImageMathematics() + imageMath.SetOperationToSubtract() + imageMath.SetInput1(startShot) + imageMath.SetInput2(endShot) + imageMath.Update() + shotDiff = imageMath.GetOutput() + # save it as a scene view + annotationLogic = slicer.modules.annotations.logic() + annotationLogic.CreateSnapShot("FiducialLayoutSwitchBug1914-Diff", "Difference between starting and ending point positions", + slicer.qMRMLScreenShotDialog.Red, screenshotScaleFactor, shotDiff) + # calculate the image difference + imageStats = vtk.vtkImageHistogramStatistics() + imageStats.SetInput(shotDiff) + imageStats.GenerateHistogramImageOff() + imageStats.Update() + meanVal = imageStats.GetMean() + self.delayDisplay("Mean of image difference = %g" % meanVal) + if meanVal > 5.0: + raise Exception("Image difference is too great!\nExpected <= 5.0, but got %g" % (meanVal)) + + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/JRC2013Vis.py b/Applications/SlicerApp/Testing/Python/JRC2013Vis.py index f9ec5c5d9c7..a52d3419f85 100644 --- a/Applications/SlicerApp/Testing/Python/JRC2013Vis.py +++ b/Applications/SlicerApp/Testing/Python/JRC2013Vis.py @@ -14,18 +14,18 @@ # class JRC2013Vis(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "JRC2013Vis" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Nicholas Herlambang (AZE R&D)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "JRC2013Vis" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Nicholas Herlambang (AZE R&D)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ This module was developed as a self test to perform the operations needed for the JRC 2013 Visualization Tutorial """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -34,105 +34,105 @@ def __init__(self, parent): class JRC2013VisWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... - - # start/stop DICOM peer - self.startStopDicomPeerButton = qt.QPushButton("Start/Stop DICOM peer") - self.startStopDicomPeerButton.setCheckable(True) - self.layout.addWidget(self.startStopDicomPeerButton) - self.startStopDicomPeerButton.connect('toggled(bool)', self.onStartStopDicomPeer) - - # Collapsible button - testsCollapsibleButton = ctk.ctkCollapsibleButton() - testsCollapsibleButton.text = "A collapsible button" - self.layout.addWidget(testsCollapsibleButton) - - # Layout within the collapsible button - formLayout = qt.QFormLayout(testsCollapsibleButton) - - # test buttons - tests = ( ("Part 1: DICOM",self.onPart1DICOM),("Part 2: Head", self.onPart2Head),("Part 3: Liver", self.onPart3Liver),("Part 4: Lung", self.onPart4Lung),) - for text,slot in tests: - testButton = qt.QPushButton(text) - testButton.toolTip = "Run the test." - formLayout.addWidget(testButton) - testButton.connect('clicked(bool)', slot) - - # Add vertical spacer - self.layout.addStretch(1) - - def onPart1DICOM(self): - tester = JRC2013VisTest() - tester.setUp() - tester.test_Part1DICOM() - - def onPart2Head(self): - tester = JRC2013VisTest() - tester.setUp() - tester.test_Part2Head() - - def onPart3Liver(self): - tester = JRC2013VisTest() - tester.setUp() - tester.test_Part3Liver() - - def onPart4Lung(self): - tester = JRC2013VisTest() - tester.setUp() - tester.test_Part4Lung() - - def onStartStopDicomPeer(self,flag): - if flag: - self.startStopDicomPeerButton.setEnabled(False) - dicomFilesDirectory = slicer.app.temporaryPath - configFilePath = dicomFilesDirectory + '/Dcmtk-db/dcmqrscp.cfg' - processCurrentPath = dicomFilesDirectory + '/Dcmtk-db/' - - if slicer.util.confirmYesNoDisplay('Do you want to choose local DCMTK database folder?'): - print('Yes') - dicomFilesDirectory = qt.QFileDialog.getExistingDirectory(None, 'Select DCMTK database folder') - configFilePath = dicomFilesDirectory + '/dcmqrscp.cfg' - processCurrentPath = dicomFilesDirectory - else: - import SampleData - SampleData.downloadFromURL( - fileNames='Dcmtk-db.zip', - uris=TESTING_DATA_URL + 'MD5/6bfb01cf5ffb8e3af9b1c0c9556f0c6b45f0ec40305a9539ed7a9f0dcfe378e3', - checksums='SHA256:6bfb01cf5ffb8e3af9b1c0c9556f0c6b45f0ec40305a9539ed7a9f0dcfe378e3')[0] - - import subprocess - dcmqrscpExeOptions = ( - '/bin', - '/../CTK-build/CMakeExternals/Install/bin', - '/../DCMTK-install/bin', - '/../DCMTK-build/bin', - '/../DCMTK-build/bin/Release' - '/../DCMTK-build/bin/Debug' - '/../DCMTK-build/bin/RelWithDebInfo' - '/../DCMTK-build/bin/MinSizeRel' - ) - - dcmqrscpExePath = None - dcmqrscpExeName = '/dcmqrscp' - if slicer.app.os == 'win': - dcmqrscpExeName = dcmqrscpExeName + '.exe' - for path in dcmqrscpExeOptions: - testPath = slicer.app.slicerHome + path + dcmqrscpExeName - if os.path.exists(testPath): - dcmqrscpExePath = testPath - break - if not dcmqrscpExePath: - raise UserWarning("Could not find dcmqrscp executable") - - args = (dcmqrscpExePath, '-c', configFilePath) - print('Start DICOM peer') - self.popen = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=processCurrentPath) - self.startStopDicomPeerButton.setEnabled(True) - else: - print('Stop DICOM peer') - self.popen.kill() + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... + + # start/stop DICOM peer + self.startStopDicomPeerButton = qt.QPushButton("Start/Stop DICOM peer") + self.startStopDicomPeerButton.setCheckable(True) + self.layout.addWidget(self.startStopDicomPeerButton) + self.startStopDicomPeerButton.connect('toggled(bool)', self.onStartStopDicomPeer) + + # Collapsible button + testsCollapsibleButton = ctk.ctkCollapsibleButton() + testsCollapsibleButton.text = "A collapsible button" + self.layout.addWidget(testsCollapsibleButton) + + # Layout within the collapsible button + formLayout = qt.QFormLayout(testsCollapsibleButton) + + # test buttons + tests = (("Part 1: DICOM", self.onPart1DICOM), ("Part 2: Head", self.onPart2Head), ("Part 3: Liver", self.onPart3Liver), ("Part 4: Lung", self.onPart4Lung),) + for text, slot in tests: + testButton = qt.QPushButton(text) + testButton.toolTip = "Run the test." + formLayout.addWidget(testButton) + testButton.connect('clicked(bool)', slot) + + # Add vertical spacer + self.layout.addStretch(1) + + def onPart1DICOM(self): + tester = JRC2013VisTest() + tester.setUp() + tester.test_Part1DICOM() + + def onPart2Head(self): + tester = JRC2013VisTest() + tester.setUp() + tester.test_Part2Head() + + def onPart3Liver(self): + tester = JRC2013VisTest() + tester.setUp() + tester.test_Part3Liver() + + def onPart4Lung(self): + tester = JRC2013VisTest() + tester.setUp() + tester.test_Part4Lung() + + def onStartStopDicomPeer(self, flag): + if flag: + self.startStopDicomPeerButton.setEnabled(False) + dicomFilesDirectory = slicer.app.temporaryPath + configFilePath = dicomFilesDirectory + '/Dcmtk-db/dcmqrscp.cfg' + processCurrentPath = dicomFilesDirectory + '/Dcmtk-db/' + + if slicer.util.confirmYesNoDisplay('Do you want to choose local DCMTK database folder?'): + print('Yes') + dicomFilesDirectory = qt.QFileDialog.getExistingDirectory(None, 'Select DCMTK database folder') + configFilePath = dicomFilesDirectory + '/dcmqrscp.cfg' + processCurrentPath = dicomFilesDirectory + else: + import SampleData + SampleData.downloadFromURL( + fileNames='Dcmtk-db.zip', + uris=TESTING_DATA_URL + 'MD5/6bfb01cf5ffb8e3af9b1c0c9556f0c6b45f0ec40305a9539ed7a9f0dcfe378e3', + checksums='SHA256:6bfb01cf5ffb8e3af9b1c0c9556f0c6b45f0ec40305a9539ed7a9f0dcfe378e3')[0] + + import subprocess + dcmqrscpExeOptions = ( + '/bin', + '/../CTK-build/CMakeExternals/Install/bin', + '/../DCMTK-install/bin', + '/../DCMTK-build/bin', + '/../DCMTK-build/bin/Release' + '/../DCMTK-build/bin/Debug' + '/../DCMTK-build/bin/RelWithDebInfo' + '/../DCMTK-build/bin/MinSizeRel' + ) + + dcmqrscpExePath = None + dcmqrscpExeName = '/dcmqrscp' + if slicer.app.os == 'win': + dcmqrscpExeName = dcmqrscpExeName + '.exe' + for path in dcmqrscpExeOptions: + testPath = slicer.app.slicerHome + path + dcmqrscpExeName + if os.path.exists(testPath): + dcmqrscpExePath = testPath + break + if not dcmqrscpExePath: + raise UserWarning("Could not find dcmqrscp executable") + + args = (dcmqrscpExePath, '-c', configFilePath) + print('Start DICOM peer') + self.popen = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=processCurrentPath) + self.startStopDicomPeerButton.setEnabled(True) + else: + print('Stop DICOM peer') + self.popen.kill() # @@ -140,378 +140,378 @@ def onStartStopDicomPeer(self,flag): # class JRC2013VisLogic(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 - """ - pass + """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 + """ + pass class JRC2013VisTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. - """ - self.delayDisplay("Closing the scene") - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - slicer.mrmlScene.Clear(0) - - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_Part1DICOM() - self.setUp() - self.test_Part2Head() - self.setUp() - self.test_Part3Liver() - self.setUp() - self.test_Part4Lung() - - def test_Part1DICOM(self): - """ Test the DICOM part of the test using the head atlas - """ - import os - self.delayDisplay("Starting the DICOM test") - # - # first, get the data - a zip file of dicom data - # - import SampleData - dicomFilesDirectory = SampleData.downloadFromURL( - fileNames='Dcmtk-db.zip', - uris=TESTING_DATA_URL + 'MD5/7a43d121a51a631ab0df02071e5ba6ed', - checksums='MD5:7a43d121a51a631ab0df02071e5ba6ed')[0] - - try: - self.delayDisplay("Switching to temp database directory") - originalDatabaseDirectory = DICOMUtils.openTemporaryDatabase('tempDICOMDatbase') - - self.delayDisplay('Start Local DICOM Q/R SCP') - import subprocess - import os - configFilePath = dicomFilesDirectory + '/Dcmtk-db/dcmqrscp.cfg' - processCurrentPath = dicomFilesDirectory + '/Dcmtk-db/' - print("configFilePath: "+os.path.abspath(configFilePath)) - print("processCurrentPath: "+os.path.abspath(processCurrentPath)) - - dcmqrscpExeOptions = ( - '/bin', - '/../CTK-build/CMakeExternals/Install/bin', - '/../DCMTK-install/bin', - '/../DCMTK-build/bin', - '/../DCMTK-build/bin/Release', - '/../DCMTK-build/bin/Debug', - '/../DCMTK-build/bin/RelWithDebInfo' - '/../DCMTK-build/bin/MinSizeRel' - ) - - dcmqrscpExePath = None - dcmqrscpExeName = '/dcmqrscp' - if slicer.app.os == 'win': - dcmqrscpExeName = dcmqrscpExeName + '.exe' - for path in dcmqrscpExeOptions: - testPath = slicer.app.slicerHome + path + dcmqrscpExeName - if os.path.exists(testPath): - dcmqrscpExePath = testPath - break - if not dcmqrscpExePath: - raise UserWarning("Could not find dcmqrscp executable") - - args = (dcmqrscpExePath, '-c', configFilePath) - popen = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=processCurrentPath) - - self.delayDisplay('Retrieve DICOM') - slicer.util.selectModule('DICOM') - dicomRetrieve = ctk.ctkDICOMRetrieve() - dicomRetrieve.setKeepAssociationOpen(True) - dicomRetrieve.setDatabase(slicer.dicomDatabase) - dicomRetrieve.setCallingAETitle('SlicerAE') - dicomRetrieve.setCalledAETitle('DCMTK') - dicomRetrieve.setPort(12345) - dicomRetrieve.setHost('localhost') - dicomRetrieve.getStudy('1.2.124.113932.1.170.223.162.178.20050502.160340.12640015') - popen.kill() - - # Select first patient - browserWidget = slicer.modules.DICOMWidget.browserWidget - browserWidget.dicomBrowser.dicomTableManager().patientsTable().selectFirst() - browserWidget.examineForLoading() - - self.delayDisplay('Loading Selection') - browserWidget.loadCheckedLoadables() - - self.delayDisplay('Change Level') - layoutManager = slicer.app.layoutManager() - redWidget = layoutManager.sliceWidget('Red') - slicer.util.clickAndDrag(redWidget,start=(10,10),end=(10,40)) - - self.delayDisplay('Change Window') - slicer.util.clickAndDrag(redWidget,start=(10,10),end=(40,10)) - - self.delayDisplay('Change Layout') - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView) - - self.delayDisplay('Zoom') - slicer.util.clickAndDrag(redWidget,button='Right',start=(10,10),end=(10,40)) - - self.delayDisplay('Pan') - slicer.util.clickAndDrag(redWidget,button='Middle',start=(10,10),end=(40,40)) - - self.delayDisplay('Center') - redWidget.sliceController().fitSliceToBackground() - - self.delayDisplay('Lightbox') - redWidget.sliceController().setLightboxTo6x6() - - self.delayDisplay('Conventional Layout') - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - - self.delayDisplay('No Lightbox') - redWidget.sliceController().setLightboxTo1x1() - - self.delayDisplay('Four Up Layout') - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView) - - self.delayDisplay('Shift Mouse') - slicer.util.clickAndDrag(redWidget,button='None',start=(100,100),end=(140,140),modifiers=['Shift']) - - self.delayDisplay('Conventional, Link, Slice Model') - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - redWidget.sliceController().setSliceLink(True) - redWidget.sliceController().setSliceVisible(True) - - self.delayDisplay('Rotate') - threeDView = layoutManager.threeDWidget(0).threeDView() - slicer.util.clickAndDrag(threeDView) - - self.delayDisplay('Zoom') - threeDView = layoutManager.threeDWidget(0).threeDView() - slicer.util.clickAndDrag(threeDView,button='Right') - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) - - self.delayDisplay("Restoring original database directory") - DICOMUtils.closeTemporaryDatabase(originalDatabaseDirectory) - - def test_Part2Head(self): - """ Test using the head atlas - may not be needed - Slicer4Minute is already tested """ - self.delayDisplay("Starting the test") - # - # first, get some data - # TODO: This is a very old scene with missing scene view screenshots - # that is why error is reported while attempting to load it. - # It would be better to replace with a new scene. - # - import SampleData - SampleData.downloadFromURL( - fileNames='3DHeadData.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/e2c7944095dd92be7961bed37f3c8f49e6f40c7f31d4fe865753b6efddae7993', - checksums='SHA256:e2c7944095dd92be7961bed37f3c8f49e6f40c7f31d4fe865753b6efddae7993') - self.delayDisplay('Finished with download and loading\n') - - try: - logic = JRC2013VisLogic() - mainWindow = slicer.util.mainWindow() - layoutManager = slicer.app.layoutManager() - threeDView = layoutManager.threeDWidget(0).threeDView() - redWidget = layoutManager.sliceWidget('vtkMRMLSliceNode1') # it would be 'Red' in a recent scene - redController = redWidget.sliceController() - greenWidget = layoutManager.sliceWidget('vtkMRMLSliceNode2') # it would be 'Green' in a recent scene - greenController = greenWidget.sliceController() - - self.delayDisplay('Models and Slice Model') - mainWindow.moduleSelector().selectModule('Models') - redWidget.sliceController().setSliceVisible(True) - - self.delayDisplay('Scroll Slices') - for offset in range(-20,20,2): - redController.setSliceOffsetValue(offset) - - self.delayDisplay('Skin Opacity') - # turn off skin and skull - skin = slicer.util.getNode(pattern='Skin.vtk') - skin.GetDisplayNode().SetOpacity(0.5) - - self.delayDisplay('Skin and Skull Visibility') - skin.GetDisplayNode().SetVisibility(0) - skull = slicer.util.getNode(pattern='skull_bone.vtk') - skull.GetDisplayNode().SetVisibility(0) - - self.delayDisplay('Green slice and Clipping') - greenWidget.sliceController().setSliceVisible(True) - hemispheric_white_matter = slicer.util.getNode(pattern='hemispheric_white_matter.vtk') - hemispheric_white_matter.GetDisplayNode().SetClipping(1) - clip = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLClipModelsNode') - clip.SetRedSliceClipState(0) - clip.SetYellowSliceClipState(0) - clip.SetGreenSliceClipState(2) - - viewNode = threeDView.mrmlViewNode() - cameras = slicer.util.getNodes('vtkMRMLCameraNode*') - for cameraNode in cameras.values(): - if cameraNode.GetActiveTag() == viewNode.GetID(): - break - cameraNode.GetCamera().Azimuth(90) - cameraNode.GetCamera().Elevation(20) - - self.delayDisplay('Rotate') - slicer.util.clickAndDrag(threeDView) - - self.delayDisplay('Zoom') - threeDView = layoutManager.threeDWidget(0).threeDView() - slicer.util.clickAndDrag(threeDView,button='Right') - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) - - def test_Part3Liver(self): - """ Test using the liver example data - """ - self.delayDisplay("Starting the test") - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL( - fileNames='LiverData.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/a39075d3e87f80bbf8eba1e0222ee68c60036e57c3db830db08f3022f424e221', - checksums='SHA256:a39075d3e87f80bbf8eba1e0222ee68c60036e57c3db830db08f3022f424e221') - self.delayDisplay('Finished with download and loading\n') - - try: - logic = JRC2013VisLogic() - mainWindow = slicer.util.mainWindow() - layoutManager = slicer.app.layoutManager() - threeDView = layoutManager.threeDWidget(0).threeDView() - redWidget = layoutManager.sliceWidget('Red') - redController = redWidget.sliceController() - viewNode = threeDView.mrmlViewNode() - cameras = slicer.util.getNodes('vtkMRMLCameraNode*') - for cameraNode in cameras.values(): - if cameraNode.GetActiveTag() == viewNode.GetID(): - break - - self.delayDisplay('Segment II invisible') - mainWindow.moduleSelector().selectModule('Models') - segmentII = slicer.util.getNode('LiverSegment_II') - segmentII.GetDisplayNode().SetVisibility(0) - slicer.util.clickAndDrag(threeDView,start=(10,200),end=(10,10)) - - self.delayDisplay('Segment II visible') - segmentII.GetDisplayNode().SetVisibility(1) - cameraNode.GetCamera().Azimuth(0) - cameraNode.GetCamera().Elevation(0) - - self.delayDisplay('View Adrenal') - segmentII.GetDisplayNode().SetVisibility(1) - cameraNode.GetCamera().Azimuth(180) - cameraNode.GetCamera().Elevation(-30) - - segmentVII = slicer.util.getNode('LiverSegment_II') - redWidget.sliceController().setSliceVisible(True) - - self.delayDisplay('Middle Hepatic') - models = slicer.util.getNodes('vtkMRMLModelNode*') - for modelNode in models.values(): - modelNode.GetDisplayNode().SetVisibility(0) - - segmentVII = slicer.util.getNode('LiverSegment_II') - transparentNodes = ('MiddleHepaticVein_and_Branches','LiverSegment_IVb','LiverSegmentV',) - for nodeName in transparentNodes: - modelNode = slicer.util.getNode(nodeName) - modelNode.GetDisplayNode().SetOpacity(0.5) - modelNode.GetDisplayNode().SetVisibility(1) - redWidget.sliceController().setSliceVisible(True) - cameraNode.GetCamera().Azimuth(30) - cameraNode.GetCamera().Elevation(-20) - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) - - def test_Part4Lung(self): - """ Test using the lung data + This is the test case for your scripted module. """ - self.delayDisplay("Starting the test") - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL( - fileNames='LungData.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/9da091065aa42edbba2d436a2ef21a093792e8a76455c28e5b80590b04f5a73e', - checksums='SHA256:9da091065aa42edbba2d436a2ef21a093792e8a76455c28e5b80590b04f5a73e') - self.delayDisplay('Finished with download and loading\n') - - try: - mainWindow = slicer.util.mainWindow() - layoutManager = slicer.app.layoutManager() - threeDView = layoutManager.threeDWidget(0).threeDView() - redWidget = layoutManager.sliceWidget('Red') - redController = redWidget.sliceController() - viewNode = threeDView.mrmlViewNode() - cameras = slicer.util.getNodes('vtkMRMLCameraNode*') - for cameraNode in cameras.values(): - if cameraNode.GetActiveTag() == viewNode.GetID(): - break - - self.delayDisplay('Reset view') - threeDView.resetFocalPoint() - mainWindow.moduleSelector().selectModule('Models') - - self.delayDisplay('View Question 1') - cameraNode.GetCamera().Azimuth(-100) - cameraNode.GetCamera().Elevation(-40) - redWidget.sliceController().setSliceVisible(True) - lungs = slicer.util.getNode('chestCT_lungs') - lungs.GetDisplayNode().SetVisibility(0) - - self.delayDisplay('View Question 2') - cameraNode.GetCamera().Azimuth(-65) - cameraNode.GetCamera().Elevation(-20) - lungs.GetDisplayNode().SetVisibility(1) - lungs.GetDisplayNode().SetOpacity(0.24) - redController.setSliceOffsetValue(-50) - - self.delayDisplay('View Question 3') - cameraNode.GetCamera().Azimuth(-165) - cameraNode.GetCamera().Elevation(-10) - redWidget.sliceController().setSliceVisible(False) - - self.delayDisplay('View Question 4') - cameraNode.GetCamera().Azimuth(20) - cameraNode.GetCamera().Elevation(-10) - lowerLobeNodes = slicer.util.getNodes('*LowerLobe*') - for showNode in lowerLobeNodes: - self.delayDisplay('Showing Node %s' % showNode, 300) - for node in lowerLobeNodes: - displayNode = lowerLobeNodes[node].GetDisplayNode() - if displayNode: - displayNode.SetVisibility(1 if node == showNode else 0) - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + self.delayDisplay("Closing the scene") + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + slicer.mrmlScene.Clear(0) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_Part1DICOM() + self.setUp() + self.test_Part2Head() + self.setUp() + self.test_Part3Liver() + self.setUp() + self.test_Part4Lung() + + def test_Part1DICOM(self): + """ Test the DICOM part of the test using the head atlas + """ + import os + self.delayDisplay("Starting the DICOM test") + # + # first, get the data - a zip file of dicom data + # + import SampleData + dicomFilesDirectory = SampleData.downloadFromURL( + fileNames='Dcmtk-db.zip', + uris=TESTING_DATA_URL + 'MD5/7a43d121a51a631ab0df02071e5ba6ed', + checksums='MD5:7a43d121a51a631ab0df02071e5ba6ed')[0] + + try: + self.delayDisplay("Switching to temp database directory") + originalDatabaseDirectory = DICOMUtils.openTemporaryDatabase('tempDICOMDatbase') + + self.delayDisplay('Start Local DICOM Q/R SCP') + import subprocess + import os + configFilePath = dicomFilesDirectory + '/Dcmtk-db/dcmqrscp.cfg' + processCurrentPath = dicomFilesDirectory + '/Dcmtk-db/' + print("configFilePath: " + os.path.abspath(configFilePath)) + print("processCurrentPath: " + os.path.abspath(processCurrentPath)) + + dcmqrscpExeOptions = ( + '/bin', + '/../CTK-build/CMakeExternals/Install/bin', + '/../DCMTK-install/bin', + '/../DCMTK-build/bin', + '/../DCMTK-build/bin/Release', + '/../DCMTK-build/bin/Debug', + '/../DCMTK-build/bin/RelWithDebInfo' + '/../DCMTK-build/bin/MinSizeRel' + ) + + dcmqrscpExePath = None + dcmqrscpExeName = '/dcmqrscp' + if slicer.app.os == 'win': + dcmqrscpExeName = dcmqrscpExeName + '.exe' + for path in dcmqrscpExeOptions: + testPath = slicer.app.slicerHome + path + dcmqrscpExeName + if os.path.exists(testPath): + dcmqrscpExePath = testPath + break + if not dcmqrscpExePath: + raise UserWarning("Could not find dcmqrscp executable") + + args = (dcmqrscpExePath, '-c', configFilePath) + popen = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=processCurrentPath) + + self.delayDisplay('Retrieve DICOM') + slicer.util.selectModule('DICOM') + dicomRetrieve = ctk.ctkDICOMRetrieve() + dicomRetrieve.setKeepAssociationOpen(True) + dicomRetrieve.setDatabase(slicer.dicomDatabase) + dicomRetrieve.setCallingAETitle('SlicerAE') + dicomRetrieve.setCalledAETitle('DCMTK') + dicomRetrieve.setPort(12345) + dicomRetrieve.setHost('localhost') + dicomRetrieve.getStudy('1.2.124.113932.1.170.223.162.178.20050502.160340.12640015') + popen.kill() + + # Select first patient + browserWidget = slicer.modules.DICOMWidget.browserWidget + browserWidget.dicomBrowser.dicomTableManager().patientsTable().selectFirst() + browserWidget.examineForLoading() + + self.delayDisplay('Loading Selection') + browserWidget.loadCheckedLoadables() + + self.delayDisplay('Change Level') + layoutManager = slicer.app.layoutManager() + redWidget = layoutManager.sliceWidget('Red') + slicer.util.clickAndDrag(redWidget, start=(10, 10), end=(10, 40)) + + self.delayDisplay('Change Window') + slicer.util.clickAndDrag(redWidget, start=(10, 10), end=(40, 10)) + + self.delayDisplay('Change Layout') + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView) + + self.delayDisplay('Zoom') + slicer.util.clickAndDrag(redWidget, button='Right', start=(10, 10), end=(10, 40)) + + self.delayDisplay('Pan') + slicer.util.clickAndDrag(redWidget, button='Middle', start=(10, 10), end=(40, 40)) + + self.delayDisplay('Center') + redWidget.sliceController().fitSliceToBackground() + + self.delayDisplay('Lightbox') + redWidget.sliceController().setLightboxTo6x6() + + self.delayDisplay('Conventional Layout') + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + + self.delayDisplay('No Lightbox') + redWidget.sliceController().setLightboxTo1x1() + + self.delayDisplay('Four Up Layout') + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView) + + self.delayDisplay('Shift Mouse') + slicer.util.clickAndDrag(redWidget, button='None', start=(100, 100), end=(140, 140), modifiers=['Shift']) + + self.delayDisplay('Conventional, Link, Slice Model') + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + redWidget.sliceController().setSliceLink(True) + redWidget.sliceController().setSliceVisible(True) + + self.delayDisplay('Rotate') + threeDView = layoutManager.threeDWidget(0).threeDView() + slicer.util.clickAndDrag(threeDView) + + self.delayDisplay('Zoom') + threeDView = layoutManager.threeDWidget(0).threeDView() + slicer.util.clickAndDrag(threeDView, button='Right') + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) + + self.delayDisplay("Restoring original database directory") + DICOMUtils.closeTemporaryDatabase(originalDatabaseDirectory) + + def test_Part2Head(self): + """ Test using the head atlas - may not be needed - Slicer4Minute is already tested + """ + self.delayDisplay("Starting the test") + # + # first, get some data + # TODO: This is a very old scene with missing scene view screenshots + # that is why error is reported while attempting to load it. + # It would be better to replace with a new scene. + # + import SampleData + SampleData.downloadFromURL( + fileNames='3DHeadData.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/e2c7944095dd92be7961bed37f3c8f49e6f40c7f31d4fe865753b6efddae7993', + checksums='SHA256:e2c7944095dd92be7961bed37f3c8f49e6f40c7f31d4fe865753b6efddae7993') + self.delayDisplay('Finished with download and loading\n') + + try: + logic = JRC2013VisLogic() + mainWindow = slicer.util.mainWindow() + layoutManager = slicer.app.layoutManager() + threeDView = layoutManager.threeDWidget(0).threeDView() + redWidget = layoutManager.sliceWidget('vtkMRMLSliceNode1') # it would be 'Red' in a recent scene + redController = redWidget.sliceController() + greenWidget = layoutManager.sliceWidget('vtkMRMLSliceNode2') # it would be 'Green' in a recent scene + greenController = greenWidget.sliceController() + + self.delayDisplay('Models and Slice Model') + mainWindow.moduleSelector().selectModule('Models') + redWidget.sliceController().setSliceVisible(True) + + self.delayDisplay('Scroll Slices') + for offset in range(-20, 20, 2): + redController.setSliceOffsetValue(offset) + + self.delayDisplay('Skin Opacity') + # turn off skin and skull + skin = slicer.util.getNode(pattern='Skin.vtk') + skin.GetDisplayNode().SetOpacity(0.5) + + self.delayDisplay('Skin and Skull Visibility') + skin.GetDisplayNode().SetVisibility(0) + skull = slicer.util.getNode(pattern='skull_bone.vtk') + skull.GetDisplayNode().SetVisibility(0) + + self.delayDisplay('Green slice and Clipping') + greenWidget.sliceController().setSliceVisible(True) + hemispheric_white_matter = slicer.util.getNode(pattern='hemispheric_white_matter.vtk') + hemispheric_white_matter.GetDisplayNode().SetClipping(1) + clip = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLClipModelsNode') + clip.SetRedSliceClipState(0) + clip.SetYellowSliceClipState(0) + clip.SetGreenSliceClipState(2) + + viewNode = threeDView.mrmlViewNode() + cameras = slicer.util.getNodes('vtkMRMLCameraNode*') + for cameraNode in cameras.values(): + if cameraNode.GetActiveTag() == viewNode.GetID(): + break + cameraNode.GetCamera().Azimuth(90) + cameraNode.GetCamera().Elevation(20) + + self.delayDisplay('Rotate') + slicer.util.clickAndDrag(threeDView) + + self.delayDisplay('Zoom') + threeDView = layoutManager.threeDWidget(0).threeDView() + slicer.util.clickAndDrag(threeDView, button='Right') + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) + + def test_Part3Liver(self): + """ Test using the liver example data + """ + self.delayDisplay("Starting the test") + # + # first, get some data + # + import SampleData + SampleData.downloadFromURL( + fileNames='LiverData.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/a39075d3e87f80bbf8eba1e0222ee68c60036e57c3db830db08f3022f424e221', + checksums='SHA256:a39075d3e87f80bbf8eba1e0222ee68c60036e57c3db830db08f3022f424e221') + self.delayDisplay('Finished with download and loading\n') + + try: + logic = JRC2013VisLogic() + mainWindow = slicer.util.mainWindow() + layoutManager = slicer.app.layoutManager() + threeDView = layoutManager.threeDWidget(0).threeDView() + redWidget = layoutManager.sliceWidget('Red') + redController = redWidget.sliceController() + viewNode = threeDView.mrmlViewNode() + cameras = slicer.util.getNodes('vtkMRMLCameraNode*') + for cameraNode in cameras.values(): + if cameraNode.GetActiveTag() == viewNode.GetID(): + break + + self.delayDisplay('Segment II invisible') + mainWindow.moduleSelector().selectModule('Models') + segmentII = slicer.util.getNode('LiverSegment_II') + segmentII.GetDisplayNode().SetVisibility(0) + slicer.util.clickAndDrag(threeDView, start=(10, 200), end=(10, 10)) + + self.delayDisplay('Segment II visible') + segmentII.GetDisplayNode().SetVisibility(1) + cameraNode.GetCamera().Azimuth(0) + cameraNode.GetCamera().Elevation(0) + + self.delayDisplay('View Adrenal') + segmentII.GetDisplayNode().SetVisibility(1) + cameraNode.GetCamera().Azimuth(180) + cameraNode.GetCamera().Elevation(-30) + + segmentVII = slicer.util.getNode('LiverSegment_II') + redWidget.sliceController().setSliceVisible(True) + + self.delayDisplay('Middle Hepatic') + models = slicer.util.getNodes('vtkMRMLModelNode*') + for modelNode in models.values(): + modelNode.GetDisplayNode().SetVisibility(0) + + segmentVII = slicer.util.getNode('LiverSegment_II') + transparentNodes = ('MiddleHepaticVein_and_Branches', 'LiverSegment_IVb', 'LiverSegmentV',) + for nodeName in transparentNodes: + modelNode = slicer.util.getNode(nodeName) + modelNode.GetDisplayNode().SetOpacity(0.5) + modelNode.GetDisplayNode().SetVisibility(1) + redWidget.sliceController().setSliceVisible(True) + cameraNode.GetCamera().Azimuth(30) + cameraNode.GetCamera().Elevation(-20) + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) + + def test_Part4Lung(self): + """ Test using the lung data + """ + + self.delayDisplay("Starting the test") + # + # first, get some data + # + import SampleData + SampleData.downloadFromURL( + fileNames='LungData.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/9da091065aa42edbba2d436a2ef21a093792e8a76455c28e5b80590b04f5a73e', + checksums='SHA256:9da091065aa42edbba2d436a2ef21a093792e8a76455c28e5b80590b04f5a73e') + self.delayDisplay('Finished with download and loading\n') + + try: + mainWindow = slicer.util.mainWindow() + layoutManager = slicer.app.layoutManager() + threeDView = layoutManager.threeDWidget(0).threeDView() + redWidget = layoutManager.sliceWidget('Red') + redController = redWidget.sliceController() + viewNode = threeDView.mrmlViewNode() + cameras = slicer.util.getNodes('vtkMRMLCameraNode*') + for cameraNode in cameras.values(): + if cameraNode.GetActiveTag() == viewNode.GetID(): + break + + self.delayDisplay('Reset view') + threeDView.resetFocalPoint() + mainWindow.moduleSelector().selectModule('Models') + + self.delayDisplay('View Question 1') + cameraNode.GetCamera().Azimuth(-100) + cameraNode.GetCamera().Elevation(-40) + redWidget.sliceController().setSliceVisible(True) + lungs = slicer.util.getNode('chestCT_lungs') + lungs.GetDisplayNode().SetVisibility(0) + + self.delayDisplay('View Question 2') + cameraNode.GetCamera().Azimuth(-65) + cameraNode.GetCamera().Elevation(-20) + lungs.GetDisplayNode().SetVisibility(1) + lungs.GetDisplayNode().SetOpacity(0.24) + redController.setSliceOffsetValue(-50) + + self.delayDisplay('View Question 3') + cameraNode.GetCamera().Azimuth(-165) + cameraNode.GetCamera().Elevation(-10) + redWidget.sliceController().setSliceVisible(False) + + self.delayDisplay('View Question 4') + cameraNode.GetCamera().Azimuth(20) + cameraNode.GetCamera().Elevation(-10) + lowerLobeNodes = slicer.util.getNodes('*LowerLobe*') + for showNode in lowerLobeNodes: + self.delayDisplay('Showing Node %s' % showNode, 300) + for node in lowerLobeNodes: + displayNode = lowerLobeNodes[node].GetDisplayNode() + if displayNode: + displayNode.SetVisibility(1 if node == showNode else 0) + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) diff --git a/Applications/SlicerApp/Testing/Python/KneeAtlasTest.py b/Applications/SlicerApp/Testing/Python/KneeAtlasTest.py index dbdf9fb2e57..befdba76f5f 100644 --- a/Applications/SlicerApp/Testing/Python/KneeAtlasTest.py +++ b/Applications/SlicerApp/Testing/Python/KneeAtlasTest.py @@ -4,20 +4,20 @@ class testClass: - """ Run the knee atlas test by itself - """ + """ Run the knee atlas test by itself + """ - def setUp(self): - print("Import the atlas tests...") - atlasTests = AtlasTests.AtlasTestsTest() - atlasTests.test_KneeAtlasTest() + def setUp(self): + print("Import the atlas tests...") + atlasTests = AtlasTests.AtlasTestsTest() + atlasTests.test_KneeAtlasTest() class KneeAtlasTest(unittest.TestCase): - def setUp(self): - pass + def setUp(self): + pass - def test_testClass(self): - test = testClass() - test.setUp() + def test_testClass(self): + test = testClass() + test.setUp() diff --git a/Applications/SlicerApp/Testing/Python/MRMLCreateNodeByClassWithSetReferenceCountMinusOne.py b/Applications/SlicerApp/Testing/Python/MRMLCreateNodeByClassWithSetReferenceCountMinusOne.py index df7984a56ae..e82fbb7ec58 100644 --- a/Applications/SlicerApp/Testing/Python/MRMLCreateNodeByClassWithSetReferenceCountMinusOne.py +++ b/Applications/SlicerApp/Testing/Python/MRMLCreateNodeByClassWithSetReferenceCountMinusOne.py @@ -2,10 +2,10 @@ def testMRMLCreateNodeByClassWithSetReferenceCountMinusOne(): - n = slicer.mrmlScene.CreateNodeByClass('vtkMRMLViewNode') - n.UnRegister(None) # the node object is now owned by n Python variable therefore we can release the reference that CreateNodeByClass added - slicer.mrmlScene.AddNode(n) + n = slicer.mrmlScene.CreateNodeByClass('vtkMRMLViewNode') + n.UnRegister(None) # the node object is now owned by n Python variable therefore we can release the reference that CreateNodeByClass added + slicer.mrmlScene.AddNode(n) if __name__ == '__main__': - testMRMLCreateNodeByClassWithSetReferenceCountMinusOne() + testMRMLCreateNodeByClassWithSetReferenceCountMinusOne() diff --git a/Applications/SlicerApp/Testing/Python/MRMLCreateNodeByClassWithoutSetReferenceCount.py b/Applications/SlicerApp/Testing/Python/MRMLCreateNodeByClassWithoutSetReferenceCount.py index d78faa4d27e..b767578a9c5 100644 --- a/Applications/SlicerApp/Testing/Python/MRMLCreateNodeByClassWithoutSetReferenceCount.py +++ b/Applications/SlicerApp/Testing/Python/MRMLCreateNodeByClassWithoutSetReferenceCount.py @@ -3,18 +3,18 @@ def testMRMLCreateNodeByClassWithoutSetReferenceCount(): - # Always run this test as if CTest ran it. - # It is necessary because without this the test on Windows - # would report leaks in a popup window that has to be closed manually - # (or wait a long time for timeout) when run from VisualStudio by - # building the RUN_TESTS project. - slicer.app.setEnvironmentVariable("DASHBOARD_TEST_FROM_CTEST", "1") + # Always run this test as if CTest ran it. + # It is necessary because without this the test on Windows + # would report leaks in a popup window that has to be closed manually + # (or wait a long time for timeout) when run from VisualStudio by + # building the RUN_TESTS project. + slicer.app.setEnvironmentVariable("DASHBOARD_TEST_FROM_CTEST", "1") - n = slicer.mrmlScene.CreateNodeByClass('vtkMRMLViewNode') - slicer.mrmlScene.AddNode(n) - # This is expected to leak memory because CreateNodeByClass increments the reference count by one - # and nothing decrements it. + n = slicer.mrmlScene.CreateNodeByClass('vtkMRMLViewNode') + slicer.mrmlScene.AddNode(n) + # This is expected to leak memory because CreateNodeByClass increments the reference count by one + # and nothing decrements it. if __name__ == '__main__': - testMRMLCreateNodeByClassWithoutSetReferenceCount() + testMRMLCreateNodeByClassWithoutSetReferenceCount() diff --git a/Applications/SlicerApp/Testing/Python/MRMLSceneImportAndExport.py b/Applications/SlicerApp/Testing/Python/MRMLSceneImportAndExport.py index 8e535aad357..ab26a15698c 100644 --- a/Applications/SlicerApp/Testing/Python/MRMLSceneImportAndExport.py +++ b/Applications/SlicerApp/Testing/Python/MRMLSceneImportAndExport.py @@ -3,17 +3,17 @@ def testMRMLSceneImportAndExport(): - tempDir = slicer.app.temporaryPath - scenePath = tempDir + '/temp_scene.mrml' - slicer.mrmlScene.SetURL(scenePath) - if not slicer.mrmlScene.Commit(scenePath): - raise Exception('Saving a MRML scene failed !') + tempDir = slicer.app.temporaryPath + scenePath = tempDir + '/temp_scene.mrml' + slicer.mrmlScene.SetURL(scenePath) + if not slicer.mrmlScene.Commit(scenePath): + raise Exception('Saving a MRML scene failed !') - success = slicer.mrmlScene.Import() - os.remove(scenePath) - if not success: - raise Exception('Importing back a MRML scene failed !') + success = slicer.mrmlScene.Import() + os.remove(scenePath) + if not success: + raise Exception('Importing back a MRML scene failed !') if __name__ == '__main__': - testMRMLSceneImportAndExport() + testMRMLSceneImportAndExport() diff --git a/Applications/SlicerApp/Testing/Python/MeasureStartupTimes.py b/Applications/SlicerApp/Testing/Python/MeasureStartupTimes.py index c31c9f5c196..9b136add340 100755 --- a/Applications/SlicerApp/Testing/Python/MeasureStartupTimes.py +++ b/Applications/SlicerApp/Testing/Python/MeasureStartupTimes.py @@ -32,76 +32,80 @@ def TemporaryPythonScript(code, *args, **kwargs): - if 'suffix' not in kwargs: - kwargs['suffix'] = '.py' - if 'mode' not in kwargs: - kwargs['mode'] = 'w' - script = tempfile.NamedTemporaryFile(*args, **kwargs) - script.write(code) - script.flush() - print(f"Written script {script.name} [{code}]") - return script + if 'suffix' not in kwargs: + kwargs['suffix'] = '.py' + if 'mode' not in kwargs: + kwargs['mode'] = 'w' + script = tempfile.NamedTemporaryFile(*args, **kwargs) + script.write(code) + script.flush() + print(f"Written script {script.name} [{code}]") + return script def collect_startup_times_normal(output_file, drop_cache=False, display_output=False): - results= {} - test = [] - (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) - (returnCode, stdout, stderr) = result - if display_output: - if stdout: print("STDOUT [%s]\n" % stdout) - if stderr and returnCode == EXIT_SUCCESS: print("STDERR [%s]\n" % stderr) - results[" ".join(test)] = duration - with open(output_file, 'w') as file: - file.write(json.dumps(results, indent=4)) + results = {} + test = [] + (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) + (returnCode, stdout, stderr) = result + if display_output: + if stdout: + print("STDOUT [%s]\n" % stdout) + if stderr and returnCode == EXIT_SUCCESS: + print("STDERR [%s]\n" % stderr) + results[" ".join(test)] = duration + with open(output_file, 'w') as file: + file.write(json.dumps(results, indent=4)) def collect_startup_times_overall(output_file, drop_cache=False, display_output=False): - results= {} - - test = ["--help"] - (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) - results[" ".join(test)] = duration - - tests = [ - [], - ['--disable-builtin-cli-modules'], - ['--disable-builtin-loadable-modules'], - ['--disable-builtin-scripted-loadable-modules'], - ['--disable-builtin-cli-modules', '--disable-builtin-scripted-loadable-modules'], - ['--disable-modules'] - ] + results = {} - for test in tests: + test = ["--help"] (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) results[" ".join(test)] = duration - for test in tests: - test.insert(0, '--disable-python') - (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) - results[" ".join(test)] = duration + tests = [ + [], + ['--disable-builtin-cli-modules'], + ['--disable-builtin-loadable-modules'], + ['--disable-builtin-scripted-loadable-modules'], + ['--disable-builtin-cli-modules', '--disable-builtin-scripted-loadable-modules'], + ['--disable-modules'] + ] - (returnCode, stdout, stderr) = result - if display_output: - if stdout: print("STDOUT [%s]\n" % stdout) - if stderr and returnCode == EXIT_SUCCESS: print("STDERR [%s]\n" % stderr) + for test in tests: + (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) + results[" ".join(test)] = duration + + for test in tests: + test.insert(0, '--disable-python') + (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) + results[" ".join(test)] = duration - with open(output_file, 'w') as file: - file.write(json.dumps(results, indent=4)) + (returnCode, stdout, stderr) = result + if display_output: + if stdout: + print("STDOUT [%s]\n" % stdout) + if stderr and returnCode == EXIT_SUCCESS: + print("STDERR [%s]\n" % stderr) + + with open(output_file, 'w') as file: + file.write(json.dumps(results, indent=4)) def read_modules(input_file): - # Read list of modules - with open(input_file) as input: - modules = json.load(input) - print("Found %d modules reading %s\n" % (len(modules), input_file)) - return modules + # Read list of modules + with open(input_file) as input: + modules = json.load(input) + print("Found %d modules reading %s\n" % (len(modules), input_file)) + return modules def collect_modules(output_file): - # Collect list of all modules and their associated types - python_script = TemporaryPythonScript(""" + # Collect list of all modules and their associated types + python_script = TemporaryPythonScript(""" import json modules = {{}} @@ -127,148 +131,154 @@ def collect_modules(output_file): output.write(json.dumps(modules, indent=4)) """.format(output_file)) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, ['--python-script', python_script.name]) - assert returnCode == EXIT_SUCCESS - print("=> ok\n") + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, ['--python-script', python_script.name]) + assert returnCode == EXIT_SUCCESS + print("=> ok\n") - return read_modules(output_file) + return read_modules(output_file) def slicerRevision(): - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, [ - '--no-main-window', '--ignore-slicerrc', '--disable-modules', - '--python-code', 'print(slicer.app.repositoryRevision)' - ]) - assert returnCode == EXIT_SUCCESS - return stdout.split()[0] + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, [ + '--no-main-window', '--ignore-slicerrc', '--disable-modules', + '--python-code', 'print(slicer.app.repositoryRevision)' + ]) + assert returnCode == EXIT_SUCCESS + return stdout.split()[0] def collect_startup_times_including_one_module(output_file, module_list, drop_cache=False, display_output=False): - modules = collect_modules(module_list) - # Collect startup times disabling each module one by one - moduleTimes = {} - for (idx, (moduleName, moduleType)) in enumerate(modules.iteritems(), start=1): - modules_minus_one = list(modules.keys()) - del modules_minus_one[modules_minus_one.index(moduleName)] - print("[%d/%d] including %s" % (idx, len(modules), moduleName)) - test = ['--testing', '--modules-to-ignore', ",".join(modules_minus_one)] - (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) - (returnCode, stdout, stderr) = result - if display_output: - if stdout: print("STDOUT [%s]\n" % stdout) - if stderr and returnCode == EXIT_SUCCESS: print("STDERR [%s]\n" % stderr) - if returnCode != EXIT_SUCCESS: - # XXX Ignore module with dependencies - duration = None - print("=> failed\n") - else: - moduleTimes[moduleName] = duration - - with open(output_file, 'w') as file: - file.write(json.dumps(moduleTimes, indent=4)) + modules = collect_modules(module_list) + # Collect startup times disabling each module one by one + moduleTimes = {} + for (idx, (moduleName, moduleType)) in enumerate(modules.iteritems(), start=1): + modules_minus_one = list(modules.keys()) + del modules_minus_one[modules_minus_one.index(moduleName)] + print("[%d/%d] including %s" % (idx, len(modules), moduleName)) + test = ['--testing', '--modules-to-ignore', ",".join(modules_minus_one)] + (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) + (returnCode, stdout, stderr) = result + if display_output: + if stdout: + print("STDOUT [%s]\n" % stdout) + if stderr and returnCode == EXIT_SUCCESS: + print("STDERR [%s]\n" % stderr) + if returnCode != EXIT_SUCCESS: + # XXX Ignore module with dependencies + duration = None + print("=> failed\n") + else: + moduleTimes[moduleName] = duration + + with open(output_file, 'w') as file: + file.write(json.dumps(moduleTimes, indent=4)) def collect_startup_times_excluding_one_module(output_file, module_list, drop_cache=False, display_output=False): - modules = collect_modules(module_list) - # Collect startup times disabling each module one by one - moduleTimes = {} - for (idx, (moduleName, moduleType)) in enumerate(modules.iteritems(), start=1): - #if moduleType == "CLI": - # print("=> Skipping CLI [%s]\n" % moduleName) - # continue - print("[%d/%d]" % (idx, len(modules))) - (duration, result) = runSlicerAndExitWithTime(slicer_executable, ['--testing', '--modules-to-ignore', moduleName], drop_cache=drop_cache) - (returnCode, stdout, stderr) = result - if display_output: - if stdout: print("STDOUT [%s]\n" % stdout) - if stderr and returnCode == EXIT_SUCCESS: print("STDERR [%s]\n" % stderr) - if returnCode != EXIT_SUCCESS: - # XXX Ignore module with dependencies - duration = None - print("=> failed\n") - else: - moduleTimes[moduleName] = duration - - with open(output_file, 'w') as file: - file.write(json.dumps(moduleTimes, indent=4)) + modules = collect_modules(module_list) + # Collect startup times disabling each module one by one + moduleTimes = {} + for (idx, (moduleName, moduleType)) in enumerate(modules.iteritems(), start=1): + # if moduleType == "CLI": + # print("=> Skipping CLI [%s]\n" % moduleName) + # continue + print("[%d/%d]" % (idx, len(modules))) + (duration, result) = runSlicerAndExitWithTime(slicer_executable, ['--testing', '--modules-to-ignore', moduleName], drop_cache=drop_cache) + (returnCode, stdout, stderr) = result + if display_output: + if stdout: + print("STDOUT [%s]\n" % stdout) + if stderr and returnCode == EXIT_SUCCESS: + print("STDERR [%s]\n" % stderr) + if returnCode != EXIT_SUCCESS: + # XXX Ignore module with dependencies + duration = None + print("=> failed\n") + else: + moduleTimes[moduleName] = duration + + with open(output_file, 'w') as file: + file.write(json.dumps(moduleTimes, indent=4)) def collect_startup_times_modules_to_load(output_file, modules_to_load, module_list, drop_cache=False, display_output=False): - modules = collect_modules(module_list) - modulesToIgnore = list(modules.keys()) - for moduleName in modules_to_load.split(","): - print("Including %s" % moduleName) - del modulesToIgnore[modulesToIgnore.index(moduleName)] + modules = collect_modules(module_list) + modulesToIgnore = list(modules.keys()) + for moduleName in modules_to_load.split(","): + print("Including %s" % moduleName) + del modulesToIgnore[modulesToIgnore.index(moduleName)] - test = ['--testing', '--modules-to-ignore', ",".join(modulesToIgnore)] - (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) - (returnCode, stdout, stderr) = result - if display_output: - if stdout: print("STDOUT [%s]\n" % stdout) - if stderr and returnCode == EXIT_SUCCESS: print("STDERR [%s]\n" % stderr) + test = ['--testing', '--modules-to-ignore', ",".join(modulesToIgnore)] + (duration, result) = runSlicerAndExitWithTime(slicer_executable, test, drop_cache=drop_cache) + (returnCode, stdout, stderr) = result + if display_output: + if stdout: + print("STDOUT [%s]\n" % stdout) + if stderr and returnCode == EXIT_SUCCESS: + print("STDERR [%s]\n" % stderr) - results= {} - results[" ".join(modulesToIgnore)] = duration - with open(output_file, 'w') as file: - file.write(json.dumps(results, indent=4)) + results = {} + results[" ".join(modulesToIgnore)] = duration + with open(output_file, 'w') as file: + file.write(json.dumps(results, indent=4)) if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Measure startup times.') - # Experiments - parser.add_argument("--normal", action="store_true") - parser.add_argument("--modules-to-load") - parser.add_argument("--overall", action="store_true") - parser.add_argument("--excluding-one-module", action="store_true") - parser.add_argument("--including-one-module", action="store_true") - # Common options - parser.add_argument("-n", "--repeat", default=1, type=int) - parser.add_argument("--drop-cache", action="store_true") - parser.add_argument("--reuse-module-list", action="store_true") - parser.add_argument("--display-slicer-output", action="store_true") - parser.add_argument("/path/to/Slicer") - args = parser.parse_args() - - slicer_executable = os.path.expanduser(getattr(args, "/path/to/Slicer")) - all = (not args.normal - and not args.modules_to_load - and not args.overall - and not args.excluding_one_module - and not args.including_one_module) - - runSlicerAndExitWithTime = timecall(runSlicerAndExit, repeat=args.repeat) - - sliver_revision = slicerRevision() - module_list = "Modules-r%s.json" % sliver_revision - - if args.reuse_module_list: - print("Loading existing module listing") - collect_modules = read_modules # noqa: F811 - - common_kwargs = { - 'display_output': args.display_slicer_output, - 'drop_cache': args.drop_cache + parser = argparse.ArgumentParser(description='Measure startup times.') + # Experiments + parser.add_argument("--normal", action="store_true") + parser.add_argument("--modules-to-load") + parser.add_argument("--overall", action="store_true") + parser.add_argument("--excluding-one-module", action="store_true") + parser.add_argument("--including-one-module", action="store_true") + # Common options + parser.add_argument("-n", "--repeat", default=1, type=int) + parser.add_argument("--drop-cache", action="store_true") + parser.add_argument("--reuse-module-list", action="store_true") + parser.add_argument("--display-slicer-output", action="store_true") + parser.add_argument("/path/to/Slicer") + args = parser.parse_args() + + slicer_executable = os.path.expanduser(getattr(args, "/path/to/Slicer")) + all = (not args.normal + and not args.modules_to_load + and not args.overall + and not args.excluding_one_module + and not args.including_one_module) + + runSlicerAndExitWithTime = timecall(runSlicerAndExit, repeat=args.repeat) + + sliver_revision = slicerRevision() + module_list = "Modules-r%s.json" % sliver_revision + + if args.reuse_module_list: + print("Loading existing module listing") + collect_modules = read_modules # noqa: F811 + + common_kwargs = { + 'display_output': args.display_slicer_output, + 'drop_cache': args.drop_cache } - # Since the "normal" experiment is included in the "overall" one, - # it is not executed by default. - if args.normal: - collect_startup_times_normal("StartupTimesNormal-r%s.json" % sliver_revision, **common_kwargs) + # Since the "normal" experiment is included in the "overall" one, + # it is not executed by default. + if args.normal: + collect_startup_times_normal("StartupTimesNormal-r%s.json" % sliver_revision, **common_kwargs) - # Since the "modules-to-load" experiment requires user input and is provided - # for convenience, it is not executed by default. - if args.modules_to_load: - collect_startup_times_modules_to_load( - "StartupTimesSelectedModules.json", args.modules_to_load, module_list, **common_kwargs) + # Since the "modules-to-load" experiment requires user input and is provided + # for convenience, it is not executed by default. + if args.modules_to_load: + collect_startup_times_modules_to_load( + "StartupTimesSelectedModules.json", args.modules_to_load, module_list, **common_kwargs) - if all or args.overall: - collect_startup_times_overall("StartupTimes-r%s.json" % sliver_revision, **common_kwargs) + if all or args.overall: + collect_startup_times_overall("StartupTimes-r%s.json" % sliver_revision, **common_kwargs) - if all or args.excluding_one_module: - collect_startup_times_excluding_one_module( - "StartupTimesExcludingOneModule-r%s.json" % sliver_revision, module_list, **common_kwargs) + if all or args.excluding_one_module: + collect_startup_times_excluding_one_module( + "StartupTimesExcludingOneModule-r%s.json" % sliver_revision, module_list, **common_kwargs) - if all or args.including_one_module: - collect_startup_times_including_one_module( - "StartupTimesIncludingOneModule-r%s.json" % sliver_revision, module_list, **common_kwargs) + if all or args.including_one_module: + collect_startup_times_including_one_module( + "StartupTimesIncludingOneModule-r%s.json" % sliver_revision, module_list, **common_kwargs) diff --git a/Applications/SlicerApp/Testing/Python/RSNA2012ProstateDemo.py b/Applications/SlicerApp/Testing/Python/RSNA2012ProstateDemo.py index 5ca2305f052..c0c00d4b6cc 100644 --- a/Applications/SlicerApp/Testing/Python/RSNA2012ProstateDemo.py +++ b/Applications/SlicerApp/Testing/Python/RSNA2012ProstateDemo.py @@ -8,22 +8,22 @@ # class RSNA2012ProstateDemo(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) - parent.title = "RSNA2012ProstateDemo" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + """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) + parent.title = "RSNA2012ProstateDemo" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ This module was developed as a self test to perform the operations needed for the RSNA 2012 Prostate Demo """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -31,58 +31,58 @@ def __init__(self, parent): # class RSNA2012ProstateDemoWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - def cleanup(self): - pass + def cleanup(self): + pass class RSNA2012ProstateDemoTest(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 - """ + """ + 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): - slicer.mrmlScene.Clear(0) + def setUp(self): + slicer.mrmlScene.Clear(0) - def runTest(self): - self.setUp() - self.test_RSNA2012ProstateDemo() + def runTest(self): + self.setUp() + self.test_RSNA2012ProstateDemo() - def test_RSNA2012ProstateDemo(self): - """ - Replicate one of the crashes in issue 2512 - """ + def test_RSNA2012ProstateDemo(self): + """ + Replicate one of the crashes in issue 2512 + """ - print("Running RSNA2012ProstateDemo Test case:") + print("Running RSNA2012ProstateDemo Test case:") - import SampleData - SampleData.downloadFromURL( - fileNames='RSNA2012ProstateDemo.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/2627388ee213564f8783d0242993212ba01189f4c6640d57c4cde4e28fc5f97b', - checksums='SHA256:2627388ee213564f8783d0242993212ba01189f4c6640d57c4cde4e28fc5f97b') + import SampleData + SampleData.downloadFromURL( + fileNames='RSNA2012ProstateDemo.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/2627388ee213564f8783d0242993212ba01189f4c6640d57c4cde4e28fc5f97b', + checksums='SHA256:2627388ee213564f8783d0242993212ba01189f4c6640d57c4cde4e28fc5f97b') - # get all scene view nodes and test switching - svns = slicer.util.getNodes('vtkMRMLSceneViewNode*') + # get all scene view nodes and test switching + svns = slicer.util.getNodes('vtkMRMLSceneViewNode*') - for reps in range(5): - for svname,svnode in svns.items(): - self.delayDisplay('Restoring scene view %s ...' % svname ) - svnode.RestoreScene() - self.delayDisplay('OK') + for reps in range(5): + for svname, svnode in svns.items(): + self.delayDisplay('Restoring scene view %s ...' % svname) + svnode.RestoreScene() + self.delayDisplay('OK') - self.delayDisplay('Done testing scene views, will clear the scene') - slicer.mrmlScene.Clear(0) - self.delayDisplay('Test passed') + self.delayDisplay('Done testing scene views, will clear the scene') + slicer.mrmlScene.Clear(0) + self.delayDisplay('Test passed') diff --git a/Applications/SlicerApp/Testing/Python/RSNAQuantTutorial.py b/Applications/SlicerApp/Testing/Python/RSNAQuantTutorial.py index 67baf45f3a8..c62a8eaf040 100644 --- a/Applications/SlicerApp/Testing/Python/RSNAQuantTutorial.py +++ b/Applications/SlicerApp/Testing/Python/RSNAQuantTutorial.py @@ -11,22 +11,22 @@ # class RSNAQuantTutorial(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) - parent.title = "RSNAQuantTutorial" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + """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) + parent.title = "RSNAQuantTutorial" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ This module was developed as a self test to perform the operations needed for the RSNA 2012 Quantitative Imaging Tutorial """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -34,83 +34,83 @@ def __init__(self, parent): # class RSNAQuantTutorialWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... - - # Collapsible button - testsCollapsibleButton = ctk.ctkCollapsibleButton() - testsCollapsibleButton.text = "Tests" - self.layout.addWidget(testsCollapsibleButton) - - # Layout within the collapsible button - formLayout = qt.QFormLayout(testsCollapsibleButton) - - # test buttons - tests = ( ("Part 1 : Ruler", self.onPart1Ruler),("Part 2: ChangeTracker", self.onPart2ChangeTracker),("Part 3 : PETCT", self.onPart3PETCT) ) - for text,slot in tests: - testButton = qt.QPushButton(text) - testButton.toolTip = "Run the test." - formLayout.addWidget(testButton) - testButton.connect('clicked(bool)', slot) - - # A collapsible button to hide screen shot options - screenShotsCollapsibleButton = ctk.ctkCollapsibleButton() - screenShotsCollapsibleButton.text = "Screen shot options" - self.layout.addWidget(screenShotsCollapsibleButton) - - # layout within the collapsible button - screenShotsFormLayout = qt.QFormLayout(screenShotsCollapsibleButton) - - # - # check box to trigger taking screen shots for later use in tutorials - # - self.enableScreenshotsFlagCheckBox = qt.QCheckBox() - self.enableScreenshotsFlagCheckBox.checked = 0 - self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") - screenShotsFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) - - # - # scale factor for screen shots - # - self.screenshotScaleFactorSliderWidget = ctk.ctkSliderWidget() - self.screenshotScaleFactorSliderWidget.singleStep = 1.0 - self.screenshotScaleFactorSliderWidget.minimum = 1.0 - self.screenshotScaleFactorSliderWidget.maximum = 50.0 - self.screenshotScaleFactorSliderWidget.value = 1.0 - self.screenshotScaleFactorSliderWidget.setToolTip("Set scale factor for the screen shots.") - screenShotsFormLayout.addRow("Screenshot scale factor", self.screenshotScaleFactorSliderWidget) - - # Add vertical spacer - self.layout.addStretch(1) - - def onPart1Ruler(self): - enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked - screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) - - tester = RSNAQuantTutorialTest() - tester.setUp() - tester.test_Part1Ruler(enableScreenshotsFlag,screenshotScaleFactor) - - def onPart2ChangeTracker(self): - enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked - screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) - - tester = RSNAQuantTutorialTest() - tester.setUp() - tester.test_Part2ChangeTracker(enableScreenshotsFlag,screenshotScaleFactor) - - def onPart3PETCT(self): - enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked - screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) - - tester = RSNAQuantTutorialTest() - tester.setUp() - tester.test_Part3PETCT(enableScreenshotsFlag,screenshotScaleFactor) + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... + + # Collapsible button + testsCollapsibleButton = ctk.ctkCollapsibleButton() + testsCollapsibleButton.text = "Tests" + self.layout.addWidget(testsCollapsibleButton) + + # Layout within the collapsible button + formLayout = qt.QFormLayout(testsCollapsibleButton) + + # test buttons + tests = (("Part 1 : Ruler", self.onPart1Ruler), ("Part 2: ChangeTracker", self.onPart2ChangeTracker), ("Part 3 : PETCT", self.onPart3PETCT)) + for text, slot in tests: + testButton = qt.QPushButton(text) + testButton.toolTip = "Run the test." + formLayout.addWidget(testButton) + testButton.connect('clicked(bool)', slot) + + # A collapsible button to hide screen shot options + screenShotsCollapsibleButton = ctk.ctkCollapsibleButton() + screenShotsCollapsibleButton.text = "Screen shot options" + self.layout.addWidget(screenShotsCollapsibleButton) + + # layout within the collapsible button + screenShotsFormLayout = qt.QFormLayout(screenShotsCollapsibleButton) + + # + # check box to trigger taking screen shots for later use in tutorials + # + self.enableScreenshotsFlagCheckBox = qt.QCheckBox() + self.enableScreenshotsFlagCheckBox.checked = 0 + self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") + screenShotsFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) + + # + # scale factor for screen shots + # + self.screenshotScaleFactorSliderWidget = ctk.ctkSliderWidget() + self.screenshotScaleFactorSliderWidget.singleStep = 1.0 + self.screenshotScaleFactorSliderWidget.minimum = 1.0 + self.screenshotScaleFactorSliderWidget.maximum = 50.0 + self.screenshotScaleFactorSliderWidget.value = 1.0 + self.screenshotScaleFactorSliderWidget.setToolTip("Set scale factor for the screen shots.") + screenShotsFormLayout.addRow("Screenshot scale factor", self.screenshotScaleFactorSliderWidget) + + # Add vertical spacer + self.layout.addStretch(1) + + def onPart1Ruler(self): + enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked + screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) + + tester = RSNAQuantTutorialTest() + tester.setUp() + tester.test_Part1Ruler(enableScreenshotsFlag, screenshotScaleFactor) + + def onPart2ChangeTracker(self): + enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked + screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) + + tester = RSNAQuantTutorialTest() + tester.setUp() + tester.test_Part2ChangeTracker(enableScreenshotsFlag, screenshotScaleFactor) + + def onPart3PETCT(self): + enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked + screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) + + tester = RSNAQuantTutorialTest() + tester.setUp() + tester.test_Part3PETCT(enableScreenshotsFlag, screenshotScaleFactor) # @@ -118,311 +118,311 @@ def onPart3PETCT(self): # class RSNAQuantTutorialLogic(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 - """ - pass + """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 + """ + pass class RSNAQuantTutorialTest(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. """ - self.delayDisplay("Closing the scene") - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - slicer.mrmlScene.Clear(0) - - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_Part1Ruler() - self.setUp() - self.test_Part2ChangeTracker() - self.setUp() - self.test_Part3PETCT() - - def test_Part1Ruler(self,enableScreenshotsFlag=0,screenshotScaleFactor=1): - """ Test using rulers - """ - self.enableScreenshots = enableScreenshotsFlag - self.screenshotScaleFactor = screenshotScaleFactor - - self.delayDisplay("Starting the test") - - # - # first, get some data - # - import SampleData - tumor = SampleData.downloadSample('MRBrainTumor1') - - try: - # four up view - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView) - - # annotations module - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('Annotations') - - # add ruler 1 - rulerNode1 = slicer.vtkMRMLAnnotationRulerNode() - rulerNode1.SetName("d1") - rulerNode1.SetPosition1(-7.59519,43.544,28.6) - rulerNode1.SetPosition2(-5.56987,14.177,28.6) - rulerNode1.Initialize(slicer.mrmlScene) - self.delayDisplay("Ruler 1") - - # add ruler 2 - rulerNode2 = slicer.vtkMRMLAnnotationRulerNode() - rulerNode2.SetName("d2") - rulerNode2.SetPosition1(-3.54455,27.656,13.1646) - rulerNode2.SetPosition2(-2.5319,27.656,47.5949) - rulerNode2.Initialize(slicer.mrmlScene) - self.delayDisplay("Ruler 2") - - # scroll - annotLogic = slicer.modules.annotations.logic() - annotLogic.JumpSlicesToAnnotationCoordinate(rulerNode1.GetID()) - - # show slices - redWidget = layoutManager.sliceWidget('Red') - redWidget.sliceController().setSliceLink(True) - redWidget.sliceController().setSliceVisible(True) - - self.takeScreenshot('Ruler','Ruler used to measure tumor diameter',-1) - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) - - def test_Part3PETCT(self,enableScreenshotsFlag=0,screenshotScaleFactor=1): - """ Test using the PETCT module + 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 """ - self.enableScreenshots = enableScreenshotsFlag - self.screenshotScaleFactor = screenshotScaleFactor - - self.delayDisplay("Starting the test") - - # - # first, get some data - # - import SampleData - extractPath = SampleData.downloadFromURL( - fileNames='dataset3_PETCT.zip', - uris=TESTING_DATA_URL + 'SHA256/11e81af3462076f4ca371b632e03ed435240042915c2daf07f80059b3f78f88d', - checksums='SHA256:11e81af3462076f4ca371b632e03ed435240042915c2daf07f80059b3f78f88d')[0] - - self.delayDisplay("Loading PET_CT_pre-treatment.mrb") - preTreatmentPath = extractPath + '/PET_CT_pre-treatment.mrb' - slicer.util.loadScene(preTreatmentPath) - self.takeScreenshot('PETCT-LoadedPre','Loaded pre-treatement scene',-1) - - try: - mainWindow = slicer.util.mainWindow() - layoutManager = slicer.app.layoutManager() - threeDView = layoutManager.threeDWidget(0).threeDView() - redWidget = layoutManager.sliceWidget('Red') - redController = redWidget.sliceController() - greenWidget = layoutManager.sliceWidget('Green') - greenController = greenWidget.sliceController() - yellowWidget = layoutManager.sliceWidget('Yellow') - yellowController = yellowWidget.sliceController() - viewNode = threeDView.mrmlViewNode() - cameras = slicer.util.getNodes('vtkMRMLCameraNode*') - for cameraNode in cameras.values(): - if cameraNode.GetActiveTag() == viewNode.GetID(): - break - - threeDView.resetFocalPoint() - slicer.util.clickAndDrag(threeDView,button='Right') - redWidget.sliceController().setSliceVisible(True) - yellowWidget.sliceController().setSliceVisible(True) - self.takeScreenshot('PETCT-ConfigureView','Configure View',-1) - - mainWindow.moduleSelector().selectModule('Volumes') - compositNode = redWidget.mrmlSliceCompositeNode() - compositNode.SetForegroundOpacity(0.2) - self.takeScreenshot('PETCT-ShowVolumes','Show Volumes with lesion',-1) - - compositNode.SetForegroundOpacity(0.5) - self.takeScreenshot('PETCT-CTOpacity','CT1 volume opacity to 0.5',-1) - - yellowWidget.sliceController().setSliceVisible(False) - greenWidget.sliceController().setSliceVisible(True) - self.takeScreenshot('PETCT-ShowSlices','Show axial and sagittal slices',-1) - - self.delayDisplay('SUV Computation') - if not hasattr(slicer.modules, 'petstandarduptakevaluecomputation'): - self.delayDisplay("PET SUV Computation not available, skipping the test.") - return - - slicer.util.selectModule('PETStandardUptakeValueComputation') - - parameters = { - "PETDICOMPath": extractPath + '/' + 'PET1', - "PETVolume": slicer.util.getNode('PET1'), - "VOIVolume": slicer.util.getNode('PET1-label'), - } - - suvComputation = slicer.modules.petstandarduptakevaluecomputation - self.CLINode1 = None - self.CLINode1 = slicer.cli.runSync(suvComputation, self.CLINode1, parameters, delete_temporary_files=False) - - # close the scene - slicer.mrmlScene.Clear(0) - - self.delayDisplay("Loading PET_CT_post-treatment.mrb") - postTreatmentPath = extractPath + '/PET_CT_post-treatment.mrb' - slicer.util.loadScene(postTreatmentPath) - self.takeScreenshot('PETCT-LoadedPost','Loaded post-treatement scene',-1) - - compositNode.SetForegroundOpacity(0.5) - self.takeScreenshot('PETCT-CT2Opacity','CT2 volume opacity to 0.5',-1) - - redController.setSliceOffsetValue(-165.01) - self.takeScreenshot('PETCT-LarynxUptake','Mild uptake in the larynx and pharynx',-1) - - redController.setSliceOffsetValue(-106.15) - self.takeScreenshot('PETCT-TumorUptake','No uptake in the area of the primary tumor',-1) - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) - - def test_Part2ChangeTracker(self,enableScreenshotsFlag=0,screenshotScaleFactor=1): - """ Test the ChangeTracker module - """ - self.enableScreenshots = enableScreenshotsFlag - self.screenshotScaleFactor = screenshotScaleFactor - - self.delayDisplay("Starting the test") - - if not hasattr(slicer.modules, 'changetracker'): - self.delayDisplay("ChangeTracker not available, skipping the test.") - return - - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL( - fileNames='ChangeTrackerScene.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/64734cbbf8ebafe4a52f551d1510a8f6f3d0625eb5b6c1e328be117c48e2c653', - checksums='SHA256:64734cbbf8ebafe4a52f551d1510a8f6f3d0625eb5b6c1e328be117c48e2c653') - self.takeScreenshot('ChangeTracker-Loaded','Finished with download and loading',-1) - - try: - mainWindow = slicer.util.mainWindow() - layoutManager = slicer.app.layoutManager() - threeDView = layoutManager.threeDWidget(0).threeDView() - redWidget = layoutManager.sliceWidget('Red') - redController = redWidget.sliceController() - viewNode = threeDView.mrmlViewNode() - cameras = slicer.util.getNodes('vtkMRMLCameraNode*') - for cameraNode in cameras.values(): - if cameraNode.GetActiveTag() == viewNode.GetID(): - break - - self.delayDisplay('Configure Module') - mainWindow.moduleSelector().selectModule('ChangeTracker') - self.takeScreenshot('ChangeTracker-ModuleGUI','Select the ChangeTracker module',-1) - - changeTracker = slicer.modules.changetracker.widgetRepresentation().self() - - baselineNode = slicer.util.getNode('2006-spgr1') - followupNode = slicer.util.getNode('2007-spgr1') - changeTracker.selectScansStep._ChangeTrackerSelectScansStep__baselineVolumeSelector.setCurrentNode(baselineNode) - changeTracker.selectScansStep._ChangeTrackerSelectScansStep__followupVolumeSelector.setCurrentNode(followupNode) - self.takeScreenshot('ChangeTracker-SetInputs','Select input scans',-1) - - changeTracker.workflow.goForward() - self.takeScreenshot('ChangeTracker-GoForward','Go Forward',-1) - - slicer.util.clickAndDrag(redWidget,button='Right') - self.takeScreenshot('ChangeTracker-Zoom','Inspect - zoom',-1) - - slicer.util.clickAndDrag(redWidget,button='Middle') - self.takeScreenshot('ChangeTracker-Pan','Inspect - pan',-1) - - for offset in range(-20,20,2): - redController.setSliceOffsetValue(offset) - self.takeScreenshot('ChangeTracker-Scroll','Inspect - scroll',-1) - - self.delayDisplay('Set ROI') - roi = changeTracker.defineROIStep._ChangeTrackerDefineROIStep__roi - roi.SetXYZ(-2.81037, 28.7629, 28.4536) - self.takeScreenshot('ChangeTracker-SetROICenter','Center VOI',-1) - roi.SetRadiusXYZ(22.6467, 22.6804, 22.9897) - self.takeScreenshot('ChangeTracker-SetROIExtent','Resize the VOI',-1) - - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalWidescreenView) - self.takeScreenshot('ChangeTracker-ConventionalWidescreen','Select the viewing mode Conventional Widescreen',-1) - - slicer.util.clickAndDrag(redWidget,button='Right') - self.takeScreenshot('ChangeTracker-ZoomVOI','Zoom',-1) - - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView) - self.takeScreenshot('ChangeTracker-FourUpLayout','Go back to Four-Up layout',-1) - - changeTracker.workflow.goForward() - self.takeScreenshot('ChangeTracker-GoForward','Go Forward',-1) - - changeTracker.segmentROIStep._ChangeTrackerSegmentROIStep__threshRange.minimumValue = 120 - self.takeScreenshot('ChangeTracker-Threshold','Set threshold',-1) - - changeTracker.workflow.goForward() - self.takeScreenshot('ChangeTracker-GoForward','Go Forward',-1) - - checkList = changeTracker.analyzeROIStep._ChangeTrackerAnalyzeROIStep__metricCheckboxList - index = list(checkList.values()).index('IntensityDifferenceMetric') - list(checkList.keys())[index].checked = True - self.takeScreenshot('ChangeTracker-PickMetric','Select the ROI analysis method',-1) - - changeTracker.workflow.goForward() - self.takeScreenshot('ChangeTracker-GoForward','Go Forward',-1) - - self.delayDisplay('Look!') - redWidget.sliceController().setSliceVisible(True) - - self.delayDisplay('Crosshairs') - compareWidget = layoutManager.sliceWidget('Compare1') - style = compareWidget.interactorStyle() - interactor = style.GetInteractor() - for step in range(100): - interactor.SetEventPosition(10,step) - style.OnMouseMove() - - self.delayDisplay('Zoom') - slicer.util.clickAndDrag(compareWidget,button='Right') - - self.delayDisplay('Pan') - slicer.util.clickAndDrag(compareWidget,button='Middle') - - self.delayDisplay('Inspect - scroll') - compareController = redWidget.sliceController() - for offset in range(10,30,2): - compareController.setSliceOffsetValue(offset) - - self.takeScreenshot('ChangeTracker-InspectResults','Inspected results',-1) - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + self.delayDisplay("Closing the scene") + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + slicer.mrmlScene.Clear(0) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_Part1Ruler() + self.setUp() + self.test_Part2ChangeTracker() + self.setUp() + self.test_Part3PETCT() + + def test_Part1Ruler(self, enableScreenshotsFlag=0, screenshotScaleFactor=1): + """ Test using rulers + """ + self.enableScreenshots = enableScreenshotsFlag + self.screenshotScaleFactor = screenshotScaleFactor + + self.delayDisplay("Starting the test") + + # + # first, get some data + # + import SampleData + tumor = SampleData.downloadSample('MRBrainTumor1') + + try: + # four up view + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView) + + # annotations module + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('Annotations') + + # add ruler 1 + rulerNode1 = slicer.vtkMRMLAnnotationRulerNode() + rulerNode1.SetName("d1") + rulerNode1.SetPosition1(-7.59519, 43.544, 28.6) + rulerNode1.SetPosition2(-5.56987, 14.177, 28.6) + rulerNode1.Initialize(slicer.mrmlScene) + self.delayDisplay("Ruler 1") + + # add ruler 2 + rulerNode2 = slicer.vtkMRMLAnnotationRulerNode() + rulerNode2.SetName("d2") + rulerNode2.SetPosition1(-3.54455, 27.656, 13.1646) + rulerNode2.SetPosition2(-2.5319, 27.656, 47.5949) + rulerNode2.Initialize(slicer.mrmlScene) + self.delayDisplay("Ruler 2") + + # scroll + annotLogic = slicer.modules.annotations.logic() + annotLogic.JumpSlicesToAnnotationCoordinate(rulerNode1.GetID()) + + # show slices + redWidget = layoutManager.sliceWidget('Red') + redWidget.sliceController().setSliceLink(True) + redWidget.sliceController().setSliceVisible(True) + + self.takeScreenshot('Ruler', 'Ruler used to measure tumor diameter', -1) + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) + + def test_Part3PETCT(self, enableScreenshotsFlag=0, screenshotScaleFactor=1): + """ Test using the PETCT module + """ + self.enableScreenshots = enableScreenshotsFlag + self.screenshotScaleFactor = screenshotScaleFactor + + self.delayDisplay("Starting the test") + + # + # first, get some data + # + import SampleData + extractPath = SampleData.downloadFromURL( + fileNames='dataset3_PETCT.zip', + uris=TESTING_DATA_URL + 'SHA256/11e81af3462076f4ca371b632e03ed435240042915c2daf07f80059b3f78f88d', + checksums='SHA256:11e81af3462076f4ca371b632e03ed435240042915c2daf07f80059b3f78f88d')[0] + + self.delayDisplay("Loading PET_CT_pre-treatment.mrb") + preTreatmentPath = extractPath + '/PET_CT_pre-treatment.mrb' + slicer.util.loadScene(preTreatmentPath) + self.takeScreenshot('PETCT-LoadedPre', 'Loaded pre-treatement scene', -1) + + try: + mainWindow = slicer.util.mainWindow() + layoutManager = slicer.app.layoutManager() + threeDView = layoutManager.threeDWidget(0).threeDView() + redWidget = layoutManager.sliceWidget('Red') + redController = redWidget.sliceController() + greenWidget = layoutManager.sliceWidget('Green') + greenController = greenWidget.sliceController() + yellowWidget = layoutManager.sliceWidget('Yellow') + yellowController = yellowWidget.sliceController() + viewNode = threeDView.mrmlViewNode() + cameras = slicer.util.getNodes('vtkMRMLCameraNode*') + for cameraNode in cameras.values(): + if cameraNode.GetActiveTag() == viewNode.GetID(): + break + + threeDView.resetFocalPoint() + slicer.util.clickAndDrag(threeDView, button='Right') + redWidget.sliceController().setSliceVisible(True) + yellowWidget.sliceController().setSliceVisible(True) + self.takeScreenshot('PETCT-ConfigureView', 'Configure View', -1) + + mainWindow.moduleSelector().selectModule('Volumes') + compositNode = redWidget.mrmlSliceCompositeNode() + compositNode.SetForegroundOpacity(0.2) + self.takeScreenshot('PETCT-ShowVolumes', 'Show Volumes with lesion', -1) + + compositNode.SetForegroundOpacity(0.5) + self.takeScreenshot('PETCT-CTOpacity', 'CT1 volume opacity to 0.5', -1) + + yellowWidget.sliceController().setSliceVisible(False) + greenWidget.sliceController().setSliceVisible(True) + self.takeScreenshot('PETCT-ShowSlices', 'Show axial and sagittal slices', -1) + + self.delayDisplay('SUV Computation') + if not hasattr(slicer.modules, 'petstandarduptakevaluecomputation'): + self.delayDisplay("PET SUV Computation not available, skipping the test.") + return + + slicer.util.selectModule('PETStandardUptakeValueComputation') + + parameters = { + "PETDICOMPath": extractPath + '/' + 'PET1', + "PETVolume": slicer.util.getNode('PET1'), + "VOIVolume": slicer.util.getNode('PET1-label'), + } + + suvComputation = slicer.modules.petstandarduptakevaluecomputation + self.CLINode1 = None + self.CLINode1 = slicer.cli.runSync(suvComputation, self.CLINode1, parameters, delete_temporary_files=False) + + # close the scene + slicer.mrmlScene.Clear(0) + + self.delayDisplay("Loading PET_CT_post-treatment.mrb") + postTreatmentPath = extractPath + '/PET_CT_post-treatment.mrb' + slicer.util.loadScene(postTreatmentPath) + self.takeScreenshot('PETCT-LoadedPost', 'Loaded post-treatement scene', -1) + + compositNode.SetForegroundOpacity(0.5) + self.takeScreenshot('PETCT-CT2Opacity', 'CT2 volume opacity to 0.5', -1) + + redController.setSliceOffsetValue(-165.01) + self.takeScreenshot('PETCT-LarynxUptake', 'Mild uptake in the larynx and pharynx', -1) + + redController.setSliceOffsetValue(-106.15) + self.takeScreenshot('PETCT-TumorUptake', 'No uptake in the area of the primary tumor', -1) + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) + + def test_Part2ChangeTracker(self, enableScreenshotsFlag=0, screenshotScaleFactor=1): + """ Test the ChangeTracker module + """ + self.enableScreenshots = enableScreenshotsFlag + self.screenshotScaleFactor = screenshotScaleFactor + + self.delayDisplay("Starting the test") + + if not hasattr(slicer.modules, 'changetracker'): + self.delayDisplay("ChangeTracker not available, skipping the test.") + return + + # + # first, get some data + # + import SampleData + SampleData.downloadFromURL( + fileNames='ChangeTrackerScene.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/64734cbbf8ebafe4a52f551d1510a8f6f3d0625eb5b6c1e328be117c48e2c653', + checksums='SHA256:64734cbbf8ebafe4a52f551d1510a8f6f3d0625eb5b6c1e328be117c48e2c653') + self.takeScreenshot('ChangeTracker-Loaded', 'Finished with download and loading', -1) + + try: + mainWindow = slicer.util.mainWindow() + layoutManager = slicer.app.layoutManager() + threeDView = layoutManager.threeDWidget(0).threeDView() + redWidget = layoutManager.sliceWidget('Red') + redController = redWidget.sliceController() + viewNode = threeDView.mrmlViewNode() + cameras = slicer.util.getNodes('vtkMRMLCameraNode*') + for cameraNode in cameras.values(): + if cameraNode.GetActiveTag() == viewNode.GetID(): + break + + self.delayDisplay('Configure Module') + mainWindow.moduleSelector().selectModule('ChangeTracker') + self.takeScreenshot('ChangeTracker-ModuleGUI', 'Select the ChangeTracker module', -1) + + changeTracker = slicer.modules.changetracker.widgetRepresentation().self() + + baselineNode = slicer.util.getNode('2006-spgr1') + followupNode = slicer.util.getNode('2007-spgr1') + changeTracker.selectScansStep._ChangeTrackerSelectScansStep__baselineVolumeSelector.setCurrentNode(baselineNode) + changeTracker.selectScansStep._ChangeTrackerSelectScansStep__followupVolumeSelector.setCurrentNode(followupNode) + self.takeScreenshot('ChangeTracker-SetInputs', 'Select input scans', -1) + + changeTracker.workflow.goForward() + self.takeScreenshot('ChangeTracker-GoForward', 'Go Forward', -1) + + slicer.util.clickAndDrag(redWidget, button='Right') + self.takeScreenshot('ChangeTracker-Zoom', 'Inspect - zoom', -1) + + slicer.util.clickAndDrag(redWidget, button='Middle') + self.takeScreenshot('ChangeTracker-Pan', 'Inspect - pan', -1) + + for offset in range(-20, 20, 2): + redController.setSliceOffsetValue(offset) + self.takeScreenshot('ChangeTracker-Scroll', 'Inspect - scroll', -1) + + self.delayDisplay('Set ROI') + roi = changeTracker.defineROIStep._ChangeTrackerDefineROIStep__roi + roi.SetXYZ(-2.81037, 28.7629, 28.4536) + self.takeScreenshot('ChangeTracker-SetROICenter', 'Center VOI', -1) + roi.SetRadiusXYZ(22.6467, 22.6804, 22.9897) + self.takeScreenshot('ChangeTracker-SetROIExtent', 'Resize the VOI', -1) + + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalWidescreenView) + self.takeScreenshot('ChangeTracker-ConventionalWidescreen', 'Select the viewing mode Conventional Widescreen', -1) + + slicer.util.clickAndDrag(redWidget, button='Right') + self.takeScreenshot('ChangeTracker-ZoomVOI', 'Zoom', -1) + + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView) + self.takeScreenshot('ChangeTracker-FourUpLayout', 'Go back to Four-Up layout', -1) + + changeTracker.workflow.goForward() + self.takeScreenshot('ChangeTracker-GoForward', 'Go Forward', -1) + + changeTracker.segmentROIStep._ChangeTrackerSegmentROIStep__threshRange.minimumValue = 120 + self.takeScreenshot('ChangeTracker-Threshold', 'Set threshold', -1) + + changeTracker.workflow.goForward() + self.takeScreenshot('ChangeTracker-GoForward', 'Go Forward', -1) + + checkList = changeTracker.analyzeROIStep._ChangeTrackerAnalyzeROIStep__metricCheckboxList + index = list(checkList.values()).index('IntensityDifferenceMetric') + list(checkList.keys())[index].checked = True + self.takeScreenshot('ChangeTracker-PickMetric', 'Select the ROI analysis method', -1) + + changeTracker.workflow.goForward() + self.takeScreenshot('ChangeTracker-GoForward', 'Go Forward', -1) + + self.delayDisplay('Look!') + redWidget.sliceController().setSliceVisible(True) + + self.delayDisplay('Crosshairs') + compareWidget = layoutManager.sliceWidget('Compare1') + style = compareWidget.interactorStyle() + interactor = style.GetInteractor() + for step in range(100): + interactor.SetEventPosition(10, step) + style.OnMouseMove() + + self.delayDisplay('Zoom') + slicer.util.clickAndDrag(compareWidget, button='Right') + + self.delayDisplay('Pan') + slicer.util.clickAndDrag(compareWidget, button='Middle') + + self.delayDisplay('Inspect - scroll') + compareController = redWidget.sliceController() + for offset in range(10, 30, 2): + compareController.setSliceOffsetValue(offset) + + self.takeScreenshot('ChangeTracker-InspectResults', 'Inspected results', -1) + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) diff --git a/Applications/SlicerApp/Testing/Python/RSNAVisTutorial.py b/Applications/SlicerApp/Testing/Python/RSNAVisTutorial.py index 5290605d01f..702af320008 100644 --- a/Applications/SlicerApp/Testing/Python/RSNAVisTutorial.py +++ b/Applications/SlicerApp/Testing/Python/RSNAVisTutorial.py @@ -12,22 +12,22 @@ class RSNAVisTutorial(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) - parent.title = "RSNAVisTutorial" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + """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) + parent.title = "RSNAVisTutorial" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ This module was developed as a self test to perform the operations needed for the RSNA 2012 Visualization Tutorial """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -35,103 +35,103 @@ def __init__(self, parent): # class RSNAVisTutorialWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... - - # Collapsible button - testsCollapsibleButton = ctk.ctkCollapsibleButton() - testsCollapsibleButton.text = "Tests" - self.layout.addWidget(testsCollapsibleButton) - - # Layout within the collapsible button - formLayout = qt.QFormLayout(testsCollapsibleButton) - - # test buttons - tests = ( ("Part 1: DICOM",self.onPart1DICOM),("Part 2: Head", self.onPart2Head),("Part 3: Liver", self.onPart3Liver),("Part 4: Lung", self.onPart4Lung),) - for text,slot in tests: - testButton = qt.QPushButton(text) - testButton.toolTip = "Run the test." - formLayout.addWidget(testButton) - testButton.connect('clicked(bool)', slot) - - # A collapsible button to hide screen shot options - screenShotsCollapsibleButton = ctk.ctkCollapsibleButton() - screenShotsCollapsibleButton.text = "Screen shot options" - self.layout.addWidget(screenShotsCollapsibleButton) - - # layout within the collapsible button - screenShotsFormLayout = qt.QFormLayout(screenShotsCollapsibleButton) - - # - # check box to trigger taking screen shots for later use in tutorials - # - self.enableScreenshotsFlagCheckBox = qt.QCheckBox() - self.enableScreenshotsFlagCheckBox.checked = 0 - self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") - screenShotsFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) - - # - # scale factor for screen shots - # - self.screenshotScaleFactorSliderWidget = ctk.ctkSliderWidget() - self.screenshotScaleFactorSliderWidget.singleStep = 1.0 - self.screenshotScaleFactorSliderWidget.minimum = 1.0 - self.screenshotScaleFactorSliderWidget.maximum = 50.0 - self.screenshotScaleFactorSliderWidget.value = 1.0 - self.screenshotScaleFactorSliderWidget.setToolTip("Set scale factor for the screen shots.") - screenShotsFormLayout.addRow("Screenshot scale factor", self.screenshotScaleFactorSliderWidget) - - # Add vertical spacer - self.layout.addStretch(1) - - def onPart1DICOM(self): - enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked - screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) - - tester = RSNAVisTutorialTest() - tester.setUp() - tester.test_Part1DICOM(enableScreenshotsFlag,screenshotScaleFactor) - - def onPart2Head(self): - enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked - screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) - - tester = RSNAVisTutorialTest() - tester.setUp() - tester.test_Part2Head(enableScreenshotsFlag,screenshotScaleFactor) - - def onPart3Liver(self): - enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked - screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) - - tester = RSNAVisTutorialTest() - tester.setUp() - tester.test_Part3Liver(enableScreenshotsFlag,screenshotScaleFactor) - - def onPart4Lung(self): - enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked - screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) - - tester = RSNAVisTutorialTest() - tester.setUp() - tester.test_Part4Lung(enableScreenshotsFlag,screenshotScaleFactor) - - def onReload(self,moduleName="RSNAVisTutorial"): - """Generic reload method for any scripted module. - ModuleWizard will substitute correct default moduleName. + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - globals()[moduleName] = slicer.util.reloadScriptedModule(moduleName) - def onReloadAndTest(self,moduleName="RSNAVisTutorial"): - self.onReload() - evalString = f'globals()["{moduleName}"].{moduleName}Test()' - tester = eval(evalString) - tester.runTest() + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... + + # Collapsible button + testsCollapsibleButton = ctk.ctkCollapsibleButton() + testsCollapsibleButton.text = "Tests" + self.layout.addWidget(testsCollapsibleButton) + + # Layout within the collapsible button + formLayout = qt.QFormLayout(testsCollapsibleButton) + + # test buttons + tests = (("Part 1: DICOM", self.onPart1DICOM), ("Part 2: Head", self.onPart2Head), ("Part 3: Liver", self.onPart3Liver), ("Part 4: Lung", self.onPart4Lung),) + for text, slot in tests: + testButton = qt.QPushButton(text) + testButton.toolTip = "Run the test." + formLayout.addWidget(testButton) + testButton.connect('clicked(bool)', slot) + + # A collapsible button to hide screen shot options + screenShotsCollapsibleButton = ctk.ctkCollapsibleButton() + screenShotsCollapsibleButton.text = "Screen shot options" + self.layout.addWidget(screenShotsCollapsibleButton) + + # layout within the collapsible button + screenShotsFormLayout = qt.QFormLayout(screenShotsCollapsibleButton) + + # + # check box to trigger taking screen shots for later use in tutorials + # + self.enableScreenshotsFlagCheckBox = qt.QCheckBox() + self.enableScreenshotsFlagCheckBox.checked = 0 + self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") + screenShotsFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) + + # + # scale factor for screen shots + # + self.screenshotScaleFactorSliderWidget = ctk.ctkSliderWidget() + self.screenshotScaleFactorSliderWidget.singleStep = 1.0 + self.screenshotScaleFactorSliderWidget.minimum = 1.0 + self.screenshotScaleFactorSliderWidget.maximum = 50.0 + self.screenshotScaleFactorSliderWidget.value = 1.0 + self.screenshotScaleFactorSliderWidget.setToolTip("Set scale factor for the screen shots.") + screenShotsFormLayout.addRow("Screenshot scale factor", self.screenshotScaleFactorSliderWidget) + + # Add vertical spacer + self.layout.addStretch(1) + + def onPart1DICOM(self): + enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked + screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) + + tester = RSNAVisTutorialTest() + tester.setUp() + tester.test_Part1DICOM(enableScreenshotsFlag, screenshotScaleFactor) + + def onPart2Head(self): + enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked + screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) + + tester = RSNAVisTutorialTest() + tester.setUp() + tester.test_Part2Head(enableScreenshotsFlag, screenshotScaleFactor) + + def onPart3Liver(self): + enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked + screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) + + tester = RSNAVisTutorialTest() + tester.setUp() + tester.test_Part3Liver(enableScreenshotsFlag, screenshotScaleFactor) + + def onPart4Lung(self): + enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked + screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) + + tester = RSNAVisTutorialTest() + tester.setUp() + tester.test_Part4Lung(enableScreenshotsFlag, screenshotScaleFactor) + + def onReload(self, moduleName="RSNAVisTutorial"): + """Generic reload method for any scripted module. + ModuleWizard will substitute correct default moduleName. + """ + globals()[moduleName] = slicer.util.reloadScriptedModule(moduleName) + + def onReloadAndTest(self, moduleName="RSNAVisTutorial"): + self.onReload() + evalString = f'globals()["{moduleName}"].{moduleName}Test()' + tester = eval(evalString) + tester.runTest() # @@ -139,402 +139,402 @@ def onReloadAndTest(self,moduleName="RSNAVisTutorial"): # class RSNAVisTutorialLogic(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 - """ - pass + """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 + """ + pass class RSNAVisTutorialTest(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. - """ - self.delayDisplay("Closing the scene") - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - slicer.mrmlScene.Clear(0) - - def runTest(self): - """Run as few or as many tests as needed here. """ - self.setUp() - self.test_Part1DICOM() - self.setUp() - self.test_Part2Head() - self.setUp() - self.test_Part3Liver() - self.setUp() - self.test_Part4Lung() - - def test_Part1DICOM(self,enableScreenshotsFlag=0,screenshotScaleFactor=1): - """ Test the DICOM part of the test using the head atlas + 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 """ - self.enableScreenshots = enableScreenshotsFlag - self.screenshotScaleFactor = screenshotScaleFactor - - self.delayDisplay("Starting the DICOM test") - # - # first, get the data - a zip file of dicom data - # - import SampleData - dicomFilesDirectory = SampleData.downloadFromURL( - fileNames='dataset1_Thorax_Abdomen.zip', - uris=TESTING_DATA_URL + 'SHA256/17a4199aad03a373dab27dc17e5bfcf84fc194d0a30975b4073e5b595d43a56a', - checksums='SHA256:17a4199aad03a373dab27dc17e5bfcf84fc194d0a30975b4073e5b595d43a56a')[0] - - try: - self.delayDisplay("Switching to temp database directory") - originalDatabaseDirectory = DICOMUtils.openTemporaryDatabase('tempDICOMDatabase') - - slicer.util.selectModule('DICOM') - browserWidget = slicer.modules.DICOMWidget.browserWidget - dicomBrowser = browserWidget.dicomBrowser - dicomBrowser.importDirectory(dicomFilesDirectory, dicomBrowser.ImportDirectoryAddLink) - dicomBrowser.waitForImportFinished() - - # load the data by series UID - dicomBrowser.dicomTableManager().patientsTable().selectFirst() - browserWidget.examineForLoading() - - self.delayDisplay('Loading Selection') - browserWidget.loadCheckedLoadables() - - self.takeScreenshot('LoadingADICOMVolume-Loaded','Loaded DICOM Volume',-1) - - layoutManager = slicer.app.layoutManager() - redWidget = layoutManager.sliceWidget('Red') - slicer.util.clickAndDrag(redWidget,start=(10,10),end=(10,40)) - slicer.util.clickAndDrag(redWidget,start=(10,10),end=(40,10)) - - self.takeScreenshot('LoadingADICOMVolume-WL','Changed level and window',-1) - - redWidget.sliceController().setSliceLink(True) - redWidget.sliceController().setSliceVisible(True) - self.takeScreenshot('LoadingADICOMVolume-LinkView','Linked and visible',-1) - - slicer.util.clickAndDrag(redWidget,button='Right',start=(10,10),end=(10,40)) - self.takeScreenshot('LoadingADICOMVolume-Zoom','Zoom',-1) - - threeDView = layoutManager.threeDWidget(0).threeDView() - slicer.util.clickAndDrag(threeDView) - self.takeScreenshot('LoadingADICOMVolume-Rotate','Rotate',-1) - - threeDView.resetFocalPoint() - self.takeScreenshot('LoadingADICOMVolume-Center','Center the view',-1) - - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalWidescreenView) - self.takeScreenshot('LoadingADICOMVolume-ConventionalWidescreen','Conventional Widescreen Layout',-1) - - slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering') - self.takeScreenshot('VolumeRendering-Module','Volume Rendering',-1) - volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation() - abdomenVolume = slicer.mrmlScene.GetFirstNodeByName('6: CT_Thorax_Abdomen') - volumeRenderingWidgetRep.setMRMLVolumeNode(abdomenVolume) - self.takeScreenshot('VolumeRendering-SelectVolume','Select the volume 6: CT_Thorax_Abdomen',-1) - - presetsScene = slicer.modules.volumerendering.logic().GetPresetsScene() - ctCardiac3 = presetsScene.GetFirstNodeByName('CT-Cardiac3') - volumeRenderingWidgetRep.mrmlVolumePropertyNode().Copy(ctCardiac3) - self.takeScreenshot('VolumeRendering-SelectPreset','Select the Preset CT-Cardiac-3') - - self.delayDisplay('Skipping: Select VTK CPU Ray Casting') - - volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering') - volumeRenderingNode.SetVisibility(1) - self.takeScreenshot('VolumeRendering-ViewRendering','View Volume Rendering',-1) - - self.delayDisplay('Skipping Move the Shift slider') - - redWidget.sliceController().setSliceVisible(False) - self.takeScreenshot('VolumeRendering-SlicesOff','Turn off visibility of slices in 3D',-1) - - threeDView = layoutManager.threeDWidget(0).threeDView() - slicer.util.clickAndDrag(threeDView) - self.takeScreenshot('VolumeRendering-RotateVolumeRendering','Rotate volume rendered image',-1) - - volumeRenderingNode.SetVisibility(0) - self.takeScreenshot('VolumeRendering-TurnOffVolumeRendering','Turn off volume rendered image',-1) - - volumeRenderingNode.SetCroppingEnabled(1) - annotationROI = slicer.mrmlScene.GetFirstNodeByName('AnnotationROI') - annotationROI.SetDisplayVisibility(1) - self.takeScreenshot('VolumeRendering-DisplayROI','Enable cropping and display ROI',-1) - - redWidget.sliceController().setSliceVisible(True) - self.takeScreenshot('VolumeRendering-SlicesOn','Turn on visibility of slices in 3D',-1) - - annotationROI.SetXYZ(-79.61,154.16,-232.591) - annotationROI.SetRadiusXYZ(43.4,65.19,70.5) - self.takeScreenshot('VolumeRendering-SizedROI','Position the ROI over a kidney',-1) - - volumeRenderingNode.SetVisibility(1) - self.takeScreenshot('VolumeRendering-ROIRendering','ROI volume rendered',-1) - - annotationROI.SetXYZ(15,146,-186) - annotationROI.SetRadiusXYZ(138,57,61) - self.takeScreenshot('VolumeRendering-BothKidneys','Rendered both kidneys',-1) - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) - - self.delayDisplay("Restoring original database directory") - DICOMUtils.closeTemporaryDatabase(originalDatabaseDirectory) - - def test_Part2Head(self,enableScreenshotsFlag=0,screenshotScaleFactor=1): - """ Test using the head atlas - may not be needed - Slicer4Minute is already tested - """ - self.enableScreenshots = enableScreenshotsFlag - self.screenshotScaleFactor = screenshotScaleFactor - - self.delayDisplay("Starting the test") - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL( - fileNames='Head_Scene.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/6785e481925c912a5a3940e9c9b71935df93a78a871e10f66ab71f8478229e68', - checksums='SHA256:6785e481925c912a5a3940e9c9b71935df93a78a871e10f66ab71f8478229e68') - - self.takeScreenshot('Head-Downloaded','Finished with download and loading',-1) - - try: - mainWindow = slicer.util.mainWindow() - layoutManager = slicer.app.layoutManager() - threeDView = layoutManager.threeDWidget(0).threeDView() - redWidget = layoutManager.sliceWidget('Red') - redController = redWidget.sliceController() - greenWidget = layoutManager.sliceWidget('Green') - greenController = greenWidget.sliceController() - - mainWindow.moduleSelector().selectModule('Models') - redWidget.sliceController().setSliceVisible(True) - self.takeScreenshot('Head-ModelsAndSliceModel','Models and Slice Model',-1) - - slicer.util.clickAndDrag(threeDView) - self.takeScreenshot('Head-Rotate','Rotate',-1) - - redController.setSliceVisible(True) - self.takeScreenshot('Head-AxialSlice','Display Axial Slice',-1) - - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - self.takeScreenshot('Head-ConventionalView','Conventional Layout',-1) - - viewNode = threeDView.mrmlViewNode() - cameras = slicer.util.getNodes('vtkMRMLCameraNode*') - for cameraNode in cameras.values(): - if cameraNode.GetActiveTag() == viewNode.GetID(): - break - cameraNode.GetCamera().Azimuth(90) - cameraNode.GetCamera().Elevation(20) - - # turn off skin and skull - skin = slicer.util.getNode(pattern='Skin.vtk') - skin.GetDisplayNode().SetOpacity(0.5) - self.takeScreenshot('Head-SkinOpacity','Skin Opacity to 0.5',-1) - - skin.GetDisplayNode().SetVisibility(0) - self.takeScreenshot('Head-SkinOpacityZero','Skin Opacity to 0',-1) - - skull = slicer.util.getNode(pattern='skull_bone.vtk') - - greenWidget.sliceController().setSliceVisible(True) - self.takeScreenshot('Head-GreenSlice','Display Coronal Slice',-1) - - # hemispheric_white_matter.GetDisplayNode().SetClipping(1) - skull.GetDisplayNode().SetClipping(1) - clip = slicer.util.getNode(pattern='vtkMRMLClipModelsNode1') - clip.SetRedSliceClipState(0) - clip.SetYellowSliceClipState(0) - clip.SetGreenSliceClipState(2) - self.takeScreenshot('Head-SkullClipping','Turn on clipping for skull model',-1) - - for offset in range(-20,20,2): - greenController.setSliceOffsetValue(offset) - self.takeScreenshot('Head-ScrollCoronal','Scroll through coronal slices',-1) - - skull.GetDisplayNode().SetVisibility(0) - self.takeScreenshot('Head-HideSkull','Make the skull invisible',-1) - - for offset in range(-40,-20,2): - greenController.setSliceOffsetValue(offset) - self.takeScreenshot('Head-ScrollCoronalWhiteMatter','Scroll through coronal slices to show white matter',-1) - - hemispheric_white_matter = slicer.util.getNode(pattern='hemispheric_white_matter.vtk') - hemispheric_white_matter.GetDisplayNode().SetVisibility(0) - self.takeScreenshot('Head-HideWhiteMatter','Turn off white matter',-1) - - self.delayDisplay('Rotate') - slicer.util.clickAndDrag(threeDView) - - self.delayDisplay('Zoom') - threeDView = layoutManager.threeDWidget(0).threeDView() - slicer.util.clickAndDrag(threeDView,button='Right') - self.takeScreenshot('Head-Zoom','Zoom',-1) - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) - - def test_Part3Liver(self,enableScreenshotsFlag=0,screenshotScaleFactor=1): - """ Test using the liver example data - """ - self.enableScreenshots = enableScreenshotsFlag - self.screenshotScaleFactor = screenshotScaleFactor - - self.delayDisplay("Starting the test") - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL( - fileNames='LiverSegments_Scene.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/ff797140c13a5988a7b72920adf0d2dab390a9babeab9161d5c52613328249f7', - checksums='SHA256:ff797140c13a5988a7b72920adf0d2dab390a9babeab9161d5c52613328249f7') - - self.takeScreenshot('Liver-Loaded','Loaded Liver scene',-1) - - try: - mainWindow = slicer.util.mainWindow() - layoutManager = slicer.app.layoutManager() - threeDView = layoutManager.threeDWidget(0).threeDView() - redWidget = layoutManager.sliceWidget('Red') - redController = redWidget.sliceController() - viewNode = threeDView.mrmlViewNode() - cameras = slicer.util.getNodes('vtkMRMLCameraNode*') - for cameraNode in cameras.values(): - if cameraNode.GetActiveTag() == viewNode.GetID(): - break - - mainWindow.moduleSelector().selectModule('Models') - self.takeScreenshot('Liver-Models','Models module',-1) - - segmentII = slicer.util.getNode('LiverSegment_II') - segmentII.GetDisplayNode().SetVisibility(0) - slicer.util.clickAndDrag(threeDView,start=(10,200),end=(10,10)) - self.takeScreenshot('Liver-SegmentII','Segment II invisible',-1) - - segmentII.GetDisplayNode().SetVisibility(1) - self.takeScreenshot('Liver-SegmentII','Segment II visible',-1) - - cameraNode.GetCamera().Azimuth(0) - cameraNode.GetCamera().Elevation(0) - self.takeScreenshot('Liver-SuperiorView','Superior view',-1) - - segmentII.GetDisplayNode().SetVisibility(0) - cameraNode.GetCamera().Azimuth(180) - cameraNode.GetCamera().Elevation(-30) - redWidget.sliceController().setSliceVisible(True) - self.takeScreenshot('Liver-ViewAdrenal','View Adrenal',-1) - - models = slicer.util.getNodes('vtkMRMLModelNode*') - for modelNode in models.values(): - modelNode.GetDisplayNode().SetVisibility(0) - - transparentNodes = ('MiddleHepaticVein_and_Branches','LiverSegment_IVb','LiverSegmentV',) - for nodeName in transparentNodes: - modelNode = slicer.util.getNode(nodeName) - modelNode.GetDisplayNode().SetOpacity(0.5) - modelNode.GetDisplayNode().SetVisibility(1) - cameraNode.GetCamera().Azimuth(30) - cameraNode.GetCamera().Elevation(-20) - redWidget.sliceController().setSliceVisible(True) - self.takeScreenshot('Liver-MiddleHepatic','Middle Hepatic',-1) - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) - - def test_Part4Lung(self,enableScreenshotsFlag=0,screenshotScaleFactor=1): - """ Test using the lung data - """ - self.enableScreenshots = enableScreenshotsFlag - self.screenshotScaleFactor = screenshotScaleFactor - - self.delayDisplay("Starting the test") - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL( - fileNames='LungSegments_Scene.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/89ffc6cabd76a17dfa6beb404a5901a4b4e4b4f2f4ee46c2d5f4d34459f554a1', - checksums='SHA256:89ffc6cabd76a17dfa6beb404a5901a4b4e4b4f2f4ee46c2d5f4d34459f554a1') - - self.takeScreenshot('Lung-Loaded','Finished with download and loading',-1) - - try: - mainWindow = slicer.util.mainWindow() - layoutManager = slicer.app.layoutManager() - threeDView = layoutManager.threeDWidget(0).threeDView() - redWidget = layoutManager.sliceWidget('Red') - redController = redWidget.sliceController() - viewNode = threeDView.mrmlViewNode() - cameras = slicer.util.getNodes('vtkMRMLCameraNode*') - for cameraNode in cameras.values(): - if cameraNode.GetActiveTag() == viewNode.GetID(): - break - - threeDView.resetFocalPoint() - self.takeScreenshot('Lung-ResetView','Reset view',-1) - - mainWindow.moduleSelector().selectModule('Models') - self.takeScreenshot('Lung-Models','Models module',-1) - - cameraNode.GetCamera().Azimuth(-100) - cameraNode.GetCamera().Elevation(-40) - redWidget.sliceController().setSliceVisible(True) - lungs = slicer.util.getNode('chestCT_lungs') - lungs.GetDisplayNode().SetVisibility(0) - self.takeScreenshot('Lung-Question1','View Question 1',-1) - - cameraNode.GetCamera().Azimuth(-65) - cameraNode.GetCamera().Elevation(-20) - lungs.GetDisplayNode().SetVisibility(1) - lungs.GetDisplayNode().SetOpacity(0.24) - redController.setSliceOffsetValue(-50) - self.takeScreenshot('Lung-Question2','View Question 2',-1) - - cameraNode.GetCamera().Azimuth(-165) - cameraNode.GetCamera().Elevation(-10) - redWidget.sliceController().setSliceVisible(False) - self.takeScreenshot('Lung-Question3','View Question 3',-1) - - cameraNode.GetCamera().Azimuth(20) - cameraNode.GetCamera().Elevation(-10) - lowerLobeNodes = slicer.util.getNodes('*LowerLobe*') - for showNode in lowerLobeNodes: - self.delayDisplay('Showing Node %s' % showNode, 300) - for node in lowerLobeNodes: - displayNode = lowerLobeNodes[node].GetDisplayNode() - if displayNode: - displayNode.SetVisibility(1 if node == showNode else 0) - self.takeScreenshot('Lung-Question4','View Question 4',-1) - - self.delayDisplay('Test passed!') - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e)) + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + self.delayDisplay("Closing the scene") + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + slicer.mrmlScene.Clear(0) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_Part1DICOM() + self.setUp() + self.test_Part2Head() + self.setUp() + self.test_Part3Liver() + self.setUp() + self.test_Part4Lung() + + def test_Part1DICOM(self, enableScreenshotsFlag=0, screenshotScaleFactor=1): + """ Test the DICOM part of the test using the head atlas + """ + self.enableScreenshots = enableScreenshotsFlag + self.screenshotScaleFactor = screenshotScaleFactor + + self.delayDisplay("Starting the DICOM test") + # + # first, get the data - a zip file of dicom data + # + import SampleData + dicomFilesDirectory = SampleData.downloadFromURL( + fileNames='dataset1_Thorax_Abdomen.zip', + uris=TESTING_DATA_URL + 'SHA256/17a4199aad03a373dab27dc17e5bfcf84fc194d0a30975b4073e5b595d43a56a', + checksums='SHA256:17a4199aad03a373dab27dc17e5bfcf84fc194d0a30975b4073e5b595d43a56a')[0] + + try: + self.delayDisplay("Switching to temp database directory") + originalDatabaseDirectory = DICOMUtils.openTemporaryDatabase('tempDICOMDatabase') + + slicer.util.selectModule('DICOM') + browserWidget = slicer.modules.DICOMWidget.browserWidget + dicomBrowser = browserWidget.dicomBrowser + dicomBrowser.importDirectory(dicomFilesDirectory, dicomBrowser.ImportDirectoryAddLink) + dicomBrowser.waitForImportFinished() + + # load the data by series UID + dicomBrowser.dicomTableManager().patientsTable().selectFirst() + browserWidget.examineForLoading() + + self.delayDisplay('Loading Selection') + browserWidget.loadCheckedLoadables() + + self.takeScreenshot('LoadingADICOMVolume-Loaded', 'Loaded DICOM Volume', -1) + + layoutManager = slicer.app.layoutManager() + redWidget = layoutManager.sliceWidget('Red') + slicer.util.clickAndDrag(redWidget, start=(10, 10), end=(10, 40)) + slicer.util.clickAndDrag(redWidget, start=(10, 10), end=(40, 10)) + + self.takeScreenshot('LoadingADICOMVolume-WL', 'Changed level and window', -1) + + redWidget.sliceController().setSliceLink(True) + redWidget.sliceController().setSliceVisible(True) + self.takeScreenshot('LoadingADICOMVolume-LinkView', 'Linked and visible', -1) + + slicer.util.clickAndDrag(redWidget, button='Right', start=(10, 10), end=(10, 40)) + self.takeScreenshot('LoadingADICOMVolume-Zoom', 'Zoom', -1) + + threeDView = layoutManager.threeDWidget(0).threeDView() + slicer.util.clickAndDrag(threeDView) + self.takeScreenshot('LoadingADICOMVolume-Rotate', 'Rotate', -1) + + threeDView.resetFocalPoint() + self.takeScreenshot('LoadingADICOMVolume-Center', 'Center the view', -1) + + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalWidescreenView) + self.takeScreenshot('LoadingADICOMVolume-ConventionalWidescreen', 'Conventional Widescreen Layout', -1) + + slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering') + self.takeScreenshot('VolumeRendering-Module', 'Volume Rendering', -1) + + volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation() + abdomenVolume = slicer.mrmlScene.GetFirstNodeByName('6: CT_Thorax_Abdomen') + volumeRenderingWidgetRep.setMRMLVolumeNode(abdomenVolume) + self.takeScreenshot('VolumeRendering-SelectVolume', 'Select the volume 6: CT_Thorax_Abdomen', -1) + + presetsScene = slicer.modules.volumerendering.logic().GetPresetsScene() + ctCardiac3 = presetsScene.GetFirstNodeByName('CT-Cardiac3') + volumeRenderingWidgetRep.mrmlVolumePropertyNode().Copy(ctCardiac3) + self.takeScreenshot('VolumeRendering-SelectPreset', 'Select the Preset CT-Cardiac-3') + + self.delayDisplay('Skipping: Select VTK CPU Ray Casting') + + volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering') + volumeRenderingNode.SetVisibility(1) + self.takeScreenshot('VolumeRendering-ViewRendering', 'View Volume Rendering', -1) + + self.delayDisplay('Skipping Move the Shift slider') + + redWidget.sliceController().setSliceVisible(False) + self.takeScreenshot('VolumeRendering-SlicesOff', 'Turn off visibility of slices in 3D', -1) + + threeDView = layoutManager.threeDWidget(0).threeDView() + slicer.util.clickAndDrag(threeDView) + self.takeScreenshot('VolumeRendering-RotateVolumeRendering', 'Rotate volume rendered image', -1) + + volumeRenderingNode.SetVisibility(0) + self.takeScreenshot('VolumeRendering-TurnOffVolumeRendering', 'Turn off volume rendered image', -1) + + volumeRenderingNode.SetCroppingEnabled(1) + annotationROI = slicer.mrmlScene.GetFirstNodeByName('AnnotationROI') + annotationROI.SetDisplayVisibility(1) + self.takeScreenshot('VolumeRendering-DisplayROI', 'Enable cropping and display ROI', -1) + + redWidget.sliceController().setSliceVisible(True) + self.takeScreenshot('VolumeRendering-SlicesOn', 'Turn on visibility of slices in 3D', -1) + + annotationROI.SetXYZ(-79.61, 154.16, -232.591) + annotationROI.SetRadiusXYZ(43.4, 65.19, 70.5) + self.takeScreenshot('VolumeRendering-SizedROI', 'Position the ROI over a kidney', -1) + + volumeRenderingNode.SetVisibility(1) + self.takeScreenshot('VolumeRendering-ROIRendering', 'ROI volume rendered', -1) + + annotationROI.SetXYZ(15, 146, -186) + annotationROI.SetRadiusXYZ(138, 57, 61) + self.takeScreenshot('VolumeRendering-BothKidneys', 'Rendered both kidneys', -1) + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) + + self.delayDisplay("Restoring original database directory") + DICOMUtils.closeTemporaryDatabase(originalDatabaseDirectory) + + def test_Part2Head(self, enableScreenshotsFlag=0, screenshotScaleFactor=1): + """ Test using the head atlas - may not be needed - Slicer4Minute is already tested + """ + self.enableScreenshots = enableScreenshotsFlag + self.screenshotScaleFactor = screenshotScaleFactor + + self.delayDisplay("Starting the test") + # + # first, get some data + # + import SampleData + SampleData.downloadFromURL( + fileNames='Head_Scene.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/6785e481925c912a5a3940e9c9b71935df93a78a871e10f66ab71f8478229e68', + checksums='SHA256:6785e481925c912a5a3940e9c9b71935df93a78a871e10f66ab71f8478229e68') + + self.takeScreenshot('Head-Downloaded', 'Finished with download and loading', -1) + + try: + mainWindow = slicer.util.mainWindow() + layoutManager = slicer.app.layoutManager() + threeDView = layoutManager.threeDWidget(0).threeDView() + redWidget = layoutManager.sliceWidget('Red') + redController = redWidget.sliceController() + greenWidget = layoutManager.sliceWidget('Green') + greenController = greenWidget.sliceController() + + mainWindow.moduleSelector().selectModule('Models') + redWidget.sliceController().setSliceVisible(True) + self.takeScreenshot('Head-ModelsAndSliceModel', 'Models and Slice Model', -1) + + slicer.util.clickAndDrag(threeDView) + self.takeScreenshot('Head-Rotate', 'Rotate', -1) + + redController.setSliceVisible(True) + self.takeScreenshot('Head-AxialSlice', 'Display Axial Slice', -1) + + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + self.takeScreenshot('Head-ConventionalView', 'Conventional Layout', -1) + + viewNode = threeDView.mrmlViewNode() + cameras = slicer.util.getNodes('vtkMRMLCameraNode*') + for cameraNode in cameras.values(): + if cameraNode.GetActiveTag() == viewNode.GetID(): + break + cameraNode.GetCamera().Azimuth(90) + cameraNode.GetCamera().Elevation(20) + + # turn off skin and skull + skin = slicer.util.getNode(pattern='Skin.vtk') + skin.GetDisplayNode().SetOpacity(0.5) + self.takeScreenshot('Head-SkinOpacity', 'Skin Opacity to 0.5', -1) + + skin.GetDisplayNode().SetVisibility(0) + self.takeScreenshot('Head-SkinOpacityZero', 'Skin Opacity to 0', -1) + + skull = slicer.util.getNode(pattern='skull_bone.vtk') + + greenWidget.sliceController().setSliceVisible(True) + self.takeScreenshot('Head-GreenSlice', 'Display Coronal Slice', -1) + + # hemispheric_white_matter.GetDisplayNode().SetClipping(1) + skull.GetDisplayNode().SetClipping(1) + clip = slicer.util.getNode(pattern='vtkMRMLClipModelsNode1') + clip.SetRedSliceClipState(0) + clip.SetYellowSliceClipState(0) + clip.SetGreenSliceClipState(2) + self.takeScreenshot('Head-SkullClipping', 'Turn on clipping for skull model', -1) + + for offset in range(-20, 20, 2): + greenController.setSliceOffsetValue(offset) + self.takeScreenshot('Head-ScrollCoronal', 'Scroll through coronal slices', -1) + + skull.GetDisplayNode().SetVisibility(0) + self.takeScreenshot('Head-HideSkull', 'Make the skull invisible', -1) + + for offset in range(-40, -20, 2): + greenController.setSliceOffsetValue(offset) + self.takeScreenshot('Head-ScrollCoronalWhiteMatter', 'Scroll through coronal slices to show white matter', -1) + + hemispheric_white_matter = slicer.util.getNode(pattern='hemispheric_white_matter.vtk') + hemispheric_white_matter.GetDisplayNode().SetVisibility(0) + self.takeScreenshot('Head-HideWhiteMatter', 'Turn off white matter', -1) + + self.delayDisplay('Rotate') + slicer.util.clickAndDrag(threeDView) + + self.delayDisplay('Zoom') + threeDView = layoutManager.threeDWidget(0).threeDView() + slicer.util.clickAndDrag(threeDView, button='Right') + self.takeScreenshot('Head-Zoom', 'Zoom', -1) + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) + + def test_Part3Liver(self, enableScreenshotsFlag=0, screenshotScaleFactor=1): + """ Test using the liver example data + """ + self.enableScreenshots = enableScreenshotsFlag + self.screenshotScaleFactor = screenshotScaleFactor + + self.delayDisplay("Starting the test") + # + # first, get some data + # + import SampleData + SampleData.downloadFromURL( + fileNames='LiverSegments_Scene.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/ff797140c13a5988a7b72920adf0d2dab390a9babeab9161d5c52613328249f7', + checksums='SHA256:ff797140c13a5988a7b72920adf0d2dab390a9babeab9161d5c52613328249f7') + + self.takeScreenshot('Liver-Loaded', 'Loaded Liver scene', -1) + + try: + mainWindow = slicer.util.mainWindow() + layoutManager = slicer.app.layoutManager() + threeDView = layoutManager.threeDWidget(0).threeDView() + redWidget = layoutManager.sliceWidget('Red') + redController = redWidget.sliceController() + viewNode = threeDView.mrmlViewNode() + cameras = slicer.util.getNodes('vtkMRMLCameraNode*') + for cameraNode in cameras.values(): + if cameraNode.GetActiveTag() == viewNode.GetID(): + break + + mainWindow.moduleSelector().selectModule('Models') + self.takeScreenshot('Liver-Models', 'Models module', -1) + + segmentII = slicer.util.getNode('LiverSegment_II') + segmentII.GetDisplayNode().SetVisibility(0) + slicer.util.clickAndDrag(threeDView, start=(10, 200), end=(10, 10)) + self.takeScreenshot('Liver-SegmentII', 'Segment II invisible', -1) + + segmentII.GetDisplayNode().SetVisibility(1) + self.takeScreenshot('Liver-SegmentII', 'Segment II visible', -1) + + cameraNode.GetCamera().Azimuth(0) + cameraNode.GetCamera().Elevation(0) + self.takeScreenshot('Liver-SuperiorView', 'Superior view', -1) + + segmentII.GetDisplayNode().SetVisibility(0) + cameraNode.GetCamera().Azimuth(180) + cameraNode.GetCamera().Elevation(-30) + redWidget.sliceController().setSliceVisible(True) + self.takeScreenshot('Liver-ViewAdrenal', 'View Adrenal', -1) + + models = slicer.util.getNodes('vtkMRMLModelNode*') + for modelNode in models.values(): + modelNode.GetDisplayNode().SetVisibility(0) + + transparentNodes = ('MiddleHepaticVein_and_Branches', 'LiverSegment_IVb', 'LiverSegmentV',) + for nodeName in transparentNodes: + modelNode = slicer.util.getNode(nodeName) + modelNode.GetDisplayNode().SetOpacity(0.5) + modelNode.GetDisplayNode().SetVisibility(1) + cameraNode.GetCamera().Azimuth(30) + cameraNode.GetCamera().Elevation(-20) + redWidget.sliceController().setSliceVisible(True) + self.takeScreenshot('Liver-MiddleHepatic', 'Middle Hepatic', -1) + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) + + def test_Part4Lung(self, enableScreenshotsFlag=0, screenshotScaleFactor=1): + """ Test using the lung data + """ + self.enableScreenshots = enableScreenshotsFlag + self.screenshotScaleFactor = screenshotScaleFactor + + self.delayDisplay("Starting the test") + # + # first, get some data + # + import SampleData + SampleData.downloadFromURL( + fileNames='LungSegments_Scene.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/89ffc6cabd76a17dfa6beb404a5901a4b4e4b4f2f4ee46c2d5f4d34459f554a1', + checksums='SHA256:89ffc6cabd76a17dfa6beb404a5901a4b4e4b4f2f4ee46c2d5f4d34459f554a1') + + self.takeScreenshot('Lung-Loaded', 'Finished with download and loading', -1) + + try: + mainWindow = slicer.util.mainWindow() + layoutManager = slicer.app.layoutManager() + threeDView = layoutManager.threeDWidget(0).threeDView() + redWidget = layoutManager.sliceWidget('Red') + redController = redWidget.sliceController() + viewNode = threeDView.mrmlViewNode() + cameras = slicer.util.getNodes('vtkMRMLCameraNode*') + for cameraNode in cameras.values(): + if cameraNode.GetActiveTag() == viewNode.GetID(): + break + + threeDView.resetFocalPoint() + self.takeScreenshot('Lung-ResetView', 'Reset view', -1) + + mainWindow.moduleSelector().selectModule('Models') + self.takeScreenshot('Lung-Models', 'Models module', -1) + + cameraNode.GetCamera().Azimuth(-100) + cameraNode.GetCamera().Elevation(-40) + redWidget.sliceController().setSliceVisible(True) + lungs = slicer.util.getNode('chestCT_lungs') + lungs.GetDisplayNode().SetVisibility(0) + self.takeScreenshot('Lung-Question1', 'View Question 1', -1) + + cameraNode.GetCamera().Azimuth(-65) + cameraNode.GetCamera().Elevation(-20) + lungs.GetDisplayNode().SetVisibility(1) + lungs.GetDisplayNode().SetOpacity(0.24) + redController.setSliceOffsetValue(-50) + self.takeScreenshot('Lung-Question2', 'View Question 2', -1) + + cameraNode.GetCamera().Azimuth(-165) + cameraNode.GetCamera().Elevation(-10) + redWidget.sliceController().setSliceVisible(False) + self.takeScreenshot('Lung-Question3', 'View Question 3', -1) + + cameraNode.GetCamera().Azimuth(20) + cameraNode.GetCamera().Elevation(-10) + lowerLobeNodes = slicer.util.getNodes('*LowerLobe*') + for showNode in lowerLobeNodes: + self.delayDisplay('Showing Node %s' % showNode, 300) + for node in lowerLobeNodes: + displayNode = lowerLobeNodes[node].GetDisplayNode() + if displayNode: + displayNode.SetVisibility(1 if node == showNode else 0) + self.takeScreenshot('Lung-Question4', 'View Question 4', -1) + + self.delayDisplay('Test passed!') + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e)) diff --git a/Applications/SlicerApp/Testing/Python/ScenePerformance.py b/Applications/SlicerApp/Testing/Python/ScenePerformance.py index fbf3baa3359..0302100a835 100644 --- a/Applications/SlicerApp/Testing/Python/ScenePerformance.py +++ b/Applications/SlicerApp/Testing/Python/ScenePerformance.py @@ -12,16 +12,16 @@ # class ScenePerformance(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "Scene Performance" - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Julien Finet (Kitware)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "Scene Performance" + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Julien Finet (Kitware)"] + parent.helpText = """ This module was developed as a self test to perform the performance tests """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Julien Finet, Kitware, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. """ @@ -31,85 +31,85 @@ def __init__(self, parent): # class ScenePerformanceWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - moduleName = 'ScenePerformance' - scriptedModulesPath = os.path.dirname(slicer.util.modulePath(moduleName)) - path = os.path.join(scriptedModulesPath, 'Resources', 'UI', 'ScenePerformance.ui') - widget = slicer.util.loadUI(path) - self.layout = self.parent.layout() - self.layout.addWidget(widget) - - self.runTestsButton = qt.QPushButton("Run tests") - self.runTestsButton.toolTip = "Run all the tests." - self.runTestsButton.name = "Run tests" - self.layout.addWidget(self.runTestsButton) - self.runTestsButton.connect('clicked()', self.runTests) - - self.TimePushButton = self.findWidget(self.parent, 'TimePushButton') - self.ActionComboBox = self.findWidget(self.parent, 'ActionComboBox') - self.ActionPathLineEdit = self.findWidget(self.parent, 'ActionPathLineEdit') - self.ResultsTextEdit = self.findWidget(self.parent, 'ResultsTextEdit') - self.URLLineEdit = self.findWidget(self.parent, 'URLLineEdit') - self.URLFileNameLineEdit = self.findWidget(self.parent, 'URLFileNameLineEdit') - self.SceneViewSpinBox = self.findWidget(self.parent, 'SceneViewSpinBox') - self.LayoutSpinBox = self.findWidget(self.parent, 'LayoutSpinBox') - self.MRMLNodeComboBox = self.findWidget(self.parent, 'MRMLNodeComboBox') - self.RepeatSpinBox = self.findWidget(self.parent, 'RepeatSpinBox') - - widget.setMRMLScene(slicer.mrmlScene) - #self.MRMLNodeComboBox.setMRMLScene(slicer.mrmlScene) - - self.TimePushButton.connect('clicked()', self.timeAction) - self.ActionComboBox.connect('currentIndexChanged(int)', self.updateActionProperties) - self.updateActionProperties() - - def runTests(self): - tester = ScenePerformanceTest() - tester.testAll() - - def timeAction(self): - tester = ScenePerformanceTest() - tester.setUp() - tester.setRepeat(self.RepeatSpinBox.value) - if self.ActionComboBox.currentIndex == 0: # Add Data - if (self.URLLineEdit.text == ''): - file = self.ActionPathLineEdit.currentPath - else: - logic = ScenePerformanceLogic() - file = logic.downloadFile(self.URLLineEdit.text, self.URLFileNameLineEdit.text) - results = tester.addData(file) - self.ResultsTextEdit.append(results) - elif self.ActionComboBox.currentIndex == 1: # Restore - results = tester.restoreSceneView(self.SceneViewSpinBox.value) - self.ResultsTextEdit.append(results) - elif self.ActionComboBox.currentIndex == 3: # Layout - results = tester.setLayout(self.LayoutSpinBox.value) - self.ResultsTextEdit.append(results) - elif self.ActionComboBox.currentIndex == 2: # Close - results = tester.closeScene() - self.ResultsTextEdit.append(results) - elif self.ActionComboBox.currentIndex == 4: # Add Node - node = self.MRMLNodeComboBox.currentNode() - results = tester.addNode(node) - self.ResultsTextEdit.append(results) - elif self.ActionComboBox.currentIndex == 5: # Modify Node - node = self.MRMLNodeComboBox.currentNode() - results = tester.modifyNode(node) - self.ResultsTextEdit.append(results) - - def updateActionProperties(self): - enableAddData = True if self.ActionComboBox.currentIndex == 0 else False - self.ActionPathLineEdit.setEnabled(enableAddData) - self.URLLineEdit.setEnabled(enableAddData) - self.URLFileNameLineEdit.setEnabled(enableAddData) - self.SceneViewSpinBox.setEnabled(True if self.ActionComboBox.currentIndex == 1 else False) - self.LayoutSpinBox.setEnabled(True if self.ActionComboBox.currentIndex == 3 else False) - self.MRMLNodeComboBox.setEnabled(True if self.ActionComboBox.currentIndex == 4 or self.ActionComboBox.currentIndex == 5 else False) - - def findWidget(self, widget, objectName): - return slicer.util.findChildren(widget, objectName)[0] + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + moduleName = 'ScenePerformance' + scriptedModulesPath = os.path.dirname(slicer.util.modulePath(moduleName)) + path = os.path.join(scriptedModulesPath, 'Resources', 'UI', 'ScenePerformance.ui') + widget = slicer.util.loadUI(path) + self.layout = self.parent.layout() + self.layout.addWidget(widget) + + self.runTestsButton = qt.QPushButton("Run tests") + self.runTestsButton.toolTip = "Run all the tests." + self.runTestsButton.name = "Run tests" + self.layout.addWidget(self.runTestsButton) + self.runTestsButton.connect('clicked()', self.runTests) + + self.TimePushButton = self.findWidget(self.parent, 'TimePushButton') + self.ActionComboBox = self.findWidget(self.parent, 'ActionComboBox') + self.ActionPathLineEdit = self.findWidget(self.parent, 'ActionPathLineEdit') + self.ResultsTextEdit = self.findWidget(self.parent, 'ResultsTextEdit') + self.URLLineEdit = self.findWidget(self.parent, 'URLLineEdit') + self.URLFileNameLineEdit = self.findWidget(self.parent, 'URLFileNameLineEdit') + self.SceneViewSpinBox = self.findWidget(self.parent, 'SceneViewSpinBox') + self.LayoutSpinBox = self.findWidget(self.parent, 'LayoutSpinBox') + self.MRMLNodeComboBox = self.findWidget(self.parent, 'MRMLNodeComboBox') + self.RepeatSpinBox = self.findWidget(self.parent, 'RepeatSpinBox') + + widget.setMRMLScene(slicer.mrmlScene) + # self.MRMLNodeComboBox.setMRMLScene(slicer.mrmlScene) + + self.TimePushButton.connect('clicked()', self.timeAction) + self.ActionComboBox.connect('currentIndexChanged(int)', self.updateActionProperties) + self.updateActionProperties() + + def runTests(self): + tester = ScenePerformanceTest() + tester.testAll() + + def timeAction(self): + tester = ScenePerformanceTest() + tester.setUp() + tester.setRepeat(self.RepeatSpinBox.value) + if self.ActionComboBox.currentIndex == 0: # Add Data + if (self.URLLineEdit.text == ''): + file = self.ActionPathLineEdit.currentPath + else: + logic = ScenePerformanceLogic() + file = logic.downloadFile(self.URLLineEdit.text, self.URLFileNameLineEdit.text) + results = tester.addData(file) + self.ResultsTextEdit.append(results) + elif self.ActionComboBox.currentIndex == 1: # Restore + results = tester.restoreSceneView(self.SceneViewSpinBox.value) + self.ResultsTextEdit.append(results) + elif self.ActionComboBox.currentIndex == 3: # Layout + results = tester.setLayout(self.LayoutSpinBox.value) + self.ResultsTextEdit.append(results) + elif self.ActionComboBox.currentIndex == 2: # Close + results = tester.closeScene() + self.ResultsTextEdit.append(results) + elif self.ActionComboBox.currentIndex == 4: # Add Node + node = self.MRMLNodeComboBox.currentNode() + results = tester.addNode(node) + self.ResultsTextEdit.append(results) + elif self.ActionComboBox.currentIndex == 5: # Modify Node + node = self.MRMLNodeComboBox.currentNode() + results = tester.modifyNode(node) + self.ResultsTextEdit.append(results) + + def updateActionProperties(self): + enableAddData = True if self.ActionComboBox.currentIndex == 0 else False + self.ActionPathLineEdit.setEnabled(enableAddData) + self.URLLineEdit.setEnabled(enableAddData) + self.URLFileNameLineEdit.setEnabled(enableAddData) + self.SceneViewSpinBox.setEnabled(True if self.ActionComboBox.currentIndex == 1 else False) + self.LayoutSpinBox.setEnabled(True if self.ActionComboBox.currentIndex == 3 else False) + self.MRMLNodeComboBox.setEnabled(True if self.ActionComboBox.currentIndex == 4 or self.ActionComboBox.currentIndex == 5 else False) + + def findWidget(self, widget, objectName): + return slicer.util.findChildren(widget, objectName)[0] # @@ -117,168 +117,168 @@ def findWidget(self, widget, objectName): # class ScenePerformanceLogic(ScriptedLoadableModuleLogic): - def downloadFile(self, downloadURL, downloadFileName, downloadFileChecksum=None): - import SampleData - return SampleData.downloadFromURL( - fileNames=downloadFileName, - uris=downloadURL, - checksums=downloadFileChecksum)[0] + def downloadFile(self, downloadURL, downloadFileName, downloadFileChecksum=None): + import SampleData + return SampleData.downloadFromURL( + fileNames=downloadFileName, + uris=downloadURL, + checksums=downloadFileChecksum)[0] - def startTiming(self): - self.Timer = qt.QTime() - self.Timer.start() + def startTiming(self): + self.Timer = qt.QTime() + self.Timer.start() - def stopTiming(self): - return self.Timer.elapsed() + def stopTiming(self): + return self.Timer.elapsed() class ScenePerformanceTest(ScriptedLoadableModuleTest): - def setUp(self): - self.Repeat = 1 - self.delayDisplay("Setup") - #layoutManager = slicer.app.layoutManager() - #layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - #slicer.mrmlScene.Clear(0) - - def setRepeat(self, repeat): - self.Repeat = repeat - - def runTest(self): - self.testAll() - - def testAll(self): - self.setUp() - - self.addURLData(TESTING_DATA_URL + 'SHA256/688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1', - 'BrainAtlas2012.mrb', 'SHA256:688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1') - self.modifyNodeByID('vtkMRMLScalarVolumeNode1') - self.modifyNodeByID('vtkMRMLScalarVolumeNode2') - self.modifyNodeByID('vtkMRMLScalarVolumeNode3') - self.modifyNodeByID('vtkMRMLScalarVolumeDisplayNode2') - #self.modifyNodeByID('vtkMRMLModelHierarchyNode2') - self.modifyNodeByID('vtkMRMLModelNode4') - self.modifyNodeByID('vtkMRMLModelDisplayNode5') - #self.modifyNodeByID('vtkMRMLModelHierarchyNode3') - self.modifyNodeByID('vtkMRMLModelStorageNode1') - self.addNodeByID('vtkMRMLModelNode302') - self.setLayout(3) - self.setLayout(2) - self.setLayout(4) - self.setLayout(5) - self.setLayout(6) - self.setLayout(7) - self.setLayout(8) - self.restoreSceneView(0) - self.restoreSceneView(0) - self.closeScene() - - def reportPerformance(self, action, property, time): - message = self.displayPerformance(action, property, time) - print ( f'{time}') - return message - - def displayPerformance(self, action, property, time): - message = f'{action} ({property}) took {time} msecs ' - self.delayDisplay(message) - return message - - def addURLData(self, url, file, checksum): - logic = ScenePerformanceLogic() - file = logic.downloadFile(url, file, checksum) - self.addData(file) - - def addData(self, file): - self.delayDisplay("Starting the AddData test") - logic = ScenePerformanceLogic() - averageTime = 0 - for x in range(self.Repeat): - logic.startTiming() - ioManager = slicer.app.ioManager() - ioManager.loadFile(file) - time = logic.stopTiming() - self.displayPerformance('AddData', file, time) - averageTime = averageTime + time - averageTime = averageTime / self.Repeat - return self.reportPerformance('AddData', os.path.basename(file), averageTime) - - def closeScene(self): - self.delayDisplay("Starting the Close Scene test") - logic = ScenePerformanceLogic() - averageTime = 0 - for x in range(self.Repeat): - logic.startTiming() - slicer.mrmlScene.Clear(0) - time = logic.stopTiming() - self.displayPerformance('CloseScene', '', time) - averageTime = averageTime + time - averageTime = averageTime / self.Repeat - return self.reportPerformance('CloseScene', '', averageTime) - - def restoreSceneView(self, sceneViewIndex): - node = slicer.mrmlScene.GetNthNodeByClass(sceneViewIndex, 'vtkMRMLSceneViewNode') - return self.restoreSceneViewNode(node) - - def restoreSceneViewNode(self, node): - self.delayDisplay("Starting the Restore Scene test") - logic = ScenePerformanceLogic() - averageTime = 0 - for x in range(self.Repeat): - logic.startTiming() - node.RestoreScene() - time = logic.stopTiming() - self.displayPerformance('RestoreSceneView', node.GetID(), time) - averageTime = averageTime + time - averageTime = averageTime / self.Repeat - return self.reportPerformance('RestoreSceneView', node.GetID(), averageTime) - - def setLayout(self, layoutIndex): - self.delayDisplay("Starting the layout test") - logic = ScenePerformanceLogic() - averageTime = 0 - for x in range(self.Repeat): - logic.startTiming() - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(layoutIndex) - time = logic.stopTiming() - self.displayPerformance('Layout', layoutIndex, time) - averageTime = averageTime + time - averageTime = averageTime / self.Repeat - return self.reportPerformance('Layout', layoutIndex, averageTime) - - def addNodeByID(self, nodeID): - node = slicer.mrmlScene.GetNodeByID(nodeID) - return self.addNode(node) - - def addNode(self, node): - self.delayDisplay("Starting the add node test") - logic = ScenePerformanceLogic() - averageTime = 0 - for x in range(self.Repeat): - newNode = node.CreateNodeInstance() - newNode.UnRegister(node) - newNode.Copy(node) - logic.startTiming() - slicer.mrmlScene.AddNode(newNode) - time = logic.stopTiming() - self.displayPerformance('AddNode', node.GetID(), time) - averageTime = averageTime + time - averageTime = averageTime / self.Repeat - return self.reportPerformance('AddNode', node.GetID(), averageTime) - - def modifyNodeByID(self, nodeID): - node = slicer.mrmlScene.GetNodeByID(nodeID) - return self.modifyNode(node) - - def modifyNode(self, node): - self.delayDisplay("Starting the modify node test") - logic = ScenePerformanceLogic() - averageTime = 0 - for x in range(self.Repeat): - logic.startTiming() - node.Modified() - time = logic.stopTiming() - self.displayPerformance('ModifyNode', node.GetID(), time) - averageTime = averageTime + time - averageTime = averageTime / self.Repeat - return self.reportPerformance('ModifyNode', node.GetID(), averageTime) + def setUp(self): + self.Repeat = 1 + self.delayDisplay("Setup") + # layoutManager = slicer.app.layoutManager() + # layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + # slicer.mrmlScene.Clear(0) + + def setRepeat(self, repeat): + self.Repeat = repeat + + def runTest(self): + self.testAll() + + def testAll(self): + self.setUp() + + self.addURLData(TESTING_DATA_URL + 'SHA256/688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1', + 'BrainAtlas2012.mrb', 'SHA256:688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1') + self.modifyNodeByID('vtkMRMLScalarVolumeNode1') + self.modifyNodeByID('vtkMRMLScalarVolumeNode2') + self.modifyNodeByID('vtkMRMLScalarVolumeNode3') + self.modifyNodeByID('vtkMRMLScalarVolumeDisplayNode2') + # self.modifyNodeByID('vtkMRMLModelHierarchyNode2') + self.modifyNodeByID('vtkMRMLModelNode4') + self.modifyNodeByID('vtkMRMLModelDisplayNode5') + # self.modifyNodeByID('vtkMRMLModelHierarchyNode3') + self.modifyNodeByID('vtkMRMLModelStorageNode1') + self.addNodeByID('vtkMRMLModelNode302') + self.setLayout(3) + self.setLayout(2) + self.setLayout(4) + self.setLayout(5) + self.setLayout(6) + self.setLayout(7) + self.setLayout(8) + self.restoreSceneView(0) + self.restoreSceneView(0) + self.closeScene() + + def reportPerformance(self, action, property, time): + message = self.displayPerformance(action, property, time) + print(f'{time}') + return message + + def displayPerformance(self, action, property, time): + message = f'{action} ({property}) took {time} msecs ' + self.delayDisplay(message) + return message + + def addURLData(self, url, file, checksum): + logic = ScenePerformanceLogic() + file = logic.downloadFile(url, file, checksum) + self.addData(file) + + def addData(self, file): + self.delayDisplay("Starting the AddData test") + logic = ScenePerformanceLogic() + averageTime = 0 + for x in range(self.Repeat): + logic.startTiming() + ioManager = slicer.app.ioManager() + ioManager.loadFile(file) + time = logic.stopTiming() + self.displayPerformance('AddData', file, time) + averageTime = averageTime + time + averageTime = averageTime / self.Repeat + return self.reportPerformance('AddData', os.path.basename(file), averageTime) + + def closeScene(self): + self.delayDisplay("Starting the Close Scene test") + logic = ScenePerformanceLogic() + averageTime = 0 + for x in range(self.Repeat): + logic.startTiming() + slicer.mrmlScene.Clear(0) + time = logic.stopTiming() + self.displayPerformance('CloseScene', '', time) + averageTime = averageTime + time + averageTime = averageTime / self.Repeat + return self.reportPerformance('CloseScene', '', averageTime) + + def restoreSceneView(self, sceneViewIndex): + node = slicer.mrmlScene.GetNthNodeByClass(sceneViewIndex, 'vtkMRMLSceneViewNode') + return self.restoreSceneViewNode(node) + + def restoreSceneViewNode(self, node): + self.delayDisplay("Starting the Restore Scene test") + logic = ScenePerformanceLogic() + averageTime = 0 + for x in range(self.Repeat): + logic.startTiming() + node.RestoreScene() + time = logic.stopTiming() + self.displayPerformance('RestoreSceneView', node.GetID(), time) + averageTime = averageTime + time + averageTime = averageTime / self.Repeat + return self.reportPerformance('RestoreSceneView', node.GetID(), averageTime) + + def setLayout(self, layoutIndex): + self.delayDisplay("Starting the layout test") + logic = ScenePerformanceLogic() + averageTime = 0 + for x in range(self.Repeat): + logic.startTiming() + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(layoutIndex) + time = logic.stopTiming() + self.displayPerformance('Layout', layoutIndex, time) + averageTime = averageTime + time + averageTime = averageTime / self.Repeat + return self.reportPerformance('Layout', layoutIndex, averageTime) + + def addNodeByID(self, nodeID): + node = slicer.mrmlScene.GetNodeByID(nodeID) + return self.addNode(node) + + def addNode(self, node): + self.delayDisplay("Starting the add node test") + logic = ScenePerformanceLogic() + averageTime = 0 + for x in range(self.Repeat): + newNode = node.CreateNodeInstance() + newNode.UnRegister(node) + newNode.Copy(node) + logic.startTiming() + slicer.mrmlScene.AddNode(newNode) + time = logic.stopTiming() + self.displayPerformance('AddNode', node.GetID(), time) + averageTime = averageTime + time + averageTime = averageTime / self.Repeat + return self.reportPerformance('AddNode', node.GetID(), averageTime) + + def modifyNodeByID(self, nodeID): + node = slicer.mrmlScene.GetNodeByID(nodeID) + return self.modifyNode(node) + + def modifyNode(self, node): + self.delayDisplay("Starting the modify node test") + logic = ScenePerformanceLogic() + averageTime = 0 + for x in range(self.Repeat): + logic.startTiming() + node.Modified() + time = logic.stopTiming() + self.displayPerformance('ModifyNode', node.GetID(), time) + averageTime = averageTime + time + averageTime = averageTime / self.Repeat + return self.reportPerformance('ModifyNode', node.GetID(), averageTime) diff --git a/Applications/SlicerApp/Testing/Python/ScriptedModuleCleanupTest.py b/Applications/SlicerApp/Testing/Python/ScriptedModuleCleanupTest.py index f63a3015233..30ec25fe196 100644 --- a/Applications/SlicerApp/Testing/Python/ScriptedModuleCleanupTest.py +++ b/Applications/SlicerApp/Testing/Python/ScriptedModuleCleanupTest.py @@ -23,9 +23,9 @@ import tempfile from SlicerAppTesting import ( - EXIT_FAILURE, - EXIT_SUCCESS, - run, + EXIT_FAILURE, + EXIT_SUCCESS, + run, ) """ @@ -41,59 +41,59 @@ def check_exit_code(slicer_executable, testing_enabled=True, debug=False): - """ - If debug is set to True: - * display the path of the expected test output file - * avoid deleting the created temporary directory - """ - - temporaryModuleDirPath = tempfile.mkdtemp().replace('\\','/') - try: - # Copy helper module that creates a file when startup completed event is received - currentDirPath = os.path.dirname(__file__).replace('\\','/') - from shutil import copyfile - copyfile(currentDirPath+'/ScriptedModuleCleanupTestHelperModule.py', - temporaryModuleDirPath+'/ModuleCleanup.py') - - common_arguments = [ - '--no-splash', - '--disable-builtin-modules', - '--additional-module-path', temporaryModuleDirPath, - '--python-code', 'slicer.util.selectModule("ModuleCleanup")' - ] - - test_output_file = temporaryModuleDirPath + "/ModuleCleanupTest.out" - os.environ['SLICER_MODULE_CLEANUP_TEST_OUTPUT'] = test_output_file - if debug: - print("SLICER_MODULE_CLEANUP_TEST_OUTPUT=%s" % test_output_file) - - # Test - args = list(common_arguments) - if testing_enabled: - args.append('--testing') - else: - args.append('--exit-after-startup') - (returnCode, stdout, stderr) = run(slicer_executable, args) - - assert(os.path.isfile(test_output_file)) - - if testing_enabled: - assert(returnCode == EXIT_FAILURE) - else: - assert(returnCode == EXIT_SUCCESS) - - finally: - if not debug: - import shutil - shutil.rmtree(temporaryModuleDirPath) + """ + If debug is set to True: + * display the path of the expected test output file + * avoid deleting the created temporary directory + """ + + temporaryModuleDirPath = tempfile.mkdtemp().replace('\\', '/') + try: + # Copy helper module that creates a file when startup completed event is received + currentDirPath = os.path.dirname(__file__).replace('\\', '/') + from shutil import copyfile + copyfile(currentDirPath + '/ScriptedModuleCleanupTestHelperModule.py', + temporaryModuleDirPath + '/ModuleCleanup.py') + + common_arguments = [ + '--no-splash', + '--disable-builtin-modules', + '--additional-module-path', temporaryModuleDirPath, + '--python-code', 'slicer.util.selectModule("ModuleCleanup")' + ] + + test_output_file = temporaryModuleDirPath + "/ModuleCleanupTest.out" + os.environ['SLICER_MODULE_CLEANUP_TEST_OUTPUT'] = test_output_file + if debug: + print("SLICER_MODULE_CLEANUP_TEST_OUTPUT=%s" % test_output_file) + + # Test + args = list(common_arguments) + if testing_enabled: + args.append('--testing') + else: + args.append('--exit-after-startup') + (returnCode, stdout, stderr) = run(slicer_executable, args) + + assert(os.path.isfile(test_output_file)) + + if testing_enabled: + assert(returnCode == EXIT_FAILURE) + else: + assert(returnCode == EXIT_SUCCESS) + + finally: + if not debug: + import shutil + shutil.rmtree(temporaryModuleDirPath) if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("/path/to/Slicer") - parser.add_argument('--with-testing', dest='testing_enabled', action='store_true') - args = parser.parse_args() + parser = argparse.ArgumentParser() + parser.add_argument("/path/to/Slicer") + parser.add_argument('--with-testing', dest='testing_enabled', action='store_true') + args = parser.parse_args() - slicer_executable = os.path.expanduser(getattr(args, "/path/to/Slicer")) - check_exit_code(slicer_executable, testing_enabled=args.testing_enabled) + slicer_executable = os.path.expanduser(getattr(args, "/path/to/Slicer")) + check_exit_code(slicer_executable, testing_enabled=args.testing_enabled) diff --git a/Applications/SlicerApp/Testing/Python/ScriptedModuleCleanupTestHelperModule.py b/Applications/SlicerApp/Testing/Python/ScriptedModuleCleanupTestHelperModule.py index d29747184bc..f3a7459420f 100644 --- a/Applications/SlicerApp/Testing/Python/ScriptedModuleCleanupTestHelperModule.py +++ b/Applications/SlicerApp/Testing/Python/ScriptedModuleCleanupTestHelperModule.py @@ -4,34 +4,34 @@ class ModuleCleanup(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "Module for cleanup test" - self.parent.categories = ["ModuleCleanup"] # Explicitly add a category to work around issue #4698 - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)",] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "Module for cleanup test" + self.parent.categories = ["ModuleCleanup"] # Explicitly add a category to work around issue #4698 + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)", ] + self.parent.helpText = """ This module allows to test that exception raised during module cleanup sets exit code. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ Developed by Jean-Christophe Fillion-Robin, Kitware Inc., partially funded by NIH grant 1R01EB021391. """ class ModuleCleanupWidget(ScriptedLoadableModuleWidget): - def __init__(self, parent=None): - ScriptedLoadableModuleWidget.__init__(self, parent) + def __init__(self, parent=None): + ScriptedLoadableModuleWidget.__init__(self, parent) - self.testOutputFileName = os.environ['SLICER_MODULE_CLEANUP_TEST_OUTPUT'] - if os.path.isfile(self.testOutputFileName): - os.remove(self.testOutputFileName) + self.testOutputFileName = os.environ['SLICER_MODULE_CLEANUP_TEST_OUTPUT'] + if os.path.isfile(self.testOutputFileName): + os.remove(self.testOutputFileName) - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - print("ModuleCleanupWidget setup") + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + print("ModuleCleanupWidget setup") - def cleanup(self): - with open(self.testOutputFileName, "w") as fd: - fd.write('ModuleCleanup generated this file when application exited') - raise RuntimeError("ModuleCleanupWidget error") + def cleanup(self): + with open(self.testOutputFileName, "w") as fd: + fd.write('ModuleCleanup generated this file when application exited') + raise RuntimeError("ModuleCleanupWidget error") diff --git a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleA.py b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleA.py index 0338dc754ee..b48691f5c06 100644 --- a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleA.py +++ b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleA.py @@ -4,28 +4,28 @@ class ModuleA(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "Module A" - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)",] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "Module A" + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)", ] + self.parent.helpText = """ This module allows to test the scripted module import. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ Developed by Jean-Christophe Fillion-Robin, Kitware Inc., partially funded by NIH grant 3P41RR013218-12S1. """ - def somevar(self): - return SOMEVAR + def somevar(self): + return SOMEVAR class ModuleAWidget(ScriptedLoadableModuleWidget): - def __init__(self, parent=None): - ScriptedLoadableModuleWidget.__init__(self, parent) + def __init__(self, parent=None): + ScriptedLoadableModuleWidget.__init__(self, parent) - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - def cleanup(self): - print("ModuleAWidget finalized") + def cleanup(self): + print("ModuleAWidget finalized") diff --git a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleB.py b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleB.py index 9469aec1b8e..dbc9c976dc2 100644 --- a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleB.py +++ b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleB.py @@ -4,25 +4,25 @@ class ModuleB(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "Module B" - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)",] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "Module B" + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)", ] + self.parent.helpText = """ This module allows to test the scripted module import. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ Developed by Jean-Christophe Fillion-Robin, Kitware Inc., partially funded by NIH grant 3P41RR013218-12S1. """ - def somevar(self): - return SOMEVAR + def somevar(self): + return SOMEVAR class ModuleBWidget(ScriptedLoadableModuleWidget): - def __init__(self, parent=None): - ScriptedLoadableModuleWidget.__init__(self, parent) + def __init__(self, parent=None): + ScriptedLoadableModuleWidget.__init__(self, parent) - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) diff --git a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleC_WithoutWidget.py b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleC_WithoutWidget.py index 5eec42e3ac0..ceba5c782d5 100644 --- a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleC_WithoutWidget.py +++ b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleC_WithoutWidget.py @@ -4,17 +4,17 @@ class ModuleC_WithoutWidget(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "Module A" - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)",] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "Module A" + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)", ] + self.parent.helpText = """ This module allows to test the scripted module import. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ Developed by Jean-Christophe Fillion-Robin, Kitware Inc., partially funded by NIH grant 3P41RR013218-12S1. """ - def somevar(self): - return SOMEVAR + def somevar(self): + return SOMEVAR diff --git a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleD_WithFileDialog_WithoutWidget.py b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleD_WithFileDialog_WithoutWidget.py index 51e08c0082f..6dc627967c5 100644 --- a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleD_WithFileDialog_WithoutWidget.py +++ b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleD_WithFileDialog_WithoutWidget.py @@ -4,35 +4,35 @@ class ModuleD_WithFileDialog_WithoutWidget(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "Module A" - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)",] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "Module A" + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)", ] + self.parent.helpText = """ This module allows to test the scripted module import. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ Developed by Jean-Christophe Fillion-Robin, Kitware Inc., partially funded by NIH grant 3P41RR013218-12S1. """ - def somevar(self): - return SOMEVAR + def somevar(self): + return SOMEVAR class DICOMFileDialog: - def __init__(self,qSlicerFileDialog): - self.qSlicerFileDialog = qSlicerFileDialog - qSlicerFileDialog.fileType = 'Foo Directory' - qSlicerFileDialog.description = 'Do something awesome with Foo' - qSlicerFileDialog.action = slicer.qSlicerFileDialog.Read + def __init__(self, qSlicerFileDialog): + self.qSlicerFileDialog = qSlicerFileDialog + qSlicerFileDialog.fileType = 'Foo Directory' + qSlicerFileDialog.description = 'Do something awesome with Foo' + qSlicerFileDialog.action = slicer.qSlicerFileDialog.Read - def execDialog(self): - pass + def execDialog(self): + pass - def isMimeDataAccepted(self): - self.qSlicerFileDialog.acceptMimeData(True) + def isMimeDataAccepted(self): + self.qSlicerFileDialog.acceptMimeData(True) - def dropEvent(self): - pass + def dropEvent(self): + pass diff --git a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleE_WithFileWriter_WithoutWidget.py b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleE_WithFileWriter_WithoutWidget.py index bc585f62f47..adf15558eaf 100644 --- a/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleE_WithFileWriter_WithoutWidget.py +++ b/Applications/SlicerApp/Testing/Python/ScriptedModuleDiscoveryTest/ModuleE_WithFileWriter_WithoutWidget.py @@ -4,17 +4,17 @@ class ModuleE_WithFileWriter_WithoutWidget(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "Module A" - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)",] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "Module A" + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)", ] + self.parent.helpText = """ This module allows to test the scripted module import. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ Developed by Jean-Christophe Fillion-Robin, Kitware Inc., partially funded by NIH grant 3P41RR013218-12S1. """ - def somevar(self): - return SOMEVAR + def somevar(self): + return SOMEVAR diff --git a/Applications/SlicerApp/Testing/Python/ShaderProperties.py b/Applications/SlicerApp/Testing/Python/ShaderProperties.py index bd29a29efd3..fa3acdfceb9 100644 --- a/Applications/SlicerApp/Testing/Python/ShaderProperties.py +++ b/Applications/SlicerApp/Testing/Python/ShaderProperties.py @@ -13,16 +13,16 @@ # class ShaderProperties(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "Shader Properties" - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Simon Drouin (BWH)", "Steve Pieper (Isomics)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "Shader Properties" + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Simon Drouin (BWH)", "Steve Pieper (Isomics)"] + parent.helpText = """ This module was developed as a self test for shader properties """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed and was partially funded by NIH grant 3P41RR013218-12S1. """ @@ -32,27 +32,27 @@ def __init__(self, parent): # class ShaderPropertiesWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - moduleName = 'ShaderProperties' + moduleName = 'ShaderProperties' - self.sphereTestButton = qt.QPushButton() - self.sphereTestButton.text = "Sphere test" - self.layout.addWidget(self.sphereTestButton) - self.sphereTestButton.connect("clicked()", lambda : ShaderPropertiesTest().testSphereCut()) + self.sphereTestButton = qt.QPushButton() + self.sphereTestButton.text = "Sphere test" + self.layout.addWidget(self.sphereTestButton) + self.sphereTestButton.connect("clicked()", lambda: ShaderPropertiesTest().testSphereCut()) - self.wedgeTestButton = qt.QPushButton() - self.wedgeTestButton.text = "Wedge test" - self.layout.addWidget(self.wedgeTestButton) - self.wedgeTestButton.connect("clicked()", lambda : ShaderPropertiesTest().testWedgeCut()) + self.wedgeTestButton = qt.QPushButton() + self.wedgeTestButton.text = "Wedge test" + self.layout.addWidget(self.wedgeTestButton) + self.wedgeTestButton.connect("clicked()", lambda: ShaderPropertiesTest().testWedgeCut()) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - def runTests(self): - tester = ShaderPropertiesTest() - tester.testAll() + def runTests(self): + tester = ShaderPropertiesTest() + tester.testAll() # @@ -61,97 +61,97 @@ def runTests(self): class ShaderPropertiesTest(ScriptedLoadableModuleTest): - def setUp(self): - self.delayDisplay("Setup") - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - slicer.mrmlScene.Clear(0) - self.delayDisplay("Setup complete") - - def runTest(self): - self.testSphereCut() - self.testWedgeCut() - - def testWedgeCut(self): - - self.delayDisplay("Starting...") - self.setUp() - - fileURL = TESTING_DATA_URL + 'SHA256/19ad4f794de8dcdabbe3290c40fa18072cf5e05b6b2466fcc508ea7a42aae71e' - filePath = os.path.join(slicer.util.tempDirectory(), 'MRRobot-Shoulder-MR.nrrd') - slicer.util.downloadFile(fileURL, filePath) - shoulder = slicer.util.loadVolume(filePath) - - self.delayDisplay("Shoulder downloaded...") - - slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering') - volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation() - volumeRenderingWidgetRep.setMRMLVolumeNode(shoulder) - - volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering') - volumeRenderingNode.SetVisibility(1) - - self.delayDisplay('Volume rendering on') - - methodComboBox = slicer.util.findChildren(name='RenderingMethodComboBox')[0] - methodComboBox.currentIndex = methodComboBox.findText('VTK GPU Ray Casting') - - self.delayDisplay('GPU Ray Casting on') - - endpoints = [ [-162.94, 2.32192, -30.1792], [-144.842, 96.867, -36.8726] ] - markupNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLMarkupsLineNode()) - for endpoint in endpoints: - markupNode.AddControlPoint(vtk.vtkVector3d(endpoint)) - - self.delayDisplay('Line added') - - #------------------------------------------------------ - # Utility functions to get the position of the first - # markup line in the scene and the shader property - # node - #------------------------------------------------------ - def GetLineEndpoints(): - fn = slicer.util.getNode('vtkMRMLMarkupsLineNode1') - endpoints = [] - for n in range(2): - endpoints.append([0,]*3) - fn.GetNthControlPointPosition(n,endpoints[n]) - return endpoints - - def GetShaderPropertyNode(): - return slicer.util.getNode('vtkMRMLShaderPropertyNode1') - - #------------------------------------------------------ - # Get the shader property node which contains every custom - # shader modifications for every mapper associated with - # the first volume rendering display node - #------------------------------------------------------ - displayNode = slicer.util.getNodesByClass('vtkMRMLGPURayCastVolumeRenderingDisplayNode')[0] - shaderPropNode = displayNode.GetOrCreateShaderPropertyNode(slicer.mrmlScene) - shaderProp = shaderPropNode.GetShaderProperty() - - # turn off shading so carved region looks reasonable - volumePropertyNode = displayNode.GetVolumePropertyNode() - volumeProperty = volumePropertyNode.GetVolumeProperty() - volumeProperty.ShadeOff() - - #------------------------------------------------------ - # Declare and initialize custom uniform variables - # used in our shader replacement - #------------------------------------------------------ - shaderUniforms = shaderPropNode.GetFragmentUniforms() - shaderUniforms.RemoveAllUniforms() - endpoints = GetLineEndpoints() - shaderUniforms.SetUniform3f("endpoint0",endpoints[0]) - shaderUniforms.SetUniform3f("endpoint1",endpoints[1]) - shaderUniforms.SetUniformf("coneCutoff",0.8) - - #------------------------------------------------------ - # Replace the cropping implementation part of the - # raycasting shader to skip everything in the sphere - # defined by endpoints and radius - #------------------------------------------------------ - croppingImplShaderCode = """ + def setUp(self): + self.delayDisplay("Setup") + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + slicer.mrmlScene.Clear(0) + self.delayDisplay("Setup complete") + + def runTest(self): + self.testSphereCut() + self.testWedgeCut() + + def testWedgeCut(self): + + self.delayDisplay("Starting...") + self.setUp() + + fileURL = TESTING_DATA_URL + 'SHA256/19ad4f794de8dcdabbe3290c40fa18072cf5e05b6b2466fcc508ea7a42aae71e' + filePath = os.path.join(slicer.util.tempDirectory(), 'MRRobot-Shoulder-MR.nrrd') + slicer.util.downloadFile(fileURL, filePath) + shoulder = slicer.util.loadVolume(filePath) + + self.delayDisplay("Shoulder downloaded...") + + slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering') + volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation() + volumeRenderingWidgetRep.setMRMLVolumeNode(shoulder) + + volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering') + volumeRenderingNode.SetVisibility(1) + + self.delayDisplay('Volume rendering on') + + methodComboBox = slicer.util.findChildren(name='RenderingMethodComboBox')[0] + methodComboBox.currentIndex = methodComboBox.findText('VTK GPU Ray Casting') + + self.delayDisplay('GPU Ray Casting on') + + endpoints = [[-162.94, 2.32192, -30.1792], [-144.842, 96.867, -36.8726]] + markupNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLMarkupsLineNode()) + for endpoint in endpoints: + markupNode.AddControlPoint(vtk.vtkVector3d(endpoint)) + + self.delayDisplay('Line added') + + # ------------------------------------------------------ + # Utility functions to get the position of the first + # markup line in the scene and the shader property + # node + # ------------------------------------------------------ + def GetLineEndpoints(): + fn = slicer.util.getNode('vtkMRMLMarkupsLineNode1') + endpoints = [] + for n in range(2): + endpoints.append([0, ] * 3) + fn.GetNthControlPointPosition(n, endpoints[n]) + return endpoints + + def GetShaderPropertyNode(): + return slicer.util.getNode('vtkMRMLShaderPropertyNode1') + + # ------------------------------------------------------ + # Get the shader property node which contains every custom + # shader modifications for every mapper associated with + # the first volume rendering display node + # ------------------------------------------------------ + displayNode = slicer.util.getNodesByClass('vtkMRMLGPURayCastVolumeRenderingDisplayNode')[0] + shaderPropNode = displayNode.GetOrCreateShaderPropertyNode(slicer.mrmlScene) + shaderProp = shaderPropNode.GetShaderProperty() + + # turn off shading so carved region looks reasonable + volumePropertyNode = displayNode.GetVolumePropertyNode() + volumeProperty = volumePropertyNode.GetVolumeProperty() + volumeProperty.ShadeOff() + + # ------------------------------------------------------ + # Declare and initialize custom uniform variables + # used in our shader replacement + # ------------------------------------------------------ + shaderUniforms = shaderPropNode.GetFragmentUniforms() + shaderUniforms.RemoveAllUniforms() + endpoints = GetLineEndpoints() + shaderUniforms.SetUniform3f("endpoint0", endpoints[0]) + shaderUniforms.SetUniform3f("endpoint1", endpoints[1]) + shaderUniforms.SetUniformf("coneCutoff", 0.8) + + # ------------------------------------------------------ + # Replace the cropping implementation part of the + # raycasting shader to skip everything in the sphere + # defined by endpoints and radius + # ------------------------------------------------------ + croppingImplShaderCode = """ vec4 texCoordRAS = in_volumeMatrix[0] * in_textureDatasetMatrix[0] * vec4(g_dataPos, 1.); vec3 samplePoint = texCoordRAS.xyz; vec3 toSample = normalize(samplePoint - endpoint0); @@ -159,113 +159,113 @@ def GetShaderPropertyNode(): float onLine = dot(toEnd, toSample); g_skip = (onLine > coneCutoff); """ - shaderProp.ClearAllFragmentShaderReplacements() - shaderProp.AddFragmentShaderReplacement("//VTK::Cropping::Impl", True, croppingImplShaderCode, False) - - #------------------------------------------------------ - # Add a callback when the line moves to adjust - # the endpoints of the carving sphere accordingly - #------------------------------------------------------ - def onControlPointMoved(): - endpoints = GetLineEndpoints() - propNode = GetShaderPropertyNode() - propNode.GetFragmentUniforms().SetUniform3f("endpoint0",endpoints[0]) - propNode.GetFragmentUniforms().SetUniform3f("endpoint1",endpoints[1]) - - fn = slicer.util.getNode('vtkMRMLMarkupsLineNode1') - fn.AddObserver(fn.PointModifiedEvent, lambda caller,event: onControlPointMoved()) + shaderProp.ClearAllFragmentShaderReplacements() + shaderProp.AddFragmentShaderReplacement("//VTK::Cropping::Impl", True, croppingImplShaderCode, False) + + # ------------------------------------------------------ + # Add a callback when the line moves to adjust + # the endpoints of the carving sphere accordingly + # ------------------------------------------------------ + def onControlPointMoved(): + endpoints = GetLineEndpoints() + propNode = GetShaderPropertyNode() + propNode.GetFragmentUniforms().SetUniform3f("endpoint0", endpoints[0]) + propNode.GetFragmentUniforms().SetUniform3f("endpoint1", endpoints[1]) - self.delayDisplay("Should be a carved out shoulder now") - - def testSphereCut(self): - - self.delayDisplay("Starting...") - self.setUp() - - import SampleData - mrHead = SampleData.downloadSample('MRHead') - - self.delayDisplay("Head downloaded...") - - slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering') - volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation() - volumeRenderingWidgetRep.setMRMLVolumeNode(mrHead) - - volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering') - volumeRenderingNode.SetVisibility(1) - - self.delayDisplay('Volume rendering on') - - methodComboBox = slicer.util.findChildren(name='RenderingMethodComboBox')[0] - methodComboBox.currentIndex = methodComboBox.findText('VTK GPU Ray Casting') - - self.delayDisplay('GPU Ray Casting on') - - markupNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") - markupNode.AddControlPoint([0.0, 100.0, 0.0]) - - self.delayDisplay('Point list added') - - #------------------------------------------------------ - # Utility functions to get the position of the first - # markups point list in the scene and the shader property - # node - #------------------------------------------------------ - def GetPointPosition(): - fn = slicer.util.getNode('vtkMRMLMarkupsFiducialNode1') - p = [0.0, 0.0, 0.0] - fn.GetNthControlPointPosition(0, p) - return p - - def GetShaderPropertyNode(): - return slicer.util.getNode('vtkMRMLShaderPropertyNode1') - - #------------------------------------------------------ - # Get the shader property node which contains every custom - # shader modifications for every mapper associated with - # the first volume rendering display node - #------------------------------------------------------ - displayNode = slicer.util.getNodesByClass('vtkMRMLGPURayCastVolumeRenderingDisplayNode')[0] - shaderPropNode = displayNode.GetOrCreateShaderPropertyNode(slicer.mrmlScene) - shaderProp = shaderPropNode.GetShaderProperty() - - # turn off shading so carved region looks reasonable - volumePropertyNode = displayNode.GetVolumePropertyNode() - volumeProperty = volumePropertyNode.GetVolumeProperty() - volumeProperty.ShadeOff() - - #------------------------------------------------------ - # Declare and initialize custom uniform variables - # used in our shader replacement - #------------------------------------------------------ - shaderUniforms = shaderPropNode.GetFragmentUniforms() - shaderUniforms.RemoveAllUniforms() - pointPos = GetPointPosition() - shaderUniforms.SetUniform3f("center",pointPos) - shaderUniforms.SetUniformf("radius",50.) - - #------------------------------------------------------ - # Replace the cropping implementation part of the - # raycasting shader to skip everything in the sphere - # defined by center and radius - #------------------------------------------------------ - croppingImplShaderCode = """ + fn = slicer.util.getNode('vtkMRMLMarkupsLineNode1') + fn.AddObserver(fn.PointModifiedEvent, lambda caller, event: onControlPointMoved()) + + self.delayDisplay("Should be a carved out shoulder now") + + def testSphereCut(self): + + self.delayDisplay("Starting...") + self.setUp() + + import SampleData + mrHead = SampleData.downloadSample('MRHead') + + self.delayDisplay("Head downloaded...") + + slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering') + volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation() + volumeRenderingWidgetRep.setMRMLVolumeNode(mrHead) + + volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering') + volumeRenderingNode.SetVisibility(1) + + self.delayDisplay('Volume rendering on') + + methodComboBox = slicer.util.findChildren(name='RenderingMethodComboBox')[0] + methodComboBox.currentIndex = methodComboBox.findText('VTK GPU Ray Casting') + + self.delayDisplay('GPU Ray Casting on') + + markupNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") + markupNode.AddControlPoint([0.0, 100.0, 0.0]) + + self.delayDisplay('Point list added') + + # ------------------------------------------------------ + # Utility functions to get the position of the first + # markups point list in the scene and the shader property + # node + # ------------------------------------------------------ + def GetPointPosition(): + fn = slicer.util.getNode('vtkMRMLMarkupsFiducialNode1') + p = [0.0, 0.0, 0.0] + fn.GetNthControlPointPosition(0, p) + return p + + def GetShaderPropertyNode(): + return slicer.util.getNode('vtkMRMLShaderPropertyNode1') + + # ------------------------------------------------------ + # Get the shader property node which contains every custom + # shader modifications for every mapper associated with + # the first volume rendering display node + # ------------------------------------------------------ + displayNode = slicer.util.getNodesByClass('vtkMRMLGPURayCastVolumeRenderingDisplayNode')[0] + shaderPropNode = displayNode.GetOrCreateShaderPropertyNode(slicer.mrmlScene) + shaderProp = shaderPropNode.GetShaderProperty() + + # turn off shading so carved region looks reasonable + volumePropertyNode = displayNode.GetVolumePropertyNode() + volumeProperty = volumePropertyNode.GetVolumeProperty() + volumeProperty.ShadeOff() + + # ------------------------------------------------------ + # Declare and initialize custom uniform variables + # used in our shader replacement + # ------------------------------------------------------ + shaderUniforms = shaderPropNode.GetFragmentUniforms() + shaderUniforms.RemoveAllUniforms() + pointPos = GetPointPosition() + shaderUniforms.SetUniform3f("center", pointPos) + shaderUniforms.SetUniformf("radius", 50.) + + # ------------------------------------------------------ + # Replace the cropping implementation part of the + # raycasting shader to skip everything in the sphere + # defined by center and radius + # ------------------------------------------------------ + croppingImplShaderCode = """ vec4 texCoordRAS = in_volumeMatrix[0] * in_textureDatasetMatrix[0] * vec4(g_dataPos, 1.); g_skip = length(texCoordRAS.xyz - center) < radius; """ - shaderProp.ClearAllFragmentShaderReplacements() - shaderProp.AddFragmentShaderReplacement("//VTK::Cropping::Impl", True, croppingImplShaderCode, False) - - #------------------------------------------------------ - # Add a callback when the point moves to adjust - # the center of the carving sphere accordingly - #------------------------------------------------------ - def onPointMoved(): - p = GetPointPosition() - propNode = GetShaderPropertyNode() - propNode.GetFragmentUniforms().SetUniform3f("center",p) - - fn = slicer.util.getNode('vtkMRMLMarkupsFiducialNode1') - fn.AddObserver(fn.PointModifiedEvent, lambda caller,event: onPointMoved()) - - self.delayDisplay("Should be a carved out nose now") + shaderProp.ClearAllFragmentShaderReplacements() + shaderProp.AddFragmentShaderReplacement("//VTK::Cropping::Impl", True, croppingImplShaderCode, False) + + # ------------------------------------------------------ + # Add a callback when the point moves to adjust + # the center of the carving sphere accordingly + # ------------------------------------------------------ + def onPointMoved(): + p = GetPointPosition() + propNode = GetShaderPropertyNode() + propNode.GetFragmentUniforms().SetUniform3f("center", p) + + fn = slicer.util.getNode('vtkMRMLMarkupsFiducialNode1') + fn.AddObserver(fn.PointModifiedEvent, lambda caller, event: onPointMoved()) + + self.delayDisplay("Should be a carved out nose now") diff --git a/Applications/SlicerApp/Testing/Python/SliceLinkLogic.py b/Applications/SlicerApp/Testing/Python/SliceLinkLogic.py index c703aaebcf0..43ee6da4ef3 100644 --- a/Applications/SlicerApp/Testing/Python/SliceLinkLogic.py +++ b/Applications/SlicerApp/Testing/Python/SliceLinkLogic.py @@ -11,22 +11,22 @@ # class SliceLinkLogic(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) - parent.title = "SliceLinkLogic" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Jim Miller (GE)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + """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) + parent.title = "SliceLinkLogic" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Jim Miller (GE)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ This module tests the Slice link logic """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Jim Miller, GE and was partially funded by NIH grant U54EB005149. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -34,36 +34,36 @@ def __init__(self, parent): # class SliceLinkLogicWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... - # Collapsible button - dummyCollapsibleButton = ctk.ctkCollapsibleButton() - dummyCollapsibleButton.text = "A collapsible button" - self.layout.addWidget(dummyCollapsibleButton) + # Collapsible button + dummyCollapsibleButton = ctk.ctkCollapsibleButton() + dummyCollapsibleButton.text = "A collapsible button" + self.layout.addWidget(dummyCollapsibleButton) - # Layout within the dummy collapsible button - dummyFormLayout = qt.QFormLayout(dummyCollapsibleButton) + # Layout within the dummy collapsible button + dummyFormLayout = qt.QFormLayout(dummyCollapsibleButton) - # HelloWorld button - helloWorldButton = qt.QPushButton("Hello world") - helloWorldButton.toolTip = "Print 'Hello world' in standard output." - dummyFormLayout.addWidget(helloWorldButton) - helloWorldButton.connect('clicked(bool)', self.onHelloWorldButtonClicked) + # HelloWorld button + helloWorldButton = qt.QPushButton("Hello world") + helloWorldButton.toolTip = "Print 'Hello world' in standard output." + dummyFormLayout.addWidget(helloWorldButton) + helloWorldButton.connect('clicked(bool)', self.onHelloWorldButtonClicked) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - # Set local var as instance attribute - self.helloWorldButton = helloWorldButton + # Set local var as instance attribute + self.helloWorldButton = helloWorldButton - def onHelloWorldButtonClicked(self): - print("Hello World !") + def onHelloWorldButtonClicked(self): + print("Hello World !") # @@ -71,213 +71,213 @@ def onHelloWorldButtonClicked(self): # class SliceLinkLogicLogic(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 a dummy logic method that - returns true if the passed in volume - node has valid image data + """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 """ - if not volumeNode: - print('no volume node') - return False - if volumeNode.GetImageData() is None: - print('no image data') - return False - return True + def hasImageData(self, volumeNode): + """This is a dummy logic method that + returns true if the passed in volume + node has valid image data + """ + if not volumeNode: + print('no volume node') + return False + if volumeNode.GetImageData() is None: + print('no image data') + return False + return True -class SliceLinkLogicTest(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. +class SliceLinkLogicTest(ScriptedLoadableModuleTest): """ - self.setUp() - self.test_SliceLinkLogic1() - - def test_SliceLinkLogic1(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. + 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 """ - self.delayDisplay("Starting the test") - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL( - nodeNames='FA', - fileNames='FA.nrrd', - uris=TESTING_DATA_URL + 'SHA256/12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560', - checksums='SHA256:12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560') - self.delayDisplay('Finished with download and loading') - print('') - - volumeNode = slicer.util.getNode(pattern="FA") - logic = SliceLinkLogicLogic() - self.assertIsNotNone( logic.hasImageData(volumeNode) ) - - eps = 0.02 # on high-DPI screens FOV difference can be up to 1.25%, so set the tolerance to 2% - print('eps = ' + str(eps) + '\n') - - # Change to a CompareView - ln = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLLayoutNode') - ln.SetNumberOfCompareViewRows(3) - ln.SetNumberOfCompareViewLightboxColumns(4) - ln.SetViewArrangement(12) - self.delayDisplay('Compare View') - print('') - - # Get the slice logic, slice node and slice composite node for the - # first compare viewer - logic = slicer.app.layoutManager().sliceWidget('Compare1').sliceLogic() - compareNode = logic.GetSliceNode() - compareCNode = logic.GetSliceCompositeNode() - - # Link the viewers - compareCNode.SetLinkedControl(1) - self.delayDisplay('Linked the viewers (first Compare View)') - - # Set the data to be same on all viewers - logic.StartSliceCompositeNodeInteraction(1) #ForegroundVolumeFlag - compareCNode.SetForegroundVolumeID(volumeNode.GetID()) - logic.EndSliceCompositeNodeInteraction() - self.assertEqual( compareCNode.GetForegroundVolumeID(), volumeNode.GetID()) - print('') - - # Check that whether the volume was propagated - self.delayDisplay('Broadcasted volume selection to all Compare Views') - compareNode2 = slicer.util.getNode('vtkMRMLSliceNodeCompare2') - compareCNode2 = slicer.util.getNode('vtkMRMLSliceCompositeNodeCompare2') - self.assertEqual(compareCNode2.GetForegroundVolumeID(), volumeNode.GetID()) - compareNode3 = slicer.util.getNode('vtkMRMLSliceNodeCompare3') - compareCNode3 = slicer.util.getNode('vtkMRMLSliceCompositeNodeCompare3') - self.assertEqual(compareCNode3.GetForegroundVolumeID(), volumeNode.GetID()) - print('') - - # Set the orientation to axial - logic.StartSliceNodeInteraction(12) #OrientationFlag & ResetFieldOfViewFlag - compareNode.SetOrientation('Axial') - logic.FitSliceToAll() - compareNode.UpdateMatrices() - logic.EndSliceNodeInteraction() - - # Reset the field of view - logic.StartSliceNodeInteraction(8) #ResetFieldOfViewFlag - logic.FitSliceToAll() - compareNode.UpdateMatrices() - logic.EndSliceNodeInteraction() - # Note: we validate on fov[1] when resetting the field of view (fov[0] can - # differ by a few units) - self.delayDisplay('Broadcasted a reset of the field of view to all Compare Views') - diff = abs(compareNode2.GetFieldOfView()[1]-compareNode.GetFieldOfView()[1]) / compareNode.GetFieldOfView()[1] - print("Field of view of comparison (y) between compare viewers #1 and #2: " + str(diff)) - self.assertLess(diff, eps) - - diff = abs(compareNode3.GetFieldOfView()[1]-compareNode.GetFieldOfView()[1]) / compareNode.GetFieldOfView()[1] - print("Field of view of comparison (y) between compare viewers #1 and #3: " + str(diff)) - self.assertLess(diff, eps) - print('') - - # Changed the number of lightboxes - ln.SetNumberOfCompareViewLightboxColumns(6) - logic.StartSliceNodeInteraction(8) #ResetFieldOfViewFlag - logic.FitSliceToAll() - compareNode.UpdateMatrices() - logic.EndSliceNodeInteraction() - - # Note: we validate on fov[1] when resetting the field of view (fov[0] can - # differ by a few units) - self.delayDisplay('Changed the number of lightboxes') - diff = abs(compareNode2.GetFieldOfView()[1]-compareNode.GetFieldOfView()[1]) / compareNode.GetFieldOfView()[1] - print("Field of view of comparison (y) between compare viewers #1 and #2: " + str(diff)) - self.assertLess(diff, eps) - - diff = abs(compareNode3.GetFieldOfView()[1]-compareNode.GetFieldOfView()[1]) / compareNode.GetFieldOfView()[1] - print("Field of view of comparison between compare viewers #1 and #3: " + str(diff)) - self.assertLess(diff, eps) - print('') - - # Pan - logic.StartSliceNodeInteraction(32) #XYZOriginFlag - xyz = compareNode.GetXYZOrigin() - compareNode.SetSliceOrigin(xyz[0] + 50, xyz[1] + 50, xyz[2]) - logic.EndSliceNodeInteraction() - - self.delayDisplay('Broadcasted a pan to all Compare Views') - diff = abs(compareNode2.GetXYZOrigin()[0]-compareNode.GetXYZOrigin()[0]) - print("Origin comparison (x) between compare viewers #1 and #2: " + str(diff)) - self.assertLess(diff, eps) - - diff = abs(compareNode3.GetXYZOrigin()[0]-compareNode.GetXYZOrigin()[0]) - print("Origin comparison (x) between compare viewers #1 and #3: " + str(diff)) - self.assertLess(diff, eps) - print('') - - # Zoom - logic.StartSliceNodeInteraction(2) #FieldOfFlag - fov = compareNode.GetFieldOfView() - compareNode.SetFieldOfView(fov[0] * 0.5, fov[1] * 0.5, fov[2]) - logic.EndSliceNodeInteraction() - # Note: we validate on fov[0] when zooming (fov[1] can differ by - # a few units) - self.delayDisplay('Broadcasted a zoom to all Compare Views') - diff = abs(compareNode2.GetFieldOfView()[0]-compareNode.GetFieldOfView()[0]) / compareNode.GetFieldOfView()[0] - print("Field of view of comparison (x) between compare viewers #1 and #2: " + str(diff)) - self.assertLess(diff, eps) - - diff = abs(compareNode3.GetFieldOfView()[0]-compareNode.GetFieldOfView()[0]) / compareNode.GetFieldOfView()[0] - print("Field of view of comparison (x) between compare viewers #1 and #3: " + str(diff)) - self.assertLess(diff, eps) - print('') - - # Change the slice - logic.StartSliceNodeInteraction(1) #SliceToRAS - logic.SetSliceOffset(80) - logic.EndSliceNodeInteraction() - self.delayDisplay('Broadcasted a change in slice offset to all Compare Views') - diff = abs(compareNode2.GetSliceOffset()-compareNode.GetSliceOffset()) - print("Slice offset comparison between compare viewers #1 and #2: " + str(diff)) - self.assertLess(diff, eps) - - diff = abs(compareNode3.GetSliceOffset()-compareNode.GetSliceOffset()) - print("Slice offset comparison between compare viewers #1 and #3: " + str(diff)) - self.assertLess(diff, eps) - print('') - - # Change the orientation - logic.StartSliceNodeInteraction(12) #OrientationFlag & ResetFieldOfViewFlag - compareNode.SetOrientation('Sagittal') - logic.FitSliceToAll() - compareNode.UpdateMatrices() - logic.EndSliceNodeInteraction() - self.delayDisplay('Broadcasted a change in slice orientation to all Compare Views') - self.assertEqual(compareNode2.GetOrientationString(), compareNode.GetOrientationString()) - self.assertEqual(compareNode3.GetOrientationString(), compareNode.GetOrientationString()) - print('') - - self.delayDisplay('Test passed!') + 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_SliceLinkLogic1() + + def test_SliceLinkLogic1(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 SampleData + SampleData.downloadFromURL( + nodeNames='FA', + fileNames='FA.nrrd', + uris=TESTING_DATA_URL + 'SHA256/12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560', + checksums='SHA256:12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560') + self.delayDisplay('Finished with download and loading') + print('') + + volumeNode = slicer.util.getNode(pattern="FA") + logic = SliceLinkLogicLogic() + self.assertIsNotNone(logic.hasImageData(volumeNode)) + + eps = 0.02 # on high-DPI screens FOV difference can be up to 1.25%, so set the tolerance to 2% + print('eps = ' + str(eps) + '\n') + + # Change to a CompareView + ln = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLLayoutNode') + ln.SetNumberOfCompareViewRows(3) + ln.SetNumberOfCompareViewLightboxColumns(4) + ln.SetViewArrangement(12) + self.delayDisplay('Compare View') + print('') + + # Get the slice logic, slice node and slice composite node for the + # first compare viewer + logic = slicer.app.layoutManager().sliceWidget('Compare1').sliceLogic() + compareNode = logic.GetSliceNode() + compareCNode = logic.GetSliceCompositeNode() + + # Link the viewers + compareCNode.SetLinkedControl(1) + self.delayDisplay('Linked the viewers (first Compare View)') + + # Set the data to be same on all viewers + logic.StartSliceCompositeNodeInteraction(1) # ForegroundVolumeFlag + compareCNode.SetForegroundVolumeID(volumeNode.GetID()) + logic.EndSliceCompositeNodeInteraction() + self.assertEqual(compareCNode.GetForegroundVolumeID(), volumeNode.GetID()) + print('') + + # Check that whether the volume was propagated + self.delayDisplay('Broadcasted volume selection to all Compare Views') + compareNode2 = slicer.util.getNode('vtkMRMLSliceNodeCompare2') + compareCNode2 = slicer.util.getNode('vtkMRMLSliceCompositeNodeCompare2') + self.assertEqual(compareCNode2.GetForegroundVolumeID(), volumeNode.GetID()) + compareNode3 = slicer.util.getNode('vtkMRMLSliceNodeCompare3') + compareCNode3 = slicer.util.getNode('vtkMRMLSliceCompositeNodeCompare3') + self.assertEqual(compareCNode3.GetForegroundVolumeID(), volumeNode.GetID()) + print('') + + # Set the orientation to axial + logic.StartSliceNodeInteraction(12) # OrientationFlag & ResetFieldOfViewFlag + compareNode.SetOrientation('Axial') + logic.FitSliceToAll() + compareNode.UpdateMatrices() + logic.EndSliceNodeInteraction() + + # Reset the field of view + logic.StartSliceNodeInteraction(8) # ResetFieldOfViewFlag + logic.FitSliceToAll() + compareNode.UpdateMatrices() + logic.EndSliceNodeInteraction() + # Note: we validate on fov[1] when resetting the field of view (fov[0] can + # differ by a few units) + self.delayDisplay('Broadcasted a reset of the field of view to all Compare Views') + diff = abs(compareNode2.GetFieldOfView()[1] - compareNode.GetFieldOfView()[1]) / compareNode.GetFieldOfView()[1] + print("Field of view of comparison (y) between compare viewers #1 and #2: " + str(diff)) + self.assertLess(diff, eps) + + diff = abs(compareNode3.GetFieldOfView()[1] - compareNode.GetFieldOfView()[1]) / compareNode.GetFieldOfView()[1] + print("Field of view of comparison (y) between compare viewers #1 and #3: " + str(diff)) + self.assertLess(diff, eps) + print('') + + # Changed the number of lightboxes + ln.SetNumberOfCompareViewLightboxColumns(6) + logic.StartSliceNodeInteraction(8) # ResetFieldOfViewFlag + logic.FitSliceToAll() + compareNode.UpdateMatrices() + logic.EndSliceNodeInteraction() + + # Note: we validate on fov[1] when resetting the field of view (fov[0] can + # differ by a few units) + self.delayDisplay('Changed the number of lightboxes') + diff = abs(compareNode2.GetFieldOfView()[1] - compareNode.GetFieldOfView()[1]) / compareNode.GetFieldOfView()[1] + print("Field of view of comparison (y) between compare viewers #1 and #2: " + str(diff)) + self.assertLess(diff, eps) + + diff = abs(compareNode3.GetFieldOfView()[1] - compareNode.GetFieldOfView()[1]) / compareNode.GetFieldOfView()[1] + print("Field of view of comparison between compare viewers #1 and #3: " + str(diff)) + self.assertLess(diff, eps) + print('') + + # Pan + logic.StartSliceNodeInteraction(32) # XYZOriginFlag + xyz = compareNode.GetXYZOrigin() + compareNode.SetSliceOrigin(xyz[0] + 50, xyz[1] + 50, xyz[2]) + logic.EndSliceNodeInteraction() + + self.delayDisplay('Broadcasted a pan to all Compare Views') + diff = abs(compareNode2.GetXYZOrigin()[0] - compareNode.GetXYZOrigin()[0]) + print("Origin comparison (x) between compare viewers #1 and #2: " + str(diff)) + self.assertLess(diff, eps) + + diff = abs(compareNode3.GetXYZOrigin()[0] - compareNode.GetXYZOrigin()[0]) + print("Origin comparison (x) between compare viewers #1 and #3: " + str(diff)) + self.assertLess(diff, eps) + print('') + + # Zoom + logic.StartSliceNodeInteraction(2) # FieldOfFlag + fov = compareNode.GetFieldOfView() + compareNode.SetFieldOfView(fov[0] * 0.5, fov[1] * 0.5, fov[2]) + logic.EndSliceNodeInteraction() + # Note: we validate on fov[0] when zooming (fov[1] can differ by + # a few units) + self.delayDisplay('Broadcasted a zoom to all Compare Views') + diff = abs(compareNode2.GetFieldOfView()[0] - compareNode.GetFieldOfView()[0]) / compareNode.GetFieldOfView()[0] + print("Field of view of comparison (x) between compare viewers #1 and #2: " + str(diff)) + self.assertLess(diff, eps) + + diff = abs(compareNode3.GetFieldOfView()[0] - compareNode.GetFieldOfView()[0]) / compareNode.GetFieldOfView()[0] + print("Field of view of comparison (x) between compare viewers #1 and #3: " + str(diff)) + self.assertLess(diff, eps) + print('') + + # Change the slice + logic.StartSliceNodeInteraction(1) # SliceToRAS + logic.SetSliceOffset(80) + logic.EndSliceNodeInteraction() + self.delayDisplay('Broadcasted a change in slice offset to all Compare Views') + diff = abs(compareNode2.GetSliceOffset() - compareNode.GetSliceOffset()) + print("Slice offset comparison between compare viewers #1 and #2: " + str(diff)) + self.assertLess(diff, eps) + + diff = abs(compareNode3.GetSliceOffset() - compareNode.GetSliceOffset()) + print("Slice offset comparison between compare viewers #1 and #3: " + str(diff)) + self.assertLess(diff, eps) + print('') + + # Change the orientation + logic.StartSliceNodeInteraction(12) # OrientationFlag & ResetFieldOfViewFlag + compareNode.SetOrientation('Sagittal') + logic.FitSliceToAll() + compareNode.UpdateMatrices() + logic.EndSliceNodeInteraction() + self.delayDisplay('Broadcasted a change in slice orientation to all Compare Views') + self.assertEqual(compareNode2.GetOrientationString(), compareNode.GetOrientationString()) + self.assertEqual(compareNode3.GetOrientationString(), compareNode.GetOrientationString()) + print('') + + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/Slicer4Minute.py b/Applications/SlicerApp/Testing/Python/Slicer4Minute.py index 2d5a112d60d..df642db1ccc 100644 --- a/Applications/SlicerApp/Testing/Python/Slicer4Minute.py +++ b/Applications/SlicerApp/Testing/Python/Slicer4Minute.py @@ -11,22 +11,22 @@ # class Slicer4Minute(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) - parent.title = "Slicer4Minute" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Jim Miller (GE)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + """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) + parent.title = "Slicer4Minute" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Jim Miller (GE)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ Test suite for the Slicer 4 Minute tutorial """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Jim Miller, GE and was partially funded by NIH grant U54EB005149. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -34,36 +34,36 @@ def __init__(self, parent): # class Slicer4MinuteWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... - # Collapsible button - dummyCollapsibleButton = ctk.ctkCollapsibleButton() - dummyCollapsibleButton.text = "A collapsible button" - self.layout.addWidget(dummyCollapsibleButton) + # Collapsible button + dummyCollapsibleButton = ctk.ctkCollapsibleButton() + dummyCollapsibleButton.text = "A collapsible button" + self.layout.addWidget(dummyCollapsibleButton) - # Layout within the dummy collapsible button - dummyFormLayout = qt.QFormLayout(dummyCollapsibleButton) + # Layout within the dummy collapsible button + dummyFormLayout = qt.QFormLayout(dummyCollapsibleButton) - # HelloWorld button - helloWorldButton = qt.QPushButton("Hello world") - helloWorldButton.toolTip = "Print 'Hello world' in standard output." - dummyFormLayout.addWidget(helloWorldButton) - helloWorldButton.connect('clicked(bool)', self.onHelloWorldButtonClicked) + # HelloWorld button + helloWorldButton = qt.QPushButton("Hello world") + helloWorldButton.toolTip = "Print 'Hello world' in standard output." + dummyFormLayout.addWidget(helloWorldButton) + helloWorldButton.connect('clicked(bool)', self.onHelloWorldButtonClicked) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - # Set local var as instance attribute - self.helloWorldButton = helloWorldButton + # Set local var as instance attribute + self.helloWorldButton = helloWorldButton - def onHelloWorldButtonClicked(self): - print("Hello World !") + def onHelloWorldButtonClicked(self): + print("Hello World !") # @@ -71,114 +71,114 @@ def onHelloWorldButtonClicked(self): # class Slicer4MinuteLogic(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 a dummy logic method that - returns true if the passed in volume - node has valid image data + """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 """ - if not volumeNode: - print('no volume node') - return False - if volumeNode.GetImageData() is None: - print('no image data') - return False - return True + + def hasImageData(self, volumeNode): + """This is a dummy logic method that + returns true if the passed in volume + node has valid image data + """ + if not volumeNode: + print('no volume node') + return False + if volumeNode.GetImageData() is None: + print('no image data') + return False + return True class Slicer4MinuteTest(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. + 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 """ - self.setUp() - self.test_Slicer4Minute1() - - def test_Slicer4Minute1(self): - """ Tests parts of the Slicer4Minute tutorial. - Currently testing 'Part 2' which covers volumes, models, visibility and clipping. - """ - self.delayDisplay("Starting the test") - - logic = Slicer4MinuteLogic() - - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL( - fileNames='slicer4minute.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8', - checksums='SHA256:5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8') - self.delayDisplay('Finished with download and loading') - - # Testing "Part 2" of Tutorial - # - # - self.delayDisplay('Testing Part 2 of the Tutorial') - - # check volume is loaded out of scene - volumeNode = slicer.util.getNode(pattern="grayscale") - self.assertIsNotNone( logic.hasImageData(volumeNode) ) - - # check the slice planes - red = slicer.util.getNode(pattern="vtkMRMLSliceNode1") - red.SetSliceVisible(1) - - green = slicer.util.getNode(pattern="vtkMRMLSliceNode3") - green.SetSliceVisible(1) - - # rotate a bit - cam = slicer.util.getNode(pattern='vtkMRMLCameraNode1') - cam.GetCamera().Azimuth(90) - cam.GetCamera().Elevation(20) - - # turn off skin and skull - skin = slicer.util.getNode(pattern='Skin') - skin.GetDisplayNode().SetVisibility(0) - - skull = slicer.util.getNode(pattern='skull_bone') - skull.GetDisplayNode().SetVisibility(0) - - # clip the model hemispheric_white_matter.vtk - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('Models') - - models = slicer.util.getModule('Models') - logic = models.logic() - - hemispheric_white_matter = slicer.util.getNode(pattern='hemispheric_white_matter') - hemispheric_white_matter.GetDisplayNode().SetClipping(1) - - clip = slicer.util.getNode('ClipModelsParameters1') - clip.SetRedSliceClipState(0) - clip.SetYellowSliceClipState(0) - clip.SetGreenSliceClipState(2) - - # Can we make this more than just a Smoke Test? - self.delayDisplay('Optic chiasm should be visible. Front part of white matter should be clipped.') - - # Done - # - # - self.delayDisplay('Test passed!') + 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_Slicer4Minute1() + + def test_Slicer4Minute1(self): + """ Tests parts of the Slicer4Minute tutorial. + + Currently testing 'Part 2' which covers volumes, models, visibility and clipping. + """ + self.delayDisplay("Starting the test") + + logic = Slicer4MinuteLogic() + + # + # first, get some data + # + import SampleData + SampleData.downloadFromURL( + fileNames='slicer4minute.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8', + checksums='SHA256:5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8') + self.delayDisplay('Finished with download and loading') + + # Testing "Part 2" of Tutorial + # + # + self.delayDisplay('Testing Part 2 of the Tutorial') + + # check volume is loaded out of scene + volumeNode = slicer.util.getNode(pattern="grayscale") + self.assertIsNotNone(logic.hasImageData(volumeNode)) + + # check the slice planes + red = slicer.util.getNode(pattern="vtkMRMLSliceNode1") + red.SetSliceVisible(1) + + green = slicer.util.getNode(pattern="vtkMRMLSliceNode3") + green.SetSliceVisible(1) + + # rotate a bit + cam = slicer.util.getNode(pattern='vtkMRMLCameraNode1') + cam.GetCamera().Azimuth(90) + cam.GetCamera().Elevation(20) + + # turn off skin and skull + skin = slicer.util.getNode(pattern='Skin') + skin.GetDisplayNode().SetVisibility(0) + + skull = slicer.util.getNode(pattern='skull_bone') + skull.GetDisplayNode().SetVisibility(0) + + # clip the model hemispheric_white_matter.vtk + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('Models') + + models = slicer.util.getModule('Models') + logic = models.logic() + + hemispheric_white_matter = slicer.util.getNode(pattern='hemispheric_white_matter') + hemispheric_white_matter.GetDisplayNode().SetClipping(1) + + clip = slicer.util.getNode('ClipModelsParameters1') + clip.SetRedSliceClipState(0) + clip.SetYellowSliceClipState(0) + clip.SetGreenSliceClipState(2) + + # Can we make this more than just a Smoke Test? + self.delayDisplay('Optic chiasm should be visible. Front part of white matter should be clipped.') + + # Done + # + # + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/SlicerAppTesting.py b/Applications/SlicerApp/Testing/Python/SlicerAppTesting.py index 8cc3c74a188..6c177dd141d 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerAppTesting.py +++ b/Applications/SlicerApp/Testing/Python/SlicerAppTesting.py @@ -36,71 +36,71 @@ __all__ = ['EXIT_FAILURE', 'EXIT_SUCCESS', 'run', 'runSlicer', 'runSlicerAndExit', 'timecall'] -EXIT_FAILURE=1 -EXIT_SUCCESS=0 +EXIT_FAILURE = 1 +EXIT_SUCCESS = 0 def dropcache(): - if sys.platform in ["linux", "linux2"]: - run('/usr/bin/sudo', ['sysctl', 'vm.drop_caches=1'], drop_cache=False) - else: - # XXX Implement other platform (Windows: EmptyStandbyList ?, macOS: Purge ?) - raise Exception("--drop-cache is not supported on %s" % sys.platform) + if sys.platform in ["linux", "linux2"]: + run('/usr/bin/sudo', ['sysctl', 'vm.drop_caches=1'], drop_cache=False) + else: + # XXX Implement other platform (Windows: EmptyStandbyList ?, macOS: Purge ?) + raise Exception("--drop-cache is not supported on %s" % sys.platform) def run(executable, arguments=[], verbose=True, shell=False, drop_cache=False): - """Run ``executable`` with provided ``arguments``. - """ - if drop_cache: - dropcache() - if verbose: - print("{} {}".format(os.path.basename(executable), " ".join([pipes.quote(arg) for arg in arguments]))) - arguments.insert(0, executable) - if shell: - arguments = " ".join([pipes.quote(arg) for arg in arguments]) - p = subprocess.Popen(args=arguments, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, shell=shell) - stdout, stderr = p.communicate() + """Run ``executable`` with provided ``arguments``. + """ + if drop_cache: + dropcache() + if verbose: + print("{} {}".format(os.path.basename(executable), " ".join([pipes.quote(arg) for arg in arguments]))) + arguments.insert(0, executable) + if shell: + arguments = " ".join([pipes.quote(arg) for arg in arguments]) + p = subprocess.Popen(args=arguments, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, shell=shell) + stdout, stderr = p.communicate() - if p.returncode != EXIT_SUCCESS: - print('STDERR: ' + stderr.decode(), file=sys.stderr) + if p.returncode != EXIT_SUCCESS: + print('STDERR: ' + stderr.decode(), file=sys.stderr) - return (p.returncode, stdout.decode(), stderr.decode()) + return (p.returncode, stdout.decode(), stderr.decode()) def runSlicer(slicer_executable, arguments=[], verbose=True, **kwargs): - """Run ``slicer_executable`` with provided ``arguments``. - """ - args = ['--no-splash'] - args.extend(arguments) - return run(slicer_executable, args, verbose, **kwargs) + """Run ``slicer_executable`` with provided ``arguments``. + """ + args = ['--no-splash'] + args.extend(arguments) + return run(slicer_executable, args, verbose, **kwargs) def runSlicerAndExit(slicer_executable, arguments=[], verbose=True, **kwargs): - """Run ``slicer_executable`` with provided ``arguments`` and exit. - """ - args = ['--exit-after-startup'] - args.extend(arguments) - return runSlicer(slicer_executable, args, verbose, **kwargs) + """Run ``slicer_executable`` with provided ``arguments`` and exit. + """ + args = ['--exit-after-startup'] + args.extend(arguments) + return runSlicer(slicer_executable, args, verbose, **kwargs) def timecall(method, **kwargs): - """Wrap ``method`` and return its execution time. - """ - repeat = 1 - if 'repeat' in kwargs: - repeat = kwargs['repeat'] - - def wrapper(*args, **kwargs): - durations = [] - for iteration in range(1, repeat + 1): - start = time.time() - result = method(*args, **kwargs) - durations.append(time.time() - start) - print(f"{iteration:d}/{repeat:d}: {durations[-1]:.2f}s") - average = sum(durations) / len(durations) - print(f"Average: {average:.2f}s\n") - duration = average - return (duration, result) - - return wrapper + """Wrap ``method`` and return its execution time. + """ + repeat = 1 + if 'repeat' in kwargs: + repeat = kwargs['repeat'] + + def wrapper(*args, **kwargs): + durations = [] + for iteration in range(1, repeat + 1): + start = time.time() + result = method(*args, **kwargs) + durations.append(time.time() - start) + print(f"{iteration:d}/{repeat:d}: {durations[-1]:.2f}s") + average = sum(durations) / len(durations) + print(f"Average: {average:.2f}s\n") + duration = average + return (duration, result) + + return wrapper diff --git a/Applications/SlicerApp/Testing/Python/SlicerBoundsTest.py b/Applications/SlicerApp/Testing/Python/SlicerBoundsTest.py index 5a33914bb3b..ce02708d4e7 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerBoundsTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerBoundsTest.py @@ -8,20 +8,20 @@ # SlicerOrientationSelectorTest # class SlicerBoundsTest(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 = "BoundsTest" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = [] - self.parent.contributors = ["Johan Andruejol (Kitware Inc)"] - self.parent.helpText = """ + """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 = "BoundsTest" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = [] + self.parent.contributors = ["Johan Andruejol (Kitware Inc)"] + self.parent.helpText = """ This test has been added to check the GetRASBounds and GetBounds methods. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ This file was originally developed by Johan Andruejol, Kitware Inc. """ @@ -30,239 +30,239 @@ def __init__(self, parent): # SlicerBoundsTest # class SlicerBoundsTestWidget(ScriptedLoadableModuleWidget): - """ - """ + """ + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # # SlicerTransformInteractionTest1Logic # class SlicerBoundsTestLogic(ScriptedLoadableModuleLogic): - """ - """ - - -class SlicerBoundsTestTest(ScriptedLoadableModuleTest): - """ - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ - slicer.mrmlScene.Clear(0) + """ - def assertListAlmostEquals(self, list, expected): - for l, e in zip(list, expected): - self.assertAlmostEqual(l, e) - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_Volume() - self.test_Model() - self.test_Segmentation() - self.test_Markup() - self.test_ROI() - self.delayDisplay('Test completed.') - - def test_Volume(self): - """ Test the GetRASBounds & GetBounds method on a volume. - """ - #self.delayDisplay("Starting test_Volume") - import SampleData - volumeNode = SampleData.downloadSample('CTAAbdomenPanoramix') - - bounds = list(range(6)) - volumeNode.GetRASBounds(bounds) - untransformedBounds = [-165.68283081054688, 161.62185668945312, 7.542130470275879, 245.78431797027588, -347.62799072265625, -25.12799072265625] - self.assertListAlmostEquals(bounds, untransformedBounds) - - volumeNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - - transform = vtk.vtkTransform() - transform.Translate([-5.0, +42.0, -0.1]) - transform.RotateWXYZ(41, 0.7, 0.6, 75) - transform.Scale(2, 3, 10) - transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) - transformNode.ApplyTransform(transform) - - volumeNode.SetAndObserveTransformNodeID(transformNode.GetID()) - transformedBounds = [-764.990027409413, 222.22539693955437, -157.26286179044325, 825.0196226274144, -3477.0246375801075, -244.4287311630153] - volumeNode.GetRASBounds(bounds) - self.assertListAlmostEquals(bounds, transformedBounds) - - volumeNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - #self.delayDisplay('test_Volume passed!') - - def test_Model(self): - """ Test the GetRASBounds & GetBounds method on a model. - """ - #self.delayDisplay("Starting test_Model") - # Setup - cubeSource = vtk.vtkCubeSource() - cubeSource.SetXLength(500) - cubeSource.SetYLength(200) - cubeSource.SetZLength(300) - cubeSource.SetCenter(10, -85, 0.7) - - rotation = vtk.vtkTransform() - rotation.RotateX(15.0) - rotation.RotateZ(78) - - applyTransform = vtk.vtkTransformPolyDataFilter() - applyTransform.SetTransform(rotation) - applyTransform.SetInputConnection(cubeSource.GetOutputPort()) - applyTransform.Update() - - modelNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLModelNode()) - modelNode.SetPolyDataConnection(applyTransform.GetOutputPort()) - - # Testing - bounds = list(range(6)) - modelNode.GetRASBounds(bounds) - untransformedBounds = [-64.5710220336914, 235.01434326171875, -302.91339111328125, 287.3067932128906, -214.92703247070312, 212.1946258544922] - self.assertListAlmostEquals(bounds, untransformedBounds) - - modelNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - - transform = vtk.vtkTransform() - transform.Translate([-5.0, +42.0, -0.1]) - transform.RotateWXYZ(41, 0.7, 0.6, 75) - transform.Scale(2, 3, 10) - transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) - transformNode.ApplyTransform(transform) - - modelNode.SetAndObserveTransformNodeID(transformNode.GetID()) - transformedBounds = [-684.0789214975257, 961.8640451930095, -737.3987882515103, 1009.8075011032414, -2158.028473491281, 2129.118193451847] - modelNode.GetRASBounds(bounds) - self.assertListAlmostEquals(bounds, transformedBounds) - - modelNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - #self.delayDisplay('test_Model passed!') - - def test_Segmentation(self): - """ Test the GetRASBounds & GetBounds method on a segmentation. - """ - #self.delayDisplay("Starting test_Segmentation") - cubeSource = vtk.vtkCubeSource() - cubeSource.SetXLength(500) - cubeSource.SetYLength(200) - cubeSource.SetZLength(300) - cubeSource.SetCenter(10, -85, 0.7) - - rotation = vtk.vtkTransform() - rotation.RotateX(15.0) - rotation.RotateZ(78) - - applyTransform = vtk.vtkTransformPolyDataFilter() - applyTransform.SetTransform(rotation) - applyTransform.SetInputConnection(cubeSource.GetOutputPort()) - applyTransform.Update() - - modelNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLModelNode()) - modelNode.SetPolyDataConnection(applyTransform.GetOutputPort()) - - segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") - segmentationLogic = slicer.modules.segmentations.logic() - segmentationLogic.ImportModelToSegmentationNode(modelNode, segmentationNode) - - # Testing - bounds = list(range(6)) - segmentationNode.GetRASBounds(bounds) - untransformedBounds = [-65.41641522206768, 237.23434621664228, -303.75878430165756, 289.7072339384945, -217.463212035832, 213.6873140360733] - self.assertListAlmostEquals(bounds, untransformedBounds) - - segmentationNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - - transform = vtk.vtkTransform() - transform.Translate([-5.0, +42.0, -0.1]) - transform.RotateWXYZ(41, 0.7, 0.6, 75) - transform.Scale(2, 3, 10) - transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) - transformNode.ApplyTransform(transform) - - segmentationNode.SetAndObserveTransformNodeID(transformNode.GetID()) - transformedBounds = [-690.2701685073093, 966.991271911892, -740.4842166018336, 1018.2608117218165, -2183.4229718546612, 2144.1077463008546] - segmentationNode.GetRASBounds(bounds) - self.assertListAlmostEquals(bounds, transformedBounds) - - segmentationNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - - #self.delayDisplay('test_Segmentation passed!') - - def test_Markup(self): - """ Test the GetRASBounds & GetBounds method on a markup. +class SlicerBoundsTestTest(ScriptedLoadableModuleTest): """ - #self.delayDisplay("Starting test_Markup") - markupNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") - markupNode.AddControlPoint([1.0, 0.0, 0.0]) - markupNode.AddControlPoint([-45.0, -90.0, -180.0]) - markupNode.AddControlPoint([-200.0, 500.0, -0.23]) - markupNode.AddControlPoint([1.0, 1003.01, 0.0]) - - bounds = list(range(6)) - markupNode.GetRASBounds(bounds) - untransformedBounds = [-200, 1.0, -90, 1003.01, -180.0, 0.0] - self.assertListAlmostEquals(bounds, untransformedBounds) - - markupNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - - transform = vtk.vtkTransform() - transform.Translate([-5.0, +42.0, -0.1]) - transform.RotateWXYZ(41, 0.7, 0.6, 75) - transform.Scale(2, 3, 10) - transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) - transformNode.ApplyTransform(transform) - - markupNode.SetAndObserveTransformNodeID(transformNode.GetID()) - transformedBounds = [-1977.3875985837567, 90.6250336838986, -213.3290140037272, 2314.3030541154367, -1801.9498682023534, 24.221433153858232] - markupNode.GetRASBounds(bounds) - self.assertListAlmostEquals(bounds, transformedBounds) - - markupNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - #self.delayDisplay('test_Markup passed!') - - def test_ROI(self): - """ Test the GetRASBounds & GetBounds method on a ROI. """ - #self.delayDisplay("Starting test_ROI") - roiNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLAnnotationROINode()) - roiNode.SetXYZ(100, 300, -0.689) - roiNode.SetRadiusXYZ(700, 8, 45) - - bounds = list(range(6)) - roiNode.GetRASBounds(bounds) - untransformedBounds = [-600, 800, 292, 308, -45.689, 44.311] - self.assertListAlmostEquals(bounds, untransformedBounds) - - roiNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - - transform = vtk.vtkTransform() - transform.Translate([-5.0, +42.0, -0.1]) - transform.RotateWXYZ(41, 0.7, 0.6, 75) - transform.Scale(2, 3, 10) - transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) - transformNode.ApplyTransform(transform) - - roiNode.SetAndObserveTransformNodeID(transformNode.GetID()) - transformedBounds = [-1520.2565880625004, 631.261028317266, -85.93765163061204, 1790.9115952348277, -454.6252695921299, 454.0147697244433] - roiNode.GetRASBounds(bounds) - self.assertListAlmostEquals(bounds, transformedBounds) - - roiNode.GetBounds(bounds) - self.assertListAlmostEquals(bounds, untransformedBounds) - - #self.delayDisplay('test_ROI passed!') + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) + + def assertListAlmostEquals(self, list, expected): + for list_item, expected_item in zip(list, expected): + self.assertAlmostEqual(list_item, expected_item) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_Volume() + self.test_Model() + self.test_Segmentation() + self.test_Markup() + self.test_ROI() + self.delayDisplay('Test completed.') + + def test_Volume(self): + """ Test the GetRASBounds & GetBounds method on a volume. + """ + # self.delayDisplay("Starting test_Volume") + import SampleData + volumeNode = SampleData.downloadSample('CTAAbdomenPanoramix') + + bounds = list(range(6)) + volumeNode.GetRASBounds(bounds) + untransformedBounds = [-165.68283081054688, 161.62185668945312, 7.542130470275879, 245.78431797027588, -347.62799072265625, -25.12799072265625] + self.assertListAlmostEquals(bounds, untransformedBounds) + + volumeNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + + transform = vtk.vtkTransform() + transform.Translate([-5.0, +42.0, -0.1]) + transform.RotateWXYZ(41, 0.7, 0.6, 75) + transform.Scale(2, 3, 10) + transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) + transformNode.ApplyTransform(transform) + + volumeNode.SetAndObserveTransformNodeID(transformNode.GetID()) + transformedBounds = [-764.990027409413, 222.22539693955437, -157.26286179044325, 825.0196226274144, -3477.0246375801075, -244.4287311630153] + volumeNode.GetRASBounds(bounds) + self.assertListAlmostEquals(bounds, transformedBounds) + + volumeNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + # self.delayDisplay('test_Volume passed!') + + def test_Model(self): + """ Test the GetRASBounds & GetBounds method on a model. + """ + # self.delayDisplay("Starting test_Model") + # Setup + cubeSource = vtk.vtkCubeSource() + cubeSource.SetXLength(500) + cubeSource.SetYLength(200) + cubeSource.SetZLength(300) + cubeSource.SetCenter(10, -85, 0.7) + + rotation = vtk.vtkTransform() + rotation.RotateX(15.0) + rotation.RotateZ(78) + + applyTransform = vtk.vtkTransformPolyDataFilter() + applyTransform.SetTransform(rotation) + applyTransform.SetInputConnection(cubeSource.GetOutputPort()) + applyTransform.Update() + + modelNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLModelNode()) + modelNode.SetPolyDataConnection(applyTransform.GetOutputPort()) + + # Testing + bounds = list(range(6)) + modelNode.GetRASBounds(bounds) + untransformedBounds = [-64.5710220336914, 235.01434326171875, -302.91339111328125, 287.3067932128906, -214.92703247070312, 212.1946258544922] + self.assertListAlmostEquals(bounds, untransformedBounds) + + modelNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + + transform = vtk.vtkTransform() + transform.Translate([-5.0, +42.0, -0.1]) + transform.RotateWXYZ(41, 0.7, 0.6, 75) + transform.Scale(2, 3, 10) + transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) + transformNode.ApplyTransform(transform) + + modelNode.SetAndObserveTransformNodeID(transformNode.GetID()) + transformedBounds = [-684.0789214975257, 961.8640451930095, -737.3987882515103, 1009.8075011032414, -2158.028473491281, 2129.118193451847] + modelNode.GetRASBounds(bounds) + self.assertListAlmostEquals(bounds, transformedBounds) + + modelNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + # self.delayDisplay('test_Model passed!') + + def test_Segmentation(self): + """ Test the GetRASBounds & GetBounds method on a segmentation. + """ + # self.delayDisplay("Starting test_Segmentation") + cubeSource = vtk.vtkCubeSource() + cubeSource.SetXLength(500) + cubeSource.SetYLength(200) + cubeSource.SetZLength(300) + cubeSource.SetCenter(10, -85, 0.7) + + rotation = vtk.vtkTransform() + rotation.RotateX(15.0) + rotation.RotateZ(78) + + applyTransform = vtk.vtkTransformPolyDataFilter() + applyTransform.SetTransform(rotation) + applyTransform.SetInputConnection(cubeSource.GetOutputPort()) + applyTransform.Update() + + modelNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLModelNode()) + modelNode.SetPolyDataConnection(applyTransform.GetOutputPort()) + + segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") + segmentationLogic = slicer.modules.segmentations.logic() + segmentationLogic.ImportModelToSegmentationNode(modelNode, segmentationNode) + + # Testing + bounds = list(range(6)) + segmentationNode.GetRASBounds(bounds) + untransformedBounds = [-65.41641522206768, 237.23434621664228, -303.75878430165756, 289.7072339384945, -217.463212035832, 213.6873140360733] + self.assertListAlmostEquals(bounds, untransformedBounds) + + segmentationNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + + transform = vtk.vtkTransform() + transform.Translate([-5.0, +42.0, -0.1]) + transform.RotateWXYZ(41, 0.7, 0.6, 75) + transform.Scale(2, 3, 10) + transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) + transformNode.ApplyTransform(transform) + + segmentationNode.SetAndObserveTransformNodeID(transformNode.GetID()) + transformedBounds = [-690.2701685073093, 966.991271911892, -740.4842166018336, 1018.2608117218165, -2183.4229718546612, 2144.1077463008546] + segmentationNode.GetRASBounds(bounds) + self.assertListAlmostEquals(bounds, transformedBounds) + + segmentationNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + + # self.delayDisplay('test_Segmentation passed!') + + def test_Markup(self): + """ Test the GetRASBounds & GetBounds method on a markup. + """ + # self.delayDisplay("Starting test_Markup") + markupNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") + markupNode.AddControlPoint([1.0, 0.0, 0.0]) + markupNode.AddControlPoint([-45.0, -90.0, -180.0]) + markupNode.AddControlPoint([-200.0, 500.0, -0.23]) + markupNode.AddControlPoint([1.0, 1003.01, 0.0]) + + bounds = list(range(6)) + markupNode.GetRASBounds(bounds) + untransformedBounds = [-200, 1.0, -90, 1003.01, -180.0, 0.0] + self.assertListAlmostEquals(bounds, untransformedBounds) + + markupNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + + transform = vtk.vtkTransform() + transform.Translate([-5.0, +42.0, -0.1]) + transform.RotateWXYZ(41, 0.7, 0.6, 75) + transform.Scale(2, 3, 10) + transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) + transformNode.ApplyTransform(transform) + + markupNode.SetAndObserveTransformNodeID(transformNode.GetID()) + transformedBounds = [-1977.3875985837567, 90.6250336838986, -213.3290140037272, 2314.3030541154367, -1801.9498682023534, 24.221433153858232] + markupNode.GetRASBounds(bounds) + self.assertListAlmostEquals(bounds, transformedBounds) + + markupNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + # self.delayDisplay('test_Markup passed!') + + def test_ROI(self): + """ Test the GetRASBounds & GetBounds method on a ROI. + """ + # self.delayDisplay("Starting test_ROI") + roiNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLAnnotationROINode()) + roiNode.SetXYZ(100, 300, -0.689) + roiNode.SetRadiusXYZ(700, 8, 45) + + bounds = list(range(6)) + roiNode.GetRASBounds(bounds) + untransformedBounds = [-600, 800, 292, 308, -45.689, 44.311] + self.assertListAlmostEquals(bounds, untransformedBounds) + + roiNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + + transform = vtk.vtkTransform() + transform.Translate([-5.0, +42.0, -0.1]) + transform.RotateWXYZ(41, 0.7, 0.6, 75) + transform.Scale(2, 3, 10) + transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) + transformNode.ApplyTransform(transform) + + roiNode.SetAndObserveTransformNodeID(transformNode.GetID()) + transformedBounds = [-1520.2565880625004, 631.261028317266, -85.93765163061204, 1790.9115952348277, -454.6252695921299, 454.0147697244433] + roiNode.GetRASBounds(bounds) + self.assertListAlmostEquals(bounds, transformedBounds) + + roiNode.GetBounds(bounds) + self.assertListAlmostEquals(bounds, untransformedBounds) + + # self.delayDisplay('test_ROI passed!') diff --git a/Applications/SlicerApp/Testing/Python/SlicerCreateRulerCrashIssue4199.py b/Applications/SlicerApp/Testing/Python/SlicerCreateRulerCrashIssue4199.py index c0873b33e1d..e5eb1e77020 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerCreateRulerCrashIssue4199.py +++ b/Applications/SlicerApp/Testing/Python/SlicerCreateRulerCrashIssue4199.py @@ -9,26 +9,26 @@ # class SlicerCreateRulerCrashIssue4199(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 = "Create ruler crash (Issue 4199)" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = [] - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"] # replace with "Firstname Lastname (Organization)" - self.parent.helpText = """This test has been added to check that + """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 = "Create ruler crash (Issue 4199)" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = [] + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"] # replace with "Firstname Lastname (Organization)" + self.parent.helpText = """This test has been added to check that Slicer does not crash after creating a ruler and entering the Annotations module. Problem has been documented in issue #4199. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. and was partially funded by NIH grant 1U24CA194354-01. - """ # replace with organization, grant and thanks. + """ # replace with organization, grant and thanks. # @@ -36,12 +36,12 @@ def __init__(self, parent): # class SlicerCreateRulerCrashIssue4199Widget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -49,74 +49,74 @@ def setup(self): # class SlicerCreateRulerCrashIssue4199Logic(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 + """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 """ - 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 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 -class SlicerCreateRulerCrashIssue4199Test(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. +class SlicerCreateRulerCrashIssue4199Test(ScriptedLoadableModuleTest): """ - self.setUp() - self.test_SlicerCreateRulerCrashIssue4199() - - def test_SlicerCreateRulerCrashIssue4199(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. + 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 """ - logic = SlicerCreateRulerCrashIssue4199Logic() + 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_SlicerCreateRulerCrashIssue4199() + + def test_SlicerCreateRulerCrashIssue4199(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. + """ + + logic = SlicerCreateRulerCrashIssue4199Logic() - self.delayDisplay("Starting the test") + self.delayDisplay("Starting the test") - slicer.util.selectModule('Welcome') + slicer.util.selectModule('Welcome') - # Add a ruler - rulerNode = slicer.vtkMRMLAnnotationRulerNode() - rulerNode.SetPosition1(0, 0, 0) - rulerNode.SetPosition2(1, 1, 1) - rulerNode.Initialize(slicer.mrmlScene) + # Add a ruler + rulerNode = slicer.vtkMRMLAnnotationRulerNode() + rulerNode.SetPosition1(0, 0, 0) + rulerNode.SetPosition2(1, 1, 1) + rulerNode.Initialize(slicer.mrmlScene) - # Enter the Annotations module - slicer.util.selectModule('Annotations') + # Enter the Annotations module + slicer.util.selectModule('Annotations') - # If test reach this point without crashing it is a success + # If test reach this point without crashing it is a success - self.delayDisplay('Test passed!') + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/SlicerMRBMultipleSaveRestoreLoopTest.py b/Applications/SlicerApp/Testing/Python/SlicerMRBMultipleSaveRestoreLoopTest.py index ca87884558d..5231cd3a580 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerMRBMultipleSaveRestoreLoopTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerMRBMultipleSaveRestoreLoopTest.py @@ -9,20 +9,20 @@ # class SlicerMRBMultipleSaveRestoreLoopTest(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) - parent.title = "SlicerMRBMultipleSaveRestoreLoopTest" - parent.categories = ["Testing.TestCases"] - parent.contributors = ["Nicole Aucoin (BWH)"] - parent.helpText = """ + """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) + parent.title = "SlicerMRBMultipleSaveRestoreLoopTest" + parent.categories = ["Testing.TestCases"] + parent.contributors = ["Nicole Aucoin (BWH)"] + parent.helpText = """ Self test for MRB and Scene Views multiple save. No module interface here, only used in SelfTests module """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This test was developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218. @@ -34,124 +34,124 @@ def __init__(self, parent): # class SlicerMRBMultipleSaveRestoreLoopTestWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - -class SlicerMRBMultipleSaveRestoreLoop(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 __init__(self, methodName='runTest', numberOfIterations=5, uniqueDirectory=True, strict=False): + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - Tests the use of mrml and mrb save formats with volumes and point lists. - Checks that scene views are saved and restored as expected after multiple - MRB saves and loads. - - numberOfIterations: integer number of times to save and restore an MRB. - uniqueDirectory: boolean about save directory - False to reuse standard dir name - True timestamps dir name - strict: boolean about how carefully to check result - True then check every detail - False then confirm basic operation, but allow non-critical issues to pass - """ - ScriptedLoadableModuleTest.__init__(self, methodName) - self.numberOfIterations = numberOfIterations - self.uniqueDirectory = uniqueDirectory - self.strict = strict - def setUp(self): - slicer.mrmlScene.Clear(0) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - def runTest(self): - self.setUp() - self.test_SlicerMRBMultipleSaveRestoreLoop() - def test_SlicerMRBMultipleSaveRestoreLoop(self): +class SlicerMRBMultipleSaveRestoreLoop(ScriptedLoadableModuleTest): """ - Stress test the issue reported in bug 3956 where saving - and restoring an MRB file does not work. + 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 """ - print("Running SlicerMRBMultipleSaveRestoreLoop Test case with:") - print("numberOfIterations: %s" % self.numberOfIterations) - print("uniqueDirectory : %s" % self.uniqueDirectory) - print("strict : %s" % self.strict) - - # - # first, get the data - # - print("Getting MR Head Volume") - import SampleData - mrHeadVolume = SampleData.downloadSample("MRHead") - - # Place a control point - pointListNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", "F") - pointListNode.CreateDefaultDisplayNodes() - fid1 = [0.0, 0.0, 0.0] - fidIndex1 = pointListNode.AddControlPoint(fid1) - - self.delayDisplay('Finished with download and placing points') - - ioManager = slicer.app.ioManager() - widget = slicer.app.layoutManager().viewport() - self.pointPosition = fid1 - for i in range(self.numberOfIterations): - - print('\n\nIteration %s' % i) - # - # save the mrml scene to an mrb - # - sceneSaveDirectory = slicer.util.tempDirectory('__scene__') - mrbFilePath = slicer.util.tempDirectory('__mrb__') + '/SlicerMRBMultipleSaveRestoreLoop-' + str(i) + '.mrb' - self.delayDisplay("Saving mrb to: %s" % mrbFilePath) - screenShot = ctk.ctkWidgetsUtils.grabWidget(widget) - self.assertTrue( - ioManager.saveScene(mrbFilePath, screenShot) - ) - self.delayDisplay("Finished saving MRB %s" % i) - - # - # reload the mrb - # - slicer.mrmlScene.Clear(0) - self.delayDisplay('Now, reload the saved MRB') - mrbLoaded = ioManager.loadScene(mrbFilePath) - - # load can return false even though it succeeded - only fail if in strict mode - self.assertTrue( not self.strict or mrbLoaded ) - slicer.app.processEvents() - - # confirm that MRHead is in the background of the Red slice - redComposite = slicer.util.getNode('vtkMRMLSliceCompositeNodeRed') - mrHead = slicer.util.getNode('MRHead') - self.assertEqual(redComposite.GetBackgroundVolumeID(), mrHead.GetID()) - self.delayDisplay('The MRHead volume is AGAIN in the background of the Red viewer') - - # confirm that the point list exists with 1 points - pointListNode = slicer.util.getNode('F') - self.assertEqual(pointListNode.GetNumberOfControlPoints(), 1) - self.delayDisplay('The point list has 1 point in it') - - # adjust the fid list location - self.pointPosition = [i, i, i] - print((i, ': reset point position array to ', self.pointPosition)) - pointListNode.SetNthControlPointPosition(0, self.pointPosition) - self.delayDisplay("Loop Finished") - - print(('Point position from loop = ',self.pointPosition)) - pointListNode = slicer.util.getNode('F') - finalPointPosition = [ 0,0,0 ] - pointListNode.GetNthControlPointPosition(0, finalPointPosition) - print(('Final point scene pos = ',finalPointPosition)) - self.assertEqual(self.pointPosition, finalPointPosition) - - self.delayDisplay("Test Finished") + def __init__(self, methodName='runTest', numberOfIterations=5, uniqueDirectory=True, strict=False): + """ + Tests the use of mrml and mrb save formats with volumes and point lists. + Checks that scene views are saved and restored as expected after multiple + MRB saves and loads. + + numberOfIterations: integer number of times to save and restore an MRB. + uniqueDirectory: boolean about save directory + False to reuse standard dir name + True timestamps dir name + strict: boolean about how carefully to check result + True then check every detail + False then confirm basic operation, but allow non-critical issues to pass + """ + ScriptedLoadableModuleTest.__init__(self, methodName) + self.numberOfIterations = numberOfIterations + self.uniqueDirectory = uniqueDirectory + self.strict = strict + + def setUp(self): + slicer.mrmlScene.Clear(0) + + def runTest(self): + self.setUp() + self.test_SlicerMRBMultipleSaveRestoreLoop() + + def test_SlicerMRBMultipleSaveRestoreLoop(self): + """ + Stress test the issue reported in bug 3956 where saving + and restoring an MRB file does not work. + """ + + print("Running SlicerMRBMultipleSaveRestoreLoop Test case with:") + print("numberOfIterations: %s" % self.numberOfIterations) + print("uniqueDirectory : %s" % self.uniqueDirectory) + print("strict : %s" % self.strict) + + # + # first, get the data + # + print("Getting MR Head Volume") + import SampleData + mrHeadVolume = SampleData.downloadSample("MRHead") + + # Place a control point + pointListNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", "F") + pointListNode.CreateDefaultDisplayNodes() + fid1 = [0.0, 0.0, 0.0] + fidIndex1 = pointListNode.AddControlPoint(fid1) + + self.delayDisplay('Finished with download and placing points') + + ioManager = slicer.app.ioManager() + widget = slicer.app.layoutManager().viewport() + self.pointPosition = fid1 + for i in range(self.numberOfIterations): + + print('\n\nIteration %s' % i) + # + # save the mrml scene to an mrb + # + sceneSaveDirectory = slicer.util.tempDirectory('__scene__') + mrbFilePath = slicer.util.tempDirectory('__mrb__') + '/SlicerMRBMultipleSaveRestoreLoop-' + str(i) + '.mrb' + self.delayDisplay("Saving mrb to: %s" % mrbFilePath) + screenShot = ctk.ctkWidgetsUtils.grabWidget(widget) + self.assertTrue( + ioManager.saveScene(mrbFilePath, screenShot) + ) + self.delayDisplay("Finished saving MRB %s" % i) + + # + # reload the mrb + # + slicer.mrmlScene.Clear(0) + self.delayDisplay('Now, reload the saved MRB') + mrbLoaded = ioManager.loadScene(mrbFilePath) + + # load can return false even though it succeeded - only fail if in strict mode + self.assertTrue(not self.strict or mrbLoaded) + slicer.app.processEvents() + + # confirm that MRHead is in the background of the Red slice + redComposite = slicer.util.getNode('vtkMRMLSliceCompositeNodeRed') + mrHead = slicer.util.getNode('MRHead') + self.assertEqual(redComposite.GetBackgroundVolumeID(), mrHead.GetID()) + self.delayDisplay('The MRHead volume is AGAIN in the background of the Red viewer') + + # confirm that the point list exists with 1 points + pointListNode = slicer.util.getNode('F') + self.assertEqual(pointListNode.GetNumberOfControlPoints(), 1) + self.delayDisplay('The point list has 1 point in it') + + # adjust the fid list location + self.pointPosition = [i, i, i] + print((i, ': reset point position array to ', self.pointPosition)) + pointListNode.SetNthControlPointPosition(0, self.pointPosition) + self.delayDisplay("Loop Finished") + + print(('Point position from loop = ', self.pointPosition)) + pointListNode = slicer.util.getNode('F') + finalPointPosition = [0, 0, 0] + pointListNode.GetNthControlPointPosition(0, finalPointPosition) + print(('Final point scene pos = ', finalPointPosition)) + self.assertEqual(self.pointPosition, finalPointPosition) + + self.delayDisplay("Test Finished") diff --git a/Applications/SlicerApp/Testing/Python/SlicerMRBMultipleSaveRestoreTest.py b/Applications/SlicerApp/Testing/Python/SlicerMRBMultipleSaveRestoreTest.py index d9b2beacca2..202a9857020 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerMRBMultipleSaveRestoreTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerMRBMultipleSaveRestoreTest.py @@ -9,20 +9,20 @@ # class SlicerMRBMultipleSaveRestoreTest(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) - parent.title = "SlicerMRBMultipleSaveRestoreTest" - parent.categories = ["Testing.TestCases"] - parent.contributors = ["Nicole Aucoin (BWH)"] - parent.helpText = """ + """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) + parent.title = "SlicerMRBMultipleSaveRestoreTest" + parent.categories = ["Testing.TestCases"] + parent.contributors = ["Nicole Aucoin (BWH)"] + parent.helpText = """ Self test for MRB and Scene Views multiple save. No module interface here, only used in SelfTests module """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This tes was developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218. @@ -34,202 +34,202 @@ def __init__(self, parent): # class SlicerMRBMultipleSaveRestoreTestWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - -class SlicerMRBMultipleSaveRestore(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 __init__(self,methodName='runTest',uniqueDirectory=True,strict=False): - """ - Tests the use of mrml and mrb save formats with volumes and markups points lists. - Checks that scene views are saved and restored as expected. - Checks that after a scene view restore, MRB save and reload works as expected. - - uniqueDirectory: boolean about save directory - False to reuse standard dir name - True timestamps dir name - strict: boolean about how carefully to check result - True then check every detail - False then confirm basic operation, but allow non-critical issues to pass + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - ScriptedLoadableModuleTest.__init__(self, methodName) - self.uniqueDirectory = uniqueDirectory - self.strict = strict - def setUp(self): - slicer.mrmlScene.Clear(0) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - def runTest(self): - self.setUp() - self.test_SlicerMRBMultipleSaveRestore() - def test_SlicerMRBMultipleSaveRestore(self): - """ - Replicate the issue reported in bug 3956 where saving - and restoring an MRB file does not work. +class SlicerMRBMultipleSaveRestore(ScriptedLoadableModuleTest): """ - - print("Running SlicerMRBMultipleSaveRestore Test case with:") - print("uniqueDirectory : %s" % self.uniqueDirectory) - print("strict : %s" % self.strict) - - # - # first, get the data - # - print("Getting MR Head Volume") - import SampleData - mrHeadVolume = SampleData.downloadSample("MRHead") - - # Place a control point - pointListNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode","F") - pointListNode.CreateDefaultDisplayNodes() - eye = [33.4975, 79.4042, -10.2143] - nose = [-2.145, 116.14, -43.31] - fidIndexEye = pointListNode.AddControlPoint(eye) - fidIndexNose = pointListNode.AddControlPoint(nose) - - self.delayDisplay('Finished with download and placing markups points\n') - - # confirm that MRHead is in the background of the Red slice - redComposite = slicer.util.getNode('vtkMRMLSliceCompositeNodeRed') - mrHead = slicer.util.getNode('MRHead') - self.assertEqual( redComposite.GetBackgroundVolumeID(), mrHead.GetID() ) - self.delayDisplay('The MRHead volume is in the background of the Red viewer') - - # turn off visibility save scene view - pointListNode.SetDisplayVisibility(0) - self.delayDisplay('Not showing markup points') - self.storeSceneView('Invisible-view', "Not showing markup points") - pointListNode.SetDisplayVisibility(1) - self.delayDisplay('Showing markup points') - self.storeSceneView('Visible-view', "Showing markup points") - - # - # save the mrml scene to a temp directory, then zip it - # - applicationLogic = slicer.app.applicationLogic() - sceneSaveDirectory = slicer.util.tempDirectory('__scene__') - mrbFilePath = slicer.util.tempDirectory('__mrb__') + '/SlicerMRBMultipleSaveRestore-1.mrb' - self.delayDisplay("Saving scene to: %s\n" % sceneSaveDirectory + "Saving mrb to: %s" % mrbFilePath) - self.assertTrue( - applicationLogic.SaveSceneToSlicerDataBundleDirectory(sceneSaveDirectory, None) - ) - self.delayDisplay("Finished saving scene") - self.assertTrue( - applicationLogic.Zip(mrbFilePath,sceneSaveDirectory) - ) - self.delayDisplay("Finished saving MRB") - self.delayDisplay("Slicer mrml scene root dir after first save = %s" % slicer.mrmlScene.GetRootDirectory()) - - # - # reload the mrb and restore a scene view - # - slicer.mrmlScene.Clear(0) - mrbExtractPath = slicer.util.tempDirectory('__mrb_extract__') - self.delayDisplay('Now, reload the saved MRB') - mrbLoaded = applicationLogic.OpenSlicerDataBundle(mrbFilePath, mrbExtractPath) - # load can return false even though it succeeded - only fail if in strict mode - self.assertTrue( not self.strict or mrbLoaded ) - slicer.app.processEvents() - - # confirm again that MRHead is in the background of the Red slice - self.delayDisplay('Is the MHRead volume AGAIN in the background of the Red viewer?') - redComposite = slicer.util.getNode('vtkMRMLSliceCompositeNodeRed') - mrHead = slicer.util.getNode('MRHead') - self.assertEqual( redComposite.GetBackgroundVolumeID(), mrHead.GetID() ) - self.delayDisplay('The MRHead volume is AGAIN in the background of the Red viewer') - - # confirm that the point list exists with two points - self.delayDisplay('Does the point list have 2 points in it?') - pointListNode = slicer.util.getNode('F') - self.assertEqual(pointListNode.GetNumberOfControlPoints(), 2) - self.delayDisplay('The point list has 2 points in it') - - # Restore the invisible scene view - self.delayDisplay('About to restore Invisible-view scene') - sceneView = slicer.util.getNode('Invisible-view') - sceneView.RestoreScene() - pointListNode = slicer.util.getNode('F') - self.assertEqual(pointListNode.GetDisplayVisibility(), 0) - self.delayDisplay("NOT seeing the points") - self.delayDisplay('Does the point list still have 2 points in it after restoring a scenen view?') - self.assertEqual(pointListNode.GetNumberOfControlPoints(), 2) - self.delayDisplay('The point list has 2 points in it after scene view restore') - - # - # Save it again - # - sceneSaveDirectory = slicer.util.tempDirectory('__scene2__') - mrbFilePath= slicer.util.tempDirectory('__mrb__') + '/SlicerMRBMultipleSaveRestore-2.mrb' - self.delayDisplay("Saving scene to: %s\n" % sceneSaveDirectory + "Saving mrb to: %s" % mrbFilePath) - self.assertTrue( - applicationLogic.SaveSceneToSlicerDataBundleDirectory(sceneSaveDirectory, None) - ) - self.delayDisplay("Finished saving scene after restoring a scene view") - self.assertTrue( - applicationLogic.Zip(mrbFilePath,sceneSaveDirectory) - ) - self.delayDisplay("Finished saving MRB after restoring a scene view") - - self.delayDisplay("Slicer mrml scene root dir after second save = %s" % slicer.mrmlScene.GetRootDirectory()) - - # - # reload the second mrb and test - # - slicer.mrmlScene.Clear(0) - mrbExtractPath = slicer.util.tempDirectory('__mrb_extract2__') - self.delayDisplay('Now, reload the second saved MRB %s' % mrbFilePath) - mrbLoaded = applicationLogic.OpenSlicerDataBundle(mrbFilePath, mrbExtractPath) - # load can return false even though it succeeded - only fail if in strict mode - self.assertTrue( not self.strict or mrbLoaded ) - slicer.app.processEvents() - - # confirm that MRHead is in the background of the Red slice after mrb reload - self.delayDisplay('MRHead volume is the background of the Red viewer after mrb reload?') - redComposite = slicer.util.getNode('vtkMRMLSliceCompositeNodeRed') - fa = slicer.util.getNode('MRHead') - self.assertEqual( redComposite.GetBackgroundVolumeID(), mrHead.GetID() ) - self.delayDisplay('Yes, the MRHead volume is back in the background of the Red viewer') - - # confirm that the point list exists with two points - pointListNode = slicer.util.getNode('F') - self.assertEqual(pointListNode.GetNumberOfControlPoints(), 2) - self.delayDisplay('The point list has 2 points in it after scene view restore, save and MRB reload') - self.assertEqual(pointListNode.GetDisplayVisibility(), 0) - self.delayDisplay("NOT seeing the points") - - self.delayDisplay("Test Finished") - - def storeSceneView(self,name,description=""): - """ Store a scene view into the current scene. - TODO: this might move to slicer.util + 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 """ - layoutManager = slicer.app.layoutManager() - - sceneViewNode = slicer.vtkMRMLSceneViewNode() - view1 = layoutManager.threeDWidget(0).threeDView() - - w2i1 = vtk.vtkWindowToImageFilter() - w2i1.SetInput(view1.renderWindow()) - - w2i1.Update() - image1 = w2i1.GetOutput() - sceneViewNode.SetScreenShot(image1) - sceneViewNode.UpdateStoredScene() - slicer.mrmlScene.AddNode(sceneViewNode) - - sceneViewNode.SetName(name) - sceneViewNode.SetSceneViewDescription(description) - sceneViewNode.StoreScene() - return sceneViewNode + def __init__(self, methodName='runTest', uniqueDirectory=True, strict=False): + """ + Tests the use of mrml and mrb save formats with volumes and markups points lists. + Checks that scene views are saved and restored as expected. + Checks that after a scene view restore, MRB save and reload works as expected. + + uniqueDirectory: boolean about save directory + False to reuse standard dir name + True timestamps dir name + strict: boolean about how carefully to check result + True then check every detail + False then confirm basic operation, but allow non-critical issues to pass + """ + ScriptedLoadableModuleTest.__init__(self, methodName) + self.uniqueDirectory = uniqueDirectory + self.strict = strict + + def setUp(self): + slicer.mrmlScene.Clear(0) + + def runTest(self): + self.setUp() + self.test_SlicerMRBMultipleSaveRestore() + + def test_SlicerMRBMultipleSaveRestore(self): + """ + Replicate the issue reported in bug 3956 where saving + and restoring an MRB file does not work. + """ + + print("Running SlicerMRBMultipleSaveRestore Test case with:") + print("uniqueDirectory : %s" % self.uniqueDirectory) + print("strict : %s" % self.strict) + + # + # first, get the data + # + print("Getting MR Head Volume") + import SampleData + mrHeadVolume = SampleData.downloadSample("MRHead") + + # Place a control point + pointListNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", "F") + pointListNode.CreateDefaultDisplayNodes() + eye = [33.4975, 79.4042, -10.2143] + nose = [-2.145, 116.14, -43.31] + fidIndexEye = pointListNode.AddControlPoint(eye) + fidIndexNose = pointListNode.AddControlPoint(nose) + + self.delayDisplay('Finished with download and placing markups points\n') + + # confirm that MRHead is in the background of the Red slice + redComposite = slicer.util.getNode('vtkMRMLSliceCompositeNodeRed') + mrHead = slicer.util.getNode('MRHead') + self.assertEqual(redComposite.GetBackgroundVolumeID(), mrHead.GetID()) + self.delayDisplay('The MRHead volume is in the background of the Red viewer') + + # turn off visibility save scene view + pointListNode.SetDisplayVisibility(0) + self.delayDisplay('Not showing markup points') + self.storeSceneView('Invisible-view', "Not showing markup points") + pointListNode.SetDisplayVisibility(1) + self.delayDisplay('Showing markup points') + self.storeSceneView('Visible-view', "Showing markup points") + + # + # save the mrml scene to a temp directory, then zip it + # + applicationLogic = slicer.app.applicationLogic() + sceneSaveDirectory = slicer.util.tempDirectory('__scene__') + mrbFilePath = slicer.util.tempDirectory('__mrb__') + '/SlicerMRBMultipleSaveRestore-1.mrb' + self.delayDisplay("Saving scene to: %s\n" % sceneSaveDirectory + "Saving mrb to: %s" % mrbFilePath) + self.assertTrue( + applicationLogic.SaveSceneToSlicerDataBundleDirectory(sceneSaveDirectory, None) + ) + self.delayDisplay("Finished saving scene") + self.assertTrue( + applicationLogic.Zip(mrbFilePath, sceneSaveDirectory) + ) + self.delayDisplay("Finished saving MRB") + self.delayDisplay("Slicer mrml scene root dir after first save = %s" % slicer.mrmlScene.GetRootDirectory()) + + # + # reload the mrb and restore a scene view + # + slicer.mrmlScene.Clear(0) + mrbExtractPath = slicer.util.tempDirectory('__mrb_extract__') + self.delayDisplay('Now, reload the saved MRB') + mrbLoaded = applicationLogic.OpenSlicerDataBundle(mrbFilePath, mrbExtractPath) + # load can return false even though it succeeded - only fail if in strict mode + self.assertTrue(not self.strict or mrbLoaded) + slicer.app.processEvents() + + # confirm again that MRHead is in the background of the Red slice + self.delayDisplay('Is the MHRead volume AGAIN in the background of the Red viewer?') + redComposite = slicer.util.getNode('vtkMRMLSliceCompositeNodeRed') + mrHead = slicer.util.getNode('MRHead') + self.assertEqual(redComposite.GetBackgroundVolumeID(), mrHead.GetID()) + self.delayDisplay('The MRHead volume is AGAIN in the background of the Red viewer') + + # confirm that the point list exists with two points + self.delayDisplay('Does the point list have 2 points in it?') + pointListNode = slicer.util.getNode('F') + self.assertEqual(pointListNode.GetNumberOfControlPoints(), 2) + self.delayDisplay('The point list has 2 points in it') + + # Restore the invisible scene view + self.delayDisplay('About to restore Invisible-view scene') + sceneView = slicer.util.getNode('Invisible-view') + sceneView.RestoreScene() + pointListNode = slicer.util.getNode('F') + self.assertEqual(pointListNode.GetDisplayVisibility(), 0) + self.delayDisplay("NOT seeing the points") + self.delayDisplay('Does the point list still have 2 points in it after restoring a scenen view?') + self.assertEqual(pointListNode.GetNumberOfControlPoints(), 2) + self.delayDisplay('The point list has 2 points in it after scene view restore') + + # + # Save it again + # + sceneSaveDirectory = slicer.util.tempDirectory('__scene2__') + mrbFilePath = slicer.util.tempDirectory('__mrb__') + '/SlicerMRBMultipleSaveRestore-2.mrb' + self.delayDisplay("Saving scene to: %s\n" % sceneSaveDirectory + "Saving mrb to: %s" % mrbFilePath) + self.assertTrue( + applicationLogic.SaveSceneToSlicerDataBundleDirectory(sceneSaveDirectory, None) + ) + self.delayDisplay("Finished saving scene after restoring a scene view") + self.assertTrue( + applicationLogic.Zip(mrbFilePath, sceneSaveDirectory) + ) + self.delayDisplay("Finished saving MRB after restoring a scene view") + + self.delayDisplay("Slicer mrml scene root dir after second save = %s" % slicer.mrmlScene.GetRootDirectory()) + + # + # reload the second mrb and test + # + slicer.mrmlScene.Clear(0) + mrbExtractPath = slicer.util.tempDirectory('__mrb_extract2__') + self.delayDisplay('Now, reload the second saved MRB %s' % mrbFilePath) + mrbLoaded = applicationLogic.OpenSlicerDataBundle(mrbFilePath, mrbExtractPath) + # load can return false even though it succeeded - only fail if in strict mode + self.assertTrue(not self.strict or mrbLoaded) + slicer.app.processEvents() + + # confirm that MRHead is in the background of the Red slice after mrb reload + self.delayDisplay('MRHead volume is the background of the Red viewer after mrb reload?') + redComposite = slicer.util.getNode('vtkMRMLSliceCompositeNodeRed') + fa = slicer.util.getNode('MRHead') + self.assertEqual(redComposite.GetBackgroundVolumeID(), mrHead.GetID()) + self.delayDisplay('Yes, the MRHead volume is back in the background of the Red viewer') + + # confirm that the point list exists with two points + pointListNode = slicer.util.getNode('F') + self.assertEqual(pointListNode.GetNumberOfControlPoints(), 2) + self.delayDisplay('The point list has 2 points in it after scene view restore, save and MRB reload') + self.assertEqual(pointListNode.GetDisplayVisibility(), 0) + self.delayDisplay("NOT seeing the points") + + self.delayDisplay("Test Finished") + + def storeSceneView(self, name, description=""): + """ Store a scene view into the current scene. + TODO: this might move to slicer.util + """ + layoutManager = slicer.app.layoutManager() + + sceneViewNode = slicer.vtkMRMLSceneViewNode() + view1 = layoutManager.threeDWidget(0).threeDView() + + w2i1 = vtk.vtkWindowToImageFilter() + w2i1.SetInput(view1.renderWindow()) + + w2i1.Update() + image1 = w2i1.GetOutput() + sceneViewNode.SetScreenShot(image1) + sceneViewNode.UpdateStoredScene() + slicer.mrmlScene.AddNode(sceneViewNode) + + sceneViewNode.SetName(name) + sceneViewNode.SetSceneViewDescription(description) + sceneViewNode.StoreScene() + + return sceneViewNode diff --git a/Applications/SlicerApp/Testing/Python/SlicerMRBSaveRestoreCheckPathsTest.py b/Applications/SlicerApp/Testing/Python/SlicerMRBSaveRestoreCheckPathsTest.py index a2fc507914e..0ccc0eedf93 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerMRBSaveRestoreCheckPathsTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerMRBSaveRestoreCheckPathsTest.py @@ -10,20 +10,20 @@ # SlicerMRBSaveRestoreCheckPathsTest # class SlicerMRBSaveRestoreCheckPathsTest(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) - parent.title = "SlicerMRBSaveRestoreCheckPathsTest" - parent.categories = ["Testing.TestCases"] - parent.contributors = ["Nicole Aucoin (BWH)"] - parent.helpText = """ + """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) + parent.title = "SlicerMRBSaveRestoreCheckPathsTest" + parent.categories = ["Testing.TestCases"] + parent.contributors = ["Nicole Aucoin (BWH)"] + parent.helpText = """ Self test for MRB multiple save file paths. No module interface here, only used in SelfTests module """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This test was developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218. @@ -31,214 +31,214 @@ def __init__(self, parent): class SlicerMRBSaveRestoreCheckPaths(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 __init__(self, uniqueDirectory=True, strict=False): - """ - Tests the use of mrml and mrb save formats with volumes. - Checks that after reopening an MRB and trying to save it again that the file paths are all correct. - - uniqueDirectory: boolean about save directory - False to reuse standard dir name - True timestamps dir name - strict: boolean about how carefully to check result - True then check every detail - False then confirm basic operation, but allow non-critical issues to pass - """ - ScriptedLoadableModuleTest.__init__(self) - self.uniqueDirectory = uniqueDirectory - self.strict = strict - # this flag will need to be updated if the code in - # qSlicerSceneBundleReader::load - # is changed from the current behavior (delete expanded - # files after loading the MRB into Slicer) - self.mrbDeleteFilesAfterLoad = 1 - - def setUp(self): - slicer.mrmlScene.Clear(0) - - def runTest(self): - self.setUp() - self.test_SlicerMRBSaveRestoreCheckPaths() - - # - # find and return the storable node associated with this storage node. - # Returns None if not found. - # - def getStorableNode(self, storageNode): - numberOfStorableNodes = storageNode.GetScene().GetNumberOfNodesByClass('vtkMRMLStorableNode') - for n in range(numberOfStorableNodes): - storableNode = storageNode.GetScene().GetNthNodeByClass(n,'vtkMRMLStorableNode') - if storableNode.GetStorageNodeID() == storageNode.GetID(): - return storableNode - return None - - # - # create and return a list of all file names found in storage nodes in - # the scene, not including those in scene views - # - def checkSceneFileNames(self, scene): - # first get the main scene storage nodes - numberOfStorageNodes = scene.GetNumberOfNodesByClass('vtkMRMLStorageNode') - for n in range(numberOfStorageNodes): - storageNode = scene.GetNthNodeByClass(n,'vtkMRMLStorageNode') - storableNode = self.getStorableNode(storageNode) - if storageNode.GetSaveWithScene() and not storableNode.GetModifiedSinceRead(): - print('Checking storage node: ',storageNode.GetID()) - fileName = storageNode.GetFileName() - absFileName = storageNode.GetAbsoluteFilePath(fileName) - if not absFileName: - print('\tUnable to get absolute path for file name ',fileName) - self.numberOfFilesNotFound += 1 - elif not os.path.exists(absFileName): - print('\tfile does not exist: ',absFileName) - print('\t\tnon absolute file name = ',fileName) - print('\t\tscene of the node root dir = ',storageNode.GetScene().GetRootDirectory()) - if storableNode is not None: - print('\t\tstorable node name = ',storableNode.GetName()) - print('\t\tmodified since read = ',storableNode.GetModifiedSinceRead()) - else: - print('\t\tNo storable node found for this storage node') - # double check that it's not due to the unzipped files being deleted - if (not self.mrbDeleteFilesAfterLoad) or ('BundleSaveTemp' in absFileName): - self.numberOfFilesNotFound += 1 - else: - print('\t\tMRB files were deleted after load, not counting this file as not found for the purposes of this test') - else: - print('\tfile exists:',absFileName) - # check for the file list - numberOfFileNames = storageNode.GetNumberOfFileNames() - for n in range(numberOfFileNames): - fileName = storageNode.GetNthFileName(n) - absFileName = storageNode.GetAbsoluteFilePath(fileName) - if not os.path.exists(absFileName): - print('\t',n,'th file list member does not exist: ',absFileName) - if (not self.mrbDeleteFilesAfterLoad) or ('BundleSaveTemp' in absFileName): - self.numberOfFilesNotFound += 1 - else: - print('\t\tMRB files were deleted after load, not counting this file as not found for the purposes of this test') - - def checkSceneViewFileNames(self, scene): - # check for any scene views - numberOfSceneViews = scene.GetNumberOfNodesByClass('vtkMRMLSceneViewNode') - slicer.util.delayDisplay("Number of scene views = " + str(numberOfSceneViews)) - if numberOfSceneViews == 0: - return - for n in range(numberOfSceneViews): - sceneViewNode = slicer.mrmlScene.GetNthNodeByClass(n, 'vtkMRMLSceneViewNode') - slicer.util.delayDisplay('\nChecking scene view ' + sceneViewNode.GetName() + ', id = ' + sceneViewNode.GetID()) - self.checkSceneFileNames(sceneViewNode.GetStoredScene()) - - def checkAllFileNames(self, scene): - slicer.util.delayDisplay("\n\nChecking all file names in scene") - self.numberOfFilesNotFound = 0 - self.checkSceneFileNames(scene) - self.checkSceneViewFileNames(scene) - if self.numberOfFilesNotFound != 0: - print('checkAllFilesNames: there are ',self.numberOfFilesNotFound,'files that are missing from disk\n') - self.assertEqual(self.numberOfFilesNotFound, 0) - - def test_SlicerMRBSaveRestoreCheckPaths(self): """ - Replicate the issue reported in bug 3956 where saving - and restoring an MRB file does not work. + 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 """ - print("Running SlicerMRBSaveRestoreCheckPaths Test case with:") - print("uniqueDirectory : %s" % self.uniqueDirectory) - print("strict : %s" % self.strict) - print("files deleted after load: %d" % self.mrbDeleteFilesAfterLoad) - - # - # first, get the volume data - # - print("Getting MR Head Volume") - import SampleData - mrHeadVolume = SampleData.downloadSample("MRHead") - - slicer.util.delayDisplay('Finished with download of volume') - - # - # test all current file paths - # - self.checkAllFileNames(slicer.mrmlScene) - - ioManager = slicer.app.ioManager() - # grab a testing screen shot - layoutManager = slicer.app.layoutManager() - widget = layoutManager.threeDWidget(0) - screenShot = ctk.ctkWidgetsUtils.grabWidget(widget) - - # - # the remote download leaves the volume in a temp directory and removes - # the storage node, commit the scene to get to a more stable starting - # point with the volume saved in a regular directory - # - tempDir = slicer.util.tempDirectory('__mrml__') - slicer.util.delayDisplay('Temp dir = %s ' % tempDir) - mrmlFilePath = tempDir + '/SlicerMRBSaveRestoreCheckPath.mrml' - slicer.mrmlScene.SetURL(mrmlFilePath) - slicer.util.delayDisplay(f'Saving mrml file to {mrmlFilePath}, current url of scene is {slicer.mrmlScene.GetURL()}') - # saveScene just writes out the .mrml file - self.assertTrue(ioManager.saveScene(mrmlFilePath, screenShot)) - slicer.util.delayDisplay(f'Finished saving mrml file {mrmlFilePath}, mrml url is now {slicer.mrmlScene.GetURL()}\n\n\n') - slicer.util.delayDisplay('mrml root dir = %s' % slicer.mrmlScene.GetRootDirectory()) - # explicitly save MRHead - ioManager.addDefaultStorageNodes() - mrHeadVolume.GetStorageNode().WriteData(mrHeadVolume) - - # - # test all current file paths - # - self.checkAllFileNames(slicer.mrmlScene) - - # - # save the mrb - # - mrbFilePath= slicer.util.tempDirectory('__mrb__') + '/SlicerMRBSaveRestoreCheckPaths-1.mrb' - slicer.util.delayDisplay("\n\n\nSaving mrb to: %s" % mrbFilePath) - self.assertTrue( - ioManager.saveScene(mrbFilePath, screenShot) - ) - slicer.util.delayDisplay("Finished saving mrb\n\n\n") - - # - # test all current file paths - # - self.checkAllFileNames(slicer.mrmlScene) - - # - # reload the mrb and restore a scene view - # - slicer.mrmlScene.Clear(0) - slicer.util.delayDisplay('Now, reload the first saved MRB\n\n\n') - mrbLoaded = ioManager.loadScene(mrbFilePath) - # load can return false even though it succeeded - only fail if in strict mode - self.assertTrue( not self.strict or mrbLoaded ) - slicer.app.processEvents() - slicer.util.delayDisplay("\n\n\nFinished reloading the first saved MRB\n\n\n") - # - # test all current file paths - # - self.checkAllFileNames(slicer.mrmlScene) - - # - # Save it again - # - mrbFilePath= slicer.util.tempDirectory('__mrb__') + '/SlicerMRBSaveRestoreCheckPaths-2.mrb' - slicer.util.delayDisplay("Saving second mrb to: %s\n\n\n\n" % mrbFilePath) - self.assertTrue( - ioManager.saveScene(mrbFilePath, screenShot) - ) - slicer.util.delayDisplay("\n\n\nFinished second saving mrb %s" % mrbFilePath) - - # - # test all current file paths - # - self.checkAllFileNames(slicer.mrmlScene) - - slicer.util.delayDisplay("Test Finished") + def __init__(self, uniqueDirectory=True, strict=False): + """ + Tests the use of mrml and mrb save formats with volumes. + Checks that after reopening an MRB and trying to save it again that the file paths are all correct. + + uniqueDirectory: boolean about save directory + False to reuse standard dir name + True timestamps dir name + strict: boolean about how carefully to check result + True then check every detail + False then confirm basic operation, but allow non-critical issues to pass + """ + ScriptedLoadableModuleTest.__init__(self) + self.uniqueDirectory = uniqueDirectory + self.strict = strict + # this flag will need to be updated if the code in + # qSlicerSceneBundleReader::load + # is changed from the current behavior (delete expanded + # files after loading the MRB into Slicer) + self.mrbDeleteFilesAfterLoad = 1 + + def setUp(self): + slicer.mrmlScene.Clear(0) + + def runTest(self): + self.setUp() + self.test_SlicerMRBSaveRestoreCheckPaths() + + # + # find and return the storable node associated with this storage node. + # Returns None if not found. + # + def getStorableNode(self, storageNode): + numberOfStorableNodes = storageNode.GetScene().GetNumberOfNodesByClass('vtkMRMLStorableNode') + for n in range(numberOfStorableNodes): + storableNode = storageNode.GetScene().GetNthNodeByClass(n, 'vtkMRMLStorableNode') + if storableNode.GetStorageNodeID() == storageNode.GetID(): + return storableNode + return None + + # + # create and return a list of all file names found in storage nodes in + # the scene, not including those in scene views + # + def checkSceneFileNames(self, scene): + # first get the main scene storage nodes + numberOfStorageNodes = scene.GetNumberOfNodesByClass('vtkMRMLStorageNode') + for n in range(numberOfStorageNodes): + storageNode = scene.GetNthNodeByClass(n, 'vtkMRMLStorageNode') + storableNode = self.getStorableNode(storageNode) + if storageNode.GetSaveWithScene() and not storableNode.GetModifiedSinceRead(): + print('Checking storage node: ', storageNode.GetID()) + fileName = storageNode.GetFileName() + absFileName = storageNode.GetAbsoluteFilePath(fileName) + if not absFileName: + print('\tUnable to get absolute path for file name ', fileName) + self.numberOfFilesNotFound += 1 + elif not os.path.exists(absFileName): + print('\tfile does not exist: ', absFileName) + print('\t\tnon absolute file name = ', fileName) + print('\t\tscene of the node root dir = ', storageNode.GetScene().GetRootDirectory()) + if storableNode is not None: + print('\t\tstorable node name = ', storableNode.GetName()) + print('\t\tmodified since read = ', storableNode.GetModifiedSinceRead()) + else: + print('\t\tNo storable node found for this storage node') + # double check that it's not due to the unzipped files being deleted + if (not self.mrbDeleteFilesAfterLoad) or ('BundleSaveTemp' in absFileName): + self.numberOfFilesNotFound += 1 + else: + print('\t\tMRB files were deleted after load, not counting this file as not found for the purposes of this test') + else: + print('\tfile exists:', absFileName) + # check for the file list + numberOfFileNames = storageNode.GetNumberOfFileNames() + for n in range(numberOfFileNames): + fileName = storageNode.GetNthFileName(n) + absFileName = storageNode.GetAbsoluteFilePath(fileName) + if not os.path.exists(absFileName): + print('\t', n, 'th file list member does not exist: ', absFileName) + if (not self.mrbDeleteFilesAfterLoad) or ('BundleSaveTemp' in absFileName): + self.numberOfFilesNotFound += 1 + else: + print('\t\tMRB files were deleted after load, not counting this file as not found for the purposes of this test') + + def checkSceneViewFileNames(self, scene): + # check for any scene views + numberOfSceneViews = scene.GetNumberOfNodesByClass('vtkMRMLSceneViewNode') + slicer.util.delayDisplay("Number of scene views = " + str(numberOfSceneViews)) + if numberOfSceneViews == 0: + return + for n in range(numberOfSceneViews): + sceneViewNode = slicer.mrmlScene.GetNthNodeByClass(n, 'vtkMRMLSceneViewNode') + slicer.util.delayDisplay('\nChecking scene view ' + sceneViewNode.GetName() + ', id = ' + sceneViewNode.GetID()) + self.checkSceneFileNames(sceneViewNode.GetStoredScene()) + + def checkAllFileNames(self, scene): + slicer.util.delayDisplay("\n\nChecking all file names in scene") + self.numberOfFilesNotFound = 0 + self.checkSceneFileNames(scene) + self.checkSceneViewFileNames(scene) + if self.numberOfFilesNotFound != 0: + print('checkAllFilesNames: there are ', self.numberOfFilesNotFound, 'files that are missing from disk\n') + self.assertEqual(self.numberOfFilesNotFound, 0) + + def test_SlicerMRBSaveRestoreCheckPaths(self): + """ + Replicate the issue reported in bug 3956 where saving + and restoring an MRB file does not work. + """ + + print("Running SlicerMRBSaveRestoreCheckPaths Test case with:") + print("uniqueDirectory : %s" % self.uniqueDirectory) + print("strict : %s" % self.strict) + print("files deleted after load: %d" % self.mrbDeleteFilesAfterLoad) + + # + # first, get the volume data + # + print("Getting MR Head Volume") + import SampleData + mrHeadVolume = SampleData.downloadSample("MRHead") + + slicer.util.delayDisplay('Finished with download of volume') + + # + # test all current file paths + # + self.checkAllFileNames(slicer.mrmlScene) + + ioManager = slicer.app.ioManager() + # grab a testing screen shot + layoutManager = slicer.app.layoutManager() + widget = layoutManager.threeDWidget(0) + screenShot = ctk.ctkWidgetsUtils.grabWidget(widget) + + # + # the remote download leaves the volume in a temp directory and removes + # the storage node, commit the scene to get to a more stable starting + # point with the volume saved in a regular directory + # + tempDir = slicer.util.tempDirectory('__mrml__') + slicer.util.delayDisplay('Temp dir = %s ' % tempDir) + mrmlFilePath = tempDir + '/SlicerMRBSaveRestoreCheckPath.mrml' + slicer.mrmlScene.SetURL(mrmlFilePath) + slicer.util.delayDisplay(f'Saving mrml file to {mrmlFilePath}, current url of scene is {slicer.mrmlScene.GetURL()}') + # saveScene just writes out the .mrml file + self.assertTrue(ioManager.saveScene(mrmlFilePath, screenShot)) + slicer.util.delayDisplay(f'Finished saving mrml file {mrmlFilePath}, mrml url is now {slicer.mrmlScene.GetURL()}\n\n\n') + slicer.util.delayDisplay('mrml root dir = %s' % slicer.mrmlScene.GetRootDirectory()) + # explicitly save MRHead + ioManager.addDefaultStorageNodes() + mrHeadVolume.GetStorageNode().WriteData(mrHeadVolume) + + # + # test all current file paths + # + self.checkAllFileNames(slicer.mrmlScene) + + # + # save the mrb + # + mrbFilePath = slicer.util.tempDirectory('__mrb__') + '/SlicerMRBSaveRestoreCheckPaths-1.mrb' + slicer.util.delayDisplay("\n\n\nSaving mrb to: %s" % mrbFilePath) + self.assertTrue( + ioManager.saveScene(mrbFilePath, screenShot) + ) + slicer.util.delayDisplay("Finished saving mrb\n\n\n") + + # + # test all current file paths + # + self.checkAllFileNames(slicer.mrmlScene) + + # + # reload the mrb and restore a scene view + # + slicer.mrmlScene.Clear(0) + slicer.util.delayDisplay('Now, reload the first saved MRB\n\n\n') + mrbLoaded = ioManager.loadScene(mrbFilePath) + # load can return false even though it succeeded - only fail if in strict mode + self.assertTrue(not self.strict or mrbLoaded) + slicer.app.processEvents() + slicer.util.delayDisplay("\n\n\nFinished reloading the first saved MRB\n\n\n") + # + # test all current file paths + # + self.checkAllFileNames(slicer.mrmlScene) + + # + # Save it again + # + mrbFilePath = slicer.util.tempDirectory('__mrb__') + '/SlicerMRBSaveRestoreCheckPaths-2.mrb' + slicer.util.delayDisplay("Saving second mrb to: %s\n\n\n\n" % mrbFilePath) + self.assertTrue( + ioManager.saveScene(mrbFilePath, screenShot) + ) + slicer.util.delayDisplay("\n\n\nFinished second saving mrb %s" % mrbFilePath) + + # + # test all current file paths + # + self.checkAllFileNames(slicer.mrmlScene) + + slicer.util.delayDisplay("Test Finished") diff --git a/Applications/SlicerApp/Testing/Python/SlicerOptionDisableSettingsTest.py b/Applications/SlicerApp/Testing/Python/SlicerOptionDisableSettingsTest.py index 96f9c797382..3243cfb7011 100755 --- a/Applications/SlicerApp/Testing/Python/SlicerOptionDisableSettingsTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerOptionDisableSettingsTest.py @@ -30,79 +30,79 @@ def checkUserSettings(slicer_executable, common_args, keep_temporary_settings=False): - # Add a user setting - args = list(common_args) - args.extend(['--python-code', - 'slicer.app.userSettings().setValue("foo", "bar"); print("foo: %s" % slicer.app.userSettings().value("foo"))']) - assert runSlicerAndExit(slicer_executable, args)[0] == EXIT_SUCCESS - print("=> ok\n") - - # User settings previously added should: - # * NOT be set by detault - # * be set if '--keep-temporary-settings' is provided - args = list(common_args) - condition = 'is not None' - error = "Setting foo should NOT be set" - if keep_temporary_settings: - args.append('--keep-temporary-settings') - condition = '!= "bar"' - error = "Setting foo should be set to bar" - args.extend(['--python-code', - 'if slicer.app.userSettings().value("foo") ' + condition + ': raise Exception("' + error + '.")']) - assert runSlicerAndExit(slicer_executable, args)[0] == EXIT_SUCCESS - print("=> ok\n") + # Add a user setting + args = list(common_args) + args.extend(['--python-code', + 'slicer.app.userSettings().setValue("foo", "bar"); print("foo: %s" % slicer.app.userSettings().value("foo"))']) + assert runSlicerAndExit(slicer_executable, args)[0] == EXIT_SUCCESS + print("=> ok\n") + + # User settings previously added should: + # * NOT be set by detault + # * be set if '--keep-temporary-settings' is provided + args = list(common_args) + condition = 'is not None' + error = "Setting foo should NOT be set" + if keep_temporary_settings: + args.append('--keep-temporary-settings') + condition = '!= "bar"' + error = "Setting foo should be set to bar" + args.extend(['--python-code', + 'if slicer.app.userSettings().value("foo") ' + condition + ': raise Exception("' + error + '.")']) + assert runSlicerAndExit(slicer_executable, args)[0] == EXIT_SUCCESS + print("=> ok\n") def checkRevisionUserSettings(slicer_executable, common_args, keep_temporary_settings=False): - # Add a user revision setting - args = list(common_args) - args.extend(['--python-code', - 'slicer.app.revisionUserSettings().setValue("foo", "bar"); print("foo: %s" % slicer.app.revisionUserSettings().value("foo"))']) - assert runSlicerAndExit(slicer_executable, args)[0] == EXIT_SUCCESS - print("=> ok\n") - - # User revision settings previously added should: - # * NOT be set by detault - # * be set if '--keep-temporary-settings' is provided - args = list(common_args) - condition = 'is not None' - error = "Setting foo should NOT be set" - if keep_temporary_settings: - args.append('--keep-temporary-settings') - condition = '!= "bar"' - error = "Setting foo should be set to bar" - args.extend(['--python-code', - 'if slicer.app.revisionUserSettings().value("foo") ' + condition + ': raise Exception("' + error + '.")']) - assert runSlicerAndExit(slicer_executable, args)[0] == EXIT_SUCCESS - print("=> ok\n") + # Add a user revision setting + args = list(common_args) + args.extend(['--python-code', + 'slicer.app.revisionUserSettings().setValue("foo", "bar"); print("foo: %s" % slicer.app.revisionUserSettings().value("foo"))']) + assert runSlicerAndExit(slicer_executable, args)[0] == EXIT_SUCCESS + print("=> ok\n") + + # User revision settings previously added should: + # * NOT be set by detault + # * be set if '--keep-temporary-settings' is provided + args = list(common_args) + condition = 'is not None' + error = "Setting foo should NOT be set" + if keep_temporary_settings: + args.append('--keep-temporary-settings') + condition = '!= "bar"' + error = "Setting foo should be set to bar" + args.extend(['--python-code', + 'if slicer.app.revisionUserSettings().value("foo") ' + condition + ': raise Exception("' + error + '.")']) + assert runSlicerAndExit(slicer_executable, args)[0] == EXIT_SUCCESS + print("=> ok\n") def checkKeepTemporarySettingsWithoutDisableSettingsDisplayWarning(slicer_executable, common_args): - args = list(common_args) - args.remove('--disable-settings') - args.extend(['--keep-temporary-settings']) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - expectedMessage = "Argument '--keep-temporary-settings' requires '--settings-disabled' to be specified." - if expectedMessage not in stderr: - print(f"=> return code{returnCode}\n") - raise Exception(f"Warning [{expectedMessage}] not found in stderr [{stderr}]") - assert returnCode == EXIT_SUCCESS - print("=> ok\n") + args = list(common_args) + args.remove('--disable-settings') + args.extend(['--keep-temporary-settings']) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + expectedMessage = "Argument '--keep-temporary-settings' requires '--settings-disabled' to be specified." + if expectedMessage not in stderr: + print(f"=> return code{returnCode}\n") + raise Exception(f"Warning [{expectedMessage}] not found in stderr [{stderr}]") + assert returnCode == EXIT_SUCCESS + print("=> ok\n") if __name__ == '__main__': - if len(sys.argv) != 2: - print(os.path.basename(sys.argv[0]) +" /path/to/Slicer") - exit(EXIT_FAILURE) + if len(sys.argv) != 2: + print(os.path.basename(sys.argv[0]) + " /path/to/Slicer") + exit(EXIT_FAILURE) - slicer_executable = os.path.expanduser(sys.argv[1]) - common_args = ['--disable-settings', '--disable-modules', '--no-main-window'] + slicer_executable = os.path.expanduser(sys.argv[1]) + common_args = ['--disable-settings', '--disable-modules', '--no-main-window'] - checkUserSettings(slicer_executable, common_args) - checkRevisionUserSettings(slicer_executable, common_args) + checkUserSettings(slicer_executable, common_args) + checkRevisionUserSettings(slicer_executable, common_args) - checkUserSettings(slicer_executable, common_args, keep_temporary_settings=True) - checkRevisionUserSettings(slicer_executable, common_args, keep_temporary_settings=True) + checkUserSettings(slicer_executable, common_args, keep_temporary_settings=True) + checkRevisionUserSettings(slicer_executable, common_args, keep_temporary_settings=True) - checkKeepTemporarySettingsWithoutDisableSettingsDisplayWarning(slicer_executable, common_args) + checkKeepTemporarySettingsWithoutDisableSettingsDisplayWarning(slicer_executable, common_args) diff --git a/Applications/SlicerApp/Testing/Python/SlicerOptionIgnoreSlicerRCTest.py b/Applications/SlicerApp/Testing/Python/SlicerOptionIgnoreSlicerRCTest.py index 3d5b4ee3cdf..150aa099d50 100755 --- a/Applications/SlicerApp/Testing/Python/SlicerOptionIgnoreSlicerRCTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerOptionIgnoreSlicerRCTest.py @@ -31,61 +31,62 @@ if __name__ == '__main__': - debug = False # Set to True to: - # * display the path of the created slicerrc file - # * avoid deleting the slicerrc file + debug = False + # Set to True to: + # * display the path of the created slicerrc file + # * avoid deleting the slicerrc file - if len(sys.argv) != 2: - print(os.path.basename(sys.argv[0]) +" /path/to/Slicer") - exit(EXIT_FAILURE) + if len(sys.argv) != 2: + print(os.path.basename(sys.argv[0]) + " /path/to/Slicer") + exit(EXIT_FAILURE) - slicer_executable = os.path.expanduser(sys.argv[1]) - common_args = ['--disable-modules', '--no-main-window'] + slicer_executable = os.path.expanduser(sys.argv[1]) + common_args = ['--disable-modules', '--no-main-window'] - fd, slicerrc = tempfile.mkstemp() - assert os.path.isfile(slicerrc) - try: + fd, slicerrc = tempfile.mkstemp() + assert os.path.isfile(slicerrc) + try: - # Create a slicerrc file that creates SLICERRCTESTOUTPUT file when executed - with os.fdopen(fd, 'w') as file: - file.write("# Generated by SlicerOptionIgnoreSlicerRCTest.py\n"+\ - "import os\n"+\ - "fd = os.open(os.environ['SLICERRCTESTOUTPUT'], os.O_RDWR|os.O_CREAT)\n"+\ - "os.write(fd, 'Generated by SlicerOptionIgnoreSlicerRCTest.py test when slicerrc executed'.encode())\n"+\ - "os.close(fd)\n") + # Create a slicerrc file that creates SLICERRCTESTOUTPUT file when executed + with os.fdopen(fd, 'w') as file: + file.write("# Generated by SlicerOptionIgnoreSlicerRCTest.py\n" +\ + "import os\n" +\ + "fd = os.open(os.environ['SLICERRCTESTOUTPUT'], os.O_RDWR|os.O_CREAT)\n" +\ + "os.write(fd, 'Generated by SlicerOptionIgnoreSlicerRCTest.py test when slicerrc executed'.encode())\n" +\ + "os.close(fd)\n") - slicerrctestoutput = slicerrc + ".out" - os.environ['SLICERRC'] = slicerrc - os.environ['SLICERRCTESTOUTPUT'] = slicerrctestoutput - if debug: - print("SLICERRC=%s" % slicerrc) - print("SLICERRCTESTOUTPUT=%s" % slicerrctestoutput) + slicerrctestoutput = slicerrc + ".out" + os.environ['SLICERRC'] = slicerrc + os.environ['SLICERRCTESTOUTPUT'] = slicerrctestoutput + if debug: + print("SLICERRC=%s" % slicerrc) + print("SLICERRCTESTOUTPUT=%s" % slicerrctestoutput) - # Check that slicerrc file is loaded - args = list(common_args) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - assert os.path.isfile(slicerrctestoutput) - os.remove(slicerrctestoutput) - assert returnCode == EXIT_SUCCESS - print("=> ok\n") + # Check that slicerrc file is loaded + args = list(common_args) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert os.path.isfile(slicerrctestoutput) + os.remove(slicerrctestoutput) + assert returnCode == EXIT_SUCCESS + print("=> ok\n") - # Check that --ignore-slicerrc works - args = list(common_args) - args.extend(['--ignore-slicerrc']) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - assert not os.path.isfile(slicerrctestoutput) - assert returnCode == EXIT_SUCCESS - print("=> ok\n") + # Check that --ignore-slicerrc works + args = list(common_args) + args.extend(['--ignore-slicerrc']) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert not os.path.isfile(slicerrctestoutput) + assert returnCode == EXIT_SUCCESS + print("=> ok\n") - # Check that --testing implies --ignore-slicerrc - args = list(common_args) - args.extend(['--testing']) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - assert not os.path.isfile(slicerrctestoutput) - assert returnCode == EXIT_SUCCESS - print("=> ok\n") - finally: - if not debug: - os.remove(slicerrc) - if os.path.isfile(slicerrctestoutput): - os.remove(slicerrctestoutput) + # Check that --testing implies --ignore-slicerrc + args = list(common_args) + args.extend(['--testing']) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert not os.path.isfile(slicerrctestoutput) + assert returnCode == EXIT_SUCCESS + print("=> ok\n") + finally: + if not debug: + os.remove(slicerrc) + if os.path.isfile(slicerrctestoutput): + os.remove(slicerrctestoutput) diff --git a/Applications/SlicerApp/Testing/Python/SlicerOptionModulesToIgnoreTest.py b/Applications/SlicerApp/Testing/Python/SlicerOptionModulesToIgnoreTest.py index 64f5167f6dd..5dc4276d718 100755 --- a/Applications/SlicerApp/Testing/Python/SlicerOptionModulesToIgnoreTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerOptionModulesToIgnoreTest.py @@ -32,88 +32,89 @@ if __name__ == '__main__': - debug = False # Set to True to: - # * display the path of the created extension_dir - # * avoid deleting the extension_dir + debug = False + # Set to True to: + # * display the path of the created extension_dir + # * avoid deleting the extension_dir - if len(sys.argv) != 3: - print(os.path.basename(sys.argv[0]) +" /path/to/Slicer /path/to/slicerExtensionWizard") - exit(EXIT_FAILURE) + if len(sys.argv) != 3: + print(os.path.basename(sys.argv[0]) + " /path/to/Slicer /path/to/slicerExtensionWizard") + exit(EXIT_FAILURE) - slicer_executable = os.path.expanduser(sys.argv[1]) - slicer_extension_wizard = os.path.expanduser(sys.argv[2]) + slicer_executable = os.path.expanduser(sys.argv[1]) + slicer_extension_wizard = os.path.expanduser(sys.argv[2]) - common_args = ['--testing', '--disable-builtin-modules', '--no-main-window'] + common_args = ['--testing', '--disable-builtin-modules', '--no-main-window'] - # Prerequisites: - # * create temporary extension directory - # * create an extension with four scripted modules: A, B, C and D + # Prerequisites: + # * create temporary extension directory + # * create an extension with four scripted modules: A, B, C and D - extension_dir = tempfile.mkdtemp() - assert os.path.isdir(extension_dir) - if debug: - print("extension_dir: %s" % extension_dir) + extension_dir = tempfile.mkdtemp() + assert os.path.isdir(extension_dir) + if debug: + print("extension_dir: %s" % extension_dir) - moduleNames = ['A', 'B', 'C', 'D'] + moduleNames = ['A', 'B', 'C', 'D'] - additional_module_paths = ['--additional-module-paths'] - additional_module_paths.extend([f'{extension_dir}/Test/{moduleName}' for moduleName in moduleNames]) + additional_module_paths = ['--additional-module-paths'] + additional_module_paths.extend([f'{extension_dir}/Test/{moduleName}' for moduleName in moduleNames]) - args = ['--create', 'Test'] - for moduleName in moduleNames: - args.extend(['--addModule', 'scripted:%s' % moduleName]) - args.extend([extension_dir]) - (returnCode, stdout, stderr) = run(slicer_extension_wizard, args, shell=True) - assert returnCode == EXIT_SUCCESS - print("=> ok\n") - - try: - - # Check that the modules are loaded - args = list(common_args) - args.extend(['--python-code', 'assert len([module for module in dir(slicer.moduleNames) if module in ["A", "B", "C", "D"]]) == 4, "Failed to load modules"']) - args.extend(additional_module_paths) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - assert returnCode == EXIT_SUCCESS - print("=> ok\n") - - # Update settings disabling module B - args = list(common_args) - args.extend(['--keep-temporary-settings']) - args.extend(['--python-code', 'slicer.app.moduleManager().factoryManager().modulesToIgnore = ["B"]']) - args.extend(additional_module_paths) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - assert returnCode == EXIT_SUCCESS - print("=> ok\n") - - # Run application to check that module B is disabled - args = list(common_args) - args.extend(['--keep-temporary-settings']) - args.extend(['--python-code', 'assert len([module for module in dir(slicer.moduleNames) if module in ["A", "C", "D"]]) == 3, "Failed to disable modules"']) - args.extend(additional_module_paths) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - assert returnCode == EXIT_SUCCESS - print("=> ok\n") - - # Run application given "--modules-to-ignore A,D" and check that A, D and B are disabled - args = list(common_args) - args.extend(['--keep-temporary-settings']) - args.extend(['--modules-to-ignore', 'A,D']) - args.extend(['--python-code', 'assert len([module for module in dir(slicer.moduleNames) if module in ["C"]]) == 1, "Failed to disable modules"']) - args.extend(additional_module_paths) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - assert returnCode == EXIT_SUCCESS - print("=> ok\n") - - # Run application to check that only B remains disabled - args = list(common_args) - args.extend(['--keep-temporary-settings']) - args.extend(['--python-code', 'assert len([module for module in dir(slicer.moduleNames) if module in ["A", "C", "D"]]) == 3, "Failed to disable modules"']) - args.extend(additional_module_paths) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + args = ['--create', 'Test'] + for moduleName in moduleNames: + args.extend(['--addModule', 'scripted:%s' % moduleName]) + args.extend([extension_dir]) + (returnCode, stdout, stderr) = run(slicer_extension_wizard, args, shell=True) assert returnCode == EXIT_SUCCESS print("=> ok\n") - finally: - if not debug: - shutil.rmtree(extension_dir) + try: + + # Check that the modules are loaded + args = list(common_args) + args.extend(['--python-code', 'assert len([module for module in dir(slicer.moduleNames) if module in ["A", "B", "C", "D"]]) == 4, "Failed to load modules"']) + args.extend(additional_module_paths) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert returnCode == EXIT_SUCCESS + print("=> ok\n") + + # Update settings disabling module B + args = list(common_args) + args.extend(['--keep-temporary-settings']) + args.extend(['--python-code', 'slicer.app.moduleManager().factoryManager().modulesToIgnore = ["B"]']) + args.extend(additional_module_paths) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert returnCode == EXIT_SUCCESS + print("=> ok\n") + + # Run application to check that module B is disabled + args = list(common_args) + args.extend(['--keep-temporary-settings']) + args.extend(['--python-code', 'assert len([module for module in dir(slicer.moduleNames) if module in ["A", "C", "D"]]) == 3, "Failed to disable modules"']) + args.extend(additional_module_paths) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert returnCode == EXIT_SUCCESS + print("=> ok\n") + + # Run application given "--modules-to-ignore A,D" and check that A, D and B are disabled + args = list(common_args) + args.extend(['--keep-temporary-settings']) + args.extend(['--modules-to-ignore', 'A,D']) + args.extend(['--python-code', 'assert len([module for module in dir(slicer.moduleNames) if module in ["C"]]) == 1, "Failed to disable modules"']) + args.extend(additional_module_paths) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert returnCode == EXIT_SUCCESS + print("=> ok\n") + + # Run application to check that only B remains disabled + args = list(common_args) + args.extend(['--keep-temporary-settings']) + args.extend(['--python-code', 'assert len([module for module in dir(slicer.moduleNames) if module in ["A", "C", "D"]]) == 3, "Failed to disable modules"']) + args.extend(additional_module_paths) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert returnCode == EXIT_SUCCESS + print("=> ok\n") + + finally: + if not debug: + shutil.rmtree(extension_dir) diff --git a/Applications/SlicerApp/Testing/Python/SlicerOrientationSelectorTest.py b/Applications/SlicerApp/Testing/Python/SlicerOrientationSelectorTest.py index b92e1a38d5a..f2683f60dc8 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerOrientationSelectorTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerOrientationSelectorTest.py @@ -9,24 +9,24 @@ # class SlicerOrientationSelectorTest(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 = "Create ruler crash (Issue 4199)" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = [] - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)", - "Davide Punzo (Kapteyn astronomical institute)"] # replace with "Firstname Lastname (Organization)" - self.parent.helpText = """This test has been added to check that + """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 = "Create ruler crash (Issue 4199)" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = [] + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)", + "Davide Punzo (Kapteyn astronomical institute)"] # replace with "Firstname Lastname (Organization)" + self.parent.helpText = """This test has been added to check that orientation selector is correctly updated when updating the SliceToRAS matrix. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. and was partially funded by NIH grant 1U24CA194354-01. - """ # replace with organization, grant and thanks. + """ # replace with organization, grant and thanks. # @@ -34,12 +34,12 @@ def __init__(self, parent): # class SlicerOrientationSelectorTestWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -47,85 +47,85 @@ def setup(self): # class SlicerOrientationSelectorTestLogic(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 + """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 """ - 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 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 -class SlicerOrientationSelectorTestTest(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. +class SlicerOrientationSelectorTestTest(ScriptedLoadableModuleTest): """ - self.setUp() - self.test_SlicerOrientationSelectorTest() - - def test_SlicerOrientationSelectorTest(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. + 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 """ - logic = SlicerOrientationSelectorTestLogic() - - self.delayDisplay("Starting the test") - import SampleData - mrHeadVolume = SampleData.downloadSample("MRHead") - - slicer.util.selectModule('Reformat') - - # Select Red slice - widget = slicer.modules.reformat.widgetRepresentation() - sliceNodeSelector = slicer.util.findChildren(widget, "SliceNodeSelector")[0] - sliceNodeSelector.setCurrentNodeID("vtkMRMLSliceNodeRed") - - # Set LR value using Reformat module - lrslider = slicer.util.findChildren(widget, "LRSlider")[0] - lrslider.value = 1 - - # Get reference to the Red slice controller - lm = slicer.app.layoutManager() - sliceWidget = lm.sliceWidget("Red") - sliceOrientationSelector = slicer.util.findChildren(sliceWidget, "SliceOrientationSelector")[0] - - # Check orientations associated with orientations selector - orientations = [sliceOrientationSelector.itemText(idx) for idx in range(sliceOrientationSelector.count)] - expectedOrientations = ['Axial', 'Sagittal', 'Coronal', 'Reformat'] - if orientations != expectedOrientations: - raise Exception(f'Problem with orientation selector\norientations: {orientations}\nexpectedOrientations: {expectedOrientations}') - - self.delayDisplay('Test passed!') + 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_SlicerOrientationSelectorTest() + + def test_SlicerOrientationSelectorTest(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. + """ + + logic = SlicerOrientationSelectorTestLogic() + + self.delayDisplay("Starting the test") + import SampleData + mrHeadVolume = SampleData.downloadSample("MRHead") + + slicer.util.selectModule('Reformat') + + # Select Red slice + widget = slicer.modules.reformat.widgetRepresentation() + sliceNodeSelector = slicer.util.findChildren(widget, "SliceNodeSelector")[0] + sliceNodeSelector.setCurrentNodeID("vtkMRMLSliceNodeRed") + + # Set LR value using Reformat module + lrslider = slicer.util.findChildren(widget, "LRSlider")[0] + lrslider.value = 1 + + # Get reference to the Red slice controller + lm = slicer.app.layoutManager() + sliceWidget = lm.sliceWidget("Red") + sliceOrientationSelector = slicer.util.findChildren(sliceWidget, "SliceOrientationSelector")[0] + + # Check orientations associated with orientations selector + orientations = [sliceOrientationSelector.itemText(idx) for idx in range(sliceOrientationSelector.count)] + expectedOrientations = ['Axial', 'Sagittal', 'Coronal', 'Reformat'] + if orientations != expectedOrientations: + raise Exception(f'Problem with orientation selector\norientations: {orientations}\nexpectedOrientations: {expectedOrientations}') + + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/SlicerRestoreSceneViewCrashIssue3445.py b/Applications/SlicerApp/Testing/Python/SlicerRestoreSceneViewCrashIssue3445.py index 39d296c8386..17bef10c711 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerRestoreSceneViewCrashIssue3445.py +++ b/Applications/SlicerApp/Testing/Python/SlicerRestoreSceneViewCrashIssue3445.py @@ -8,25 +8,25 @@ # class SlicerRestoreSceneViewCrashIssue3445(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 = "SceneView restore crash (Issue 3445)" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = [] - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"] # replace with "Firstname Lastname (Organization)" - self.parent.helpText = """This test has been added to check that + """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 = "SceneView restore crash (Issue 3445)" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = [] + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"] # replace with "Firstname Lastname (Organization)" + self.parent.helpText = """This test has been added to check that Slicer does not crash while restoring scene view associated with BrainAtlas2012.mrb. Problem has been documented in issue #3445. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. and was partially funded by NIH grant 1U24CA194354-01. - """ # replace with organization, grant and thanks. + """ # replace with organization, grant and thanks. # @@ -34,12 +34,12 @@ def __init__(self, parent): # class SlicerRestoreSceneViewCrashIssue3445Widget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -47,80 +47,80 @@ def setup(self): # class SlicerRestoreSceneViewCrashIssue3445Logic(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 - """ - pass + """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 + """ + pass class SlicerRestoreSceneViewCrashIssue3445Test(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_SlicerRestoreSceneViewCrashIssue3445() - - def test_SlicerRestoreSceneViewCrashIssue3445(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. + 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 """ - logic = SlicerRestoreSceneViewCrashIssue3445Logic() + 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_SlicerRestoreSceneViewCrashIssue3445() + + def test_SlicerRestoreSceneViewCrashIssue3445(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. + """ + + logic = SlicerRestoreSceneViewCrashIssue3445Logic() - self.delayDisplay("Starting the test") + self.delayDisplay("Starting the test") - # - # first, get some data - # - import SampleData - filePath = SampleData.downloadFromURL( - fileNames='BrainAtlas2012.mrb', - loadFiles=True, - uris=TESTING_DATA_URL + 'SHA256/688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1', - checksums='SHA256:688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1')[0] + # + # first, get some data + # + import SampleData + filePath = SampleData.downloadFromURL( + fileNames='BrainAtlas2012.mrb', + loadFiles=True, + uris=TESTING_DATA_URL + 'SHA256/688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1', + checksums='SHA256:688ebcc6f45989795be2bcdc6b8b5bfc461f1656d677ed3ddef8c313532687f1')[0] - self.delayDisplay('Finished with download') + self.delayDisplay('Finished with download') - ioManager = slicer.app.ioManager() + ioManager = slicer.app.ioManager() - ioManager.loadFile(filePath) + ioManager.loadFile(filePath) - slicer.mrmlScene.Clear(0) - slicer.util.selectModule('Data') - slicer.util.selectModule('Models') + slicer.mrmlScene.Clear(0) + slicer.util.selectModule('Data') + slicer.util.selectModule('Models') - ioManager.loadFile(filePath) - slicer.mrmlScene.Clear(0) + ioManager.loadFile(filePath) + slicer.mrmlScene.Clear(0) - ioManager.loadFile(filePath) - ioManager.loadFile(filePath) + ioManager.loadFile(filePath) + ioManager.loadFile(filePath) - sceneViewNode = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLSceneViewNode') - sceneViewNode.RestoreScene() + sceneViewNode = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLSceneViewNode') + sceneViewNode.RestoreScene() - # If test reach this point without crashing it is a success + # If test reach this point without crashing it is a success - self.delayDisplay('Test passed!') + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/SlicerSceneObserverTest.py b/Applications/SlicerApp/Testing/Python/SlicerSceneObserverTest.py index cf4d0a27653..5f5e9693068 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerSceneObserverTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerSceneObserverTest.py @@ -4,24 +4,24 @@ class testClass: - """ Check that slicer exits correctly after adding an observer to the mrml scene - """ + """ Check that slicer exits correctly after adding an observer to the mrml scene + """ - def callback(self, caller, event): - print(f'Got {event} from {caller}') + def callback(self, caller, event): + print(f'Got {event} from {caller}') - def setUp(self): - print("Adding observer to the scene") - self.tag = slicer.mrmlScene.AddObserver(vtk.vtkCommand.ModifiedEvent, self.callback) - print("Modify the scene") - slicer.mrmlScene.Modified() + def setUp(self): + print("Adding observer to the scene") + self.tag = slicer.mrmlScene.AddObserver(vtk.vtkCommand.ModifiedEvent, self.callback) + print("Modify the scene") + slicer.mrmlScene.Modified() class SlicerSceneObserverTest(unittest.TestCase): - def setUp(self): - pass + def setUp(self): + pass - def test_testClass(self): - test = testClass() - test.setUp() + def test_testClass(self): + test = testClass() + test.setUp() diff --git a/Applications/SlicerApp/Testing/Python/SlicerScriptedFileReaderWriterTest.py b/Applications/SlicerApp/Testing/Python/SlicerScriptedFileReaderWriterTest.py index 9e7e2eb48cb..4700147a135 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerScriptedFileReaderWriterTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerScriptedFileReaderWriterTest.py @@ -6,168 +6,168 @@ class SlicerScriptedFileReaderWriterTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = 'SlicerScriptedFileReaderWriterTest' - parent.categories = ['Testing.TestCases'] - parent.dependencies = [] - parent.contributors = ["Andras Lasso (PerkLab, Queen's)"] - parent.helpText = ''' + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = 'SlicerScriptedFileReaderWriterTest' + parent.categories = ['Testing.TestCases'] + parent.dependencies = [] + parent.contributors = ["Andras Lasso (PerkLab, Queen's)"] + parent.helpText = ''' This module is used to test qSlicerScriptedFileReader and qSlicerScriptedFileWriter classes. ''' - parent.acknowledgementText = ''' + parent.acknowledgementText = ''' This file was originally developed by Andras Lasso, PerkLab. ''' - self.parent = parent + self.parent = parent class SlicerScriptedFileReaderWriterTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Default reload&test widgets are enough. - # Note that reader and writer is not reloaded. + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Default reload&test widgets are enough. + # Note that reader and writer is not reloaded. class SlicerScriptedFileReaderWriterTestFileReader: - def __init__(self, parent): - self.parent = parent + def __init__(self, parent): + self.parent = parent - def description(self): - return 'My file type' + def description(self): + return 'My file type' - def fileType(self): - return 'MyFileType' + def fileType(self): + return 'MyFileType' - def extensions(self): - return ['My file type (*.mft)'] + def extensions(self): + return ['My file type (*.mft)'] - def canLoadFile(self, filePath): - # Only enable this reader in testing mode - if not slicer.app.testingEnabled(): - return False + def canLoadFile(self, filePath): + # Only enable this reader in testing mode + if not slicer.app.testingEnabled(): + return False - firstLine = '' - with open(filePath) as f: - firstLine = f.readline() - validFile = 'magic' in firstLine - return validFile + firstLine = '' + with open(filePath) as f: + firstLine = f.readline() + validFile = 'magic' in firstLine + return validFile - def load(self, properties): - try: - filePath = properties['fileName'] + def load(self, properties): + try: + filePath = properties['fileName'] - # Get node base name from filename - if 'name' in properties.keys(): - baseName = properties['name'] - else: - baseName = os.path.splitext(os.path.basename(filePath))[0] - baseName = slicer.mrmlScene.GenerateUniqueName(baseName) + # Get node base name from filename + if 'name' in properties.keys(): + baseName = properties['name'] + else: + baseName = os.path.splitext(os.path.basename(filePath))[0] + baseName = slicer.mrmlScene.GenerateUniqueName(baseName) - # Read file content - with open (filePath) as myfile: - data = myfile.readlines() + # Read file content + with open(filePath) as myfile: + data = myfile.readlines() - # Check if file is valid - firstLine = data[0].rstrip() - if firstLine != 'magic': - raise ValueError('Cannot read file, it is expected to start with magic') + # Check if file is valid + firstLine = data[0].rstrip() + if firstLine != 'magic': + raise ValueError('Cannot read file, it is expected to start with magic') - # Load content into new node - loadedNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTextNode', baseName) - loadedNode.SetText(''.join(data[1:])) + # Load content into new node + loadedNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTextNode', baseName) + loadedNode.SetText(''.join(data[1:])) - # Uncomment the next line to display a warning message to the user. - # self.parent.userMessages().AddMessage(vtk.vtkCommand.WarningEvent, "This is a warning message") + # Uncomment the next line to display a warning message to the user. + # self.parent.userMessages().AddMessage(vtk.vtkCommand.WarningEvent, "This is a warning message") - except Exception as e: - logging.error('Failed to load file: '+str(e)) - import traceback - traceback.print_exc() - return False + except Exception as e: + logging.error('Failed to load file: ' + str(e)) + import traceback + traceback.print_exc() + return False - self.parent.loadedNodes = [loadedNode.GetID()] - return True + self.parent.loadedNodes = [loadedNode.GetID()] + return True class SlicerScriptedFileReaderWriterTestFileWriter: - def __init__(self, parent): - self.parent = parent + def __init__(self, parent): + self.parent = parent - def description(self): - return 'My file type' + def description(self): + return 'My file type' - def fileType(self): - return 'MyFileType' + def fileType(self): + return 'MyFileType' - def extensions(self, obj): - return ['My file type (*.mft)'] + def extensions(self, obj): + return ['My file type (*.mft)'] - def canWriteObject(self, obj): - # Only enable this writer in testing mode - if not slicer.app.testingEnabled(): - return False + def canWriteObject(self, obj): + # Only enable this writer in testing mode + if not slicer.app.testingEnabled(): + return False - return bool(obj.IsA("vtkMRMLTextNode")) + return bool(obj.IsA("vtkMRMLTextNode")) - def write(self, properties): - try: + def write(self, properties): + try: - # Get node - node = slicer.mrmlScene.GetNodeByID(properties["nodeID"]) + # Get node + node = slicer.mrmlScene.GetNodeByID(properties["nodeID"]) - # Write node content to file - filePath = properties['fileName'] - with open (filePath, 'w') as myfile: - myfile.write("magic\n") - myfile.write(node.GetText()) + # Write node content to file + filePath = properties['fileName'] + with open(filePath, 'w') as myfile: + myfile.write("magic\n") + myfile.write(node.GetText()) - except Exception as e: - logging.error('Failed to write file: '+str(e)) - import traceback - traceback.print_exc() - return False + except Exception as e: + logging.error('Failed to write file: ' + str(e)) + import traceback + traceback.print_exc() + return False - self.parent.writtenNodes = [node.GetID()] - return True + self.parent.writtenNodes = [node.GetID()] + return True class SlicerScriptedFileReaderWriterTestTest(ScriptedLoadableModuleTest): - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_Writer() - self.test_Reader() - self.tearDown() - self.delayDisplay('Testing complete') - - def setUp(self): - self.tempDir = slicer.util.tempDirectory() - logging.info("tempDir: " + self.tempDir) - self.textInNode = "This is\nsome example test" - self.validFilename = self.tempDir + "/tempSlicerScriptedFileReaderWriterTestValid.mft" - self.invalidFilename = self.tempDir + "/tempSlicerScriptedFileReaderWriterTestInvalid.mft" - slicer.mrmlScene.Clear() - - def tearDown(self): - import shutil - shutil.rmtree(self.tempDir, True) - - def test_WriterReader(self): - # Writer and reader tests are put in the same function to ensure - # that writing is done before reading (it generates input data for reading). - - self.delayDisplay('Testing node writer') - slicer.mrmlScene.Clear() - textNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTextNode') - textNode.SetText(self.textInNode) - self.assertTrue(slicer.util.saveNode(textNode, self.validFilename, {'fileType': 'MyFileType'})) - - self.delayDisplay('Testing node reader') - slicer.mrmlScene.Clear() - loadedNode = slicer.util.loadNodeFromFile(self.validFilename, 'MyFileType') - self.assertIsNotNone(loadedNode) - self.assertTrue(loadedNode.IsA('vtkMRMLTextNode')) - self.assertEqual(loadedNode.GetText(), self.textInNode) + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_Writer() + self.test_Reader() + self.tearDown() + self.delayDisplay('Testing complete') + + def setUp(self): + self.tempDir = slicer.util.tempDirectory() + logging.info("tempDir: " + self.tempDir) + self.textInNode = "This is\nsome example test" + self.validFilename = self.tempDir + "/tempSlicerScriptedFileReaderWriterTestValid.mft" + self.invalidFilename = self.tempDir + "/tempSlicerScriptedFileReaderWriterTestInvalid.mft" + slicer.mrmlScene.Clear() + + def tearDown(self): + import shutil + shutil.rmtree(self.tempDir, True) + + def test_WriterReader(self): + # Writer and reader tests are put in the same function to ensure + # that writing is done before reading (it generates input data for reading). + + self.delayDisplay('Testing node writer') + slicer.mrmlScene.Clear() + textNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTextNode') + textNode.SetText(self.textInNode) + self.assertTrue(slicer.util.saveNode(textNode, self.validFilename, {'fileType': 'MyFileType'})) + + self.delayDisplay('Testing node reader') + slicer.mrmlScene.Clear() + loadedNode = slicer.util.loadNodeFromFile(self.validFilename, 'MyFileType') + self.assertIsNotNone(loadedNode) + self.assertTrue(loadedNode.IsA('vtkMRMLTextNode')) + self.assertEqual(loadedNode.GetText(), self.textInNode) diff --git a/Applications/SlicerApp/Testing/Python/SlicerStartupCompletedTest.py b/Applications/SlicerApp/Testing/Python/SlicerStartupCompletedTest.py index 830bf84ae16..1972d636a92 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerStartupCompletedTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerStartupCompletedTest.py @@ -36,55 +36,56 @@ if __name__ == '__main__': - debug = False # Set to True to: - # * display the path of the expected test output file - # * avoid deleting the created temporary directory - - if len(sys.argv) != 2: - print(os.path.basename(sys.argv[0]) +" /path/to/Slicer") - exit(EXIT_FAILURE) - - temporaryModuleDirPath = tempfile.mkdtemp().replace('\\','/') - try: - - # Copy helper module that creates a file when startup completed event is received - currentDirPath = os.path.dirname(__file__).replace('\\','/') - from shutil import copyfile - copyfile(currentDirPath+'/SlicerStartupCompletedTestHelperModule.py', - temporaryModuleDirPath+'/SlicerStartupCompletedTestHelperModule.py') - - slicer_executable = os.path.expanduser(sys.argv[1]) - common_args = [ - '--testing', - '--no-splash', - '--disable-builtin-cli-modules', - '--disable-builtin-loadable-modules', - '--disable-builtin-scripted-loadable-modules', - '--additional-module-path', temporaryModuleDirPath, - ] - - test_output_file = temporaryModuleDirPath + "/StartupCompletedTest.out" - os.environ['SLICER_STARTUP_COMPLETED_TEST_OUTPUT'] = test_output_file - if debug: - print("SLICER_STARTUP_COMPLETED_TEST_OUTPUT=%s" % test_output_file) - - # Test startupCompleted with main window - args = list(common_args) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - assert(os.path.isfile(test_output_file)) - os.remove(test_output_file) - assert(returnCode == EXIT_SUCCESS) - print("Test startupCompleted with main window - passed\n") - - # Test startupCompleted without main window - args = list(common_args) - args.extend(['--no-main-window']) - (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) - assert(os.path.isfile(test_output_file)) - assert(returnCode == EXIT_SUCCESS) - print("Test startupCompleted without main window - passed\n") - - finally: - if not debug: - import shutil - shutil.rmtree(temporaryModuleDirPath) + debug = False + # Set to True to: + # * display the path of the expected test output file + # * avoid deleting the created temporary directory + + if len(sys.argv) != 2: + print(os.path.basename(sys.argv[0]) + " /path/to/Slicer") + exit(EXIT_FAILURE) + + temporaryModuleDirPath = tempfile.mkdtemp().replace('\\', '/') + try: + + # Copy helper module that creates a file when startup completed event is received + currentDirPath = os.path.dirname(__file__).replace('\\', '/') + from shutil import copyfile + copyfile(currentDirPath + '/SlicerStartupCompletedTestHelperModule.py', + temporaryModuleDirPath + '/SlicerStartupCompletedTestHelperModule.py') + + slicer_executable = os.path.expanduser(sys.argv[1]) + common_args = [ + '--testing', + '--no-splash', + '--disable-builtin-cli-modules', + '--disable-builtin-loadable-modules', + '--disable-builtin-scripted-loadable-modules', + '--additional-module-path', temporaryModuleDirPath, + ] + + test_output_file = temporaryModuleDirPath + "/StartupCompletedTest.out" + os.environ['SLICER_STARTUP_COMPLETED_TEST_OUTPUT'] = test_output_file + if debug: + print("SLICER_STARTUP_COMPLETED_TEST_OUTPUT=%s" % test_output_file) + + # Test startupCompleted with main window + args = list(common_args) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert(os.path.isfile(test_output_file)) + os.remove(test_output_file) + assert(returnCode == EXIT_SUCCESS) + print("Test startupCompleted with main window - passed\n") + + # Test startupCompleted without main window + args = list(common_args) + args.extend(['--no-main-window']) + (returnCode, stdout, stderr) = runSlicerAndExit(slicer_executable, args) + assert(os.path.isfile(test_output_file)) + assert(returnCode == EXIT_SUCCESS) + print("Test startupCompleted without main window - passed\n") + + finally: + if not debug: + import shutil + shutil.rmtree(temporaryModuleDirPath) diff --git a/Applications/SlicerApp/Testing/Python/SlicerStartupCompletedTestHelperModule.py b/Applications/SlicerApp/Testing/Python/SlicerStartupCompletedTestHelperModule.py index 38e2e3d3d14..1b5ba650f13 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerStartupCompletedTestHelperModule.py +++ b/Applications/SlicerApp/Testing/Python/SlicerStartupCompletedTestHelperModule.py @@ -6,25 +6,25 @@ class SlicerStartupCompletedTestHelperModule(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "SlicerStartupCompletedTest" - self.parent.categories = ["Testing.TestCases"] - self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware), Andras Lasso (PerkLab)"] - self.parent.widgetRepresentationCreationEnabled = False - - self.testOutputFileName = os.environ['SLICER_STARTUP_COMPLETED_TEST_OUTPUT'] - if os.path.isfile(self.testOutputFileName): - os.remove(self.testOutputFileName) - - slicer.app.connect("startupCompleted()", self.onStartupCompleted) - - print("SlicerStartupCompletedTestHelperModule initialized") - - def onStartupCompleted(self): - print("StartupCompleted emitted") - import os - fd = os.open(self.testOutputFileName, os.O_RDWR|os.O_CREAT) - os.write(fd, 'SlicerStartupCompletedTestHelperModule.py generated this file') - os.write(fd, 'when slicer.app emitted startupCompleted() signal\n') - os.close(fd) + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "SlicerStartupCompletedTest" + self.parent.categories = ["Testing.TestCases"] + self.parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware), Andras Lasso (PerkLab)"] + self.parent.widgetRepresentationCreationEnabled = False + + self.testOutputFileName = os.environ['SLICER_STARTUP_COMPLETED_TEST_OUTPUT'] + if os.path.isfile(self.testOutputFileName): + os.remove(self.testOutputFileName) + + slicer.app.connect("startupCompleted()", self.onStartupCompleted) + + print("SlicerStartupCompletedTestHelperModule initialized") + + def onStartupCompleted(self): + print("StartupCompleted emitted") + import os + fd = os.open(self.testOutputFileName, os.O_RDWR | os.O_CREAT) + os.write(fd, 'SlicerStartupCompletedTestHelperModule.py generated this file') + os.write(fd, 'when slicer.app emitted startupCompleted() signal\n') + os.close(fd) diff --git a/Applications/SlicerApp/Testing/Python/SlicerTransformInteractionTest1.py b/Applications/SlicerApp/Testing/Python/SlicerTransformInteractionTest1.py index 6638848eba8..0c0b12f141e 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerTransformInteractionTest1.py +++ b/Applications/SlicerApp/Testing/Python/SlicerTransformInteractionTest1.py @@ -14,18 +14,18 @@ class SlicerTransformInteractionTest1(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 = "TransformInteractionTest" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = [] - self.parent.contributors = ["Johan Andruejol (Kitware Inc)"] - self.parent.helpText = """This test has been added to check the transform interaction.""" - self.parent.acknowledgementText = """ + """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 = "TransformInteractionTest" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = [] + self.parent.contributors = ["Johan Andruejol (Kitware Inc)"] + self.parent.helpText = """This test has been added to check the transform interaction.""" + self.parent.acknowledgementText = """ This file was originally developed by Johan Andruejol, Kitware Inc. """ @@ -35,11 +35,11 @@ def __init__(self, parent): class SlicerTransformInteractionTest1Widget(ScriptedLoadableModuleWidget): - """ - """ + """ + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # # SlicerTransformInteractionTest1Logic @@ -47,566 +47,566 @@ def setup(self): class SlicerTransformInteractionTest1Logic(ScriptedLoadableModuleLogic): - """ - """ - - def addTransform(self): - """Create and add a transform node with a display node to the - mrmlScene. - Returns the transform node and its display node """ - transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) - transformDisplayNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformDisplayNode()) - transformNode.SetAndObserveDisplayNodeID(transformDisplayNode.GetID()) - return transformNode, transformDisplayNode - - def getModel3DDisplayableManager(self): - threeDViewWidget = slicer.app.layoutManager().threeDWidget(0) - managers = vtk.vtkCollection() - threeDViewWidget.getDisplayableManagers(managers) - for i in range(managers.GetNumberOfItems()): - obj = managers.GetItemAsObject(i) - if obj.IsA('vtkMRMLLinearTransformsDisplayableManager3D'): - return obj - return None - - -class SlicerTransformInteractionTest1Test(ScriptedLoadableModuleTest): - """ - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ - slicer.mrmlScene.Clear(0) - def assertTransform(self, t, expected): - return self.assertMatrix(t.GetMatrix(), expected) + def addTransform(self): + """Create and add a transform node with a display node to the + mrmlScene. + Returns the transform node and its display node + """ + transformNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformNode()) + transformDisplayNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLTransformDisplayNode()) + transformNode.SetAndObserveDisplayNodeID(transformDisplayNode.GetID()) + return transformNode, transformDisplayNode + + def getModel3DDisplayableManager(self): + threeDViewWidget = slicer.app.layoutManager().threeDWidget(0) + managers = vtk.vtkCollection() + threeDViewWidget.getDisplayableManagers(managers) + for i in range(managers.GetNumberOfItems()): + obj = managers.GetItemAsObject(i) + if obj.IsA('vtkMRMLLinearTransformsDisplayableManager3D'): + return obj + return None - def assertMatrix(self, m, expected): - for i in range(4): - for j in range(4): - self.assertAlmostEqual(m.GetElement(i,j), expected[i][j]) - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_3D_interactionDefaults() - self.test_3D_interactionVolume() - self.test_3D_interaction2Models() - self.test_3D_parentTransform() - self.test_3D_interactionSerialization() - self.test_3D_boundsUpdateROI() - - def test_3D_interactionDefaults(self): - """ Test that the interaction widget exists in the 3D view. - """ - logic = SlicerTransformInteractionTest1Logic() - - #self.delayDisplay("Starting test_3D_interactionDefaults") - logic = SlicerTransformInteractionTest1Logic() - tNode, tdNode = logic.addTransform() - self.assertFalse(tdNode.GetEditorVisibility()) - self.assertFalse(tdNode.GetEditorSliceIntersectionVisibility()) - - slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView - manager = logic.getModel3DDisplayableManager() - self.assertIsNotNone(manager) - - # Check when nothing is on - widget = manager.GetWidget(tdNode) - self.assertFalse(widget.GetEnabled()) - - # Check when interactive is on - tdNode.SetEditorVisibility(True) - self.assertTrue(widget.GetEnabled()) - - # Check default widget transform values - representation = widget.GetRepresentation() - defaultTransform = vtk.vtkTransform() - representation.GetTransform(defaultTransform) - - expectedDefaultTransform = [ - [100.0, 0.0, 0.0, 0.0], - [0.0, 100.0, 0.0, 0.0], - [0.0, 0.0, 100.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - self.assertTransform(defaultTransform, expectedDefaultTransform) - - #self.delayDisplay('test_3D_interactionDefaults passed!') - - def test_3D_interactionVolume(self): - """ Test that the interaction widget interacts correctly in the 3D view. - """ - logic = SlicerTransformInteractionTest1Logic() - - import SampleData - volume = SampleData.downloadSample('CTAAbdomenPanoramix') - - #self.delayDisplay("Starting test_3D_interactionVolume") - logic = SlicerTransformInteractionTest1Logic() - tNode, tdNode = logic.addTransform() - self.assertFalse(tdNode.GetEditorVisibility()) - self.assertFalse(tdNode.GetEditorSliceIntersectionVisibility()) - - slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView - manager = logic.getModel3DDisplayableManager() - self.assertIsNotNone(manager) - - widget = manager.GetWidget(tdNode) - tdNode.SetEditorVisibility(True) - self.assertTrue(widget.GetEnabled()) - - # - # 1- No transform - # - # Check default widget transform values - representation = widget.GetRepresentation() - transform = vtk.vtkTransform() - - expectedDefaultTransform = [ - [100.0, 0.0, 0.0, 0.0], - [0.0, 100.0, 0.0, 0.0], - [0.0, 0.0, 100.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - representation.GetTransform(transform) - self.assertTransform(transform, expectedDefaultTransform) - - # Transform the volume and check widget transform values - volume.SetAndObserveTransformNodeID(tNode.GetID()) - tdNode.UpdateEditorBounds() - volumeTransform = [ - [654.609375, 0.0, 0.0, -2.030487060546875], - [0.0, 476.484375, 0.0, 126.66322422027588], - [0.0, 0.0, 645.0, -186.37799072265625], - [0.0, 0.0, 0.0, 1.0], - ] - representation.GetTransform(transform) - self.assertTransform(transform, volumeTransform) - - # Untransform the volume - volume.SetAndObserveTransformNodeID(None) - tdNode.UpdateEditorBounds() - representation.GetTransform(transform) - self.assertTransform(transform, expectedDefaultTransform) - - # - # 1- With translation transform - # - # Add a translation to the transform - move = [-42.0, 52, 0.2] - translation = vtk.vtkTransform() - translation.Translate(move) - tNode.ApplyTransform(translation) - - defaultPlusMoveTransform = copy.deepcopy(expectedDefaultTransform) - for i in range(3): - defaultPlusMoveTransform[i][3] += move[i] - - representation.GetTransform(transform) - self.assertTransform(transform, defaultPlusMoveTransform) - - # Transform the volume - volumePlusMoveTransform = copy.deepcopy(volumeTransform) - for i in range(3): - volumePlusMoveTransform[i][3] += move[i] - - volume.SetAndObserveTransformNodeID(tNode.GetID()) - tdNode.UpdateEditorBounds() - representation.GetTransform(transform) - self.assertTransform(transform, volumePlusMoveTransform) - - # Untransform the volume - volume.SetAndObserveTransformNodeID(None) - tdNode.UpdateEditorBounds() - representation.GetTransform(transform) - self.assertTransform(transform, defaultPlusMoveTransform) - - ## - ## 1- With rotate transform (and translation) - ## - # Add a rotation to the transform - rotation = vtk.vtkTransform() - rotation.RotateZ(90) - rotation.RotateX(90) - tNode.ApplyTransformMatrix(rotation.GetMatrix()) - - defaultPlusMovePlusRotationTransform = [ - defaultPlusMoveTransform[2], #[0.0, 0.0, 100.0, 0.2] - defaultPlusMoveTransform[0], #[100.0, 0.0, 0.0, -42.0] - defaultPlusMoveTransform[1], #[0.0, 100.0, 0.0, 52.0] - defaultPlusMoveTransform[3] #[0.0, 0.0, 0.0, 1.0], - ] - representation.GetTransform(transform) - self.assertTransform(transform, defaultPlusMovePlusRotationTransform) - - # Transform the volume - volumePlusMovePlusRotationTransform = [ - volumePlusMoveTransform[2], - volumePlusMoveTransform[0], - volumePlusMoveTransform[1], - volumePlusMoveTransform[3] - ] - - volume.SetAndObserveTransformNodeID(tNode.GetID()) - tdNode.UpdateEditorBounds() - representation.GetTransform(transform) - self.assertTransform(transform, volumePlusMovePlusRotationTransform) - - # Untransform the volume - volume.SetAndObserveTransformNodeID(None) - tdNode.UpdateEditorBounds() - representation.GetTransform(transform) - self.assertTransform(transform, defaultPlusMovePlusRotationTransform) - - ## - ## 1- With scaling transform (and rotation and translation) - ## - # Add a rotation to the transform - scale = [2.0, 3.0, 7.0, 1.0] # nice prime numbers - scaling = vtk.vtkTransform() - scaling.Scale(scale[0], scale[1], scale[2]) - tNode.ApplyTransformMatrix(scaling.GetMatrix()) - - defaultPlusMovePlusRotationPlusScalingTransform = [] - for i in range(4): - defaultPlusMovePlusRotationPlusScalingTransform.append( - [x*scale[i] for x in defaultPlusMovePlusRotationTransform[i]]) - - representation.GetTransform(transform) - self.assertTransform(transform, defaultPlusMovePlusRotationPlusScalingTransform) - - # Transform the volume - volumePlusMovePlusRotationPlusScalingTransform = [] - for i in range(4): - volumePlusMovePlusRotationPlusScalingTransform.append( - [x*scale[i] for x in volumePlusMovePlusRotationTransform[i]]) - - volume.SetAndObserveTransformNodeID(tNode.GetID()) - tdNode.UpdateEditorBounds() - representation.GetTransform(transform) - self.assertTransform(transform, volumePlusMovePlusRotationPlusScalingTransform) - - # Untransform the volume - volume.SetAndObserveTransformNodeID(None) - tdNode.UpdateEditorBounds() - representation.GetTransform(transform) - self.assertTransform(transform, defaultPlusMovePlusRotationPlusScalingTransform) - - #self.delayDisplay('test_3D_interactionVolume passed!') - - def test_3D_interaction2Models(self): - """ Test that the interaction widget works with multiple models. - """ - #self.delayDisplay("Starting test_3D_interaction2Models") - # - # Setup: - # 1. Create 2 cubes: - # - 1 centered - # - Another moved. - # 2. Get the widget - # - - # Centered cube - centeredCubeSource = vtk.vtkCubeSource() - centeredCubeSize = [500, 200, 300] - centeredCubeSource.SetXLength(centeredCubeSize[0]) - centeredCubeSource.SetYLength(centeredCubeSize[1]) - centeredCubeSource.SetZLength(centeredCubeSize[2]) - centeredCubeSource.SetCenter(0.0, 0.0, 0.0) - centeredCubeSource.Update() - - centeredCubeNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLModelNode()) - centeredCubeNode.SetPolyDataConnection(centeredCubeSource.GetOutputPort()) - - # Moved cube - movedCubeSource = vtk.vtkCubeSource() - movedCubeSource.SetXLength(800) - movedCubeSource.SetYLength(233) - movedCubeSource.SetZLength(761) - movedCubeSource.SetCenter(100.0, -80.0, -700.023) - - rotation = vtk.vtkTransform() - rotation.RotateY(45.0) - - applyTransform = vtk.vtkTransformPolyDataFilter() - applyTransform.SetTransform(rotation) - applyTransform.SetInputConnection(movedCubeSource.GetOutputPort()) - applyTransform.Update() - - movedCubeNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLModelNode()) - movedCubeNode.SetPolyDataConnection(applyTransform.GetOutputPort()) - - # Get the widget - logic = SlicerTransformInteractionTest1Logic() - tNode, tdNode = logic.addTransform() - slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView - manager = logic.getModel3DDisplayableManager() - self.assertIsNotNone(manager) - - widget = manager.GetWidget(tdNode) - tdNode.SetEditorVisibility(True) - self.assertTrue(widget.GetEnabled()) - - representation = widget.GetRepresentation() - transform = vtk.vtkTransform() - - # - # No transform, make sure the widget is correctly placed around: - # 1. No cubes - # 2. The centered cube - # 3. The centered cube AND the other cube - # 4. The other cube only - # 5. No cubes - # - - # Check default widget transform values - expectedDefaultTransform = [ - [100.0, 0.0, 0.0, 0.0], - [0.0, 100.0, 0.0, 0.0], - [0.0, 0.0, 100.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - representation.GetTransform(transform) - self.assertTransform(transform, expectedDefaultTransform) - - # Transform the centered cube - centeredCubeNode.SetAndObserveTransformNodeID(tNode.GetID()) - tdNode.UpdateEditorBounds() - centeredCubeTransform = copy.deepcopy(expectedDefaultTransform) - for i in range(3): - centeredCubeTransform[i][i] = 2*centeredCubeSize[i] - - representation.GetTransform(transform) - self.assertTransform(transform, centeredCubeTransform) - - # Transform both cubes - movedCubeNode.SetAndObserveTransformNodeID(tNode.GetID()) - tdNode.UpdateEditorBounds() - bothCubeTransform = [ - [2452.35424805, 0.0, 0.0, -363.088562012], - [0.0, 593.0, 0.0, -48.25], - [0.0, 0.0, 2535.19702148, -483.799255371], - [0.0, 0.0, 0.0, 1.0], - ] - - representation.GetTransform(transform) - self.assertTransform(transform, bothCubeTransform) - - # Transform only moved cube - centeredCubeNode.SetAndObserveTransformNodeID(None) - tdNode.UpdateEditorBounds() - movedCubeTransform = [ - [2207.58724976, 0.0, 0.0, -424.280311584], - [0.0, 466.0, 0.0, -80.0], - [0.0, 0.0, 2207.58731651, -565.701681614], - [0.0, 0.0, 0.0, 1.0], - ] - - representation.GetTransform(transform) - self.assertTransform(transform, movedCubeTransform) - - # Default again - movedCubeNode.SetAndObserveTransformNodeID(None) - tdNode.UpdateEditorBounds() - - representation.GetTransform(transform) - self.assertTransform(transform, expectedDefaultTransform) - - #self.delayDisplay('test_3D_interaction2Models passed!') - - def test_3D_parentTransform(self): - """ Test that the interaction widget works with a parent transform. - """ - #self.delayDisplay('Starting test_3D_parentTransform') - # - # Setup: - # - Use a markup control points list node - # - Create a parent transform - # - Create another transform under the parent transform - # - - markupNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") - markupNode.AddControlPoint([500.0, -1000.0, 0.0]) - markupNode.AddControlPoint([1000.0, 1000.0, 200.0]) - markupNode.AddControlPoint([-1500.0, -200.0, -100.0]) - - logic = SlicerTransformInteractionTest1Logic() - parentNode, parendDisplayNode = logic.addTransform() - - leafNode, tdNode = logic.addTransform() - slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView - manager = logic.getModel3DDisplayableManager() - self.assertIsNotNone(manager) - - widget = manager.GetWidget(tdNode) - tdNode.SetEditorVisibility(True) - self.assertTrue(widget.GetEnabled()) - - representation = widget.GetRepresentation() - transform = vtk.vtkTransform() - - # - # Test the leaf transform (that has a parent) in a few situations - # - - # Test the transform with just the parent (that is identity for now) - leafNode.SetAndObserveTransformNodeID(parentNode.GetID()) - tdNode.UpdateEditorBounds() - expectedDefaultTransform = [ - [100.0, 0.0, 0.0, 0.0], - [0.0, 100.0, 0.0, 0.0], - [0.0, 0.0, 100.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - - representation.GetTransform(transform) - self.assertTransform(transform, expectedDefaultTransform) - - # Set an actual transform on the parent - parentTransform = vtk.vtkTransform() - move = [51.0, -27.0, 3.3] - scale = [2.0, 7.0, 3.0] - parentTransform.Translate(move) - parentTransform.Scale(scale[0], scale[1], scale[2]) - parentTransform.RotateZ(90) - parentTransform.RotateX(90) - parentNode.ApplyTransformMatrix(parentTransform.GetMatrix()) - - expectedTransformWithParent = [ - [0.0, 0.0, 200.0, 51.0], - [700.0, 0.0, 0.0, -27.0], - [0.0, 300.0, 0.0, 3.3], - [0.0, 0.0, 0.0, 1.0], - ] - representation.GetTransform(transform) - self.assertTransform(transform, expectedTransformWithParent) - - # Set the markup node under the leaf transform - markupNode.SetAndObserveTransformNodeID(leafNode.GetID()) - tdNode.UpdateEditorBounds() - expectedMarkupTransformWithParent = [ - [0.0, 0.0, 1200.0, 151.0], - [35000.0, 0.0, 0.0, -1777.0], - [0.0, 12000.0, 0.0, 3.3], - [0.0, 0.0, 0.0, 1.0], - ] - representation.GetTransform(transform) - self.assertTransform(transform, expectedMarkupTransformWithParent) - - # Set the parent transform to identity - parentNode.ApplyTransformMatrix(parentTransform.GetLinearInverse().GetMatrix()) - expectedMarkupTransform = [ - [5000.0, 0.0, 0.0, -250.0], - [0.0, 4000.0, 0.0, 0.0], - [0.0, 0.0, 600.0, 50.0], - [0.0, 0.0, 0.0, 1.0], - ] - representation.GetTransform(transform) - self.assertTransform(transform, expectedMarkupTransform) - - #self.delayDisplay('test_3D_parentTransform passed!') - - def test_3D_interactionSerialization(self): - """ Test that the serialzation the interaction properties. +class SlicerTransformInteractionTest1Test(ScriptedLoadableModuleTest): """ - logic = SlicerTransformInteractionTest1Logic() - - #self.delayDisplay("Starting test_3D_interactionSerialization") - # Setup - tNode, tdNode = logic.addTransform() - tNode.SetMatrixTransformToParent(vtk.vtkMatrix4x4()) - tNode.SetName('Transform') - - slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView - manager = logic.getModel3DDisplayableManager() - self.assertIsNotNone(manager) - - widget = manager.GetWidget(tdNode) - tdNode.SetEditorVisibility(True) - self.assertTrue(widget.GetEnabled()) - - # Save and clear scene - tempSceneDir = tempfile.mkdtemp('', 'InteractionSerialization', slicer.app.temporaryPath) - sceneFile = os.path.join(tempSceneDir, 'scene.mrb') - slicer.util.saveScene(sceneFile) - slicer.mrmlScene.RemoveNode(tNode) - slicer.mrmlScene.RemoveNode(tdNode) - slicer.mrmlScene.Clear(0) - - # Re-load scene and check values - slicer.util.loadScene(sceneFile) - tNode = slicer.util.getNode('Transform') - self.assertIsNotNone(tNode) - - tdNode = tNode.GetDisplayNode() - self.assertIsNotNone(tdNode) - self.assertEqual(tdNode.GetEditorVisibility(), 1) - - manager = logic.getModel3DDisplayableManager() - self.assertIsNotNone(manager) - widget = manager.GetWidget(tdNode) - self.assertTrue(widget.GetEnabled()) - - def test_3D_boundsUpdateROI(self): - """ Test that the bounds update with an ROI. """ - logic = SlicerTransformInteractionTest1Logic() - - #self.delayDisplay("Starting test_3D_boundsUpdateROI") - # Setup - roiNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLAnnotationROINode()) - roiNode.SetXYZ(100, 300, -0.689) - roiNode.SetRadiusXYZ(700, 8, 45) - - logic = SlicerTransformInteractionTest1Logic() - tNode, tdNode = logic.addTransform() - - slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView - manager = logic.getModel3DDisplayableManager() - self.assertIsNotNone(manager) - - widget = manager.GetWidget(tdNode) - tdNode.SetEditorVisibility(True) - self.assertTrue(widget.GetEnabled()) - - representation = widget.GetRepresentation() - transform = vtk.vtkTransform() - - expectedDefaultTransform = [ - [100.0, 0.0, 0.0, 0.0], - [0.0, 100.0, 0.0, 0.0], - [0.0, 0.0, 100.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - ] - representation.GetTransform(transform) - self.assertTransform(transform, expectedDefaultTransform) - - tdNode.UpdateEditorBounds() - representation.GetTransform(transform) - self.assertTransform(transform, expectedDefaultTransform) - - roiNode.SetAndObserveTransformNodeID(tNode.GetID()) - representation.GetTransform(transform) - self.assertTransform(transform, expectedDefaultTransform) - - tdNode.UpdateEditorBounds() - roiDefaultTransform = [ - [2800.0, 0.0, 0.0, 100.0], - [0.0, 32.0, 0.0, 300.0], - [0.0, 0.0, 180.0, -0.689], - [0.0, 0.0, 0.0, 1.0], - ] - representation.GetTransform(transform) - self.assertTransform(transform, roiDefaultTransform) - - roiNode.SetAndObserveTransformNodeID(None) - representation.GetTransform(transform) - self.assertTransform(transform, roiDefaultTransform) - - tdNode.UpdateEditorBounds() - representation.GetTransform(transform) - self.assertTransform(transform, expectedDefaultTransform) + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) + + def assertTransform(self, t, expected): + return self.assertMatrix(t.GetMatrix(), expected) + + def assertMatrix(self, m, expected): + for i in range(4): + for j in range(4): + self.assertAlmostEqual(m.GetElement(i, j), expected[i][j]) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_3D_interactionDefaults() + self.test_3D_interactionVolume() + self.test_3D_interaction2Models() + self.test_3D_parentTransform() + self.test_3D_interactionSerialization() + self.test_3D_boundsUpdateROI() + + def test_3D_interactionDefaults(self): + """ Test that the interaction widget exists in the 3D view. + """ + logic = SlicerTransformInteractionTest1Logic() + + # self.delayDisplay("Starting test_3D_interactionDefaults") + logic = SlicerTransformInteractionTest1Logic() + tNode, tdNode = logic.addTransform() + self.assertFalse(tdNode.GetEditorVisibility()) + self.assertFalse(tdNode.GetEditorSliceIntersectionVisibility()) + + slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView + manager = logic.getModel3DDisplayableManager() + self.assertIsNotNone(manager) + + # Check when nothing is on + widget = manager.GetWidget(tdNode) + self.assertFalse(widget.GetEnabled()) + + # Check when interactive is on + tdNode.SetEditorVisibility(True) + self.assertTrue(widget.GetEnabled()) + + # Check default widget transform values + representation = widget.GetRepresentation() + defaultTransform = vtk.vtkTransform() + representation.GetTransform(defaultTransform) + + expectedDefaultTransform = [ + [100.0, 0.0, 0.0, 0.0], + [0.0, 100.0, 0.0, 0.0], + [0.0, 0.0, 100.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + self.assertTransform(defaultTransform, expectedDefaultTransform) + + # self.delayDisplay('test_3D_interactionDefaults passed!') + + def test_3D_interactionVolume(self): + """ Test that the interaction widget interacts correctly in the 3D view. + """ + logic = SlicerTransformInteractionTest1Logic() + + import SampleData + volume = SampleData.downloadSample('CTAAbdomenPanoramix') + + # self.delayDisplay("Starting test_3D_interactionVolume") + logic = SlicerTransformInteractionTest1Logic() + tNode, tdNode = logic.addTransform() + self.assertFalse(tdNode.GetEditorVisibility()) + self.assertFalse(tdNode.GetEditorSliceIntersectionVisibility()) + + slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView + manager = logic.getModel3DDisplayableManager() + self.assertIsNotNone(manager) + + widget = manager.GetWidget(tdNode) + tdNode.SetEditorVisibility(True) + self.assertTrue(widget.GetEnabled()) + + # + # 1- No transform + # + # Check default widget transform values + representation = widget.GetRepresentation() + transform = vtk.vtkTransform() + + expectedDefaultTransform = [ + [100.0, 0.0, 0.0, 0.0], + [0.0, 100.0, 0.0, 0.0], + [0.0, 0.0, 100.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + representation.GetTransform(transform) + self.assertTransform(transform, expectedDefaultTransform) + + # Transform the volume and check widget transform values + volume.SetAndObserveTransformNodeID(tNode.GetID()) + tdNode.UpdateEditorBounds() + volumeTransform = [ + [654.609375, 0.0, 0.0, -2.030487060546875], + [0.0, 476.484375, 0.0, 126.66322422027588], + [0.0, 0.0, 645.0, -186.37799072265625], + [0.0, 0.0, 0.0, 1.0], + ] + representation.GetTransform(transform) + self.assertTransform(transform, volumeTransform) + + # Untransform the volume + volume.SetAndObserveTransformNodeID(None) + tdNode.UpdateEditorBounds() + representation.GetTransform(transform) + self.assertTransform(transform, expectedDefaultTransform) + + # + # 1- With translation transform + # + # Add a translation to the transform + move = [-42.0, 52, 0.2] + translation = vtk.vtkTransform() + translation.Translate(move) + tNode.ApplyTransform(translation) + + defaultPlusMoveTransform = copy.deepcopy(expectedDefaultTransform) + for i in range(3): + defaultPlusMoveTransform[i][3] += move[i] + + representation.GetTransform(transform) + self.assertTransform(transform, defaultPlusMoveTransform) + + # Transform the volume + volumePlusMoveTransform = copy.deepcopy(volumeTransform) + for i in range(3): + volumePlusMoveTransform[i][3] += move[i] + + volume.SetAndObserveTransformNodeID(tNode.GetID()) + tdNode.UpdateEditorBounds() + representation.GetTransform(transform) + self.assertTransform(transform, volumePlusMoveTransform) + + # Untransform the volume + volume.SetAndObserveTransformNodeID(None) + tdNode.UpdateEditorBounds() + representation.GetTransform(transform) + self.assertTransform(transform, defaultPlusMoveTransform) + + ## + # 1- With rotate transform (and translation) + ## + # Add a rotation to the transform + rotation = vtk.vtkTransform() + rotation.RotateZ(90) + rotation.RotateX(90) + tNode.ApplyTransformMatrix(rotation.GetMatrix()) + + defaultPlusMovePlusRotationTransform = [ + defaultPlusMoveTransform[2], # [0.0, 0.0, 100.0, 0.2] + defaultPlusMoveTransform[0], # [100.0, 0.0, 0.0, -42.0] + defaultPlusMoveTransform[1], # [0.0, 100.0, 0.0, 52.0] + defaultPlusMoveTransform[3] # [0.0, 0.0, 0.0, 1.0], + ] + representation.GetTransform(transform) + self.assertTransform(transform, defaultPlusMovePlusRotationTransform) + + # Transform the volume + volumePlusMovePlusRotationTransform = [ + volumePlusMoveTransform[2], + volumePlusMoveTransform[0], + volumePlusMoveTransform[1], + volumePlusMoveTransform[3] + ] + + volume.SetAndObserveTransformNodeID(tNode.GetID()) + tdNode.UpdateEditorBounds() + representation.GetTransform(transform) + self.assertTransform(transform, volumePlusMovePlusRotationTransform) + + # Untransform the volume + volume.SetAndObserveTransformNodeID(None) + tdNode.UpdateEditorBounds() + representation.GetTransform(transform) + self.assertTransform(transform, defaultPlusMovePlusRotationTransform) + + ## + # 1- With scaling transform (and rotation and translation) + ## + # Add a rotation to the transform + scale = [2.0, 3.0, 7.0, 1.0] # nice prime numbers + scaling = vtk.vtkTransform() + scaling.Scale(scale[0], scale[1], scale[2]) + tNode.ApplyTransformMatrix(scaling.GetMatrix()) + + defaultPlusMovePlusRotationPlusScalingTransform = [] + for i in range(4): + defaultPlusMovePlusRotationPlusScalingTransform.append( + [x * scale[i] for x in defaultPlusMovePlusRotationTransform[i]]) + + representation.GetTransform(transform) + self.assertTransform(transform, defaultPlusMovePlusRotationPlusScalingTransform) + + # Transform the volume + volumePlusMovePlusRotationPlusScalingTransform = [] + for i in range(4): + volumePlusMovePlusRotationPlusScalingTransform.append( + [x * scale[i] for x in volumePlusMovePlusRotationTransform[i]]) + + volume.SetAndObserveTransformNodeID(tNode.GetID()) + tdNode.UpdateEditorBounds() + representation.GetTransform(transform) + self.assertTransform(transform, volumePlusMovePlusRotationPlusScalingTransform) + + # Untransform the volume + volume.SetAndObserveTransformNodeID(None) + tdNode.UpdateEditorBounds() + representation.GetTransform(transform) + self.assertTransform(transform, defaultPlusMovePlusRotationPlusScalingTransform) + + # self.delayDisplay('test_3D_interactionVolume passed!') + + def test_3D_interaction2Models(self): + """ Test that the interaction widget works with multiple models. + """ + # self.delayDisplay("Starting test_3D_interaction2Models") + # + # Setup: + # 1. Create 2 cubes: + # - 1 centered + # - Another moved. + # 2. Get the widget + # + + # Centered cube + centeredCubeSource = vtk.vtkCubeSource() + centeredCubeSize = [500, 200, 300] + centeredCubeSource.SetXLength(centeredCubeSize[0]) + centeredCubeSource.SetYLength(centeredCubeSize[1]) + centeredCubeSource.SetZLength(centeredCubeSize[2]) + centeredCubeSource.SetCenter(0.0, 0.0, 0.0) + centeredCubeSource.Update() + + centeredCubeNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLModelNode()) + centeredCubeNode.SetPolyDataConnection(centeredCubeSource.GetOutputPort()) + + # Moved cube + movedCubeSource = vtk.vtkCubeSource() + movedCubeSource.SetXLength(800) + movedCubeSource.SetYLength(233) + movedCubeSource.SetZLength(761) + movedCubeSource.SetCenter(100.0, -80.0, -700.023) + + rotation = vtk.vtkTransform() + rotation.RotateY(45.0) + + applyTransform = vtk.vtkTransformPolyDataFilter() + applyTransform.SetTransform(rotation) + applyTransform.SetInputConnection(movedCubeSource.GetOutputPort()) + applyTransform.Update() + + movedCubeNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLModelNode()) + movedCubeNode.SetPolyDataConnection(applyTransform.GetOutputPort()) + + # Get the widget + logic = SlicerTransformInteractionTest1Logic() + tNode, tdNode = logic.addTransform() + slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView + manager = logic.getModel3DDisplayableManager() + self.assertIsNotNone(manager) + + widget = manager.GetWidget(tdNode) + tdNode.SetEditorVisibility(True) + self.assertTrue(widget.GetEnabled()) + + representation = widget.GetRepresentation() + transform = vtk.vtkTransform() + + # + # No transform, make sure the widget is correctly placed around: + # 1. No cubes + # 2. The centered cube + # 3. The centered cube AND the other cube + # 4. The other cube only + # 5. No cubes + # + + # Check default widget transform values + expectedDefaultTransform = [ + [100.0, 0.0, 0.0, 0.0], + [0.0, 100.0, 0.0, 0.0], + [0.0, 0.0, 100.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + representation.GetTransform(transform) + self.assertTransform(transform, expectedDefaultTransform) + + # Transform the centered cube + centeredCubeNode.SetAndObserveTransformNodeID(tNode.GetID()) + tdNode.UpdateEditorBounds() + centeredCubeTransform = copy.deepcopy(expectedDefaultTransform) + for i in range(3): + centeredCubeTransform[i][i] = 2 * centeredCubeSize[i] + + representation.GetTransform(transform) + self.assertTransform(transform, centeredCubeTransform) + + # Transform both cubes + movedCubeNode.SetAndObserveTransformNodeID(tNode.GetID()) + tdNode.UpdateEditorBounds() + bothCubeTransform = [ + [2452.35424805, 0.0, 0.0, -363.088562012], + [0.0, 593.0, 0.0, -48.25], + [0.0, 0.0, 2535.19702148, -483.799255371], + [0.0, 0.0, 0.0, 1.0], + ] + + representation.GetTransform(transform) + self.assertTransform(transform, bothCubeTransform) + + # Transform only moved cube + centeredCubeNode.SetAndObserveTransformNodeID(None) + tdNode.UpdateEditorBounds() + movedCubeTransform = [ + [2207.58724976, 0.0, 0.0, -424.280311584], + [0.0, 466.0, 0.0, -80.0], + [0.0, 0.0, 2207.58731651, -565.701681614], + [0.0, 0.0, 0.0, 1.0], + ] + + representation.GetTransform(transform) + self.assertTransform(transform, movedCubeTransform) + + # Default again + movedCubeNode.SetAndObserveTransformNodeID(None) + tdNode.UpdateEditorBounds() + + representation.GetTransform(transform) + self.assertTransform(transform, expectedDefaultTransform) + + # self.delayDisplay('test_3D_interaction2Models passed!') + + def test_3D_parentTransform(self): + """ Test that the interaction widget works with a parent transform. + """ + # self.delayDisplay('Starting test_3D_parentTransform') + # + # Setup: + # - Use a markup control points list node + # - Create a parent transform + # - Create another transform under the parent transform + # + + markupNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") + markupNode.AddControlPoint([500.0, -1000.0, 0.0]) + markupNode.AddControlPoint([1000.0, 1000.0, 200.0]) + markupNode.AddControlPoint([-1500.0, -200.0, -100.0]) + + logic = SlicerTransformInteractionTest1Logic() + parentNode, parendDisplayNode = logic.addTransform() + + leafNode, tdNode = logic.addTransform() + slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView + manager = logic.getModel3DDisplayableManager() + self.assertIsNotNone(manager) + + widget = manager.GetWidget(tdNode) + tdNode.SetEditorVisibility(True) + self.assertTrue(widget.GetEnabled()) + + representation = widget.GetRepresentation() + transform = vtk.vtkTransform() + + # + # Test the leaf transform (that has a parent) in a few situations + # + + # Test the transform with just the parent (that is identity for now) + leafNode.SetAndObserveTransformNodeID(parentNode.GetID()) + tdNode.UpdateEditorBounds() + expectedDefaultTransform = [ + [100.0, 0.0, 0.0, 0.0], + [0.0, 100.0, 0.0, 0.0], + [0.0, 0.0, 100.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + + representation.GetTransform(transform) + self.assertTransform(transform, expectedDefaultTransform) + + # Set an actual transform on the parent + parentTransform = vtk.vtkTransform() + move = [51.0, -27.0, 3.3] + scale = [2.0, 7.0, 3.0] + parentTransform.Translate(move) + parentTransform.Scale(scale[0], scale[1], scale[2]) + parentTransform.RotateZ(90) + parentTransform.RotateX(90) + parentNode.ApplyTransformMatrix(parentTransform.GetMatrix()) + + expectedTransformWithParent = [ + [0.0, 0.0, 200.0, 51.0], + [700.0, 0.0, 0.0, -27.0], + [0.0, 300.0, 0.0, 3.3], + [0.0, 0.0, 0.0, 1.0], + ] + representation.GetTransform(transform) + self.assertTransform(transform, expectedTransformWithParent) + + # Set the markup node under the leaf transform + markupNode.SetAndObserveTransformNodeID(leafNode.GetID()) + tdNode.UpdateEditorBounds() + expectedMarkupTransformWithParent = [ + [0.0, 0.0, 1200.0, 151.0], + [35000.0, 0.0, 0.0, -1777.0], + [0.0, 12000.0, 0.0, 3.3], + [0.0, 0.0, 0.0, 1.0], + ] + representation.GetTransform(transform) + self.assertTransform(transform, expectedMarkupTransformWithParent) + + # Set the parent transform to identity + parentNode.ApplyTransformMatrix(parentTransform.GetLinearInverse().GetMatrix()) + expectedMarkupTransform = [ + [5000.0, 0.0, 0.0, -250.0], + [0.0, 4000.0, 0.0, 0.0], + [0.0, 0.0, 600.0, 50.0], + [0.0, 0.0, 0.0, 1.0], + ] + representation.GetTransform(transform) + self.assertTransform(transform, expectedMarkupTransform) + + # self.delayDisplay('test_3D_parentTransform passed!') + + def test_3D_interactionSerialization(self): + """ Test that the serialzation the interaction properties. + """ + logic = SlicerTransformInteractionTest1Logic() + + # self.delayDisplay("Starting test_3D_interactionSerialization") + # Setup + tNode, tdNode = logic.addTransform() + tNode.SetMatrixTransformToParent(vtk.vtkMatrix4x4()) + tNode.SetName('Transform') + + slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView + manager = logic.getModel3DDisplayableManager() + self.assertIsNotNone(manager) + + widget = manager.GetWidget(tdNode) + tdNode.SetEditorVisibility(True) + self.assertTrue(widget.GetEnabled()) + + # Save and clear scene + tempSceneDir = tempfile.mkdtemp('', 'InteractionSerialization', slicer.app.temporaryPath) + sceneFile = os.path.join(tempSceneDir, 'scene.mrb') + slicer.util.saveScene(sceneFile) + slicer.mrmlScene.RemoveNode(tNode) + slicer.mrmlScene.RemoveNode(tdNode) + slicer.mrmlScene.Clear(0) + + # Re-load scene and check values + slicer.util.loadScene(sceneFile) + tNode = slicer.util.getNode('Transform') + self.assertIsNotNone(tNode) + + tdNode = tNode.GetDisplayNode() + self.assertIsNotNone(tdNode) + self.assertEqual(tdNode.GetEditorVisibility(), 1) + + manager = logic.getModel3DDisplayableManager() + self.assertIsNotNone(manager) + widget = manager.GetWidget(tdNode) + self.assertTrue(widget.GetEnabled()) + + def test_3D_boundsUpdateROI(self): + """ Test that the bounds update with an ROI. + """ + logic = SlicerTransformInteractionTest1Logic() + + # self.delayDisplay("Starting test_3D_boundsUpdateROI") + # Setup + roiNode = slicer.mrmlScene.AddNode(slicer.vtkMRMLAnnotationROINode()) + roiNode.SetXYZ(100, 300, -0.689) + roiNode.SetRadiusXYZ(700, 8, 45) + + logic = SlicerTransformInteractionTest1Logic() + tNode, tdNode = logic.addTransform() + + slicer.app.layoutManager().layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUp3DView + manager = logic.getModel3DDisplayableManager() + self.assertIsNotNone(manager) + + widget = manager.GetWidget(tdNode) + tdNode.SetEditorVisibility(True) + self.assertTrue(widget.GetEnabled()) + + representation = widget.GetRepresentation() + transform = vtk.vtkTransform() + + expectedDefaultTransform = [ + [100.0, 0.0, 0.0, 0.0], + [0.0, 100.0, 0.0, 0.0], + [0.0, 0.0, 100.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] + representation.GetTransform(transform) + self.assertTransform(transform, expectedDefaultTransform) + + tdNode.UpdateEditorBounds() + representation.GetTransform(transform) + self.assertTransform(transform, expectedDefaultTransform) + + roiNode.SetAndObserveTransformNodeID(tNode.GetID()) + representation.GetTransform(transform) + self.assertTransform(transform, expectedDefaultTransform) + + tdNode.UpdateEditorBounds() + roiDefaultTransform = [ + [2800.0, 0.0, 0.0, 100.0], + [0.0, 32.0, 0.0, 300.0], + [0.0, 0.0, 180.0, -0.689], + [0.0, 0.0, 0.0, 1.0], + ] + representation.GetTransform(transform) + self.assertTransform(transform, roiDefaultTransform) + + roiNode.SetAndObserveTransformNodeID(None) + representation.GetTransform(transform) + self.assertTransform(transform, roiDefaultTransform) + + tdNode.UpdateEditorBounds() + representation.GetTransform(transform) + self.assertTransform(transform, expectedDefaultTransform) diff --git a/Applications/SlicerApp/Testing/Python/SlicerUnitTestTest.py b/Applications/SlicerApp/Testing/Python/SlicerUnitTestTest.py index 24f4e6f9b1f..5791b4a4d7b 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerUnitTestTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerUnitTestTest.py @@ -3,26 +3,26 @@ class SlicerUnitTestTest(unittest.TestCase): - """ See https://docs.python.org/library/unittest.html#basic-example - """ + """ See https://docs.python.org/library/unittest.html#basic-example + """ - def setUp(self): - self.seq = list(range(10)) + def setUp(self): + self.seq = list(range(10)) - def test_shuffle(self): - # Make sure the shuffled sequence does not lose any elements - random.shuffle(self.seq) - self.seq.sort() - self.assertEqual(self.seq, list(range(10))) + def test_shuffle(self): + # Make sure the shuffled sequence does not lose any elements + random.shuffle(self.seq) + self.seq.sort() + self.assertEqual(self.seq, list(range(10))) - # Should raise an exception for an immutable sequence - self.assertRaises(TypeError, random.shuffle, (1,2,3)) + # Should raise an exception for an immutable sequence + self.assertRaises(TypeError, random.shuffle, (1, 2, 3)) - def test_choice(self): - element = random.choice(self.seq) - self.assertIn(element, self.seq) + def test_choice(self): + element = random.choice(self.seq) + self.assertIn(element, self.seq) - def test_sample(self): - self.assertRaises(ValueError, random.sample, self.seq, 20) - for element in random.sample(self.seq, 5): - self.assertIn(element, self.seq) + def test_sample(self): + self.assertRaises(ValueError, random.sample, self.seq, 20) + for element in random.sample(self.seq, 5): + self.assertIn(element, self.seq) diff --git a/Applications/SlicerApp/Testing/Python/SlicerUnitTestWithErrorsTest.py b/Applications/SlicerApp/Testing/Python/SlicerUnitTestWithErrorsTest.py index c917ece0d69..7e441c078c3 100644 --- a/Applications/SlicerApp/Testing/Python/SlicerUnitTestWithErrorsTest.py +++ b/Applications/SlicerApp/Testing/Python/SlicerUnitTestWithErrorsTest.py @@ -2,8 +2,8 @@ class SlicerUnitTestWithErrorsTest(unittest.TestCase): - """ See https://docs.python.org/library/unittest.html#basic-example - """ + """ See https://docs.python.org/library/unittest.html#basic-example + """ - def test_expectedtofail(self): - self.assertTrue(False) + def test_expectedtofail(self): + self.assertTrue(False) diff --git a/Applications/SlicerApp/Testing/Python/TwoCLIsInARowTest.py b/Applications/SlicerApp/Testing/Python/TwoCLIsInARowTest.py index 091518206b9..decc9968aaa 100644 --- a/Applications/SlicerApp/Testing/Python/TwoCLIsInARowTest.py +++ b/Applications/SlicerApp/Testing/Python/TwoCLIsInARowTest.py @@ -9,29 +9,29 @@ # class TwoCLIsInARowTest(ScriptedLoadableModule): - def __init__(self, parent): - parent.title = "TwoCLIsInARowTest" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = ["CLI4Test"] - parent.contributors = ["Alexis Girault (Kitware), Johan Andruejol (Kitware)"] - parent.helpText = """ + def __init__(self, parent): + parent.title = "TwoCLIsInARowTest" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = ["CLI4Test"] + parent.contributors = ["Alexis Girault (Kitware), Johan Andruejol (Kitware)"] + parent.helpText = """ This is a self test that tests the piping of two CLIs through python """ - parent.acknowledgementText = """""" # replace with organization, grant and thanks. - self.parent = parent + parent.acknowledgementText = """""" # replace with organization, grant and thanks. + self.parent = parent - # Add this test to the SelfTest module's list for discovery when the module - # is created. Since this module may be discovered before SelfTests itself, - # create the list if it doesn't already exist. - try: - slicer.selfTests - except AttributeError: - slicer.selfTests = {} - slicer.selfTests['TwoCLIsInARowTest'] = self.runTest + # Add this test to the SelfTest module's list for discovery when the module + # is created. Since this module may be discovered before SelfTests itself, + # create the list if it doesn't already exist. + try: + slicer.selfTests + except AttributeError: + slicer.selfTests = {} + slicer.selfTests['TwoCLIsInARowTest'] = self.runTest - def runTest(self): - tester = TwoCLIsInARowTestTest() - tester.runTest() + def runTest(self): + tester = TwoCLIsInARowTestTest() + tester.runTest() # @@ -40,8 +40,8 @@ def runTest(self): class TwoCLIsInARowTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -49,61 +49,61 @@ def setup(self): # class TwoCLIsInARowTestLogic(ScriptedLoadableModuleLogic): - def __init__(self): - self.Observations = [] - self.StatusModifiedEvent = slicer.vtkMRMLCommandLineModuleNode().StatusModifiedEvent - - self.parameters = {} - self.success = False - - def runTest(self): - self.runModule1() - - def runModule1(self): - cliModule = slicer.modules.cli4test - cliNode = slicer.cli.createNode(cliModule) - cliNode.SetName("CLIModule1") - self.addObserver(cliNode, self.StatusModifiedEvent, self.onModule1Modified) - cliNode = slicer.cli.run(cliModule, cliNode, self.parameters, False) - - def onModule1Modified(self, cliNode, event): - print("--",cliNode.GetStatusString(),":", cliNode.GetName()) - if not cliNode.IsBusy(): - self.removeObservers(cliNode, self.StatusModifiedEvent, self.onModule1Modified) - if cliNode.GetStatusString() == 'Completed': - self.runModule2() - - def runModule2(self): - cliModule = slicer.modules.cli4test - cliNode = slicer.cli.createNode(cliModule) - cliNode.SetName("CLIModule2") - self.addObserver(cliNode, self.StatusModifiedEvent, self.onModule2Modified) - cliNode = slicer.cli.run(cliModule, cliNode, self.parameters, False) - - def onModule2Modified(self, cliNode, event): - print("--",cliNode.GetStatusString(),":", cliNode.GetName()) - if not cliNode.IsBusy(): - self.removeObservers(cliNode, self.StatusModifiedEvent, self.onModule2Modified) - self.success = cliNode.GetStatusString() == 'Completed' - - def addObserver(self, object, event, method, group = 'none'): - if self.hasObserver(object, event, method): - print(object.GetName(),'already has observer') - return - tag = object.AddObserver(event, method) - self.Observations.append([object, event, method, group, tag]) - - def hasObserver(self, object, event, method): - for o, e, m, g, t in self.Observations: - if o == object and e == event and m == method: - return True - return False - - def removeObservers(self, object, event, method): - for o, e, m, g, t in self.Observations: - if object == o and event == e and method == m: - o.RemoveObserver(t) - self.Observations.remove([o, e, m, g, t]) + def __init__(self): + self.Observations = [] + self.StatusModifiedEvent = slicer.vtkMRMLCommandLineModuleNode().StatusModifiedEvent + + self.parameters = {} + self.success = False + + def runTest(self): + self.runModule1() + + def runModule1(self): + cliModule = slicer.modules.cli4test + cliNode = slicer.cli.createNode(cliModule) + cliNode.SetName("CLIModule1") + self.addObserver(cliNode, self.StatusModifiedEvent, self.onModule1Modified) + cliNode = slicer.cli.run(cliModule, cliNode, self.parameters, False) + + def onModule1Modified(self, cliNode, event): + print("--", cliNode.GetStatusString(), ":", cliNode.GetName()) + if not cliNode.IsBusy(): + self.removeObservers(cliNode, self.StatusModifiedEvent, self.onModule1Modified) + if cliNode.GetStatusString() == 'Completed': + self.runModule2() + + def runModule2(self): + cliModule = slicer.modules.cli4test + cliNode = slicer.cli.createNode(cliModule) + cliNode.SetName("CLIModule2") + self.addObserver(cliNode, self.StatusModifiedEvent, self.onModule2Modified) + cliNode = slicer.cli.run(cliModule, cliNode, self.parameters, False) + + def onModule2Modified(self, cliNode, event): + print("--", cliNode.GetStatusString(), ":", cliNode.GetName()) + if not cliNode.IsBusy(): + self.removeObservers(cliNode, self.StatusModifiedEvent, self.onModule2Modified) + self.success = cliNode.GetStatusString() == 'Completed' + + def addObserver(self, object, event, method, group='none'): + if self.hasObserver(object, event, method): + print(object.GetName(), 'already has observer') + return + tag = object.AddObserver(event, method) + self.Observations.append([object, event, method, group, tag]) + + def hasObserver(self, object, event, method): + for o, e, m, g, t in self.Observations: + if o == object and e == event and m == method: + return True + return False + + def removeObservers(self, object, event, method): + for o, e, m, g, t in self.Observations: + if object == o and event == e and method == m: + o.RemoveObserver(t) + self.Observations.remove([o, e, m, g, t]) # @@ -112,33 +112,33 @@ def removeObservers(self, object, event, method): class TwoCLIsInARowTestTest(ScriptedLoadableModuleTest): - def setUp(self): - """ Reset the state for testing. - """ - pass + def setUp(self): + """ Reset the state for testing. + """ + pass - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_TwoCLIsInARowTest() + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_TwoCLIsInARowTest() - def test_TwoCLIsInARowTest(self): - self.delayDisplay('Running two CLIs in a row Test') + def test_TwoCLIsInARowTest(self): + self.delayDisplay('Running two CLIs in a row Test') - tempFile = qt.QTemporaryFile("TwoCLIsInARowTest-outputFile-XXXXXX") - self.assertTrue(tempFile.open()) + tempFile = qt.QTemporaryFile("TwoCLIsInARowTest-outputFile-XXXXXX") + self.assertTrue(tempFile.open()) - logic = TwoCLIsInARowTestLogic() - logic.parameters = {} - logic.parameters["InputValue1"] = 1 - logic.parameters["InputValue2"] = 2 - logic.parameters["OperationType"] = 'Addition' - logic.parameters["OutputFile"] = tempFile.fileName() + logic = TwoCLIsInARowTestLogic() + logic.parameters = {} + logic.parameters["InputValue1"] = 1 + logic.parameters["InputValue2"] = 2 + logic.parameters["OperationType"] = 'Addition' + logic.parameters["OutputFile"] = tempFile.fileName() - logic.runTest() - while not logic.success: - self.delayDisplay('Waiting for module 2 to complete...') - self.assertTrue(logic.success) + logic.runTest() + while not logic.success: + self.delayDisplay('Waiting for module 2 to complete...') + self.assertTrue(logic.success) - self.delayDisplay('Two CLIs in a row test passed !') + self.delayDisplay('Two CLIs in a row test passed !') diff --git a/Applications/SlicerApp/Testing/Python/TwoCLIsInParallelTest.py b/Applications/SlicerApp/Testing/Python/TwoCLIsInParallelTest.py index 0df475574b1..e434f7a39b3 100644 --- a/Applications/SlicerApp/Testing/Python/TwoCLIsInParallelTest.py +++ b/Applications/SlicerApp/Testing/Python/TwoCLIsInParallelTest.py @@ -9,29 +9,29 @@ # class TwoCLIsInParallelTest(ScriptedLoadableModule): - def __init__(self, parent): - parent.title = "TwoCLIsInParallelTest" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = ["CLI4Test"] - parent.contributors = ["Johan Andruejol (Kitware)"] - parent.helpText = """ + def __init__(self, parent): + parent.title = "TwoCLIsInParallelTest" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = ["CLI4Test"] + parent.contributors = ["Johan Andruejol (Kitware)"] + parent.helpText = """ This is a self test that tests running two CLIs in parallel through python """ - parent.acknowledgementText = """""" # replace with organization, grant and thanks. - self.parent = parent + parent.acknowledgementText = """""" # replace with organization, grant and thanks. + self.parent = parent - # Add this test to the SelfTest module's list for discovery when the module - # is created. Since this module may be discovered before SelfTests itself, - # create the list if it doesn't already exist. - try: - slicer.selfTests - except AttributeError: - slicer.selfTests = {} - slicer.selfTests['TwoCLIsInParallelTest'] = self.runTest + # Add this test to the SelfTest module's list for discovery when the module + # is created. Since this module may be discovered before SelfTests itself, + # create the list if it doesn't already exist. + try: + slicer.selfTests + except AttributeError: + slicer.selfTests = {} + slicer.selfTests['TwoCLIsInParallelTest'] = self.runTest - def runTest(self): - tester = TwoCLIsInParallelTestTest() - tester.runTest() + def runTest(self): + tester = TwoCLIsInParallelTestTest() + tester.runTest() # @@ -40,8 +40,8 @@ def runTest(self): class TwoCLIsInParallelTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -49,50 +49,50 @@ def setup(self): # class TwoCLIsInParallelTestLogic(ScriptedLoadableModuleLogic): - def __init__(self): - self.Observations = [] - self.StatusModifiedEvent = slicer.vtkMRMLCommandLineModuleNode().StatusModifiedEvent - - self.parameters = {} - self.success = False - - def runModule1(self): - cliModule = slicer.modules.cli4test - cliNode = slicer.cli.createNode(cliModule) - cliNode.SetName("CLIModule1") - self.addObserver(cliNode, self.StatusModifiedEvent, self.onModule1Modified) - cliNode = slicer.cli.run(cliModule, cliNode, self.parameters, False) - - def onModule1Modified(self, cliNode, event): - print("--",cliNode.GetStatusString(),":", cliNode.GetName()) - if not cliNode.IsBusy(): - self.removeObservers(cliNode, self.StatusModifiedEvent, self.onModule1Modified) - - def runModule2(self): - cliModule = slicer.modules.cli4test - cliNode = slicer.cli.createNode(cliModule) - cliNode.SetName("CLIModule2") - cliNode = slicer.cli.run(cliModule, cliNode, self.parameters, True) - self.success = cliNode.GetStatusString() == 'Completed' - - def addObserver(self, object, event, method, group = 'none'): - if self.hasObserver(object, event, method): - print(object.GetName(),'already has observer') - return - tag = object.AddObserver(event, method) - self.Observations.append([object, event, method, group, tag]) - - def hasObserver(self, object, event, method): - for o, e, m, g, t in self.Observations: - if o == object and e == event and m == method: - return True - return False - - def removeObservers(self, object, event, method): - for o, e, m, g, t in self.Observations: - if object == o and event == e and method == m: - o.RemoveObserver(t) - self.Observations.remove([o, e, m, g, t]) + def __init__(self): + self.Observations = [] + self.StatusModifiedEvent = slicer.vtkMRMLCommandLineModuleNode().StatusModifiedEvent + + self.parameters = {} + self.success = False + + def runModule1(self): + cliModule = slicer.modules.cli4test + cliNode = slicer.cli.createNode(cliModule) + cliNode.SetName("CLIModule1") + self.addObserver(cliNode, self.StatusModifiedEvent, self.onModule1Modified) + cliNode = slicer.cli.run(cliModule, cliNode, self.parameters, False) + + def onModule1Modified(self, cliNode, event): + print("--", cliNode.GetStatusString(), ":", cliNode.GetName()) + if not cliNode.IsBusy(): + self.removeObservers(cliNode, self.StatusModifiedEvent, self.onModule1Modified) + + def runModule2(self): + cliModule = slicer.modules.cli4test + cliNode = slicer.cli.createNode(cliModule) + cliNode.SetName("CLIModule2") + cliNode = slicer.cli.run(cliModule, cliNode, self.parameters, True) + self.success = cliNode.GetStatusString() == 'Completed' + + def addObserver(self, object, event, method, group='none'): + if self.hasObserver(object, event, method): + print(object.GetName(), 'already has observer') + return + tag = object.AddObserver(event, method) + self.Observations.append([object, event, method, group, tag]) + + def hasObserver(self, object, event, method): + for o, e, m, g, t in self.Observations: + if o == object and e == event and m == method: + return True + return False + + def removeObservers(self, object, event, method): + for o, e, m, g, t in self.Observations: + if object == o and event == e and method == m: + o.RemoveObserver(t) + self.Observations.remove([o, e, m, g, t]) # @@ -101,33 +101,33 @@ def removeObservers(self, object, event, method): class TwoCLIsInParallelTestTest(ScriptedLoadableModuleTest): - def setUp(self): - """ Reset the state for testing. - """ - pass + def setUp(self): + """ Reset the state for testing. + """ + pass - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_TwoCLIsInParallelTest() + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_TwoCLIsInParallelTest() - def test_TwoCLIsInParallelTest(self): - self.delayDisplay('Running two CLIs in a row Test') + def test_TwoCLIsInParallelTest(self): + self.delayDisplay('Running two CLIs in a row Test') - tempFile = qt.QTemporaryFile("TwoCLIsInParallelTest-outputFile-XXXXXX") - self.assertTrue(tempFile.open()) + tempFile = qt.QTemporaryFile("TwoCLIsInParallelTest-outputFile-XXXXXX") + self.assertTrue(tempFile.open()) - logic = TwoCLIsInParallelTestLogic() - logic.parameters = {} - logic.parameters["InputValue1"] = 1 - logic.parameters["InputValue2"] = 2 - logic.parameters["OperationType"] = 'Addition' - logic.parameters["OutputFile"] = tempFile.fileName() + logic = TwoCLIsInParallelTestLogic() + logic.parameters = {} + logic.parameters["InputValue1"] = 1 + logic.parameters["InputValue2"] = 2 + logic.parameters["OperationType"] = 'Addition' + logic.parameters["OutputFile"] = tempFile.fileName() - logic.runModule1() - self.delayDisplay('... Waiting to start module 2 ...') - logic.runModule2() - self.assertTrue(logic.success) + logic.runModule1() + self.delayDisplay('... Waiting to start module 2 ...') + logic.runModule2() + self.assertTrue(logic.success) - self.delayDisplay('Two CLIs in parallel test passed !') + self.delayDisplay('Two CLIs in parallel test passed !') diff --git a/Applications/SlicerApp/Testing/Python/UtilTest.py b/Applications/SlicerApp/Testing/Python/UtilTest.py index 2eeab66f338..8ed4fb8dd41 100644 --- a/Applications/SlicerApp/Testing/Python/UtilTest.py +++ b/Applications/SlicerApp/Testing/Python/UtilTest.py @@ -12,16 +12,16 @@ # class UtilTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "UtilTest" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Johan Andruejol (Kitware)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "UtilTest" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Johan Andruejol (Kitware)"] + parent.helpText = """ This is a self test that tests the methods of slicer.util """ - parent.acknowledgementText = """""" # replace with organization, grant and thanks. + parent.acknowledgementText = """""" # replace with organization, grant and thanks. # @@ -29,19 +29,19 @@ def __init__(self, parent): # class UtilTestWidget(ScriptedLoadableModuleWidget): - def __init__(self, parent = None): - self.Widget = None - ScriptedLoadableModuleWidget.__init__(self, parent) + def __init__(self, parent=None): + self.Widget = None + ScriptedLoadableModuleWidget.__init__(self, parent) - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - moduleName = 'UtilTest' - scriptedModulesPath = os.path.dirname(slicer.util.modulePath(moduleName)) - path = os.path.join(scriptedModulesPath, 'Resources', 'UI', moduleName + '.ui') + moduleName = 'UtilTest' + scriptedModulesPath = os.path.dirname(slicer.util.modulePath(moduleName)) + path = os.path.join(scriptedModulesPath, 'Resources', 'UI', moduleName + '.ui') - self.Widget = slicer.util.loadUI(path) - self.layout.addWidget(self.Widget) + self.Widget = slicer.util.loadUI(path) + self.layout.addWidget(self.Widget) # @@ -49,8 +49,8 @@ def setup(self): # class UtilTestLogic(ScriptedLoadableModuleLogic): - def __init__(self): - ScriptedLoadableModuleLogic.__init__(self) + def __init__(self): + ScriptedLoadableModuleLogic.__init__(self) # @@ -59,427 +59,427 @@ def __init__(self): class UtilTestTest(ScriptedLoadableModuleTest): - def setUp(self): - """ Reset the state for testing. - """ - slicer.mrmlScene.Clear(0) - - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_setSliceViewerLayers() - self.test_loadUI() - self.test_findChild() - self.test_arrayFromVolume() - self.test_updateVolumeFromArray() - self.test_updateTableFromArray() - self.test_arrayFromModelPoints() - self.test_arrayFromVTKMatrix() - self.test_arrayFromTransformMatrix() - self.test_arrayFromMarkupsControlPoints() - self.test_array() - - def test_setSliceViewerLayers(self): - self.delayDisplay('Testing slicer.util.setSliceViewerLayers') - - layoutManager = slicer.app.layoutManager() - layoutManager.layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView - - redSliceCompositeNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceCompositeNodeRed') - self.assertIsNotNone(redSliceCompositeNode) - self.assertIsNone(redSliceCompositeNode.GetBackgroundVolumeID()) - self.assertIsNone(redSliceCompositeNode.GetForegroundVolumeID()) - self.assertIsNone(redSliceCompositeNode.GetLabelVolumeID()) - - import SampleData - - backgroundNode = SampleData.downloadSample("MRHead") - backgroundNode.SetName('Background') - foregroundNode = SampleData.downloadSample("MRHead") - foregroundNode.SetName('Foreground') - - volumesLogic = slicer.modules.volumes.logic() - labelmapNode = volumesLogic.CreateAndAddLabelVolume( slicer.mrmlScene, backgroundNode, 'Labelmap' ) - - thresholder = vtk.vtkImageThreshold() - thresholder.SetInputData(backgroundNode.GetImageData()) - thresholder.ThresholdByLower(80) - thresholder.Update() - labelmapNode.SetAndObserveImageData(thresholder.GetOutput()) - - # Try with nothing - slicer.util.setSliceViewerLayers(background = None, foreground = None, label = None) - self.assertIsNone(redSliceCompositeNode.GetBackgroundVolumeID()) - self.assertIsNone(redSliceCompositeNode.GetForegroundVolumeID()) - self.assertIsNone(redSliceCompositeNode.GetLabelVolumeID()) - - # Try with nodes - slicer.util.setSliceViewerLayers( - background = backgroundNode, - foreground = foregroundNode, - label = labelmapNode, - foregroundOpacity = 0.5, - labelOpacity = 0.1 - ) - self.assertEqual(redSliceCompositeNode.GetBackgroundVolumeID(), backgroundNode.GetID()) - self.assertEqual(redSliceCompositeNode.GetForegroundVolumeID(), foregroundNode.GetID()) - self.assertEqual(redSliceCompositeNode.GetLabelVolumeID(), labelmapNode.GetID()) - self.assertEqual(redSliceCompositeNode.GetForegroundOpacity(), 0.5) - self.assertEqual(redSliceCompositeNode.GetLabelOpacity(), 0.1) - - # Try to reset - otherBackgroundNode = SampleData.downloadSample("MRHead") - otherBackgroundNode.SetName('OtherBackground') - otherForegroundNode = SampleData.downloadSample("MRHead") - otherForegroundNode.SetName('OtherForeground') - otherLabelmapNode = volumesLogic.CreateAndAddLabelVolume( slicer.mrmlScene, backgroundNode, 'OtherLabelmap' ) - - # Try with node id's - slicer.util.setSliceViewerLayers( - background = otherBackgroundNode.GetID(), - foreground = otherForegroundNode.GetID(), - label = otherLabelmapNode.GetID(), - foregroundOpacity = 0.0, - labelOpacity = 1.0 - ) - self.assertEqual(redSliceCompositeNode.GetBackgroundVolumeID(), otherBackgroundNode.GetID()) - self.assertEqual(redSliceCompositeNode.GetForegroundVolumeID(), otherForegroundNode.GetID()) - self.assertEqual(redSliceCompositeNode.GetLabelVolumeID(), otherLabelmapNode.GetID()) - self.assertEqual(redSliceCompositeNode.GetForegroundOpacity(), 0.0) - self.assertEqual(redSliceCompositeNode.GetLabelOpacity(), 1.0) - - self.delayDisplay('Testing slicer.util.setSliceViewerLayers passed') - - def test_loadUI(self): - # Try to load a UI that does not exist and catch exception - caughtException = False - try: - slicer.util.loadUI('does/not/exists.ui') - except RuntimeError: - caughtException = True - self.assertTrue(caughtException) - - # Correct path - utilWidget = UtilTestWidget() - - # Try to get a widget that exists - label = slicer.util.findChild(utilWidget.parent, 'UtilTest_Label') - self.assertIsNotNone(label, qt.QLabel) - self.assertEqual(label.text, 'My custom UI') - - # Parent window is created automatically, delete it now to prevent memory leaks - utilWidget.parent.deleteLater() - - def test_findChild(self): - # Create a top-level widget (parent is not specified) - utilWidget = UtilTestWidget() - - # Try to get a widget that exists - label = slicer.util.findChild(utilWidget.Widget, 'UtilTest_Label') - self.assertIsNotNone(label, qt.QLabel) - self.assertEqual(label.text, 'My custom UI') - - # Try to get a widget that does not exists - caughtException = False - try: - slicer.util.findChild(utilWidget.Widget, 'Unexistant_Label') - except RuntimeError: - caughtException = True - self.assertTrue(caughtException) - - # Parent window is created automatically, delete it now to prevent memory leaks - utilWidget.parent.deleteLater() - - def test_arrayFromVolume(self): - # Test if retrieving voxels as a numpy array works - - self.delayDisplay('Download sample data') - import SampleData - volumeNode = SampleData.downloadSample("MRHead") - - self.delayDisplay('Test voxel value read') - voxelPos = [120,135,89] - voxelValueVtk = volumeNode.GetImageData().GetScalarComponentAsDouble(voxelPos[0], voxelPos[1], voxelPos[2], 0) - narray = slicer.util.arrayFromVolume(volumeNode) - voxelValueNumpy = narray[voxelPos[2], voxelPos[1], voxelPos[0]] - self.assertEqual(voxelValueVtk, voxelValueNumpy) - - self.delayDisplay('Test voxel value write') - voxelValueNumpy = 155 - narray[voxelPos[2], voxelPos[1], voxelPos[0]] = voxelValueNumpy - voxelValueVtk = volumeNode.GetImageData().GetScalarComponentAsDouble(voxelPos[0], voxelPos[1], voxelPos[2], 0) - self.assertEqual(voxelValueVtk, voxelValueNumpy) - - self.delayDisplay('Testing slicer.util.test_arrayFromVolume passed') - - def test_updateVolumeFromArray(self): - # Test if updating voxels from a numpy array works - - self.delayDisplay('Download sample data') - import SampleData - volumeNode = SampleData.downloadSample("MRHead") - - import numpy as np - - def some_func(x, y, z): - return 0.1*x*x + 0.03*y*y + 0.05*z*z - - f = np.fromfunction(some_func,(30,20,15)) - - slicer.util.updateVolumeFromArray(volumeNode, f) - - self.delayDisplay('Test voxel value update') - voxelPos = [11, 12, 4] - voxelValueNumpy = some_func(voxelPos[2], voxelPos[1], voxelPos[0]) - voxelValueVtk = volumeNode.GetImageData().GetScalarComponentAsDouble(voxelPos[0], voxelPos[1], voxelPos[2], 0) - self.assertEqual(voxelValueVtk, voxelValueNumpy) - - self.delayDisplay('Testing slicer.util.test_updateVolumeFromArray passed') - - def test_updateTableFromArray(self): - # Test if updating table values from a numpy array works - import numpy as np - - self.delayDisplay('Test simple 2D array update') - a=np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12]]) - tableNode1 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") - slicer.util.updateTableFromArray(tableNode1, a) - self.assertEqual(tableNode1.GetNumberOfColumns(), 4) - self.assertEqual(tableNode1.GetNumberOfRows(), 3) - - self.delayDisplay('Download sample data') - import SampleData - volumeNode = SampleData.downloadSample("MRHead") - - self.delayDisplay('Compute histogram') - histogram = np.histogram(slicer.util.arrayFromVolume(volumeNode)) - - self.delayDisplay('Test table update') - tableNode2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") - slicer.util.updateTableFromArray(tableNode2, histogram) - self.assertEqual(tableNode2.GetNumberOfColumns(), 2) - self.assertEqual(tableNode2.GetNumberOfRows(), 11) - - self.delayDisplay('Testing slicer.util.test_updateTableFromArray passed') - - def test_arrayFromModelPoints(self): - # Test if retrieving point coordinates as a numpy array works - - self.delayDisplay('Create a model containing a sphere') - sphere = vtk.vtkSphereSource() - sphere.SetRadius(30.0) - sphere.Update() - modelNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLModelNode') - modelNode.SetAndObservePolyData(sphere.GetOutput()) - modelNode.CreateDefaultDisplayNodes() - a = slicer.util.arrayFromModelPoints(modelNode) - - self.delayDisplay('Change Y scaling') - a[:,2] = a[:,2] * 2.5 - modelNode.GetPolyData().Modified() - - self.delayDisplay('Testing slicer.util.test_arrayFromModelPoints passed') - - def test_arrayFromVTKMatrix(self): - # Test arrayFromVTKMatrix, vtkMatrixFromArray, and updateVTKMatrixFromArray - import numpy as np - - self.delayDisplay('Test arrayFromVTKMatrix, vtkMatrixFromArray, and updateVTKMatrixFromArray') - - # Test 4x4 matrix - a = np.array([[1.5,0.5,0,4],[0,2.0,0,11],[0,0,2.5,6],[0,0,0,1]]) - vmatrix = slicer.util.vtkMatrixFromArray(a) - self.assertTrue(isinstance(vmatrix,vtk.vtkMatrix4x4)) - self.assertEqual(vmatrix.GetElement(0,1), 0.5) - self.assertEqual(vmatrix.GetElement(1,3), 11) - - narray = slicer.util.arrayFromVTKMatrix(vmatrix) - np.testing.assert_array_equal(a, narray) - - vmatrixExisting = vtk.vtkMatrix4x4() - slicer.util.updateVTKMatrixFromArray(vmatrixExisting, a) - narray = slicer.util.arrayFromVTKMatrix(vmatrixExisting) - np.testing.assert_array_equal(a, narray) - - # Test 3x3 matrix - a = np.array([[1.5,0,0],[0,2.0,0],[0,1.2,2.5]]) - vmatrix = slicer.util.vtkMatrixFromArray(a) - self.assertTrue(isinstance(vmatrix,vtk.vtkMatrix3x3)) - self.assertEqual(vmatrix.GetElement(0,0), 1.5) - self.assertEqual(vmatrix.GetElement(2,1), 1.2) - - narray = slicer.util.arrayFromVTKMatrix(vmatrix) - np.testing.assert_array_equal(a, narray) - - vmatrixExisting = vtk.vtkMatrix3x3() - slicer.util.updateVTKMatrixFromArray(vmatrixExisting, a) - narray = slicer.util.arrayFromVTKMatrix(vmatrixExisting) - np.testing.assert_array_equal(a, narray) - - # Test invalid matrix size - caughtException = False - try: - vmatrix = slicer.util.vtkMatrixFromArray(np.zeros([3,4])) - except RuntimeError: - caughtException = True - self.assertTrue(caughtException) - - def test_arrayFromTransformMatrix(self): - # Test arrayFromTransformMatrix and updateTransformMatrixFromArray - import numpy as np - - self.delayDisplay('Test arrayFromTransformMatrix') - - transformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode') - transformMatrix = vtk.vtkMatrix4x4() - transformMatrix.SetElement(0,0, 5.0) - transformMatrix.SetElement(0,1, 3.0) - transformMatrix.SetElement(1,1, 2.3) - transformMatrix.SetElement(2,2, 1.3) - transformMatrix.SetElement(0,3, 11.0) - transformMatrix.SetElement(1,3, 22.0) - transformMatrix.SetElement(2,3, 44.0) - transformNode.SetMatrixTransformToParent(transformMatrix) - - narray = slicer.util.arrayFromTransformMatrix(transformNode) - self.assertEqual(narray.shape, (4, 4)) - self.assertEqual(narray[0,0], 5.0) - self.assertEqual(narray[0,1], 3.0) - self.assertEqual(narray[0,3], 11.0) - self.assertEqual(narray[2,3], 44.0) - - self.delayDisplay('Test arrayFromTransformMatrix with toWorld=True') - - parentTransformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode') - parentTransformMatrix = vtk.vtkMatrix4x4() - parentTransformMatrix.SetElement(1,1, 0.3) - parentTransformMatrix.SetElement(0,3, 30.0) - parentTransformMatrix.SetElement(1,3, 20.0) - parentTransformMatrix.SetElement(2,3, 10.0) - parentTransformNode.SetMatrixTransformToParent(parentTransformMatrix) - narrayParent = slicer.util.arrayFromTransformMatrix(parentTransformNode) - - transformNode.SetAndObserveTransformNodeID(parentTransformNode.GetID()) - - narrayToWorld = slicer.util.arrayFromTransformMatrix(transformNode, toWorld=True) - narrayToWorldExpected = np.dot(narrayParent, narray) - np.testing.assert_array_equal(narrayToWorld, narrayToWorldExpected) - - self.delayDisplay('Test updateTransformMatrixFromArray') - narrayUpdated = np.array([[1.5,0.5,0,4],[0,2.0,0,11],[0,0,2.5,6],[0,0,0,1]]) - slicer.util.updateTransformMatrixFromArray(transformNode, narrayUpdated) - transformMatrixUpdated = vtk.vtkMatrix4x4() - transformNode.GetMatrixTransformToParent(transformMatrixUpdated) - for r in range(4): - for c in range(4): - self.assertEqual(narrayUpdated[r,c], transformMatrixUpdated.GetElement(r,c)) - - self.delayDisplay('Test updateTransformMatrixFromArray with toWorld=True') - narrayUpdated = np.array([[2.5,1.5,0,2],[0,2.0,0,15],[0,1,3.5,6],[0,0,0,1]]) - slicer.util.updateTransformMatrixFromArray(transformNode, narrayUpdated, toWorld=True) - transformMatrixUpdated = vtk.vtkMatrix4x4() - transformNode.GetMatrixTransformToWorld(transformMatrixUpdated) - for r in range(4): - for c in range(4): - self.assertEqual(narrayUpdated[r,c], transformMatrixUpdated.GetElement(r,c)) - - def test_arrayFromMarkupsControlPoints(self): - # Test if retrieving markups control coordinates as a numpy array works - import numpy as np - - self.delayDisplay('Test arrayFromMarkupsControlPoints') - - markupsNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsFiducialNode') - markupsNode.AddControlPoint(vtk.vtkVector3d(10,20,30)) - markupsNode.AddControlPoint(vtk.vtkVector3d(21,21,31)) - markupsNode.AddControlPoint(vtk.vtkVector3d(32,33,44)) - markupsNode.AddControlPoint(vtk.vtkVector3d(45,45,55)) - markupsNode.AddControlPoint(vtk.vtkVector3d(51,41,59)) - - translation = [10.0, 30.0, 20.0] - transformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode') - transformMatrix = vtk.vtkMatrix4x4() - transformMatrix.SetElement(0,3, translation[0]) - transformMatrix.SetElement(1,3, translation[1]) - transformMatrix.SetElement(2,3, translation[2]) - transformNode.SetMatrixTransformToParent(transformMatrix) - markupsNode.SetAndObserveTransformNodeID(transformNode.GetID()) - - narray = slicer.util.arrayFromMarkupsControlPoints(markupsNode) - self.assertEqual(narray.shape, (5, 3)) - self.assertEqual(narray[0,0], 10) - self.assertEqual(narray[1,2], 31) - self.assertEqual(narray[4,2], 59) - - self.delayDisplay('Test arrayFromMarkupsControlPoints with world=True') - - narray = slicer.util.arrayFromMarkupsControlPoints(markupsNode, world=True) - self.assertEqual(narray.shape, (5, 3)) - self.assertEqual(narray[0,0], 10+translation[0]) - self.assertEqual(narray[1,2], 31+translation[2]) - self.assertEqual(narray[4,2], 59+translation[2]) - - self.delayDisplay('Test updateMarkupsControlPointsFromArray') - - narray = np.array([[2,3,4],[6,7,8]]) - slicer.util.updateMarkupsControlPointsFromArray(markupsNode, narray) - self.assertEqual(markupsNode.GetNumberOfControlPoints(), 2) - position = [0]*3 - markupsNode.GetNthControlPointPosition(1,position) - np.testing.assert_array_equal(position,narray[1,:]) - - self.delayDisplay('Test updateMarkupsControlPointsFromArray with world=True') - - narray = np.array([[2,3,4],[6,7,8]]) - slicer.util.updateMarkupsControlPointsFromArray(markupsNode, narray, world=True) - self.assertEqual(markupsNode.GetNumberOfControlPoints(), 2) - markupsNode.GetNthControlPointPositionWorld(1,position) - np.testing.assert_array_equal(position,narray[1,:]) - - def test_array(self): - # Test if convenience function of getting numpy array from various nodes works - - self.delayDisplay('Test array with scalar image') - import SampleData - volumeNode = SampleData.downloadSample("MRHead") - voxelPos = [120,135,89] - voxelValueVtk = volumeNode.GetImageData().GetScalarComponentAsDouble(voxelPos[0], voxelPos[1], voxelPos[2], 0) - narray = slicer.util.arrayFromVolume(volumeNode) - voxelValueNumpy = narray[voxelPos[2], voxelPos[1], voxelPos[0]] - self.assertEqual(voxelValueVtk, voxelValueNumpy) - - # self.delayDisplay('Test array with tensor image') - # tensorVolumeNode = SampleData.downloadSample('DTIBrain') - # narray = slicer.util.array(tensorVolumeNode.GetName()) - # self.assertEqual(narray.shape, (85, 144, 144, 3, 3)) - - self.delayDisplay('Test array with model points') - sphere = vtk.vtkSphereSource() - modelNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLModelNode') - modelNode.SetPolyDataConnection(sphere.GetOutputPort()) - narray = slicer.util.array(modelNode.GetName()) - self.assertEqual(narray.shape, (50, 3)) - - self.delayDisplay('Test array with markups fiducials') - markupsNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsFiducialNode') - markupsNode.AddControlPoint(vtk.vtkVector3d(10,20,30)) - markupsNode.AddControlPoint(vtk.vtkVector3d(21,21,31)) - markupsNode.AddControlPoint(vtk.vtkVector3d(32,33,44)) - markupsNode.AddControlPoint(vtk.vtkVector3d(45,45,55)) - markupsNode.AddControlPoint(vtk.vtkVector3d(51,41,59)) - narray = slicer.util.array(markupsNode.GetName()) - self.assertEqual(narray.shape, (5, 3)) - - self.delayDisplay('Test array with transforms') - transformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode') - transformMatrix = vtk.vtkMatrix4x4() - transformMatrix.SetElement(0,0, 2.0) - transformMatrix.SetElement(0,3, 11.0) - transformMatrix.SetElement(1,3, 22.0) - transformMatrix.SetElement(2,3, 44.0) - transformNode.SetMatrixTransformToParent(transformMatrix) - narray = slicer.util.array(transformNode.GetName()) - self.assertEqual(narray.shape, (4, 4)) - - self.delayDisplay('Testing slicer.util.test_array passed') + def setUp(self): + """ Reset the state for testing. + """ + slicer.mrmlScene.Clear(0) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_setSliceViewerLayers() + self.test_loadUI() + self.test_findChild() + self.test_arrayFromVolume() + self.test_updateVolumeFromArray() + self.test_updateTableFromArray() + self.test_arrayFromModelPoints() + self.test_arrayFromVTKMatrix() + self.test_arrayFromTransformMatrix() + self.test_arrayFromMarkupsControlPoints() + self.test_array() + + def test_setSliceViewerLayers(self): + self.delayDisplay('Testing slicer.util.setSliceViewerLayers') + + layoutManager = slicer.app.layoutManager() + layoutManager.layout = slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView + + redSliceCompositeNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceCompositeNodeRed') + self.assertIsNotNone(redSliceCompositeNode) + self.assertIsNone(redSliceCompositeNode.GetBackgroundVolumeID()) + self.assertIsNone(redSliceCompositeNode.GetForegroundVolumeID()) + self.assertIsNone(redSliceCompositeNode.GetLabelVolumeID()) + + import SampleData + + backgroundNode = SampleData.downloadSample("MRHead") + backgroundNode.SetName('Background') + foregroundNode = SampleData.downloadSample("MRHead") + foregroundNode.SetName('Foreground') + + volumesLogic = slicer.modules.volumes.logic() + labelmapNode = volumesLogic.CreateAndAddLabelVolume(slicer.mrmlScene, backgroundNode, 'Labelmap') + + thresholder = vtk.vtkImageThreshold() + thresholder.SetInputData(backgroundNode.GetImageData()) + thresholder.ThresholdByLower(80) + thresholder.Update() + labelmapNode.SetAndObserveImageData(thresholder.GetOutput()) + + # Try with nothing + slicer.util.setSliceViewerLayers(background=None, foreground=None, label=None) + self.assertIsNone(redSliceCompositeNode.GetBackgroundVolumeID()) + self.assertIsNone(redSliceCompositeNode.GetForegroundVolumeID()) + self.assertIsNone(redSliceCompositeNode.GetLabelVolumeID()) + + # Try with nodes + slicer.util.setSliceViewerLayers( + background=backgroundNode, + foreground=foregroundNode, + label=labelmapNode, + foregroundOpacity=0.5, + labelOpacity=0.1 + ) + self.assertEqual(redSliceCompositeNode.GetBackgroundVolumeID(), backgroundNode.GetID()) + self.assertEqual(redSliceCompositeNode.GetForegroundVolumeID(), foregroundNode.GetID()) + self.assertEqual(redSliceCompositeNode.GetLabelVolumeID(), labelmapNode.GetID()) + self.assertEqual(redSliceCompositeNode.GetForegroundOpacity(), 0.5) + self.assertEqual(redSliceCompositeNode.GetLabelOpacity(), 0.1) + + # Try to reset + otherBackgroundNode = SampleData.downloadSample("MRHead") + otherBackgroundNode.SetName('OtherBackground') + otherForegroundNode = SampleData.downloadSample("MRHead") + otherForegroundNode.SetName('OtherForeground') + otherLabelmapNode = volumesLogic.CreateAndAddLabelVolume(slicer.mrmlScene, backgroundNode, 'OtherLabelmap') + + # Try with node id's + slicer.util.setSliceViewerLayers( + background=otherBackgroundNode.GetID(), + foreground=otherForegroundNode.GetID(), + label=otherLabelmapNode.GetID(), + foregroundOpacity=0.0, + labelOpacity=1.0 + ) + self.assertEqual(redSliceCompositeNode.GetBackgroundVolumeID(), otherBackgroundNode.GetID()) + self.assertEqual(redSliceCompositeNode.GetForegroundVolumeID(), otherForegroundNode.GetID()) + self.assertEqual(redSliceCompositeNode.GetLabelVolumeID(), otherLabelmapNode.GetID()) + self.assertEqual(redSliceCompositeNode.GetForegroundOpacity(), 0.0) + self.assertEqual(redSliceCompositeNode.GetLabelOpacity(), 1.0) + + self.delayDisplay('Testing slicer.util.setSliceViewerLayers passed') + + def test_loadUI(self): + # Try to load a UI that does not exist and catch exception + caughtException = False + try: + slicer.util.loadUI('does/not/exists.ui') + except RuntimeError: + caughtException = True + self.assertTrue(caughtException) + + # Correct path + utilWidget = UtilTestWidget() + + # Try to get a widget that exists + label = slicer.util.findChild(utilWidget.parent, 'UtilTest_Label') + self.assertIsNotNone(label, qt.QLabel) + self.assertEqual(label.text, 'My custom UI') + + # Parent window is created automatically, delete it now to prevent memory leaks + utilWidget.parent.deleteLater() + + def test_findChild(self): + # Create a top-level widget (parent is not specified) + utilWidget = UtilTestWidget() + + # Try to get a widget that exists + label = slicer.util.findChild(utilWidget.Widget, 'UtilTest_Label') + self.assertIsNotNone(label, qt.QLabel) + self.assertEqual(label.text, 'My custom UI') + + # Try to get a widget that does not exists + caughtException = False + try: + slicer.util.findChild(utilWidget.Widget, 'Unexistant_Label') + except RuntimeError: + caughtException = True + self.assertTrue(caughtException) + + # Parent window is created automatically, delete it now to prevent memory leaks + utilWidget.parent.deleteLater() + + def test_arrayFromVolume(self): + # Test if retrieving voxels as a numpy array works + + self.delayDisplay('Download sample data') + import SampleData + volumeNode = SampleData.downloadSample("MRHead") + + self.delayDisplay('Test voxel value read') + voxelPos = [120, 135, 89] + voxelValueVtk = volumeNode.GetImageData().GetScalarComponentAsDouble(voxelPos[0], voxelPos[1], voxelPos[2], 0) + narray = slicer.util.arrayFromVolume(volumeNode) + voxelValueNumpy = narray[voxelPos[2], voxelPos[1], voxelPos[0]] + self.assertEqual(voxelValueVtk, voxelValueNumpy) + + self.delayDisplay('Test voxel value write') + voxelValueNumpy = 155 + narray[voxelPos[2], voxelPos[1], voxelPos[0]] = voxelValueNumpy + voxelValueVtk = volumeNode.GetImageData().GetScalarComponentAsDouble(voxelPos[0], voxelPos[1], voxelPos[2], 0) + self.assertEqual(voxelValueVtk, voxelValueNumpy) + + self.delayDisplay('Testing slicer.util.test_arrayFromVolume passed') + + def test_updateVolumeFromArray(self): + # Test if updating voxels from a numpy array works + + self.delayDisplay('Download sample data') + import SampleData + volumeNode = SampleData.downloadSample("MRHead") + + import numpy as np + + def some_func(x, y, z): + return 0.1 * x * x + 0.03 * y * y + 0.05 * z * z + + f = np.fromfunction(some_func, (30, 20, 15)) + + slicer.util.updateVolumeFromArray(volumeNode, f) + + self.delayDisplay('Test voxel value update') + voxelPos = [11, 12, 4] + voxelValueNumpy = some_func(voxelPos[2], voxelPos[1], voxelPos[0]) + voxelValueVtk = volumeNode.GetImageData().GetScalarComponentAsDouble(voxelPos[0], voxelPos[1], voxelPos[2], 0) + self.assertEqual(voxelValueVtk, voxelValueNumpy) + + self.delayDisplay('Testing slicer.util.test_updateVolumeFromArray passed') + + def test_updateTableFromArray(self): + # Test if updating table values from a numpy array works + import numpy as np + + self.delayDisplay('Test simple 2D array update') + a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) + tableNode1 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") + slicer.util.updateTableFromArray(tableNode1, a) + self.assertEqual(tableNode1.GetNumberOfColumns(), 4) + self.assertEqual(tableNode1.GetNumberOfRows(), 3) + + self.delayDisplay('Download sample data') + import SampleData + volumeNode = SampleData.downloadSample("MRHead") + + self.delayDisplay('Compute histogram') + histogram = np.histogram(slicer.util.arrayFromVolume(volumeNode)) + + self.delayDisplay('Test table update') + tableNode2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") + slicer.util.updateTableFromArray(tableNode2, histogram) + self.assertEqual(tableNode2.GetNumberOfColumns(), 2) + self.assertEqual(tableNode2.GetNumberOfRows(), 11) + + self.delayDisplay('Testing slicer.util.test_updateTableFromArray passed') + + def test_arrayFromModelPoints(self): + # Test if retrieving point coordinates as a numpy array works + + self.delayDisplay('Create a model containing a sphere') + sphere = vtk.vtkSphereSource() + sphere.SetRadius(30.0) + sphere.Update() + modelNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLModelNode') + modelNode.SetAndObservePolyData(sphere.GetOutput()) + modelNode.CreateDefaultDisplayNodes() + a = slicer.util.arrayFromModelPoints(modelNode) + + self.delayDisplay('Change Y scaling') + a[:, 2] = a[:, 2] * 2.5 + modelNode.GetPolyData().Modified() + + self.delayDisplay('Testing slicer.util.test_arrayFromModelPoints passed') + + def test_arrayFromVTKMatrix(self): + # Test arrayFromVTKMatrix, vtkMatrixFromArray, and updateVTKMatrixFromArray + import numpy as np + + self.delayDisplay('Test arrayFromVTKMatrix, vtkMatrixFromArray, and updateVTKMatrixFromArray') + + # Test 4x4 matrix + a = np.array([[1.5, 0.5, 0, 4], [0, 2.0, 0, 11], [0, 0, 2.5, 6], [0, 0, 0, 1]]) + vmatrix = slicer.util.vtkMatrixFromArray(a) + self.assertTrue(isinstance(vmatrix, vtk.vtkMatrix4x4)) + self.assertEqual(vmatrix.GetElement(0, 1), 0.5) + self.assertEqual(vmatrix.GetElement(1, 3), 11) + + narray = slicer.util.arrayFromVTKMatrix(vmatrix) + np.testing.assert_array_equal(a, narray) + + vmatrixExisting = vtk.vtkMatrix4x4() + slicer.util.updateVTKMatrixFromArray(vmatrixExisting, a) + narray = slicer.util.arrayFromVTKMatrix(vmatrixExisting) + np.testing.assert_array_equal(a, narray) + + # Test 3x3 matrix + a = np.array([[1.5, 0, 0], [0, 2.0, 0], [0, 1.2, 2.5]]) + vmatrix = slicer.util.vtkMatrixFromArray(a) + self.assertTrue(isinstance(vmatrix, vtk.vtkMatrix3x3)) + self.assertEqual(vmatrix.GetElement(0, 0), 1.5) + self.assertEqual(vmatrix.GetElement(2, 1), 1.2) + + narray = slicer.util.arrayFromVTKMatrix(vmatrix) + np.testing.assert_array_equal(a, narray) + + vmatrixExisting = vtk.vtkMatrix3x3() + slicer.util.updateVTKMatrixFromArray(vmatrixExisting, a) + narray = slicer.util.arrayFromVTKMatrix(vmatrixExisting) + np.testing.assert_array_equal(a, narray) + + # Test invalid matrix size + caughtException = False + try: + vmatrix = slicer.util.vtkMatrixFromArray(np.zeros([3, 4])) + except RuntimeError: + caughtException = True + self.assertTrue(caughtException) + + def test_arrayFromTransformMatrix(self): + # Test arrayFromTransformMatrix and updateTransformMatrixFromArray + import numpy as np + + self.delayDisplay('Test arrayFromTransformMatrix') + + transformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode') + transformMatrix = vtk.vtkMatrix4x4() + transformMatrix.SetElement(0, 0, 5.0) + transformMatrix.SetElement(0, 1, 3.0) + transformMatrix.SetElement(1, 1, 2.3) + transformMatrix.SetElement(2, 2, 1.3) + transformMatrix.SetElement(0, 3, 11.0) + transformMatrix.SetElement(1, 3, 22.0) + transformMatrix.SetElement(2, 3, 44.0) + transformNode.SetMatrixTransformToParent(transformMatrix) + + narray = slicer.util.arrayFromTransformMatrix(transformNode) + self.assertEqual(narray.shape, (4, 4)) + self.assertEqual(narray[0, 0], 5.0) + self.assertEqual(narray[0, 1], 3.0) + self.assertEqual(narray[0, 3], 11.0) + self.assertEqual(narray[2, 3], 44.0) + + self.delayDisplay('Test arrayFromTransformMatrix with toWorld=True') + + parentTransformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode') + parentTransformMatrix = vtk.vtkMatrix4x4() + parentTransformMatrix.SetElement(1, 1, 0.3) + parentTransformMatrix.SetElement(0, 3, 30.0) + parentTransformMatrix.SetElement(1, 3, 20.0) + parentTransformMatrix.SetElement(2, 3, 10.0) + parentTransformNode.SetMatrixTransformToParent(parentTransformMatrix) + narrayParent = slicer.util.arrayFromTransformMatrix(parentTransformNode) + + transformNode.SetAndObserveTransformNodeID(parentTransformNode.GetID()) + + narrayToWorld = slicer.util.arrayFromTransformMatrix(transformNode, toWorld=True) + narrayToWorldExpected = np.dot(narrayParent, narray) + np.testing.assert_array_equal(narrayToWorld, narrayToWorldExpected) + + self.delayDisplay('Test updateTransformMatrixFromArray') + narrayUpdated = np.array([[1.5, 0.5, 0, 4], [0, 2.0, 0, 11], [0, 0, 2.5, 6], [0, 0, 0, 1]]) + slicer.util.updateTransformMatrixFromArray(transformNode, narrayUpdated) + transformMatrixUpdated = vtk.vtkMatrix4x4() + transformNode.GetMatrixTransformToParent(transformMatrixUpdated) + for r in range(4): + for c in range(4): + self.assertEqual(narrayUpdated[r, c], transformMatrixUpdated.GetElement(r, c)) + + self.delayDisplay('Test updateTransformMatrixFromArray with toWorld=True') + narrayUpdated = np.array([[2.5, 1.5, 0, 2], [0, 2.0, 0, 15], [0, 1, 3.5, 6], [0, 0, 0, 1]]) + slicer.util.updateTransformMatrixFromArray(transformNode, narrayUpdated, toWorld=True) + transformMatrixUpdated = vtk.vtkMatrix4x4() + transformNode.GetMatrixTransformToWorld(transformMatrixUpdated) + for r in range(4): + for c in range(4): + self.assertEqual(narrayUpdated[r, c], transformMatrixUpdated.GetElement(r, c)) + + def test_arrayFromMarkupsControlPoints(self): + # Test if retrieving markups control coordinates as a numpy array works + import numpy as np + + self.delayDisplay('Test arrayFromMarkupsControlPoints') + + markupsNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsFiducialNode') + markupsNode.AddControlPoint(vtk.vtkVector3d(10, 20, 30)) + markupsNode.AddControlPoint(vtk.vtkVector3d(21, 21, 31)) + markupsNode.AddControlPoint(vtk.vtkVector3d(32, 33, 44)) + markupsNode.AddControlPoint(vtk.vtkVector3d(45, 45, 55)) + markupsNode.AddControlPoint(vtk.vtkVector3d(51, 41, 59)) + + translation = [10.0, 30.0, 20.0] + transformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode') + transformMatrix = vtk.vtkMatrix4x4() + transformMatrix.SetElement(0, 3, translation[0]) + transformMatrix.SetElement(1, 3, translation[1]) + transformMatrix.SetElement(2, 3, translation[2]) + transformNode.SetMatrixTransformToParent(transformMatrix) + markupsNode.SetAndObserveTransformNodeID(transformNode.GetID()) + + narray = slicer.util.arrayFromMarkupsControlPoints(markupsNode) + self.assertEqual(narray.shape, (5, 3)) + self.assertEqual(narray[0, 0], 10) + self.assertEqual(narray[1, 2], 31) + self.assertEqual(narray[4, 2], 59) + + self.delayDisplay('Test arrayFromMarkupsControlPoints with world=True') + + narray = slicer.util.arrayFromMarkupsControlPoints(markupsNode, world=True) + self.assertEqual(narray.shape, (5, 3)) + self.assertEqual(narray[0, 0], 10 + translation[0]) + self.assertEqual(narray[1, 2], 31 + translation[2]) + self.assertEqual(narray[4, 2], 59 + translation[2]) + + self.delayDisplay('Test updateMarkupsControlPointsFromArray') + + narray = np.array([[2, 3, 4], [6, 7, 8]]) + slicer.util.updateMarkupsControlPointsFromArray(markupsNode, narray) + self.assertEqual(markupsNode.GetNumberOfControlPoints(), 2) + position = [0] * 3 + markupsNode.GetNthControlPointPosition(1, position) + np.testing.assert_array_equal(position, narray[1, :]) + + self.delayDisplay('Test updateMarkupsControlPointsFromArray with world=True') + + narray = np.array([[2, 3, 4], [6, 7, 8]]) + slicer.util.updateMarkupsControlPointsFromArray(markupsNode, narray, world=True) + self.assertEqual(markupsNode.GetNumberOfControlPoints(), 2) + markupsNode.GetNthControlPointPositionWorld(1, position) + np.testing.assert_array_equal(position, narray[1, :]) + + def test_array(self): + # Test if convenience function of getting numpy array from various nodes works + + self.delayDisplay('Test array with scalar image') + import SampleData + volumeNode = SampleData.downloadSample("MRHead") + voxelPos = [120, 135, 89] + voxelValueVtk = volumeNode.GetImageData().GetScalarComponentAsDouble(voxelPos[0], voxelPos[1], voxelPos[2], 0) + narray = slicer.util.arrayFromVolume(volumeNode) + voxelValueNumpy = narray[voxelPos[2], voxelPos[1], voxelPos[0]] + self.assertEqual(voxelValueVtk, voxelValueNumpy) + + # self.delayDisplay('Test array with tensor image') + # tensorVolumeNode = SampleData.downloadSample('DTIBrain') + # narray = slicer.util.array(tensorVolumeNode.GetName()) + # self.assertEqual(narray.shape, (85, 144, 144, 3, 3)) + + self.delayDisplay('Test array with model points') + sphere = vtk.vtkSphereSource() + modelNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLModelNode') + modelNode.SetPolyDataConnection(sphere.GetOutputPort()) + narray = slicer.util.array(modelNode.GetName()) + self.assertEqual(narray.shape, (50, 3)) + + self.delayDisplay('Test array with markups fiducials') + markupsNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsFiducialNode') + markupsNode.AddControlPoint(vtk.vtkVector3d(10, 20, 30)) + markupsNode.AddControlPoint(vtk.vtkVector3d(21, 21, 31)) + markupsNode.AddControlPoint(vtk.vtkVector3d(32, 33, 44)) + markupsNode.AddControlPoint(vtk.vtkVector3d(45, 45, 55)) + markupsNode.AddControlPoint(vtk.vtkVector3d(51, 41, 59)) + narray = slicer.util.array(markupsNode.GetName()) + self.assertEqual(narray.shape, (5, 3)) + + self.delayDisplay('Test array with transforms') + transformNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLTransformNode') + transformMatrix = vtk.vtkMatrix4x4() + transformMatrix.SetElement(0, 0, 2.0) + transformMatrix.SetElement(0, 3, 11.0) + transformMatrix.SetElement(1, 3, 22.0) + transformMatrix.SetElement(2, 3, 44.0) + transformNode.SetMatrixTransformToParent(transformMatrix) + narray = slicer.util.array(transformNode.GetName()) + self.assertEqual(narray.shape, (4, 4)) + + self.delayDisplay('Testing slicer.util.test_array passed') diff --git a/Applications/SlicerApp/Testing/Python/ViewControllersSliceInterpolationBug1926.py b/Applications/SlicerApp/Testing/Python/ViewControllersSliceInterpolationBug1926.py index 1a835caf61c..05579b36ca0 100644 --- a/Applications/SlicerApp/Testing/Python/ViewControllersSliceInterpolationBug1926.py +++ b/Applications/SlicerApp/Testing/Python/ViewControllersSliceInterpolationBug1926.py @@ -10,22 +10,22 @@ # class ViewControllersSliceInterpolationBug1926(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) - parent.title = "ViewControllers Slice Interpolation Bug 1926" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Jim Miller (GE)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + """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) + parent.title = "ViewControllers Slice Interpolation Bug 1926" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Jim Miller (GE)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ Test case for the interaction between the ViewControllers module, linking, slice interpolation, and the selection of background, foreground, and label images. When entering the ViewControllers module, extra View Controllers are created and configured. If linking is on, then foreground, background, and label selection can be propagated to the other views incorrectly. If the node selectors are bocked from emitting signals, then the viewers maintain their proper volumes. However the slice interpolation widget is then not managed properly. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Jim Miller, GE. and was partially funded by NIH grant U54EB005149. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -33,137 +33,137 @@ def __init__(self, parent): # class ViewControllersSliceInterpolationBug1926Widget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... - # Collapsible button - dummyCollapsibleButton = ctk.ctkCollapsibleButton() - dummyCollapsibleButton.text = "A collapsible button" - self.layout.addWidget(dummyCollapsibleButton) + # Collapsible button + dummyCollapsibleButton = ctk.ctkCollapsibleButton() + dummyCollapsibleButton.text = "A collapsible button" + self.layout.addWidget(dummyCollapsibleButton) - # Layout within the dummy collapsible button - dummyFormLayout = qt.QFormLayout(dummyCollapsibleButton) + # Layout within the dummy collapsible button + dummyFormLayout = qt.QFormLayout(dummyCollapsibleButton) - # HelloWorld button - helloWorldButton = qt.QPushButton("Hello world") - helloWorldButton.toolTip = "Print 'Hello world' in standard output." - dummyFormLayout.addWidget(helloWorldButton) - helloWorldButton.connect('clicked(bool)', self.onHelloWorldButtonClicked) + # HelloWorld button + helloWorldButton = qt.QPushButton("Hello world") + helloWorldButton.toolTip = "Print 'Hello world' in standard output." + dummyFormLayout.addWidget(helloWorldButton) + helloWorldButton.connect('clicked(bool)', self.onHelloWorldButtonClicked) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - # Set local var as instance attribute - self.helloWorldButton = helloWorldButton + # Set local var as instance attribute + self.helloWorldButton = helloWorldButton - def onHelloWorldButtonClicked(self): - print("Hello World !") + def onHelloWorldButtonClicked(self): + print("Hello World !") class ViewControllersSliceInterpolationBug1926Test(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_ViewControllersSliceInterpolationBug19261() - - def test_ViewControllersSliceInterpolationBug19261(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. + 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 """ - self.delayDisplay("Starting the test") - # - # first, get some data - # - self.delayDisplay("Getting Data") - import SampleData - head = SampleData.downloadSample("MRHead") - tumor = SampleData.downloadSample('MRBrainTumor1') - - # Change to a CompareView - ln = slicer.util.getNode(pattern='vtkMRMLLayoutNode*') - ln.SetNumberOfCompareViewRows(2) - ln.SetNumberOfCompareViewLightboxColumns(4) - ln.SetViewArrangement(12) - self.delayDisplay('Compare View') - - # Get the slice logic, slice node and slice composite node for the - # first compare viewer - logic = slicer.app.layoutManager().sliceWidget('Compare1').sliceLogic() - compareNode = logic.GetSliceNode() - compareCNode = logic.GetSliceCompositeNode() - - # Link the viewers - compareCNode.SetLinkedControl(1) - self.delayDisplay('Linked the viewers (first Compare View)') - - # Set the data to be same on all viewers - logic.StartSliceCompositeNodeInteraction(2) #BackgroundVolumeFlag - compareCNode.SetBackgroundVolumeID(tumor.GetID()) - logic.EndSliceCompositeNodeInteraction() - self.assertEqual( compareCNode.GetBackgroundVolumeID(), tumor.GetID()) - self.delayDisplay('Compare views configured') - - # Get handles to the Red viewer - redLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic() - redNode = redLogic.GetSliceNode() - redCNode = redLogic.GetSliceCompositeNode() - - # Configure the red viewer with a different dataset - redCNode.SetLinkedControl(0) - redLogic.StartSliceCompositeNodeInteraction(2) #BackgroundVolumeFlag - redCNode.SetBackgroundVolumeID(head.GetID()) - redLogic.EndSliceCompositeNodeInteraction() - redCNode.SetLinkedControl(1) - self.delayDisplay('Red viewer configured') - - # Get handles to the second compare view - compareNode2 = slicer.util.getNode('vtkMRMLSliceNodeCompare2') - compareCNode2 = slicer.util.getNode('vtkMRMLSliceCompositeNodeCompare2') - - # Check whether the viewers have the proper data initially - self.assertEqual( redCNode.GetBackgroundVolumeID(), head.GetID()) - self.assertEqual( compareCNode.GetBackgroundVolumeID(), tumor.GetID()) - self.assertEqual( compareCNode2.GetBackgroundVolumeID(), tumor.GetID()) - self.delayDisplay('All viewers configured properly') - - # Switch to the View Controllers module - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('ViewControllers') - self.delayDisplay("Entered View Controllers module") - - # Check the volume selectors - self.assertTrue( redCNode.GetBackgroundVolumeID() == head.GetID()) - self.assertTrue( compareCNode.GetBackgroundVolumeID() == tumor.GetID()) - self.assertTrue( compareCNode2.GetBackgroundVolumeID() == tumor.GetID()) - self.delayDisplay('All viewers still configured properly') - - # Check whether we can change the interpolation (needs to check gui) - redWidget = slicer.app.layoutManager().sliceWidget('Red') - redController = redWidget.sliceController() - - self.delayDisplay('Test passed!') + 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_ViewControllersSliceInterpolationBug19261() + + def test_ViewControllersSliceInterpolationBug19261(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 + # + self.delayDisplay("Getting Data") + import SampleData + head = SampleData.downloadSample("MRHead") + tumor = SampleData.downloadSample('MRBrainTumor1') + + # Change to a CompareView + ln = slicer.util.getNode(pattern='vtkMRMLLayoutNode*') + ln.SetNumberOfCompareViewRows(2) + ln.SetNumberOfCompareViewLightboxColumns(4) + ln.SetViewArrangement(12) + self.delayDisplay('Compare View') + + # Get the slice logic, slice node and slice composite node for the + # first compare viewer + logic = slicer.app.layoutManager().sliceWidget('Compare1').sliceLogic() + compareNode = logic.GetSliceNode() + compareCNode = logic.GetSliceCompositeNode() + + # Link the viewers + compareCNode.SetLinkedControl(1) + self.delayDisplay('Linked the viewers (first Compare View)') + + # Set the data to be same on all viewers + logic.StartSliceCompositeNodeInteraction(2) # BackgroundVolumeFlag + compareCNode.SetBackgroundVolumeID(tumor.GetID()) + logic.EndSliceCompositeNodeInteraction() + self.assertEqual(compareCNode.GetBackgroundVolumeID(), tumor.GetID()) + self.delayDisplay('Compare views configured') + + # Get handles to the Red viewer + redLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic() + redNode = redLogic.GetSliceNode() + redCNode = redLogic.GetSliceCompositeNode() + + # Configure the red viewer with a different dataset + redCNode.SetLinkedControl(0) + redLogic.StartSliceCompositeNodeInteraction(2) # BackgroundVolumeFlag + redCNode.SetBackgroundVolumeID(head.GetID()) + redLogic.EndSliceCompositeNodeInteraction() + redCNode.SetLinkedControl(1) + self.delayDisplay('Red viewer configured') + + # Get handles to the second compare view + compareNode2 = slicer.util.getNode('vtkMRMLSliceNodeCompare2') + compareCNode2 = slicer.util.getNode('vtkMRMLSliceCompositeNodeCompare2') + + # Check whether the viewers have the proper data initially + self.assertEqual(redCNode.GetBackgroundVolumeID(), head.GetID()) + self.assertEqual(compareCNode.GetBackgroundVolumeID(), tumor.GetID()) + self.assertEqual(compareCNode2.GetBackgroundVolumeID(), tumor.GetID()) + self.delayDisplay('All viewers configured properly') + + # Switch to the View Controllers module + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('ViewControllers') + self.delayDisplay("Entered View Controllers module") + + # Check the volume selectors + self.assertTrue(redCNode.GetBackgroundVolumeID() == head.GetID()) + self.assertTrue(compareCNode.GetBackgroundVolumeID() == tumor.GetID()) + self.assertTrue(compareCNode2.GetBackgroundVolumeID() == tumor.GetID()) + self.delayDisplay('All viewers still configured properly') + + # Check whether we can change the interpolation (needs to check gui) + redWidget = slicer.app.layoutManager().sliceWidget('Red') + redController = redWidget.sliceController() + + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/WebEngine.py b/Applications/SlicerApp/Testing/Python/WebEngine.py index 33a0326aeae..ff11ca19c95 100644 --- a/Applications/SlicerApp/Testing/Python/WebEngine.py +++ b/Applications/SlicerApp/Testing/Python/WebEngine.py @@ -10,22 +10,22 @@ # class WebEngine(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) - parent.title = "WebEngine" - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Steve Pieper (Isomics)"] - parent.helpText = """ + """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) + parent.title = "WebEngine" + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Steve Pieper (Isomics)"] + parent.helpText = """ Module to test WebEngine. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Steve Pieper and was partially funded by NSF grant 1759883 -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -33,160 +33,160 @@ def __init__(self, parent): # class WebEngineWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def __init__(self, parent): - ScriptedLoadableModuleWidget.__init__(self, parent) - self.webWidgets = [] # hold references so windows persist - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... - - # Collapsible button - sitesCollapsibleButton = ctk.ctkCollapsibleButton() - sitesCollapsibleButton.text = "Sample Sites" - self.layout.addWidget(sitesCollapsibleButton) - - # Layout within the collapsible button - sitesFormLayout = qt.QFormLayout(sitesCollapsibleButton) - - # site buttons - buttons = [] - self.sites = [ - { - "label": "Web Console", "url": "http://localhost:1337" - }, - { - "label": "Crowds Cure Cancer", "url": "http://cancer.crowds-cure.org" - }, - { - "label": "Slicer Home Page", "url": "https://slicer.org" - }, - { - "label": "MorphoSource", "url": "https://www.morphosource.org" - }, - { - "label": "Slicer SampleData", "url": "https://www.slicer.org/wiki/SampleData" - }, - { - "label": "SlicerMorph", "url": "https://slicermorph.github.io" - }, - ] - for site in self.sites: - button = qt.QPushButton(site["label"]) - button.toolTip = "Open %s" % site["url"] - sitesFormLayout.addWidget(button) - onClick = lambda click, site=site: self.onSiteButtonClicked(site) - button.connect('clicked(bool)', onClick) - buttons.append(button) - - button = qt.QPushButton("Close All") - button.toolTip = "Close all the web views" - button.connect('clicked(bool)', self.onCloseAll) - self.layout.addWidget(button) - - # Add vertical spacer - self.layout.addStretch(1) - - def onSiteButtonClicked(self, site): - webWidget = slicer.qSlicerWebWidget() - slicerGeometry = slicer.util.mainWindow().geometry - webWidget.size = qt.QSize(1536,1024) - webWidget.pos = qt.QPoint(slicerGeometry.x() + 256, slicerGeometry.y() + 128) - webWidget.url = site["url"] - webWidget.show() - self.webWidgets.append(webWidget) - - def onCloseAll(self): - for widget in self.webWidgets: - del widget - self.webWidgets = [] + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + def __init__(self, parent): + ScriptedLoadableModuleWidget.__init__(self, parent) + self.webWidgets = [] # hold references so windows persist + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... + + # Collapsible button + sitesCollapsibleButton = ctk.ctkCollapsibleButton() + sitesCollapsibleButton.text = "Sample Sites" + self.layout.addWidget(sitesCollapsibleButton) + + # Layout within the collapsible button + sitesFormLayout = qt.QFormLayout(sitesCollapsibleButton) + + # site buttons + buttons = [] + self.sites = [ + { + "label": "Web Console", "url": "http://localhost:1337" + }, + { + "label": "Crowds Cure Cancer", "url": "http://cancer.crowds-cure.org" + }, + { + "label": "Slicer Home Page", "url": "https://slicer.org" + }, + { + "label": "MorphoSource", "url": "https://www.morphosource.org" + }, + { + "label": "Slicer SampleData", "url": "https://www.slicer.org/wiki/SampleData" + }, + { + "label": "SlicerMorph", "url": "https://slicermorph.github.io" + }, + ] + for site in self.sites: + button = qt.QPushButton(site["label"]) + button.toolTip = "Open %s" % site["url"] + sitesFormLayout.addWidget(button) + onClick = lambda click, site=site: self.onSiteButtonClicked(site) + button.connect('clicked(bool)', onClick) + buttons.append(button) + + button = qt.QPushButton("Close All") + button.toolTip = "Close all the web views" + button.connect('clicked(bool)', self.onCloseAll) + self.layout.addWidget(button) + + # Add vertical spacer + self.layout.addStretch(1) + + def onSiteButtonClicked(self, site): + webWidget = slicer.qSlicerWebWidget() + slicerGeometry = slicer.util.mainWindow().geometry + webWidget.size = qt.QSize(1536, 1024) + webWidget.pos = qt.QPoint(slicerGeometry.x() + 256, slicerGeometry.y() + 128) + webWidget.url = site["url"] + webWidget.show() + self.webWidgets.append(webWidget) + + def onCloseAll(self): + for widget in self.webWidgets: + del widget + self.webWidgets = [] -class WebEngineTest(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. - """ - self.gotResponse = False - self.gotCorrectResponse = False - def runTest(self): - """Run as few or as many tests as needed here. +class WebEngineTest(ScriptedLoadableModuleTest): """ - self.setUp() - self.test_WebEngine1() - - def onEvalResult(self, js, result): - if js == "valueFromSlicer;": - self.delayDisplay("Got Slicer result back from JavaScript") - self.gotResponse = True - if result == "42": - self.gotCorrectResponse = True - self.delayDisplay("Got the expected result back from JavaScript") - else: - self.delayDisplay("Got a result back from JavaScript") - print((js, result)) - - def test_WebEngine1(self): - """ Testing WebEngine + 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 """ - self.delayDisplay("Starting the test") - - webWidget = slicer.qSlicerWebWidget() - webWidget.size = qt.QSize(1024,512) - webWidget.webView().url = qt.QUrl("") - webWidget.show() - self.delayDisplay('Showing widget') - - webWidget.evalJS(""" + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + self.gotResponse = False + self.gotCorrectResponse = False + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_WebEngine1() + + def onEvalResult(self, js, result): + if js == "valueFromSlicer;": + self.delayDisplay("Got Slicer result back from JavaScript") + self.gotResponse = True + if result == "42": + self.gotCorrectResponse = True + self.delayDisplay("Got the expected result back from JavaScript") + else: + self.delayDisplay("Got a result back from JavaScript") + print((js, result)) + + def test_WebEngine1(self): + """ Testing WebEngine + """ + + self.delayDisplay("Starting the test") + + webWidget = slicer.qSlicerWebWidget() + webWidget.size = qt.QSize(1024, 512) + webWidget.webView().url = qt.QUrl("") + webWidget.show() + self.delayDisplay('Showing widget') + + webWidget.evalJS(""" const paragraph = document.createElement('p'); paragraph.innerText = 'Hello from Slicer!'; document.body.appendChild(paragraph); """) - self.delayDisplay('Slicer should be saying hello!') + self.delayDisplay('Slicer should be saying hello!') - # - # Test javascript evaluation + use of "evalResult()" signal - # - webWidget.connect("evalResult(QString,QString)", self.onEvalResult) + # + # Test javascript evaluation + use of "evalResult()" signal + # + webWidget.connect("evalResult(QString,QString)", self.onEvalResult) - self.delayDisplay('Slicer setting a javascript value') + self.delayDisplay('Slicer setting a javascript value') - webWidget.evalJS("const valueFromSlicer = 42;") - webWidget.evalJS("valueFromSlicer;") + webWidget.evalJS("const valueFromSlicer = 42;") + webWidget.evalJS("valueFromSlicer;") - iteration = 0 - while not self.gotResponse and iteration < 3: - # Specify an explicit delay to ensure async execution by the - # webengine has completed. - self.delayDisplay('Waiting for response...', msec=500) - iteration += 1 - webWidget.disconnect("evalResult(QString,QString)", self.onEvalResult) + iteration = 0 + while not self.gotResponse and iteration < 3: + # Specify an explicit delay to ensure async execution by the + # webengine has completed. + self.delayDisplay('Waiting for response...', msec=500) + iteration += 1 + webWidget.disconnect("evalResult(QString,QString)", self.onEvalResult) - if not self.gotResponse: - raise RuntimeError("Never got response from evalJS") + if not self.gotResponse: + raise RuntimeError("Never got response from evalJS") - if not self.gotCorrectResponse: - raise AssertionError("Did not get back expected result!") + if not self.gotCorrectResponse: + raise AssertionError("Did not get back expected result!") - # - # Test python evaluation from javascript - # - self.delayDisplay('Call a python method') + # + # Test python evaluation from javascript + # + self.delayDisplay('Call a python method') - slicer.app.settings().setValue("WebEngine/AllowPythonExecution", ctk.ctkMessageBox.AcceptRole) + slicer.app.settings().setValue("WebEngine/AllowPythonExecution", ctk.ctkMessageBox.AcceptRole) - webWidget.evalJS(r""" + webWidget.evalJS(r""" let pythonCode = "dialog = qt.QInputDialog(slicer.util.mainWindow())\n"; pythonCode += "dialog.setLabelText('hello')\n"; pythonCode += "dialog.open()\n"; @@ -195,27 +195,27 @@ def test_WebEngine1(self): window.slicerPython.evalPython(pythonCode); """) - self.delayDisplay('Test access to python via js', msec=500) + self.delayDisplay('Test access to python via js', msec=500) - if hasattr(slicer.modules, 'slicerPythonValueFromJS'): - del slicer.modules.slicerPythonValueFromJS + if hasattr(slicer.modules, 'slicerPythonValueFromJS'): + del slicer.modules.slicerPythonValueFromJS - webWidget.evalJS(""" + webWidget.evalJS(""" window.slicerPython.evalPython("slicer.modules.slicerPythonValueFromJS = 42"); """) - iteration = 0 - while iteration < 3 and not hasattr(slicer.modules, 'slicerPythonValueFromJS'): - # Specify an explicit delay to ensure async execution by the - # webengine has completed. - self.delayDisplay('Waiting for python value from JS...', msec=500) - iteration += 1 + iteration = 0 + while iteration < 3 and not hasattr(slicer.modules, 'slicerPythonValueFromJS'): + # Specify an explicit delay to ensure async execution by the + # webengine has completed. + self.delayDisplay('Waiting for python value from JS...', msec=500) + iteration += 1 - if iteration >= 3: - raise RuntimeError("Couldn't get python value back from JS") + if iteration >= 3: + raise RuntimeError("Couldn't get python value back from JS") - self.delayDisplay('Value of %d received via javascipt' % slicer.modules.slicerPythonValueFromJS) + self.delayDisplay('Value of %d received via javascipt' % slicer.modules.slicerPythonValueFromJS) - del slicer.modules.slicerPythonValueFromJS + del slicer.modules.slicerPythonValueFromJS - self.delayDisplay('Test passed!') + self.delayDisplay('Test passed!') diff --git a/Applications/SlicerApp/Testing/Python/sceneImport2428.py b/Applications/SlicerApp/Testing/Python/sceneImport2428.py index ccae97ee153..f0979d0eb73 100644 --- a/Applications/SlicerApp/Testing/Python/sceneImport2428.py +++ b/Applications/SlicerApp/Testing/Python/sceneImport2428.py @@ -11,23 +11,23 @@ class sceneImport2428(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) - parent.title = "Scene Import (Issue 2428)" # make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + """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) + parent.title = "Scene Import (Issue 2428)" # make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Steve Pieper (Isomics)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ This is an example of scripted loadable module bundled in an extension. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. This is a module to support testing of https://mantisarchive.slicer.org/view.php?id=2428 -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # # qsceneImport2428Widget @@ -35,192 +35,192 @@ def __init__(self, parent): class sceneImport2428Widget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... - # Collapsible button - dummyCollapsibleButton = ctk.ctkCollapsibleButton() - dummyCollapsibleButton.text = "A collapsible button" - self.layout.addWidget(dummyCollapsibleButton) + # Collapsible button + dummyCollapsibleButton = ctk.ctkCollapsibleButton() + dummyCollapsibleButton.text = "A collapsible button" + self.layout.addWidget(dummyCollapsibleButton) - # Layout within the dummy collapsible button - dummyFormLayout = qt.QFormLayout(dummyCollapsibleButton) + # Layout within the dummy collapsible button + dummyFormLayout = qt.QFormLayout(dummyCollapsibleButton) - # HelloWorld button - helloWorldButton = qt.QPushButton("Hello world") - helloWorldButton.toolTip = "Print 'Hello world' in standard output." - dummyFormLayout.addWidget(helloWorldButton) - helloWorldButton.connect('clicked(bool)', self.onHelloWorldButtonClicked) + # HelloWorld button + helloWorldButton = qt.QPushButton("Hello world") + helloWorldButton.toolTip = "Print 'Hello world' in standard output." + dummyFormLayout.addWidget(helloWorldButton) + helloWorldButton.connect('clicked(bool)', self.onHelloWorldButtonClicked) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - # Set local var as instance attribute - self.helloWorldButton = helloWorldButton + # Set local var as instance attribute + self.helloWorldButton = helloWorldButton - def onHelloWorldButtonClicked(self): - print("Hello World !") + def onHelloWorldButtonClicked(self): + print("Hello World !") class sceneImport2428Test(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_sceneImport24281() - - def test_sceneImport24281(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 - # - self.delayDisplay("Getting Data") - import SampleData - head = SampleData.downloadSample("MRHead") - - # Create segmentation - segmentationNode = slicer.vtkMRMLSegmentationNode() - slicer.mrmlScene.AddNode(segmentationNode) - segmentationNode.CreateDefaultDisplayNodes() # only needed for display - segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(head) - - # Add a few segments - segments = [ - ["Segment A", [0,65,32], 25, [1.0,0.0,0.0]], - ["Segment B", [1, -14, 30], 30, [1.0,1.0,0.0]], - ["Segment C", [0, 28, -7], 15, [0.0,1.0,1.0]], - ["Segment D", [31, 33, 27], 25, [0.0,0.0,1.0]] ] - for [name, position, radius, color] in segments: - seed = vtk.vtkSphereSource() - seed.SetCenter(position) - seed.SetRadius(radius) - seed.Update() - segmentationNode.AddSegmentFromClosedSurfaceRepresentation(seed.GetOutput(), name, color) - - # Export to labelmap volume - headLabel = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode') - slicer.modules.segmentations.logic().ExportVisibleSegmentsToLabelmapNode(segmentationNode, headLabel, head) - - selectionNode = slicer.app.applicationLogic().GetSelectionNode() - selectionNode.SetActiveVolumeID( head.GetID() ) - selectionNode.SetActiveLabelVolumeID( headLabel.GetID() ) - slicer.app.applicationLogic().PropagateVolumeSelection(0) - - # - # now build: - # create a model using the command line module - # based on the current editor parameters - # - make a new hierarchy node - # - - self.delayDisplay( "Building..." ) - - parameters = {} - parameters["InputVolume"] = headLabel.GetID() - # create models for all labels - parameters["JointSmoothing"] = True - parameters["StartLabel"] = -1 - parameters["EndLabel"] = -1 - outHierarchy = slicer.vtkMRMLModelHierarchyNode() - outHierarchy.SetScene( slicer.mrmlScene ) - outHierarchy.SetName( "sceneImport2428Hierachy" ) - slicer.mrmlScene.AddNode( outHierarchy ) - parameters["ModelSceneFile"] = outHierarchy - - modelMaker = slicer.modules.modelmaker - self.CLINode = None - self.CLINode = slicer.cli.runSync(modelMaker, self.CLINode, parameters, delete_temporary_files=False) - - self.delayDisplay("Models built") - - success = self.verifyModels() - - success = success and (slicer.mrmlScene.GetNumberOfNodesByClass( "vtkMRMLModelNode" ) > 3) - - self.delayDisplay("Test finished") - - if success: - self.delayDisplay("Ahh... test passed.") - else: - self.delayDisplay("!$!$!#!@#!@!@$%! Test Failed!!") - - self.assertTrue(success) - - def verifyModels(self): - """Return True if the models have unique polydata and have the - same polydata as their display nodes have. - -# paste this in the slicer console for testing/verifying any scene -def verifyModels(): - nn = getNodes('*ModelNode*') - for n in nn: - mpd = nn[n].GetPolyData() - mdpd = nn[n].GetDisplayNode().GetInputPolyData() - if mpd != mdpd: - print(nn[n].GetName()) - print(mpd,mdpd) - -verifyModels() + 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 """ - # - # now check that all models have the same poly data in the - # model node as in the display node - # - polyDataInScene = [] - fileNamesInScene = [] - success = True - numModels = slicer.mrmlScene.GetNumberOfNodesByClass( "vtkMRMLModelNode" ) - for n in range(numModels): - modelNode = slicer.mrmlScene.GetNthNodeByClass( n, "vtkMRMLModelNode" ) - polyDataInScene.append(modelNode.GetPolyData()) - for dn in range(modelNode.GetNumberOfDisplayNodes()): - displayNode = modelNode.GetNthDisplayNode(dn) - if modelNode.GetPolyData() != displayNode.GetInputPolyData(): - self.delayDisplay("Model %d does not match its display node %d! (name: %s, ids: %s and %s)" % (n,dn,modelNode.GetName(), modelNode.GetID(),displayNode.GetID())) - success = False - for sn in range(modelNode.GetNumberOfStorageNodes()): - storageNode = modelNode.GetNthStorageNode(sn) - fileName = storageNode.GetFileName() - fileNamesInScene.append(fileName) - if fileName in fileNamesInScene: - self.delayDisplay("Model %d has duplicate file name %s! (ids: %s and %s)" % (n,fileName,modelNode.GetID(),storageNode.GetID())) - success = False - - # - # now check that each model has a unique polydata - # - for n in range(numModels): - modelNode = slicer.mrmlScene.GetNthNodeByClass( n, "vtkMRMLModelNode" ) - if polyDataInScene.count(modelNode.GetPolyData()) > 1: - self.delayDisplay(f"Polydata for Model is duplicated! (id: {n} and {modelNode.GetID()})") - success = False - - return success + 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_sceneImport24281() + + def test_sceneImport24281(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 + # + self.delayDisplay("Getting Data") + import SampleData + head = SampleData.downloadSample("MRHead") + + # Create segmentation + segmentationNode = slicer.vtkMRMLSegmentationNode() + slicer.mrmlScene.AddNode(segmentationNode) + segmentationNode.CreateDefaultDisplayNodes() # only needed for display + segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(head) + + # Add a few segments + segments = [ + ["Segment A", [0, 65, 32], 25, [1.0, 0.0, 0.0]], + ["Segment B", [1, -14, 30], 30, [1.0, 1.0, 0.0]], + ["Segment C", [0, 28, -7], 15, [0.0, 1.0, 1.0]], + ["Segment D", [31, 33, 27], 25, [0.0, 0.0, 1.0]]] + for [name, position, radius, color] in segments: + seed = vtk.vtkSphereSource() + seed.SetCenter(position) + seed.SetRadius(radius) + seed.Update() + segmentationNode.AddSegmentFromClosedSurfaceRepresentation(seed.GetOutput(), name, color) + + # Export to labelmap volume + headLabel = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode') + slicer.modules.segmentations.logic().ExportVisibleSegmentsToLabelmapNode(segmentationNode, headLabel, head) + + selectionNode = slicer.app.applicationLogic().GetSelectionNode() + selectionNode.SetActiveVolumeID(head.GetID()) + selectionNode.SetActiveLabelVolumeID(headLabel.GetID()) + slicer.app.applicationLogic().PropagateVolumeSelection(0) + + # + # now build: + # create a model using the command line module + # based on the current editor parameters + # - make a new hierarchy node + # + + self.delayDisplay("Building...") + + parameters = {} + parameters["InputVolume"] = headLabel.GetID() + # create models for all labels + parameters["JointSmoothing"] = True + parameters["StartLabel"] = -1 + parameters["EndLabel"] = -1 + outHierarchy = slicer.vtkMRMLModelHierarchyNode() + outHierarchy.SetScene(slicer.mrmlScene) + outHierarchy.SetName("sceneImport2428Hierachy") + slicer.mrmlScene.AddNode(outHierarchy) + parameters["ModelSceneFile"] = outHierarchy + + modelMaker = slicer.modules.modelmaker + self.CLINode = None + self.CLINode = slicer.cli.runSync(modelMaker, self.CLINode, parameters, delete_temporary_files=False) + + self.delayDisplay("Models built") + + success = self.verifyModels() + + success = success and (slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLModelNode") > 3) + + self.delayDisplay("Test finished") + + if success: + self.delayDisplay("Ahh... test passed.") + else: + self.delayDisplay("!$!$!#!@#!@!@$%! Test Failed!!") + + self.assertTrue(success) + + def verifyModels(self): + """Return True if the models have unique polydata and have the + same polydata as their display nodes have. + + # paste this in the slicer console for testing/verifying any scene + def verifyModels(): + nn = getNodes('*ModelNode*') + for n in nn: + mpd = nn[n].GetPolyData() + mdpd = nn[n].GetDisplayNode().GetInputPolyData() + if mpd != mdpd: + print(nn[n].GetName()) + print(mpd,mdpd) + + verifyModels() + """ + + # + # now check that all models have the same poly data in the + # model node as in the display node + # + polyDataInScene = [] + fileNamesInScene = [] + success = True + numModels = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLModelNode") + for n in range(numModels): + modelNode = slicer.mrmlScene.GetNthNodeByClass(n, "vtkMRMLModelNode") + polyDataInScene.append(modelNode.GetPolyData()) + for dn in range(modelNode.GetNumberOfDisplayNodes()): + displayNode = modelNode.GetNthDisplayNode(dn) + if modelNode.GetPolyData() != displayNode.GetInputPolyData(): + self.delayDisplay("Model %d does not match its display node %d! (name: %s, ids: %s and %s)" % (n, dn, modelNode.GetName(), modelNode.GetID(), displayNode.GetID())) + success = False + for sn in range(modelNode.GetNumberOfStorageNodes()): + storageNode = modelNode.GetNthStorageNode(sn) + fileName = storageNode.GetFileName() + fileNamesInScene.append(fileName) + if fileName in fileNamesInScene: + self.delayDisplay("Model %d has duplicate file name %s! (ids: %s and %s)" % (n, fileName, modelNode.GetID(), storageNode.GetID())) + success = False + + # + # now check that each model has a unique polydata + # + for n in range(numModels): + modelNode = slicer.mrmlScene.GetNthNodeByClass(n, "vtkMRMLModelNode") + if polyDataInScene.count(modelNode.GetPolyData()) > 1: + self.delayDisplay(f"Polydata for Model is duplicated! (id: {n} and {modelNode.GetID()})") + success = False + + return success diff --git a/Base/Python/sitkUtils.py b/Base/Python/sitkUtils.py index 07438d2e69e..ad10167b9e8 100644 --- a/Base/Python/sitkUtils.py +++ b/Base/Python/sitkUtils.py @@ -42,7 +42,7 @@ def GetSlicerITKReadWriteAddress(nodeObjectOrName): so that the image can be read directly from the MRML scene """ myNode = nodeObjectOrName if isinstance(nodeObjectOrName, slicer.vtkMRMLNode) else slicer.util.getNode(nodeObjectOrName) - myNodeSceneAddress = myNode.GetScene().GetAddressAsString("").replace('Addr=','') + myNodeSceneAddress = myNode.GetScene().GetAddressAsString("").replace('Addr=', '') myNodeSceneID = myNode.GetID() myNodeFullITKAddress = 'slicer:' + myNodeSceneAddress + '#' + myNodeSceneID return myNodeFullITKAddress @@ -52,8 +52,8 @@ def EnsureRegistration(): """Make sure MRMLIDImageIO reader is registered. """ if 'MRMLIDImageIO' in sitk.ImageFileReader().GetRegisteredImageIOs(): - # already registered - return + # already registered + return # Probably this hack is not needed anymore, but it would require some work to verify this, # so for now just leave this here: @@ -61,7 +61,7 @@ def EnsureRegistration(): # has a side effect of registering the MRMLIDImageIO file reader. global __sitk__MRMLIDImageIO_Registered__ if __sitk__MRMLIDImageIO_Registered__: - return + return vl = slicer.modules.volumes.logic() - volumeNode = vl.AddArchetypeVolume('_DUMMY_DOES_NOT_EXIST__','invalidRead') + volumeNode = vl.AddArchetypeVolume('_DUMMY_DOES_NOT_EXIST__', 'invalidRead') __sitk__MRMLIDImageIO_Registered__ = True diff --git a/Base/Python/slicer/ScriptedLoadableModule.py b/Base/Python/slicer/ScriptedLoadableModule.py index 9dc8f6a034c..a6584c849b6 100644 --- a/Base/Python/slicer/ScriptedLoadableModule.py +++ b/Base/Python/slicer/ScriptedLoadableModule.py @@ -14,393 +14,393 @@ class ScriptedLoadableModule: - def __init__(self, parent): - super().__init__() - self.parent = parent - self.moduleName = self.__class__.__name__ - - parent.title = "" - parent.categories = [] - parent.dependencies = [] - parent.contributors = ["Andras Lasso (PerkLab, Queen's University), Steve Pieper (Isomics)"] - parent.helpText = """ + def __init__(self, parent): + super().__init__() + self.parent = parent + self.moduleName = self.__class__.__name__ + + parent.title = "" + parent.categories = [] + parent.dependencies = [] + parent.contributors = ["Andras Lasso (PerkLab, Queen's University), Steve Pieper (Isomics)"] + parent.helpText = """ This module was created from a template and the help section has not yet been updated. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This work is supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details. This work is partially supported by PAR-07-249: R01CA131718 NA-MIC Virtual Colonoscopy (See https://www.na-mic.org/Wiki/index.php/NA-MIC_NCBC_Collaboration:NA-MIC_virtual_colonoscopy). """ - # Set module icon from Resources/Icons/.png - moduleDir = os.path.dirname(self.parent.path) - for iconExtension in ['.svg', '.png']: - iconPath = os.path.join(moduleDir, 'Resources/Icons', self.moduleName+iconExtension) - if os.path.isfile(iconPath): - parent.icon = qt.QIcon(iconPath) - break - - # Add this test to the SelfTest module's list for discovery when the module - # is created. Since this module may be discovered before SelfTests itself, - # create the list if it doesn't already exist. - try: - slicer.selfTests - except AttributeError: - slicer.selfTests = {} - slicer.selfTests[self.moduleName] = self.runTest - - def getDefaultModuleDocumentationLink(self, docPage=None): - """Return string that can be inserted into the application help text that contains - link to the module's documentation in current Slicer version's documentation. - The text is "For more information see the online documentation." - If docPage is not specified then the link points to URL returned by :func:`slicer.app.moduleDocumentationUrl`. - """ - if docPage: - url = slicer.app.documentationBaseUrl + docPage - else: - url = slicer.app.moduleDocumentationUrl(self.moduleName) - linkText = f'

For more information see the online documentation.

' - return linkText - - def runTest(self, msec=100, **kwargs): - """ - :param msec: delay to associate with :func:`ScriptedLoadableModuleTest.delayDisplay()`. - """ - # Name of the test case class is expected to be Test - module = importlib.import_module(self.__module__) - className = self.moduleName + 'Test' - try: - TestCaseClass = getattr(module, className) - except AttributeError: - # Treat missing test case class as a failure; provide useful error message - raise AssertionError(f'Test case class not found: {self.__module__}.{className} ') - - testCase = TestCaseClass() - testCase.messageDelay = msec - testCase.runTest(**kwargs) + # Set module icon from Resources/Icons/.png + moduleDir = os.path.dirname(self.parent.path) + for iconExtension in ['.svg', '.png']: + iconPath = os.path.join(moduleDir, 'Resources/Icons', self.moduleName + iconExtension) + if os.path.isfile(iconPath): + parent.icon = qt.QIcon(iconPath) + break + + # Add this test to the SelfTest module's list for discovery when the module + # is created. Since this module may be discovered before SelfTests itself, + # create the list if it doesn't already exist. + try: + slicer.selfTests + except AttributeError: + slicer.selfTests = {} + slicer.selfTests[self.moduleName] = self.runTest + + def getDefaultModuleDocumentationLink(self, docPage=None): + """Return string that can be inserted into the application help text that contains + link to the module's documentation in current Slicer version's documentation. + The text is "For more information see the online documentation." + If docPage is not specified then the link points to URL returned by :func:`slicer.app.moduleDocumentationUrl`. + """ + if docPage: + url = slicer.app.documentationBaseUrl + docPage + else: + url = slicer.app.moduleDocumentationUrl(self.moduleName) + linkText = f'

For more information see the online documentation.

' + return linkText + + def runTest(self, msec=100, **kwargs): + """ + :param msec: delay to associate with :func:`ScriptedLoadableModuleTest.delayDisplay()`. + """ + # Name of the test case class is expected to be Test + module = importlib.import_module(self.__module__) + className = self.moduleName + 'Test' + try: + TestCaseClass = getattr(module, className) + except AttributeError: + # Treat missing test case class as a failure; provide useful error message + raise AssertionError(f'Test case class not found: {self.__module__}.{className} ') + + testCase = TestCaseClass() + testCase.messageDelay = msec + testCase.runTest(**kwargs) class ScriptedLoadableModuleWidget: - def __init__(self, parent = None): - """If parent widget is not specified: a top-level widget is created automatically; - the application has to delete this widget (by calling widget.parent.deleteLater() to avoid memory leaks. - """ - super().__init__() - # Get module name by stripping 'Widget' from the class name - self.moduleName = self.__class__.__name__ - if self.moduleName.endswith('Widget'): - self.moduleName = self.moduleName[:-6] - self.developerMode = slicer.util.settingsValue('Developer/DeveloperMode', False, converter=slicer.util.toBool) - if not parent: - self.parent = slicer.qMRMLWidget() - self.parent.setLayout(qt.QVBoxLayout()) - self.parent.setMRMLScene(slicer.mrmlScene) - else: - self.parent = parent - self.layout = self.parent.layout() - if not parent: - self.setup() - self.parent.show() - slicer.app.moduleManager().connect( - 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded) - - def resourcePath(self, filename): - scriptedModulesPath = os.path.dirname(slicer.util.modulePath(self.moduleName)) - return os.path.join(scriptedModulesPath, 'Resources', filename) - - def cleanup(self): - """Override this function to implement module widget specific cleanup. - - It is invoked when the signal `qSlicerModuleManager::moduleAboutToBeUnloaded(QString)` - corresponding to the current module is emitted and just before a module is - effectively unloaded. - """ - pass - - def _onModuleAboutToBeUnloaded(self, moduleName): - """This slot calls `cleanup()` if the module about to be unloaded is the - current one. - """ - if moduleName == self.moduleName: - self.cleanup() - slicer.app.moduleManager().disconnect( - 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded) - - def setupDeveloperSection(self): - if not self.developerMode: - return - - def createHLayout(elements): - rowLayout = qt.QHBoxLayout() - for element in elements: - rowLayout.addWidget(element) - return rowLayout - - # - # Reload and Test area - # Used during development, but hidden when delivering - # developer mode is turned off. - - self.reloadCollapsibleButton = ctk.ctkCollapsibleButton() - self.reloadCollapsibleButton.text = "Reload && Test" - self.layout.addWidget(self.reloadCollapsibleButton) - reloadFormLayout = qt.QFormLayout(self.reloadCollapsibleButton) - - # reload button - self.reloadButton = qt.QPushButton("Reload") - self.reloadButton.toolTip = "Reload this module." - self.reloadButton.name = "ScriptedLoadableModuleTemplate Reload" - self.reloadButton.connect('clicked()', self.onReload) - - # reload and test button - self.reloadAndTestButton = qt.QPushButton("Reload and Test") - self.reloadAndTestButton.toolTip = "Reload this module and then run the self tests." - self.reloadAndTestButton.connect('clicked()', self.onReloadAndTest) - - # edit python source code - self.editSourceButton = qt.QPushButton("Edit") - self.editSourceButton.toolTip = "Edit the module's source code." - self.editSourceButton.connect('clicked()', self.onEditSource) - - self.editModuleUiButton = None - moduleUiFileName = self.resourcePath('UI/%s.ui' % self.moduleName) - import os.path - if os.path.isfile(moduleUiFileName): - # Module UI file exists - self.editModuleUiButton = qt.QPushButton("Edit UI") - self.editModuleUiButton.toolTip = "Edit the module's .ui file." - self.editModuleUiButton.connect('clicked()', lambda filename=moduleUiFileName: slicer.util.startQtDesigner(moduleUiFileName)) - - # restart Slicer button - # (use this during development, but remove it when delivering - # your module to users) - self.restartButton = qt.QPushButton("Restart Slicer") - self.restartButton.toolTip = "Restart Slicer" - self.restartButton.name = "ScriptedLoadableModuleTemplate Restart" - self.restartButton.connect('clicked()', slicer.app.restart) - - if self.editModuleUiButton: - # There are many buttons, distribute them in two rows - reloadFormLayout.addRow(createHLayout([self.reloadButton, self.reloadAndTestButton, self.restartButton])) - reloadFormLayout.addRow(createHLayout([self.editSourceButton, self.editModuleUiButton])) - else: - reloadFormLayout.addRow(createHLayout([self.reloadButton, self.reloadAndTestButton, self.editSourceButton, self.restartButton])) - - def setup(self): - # Instantiate and connect default widgets ... - self.setupDeveloperSection() - - def onReload(self): - """ - Reload scripted module widget representation. - """ - - # Print a clearly visible separator to make it easier - # to distinguish new error messages (during/after reload) - # from old ones. - print('\n' * 2) - print('-' * 30) - print('Reloading module: '+self.moduleName) - print('-' * 30) - print('\n' * 2) - - slicer.util.reloadScriptedModule(self.moduleName) - - def onReloadAndTest(self, **kwargs): - """Reload scripted module widget representation and call :func:`ScriptedLoadableModuleTest.runTest()` - passing ``kwargs``. - """ - with slicer.util.tryWithErrorDisplay("Reload and Test failed."): - self.onReload() - test = slicer.selfTests[self.moduleName] - test(msec=int(slicer.app.userSettings().value("Developer/SelfTestDisplayMessageDelay")), **kwargs) - - def onEditSource(self): - filePath = slicer.util.modulePath(self.moduleName) - qt.QDesktopServices.openUrl(qt.QUrl("file:///"+filePath, qt.QUrl.TolerantMode)) + def __init__(self, parent=None): + """If parent widget is not specified: a top-level widget is created automatically; + the application has to delete this widget (by calling widget.parent.deleteLater() to avoid memory leaks. + """ + super().__init__() + # Get module name by stripping 'Widget' from the class name + self.moduleName = self.__class__.__name__ + if self.moduleName.endswith('Widget'): + self.moduleName = self.moduleName[:-6] + self.developerMode = slicer.util.settingsValue('Developer/DeveloperMode', False, converter=slicer.util.toBool) + if not parent: + self.parent = slicer.qMRMLWidget() + self.parent.setLayout(qt.QVBoxLayout()) + self.parent.setMRMLScene(slicer.mrmlScene) + else: + self.parent = parent + self.layout = self.parent.layout() + if not parent: + self.setup() + self.parent.show() + slicer.app.moduleManager().connect( + 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded) + + def resourcePath(self, filename): + scriptedModulesPath = os.path.dirname(slicer.util.modulePath(self.moduleName)) + return os.path.join(scriptedModulesPath, 'Resources', filename) + + def cleanup(self): + """Override this function to implement module widget specific cleanup. + + It is invoked when the signal `qSlicerModuleManager::moduleAboutToBeUnloaded(QString)` + corresponding to the current module is emitted and just before a module is + effectively unloaded. + """ + pass + + def _onModuleAboutToBeUnloaded(self, moduleName): + """This slot calls `cleanup()` if the module about to be unloaded is the + current one. + """ + if moduleName == self.moduleName: + self.cleanup() + slicer.app.moduleManager().disconnect( + 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded) + + def setupDeveloperSection(self): + if not self.developerMode: + return + + def createHLayout(elements): + rowLayout = qt.QHBoxLayout() + for element in elements: + rowLayout.addWidget(element) + return rowLayout + + # + # Reload and Test area + # Used during development, but hidden when delivering + # developer mode is turned off. + + self.reloadCollapsibleButton = ctk.ctkCollapsibleButton() + self.reloadCollapsibleButton.text = "Reload && Test" + self.layout.addWidget(self.reloadCollapsibleButton) + reloadFormLayout = qt.QFormLayout(self.reloadCollapsibleButton) + + # reload button + self.reloadButton = qt.QPushButton("Reload") + self.reloadButton.toolTip = "Reload this module." + self.reloadButton.name = "ScriptedLoadableModuleTemplate Reload" + self.reloadButton.connect('clicked()', self.onReload) + + # reload and test button + self.reloadAndTestButton = qt.QPushButton("Reload and Test") + self.reloadAndTestButton.toolTip = "Reload this module and then run the self tests." + self.reloadAndTestButton.connect('clicked()', self.onReloadAndTest) + + # edit python source code + self.editSourceButton = qt.QPushButton("Edit") + self.editSourceButton.toolTip = "Edit the module's source code." + self.editSourceButton.connect('clicked()', self.onEditSource) + + self.editModuleUiButton = None + moduleUiFileName = self.resourcePath('UI/%s.ui' % self.moduleName) + import os.path + if os.path.isfile(moduleUiFileName): + # Module UI file exists + self.editModuleUiButton = qt.QPushButton("Edit UI") + self.editModuleUiButton.toolTip = "Edit the module's .ui file." + self.editModuleUiButton.connect('clicked()', lambda filename=moduleUiFileName: slicer.util.startQtDesigner(moduleUiFileName)) + + # restart Slicer button + # (use this during development, but remove it when delivering + # your module to users) + self.restartButton = qt.QPushButton("Restart Slicer") + self.restartButton.toolTip = "Restart Slicer" + self.restartButton.name = "ScriptedLoadableModuleTemplate Restart" + self.restartButton.connect('clicked()', slicer.app.restart) + + if self.editModuleUiButton: + # There are many buttons, distribute them in two rows + reloadFormLayout.addRow(createHLayout([self.reloadButton, self.reloadAndTestButton, self.restartButton])) + reloadFormLayout.addRow(createHLayout([self.editSourceButton, self.editModuleUiButton])) + else: + reloadFormLayout.addRow(createHLayout([self.reloadButton, self.reloadAndTestButton, self.editSourceButton, self.restartButton])) + + def setup(self): + # Instantiate and connect default widgets ... + self.setupDeveloperSection() + + def onReload(self): + """ + Reload scripted module widget representation. + """ + + # Print a clearly visible separator to make it easier + # to distinguish new error messages (during/after reload) + # from old ones. + print('\n' * 2) + print('-' * 30) + print('Reloading module: ' + self.moduleName) + print('-' * 30) + print('\n' * 2) + + slicer.util.reloadScriptedModule(self.moduleName) + + def onReloadAndTest(self, **kwargs): + """Reload scripted module widget representation and call :func:`ScriptedLoadableModuleTest.runTest()` + passing ``kwargs``. + """ + with slicer.util.tryWithErrorDisplay("Reload and Test failed."): + self.onReload() + test = slicer.selfTests[self.moduleName] + test(msec=int(slicer.app.userSettings().value("Developer/SelfTestDisplayMessageDelay")), **kwargs) + + def onEditSource(self): + filePath = slicer.util.modulePath(self.moduleName) + qt.QDesktopServices.openUrl(qt.QUrl("file:///" + filePath, qt.QUrl.TolerantMode)) class ScriptedLoadableModuleLogic: - def __init__(self, parent = None): - super().__init__() - # Get module name by stripping 'Logic' from the class name - self.moduleName = self.__class__.__name__ - if self.moduleName.endswith('Logic'): - self.moduleName = self.moduleName[:-5] - - # If parameter node is singleton then only one parameter node - # is allowed in a scene. - # Derived classes can set self.isSingletonParameterNode = False - # to allow having multiple parameter nodes in the scene. - self.isSingletonParameterNode = True - - def getParameterNode(self): - """ - Return the first available parameter node for this module - If no parameter nodes are available for this module then a new one is created. - """ - if self.isSingletonParameterNode: - parameterNode = slicer.mrmlScene.GetSingletonNode(self.moduleName, "vtkMRMLScriptedModuleNode") - if parameterNode: - # After close scene, ModuleName attribute may be removed, restore it now - if parameterNode.GetAttribute("ModuleName") != self.moduleName: - parameterNode.SetAttribute("ModuleName", self.moduleName) + def __init__(self, parent=None): + super().__init__() + # Get module name by stripping 'Logic' from the class name + self.moduleName = self.__class__.__name__ + if self.moduleName.endswith('Logic'): + self.moduleName = self.moduleName[:-5] + + # If parameter node is singleton then only one parameter node + # is allowed in a scene. + # Derived classes can set self.isSingletonParameterNode = False + # to allow having multiple parameter nodes in the scene. + self.isSingletonParameterNode = True + + def getParameterNode(self): + """ + Return the first available parameter node for this module + If no parameter nodes are available for this module then a new one is created. + """ + if self.isSingletonParameterNode: + parameterNode = slicer.mrmlScene.GetSingletonNode(self.moduleName, "vtkMRMLScriptedModuleNode") + if parameterNode: + # After close scene, ModuleName attribute may be removed, restore it now + if parameterNode.GetAttribute("ModuleName") != self.moduleName: + parameterNode.SetAttribute("ModuleName", self.moduleName) + return parameterNode + else: + numberOfScriptedModuleNodes = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScriptedModuleNode") + for nodeIndex in range(numberOfScriptedModuleNodes): + parameterNode = slicer.mrmlScene.GetNthNodeByClass(nodeIndex, "vtkMRMLScriptedModuleNode") + if parameterNode.GetAttribute("ModuleName") == self.moduleName: + return parameterNode + # no parameter node was found for this module, therefore we add a new one now + parameterNode = slicer.mrmlScene.AddNode(self.createParameterNode()) return parameterNode - else: - numberOfScriptedModuleNodes = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScriptedModuleNode") - for nodeIndex in range(numberOfScriptedModuleNodes): - parameterNode = slicer.mrmlScene.GetNthNodeByClass( nodeIndex, "vtkMRMLScriptedModuleNode" ) - if parameterNode.GetAttribute("ModuleName") == self.moduleName: - return parameterNode - # no parameter node was found for this module, therefore we add a new one now - parameterNode = slicer.mrmlScene.AddNode(self.createParameterNode()) - return parameterNode - - def getAllParameterNodes(self): - """ - Return a list of all parameter nodes for this module - Multiple parameter nodes are useful for storing multiple parameter sets in a single scene. - """ - foundParameterNodes = [] - numberOfScriptedModuleNodes = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScriptedModuleNode") - for nodeIndex in range(numberOfScriptedModuleNodes): - parameterNode = slicer.mrmlScene.GetNthNodeByClass( nodeIndex, "vtkMRMLScriptedModuleNode" ) - if parameterNode.GetAttribute("ModuleName") == self.moduleName: - foundParameterNodes.append(parameterNode) - return foundParameterNodes - - def createParameterNode(self): - """ - Create a new parameter node - The node is of vtkMRMLScriptedModuleNode class. Module name is added as an attribute to allow filtering - in node selector widgets (attribute name: ModuleName, attribute value: the module's name). - This method can be overridden in derived classes to create a default parameter node with all - parameter values set to their default. - """ - if slicer.mrmlScene is None: - return - node = slicer.mrmlScene.CreateNodeByClass("vtkMRMLScriptedModuleNode") - node.UnRegister(None) # object is owned by the Python variable now - if self.isSingletonParameterNode: - node.SetSingletonTag( self.moduleName ) - # Add module name in an attribute to allow filtering in node selector widgets - # Note that SetModuleName is not used anymore as it would be redundant with the ModuleName attribute. - node.SetAttribute( "ModuleName", self.moduleName ) - node.SetName(slicer.mrmlScene.GenerateUniqueName(self.moduleName)) - return node + def getAllParameterNodes(self): + """ + Return a list of all parameter nodes for this module + Multiple parameter nodes are useful for storing multiple parameter sets in a single scene. + """ + foundParameterNodes = [] + numberOfScriptedModuleNodes = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScriptedModuleNode") + for nodeIndex in range(numberOfScriptedModuleNodes): + parameterNode = slicer.mrmlScene.GetNthNodeByClass(nodeIndex, "vtkMRMLScriptedModuleNode") + if parameterNode.GetAttribute("ModuleName") == self.moduleName: + foundParameterNodes.append(parameterNode) + return foundParameterNodes + + def createParameterNode(self): + """ + Create a new parameter node + The node is of vtkMRMLScriptedModuleNode class. Module name is added as an attribute to allow filtering + in node selector widgets (attribute name: ModuleName, attribute value: the module's name). + This method can be overridden in derived classes to create a default parameter node with all + parameter values set to their default. + """ + if slicer.mrmlScene is None: + return + + node = slicer.mrmlScene.CreateNodeByClass("vtkMRMLScriptedModuleNode") + node.UnRegister(None) # object is owned by the Python variable now + if self.isSingletonParameterNode: + node.SetSingletonTag(self.moduleName) + # Add module name in an attribute to allow filtering in node selector widgets + # Note that SetModuleName is not used anymore as it would be redundant with the ModuleName attribute. + node.SetAttribute("ModuleName", self.moduleName) + node.SetName(slicer.mrmlScene.GenerateUniqueName(self.moduleName)) + return node class ScriptedLoadableModuleTest(unittest.TestCase): - """ - Base class for module tester class. - Setting messageDelay to something small, like 50ms allows - faster development time. - """ - - def __init__(self, *args, **kwargs): - super(ScriptedLoadableModuleTest, self).__init__(*args, **kwargs) - - # See https://github.com/Slicer/Slicer/pull/6243#issuecomment-1061800718 for more information. - # Do not pass *args, **kwargs since there is no base class after `unittest.TestCase`. This is only relevant to - # mixins of derived classes, which should each have the same signature as `object()`. - super(unittest.TestCase, self).__init__() - - # takeScreenshot default parameters - self.enableScreenshots = False - self.screenshotScaleFactor = 1.0 - - def delayDisplay(self,message,requestedDelay=None,msec=None): """ - Display messages to the user/tester during testing. - - By default, the delay is 50ms. - - The function accepts the keyword arguments ``requestedDelay`` or ``msec``. If both - are specified, the value associated with ``msec`` is used. - - This method can be temporarily overridden to allow tests running - with longer or shorter message display time. - - Displaying a dialog and waiting does two things: - 1) it lets the event loop catch up to the state of the test so - that rendering and widget updates have all taken place before - the test continues and - 2) it shows the user/developer/tester the state of the test - so that we'll know when it breaks. - - Note: - Information that might be useful (but not important enough to show - to the user) can be logged using logging.info() function - (printed to console and application log) or logging.debug() - function (printed to application log only). - Error messages should be logged by logging.error() function - and displayed to user by slicer.util.errorDisplay function. - """ - if hasattr(self, "messageDelay"): - msec = self.messageDelay - if msec is None: - msec = requestedDelay - if msec is None: - msec = 100 - - slicer.util.delayDisplay(message, msec) - - def takeScreenshot(self,name,description,type=-1): - """ Take a screenshot of the selected viewport and store as and - annotation snapshot node. Convenience method for automated testing. - - If self.enableScreenshots is False then only a message is displayed but screenshot - is not stored. Screenshots are scaled by self.screenshotScaleFactor. - - :param name: snapshot node name - :param description: description of the node - :param type: which viewport to capture. If not specified then captures the entire window. - Valid values: slicer.qMRMLScreenShotDialog.FullLayout, - slicer.qMRMLScreenShotDialog.ThreeD, slicer.qMRMLScreenShotDialog.Red, - slicer.qMRMLScreenShotDialog.Yellow, slicer.qMRMLScreenShotDialog.Green. + Base class for module tester class. + Setting messageDelay to something small, like 50ms allows + faster development time. """ - # show the message even if not taking a screen shot - self.delayDisplay(description) - - if not self.enableScreenshots: - return - - lm = slicer.app.layoutManager() - # switch on the type to get the requested window - widget = 0 - if type == slicer.qMRMLScreenShotDialog.FullLayout: - # full layout - widget = lm.viewport() - elif type == slicer.qMRMLScreenShotDialog.ThreeD: - # just the 3D window - widget = lm.threeDWidget(0).threeDView() - elif type == slicer.qMRMLScreenShotDialog.Red: - # red slice window - widget = lm.sliceWidget("Red") - elif type == slicer.qMRMLScreenShotDialog.Yellow: - # yellow slice window - widget = lm.sliceWidget("Yellow") - elif type == slicer.qMRMLScreenShotDialog.Green: - # green slice window - widget = lm.sliceWidget("Green") - else: - # default to using the full window - widget = slicer.util.mainWindow() - # reset the type so that the node is set correctly - type = slicer.qMRMLScreenShotDialog.FullLayout - - # grab and convert to vtk image data - qimage = ctk.ctkWidgetsUtils.grabWidget(widget) - imageData = vtk.vtkImageData() - slicer.qMRMLUtils().qImageToVtkImageData(qimage,imageData) - - annotationLogic = slicer.modules.annotations.logic() - annotationLogic.CreateSnapShot(name, description, type, self.screenshotScaleFactor, imageData) - - def runTest(self): - """ - Run a default selection of tests here. - """ - logging.warning('No test is defined in '+self.__class__.__name__) + def __init__(self, *args, **kwargs): + super(ScriptedLoadableModuleTest, self).__init__(*args, **kwargs) + + # See https://github.com/Slicer/Slicer/pull/6243#issuecomment-1061800718 for more information. + # Do not pass *args, **kwargs since there is no base class after `unittest.TestCase`. This is only relevant to + # mixins of derived classes, which should each have the same signature as `object()`. + super(unittest.TestCase, self).__init__() + + # takeScreenshot default parameters + self.enableScreenshots = False + self.screenshotScaleFactor = 1.0 + + def delayDisplay(self, message, requestedDelay=None, msec=None): + """ + Display messages to the user/tester during testing. + + By default, the delay is 50ms. + + The function accepts the keyword arguments ``requestedDelay`` or ``msec``. If both + are specified, the value associated with ``msec`` is used. + + This method can be temporarily overridden to allow tests running + with longer or shorter message display time. + + Displaying a dialog and waiting does two things: + 1) it lets the event loop catch up to the state of the test so + that rendering and widget updates have all taken place before + the test continues and + 2) it shows the user/developer/tester the state of the test + so that we'll know when it breaks. + + Note: + Information that might be useful (but not important enough to show + to the user) can be logged using logging.info() function + (printed to console and application log) or logging.debug() + function (printed to application log only). + Error messages should be logged by logging.error() function + and displayed to user by slicer.util.errorDisplay function. + """ + if hasattr(self, "messageDelay"): + msec = self.messageDelay + if msec is None: + msec = requestedDelay + if msec is None: + msec = 100 + + slicer.util.delayDisplay(message, msec) + + def takeScreenshot(self, name, description, type=-1): + """ Take a screenshot of the selected viewport and store as and + annotation snapshot node. Convenience method for automated testing. + + If self.enableScreenshots is False then only a message is displayed but screenshot + is not stored. Screenshots are scaled by self.screenshotScaleFactor. + + :param name: snapshot node name + :param description: description of the node + :param type: which viewport to capture. If not specified then captures the entire window. + Valid values: slicer.qMRMLScreenShotDialog.FullLayout, + slicer.qMRMLScreenShotDialog.ThreeD, slicer.qMRMLScreenShotDialog.Red, + slicer.qMRMLScreenShotDialog.Yellow, slicer.qMRMLScreenShotDialog.Green. + """ + + # show the message even if not taking a screen shot + self.delayDisplay(description) + + if not self.enableScreenshots: + return + + lm = slicer.app.layoutManager() + # switch on the type to get the requested window + widget = 0 + if type == slicer.qMRMLScreenShotDialog.FullLayout: + # full layout + widget = lm.viewport() + elif type == slicer.qMRMLScreenShotDialog.ThreeD: + # just the 3D window + widget = lm.threeDWidget(0).threeDView() + elif type == slicer.qMRMLScreenShotDialog.Red: + # red slice window + widget = lm.sliceWidget("Red") + elif type == slicer.qMRMLScreenShotDialog.Yellow: + # yellow slice window + widget = lm.sliceWidget("Yellow") + elif type == slicer.qMRMLScreenShotDialog.Green: + # green slice window + widget = lm.sliceWidget("Green") + else: + # default to using the full window + widget = slicer.util.mainWindow() + # reset the type so that the node is set correctly + type = slicer.qMRMLScreenShotDialog.FullLayout + + # grab and convert to vtk image data + qimage = ctk.ctkWidgetsUtils.grabWidget(widget) + imageData = vtk.vtkImageData() + slicer.qMRMLUtils().qImageToVtkImageData(qimage, imageData) + + annotationLogic = slicer.modules.annotations.logic() + annotationLogic.CreateSnapShot(name, description, type, self.screenshotScaleFactor, imageData) + + def runTest(self): + """ + Run a default selection of tests here. + """ + logging.warning('No test is defined in ' + self.__class__.__name__) diff --git a/Base/Python/slicer/__init__.py b/Base/Python/slicer/__init__.py index 7ec0237cc45..2b37efa2d6a 100644 --- a/Base/Python/slicer/__init__.py +++ b/Base/Python/slicer/__init__.py @@ -1,60 +1,60 @@ """ This module sets up root logging and loads the Slicer library modules into its namespace.""" -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _createModule(name, globals, docstring): - import imp - import sys - moduleName = name.split('.')[-1] - module = imp.new_module( moduleName ) - module.__file__ = __file__ - module.__doc__ = docstring - sys.modules[name] = module - globals[moduleName] = module + import imp + import sys + moduleName = name.split('.')[-1] + module = imp.new_module(moduleName) + module.__file__ = __file__ + module.__doc__ = docstring + sys.modules[name] = module + globals[moduleName] = module -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Create slicer.modules and slicer.moduleNames _createModule('slicer.modules', globals(), -"""This module provides an access to all instantiated Slicer modules. + """This module provides an access to all instantiated Slicer modules. The module attributes are the lower-cased Slicer module names, the associated value is an instance of ``qSlicerAbstractCoreModule``. """) _createModule('slicer.moduleNames', globals(), -"""This module provides an access to all instantiated Slicer module names. + """This module provides an access to all instantiated Slicer module names. The module attributes are the Slicer modules names, the associated value is the module name. """) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Load modules: Add VTK and PythonQt python module attributes into slicer namespace try: - from .kits import available_kits + from .kits import available_kits except ImportError as detail: - available_kits = [] + available_kits = [] import os import sys standalone_python = "python" in str.lower(os.path.split(sys.executable)[-1]) for kit in available_kits: - # skip PythonQt kits if we are running in a regular python interpreter - if standalone_python and "PythonQt" in kit: - continue + # skip PythonQt kits if we are running in a regular python interpreter + if standalone_python and "PythonQt" in kit: + continue - try: - exec("from %s import *" % (kit)) - except ImportError as detail: - print(detail) + try: + exec("from %s import *" % (kit)) + except ImportError as detail: + print(detail) - del kit + del kit -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Import numpy and scipy early, as a workaround for application hang in import # of numpy or scipy at application startup on Windows 11 due to output redirection # (only needed for embedded Python, not for standalone). @@ -64,13 +64,13 @@ def _createModule(name, globals, docstring): # between different platforms. if not standalone_python: - try: - import numpy # noqa: F401 - import scipy # noqa: F401 - except ImportError as detail: - print(detail) + try: + import numpy # noqa: F401 + import scipy # noqa: F401 + except ImportError as detail: + print(detail) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Cleanup: Removing things the user shouldn't have to see. del _createModule diff --git a/Base/Python/slicer/cli.py b/Base/Python/slicer/cli.py index 25da1846221..9cccb9b9fc4 100644 --- a/Base/Python/slicer/cli.py +++ b/Base/Python/slicer/cli.py @@ -1,97 +1,97 @@ """ This module is a place holder for convenient functions allowing to interact with CLI.""" -def createNode(cliModule, parameters = None): - """Creates a new vtkMRMLCommandLineModuleNode for a specific module, with - optional parameters""" - if not cliModule: - return None - cliLogic = cliModule.logic() - if not cliLogic: - print("Could not find logic for module '%s'" % cliModule.name) - return None - node = cliLogic.CreateNodeInScene() - setNodeParameters(node, parameters) - return node +def createNode(cliModule, parameters=None): + """Creates a new vtkMRMLCommandLineModuleNode for a specific module, with + optional parameters""" + if not cliModule: + return None + cliLogic = cliModule.logic() + if not cliLogic: + print("Could not find logic for module '%s'" % cliModule.name) + return None + node = cliLogic.CreateNodeInScene() + setNodeParameters(node, parameters) + return node def setNodeParameters(node, parameters): - """Sets parameters for a vtkMRMLCommandLineModuleNode given a dictionary - of (parameterName, parameterValue) pairs - For vectors: provide a list, tuple or comma-separated string - For enumerations, provide the single enumeration value - For files and directories, provide a string - For images, geometry, points and regions, provide a vtkMRMLNode - """ - import slicer - if not node: - return None - if not parameters: - return None - for key, value in parameters.items(): - if isinstance(value, str): - node.SetParameterAsString(key, value) - elif isinstance(value, bool): - node.SetParameterAsBool(key, value) - elif isinstance(value, int): - node.SetParameterAsInt(key, value) - elif isinstance(value, float): - node.SetParameterAsDouble(key, value) - elif isinstance(value, slicer.vtkMRMLNode): - node.SetParameterAsNode(key, value) - elif isinstance(value, list) or isinstance(value, tuple): - commaSeparatedString = str(value) - commaSeparatedString = commaSeparatedString[1:len(commaSeparatedString)-1] - node.SetParameterAsString(key, commaSeparatedString) - #TODO: file support - else: - print("parameter ", key, " has unsupported type ", value.__class__.__name__) + """Sets parameters for a vtkMRMLCommandLineModuleNode given a dictionary + of (parameterName, parameterValue) pairs + For vectors: provide a list, tuple or comma-separated string + For enumerations, provide the single enumeration value + For files and directories, provide a string + For images, geometry, points and regions, provide a vtkMRMLNode + """ + import slicer + if not node: + return None + if not parameters: + return None + for key, value in parameters.items(): + if isinstance(value, str): + node.SetParameterAsString(key, value) + elif isinstance(value, bool): + node.SetParameterAsBool(key, value) + elif isinstance(value, int): + node.SetParameterAsInt(key, value) + elif isinstance(value, float): + node.SetParameterAsDouble(key, value) + elif isinstance(value, slicer.vtkMRMLNode): + node.SetParameterAsNode(key, value) + elif isinstance(value, list) or isinstance(value, tuple): + commaSeparatedString = str(value) + commaSeparatedString = commaSeparatedString[1:len(commaSeparatedString) - 1] + node.SetParameterAsString(key, commaSeparatedString) + # TODO: file support + else: + print("parameter ", key, " has unsupported type ", value.__class__.__name__) def runSync(module, node=None, parameters=None, delete_temporary_files=True, update_display=True): - """Run a CLI synchronously, optionally given a node with optional parameters, - returning the node (or the new one if created) - node: existing parameter node (None by default) - parameters: dictionary of parameters for cli (None by default) - delete_temporary_files: remove temp files created during execution (True by default) - update_display: show output nodes after completion - """ - return run(module, node=node, parameters=parameters, wait_for_completion=True, delete_temporary_files=delete_temporary_files, update_display=update_display) + """Run a CLI synchronously, optionally given a node with optional parameters, + returning the node (or the new one if created) + node: existing parameter node (None by default) + parameters: dictionary of parameters for cli (None by default) + delete_temporary_files: remove temp files created during execution (True by default) + update_display: show output nodes after completion + """ + return run(module, node=node, parameters=parameters, wait_for_completion=True, delete_temporary_files=delete_temporary_files, update_display=update_display) -def run(module, node = None, parameters = None, wait_for_completion = False, delete_temporary_files = True, update_display=True): - """Runs a CLI, optionally given a node with optional parameters, returning - back the node (or the new one if created) - node: existing parameter node (None by default) - parameters: dictionary of parameters for cli (None by default) - wait_for_completion: block if True (False by default) - delete_temporary_files: remove temp files created during execution (True by default) - update_display: show output nodes after completion - """ - if node: - setNodeParameters(node, parameters) - else: - node = createNode(module, parameters) - if not node: - return None +def run(module, node=None, parameters=None, wait_for_completion=False, delete_temporary_files=True, update_display=True): + """Runs a CLI, optionally given a node with optional parameters, returning + back the node (or the new one if created) + node: existing parameter node (None by default) + parameters: dictionary of parameters for cli (None by default) + wait_for_completion: block if True (False by default) + delete_temporary_files: remove temp files created during execution (True by default) + update_display: show output nodes after completion + """ + if node: + setNodeParameters(node, parameters) + else: + node = createNode(module, parameters) + if not node: + return None - logic = module.logic() + logic = module.logic() - logic.SetDeleteTemporaryFiles(1 if delete_temporary_files else 0) + logic.SetDeleteTemporaryFiles(1 if delete_temporary_files else 0) - if wait_for_completion: - logic.ApplyAndWait(node, update_display) - else: - logic.Apply(node, update_display) - #import slicer.util - #widget = slicer.util.getModuleGui(module) - #if not widget: - # print "Could not find widget representation for module" - # return None - #widget.setCurrentCommandLineModuleNode(node) - #widget.apply() - return node + if wait_for_completion: + logic.ApplyAndWait(node, update_display) + else: + logic.Apply(node, update_display) + # import slicer.util + # widget = slicer.util.getModuleGui(module) + # if not widget: + # print "Could not find widget representation for module" + # return None + # widget.setCurrentCommandLineModuleNode(node) + # widget.apply() + return node def cancel(node): - print("Not yet implemented") + print("Not yet implemented") diff --git a/Base/Python/slicer/release/wiki.py b/Base/Python/slicer/release/wiki.py index 87ab658bd6d..25f2887d8e5 100644 --- a/Base/Python/slicer/release/wiki.py +++ b/Base/Python/slicer/release/wiki.py @@ -114,406 +114,406 @@ class Wiki: - def __init__(self, username="UpdateBot", password=None): - log.info("connecting") - self.doc = mwdoc.Documentation('www.slicer.org', '/w/') - if password is not None: - log.info("signing in") - self.doc.login(username, password) - - def page_content(self, page_name): - """Return the content of ``page_name``.""" - log.debug("accessing page [%s]" % page_name) - page = self.doc.site.pages[page_name] - content = page.text() - log.debug("-" * 80) - log.debug("page content:\n%s" % content) - log.debug("-" * 80) - return content - - def set_page_content(self, page_name, content, summary): - """Update ``page_name`` with ``content``.""" - log.info("updating %s" % page_name) - log.debug("accessing page [%s]" % page_name) - page = self.doc.site.pages[page_name] - if page.text() == content: - log.info("skipping %s: content up-to-date" % page_name) - return - page.save(content, summary=summary) - - def version_pages(self, release_version): - """Copy ``Documentation/Nightly`` and ``Template:Documentation/Nightly`` - pages into ``release_version`` namespace.""" - self.doc.versionPages( - 'Nightly', release_version, - ['Documentation', 'Template:Documentation']) - - VERSION_INFO_PAGES = { - "previous": "Template:Documentation/prevversion", - "current": "Template:Documentation/currentversion", - "next": "Template:Documentation/nextversion" - } - - def version_info(self, page_name): - content = self.page_content(page_name) - result = re.match(r"([0-9]\.[0-9]+)", content) - return result.group(1) - - def previous_version(self): - return self.version_info(Wiki.VERSION_INFO_PAGES["previous"]) - - def next_version(self): - return self.version_info(Wiki.VERSION_INFO_PAGES["next"]) - - def current_version(self): - return self.version_info(Wiki.VERSION_INFO_PAGES["current"]) - - def compute_updated_version_info(self, target_release_version): - target_major = int(target_release_version.split(".")[0]) - target_minor = int(target_release_version.split(".")[1]) - return { - "previous": self.current_version(), - "current": target_release_version, - "next": "%d.%d" % (target_major, target_minor + 2) + def __init__(self, username="UpdateBot", password=None): + log.info("connecting") + self.doc = mwdoc.Documentation('www.slicer.org', '/w/') + if password is not None: + log.info("signing in") + self.doc.login(username, password) + + def page_content(self, page_name): + """Return the content of ``page_name``.""" + log.debug("accessing page [%s]" % page_name) + page = self.doc.site.pages[page_name] + content = page.text() + log.debug("-" * 80) + log.debug("page content:\n%s" % content) + log.debug("-" * 80) + return content + + def set_page_content(self, page_name, content, summary): + """Update ``page_name`` with ``content``.""" + log.info("updating %s" % page_name) + log.debug("accessing page [%s]" % page_name) + page = self.doc.site.pages[page_name] + if page.text() == content: + log.info("skipping %s: content up-to-date" % page_name) + return + page.save(content, summary=summary) + + def version_pages(self, release_version): + """Copy ``Documentation/Nightly`` and ``Template:Documentation/Nightly`` + pages into ``release_version`` namespace.""" + self.doc.versionPages( + 'Nightly', release_version, + ['Documentation', 'Template:Documentation']) + + VERSION_INFO_PAGES = { + "previous": "Template:Documentation/prevversion", + "current": "Template:Documentation/currentversion", + "next": "Template:Documentation/nextversion" } - def update_version_info_pages(self, release_version): - updated_version_info = self.compute_updated_version_info(release_version) - for page_short_name, page_name in Wiki.VERSION_INFO_PAGES.items(): - # check if update is needed - current_version = self.version_info(page_name) - updated_version = updated_version_info[page_short_name] - log.debug(f"{page_short_name}: current version: {current_version}") - log.debug(f"{page_short_name}: updated version: {updated_version}") - if current_version == updated_version: - log.info(f"skipping {page_name}: version is {updated_version}") - break - # update page - content = self.page_content(page_name) - template = "%s" - log.debug("replacing '{}' with '{}'".format( - template % current_version, template % updated_version)) - content = content.replace( - template % current_version, template % updated_version) - summary = "Update {} version from {} to {}".format( - page_short_name, current_version, updated_version) - self.set_page_content(page_name, content, summary) - # sanity check - current_version = self.version_info(page_name) - if current_version != updated_version: - raise RuntimeError( - "Failed to update {}: {} version is {}".format( - page_name, page_short_name, current_version)) - - def version_list(self): - content = self.page_content("Template:Documentation/versionlist") - return re.findall( - r"\[\[Documentation/(?:[.\w]+)\|([.\w]+)\]\]", content) - - def update_version_list(self, release_version): - # check if update is needed - current_list = self.version_list() - page_name = "Template:Documentation/versionlist" - if release_version in current_list: - log.info(f"skipping {page_name}: version is {release_version}") - return - # update page - assert current_list[0] == "Nightly" - current_version = current_list[1] - content = self.page_content(page_name) - template = "[[Documentation/%s|%s]]" - current = template % (current_version, current_version) - updated = template % (release_version, release_version) + " " + current - log.debug(f"replacing '{current}' with '{updated}'") - content = content.replace(current, updated) - summary = "Add %s to version list" % current_version - self.set_page_content(page_name, content, summary) - # sanity check - current_list = self.version_list() - if release_version not in current_list: - raise RuntimeError( - "Failed to update {}: version {} is not in the list".format( - page_name, release_version)) - - def acknowledgments_main_version(self): - content = self.page_content( - "Template:Documentation/acknowledgments-versionlist") - return re.findall( - r"\[\[Documentation/(?:[.\w]+)/Acknowledgments\|([.\w]+)\]\]", content)[0] - - REDIRECT_PAGES = [ - "FAQ", - "Documentation/Release", - "Documentation/Release/Announcements", - "Documentation/Release/Report a problem", - "Documentation/UserTraining", - "Documentation/UserFeedback", - "Documentation/Release/SlicerApplication/HardwareConfiguration" - ] - - def redirect_page_version(self, page_name): - content = self.page_content(page_name) - result = re.match(r"#REDIRECT \[\[Documentation/([.\w]+)", content) - return result.group(1) - - def redirect_pages_version(self): - for redirect_page in Wiki.REDIRECT_PAGES: - yield redirect_page, self.redirect_page_version(redirect_page) - - def update_redirect_pages(self, release_version): - for redirect_page in Wiki.REDIRECT_PAGES: - # check if update is needed - current_version = self.redirect_page_version(redirect_page) - if current_version == release_version: - log.info(f"skipping {redirect_page}: version is {release_version}") - break - # update page - content = self.page_content(redirect_page) - template = "#REDIRECT [[Documentation/%s" - log.debug("replacing %s" % (template % current_version)) - content = content.replace( - template % current_version, template % release_version) - summary = "Update REDIRECT from Documentation/{} to Documentation/{}".format( - current_version, release_version) - self.set_page_content(redirect_page, content, summary) - # sanity check - current_version = self.redirect_page_version(redirect_page) - if current_version != release_version: - raise RuntimeError( - "Failed to update {}: version is {}".format( - redirect_page, current_version)) - - def update_top_level_documentation_page(self, release_version): - # check if update is needed - page_name = "Documentation" - template = "* [[Documentation/{version}|{version}]] / " \ - "[[Documentation/{version}/ReleaseNotes|Release notes]] / " \ - "[[Documentation/{version}/Announcements | Announcement]] / " \ - "[[Documentation/{version}/Acknowledgments | Acknowledgments]]" - marker = "" - content = self.page_content(page_name) - if marker not in content: - log.error( - f"failed to update {page_name}: marker {marker} not found") - return - if template.format(version=release_version) in content: - log.info( - f"skipping {page_name}: version {release_version} already added") - return - # update page - content = content.replace( - marker, - marker + "\n" + template.format(version=release_version) - ) - summary = "Add %s" % release_version - self.set_page_content(page_name, content, summary) - - @staticmethod - def is_valid_version(text): - return re.match(r"^[0-9].[0-9]+$", text) is not None + def version_info(self, page_name): + content = self.page_content(page_name) + result = re.match(r"([0-9]\.[0-9]+)", content) + return result.group(1) + + def previous_version(self): + return self.version_info(Wiki.VERSION_INFO_PAGES["previous"]) + + def next_version(self): + return self.version_info(Wiki.VERSION_INFO_PAGES["next"]) + + def current_version(self): + return self.version_info(Wiki.VERSION_INFO_PAGES["current"]) + + def compute_updated_version_info(self, target_release_version): + target_major = int(target_release_version.split(".")[0]) + target_minor = int(target_release_version.split(".")[1]) + return { + "previous": self.current_version(), + "current": target_release_version, + "next": "%d.%d" % (target_major, target_minor + 2) + } + + def update_version_info_pages(self, release_version): + updated_version_info = self.compute_updated_version_info(release_version) + for page_short_name, page_name in Wiki.VERSION_INFO_PAGES.items(): + # check if update is needed + current_version = self.version_info(page_name) + updated_version = updated_version_info[page_short_name] + log.debug(f"{page_short_name}: current version: {current_version}") + log.debug(f"{page_short_name}: updated version: {updated_version}") + if current_version == updated_version: + log.info(f"skipping {page_name}: version is {updated_version}") + break + # update page + content = self.page_content(page_name) + template = "%s" + log.debug("replacing '{}' with '{}'".format( + template % current_version, template % updated_version)) + content = content.replace( + template % current_version, template % updated_version) + summary = "Update {} version from {} to {}".format( + page_short_name, current_version, updated_version) + self.set_page_content(page_name, content, summary) + # sanity check + current_version = self.version_info(page_name) + if current_version != updated_version: + raise RuntimeError( + "Failed to update {}: {} version is {}".format( + page_name, page_short_name, current_version)) + + def version_list(self): + content = self.page_content("Template:Documentation/versionlist") + return re.findall( + r"\[\[Documentation/(?:[.\w]+)\|([.\w]+)\]\]", content) + + def update_version_list(self, release_version): + # check if update is needed + current_list = self.version_list() + page_name = "Template:Documentation/versionlist" + if release_version in current_list: + log.info(f"skipping {page_name}: version is {release_version}") + return + # update page + assert current_list[0] == "Nightly" + current_version = current_list[1] + content = self.page_content(page_name) + template = "[[Documentation/%s|%s]]" + current = template % (current_version, current_version) + updated = template % (release_version, release_version) + " " + current + log.debug(f"replacing '{current}' with '{updated}'") + content = content.replace(current, updated) + summary = "Add %s to version list" % current_version + self.set_page_content(page_name, content, summary) + # sanity check + current_list = self.version_list() + if release_version not in current_list: + raise RuntimeError( + "Failed to update {}: version {} is not in the list".format( + page_name, release_version)) + + def acknowledgments_main_version(self): + content = self.page_content( + "Template:Documentation/acknowledgments-versionlist") + return re.findall( + r"\[\[Documentation/(?:[.\w]+)/Acknowledgments\|([.\w]+)\]\]", content)[0] + + REDIRECT_PAGES = [ + "FAQ", + "Documentation/Release", + "Documentation/Release/Announcements", + "Documentation/Release/Report a problem", + "Documentation/UserTraining", + "Documentation/UserFeedback", + "Documentation/Release/SlicerApplication/HardwareConfiguration" + ] + + def redirect_page_version(self, page_name): + content = self.page_content(page_name) + result = re.match(r"#REDIRECT \[\[Documentation/([.\w]+)", content) + return result.group(1) + + def redirect_pages_version(self): + for redirect_page in Wiki.REDIRECT_PAGES: + yield redirect_page, self.redirect_page_version(redirect_page) + + def update_redirect_pages(self, release_version): + for redirect_page in Wiki.REDIRECT_PAGES: + # check if update is needed + current_version = self.redirect_page_version(redirect_page) + if current_version == release_version: + log.info(f"skipping {redirect_page}: version is {release_version}") + break + # update page + content = self.page_content(redirect_page) + template = "#REDIRECT [[Documentation/%s" + log.debug("replacing %s" % (template % current_version)) + content = content.replace( + template % current_version, template % release_version) + summary = "Update REDIRECT from Documentation/{} to Documentation/{}".format( + current_version, release_version) + self.set_page_content(redirect_page, content, summary) + # sanity check + current_version = self.redirect_page_version(redirect_page) + if current_version != release_version: + raise RuntimeError( + "Failed to update {}: version is {}".format( + redirect_page, current_version)) + + def update_top_level_documentation_page(self, release_version): + # check if update is needed + page_name = "Documentation" + template = "* [[Documentation/{version}|{version}]] / " \ + "[[Documentation/{version}/ReleaseNotes|Release notes]] / " \ + "[[Documentation/{version}/Announcements | Announcement]] / " \ + "[[Documentation/{version}/Acknowledgments | Acknowledgments]]" + marker = "" + content = self.page_content(page_name) + if marker not in content: + log.error( + f"failed to update {page_name}: marker {marker} not found") + return + if template.format(version=release_version) in content: + log.info( + f"skipping {page_name}: version {release_version} already added") + return + # update page + content = content.replace( + marker, + marker + "\n" + template.format(version=release_version) + ) + summary = "Add %s" % release_version + self.set_page_content(page_name, content, summary) + + @staticmethod + def is_valid_version(text): + return re.match(r"^[0-9].[0-9]+$", text) is not None def handle_query(wiki, args): - def display_version_info(): - print("Version info:") - for page_short_name, page_name in Wiki.VERSION_INFO_PAGES.items(): - method = getattr(wiki, "%s_version" % page_short_name) - print(f" {page_name}: {method()}") + def display_version_info(): + print("Version info:") + for page_short_name, page_name in Wiki.VERSION_INFO_PAGES.items(): + method = getattr(wiki, "%s_version" % page_short_name) + print(f" {page_name}: {method()}") - def display_next_version_info(): - print("Next version info:") - for page_short_name, version in \ - wiki.compute_updated_version_info(wiki.next_version()).items(): - page_name = Wiki.VERSION_INFO_PAGES[page_short_name] - print(f" {page_name}: {version}") + def display_next_version_info(): + print("Next version info:") + for page_short_name, version in \ + wiki.compute_updated_version_info(wiki.next_version()).items(): + page_name = Wiki.VERSION_INFO_PAGES[page_short_name] + print(f" {page_name}: {version}") - def display_version_list(): - print("Versions: %s" % " ".join(wiki.version_list())) + def display_version_list(): + print("Versions: %s" % " ".join(wiki.version_list())) - def display_acknowledgments_main_version(): - print( - "Acknowledgments main version: %s" % wiki.acknowledgments_main_version()) + def display_acknowledgments_main_version(): + print( + "Acknowledgments main version: %s" % wiki.acknowledgments_main_version()) - def display_redirect_pages_version(): - print("Redirect pages:") - for redirect_page, version in wiki.redirect_pages_version(): - print(f" {redirect_page}: {version}") + def display_redirect_pages_version(): + print("Redirect pages:") + for redirect_page, version in wiki.redirect_pages_version(): + print(f" {redirect_page}: {version}") - def display_all(): - display_version_info() - display_next_version_info() - display_version_list() - display_redirect_pages_version() + def display_all(): + display_version_info() + display_next_version_info() + display_version_list() + display_redirect_pages_version() - query_args = [] + query_args = [] - def _check(arg_name): - query_args.append(getattr(args, arg_name)) - return query_args[-1] + def _check(arg_name): + query_args.append(getattr(args, arg_name)) + return query_args[-1] - if _check('version_info'): - display_version_info() + if _check('version_info'): + display_version_info() - if _check('next_version_info'): - display_next_version_info() + if _check('next_version_info'): + display_next_version_info() - if _check('version_list'): - display_version_list() + if _check('version_list'): + display_version_list() - if _check('redirect_pages_version'): - display_redirect_pages_version() + if _check('redirect_pages_version'): + display_redirect_pages_version() - if not any(query_args): - display_all() + if not any(query_args): + display_all() def handle_copy(wiki, args): - release_version = args.release_version - if not Wiki.is_valid_version(release_version): - log.error("invalid release version: %s" % release_version) - return - wiki.version_pages(release_version) + release_version = args.release_version + if not Wiki.is_valid_version(release_version): + log.error("invalid release version: %s" % release_version) + return + wiki.version_pages(release_version) def handle_update(wiki, args): - release_version = args.release_version - if not Wiki.is_valid_version(release_version): - log.error("invalid release version: %s" % release_version) - return + release_version = args.release_version + if not Wiki.is_valid_version(release_version): + log.error("invalid release version: %s" % release_version) + return - update_args = [] + update_args = [] - def _check(arg_name): - update_args.append(getattr(args, arg_name)) - return update_args[-1] + def _check(arg_name): + update_args.append(getattr(args, arg_name)) + return update_args[-1] - if _check('version_info_pages'): - wiki.update_version_info_pages(release_version) + if _check('version_info_pages'): + wiki.update_version_info_pages(release_version) - if _check('redirect_pages'): - wiki.update_redirect_pages(release_version) + if _check('redirect_pages'): + wiki.update_redirect_pages(release_version) - if _check('version_list'): - wiki.update_version_list(release_version) + if _check('version_list'): + wiki.update_version_list(release_version) - if _check('top_level_documentation_page'): - wiki.update_top_level_documentation_page(release_version) + if _check('top_level_documentation_page'): + wiki.update_top_level_documentation_page(release_version) - if not any(update_args): - wiki.update_version_info_pages(release_version) - wiki.update_redirect_pages(release_version) - wiki.update_version_list(release_version) - wiki.update_top_level_documentation_page(release_version) + if not any(update_args): + wiki.update_version_info_pages(release_version) + wiki.update_redirect_pages(release_version) + wiki.update_version_list(release_version) + wiki.update_top_level_documentation_page(release_version) def main(): - """This command-line tool allows to `version` Slicer mediawiki documentation. - - It has three main modes of operation: ``query``, `copy` and ``update``. - """ - parser = argparse.ArgumentParser(description=main.__doc__) - parser.add_argument( - '--log-level', dest='log_level', - default='INFO', - help='Level of debug verbosity. DEBUG, INFO, WARNING, ERROR, CRITICAL.', - ) - parser.add_argument( - "--password", type=str, default=os.environ.get('SLICER_WIKI_UPDATEBOT_PWD'), - help="password for 'UpdateBot' user. By default, try to get password from " - "'SLICER_WIKI_UPDATEBOT_PWD' environment variable." - ) - - subparsers = parser.add_subparsers( - help='available sub-commands', dest='command') - - # sub-command parser - parser_query = subparsers.add_parser( - 'query', help='obtain version information') - - parser_query.add_argument( - "--version-info", action="store_true", - help="display the version associated with pages %s" % ", ".join( - ['%s' % page_name for page_name in Wiki.VERSION_INFO_PAGES.values()]) - ) - parser_query.add_argument( - "--next-version-info", action="store_true", - help="display what would be the *next* version associated " - "with pages %s" % ", ".join( - ['%s' % page_name for page_name in Wiki.VERSION_INFO_PAGES.values()]) - ) - parser_query.add_argument( - "--version-list", action="store_true", - help="display the versions associated with page " - "'Template::Documentation/versionlist'" - ) - parser_query.add_argument( - "--acknowledgments-main-version", action="store_true", - help="display the version associated with page " - "'Template:Documentation/acknowledgments-versionlist'" - ) - parser_query.add_argument( - "--redirect-pages-version", action="store_true", - help="display the version associated with pages with redirect" - ) - - # sub-command parser - parser_copy = subparsers.add_parser( - 'copy', help='copy Nightly pages into RELEASE_VERSION namespace') - parser_copy.add_argument( - "release_version", type=str, metavar="RELEASE_VERSION", - help="the release version where Nightly pages will be copied into" - ) - - # sub-command parser - parser_update = subparsers.add_parser( - 'update', help='create and/or update wiki pages with RELEASE_VERSION') - - parser_update.add_argument( - "release_version", type=str, metavar="RELEASE_VERSION", - help="the release version used to update permanent pages" - ) - parser_update.add_argument( - "--version-info-pages", action="store_true", - help="update the version associated with pages %s" % ", ".join( - ['%s' % page_name for page_name in Wiki.VERSION_INFO_PAGES.values()]) - ) - parser_update.add_argument( - "--redirect-pages", action="store_true", - help="update the version associated with redirect pages" - ) - parser_update.add_argument( - "--version-list", action="store_true", - help="add RELEASE_VERSION to page " - "'Template::Documentation/versionlist'" - ) - parser_update.add_argument( - "--acknowledgments-main-version", action="store_true", - help="add RELEASE_VERSION to page " - "'Template:Documentation/acknowledgments-versionlist'" - ) - parser_update.add_argument( - "--top-level-documentation-page", action="store_true", - help="add RELEASE_VERSION to page 'Documentation'" - ) - - args = parser.parse_args() - - log.setLevel(args.log_level.upper()) - log.addHandler(logging.StreamHandler()) - - wiki = Wiki(password=args.password) - - if args.command == "query": - handle_query(wiki, args) - elif args.command == "copy": - handle_copy(wiki, args) - elif args.command == "update": - handle_update(wiki, args) + """This command-line tool allows to `version` Slicer mediawiki documentation. + + It has three main modes of operation: ``query``, `copy` and ``update``. + """ + parser = argparse.ArgumentParser(description=main.__doc__) + parser.add_argument( + '--log-level', dest='log_level', + default='INFO', + help='Level of debug verbosity. DEBUG, INFO, WARNING, ERROR, CRITICAL.', + ) + parser.add_argument( + "--password", type=str, default=os.environ.get('SLICER_WIKI_UPDATEBOT_PWD'), + help="password for 'UpdateBot' user. By default, try to get password from " + "'SLICER_WIKI_UPDATEBOT_PWD' environment variable." + ) + + subparsers = parser.add_subparsers( + help='available sub-commands', dest='command') + + # sub-command parser + parser_query = subparsers.add_parser( + 'query', help='obtain version information') + + parser_query.add_argument( + "--version-info", action="store_true", + help="display the version associated with pages %s" % ", ".join( + ['%s' % page_name for page_name in Wiki.VERSION_INFO_PAGES.values()]) + ) + parser_query.add_argument( + "--next-version-info", action="store_true", + help="display what would be the *next* version associated " + "with pages %s" % ", ".join( + ['%s' % page_name for page_name in Wiki.VERSION_INFO_PAGES.values()]) + ) + parser_query.add_argument( + "--version-list", action="store_true", + help="display the versions associated with page " + "'Template::Documentation/versionlist'" + ) + parser_query.add_argument( + "--acknowledgments-main-version", action="store_true", + help="display the version associated with page " + "'Template:Documentation/acknowledgments-versionlist'" + ) + parser_query.add_argument( + "--redirect-pages-version", action="store_true", + help="display the version associated with pages with redirect" + ) + + # sub-command parser + parser_copy = subparsers.add_parser( + 'copy', help='copy Nightly pages into RELEASE_VERSION namespace') + parser_copy.add_argument( + "release_version", type=str, metavar="RELEASE_VERSION", + help="the release version where Nightly pages will be copied into" + ) + + # sub-command parser + parser_update = subparsers.add_parser( + 'update', help='create and/or update wiki pages with RELEASE_VERSION') + + parser_update.add_argument( + "release_version", type=str, metavar="RELEASE_VERSION", + help="the release version used to update permanent pages" + ) + parser_update.add_argument( + "--version-info-pages", action="store_true", + help="update the version associated with pages %s" % ", ".join( + ['%s' % page_name for page_name in Wiki.VERSION_INFO_PAGES.values()]) + ) + parser_update.add_argument( + "--redirect-pages", action="store_true", + help="update the version associated with redirect pages" + ) + parser_update.add_argument( + "--version-list", action="store_true", + help="add RELEASE_VERSION to page " + "'Template::Documentation/versionlist'" + ) + parser_update.add_argument( + "--acknowledgments-main-version", action="store_true", + help="add RELEASE_VERSION to page " + "'Template:Documentation/acknowledgments-versionlist'" + ) + parser_update.add_argument( + "--top-level-documentation-page", action="store_true", + help="add RELEASE_VERSION to page 'Documentation'" + ) + + args = parser.parse_args() + + log.setLevel(args.log_level.upper()) + log.addHandler(logging.StreamHandler()) + + wiki = Wiki(password=args.password) + + if args.command == "query": + handle_query(wiki, args) + elif args.command == "copy": + handle_copy(wiki, args) + elif args.command == "update": + handle_update(wiki, args) if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print("interrupt received, stopping...") + try: + main() + except KeyboardInterrupt: + print("interrupt received, stopping...") diff --git a/Base/Python/slicer/slicerqt.py b/Base/Python/slicer/slicerqt.py index c67514ca570..08e74010015 100644 --- a/Base/Python/slicer/slicerqt.py +++ b/Base/Python/slicer/slicerqt.py @@ -11,90 +11,91 @@ # HACK Ideally constant from vtkSlicerConfigure should be wrapped, # that way the following try/except could be avoided. try: - import slicer.cli -except: pass + import slicer.cli +except: + pass -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- class _LogReverseLevelFilter(logging.Filter): - """ - Rejects log records that are at or above the specified level - """ + """ + Rejects log records that are at or above the specified level + """ - def __init__(self, levelLimit): - self._levelLimit = levelLimit + def __init__(self, levelLimit): + self._levelLimit = levelLimit - def filter(self, record): - return record.levelno < self._levelLimit + def filter(self, record): + return record.levelno < self._levelLimit -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- class SlicerApplicationLogHandler(logging.Handler): - """ - Writes logging records to Slicer application log. - """ - - def __init__(self): - logging.Handler.__init__(self) - if hasattr(ctk, 'ctkErrorLogLevel'): - self.pythonToCtkLevelConverter = { - logging.DEBUG : ctk.ctkErrorLogLevel.Debug, - logging.INFO : ctk.ctkErrorLogLevel.Info, - logging.WARNING : ctk.ctkErrorLogLevel.Warning, - logging.ERROR : ctk.ctkErrorLogLevel.Error } - self.origin = "Python" - self.category = "Python" - - def emit(self, record): - try: - msg = self.format(record) - context = ctk.ctkErrorLogContext() - context.setCategory(self.category) - context.setLine(record.lineno) - context.setFile(record.pathname) - context.setFunction(record.funcName) - context.setMessage(msg) - threadId = f"{record.threadName}({record.thread})" - slicer.app.errorLogModel().addEntry(qt.QDateTime.currentDateTime(), threadId, - self.pythonToCtkLevelConverter[record.levelno], self.origin, context, msg) - except: - self.handleError(record) - - -#----------------------------------------------------------------------------- + """ + Writes logging records to Slicer application log. + """ + + def __init__(self): + logging.Handler.__init__(self) + if hasattr(ctk, 'ctkErrorLogLevel'): + self.pythonToCtkLevelConverter = { + logging.DEBUG: ctk.ctkErrorLogLevel.Debug, + logging.INFO: ctk.ctkErrorLogLevel.Info, + logging.WARNING: ctk.ctkErrorLogLevel.Warning, + logging.ERROR: ctk.ctkErrorLogLevel.Error} + self.origin = "Python" + self.category = "Python" + + def emit(self, record): + try: + msg = self.format(record) + context = ctk.ctkErrorLogContext() + context.setCategory(self.category) + context.setLine(record.lineno) + context.setFile(record.pathname) + context.setFunction(record.funcName) + context.setMessage(msg) + threadId = f"{record.threadName}({record.thread})" + slicer.app.errorLogModel().addEntry(qt.QDateTime.currentDateTime(), threadId, + self.pythonToCtkLevelConverter[record.levelno], self.origin, context, msg) + except: + self.handleError(record) + + +# ----------------------------------------------------------------------------- def initLogging(logger): - """ - Initialize logging by creating log handlers and setting default log level. - """ - - # Prints debug messages to Slicer application log. - # Only debug level messages are logged this way, as higher level messages are printed on console - # and all console outputs are sent automatically to the application log anyway. - applicationLogHandler = SlicerApplicationLogHandler() - applicationLogHandler.setLevel(logging.DEBUG) - # We could filter out messages at INFO level or above (as they will be printed on the console anyway) by adding - # applicationLogHandler.addFilter(_LogReverseLevelFilter(logging.INFO)) - # but then we would not log file name and line number of info, warning, and error level messages. - applicationLogHandler.setFormatter(logging.Formatter('%(message)s')) - logger.addHandler(applicationLogHandler) - - # Prints info message to stdout (anything on stdout will also show up in the application log) - consoleInfoHandler = logging.StreamHandler(sys.stdout) - consoleInfoHandler.setLevel(logging.INFO) - # Filter messages at WARNING level or above (they will be printed on stderr) - consoleInfoHandler.addFilter(_LogReverseLevelFilter(logging.WARNING)) - logger.addHandler(consoleInfoHandler) - - # Prints error and warning messages to stderr (anything on stderr will also show it in the application log) - consoleErrorHandler = logging.StreamHandler(sys.stderr) - consoleErrorHandler.setLevel(logging.WARNING) - logger.addHandler(consoleErrorHandler) - - # Log debug messages from scripts by default, as they are useful for troubleshooting with users - logger.setLevel(logging.DEBUG) - - -#----------------------------------------------------------------------------- + """ + Initialize logging by creating log handlers and setting default log level. + """ + + # Prints debug messages to Slicer application log. + # Only debug level messages are logged this way, as higher level messages are printed on console + # and all console outputs are sent automatically to the application log anyway. + applicationLogHandler = SlicerApplicationLogHandler() + applicationLogHandler.setLevel(logging.DEBUG) + # We could filter out messages at INFO level or above (as they will be printed on the console anyway) by adding + # applicationLogHandler.addFilter(_LogReverseLevelFilter(logging.INFO)) + # but then we would not log file name and line number of info, warning, and error level messages. + applicationLogHandler.setFormatter(logging.Formatter('%(message)s')) + logger.addHandler(applicationLogHandler) + + # Prints info message to stdout (anything on stdout will also show up in the application log) + consoleInfoHandler = logging.StreamHandler(sys.stdout) + consoleInfoHandler.setLevel(logging.INFO) + # Filter messages at WARNING level or above (they will be printed on stderr) + consoleInfoHandler.addFilter(_LogReverseLevelFilter(logging.WARNING)) + logger.addHandler(consoleInfoHandler) + + # Prints error and warning messages to stderr (anything on stderr will also show it in the application log) + consoleErrorHandler = logging.StreamHandler(sys.stderr) + consoleErrorHandler.setLevel(logging.WARNING) + logger.addHandler(consoleErrorHandler) + + # Log debug messages from scripts by default, as they are useful for troubleshooting with users + logger.setLevel(logging.DEBUG) + + +# ----------------------------------------------------------------------------- # Set up the root logger # # We initialize the root logger because if somebody just called logging.debug(), @@ -106,117 +107,117 @@ def initLogging(logger): def getSlicerRCFileName(): - """Return application startup file (Slicer resource script) file name. - If a .slicerrc.py file is found in slicer.app.slicerHome folder then that will be used. - If that is not found then the path defined in SLICERRC environment variable will be used. - If that environment variable is not specified then .slicerrc.py in the user's home folder - will be used ('~/.slicerrc.py').""" - import os - rcfile = os.path.join(slicer.app.slicerHome,".slicerrc.py") - if not os.path.exists(rcfile): - if 'SLICERRC' in os.environ: - rcfile = os.environ['SLICERRC'] - else: - rcfile = os.path.expanduser( '~/.slicerrc.py' ) - rcfile = rcfile.replace('\\','/') # make slashed consistent on Windows - return rcfile - - -#----------------------------------------------------------------------------- + """Return application startup file (Slicer resource script) file name. + If a .slicerrc.py file is found in slicer.app.slicerHome folder then that will be used. + If that is not found then the path defined in SLICERRC environment variable will be used. + If that environment variable is not specified then .slicerrc.py in the user's home folder + will be used ('~/.slicerrc.py').""" + import os + rcfile = os.path.join(slicer.app.slicerHome, ".slicerrc.py") + if not os.path.exists(rcfile): + if 'SLICERRC' in os.environ: + rcfile = os.environ['SLICERRC'] + else: + rcfile = os.path.expanduser('~/.slicerrc.py') + rcfile = rcfile.replace('\\', '/') # make slashed consistent on Windows + return rcfile + + +# ----------------------------------------------------------------------------- # # loadSlicerRCFile - Let's not add this function to 'slicer.util' so that # the global dictionary of the main context is passed to exec(). # def loadSlicerRCFile(): - """If it exists, execute slicer resource script""" - import os - rcfile = getSlicerRCFileName() - if os.path.isfile( rcfile ): - print('Loading Slicer RC file [%s]' % ( rcfile )) - exec(open(rcfile).read(), globals()) + """If it exists, execute slicer resource script""" + import os + rcfile = getSlicerRCFileName() + if os.path.isfile(rcfile): + print('Loading Slicer RC file [%s]' % (rcfile)) + exec(open(rcfile).read(), globals()) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # # Internal # class _Internal: - def __init__( self ): - - # Set attribute 'slicer.app' - setattr( slicer, 'app', _qSlicerCoreApplicationInstance ) - - # Listen factory and module manager to update slicer.{modules, moduleNames} when appropriate - moduleManager = slicer.app.moduleManager() - - # If the qSlicerApplication is only minimally initialized, the factoryManager - # does *NOT* exist. - # This would be the case if, for example, a commandline module wants to - # use qSlicerApplication for tcl access but without all the managers. - # Note: This is not the default behavior. - if hasattr( moduleManager, 'factoryManager' ): - factoryManager = moduleManager.factoryManager() - factoryManager.connect( 'modulesRegistered(QStringList)', self.setSlicerModuleNames ) - moduleManager.connect( 'moduleLoaded(QString)', self.setSlicerModules ) - moduleManager.connect( 'moduleAboutToBeUnloaded(QString)', self.unsetSlicerModule ) - - # Retrieve current instance of the scene and set 'slicer.mrmlScene' - setattr( slicer, 'mrmlScene', slicer.app.mrmlScene() ) - - # HACK - Since qt.QTimer.singleShot is both a property and a static method, the property - # is wrapped in python and prevent the call to the convenient static method having - # the same name. To fix the problem, let's overwrite it's value. - # Ideally this should be fixed in PythonQt itself. - def _singleShot( msec, receiverOrCallable, member=None ): - """Calls either a python function or a slot after a given time interval.""" - # Add 'moduleManager' as parent to prevent the premature destruction of the timer. - # Doing so, we ensure that the QTimer will be deleted before PythonQt is cleanup. - # Indeed, the moduleManager is destroyed before the pythonManager. - timer = qt.QTimer( slicer.app.moduleManager() ) - timer.setSingleShot( True ) - if callable( receiverOrCallable ): - timer.connect( "timeout()", receiverOrCallable ) - else: - timer.connect( "timeout()", receiverOrCallable, member ) - timer.start( msec ) - - qt.QTimer.singleShot = staticmethod( _singleShot ) - - def setSlicerModuleNames( self, moduleNames): - """Add module names as attributes of module slicer.moduleNames""" - for name in moduleNames: - setattr( slicer.moduleNames, name, name ) - # HACK For backward compatibility with ITKv3, map "dwiconvert" module name to "dicomtonrrdconverter" - if name == 'DWIConvert': - setattr( slicer.moduleNames, 'DicomToNrrdConverter', name ) - - def setSlicerModules( self, moduleName ): - """Add modules as attributes of module slicer.modules""" - moduleManager = slicer.app.moduleManager() - setattr( slicer.modules, moduleName.lower(), moduleManager.module(moduleName) ) - # HACK For backward compatibility with ITKv3, map "dicomtonrrdconverter" module to "dwiconvert" - if moduleName == 'DWIConvert': - setattr( slicer.modules, 'dicomtonrrdconverter', moduleManager.module(moduleName) ) - - def unsetSlicerModule( self, moduleName ): - """Remove attribute from ``slicer.modules`` - """ - if hasattr(slicer.modules, moduleName + "Instance"): - delattr(slicer.modules, moduleName + "Instance") - if hasattr(slicer.modules, moduleName + "Widget"): - delattr(slicer.modules, moduleName + "Widget") - if hasattr(slicer.moduleNames, moduleName): - delattr(slicer.moduleNames, moduleName) - delattr(slicer.modules, moduleName.lower()) - try: - slicer.selfTests - except AttributeError: - slicer.selfTests = {} - if moduleName in slicer.selfTests: - del slicer.selfTests[moduleName] + def __init__(self): + + # Set attribute 'slicer.app' + setattr(slicer, 'app', _qSlicerCoreApplicationInstance) + + # Listen factory and module manager to update slicer.{modules, moduleNames} when appropriate + moduleManager = slicer.app.moduleManager() + + # If the qSlicerApplication is only minimally initialized, the factoryManager + # does *NOT* exist. + # This would be the case if, for example, a commandline module wants to + # use qSlicerApplication for tcl access but without all the managers. + # Note: This is not the default behavior. + if hasattr(moduleManager, 'factoryManager'): + factoryManager = moduleManager.factoryManager() + factoryManager.connect('modulesRegistered(QStringList)', self.setSlicerModuleNames) + moduleManager.connect('moduleLoaded(QString)', self.setSlicerModules) + moduleManager.connect('moduleAboutToBeUnloaded(QString)', self.unsetSlicerModule) + + # Retrieve current instance of the scene and set 'slicer.mrmlScene' + setattr(slicer, 'mrmlScene', slicer.app.mrmlScene()) + + # HACK - Since qt.QTimer.singleShot is both a property and a static method, the property + # is wrapped in python and prevent the call to the convenient static method having + # the same name. To fix the problem, let's overwrite it's value. + # Ideally this should be fixed in PythonQt itself. + def _singleShot(msec, receiverOrCallable, member=None): + """Calls either a python function or a slot after a given time interval.""" + # Add 'moduleManager' as parent to prevent the premature destruction of the timer. + # Doing so, we ensure that the QTimer will be deleted before PythonQt is cleanup. + # Indeed, the moduleManager is destroyed before the pythonManager. + timer = qt.QTimer(slicer.app.moduleManager()) + timer.setSingleShot(True) + if callable(receiverOrCallable): + timer.connect("timeout()", receiverOrCallable) + else: + timer.connect("timeout()", receiverOrCallable, member) + timer.start(msec) + + qt.QTimer.singleShot = staticmethod(_singleShot) + + def setSlicerModuleNames(self, moduleNames): + """Add module names as attributes of module slicer.moduleNames""" + for name in moduleNames: + setattr(slicer.moduleNames, name, name) + # HACK For backward compatibility with ITKv3, map "dwiconvert" module name to "dicomtonrrdconverter" + if name == 'DWIConvert': + setattr(slicer.moduleNames, 'DicomToNrrdConverter', name) + + def setSlicerModules(self, moduleName): + """Add modules as attributes of module slicer.modules""" + moduleManager = slicer.app.moduleManager() + setattr(slicer.modules, moduleName.lower(), moduleManager.module(moduleName)) + # HACK For backward compatibility with ITKv3, map "dicomtonrrdconverter" module to "dwiconvert" + if moduleName == 'DWIConvert': + setattr(slicer.modules, 'dicomtonrrdconverter', moduleManager.module(moduleName)) + + def unsetSlicerModule(self, moduleName): + """Remove attribute from ``slicer.modules`` + """ + if hasattr(slicer.modules, moduleName + "Instance"): + delattr(slicer.modules, moduleName + "Instance") + if hasattr(slicer.modules, moduleName + "Widget"): + delattr(slicer.modules, moduleName + "Widget") + if hasattr(slicer.moduleNames, moduleName): + delattr(slicer.moduleNames, moduleName) + delattr(slicer.modules, moduleName.lower()) + try: + slicer.selfTests + except AttributeError: + slicer.selfTests = {} + if moduleName in slicer.selfTests: + del slicer.selfTests[moduleName] _internalInstance = _Internal() diff --git a/Base/Python/slicer/testing.py b/Base/Python/slicer/testing.py index 18b88ccfe37..4682ff8b664 100644 --- a/Base/Python/slicer/testing.py +++ b/Base/Python/slicer/testing.py @@ -3,24 +3,24 @@ # def exitSuccess(): - pass + pass -def exitFailure(message = ""): - raise Exception(message) +def exitFailure(message=""): + raise Exception(message) def runUnitTest(path, testname): - import sys - import unittest - if isinstance(path, str): - sys.path.append(path) - else: - sys.path.extend(path) - print("-------------------------------------------") - print(f"path: {path}\ntestname: {testname}") - print("-------------------------------------------") - suite = unittest.TestLoader().loadTestsFromName(testname) - result = unittest.TextTestRunner(verbosity=2).run(suite) - if not result.wasSuccessful(): - exitFailure() + import sys + import unittest + if isinstance(path, str): + sys.path.append(path) + else: + sys.path.extend(path) + print("-------------------------------------------") + print(f"path: {path}\ntestname: {testname}") + print("-------------------------------------------") + suite = unittest.TestLoader().loadTestsFromName(testname) + result = unittest.TextTestRunner(verbosity=2).run(suite) + if not result.wasSuccessful(): + exitFailure() diff --git a/Base/Python/slicer/tests/test_slicer_environment.py b/Base/Python/slicer/tests/test_slicer_environment.py index b15ac98a585..d56faabfc8f 100644 --- a/Base/Python/slicer/tests/test_slicer_environment.py +++ b/Base/Python/slicer/tests/test_slicer_environment.py @@ -5,20 +5,20 @@ class SlicerEnvironmentTests(unittest.TestCase): - def setUp(self): - pass + def setUp(self): + pass - def test_slicer_util_startupEnvironment(self): - startupEnv = slicer.util.startupEnvironment() - assert isinstance(startupEnv, dict) - assert "PATH" not in startupEnv or "Slicer-build" not in startupEnv["PATH"] + def test_slicer_util_startupEnvironment(self): + startupEnv = slicer.util.startupEnvironment() + assert isinstance(startupEnv, dict) + assert "PATH" not in startupEnv or "Slicer-build" not in startupEnv["PATH"] - def test_slicer_app_startupEnvironment(self): - startupEnv = slicer.app.startupEnvironment() - assert isinstance(startupEnv, qt.QProcessEnvironment) - assert "Slicer-build" not in startupEnv.value("PATH", "") + def test_slicer_app_startupEnvironment(self): + startupEnv = slicer.app.startupEnvironment() + assert isinstance(startupEnv, qt.QProcessEnvironment) + assert "Slicer-build" not in startupEnv.value("PATH", "") - def test_slicer_app_environment(self): - env = slicer.app.environment() - assert isinstance(env, qt.QProcessEnvironment) - assert "Slicer-build" in env.value("PATH") + def test_slicer_app_environment(self): + env = slicer.app.environment() + assert isinstance(env, qt.QProcessEnvironment) + assert "Slicer-build" in env.value("PATH") diff --git a/Base/Python/slicer/tests/test_slicer_mgh.py b/Base/Python/slicer/tests/test_slicer_mgh.py index da8aef947c6..7dc12204cbf 100644 --- a/Base/Python/slicer/tests/test_slicer_mgh.py +++ b/Base/Python/slicer/tests/test_slicer_mgh.py @@ -6,28 +6,28 @@ class SlicerUtilLoadSaveMGHTests(unittest.TestCase): - def setUp(self): - for MGHFileNames in ['MGH_T1.mgz','MGH_T1_longname.mgh.gz','MGH_T1_uncompressed.mgz']: - try: - os.remove(os.path.join(slicer.app.temporaryPath, MGHFileNames)) - except OSError: - pass - shutil.rmtree(os.path.join(slicer.app.temporaryPath, 'SlicerUtilLoadSaveMGHTests' ), True) + def setUp(self): + for MGHFileNames in ['MGH_T1.mgz', 'MGH_T1_longname.mgh.gz', 'MGH_T1_uncompressed.mgz']: + try: + os.remove(os.path.join(slicer.app.temporaryPath, MGHFileNames)) + except OSError: + pass + shutil.rmtree(os.path.join(slicer.app.temporaryPath, 'SlicerUtilLoadSaveMGHTests'), True) - def test_saveShortCompressedNode(self): - node = slicer.util.getNode('T1') - filename = os.path.join(slicer.app.temporaryPath, 'MGH_T1.mgz') - self.assertTrue(slicer.util.saveNode(node, filename)) - self.assertTrue(os.path.exists(filename)) + def test_saveShortCompressedNode(self): + node = slicer.util.getNode('T1') + filename = os.path.join(slicer.app.temporaryPath, 'MGH_T1.mgz') + self.assertTrue(slicer.util.saveNode(node, filename)) + self.assertTrue(os.path.exists(filename)) - def test_saveUnCompressedNode(self): - node = slicer.util.getNode('T1_uncompressed') - filename = os.path.join(slicer.app.temporaryPath, 'MGH_T1_uncompressed.mgh') - self.assertTrue(slicer.util.saveNode(node, filename)) - self.assertTrue(os.path.exists(filename)) + def test_saveUnCompressedNode(self): + node = slicer.util.getNode('T1_uncompressed') + filename = os.path.join(slicer.app.temporaryPath, 'MGH_T1_uncompressed.mgh') + self.assertTrue(slicer.util.saveNode(node, filename)) + self.assertTrue(os.path.exists(filename)) - def test_saveLongCompressedNode(self): - node = slicer.util.getNode('T1_longname') - filename = os.path.join(slicer.app.temporaryPath, 'MGH_T1_longname.mgh.gz') - self.assertTrue(slicer.util.saveNode(node, filename)) - self.assertTrue(os.path.exists(filename)) + def test_saveLongCompressedNode(self): + node = slicer.util.getNode('T1_longname') + filename = os.path.join(slicer.app.temporaryPath, 'MGH_T1_longname.mgh.gz') + self.assertTrue(slicer.util.saveNode(node, filename)) + self.assertTrue(os.path.exists(filename)) diff --git a/Base/Python/slicer/tests/test_slicer_minc.py b/Base/Python/slicer/tests/test_slicer_minc.py index 2ca5cb59bb3..7ae2778b3e2 100644 --- a/Base/Python/slicer/tests/test_slicer_minc.py +++ b/Base/Python/slicer/tests/test_slicer_minc.py @@ -6,16 +6,16 @@ class SlicerUtilLoadSaveMINCTests(unittest.TestCase): - def setUp(self): - for MINCFileNames in ['MINC_pd_z-_float_xyz.mnc']: - try: - os.remove(os.path.join(slicer.app.temporaryPath, MINCFileNames)) - except OSError: - pass - shutil.rmtree(os.path.join(slicer.app.temporaryPath, 'SlicerUtilLoadSaveMINCTests' ), True) + def setUp(self): + for MINCFileNames in ['MINC_pd_z-_float_xyz.mnc']: + try: + os.remove(os.path.join(slicer.app.temporaryPath, MINCFileNames)) + except OSError: + pass + shutil.rmtree(os.path.join(slicer.app.temporaryPath, 'SlicerUtilLoadSaveMINCTests'), True) - def test_saveMINCNode(self): - node = slicer.util.getNode('pd_z-_float_xyz') - filename = os.path.join(slicer.app.temporaryPath, 'MINC_pd_z-_float_xyz.mnc') - self.assertTrue(slicer.util.saveNode(node, filename)) - self.assertTrue(os.path.exists(filename)) + def test_saveMINCNode(self): + node = slicer.util.getNode('pd_z-_float_xyz') + filename = os.path.join(slicer.app.temporaryPath, 'MINC_pd_z-_float_xyz.mnc') + self.assertTrue(slicer.util.saveNode(node, filename)) + self.assertTrue(os.path.exists(filename)) diff --git a/Base/Python/slicer/tests/test_slicer_python_lzma.py b/Base/Python/slicer/tests/test_slicer_python_lzma.py index ba2f71ee4e2..af9a0df887b 100644 --- a/Base/Python/slicer/tests/test_slicer_python_lzma.py +++ b/Base/Python/slicer/tests/test_slicer_python_lzma.py @@ -2,28 +2,28 @@ class SlicerPythonLzmaTests(unittest.TestCase): - """This test verifies that Python is build with lzma enabled. - """ + """This test verifies that Python is build with lzma enabled. + """ - def setUp(self): - pass + def setUp(self): + pass - def tearDown(self): - pass + def tearDown(self): + pass - def test_compressionDecompressionRoundtrip(self): - # Generate some input data - someText = "something..." - originalData = someText.encode() + def test_compressionDecompressionRoundtrip(self): + # Generate some input data + someText = "something..." + originalData = someText.encode() - # Compress - import lzma - lzc = lzma.LZMACompressor() - compressedData = lzc.compress(originalData) + lzc.flush() + # Compress + import lzma + lzc = lzma.LZMACompressor() + compressedData = lzc.compress(originalData) + lzc.flush() - # Uncompress - lzd = lzma.LZMADecompressor() - uncompressedData = lzd.decompress(compressedData) + # Uncompress + lzd = lzma.LZMADecompressor() + uncompressedData = lzd.decompress(compressedData) - # Test if data after compression&decompression is the same as the original - self.assertEqual(originalData, uncompressedData) + # Test if data after compression&decompression is the same as the original + self.assertEqual(originalData, uncompressedData) diff --git a/Base/Python/slicer/tests/test_slicer_python_sqlite3.py b/Base/Python/slicer/tests/test_slicer_python_sqlite3.py index 50be2d2fb2c..db68e56dc49 100644 --- a/Base/Python/slicer/tests/test_slicer_python_sqlite3.py +++ b/Base/Python/slicer/tests/test_slicer_python_sqlite3.py @@ -5,33 +5,33 @@ class SlicerPythonSqlite3Tests(unittest.TestCase): - """This test verifies that Python is build with sqlite3 enabled. - """ + """This test verifies that Python is build with sqlite3 enabled. + """ - def setUp(self): - self.tempDir = slicer.util.tempDirectory() + def setUp(self): + self.tempDir = slicer.util.tempDirectory() - def tearDown(self): - shutil.rmtree(self.tempDir, True) + def tearDown(self): + shutil.rmtree(self.tempDir, True) - def test_sqliteDatabase(self): - import sqlite3 + def test_sqliteDatabase(self): + import sqlite3 - database_filename = os.path.join(self.tempDir, 'database.sql') - print("database_filename="+database_filename) + database_filename = os.path.join(self.tempDir, 'database.sql') + print("database_filename=" + database_filename) - self.connection = sqlite3.connect(database_filename) - print(sqlite3.version) - self.assertIsNotNone(self.connection) - self.assertTrue(os.path.exists(database_filename)) + self.connection = sqlite3.connect(database_filename) + print(sqlite3.version) + self.assertIsNotNone(self.connection) + self.assertTrue(os.path.exists(database_filename)) - create_table_sql = """ CREATE TABLE IF NOT EXISTS projects ( + create_table_sql = """ CREATE TABLE IF NOT EXISTS projects ( id integer PRIMARY KEY, name text NOT NULL, begin_date text, end_date text ); """ - c = self.connection.cursor() - c.execute(create_table_sql) + c = self.connection.cursor() + c.execute(create_table_sql) - self.connection.close() + self.connection.close() diff --git a/Base/Python/slicer/tests/test_slicer_util_VTKObservationMixin.py b/Base/Python/slicer/tests/test_slicer_util_VTKObservationMixin.py index 1013f520f1c..4d48d1d15be 100644 --- a/Base/Python/slicer/tests/test_slicer_util_VTKObservationMixin.py +++ b/Base/Python/slicer/tests/test_slicer_util_VTKObservationMixin.py @@ -8,248 +8,248 @@ class Foo(VTKObservationMixin): - def __init__(self): - VTKObservationMixin.__init__(self) - self.ModifiedEventCount = {} + def __init__(self): + VTKObservationMixin.__init__(self) + self.ModifiedEventCount = {} - def onObjectModified(self, caller, event): - self.ModifiedEventCount[caller] = self.modifiedEventCount(caller) + 1 + def onObjectModified(self, caller, event): + self.ModifiedEventCount[caller] = self.modifiedEventCount(caller) + 1 - def onObjectModifiedAgain(self, caller, event): - self.ModifiedEventCount[caller] = self.modifiedEventCount(caller) + 1 + def onObjectModifiedAgain(self, caller, event): + self.ModifiedEventCount[caller] = self.modifiedEventCount(caller) + 1 - def modifiedEventCount(self, caller): - if caller not in self.ModifiedEventCount: - return 0 - return self.ModifiedEventCount[caller] + def modifiedEventCount(self, caller): + if caller not in self.ModifiedEventCount: + return 0 + return self.ModifiedEventCount[caller] class SlicerUtilVTKObservationMixinTests(unittest.TestCase): - def setUp(self): - pass - - def test_addObserver(self): - foo = Foo() - object = vtk.vtkObject() - object2 = vtk.vtkObject() - event = vtk.vtkCommand.ModifiedEvent - callback = foo.onObjectModified - self.assertEqual(len(foo.Observations), 0) - - foo.ModifiedEventCount = {} - foo.addObserver(object, event, callback) - object.Modified() - self.assertEqual(len(foo.Observations), 1) - self.assertEqual(foo.modifiedEventCount(object), 1) - self.assertEqual(foo.modifiedEventCount(object2), 0) - - foo.ModifiedEventCount = {} - with self.assertWarns(UserWarning): - foo.addObserver(object, event, callback) - object.Modified() - self.assertEqual(len(foo.Observations), 1) - self.assertEqual(foo.modifiedEventCount(object), 1) - self.assertEqual(foo.modifiedEventCount(object2), 0) - - foo.ModifiedEventCount = {} - foo.addObserver(object, event, callback, group='a') - object.Modified() - self.assertEqual(len(foo.Observations), 1) - self.assertEqual(foo.modifiedEventCount(object), 1) - self.assertEqual(foo.modifiedEventCount(object2), 0) - - foo.ModifiedEventCount = {} - foo.addObserver(object2, event, callback) - object.Modified() - self.assertEqual(len(foo.Observations), 2) - self.assertEqual(foo.modifiedEventCount(object), 1) - self.assertEqual(foo.modifiedEventCount(object2), 0) - - foo.ModifiedEventCount = {} - object2.Modified() - self.assertEqual(len(foo.Observations), 2) - self.assertEqual(foo.modifiedEventCount(object), 0) - self.assertEqual(foo.modifiedEventCount(object2), 1) - - def test_hasObserver(self): - foo = Foo() - object = vtk.vtkObject() - object2 = vtk.vtkObject() - event = vtk.vtkCommand.ModifiedEvent - callback = foo.onObjectModified - self.assertFalse(foo.hasObserver(object, event, callback)) - self.assertFalse(foo.hasObserver(object2, event, callback)) - - foo.addObserver(object, event, callback) - self.assertTrue(foo.hasObserver(object, event, callback)) - self.assertFalse(foo.hasObserver(object2, event, callback)) - - foo.addObserver(object2, event, callback) - self.assertTrue(foo.hasObserver(object, event, callback)) - self.assertTrue(foo.hasObserver(object2, event, callback)) - - def test_observer(self): - foo = Foo() - object = vtk.vtkObject() - object2 = vtk.vtkObject() - event = vtk.vtkCommand.ModifiedEvent - callback = foo.onObjectModified - callback2 = foo.onObjectModifiedAgain - self.assertEqual(foo.observer(event, callback), None) - - foo.addObserver(object, event, callback) - self.assertEqual(foo.observer(event, callback), object) - - # observer function return the first observer - foo.addObserver(object2, event, callback) - self.assertEqual(foo.observer(event, callback), object) - - foo.addObserver(object2, event, callback2) - self.assertEqual(foo.observer(event, callback2), object2) - - def test_getObserver(self): - foo = Foo() - obj = vtk.vtkObject() - event = vtk.vtkCommand.ModifiedEvent - callback = foo.onObjectModified - - group = 'a' - priority = 42.0 - - foo.addObserver(obj, event, callback, group=group, priority=priority) - group_, tag_, priority_ = foo.getObserver(obj, event, callback) - - self.assertEqual(group, group_) - self.assertEqual(priority, priority_) - - def test_releaseNodes(self): - foo = Foo() - node = vtk.vtkObject() - callback = unittest.mock.Mock() - - foo.addObserver(node, vtk.vtkCommand.DeleteEvent, callback) - - self.assertEqual(len(foo.Observations), 1) - callback.assert_not_called() - del node - callback.assert_called_once() - self.assertEqual(len(foo.Observations), 0) - - def test_removeObserver(self): - foo = Foo() - object = vtk.vtkObject() - object2 = vtk.vtkObject() - event = vtk.vtkCommand.ModifiedEvent - callback = foo.onObjectModified - callback2 = foo.onObjectModifiedAgain - self.assertEqual(len(foo.Observations), 0) - - foo.addObserver(object, event, callback) - foo.addObserver(object2, event, callback) - foo.addObserver(object, event, callback2) - foo.addObserver(object2, event, callback2) - self.assertEqual(len(foo.Observations), 4) - - foo.removeObserver(object2, event, callback) - self.assertEqual(len(foo.Observations), 3) - - foo.removeObserver(object, event, callback) - self.assertEqual(len(foo.Observations), 2) - - foo.removeObserver(object, event, callback2) - self.assertEqual(len(foo.Observations), 1) - - foo.removeObserver(object2, event, callback2) - self.assertEqual(len(foo.Observations), 0) + def setUp(self): + pass + + def test_addObserver(self): + foo = Foo() + object = vtk.vtkObject() + object2 = vtk.vtkObject() + event = vtk.vtkCommand.ModifiedEvent + callback = foo.onObjectModified + self.assertEqual(len(foo.Observations), 0) + + foo.ModifiedEventCount = {} + foo.addObserver(object, event, callback) + object.Modified() + self.assertEqual(len(foo.Observations), 1) + self.assertEqual(foo.modifiedEventCount(object), 1) + self.assertEqual(foo.modifiedEventCount(object2), 0) + + foo.ModifiedEventCount = {} + with self.assertWarns(UserWarning): + foo.addObserver(object, event, callback) + object.Modified() + self.assertEqual(len(foo.Observations), 1) + self.assertEqual(foo.modifiedEventCount(object), 1) + self.assertEqual(foo.modifiedEventCount(object2), 0) + + foo.ModifiedEventCount = {} + foo.addObserver(object, event, callback, group='a') + object.Modified() + self.assertEqual(len(foo.Observations), 1) + self.assertEqual(foo.modifiedEventCount(object), 1) + self.assertEqual(foo.modifiedEventCount(object2), 0) + + foo.ModifiedEventCount = {} + foo.addObserver(object2, event, callback) + object.Modified() + self.assertEqual(len(foo.Observations), 2) + self.assertEqual(foo.modifiedEventCount(object), 1) + self.assertEqual(foo.modifiedEventCount(object2), 0) + + foo.ModifiedEventCount = {} + object2.Modified() + self.assertEqual(len(foo.Observations), 2) + self.assertEqual(foo.modifiedEventCount(object), 0) + self.assertEqual(foo.modifiedEventCount(object2), 1) + + def test_hasObserver(self): + foo = Foo() + object = vtk.vtkObject() + object2 = vtk.vtkObject() + event = vtk.vtkCommand.ModifiedEvent + callback = foo.onObjectModified + self.assertFalse(foo.hasObserver(object, event, callback)) + self.assertFalse(foo.hasObserver(object2, event, callback)) + + foo.addObserver(object, event, callback) + self.assertTrue(foo.hasObserver(object, event, callback)) + self.assertFalse(foo.hasObserver(object2, event, callback)) + + foo.addObserver(object2, event, callback) + self.assertTrue(foo.hasObserver(object, event, callback)) + self.assertTrue(foo.hasObserver(object2, event, callback)) + + def test_observer(self): + foo = Foo() + object = vtk.vtkObject() + object2 = vtk.vtkObject() + event = vtk.vtkCommand.ModifiedEvent + callback = foo.onObjectModified + callback2 = foo.onObjectModifiedAgain + self.assertEqual(foo.observer(event, callback), None) + + foo.addObserver(object, event, callback) + self.assertEqual(foo.observer(event, callback), object) + + # observer function return the first observer + foo.addObserver(object2, event, callback) + self.assertEqual(foo.observer(event, callback), object) + + foo.addObserver(object2, event, callback2) + self.assertEqual(foo.observer(event, callback2), object2) + + def test_getObserver(self): + foo = Foo() + obj = vtk.vtkObject() + event = vtk.vtkCommand.ModifiedEvent + callback = foo.onObjectModified + + group = 'a' + priority = 42.0 + + foo.addObserver(obj, event, callback, group=group, priority=priority) + group_, tag_, priority_ = foo.getObserver(obj, event, callback) + + self.assertEqual(group, group_) + self.assertEqual(priority, priority_) + + def test_releaseNodes(self): + foo = Foo() + node = vtk.vtkObject() + callback = unittest.mock.Mock() + + foo.addObserver(node, vtk.vtkCommand.DeleteEvent, callback) + + self.assertEqual(len(foo.Observations), 1) + callback.assert_not_called() + del node + callback.assert_called_once() + self.assertEqual(len(foo.Observations), 0) + + def test_removeObserver(self): + foo = Foo() + object = vtk.vtkObject() + object2 = vtk.vtkObject() + event = vtk.vtkCommand.ModifiedEvent + callback = foo.onObjectModified + callback2 = foo.onObjectModifiedAgain + self.assertEqual(len(foo.Observations), 0) + + foo.addObserver(object, event, callback) + foo.addObserver(object2, event, callback) + foo.addObserver(object, event, callback2) + foo.addObserver(object2, event, callback2) + self.assertEqual(len(foo.Observations), 4) + + foo.removeObserver(object2, event, callback) + self.assertEqual(len(foo.Observations), 3) + + foo.removeObserver(object, event, callback) + self.assertEqual(len(foo.Observations), 2) + + foo.removeObserver(object, event, callback2) + self.assertEqual(len(foo.Observations), 1) + + foo.removeObserver(object2, event, callback2) + self.assertEqual(len(foo.Observations), 0) - def test_removeObservers(self): - foo = Foo() - object = vtk.vtkObject() - object2 = vtk.vtkObject() - event = vtk.vtkCommand.ModifiedEvent - callback = foo.onObjectModified - callback2 = foo.onObjectModifiedAgain - self.assertEqual(len(foo.Observations), 0) + def test_removeObservers(self): + foo = Foo() + object = vtk.vtkObject() + object2 = vtk.vtkObject() + event = vtk.vtkCommand.ModifiedEvent + callback = foo.onObjectModified + callback2 = foo.onObjectModifiedAgain + self.assertEqual(len(foo.Observations), 0) - foo.addObserver(object, event, callback) - foo.addObserver(object2, event, callback) - self.assertEqual(len(foo.Observations), 2) + foo.addObserver(object, event, callback) + foo.addObserver(object2, event, callback) + self.assertEqual(len(foo.Observations), 2) - foo.removeObservers() - self.assertEqual(len(foo.Observations), 0) + foo.removeObservers() + self.assertEqual(len(foo.Observations), 0) - foo.addObserver(object, event, callback) - foo.addObserver(object2, event, callback) - foo.addObserver(object2, event, callback2) - self.assertEqual(len(foo.Observations), 3) + foo.addObserver(object, event, callback) + foo.addObserver(object2, event, callback) + foo.addObserver(object2, event, callback2) + self.assertEqual(len(foo.Observations), 3) - foo.removeObservers(method=callback) - self.assertEqual(len(foo.Observations), 1) + foo.removeObservers(method=callback) + self.assertEqual(len(foo.Observations), 1) - def test_moduleWidgetMixin(self): - class MyModule(ScriptedLoadableModuleWidget, VTKObservationMixin): - pass + def test_moduleWidgetMixin(self): + class MyModule(ScriptedLoadableModuleWidget, VTKObservationMixin): + pass - parent = unittest.mock.Mock() - module = MyModule(parent) + parent = unittest.mock.Mock() + module = MyModule(parent) - obj = vtk.vtkObject() - event = vtk.vtkCommand.ModifiedEvent - callback = unittest.mock.Mock() + obj = vtk.vtkObject() + event = vtk.vtkCommand.ModifiedEvent + callback = unittest.mock.Mock() - module.addObserver(obj, event, callback) + module.addObserver(obj, event, callback) - callback.assert_not_called() - obj.Modified() - callback.assert_called() + callback.assert_not_called() + obj.Modified() + callback.assert_called() - def test_moduleLogicMixin(self): - class MyModuleLogic(ScriptedLoadableModuleLogic, VTKObservationMixin): - pass + def test_moduleLogicMixin(self): + class MyModuleLogic(ScriptedLoadableModuleLogic, VTKObservationMixin): + pass - logic = MyModuleLogic() + logic = MyModuleLogic() - obj = vtk.vtkObject() - event = vtk.vtkCommand.ModifiedEvent - callback = unittest.mock.Mock() + obj = vtk.vtkObject() + event = vtk.vtkCommand.ModifiedEvent + callback = unittest.mock.Mock() - logic.addObserver(obj, event, callback) + logic.addObserver(obj, event, callback) - callback.assert_not_called() - obj.Modified() - callback.assert_called() + callback.assert_not_called() + obj.Modified() + callback.assert_called() - def test_moduleTestMixin(self): - class MyModuleTest(ScriptedLoadableModuleTest, VTKObservationMixin): - pass + def test_moduleTestMixin(self): + class MyModuleTest(ScriptedLoadableModuleTest, VTKObservationMixin): + pass - test = MyModuleTest() + test = MyModuleTest() - obj = vtk.vtkObject() - event = vtk.vtkCommand.ModifiedEvent - callback = unittest.mock.Mock() + obj = vtk.vtkObject() + event = vtk.vtkCommand.ModifiedEvent + callback = unittest.mock.Mock() - test.addObserver(obj, event, callback) + test.addObserver(obj, event, callback) - callback.assert_not_called() - obj.Modified() - callback.assert_called() + callback.assert_not_called() + obj.Modified() + callback.assert_called() - def test_moduleTestInitCount(self): - # if this fails, then unittest.TestCase.__init__ may have added a super().__init__() call. - # See https://github.com/Slicer/Slicer/pull/6243#issuecomment-1061800718 for more information. + def test_moduleTestInitCount(self): + # if this fails, then unittest.TestCase.__init__ may have added a super().__init__() call. + # See https://github.com/Slicer/Slicer/pull/6243#issuecomment-1061800718 for more information. - class CountInitCalls: - count = 0 + class CountInitCalls: + count = 0 - def __init__(self): - super().__init__() + def __init__(self): + super().__init__() - self.count += 1 + self.count += 1 - class MyModuleTest(ScriptedLoadableModuleTest, CountInitCalls): - pass + class MyModuleTest(ScriptedLoadableModuleTest, CountInitCalls): + pass - test = MyModuleTest() - self.assertEqual(test.count, 1) + test = MyModuleTest() + self.assertEqual(test.count, 1) diff --git a/Base/Python/slicer/tests/test_slicer_util_chdir.py b/Base/Python/slicer/tests/test_slicer_util_chdir.py index 25c8722cb3b..c14a275ddf0 100644 --- a/Base/Python/slicer/tests/test_slicer_util_chdir.py +++ b/Base/Python/slicer/tests/test_slicer_util_chdir.py @@ -4,44 +4,44 @@ class SlicerUtilChdirTests(unittest.TestCase): - """Available in Python 3.11 as ``TestChdir`` found in ``Lib/test/test_contextlib.py`` - and adapted from https://github.com/python/cpython/pull/28271 - """ - def test_simple(self): - old_cwd = os.getcwd() - target = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../slicer")) - self.assertNotEqual(old_cwd, target) + """Available in Python 3.11 as ``TestChdir`` found in ``Lib/test/test_contextlib.py`` + and adapted from https://github.com/python/cpython/pull/28271 + """ + def test_simple(self): + old_cwd = os.getcwd() + target = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../slicer")) + self.assertNotEqual(old_cwd, target) - with slicer.util.chdir(target): - self.assertEqual(os.getcwd(), target) - self.assertEqual(os.getcwd(), old_cwd) + with slicer.util.chdir(target): + self.assertEqual(os.getcwd(), target) + self.assertEqual(os.getcwd(), old_cwd) - def test_reentrant(self): - old_cwd = os.getcwd() - target1 = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../slicer")) - target2 = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../tests")) - self.assertNotIn(old_cwd, (target1, target2)) - chdir1, chdir2 = slicer.util.chdir(target1), slicer.util.chdir(target2) + def test_reentrant(self): + old_cwd = os.getcwd() + target1 = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../slicer")) + target2 = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../tests")) + self.assertNotIn(old_cwd, (target1, target2)) + chdir1, chdir2 = slicer.util.chdir(target1), slicer.util.chdir(target2) - with chdir1: - self.assertEqual(os.getcwd(), target1) - with chdir2: - self.assertEqual(os.getcwd(), target2) with chdir1: - self.assertEqual(os.getcwd(), target1) - self.assertEqual(os.getcwd(), target2) - self.assertEqual(os.getcwd(), target1) - self.assertEqual(os.getcwd(), old_cwd) + self.assertEqual(os.getcwd(), target1) + with chdir2: + self.assertEqual(os.getcwd(), target2) + with chdir1: + self.assertEqual(os.getcwd(), target1) + self.assertEqual(os.getcwd(), target2) + self.assertEqual(os.getcwd(), target1) + self.assertEqual(os.getcwd(), old_cwd) - def test_exception(self): - old_cwd = os.getcwd() - target = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../slicer")) - self.assertNotEqual(old_cwd, target) + def test_exception(self): + old_cwd = os.getcwd() + target = os.path.realpath(os.path.join(os.path.dirname(__file__), "../../slicer")) + self.assertNotEqual(old_cwd, target) - try: - with slicer.util.chdir(target): - self.assertEqual(os.getcwd(), target) - raise RuntimeError("boom") - except RuntimeError as re: - self.assertEqual(str(re), "boom") - self.assertEqual(os.getcwd(), old_cwd) + try: + with slicer.util.chdir(target): + self.assertEqual(os.getcwd(), target) + raise RuntimeError("boom") + except RuntimeError as re: + self.assertEqual(str(re), "boom") + self.assertEqual(os.getcwd(), old_cwd) diff --git a/Base/Python/slicer/tests/test_slicer_util_getNodes.py b/Base/Python/slicer/tests/test_slicer_util_getNodes.py index ef63bddc82a..20e37af9ab9 100644 --- a/Base/Python/slicer/tests/test_slicer_util_getNodes.py +++ b/Base/Python/slicer/tests/test_slicer_util_getNodes.py @@ -22,15 +22,15 @@ def _configure_scene(scene): return nodes def test_getFirstNodeByName(self): - self.assertEqual(slicer.util.getFirstNodeByName("Volume", 'vtkMRMLScalarVolumeNode').GetName(), "Volume1" ) + self.assertEqual(slicer.util.getFirstNodeByName("Volume", 'vtkMRMLScalarVolumeNode').GetName(), "Volume1") def test_getNode(self): # Test handling of requesting non-existing node with self.assertRaises(slicer.util.MRMLNodeNotFoundException): - slicer.util.getNode("") + slicer.util.getNode("") with self.assertRaises(slicer.util.MRMLNodeNotFoundException): - slicer.util.getNode("NotExistingNodeName") + slicer.util.getNode("NotExistingNodeName") # For the following tests, use a dedicated scene where # all nodes are known. @@ -60,5 +60,5 @@ def test_getNodesMultipleNodesSharingName(self): self.assertEqual(list(slicer.util.getNodes("Volume").keys()), ["Volume"]) self.assertIsInstance(slicer.util.getNodes("Volume")["Volume"], vtk.vtkObject) - self.assertEqual(list(slicer.util.getNodes("Volume",useLists=True).keys()), ["Volume"]) - self.assertIsInstance(slicer.util.getNodes("Volume",useLists=True)["Volume"], list) + self.assertEqual(list(slicer.util.getNodes("Volume", useLists=True).keys()), ["Volume"]) + self.assertIsInstance(slicer.util.getNodes("Volume", useLists=True)["Volume"], list) diff --git a/Base/Python/slicer/tests/test_slicer_util_save.py b/Base/Python/slicer/tests/test_slicer_util_save.py index 654ea06717e..505219b9672 100644 --- a/Base/Python/slicer/tests/test_slicer_util_save.py +++ b/Base/Python/slicer/tests/test_slicer_util_save.py @@ -6,36 +6,36 @@ class SlicerUtilSaveTests(unittest.TestCase): - def setUp(self): - for extension in ['nrrd', 'mrml', 'mrb']: - try: - os.remove(slicer.app.temporaryPath + '/SlicerUtilSaveTests.' + extension) - except OSError: - pass - shutil.rmtree(slicer.app.temporaryPath + '/SlicerUtilSaveTests', True) + def setUp(self): + for extension in ['nrrd', 'mrml', 'mrb']: + try: + os.remove(slicer.app.temporaryPath + '/SlicerUtilSaveTests.' + extension) + except OSError: + pass + shutil.rmtree(slicer.app.temporaryPath + '/SlicerUtilSaveTests', True) - def test_saveNode(self): - node = slicer.util.getNode('MR-head') - filename = slicer.app.temporaryPath + '/SlicerUtilSaveTests.nrrd' - self.assertTrue(slicer.util.saveNode(node, filename)) - self.assertTrue(os.path.exists(filename)) + def test_saveNode(self): + node = slicer.util.getNode('MR-head') + filename = slicer.app.temporaryPath + '/SlicerUtilSaveTests.nrrd' + self.assertTrue(slicer.util.saveNode(node, filename)) + self.assertTrue(os.path.exists(filename)) - def test_saveSceneAsMRMLFile(self): - filename = slicer.app.temporaryPath + '/SlicerUtilSaveTests.mrml' - self.assertTrue(slicer.util.saveScene(filename)) - self.assertTrue(os.path.exists(filename)) + def test_saveSceneAsMRMLFile(self): + filename = slicer.app.temporaryPath + '/SlicerUtilSaveTests.mrml' + self.assertTrue(slicer.util.saveScene(filename)) + self.assertTrue(os.path.exists(filename)) - def test_saveSceneAsMRB(self): - filename = slicer.app.temporaryPath + '/SlicerUtilSaveTests.mrb' - self.assertTrue(slicer.util.saveScene(filename)) - self.assertTrue(os.path.exists(filename)) + def test_saveSceneAsMRB(self): + filename = slicer.app.temporaryPath + '/SlicerUtilSaveTests.mrb' + self.assertTrue(slicer.util.saveScene(filename)) + self.assertTrue(os.path.exists(filename)) - def test_saveSceneAsDirectory(self): - """Execution of 'test_saveNode' implies that the filename associated - MR-head storage node is set to 'SlicerUtilSaveTests.nrrd' - """ - filename = slicer.app.temporaryPath + '/SlicerUtilSaveTests' - self.assertTrue(slicer.util.saveScene(filename)) - self.assertTrue(os.path.exists(filename)) - self.assertTrue(os.path.exists(filename + '/SlicerUtilSaveTests.mrml')) - self.assertTrue(os.path.exists(filename + '/Data/SlicerUtilSaveTests.nrrd')) + def test_saveSceneAsDirectory(self): + """Execution of 'test_saveNode' implies that the filename associated + MR-head storage node is set to 'SlicerUtilSaveTests.nrrd' + """ + filename = slicer.app.temporaryPath + '/SlicerUtilSaveTests' + self.assertTrue(slicer.util.saveScene(filename)) + self.assertTrue(os.path.exists(filename)) + self.assertTrue(os.path.exists(filename + '/SlicerUtilSaveTests.mrml')) + self.assertTrue(os.path.exists(filename + '/Data/SlicerUtilSaveTests.nrrd')) diff --git a/Base/Python/slicer/tests/test_slicer_util_without_modules.py b/Base/Python/slicer/tests/test_slicer_util_without_modules.py index fa78bf7493c..f6c58aff614 100644 --- a/Base/Python/slicer/tests/test_slicer_util_without_modules.py +++ b/Base/Python/slicer/tests/test_slicer_util_without_modules.py @@ -7,34 +7,34 @@ class SlicerUtilWithoutModulesTest(unittest.TestCase): - def setUp(self): - pass + def setUp(self): + pass - def test_computeChecksum(self): - with self.assertRaises(IOError): - slicer.util.computeChecksum('SHA256', 'compute-checksum-nonexistent.txt') + def test_computeChecksum(self): + with self.assertRaises(IOError): + slicer.util.computeChecksum('SHA256', 'compute-checksum-nonexistent.txt') - with tempfile.TemporaryDirectory() as tmpdirname: + with tempfile.TemporaryDirectory() as tmpdirname: - input_file = os.path.join(tmpdirname, 'compute-checksum.txt') - with open(input_file, 'w', newline='\n') as content: - content.write('This is a text file!\n') + input_file = os.path.join(tmpdirname, 'compute-checksum.txt') + with open(input_file, 'w', newline='\n') as content: + content.write('This is a text file!\n') - self.assertTrue(os.path.exists(input_file)) + self.assertTrue(os.path.exists(input_file)) - self.assertEqual(slicer.util.computeChecksum('SHA256', input_file), '4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338') - self.assertEqual(slicer.util.computeChecksum('SHA512', input_file), '5080ee92e951c5f8336053f11d278c23f5d26b5eb78805c952960eac0194f357f98b0e350611ce081d4a1e28dd8ea182d3a276c99b1752e0def2de0f47b8b27b') + self.assertEqual(slicer.util.computeChecksum('SHA256', input_file), '4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338') + self.assertEqual(slicer.util.computeChecksum('SHA512', input_file), '5080ee92e951c5f8336053f11d278c23f5d26b5eb78805c952960eac0194f357f98b0e350611ce081d4a1e28dd8ea182d3a276c99b1752e0def2de0f47b8b27b') - with self.assertRaises(ValueError): - slicer.util.computeChecksum('SHAINVALID', input_file) + with self.assertRaises(ValueError): + slicer.util.computeChecksum('SHAINVALID', input_file) - def test_extractAlgoAndDigest(self): - with self.assertRaises(ValueError): - slicer.util.extractAlgoAndDigest('4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338') + def test_extractAlgoAndDigest(self): + with self.assertRaises(ValueError): + slicer.util.extractAlgoAndDigest('4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338') - with self.assertRaises(ValueError): - slicer.util.extractAlgoAndDigest('SHAINVALID:4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338') + with self.assertRaises(ValueError): + slicer.util.extractAlgoAndDigest('SHAINVALID:4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338') - self.assertEqual( - slicer.util.extractAlgoAndDigest('SHA256:4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338'), - ('SHA256', '4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338')) + self.assertEqual( + slicer.util.extractAlgoAndDigest('SHA256:4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338'), + ('SHA256', '4a57f3207b97f26a6061f86948483c00b03893ddfef9e82b639ebe66e3aba338')) diff --git a/Base/Python/slicer/util.py b/Base/Python/slicer/util.py index 2bedcca9b33..38baecc9497 100644 --- a/Base/Python/slicer/util.py +++ b/Base/Python/slicer/util.py @@ -20,113 +20,113 @@ def quit(): - exit(EXIT_SUCCESS) + exit(EXIT_SUCCESS) def exit(status=EXIT_SUCCESS): - """Exits the application with the specified exit code. + """Exits the application with the specified exit code. - The method does not stops the process immediately but lets - pending events to be processed. - If exit() is called again while processing pending events, - the error code will be overwritten. + The method does not stops the process immediately but lets + pending events to be processed. + If exit() is called again while processing pending events, + the error code will be overwritten. - To make the application exit immediately, this code can be used. - Note that forcing the application to exit may result in - improperly released files and other resources. + To make the application exit immediately, this code can be used. + Note that forcing the application to exit may result in + improperly released files and other resources. - .. code-block:: python + .. code-block:: python - import sys - sys.exit(status) + import sys + sys.exit(status) - """ + """ - from slicer import app - # Prevent automatic application exit (for example, triggered by starting Slicer - # with "--testing" argument) from overwriting the exit code that we set now. - app.commandOptions().runPythonAndExit = False - app.exit(status) + from slicer import app + # Prevent automatic application exit (for example, triggered by starting Slicer + # with "--testing" argument) from overwriting the exit code that we set now. + app.commandOptions().runPythonAndExit = False + app.exit(status) def restart(): - """Restart the application. + """Restart the application. - No confirmation popup is displayed. - """ + No confirmation popup is displayed. + """ - from slicer import app - app.restart() + from slicer import app + app.restart() def _readCMakeCache(var): - import os - from slicer import app + import os + from slicer import app - prefix = var + ":" + prefix = var + ":" - try: - with open(os.path.join(app.slicerHome, "CMakeCache.txt")) as cache: - for line in cache: - if line.startswith(prefix): - return line.split("=", 1)[1].rstrip() + try: + with open(os.path.join(app.slicerHome, "CMakeCache.txt")) as cache: + for line in cache: + if line.startswith(prefix): + return line.split("=", 1)[1].rstrip() - except: - pass + except: + pass - return None + return None def sourceDir(): - """Location of the Slicer source directory. + """Location of the Slicer source directory. - :type: :class:`str` or ``None`` + :type: :class:`str` or ``None`` - This provides the location of the Slicer source directory, if Slicer is being - run from a CMake build directory. If the Slicer home directory does not - contain a ``CMakeCache.txt`` (e.g. for an installed Slicer), the property - will have the value ``None``. - """ - return _readCMakeCache('Slicer_SOURCE_DIR') + This provides the location of the Slicer source directory, if Slicer is being + run from a CMake build directory. If the Slicer home directory does not + contain a ``CMakeCache.txt`` (e.g. for an installed Slicer), the property + will have the value ``None``. + """ + return _readCMakeCache('Slicer_SOURCE_DIR') def startupEnvironment(): - """Returns the environment without the Slicer specific values. + """Returns the environment without the Slicer specific values. - Path environment variables like `PATH`, `LD_LIBRARY_PATH` or `PYTHONPATH` - will not contain values found in the launcher settings. + Path environment variables like `PATH`, `LD_LIBRARY_PATH` or `PYTHONPATH` + will not contain values found in the launcher settings. - Similarly `key=value` environment variables also found in the launcher - settings are excluded. Note that if a value was associated with a key prior - starting Slicer, it will not be set in the environment returned by this - function. + Similarly `key=value` environment variables also found in the launcher + settings are excluded. Note that if a value was associated with a key prior + starting Slicer, it will not be set in the environment returned by this + function. - The function excludes both the Slicer launcher settings and the revision - specific launcher settings. - """ - import slicer - startupEnv = slicer.app.startupEnvironment() - import os - # "if varname" is added to reject empty key (it is invalid) - if os.name == 'nt': - # On Windows, subprocess functions expect environment to contain strings - # and Qt provide us unicode strings, so we need to convert them. - return {str(varname): str(startupEnv.value(varname)) for varname in list(startupEnv.keys()) if varname} - else: - return {varname: startupEnv.value(varname) for varname in list(startupEnv.keys()) if varname} + The function excludes both the Slicer launcher settings and the revision + specific launcher settings. + """ + import slicer + startupEnv = slicer.app.startupEnvironment() + import os + # "if varname" is added to reject empty key (it is invalid) + if os.name == 'nt': + # On Windows, subprocess functions expect environment to contain strings + # and Qt provide us unicode strings, so we need to convert them. + return {str(varname): str(startupEnv.value(varname)) for varname in list(startupEnv.keys()) if varname} + else: + return {varname: startupEnv.value(varname) for varname in list(startupEnv.keys()) if varname} # # Custom Import # -def importVTKClassesFromDirectory(directory, dest_module_name, filematch = '*'): - from vtk import vtkObjectBase - importClassesFromDirectory(directory, dest_module_name, vtkObjectBase, filematch) +def importVTKClassesFromDirectory(directory, dest_module_name, filematch='*'): + from vtk import vtkObjectBase + importClassesFromDirectory(directory, dest_module_name, vtkObjectBase, filematch) -def importQtClassesFromDirectory(directory, dest_module_name, filematch = '*'): - importClassesFromDirectory(directory, dest_module_name, 'PythonQtClassWrapper', filematch) +def importQtClassesFromDirectory(directory, dest_module_name, filematch='*'): + importClassesFromDirectory(directory, dest_module_name, 'PythonQtClassWrapper', filematch) # To avoid globbing multiple times the same directory, successful @@ -137,63 +137,63 @@ def importQtClassesFromDirectory(directory, dest_module_name, filematch = '*'): __import_classes_cache = set() -def importClassesFromDirectory(directory, dest_module_name, type_info, filematch = '*'): - # Create entry for __import_classes_cache - cache_key = ",".join([str(arg) for arg in [directory, dest_module_name, type_info, filematch]]) - # Check if function has already been called with this set of parameters - if cache_key in __import_classes_cache: - return +def importClassesFromDirectory(directory, dest_module_name, type_info, filematch='*'): + # Create entry for __import_classes_cache + cache_key = ",".join([str(arg) for arg in [directory, dest_module_name, type_info, filematch]]) + # Check if function has already been called with this set of parameters + if cache_key in __import_classes_cache: + return - import glob, os, re, fnmatch - re_filematch = re.compile(fnmatch.translate(filematch)) - for fname in glob.glob(os.path.join(directory, filematch)): - if not re_filematch.match(os.path.basename(fname)): - continue - try: - from_module_name = os.path.splitext(os.path.basename(fname))[0] - importModuleObjects(from_module_name, dest_module_name, type_info) - except ImportError as detail: - import sys - print(detail, file=sys.stderr) + import glob, os, re, fnmatch + re_filematch = re.compile(fnmatch.translate(filematch)) + for fname in glob.glob(os.path.join(directory, filematch)): + if not re_filematch.match(os.path.basename(fname)): + continue + try: + from_module_name = os.path.splitext(os.path.basename(fname))[0] + importModuleObjects(from_module_name, dest_module_name, type_info) + except ImportError as detail: + import sys + print(detail, file=sys.stderr) - __import_classes_cache.add(cache_key) + __import_classes_cache.add(cache_key) def importModuleObjects(from_module_name, dest_module_name, type_info): - """Import object of type 'type_info' (str or type) from module identified - by 'from_module_name' into the module identified by 'dest_module_name'.""" + """Import object of type 'type_info' (str or type) from module identified + by 'from_module_name' into the module identified by 'dest_module_name'.""" - # Obtain a reference to the module identifed by 'dest_module_name' - import sys - dest_module = sys.modules[dest_module_name] - - # Skip if module has already been loaded - if from_module_name in dir(dest_module): - return - - # Obtain a reference to the module identified by 'from_module_name' - import imp - fp, pathname, description = imp.find_module(from_module_name) - module = imp.load_module(from_module_name, fp, pathname, description) - - # Loop over content of the python module associated with the given python library - for item_name in dir(module): - - # Obtain a reference associated with the current object - item = getattr(module, item_name) - - # Check type match by type or type name - match = False - if isinstance(type_info, type): - try: - match = issubclass(item, type_info) - except TypeError as e: - pass - else: - match = type(item).__name__ == type_info + # Obtain a reference to the module identifed by 'dest_module_name' + import sys + dest_module = sys.modules[dest_module_name] + + # Skip if module has already been loaded + if from_module_name in dir(dest_module): + return + + # Obtain a reference to the module identified by 'from_module_name' + import imp + fp, pathname, description = imp.find_module(from_module_name) + module = imp.load_module(from_module_name, fp, pathname, description) + + # Loop over content of the python module associated with the given python library + for item_name in dir(module): + + # Obtain a reference associated with the current object + item = getattr(module, item_name) + + # Check type match by type or type name + match = False + if isinstance(type_info, type): + try: + match = issubclass(item, type_info) + except TypeError as e: + pass + else: + match = type(item).__name__ == type_info - if match: - setattr(dest_module, item_name, item) + if match: + setattr(dest_module, item_name, item) # @@ -201,447 +201,448 @@ def importModuleObjects(from_module_name, dest_module_name, type_info): # def lookupTopLevelWidget(objectName): - """Loop over all top level widget associated with 'slicer.app' and - return the one matching 'objectName' + """Loop over all top level widget associated with 'slicer.app' and + return the one matching 'objectName' - :raises RuntimeError: if no top-level widget is found by that name - """ - from slicer import app - for w in app.topLevelWidgets(): - if hasattr(w,'objectName'): - if w.objectName == objectName: return w - # not found - raise RuntimeError("Failed to obtain reference to '%s'" % objectName) + :raises RuntimeError: if no top-level widget is found by that name + """ + from slicer import app + for w in app.topLevelWidgets(): + if hasattr(w, 'objectName'): + if w.objectName == objectName: + return w + # not found + raise RuntimeError("Failed to obtain reference to '%s'" % objectName) def mainWindow(): - """Get main window widget (qSlicerMainWindow object) + """Get main window widget (qSlicerMainWindow object) - :return: main window widget, or ``None`` if there is no main window - """ - try: - mw = lookupTopLevelWidget('qSlicerMainWindow') - except RuntimeError: - # main window not found, return None - # Note: we do not raise an exception so that this function can be conveniently used - # in expressions such as `parent if parent else mainWindow()` - mw = None - return mw + :return: main window widget, or ``None`` if there is no main window + """ + try: + mw = lookupTopLevelWidget('qSlicerMainWindow') + except RuntimeError: + # main window not found, return None + # Note: we do not raise an exception so that this function can be conveniently used + # in expressions such as `parent if parent else mainWindow()` + mw = None + return mw def pythonShell(): - """Get Python console widget (ctkPythonConsole object) + """Get Python console widget (ctkPythonConsole object) - :raises RuntimeError: if not found - """ - from slicer import app - console = app.pythonConsole() - if not console: - raise RuntimeError("Failed to obtain reference to python shell") - return console + :raises RuntimeError: if not found + """ + from slicer import app + console = app.pythonConsole() + if not console: + raise RuntimeError("Failed to obtain reference to python shell") + return console -def showStatusMessage(message, duration = 0): - """Display ``message`` in the status bar. - """ - mw = mainWindow() - if not mw or not mw.statusBar(): - return False - mw.statusBar().showMessage(message, duration) - return True +def showStatusMessage(message, duration=0): + """Display ``message`` in the status bar. + """ + mw = mainWindow() + if not mw or not mw.statusBar(): + return False + mw.statusBar().showMessage(message, duration) + return True def findChildren(widget=None, name="", text="", title="", className=""): - """ Return a list of child widgets that meet all the given criteria. - - If no criteria are provided, the function will return all widgets descendants. - If no widget is provided, slicer.util.mainWindow() is used. - :param widget: parent widget where the widgets will be searched - :param name: name attribute of the widget - :param text: text attribute of the widget - :param title: title attribute of the widget - :param className: className() attribute of the widget - :return: list with all the widgets that meet all the given criteria. - """ - # TODO: figure out why the native QWidget.findChildren method does not seem to work from PythonQt - import fnmatch - if not widget: - widget = mainWindow() - if not widget: - return [] - children = [] - parents = [widget] - kwargs = {'name': name, 'text': text, 'title': title, 'className': className} - expected_matches = [] - for kwarg in kwargs.keys(): - if kwargs[kwarg]: - expected_matches.append(kwarg) - while parents: - p = parents.pop() - # sometimes, p is null, f.e. when using --python-script or --python-code - if not p: - continue - if not hasattr(p,'children'): - continue - parents += p.children() - matched_filter_criteria = 0 - for attribute in expected_matches: - if hasattr(p, attribute): - attr_name = getattr(p, attribute) - if attribute == 'className': - # className is a method, not a direct attribute. Invoke the method - attr_name = attr_name() - # Objects may have text attributes with non-string value (for example, - # QUndoStack objects have text attribute of 'builtin_qt_slot' type. - # We only consider string type attributes. - if isinstance(attr_name, str): - if fnmatch.fnmatchcase(attr_name, kwargs[attribute]): - matched_filter_criteria = matched_filter_criteria + 1 - if matched_filter_criteria == len(expected_matches): - children.append(p) - return children + """ Return a list of child widgets that meet all the given criteria. + + If no criteria are provided, the function will return all widgets descendants. + If no widget is provided, slicer.util.mainWindow() is used. + :param widget: parent widget where the widgets will be searched + :param name: name attribute of the widget + :param text: text attribute of the widget + :param title: title attribute of the widget + :param className: className() attribute of the widget + :return: list with all the widgets that meet all the given criteria. + """ + # TODO: figure out why the native QWidget.findChildren method does not seem to work from PythonQt + import fnmatch + if not widget: + widget = mainWindow() + if not widget: + return [] + children = [] + parents = [widget] + kwargs = {'name': name, 'text': text, 'title': title, 'className': className} + expected_matches = [] + for kwarg in kwargs.keys(): + if kwargs[kwarg]: + expected_matches.append(kwarg) + while parents: + p = parents.pop() + # sometimes, p is null, f.e. when using --python-script or --python-code + if not p: + continue + if not hasattr(p, 'children'): + continue + parents += p.children() + matched_filter_criteria = 0 + for attribute in expected_matches: + if hasattr(p, attribute): + attr_name = getattr(p, attribute) + if attribute == 'className': + # className is a method, not a direct attribute. Invoke the method + attr_name = attr_name() + # Objects may have text attributes with non-string value (for example, + # QUndoStack objects have text attribute of 'builtin_qt_slot' type. + # We only consider string type attributes. + if isinstance(attr_name, str): + if fnmatch.fnmatchcase(attr_name, kwargs[attribute]): + matched_filter_criteria = matched_filter_criteria + 1 + if matched_filter_criteria == len(expected_matches): + children.append(p) + return children def findChild(widget, name): - """Convenience method to access a widget by its ``name``. + """Convenience method to access a widget by its ``name``. - :raises RuntimeError: if the widget with the given ``name`` does not exist. - """ - errorMessage = "Widget named " + str(name) + " does not exists." - child = None - try: - child = findChildren(widget, name=name)[0] - if not child: - raise RuntimeError(errorMessage) - except IndexError: - raise RuntimeError(errorMessage) - return child + :raises RuntimeError: if the widget with the given ``name`` does not exist. + """ + errorMessage = "Widget named " + str(name) + " does not exists." + child = None + try: + child = findChildren(widget, name=name)[0] + if not child: + raise RuntimeError(errorMessage) + except IndexError: + raise RuntimeError(errorMessage) + return child def loadUI(path): - """ Load UI file ``path`` and return the corresponding widget. + """ Load UI file ``path`` and return the corresponding widget. - :raises RuntimeError: if the UI file is not found or if no - widget was instantiated. - """ - import qt - qfile = qt.QFile(path) - if not qfile.exists(): - errorMessage = "Could not load UI file: file not found " + str(path) + "\n\n" - raise RuntimeError(errorMessage) - qfile.open(qt.QFile.ReadOnly) - loader = qt.QUiLoader() - widget = loader.load(qfile) - if not widget: - errorMessage = "Could not load UI file: " + str(path) + "\n\n" - raise RuntimeError(errorMessage) - return widget - - -def startQtDesigner(args = None): - """ Start Qt Designer application to allow editing UI files. - """ - import slicer - cmdLineArguments = [] - if args is not None: - if isinstance(args, str): - cmdLineArguments.append(args) - else: - cmdLineArguments.extend(args) - return slicer.app.launchDesigner(cmdLineArguments) + :raises RuntimeError: if the UI file is not found or if no + widget was instantiated. + """ + import qt + qfile = qt.QFile(path) + if not qfile.exists(): + errorMessage = "Could not load UI file: file not found " + str(path) + "\n\n" + raise RuntimeError(errorMessage) + qfile.open(qt.QFile.ReadOnly) + loader = qt.QUiLoader() + widget = loader.load(qfile) + if not widget: + errorMessage = "Could not load UI file: " + str(path) + "\n\n" + raise RuntimeError(errorMessage) + return widget + + +def startQtDesigner(args=None): + """ Start Qt Designer application to allow editing UI files. + """ + import slicer + cmdLineArguments = [] + if args is not None: + if isinstance(args, str): + cmdLineArguments.append(args) + else: + cmdLineArguments.extend(args) + return slicer.app.launchDesigner(cmdLineArguments) def childWidgetVariables(widget): - """ Get child widgets as attributes of an object. + """ Get child widgets as attributes of an object. - Each named child widget is accessible as an attribute of the returned object, - with the attribute name matching the child widget name. - This function provides convenient access to widgets in a loaded UI file. + Each named child widget is accessible as an attribute of the returned object, + with the attribute name matching the child widget name. + This function provides convenient access to widgets in a loaded UI file. - Example:: + Example:: - uiWidget = slicer.util.loadUI(myUiFilePath) - self.ui = slicer.util.childWidgetVariables(uiWidget) - self.ui.inputSelector.setMRMLScene(slicer.mrmlScene) - self.ui.outputSelector.setMRMLScene(slicer.mrmlScene) + uiWidget = slicer.util.loadUI(myUiFilePath) + self.ui = slicer.util.childWidgetVariables(uiWidget) + self.ui.inputSelector.setMRMLScene(slicer.mrmlScene) + self.ui.outputSelector.setMRMLScene(slicer.mrmlScene) - """ - ui = type('', (), {})() # empty object - childWidgets = findChildren(widget) - for childWidget in childWidgets: - if hasattr(childWidget, "name"): - setattr(ui, childWidget.name, childWidget) - return ui + """ + ui = type('', (), {})() # empty object + childWidgets = findChildren(widget) + for childWidget in childWidgets: + if hasattr(childWidget, "name"): + setattr(ui, childWidget.name, childWidget) + return ui def addParameterEditWidgetConnections(parameterEditWidgets, updateParameterNodeFromGUI): - """ Add connections to get notification of a widget change. + """ Add connections to get notification of a widget change. - The function is useful for calling updateParameterNodeFromGUI method in scripted module widgets. + The function is useful for calling updateParameterNodeFromGUI method in scripted module widgets. - .. note:: Not all widget classes are supported yet. Report any missing classes at https://discourse.slicer.org. + .. note:: Not all widget classes are supported yet. Report any missing classes at https://discourse.slicer.org. - Example:: + Example:: - class SurfaceToolboxWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): - ... - def setup(self): + class SurfaceToolboxWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): ... - self.parameterEditWidgets = [ - (self.ui.inputModelSelector, "inputModel"), - (self.ui.outputModelSelector, "outputModel"), - (self.ui.decimationButton, "decimation"), - ...] - slicer.util.addParameterEditWidgetConnections(self.parameterEditWidgets, self.updateParameterNodeFromGUI) - - def updateGUIFromParameterNode(self, caller=None, event=None): - if self._parameterNode is None or self._updatingGUIFromParameterNode: - return - self._updatingGUIFromParameterNode = True - slicer.util.updateParameterEditWidgetsFromNode(self.parameterEditWidgets, self._parameterNode) - self._updatingGUIFromParameterNode = False - - def updateParameterNodeFromGUI(self, caller=None, event=None): - if self._parameterNode is None or self._updatingGUIFromParameterNode: - return - wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch - slicer.util.updateNodeFromParameterEditWidgets(self.parameterEditWidgets, self._parameterNode) - self._parameterNode.EndModify(wasModified) - """ + def setup(self): + ... + self.parameterEditWidgets = [ + (self.ui.inputModelSelector, "inputModel"), + (self.ui.outputModelSelector, "outputModel"), + (self.ui.decimationButton, "decimation"), + ...] + slicer.util.addParameterEditWidgetConnections(self.parameterEditWidgets, self.updateParameterNodeFromGUI) + + def updateGUIFromParameterNode(self, caller=None, event=None): + if self._parameterNode is None or self._updatingGUIFromParameterNode: + return + self._updatingGUIFromParameterNode = True + slicer.util.updateParameterEditWidgetsFromNode(self.parameterEditWidgets, self._parameterNode) + self._updatingGUIFromParameterNode = False + + def updateParameterNodeFromGUI(self, caller=None, event=None): + if self._parameterNode is None or self._updatingGUIFromParameterNode: + return + wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch + slicer.util.updateNodeFromParameterEditWidgets(self.parameterEditWidgets, self._parameterNode) + self._parameterNode.EndModify(wasModified) + """ - for (widget, parameterName) in parameterEditWidgets: - widgetClassName = widget.className() - if widgetClassName=="QSpinBox": - widget.connect("valueChanged(int)", updateParameterNodeFromGUI) - elif widgetClassName=="QCheckBox": - widget.connect("clicked()", updateParameterNodeFromGUI) - elif widgetClassName=="QPushButton": - widget.connect("toggled(bool)", updateParameterNodeFromGUI) - elif widgetClassName=="qMRMLNodeComboBox": - widget.connect("currentNodeIDChanged(QString)", updateParameterNodeFromGUI) - elif widgetClassName=="QComboBox": - widget.connect("currentIndexChanged(int)", updateParameterNodeFromGUI) - elif widgetClassName=="ctkSliderWidget": - widget.connect("valueChanged(double)", updateParameterNodeFromGUI) + for (widget, parameterName) in parameterEditWidgets: + widgetClassName = widget.className() + if widgetClassName == "QSpinBox": + widget.connect("valueChanged(int)", updateParameterNodeFromGUI) + elif widgetClassName == "QCheckBox": + widget.connect("clicked()", updateParameterNodeFromGUI) + elif widgetClassName == "QPushButton": + widget.connect("toggled(bool)", updateParameterNodeFromGUI) + elif widgetClassName == "qMRMLNodeComboBox": + widget.connect("currentNodeIDChanged(QString)", updateParameterNodeFromGUI) + elif widgetClassName == "QComboBox": + widget.connect("currentIndexChanged(int)", updateParameterNodeFromGUI) + elif widgetClassName == "ctkSliderWidget": + widget.connect("valueChanged(double)", updateParameterNodeFromGUI) def removeParameterEditWidgetConnections(parameterEditWidgets, updateParameterNodeFromGUI): - """ Remove connections created by :py:meth:`addParameterEditWidgetConnections`. - """ + """ Remove connections created by :py:meth:`addParameterEditWidgetConnections`. + """ - for (widget, parameterName) in parameterEditWidgets: - widgetClassName = widget.className() - if widgetClassName=="QSpinBox": - widget.disconnect("valueChanged(int)", updateParameterNodeFromGUI) - elif widgetClassName=="QPushButton": - widget.disconnect("toggled(bool)", updateParameterNodeFromGUI) - elif widgetClassName=="qMRMLNodeComboBox": - widget.disconnect("currentNodeIDChanged(QString)", updateParameterNodeFromGUI) - elif widgetClassName=="QComboBox": - widget.disconnect("currentIndexChanged(int)", updateParameterNodeFromGUI) - elif widgetClassName=="ctkSliderWidget": - widget.disconnect("valueChanged(double)", updateParameterNodeFromGUI) + for (widget, parameterName) in parameterEditWidgets: + widgetClassName = widget.className() + if widgetClassName == "QSpinBox": + widget.disconnect("valueChanged(int)", updateParameterNodeFromGUI) + elif widgetClassName == "QPushButton": + widget.disconnect("toggled(bool)", updateParameterNodeFromGUI) + elif widgetClassName == "qMRMLNodeComboBox": + widget.disconnect("currentNodeIDChanged(QString)", updateParameterNodeFromGUI) + elif widgetClassName == "QComboBox": + widget.disconnect("currentIndexChanged(int)", updateParameterNodeFromGUI) + elif widgetClassName == "ctkSliderWidget": + widget.disconnect("valueChanged(double)", updateParameterNodeFromGUI) def updateParameterEditWidgetsFromNode(parameterEditWidgets, parameterNode): - """ Update widgets from values stored in a vtkMRMLScriptedModuleNode. + """ Update widgets from values stored in a vtkMRMLScriptedModuleNode. - The function is useful for implementing updateGUIFromParameterNode. + The function is useful for implementing updateGUIFromParameterNode. - Note: Only a few widget classes are supported now. More will be added later. Report any missing classes at discourse.slicer.org. + Note: Only a few widget classes are supported now. More will be added later. Report any missing classes at discourse.slicer.org. - See example in :py:meth:`addParameterEditWidgetConnections` documentation. - """ + See example in :py:meth:`addParameterEditWidgetConnections` documentation. + """ - for (widget, parameterName) in parameterEditWidgets: - widgetClassName = widget.className() - parameterValue = parameterNode.GetParameter(parameterName) - if widgetClassName=="QSpinBox": - if parameterValue: - widget.value = int(float(parameterValue)) - else: - widget.value = 0 - if widgetClassName=="ctkSliderWidget": - if parameterValue: - widget.value = float(parameterValue) - else: - widget.value = 0.0 - elif widgetClassName=="QCheckBox" or widgetClassName=="QPushButton": - widget.checked = (parameterValue == "true") - elif widgetClassName=="QComboBox": - widget.setCurrentText(parameterValue) - elif widgetClassName=="qMRMLNodeComboBox": - widget.currentNodeID = parameterNode.GetNodeReferenceID(parameterName) + for (widget, parameterName) in parameterEditWidgets: + widgetClassName = widget.className() + parameterValue = parameterNode.GetParameter(parameterName) + if widgetClassName == "QSpinBox": + if parameterValue: + widget.value = int(float(parameterValue)) + else: + widget.value = 0 + if widgetClassName == "ctkSliderWidget": + if parameterValue: + widget.value = float(parameterValue) + else: + widget.value = 0.0 + elif widgetClassName == "QCheckBox" or widgetClassName == "QPushButton": + widget.checked = (parameterValue == "true") + elif widgetClassName == "QComboBox": + widget.setCurrentText(parameterValue) + elif widgetClassName == "qMRMLNodeComboBox": + widget.currentNodeID = parameterNode.GetNodeReferenceID(parameterName) def updateNodeFromParameterEditWidgets(parameterEditWidgets, parameterNode): - """ Update vtkMRMLScriptedModuleNode from widgets. + """ Update vtkMRMLScriptedModuleNode from widgets. - The function is useful for implementing updateParameterNodeFromGUI. + The function is useful for implementing updateParameterNodeFromGUI. - Note: Only a few widget classes are supported now. More will be added later. Report any missing classes at discourse.slicer.org. + Note: Only a few widget classes are supported now. More will be added later. Report any missing classes at discourse.slicer.org. - See example in :py:meth:`addParameterEditWidgetConnections` documentation. - """ + See example in :py:meth:`addParameterEditWidgetConnections` documentation. + """ - for (widget, parameterName) in parameterEditWidgets: - widgetClassName = widget.className() - if widgetClassName=="QSpinBox" or widgetClassName=="ctkSliderWidget": - parameterNode.SetParameter(parameterName, str(widget.value)) - elif widgetClassName=="QCheckBox" or widgetClassName=="QPushButton": - parameterNode.SetParameter(parameterName, "true" if widget.checked else "false") - elif widgetClassName=="QComboBox": - parameterNode.SetParameter(parameterName, widget.currentText) - elif widgetClassName=="qMRMLNodeComboBox": - parameterNode.SetNodeReferenceID(parameterName, widget.currentNodeID) + for (widget, parameterName) in parameterEditWidgets: + widgetClassName = widget.className() + if widgetClassName == "QSpinBox" or widgetClassName == "ctkSliderWidget": + parameterNode.SetParameter(parameterName, str(widget.value)) + elif widgetClassName == "QCheckBox" or widgetClassName == "QPushButton": + parameterNode.SetParameter(parameterName, "true" if widget.checked else "false") + elif widgetClassName == "QComboBox": + parameterNode.SetParameter(parameterName, widget.currentText) + elif widgetClassName == "qMRMLNodeComboBox": + parameterNode.SetNodeReferenceID(parameterName, widget.currentNodeID) def setSliceViewerLayers(background='keep-current', foreground='keep-current', label='keep-current', foregroundOpacity=None, labelOpacity=None, fit=False, rotateToVolumePlane=False): - """ Set the slice views with the given nodes. + """ Set the slice views with the given nodes. - If node ID is not specified (or value is 'keep-current') then the layer will not be modified. + If node ID is not specified (or value is 'keep-current') then the layer will not be modified. - :param background: node or node ID to be used for the background layer - :param foreground: node or node ID to be used for the foreground layer - :param label: node or node ID to be used for the label layer - :param foregroundOpacity: opacity of the foreground layer - :param labelOpacity: opacity of the label layer - :param rotateToVolumePlane: rotate views to closest axis of the selected background, foreground, or label volume - :param fit: fit slice views to their content (position&zoom to show all visible layers) - """ - import slicer - - def _nodeID(nodeOrID): - nodeID = nodeOrID - if isinstance(nodeOrID, slicer.vtkMRMLNode): - nodeID = nodeOrID.GetID() - return nodeID - - num = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSliceCompositeNode') - for i in range(num): - sliceViewer = slicer.mrmlScene.GetNthNodeByClass(i, 'vtkMRMLSliceCompositeNode') - if background != 'keep-current': - sliceViewer.SetBackgroundVolumeID(_nodeID(background)) - if foreground != 'keep-current': - sliceViewer.SetForegroundVolumeID(_nodeID(foreground)) - if foregroundOpacity is not None: - sliceViewer.SetForegroundOpacity(foregroundOpacity) - if label != 'keep-current': - sliceViewer.SetLabelVolumeID(_nodeID(label)) - if labelOpacity is not None: - sliceViewer.SetLabelOpacity(labelOpacity) - - if rotateToVolumePlane: - if background != 'keep-current': - volumeNode = slicer.mrmlScene.GetNodeByID(_nodeID(background)) - elif foreground != 'keep-current': - volumeNode = slicer.mrmlScene.GetNodeByID(_nodeID(foreground)) - elif label != 'keep-current': - volumeNode = slicer.mrmlScene.GetNodeByID(_nodeID(label)) - else: - volumeNode = None - if volumeNode: - layoutManager = slicer.app.layoutManager() - for sliceViewName in layoutManager.sliceViewNames(): - layoutManager.sliceWidget(sliceViewName).mrmlSliceNode().RotateToVolumePlane(volumeNode) - - if fit: - layoutManager = slicer.app.layoutManager() - if layoutManager is not None: - sliceLogics = layoutManager.mrmlSliceLogics() - for i in range(sliceLogics.GetNumberOfItems()): - sliceLogic = sliceLogics.GetItemAsObject(i) - if sliceLogic: - sliceLogic.FitSliceToAll() + :param background: node or node ID to be used for the background layer + :param foreground: node or node ID to be used for the foreground layer + :param label: node or node ID to be used for the label layer + :param foregroundOpacity: opacity of the foreground layer + :param labelOpacity: opacity of the label layer + :param rotateToVolumePlane: rotate views to closest axis of the selected background, foreground, or label volume + :param fit: fit slice views to their content (position&zoom to show all visible layers) + """ + import slicer + + def _nodeID(nodeOrID): + nodeID = nodeOrID + if isinstance(nodeOrID, slicer.vtkMRMLNode): + nodeID = nodeOrID.GetID() + return nodeID + + num = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSliceCompositeNode') + for i in range(num): + sliceViewer = slicer.mrmlScene.GetNthNodeByClass(i, 'vtkMRMLSliceCompositeNode') + if background != 'keep-current': + sliceViewer.SetBackgroundVolumeID(_nodeID(background)) + if foreground != 'keep-current': + sliceViewer.SetForegroundVolumeID(_nodeID(foreground)) + if foregroundOpacity is not None: + sliceViewer.SetForegroundOpacity(foregroundOpacity) + if label != 'keep-current': + sliceViewer.SetLabelVolumeID(_nodeID(label)) + if labelOpacity is not None: + sliceViewer.SetLabelOpacity(labelOpacity) + + if rotateToVolumePlane: + if background != 'keep-current': + volumeNode = slicer.mrmlScene.GetNodeByID(_nodeID(background)) + elif foreground != 'keep-current': + volumeNode = slicer.mrmlScene.GetNodeByID(_nodeID(foreground)) + elif label != 'keep-current': + volumeNode = slicer.mrmlScene.GetNodeByID(_nodeID(label)) + else: + volumeNode = None + if volumeNode: + layoutManager = slicer.app.layoutManager() + for sliceViewName in layoutManager.sliceViewNames(): + layoutManager.sliceWidget(sliceViewName).mrmlSliceNode().RotateToVolumePlane(volumeNode) + + if fit: + layoutManager = slicer.app.layoutManager() + if layoutManager is not None: + sliceLogics = layoutManager.mrmlSliceLogics() + for i in range(sliceLogics.GetNumberOfItems()): + sliceLogic = sliceLogics.GetItemAsObject(i) + if sliceLogic: + sliceLogic.FitSliceToAll() def setToolbarsVisible(visible, ignore=None): - """Show/hide all existing toolbars, except those listed in ignore list. + """Show/hide all existing toolbars, except those listed in ignore list. - If there is no main window then the function has no effect. - """ - mw = mainWindow() - if not mw: - return - for toolbar in mainWindow().findChildren('QToolBar'): - if ignore is not None and toolbar in ignore: - continue - toolbar.setVisible(visible) - - # Prevent sequence browser toolbar showing up automatically - # when a sequence is loaded. - # (put in try block because Sequence Browser module is not always installed) - try: - import slicer - slicer.modules.sequences.autoShowToolBar = visible - except: - # Sequences module is not installed - pass + If there is no main window then the function has no effect. + """ + mw = mainWindow() + if not mw: + return + for toolbar in mainWindow().findChildren('QToolBar'): + if ignore is not None and toolbar in ignore: + continue + toolbar.setVisible(visible) + + # Prevent sequence browser toolbar showing up automatically + # when a sequence is loaded. + # (put in try block because Sequence Browser module is not always installed) + try: + import slicer + slicer.modules.sequences.autoShowToolBar = visible + except: + # Sequences module is not installed + pass def setMenuBarsVisible(visible, ignore=None): - """Show/hide all menu bars, except those listed in ignore list. + """Show/hide all menu bars, except those listed in ignore list. - If there is no main window then the function has no effect. - """ - mw = mainWindow() - if not mw: - return - for menubar in mw.findChildren('QMenuBar'): - if ignore is not None and menubar in ignore: - continue - menubar.setVisible(visible) + If there is no main window then the function has no effect. + """ + mw = mainWindow() + if not mw: + return + for menubar in mw.findChildren('QMenuBar'): + if ignore is not None and menubar in ignore: + continue + menubar.setVisible(visible) def setPythonConsoleVisible(visible): - """Show/hide Python console. + """Show/hide Python console. - If there is no main window then the function has no effect. - """ - mw = mainWindow() - if not mw: - return - mw.pythonConsole().parent().setVisible(visible) + If there is no main window then the function has no effect. + """ + mw = mainWindow() + if not mw: + return + mw.pythonConsole().parent().setVisible(visible) def setStatusBarVisible(visible): - """Show/hide status bar + """Show/hide status bar - If there is no main window or status bar then the function has no effect. - """ - mw = mainWindow() - if not mw or not mw.statusBar(): - return - mw.statusBar().setVisible(visible) + If there is no main window or status bar then the function has no effect. + """ + mw = mainWindow() + if not mw or not mw.statusBar(): + return + mw.statusBar().setVisible(visible) def setViewControllersVisible(visible): - """Show/hide view controller toolbar at the top of slice and 3D views""" - import slicer - lm = slicer.app.layoutManager() - for viewIndex in range(lm.threeDViewCount): - lm.threeDWidget(viewIndex).threeDController().setVisible(visible) - for sliceViewName in lm.sliceViewNames(): - lm.sliceWidget(sliceViewName).sliceController().setVisible(visible) - for viewIndex in range(lm.tableViewCount): - lm.tableWidget(viewIndex).tableController().setVisible(visible) - for viewIndex in range(lm.plotViewCount): - lm.plotWidget(viewIndex).plotController().setVisible(visible) + """Show/hide view controller toolbar at the top of slice and 3D views""" + import slicer + lm = slicer.app.layoutManager() + for viewIndex in range(lm.threeDViewCount): + lm.threeDWidget(viewIndex).threeDController().setVisible(visible) + for sliceViewName in lm.sliceViewNames(): + lm.sliceWidget(sliceViewName).sliceController().setVisible(visible) + for viewIndex in range(lm.tableViewCount): + lm.tableWidget(viewIndex).tableController().setVisible(visible) + for viewIndex in range(lm.plotViewCount): + lm.plotWidget(viewIndex).plotController().setVisible(visible) def forceRenderAllViews(): - """Force rendering of all views""" - import slicer - lm = slicer.app.layoutManager() - for viewIndex in range(lm.threeDViewCount): - lm.threeDWidget(viewIndex).threeDView().forceRender() - for sliceViewName in lm.sliceViewNames(): - lm.sliceWidget(sliceViewName).sliceView().forceRender() - for viewIndex in range(lm.tableViewCount): - lm.tableWidget(viewIndex).tableView().repaint() - for viewIndex in range(lm.plotViewCount): - lm.plotWidget(viewIndex).plotView().repaint() + """Force rendering of all views""" + import slicer + lm = slicer.app.layoutManager() + for viewIndex in range(lm.threeDViewCount): + lm.threeDWidget(viewIndex).threeDView().forceRender() + for sliceViewName in lm.sliceViewNames(): + lm.sliceWidget(sliceViewName).sliceView().forceRender() + for viewIndex in range(lm.tableViewCount): + lm.tableWidget(viewIndex).tableView().repaint() + for viewIndex in range(lm.plotViewCount): + lm.plotWidget(viewIndex).plotView().repaint() # @@ -649,724 +650,761 @@ def forceRenderAllViews(): # def loadNodeFromFile(filename, filetype, properties={}, returnNode=False): - """Load node into the scene from a file. - - :param filename: full path of the file to load. - :param filetype: specifies the file type, which determines which IO class will load the file. - :param properties: map containing additional parameters for the loading. - :param returnNode: Deprecated. If set to true then the method returns status flag and node - instead of signalling error by throwing an exception. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - :raises RuntimeError: in case of failure - """ - from slicer import app - from vtk import vtkCollection - properties['fileName'] = filename + """Load node into the scene from a file. + + :param filename: full path of the file to load. + :param filetype: specifies the file type, which determines which IO class will load the file. + :param properties: map containing additional parameters for the loading. + :param returnNode: Deprecated. If set to true then the method returns status flag and node + instead of signalling error by throwing an exception. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + :raises RuntimeError: in case of failure + """ + from slicer import app, vtkMRMLMessageCollection + from vtk import vtkCollection + properties['fileName'] = filename - loadedNodesCollection = vtkCollection() - success = app.coreIOManager().loadNodes(filetype, properties, loadedNodesCollection) - loadedNode = loadedNodesCollection.GetItemAsObject(0) if loadedNodesCollection.GetNumberOfItems() > 0 else None + loadedNodesCollection = vtkCollection() + userMessages = vtkMRMLMessageCollection() + success = app.coreIOManager().loadNodes(filetype, properties, loadedNodesCollection, userMessages) + loadedNode = loadedNodesCollection.GetItemAsObject(0) if loadedNodesCollection.GetNumberOfItems() > 0 else None - # Deprecated way of returning status and node - if returnNode: - import logging - logging.warning("loadNodeFromFile `returnNode` argument is deprecated. Loaded node is now returned directly if `returnNode` is not specified.") - import traceback - logging.debug("loadNodeFromFile was called from " + ("".join(traceback.format_stack()))) - return success, loadedNode + # Deprecated way of returning status and node + if returnNode: + import logging + logging.warning("loadNodeFromFile `returnNode` argument is deprecated. Loaded node is now returned directly if `returnNode` is not specified.") + import traceback + logging.debug("loadNodeFromFile was called from " + ("".join(traceback.format_stack()))) + return success, loadedNode - if not success: - errorMessage = "Failed to load node from file: " + str(filename) - raise RuntimeError(errorMessage) + if not success: + errorMessage = f"Failed to load node from file: {filename}" + if userMessages.GetNumberOfMessages() > 0: + errorMessage += "\n" + userMessages.GetAllMessagesAsString() + raise RuntimeError(errorMessage) - return loadedNode + return loadedNode def loadNodesFromFile(filename, filetype, properties={}, returnNode=False): - """Load nodes into the scene from a file. + """Load nodes into the scene from a file. - It differs from `loadNodeFromFile` in that it returns loaded node(s) in an iterator. + It differs from `loadNodeFromFile` in that it returns loaded node(s) in an iterator. - :param filename: full path of the file to load. - :param filetype: specifies the file type, which determines which IO class will load the file. - :param properties: map containing additional parameters for the loading. - :return: loaded node(s) in an iterator object. - :raises RuntimeError: in case of failure - """ - from slicer import app - from vtk import vtkCollection - properties['fileName'] = filename + :param filename: full path of the file to load. + :param filetype: specifies the file type, which determines which IO class will load the file. + :param properties: map containing additional parameters for the loading. + :return: loaded node(s) in an iterator object. + :raises RuntimeError: in case of failure + """ + from slicer import app, vtkMRMLMessageCollection + from vtk import vtkCollection + properties['fileName'] = filename - loadedNodesCollection = vtkCollection() - success = app.coreIOManager().loadNodes(filetype, properties, loadedNodesCollection) - if not success: - errorMessage = "Failed to load nodes from file: " + str(filename) - raise RuntimeError(errorMessage) + loadedNodesCollection = vtkCollection() + userMessages = vtkMRMLMessageCollection() + success = app.coreIOManager().loadNodes(filetype, properties, loadedNodesCollection, userMessages) + if not success: + errorMessage = f"Failed to load node from file: {filename}" + if userMessages.GetNumberOfMessages() > 0: + errorMessage += "\n" + userMessages.GetAllMessagesAsString() + raise RuntimeError(errorMessage) - return iter(loadedNodesCollection) + return iter(loadedNodesCollection) def loadColorTable(filename, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'ColorTableFile', {}, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'ColorTableFile', {}, returnNode) def loadFiberBundle(filename, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'FiberBundleFile', {}, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'FiberBundleFile', {}, returnNode) def loadAnnotationFiducial(filename, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'AnnotationFile', {'fiducial': 1}, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'AnnotationFile', {'fiducial': 1}, returnNode) def loadAnnotationRuler(filename, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'AnnotationFile', {'ruler': 1}, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'AnnotationFile', {'ruler': 1}, returnNode) def loadAnnotationROI(filename, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'AnnotationFile', {'roi': 1}, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'AnnotationFile', {'roi': 1}, returnNode) def loadMarkupsFiducialList(filename, returnNode=False): - """Load markups fiducials from file. + """Load markups fiducials from file. - .. deprecated:: 4.13.0 - Use the universal :func:`loadMarkups` function instead. - """ - if returnNode: - return loadMarkups(filename) - else: - node = loadMarkups(filename) - return [node is not None, node] + .. deprecated:: 4.13.0 + Use the universal :func:`loadMarkups` function instead. + """ + if returnNode: + return loadMarkups(filename) + else: + node = loadMarkups(filename) + return [node is not None, node] def loadMarkupsCurve(filename): - """Load markups curve from file. + """Load markups curve from file. - .. deprecated:: 4.13.0 - Use the universal :func:`loadMarkups` function instead. - """ - return loadMarkups(filename) + .. deprecated:: 4.13.0 + Use the universal :func:`loadMarkups` function instead. + """ + return loadMarkups(filename) def loadMarkupsClosedCurve(filename): - """Load markups closed curve from file. + """Load markups closed curve from file. - .. deprecated:: 4.13.0 - Use the universal :func:`loadMarkups` function instead. - """ - return loadMarkups(filename) + .. deprecated:: 4.13.0 + Use the universal :func:`loadMarkups` function instead. + """ + return loadMarkups(filename) def loadMarkups(filename): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - """ - return loadNodeFromFile(filename, 'MarkupsFile') + :param filename: full path of the file to load. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + """ + return loadNodeFromFile(filename, 'MarkupsFile') def loadModel(filename, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'ModelFile', {}, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'ModelFile', {}, returnNode) def loadScalarOverlay(filename, modelNodeID, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'ScalarOverlayFile', {'modelNodeId': modelNodeID }, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'ScalarOverlayFile', {'modelNodeId': modelNodeID}, returnNode) def loadSegmentation(filename, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'SegmentationFile', {}, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'SegmentationFile', {}, returnNode) def loadTransform(filename, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'TransformFile', {}, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'TransformFile', {}, returnNode) def loadTable(filename): - """Load table node from file. + """Load table node from file. - :param filename: full path of the file to load. - :return: loaded table node - """ - return loadNodeFromFile(filename, 'TableFile') + :param filename: full path of the file to load. + :return: loaded table node + """ + return loadNodeFromFile(filename, 'TableFile') def loadLabelVolume(filename, properties={}, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - properties['labelmap'] = True - return loadNodeFromFile(filename, 'VolumeFile', properties, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + properties['labelmap'] = True + return loadNodeFromFile(filename, 'VolumeFile', properties, returnNode) def loadShaderProperty(filename, returnNode=False): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - return loadNodeFromFile(filename, 'ShaderPropertyFile', {}, returnNode) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + return loadNodeFromFile(filename, 'ShaderPropertyFile', {}, returnNode) def loadText(filename): - """Load node from file. + """Load node from file. - :param filename: full path of the text file to load. - :return: loaded text node. - """ - return loadNodeFromFile(filename, 'TextFile') + :param filename: full path of the text file to load. + :return: loaded text node. + """ + return loadNodeFromFile(filename, 'TextFile') def loadVolume(filename, properties={}, returnNode=False): - """Load node from file. - - :param filename: full path of the file to load. - :param properties: - - name: this name will be used as node name for the loaded volume - - labelmap: interpret volume as labelmap - - singleFile: ignore all other files in the directory - - center: ignore image position - - discardOrientation: ignore image axis directions - - autoWindowLevel: compute window/level automatically - - show: display volume in slice viewers after loading is completed - - fileNames: list of filenames to load the volume from - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - filetype = 'VolumeFile' - return loadNodeFromFile(filename, filetype, properties, returnNode) + """Load node from file. + + :param filename: full path of the file to load. + :param properties: + - name: this name will be used as node name for the loaded volume + - labelmap: interpret volume as labelmap + - singleFile: ignore all other files in the directory + - center: ignore image position + - discardOrientation: ignore image axis directions + - autoWindowLevel: compute window/level automatically + - show: display volume in slice viewers after loading is completed + - fileNames: list of filenames to load the volume from + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + filetype = 'VolumeFile' + return loadNodeFromFile(filename, filetype, properties, returnNode) def loadSequence(filename, properties={}): - """Load sequence (4D data set) from file. - - :param filename: full path of the file to load. - :param properties: - - name: this name will be used as node name for the loaded volume - - show: display volume in slice viewers after loading is completed - - colorNodeID: color node to set in the proxy nodes's display node - :return: loaded sequence node. - """ - filetype = 'SequenceFile' - return loadNodeFromFile(filename, filetype, properties) + """Load sequence (4D data set) from file. + + :param filename: full path of the file to load. + :param properties: + - name: this name will be used as node name for the loaded volume + - show: display volume in slice viewers after loading is completed + - colorNodeID: color node to set in the proxy nodes's display node + :return: loaded sequence node. + """ + filetype = 'SequenceFile' + return loadNodeFromFile(filename, filetype, properties) def loadScene(filename, properties={}): - """Load node from file. + """Load node from file. - :param filename: full path of the file to load. - :param returnNode: Deprecated. - :return: loaded node (if multiple nodes are loaded then a list of nodes). - If returnNode is True then a status flag and loaded node are returned. - """ - filetype = 'SceneFile' - return loadNodeFromFile(filename, filetype, properties, returnNode=False) + :param filename: full path of the file to load. + :param returnNode: Deprecated. + :return: loaded node (if multiple nodes are loaded then a list of nodes). + If returnNode is True then a status flag and loaded node are returned. + """ + filetype = 'SceneFile' + return loadNodeFromFile(filename, filetype, properties, returnNode=False) def openAddDataDialog(): - from slicer import app - return app.coreIOManager().openAddDataDialog() + from slicer import app + return app.coreIOManager().openAddDataDialog() def openAddVolumeDialog(): - from slicer import app - return app.coreIOManager().openAddVolumeDialog() + from slicer import app + return app.coreIOManager().openAddVolumeDialog() def openAddModelDialog(): - from slicer import app - return app.coreIOManager().openAddModelDialog() + from slicer import app + return app.coreIOManager().openAddModelDialog() def openAddScalarOverlayDialog(): - from slicer import app - return app.coreIOManager().openAddScalarOverlayDialog() + from slicer import app + return app.coreIOManager().openAddScalarOverlayDialog() def openAddSegmentationDialog(): - from slicer import app, qSlicerFileDialog - return app.coreIOManager().openDialog('SegmentationFile', qSlicerFileDialog.Read) + from slicer import app, qSlicerFileDialog + return app.coreIOManager().openDialog('SegmentationFile', qSlicerFileDialog.Read) def openAddTransformDialog(): - from slicer import app - return app.coreIOManager().openAddTransformDialog() + from slicer import app + return app.coreIOManager().openAddTransformDialog() def openAddColorTableDialog(): - from slicer import app - return app.coreIOManager().openAddColorTableDialog() + from slicer import app + return app.coreIOManager().openAddColorTableDialog() def openAddFiducialDialog(): - from slicer import app - return app.coreIOManager().openAddFiducialDialog() + from slicer import app + return app.coreIOManager().openAddFiducialDialog() def openAddMarkupsDialog(): - from slicer import app - return app.coreIOManager().openAddMarkupsDialog() + from slicer import app + return app.coreIOManager().openAddMarkupsDialog() def openAddFiberBundleDialog(): - from slicer import app - return app.coreIOManager().openAddFiberBundleDialog() + from slicer import app + return app.coreIOManager().openAddFiberBundleDialog() def openAddShaderPropertyDialog(): - from slicer import app, qSlicerFileDialog - return app.coreIOManager().openDialog('ShaderPropertyFile', qSlicerFileDialog.Read) + from slicer import app, qSlicerFileDialog + return app.coreIOManager().openDialog('ShaderPropertyFile', qSlicerFileDialog.Read) def openSaveDataDialog(): - from slicer import app - return app.coreIOManager().openSaveDataDialog() + from slicer import app + return app.coreIOManager().openSaveDataDialog() def saveNode(node, filename, properties={}): - """Save 'node' data into 'filename'. + """Save 'node' data into 'filename'. - It is the user responsibility to provide the appropriate file extension. + It is the user responsibility to provide the appropriate file extension. - User has also the possibility to overwrite the fileType internally retrieved using - method 'qSlicerCoreIOManager::fileWriterFileType(vtkObject*)'. This can be done - by specifying a 'fileType'attribute to the optional 'properties' dictionary. - """ - from slicer import app - properties["nodeID"] = node.GetID() - properties["fileName"] = filename - if hasattr(properties, "fileType"): - filetype = properties["fileType"] - else: - filetype = app.coreIOManager().fileWriterFileType(node) - return app.coreIOManager().saveNodes(filetype, properties) + User has also the possibility to overwrite the fileType internally retrieved using + method 'qSlicerCoreIOManager::fileWriterFileType(vtkObject*)'. This can be done + by specifying a 'fileType'attribute to the optional 'properties' dictionary. + """ + from slicer import app, vtkMRMLMessageCollection + + properties["nodeID"] = node.GetID() + properties["fileName"] = filename + if hasattr(properties, "fileType"): + filetype = properties["fileType"] + else: + filetype = app.coreIOManager().fileWriterFileType(node) + userMessages = vtkMRMLMessageCollection() + success = app.coreIOManager().saveNodes(filetype, properties, userMessages) + + if not success: + import logging + errorMessage = f"Failed to save node to file: {filename}" + if userMessages.GetNumberOfMessages() > 0: + errorMessage += "\n" + userMessages.GetAllMessagesAsString() + logging.error(errorMessage) + + return success def saveScene(filename, properties={}): - """Save the current scene. + """Save the current scene. - Based on the value of 'filename', the current scene is saved either - as a MRML file, MRB file or directory. + Based on the value of 'filename', the current scene is saved either + as a MRML file, MRB file or directory. - If filename ends with '.mrml', the scene is saved as a single file - without associated data. + If filename ends with '.mrml', the scene is saved as a single file + without associated data. - If filename ends with '.mrb', the scene is saved as a MRML bundle (Zip - archive with scene and data files). + If filename ends with '.mrb', the scene is saved as a MRML bundle (Zip + archive with scene and data files). - In every other case, the scene is saved in the directory - specified by 'filename'. Both MRML scene file and data - will be written to disk. If needed, directories and sub-directories - will be created. - """ - from slicer import app - filetype = 'SceneFile' - properties['fileName'] = filename - return app.coreIOManager().saveNodes(filetype, properties) + In every other case, the scene is saved in the directory + specified by 'filename'. Both MRML scene file and data + will be written to disk. If needed, directories and sub-directories + will be created. + """ + from slicer import app, vtkMRMLMessageCollection + filetype = 'SceneFile' + properties['fileName'] = filename + userMessages = vtkMRMLMessageCollection() + success = app.coreIOManager().saveNodes(filetype, properties, userMessages) + if not success: + import logging + errorMessage = f"Failed to save scene to file: {filename}" + if userMessages.GetNumberOfMessages() > 0: + errorMessage += "\n" + userMessages.GetAllMessagesAsString() + logging.error(errorMessage) -def exportNode(node, filename, properties={}, world=False): - """Export 'node' data into 'filename'. + return success - If `world` is set to True then the node will be exported in the world coordinate system - (equivalent to hardening the transform before exporting). - This method is different from saveNode in that it does not modify any existing storage node - and therefore does not change the filename or filetype that is used when saving the scene. - """ - from slicer import app, vtkDataFileFormatHelper - nodeIDs = [node.GetID()] - fileNames = [filename] - hardenTransform = world - - if "fileFormat" not in properties: - foundFileFormat = None - currentExtension = app.coreIOManager().extractKnownExtension(filename, node) - fileWriterExtensions = app.coreIOManager().fileWriterExtensions(node) - for fileFormat in fileWriterExtensions: - extension = vtkDataFileFormatHelper.GetFileExtensionFromFormatString(fileFormat) - if extension == currentExtension: - foundFileFormat = fileFormat - break - if not foundFileFormat: - raise ValueError(f"Failed to export {node.GetID()} - no known file format was found for filename {filename}") - properties["fileFormat"] = foundFileFormat - - return app.coreIOManager().exportNodes(nodeIDs, fileNames, properties, hardenTransform) +def exportNode(node, filename, properties={}, world=False): + """Export 'node' data into 'filename'. + If `world` is set to True then the node will be exported in the world coordinate system + (equivalent to hardening the transform before exporting). + + This method is different from saveNode in that it does not modify any existing storage node + and therefore does not change the filename or filetype that is used when saving the scene. + """ + from slicer import app, vtkDataFileFormatHelper, vtkMRMLMessageCollection + nodeIDs = [node.GetID()] + fileNames = [filename] + hardenTransform = world + + if "fileFormat" not in properties: + foundFileFormat = None + currentExtension = app.coreIOManager().extractKnownExtension(filename, node) + fileWriterExtensions = app.coreIOManager().fileWriterExtensions(node) + for fileFormat in fileWriterExtensions: + extension = vtkDataFileFormatHelper.GetFileExtensionFromFormatString(fileFormat) + if extension == currentExtension: + foundFileFormat = fileFormat + break + if not foundFileFormat: + raise ValueError(f"Failed to export {node.GetID()} - no known file format was found for filename {filename}") + properties["fileFormat"] = foundFileFormat + + userMessages = vtkMRMLMessageCollection() + success = app.coreIOManager().exportNodes(nodeIDs, fileNames, properties, hardenTransform, userMessages) + + if not success: + import logging + errorMessage = f"Failed to export node to file: {filename}" + if userMessages.GetNumberOfMessages() > 0: + errorMessage += "\n" + userMessages.GetAllMessagesAsString() + logging.error(errorMessage) + + return success # # Module # + def moduleSelector(): - """Return module selector widget. + """Return module selector widget. - :return: module widget object - :raises RuntimeError: if there is no module selector (for example, the application runs without a main window). - """ - mw = mainWindow() - if not mw: - raise RuntimeError("Could not find main window") - return mw.moduleSelector() + :return: module widget object + :raises RuntimeError: if there is no module selector (for example, the application runs without a main window). + """ + mw = mainWindow() + if not mw: + raise RuntimeError("Could not find main window") + return mw.moduleSelector() def selectModule(module): - """Set currently active module. + """Set currently active module. - Throws a RuntimeError exception in case of failure (no such module or the application runs without a main window). - :param module: module name or object - :raises RuntimeError: in case of failure - """ - moduleName = module - if not isinstance(module, str): - moduleName = module.name - selector = moduleSelector() - if not selector: - raise RuntimeError("Could not find moduleSelector in the main window") - moduleSelector().selectModule(moduleName) + Throws a RuntimeError exception in case of failure (no such module or the application runs without a main window). + :param module: module name or object + :raises RuntimeError: in case of failure + """ + moduleName = module + if not isinstance(module, str): + moduleName = module.name + selector = moduleSelector() + if not selector: + raise RuntimeError("Could not find moduleSelector in the main window") + moduleSelector().selectModule(moduleName) def selectedModule(): - """Return currently active module. + """Return currently active module. - :return: module object - :raises RuntimeError: in case of failure (no such module or the application runs without a main window). - """ - selector = moduleSelector() - if not selector: - raise RuntimeError("Could not find moduleSelector in the main window") - return selector.selectedModule + :return: module object + :raises RuntimeError: in case of failure (no such module or the application runs without a main window). + """ + selector = moduleSelector() + if not selector: + raise RuntimeError("Could not find moduleSelector in the main window") + return selector.selectedModule def moduleNames(): - """Get list containing name of all successfully loaded modules. + """Get list containing name of all successfully loaded modules. - :return: list of module names - """ - from slicer import app - return app.moduleManager().factoryManager().loadedModuleNames() + :return: list of module names + """ + from slicer import app + return app.moduleManager().factoryManager().loadedModuleNames() def getModule(moduleName): - """Get module object from module name. + """Get module object from module name. - :return: module object - :raises RuntimeError: in case of failure (no such module). - """ - from slicer import app - module = app.moduleManager().module(moduleName) - if not module: - raise RuntimeError("Could not find module with name '%s'" % moduleName) - return module + :return: module object + :raises RuntimeError: in case of failure (no such module). + """ + from slicer import app + module = app.moduleManager().module(moduleName) + if not module: + raise RuntimeError("Could not find module with name '%s'" % moduleName) + return module def getModuleGui(module): - """Get module widget. + """Get module widget. - .. deprecated:: 4.13.0 - Use the universal :func:`getModuleWidget` function instead. - """ - return getModuleWidget(module) + .. deprecated:: 4.13.0 + Use the universal :func:`getModuleWidget` function instead. + """ + return getModuleWidget(module) def getNewModuleGui(module): - """Create new module widget. + """Create new module widget. - .. deprecated:: 4.13.0 - Use the universal :func:`getNewModuleWidget` function instead. - """ - return getNewModuleWidget(module) + .. deprecated:: 4.13.0 + Use the universal :func:`getNewModuleWidget` function instead. + """ + return getNewModuleWidget(module) def getModuleWidget(module): - """Return module widget (user interface) object for a module. + """Return module widget (user interface) object for a module. - :param module: module name or module object - :return: module widget object - :raises RuntimeError: if the module does not have widget. - """ - import slicer - if isinstance(module, str): - module = getModule(module) - widgetRepr = module.widgetRepresentation() - if not widgetRepr: - raise RuntimeError("Could not find module widget representation with name '%s'" % module.name) - if isinstance(widgetRepr, slicer.qSlicerScriptedLoadableModuleWidget): - # Scripted module, return the Python class - return widgetRepr.self() - else: - # C++ module - return widgetRepr + :param module: module name or module object + :return: module widget object + :raises RuntimeError: if the module does not have widget. + """ + import slicer + if isinstance(module, str): + module = getModule(module) + widgetRepr = module.widgetRepresentation() + if not widgetRepr: + raise RuntimeError("Could not find module widget representation with name '%s'" % module.name) + if isinstance(widgetRepr, slicer.qSlicerScriptedLoadableModuleWidget): + # Scripted module, return the Python class + return widgetRepr.self() + else: + # C++ module + return widgetRepr def getNewModuleWidget(module): - """Create new module widget instance. + """Create new module widget instance. - In general, not recommended, as module widget may be developed expecting that there is only a single - instance of this widget. Instead, of instantiating a complete module GUI, it is recommended to create - only selected widgets that are used in the module GUI. + In general, not recommended, as module widget may be developed expecting that there is only a single + instance of this widget. Instead, of instantiating a complete module GUI, it is recommended to create + only selected widgets that are used in the module GUI. - :param module: module name or module object - :return: module widget object - :raises RuntimeError: if the module does not have widget. - """ - import slicer - if isinstance(module, str): - module = getModule(module) - widgetRepr = module.createNewWidgetRepresentation() - if not widgetRepr: - raise RuntimeError("Could not find module widget representation with name '%s'" % module.name) - if isinstance(widgetRepr, slicer.qSlicerScriptedLoadableModuleWidget): - # Scripted module, return the Python class - return widgetRepr.self() - else: - # C++ module - return widgetRepr + :param module: module name or module object + :return: module widget object + :raises RuntimeError: if the module does not have widget. + """ + import slicer + if isinstance(module, str): + module = getModule(module) + widgetRepr = module.createNewWidgetRepresentation() + if not widgetRepr: + raise RuntimeError("Could not find module widget representation with name '%s'" % module.name) + if isinstance(widgetRepr, slicer.qSlicerScriptedLoadableModuleWidget): + # Scripted module, return the Python class + return widgetRepr.self() + else: + # C++ module + return widgetRepr def getModuleLogic(module): - """Get module logic object. + """Get module logic object. - Module logic allows a module to use features offered by another module. + Module logic allows a module to use features offered by another module. - :param module: module name or module object - :return: module logic object - :raises RuntimeError: if the module does not have widget. - """ - import slicer - if isinstance(module, str): - module = getModule(module) - if isinstance(module, slicer.qSlicerScriptedLoadableModule): - try: - logic = getModuleGui(module).logic - except AttributeError: - # This widget does not have a logic instance in its widget class - logic = None - else: - logic = module.logic() - if not logic: - raise RuntimeError("Could not find module widget representation with name '%s'" % module.name) - return logic + :param module: module name or module object + :return: module logic object + :raises RuntimeError: if the module does not have widget. + """ + import slicer + if isinstance(module, str): + module = getModule(module) + if isinstance(module, slicer.qSlicerScriptedLoadableModule): + try: + logic = getModuleGui(module).logic + except AttributeError: + # This widget does not have a logic instance in its widget class + logic = None + else: + logic = module.logic() + if not logic: + raise RuntimeError("Could not find module widget representation with name '%s'" % module.name) + return logic def modulePath(moduleName): - """Get module logic object. + """Get module logic object. - Module logic allows a module to use features offered by another module. - Throws a RuntimeError exception if the module does not have widget. - :param moduleName: module name - :return: file path of the module - """ - import slicer # noqa: F401 - return eval('slicer.modules.%s.path' % moduleName.lower()) + Module logic allows a module to use features offered by another module. + Throws a RuntimeError exception if the module does not have widget. + :param moduleName: module name + :return: file path of the module + """ + import slicer # noqa: F401 + return eval('slicer.modules.%s.path' % moduleName.lower()) def reloadScriptedModule(moduleName): - """Generic reload method for any scripted module. + """Generic reload method for any scripted module. - The function performs the following: + The function performs the following: - * Ensure ``sys.path`` includes the module path and use ``imp.load_module`` - to load the associated script. - * For the current module widget representation: + * Ensure ``sys.path`` includes the module path and use ``imp.load_module`` + to load the associated script. + * For the current module widget representation: - * Hide all children widgets - * Call ``cleanup()`` function and disconnect ``ScriptedLoadableModuleWidget_onModuleAboutToBeUnloaded`` - * Remove layout items + * Hide all children widgets + * Call ``cleanup()`` function and disconnect ``ScriptedLoadableModuleWidget_onModuleAboutToBeUnloaded`` + * Remove layout items - * Instantiate new widget representation - * Call ``setup()`` function - * Update ``slicer.modules.Widget`` attribute - """ - import imp, sys, os - import slicer + * Instantiate new widget representation + * Call ``setup()`` function + * Update ``slicer.modules.Widget`` attribute + """ + import imp, sys, os + import slicer - widgetName = moduleName + "Widget" + widgetName = moduleName + "Widget" - # reload the source code - filePath = modulePath(moduleName) - p = os.path.dirname(filePath) + # reload the source code + filePath = modulePath(moduleName) + p = os.path.dirname(filePath) - if not p in sys.path: - sys.path.insert(0,p) + if p not in sys.path: + sys.path.insert(0, p) - with open(filePath, encoding='utf8') as fp: - reloaded_module = imp.load_module( - moduleName, fp, filePath, ('.py', 'r', imp.PY_SOURCE)) + with open(filePath, encoding='utf8') as fp: + reloaded_module = imp.load_module( + moduleName, fp, filePath, ('.py', 'r', imp.PY_SOURCE)) - # find and hide the existing widget - parent = eval('slicer.modules.%s.widgetRepresentation()' % moduleName.lower()) - for child in parent.children(): - try: - child.hide() - except AttributeError: - pass + # find and hide the existing widget + parent = eval('slicer.modules.%s.widgetRepresentation()' % moduleName.lower()) + for child in parent.children(): + try: + child.hide() + except AttributeError: + pass - # if the module widget has been instantiated, call cleanup function and - # disconnect "_onModuleAboutToBeUnloaded" (this avoids double-cleanup on - # application exit) - if hasattr(slicer.modules, widgetName): - widget = getattr(slicer.modules, widgetName) - widget.cleanup() + # if the module widget has been instantiated, call cleanup function and + # disconnect "_onModuleAboutToBeUnloaded" (this avoids double-cleanup on + # application exit) + if hasattr(slicer.modules, widgetName): + widget = getattr(slicer.modules, widgetName) + widget.cleanup() - if hasattr(widget, '_onModuleAboutToBeUnloaded'): - slicer.app.moduleManager().disconnect('moduleAboutToBeUnloaded(QString)', widget._onModuleAboutToBeUnloaded) + if hasattr(widget, '_onModuleAboutToBeUnloaded'): + slicer.app.moduleManager().disconnect('moduleAboutToBeUnloaded(QString)', widget._onModuleAboutToBeUnloaded) - # remove layout items (remaining spacer items would add space above the widget) - items = [] - for itemIndex in range(parent.layout().count()): - items.append(parent.layout().itemAt(itemIndex)) - for item in items: - parent.layout().removeItem(item) + # remove layout items (remaining spacer items would add space above the widget) + items = [] + for itemIndex in range(parent.layout().count()): + items.append(parent.layout().itemAt(itemIndex)) + for item in items: + parent.layout().removeItem(item) - # create new widget inside existing parent - widget = eval('reloaded_module.%s(parent)' % widgetName) - widget.setup() - setattr(slicer.modules, widgetName, widget) + # create new widget inside existing parent + widget = eval('reloaded_module.%s(parent)' % widgetName) + widget.setup() + setattr(slicer.modules, widgetName, widget) - return reloaded_module + return reloaded_module def setModulePanelTitleVisible(visible): - """Show/hide module panel title bar at the top of module panel. + """Show/hide module panel title bar at the top of module panel. - If the title bar is not visible then it is not possible to drag and dock the - module panel to a different location. + If the title bar is not visible then it is not possible to drag and dock the + module panel to a different location. - If there is no main window then the function has no effect. - """ - mw = mainWindow() - if mw is None: - return - modulePanelDockWidget = mw.findChildren('QDockWidget','PanelDockWidget')[0] - if visible: - modulePanelDockWidget.setTitleBarWidget(None) - else: - import qt - modulePanelDockWidget.setTitleBarWidget(qt.QWidget(modulePanelDockWidget)) + If there is no main window then the function has no effect. + """ + mw = mainWindow() + if mw is None: + return + modulePanelDockWidget = mw.findChildren('QDockWidget', 'PanelDockWidget')[0] + if visible: + modulePanelDockWidget.setTitleBarWidget(None) + else: + import qt + modulePanelDockWidget.setTitleBarWidget(qt.QWidget(modulePanelDockWidget)) def setApplicationLogoVisible(visible=True, scaleFactor=None, icon=None): - """Customize appearance of the application logo at the top of module panel. + """Customize appearance of the application logo at the top of module panel. - :param visible: if True then the logo is displayed, otherwise the area is left empty. - :param scaleFactor: specifies the displayed size of the icon. 1.0 means original size, larger value means larger displayed size. - :param icon: a qt.QIcon object specifying what icon to display as application logo. + :param visible: if True then the logo is displayed, otherwise the area is left empty. + :param scaleFactor: specifies the displayed size of the icon. 1.0 means original size, larger value means larger displayed size. + :param icon: a qt.QIcon object specifying what icon to display as application logo. - If there is no main window then the function has no effect. - """ + If there is no main window then the function has no effect. + """ - mw = mainWindow() - if mw is None: - return - logoLabel = findChild(mw, "LogoLabel") + mw = mainWindow() + if mw is None: + return + logoLabel = findChild(mw, "LogoLabel") - if icon is not None or scaleFactor is not None: - if icon is None: - import qt - icon = qt.QIcon(":/ModulePanelLogo.png") - if scaleFactor is None: - scaleFactor = 1.0 - logo = icon.pixmap(icon.availableSizes()[0] * scaleFactor) - logoLabel.setPixmap(logo) + if icon is not None or scaleFactor is not None: + if icon is None: + import qt + icon = qt.QIcon(":/ModulePanelLogo.png") + if scaleFactor is None: + scaleFactor = 1.0 + logo = icon.pixmap(icon.availableSizes()[0] * scaleFactor) + logoLabel.setPixmap(logo) - logoLabel.setVisible(visible) + logoLabel.setVisible(visible) def setModuleHelpSectionVisible(visible): - """Show/hide Help section at the top of module panel. + """Show/hide Help section at the top of module panel. - If there is no main window then the function has no effect. - """ - mw = mainWindow() - if mw is None: - return - modulePanel = findChild(mw, "ModulePanel") - modulePanel.helpAndAcknowledgmentVisible = visible + If there is no main window then the function has no effect. + """ + mw = mainWindow() + if mw is None: + return + modulePanel = findChild(mw, "ModulePanel") + modulePanel.helpAndAcknowledgmentVisible = visible def setDataProbeVisible(visible): - """Show/hide Data probe at the bottom of module panel. + """Show/hide Data probe at the bottom of module panel. - If there is no main window then the function has no effect. - """ - mw = mainWindow() - if mw is None: - return - widget = findChild(mw, "DataProbeCollapsibleWidget") - widget.setVisible(visible) + If there is no main window then the function has no effect. + """ + mw = mainWindow() + if mw is None: + return + widget = findChild(mw, "DataProbeCollapsibleWidget") + widget.setVisible(visible) # @@ -1374,15 +1412,15 @@ def setDataProbeVisible(visible): # def resetThreeDViews(): - """Reset focal view around volumes""" - import slicer - slicer.app.layoutManager().resetThreeDViews() + """Reset focal view around volumes""" + import slicer + slicer.app.layoutManager().resetThreeDViews() def resetSliceViews(): - """Reset focal view around volumes""" - import slicer - manager = slicer.app.layoutManager().resetSliceViews() + """Reset focal view around volumes""" + import slicer + manager = slicer.app.layoutManager().resetSliceViews() # @@ -1390,1003 +1428,1003 @@ def resetSliceViews(): # class MRMLNodeNotFoundException(Exception): - """Exception raised when a requested MRML node was not found.""" - pass + """Exception raised when a requested MRML node was not found.""" + pass def getNodes(pattern="*", scene=None, useLists=False): - """Return a dictionary of nodes where the name or id matches the ``pattern``. + """Return a dictionary of nodes where the name or id matches the ``pattern``. - By default, ``pattern`` is a wildcard and it returns all nodes associated - with ``slicer.mrmlScene``. + By default, ``pattern`` is a wildcard and it returns all nodes associated + with ``slicer.mrmlScene``. - If multiple node share the same name, using ``useLists=False`` (default behavior) - returns only the last node with that name. If ``useLists=True``, it returns - a dictionary of lists of nodes. - """ - import slicer, collections, fnmatch - nodes = collections.OrderedDict() - if scene is None: - scene = slicer.mrmlScene - count = scene.GetNumberOfNodes() - for idx in range(count): - node = scene.GetNthNode(idx) - name = node.GetName() - id = node.GetID() - if (fnmatch.fnmatchcase(name, pattern) or - fnmatch.fnmatchcase(id, pattern)): - if useLists: - nodes.setdefault(node.GetName(), []).append(node) - else: - nodes[node.GetName()] = node - return nodes + If multiple node share the same name, using ``useLists=False`` (default behavior) + returns only the last node with that name. If ``useLists=True``, it returns + a dictionary of lists of nodes. + """ + import slicer, collections, fnmatch + nodes = collections.OrderedDict() + if scene is None: + scene = slicer.mrmlScene + count = scene.GetNumberOfNodes() + for idx in range(count): + node = scene.GetNthNode(idx) + name = node.GetName() + id = node.GetID() + if (fnmatch.fnmatchcase(name, pattern) or + fnmatch.fnmatchcase(id, pattern)): + if useLists: + nodes.setdefault(node.GetName(), []).append(node) + else: + nodes[node.GetName()] = node + return nodes def getNode(pattern="*", index=0, scene=None): - """Return the indexth node where name or id matches ``pattern``. + """Return the indexth node where name or id matches ``pattern``. - By default, ``pattern`` is a wildcard and it returns the first node - associated with ``slicer.mrmlScene``. + By default, ``pattern`` is a wildcard and it returns the first node + associated with ``slicer.mrmlScene``. - :raises MRMLNodeNotFoundException: if no node is found - that matches the specified pattern. - """ - nodes = getNodes(pattern, scene) - if not nodes: - raise MRMLNodeNotFoundException("could not find nodes in the scene by name or id '%s'" % (pattern if (isinstance(pattern, str)) else "")) - return list(nodes.values())[index] + :raises MRMLNodeNotFoundException: if no node is found + that matches the specified pattern. + """ + nodes = getNodes(pattern, scene) + if not nodes: + raise MRMLNodeNotFoundException("could not find nodes in the scene by name or id '%s'" % (pattern if (isinstance(pattern, str)) else "")) + return list(nodes.values())[index] def getNodesByClass(className, scene=None): - """Return all nodes in the scene of the specified class.""" - import slicer - if scene is None: - scene = slicer.mrmlScene - nodes = slicer.mrmlScene.GetNodesByClass(className) - nodes.UnRegister(slicer.mrmlScene) - nodeList = [] - nodes.InitTraversal() - node = nodes.GetNextItemAsObject() - while node: - nodeList.append(node) + """Return all nodes in the scene of the specified class.""" + import slicer + if scene is None: + scene = slicer.mrmlScene + nodes = slicer.mrmlScene.GetNodesByClass(className) + nodes.UnRegister(slicer.mrmlScene) + nodeList = [] + nodes.InitTraversal() node = nodes.GetNextItemAsObject() - return nodeList + while node: + nodeList.append(node) + node = nodes.GetNextItemAsObject() + return nodeList def getFirstNodeByClassByName(className, name, scene=None): - """Return the first node in the scene that matches the specified node name and node class.""" - import slicer - if scene is None: - scene = slicer.mrmlScene - return scene.GetFirstNode(name, className) + """Return the first node in the scene that matches the specified node name and node class.""" + import slicer + if scene is None: + scene = slicer.mrmlScene + return scene.GetFirstNode(name, className) def getFirstNodeByName(name, className=None): - """Get the first MRML node that name starts with the specified name. + """Get the first MRML node that name starts with the specified name. - Optionally specify a classname that must also match. - """ - import slicer - scene = slicer.mrmlScene - return scene.GetFirstNode(name, className, False, False) + Optionally specify a classname that must also match. + """ + import slicer + scene = slicer.mrmlScene + return scene.GetFirstNode(name, className, False, False) class NodeModify: - """Context manager to conveniently compress mrml node modified event.""" + """Context manager to conveniently compress mrml node modified event.""" - def __init__(self, node): - self.node = node + def __init__(self, node): + self.node = node - def __enter__(self): - self.wasModifying = self.node.StartModify() - return self.node + def __enter__(self): + self.wasModifying = self.node.StartModify() + return self.node - def __exit__(self, type, value, traceback): - self.node.EndModify(self.wasModifying) + def __exit__(self, type, value, traceback): + self.node.EndModify(self.wasModifying) class RenderBlocker: - """ - Context manager to conveniently pause and resume view rendering. This makes sure that we are not displaying incomplete states to the user. - Pausing the views can be useful to improve performance and ensure consistency by skipping all rendering calls until the current code block has completed. + """ + Context manager to conveniently pause and resume view rendering. This makes sure that we are not displaying incomplete states to the user. + Pausing the views can be useful to improve performance and ensure consistency by skipping all rendering calls until the current code block has completed. - Code blocks such as:: + Code blocks such as:: - try: - slicer.app.pauseRender() - # Do things - finally: - slicer.app.resumeRender() + try: + slicer.app.pauseRender() + # Do things + finally: + slicer.app.resumeRender() - Can be written as:: + Can be written as:: - with slicer.util.RenderBlocker(): - # Do things + with slicer.util.RenderBlocker(): + # Do things -""" + """ - def __enter__(self): - import slicer - slicer.app.pauseRender() + def __enter__(self): + import slicer + slicer.app.pauseRender() - def __exit__(self, type, value, traceback): - import slicer - slicer.app.resumeRender() + def __exit__(self, type, value, traceback): + import slicer + slicer.app.resumeRender() # # Subject hierarchy # def getSubjectHierarchyItemChildren(parentItem=None, recursive=False): - """Convenience method to get children of a subject hierarchy item. + """Convenience method to get children of a subject hierarchy item. - :param vtkIdType parentItem: Item for which to get children for. If omitted - or None then use scene item (i.e. get all items) - :param bool recursive: Whether the query is recursive. False by default - :return: List of child item IDs - """ - import slicer, vtk - children = [] - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - # Use scene as parent item if not given - if not parentItem: - parentItem = shNode.GetSceneItemID() - childrenIdList = vtk.vtkIdList() - shNode.GetItemChildren(parentItem, childrenIdList, recursive) - for childIndex in range(childrenIdList.GetNumberOfIds()): - children.append(childrenIdList.GetId(childIndex)) - return children + :param vtkIdType parentItem: Item for which to get children for. If omitted + or None then use scene item (i.e. get all items) + :param bool recursive: Whether the query is recursive. False by default + :return: List of child item IDs + """ + import slicer, vtk + children = [] + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + # Use scene as parent item if not given + if not parentItem: + parentItem = shNode.GetSceneItemID() + childrenIdList = vtk.vtkIdList() + shNode.GetItemChildren(parentItem, childrenIdList, recursive) + for childIndex in range(childrenIdList.GetNumberOfIds()): + children.append(childrenIdList.GetId(childIndex)) + return children # # MRML-numpy # -def array(pattern = "", index = 0): - """Return the array you are "most likely to want" from the indexth +def array(pattern="", index=0): + """Return the array you are "most likely to want" from the indexth - MRML node that matches the pattern. + MRML node that matches the pattern. - :raises RuntimeError: if the node cannot be accessed as an array. + :raises RuntimeError: if the node cannot be accessed as an array. - .. warning:: + .. warning:: - Meant to be used in the python console for quick debugging/testing. + Meant to be used in the python console for quick debugging/testing. - More specific API should be used in scripts to be sure you get exactly - what you want, such as :py:meth:`arrayFromVolume`, :py:meth:`arrayFromModelPoints`, - and :py:meth:`arrayFromGridTransform`. - """ - node = getNode(pattern=pattern, index=index) - import slicer - if isinstance(node, slicer.vtkMRMLVolumeNode): - return arrayFromVolume(node) - elif isinstance(node, slicer.vtkMRMLModelNode): - return arrayFromModelPoints(node) - elif isinstance(node, slicer.vtkMRMLGridTransformNode): - return arrayFromGridTransform(node) - elif isinstance(node, slicer.vtkMRMLMarkupsNode): - return arrayFromMarkupsControlPoints(node) - elif isinstance(node, slicer.vtkMRMLTransformNode): - return arrayFromTransformMatrix(node) - - # TODO: accessors for other node types: polydata (verts, polys...), colors - raise RuntimeError("Cannot get node "+node.GetID()+" as array") + More specific API should be used in scripts to be sure you get exactly + what you want, such as :py:meth:`arrayFromVolume`, :py:meth:`arrayFromModelPoints`, + and :py:meth:`arrayFromGridTransform`. + """ + node = getNode(pattern=pattern, index=index) + import slicer + if isinstance(node, slicer.vtkMRMLVolumeNode): + return arrayFromVolume(node) + elif isinstance(node, slicer.vtkMRMLModelNode): + return arrayFromModelPoints(node) + elif isinstance(node, slicer.vtkMRMLGridTransformNode): + return arrayFromGridTransform(node) + elif isinstance(node, slicer.vtkMRMLMarkupsNode): + return arrayFromMarkupsControlPoints(node) + elif isinstance(node, slicer.vtkMRMLTransformNode): + return arrayFromTransformMatrix(node) + + # TODO: accessors for other node types: polydata (verts, polys...), colors + raise RuntimeError("Cannot get node " + node.GetID() + " as array") def arrayFromVolume(volumeNode): - """Return voxel array from volume node as numpy array. + """Return voxel array from volume node as numpy array. - Voxels values are not copied. Voxel values in the volume node can be modified - by changing values in the numpy array. - After all modifications has been completed, call :py:meth:`arrayFromVolumeModified`. + Voxels values are not copied. Voxel values in the volume node can be modified + by changing values in the numpy array. + After all modifications has been completed, call :py:meth:`arrayFromVolumeModified`. - :raises RuntimeError: in case of failure + :raises RuntimeError: in case of failure - .. warning:: Memory area of the returned array is managed by VTK, therefore - values in the array may be changed, but the array must not be reallocated - (change array size, shallow-copy content from other array most likely causes - application crash). To allow arbitrary numpy operations on a volume array: + .. warning:: Memory area of the returned array is managed by VTK, therefore + values in the array may be changed, but the array must not be reallocated + (change array size, shallow-copy content from other array most likely causes + application crash). To allow arbitrary numpy operations on a volume array: - 1. Make a deep-copy of the returned VTK-managed array using :func:`numpy.copy`. - 2. Perform any computations using the copied array. - 3. Write results back to the image data using :py:meth:`updateVolumeFromArray`. - """ - scalarTypes = ['vtkMRMLScalarVolumeNode', 'vtkMRMLLabelMapVolumeNode'] - vectorTypes = ['vtkMRMLVectorVolumeNode', 'vtkMRMLMultiVolumeNode', 'vtkMRMLDiffusionWeightedVolumeNode'] - tensorTypes = ['vtkMRMLDiffusionTensorVolumeNode'] - vimage = volumeNode.GetImageData() - nshape = tuple(reversed(volumeNode.GetImageData().GetDimensions())) - import vtk.util.numpy_support - narray = None - if volumeNode.GetClassName() in scalarTypes: - narray = vtk.util.numpy_support.vtk_to_numpy(vimage.GetPointData().GetScalars()).reshape(nshape) - elif volumeNode.GetClassName() in vectorTypes: - components = vimage.GetNumberOfScalarComponents() - if components > 1: - nshape = nshape + (components,) - narray = vtk.util.numpy_support.vtk_to_numpy(vimage.GetPointData().GetScalars()).reshape(nshape) - elif volumeNode.GetClassName() in tensorTypes: - narray = vtk.util.numpy_support.vtk_to_numpy(vimage.GetPointData().GetTensors()).reshape(nshape+(3,3)) - else: - raise RuntimeError("Unsupported volume type: "+volumeNode.GetClassName()) - return narray + 1. Make a deep-copy of the returned VTK-managed array using :func:`numpy.copy`. + 2. Perform any computations using the copied array. + 3. Write results back to the image data using :py:meth:`updateVolumeFromArray`. + """ + scalarTypes = ['vtkMRMLScalarVolumeNode', 'vtkMRMLLabelMapVolumeNode'] + vectorTypes = ['vtkMRMLVectorVolumeNode', 'vtkMRMLMultiVolumeNode', 'vtkMRMLDiffusionWeightedVolumeNode'] + tensorTypes = ['vtkMRMLDiffusionTensorVolumeNode'] + vimage = volumeNode.GetImageData() + nshape = tuple(reversed(volumeNode.GetImageData().GetDimensions())) + import vtk.util.numpy_support + narray = None + if volumeNode.GetClassName() in scalarTypes: + narray = vtk.util.numpy_support.vtk_to_numpy(vimage.GetPointData().GetScalars()).reshape(nshape) + elif volumeNode.GetClassName() in vectorTypes: + components = vimage.GetNumberOfScalarComponents() + if components > 1: + nshape = nshape + (components,) + narray = vtk.util.numpy_support.vtk_to_numpy(vimage.GetPointData().GetScalars()).reshape(nshape) + elif volumeNode.GetClassName() in tensorTypes: + narray = vtk.util.numpy_support.vtk_to_numpy(vimage.GetPointData().GetTensors()).reshape(nshape + (3, 3)) + else: + raise RuntimeError("Unsupported volume type: " + volumeNode.GetClassName()) + return narray def arrayFromVolumeModified(volumeNode): - """Indicate that modification of a numpy array returned by :py:meth:`arrayFromVolume` has been completed.""" - imageData = volumeNode.GetImageData() - pointData = imageData.GetPointData() if imageData else None - if pointData: - if pointData.GetScalars(): - pointData.GetScalars().Modified() - if pointData.GetTensors(): - pointData.GetTensors().Modified() - volumeNode.Modified() + """Indicate that modification of a numpy array returned by :py:meth:`arrayFromVolume` has been completed.""" + imageData = volumeNode.GetImageData() + pointData = imageData.GetPointData() if imageData else None + if pointData: + if pointData.GetScalars(): + pointData.GetScalars().Modified() + if pointData.GetTensors(): + pointData.GetTensors().Modified() + volumeNode.Modified() def arrayFromModelPoints(modelNode): - """Return point positions of a model node as numpy array. + """Return point positions of a model node as numpy array. - Point coordinates can be modified by modifying the numpy array. - After all modifications has been completed, call :py:meth:`arrayFromModelPointsModified`. + Point coordinates can be modified by modifying the numpy array. + After all modifications has been completed, call :py:meth:`arrayFromModelPointsModified`. - .. warning:: Important: memory area of the returned array is managed by VTK, - therefore values in the array may be changed, but the array must not be reallocated. - See :py:meth:`arrayFromVolume` for details. - """ - import vtk.util.numpy_support - pointData = modelNode.GetMesh().GetPoints().GetData() - narray = vtk.util.numpy_support.vtk_to_numpy(pointData) - return narray + .. warning:: Important: memory area of the returned array is managed by VTK, + therefore values in the array may be changed, but the array must not be reallocated. + See :py:meth:`arrayFromVolume` for details. + """ + import vtk.util.numpy_support + pointData = modelNode.GetMesh().GetPoints().GetData() + narray = vtk.util.numpy_support.vtk_to_numpy(pointData) + return narray def arrayFromModelPointsModified(modelNode): - """Indicate that modification of a numpy array returned by :py:meth:`arrayFromModelPoints` has been completed.""" - if modelNode.GetMesh(): - modelNode.GetMesh().GetPoints().GetData().Modified() - # Trigger re-render - modelNode.GetDisplayNode().Modified() + """Indicate that modification of a numpy array returned by :py:meth:`arrayFromModelPoints` has been completed.""" + if modelNode.GetMesh(): + modelNode.GetMesh().GetPoints().GetData().Modified() + # Trigger re-render + modelNode.GetDisplayNode().Modified() def _vtkArrayFromModelData(modelNode, arrayName, location): - """Helper function for getting VTK point data array that throws exception + """Helper function for getting VTK point data array that throws exception - with informative error message if the data array is not found. - Point or cell data can be selected by setting 'location' argument to 'point' or 'cell'. + with informative error message if the data array is not found. + Point or cell data can be selected by setting 'location' argument to 'point' or 'cell'. - :raises ValueError: in case of failure - """ - if location=='point': - modelData = modelNode.GetMesh().GetPointData() - elif location=='cell': - modelData = modelNode.GetMesh().GetCellData() - else: - raise ValueError("Location attribute must be set to 'point' or 'cell'") - if not modelData or modelData.GetNumberOfArrays() == 0: - raise ValueError(f"Input modelNode does not contain {location} data") - arrayVtk = modelData.GetArray(arrayName) - if not arrayVtk: - availableArrayNames = [modelData.GetArrayName(i) for i in range(modelData.GetNumberOfArrays())] - raise ValueError("Input modelNode does not contain {} data array '{}'. Available array names: '{}'".format( - location, arrayName, "', '".join(availableArrayNames))) - return arrayVtk + :raises ValueError: in case of failure + """ + if location == 'point': + modelData = modelNode.GetMesh().GetPointData() + elif location == 'cell': + modelData = modelNode.GetMesh().GetCellData() + else: + raise ValueError("Location attribute must be set to 'point' or 'cell'") + if not modelData or modelData.GetNumberOfArrays() == 0: + raise ValueError(f"Input modelNode does not contain {location} data") + arrayVtk = modelData.GetArray(arrayName) + if not arrayVtk: + availableArrayNames = [modelData.GetArrayName(i) for i in range(modelData.GetNumberOfArrays())] + raise ValueError("Input modelNode does not contain {} data array '{}'. Available array names: '{}'".format( + location, arrayName, "', '".join(availableArrayNames))) + return arrayVtk def arrayFromModelPointData(modelNode, arrayName): - """Return point data array of a model node as numpy array. + """Return point data array of a model node as numpy array. - .. warning:: Important: memory area of the returned array is managed by VTK, - therefore values in the array may be changed, but the array must not be reallocated. - See :py:meth:`arrayFromVolume` for details. - """ - import vtk.util.numpy_support - arrayVtk = _vtkArrayFromModelData(modelNode, arrayName, 'point') - narray = vtk.util.numpy_support.vtk_to_numpy(arrayVtk) - return narray + .. warning:: Important: memory area of the returned array is managed by VTK, + therefore values in the array may be changed, but the array must not be reallocated. + See :py:meth:`arrayFromVolume` for details. + """ + import vtk.util.numpy_support + arrayVtk = _vtkArrayFromModelData(modelNode, arrayName, 'point') + narray = vtk.util.numpy_support.vtk_to_numpy(arrayVtk) + return narray def arrayFromModelPointDataModified(modelNode, arrayName): - """Indicate that modification of a numpy array returned by :py:meth:`arrayFromModelPointData` has been completed.""" - arrayVtk = _vtkArrayFromModelData(modelNode, arrayName, 'point') - arrayVtk.Modified() + """Indicate that modification of a numpy array returned by :py:meth:`arrayFromModelPointData` has been completed.""" + arrayVtk = _vtkArrayFromModelData(modelNode, arrayName, 'point') + arrayVtk.Modified() def arrayFromModelCellData(modelNode, arrayName): - """Return cell data array of a model node as numpy array. + """Return cell data array of a model node as numpy array. - .. warning:: Important: memory area of the returned array is managed by VTK, - therefore values in the array may be changed, but the array must not be reallocated. - See :py:meth:`arrayFromVolume` for details. - """ - import vtk.util.numpy_support - arrayVtk = _vtkArrayFromModelData(modelNode, arrayName, 'cell') - narray = vtk.util.numpy_support.vtk_to_numpy(arrayVtk) - return narray + .. warning:: Important: memory area of the returned array is managed by VTK, + therefore values in the array may be changed, but the array must not be reallocated. + See :py:meth:`arrayFromVolume` for details. + """ + import vtk.util.numpy_support + arrayVtk = _vtkArrayFromModelData(modelNode, arrayName, 'cell') + narray = vtk.util.numpy_support.vtk_to_numpy(arrayVtk) + return narray def arrayFromModelCellDataModified(modelNode, arrayName): - """Indicate that modification of a numpy array returned by :py:meth:`arrayFromModelCellData` has been completed.""" - arrayVtk = _vtkArrayFromModelData(modelNode, arrayName, 'cell') - arrayVtk.Modified() + """Indicate that modification of a numpy array returned by :py:meth:`arrayFromModelCellData` has been completed.""" + arrayVtk = _vtkArrayFromModelData(modelNode, arrayName, 'cell') + arrayVtk.Modified() def arrayFromMarkupsControlPointData(markupsNode, arrayName): - """Return control point data array of a markups node as numpy array. + """Return control point data array of a markups node as numpy array. - .. warning:: Important: memory area of the returned array is managed by VTK, - therefore values in the array may be changed, but the array must not be reallocated. - See :py:meth:`arrayFromVolume` for details. - """ - import vtk.util.numpy_support - for measurementIndex in range(markupsNode.GetNumberOfMeasurements()): - measurement = markupsNode.GetNthMeasurement(measurementIndex) - doubleArrayVtk = measurement.GetControlPointValues() - if doubleArrayVtk and doubleArrayVtk.GetName() == arrayName: - narray = vtk.util.numpy_support.vtk_to_numpy(doubleArrayVtk) - return narray + .. warning:: Important: memory area of the returned array is managed by VTK, + therefore values in the array may be changed, but the array must not be reallocated. + See :py:meth:`arrayFromVolume` for details. + """ + import vtk.util.numpy_support + for measurementIndex in range(markupsNode.GetNumberOfMeasurements()): + measurement = markupsNode.GetNthMeasurement(measurementIndex) + doubleArrayVtk = measurement.GetControlPointValues() + if doubleArrayVtk and doubleArrayVtk.GetName() == arrayName: + narray = vtk.util.numpy_support.vtk_to_numpy(doubleArrayVtk) + return narray def arrayFromMarkupsControlPointDataModified(markupsNode, arrayName): - """Indicate that modification of a numpy array returned by :py:meth:`arrayFromMarkupsControlPointData` has been completed.""" - for measurementIndex in range(markupsNode.GetNumberOfMeasurements()): - measurement = markupsNode.GetNthMeasurement(measurementIndex) - doubleArrayVtk = measurement.GetControlPointValues() - if doubleArrayVtk and doubleArrayVtk.GetName() == arrayName: - doubleArrayVtk.Modified() + """Indicate that modification of a numpy array returned by :py:meth:`arrayFromMarkupsControlPointData` has been completed.""" + for measurementIndex in range(markupsNode.GetNumberOfMeasurements()): + measurement = markupsNode.GetNthMeasurement(measurementIndex) + doubleArrayVtk = measurement.GetControlPointValues() + if doubleArrayVtk and doubleArrayVtk.GetName() == arrayName: + doubleArrayVtk.Modified() def arrayFromModelPolyIds(modelNode): - """Return poly id array of a model node as numpy array. + """Return poly id array of a model node as numpy array. - These ids are the following format: - [ n(0), i(0,0), i(0,1), ... i(0,n(00),..., n(j), i(j,0), ... i(j,n(j))...] - where n(j) is the number of vertices in polygon j - and i(j,k) is the index into the vertex array for vertex k of poly j. + These ids are the following format: + [ n(0), i(0,0), i(0,1), ... i(0,n(00),..., n(j), i(j,0), ... i(j,n(j))...] + where n(j) is the number of vertices in polygon j + and i(j,k) is the index into the vertex array for vertex k of poly j. - As described here: - https://vtk.org/wp-content/uploads/2015/04/file-formats.pdf + As described here: + https://vtk.org/wp-content/uploads/2015/04/file-formats.pdf - Typically in Slicer n(j) will always be 3 because a model node's - polygons will be triangles. + Typically in Slicer n(j) will always be 3 because a model node's + polygons will be triangles. - .. warning:: Important: memory area of the returned array is managed by VTK, - therefore values in the array may be changed, but the array must not be reallocated. - See :py:meth:`arrayFromVolume` for details. - """ - import vtk.util.numpy_support - arrayVtk = modelNode.GetPolyData().GetPolys().GetData() - narray = vtk.util.numpy_support.vtk_to_numpy(arrayVtk) - return narray + .. warning:: Important: memory area of the returned array is managed by VTK, + therefore values in the array may be changed, but the array must not be reallocated. + See :py:meth:`arrayFromVolume` for details. + """ + import vtk.util.numpy_support + arrayVtk = modelNode.GetPolyData().GetPolys().GetData() + narray = vtk.util.numpy_support.vtk_to_numpy(arrayVtk) + return narray def arrayFromGridTransform(gridTransformNode): - """Return voxel array from transform node as numpy array. + """Return voxel array from transform node as numpy array. - Vector values are not copied. Values in the transform node can be modified - by changing values in the numpy array. - After all modifications has been completed, call :py:meth:`arrayFromGridTransformModified`. + Vector values are not copied. Values in the transform node can be modified + by changing values in the numpy array. + After all modifications has been completed, call :py:meth:`arrayFromGridTransformModified`. - .. warning:: Important: memory area of the returned array is managed by VTK, - therefore values in the array may be changed, but the array must not be reallocated. - See :py:meth:`arrayFromVolume` for details. - """ - transformGrid = gridTransformNode.GetTransformFromParent() - displacementGrid = transformGrid.GetDisplacementGrid() - nshape = tuple(reversed(displacementGrid.GetDimensions())) - import vtk.util.numpy_support - nshape = nshape + (3,) - narray = vtk.util.numpy_support.vtk_to_numpy(displacementGrid.GetPointData().GetScalars()).reshape(nshape) - return narray + .. warning:: Important: memory area of the returned array is managed by VTK, + therefore values in the array may be changed, but the array must not be reallocated. + See :py:meth:`arrayFromVolume` for details. + """ + transformGrid = gridTransformNode.GetTransformFromParent() + displacementGrid = transformGrid.GetDisplacementGrid() + nshape = tuple(reversed(displacementGrid.GetDimensions())) + import vtk.util.numpy_support + nshape = nshape + (3,) + narray = vtk.util.numpy_support.vtk_to_numpy(displacementGrid.GetPointData().GetScalars()).reshape(nshape) + return narray def arrayFromVTKMatrix(vmatrix): - """Return vtkMatrix4x4 or vtkMatrix3x3 elements as numpy array. + """Return vtkMatrix4x4 or vtkMatrix3x3 elements as numpy array. - :raises RuntimeError: in case of failure + :raises RuntimeError: in case of failure - The returned array is just a copy and so any modification in the array will not affect the input matrix. - To set VTK matrix from a numpy array, use :py:meth:`vtkMatrixFromArray` or - :py:meth:`updateVTKMatrixFromArray`. - """ - from vtk import vtkMatrix4x4 - from vtk import vtkMatrix3x3 - import numpy as np - if isinstance(vmatrix, vtkMatrix4x4): - matrixSize = 4 - elif isinstance(vmatrix, vtkMatrix3x3): - matrixSize = 3 - else: - raise RuntimeError("Input must be vtk.vtkMatrix3x3 or vtk.vtkMatrix4x4") - narray = np.eye(matrixSize) - vmatrix.DeepCopy(narray.ravel(), vmatrix) - return narray + The returned array is just a copy and so any modification in the array will not affect the input matrix. + To set VTK matrix from a numpy array, use :py:meth:`vtkMatrixFromArray` or + :py:meth:`updateVTKMatrixFromArray`. + """ + from vtk import vtkMatrix4x4 + from vtk import vtkMatrix3x3 + import numpy as np + if isinstance(vmatrix, vtkMatrix4x4): + matrixSize = 4 + elif isinstance(vmatrix, vtkMatrix3x3): + matrixSize = 3 + else: + raise RuntimeError("Input must be vtk.vtkMatrix3x3 or vtk.vtkMatrix4x4") + narray = np.eye(matrixSize) + vmatrix.DeepCopy(narray.ravel(), vmatrix) + return narray def vtkMatrixFromArray(narray): - """Create VTK matrix from a 3x3 or 4x4 numpy array. + """Create VTK matrix from a 3x3 or 4x4 numpy array. - :param narray: input numpy array - :raises RuntimeError: in case of failure + :param narray: input numpy array + :raises RuntimeError: in case of failure - The returned matrix is just a copy and so any modification in the array will not affect the output matrix. - To set numpy array from VTK matrix, use :py:meth:`arrayFromVTKMatrix`. - """ - from vtk import vtkMatrix4x4 - from vtk import vtkMatrix3x3 - narrayshape = narray.shape - if narrayshape == (4,4): - vmatrix = vtkMatrix4x4() - updateVTKMatrixFromArray(vmatrix, narray) - return vmatrix - elif narrayshape == (3,3): - vmatrix = vtkMatrix3x3() - updateVTKMatrixFromArray(vmatrix, narray) - return vmatrix - else: - raise RuntimeError("Unsupported numpy array shape: "+str(narrayshape)+" expected (4,4)") + The returned matrix is just a copy and so any modification in the array will not affect the output matrix. + To set numpy array from VTK matrix, use :py:meth:`arrayFromVTKMatrix`. + """ + from vtk import vtkMatrix4x4 + from vtk import vtkMatrix3x3 + narrayshape = narray.shape + if narrayshape == (4, 4): + vmatrix = vtkMatrix4x4() + updateVTKMatrixFromArray(vmatrix, narray) + return vmatrix + elif narrayshape == (3, 3): + vmatrix = vtkMatrix3x3() + updateVTKMatrixFromArray(vmatrix, narray) + return vmatrix + else: + raise RuntimeError("Unsupported numpy array shape: " + str(narrayshape) + " expected (4,4)") def updateVTKMatrixFromArray(vmatrix, narray): - """Update VTK matrix values from a numpy array. + """Update VTK matrix values from a numpy array. - :param vmatrix: VTK matrix (vtkMatrix4x4 or vtkMatrix3x3) that will be update - :param narray: input numpy array - :raises RuntimeError: in case of failure + :param vmatrix: VTK matrix (vtkMatrix4x4 or vtkMatrix3x3) that will be update + :param narray: input numpy array + :raises RuntimeError: in case of failure - To set numpy array from VTK matrix, use :py:meth:`arrayFromVTKMatrix`. - """ - from vtk import vtkMatrix4x4 - from vtk import vtkMatrix3x3 - if isinstance(vmatrix, vtkMatrix4x4): - matrixSize = 4 - elif isinstance(vmatrix, vtkMatrix3x3): - matrixSize = 3 - else: - raise RuntimeError("Output vmatrix must be vtk.vtkMatrix3x3 or vtk.vtkMatrix4x4") - if narray.shape != (matrixSize, matrixSize): - raise RuntimeError("Input narray size must match output vmatrix size ({0}x{0})".format(matrixSize)) - vmatrix.DeepCopy(narray.ravel()) + To set numpy array from VTK matrix, use :py:meth:`arrayFromVTKMatrix`. + """ + from vtk import vtkMatrix4x4 + from vtk import vtkMatrix3x3 + if isinstance(vmatrix, vtkMatrix4x4): + matrixSize = 4 + elif isinstance(vmatrix, vtkMatrix3x3): + matrixSize = 3 + else: + raise RuntimeError("Output vmatrix must be vtk.vtkMatrix3x3 or vtk.vtkMatrix4x4") + if narray.shape != (matrixSize, matrixSize): + raise RuntimeError("Input narray size must match output vmatrix size ({0}x{0})".format(matrixSize)) + vmatrix.DeepCopy(narray.ravel()) def arrayFromTransformMatrix(transformNode, toWorld=False): - """Return 4x4 transformation matrix as numpy array. + """Return 4x4 transformation matrix as numpy array. - :param toWorld: if set to True then the transform to world coordinate system is returned - (effect of parent transform to the node is applied), otherwise transform to parent transform is returned. - :return: numpy array - :raises RuntimeError: in case of failure + :param toWorld: if set to True then the transform to world coordinate system is returned + (effect of parent transform to the node is applied), otherwise transform to parent transform is returned. + :return: numpy array + :raises RuntimeError: in case of failure - The returned array is just a copy and so any modification in the array will not affect the transform node. + The returned array is just a copy and so any modification in the array will not affect the transform node. - To set transformation matrix from a numpy array, use :py:meth:`updateTransformMatrixFromArray`. - """ - from vtk import vtkMatrix4x4 - vmatrix = vtkMatrix4x4() - if toWorld: - success = transformNode.GetMatrixTransformToWorld(vmatrix) - else: - success = transformNode.GetMatrixTransformToParent(vmatrix) - if not success: - raise RuntimeError("Failed to get transformation matrix from node "+transformNode.GetID()) - return arrayFromVTKMatrix(vmatrix) - - -def updateTransformMatrixFromArray(transformNode, narray, toWorld = False): - """Set transformation matrix from a numpy array of size 4x4 (toParent). - - :param world: if set to True then the transform will be set so that transform - to world matrix will be equal to narray; otherwise transform to parent will be - set as narray. - :raises RuntimeError: in case of failure - """ - import numpy as np - from vtk import vtkMatrix4x4 - narrayshape = narray.shape - if narrayshape != (4,4): - raise RuntimeError("Unsupported numpy array shape: "+str(narrayshape)+" expected (4,4)") - if toWorld and transformNode.GetParentTransformNode(): - # thisToParent = worldToParent * thisToWorld = inv(parentToWorld) * toWorld - narrayParentToWorld = arrayFromTransformMatrix(transformNode.GetParentTransformNode()) - thisToParent = np.dot(np.linalg.inv(narrayParentToWorld), narray) - updateTransformMatrixFromArray(transformNode, thisToParent, toWorld = False) - else: + To set transformation matrix from a numpy array, use :py:meth:`updateTransformMatrixFromArray`. + """ + from vtk import vtkMatrix4x4 vmatrix = vtkMatrix4x4() - updateVTKMatrixFromArray(vmatrix, narray) - transformNode.SetMatrixTransformToParent(vmatrix) + if toWorld: + success = transformNode.GetMatrixTransformToWorld(vmatrix) + else: + success = transformNode.GetMatrixTransformToParent(vmatrix) + if not success: + raise RuntimeError("Failed to get transformation matrix from node " + transformNode.GetID()) + return arrayFromVTKMatrix(vmatrix) + + +def updateTransformMatrixFromArray(transformNode, narray, toWorld=False): + """Set transformation matrix from a numpy array of size 4x4 (toParent). + + :param world: if set to True then the transform will be set so that transform + to world matrix will be equal to narray; otherwise transform to parent will be + set as narray. + :raises RuntimeError: in case of failure + """ + import numpy as np + from vtk import vtkMatrix4x4 + narrayshape = narray.shape + if narrayshape != (4, 4): + raise RuntimeError("Unsupported numpy array shape: " + str(narrayshape) + " expected (4,4)") + if toWorld and transformNode.GetParentTransformNode(): + # thisToParent = worldToParent * thisToWorld = inv(parentToWorld) * toWorld + narrayParentToWorld = arrayFromTransformMatrix(transformNode.GetParentTransformNode()) + thisToParent = np.dot(np.linalg.inv(narrayParentToWorld), narray) + updateTransformMatrixFromArray(transformNode, thisToParent, toWorld=False) + else: + vmatrix = vtkMatrix4x4() + updateVTKMatrixFromArray(vmatrix, narray) + transformNode.SetMatrixTransformToParent(vmatrix) def arrayFromGridTransformModified(gridTransformNode): - """Indicate that modification of a numpy array returned by :py:meth:`arrayFromGridTransform` has been completed.""" - transformGrid = gridTransformNode.GetTransformFromParent() - displacementGrid = transformGrid.GetDisplacementGrid() - displacementGrid.GetPointData().GetScalars().Modified() - displacementGrid.Modified() + """Indicate that modification of a numpy array returned by :py:meth:`arrayFromGridTransform` has been completed.""" + transformGrid = gridTransformNode.GetTransformFromParent() + displacementGrid = transformGrid.GetDisplacementGrid() + displacementGrid.GetPointData().GetScalars().Modified() + displacementGrid.Modified() def arrayFromSegment(segmentationNode, segmentId): - """Get segment as numpy array. + """Get segment as numpy array. - .. warning:: Important: binary labelmap representation may be shared between multiple segments. + .. warning:: Important: binary labelmap representation may be shared between multiple segments. - .. deprecated:: 4.13.0 - Use arrayFromSegmentBinaryLabelmap to access a copy of the binary labelmap that will not modify the original labelmap." - Use arrayFromSegmentInternalBinaryLabelmap to access a modifiable internal labelmap representation that may be shared" - between multiple segments. - """ - import logging - logging.warning("arrayFromSegment is deprecated. Binary labelmap representation may be shared between multiple segments.") - return arrayFromSegmentBinaryLabelmap(segmentationNode, segmentId) + .. deprecated:: 4.13.0 + Use arrayFromSegmentBinaryLabelmap to access a copy of the binary labelmap that will not modify the original labelmap." + Use arrayFromSegmentInternalBinaryLabelmap to access a modifiable internal labelmap representation that may be shared" + between multiple segments. + """ + import logging + logging.warning("arrayFromSegment is deprecated. Binary labelmap representation may be shared between multiple segments.") + return arrayFromSegmentBinaryLabelmap(segmentationNode, segmentId) def arrayFromSegmentInternalBinaryLabelmap(segmentationNode, segmentId): - """Return voxel array of a segment's binary labelmap representation as numpy array. + """Return voxel array of a segment's binary labelmap representation as numpy array. - Voxels values are not copied. - The labelmap containing the specified segment may be a shared labelmap containing multiple segments. + Voxels values are not copied. + The labelmap containing the specified segment may be a shared labelmap containing multiple segments. - To get and modify the array for a single segment, calling:: + To get and modify the array for a single segment, calling:: - segmentationNode->GetSegmentation()->SeparateSegment(segmentId) + segmentationNode->GetSegmentation()->SeparateSegment(segmentId) - will transfer the segment from a shared labelmap into a new layer. + will transfer the segment from a shared labelmap into a new layer. - Layers can be merged by calling:: + Layers can be merged by calling:: - segmentationNode->GetSegmentation()->CollapseBinaryLabelmaps() + segmentationNode->GetSegmentation()->CollapseBinaryLabelmaps() - If binary labelmap is the master representation then voxel values in the volume node can be modified - by changing values in the numpy array. After all modifications has been completed, call:: + If binary labelmap is the master representation then voxel values in the volume node can be modified + by changing values in the numpy array. After all modifications has been completed, call:: - segmentationNode.GetSegmentation().GetSegment(segmentID).Modified() + segmentationNode.GetSegmentation().GetSegment(segmentID).Modified() - .. warning:: Important: memory area of the returned array is managed by VTK, - therefore values in the array may be changed, but the array must not be reallocated. - See :py:meth:`arrayFromVolume` for details. - """ - vimage = segmentationNode.GetBinaryLabelmapInternalRepresentation(segmentId) - nshape = tuple(reversed(vimage.GetDimensions())) - import vtk.util.numpy_support - narray = vtk.util.numpy_support.vtk_to_numpy(vimage.GetPointData().GetScalars()).reshape(nshape) - return narray + .. warning:: Important: memory area of the returned array is managed by VTK, + therefore values in the array may be changed, but the array must not be reallocated. + See :py:meth:`arrayFromVolume` for details. + """ + vimage = segmentationNode.GetBinaryLabelmapInternalRepresentation(segmentId) + nshape = tuple(reversed(vimage.GetDimensions())) + import vtk.util.numpy_support + narray = vtk.util.numpy_support.vtk_to_numpy(vimage.GetPointData().GetScalars()).reshape(nshape) + return narray def arrayFromSegmentBinaryLabelmap(segmentationNode, segmentId, referenceVolumeNode=None): - """Return voxel array of a segment's binary labelmap representation as numpy array. + """Return voxel array of a segment's binary labelmap representation as numpy array. - :param segmentationNode: source segmentation node. - :param segmentId: ID of the source segment. - Can be determined from segment name by calling ``segmentationNode.GetSegmentation().GetSegmentIdBySegmentName(segmentName)``. - :param referenceVolumeNode: a volume node that determines geometry (origin, spacing, axis directions, extents) of the array. - If not specified then the volume that was used for setting the segmentation's geometry is used as reference volume. + :param segmentationNode: source segmentation node. + :param segmentId: ID of the source segment. + Can be determined from segment name by calling ``segmentationNode.GetSegmentation().GetSegmentIdBySegmentName(segmentName)``. + :param referenceVolumeNode: a volume node that determines geometry (origin, spacing, axis directions, extents) of the array. + If not specified then the volume that was used for setting the segmentation's geometry is used as reference volume. - :raises RuntimeError: in case of failure + :raises RuntimeError: in case of failure - Voxels values are copied, therefore changing the returned numpy array has no effect on the source segmentation. - The modified array can be written back to the segmentation by calling :py:meth:`updateSegmentBinaryLabelmapFromArray`. + Voxels values are copied, therefore changing the returned numpy array has no effect on the source segmentation. + The modified array can be written back to the segmentation by calling :py:meth:`updateSegmentBinaryLabelmapFromArray`. - To get voxels of a segment as a modifiable numpy array, you can use :py:meth:`arrayFromSegmentInternalBinaryLabelmap`. - """ + To get voxels of a segment as a modifiable numpy array, you can use :py:meth:`arrayFromSegmentInternalBinaryLabelmap`. + """ - import slicer - import vtk + import slicer + import vtk - # Get reference volume - if not referenceVolumeNode: - referenceVolumeNode = segmentationNode.GetNodeReference(slicer.vtkMRMLSegmentationNode.GetReferenceImageGeometryReferenceRole()) + # Get reference volume if not referenceVolumeNode: - raise RuntimeError("No reference volume is found in the input segmentationNode, therefore a valid referenceVolumeNode input is required.") + referenceVolumeNode = segmentationNode.GetNodeReference(slicer.vtkMRMLSegmentationNode.GetReferenceImageGeometryReferenceRole()) + if not referenceVolumeNode: + raise RuntimeError("No reference volume is found in the input segmentationNode, therefore a valid referenceVolumeNode input is required.") - # Export segment as vtkImageData (via temporary labelmap volume node) - segmentIds = vtk.vtkStringArray() - segmentIds.InsertNextValue(segmentId) - labelmapVolumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode", "__temp__") - try: - if not slicer.modules.segmentations.logic().ExportSegmentsToLabelmapNode(segmentationNode, segmentIds, labelmapVolumeNode, referenceVolumeNode): - raise RuntimeError("Export of segment failed.") - narray = slicer.util.arrayFromVolume(labelmapVolumeNode) - finally: - slicer.mrmlScene.RemoveNode(labelmapVolumeNode) + # Export segment as vtkImageData (via temporary labelmap volume node) + segmentIds = vtk.vtkStringArray() + segmentIds.InsertNextValue(segmentId) + labelmapVolumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode", "__temp__") + try: + if not slicer.modules.segmentations.logic().ExportSegmentsToLabelmapNode(segmentationNode, segmentIds, labelmapVolumeNode, referenceVolumeNode): + raise RuntimeError("Export of segment failed.") + narray = slicer.util.arrayFromVolume(labelmapVolumeNode) + finally: + slicer.mrmlScene.RemoveNode(labelmapVolumeNode) - return narray + return narray def updateSegmentBinaryLabelmapFromArray(narray, segmentationNode, segmentId, referenceVolumeNode=None): - """Sets binary labelmap representation of a segment from a numpy array. - :param narray: voxel array, containing 0 outside the segment, 1 inside the segment. - :param segmentationNode: segmentation node that will be updated. - :param segmentId: ID of the segment that will be updated. - Can be determined from segment name by calling ``segmentationNode.GetSegmentation().GetSegmentIdBySegmentName(segmentName)``. - :param referenceVolumeNode: a volume node that determines geometry (origin, spacing, axis directions, extents) of the array. - If not specified then the volume that was used for setting the segmentation's geometry is used as reference volume. + """Sets binary labelmap representation of a segment from a numpy array. + :param narray: voxel array, containing 0 outside the segment, 1 inside the segment. + :param segmentationNode: segmentation node that will be updated. + :param segmentId: ID of the segment that will be updated. + Can be determined from segment name by calling ``segmentationNode.GetSegmentation().GetSegmentIdBySegmentName(segmentName)``. + :param referenceVolumeNode: a volume node that determines geometry (origin, spacing, axis directions, extents) of the array. + If not specified then the volume that was used for setting the segmentation's geometry is used as reference volume. - :raises RuntimeError: in case of failure + :raises RuntimeError: in case of failure - Voxels values are deep-copied, therefore if the numpy array is modified after calling this method, segmentation node will not change. - """ + Voxels values are deep-copied, therefore if the numpy array is modified after calling this method, segmentation node will not change. + """ - # Export segment as vtkImageData (via temporary labelmap volume node) - import slicer - import vtk + # Export segment as vtkImageData (via temporary labelmap volume node) + import slicer + import vtk - # Get reference volume - if not referenceVolumeNode: - referenceVolumeNode = segmentationNode.GetNodeReference(slicer.vtkMRMLSegmentationNode.GetReferenceImageGeometryReferenceRole()) + # Get reference volume if not referenceVolumeNode: - raise RuntimeError("No reference volume is found in the input segmentationNode, therefore a valid referenceVolumeNode input is required.") - - # Update segment in segmentation - labelmapVolumeNode = slicer.modules.volumes.logic().CreateAndAddLabelVolume(referenceVolumeNode, "__temp__") - try: - if narray.min() >= 0 and narray.max() <= 1: - # input array seems to be valid, use it as is (faster) - updateVolumeFromArray(labelmapVolumeNode, narray) - else: - # need to normalize the data because the label value must be 1 - import numpy as np - narrayNormalized = np.zeros(narray.shape, np.uint8) - narrayNormalized[narray > 0] = 1 - updateVolumeFromArray(labelmapVolumeNode, narrayNormalized) - segmentIds = vtk.vtkStringArray() - segmentIds.InsertNextValue(segmentId) - if not slicer.modules.segmentations.logic().ImportLabelmapToSegmentationNode(labelmapVolumeNode, segmentationNode, segmentIds): - raise RuntimeError("Importing of segment failed.") - finally: - slicer.mrmlScene.RemoveNode(labelmapVolumeNode) + referenceVolumeNode = segmentationNode.GetNodeReference(slicer.vtkMRMLSegmentationNode.GetReferenceImageGeometryReferenceRole()) + if not referenceVolumeNode: + raise RuntimeError("No reference volume is found in the input segmentationNode, therefore a valid referenceVolumeNode input is required.") + # Update segment in segmentation + labelmapVolumeNode = slicer.modules.volumes.logic().CreateAndAddLabelVolume(referenceVolumeNode, "__temp__") + try: + if narray.min() >= 0 and narray.max() <= 1: + # input array seems to be valid, use it as is (faster) + updateVolumeFromArray(labelmapVolumeNode, narray) + else: + # need to normalize the data because the label value must be 1 + import numpy as np + narrayNormalized = np.zeros(narray.shape, np.uint8) + narrayNormalized[narray > 0] = 1 + updateVolumeFromArray(labelmapVolumeNode, narrayNormalized) + segmentIds = vtk.vtkStringArray() + segmentIds.InsertNextValue(segmentId) + if not slicer.modules.segmentations.logic().ImportLabelmapToSegmentationNode(labelmapVolumeNode, segmentationNode, segmentIds): + raise RuntimeError("Importing of segment failed.") + finally: + slicer.mrmlScene.RemoveNode(labelmapVolumeNode) -def arrayFromMarkupsControlPoints(markupsNode, world = False): - """Return control point positions of a markups node as rows in a numpy array (of size Nx3). - :param world: if set to True then the control points coordinates are returned in world coordinate system - (effect of parent transform to the node is applied). +def arrayFromMarkupsControlPoints(markupsNode, world=False): + """Return control point positions of a markups node as rows in a numpy array (of size Nx3). - The returned array is just a copy and so any modification in the array will not affect the markup node. + :param world: if set to True then the control points coordinates are returned in world coordinate system + (effect of parent transform to the node is applied). - To modify markup control points based on a numpy array, use :py:meth:`updateMarkupsControlPointsFromArray`. - """ - numberOfControlPoints = markupsNode.GetNumberOfControlPoints() - import numpy as np - narray = np.zeros([numberOfControlPoints, 3]) - for controlPointIndex in range(numberOfControlPoints): - if world: - markupsNode.GetNthControlPointPositionWorld(controlPointIndex, narray[controlPointIndex,:]) - else: - markupsNode.GetNthControlPointPosition(controlPointIndex, narray[controlPointIndex,:]) - return narray + The returned array is just a copy and so any modification in the array will not affect the markup node. + To modify markup control points based on a numpy array, use :py:meth:`updateMarkupsControlPointsFromArray`. + """ + numberOfControlPoints = markupsNode.GetNumberOfControlPoints() + import numpy as np + narray = np.zeros([numberOfControlPoints, 3]) + for controlPointIndex in range(numberOfControlPoints): + if world: + markupsNode.GetNthControlPointPositionWorld(controlPointIndex, narray[controlPointIndex, :]) + else: + markupsNode.GetNthControlPointPosition(controlPointIndex, narray[controlPointIndex, :]) + return narray -def updateMarkupsControlPointsFromArray(markupsNode, narray, world = False): - """Sets control point positions in a markups node from a numpy array of size Nx3. - :param world: if set to True then the control point coordinates are expected in world coordinate system. - :raises RuntimeError: in case of failure +def updateMarkupsControlPointsFromArray(markupsNode, narray, world=False): + """Sets control point positions in a markups node from a numpy array of size Nx3. - All previous content of the node is deleted. - """ - narrayshape = narray.shape - if narrayshape == (0,): - markupsNode.RemoveAllControlPoints() - return - if len(narrayshape) != 2 or narrayshape[1] != 3: - raise RuntimeError("Unsupported numpy array shape: "+str(narrayshape)+" expected (N,3)") - numberOfControlPoints = narrayshape[0] - oldNumberOfControlPoints = markupsNode.GetNumberOfControlPoints() - # Update existing control points - for controlPointIndex in range(min(numberOfControlPoints, oldNumberOfControlPoints)): - if world: - markupsNode.SetNthControlPointPositionWorldFromArray(controlPointIndex, narray[controlPointIndex,:]) + :param world: if set to True then the control point coordinates are expected in world coordinate system. + :raises RuntimeError: in case of failure + + All previous content of the node is deleted. + """ + narrayshape = narray.shape + if narrayshape == (0,): + markupsNode.RemoveAllControlPoints() + return + if len(narrayshape) != 2 or narrayshape[1] != 3: + raise RuntimeError("Unsupported numpy array shape: " + str(narrayshape) + " expected (N,3)") + numberOfControlPoints = narrayshape[0] + oldNumberOfControlPoints = markupsNode.GetNumberOfControlPoints() + # Update existing control points + for controlPointIndex in range(min(numberOfControlPoints, oldNumberOfControlPoints)): + if world: + markupsNode.SetNthControlPointPositionWorldFromArray(controlPointIndex, narray[controlPointIndex, :]) + else: + markupsNode.SetNthControlPointPositionFromArray(controlPointIndex, narray[controlPointIndex, :]) + if numberOfControlPoints >= oldNumberOfControlPoints: + # Add new points to the markup node + from vtk import vtkVector3d + for controlPointIndex in range(oldNumberOfControlPoints, numberOfControlPoints): + if world: + markupsNode.AddControlPointWorld(vtkVector3d(narray[controlPointIndex, :])) + else: + markupsNode.AddControlPoint(vtkVector3d(narray[controlPointIndex, :])) else: - markupsNode.SetNthControlPointPositionFromArray(controlPointIndex, narray[controlPointIndex,:]) - if numberOfControlPoints >= oldNumberOfControlPoints: - # Add new points to the markup node - from vtk import vtkVector3d - for controlPointIndex in range(oldNumberOfControlPoints, numberOfControlPoints): - if world: - markupsNode.AddControlPointWorld(vtkVector3d(narray[controlPointIndex,:])) - else: - markupsNode.AddControlPoint(vtkVector3d(narray[controlPointIndex,:])) - else: - # Remove extra point from the markup node - for controlPointIndex in range(oldNumberOfControlPoints, numberOfControlPoints, -1): - markupsNode.RemoveNthControlPoint(controlPointIndex-1) - - -def arrayFromMarkupsCurvePoints(markupsNode, world = False): - """Return interpolated curve point positions of a markups node as rows in a numpy array (of size Nx3). - - :param world: if set to True then the point coordinates are returned in world coordinate system - (effect of parent transform to the node is applied). - - The returned array is just a copy and so any modification in the array will not affect the markup node. - """ - import vtk.util.numpy_support - if world: - pointData = markupsNode.GetCurvePointsWorld().GetData() - else: - pointData = markupsNode.GetCurvePoints().GetData() - narray = vtk.util.numpy_support.vtk_to_numpy(pointData) - return narray + # Remove extra point from the markup node + for controlPointIndex in range(oldNumberOfControlPoints, numberOfControlPoints, -1): + markupsNode.RemoveNthControlPoint(controlPointIndex - 1) -def arrayFromMarkupsCurveData(markupsNode, arrayName, world=False): - """Return curve measurement results from a markups node as a numpy array. +def arrayFromMarkupsCurvePoints(markupsNode, world=False): + """Return interpolated curve point positions of a markups node as rows in a numpy array (of size Nx3). - :param markupsNode: node to get the curve point data from. - :param arrayName: array name to get (for example `Curvature`) - :param world: if set to True then the point coordinates are returned in world coordinate system - (effect of parent transform to the node is applied). - :raises ValueError: in case of failure + :param world: if set to True then the point coordinates are returned in world coordinate system + (effect of parent transform to the node is applied). - Note that not all array may be available in both node and world coordinate systems. - For example, `Curvature` is only computed for the curve in world coordinate system. + The returned array is just a copy and so any modification in the array will not affect the markup node. + """ + import vtk.util.numpy_support + if world: + pointData = markupsNode.GetCurvePointsWorld().GetData() + else: + pointData = markupsNode.GetCurvePoints().GetData() + narray = vtk.util.numpy_support.vtk_to_numpy(pointData) + return narray - The returned array is not intended to be modified, as arrays are expected to be written only - by measurement objects. - """ - import vtk.util.numpy_support - if world: - curvePolyData = markupsNode.GetCurveWorld() - else: - curvePolyData = markupsNode.GetCurve() - pointData = curvePolyData.GetPointData() - if not pointData or pointData.GetNumberOfArrays() == 0: - raise ValueError(f"Input markups curve does not contain point data") - arrayVtk = pointData.GetArray(arrayName) - if not arrayVtk: - availableArrayNames = [pointData.GetArrayName(i) for i in range(pointData.GetNumberOfArrays())] - raise ValueError("Input markupsNode does not contain curve point data array '{}'. Available array names: '{}'".format( - arrayName, "', '".join(availableArrayNames))) +def arrayFromMarkupsCurveData(markupsNode, arrayName, world=False): + """Return curve measurement results from a markups node as a numpy array. - narray = vtk.util.numpy_support.vtk_to_numpy(arrayVtk) - return narray + :param markupsNode: node to get the curve point data from. + :param arrayName: array name to get (for example `Curvature`) + :param world: if set to True then the point coordinates are returned in world coordinate system + (effect of parent transform to the node is applied). + :raises ValueError: in case of failure + Note that not all array may be available in both node and world coordinate systems. + For example, `Curvature` is only computed for the curve in world coordinate system. -def updateVolumeFromArray(volumeNode, narray): - """Sets voxels of a volume node from a numpy array. + The returned array is not intended to be modified, as arrays are expected to be written only + by measurement objects. + """ + import vtk.util.numpy_support + if world: + curvePolyData = markupsNode.GetCurveWorld() + else: + curvePolyData = markupsNode.GetCurve() + pointData = curvePolyData.GetPointData() + if not pointData or pointData.GetNumberOfArrays() == 0: + raise ValueError(f"Input markups curve does not contain point data") - :raises RuntimeError: in case of failure + arrayVtk = pointData.GetArray(arrayName) + if not arrayVtk: + availableArrayNames = [pointData.GetArrayName(i) for i in range(pointData.GetNumberOfArrays())] + raise ValueError("Input markupsNode does not contain curve point data array '{}'. Available array names: '{}'".format( + arrayName, "', '".join(availableArrayNames))) - Voxels values are deep-copied, therefore if the numpy array - is modified after calling this method, voxel values in the volume node will not change. - Dimensions and data size of the source numpy array does not have to match the current - content of the volume node. - """ + narray = vtk.util.numpy_support.vtk_to_numpy(arrayVtk) + return narray - vshape = tuple(reversed(narray.shape)) - if len(vshape) == 3: - # Scalar volume - vcomponents = 1 - elif len(vshape) == 4: - # Vector volume - vcomponents = vshape[0] - vshape = vshape[1:4] - else: - # TODO: add support for tensor volumes - raise RuntimeError("Unsupported numpy array shape: "+str(narray.shape)) - - vimage = volumeNode.GetImageData() - if not vimage: - import vtk - vimage = vtk.vtkImageData() - volumeNode.SetAndObserveImageData(vimage) - import vtk.util.numpy_support - vtype = vtk.util.numpy_support.get_vtk_array_type(narray.dtype) - # Volumes with "long long" scalar type are not rendered correctly. - # Probably this could be fixed in VTK or Slicer but for now just reject it. - if vtype == vtk.VTK_LONG_LONG: - raise RuntimeError("Unsupported numpy array type: long long") +def updateVolumeFromArray(volumeNode, narray): + """Sets voxels of a volume node from a numpy array. - vimage.SetDimensions(vshape) - vimage.AllocateScalars(vtype, vcomponents) + :raises RuntimeError: in case of failure - narrayTarget = arrayFromVolume(volumeNode) - narrayTarget[:] = narray + Voxels values are deep-copied, therefore if the numpy array + is modified after calling this method, voxel values in the volume node will not change. + Dimensions and data size of the source numpy array does not have to match the current + content of the volume node. + """ - # Notify the application that image data is changed - # (same notifications as in vtkMRMLVolumeNode.SetImageDataConnection) - import slicer - volumeNode.StorableModified() - volumeNode.Modified() - volumeNode.InvokeEvent(slicer.vtkMRMLVolumeNode.ImageDataModifiedEvent, volumeNode) + vshape = tuple(reversed(narray.shape)) + if len(vshape) == 3: + # Scalar volume + vcomponents = 1 + elif len(vshape) == 4: + # Vector volume + vcomponents = vshape[0] + vshape = vshape[1:4] + else: + # TODO: add support for tensor volumes + raise RuntimeError("Unsupported numpy array shape: " + str(narray.shape)) + + vimage = volumeNode.GetImageData() + if not vimage: + import vtk + vimage = vtk.vtkImageData() + volumeNode.SetAndObserveImageData(vimage) + import vtk.util.numpy_support + vtype = vtk.util.numpy_support.get_vtk_array_type(narray.dtype) + + # Volumes with "long long" scalar type are not rendered correctly. + # Probably this could be fixed in VTK or Slicer but for now just reject it. + if vtype == vtk.VTK_LONG_LONG: + raise RuntimeError("Unsupported numpy array type: long long") + + vimage.SetDimensions(vshape) + vimage.AllocateScalars(vtype, vcomponents) + + narrayTarget = arrayFromVolume(volumeNode) + narrayTarget[:] = narray + + # Notify the application that image data is changed + # (same notifications as in vtkMRMLVolumeNode.SetImageDataConnection) + import slicer + volumeNode.StorableModified() + volumeNode.Modified() + volumeNode.InvokeEvent(slicer.vtkMRMLVolumeNode.ImageDataModifiedEvent, volumeNode) def addVolumeFromArray(narray, ijkToRAS=None, name=None, nodeClassName=None): - """Create a new volume node from content of a numpy array and add it to the scene. + """Create a new volume node from content of a numpy array and add it to the scene. - Voxels values are deep-copied, therefore if the numpy array - is modified after calling this method, voxel values in the volume node will not change. + Voxels values are deep-copied, therefore if the numpy array + is modified after calling this method, voxel values in the volume node will not change. - :param narray: numpy array containing volume voxels. - :param ijkToRAS: 4x4 numpy array or vtk.vtkMatrix4x4 that defines mapping from IJK to RAS coordinate system (specifying origin, spacing, directions) - :param name: volume node name - :param nodeClassName: type of created volume, default: ``vtkMRMLScalarVolumeNode``. - Use ``vtkMRMLLabelMapVolumeNode`` for labelmap volume, ``vtkMRMLVectorVolumeNode`` for vector volume. - :return: created new volume node + :param narray: numpy array containing volume voxels. + :param ijkToRAS: 4x4 numpy array or vtk.vtkMatrix4x4 that defines mapping from IJK to RAS coordinate system (specifying origin, spacing, directions) + :param name: volume node name + :param nodeClassName: type of created volume, default: ``vtkMRMLScalarVolumeNode``. + Use ``vtkMRMLLabelMapVolumeNode`` for labelmap volume, ``vtkMRMLVectorVolumeNode`` for vector volume. + :return: created new volume node - Example:: + Example:: - # create zero-filled volume - import numpy as np - volumeNode = slicer.util.addVolumeFromArray(np.zeros((30, 40, 50))) + # create zero-filled volume + import numpy as np + volumeNode = slicer.util.addVolumeFromArray(np.zeros((30, 40, 50))) - Example:: + Example:: - # create labelmap volume filled with voxel value of 120 - import numpy as np - volumeNode = slicer.util.addVolumeFromArray(np.ones((30, 40, 50), 'int8') * 120, - np.diag([0.2, 0.2, 0.5, 1.0]), nodeClassName="vtkMRMLLabelMapVolumeNode") - """ - import slicer - from vtk import vtkMatrix4x4 + # create labelmap volume filled with voxel value of 120 + import numpy as np + volumeNode = slicer.util.addVolumeFromArray(np.ones((30, 40, 50), 'int8') * 120, + np.diag([0.2, 0.2, 0.5, 1.0]), nodeClassName="vtkMRMLLabelMapVolumeNode") + """ + import slicer + from vtk import vtkMatrix4x4 - if name is None: - name = "" - if nodeClassName is None: - nodeClassName = "vtkMRMLScalarVolumeNode" + if name is None: + name = "" + if nodeClassName is None: + nodeClassName = "vtkMRMLScalarVolumeNode" - volumeNode = slicer.mrmlScene.AddNewNodeByClass(nodeClassName, name) - if ijkToRAS is not None: - if not isinstance(ijkToRAS, vtkMatrix4x4): - ijkToRAS = vtkMatrixFromArray(ijkToRAS) - volumeNode.SetIJKToRASMatrix(ijkToRAS) - updateVolumeFromArray(volumeNode, narray) - volumeNode.CreateDefaultDisplayNodes() + volumeNode = slicer.mrmlScene.AddNewNodeByClass(nodeClassName, name) + if ijkToRAS is not None: + if not isinstance(ijkToRAS, vtkMatrix4x4): + ijkToRAS = vtkMatrixFromArray(ijkToRAS) + volumeNode.SetIJKToRASMatrix(ijkToRAS) + updateVolumeFromArray(volumeNode, narray) + volumeNode.CreateDefaultDisplayNodes() - return volumeNode + return volumeNode def arrayFromTableColumn(tableNode, columnName): - """Return values of a table node's column as numpy array. + """Return values of a table node's column as numpy array. - Values can be modified by modifying the numpy array. - After all modifications has been completed, call :py:meth:`arrayFromTableColumnModified`. + Values can be modified by modifying the numpy array. + After all modifications has been completed, call :py:meth:`arrayFromTableColumnModified`. - .. warning:: Important: memory area of the returned array is managed by VTK, - therefore values in the array may be changed, but the array must not be reallocated. - See :py:meth:`arrayFromVolume` for details. - """ - import vtk.util.numpy_support - columnData = tableNode.GetTable().GetColumnByName(columnName) - narray = vtk.util.numpy_support.vtk_to_numpy(columnData) - return narray + .. warning:: Important: memory area of the returned array is managed by VTK, + therefore values in the array may be changed, but the array must not be reallocated. + See :py:meth:`arrayFromVolume` for details. + """ + import vtk.util.numpy_support + columnData = tableNode.GetTable().GetColumnByName(columnName) + narray = vtk.util.numpy_support.vtk_to_numpy(columnData) + return narray def arrayFromTableColumnModified(tableNode, columnName): - """Indicate that modification of a numpy array returned by :py:meth:`arrayFromTableColumn` has been completed.""" - columnData = tableNode.GetTable().GetColumnByName(columnName) - columnData.Modified() - tableNode.GetTable().Modified() + """Indicate that modification of a numpy array returned by :py:meth:`arrayFromTableColumn` has been completed.""" + columnData = tableNode.GetTable().GetColumnByName(columnName) + columnData.Modified() + tableNode.GetTable().Modified() def updateTableFromArray(tableNode, narrays, columnNames=None): - """Set values in a table node from a numpy array. + """Set values in a table node from a numpy array. - :param columnNames: may contain a string or list of strings that will be used as column name(s). - :raises ValueError: in case of failure + :param columnNames: may contain a string or list of strings that will be used as column name(s). + :raises ValueError: in case of failure - Values are copied, therefore if the numpy array is modified after calling this method, - values in the table node will not change. - All previous content of the table is deleted. + Values are copied, therefore if the numpy array is modified after calling this method, + values in the table node will not change. + All previous content of the table is deleted. - Example:: + Example:: + import numpy as np + histogram = np.histogram(arrayFromVolume(getNode('MRHead'))) + tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") + updateTableFromArray(tableNode, histogram, ["Count", "Intensity"]) + """ import numpy as np - histogram = np.histogram(arrayFromVolume(getNode('MRHead'))) - tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") - updateTableFromArray(tableNode, histogram, ["Count", "Intensity"]) - """ - import numpy as np - import vtk.util.numpy_support - import slicer - - if tableNode is None: - tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") - if isinstance(narrays, np.ndarray) and len(narrays.shape) == 1: - ncolumns = [narrays] - elif isinstance(narrays, np.ndarray) and len(narrays.shape) == 2: - ncolumns = narrays.T - elif isinstance(narrays, tuple) or isinstance(narrays, list): - ncolumns = narrays - else: - raise ValueError('Expected narrays is a numpy ndarray, or tuple or list of numpy ndarrays, got %s instead.' % (str(type(narrays)))) - tableNode.RemoveAllColumns() - # Convert single string to a single-element string list - if columnNames is None: - columnNames = [] - if isinstance(columnNames, str): - columnNames = [columnNames] - for columnIndex, ncolumn in enumerate(ncolumns): - vcolumn = vtk.util.numpy_support.numpy_to_vtk(num_array=ncolumn.ravel(),deep=True,array_type=vtk.VTK_FLOAT) - if (columnNames is not None) and (columnIndex < len(columnNames)): - vcolumn.SetName(columnNames[columnIndex]) - tableNode.AddColumn(vcolumn) - return tableNode + import vtk.util.numpy_support + import slicer + + if tableNode is None: + tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") + if isinstance(narrays, np.ndarray) and len(narrays.shape) == 1: + ncolumns = [narrays] + elif isinstance(narrays, np.ndarray) and len(narrays.shape) == 2: + ncolumns = narrays.T + elif isinstance(narrays, tuple) or isinstance(narrays, list): + ncolumns = narrays + else: + raise ValueError('Expected narrays is a numpy ndarray, or tuple or list of numpy ndarrays, got %s instead.' % (str(type(narrays)))) + tableNode.RemoveAllColumns() + # Convert single string to a single-element string list + if columnNames is None: + columnNames = [] + if isinstance(columnNames, str): + columnNames = [columnNames] + for columnIndex, ncolumn in enumerate(ncolumns): + vcolumn = vtk.util.numpy_support.numpy_to_vtk(num_array=ncolumn.ravel(), deep=True, array_type=vtk.VTK_FLOAT) + if (columnNames is not None) and (columnIndex < len(columnNames)): + vcolumn.SetName(columnNames[columnIndex]) + tableNode.AddColumn(vcolumn) + return tableNode def dataframeFromTable(tableNode): - """Convert table node content to pandas dataframe. + """Convert table node content to pandas dataframe. - Table content is copied. Therefore, changes in table node do not affect the dataframe, - and dataframe changes do not affect the original table node. - """ - try: - # Suppress "lzma compression not available" UserWarning when loading pandas - import warnings - with warnings.catch_warnings(): - warnings.simplefilter(action='ignore', category=UserWarning) - import pandas as pd - except ImportError: - raise ImportError("Failed to convert to pandas dataframe. Please install pandas by running `slicer.util.pip_install('pandas')`") - dataframe = pd.DataFrame() - vtable = tableNode.GetTable() - for columnIndex in range (vtable.GetNumberOfColumns()): - vcolumn = vtable.GetColumn(columnIndex) - column = [] - numberOfComponents = vcolumn.GetNumberOfComponents() - if numberOfComponents==1: - # most common, simple case - for rowIndex in range(vcolumn.GetNumberOfValues()): - column.append(vcolumn.GetValue(rowIndex)) - else: - # rare case: column contains multiple components - valueIndex = 0 - for rowIndex in range(vcolumn.GetNumberOfTuples()): - item = [] - for componentIndex in range(numberOfComponents): - item.append(vcolumn.GetValue(valueIndex)) - valueIndex += 1 - column.append(item) - dataframe[vcolumn.GetName()] = column - return dataframe + Table content is copied. Therefore, changes in table node do not affect the dataframe, + and dataframe changes do not affect the original table node. + """ + try: + # Suppress "lzma compression not available" UserWarning when loading pandas + import warnings + with warnings.catch_warnings(): + warnings.simplefilter(action='ignore', category=UserWarning) + import pandas as pd + except ImportError: + raise ImportError("Failed to convert to pandas dataframe. Please install pandas by running `slicer.util.pip_install('pandas')`") + dataframe = pd.DataFrame() + vtable = tableNode.GetTable() + for columnIndex in range(vtable.GetNumberOfColumns()): + vcolumn = vtable.GetColumn(columnIndex) + column = [] + numberOfComponents = vcolumn.GetNumberOfComponents() + if numberOfComponents == 1: + # most common, simple case + for rowIndex in range(vcolumn.GetNumberOfValues()): + column.append(vcolumn.GetValue(rowIndex)) + else: + # rare case: column contains multiple components + valueIndex = 0 + for rowIndex in range(vcolumn.GetNumberOfTuples()): + item = [] + for componentIndex in range(numberOfComponents): + item.append(vcolumn.GetValue(valueIndex)) + valueIndex += 1 + column.append(item) + dataframe[vcolumn.GetName()] = column + return dataframe def dataframeFromMarkups(markupsNode): - """Convert markups node content to pandas dataframe. + """Convert markups node content to pandas dataframe. - Markups content is copied. Therefore, changes in markups node do not affect the dataframe, - and dataframe changes do not affect the original markups node. - """ - try: - # Suppress "lzma compression not available" UserWarning when loading pandas - import warnings - with warnings.catch_warnings(): - warnings.simplefilter(action='ignore', category=UserWarning) - import pandas as pd - except ImportError: - raise ImportError("Failed to convert to pandas dataframe. Please install pandas by running `slicer.util.pip_install('pandas')`") - - label = [] - description = [] - positionWorldR = [] - positionWorldA = [] - positionWorldS = [] - selected = [] - visible = [] - - numberOfControlPoints = markupsNode.GetNumberOfControlPoints() - for controlPointIndex in range(numberOfControlPoints): - label.append(markupsNode.GetNthControlPointLabel(controlPointIndex)) - description.append(markupsNode.GetNthControlPointDescription(controlPointIndex)) - p=[0,0,0] - markupsNode.GetNthControlPointPositionWorld(controlPointIndex, p) - positionWorldR.append(p[0]) - positionWorldA.append(p[1]) - positionWorldS.append(p[2]) - selected.append(markupsNode.GetNthControlPointSelected(controlPointIndex) != 0) - visible.append(markupsNode.GetNthControlPointVisibility(controlPointIndex) != 0) - - dataframe = pd.DataFrame({ - 'label': label, - 'position.R': positionWorldR, - 'position.A': positionWorldA, - 'position.S': positionWorldS, - 'selected': selected, - 'visible': visible, - 'description': description}) - return dataframe + Markups content is copied. Therefore, changes in markups node do not affect the dataframe, + and dataframe changes do not affect the original markups node. + """ + try: + # Suppress "lzma compression not available" UserWarning when loading pandas + import warnings + with warnings.catch_warnings(): + warnings.simplefilter(action='ignore', category=UserWarning) + import pandas as pd + except ImportError: + raise ImportError("Failed to convert to pandas dataframe. Please install pandas by running `slicer.util.pip_install('pandas')`") + + label = [] + description = [] + positionWorldR = [] + positionWorldA = [] + positionWorldS = [] + selected = [] + visible = [] + + numberOfControlPoints = markupsNode.GetNumberOfControlPoints() + for controlPointIndex in range(numberOfControlPoints): + label.append(markupsNode.GetNthControlPointLabel(controlPointIndex)) + description.append(markupsNode.GetNthControlPointDescription(controlPointIndex)) + p = [0, 0, 0] + markupsNode.GetNthControlPointPositionWorld(controlPointIndex, p) + positionWorldR.append(p[0]) + positionWorldA.append(p[1]) + positionWorldS.append(p[2]) + selected.append(markupsNode.GetNthControlPointSelected(controlPointIndex) != 0) + visible.append(markupsNode.GetNthControlPointVisibility(controlPointIndex) != 0) + + dataframe = pd.DataFrame({ + 'label': label, + 'position.R': positionWorldR, + 'position.A': positionWorldA, + 'position.S': positionWorldS, + 'selected': selected, + 'visible': visible, + 'description': description}) + return dataframe # @@ -2394,342 +2432,342 @@ def dataframeFromMarkups(markupsNode): # class VTKObservationMixin: - def __init__(self): - from weakref import WeakKeyDictionary - - super().__init__() - - self.__observations = WeakKeyDictionary() - # {obj: {event: {method: (group, tag, priority)}}} - - @property - def Observations(self): - return [ - (obj, event, method, group, tag, priority) - for obj, events in self.__observations.items() - for event, methods in events.items() - for method, (group, tag, priority) in methods.items() - ] - - def removeObservers(self, method=None): - if method is None: - for obj, _, _, _, tag, _ in self.Observations: - obj.RemoveObserver(tag) - self.__observations.clear() - else: - for obj, events in self.__observations.items(): - for e, methods in events.items(): - if method in methods: - g, t, p = methods.pop(method) - obj.RemoveObserver(t) + def __init__(self): + from weakref import WeakKeyDictionary + + super().__init__() + + self.__observations = WeakKeyDictionary() + # {obj: {event: {method: (group, tag, priority)}}} + + @property + def Observations(self): + return [ + (obj, event, method, group, tag, priority) + for obj, events in self.__observations.items() + for event, methods in events.items() + for method, (group, tag, priority) in methods.items() + ] + + def removeObservers(self, method=None): + if method is None: + for obj, _, _, _, tag, _ in self.Observations: + obj.RemoveObserver(tag) + self.__observations.clear() + else: + for obj, events in self.__observations.items(): + for e, methods in events.items(): + if method in methods: + g, t, p = methods.pop(method) + obj.RemoveObserver(t) - def addObserver(self, obj, event, method, group='none', priority=0.0): - from warnings import warn + def addObserver(self, obj, event, method, group='none', priority=0.0): + from warnings import warn - events = self.__observations.setdefault(obj, {}) - methods = events.setdefault(event, {}) + events = self.__observations.setdefault(obj, {}) + methods = events.setdefault(event, {}) - if method in methods: - warn('already has observer') - return + if method in methods: + warn('already has observer') + return - tag = obj.AddObserver(event, method, priority) - methods[method] = group, tag, priority + tag = obj.AddObserver(event, method, priority) + methods[method] = group, tag, priority - def removeObserver(self, obj, event, method): - from warnings import warn + def removeObserver(self, obj, event, method): + from warnings import warn - try: - events = self.__observations[obj] - methods = events[event] - group, tag, priority = methods.pop(method) - obj.RemoveObserver(tag) - except KeyError: - warn('does not have observer') - - def getObserver(self, obj, event, method, default=None): - try: - events = self.__observations[obj] - methods = events[event] - group, tag, priority = methods[method] - return group, tag, priority - except KeyError: - return default + try: + events = self.__observations[obj] + methods = events[event] + group, tag, priority = methods.pop(method) + obj.RemoveObserver(tag) + except KeyError: + warn('does not have observer') + + def getObserver(self, obj, event, method, default=None): + try: + events = self.__observations[obj] + methods = events[event] + group, tag, priority = methods[method] + return group, tag, priority + except KeyError: + return default - def hasObserver(self, obj, event, method): - return self.getObserver(obj, event, method) is not None + def hasObserver(self, obj, event, method): + return self.getObserver(obj, event, method) is not None - def observer(self, event, method, default=None): - for obj, events in self.__observations.items(): - if self.hasObserver(obj, event, method): - return obj + def observer(self, event, method, default=None): + for obj, events in self.__observations.items(): + if self.hasObserver(obj, event, method): + return obj - return default + return default def toVTKString(text): - """Convert unicode string into VTK string. + """Convert unicode string into VTK string. - .. deprecated:: 4.11.0 - Since now VTK assumes that all strings are in UTF-8 and all strings in Slicer are UTF-8, too, - conversion is no longer necessary. - The method is only kept for backward compatibility and will be removed in the future. - """ - import logging - logging.warning("toVTKString is deprecated! Conversion is no longer necessary.") - import traceback - logging.debug("toVTKString was called from " + ("".join(traceback.format_stack()))) - return text + .. deprecated:: 4.11.0 + Since now VTK assumes that all strings are in UTF-8 and all strings in Slicer are UTF-8, too, + conversion is no longer necessary. + The method is only kept for backward compatibility and will be removed in the future. + """ + import logging + logging.warning("toVTKString is deprecated! Conversion is no longer necessary.") + import traceback + logging.debug("toVTKString was called from " + ("".join(traceback.format_stack()))) + return text def toLatin1String(text): - """Convert string to latin1 encoding.""" - vtkStr = "" - for c in text: - try: - cc = c.encode("latin1", "ignore").decode() - except (UnicodeDecodeError): - cc = "?" - vtkStr = vtkStr + cc - return vtkStr + """Convert string to latin1 encoding.""" + vtkStr = "" + for c in text: + try: + cc = c.encode("latin1", "ignore").decode() + except (UnicodeDecodeError): + cc = "?" + vtkStr = vtkStr + cc + return vtkStr # # File Utilities # -def tempDirectory(key='__SlicerTemp__',tempDir=None,includeDateTime=True): - """Come up with a unique directory name in the temp dir and make it and return it +def tempDirectory(key='__SlicerTemp__', tempDir=None, includeDateTime=True): + """Come up with a unique directory name in the temp dir and make it and return it - Note: this directory is not automatically cleaned up - """ - # TODO: switch to QTemporaryDir in Qt5. - import qt, slicer - if not tempDir: - tempDir = qt.QDir(slicer.app.temporaryPath) - if includeDateTime: - tempDirName = key + qt.QDateTime().currentDateTime().toString("yyyy-MM-dd_hh+mm+ss.zzz") - else: - tempDirName = key - fileInfo = qt.QFileInfo(qt.QDir(tempDir), tempDirName) - dirPath = fileInfo.absoluteFilePath() - qt.QDir().mkpath(dirPath) - return dirPath + Note: this directory is not automatically cleaned up + """ + # TODO: switch to QTemporaryDir in Qt5. + import qt, slicer + if not tempDir: + tempDir = qt.QDir(slicer.app.temporaryPath) + if includeDateTime: + tempDirName = key + qt.QDateTime().currentDateTime().toString("yyyy-MM-dd_hh+mm+ss.zzz") + else: + tempDirName = key + fileInfo = qt.QFileInfo(qt.QDir(tempDir), tempDirName) + dirPath = fileInfo.absoluteFilePath() + qt.QDir().mkpath(dirPath) + return dirPath def delayDisplay(message, autoCloseMsec=1000, parent=None, **kwargs): - """Display an information message in a popup window for a short time. + """Display an information message in a popup window for a short time. - If ``autoCloseMsec < 0`` then the window is not closed until the user clicks on it + If ``autoCloseMsec < 0`` then the window is not closed until the user clicks on it - If ``0 <= autoCloseMsec < 400`` then only ``slicer.app.processEvents()`` is called. + If ``0 <= autoCloseMsec < 400`` then only ``slicer.app.processEvents()`` is called. - If ``autoCloseMsec >= 400`` then the window is closed after waiting for autoCloseMsec milliseconds - """ - import qt, slicer - import logging - logging.info(message) - if 0 <= autoCloseMsec < 400: - slicer.app.processEvents() - return - messagePopup = qt.QDialog(parent if parent else mainWindow()) - for key, value in kwargs.items(): - if hasattr(messagePopup, key): - setattr(messagePopup, key, value) - layout = qt.QVBoxLayout() - messagePopup.setLayout(layout) - label = qt.QLabel(message,messagePopup) - layout.addWidget(label) - if autoCloseMsec >= 0: - qt.QTimer.singleShot(autoCloseMsec, messagePopup.close) - else: - okButton = qt.QPushButton("OK") - layout.addWidget(okButton) - okButton.connect('clicked()', messagePopup.close) - # Windows 10 peek feature in taskbar shows all hidden but not destroyed windows - # (after creating and closing a messagebox, hovering over the mouse on Slicer icon, moving up the - # mouse to the peek thumbnail would show it again). - # Popup windows in other Qt applications often show closed popups (such as - # Paraview's Edit / Find data dialog, MeshMixer's File/Preferences dialog). - # By calling deleteLater, the messagebox is permanently deleted when the current call is completed. - messagePopup.deleteLater() - messagePopup.exec_() + If ``autoCloseMsec >= 400`` then the window is closed after waiting for autoCloseMsec milliseconds + """ + import qt, slicer + import logging + logging.info(message) + if 0 <= autoCloseMsec < 400: + slicer.app.processEvents() + return + messagePopup = qt.QDialog(parent if parent else mainWindow()) + for key, value in kwargs.items(): + if hasattr(messagePopup, key): + setattr(messagePopup, key, value) + layout = qt.QVBoxLayout() + messagePopup.setLayout(layout) + label = qt.QLabel(message, messagePopup) + layout.addWidget(label) + if autoCloseMsec >= 0: + qt.QTimer.singleShot(autoCloseMsec, messagePopup.close) + else: + okButton = qt.QPushButton("OK") + layout.addWidget(okButton) + okButton.connect('clicked()', messagePopup.close) + # Windows 10 peek feature in taskbar shows all hidden but not destroyed windows + # (after creating and closing a messagebox, hovering over the mouse on Slicer icon, moving up the + # mouse to the peek thumbnail would show it again). + # Popup windows in other Qt applications often show closed popups (such as + # Paraview's Edit / Find data dialog, MeshMixer's File/Preferences dialog). + # By calling deleteLater, the messagebox is permanently deleted when the current call is completed. + messagePopup.deleteLater() + messagePopup.exec_() def infoDisplay(text, windowTitle=None, parent=None, standardButtons=None, **kwargs): - """Display popup with a info message. + """Display popup with a info message. - If there is no main window, or if the application is running in testing mode (``slicer.app.testingEnabled() == True``), - then the text is only logged (at info level). - """ - import qt, logging - standardButtons = standardButtons if standardButtons else qt.QMessageBox.Ok - _messageDisplay(logging.INFO, text, None, parent=parent, windowTitle=windowTitle, mainWindowNeeded=True, - icon=qt.QMessageBox.Information, standardButtons=standardButtons, **kwargs) + If there is no main window, or if the application is running in testing mode (``slicer.app.testingEnabled() == True``), + then the text is only logged (at info level). + """ + import qt, logging + standardButtons = standardButtons if standardButtons else qt.QMessageBox.Ok + _messageDisplay(logging.INFO, text, None, parent=parent, windowTitle=windowTitle, mainWindowNeeded=True, + icon=qt.QMessageBox.Information, standardButtons=standardButtons, **kwargs) def warningDisplay(text, windowTitle=None, parent=None, standardButtons=None, **kwargs): - """Display popup with a warning message. + """Display popup with a warning message. - If there is no main window, or if the application is running in testing mode (``slicer.app.testingEnabled() == True``), - then the text is only logged (at warning level). - """ - import qt, logging - standardButtons = standardButtons if standardButtons else qt.QMessageBox.Ok - _messageDisplay(logging.WARNING, text, None, parent=parent, windowTitle=windowTitle, mainWindowNeeded=True, - icon=qt.QMessageBox.Warning, standardButtons=standardButtons, **kwargs) + If there is no main window, or if the application is running in testing mode (``slicer.app.testingEnabled() == True``), + then the text is only logged (at warning level). + """ + import qt, logging + standardButtons = standardButtons if standardButtons else qt.QMessageBox.Ok + _messageDisplay(logging.WARNING, text, None, parent=parent, windowTitle=windowTitle, mainWindowNeeded=True, + icon=qt.QMessageBox.Warning, standardButtons=standardButtons, **kwargs) def errorDisplay(text, windowTitle=None, parent=None, standardButtons=None, **kwargs): - """Display an error popup. + """Display an error popup. - If there is no main window, or if the application is running in testing mode (``slicer.app.testingEnabled() == True``), - then the text is only logged (at error level). - """ - import qt, logging - standardButtons = standardButtons if standardButtons else qt.QMessageBox.Ok - _messageDisplay(logging.ERROR, text, None, parent=parent, windowTitle=windowTitle, mainWindowNeeded=True, - icon=qt.QMessageBox.Critical, standardButtons=standardButtons, **kwargs) + If there is no main window, or if the application is running in testing mode (``slicer.app.testingEnabled() == True``), + then the text is only logged (at error level). + """ + import qt, logging + standardButtons = standardButtons if standardButtons else qt.QMessageBox.Ok + _messageDisplay(logging.ERROR, text, None, parent=parent, windowTitle=windowTitle, mainWindowNeeded=True, + icon=qt.QMessageBox.Critical, standardButtons=standardButtons, **kwargs) def confirmOkCancelDisplay(text, windowTitle=None, parent=None, **kwargs): - """Display a confirmation popup. Return if confirmed with OK. + """Display a confirmation popup. Return if confirmed with OK. - When the application is running in testing mode (``slicer.app.testingEnabled() == True``), - the popup is skipped and True ("Ok") is returned, with a message being logged to indicate this. - """ - import qt, slicer, logging - if not windowTitle: - windowTitle = slicer.app.applicationName + " confirmation" - result = _messageDisplay(logging.INFO, text, True, parent=parent, windowTitle=windowTitle, icon=qt.QMessageBox.Question, - standardButtons=qt.QMessageBox.Ok | qt.QMessageBox.Cancel, **kwargs) - return result == qt.QMessageBox.Ok + When the application is running in testing mode (``slicer.app.testingEnabled() == True``), + the popup is skipped and True ("Ok") is returned, with a message being logged to indicate this. + """ + import qt, slicer, logging + if not windowTitle: + windowTitle = slicer.app.applicationName + " confirmation" + result = _messageDisplay(logging.INFO, text, True, parent=parent, windowTitle=windowTitle, icon=qt.QMessageBox.Question, + standardButtons=qt.QMessageBox.Ok | qt.QMessageBox.Cancel, **kwargs) + return result == qt.QMessageBox.Ok def confirmYesNoDisplay(text, windowTitle=None, parent=None, **kwargs): - """Display a confirmation popup. Return if confirmed with Yes. + """Display a confirmation popup. Return if confirmed with Yes. - When the application is running in testing mode (``slicer.app.testingEnabled() == True``), - the popup is skipped and True ("Yes") is returned, with a message being logged to indicate this. - """ - import qt, slicer, logging - if not windowTitle: - windowTitle = slicer.app.applicationName + " confirmation" - result = _messageDisplay(logging.INFO, text, True, parent=parent, windowTitle=windowTitle, icon=qt.QMessageBox.Question, - standardButtons=qt.QMessageBox.Yes | qt.QMessageBox.No, **kwargs) - return result == qt.QMessageBox.Yes + When the application is running in testing mode (``slicer.app.testingEnabled() == True``), + the popup is skipped and True ("Yes") is returned, with a message being logged to indicate this. + """ + import qt, slicer, logging + if not windowTitle: + windowTitle = slicer.app.applicationName + " confirmation" + result = _messageDisplay(logging.INFO, text, True, parent=parent, windowTitle=windowTitle, icon=qt.QMessageBox.Question, + standardButtons=qt.QMessageBox.Yes | qt.QMessageBox.No, **kwargs) + return result == qt.QMessageBox.Yes def confirmRetryCloseDisplay(text, windowTitle=None, parent=None, **kwargs): - """Display an error popup asking whether to retry, logging the text at error level. - Return if confirmed with Retry. + """Display an error popup asking whether to retry, logging the text at error level. + Return if confirmed with Retry. - When the application is running in testing mode (``slicer.app.testingEnabled() == True``), - the popup is skipped and False ("Close") is returned, with a message being logged to indicate this. - """ - import qt, logging - result = _messageDisplay(logging.ERROR, text, False, parent=parent, windowTitle=windowTitle, - icon=qt.QMessageBox.Critical, standardButtons=qt.QMessageBox.Retry | qt.QMessageBox.Close, **kwargs) - return result == qt.QMessageBox.Retry + When the application is running in testing mode (``slicer.app.testingEnabled() == True``), + the popup is skipped and False ("Close") is returned, with a message being logged to indicate this. + """ + import qt, logging + result = _messageDisplay(logging.ERROR, text, False, parent=parent, windowTitle=windowTitle, + icon=qt.QMessageBox.Critical, standardButtons=qt.QMessageBox.Retry | qt.QMessageBox.Close, **kwargs) + return result == qt.QMessageBox.Retry def _messageDisplay(logLevel, text, testingReturnValue, mainWindowNeeded=False, parent=None, windowTitle=None, **kwargs): - """Displays a messagebox and logs message text; knows what to do in testing mode. - - :param logLevel: The level at which to log text, e.g. ``logging.INFO``, ``logging.ERROR`` - :param text: Message text - :type text: str - :param testingReturnValue: When the application is in testing mode, this value is returned instead of raising the message box. - :param mainWindowNeeded: If True then the message box will not be raised if there is no mainWindow, but the text is still logged. - :type mainWindowNeeded: bool, optional - :param parent: The message box parent; by default it is set to main window by slicer.util.messageBox - :type parent: QWidget, optional - :param windowTitle: Window title; defaults to a generic title based on log level. - :type windowTitle: str, optional - :param kwargs: passed to :func:`messageBox` - - Returns: - The output of :func:`messageBox`, with two exceptions: - - If the application is running in testing mode, then ``testingReturnValue`` is returned. - - Otherwise, if ``mainWindowNeeded`` is True and there is no main window, then None is returned. - """ - import slicer, logging - logging.log(logLevel, text) - logLevelString = logging.getLevelName(logLevel).lower() # e.g. this is "error" when logLevel is logging.ERROR - if not windowTitle: - windowTitle = slicer.app.applicationName + " " + logLevelString - if slicer.app.testingEnabled(): - logging.info("Testing mode is enabled: Returning %s and skipping message box [%s]." % (testingReturnValue, windowTitle)) - return testingReturnValue - if mainWindowNeeded and mainWindow() is None: - return - return messageBox(text, parent=parent, windowTitle=windowTitle, **kwargs) + """Displays a messagebox and logs message text; knows what to do in testing mode. + + :param logLevel: The level at which to log text, e.g. ``logging.INFO``, ``logging.ERROR`` + :param text: Message text + :type text: str + :param testingReturnValue: When the application is in testing mode, this value is returned instead of raising the message box. + :param mainWindowNeeded: If True then the message box will not be raised if there is no mainWindow, but the text is still logged. + :type mainWindowNeeded: bool, optional + :param parent: The message box parent; by default it is set to main window by slicer.util.messageBox + :type parent: QWidget, optional + :param windowTitle: Window title; defaults to a generic title based on log level. + :type windowTitle: str, optional + :param kwargs: passed to :func:`messageBox` + + Returns: + The output of :func:`messageBox`, with two exceptions: + - If the application is running in testing mode, then ``testingReturnValue`` is returned. + - Otherwise, if ``mainWindowNeeded`` is True and there is no main window, then None is returned. + """ + import slicer, logging + logging.log(logLevel, text) + logLevelString = logging.getLevelName(logLevel).lower() # e.g. this is "error" when logLevel is logging.ERROR + if not windowTitle: + windowTitle = slicer.app.applicationName + " " + logLevelString + if slicer.app.testingEnabled(): + logging.info("Testing mode is enabled: Returning %s and skipping message box [%s]." % (testingReturnValue, windowTitle)) + return testingReturnValue + if mainWindowNeeded and mainWindow() is None: + return + return messageBox(text, parent=parent, windowTitle=windowTitle, **kwargs) def messageBox(text, parent=None, **kwargs): - """Displays a messagebox. + """Displays a messagebox. - ctkMessageBox is used instead of a default qMessageBox to provide "Don't show again" checkbox. + ctkMessageBox is used instead of a default qMessageBox to provide "Don't show again" checkbox. - For example:: + For example:: - slicer.util.messageBox("Some message", dontShowAgainSettingsKey = "MainWindow/DontShowSomeMessage") + slicer.util.messageBox("Some message", dontShowAgainSettingsKey = "MainWindow/DontShowSomeMessage") - When the application is running in testing mode (``slicer.app.testingEnabled() == True``), - an auto-closing popup with a delay of 3s is shown using :func:`delayDisplay()` and ``qt.QMessageBox.Ok`` - is returned, with the text being logged to indicate this. - """ - import logging, qt, slicer - if slicer.app.testingEnabled(): - testingReturnValue = qt.QMessageBox.Ok - logging.info("Testing mode is enabled: Returning %s (qt.QMessageBox.Ok) and displaying an auto-closing message box [%s]." % (testingReturnValue, text)) - slicer.util.delayDisplay(text, autoCloseMsec=3000, parent=parent, **kwargs) - return testingReturnValue - - import ctk - mbox = ctk.ctkMessageBox(parent if parent else mainWindow()) - mbox.text = text - for key, value in kwargs.items(): - if hasattr(mbox, key): - setattr(mbox, key, value) - # Windows 10 peek feature in taskbar shows all hidden but not destroyed windows - # (after creating and closing a messagebox, hovering over the mouse on Slicer icon, moving up the - # mouse to the peek thumbnail would show it again). - # Popup windows in other Qt applications often show closed popups (such as - # Paraview's Edit / Find data dialog, MeshMixer's File/Preferences dialog). - # By calling deleteLater, the messagebox is permanently deleted when the current call is completed. - mbox.deleteLater() - return mbox.exec_() + When the application is running in testing mode (``slicer.app.testingEnabled() == True``), + an auto-closing popup with a delay of 3s is shown using :func:`delayDisplay()` and ``qt.QMessageBox.Ok`` + is returned, with the text being logged to indicate this. + """ + import logging, qt, slicer + if slicer.app.testingEnabled(): + testingReturnValue = qt.QMessageBox.Ok + logging.info("Testing mode is enabled: Returning %s (qt.QMessageBox.Ok) and displaying an auto-closing message box [%s]." % (testingReturnValue, text)) + slicer.util.delayDisplay(text, autoCloseMsec=3000, parent=parent, **kwargs) + return testingReturnValue + + import ctk + mbox = ctk.ctkMessageBox(parent if parent else mainWindow()) + mbox.text = text + for key, value in kwargs.items(): + if hasattr(mbox, key): + setattr(mbox, key, value) + # Windows 10 peek feature in taskbar shows all hidden but not destroyed windows + # (after creating and closing a messagebox, hovering over the mouse on Slicer icon, moving up the + # mouse to the peek thumbnail would show it again). + # Popup windows in other Qt applications often show closed popups (such as + # Paraview's Edit / Find data dialog, MeshMixer's File/Preferences dialog). + # By calling deleteLater, the messagebox is permanently deleted when the current call is completed. + mbox.deleteLater() + return mbox.exec_() def createProgressDialog(parent=None, value=0, maximum=100, labelText="", windowTitle="Processing...", **kwargs): - """Display a modal QProgressDialog. + """Display a modal QProgressDialog. - Go to `QProgressDialog documentation `_ to - learn about the available keyword arguments. + Go to `QProgressDialog documentation `_ to + learn about the available keyword arguments. - Examples:: + Examples:: - # Prevent progress dialog from automatically closing - progressbar = createProgressDialog(autoClose=False) + # Prevent progress dialog from automatically closing + progressbar = createProgressDialog(autoClose=False) - # Update progress value - progressbar.value = 50 + # Update progress value + progressbar.value = 50 - # Update label text - progressbar.labelText = "processing XYZ" - """ - import qt - progressIndicator = qt.QProgressDialog(parent if parent else mainWindow()) - progressIndicator.minimumDuration = 0 - progressIndicator.maximum = maximum - progressIndicator.value = value - progressIndicator.windowTitle = windowTitle - progressIndicator.labelText = labelText - for key, value in kwargs.items(): - if hasattr(progressIndicator, key): - setattr(progressIndicator, key, value) - return progressIndicator + # Update label text + progressbar.labelText = "processing XYZ" + """ + import qt + progressIndicator = qt.QProgressDialog(parent if parent else mainWindow()) + progressIndicator.minimumDuration = 0 + progressIndicator.maximum = maximum + progressIndicator.value = value + progressIndicator.windowTitle = windowTitle + progressIndicator.labelText = labelText + for key, value in kwargs.items(): + if hasattr(progressIndicator, key): + setattr(progressIndicator, key, value) + return progressIndicator from contextlib import contextmanager @@ -2737,817 +2775,817 @@ def createProgressDialog(parent=None, value=0, maximum=100, labelText="", window @contextmanager def displayPythonShell(display=True): - """Show the Python console while the code in the context manager is being run. + """Show the Python console while the code in the context manager is being run. - The console stays visible only if it was visible already. + The console stays visible only if it was visible already. - :param display: If show is False, the context manager has no effect. + :param display: If show is False, the context manager has no effect. - .. code-block:: python + .. code-block:: python - with slicer.util.displayPythonShell(): - slicer.util.pip_install('nibabel') + with slicer.util.displayPythonShell(): + slicer.util.pip_install('nibabel') - """ - import slicer + """ + import slicer + + def dockableWindowEnabled(): + import qt + return toBool(qt.QSettings().value("Python/DockableWindow")) + + def getConsole(): + return mainWindow().pythonConsole().parent() if dockableWindowEnabled() else pythonShell() - def dockableWindowEnabled(): - import qt - return toBool(qt.QSettings().value("Python/DockableWindow")) - - def getConsole(): - return mainWindow().pythonConsole().parent() if dockableWindowEnabled() else pythonShell() - - if display: - console = getConsole() - consoleVisible = console.visible - console.show() - slicer.app.processEvents() - try: - yield - finally: if display: - console.setVisible(consoleVisible) + console = getConsole() + consoleVisible = console.visible + console.show() + slicer.app.processEvents() + try: + yield + finally: + if display: + console.setVisible(consoleVisible) class WaitCursor: - """Display a wait cursor while the code in the context manager is being run. + """Display a wait cursor while the code in the context manager is being run. - :param show: If show is False, no wait cursor is shown. + :param show: If show is False, no wait cursor is shown. - .. code-block:: python + .. code-block:: python - import time + import time - n = 2 - with slicer.util.MessageDialog(f'Sleeping for {n} seconds...'): - with slicer.util.WaitCursor(): - time.sleep(n) + n = 2 + with slicer.util.MessageDialog(f'Sleeping for {n} seconds...'): + with slicer.util.WaitCursor(): + time.sleep(n) - """ + """ - def __init__(self, show=True): - """Set the cursor to waiting mode while the code in the context manager is being run. + def __init__(self, show=True): + """Set the cursor to waiting mode while the code in the context manager is being run. - :param show: If show is False, this context manager has no effect. + :param show: If show is False, this context manager has no effect. - .. code-block:: python + .. code-block:: python - import time + import time - with slicer.util.WaitCursor(): - time.sleep(2) - """ - self.show = show + with slicer.util.WaitCursor(): + time.sleep(2) + """ + self.show = show - def __enter__(self): - import qt, slicer - if self.show: - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - slicer.app.processEvents() + def __enter__(self): + import qt, slicer + if self.show: + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + slicer.app.processEvents() - def __exit__(self, type, value, traceback): - if self.show: - import qt - qt.QApplication.restoreOverrideCursor() + def __exit__(self, type, value, traceback): + if self.show: + import qt + qt.QApplication.restoreOverrideCursor() class MessageDialog: - def __init__(self, message, show=True, logLevel=None): - """Log the message and show a message box while the code in the context manager is being run. - When the application is running in testing mode (``slicer.app.testingEnabled() == True``), the message box is skipped. + def __init__(self, message, show=True, logLevel=None): + """Log the message and show a message box while the code in the context manager is being run. + When the application is running in testing mode (``slicer.app.testingEnabled() == True``), the message box is skipped. - :param message: Text shown in the message box. - :param show: If show is False, no dialog is shown. - :param logLevel: Log level used to log the message. Default: logging.INFO + :param message: Text shown in the message box. + :param show: If show is False, no dialog is shown. + :param logLevel: Log level used to log the message. Default: logging.INFO - .. code-block:: python + .. code-block:: python - import time + import time - n = 2 - with slicer.util.MessageDialog(f'Sleeping for {n} seconds...'): - with slicer.util.WaitCursor(): - time.sleep(n) + n = 2 + with slicer.util.MessageDialog(f'Sleeping for {n} seconds...'): + with slicer.util.WaitCursor(): + time.sleep(n) - """ - import logging - import slicer + """ + import logging + import slicer - if logLevel is None: - logLevel = logging.INFO - if not isinstance(logLevel, int): - raise ValueError(f'Invalid log level: {logLevel}') + if logLevel is None: + logLevel = logging.INFO + if not isinstance(logLevel, int): + raise ValueError(f'Invalid log level: {logLevel}') - self.message = message - self.show = show and not slicer.app.testingEnabled() - self.logLevel = logLevel - self.box = None + self.message = message + self.show = show and not slicer.app.testingEnabled() + self.logLevel = logLevel + self.box = None - def __enter__(self): - import logging - logging.log(self.logLevel, self.message) + def __enter__(self): + import logging + logging.log(self.logLevel, self.message) - if self.show: - import qt, slicer - self.box = qt.QMessageBox() - self.box.setStandardButtons(qt.QMessageBox.NoButton) - self.box.setText(self.message) - self.box.show() - slicer.app.processEvents() + if self.show: + import qt, slicer + self.box = qt.QMessageBox() + self.box.setStandardButtons(qt.QMessageBox.NoButton) + self.box.setText(self.message) + self.box.show() + slicer.app.processEvents() - def __exit__(self, type, value, traceback): - if self.show: - self.box.accept() + def __exit__(self, type, value, traceback): + if self.show: + self.box.accept() @contextmanager def tryWithErrorDisplay(message=None, show=True, waitCursor=False): - """Show an error display with the error details if an exception is raised. + """Show an error display with the error details if an exception is raised. - :param message: Text shown in the message box. - :param show: If show is False, the context manager has no effect. - :param waitCursor: If waitCrusor is set to True then mouse cursor is changed to - wait cursor while the context manager is being run. + :param message: Text shown in the message box. + :param show: If show is False, the context manager has no effect. + :param waitCursor: If waitCrusor is set to True then mouse cursor is changed to + wait cursor while the context manager is being run. - .. code-block:: python + .. code-block:: python - import random + import random - def risky(): - if random.choice((True, False)): - raise Exception('Error while trying to do some internal operations.') + def risky(): + if random.choice((True, False)): + raise Exception('Error while trying to do some internal operations.') - with slicer.util.tryWithErrorDisplay("Risky operation failed."): - risky() - """ - try: - if waitCursor: - import slicer, qt - slicer.app.setOverrideCursor(qt.Qt.WaitCursor) - yield - if waitCursor: - slicer.app.restoreOverrideCursor() - except Exception as e: - import slicer - if waitCursor: - slicer.app.restoreOverrideCursor() - if show and not slicer.app.testingEnabled(): - if message is not None: - errorMessage = f'{message}\n\n{e}' - else: - errorMessage = str(e) - import traceback - errorDisplay(errorMessage, detailedText=traceback.format_exc()) - raise + with slicer.util.tryWithErrorDisplay("Risky operation failed."): + risky() + """ + try: + if waitCursor: + import slicer, qt + slicer.app.setOverrideCursor(qt.Qt.WaitCursor) + yield + if waitCursor: + slicer.app.restoreOverrideCursor() + except Exception as e: + import slicer + if waitCursor: + slicer.app.restoreOverrideCursor() + if show and not slicer.app.testingEnabled(): + if message is not None: + errorMessage = f'{message}\n\n{e}' + else: + errorMessage = str(e) + import traceback + errorDisplay(errorMessage, detailedText=traceback.format_exc()) + raise def toBool(value): - """Convert any type of value to a boolean. - - The function uses the following heuristic: - - 1. If the value can be converted to an integer, the integer is then - converted to a boolean. - 2. If the value is a string, return True if it is equal to 'true'. False otherwise. - Note that the comparison is case insensitive. - 3. If the value is neither an integer or a string, the bool() function is applied. - - >>> [toBool(x) for x in range(-2, 2)] - [True, True, False, True] - >>> [toBool(x) for x in ['-2', '-1', '0', '1', '2', 'Hello']] - [True, True, False, True, True, False] - >>> toBool(object()) - True - >>> toBool(None) - False - """ - try: - return bool(int(value)) - except (ValueError, TypeError): - return value.lower() in ['true'] if isinstance(value, str) else bool(value) + """Convert any type of value to a boolean. + + The function uses the following heuristic: + + 1. If the value can be converted to an integer, the integer is then + converted to a boolean. + 2. If the value is a string, return True if it is equal to 'true'. False otherwise. + Note that the comparison is case insensitive. + 3. If the value is neither an integer or a string, the bool() function is applied. + + >>> [toBool(x) for x in range(-2, 2)] + [True, True, False, True] + >>> [toBool(x) for x in ['-2', '-1', '0', '1', '2', 'Hello']] + [True, True, False, True, True, False] + >>> toBool(object()) + True + >>> toBool(None) + False + """ + try: + return bool(int(value)) + except (ValueError, TypeError): + return value.lower() in ['true'] if isinstance(value, str) else bool(value) def settingsValue(key, default, converter=lambda v: v, settings=None): - """Return settings value associated with key if it exists or the provided default otherwise. + """Return settings value associated with key if it exists or the provided default otherwise. - ``settings`` parameter is expected to be a valid ``qt.Settings`` object. - """ - import qt - settings = qt.QSettings() if settings is None else settings - return converter(settings.value(key)) if settings.contains(key) else default + ``settings`` parameter is expected to be a valid ``qt.Settings`` object. + """ + import qt + settings = qt.QSettings() if settings is None else settings + return converter(settings.value(key)) if settings.contains(key) else default -def clickAndDrag(widget,button='Left',start=(10,10),end=(10,40),steps=20,modifiers=[]): - """Send synthetic mouse events to the specified widget (qMRMLSliceWidget or qMRMLThreeDView) +def clickAndDrag(widget, button='Left', start=(10, 10), end=(10, 40), steps=20, modifiers=[]): + """Send synthetic mouse events to the specified widget (qMRMLSliceWidget or qMRMLThreeDView) - :param button: "Left", "Middle", "Right", or "None" - start, end : window coordinates for action - :param steps: number of steps to move in, if <2 then mouse jumps to the end position - :param modifiers: list containing zero or more of "Shift" or "Control" - :raises RuntimeError: in case of failure + :param button: "Left", "Middle", "Right", or "None" + start, end : window coordinates for action + :param steps: number of steps to move in, if <2 then mouse jumps to the end position + :param modifiers: list containing zero or more of "Shift" or "Control" + :raises RuntimeError: in case of failure - .. hint:: + .. hint:: - For generating test data you can use this snippet of code:: + For generating test data you can use this snippet of code:: - layoutManager = slicer.app.layoutManager() - threeDView = layoutManager.threeDWidget(0).threeDView() - style = threeDView.interactorStyle() - interactor = style.GetInteractor() + layoutManager = slicer.app.layoutManager() + threeDView = layoutManager.threeDWidget(0).threeDView() + style = threeDView.interactorStyle() + interactor = style.GetInteractor() - def onClick(caller,event): - print(interactor.GetEventPosition()) + def onClick(caller,event): + print(interactor.GetEventPosition()) - interactor.AddObserver(vtk.vtkCommand.LeftButtonPressEvent, onClick) - """ - style = widget.interactorStyle() - interactor = style.GetInteractor() - if button == 'Left': - down = interactor.LeftButtonPressEvent - up = interactor.LeftButtonReleaseEvent - elif button == 'Right': - down = interactor.RightButtonPressEvent - up = interactor.RightButtonReleaseEvent - elif button == 'Middle': - down = interactor.MiddleButtonPressEvent - up = interactor.MiddleButtonReleaseEvent - elif button == 'None' or not button: - down = lambda : None - up = lambda : None - else: - raise RuntimeError("Bad button - should be Left or Right, not %s" % button) - if 'Shift' in modifiers: - interactor.SetShiftKey(1) - if 'Control' in modifiers: - interactor.SetControlKey(1) - interactor.SetEventPosition(*start) - down() - if (steps<2): - interactor.SetEventPosition(end[0], end[1]) - interactor.MouseMoveEvent() - else: - for step in range(steps): - frac = float(step)/(steps-1) - x = int(start[0] + frac*(end[0]-start[0])) - y = int(start[1] + frac*(end[1]-start[1])) - interactor.SetEventPosition(x,y) - interactor.MouseMoveEvent() - up() - interactor.SetShiftKey(0) - interactor.SetControlKey(0) + interactor.AddObserver(vtk.vtkCommand.LeftButtonPressEvent, onClick) + """ + style = widget.interactorStyle() + interactor = style.GetInteractor() + if button == 'Left': + down = interactor.LeftButtonPressEvent + up = interactor.LeftButtonReleaseEvent + elif button == 'Right': + down = interactor.RightButtonPressEvent + up = interactor.RightButtonReleaseEvent + elif button == 'Middle': + down = interactor.MiddleButtonPressEvent + up = interactor.MiddleButtonReleaseEvent + elif button == 'None' or not button: + down = lambda: None + up = lambda: None + else: + raise RuntimeError("Bad button - should be Left or Right, not %s" % button) + if 'Shift' in modifiers: + interactor.SetShiftKey(1) + if 'Control' in modifiers: + interactor.SetControlKey(1) + interactor.SetEventPosition(*start) + down() + if (steps < 2): + interactor.SetEventPosition(end[0], end[1]) + interactor.MouseMoveEvent() + else: + for step in range(steps): + frac = float(step) / (steps - 1) + x = int(start[0] + frac * (end[0] - start[0])) + y = int(start[1] + frac * (end[1] - start[1])) + interactor.SetEventPosition(x, y) + interactor.MouseMoveEvent() + up() + interactor.SetShiftKey(0) + interactor.SetControlKey(0) def downloadFile(url, targetFilePath, checksum=None, reDownloadIfChecksumInvalid=True): - """Download ``url`` to local storage as ``targetFilePath`` + """Download ``url`` to local storage as ``targetFilePath`` - Target file path needs to indicate the file name and extension as well + Target file path needs to indicate the file name and extension as well - If specified, the ``checksum`` is used to verify that the downloaded file is the expected one. - It must be specified as ``:``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. - """ - import os - import logging - try: - (algo, digest) = extractAlgoAndDigest(checksum) - except ValueError as excinfo: - logging.error('Failed to parse checksum: ' + excinfo.message) - return False - if not os.path.exists(targetFilePath) or os.stat(targetFilePath).st_size == 0: - logging.info(f'Downloading from\n {url}\nas file\n {targetFilePath}\nIt may take a few minutes...') + If specified, the ``checksum`` is used to verify that the downloaded file is the expected one. + It must be specified as ``:``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. + """ + import os + import logging try: - import urllib.request, urllib.parse, urllib.error - urllib.request.urlretrieve(url, targetFilePath) - except Exception as e: - import traceback - traceback.print_exc() - logging.error('Failed to download file from ' + url) - return False - if algo is not None: - logging.info('Verifying checksum\n %s' % targetFilePath) - current_digest = computeChecksum(algo, targetFilePath) - if current_digest != digest: - logging.error('Downloaded file does not have expected checksum.' - '\n current checksum: %s' - '\n expected checksum: %s' % (current_digest, digest)) + (algo, digest) = extractAlgoAndDigest(checksum) + except ValueError as excinfo: + logging.error('Failed to parse checksum: ' + excinfo.message) return False - else: - logging.info('Checksum OK') - else: - if algo is not None: - current_digest = computeChecksum(algo, targetFilePath) - if current_digest != digest: - if reDownloadIfChecksumInvalid: - logging.info('Requested file has been found but its checksum is different: deleting and re-downloading') - os.remove(targetFilePath) - return downloadFile(url, targetFilePath, checksum, reDownloadIfChecksumInvalid=False) - else: - logging.error('Requested file has been found but its checksum is different:' - '\n current checksum: %s' - '\n expected checksum: %s' % (current_digest, digest)) - return False - else: - logging.info('Requested file has been found and checksum is OK: ' + targetFilePath) + if not os.path.exists(targetFilePath) or os.stat(targetFilePath).st_size == 0: + logging.info(f'Downloading from\n {url}\nas file\n {targetFilePath}\nIt may take a few minutes...') + try: + import urllib.request, urllib.parse, urllib.error + urllib.request.urlretrieve(url, targetFilePath) + except Exception as e: + import traceback + traceback.print_exc() + logging.error('Failed to download file from ' + url) + return False + if algo is not None: + logging.info('Verifying checksum\n %s' % targetFilePath) + current_digest = computeChecksum(algo, targetFilePath) + if current_digest != digest: + logging.error('Downloaded file does not have expected checksum.' + '\n current checksum: %s' + '\n expected checksum: %s' % (current_digest, digest)) + return False + else: + logging.info('Checksum OK') else: - logging.info('Requested file has been found: ' + targetFilePath) - return True + if algo is not None: + current_digest = computeChecksum(algo, targetFilePath) + if current_digest != digest: + if reDownloadIfChecksumInvalid: + logging.info('Requested file has been found but its checksum is different: deleting and re-downloading') + os.remove(targetFilePath) + return downloadFile(url, targetFilePath, checksum, reDownloadIfChecksumInvalid=False) + else: + logging.error('Requested file has been found but its checksum is different:' + '\n current checksum: %s' + '\n expected checksum: %s' % (current_digest, digest)) + return False + else: + logging.info('Requested file has been found and checksum is OK: ' + targetFilePath) + else: + logging.info('Requested file has been found: ' + targetFilePath) + return True def extractArchive(archiveFilePath, outputDir, expectedNumberOfExtractedFiles=None): - """ Extract file ``archiveFilePath`` into folder ``outputDir``. + """ Extract file ``archiveFilePath`` into folder ``outputDir``. - Number of expected files unzipped may be specified in ``expectedNumberOfExtractedFiles``. - If folder contains the same number of files as expected (if specified), then it will be - assumed that unzipping has been successfully done earlier. - """ - import os - import logging - from slicer import app - if not os.path.exists(archiveFilePath): - logging.error('Specified file %s does not exist' % (archiveFilePath)) - return False - fileName, fileExtension = os.path.splitext(archiveFilePath) - if fileExtension.lower() != '.zip': - #TODO: Support other archive types - logging.error('Only zip archives are supported now, got ' + fileExtension) - return False + Number of expected files unzipped may be specified in ``expectedNumberOfExtractedFiles``. + If folder contains the same number of files as expected (if specified), then it will be + assumed that unzipping has been successfully done earlier. + """ + import os + import logging + from slicer import app + if not os.path.exists(archiveFilePath): + logging.error('Specified file %s does not exist' % (archiveFilePath)) + return False + fileName, fileExtension = os.path.splitext(archiveFilePath) + if fileExtension.lower() != '.zip': + # TODO: Support other archive types + logging.error('Only zip archives are supported now, got ' + fileExtension) + return False - numOfFilesInOutputDir = len(getFilesInDirectory(outputDir, False)) - if expectedNumberOfExtractedFiles is not None \ - and numOfFilesInOutputDir == expectedNumberOfExtractedFiles: - logging.info(f'File {archiveFilePath} already unzipped into {outputDir}') + numOfFilesInOutputDir = len(getFilesInDirectory(outputDir, False)) + if expectedNumberOfExtractedFiles is not None \ + and numOfFilesInOutputDir == expectedNumberOfExtractedFiles: + logging.info(f'File {archiveFilePath} already unzipped into {outputDir}') + return True + + extractSuccessful = app.applicationLogic().Unzip(archiveFilePath, outputDir) + numOfFilesInOutputDirTest = len(getFilesInDirectory(outputDir, False)) + if extractSuccessful is False or (expectedNumberOfExtractedFiles is not None \ + and numOfFilesInOutputDirTest != expectedNumberOfExtractedFiles): + logging.error(f'Unzipping {archiveFilePath} into {outputDir} failed') + return False + logging.info(f'Unzipping {archiveFilePath} into {outputDir} successful') return True - extractSuccessful = app.applicationLogic().Unzip(archiveFilePath, outputDir) - numOfFilesInOutputDirTest = len(getFilesInDirectory(outputDir, False)) - if extractSuccessful is False or (expectedNumberOfExtractedFiles is not None \ - and numOfFilesInOutputDirTest != expectedNumberOfExtractedFiles): - logging.error(f'Unzipping {archiveFilePath} into {outputDir} failed') - return False - logging.info(f'Unzipping {archiveFilePath} into {outputDir} successful') - return True - def computeChecksum(algo, filePath): - """Compute digest of ``filePath`` using ``algo``. + """Compute digest of ``filePath`` using ``algo``. - Supported hashing algorithms are SHA256, SHA512, and MD5. + Supported hashing algorithms are SHA256, SHA512, and MD5. - It internally reads the file by chunk of 8192 bytes. + It internally reads the file by chunk of 8192 bytes. - :raises ValueError: if algo is unknown. - :raises IOError: if filePath does not exist. - """ - import hashlib + :raises ValueError: if algo is unknown. + :raises IOError: if filePath does not exist. + """ + import hashlib - if algo not in ['SHA256', 'SHA512', 'MD5']: - raise ValueError("unsupported hashing algorithm %s" % algo) + if algo not in ['SHA256', 'SHA512', 'MD5']: + raise ValueError("unsupported hashing algorithm %s" % algo) - with open(filePath, 'rb') as content: - hash = hashlib.new(algo) - while True: - chunk = content.read(8192) - if not chunk: - break - hash.update(chunk) - return hash.hexdigest() + with open(filePath, 'rb') as content: + hash = hashlib.new(algo) + while True: + chunk = content.read(8192) + if not chunk: + break + hash.update(chunk) + return hash.hexdigest() def extractAlgoAndDigest(checksum): - """Given a checksum string formatted as ``:`` returns the tuple ``(algo, digest)``. + """Given a checksum string formatted as ``:`` returns the tuple ``(algo, digest)``. - ```` is expected to be `SHA256`, `SHA512`, or `MD5`. - ```` is expected to be the full length hexadecimal digest. + ```` is expected to be `SHA256`, `SHA512`, or `MD5`. + ```` is expected to be the full length hexadecimal digest. - :raises ValueError: if checksum is incorrectly formatted. - """ - if checksum is None: - return None, None - if len(checksum.split(':')) != 2: - raise ValueError("invalid checksum '%s'. Expected format is ':'." % checksum) - (algo, digest) = checksum.split(':') - expected_algos = ['SHA256', 'SHA512', 'MD5'] - if algo not in expected_algos: - raise ValueError("invalid algo '{}'. Algo must be one of {}".format(algo, ", ".join(expected_algos))) - expected_digest_length = {'SHA256': 64, 'SHA512': 128, 'MD5': 32} - if len(digest) != expected_digest_length[algo]: - raise ValueError("invalid digest length %d. Expected digest length for %s is %d" % (len(digest), algo, expected_digest_length[algo])) - return algo, digest + :raises ValueError: if checksum is incorrectly formatted. + """ + if checksum is None: + return None, None + if len(checksum.split(':')) != 2: + raise ValueError("invalid checksum '%s'. Expected format is ':'." % checksum) + (algo, digest) = checksum.split(':') + expected_algos = ['SHA256', 'SHA512', 'MD5'] + if algo not in expected_algos: + raise ValueError("invalid algo '{}'. Algo must be one of {}".format(algo, ", ".join(expected_algos))) + expected_digest_length = {'SHA256': 64, 'SHA512': 128, 'MD5': 32} + if len(digest) != expected_digest_length[algo]: + raise ValueError("invalid digest length %d. Expected digest length for %s is %d" % (len(digest), algo, expected_digest_length[algo])) + return algo, digest def downloadAndExtractArchive(url, archiveFilePath, outputDir, \ expectedNumberOfExtractedFiles=None, numberOfTrials=3, checksum=None): - """ Downloads an archive from ``url`` as ``archiveFilePath``, and extracts it to ``outputDir``. + """ Downloads an archive from ``url`` as ``archiveFilePath``, and extracts it to ``outputDir``. - This combined function tests the success of the download by the extraction step, - and re-downloads if extraction failed. + This combined function tests the success of the download by the extraction step, + and re-downloads if extraction failed. - If specified, the ``checksum`` is used to verify that the downloaded file is the expected one. - It must be specified as ``:``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. - """ - import os - import shutil - import logging - - maxNumberOfTrials = numberOfTrials - - def _cleanup(): - # If there was a failure, delete downloaded file and empty output folder - logging.warning('Download and extract failed, removing archive and destination folder and retrying. Attempt #%d...' % (maxNumberOfTrials - numberOfTrials)) - os.remove(archiveFilePath) - shutil.rmtree(outputDir) - os.mkdir(outputDir) - - while numberOfTrials: - if not downloadFile(url, archiveFilePath, checksum): - numberOfTrials -= 1 - _cleanup() - continue - if not extractArchive(archiveFilePath, outputDir, expectedNumberOfExtractedFiles): - numberOfTrials -= 1 - _cleanup() - continue - return True + If specified, the ``checksum`` is used to verify that the downloaded file is the expected one. + It must be specified as ``:``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. + """ + import os + import shutil + import logging - _cleanup() - return False + maxNumberOfTrials = numberOfTrials + + def _cleanup(): + # If there was a failure, delete downloaded file and empty output folder + logging.warning('Download and extract failed, removing archive and destination folder and retrying. Attempt #%d...' % (maxNumberOfTrials - numberOfTrials)) + os.remove(archiveFilePath) + shutil.rmtree(outputDir) + os.mkdir(outputDir) + + while numberOfTrials: + if not downloadFile(url, archiveFilePath, checksum): + numberOfTrials -= 1 + _cleanup() + continue + if not extractArchive(archiveFilePath, outputDir, expectedNumberOfExtractedFiles): + numberOfTrials -= 1 + _cleanup() + continue + return True + + _cleanup() + return False def getFilesInDirectory(directory, absolutePath=True): - """Collect all files in a directory and its subdirectories in a list.""" - import os - allFiles=[] - for root, subdirs, files in os.walk(directory): - for fileName in files: - if absolutePath: - fileAbsolutePath = os.path.abspath(os.path.join(root, fileName)).replace('\\','/') - allFiles.append(fileAbsolutePath) - else: - allFiles.append(fileName) - return allFiles + """Collect all files in a directory and its subdirectories in a list.""" + import os + allFiles = [] + for root, subdirs, files in os.walk(directory): + for fileName in files: + if absolutePath: + fileAbsolutePath = os.path.abspath(os.path.join(root, fileName)).replace('\\', '/') + allFiles.append(fileAbsolutePath) + else: + allFiles.append(fileName) + return allFiles class chdir: - """Non thread-safe context manager to change the current working directory. + """Non thread-safe context manager to change the current working directory. - .. note:: + .. note:: - Available in Python 3.11 as ``contextlib.chdir`` and adapted from https://github.com/python/cpython/pull/28271 + Available in Python 3.11 as ``contextlib.chdir`` and adapted from https://github.com/python/cpython/pull/28271 - Available in CTK as ``ctkScopedCurrentDir`` C++ class - """ - def __init__(self, path): - self.path = path - self._old_cwd = [] + Available in CTK as ``ctkScopedCurrentDir`` C++ class + """ + def __init__(self, path): + self.path = path + self._old_cwd = [] - def __enter__(self): - import os - self._old_cwd.append(os.getcwd()) - os.chdir(self.path) + def __enter__(self): + import os + self._old_cwd.append(os.getcwd()) + os.chdir(self.path) - def __exit__(self, *excinfo): - import os - os.chdir(self._old_cwd.pop()) + def __exit__(self, *excinfo): + import os + os.chdir(self._old_cwd.pop()) -def plot(narray, xColumnIndex = -1, columnNames = None, title = None, show = True, nodes = None): - """Create a plot from a numpy array that contains two or more columns. +def plot(narray, xColumnIndex=-1, columnNames=None, title=None, show=True, nodes=None): + """Create a plot from a numpy array that contains two or more columns. - :param narray: input numpy array containing data series in columns. - :param xColumnIndex: index of column that will be used as x axis. - If it is set to negative number (by default) then row index will be used as x coordinate. - :param columnNames: names of each column of the input array. If title is specified for the plot - then title+columnName will be used as series name. - :param title: title of the chart. Plot node names are set based on this value. - :param nodes: plot chart, table, and list of plot series nodes. - Specified in a dictionary, with keys: 'chart', 'table', 'series'. - Series contains a list of plot series nodes (one for each table column). - The parameter is used both as an input and output. - :return: plot chart node. Plot chart node provides access to chart properties and plot series nodes. + :param narray: input numpy array containing data series in columns. + :param xColumnIndex: index of column that will be used as x axis. + If it is set to negative number (by default) then row index will be used as x coordinate. + :param columnNames: names of each column of the input array. If title is specified for the plot + then title+columnName will be used as series name. + :param title: title of the chart. Plot node names are set based on this value. + :param nodes: plot chart, table, and list of plot series nodes. + Specified in a dictionary, with keys: 'chart', 'table', 'series'. + Series contains a list of plot series nodes (one for each table column). + The parameter is used both as an input and output. + :return: plot chart node. Plot chart node provides access to chart properties and plot series nodes. - Example 1: simple plot + Example 1: simple plot - .. code-block:: python + .. code-block:: python - # Get sample data - import numpy as np - import SampleData - volumeNode = SampleData.downloadSample("MRHead") + # Get sample data + import numpy as np + import SampleData + volumeNode = SampleData.downloadSample("MRHead") - # Create new plot - histogram = np.histogram(arrayFromVolume(volumeNode), bins=50) - chartNode = plot(histogram, xColumnIndex = 1) + # Create new plot + histogram = np.histogram(arrayFromVolume(volumeNode), bins=50) + chartNode = plot(histogram, xColumnIndex = 1) - # Change some plot properties - chartNode.SetTitle("My histogram") - chartNode.GetNthPlotSeriesNode(0).SetPlotType(slicer.vtkMRMLPlotSeriesNode.PlotTypeScatterBar) + # Change some plot properties + chartNode.SetTitle("My histogram") + chartNode.GetNthPlotSeriesNode(0).SetPlotType(slicer.vtkMRMLPlotSeriesNode.PlotTypeScatterBar) - Example 2: plot with multiple updates + Example 2: plot with multiple updates - .. code-block:: python + .. code-block:: python - # Get sample data - import numpy as np - import SampleData - volumeNode = SampleData.downloadSample("MRHead") + # Get sample data + import numpy as np + import SampleData + volumeNode = SampleData.downloadSample("MRHead") - # Create variable that will store plot nodes (chart, table, series) - plotNodes = {} + # Create variable that will store plot nodes (chart, table, series) + plotNodes = {} - # Create new plot - histogram = np.histogram(arrayFromVolume(volumeNode), bins=80) - plot(histogram, xColumnIndex = 1, nodes = plotNodes) + # Create new plot + histogram = np.histogram(arrayFromVolume(volumeNode), bins=80) + plot(histogram, xColumnIndex = 1, nodes = plotNodes) - # Update plot - histogram = np.histogram(arrayFromVolume(volumeNode), bins=40) - plot(histogram, xColumnIndex = 1, nodes = plotNodes) - """ - import slicer - - chartNode = None - tableNode = None - seriesNodes = [] - - # Retrieve nodes that must be reused - if nodes is not None: - if 'chart' in nodes: - chartNode = nodes['chart'] - if 'table' in nodes: - tableNode = nodes['table'] - if 'series' in nodes: - seriesNodes = nodes['series'] - - # Create table node - if tableNode is None: - tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") - - if title is not None: - tableNode.SetName(title+' table') - updateTableFromArray(tableNode, narray) - # Update column names - numberOfColumns = tableNode.GetTable().GetNumberOfColumns() - yColumnIndex = 0 - for columnIndex in range(numberOfColumns): - if (columnNames is not None) and (len(columnNames) > columnIndex): - columnName = columnNames[columnIndex] - else: - if columnIndex == xColumnIndex: - columnName = "X" - elif yColumnIndex == 0: - columnName = "Y" - yColumnIndex += 1 - else: - columnName = "Y"+str(yColumnIndex) - yColumnIndex += 1 - tableNode.GetTable().GetColumn(columnIndex).SetName(columnName) - - # Create chart and add plot - if chartNode is None: - chartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode") - if title is not None: - chartNode.SetName(title + ' chart') - chartNode.SetTitle(title) - - # Create plot series node(s) - xColumnName = columnNames[xColumnIndex] if (columnNames is not None) and (len(columnNames) > 0) else "X" - seriesIndex = -1 - for columnIndex in range(numberOfColumns): - if columnIndex == xColumnIndex: - continue - seriesIndex += 1 - if len(seriesNodes) > seriesIndex: - seriesNode = seriesNodes[seriesIndex] - else: - seriesNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode") - seriesNodes.append(seriesNode) - seriesNode.SetUniqueColor() - seriesNode.SetAndObserveTableNodeID(tableNode.GetID()) - if xColumnIndex < 0: - seriesNode.SetXColumnName("") - seriesNode.SetPlotType(seriesNode.PlotTypeLine) - else: - seriesNode.SetXColumnName(xColumnName) - seriesNode.SetPlotType(seriesNode.PlotTypeScatter) - yColumnName = tableNode.GetTable().GetColumn(columnIndex).GetName() - seriesNode.SetYColumnName(yColumnName) - if title: - seriesNode.SetName(title + " " + yColumnName) - if not chartNode.HasPlotSeriesNodeID(seriesNode.GetID()): - chartNode.AddAndObservePlotSeriesNodeID(seriesNode.GetID()) + # Update plot + histogram = np.histogram(arrayFromVolume(volumeNode), bins=40) + plot(histogram, xColumnIndex = 1, nodes = plotNodes) + """ + import slicer + + chartNode = None + tableNode = None + seriesNodes = [] + + # Retrieve nodes that must be reused + if nodes is not None: + if 'chart' in nodes: + chartNode = nodes['chart'] + if 'table' in nodes: + tableNode = nodes['table'] + if 'series' in nodes: + seriesNodes = nodes['series'] + + # Create table node + if tableNode is None: + tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") + + if title is not None: + tableNode.SetName(title + ' table') + updateTableFromArray(tableNode, narray) + # Update column names + numberOfColumns = tableNode.GetTable().GetNumberOfColumns() + yColumnIndex = 0 + for columnIndex in range(numberOfColumns): + if (columnNames is not None) and (len(columnNames) > columnIndex): + columnName = columnNames[columnIndex] + else: + if columnIndex == xColumnIndex: + columnName = "X" + elif yColumnIndex == 0: + columnName = "Y" + yColumnIndex += 1 + else: + columnName = "Y" + str(yColumnIndex) + yColumnIndex += 1 + tableNode.GetTable().GetColumn(columnIndex).SetName(columnName) + + # Create chart and add plot + if chartNode is None: + chartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode") + if title is not None: + chartNode.SetName(title + ' chart') + chartNode.SetTitle(title) + + # Create plot series node(s) + xColumnName = columnNames[xColumnIndex] if (columnNames is not None) and (len(columnNames) > 0) else "X" + seriesIndex = -1 + for columnIndex in range(numberOfColumns): + if columnIndex == xColumnIndex: + continue + seriesIndex += 1 + if len(seriesNodes) > seriesIndex: + seriesNode = seriesNodes[seriesIndex] + else: + seriesNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode") + seriesNodes.append(seriesNode) + seriesNode.SetUniqueColor() + seriesNode.SetAndObserveTableNodeID(tableNode.GetID()) + if xColumnIndex < 0: + seriesNode.SetXColumnName("") + seriesNode.SetPlotType(seriesNode.PlotTypeLine) + else: + seriesNode.SetXColumnName(xColumnName) + seriesNode.SetPlotType(seriesNode.PlotTypeScatter) + yColumnName = tableNode.GetTable().GetColumn(columnIndex).GetName() + seriesNode.SetYColumnName(yColumnName) + if title: + seriesNode.SetName(title + " " + yColumnName) + if not chartNode.HasPlotSeriesNodeID(seriesNode.GetID()): + chartNode.AddAndObservePlotSeriesNodeID(seriesNode.GetID()) - # Show plot in layout - if show: - slicer.modules.plots.logic().ShowChartInLayout(chartNode) + # Show plot in layout + if show: + slicer.modules.plots.logic().ShowChartInLayout(chartNode) - # Without this, chart view may show up completely empty when the same nodes are updated - # (this is probably due to a bug in plotting nodes or widgets). - chartNode.Modified() + # Without this, chart view may show up completely empty when the same nodes are updated + # (this is probably due to a bug in plotting nodes or widgets). + chartNode.Modified() - if nodes is not None: - nodes['table'] = tableNode - nodes['chart'] = chartNode - nodes['series'] = seriesNodes + if nodes is not None: + nodes['table'] = tableNode + nodes['chart'] = chartNode + nodes['series'] = seriesNodes - return chartNode + return chartNode def launchConsoleProcess(args, useStartupEnvironment=True, updateEnvironment=None, cwd=None): - """Launch a process. Hiding the console and captures the process output. + """Launch a process. Hiding the console and captures the process output. - The console window is hidden when running on Windows. + The console window is hidden when running on Windows. - :param args: executable name, followed by command-line arguments - :param useStartupEnvironment: launch the process in the original environment as the original Slicer process - :param updateEnvironment: map containing optional additional environment variables (existing variables are overwritten) - :param cwd: current working directory - :return: process object. - """ - import subprocess - import os - if useStartupEnvironment: - startupEnv = startupEnvironment() - if updateEnvironment: - startupEnv.update(updateEnvironment) - else: - if updateEnvironment: - startupEnv = os.environ.copy() - startupEnv.update(updateEnvironment) + :param args: executable name, followed by command-line arguments + :param useStartupEnvironment: launch the process in the original environment as the original Slicer process + :param updateEnvironment: map containing optional additional environment variables (existing variables are overwritten) + :param cwd: current working directory + :return: process object. + """ + import subprocess + import os + if useStartupEnvironment: + startupEnv = startupEnvironment() + if updateEnvironment: + startupEnv.update(updateEnvironment) else: - startupEnv = None - if os.name == 'nt': - # Hide console window (only needed on Windows) - info = subprocess.STARTUPINFO() - info.dwFlags = 1 - info.wShowWindow = 0 - proc = subprocess.Popen(args, env=startupEnv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, startupinfo=info, cwd=cwd) - else: - proc = subprocess.Popen(args, env=startupEnv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, cwd=cwd) - return proc + if updateEnvironment: + startupEnv = os.environ.copy() + startupEnv.update(updateEnvironment) + else: + startupEnv = None + if os.name == 'nt': + # Hide console window (only needed on Windows) + info = subprocess.STARTUPINFO() + info.dwFlags = 1 + info.wShowWindow = 0 + proc = subprocess.Popen(args, env=startupEnv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, startupinfo=info, cwd=cwd) + else: + proc = subprocess.Popen(args, env=startupEnv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, cwd=cwd) + return proc def logProcessOutput(proc): - """Continuously write process output to the application log and the Python console. - - :param proc: process object. - """ - from subprocess import CalledProcessError - import logging - try: - from slicer import app - guiApp = app - except ImportError: - # Running from console - guiApp = None + """Continuously write process output to the application log and the Python console. - while True: + :param proc: process object. + """ + from subprocess import CalledProcessError + import logging try: - line = proc.stdout.readline() - if not line: - break - if guiApp: - logging.info(line.rstrip()) - guiApp.processEvents() # give a chance the application to refresh GUI - else: - print(line.rstrip()) - except UnicodeDecodeError as e: - # Code page conversion happens because `universal_newlines=True` sets process output to text mode, - # and it fails because probably system locale is not UTF8. We just ignore the error and discard the string, - # as we only guarantee correct behavior if an UTF8 locale is used. - pass - - proc.wait() - retcode=proc.returncode - if retcode != 0: - raise CalledProcessError(retcode, proc.args, output=proc.stdout, stderr=proc.stderr) + from slicer import app + guiApp = app + except ImportError: + # Running from console + guiApp = None + while True: + try: + line = proc.stdout.readline() + if not line: + break + if guiApp: + logging.info(line.rstrip()) + guiApp.processEvents() # give a chance the application to refresh GUI + else: + print(line.rstrip()) + except UnicodeDecodeError as e: + # Code page conversion happens because `universal_newlines=True` sets process output to text mode, + # and it fails because probably system locale is not UTF8. We just ignore the error and discard the string, + # as we only guarantee correct behavior if an UTF8 locale is used. + pass + + proc.wait() + retcode = proc.returncode + if retcode != 0: + raise CalledProcessError(retcode, proc.args, output=proc.stdout, stderr=proc.stderr) -def _executePythonModule(module, args): - """Execute a Python module as a script in Slicer's Python environment. - Internally python -m is called with the module name and additional arguments. +def _executePythonModule(module, args): + """Execute a Python module as a script in Slicer's Python environment. - :raises RuntimeError: in case of failure - """ - # Determine pythonSlicerExecutablePath - try: - from slicer import app # noqa: F401 - # If we get to this line then import from "app" is succeeded, - # which means that we run this function from Slicer Python interpreter. - # PythonSlicer is added to PATH environment variable in Slicer - # therefore shutil.which will be able to find it. - import shutil - pythonSlicerExecutablePath = shutil.which('PythonSlicer') - if not pythonSlicerExecutablePath: - raise RuntimeError("PythonSlicer executable not found") - except ImportError: - # Running from console - import os - import sys - pythonSlicerExecutablePath = os.path.dirname(sys.executable)+"/PythonSlicer" - if os.name == 'nt': - pythonSlicerExecutablePath += ".exe" + Internally python -m is called with the module name and additional arguments. - commandLine = [pythonSlicerExecutablePath, "-m", module, *args] - proc = launchConsoleProcess(commandLine, useStartupEnvironment=False) - logProcessOutput(proc) + :raises RuntimeError: in case of failure + """ + # Determine pythonSlicerExecutablePath + try: + from slicer import app # noqa: F401 + # If we get to this line then import from "app" is succeeded, + # which means that we run this function from Slicer Python interpreter. + # PythonSlicer is added to PATH environment variable in Slicer + # therefore shutil.which will be able to find it. + import shutil + pythonSlicerExecutablePath = shutil.which('PythonSlicer') + if not pythonSlicerExecutablePath: + raise RuntimeError("PythonSlicer executable not found") + except ImportError: + # Running from console + import os + import sys + pythonSlicerExecutablePath = os.path.dirname(sys.executable) + "/PythonSlicer" + if os.name == 'nt': + pythonSlicerExecutablePath += ".exe" + + commandLine = [pythonSlicerExecutablePath, "-m", module, *args] + proc = launchConsoleProcess(commandLine, useStartupEnvironment=False) + logProcessOutput(proc) def pip_install(requirements): - """Install python packages. + """Install python packages. - Currently, the method simply calls ``python -m pip install`` but in the future further checks, optimizations, - user confirmation may be implemented, therefore it is recommended to use this method call instead of a plain - pip install. - :param requirements: requirement specifier in the same format as used by pip (https://docs.python.org/3/installing/index.html). - It can be either a single string or a list of command-line arguments. It may be simpler to pass command-line arguments as a list - if the arguments may contain spaces (because no escaping of the strings with quotes is necessary). + Currently, the method simply calls ``python -m pip install`` but in the future further checks, optimizations, + user confirmation may be implemented, therefore it is recommended to use this method call instead of a plain + pip install. + :param requirements: requirement specifier in the same format as used by pip (https://docs.python.org/3/installing/index.html). + It can be either a single string or a list of command-line arguments. It may be simpler to pass command-line arguments as a list + if the arguments may contain spaces (because no escaping of the strings with quotes is necessary). - Example: calling from Slicer GUI + Example: calling from Slicer GUI - .. code-block:: python + .. code-block:: python - pip_install("tensorflow keras scikit-learn ipywidgets") + pip_install("tensorflow keras scikit-learn ipywidgets") - Example: calling from PythonSlicer console + Example: calling from PythonSlicer console - .. code-block:: python + .. code-block:: python - from slicer.util import pip_install - pip_install("tensorflow") + from slicer.util import pip_install + pip_install("tensorflow") - """ + """ - if type(requirements) == str: - # shlex.split splits string the same way as the shell (keeping quoted string as a single argument) - import shlex - args = 'install', *(shlex.split(requirements)) - elif type(requirements) == list: - args = 'install', *requirements - else: - raise ValueError("pip_install requirement input must be string or list") + if type(requirements) == str: + # shlex.split splits string the same way as the shell (keeping quoted string as a single argument) + import shlex + args = 'install', *(shlex.split(requirements)) + elif type(requirements) == list: + args = 'install', *requirements + else: + raise ValueError("pip_install requirement input must be string or list") - _executePythonModule('pip', args) + _executePythonModule('pip', args) def pip_uninstall(requirements): - """Uninstall python packages. + """Uninstall python packages. - Currently, the method simply calls ``python -m pip uninstall`` but in the future further checks, optimizations, - user confirmation may be implemented, therefore it is recommended to use this method call instead of a plain - pip uninstall. + Currently, the method simply calls ``python -m pip uninstall`` but in the future further checks, optimizations, + user confirmation may be implemented, therefore it is recommended to use this method call instead of a plain + pip uninstall. - :param requirements: requirement specifier in the same format as used by pip (https://docs.python.org/3/installing/index.html). - It can be either a single string or a list of command-line arguments. It may be simpler to pass command-line arguments as a list - if the arguments may contain spaces (because no escaping of the strings with quotes is necessary). + :param requirements: requirement specifier in the same format as used by pip (https://docs.python.org/3/installing/index.html). + It can be either a single string or a list of command-line arguments. It may be simpler to pass command-line arguments as a list + if the arguments may contain spaces (because no escaping of the strings with quotes is necessary). - Example: calling from Slicer GUI + Example: calling from Slicer GUI - .. code-block:: python + .. code-block:: python - pip_uninstall("tensorflow keras scikit-learn ipywidgets") + pip_uninstall("tensorflow keras scikit-learn ipywidgets") - Example: calling from PythonSlicer console + Example: calling from PythonSlicer console - .. code-block:: python + .. code-block:: python - from slicer.util import pip_uninstall - pip_uninstall("tensorflow") + from slicer.util import pip_uninstall + pip_uninstall("tensorflow") - """ - if type(requirements) == str: - # shlex.split splits string the same way as the shell (keeping quoted string as a single argument) - import shlex - args = 'uninstall', *(shlex.split(requirements)), '--yes' - elif type(requirements) == list: - args = 'uninstall', *requirements, '--yes' - else: - raise ValueError("pip_uninstall requirement input must be string or list") - _executePythonModule('pip', args) + """ + if type(requirements) == str: + # shlex.split splits string the same way as the shell (keeping quoted string as a single argument) + import shlex + args = 'uninstall', *(shlex.split(requirements)), '--yes' + elif type(requirements) == list: + args = 'uninstall', *requirements, '--yes' + else: + raise ValueError("pip_uninstall requirement input must be string or list") + _executePythonModule('pip', args) def longPath(path): - """Make long paths work on Windows, where the maximum path length is 260 characters. + """Make long paths work on Windows, where the maximum path length is 260 characters. - For example, the files in the DICOM database may have paths longer than this limit. - Accessing these can be made safe by prefixing it with the UNC prefix ('\\?\'). + For example, the files in the DICOM database may have paths longer than this limit. + Accessing these can be made safe by prefixing it with the UNC prefix ('\\?\'). - :param string path: Path to be made safe if too long + :param string path: Path to be made safe if too long - :return string: Safe path - """ - # Return path as is if conversion is disabled - longPathConversionEnabled = settingsValue('General/LongPathConversionEnabled', True, converter=toBool) - if not longPathConversionEnabled: - return path - # Return path as is on operating systems other than Windows - import qt - sysInfo = qt.QSysInfo() - if sysInfo.productType() != 'windows': - return path - # Skip prefixing relative paths as UNC prefix wors only on absolute paths - if not qt.QDir.isAbsolutePath(path): - return path - # Return path as is if UNC prefix is already applied - if path[:4] == '\\\\?\\': - return path - return "\\\\?\\" + path.replace('/', '\\') + :return string: Safe path + """ + # Return path as is if conversion is disabled + longPathConversionEnabled = settingsValue('General/LongPathConversionEnabled', True, converter=toBool) + if not longPathConversionEnabled: + return path + # Return path as is on operating systems other than Windows + import qt + sysInfo = qt.QSysInfo() + if sysInfo.productType() != 'windows': + return path + # Skip prefixing relative paths as UNC prefix wors only on absolute paths + if not qt.QDir.isAbsolutePath(path): + return path + # Return path as is if UNC prefix is already applied + if path[:4] == '\\\\?\\': + return path + return "\\\\?\\" + path.replace('/', '\\') diff --git a/Base/Python/tests/test_PythonManager.py b/Base/Python/tests/test_PythonManager.py index 5309b377c60..b0660818e22 100644 --- a/Base/Python/tests/test_PythonManager.py +++ b/Base/Python/tests/test_PythonManager.py @@ -21,12 +21,12 @@ def test_toPythonStringLiteral(self): 'test with both single \' and double " quotes', 'test backslash \\ and \'single\' and "double" quotes' "'test string in single quotes'" - '"test string in double quotes"' ] + '"test string in double quotes"'] for test_string in test_strings: test_string_literal = slicer.app.pythonManager().toPythonStringLiteral(test_string) - exec("test_string_literal_value = "+test_string_literal, globals()) - print("Test: "+test_string+" -> "+test_string_literal+" -> "+test_string_literal_value) + exec("test_string_literal_value = " + test_string_literal, globals()) + print("Test: " + test_string + " -> " + test_string_literal + " -> " + test_string_literal_value) self.assertEqual(test_string, test_string_literal_value) def tearDown(self): diff --git a/Base/Python/tests/test_sitkUtils.py b/Base/Python/tests/test_sitkUtils.py index cc5680f2cf2..2c72c23bb4c 100644 --- a/Base/Python/tests/test_sitkUtils.py +++ b/Base/Python/tests/test_sitkUtils.py @@ -31,7 +31,7 @@ def test_SimpleITK_SlicerPushPull(self): self.assertEqual(volumeNode1, slicer.util.getNode('MRHead'), 'Original volume is changed') self.assertNotEqual(volumeNode1, volumeNode1Copy, - 'Copy of original volume is not created') + 'Copy of original volume is not created') """ Few modification of the image : Direction, Origin """ sitkimage.SetDirection((-1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 1.0, 0.0, 1.0)) @@ -39,10 +39,10 @@ def test_SimpleITK_SlicerPushPull(self): """ Few pixel changed """ size = sitkimage.GetSize() - for x in range(0,size[0],int(size[0]/10)): - for y in range(0,size[1],int(size[1]/10)): - for z in range(0,size[2],int(size[2]/10)): - sitkimage.SetPixel(x,y,z,0) + for x in range(0, size[0], int(size[0] / 10)): + for y in range(0, size[1], int(size[1] / 10)): + for z in range(0, size[2], int(size[2] / 10)): + sitkimage.SetPixel(x, y, z, 0) volumeNode1Modified = su.PushVolumeToSlicer(sitkimage, name="ImageChanged", className="vtkMRMLScalarVolumeNode") self.assertEqual(volumeNode1Modified.GetName(), "ImageChanged", @@ -54,7 +54,7 @@ def test_SimpleITK_SlicerPushPull(self): """ tmp = volumeNode1Modified.GetOrigin() valToCompare = (-tmp[0], -tmp[1], tmp[2]) - self.assertEqual(valToCompare,sitkimage.GetOrigin(), + self.assertEqual(valToCompare, sitkimage.GetOrigin(), 'Modified origin mismatch') """ Test push with all parameter combinations """ @@ -62,13 +62,13 @@ def test_SimpleITK_SlicerPushPull(self): volumeNodeTested = None volumeNodeNew = None for pushToNewNode in [True, False]: - print("volumeClassName : %s" % volumeClassName ) - print("pushToNewNode : %s " % pushToNewNode ) + print("volumeClassName : %s" % volumeClassName) + print("pushToNewNode : %s " % pushToNewNode) if pushToNewNode: volumeNodeTested = su.PushVolumeToSlicer(sitkimage, - name='volumeNode-'+volumeClassName+"-"+str(pushToNewNode), - className=volumeClassName) + name='volumeNode-' + volumeClassName + "-" + str(pushToNewNode), + className=volumeClassName) existingVolumeNode = volumeNodeTested else: volumeNodeTested = su.PushVolumeToSlicer(sitkimage, existingVolumeNode) diff --git a/Base/QTApp/Resources/UI/qSlicerMainWindow.ui b/Base/QTApp/Resources/UI/qSlicerMainWindow.ui index e578e087716..ae48f683910 100644 --- a/Base/QTApp/Resources/UI/qSlicerMainWindow.ui +++ b/Base/QTApp/Resources/UI/qSlicerMainWindow.ui @@ -352,6 +352,9 @@ Raise the DICOM module for loading DICOM datasets. + + false + diff --git a/Base/QTCore/Resources/Certs/Slicer.crt b/Base/QTCore/Resources/Certs/Slicer.crt index 8abb3fb2eea..a2593235872 100644 --- a/Base/QTCore/Resources/Certs/Slicer.crt +++ b/Base/QTCore/Resources/Certs/Slicer.crt @@ -1,87 +1,3 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 6643877497813316402 (0x5c33cb622c5fb332) - Signature Algorithm: sha256WithRSAEncryption - Issuer: CN = Atos TrustedRoot 2011, O = Atos, C = DE - Validity - Not Before: Jul 7 14:58:30 2011 GMT - Not After : Dec 31 23:59:59 2030 GMT - Subject: CN = Atos TrustedRoot 2011, O = Atos, C = DE - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (2048 bit) - Modulus: - 00:95:85:3b:97:6f:2a:3b:2e:3b:cf:a6:f3:29:35: - be:cf:18:ac:3e:aa:d9:f8:4d:a0:3e:1a:47:b9:bc: - 9a:df:f2:fe:cc:3e:47:e8:7a:96:c2:24:8e:35:f4: - a9:0c:fc:82:fd:6d:c1:72:62:27:bd:ea:6b:eb:e7: - 8a:cc:54:3e:90:50:cf:80:d4:95:fb:e8:b5:82:d4: - 14:c5:b6:a9:55:25:57:db:b1:50:f6:b0:60:64:59: - 7a:69:cf:03:b7:6f:0d:be:ca:3e:6f:74:72:ea:aa: - 30:2a:73:62:be:49:91:61:c8:11:fe:0e:03:2a:f7: - 6a:20:dc:02:15:0d:5e:15:6a:fc:e3:82:c1:b5:c5: - 9d:64:09:6c:a3:59:98:07:27:c7:1b:96:2b:61:74: - 71:6c:43:f1:f7:35:89:10:e0:9e:ec:55:a1:37:22: - a2:87:04:05:2c:47:7d:b4:1c:b9:62:29:66:28:ca: - b7:e1:93:f5:a4:94:03:99:b9:70:85:b5:e6:48:ea: - 8d:50:fc:d9:de:cc:6f:07:0e:dd:0b:72:9d:80:30: - 16:07:95:3f:28:0e:fd:c5:75:4f:53:d6:74:9a:b4: - 24:2e:8e:02:91:cf:76:c5:9b:1e:55:74:9c:78:21: - b1:f0:2d:f1:0b:9f:c2:d5:96:18:1f:f0:54:22:7a: - 8c:07 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - A7:A5:06:B1:2C:A6:09:60:EE:D1:97:E9:70:AE:BC:3B:19:6C:DB:21 - X509v3 Basic Constraints: critical - CA:TRUE - X509v3 Authority Key Identifier: - keyid:A7:A5:06:B1:2C:A6:09:60:EE:D1:97:E9:70:AE:BC:3B:19:6C:DB:21 - - X509v3 Certificate Policies: - Policy: 1.3.6.1.4.1.6189.3.4.1.1 - - X509v3 Key Usage: critical - Digital Signature, Certificate Sign, CRL Sign - Signature Algorithm: sha256WithRSAEncryption - 26:77:34:db:94:48:86:2a:41:9d:2c:3e:06:90:60:c4:8c:ac: - 0b:54:b8:1f:b9:7b:d3:07:39:e4:fa:3e:7b:b2:3d:4e:ed:9f: - 23:bd:97:f3:6b:5c:ef:ee:fd:40:a6:df:a1:93:a1:0a:86:ac: - ef:20:d0:79:01:bd:78:f7:19:d8:24:31:34:04:01:a6:ba:15: - 9a:c3:27:dc:d8:4f:0f:cc:18:63:ff:99:0f:0e:91:6b:75:16: - e1:21:fc:d8:26:c7:47:b7:a6:cf:58:72:71:7e:ba:e1:4d:95: - 47:3b:c9:af:6d:a1:b4:c1:ec:89:f6:b4:0f:38:b5:e2:64:dc: - 25:cf:a6:db:eb:9a:5c:99:a1:c5:08:de:fd:e6:da:d5:d6:5a: - 45:0c:c4:b7:c2:b5:14:ef:b4:11:ff:0e:15:b5:f5:f5:db:c6: - bd:eb:5a:a7:f0:56:22:a9:3c:65:54:c6:15:a8:bd:86:9e:cd: - 83:96:68:7a:71:81:89:e1:0b:e1:ea:11:1b:68:08:cc:69:9e: - ec:9e:41:9e:44:32:26:7a:e2:87:0a:71:3d:eb:e4:5a:a4:d2: - db:c5:cd:c6:de:60:7f:b9:f3:4f:44:92:ef:2a:b7:18:3e:a7: - 19:d9:0b:7d:b1:37:41:42:b0:ba:60:1d:f2:fe:09:11:b0:f0: - 87:7b:a7:9d -SHA1 Fingerprint=2B:B1:F5:3E:55:0C:1D:C5:F1:D4:E6:B7:6A:46:4B:55:06:02:AC:21 ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE -AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG -EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM -FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC -REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp -Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM -VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ -SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ -4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L -cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi -eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG -A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 -DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j -vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP -DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc -maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D -lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv -KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed ------END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -831,96 +747,6 @@ r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 /YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ -----END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 1 (0x1) - Signature Algorithm: sha1WithRSAEncryption - Issuer: C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services - Validity - Not Before: Jan 1 00:00:00 2004 GMT - Not After : Dec 31 23:59:59 2028 GMT - Subject: C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (2048 bit) - Modulus: - 00:be:40:9d:f4:6e:e1:ea:76:87:1c:4d:45:44:8e: - be:46:c8:83:06:9d:c1:2a:fe:18:1f:8e:e4:02:fa: - f3:ab:5d:50:8a:16:31:0b:9a:06:d0:c5:70:22:cd: - 49:2d:54:63:cc:b6:6e:68:46:0b:53:ea:cb:4c:24: - c0:bc:72:4e:ea:f1:15:ae:f4:54:9a:12:0a:c3:7a: - b2:33:60:e2:da:89:55:f3:22:58:f3:de:dc:cf:ef: - 83:86:a2:8c:94:4f:9f:68:f2:98:90:46:84:27:c7: - 76:bf:e3:cc:35:2c:8b:5e:07:64:65:82:c0:48:b0: - a8:91:f9:61:9f:76:20:50:a8:91:c7:66:b5:eb:78: - 62:03:56:f0:8a:1a:13:ea:31:a3:1e:a0:99:fd:38: - f6:f6:27:32:58:6f:07:f5:6b:b8:fb:14:2b:af:b7: - aa:cc:d6:63:5f:73:8c:da:05:99:a8:38:a8:cb:17: - 78:36:51:ac:e9:9e:f4:78:3a:8d:cf:0f:d9:42:e2: - 98:0c:ab:2f:9f:0e:01:de:ef:9f:99:49:f1:2d:df: - ac:74:4d:1b:98:b5:47:c5:e5:29:d1:f9:90:18:c7: - 62:9c:be:83:c7:26:7b:3e:8a:25:c7:c0:dd:9d:e6: - 35:68:10:20:9d:8f:d8:de:d2:c3:84:9c:0d:5e:e8: - 2f:c9 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - A0:11:0A:23:3E:96:F1:07:EC:E2:AF:29:EF:82:A5:7F:D0:30:A4:B4 - X509v3 Key Usage: critical - Certificate Sign, CRL Sign - X509v3 Basic Constraints: critical - CA:TRUE - X509v3 CRL Distribution Points: - - Full Name: - URI:http://crl.comodoca.com/AAACertificateServices.crl - - Full Name: - URI:http://crl.comodo.net/AAACertificateServices.crl - - Signature Algorithm: sha1WithRSAEncryption - 08:56:fc:02:f0:9b:e8:ff:a4:fa:d6:7b:c6:44:80:ce:4f:c4: - c5:f6:00:58:cc:a6:b6:bc:14:49:68:04:76:e8:e6:ee:5d:ec: - 02:0f:60:d6:8d:50:18:4f:26:4e:01:e3:e6:b0:a5:ee:bf:bc: - 74:54:41:bf:fd:fc:12:b8:c7:4f:5a:f4:89:60:05:7f:60:b7: - 05:4a:f3:f6:f1:c2:bf:c4:b9:74:86:b6:2d:7d:6b:cc:d2:f3: - 46:dd:2f:c6:e0:6a:c3:c3:34:03:2c:7d:96:dd:5a:c2:0e:a7: - 0a:99:c1:05:8b:ab:0c:2f:f3:5c:3a:cf:6c:37:55:09:87:de: - 53:40:6c:58:ef:fc:b6:ab:65:6e:04:f6:1b:dc:3c:e0:5a:15: - c6:9e:d9:f1:59:48:30:21:65:03:6c:ec:e9:21:73:ec:9b:03: - a1:e0:37:ad:a0:15:18:8f:fa:ba:02:ce:a7:2c:a9:10:13:2c: - d4:e5:08:26:ab:22:97:60:f8:90:5e:74:d4:a2:9a:53:bd:f2: - a9:68:e0:a2:6e:c2:d7:6c:b1:a3:0f:9e:bf:eb:68:e7:56:f2: - ae:f2:e3:2b:38:3a:09:81:b5:6b:85:d7:be:2d:ed:3f:1a:b7: - b2:63:e2:f5:62:2c:82:d4:6a:00:41:50:f1:39:83:9f:95:e9: - 36:96:98:6e -SHA1 Fingerprint=D1:EB:23:A4:6D:17:D6:8F:D9:25:64:C2:F1:F1:60:17:64:D8:E3:49 ------BEGIN CERTIFICATE----- -MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb -MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow -GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj -YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM -GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua -BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe -3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 -YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR -rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm -ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU -oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF -MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v -QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t -b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF -AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q -GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz -Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 -G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi -l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 -smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== ------END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -1047,73 +873,163 @@ NVOFBkpdn627G190 Certificate: Data: Version: 3 (0x2) - Serial Number: - 01:fd:6d:30:fc:a3:ca:51:a8:1b:bc:64:0e:35:03:2d - Signature Algorithm: sha384WithRSAEncryption - Issuer: C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust RSA Certification Authority + Serial Number: 1 (0x1) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services Validity - Not Before: Feb 1 00:00:00 2010 GMT - Not After : Jan 18 23:59:59 2038 GMT - Subject: C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust RSA Certification Authority + Not Before: Jan 1 00:00:00 2004 GMT + Not After : Dec 31 23:59:59 2028 GMT + Subject: C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services Subject Public Key Info: Public Key Algorithm: rsaEncryption - RSA Public-Key: (4096 bit) + RSA Public-Key: (2048 bit) Modulus: - 00:80:12:65:17:36:0e:c3:db:08:b3:d0:ac:57:0d: - 76:ed:cd:27:d3:4c:ad:50:83:61:e2:aa:20:4d:09: - 2d:64:09:dc:ce:89:9f:cc:3d:a9:ec:f6:cf:c1:dc: - f1:d3:b1:d6:7b:37:28:11:2b:47:da:39:c6:bc:3a: - 19:b4:5f:a6:bd:7d:9d:a3:63:42:b6:76:f2:a9:3b: - 2b:91:f8:e2:6f:d0:ec:16:20:90:09:3e:e2:e8:74: - c9:18:b4:91:d4:62:64:db:7f:a3:06:f1:88:18:6a: - 90:22:3c:bc:fe:13:f0:87:14:7b:f6:e4:1f:8e:d4: - e4:51:c6:11:67:46:08:51:cb:86:14:54:3f:bc:33: - fe:7e:6c:9c:ff:16:9d:18:bd:51:8e:35:a6:a7:66: - c8:72:67:db:21:66:b1:d4:9b:78:03:c0:50:3a:e8: - cc:f0:dc:bc:9e:4c:fe:af:05:96:35:1f:57:5a:b7: - ff:ce:f9:3d:b7:2c:b6:f6:54:dd:c8:e7:12:3a:4d: - ae:4c:8a:b7:5c:9a:b4:b7:20:3d:ca:7f:22:34:ae: - 7e:3b:68:66:01:44:e7:01:4e:46:53:9b:33:60:f7: - 94:be:53:37:90:73:43:f3:32:c3:53:ef:db:aa:fe: - 74:4e:69:c7:6b:8c:60:93:de:c4:c7:0c:df:e1:32: - ae:cc:93:3b:51:78:95:67:8b:ee:3d:56:fe:0c:d0: - 69:0f:1b:0f:f3:25:26:6b:33:6d:f7:6e:47:fa:73: - 43:e5:7e:0e:a5:66:b1:29:7c:32:84:63:55:89:c4: - 0d:c1:93:54:30:19:13:ac:d3:7d:37:a7:eb:5d:3a: - 6c:35:5c:db:41:d7:12:da:a9:49:0b:df:d8:80:8a: - 09:93:62:8e:b5:66:cf:25:88:cd:84:b8:b1:3f:a4: - 39:0f:d9:02:9e:eb:12:4c:95:7c:f3:6b:05:a9:5e: - 16:83:cc:b8:67:e2:e8:13:9d:cc:5b:82:d3:4c:b3: - ed:5b:ff:de:e5:73:ac:23:3b:2d:00:bf:35:55:74: - 09:49:d8:49:58:1a:7f:92:36:e6:51:92:0e:f3:26: - 7d:1c:4d:17:bc:c9:ec:43:26:d0:bf:41:5f:40:a9: - 44:44:f4:99:e7:57:87:9e:50:1f:57:54:a8:3e:fd: - 74:63:2f:b1:50:65:09:e6:58:42:2e:43:1a:4c:b4: - f0:25:47:59:fa:04:1e:93:d4:26:46:4a:50:81:b2: - de:be:78:b7:fc:67:15:e1:c9:57:84:1e:0f:63:d6: - e9:62:ba:d6:5f:55:2e:ea:5c:c6:28:08:04:25:39: - b8:0e:2b:a9:f2:4c:97:1c:07:3f:0d:52:f5:ed:ef: - 2f:82:0f + 00:be:40:9d:f4:6e:e1:ea:76:87:1c:4d:45:44:8e: + be:46:c8:83:06:9d:c1:2a:fe:18:1f:8e:e4:02:fa: + f3:ab:5d:50:8a:16:31:0b:9a:06:d0:c5:70:22:cd: + 49:2d:54:63:cc:b6:6e:68:46:0b:53:ea:cb:4c:24: + c0:bc:72:4e:ea:f1:15:ae:f4:54:9a:12:0a:c3:7a: + b2:33:60:e2:da:89:55:f3:22:58:f3:de:dc:cf:ef: + 83:86:a2:8c:94:4f:9f:68:f2:98:90:46:84:27:c7: + 76:bf:e3:cc:35:2c:8b:5e:07:64:65:82:c0:48:b0: + a8:91:f9:61:9f:76:20:50:a8:91:c7:66:b5:eb:78: + 62:03:56:f0:8a:1a:13:ea:31:a3:1e:a0:99:fd:38: + f6:f6:27:32:58:6f:07:f5:6b:b8:fb:14:2b:af:b7: + aa:cc:d6:63:5f:73:8c:da:05:99:a8:38:a8:cb:17: + 78:36:51:ac:e9:9e:f4:78:3a:8d:cf:0f:d9:42:e2: + 98:0c:ab:2f:9f:0e:01:de:ef:9f:99:49:f1:2d:df: + ac:74:4d:1b:98:b5:47:c5:e5:29:d1:f9:90:18:c7: + 62:9c:be:83:c7:26:7b:3e:8a:25:c7:c0:dd:9d:e6: + 35:68:10:20:9d:8f:d8:de:d2:c3:84:9c:0d:5e:e8: + 2f:c9 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Subject Key Identifier: - 53:79:BF:5A:AA:2B:4A:CF:54:80:E1:D8:9B:C0:9D:F2:B2:03:66:CB + A0:11:0A:23:3E:96:F1:07:EC:E2:AF:29:EF:82:A5:7F:D0:30:A4:B4 X509v3 Key Usage: critical Certificate Sign, CRL Sign X509v3 Basic Constraints: critical CA:TRUE - Signature Algorithm: sha384WithRSAEncryption - 5c:d4:7c:0d:cf:f7:01:7d:41:99:65:0c:73:c5:52:9f:cb:f8: - cf:99:06:7f:1b:da:43:15:9f:9e:02:55:57:96:14:f1:52:3c: - 27:87:94:28:ed:1f:3a:01:37:a2:76:fc:53:50:c0:84:9b:c6: - 6b:4e:ba:8c:21:4f:a2:8e:55:62:91:f3:69:15:d8:bc:88:e3: - c4:aa:0b:fd:ef:a8:e9:4b:55:2a:06:20:6d:55:78:29:19:ee: - 5f:30:5c:4b:24:11:55:ff:24:9a:6e:5e:2a:2b:ee:0b:4d:9f: - 7f:f7:01:38:94:14:95:43:07:09:fb:60:a9:ee:1c:ab:12:8c: - a0:9a:5e:a7:98:6a:59:6d:8b:3f:08:fb:c8:d1:45:af:18:15: - 64:90:12:0f:73:28:2e:c5:e2:24:4e:fc:58:ec:f0:f4:45:fe: - 22:b3:eb:2f:8e:d2:d9:45:61:05:c1:97:6f:a8:76:72:8f:8b: - 8c:36:af:bf:0d:05:ce:71:8d:e6:a6:6f:1f:6c:a6:71:62:c5: + X509v3 CRL Distribution Points: + + Full Name: + URI:http://crl.comodoca.com/AAACertificateServices.crl + + Full Name: + URI:http://crl.comodo.net/AAACertificateServices.crl + + Signature Algorithm: sha1WithRSAEncryption + 08:56:fc:02:f0:9b:e8:ff:a4:fa:d6:7b:c6:44:80:ce:4f:c4: + c5:f6:00:58:cc:a6:b6:bc:14:49:68:04:76:e8:e6:ee:5d:ec: + 02:0f:60:d6:8d:50:18:4f:26:4e:01:e3:e6:b0:a5:ee:bf:bc: + 74:54:41:bf:fd:fc:12:b8:c7:4f:5a:f4:89:60:05:7f:60:b7: + 05:4a:f3:f6:f1:c2:bf:c4:b9:74:86:b6:2d:7d:6b:cc:d2:f3: + 46:dd:2f:c6:e0:6a:c3:c3:34:03:2c:7d:96:dd:5a:c2:0e:a7: + 0a:99:c1:05:8b:ab:0c:2f:f3:5c:3a:cf:6c:37:55:09:87:de: + 53:40:6c:58:ef:fc:b6:ab:65:6e:04:f6:1b:dc:3c:e0:5a:15: + c6:9e:d9:f1:59:48:30:21:65:03:6c:ec:e9:21:73:ec:9b:03: + a1:e0:37:ad:a0:15:18:8f:fa:ba:02:ce:a7:2c:a9:10:13:2c: + d4:e5:08:26:ab:22:97:60:f8:90:5e:74:d4:a2:9a:53:bd:f2: + a9:68:e0:a2:6e:c2:d7:6c:b1:a3:0f:9e:bf:eb:68:e7:56:f2: + ae:f2:e3:2b:38:3a:09:81:b5:6b:85:d7:be:2d:ed:3f:1a:b7: + b2:63:e2:f5:62:2c:82:d4:6a:00:41:50:f1:39:83:9f:95:e9: + 36:96:98:6e +SHA1 Fingerprint=D1:EB:23:A4:6D:17:D6:8F:D9:25:64:C2:F1:F1:60:17:64:D8:E3:49 +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb +MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow +GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj +YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM +GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua +BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 +YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR +rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm +ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU +oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v +QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t +b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF +AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz +Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 +G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi +l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 +smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 01:fd:6d:30:fc:a3:ca:51:a8:1b:bc:64:0e:35:03:2d + Signature Algorithm: sha384WithRSAEncryption + Issuer: C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust RSA Certification Authority + Validity + Not Before: Feb 1 00:00:00 2010 GMT + Not After : Jan 18 23:59:59 2038 GMT + Subject: C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust RSA Certification Authority + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (4096 bit) + Modulus: + 00:80:12:65:17:36:0e:c3:db:08:b3:d0:ac:57:0d: + 76:ed:cd:27:d3:4c:ad:50:83:61:e2:aa:20:4d:09: + 2d:64:09:dc:ce:89:9f:cc:3d:a9:ec:f6:cf:c1:dc: + f1:d3:b1:d6:7b:37:28:11:2b:47:da:39:c6:bc:3a: + 19:b4:5f:a6:bd:7d:9d:a3:63:42:b6:76:f2:a9:3b: + 2b:91:f8:e2:6f:d0:ec:16:20:90:09:3e:e2:e8:74: + c9:18:b4:91:d4:62:64:db:7f:a3:06:f1:88:18:6a: + 90:22:3c:bc:fe:13:f0:87:14:7b:f6:e4:1f:8e:d4: + e4:51:c6:11:67:46:08:51:cb:86:14:54:3f:bc:33: + fe:7e:6c:9c:ff:16:9d:18:bd:51:8e:35:a6:a7:66: + c8:72:67:db:21:66:b1:d4:9b:78:03:c0:50:3a:e8: + cc:f0:dc:bc:9e:4c:fe:af:05:96:35:1f:57:5a:b7: + ff:ce:f9:3d:b7:2c:b6:f6:54:dd:c8:e7:12:3a:4d: + ae:4c:8a:b7:5c:9a:b4:b7:20:3d:ca:7f:22:34:ae: + 7e:3b:68:66:01:44:e7:01:4e:46:53:9b:33:60:f7: + 94:be:53:37:90:73:43:f3:32:c3:53:ef:db:aa:fe: + 74:4e:69:c7:6b:8c:60:93:de:c4:c7:0c:df:e1:32: + ae:cc:93:3b:51:78:95:67:8b:ee:3d:56:fe:0c:d0: + 69:0f:1b:0f:f3:25:26:6b:33:6d:f7:6e:47:fa:73: + 43:e5:7e:0e:a5:66:b1:29:7c:32:84:63:55:89:c4: + 0d:c1:93:54:30:19:13:ac:d3:7d:37:a7:eb:5d:3a: + 6c:35:5c:db:41:d7:12:da:a9:49:0b:df:d8:80:8a: + 09:93:62:8e:b5:66:cf:25:88:cd:84:b8:b1:3f:a4: + 39:0f:d9:02:9e:eb:12:4c:95:7c:f3:6b:05:a9:5e: + 16:83:cc:b8:67:e2:e8:13:9d:cc:5b:82:d3:4c:b3: + ed:5b:ff:de:e5:73:ac:23:3b:2d:00:bf:35:55:74: + 09:49:d8:49:58:1a:7f:92:36:e6:51:92:0e:f3:26: + 7d:1c:4d:17:bc:c9:ec:43:26:d0:bf:41:5f:40:a9: + 44:44:f4:99:e7:57:87:9e:50:1f:57:54:a8:3e:fd: + 74:63:2f:b1:50:65:09:e6:58:42:2e:43:1a:4c:b4: + f0:25:47:59:fa:04:1e:93:d4:26:46:4a:50:81:b2: + de:be:78:b7:fc:67:15:e1:c9:57:84:1e:0f:63:d6: + e9:62:ba:d6:5f:55:2e:ea:5c:c6:28:08:04:25:39: + b8:0e:2b:a9:f2:4c:97:1c:07:3f:0d:52:f5:ed:ef: + 2f:82:0f + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + 53:79:BF:5A:AA:2B:4A:CF:54:80:E1:D8:9B:C0:9D:F2:B2:03:66:CB + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + Signature Algorithm: sha384WithRSAEncryption + 5c:d4:7c:0d:cf:f7:01:7d:41:99:65:0c:73:c5:52:9f:cb:f8: + cf:99:06:7f:1b:da:43:15:9f:9e:02:55:57:96:14:f1:52:3c: + 27:87:94:28:ed:1f:3a:01:37:a2:76:fc:53:50:c0:84:9b:c6: + 6b:4e:ba:8c:21:4f:a2:8e:55:62:91:f3:69:15:d8:bc:88:e3: + c4:aa:0b:fd:ef:a8:e9:4b:55:2a:06:20:6d:55:78:29:19:ee: + 5f:30:5c:4b:24:11:55:ff:24:9a:6e:5e:2a:2b:ee:0b:4d:9f: + 7f:f7:01:38:94:14:95:43:07:09:fb:60:a9:ee:1c:ab:12:8c: + a0:9a:5e:a7:98:6a:59:6d:8b:3f:08:fb:c8:d1:45:af:18:15: + 64:90:12:0f:73:28:2e:c5:e2:24:4e:fc:58:ec:f0:f4:45:fe: + 22:b3:eb:2f:8e:d2:d9:45:61:05:c1:97:6f:a8:76:72:8f:8b: + 8c:36:af:bf:0d:05:ce:71:8d:e6:a6:6f:1f:6c:a6:71:62:c5: d8:d0:83:72:0c:f1:67:11:89:0c:9c:13:4c:72:34:df:bc:d5: 71:df:aa:71:dd:e1:b9:6c:8c:3c:12:5d:65:da:bd:57:12:b6: 43:6b:ff:e5:de:4d:66:11:51:cf:99:ae:ec:17:b6:e8:71:91: @@ -1774,6 +1690,129 @@ BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G -----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 407555286 (0x184accd6) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C = CN, O = China Financial Certification Authority, CN = CFCA EV ROOT + Validity + Not Before: Aug 8 03:07:01 2012 GMT + Not After : Dec 31 03:07:01 2029 GMT + Subject: C = CN, O = China Financial Certification Authority, CN = CFCA EV ROOT + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (4096 bit) + Modulus: + 00:d7:5d:6b:cd:10:3f:1f:05:59:d5:05:4d:37:b1: + 0e:ec:98:2b:8e:15:1d:fa:93:4b:17:82:21:71:10: + 52:d7:51:64:70:16:c2:55:69:4d:8e:15:6d:9f:bf: + 0c:1b:c2:e0:a3:67:d6:0c:ac:cf:22:ae:af:77:54: + 2a:4b:4c:8a:53:52:7a:c3:ee:2e:de:b3:71:25:c1: + e9:5d:3d:ee:a1:2f:a3:f7:2a:3c:c9:23:1d:6a:ab: + 1d:a1:a7:f1:f3:ec:a0:d5:44:cf:15:cf:72:2f:1d: + 63:97:e8:99:f9:fd:93:a4:54:80:4c:52:d4:52:ab: + 2e:49:df:90:cd:b8:5f:be:3f:de:a1:ca:4d:20:d4: + 25:e8:84:29:53:b7:b1:88:1f:ff:fa:da:90:9f:0a: + a9:2d:41:3f:b1:f1:18:29:ee:16:59:2c:34:49:1a: + a8:06:d7:a8:88:d2:03:72:7a:32:e2:ea:68:4d:6e: + 2c:96:65:7b:ca:59:fa:f2:e2:dd:ee:30:2c:fb:cc: + 46:ac:c4:63:eb:6f:7f:36:2b:34:73:12:94:7f:df: + cc:26:9e:f1:72:5d:50:65:59:8f:69:b3:87:5e:32: + 6f:c3:18:8a:b5:95:8f:b0:7a:37:de:5a:45:3b:c7: + 36:e1:ef:67:d1:39:d3:97:5b:73:62:19:48:2d:87: + 1c:06:fb:74:98:20:49:73:f0:05:d2:1b:b1:a0:a3: + b7:1b:70:d3:88:69:b9:5a:d6:38:f4:62:dc:25:8b: + 78:bf:f8:e8:7e:b8:5c:c9:95:4f:5f:a7:2d:b9:20: + 6b:cf:6b:dd:f5:0d:f4:82:b7:f4:b2:66:2e:10:28: + f6:97:5a:7b:96:16:8f:01:19:2d:6c:6e:7f:39:58: + 06:64:83:01:83:83:c3:4d:92:dd:32:c6:87:a4:37: + e9:16:ce:aa:2d:68:af:0a:81:65:3a:70:c1:9b:ad: + 4d:6d:54:ca:2a:2d:4b:85:1b:b3:80:e6:70:45:0d: + 6b:5e:35:f0:7f:3b:b8:9c:e4:04:70:89:12:25:93: + da:0a:99:22:60:6a:63:60:4e:76:06:98:4e:bd:83: + ad:1d:58:8a:25:85:d2:c7:65:1e:2d:8e:c6:df:b6: + c6:e1:7f:8a:04:21:15:29:74:f0:3e:9c:90:9d:0c: + 2e:f1:8a:3e:5a:aa:0c:09:1e:c7:d5:3c:a3:ed:97: + c3:1e:34:fa:38:f9:08:0e:e3:c0:5d:2b:83:d1:56: + 6a:c9:b6:a8:54:53:2e:78:32:67:3d:82:7f:74:d0: + fb:e1:b6:05:60:b9:70:db:8e:0b:f9:13:58:6f:71: + 60:10:52:10:b9:c1:41:09:ef:72:1f:67:31:78:ff: + 96:05:8d + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Authority Key Identifier: + keyid:E3:FE:2D:FD:28:D0:0B:B5:BA:B6:A2:C4:BF:06:AA:05:8C:93:FB:2F + + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Subject Key Identifier: + E3:FE:2D:FD:28:D0:0B:B5:BA:B6:A2:C4:BF:06:AA:05:8C:93:FB:2F + Signature Algorithm: sha256WithRSAEncryption + 25:c6:ba:6b:eb:87:cb:de:82:39:96:3d:f0:44:a7:6b:84:73: + 03:de:9d:2b:4f:ba:20:7f:bc:78:b2:cf:97:b0:1b:9c:f3:d7: + 79:2e:f5:48:b6:d2:fb:17:88:e6:d3:7a:3f:ed:53:13:d0:e2: + 2f:6a:79:cb:00:23:28:e6:1e:37:57:35:89:84:c2:76:4f:34: + 36:ad:67:c3:ce:41:06:88:c5:f7:ee:d8:1a:b8:d6:0b:7f:50: + ff:93:aa:17:4b:8c:ec:ed:52:60:b2:a4:06:ea:4e:eb:f4:6b: + 19:fd:eb:f5:1a:e0:25:2a:9a:dc:c7:41:36:f7:c8:74:05:84: + 39:95:39:d6:0b:3b:a4:27:fa:08:d8:5c:1e:f8:04:60:52:11: + 28:28:03:ff:ef:53:66:00:a5:4a:34:16:66:7c:fd:09:a4:ae: + 9e:67:1a:6f:41:0b:6b:06:13:9b:8f:86:71:05:b4:2f:8d:89: + 66:33:29:76:54:9a:11:f8:27:fa:b2:3f:91:e0:ce:0d:1b:f3: + 30:1a:ad:bf:22:5d:1b:d3:bf:25:05:4d:e1:92:1a:7f:99:9f: + 3c:44:93:ca:d4:40:49:6c:80:87:d7:04:3a:c3:32:52:35:0e: + 56:f8:a5:dd:7d:c4:8b:0d:11:1f:53:cb:1e:b2:17:b6:68:77: + 5a:e0:d4:cb:c8:07:ae:f5:3a:2e:8e:37:b7:d0:01:4b:43:29: + 77:8c:39:97:8f:82:5a:f8:51:e5:89:a0:18:e7:68:7f:5d:0a: + 2e:fb:a3:47:0e:3d:a6:23:7a:c6:01:c7:8f:c8:5e:bf:6d:80: + 56:be:8a:24:ba:33:ea:9f:e1:32:11:9e:f1:d2:4f:80:f6:1b: + 40:af:38:9e:11:50:79:73:12:12:cd:e6:6c:9d:2c:88:72:3c: + 30:81:06:91:22:ea:59:ad:da:19:2e:22:c2:8d:b9:8c:87:e0: + 66:bc:73:23:5f:21:64:63:80:48:f5:a0:3c:18:3d:94:c8:48: + 41:1d:40:ba:5e:fe:fe:56:39:a1:c8:cf:5e:9e:19:64:46:10: + da:17:91:b7:05:80:ac:8b:99:92:7d:e7:a2:d8:07:0b:36:27: + e7:48:79:60:8a:c3:d7:13:5c:f8:72:40:df:4a:cb:cf:99:00: + 0a:00:0b:11:95:da:56:45:03:88:0a:9f:67:d0:d5:79:b1:a8: + 8d:40:6d:0d:c2:7a:40:fa:f3:5f:64:47:92:cb:53:b9:bb:59: + ce:4f:fd:d0:15:53:01:d8:df:eb:d9:e6:76:ef:d0:23:bb:3b: + a9:79:b3:d5:02:29:cd:89:a3:96:0f:4a:35:e7:4e:42:c0:75: + cd:07:cf:e6:2c:eb:7b:2e +SHA1 Fingerprint=E2:B8:29:4B:55:84:AB:6B:58:C2:90:46:6C:AC:3F:B8:39:8F:84:83 +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -1900,129 +1939,6 @@ ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y 4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza 8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u -----END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 407555286 (0x184accd6) - Signature Algorithm: sha256WithRSAEncryption - Issuer: C = CN, O = China Financial Certification Authority, CN = CFCA EV ROOT - Validity - Not Before: Aug 8 03:07:01 2012 GMT - Not After : Dec 31 03:07:01 2029 GMT - Subject: C = CN, O = China Financial Certification Authority, CN = CFCA EV ROOT - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (4096 bit) - Modulus: - 00:d7:5d:6b:cd:10:3f:1f:05:59:d5:05:4d:37:b1: - 0e:ec:98:2b:8e:15:1d:fa:93:4b:17:82:21:71:10: - 52:d7:51:64:70:16:c2:55:69:4d:8e:15:6d:9f:bf: - 0c:1b:c2:e0:a3:67:d6:0c:ac:cf:22:ae:af:77:54: - 2a:4b:4c:8a:53:52:7a:c3:ee:2e:de:b3:71:25:c1: - e9:5d:3d:ee:a1:2f:a3:f7:2a:3c:c9:23:1d:6a:ab: - 1d:a1:a7:f1:f3:ec:a0:d5:44:cf:15:cf:72:2f:1d: - 63:97:e8:99:f9:fd:93:a4:54:80:4c:52:d4:52:ab: - 2e:49:df:90:cd:b8:5f:be:3f:de:a1:ca:4d:20:d4: - 25:e8:84:29:53:b7:b1:88:1f:ff:fa:da:90:9f:0a: - a9:2d:41:3f:b1:f1:18:29:ee:16:59:2c:34:49:1a: - a8:06:d7:a8:88:d2:03:72:7a:32:e2:ea:68:4d:6e: - 2c:96:65:7b:ca:59:fa:f2:e2:dd:ee:30:2c:fb:cc: - 46:ac:c4:63:eb:6f:7f:36:2b:34:73:12:94:7f:df: - cc:26:9e:f1:72:5d:50:65:59:8f:69:b3:87:5e:32: - 6f:c3:18:8a:b5:95:8f:b0:7a:37:de:5a:45:3b:c7: - 36:e1:ef:67:d1:39:d3:97:5b:73:62:19:48:2d:87: - 1c:06:fb:74:98:20:49:73:f0:05:d2:1b:b1:a0:a3: - b7:1b:70:d3:88:69:b9:5a:d6:38:f4:62:dc:25:8b: - 78:bf:f8:e8:7e:b8:5c:c9:95:4f:5f:a7:2d:b9:20: - 6b:cf:6b:dd:f5:0d:f4:82:b7:f4:b2:66:2e:10:28: - f6:97:5a:7b:96:16:8f:01:19:2d:6c:6e:7f:39:58: - 06:64:83:01:83:83:c3:4d:92:dd:32:c6:87:a4:37: - e9:16:ce:aa:2d:68:af:0a:81:65:3a:70:c1:9b:ad: - 4d:6d:54:ca:2a:2d:4b:85:1b:b3:80:e6:70:45:0d: - 6b:5e:35:f0:7f:3b:b8:9c:e4:04:70:89:12:25:93: - da:0a:99:22:60:6a:63:60:4e:76:06:98:4e:bd:83: - ad:1d:58:8a:25:85:d2:c7:65:1e:2d:8e:c6:df:b6: - c6:e1:7f:8a:04:21:15:29:74:f0:3e:9c:90:9d:0c: - 2e:f1:8a:3e:5a:aa:0c:09:1e:c7:d5:3c:a3:ed:97: - c3:1e:34:fa:38:f9:08:0e:e3:c0:5d:2b:83:d1:56: - 6a:c9:b6:a8:54:53:2e:78:32:67:3d:82:7f:74:d0: - fb:e1:b6:05:60:b9:70:db:8e:0b:f9:13:58:6f:71: - 60:10:52:10:b9:c1:41:09:ef:72:1f:67:31:78:ff: - 96:05:8d - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Authority Key Identifier: - keyid:E3:FE:2D:FD:28:D0:0B:B5:BA:B6:A2:C4:BF:06:AA:05:8C:93:FB:2F - - X509v3 Basic Constraints: critical - CA:TRUE - X509v3 Key Usage: critical - Certificate Sign, CRL Sign - X509v3 Subject Key Identifier: - E3:FE:2D:FD:28:D0:0B:B5:BA:B6:A2:C4:BF:06:AA:05:8C:93:FB:2F - Signature Algorithm: sha256WithRSAEncryption - 25:c6:ba:6b:eb:87:cb:de:82:39:96:3d:f0:44:a7:6b:84:73: - 03:de:9d:2b:4f:ba:20:7f:bc:78:b2:cf:97:b0:1b:9c:f3:d7: - 79:2e:f5:48:b6:d2:fb:17:88:e6:d3:7a:3f:ed:53:13:d0:e2: - 2f:6a:79:cb:00:23:28:e6:1e:37:57:35:89:84:c2:76:4f:34: - 36:ad:67:c3:ce:41:06:88:c5:f7:ee:d8:1a:b8:d6:0b:7f:50: - ff:93:aa:17:4b:8c:ec:ed:52:60:b2:a4:06:ea:4e:eb:f4:6b: - 19:fd:eb:f5:1a:e0:25:2a:9a:dc:c7:41:36:f7:c8:74:05:84: - 39:95:39:d6:0b:3b:a4:27:fa:08:d8:5c:1e:f8:04:60:52:11: - 28:28:03:ff:ef:53:66:00:a5:4a:34:16:66:7c:fd:09:a4:ae: - 9e:67:1a:6f:41:0b:6b:06:13:9b:8f:86:71:05:b4:2f:8d:89: - 66:33:29:76:54:9a:11:f8:27:fa:b2:3f:91:e0:ce:0d:1b:f3: - 30:1a:ad:bf:22:5d:1b:d3:bf:25:05:4d:e1:92:1a:7f:99:9f: - 3c:44:93:ca:d4:40:49:6c:80:87:d7:04:3a:c3:32:52:35:0e: - 56:f8:a5:dd:7d:c4:8b:0d:11:1f:53:cb:1e:b2:17:b6:68:77: - 5a:e0:d4:cb:c8:07:ae:f5:3a:2e:8e:37:b7:d0:01:4b:43:29: - 77:8c:39:97:8f:82:5a:f8:51:e5:89:a0:18:e7:68:7f:5d:0a: - 2e:fb:a3:47:0e:3d:a6:23:7a:c6:01:c7:8f:c8:5e:bf:6d:80: - 56:be:8a:24:ba:33:ea:9f:e1:32:11:9e:f1:d2:4f:80:f6:1b: - 40:af:38:9e:11:50:79:73:12:12:cd:e6:6c:9d:2c:88:72:3c: - 30:81:06:91:22:ea:59:ad:da:19:2e:22:c2:8d:b9:8c:87:e0: - 66:bc:73:23:5f:21:64:63:80:48:f5:a0:3c:18:3d:94:c8:48: - 41:1d:40:ba:5e:fe:fe:56:39:a1:c8:cf:5e:9e:19:64:46:10: - da:17:91:b7:05:80:ac:8b:99:92:7d:e7:a2:d8:07:0b:36:27: - e7:48:79:60:8a:c3:d7:13:5c:f8:72:40:df:4a:cb:cf:99:00: - 0a:00:0b:11:95:da:56:45:03:88:0a:9f:67:d0:d5:79:b1:a8: - 8d:40:6d:0d:c2:7a:40:fa:f3:5f:64:47:92:cb:53:b9:bb:59: - ce:4f:fd:d0:15:53:01:d8:df:eb:d9:e6:76:ef:d0:23:bb:3b: - a9:79:b3:d5:02:29:cd:89:a3:96:0f:4a:35:e7:4e:42:c0:75: - cd:07:cf:e6:2c:eb:7b:2e -SHA1 Fingerprint=E2:B8:29:4B:55:84:AB:6B:58:C2:90:46:6C:AC:3F:B8:39:8F:84:83 ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD -TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y -aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx -MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j -aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP -T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 -sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL -TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 -/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp -7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz -EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt -hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP -a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot -aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg -TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV -PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv -cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL -tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd -BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB -ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT -ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL -jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS -ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy -P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 -xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d -Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN -5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe -/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z -AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ -5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su ------END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -2898,35 +2814,134 @@ Certificate: d6:c7:d3:e0:fb:09:60:6c SHA1 Fingerprint=5A:8C:EF:45:D7:A6:98:59:76:7A:8C:8B:44:96:B5:78:CF:47:4B:1A -----BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK -gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ -W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg -1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K -8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r -2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me -z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR -8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj -mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz -7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 -+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI -0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB -Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm -UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 -LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY -+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS -k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl -7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm -btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl -urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ -fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 -n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE -76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H -9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT -4PsJYGw= +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 06:6c:9f:d5:74:97:36:66:3f:3b:0b:9a:d9:e8:9e:76:03:f2:4a + Signature Algorithm: ecdsa-with-SHA256 + Issuer: C = US, O = Amazon, CN = Amazon Root CA 3 + Validity + Not Before: May 26 00:00:00 2015 GMT + Not After : May 26 00:00:00 2040 GMT + Subject: C = US, O = Amazon, CN = Amazon Root CA 3 + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:29:97:a7:c6:41:7f:c0:0d:9b:e8:01:1b:56:c6: + f2:52:a5:ba:2d:b2:12:e8:d2:2e:d7:fa:c9:c5:d8: + aa:6d:1f:73:81:3b:3b:98:6b:39:7c:33:a5:c5:4e: + 86:8e:80:17:68:62:45:57:7d:44:58:1d:b3:37:e5: + 67:08:eb:66:de + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Key Usage: critical + Digital Signature, Certificate Sign, CRL Sign + X509v3 Subject Key Identifier: + AB:B6:DB:D7:06:9E:37:AC:30:86:07:91:70:C7:9C:C4:19:B1:78:C0 + Signature Algorithm: ecdsa-with-SHA256 + 30:46:02:21:00:e0:85:92:a3:17:b7:8d:f9:2b:06:a5:93:ac: + 1a:98:68:61:72:fa:e1:a1:d0:fb:1c:78:60:a6:43:99:c5:b8: + c4:02:21:00:9c:02:ef:f1:94:9c:b3:96:f9:eb:c6:2a:f8:b6: + 2c:fe:3a:90:14:16:d7:8c:63:24:48:1c:df:30:7d:d5:68:3b +SHA1 Fingerprint=0D:44:DD:8C:3C:8C:1A:1A:58:75:64:81:E9:0F:2E:2A:FF:B3:D2:6E +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 06:6c:9f:d7:c1:bb:10:4c:29:43:e5:71:7b:7b:2c:c8:1a:c1:0e + Signature Algorithm: ecdsa-with-SHA384 + Issuer: C = US, O = Amazon, CN = Amazon Root CA 4 + Validity + Not Before: May 26 00:00:00 2015 GMT + Not After : May 26 00:00:00 2040 GMT + Subject: C = US, O = Amazon, CN = Amazon Root CA 4 + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (384 bit) + pub: + 04:d2:ab:8a:37:4f:a3:53:0d:fe:c1:8a:7b:4b:a8: + 7b:46:4b:63:b0:62:f6:2d:1b:db:08:71:21:d2:00: + e8:63:bd:9a:27:fb:f0:39:6e:5d:ea:3d:a5:c9:81: + aa:a3:5b:20:98:45:5d:16:db:fd:e8:10:6d:e3:9c: + e0:e3:bd:5f:84:62:f3:70:64:33:a0:cb:24:2f:70: + ba:88:a1:2a:a0:75:f8:81:ae:62:06:c4:81:db:39: + 6e:29:b0:1e:fa:2e:5c + ASN1 OID: secp384r1 + NIST CURVE: P-384 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Key Usage: critical + Digital Signature, Certificate Sign, CRL Sign + X509v3 Subject Key Identifier: + D3:EC:C7:3A:65:6E:CC:E1:DA:76:9A:56:FB:9C:F3:86:6D:57:E5:81 + Signature Algorithm: ecdsa-with-SHA384 + 30:65:02:30:3a:8b:21:f1:bd:7e:11:ad:d0:ef:58:96:2f:d6: + eb:9d:7e:90:8d:2b:cf:66:55:c3:2c:e3:28:a9:70:0a:47:0e: + f0:37:59:12:ff:2d:99:94:28:4e:2a:4f:35:4d:33:5a:02:31: + 00:ea:75:00:4e:3b:c4:3a:94:12:91:c9:58:46:9d:21:13:72: + a7:88:9c:8a:e4:4c:4a:db:96:d4:ac:8b:6b:6b:49:12:53:33: + ad:d7:e4:be:24:fc:b5:0a:76:d4:a5:bc:10 +SHA1 Fingerprint=F6:10:84:07:D6:F8:BB:67:98:0C:C2:E2:44:C2:EB:AE:1C:EF:63:BE +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== -----END CERTIFICATE----- Certificate: Data: @@ -3065,105 +3080,6 @@ Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK 4SVhM7JZG+Ju1zdXtg2pEto= -----END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - 06:6c:9f:d5:74:97:36:66:3f:3b:0b:9a:d9:e8:9e:76:03:f2:4a - Signature Algorithm: ecdsa-with-SHA256 - Issuer: C = US, O = Amazon, CN = Amazon Root CA 3 - Validity - Not Before: May 26 00:00:00 2015 GMT - Not After : May 26 00:00:00 2040 GMT - Subject: C = US, O = Amazon, CN = Amazon Root CA 3 - Subject Public Key Info: - Public Key Algorithm: id-ecPublicKey - Public-Key: (256 bit) - pub: - 04:29:97:a7:c6:41:7f:c0:0d:9b:e8:01:1b:56:c6: - f2:52:a5:ba:2d:b2:12:e8:d2:2e:d7:fa:c9:c5:d8: - aa:6d:1f:73:81:3b:3b:98:6b:39:7c:33:a5:c5:4e: - 86:8e:80:17:68:62:45:57:7d:44:58:1d:b3:37:e5: - 67:08:eb:66:de - ASN1 OID: prime256v1 - NIST CURVE: P-256 - X509v3 extensions: - X509v3 Basic Constraints: critical - CA:TRUE - X509v3 Key Usage: critical - Digital Signature, Certificate Sign, CRL Sign - X509v3 Subject Key Identifier: - AB:B6:DB:D7:06:9E:37:AC:30:86:07:91:70:C7:9C:C4:19:B1:78:C0 - Signature Algorithm: ecdsa-with-SHA256 - 30:46:02:21:00:e0:85:92:a3:17:b7:8d:f9:2b:06:a5:93:ac: - 1a:98:68:61:72:fa:e1:a1:d0:fb:1c:78:60:a6:43:99:c5:b8: - c4:02:21:00:9c:02:ef:f1:94:9c:b3:96:f9:eb:c6:2a:f8:b6: - 2c:fe:3a:90:14:16:d7:8c:63:24:48:1c:df:30:7d:d5:68:3b -SHA1 Fingerprint=0D:44:DD:8C:3C:8C:1A:1A:58:75:64:81:E9:0F:2E:2A:FF:B3:D2:6E ------BEGIN CERTIFICATE----- -MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl -ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j -QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr -ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr -BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM -YyRIHN8wfdVoOw== ------END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - 06:6c:9f:d7:c1:bb:10:4c:29:43:e5:71:7b:7b:2c:c8:1a:c1:0e - Signature Algorithm: ecdsa-with-SHA384 - Issuer: C = US, O = Amazon, CN = Amazon Root CA 4 - Validity - Not Before: May 26 00:00:00 2015 GMT - Not After : May 26 00:00:00 2040 GMT - Subject: C = US, O = Amazon, CN = Amazon Root CA 4 - Subject Public Key Info: - Public Key Algorithm: id-ecPublicKey - Public-Key: (384 bit) - pub: - 04:d2:ab:8a:37:4f:a3:53:0d:fe:c1:8a:7b:4b:a8: - 7b:46:4b:63:b0:62:f6:2d:1b:db:08:71:21:d2:00: - e8:63:bd:9a:27:fb:f0:39:6e:5d:ea:3d:a5:c9:81: - aa:a3:5b:20:98:45:5d:16:db:fd:e8:10:6d:e3:9c: - e0:e3:bd:5f:84:62:f3:70:64:33:a0:cb:24:2f:70: - ba:88:a1:2a:a0:75:f8:81:ae:62:06:c4:81:db:39: - 6e:29:b0:1e:fa:2e:5c - ASN1 OID: secp384r1 - NIST CURVE: P-384 - X509v3 extensions: - X509v3 Basic Constraints: critical - CA:TRUE - X509v3 Key Usage: critical - Digital Signature, Certificate Sign, CRL Sign - X509v3 Subject Key Identifier: - D3:EC:C7:3A:65:6E:CC:E1:DA:76:9A:56:FB:9C:F3:86:6D:57:E5:81 - Signature Algorithm: ecdsa-with-SHA384 - 30:65:02:30:3a:8b:21:f1:bd:7e:11:ad:d0:ef:58:96:2f:d6: - eb:9d:7e:90:8d:2b:cf:66:55:c3:2c:e3:28:a9:70:0a:47:0e: - f0:37:59:12:ff:2d:99:94:28:4e:2a:4f:35:4d:33:5a:02:31: - 00:ea:75:00:4e:3b:c4:3a:94:12:91:c9:58:46:9d:21:13:72: - a7:88:9c:8a:e4:4c:4a:db:96:d4:ac:8b:6b:6b:49:12:53:33: - ad:d7:e4:be:24:fc:b5:0a:76:d4:a5:bc:10 -SHA1 Fingerprint=F6:10:84:07:D6:F8:BB:67:98:0C:C2:E2:44:C2:EB:AE:1C:EF:63:BE ------BEGIN CERTIFICATE----- -MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 -MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g -Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG -A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg -Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi -9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk -M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB -/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB -MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw -CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW -1KyLa2tJElMzrdfkviT8tQp21KW8EA== ------END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -3844,86 +3760,9 @@ BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI 7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD -VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T -kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ -gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl ------END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 0 (0x0) - Signature Algorithm: sha1WithRSAEncryption - Issuer: C = JP, O = SECOM Trust.net, OU = Security Communication RootCA1 - Validity - Not Before: Sep 30 04:20:49 2003 GMT - Not After : Sep 30 04:20:49 2023 GMT - Subject: C = JP, O = SECOM Trust.net, OU = Security Communication RootCA1 - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (2048 bit) - Modulus: - 00:b3:b3:fe:7f:d3:6d:b1:ef:16:7c:57:a5:0c:6d: - 76:8a:2f:4b:bf:64:fb:4c:ee:8a:f0:f3:29:7c:f5: - ff:ee:2a:e0:e9:e9:ba:5b:64:22:9a:9a:6f:2c:3a: - 26:69:51:05:99:26:dc:d5:1c:6a:71:c6:9a:7d:1e: - 9d:dd:7c:6c:c6:8c:67:67:4a:3e:f8:71:b0:19:27: - a9:09:0c:a6:95:bf:4b:8c:0c:fa:55:98:3b:d8:e8: - 22:a1:4b:71:38:79:ac:97:92:69:b3:89:7e:ea:21: - 68:06:98:14:96:87:d2:61:36:bc:6d:27:56:9e:57: - ee:c0:c0:56:fd:32:cf:a4:d9:8e:c2:23:d7:8d:a8: - f3:d8:25:ac:97:e4:70:38:f4:b6:3a:b4:9d:3b:97: - 26:43:a3:a1:bc:49:59:72:4c:23:30:87:01:58:f6: - 4e:be:1c:68:56:66:af:cd:41:5d:c8:b3:4d:2a:55: - 46:ab:1f:da:1e:e2:40:3d:db:cd:7d:b9:92:80:9c: - 37:dd:0c:96:64:9d:dc:22:f7:64:8b:df:61:de:15: - 94:52:15:a0:7d:52:c9:4b:a8:21:c9:c6:b1:ed:cb: - c3:95:60:d1:0f:f0:ab:70:f8:df:cb:4d:7e:ec:d6: - fa:ab:d9:bd:7f:54:f2:a5:e9:79:fa:d9:d6:76:24: - 28:73 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - A0:73:49:99:68:DC:85:5B:65:E3:9B:28:2F:57:9F:BD:33:BC:07:48 - X509v3 Key Usage: - Certificate Sign, CRL Sign - X509v3 Basic Constraints: critical - CA:TRUE - Signature Algorithm: sha1WithRSAEncryption - 68:40:a9:a8:bb:e4:4f:5d:79:b3:05:b5:17:b3:60:13:eb:c6: - 92:5d:e0:d1:d3:6a:fe:fb:be:9b:6d:bf:c7:05:6d:59:20:c4: - 1c:f0:b7:da:84:58:02:63:fa:48:16:ef:4f:a5:0b:f7:4a:98: - f2:3f:9e:1b:ad:47:6b:63:ce:08:47:eb:52:3f:78:9c:af:4d: - ae:f8:d5:4f:cf:9a:98:2a:10:41:39:52:c4:dd:d9:9b:0e:ef: - 93:01:ae:b2:2e:ca:68:42:24:42:6c:b0:b3:3a:3e:cd:e9:da: - 48:c4:15:cb:e9:f9:07:0f:92:50:49:8a:dd:31:97:5f:c9:e9: - 37:aa:3b:59:65:97:94:32:c9:b3:9f:3e:3a:62:58:c5:49:ad: - 62:0e:71:a5:32:aa:2f:c6:89:76:43:40:13:13:67:3d:a2:54: - 25:10:cb:f1:3a:f2:d9:fa:db:49:56:bb:a6:fe:a7:41:35:c3: - e0:88:61:c9:88:c7:df:36:10:22:98:59:ea:b0:4a:fb:56:16: - 73:6e:ac:4d:f7:22:a1:4f:ad:1d:7a:2d:45:27:e5:30:c1:5e: - f2:da:13:cb:25:42:51:95:47:03:8c:6c:21:cc:74:42:ed:53: - ff:33:8b:8f:0f:57:01:16:2f:cf:a6:ee:c9:70:22:14:bd:fd: - be:6c:0b:03 -SHA1 Fingerprint=36:B1:2B:49:F9:81:9E:D7:4C:9E:BC:38:0F:C6:56:8F:5D:AC:B2:F7 ------BEGIN CERTIFICATE----- -MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY -MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t -dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 -WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD -VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 -9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ -DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 -Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N -QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ -xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G -A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T -AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG -kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr -Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 -Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU -JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot -RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl -----END CERTIFICATE----- Certificate: Data: @@ -4050,6 +3889,83 @@ w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== -----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 0 (0x0) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C = JP, O = SECOM Trust.net, OU = Security Communication RootCA1 + Validity + Not Before: Sep 30 04:20:49 2003 GMT + Not After : Sep 30 04:20:49 2023 GMT + Subject: C = JP, O = SECOM Trust.net, OU = Security Communication RootCA1 + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:b3:b3:fe:7f:d3:6d:b1:ef:16:7c:57:a5:0c:6d: + 76:8a:2f:4b:bf:64:fb:4c:ee:8a:f0:f3:29:7c:f5: + ff:ee:2a:e0:e9:e9:ba:5b:64:22:9a:9a:6f:2c:3a: + 26:69:51:05:99:26:dc:d5:1c:6a:71:c6:9a:7d:1e: + 9d:dd:7c:6c:c6:8c:67:67:4a:3e:f8:71:b0:19:27: + a9:09:0c:a6:95:bf:4b:8c:0c:fa:55:98:3b:d8:e8: + 22:a1:4b:71:38:79:ac:97:92:69:b3:89:7e:ea:21: + 68:06:98:14:96:87:d2:61:36:bc:6d:27:56:9e:57: + ee:c0:c0:56:fd:32:cf:a4:d9:8e:c2:23:d7:8d:a8: + f3:d8:25:ac:97:e4:70:38:f4:b6:3a:b4:9d:3b:97: + 26:43:a3:a1:bc:49:59:72:4c:23:30:87:01:58:f6: + 4e:be:1c:68:56:66:af:cd:41:5d:c8:b3:4d:2a:55: + 46:ab:1f:da:1e:e2:40:3d:db:cd:7d:b9:92:80:9c: + 37:dd:0c:96:64:9d:dc:22:f7:64:8b:df:61:de:15: + 94:52:15:a0:7d:52:c9:4b:a8:21:c9:c6:b1:ed:cb: + c3:95:60:d1:0f:f0:ab:70:f8:df:cb:4d:7e:ec:d6: + fa:ab:d9:bd:7f:54:f2:a5:e9:79:fa:d9:d6:76:24: + 28:73 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + A0:73:49:99:68:DC:85:5B:65:E3:9B:28:2F:57:9F:BD:33:BC:07:48 + X509v3 Key Usage: + Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + Signature Algorithm: sha1WithRSAEncryption + 68:40:a9:a8:bb:e4:4f:5d:79:b3:05:b5:17:b3:60:13:eb:c6: + 92:5d:e0:d1:d3:6a:fe:fb:be:9b:6d:bf:c7:05:6d:59:20:c4: + 1c:f0:b7:da:84:58:02:63:fa:48:16:ef:4f:a5:0b:f7:4a:98: + f2:3f:9e:1b:ad:47:6b:63:ce:08:47:eb:52:3f:78:9c:af:4d: + ae:f8:d5:4f:cf:9a:98:2a:10:41:39:52:c4:dd:d9:9b:0e:ef: + 93:01:ae:b2:2e:ca:68:42:24:42:6c:b0:b3:3a:3e:cd:e9:da: + 48:c4:15:cb:e9:f9:07:0f:92:50:49:8a:dd:31:97:5f:c9:e9: + 37:aa:3b:59:65:97:94:32:c9:b3:9f:3e:3a:62:58:c5:49:ad: + 62:0e:71:a5:32:aa:2f:c6:89:76:43:40:13:13:67:3d:a2:54: + 25:10:cb:f1:3a:f2:d9:fa:db:49:56:bb:a6:fe:a7:41:35:c3: + e0:88:61:c9:88:c7:df:36:10:22:98:59:ea:b0:4a:fb:56:16: + 73:6e:ac:4d:f7:22:a1:4f:ad:1d:7a:2d:45:27:e5:30:c1:5e: + f2:da:13:cb:25:42:51:95:47:03:8c:6c:21:cc:74:42:ed:53: + ff:33:8b:8f:0f:57:01:16:2f:cf:a6:ee:c9:70:22:14:bd:fd: + be:6c:0b:03 +SHA1 Fingerprint=36:B1:2B:49:F9:81:9E:D7:4C:9E:BC:38:0F:C6:56:8F:5D:AC:B2:F7 +-----BEGIN CERTIFICATE----- +MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY +MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t +dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5 +WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD +VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8 +9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ +DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9 +Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N +QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ +xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G +A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG +kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr +Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5 +Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU +JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot +RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw== +-----END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -6665,98 +6581,6 @@ CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= -----END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - 50:94:6c:ec:18:ea:d5:9c:4d:d5:97:ef:75:8f:a0:ad - Signature Algorithm: sha1WithRSAEncryption - Issuer: C = US, OU = www.xrampsecurity.com, O = XRamp Security Services Inc, CN = XRamp Global Certification Authority - Validity - Not Before: Nov 1 17:14:04 2004 GMT - Not After : Jan 1 05:37:19 2035 GMT - Subject: C = US, OU = www.xrampsecurity.com, O = XRamp Security Services Inc, CN = XRamp Global Certification Authority - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (2048 bit) - Modulus: - 00:98:24:1e:bd:15:b4:ba:df:c7:8c:a5:27:b6:38: - 0b:69:f3:b6:4e:a8:2c:2e:21:1d:5c:44:df:21:5d: - 7e:23:74:fe:5e:7e:b4:4a:b7:a6:ad:1f:ae:e0:06: - 16:e2:9b:5b:d9:67:74:6b:5d:80:8f:29:9d:86:1b: - d9:9c:0d:98:6d:76:10:28:58:e4:65:b0:7f:4a:98: - 79:9f:e0:c3:31:7e:80:2b:b5:8c:c0:40:3b:11:86: - d0:cb:a2:86:36:60:a4:d5:30:82:6d:d9:6e:d0:0f: - 12:04:33:97:5f:4f:61:5a:f0:e4:f9:91:ab:e7:1d: - 3b:bc:e8:cf:f4:6b:2d:34:7c:e2:48:61:1c:8e:f3: - 61:44:cc:6f:a0:4a:a9:94:b0:4d:da:e7:a9:34:7a: - 72:38:a8:41:cc:3c:94:11:7d:eb:c8:a6:8c:b7:86: - cb:ca:33:3b:d9:3d:37:8b:fb:7a:3e:86:2c:e7:73: - d7:0a:57:ac:64:9b:19:eb:f4:0f:04:08:8a:ac:03: - 17:19:64:f4:5a:25:22:8d:34:2c:b2:f6:68:1d:12: - 6d:d3:8a:1e:14:da:c4:8f:a6:e2:23:85:d5:7a:0d: - bd:6a:e0:e9:ec:ec:17:bb:42:1b:67:aa:25:ed:45: - 83:21:fc:c1:c9:7c:d5:62:3e:fa:f2:c5:2d:d3:fd: - d4:65 - Exponent: 65537 (0x10001) - X509v3 extensions: - 1.3.6.1.4.1.311.20.2: - ...C.A - X509v3 Key Usage: - Digital Signature, Certificate Sign, CRL Sign - X509v3 Basic Constraints: critical - CA:TRUE - X509v3 Subject Key Identifier: - C6:4F:A2:3D:06:63:84:09:9C:CE:62:E4:04:AC:8D:5C:B5:E9:B6:1B - X509v3 CRL Distribution Points: - - Full Name: - URI:http://crl.xrampsecurity.com/XGCA.crl - - 1.3.6.1.4.1.311.21.1: - ... - Signature Algorithm: sha1WithRSAEncryption - 91:15:39:03:01:1b:67:fb:4a:1c:f9:0a:60:5b:a1:da:4d:97: - 62:f9:24:53:27:d7:82:64:4e:90:2e:c3:49:1b:2b:9a:dc:fc: - a8:78:67:35:f1:1d:f0:11:bd:b7:48:e3:10:f6:0d:df:3f:d2: - c9:b6:aa:55:a4:48:ba:02:db:de:59:2e:15:5b:3b:9d:16:7d: - 47:d7:37:ea:5f:4d:76:12:36:bb:1f:d7:a1:81:04:46:20:a3: - 2c:6d:a9:9e:01:7e:3f:29:ce:00:93:df:fd:c9:92:73:89:89: - 64:9e:e7:2b:e4:1c:91:2c:d2:b9:ce:7d:ce:6f:31:99:d3:e6: - be:d2:1e:90:f0:09:14:79:5c:23:ab:4d:d2:da:21:1f:4d:99: - 79:9d:e1:cf:27:9f:10:9b:1c:88:0d:b0:8a:64:41:31:b8:0e: - 6c:90:24:a4:9b:5c:71:8f:ba:bb:7e:1c:1b:db:6a:80:0f:21: - bc:e9:db:a6:b7:40:f4:b2:8b:a9:b1:e4:ef:9a:1a:d0:3d:69: - 99:ee:a8:28:a3:e1:3c:b3:f0:b2:11:9c:cf:7c:40:e6:dd:e7: - 43:7d:a2:d8:3a:b5:a9:8d:f2:34:99:c4:d4:10:e1:06:fd:09: - 84:10:3b:ee:c4:4c:f4:ec:27:7c:42:c2:74:7c:82:8a:09:c9: - b4:03:25:bc -SHA1 Fingerprint=B8:01:86:D1:EB:9C:86:A5:41:04:CF:30:54:F3:4C:52:B7:E5:58:C6 ------BEGIN CERTIFICATE----- -MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB -gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk -MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY -UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx -NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 -dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy -dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 -38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP -KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q -DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 -qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa -JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi -PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P -BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs -jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 -eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD -ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR -vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt -qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa -IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy -i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ -O+7ETPTsJ3xCwnR8gooJybQDJbw= ------END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -6879,6 +6703,98 @@ aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU 63ZTGI0RmLo= -----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 50:94:6c:ec:18:ea:d5:9c:4d:d5:97:ef:75:8f:a0:ad + Signature Algorithm: sha1WithRSAEncryption + Issuer: C = US, OU = www.xrampsecurity.com, O = XRamp Security Services Inc, CN = XRamp Global Certification Authority + Validity + Not Before: Nov 1 17:14:04 2004 GMT + Not After : Jan 1 05:37:19 2035 GMT + Subject: C = US, OU = www.xrampsecurity.com, O = XRamp Security Services Inc, CN = XRamp Global Certification Authority + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:98:24:1e:bd:15:b4:ba:df:c7:8c:a5:27:b6:38: + 0b:69:f3:b6:4e:a8:2c:2e:21:1d:5c:44:df:21:5d: + 7e:23:74:fe:5e:7e:b4:4a:b7:a6:ad:1f:ae:e0:06: + 16:e2:9b:5b:d9:67:74:6b:5d:80:8f:29:9d:86:1b: + d9:9c:0d:98:6d:76:10:28:58:e4:65:b0:7f:4a:98: + 79:9f:e0:c3:31:7e:80:2b:b5:8c:c0:40:3b:11:86: + d0:cb:a2:86:36:60:a4:d5:30:82:6d:d9:6e:d0:0f: + 12:04:33:97:5f:4f:61:5a:f0:e4:f9:91:ab:e7:1d: + 3b:bc:e8:cf:f4:6b:2d:34:7c:e2:48:61:1c:8e:f3: + 61:44:cc:6f:a0:4a:a9:94:b0:4d:da:e7:a9:34:7a: + 72:38:a8:41:cc:3c:94:11:7d:eb:c8:a6:8c:b7:86: + cb:ca:33:3b:d9:3d:37:8b:fb:7a:3e:86:2c:e7:73: + d7:0a:57:ac:64:9b:19:eb:f4:0f:04:08:8a:ac:03: + 17:19:64:f4:5a:25:22:8d:34:2c:b2:f6:68:1d:12: + 6d:d3:8a:1e:14:da:c4:8f:a6:e2:23:85:d5:7a:0d: + bd:6a:e0:e9:ec:ec:17:bb:42:1b:67:aa:25:ed:45: + 83:21:fc:c1:c9:7c:d5:62:3e:fa:f2:c5:2d:d3:fd: + d4:65 + Exponent: 65537 (0x10001) + X509v3 extensions: + 1.3.6.1.4.1.311.20.2: + ...C.A + X509v3 Key Usage: + Digital Signature, Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + C6:4F:A2:3D:06:63:84:09:9C:CE:62:E4:04:AC:8D:5C:B5:E9:B6:1B + X509v3 CRL Distribution Points: + + Full Name: + URI:http://crl.xrampsecurity.com/XGCA.crl + + 1.3.6.1.4.1.311.21.1: + ... + Signature Algorithm: sha1WithRSAEncryption + 91:15:39:03:01:1b:67:fb:4a:1c:f9:0a:60:5b:a1:da:4d:97: + 62:f9:24:53:27:d7:82:64:4e:90:2e:c3:49:1b:2b:9a:dc:fc: + a8:78:67:35:f1:1d:f0:11:bd:b7:48:e3:10:f6:0d:df:3f:d2: + c9:b6:aa:55:a4:48:ba:02:db:de:59:2e:15:5b:3b:9d:16:7d: + 47:d7:37:ea:5f:4d:76:12:36:bb:1f:d7:a1:81:04:46:20:a3: + 2c:6d:a9:9e:01:7e:3f:29:ce:00:93:df:fd:c9:92:73:89:89: + 64:9e:e7:2b:e4:1c:91:2c:d2:b9:ce:7d:ce:6f:31:99:d3:e6: + be:d2:1e:90:f0:09:14:79:5c:23:ab:4d:d2:da:21:1f:4d:99: + 79:9d:e1:cf:27:9f:10:9b:1c:88:0d:b0:8a:64:41:31:b8:0e: + 6c:90:24:a4:9b:5c:71:8f:ba:bb:7e:1c:1b:db:6a:80:0f:21: + bc:e9:db:a6:b7:40:f4:b2:8b:a9:b1:e4:ef:9a:1a:d0:3d:69: + 99:ee:a8:28:a3:e1:3c:b3:f0:b2:11:9c:cf:7c:40:e6:dd:e7: + 43:7d:a2:d8:3a:b5:a9:8d:f2:34:99:c4:d4:10:e1:06:fd:09: + 84:10:3b:ee:c4:4c:f4:ec:27:7c:42:c2:74:7c:82:8a:09:c9: + b4:03:25:bc +SHA1 Fingerprint=B8:01:86:D1:EB:9C:86:A5:41:04:CF:30:54:F3:4C:52:B7:E5:58:C6 +-----BEGIN CERTIFICATE----- +MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB +gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk +MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY +UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx +NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3 +dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy +dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6 +38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP +KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q +DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4 +qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa +JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi +PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P +BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs +jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0 +eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD +ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR +vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt +qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa +IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy +i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ +O+7ETPTsJ3xCwnR8gooJybQDJbw= +-----END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -7575,90 +7491,6 @@ Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT 2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c -----END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 0 (0x0) - Signature Algorithm: sha1WithRSAEncryption - Issuer: C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority - Validity - Not Before: Jun 29 17:06:20 2004 GMT - Not After : Jun 29 17:06:20 2034 GMT - Subject: C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (2048 bit) - Modulus: - 00:de:9d:d7:ea:57:18:49:a1:5b:eb:d7:5f:48:86: - ea:be:dd:ff:e4:ef:67:1c:f4:65:68:b3:57:71:a0: - 5e:77:bb:ed:9b:49:e9:70:80:3d:56:18:63:08:6f: - da:f2:cc:d0:3f:7f:02:54:22:54:10:d8:b2:81:d4: - c0:75:3d:4b:7f:c7:77:c3:3e:78:ab:1a:03:b5:20: - 6b:2f:6a:2b:b1:c5:88:7e:c4:bb:1e:b0:c1:d8:45: - 27:6f:aa:37:58:f7:87:26:d7:d8:2d:f6:a9:17:b7: - 1f:72:36:4e:a6:17:3f:65:98:92:db:2a:6e:5d:a2: - fe:88:e0:0b:de:7f:e5:8d:15:e1:eb:cb:3a:d5:e2: - 12:a2:13:2d:d8:8e:af:5f:12:3d:a0:08:05:08:b6: - 5c:a5:65:38:04:45:99:1e:a3:60:60:74:c5:41:a5: - 72:62:1b:62:c5:1f:6f:5f:1a:42:be:02:51:65:a8: - ae:23:18:6a:fc:78:03:a9:4d:7f:80:c3:fa:ab:5a: - fc:a1:40:a4:ca:19:16:fe:b2:c8:ef:5e:73:0d:ee: - 77:bd:9a:f6:79:98:bc:b1:07:67:a2:15:0d:dd:a0: - 58:c6:44:7b:0a:3e:62:28:5f:ba:41:07:53:58:cf: - 11:7e:38:74:c5:f8:ff:b5:69:90:8f:84:74:ea:97: - 1b:af - Exponent: 3 (0x3) - X509v3 extensions: - X509v3 Subject Key Identifier: - D2:C4:B0:D2:91:D4:4C:11:71:B3:61:CB:3D:A1:FE:DD:A8:6A:D4:E3 - X509v3 Authority Key Identifier: - keyid:D2:C4:B0:D2:91:D4:4C:11:71:B3:61:CB:3D:A1:FE:DD:A8:6A:D4:E3 - DirName:/C=US/O=The Go Daddy Group, Inc./OU=Go Daddy Class 2 Certification Authority - serial:00 - - X509v3 Basic Constraints: - CA:TRUE - Signature Algorithm: sha1WithRSAEncryption - 32:4b:f3:b2:ca:3e:91:fc:12:c6:a1:07:8c:8e:77:a0:33:06: - 14:5c:90:1e:18:f7:08:a6:3d:0a:19:f9:87:80:11:6e:69:e4: - 96:17:30:ff:34:91:63:72:38:ee:cc:1c:01:a3:1d:94:28:a4: - 31:f6:7a:c4:54:d7:f6:e5:31:58:03:a2:cc:ce:62:db:94:45: - 73:b5:bf:45:c9:24:b5:d5:82:02:ad:23:79:69:8d:b8:b6:4d: - ce:cf:4c:ca:33:23:e8:1c:88:aa:9d:8b:41:6e:16:c9:20:e5: - 89:9e:cd:3b:da:70:f7:7e:99:26:20:14:54:25:ab:6e:73:85: - e6:9b:21:9d:0a:6c:82:0e:a8:f8:c2:0c:fa:10:1e:6c:96:ef: - 87:0d:c4:0f:61:8b:ad:ee:83:2b:95:f8:8e:92:84:72:39:eb: - 20:ea:83:ed:83:cd:97:6e:08:bc:eb:4e:26:b6:73:2b:e4:d3: - f6:4c:fe:26:71:e2:61:11:74:4a:ff:57:1a:87:0f:75:48:2e: - cf:51:69:17:a0:02:12:61:95:d5:d1:40:b2:10:4c:ee:c4:ac: - 10:43:a6:a5:9e:0a:d5:95:62:9a:0d:cf:88:82:c5:32:0c:e4: - 2b:9f:45:e6:0d:9f:28:9c:b1:b9:2a:5a:57:ad:37:0f:af:1d: - 7f:db:bd:9f -SHA1 Fingerprint=27:96:BA:E6:3F:18:01:E2:77:26:1B:A0:D7:77:70:02:8F:20:EE:E4 ------BEGIN CERTIFICATE----- -MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh -MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE -YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 -MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo -ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg -MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN -ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA -PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w -wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi -EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY -avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ -YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE -sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h -/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 -IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj -YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD -ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy -OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P -TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ -HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER -dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf -ReYNnyicsbkqWletNw+vHX/bvZ8= ------END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -7779,6 +7611,90 @@ JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV 7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl 6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL -----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 0 (0x0) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority + Validity + Not Before: Jun 29 17:06:20 2004 GMT + Not After : Jun 29 17:06:20 2034 GMT + Subject: C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:de:9d:d7:ea:57:18:49:a1:5b:eb:d7:5f:48:86: + ea:be:dd:ff:e4:ef:67:1c:f4:65:68:b3:57:71:a0: + 5e:77:bb:ed:9b:49:e9:70:80:3d:56:18:63:08:6f: + da:f2:cc:d0:3f:7f:02:54:22:54:10:d8:b2:81:d4: + c0:75:3d:4b:7f:c7:77:c3:3e:78:ab:1a:03:b5:20: + 6b:2f:6a:2b:b1:c5:88:7e:c4:bb:1e:b0:c1:d8:45: + 27:6f:aa:37:58:f7:87:26:d7:d8:2d:f6:a9:17:b7: + 1f:72:36:4e:a6:17:3f:65:98:92:db:2a:6e:5d:a2: + fe:88:e0:0b:de:7f:e5:8d:15:e1:eb:cb:3a:d5:e2: + 12:a2:13:2d:d8:8e:af:5f:12:3d:a0:08:05:08:b6: + 5c:a5:65:38:04:45:99:1e:a3:60:60:74:c5:41:a5: + 72:62:1b:62:c5:1f:6f:5f:1a:42:be:02:51:65:a8: + ae:23:18:6a:fc:78:03:a9:4d:7f:80:c3:fa:ab:5a: + fc:a1:40:a4:ca:19:16:fe:b2:c8:ef:5e:73:0d:ee: + 77:bd:9a:f6:79:98:bc:b1:07:67:a2:15:0d:dd:a0: + 58:c6:44:7b:0a:3e:62:28:5f:ba:41:07:53:58:cf: + 11:7e:38:74:c5:f8:ff:b5:69:90:8f:84:74:ea:97: + 1b:af + Exponent: 3 (0x3) + X509v3 extensions: + X509v3 Subject Key Identifier: + D2:C4:B0:D2:91:D4:4C:11:71:B3:61:CB:3D:A1:FE:DD:A8:6A:D4:E3 + X509v3 Authority Key Identifier: + keyid:D2:C4:B0:D2:91:D4:4C:11:71:B3:61:CB:3D:A1:FE:DD:A8:6A:D4:E3 + DirName:/C=US/O=The Go Daddy Group, Inc./OU=Go Daddy Class 2 Certification Authority + serial:00 + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha1WithRSAEncryption + 32:4b:f3:b2:ca:3e:91:fc:12:c6:a1:07:8c:8e:77:a0:33:06: + 14:5c:90:1e:18:f7:08:a6:3d:0a:19:f9:87:80:11:6e:69:e4: + 96:17:30:ff:34:91:63:72:38:ee:cc:1c:01:a3:1d:94:28:a4: + 31:f6:7a:c4:54:d7:f6:e5:31:58:03:a2:cc:ce:62:db:94:45: + 73:b5:bf:45:c9:24:b5:d5:82:02:ad:23:79:69:8d:b8:b6:4d: + ce:cf:4c:ca:33:23:e8:1c:88:aa:9d:8b:41:6e:16:c9:20:e5: + 89:9e:cd:3b:da:70:f7:7e:99:26:20:14:54:25:ab:6e:73:85: + e6:9b:21:9d:0a:6c:82:0e:a8:f8:c2:0c:fa:10:1e:6c:96:ef: + 87:0d:c4:0f:61:8b:ad:ee:83:2b:95:f8:8e:92:84:72:39:eb: + 20:ea:83:ed:83:cd:97:6e:08:bc:eb:4e:26:b6:73:2b:e4:d3: + f6:4c:fe:26:71:e2:61:11:74:4a:ff:57:1a:87:0f:75:48:2e: + cf:51:69:17:a0:02:12:61:95:d5:d1:40:b2:10:4c:ee:c4:ac: + 10:43:a6:a5:9e:0a:d5:95:62:9a:0d:cf:88:82:c5:32:0c:e4: + 2b:9f:45:e6:0d:9f:28:9c:b1:b9:2a:5a:57:ad:37:0f:af:1d: + 7f:db:bd:9f +SHA1 Fingerprint=27:96:BA:E6:3F:18:01:E2:77:26:1B:A0:D7:77:70:02:8F:20:EE:E4 +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh +MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE +YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 +MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo +ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg +MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN +ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA +PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w +wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi +EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY +avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ +YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE +sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h +/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 +IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD +ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy +OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P +TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ +HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER +dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf +ReYNnyicsbkqWletNw+vHX/bvZ8= +-----END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -8100,42 +8016,335 @@ Certificate: X509v3 Basic Constraints: critical CA:TRUE X509v3 Subject Key Identifier: - 7F:10:01:16:37:3A:A4:28:E4:50:F8:A4:F7:EC:6B:32:B6:FE:E9:8B - X509v3 Key Usage: critical - Certificate Sign, CRL Sign - X509v3 CRL Distribution Points: - - Full Name: - URI:http://crl.d-trust.net/crl/d-trust_ev_root_ca_1_2020.crl - - Full Name: - URI:ldap://directory.d-trust.net/CN=D-TRUST%20EV%20Root%20CA%201%202020,O=D-Trust%20GmbH,C=DE?certificaterevocationlist - - Signature Algorithm: ecdsa-with-SHA384 - 30:66:02:31:00:ca:3c:c6:2a:75:c2:5e:75:62:39:36:00:60: - 5a:8b:c1:93:99:cc:d9:db:41:3b:3b:87:99:17:3b:d5:cc:4f: - ca:22:f7:a0:80:cb:f9:b4:b1:1b:56:f5:72:d2:fc:19:d1:02: - 31:00:91:f7:30:93:3f:10:46:2b:71:a4:d0:3b:44:9b:c0:29: - 02:05:b2:41:77:51:f3:79:5a:9e:8e:14:a0:4e:42:d2:5b:81: - f3:34:6a:03:e7:22:38:50:5b:ed:19:4f:43:16 -SHA1 Fingerprint=61:DB:8C:21:59:69:03:90:D8:7C:9C:12:86:54:CF:9D:3D:F4:DD:07 ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw -CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS -VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 -NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG -A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB -BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC -/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD -wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 -OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g -PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf -Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l -dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 -c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO -PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA -y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb -gfM0agPnIjhQW+0ZT0MW + 7F:10:01:16:37:3A:A4:28:E4:50:F8:A4:F7:EC:6B:32:B6:FE:E9:8B + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 CRL Distribution Points: + + Full Name: + URI:http://crl.d-trust.net/crl/d-trust_ev_root_ca_1_2020.crl + + Full Name: + URI:ldap://directory.d-trust.net/CN=D-TRUST%20EV%20Root%20CA%201%202020,O=D-Trust%20GmbH,C=DE?certificaterevocationlist + + Signature Algorithm: ecdsa-with-SHA384 + 30:66:02:31:00:ca:3c:c6:2a:75:c2:5e:75:62:39:36:00:60: + 5a:8b:c1:93:99:cc:d9:db:41:3b:3b:87:99:17:3b:d5:cc:4f: + ca:22:f7:a0:80:cb:f9:b4:b1:1b:56:f5:72:d2:fc:19:d1:02: + 31:00:91:f7:30:93:3f:10:46:2b:71:a4:d0:3b:44:9b:c0:29: + 02:05:b2:41:77:51:f3:79:5a:9e:8e:14:a0:4e:42:d2:5b:81: + f3:34:6a:03:e7:22:38:50:5b:ed:19:4f:43:16 +SHA1 Fingerprint=61:DB:8C:21:59:69:03:90:D8:7C:9C:12:86:54:CF:9D:3D:F4:DD:07 +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 09:e0:93:65:ac:f7:d9:c8:b9:3e:1c:0b:04:2a:2e:f3 + Signature Algorithm: ecdsa-with-SHA384 + Issuer: C = US, O = "DigiCert, Inc.", CN = DigiCert TLS ECC P384 Root G5 + Validity + Not Before: Jan 15 00:00:00 2021 GMT + Not After : Jan 14 23:59:59 2046 GMT + Subject: C = US, O = "DigiCert, Inc.", CN = DigiCert TLS ECC P384 Root G5 + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (384 bit) + pub: + 04:c1:44:a1:cf:11:97:50:9a:de:23:82:35:07:cd: + d0:cb:18:9d:d2:f1:7f:77:35:4f:3b:dd:94:72:52: + ed:c2:3b:f8:ec:fa:7b:6b:58:20:ec:99:ae:c9:fc: + 68:b3:75:b9:db:09:ec:c8:13:f5:4e:c6:0a:1d:66: + 30:4c:bb:1f:47:0a:3c:61:10:42:29:7c:a5:08:0e: + e0:22:e9:d3:35:68:ce:9b:63:9f:84:b5:99:4d:58: + a0:8e:f5:54:e7:95:c9 + ASN1 OID: secp384r1 + NIST CURVE: P-384 + X509v3 extensions: + X509v3 Subject Key Identifier: + C1:51:45:50:59:AB:3E:E7:2C:5A:FA:20:22:12:07:80:88:7C:11:6A + X509v3 Key Usage: critical + Digital Signature, Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + Signature Algorithm: ecdsa-with-SHA384 + 30:65:02:31:00:89:6a:8d:47:e7:ec:fc:6e:55:03:d9:67:6c: + 26:4e:83:c6:fd:c9:fb:2b:13:bc:b7:7a:8c:b4:65:d2:69:69: + 63:13:63:3b:26:50:2e:01:a1:79:06:91:9d:48:bf:c2:be:02: + 30:47:c3:15:7b:b1:a0:91:99:49:93:a8:3c:7c:e8:46:06:8b: + 2c:f2:31:00:94:9d:62:c8:89:bd:19:84:14:e9:a5:fb:01:b8: + 0d:76:43:8c:2e:53:cb:7c:df:0c:17:96:50 +SHA1 Fingerprint=17:F3:DE:5E:9F:0F:19:E9:8E:F6:1F:32:26:6E:20:C4:07:AE:30:EE +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 08:f9:b4:78:a8:fa:7e:da:6a:33:37:89:de:7c:cf:8a + Signature Algorithm: sha384WithRSAEncryption + Issuer: C = US, O = "DigiCert, Inc.", CN = DigiCert TLS RSA4096 Root G5 + Validity + Not Before: Jan 15 00:00:00 2021 GMT + Not After : Jan 14 23:59:59 2046 GMT + Subject: C = US, O = "DigiCert, Inc.", CN = DigiCert TLS RSA4096 Root G5 + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (4096 bit) + Modulus: + 00:b3:d0:f4:c9:79:11:9d:fd:fc:66:81:e7:cc:d5: + e4:bc:ec:81:3e:6a:35:8e:2e:b7:e7:de:af:f9:07: + 4d:cf:30:9d:ea:09:0b:99:bd:6c:57:da:18:4a:b8: + 78:ac:3a:39:a8:a6:48:ac:2e:72:e5:bd:eb:f1:1a: + cd:e7:a4:03:a9:3f:11:b4:d8:2f:89:16:fb:94:01: + 3d:bb:2f:f8:13:05:a1:78:1c:8e:28:e0:45:e0:83: + f4:59:1b:95:b3:ae:7e:03:45:e5:be:c2:42:fe:ee: + f2:3c:b6:85:13:98:32:9d:16:a8:29:c2:0b:1c:38: + dc:9f:31:77:5c:bf:27:a3:fc:27:ac:b7:2b:bd:74: + 9b:17:2d:f2:81:da:5d:b0:e1:23:17:3e:88:4a:12: + 23:d0:ea:cf:9d:de:03:17:b1:42:4a:a0:16:4c:a4: + 6d:93:e9:3f:3a:ee:3a:7c:9d:58:9d:f4:4e:8f:fc: + 3b:23:c8:6d:b8:e2:05:da:cc:eb:ec:c3:31:f4:d7: + a7:29:54:80:cf:44:5b:4c:6f:30:9e:f3:cc:dd:1f: + 94:43:9d:4d:7f:70:70:0d:d4:3a:d1:37:f0:6c:9d: + 9b:c0:14:93:58:ef:cd:41:38:75:bc:13:03:95:7c: + 7f:e3:5c:e9:d5:0d:d5:e2:7c:10:62:aa:6b:f0:3d: + 76:f3:3f:a3:e8:b0:c1:fd:ef:aa:57:4d:ac:86:a7: + 18:b4:29:c1:2c:0e:bf:64:be:29:8c:d8:02:2d:cd: + 5c:2f:f2:7f:ef:15:f4:0c:15:ac:0a:b0:f1:d3:0d: + 4f:6a:4d:77:97:01:a0:f1:66:b7:b7:ce:ef:ce:ec: + ec:a5:75:ca:ac:e3:e1:63:f7:b8:a1:04:c8:bc:7b: + 3f:5d:2d:16:22:56:ed:48:49:fe:a7:2f:79:30:25: + 9b:ba:6b:2d:3f:9d:3b:c4:17:e7:1d:2e:fb:f2:cf: + a6:fc:e3:14:2c:96:98:21:8c:b4:91:e9:19:60:83: + f2:30:2b:06:73:50:d5:98:3b:06:e9:c7:8a:0c:60: + 8c:28:f8:52:9b:6e:e1:f6:4d:bb:06:24:9b:d7:2b: + 26:3f:fd:2a:2f:71:f5:d6:24:be:7f:31:9e:0f:6d: + e8:8f:4f:4d:a3:3f:ff:35:ea:df:49:5e:41:8f:86: + f9:f1:77:79:4b:1b:b4:a3:5e:2f:fb:46:02:d0:66: + 13:5e:5e:85:4f:ce:d8:70:88:7b:ce:01:b5:96:97: + d7:cd:7d:fd:82:f8:c2:24:c1:ca:01:39:4f:8d:a2: + c1:14:40:1f:9c:66:d5:0c:09:46:d6:f2:d0:d1:48: + 76:56:3a:43:cb:b6:0a:11:39:ba:8c:13:6c:06:b5: + 9e:cf:eb + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + 51:33:1C:ED:36:40:AF:17:D3:25:CD:69:68:F2:AF:4E:23:3E:B3:41 + X509v3 Key Usage: critical + Digital Signature, Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + Signature Algorithm: sha384WithRSAEncryption + 60:a6:af:5b:5f:57:da:89:db:4b:50:a9:c4:23:35:21:ff:d0: + 61:30:84:91:b7:3f:10:cf:25:8e:c9:bf:46:34:d9:c1:21:26: + 1c:70:19:72:1e:a3:c9:87:fe:a9:43:64:96:3a:c8:53:04:0a: + b6:41:bb:c4:47:00:d9:9f:18:18:3b:b2:0e:f3:34:ea:24:f7: + dd:af:20:60:ae:92:28:5f:36:e7:5d:e4:de:c7:3c:db:50:39: + ad:bb:3d:28:4d:96:7c:76:c6:5b:f4:c1:db:14:a5:ab:19:62: + 07:18:40:5f:97:91:dc:9c:c7:ab:b5:51:0d:e6:69:53:55:cc: + 39:7d:da:c5:11:55:72:c5:3b:8b:89:f8:34:2d:a4:17:e5:17: + e6:99:7d:30:88:21:37:cd:30:17:3d:b8:f2:bc:a8:75:a0:43: + dc:3e:89:4b:90:ae:6d:03:e0:1c:a3:a0:96:09:bb:7d:a3:b7: + 2a:10:44:4b:46:07:34:63:ed:31:b9:04:ee:a3:9b:9a:ae:e6: + 31:78:f4:ea:24:61:3b:ab:58:64:ff:bb:87:27:62:25:81:df: + dc:a1:2f:f6:ed:a7:ff:7a:8f:51:2e:30:f8:a4:01:d2:85:39: + 5f:01:99:96:6f:5a:5b:70:19:46:fe:86:60:3e:ad:80:10:09: + dd:39:25:2f:58:7f:bb:d2:74:f0:f7:46:1f:46:39:4a:d8:53: + d0:f3:2e:3b:71:a5:d4:6f:fc:f3:67:e4:07:8f:dd:26:19:e1: + 8d:5b:fa:a3:93:11:9b:e9:c8:3a:c3:55:68:9a:92:e1:52:76: + 38:e8:e1:ba:bd:fb:4f:d5:ef:b3:e7:48:83:31:f0:82:21:e3: + b6:be:a7:ab:6f:ef:9f:df:4c:cf:01:b8:62:6a:23:3d:e7:09: + 4d:80:1b:7b:30:a4:c3:dd:07:7f:34:be:a4:26:b2:f6:41:e8: + 09:1d:e3:20:98:aa:37:4f:ff:f7:f1:e2:29:70:31:47:3f:74: + d0:14:16:fa:21:8a:02:d5:8a:09:94:77:2e:f2:59:28:8b:7c: + 50:92:0a:66:78:38:83:75:c4:b5:5a:a8:11:c6:e5:c1:9d:66: + 55:cf:53:c4:af:d7:75:85:a9:42:13:56:ec:21:77:81:93:5a: + 0c:ea:96:d9:49:ca:a1:08:f2:97:3b:6d:9b:04:18:24:44:8e: + 7c:01:f2:dc:25:d8:5e:86:9a:b1:39:db:f5:91:32:6a:d1:a6: + 70:8a:a2:f7:de:a4:45:85:26:a8:1e:8c:5d:29:5b:c8:4b:d8: + 9a:6a:03:5e:70:f2:85:4f:6c:4b:68:2f:ca:54:f6:8c:da:32: + fe:c3:6b:83:3f:38:c6:7e +SHA1 Fingerprint=A7:88:49:DC:5D:7C:75:8C:8C:DE:39:98:56:B3:AA:D0:B2:A5:71:35 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 8e:0f:f9:4b:90:71:68:65:33:54:f4:d4:44:39:b7:e0 + Signature Algorithm: sha256WithRSAEncryption + Issuer: C = US, O = Certainly, CN = Certainly Root R1 + Validity + Not Before: Apr 1 00:00:00 2021 GMT + Not After : Apr 1 00:00:00 2046 GMT + Subject: C = US, O = Certainly, CN = Certainly Root R1 + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (4096 bit) + Modulus: + 00:d0:36:d4:1f:ea:dd:ab:e4:d1:b6:e6:fb:22:c0: + dd:13:0d:6a:7b:22:13:1c:97:3c:68:63:66:32:9c: + 03:b5:8d:a4:81:83:da:78:30:11:cf:dc:b2:2b:be: + 92:bf:8e:e4:c4:13:be:a4:68:4c:da:02:68:16:74: + be:b2:dd:04:e4:6b:2a:dd:37:1f:60:2c:db:f5:f7: + a1:7c:95:b7:0c:70:86:2e:f1:3a:ef:52:f7:cc:d3: + 9b:f9:8b:be:0e:df:31:b7:9d:68:5c:92:a6:f5:e5: + f3:0a:34:b5:ff:7b:a2:e4:87:a1:c6:af:17:00:ef: + 03:91:ed:a9:1c:4e:71:3d:d2:8b:6c:89:f4:78:86: + e6:6a:49:a0:ce:b5:d2:b0:ab:9b:f6:f4:d4:2e:e3: + 72:f9:36:c6:eb:15:b7:25:8c:3a:fc:25:0d:b3:22: + 73:21:74:c8:4a:96:61:92:f5:2f:0b:18:a5:f4:ad: + e2:ee:41:bd:01:79:fa:96:8c:8d:17:02:30:b4:f9: + af:78:1a:8c:b4:36:10:10:07:05:70:d0:f4:31:90: + 8a:51:c5:86:26:79:b2:11:88:5e:c5:f0:0a:54:cd: + 49:a6:bf:02:9c:d2:44:a7:ed:e3:78:ef:46:5e:6d: + 71:d1:79:70:1c:46:5f:51:e9:c9:37:dc:5f:7e:69: + 7b:41:df:34:45:e0:3b:84:f4:a1:8a:0a:36:9e:37: + cc:62:52:e1:89:0d:28:f9:7a:23:b1:0d:3d:3d:9a: + fd:9d:81:ef:2c:90:c0:7b:44:4e:bb:49:e0:0e:4a: + 56:92:bc:cb:b5:dd:79:17:89:91:de:61:89:74:92: + a8:e3:32:85:be:4e:85:a4:4b:59:cb:2b:c5:78:8e: + 71:54:d0:02:37:99:8c:e5:49:ea:e0:54:72:a4:11: + 06:2f:0b:8c:c1:5b:be:b5:a1:b0:53:6e:9c:b8:60: + 91:1f:59:6b:f9:2d:f4:94:0a:97:b5:ec:c5:76:03: + 54:1b:65:52:ba:4c:92:56:51:35:a0:40:d8:29:db: + ae:52:76:3b:2d:30:40:9b:8a:d0:42:56:b4:b7:88: + 01:a4:87:3b:53:96:cd:a3:16:8f:f3:66:aa:17:b1: + c7:60:e0:c1:43:05:0c:ee:9b:5b:60:6f:06:5c:87: + 5b:27:f9:40:11:9e:9c:33:c1:b7:e5:35:57:05:7f: + 27:ce:17:20:8c:1c:fc:f1:fb:da:31:29:49:ed:f5: + 0b:84:a7:4f:c1:f6:4e:c2:28:9c:fa:ee:e0:af:07: + fb:33:11:7a:21:4f:0b:21:10:b6:40:3a:ab:22:3a: + 04:9c:8b:9b:84:86:72:9a:d2:a7:a5:c4:b4:75:91: + a9:2b:23 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + E0:AA:3F:25:8D:9F:44:5C:C1:3A:E8:2E:AE:77:4C:84:3E:67:0C:F4 + Signature Algorithm: sha256WithRSAEncryption + b9:57:af:b8:12:da:57:83:8f:68:0b:33:1d:03:53:55:f4:95: + 70:e4:2b:3d:b0:39:eb:fa:89:62:fd:f7:d6:18:04:2f:21:34: + dd:f1:68:f0:d5:96:5a:de:c2:80:a3:c1:8d:c6:6a:f7:59:77: + ae:15:64:cf:5b:79:05:77:66:ea:8c:d3:6b:0d:dd:f1:59:2c: + c1:33:a5:30:80:15:45:07:45:1a:31:22:b6:92:00:ab:99:4d: + 3a:8f:77:af:a9:22:ca:2f:63:ca:15:d6:c7:c6:f0:3d:6c:fc: + 1c:0d:98:10:61:9e:11:a2:22:d7:0a:f2:91:7a:6b:39:0e:2f: + 30:c3:36:49:9f:e0:e9:0f:02:44:50:37:94:55:7d:ea:9f:f6: + 3b:ba:94:a5:4c:e9:bc:3e:51:b4:e8:ca:92:36:54:6d:5c:25: + 28:da:dd:ad:14:fd:d3:ee:e2:22:05:eb:d0:f2:b7:68:12:d7: + 5a:8a:41:1a:c6:92:a5:5a:3b:63:45:4f:bf:e1:3a:77:22:2f: + 5c:bf:46:f9:5a:03:85:13:42:5f:ca:de:53:d7:62:b5:a6:35: + 04:c2:47:ff:99:fd:84:df:5c:ce:e9:5e:80:28:41:f2:7d:e7: + 1e:90:d8:4f:76:3e:82:3c:0d:fc:a5:03:fa:7b:1a:d9:45:1e: + 60:da:c4:8e:f9:fc:2b:c9:7b:95:c5:2a:ff:aa:89:df:82:31: + 0f:72:ff:0c:27:d7:0a:1e:56:00:50:1e:0c:90:c1:96:b5:d8: + 14:85:bb:a7:0d:16:c1:f8:07:24:1b:ba:85:a1:1a:05:09:80: + ba:95:63:c9:3a:ec:25:9f:7f:9d:ba:a4:47:15:9b:44:70:f1: + 6a:4b:d6:38:5e:43:f3:18:7e:50:6e:e9:5a:28:e6:65:e6:77: + 1b:3a:fd:1d:be:03:26:a3:db:d4:e1:bb:7e:96:27:2b:1d:ee: + a4:fb:da:25:54:13:03:de:39:c6:c3:1f:4d:90:ec:8f:1b:4a: + d2:1c:ed:85:95:38:50:79:46:d6:c1:90:50:31:a9:5c:9a:6e: + 1d:f5:33:56:8b:a7:99:d2:f2:c8:2c:33:93:92:30:c7:4e:8c: + 65:33:10:64:17:fd:24:17:96:d1:8d:c2:3a:6a:2b:eb:13:8b: + 44:f2:21:f3:4a:1a:b7:77:5f:d7:ed:88:a4:72:e5:39:1f:95: + 9d:be:67:c1:70:11:3d:bb:f4:f8:49:b7:e3:26:97:3a:9f:d2: + 5f:7c:fb:c0:99:7c:39:29:e0:7b:1d:bf:0d:a7:8f:d2:29:34: + 6e:24:15:cb:de:90:5e:bf:1a:c4:66:ea:c2:e6:ba:39:5f:8a: + 99:a9:41:59:07:b0:2c:af +SHA1 Fingerprint=A0:50:EE:0F:28:71:F4:27:B2:12:6D:6F:50:96:25:BA:CC:86:42:AF +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= -----END CERTIFICATE----- Certificate: Data: @@ -8221,6 +8430,243 @@ xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= -----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 06:25:33:b1:47:03:33:27:5c:f9:8d:9a:b9:bf:cc:f8 + Signature Algorithm: ecdsa-with-SHA384 + Issuer: C = US, O = Certainly, CN = Certainly Root E1 + Validity + Not Before: Apr 1 00:00:00 2021 GMT + Not After : Apr 1 00:00:00 2046 GMT + Subject: C = US, O = Certainly, CN = Certainly Root E1 + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (384 bit) + pub: + 04:de:6f:f8:7f:1c:df:ed:f9:47:87:86:b1:a4:c0: + 8a:f8:82:97:80:ea:8f:c8:4a:5e:2a:7d:88:68:a7: + 01:62:14:91:24:7a:5c:9e:a3:17:7d:8a:86:21:34: + 18:50:1b:10:de:d0:37:4b:26:c7:19:60:80:e9:34: + bd:60:19:36:40:d6:29:87:09:3c:91:7a:f6:bc:13: + 23:dd:59:4e:04:5e:cf:c8:02:1c:18:53:c1:31:d8: + da:20:e9:44:8d:e4:76 + ASN1 OID: secp384r1 + NIST CURVE: P-384 + X509v3 extensions: + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + F3:28:18:CB:64:75:EE:29:2A:EB:ED:AE:23:58:38:85:EB:C8:22:07 + Signature Algorithm: ecdsa-with-SHA384 + 30:65:02:31:00:b1:8e:5a:20:c3:b2:19:62:4d:de:b0:4f:df: + 6e:d2:70:8a:f1:9f:7e:6a:8c:e6:ba:de:83:69:ca:69:b3:a9: + 05:b5:96:92:17:87:c2:d2:ea:d0:7b:ce:d8:41:5b:7c:ae:02: + 30:46:de:ea:cb:5d:9a:ec:32:c2:65:16:b0:4c:30:5c:30:f3: + da:4e:73:86:06:d8:ce:89:04:48:37:37:f8:dd:33:51:9d:70: + af:7b:55:d8:01:2e:7d:05:64:0e:86:b8:91 +SHA1 Fingerprint=F9:E1:6D:DC:01:89:CF:D5:82:45:63:3E:C5:37:7D:C2:EB:93:6F:2B +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 0d:4d:c5:cd:16:22:95:96:08:7e:b8:0b:7f:15:06:34:fb:79:10:34 + Signature Algorithm: sha256WithRSAEncryption + Issuer: C = TR, L = Ankara, O = E-Tugra EBG A.S., OU = E-Tugra Trust Center, CN = E-Tugra Global Root CA RSA v3 + Validity + Not Before: Mar 18 09:07:17 2020 GMT + Not After : Mar 12 09:07:17 2045 GMT + Subject: C = TR, L = Ankara, O = E-Tugra EBG A.S., OU = E-Tugra Trust Center, CN = E-Tugra Global Root CA RSA v3 + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (4096 bit) + Modulus: + 00:a2:66:f0:89:b7:72:7b:ee:09:c9:63:d2:d3:43: + dd:5e:c3:a6:84:38:4a:f1:8d:81:bb:14:bd:47:e8: + 40:17:f3:3d:c3:78:45:72:a6:2e:90:de:9a:3a:d4: + 20:71:ca:bc:9f:1d:4b:97:0a:c7:31:ba:3e:d7:fe: + 25:a9:2a:8e:36:f4:d1:2f:c7:b7:a9:5d:33:dc:30: + 70:f8:40:6c:4b:b2:a6:31:61:d1:34:3c:3d:31:7a: + c7:af:c4:a7:a7:84:e1:97:a4:e8:4b:f6:17:7c:ee: + 3c:07:ed:e2:8a:57:dc:b6:fb:f8:43:25:50:ea:27: + 81:a8:86:bc:8f:52:4a:96:3a:60:1a:96:bb:fd:73: + f4:85:fd:83:fd:7f:84:6d:34:6c:7f:6a:b7:4b:01: + 03:bf:ad:69:b7:d7:32:d9:f5:57:6a:e9:86:82:3e: + a5:66:31:b3:16:3d:c2:f3:26:60:32:d3:52:1e:b0: + 6c:a4:37:3e:f4:f5:af:eb:e1:df:80:06:cf:2a:41: + e7:66:09:e1:4b:97:e7:77:bd:21:6d:29:b6:67:c3: + 2d:7e:ed:d6:79:65:d1:cf:3a:b6:d1:b1:5e:56:61: + 50:7a:5a:ce:4e:50:31:80:03:98:47:e7:e4:18:7c: + 44:5a:c6:a4:b3:3b:c6:c6:c3:3a:f0:6c:c3:8b:c8: + a4:91:05:f3:f5:d9:b6:aa:06:a1:b7:ab:e4:b1:ea: + 21:14:5c:83:a4:fc:ff:b6:50:d3:8c:12:26:99:76: + 70:e9:c0:0f:a6:74:fc:bb:d0:1b:78:ce:72:92:e2: + 28:9c:bc:e6:e9:09:d8:3a:d3:89:e6:be:2e:77:df: + 01:0a:6f:96:f6:e5:8d:3c:4d:52:76:1a:56:e1:73: + 7e:17:ac:3d:ad:6c:a3:52:12:18:70:e6:80:4e:33: + f2:7e:26:32:ac:05:8d:38:a4:e6:76:3c:9f:10:69: + 0e:6d:9d:d2:c1:79:20:6b:5b:cf:33:8d:d1:94:76: + 35:e7:5d:55:c7:b7:ac:28:ab:46:cc:e7:3b:21:b5: + 0a:0a:e4:4a:59:dc:81:35:4b:44:95:12:0a:67:a5: + a1:ff:5b:00:07:d2:c0:cc:f9:3f:fc:9f:33:f2:00: + f8:8c:6c:87:9d:06:2d:f1:ef:e3:e6:06:fa:c5:66: + 13:5b:fc:50:07:9e:71:86:b2:da:6f:74:30:cf:93: + 53:e8:dc:22:d6:de:20:1f:61:8d:a3:2e:a3:78:32: + 90:6c:dc:ac:32:b5:05:e4:f5:3c:33:0d:d6:e0:87: + 77:17:4c:9d:b0:d8:09:a8:0d:57:f7:44:85:f0:c8: + 04:be:5c:5d:5a:e3:17:8e:54:63:69:7f:49:74:64: + 05:8c:a3 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Authority Key Identifier: + keyid:B2:B4:AE:E6:2D:F7:26:D5:AA:75:2D:76:4B:C0:1B:53:21:D0:48:EF + + X509v3 Subject Key Identifier: + B2:B4:AE:E6:2D:F7:26:D5:AA:75:2D:76:4B:C0:1B:53:21:D0:48:EF + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + Signature Algorithm: sha256WithRSAEncryption + 89:a8:72:7f:8c:eb:ce:2e:18:c4:10:80:2d:10:0c:ff:fb:14: + cd:04:e0:14:3c:4e:9a:fb:9f:29:bf:22:9e:57:b9:82:73:12: + 63:26:b5:cc:90:e9:d2:2a:29:ee:9c:2d:cc:2c:99:be:45:27: + e4:b1:71:ed:e4:38:95:31:41:f2:7d:7a:63:78:df:ca:36:16: + 2f:82:88:9f:bc:11:47:4f:76:4d:c8:2d:8e:eb:df:2d:7c:4e: + 3b:da:ae:f6:e3:da:5d:14:a6:ae:e8:85:44:9d:06:6e:8e:fb: + ef:7a:4a:6a:2d:2b:28:18:fe:bf:90:2c:75:16:9f:0f:ea:96: + 7d:05:ee:9b:13:a5:44:6c:f8:03:d0:dd:23:e1:fd:03:12:12: + 08:f4:18:34:b3:e0:37:0b:77:11:01:48:bf:61:b4:b5:f8:19: + d9:cb:4d:ea:a3:8c:ef:fd:f0:06:b5:6d:92:f4:4a:61:50:84: + ed:ec:49:d3:e4:be:68:e6:2e:e3:31:0b:54:0b:1a:92:d6:82: + d8:b6:a2:65:3c:66:04:f9:55:da:6c:fb:db:b5:14:66:4d:94: + 83:3b:cd:1e:a6:2b:b2:fe:77:40:86:ab:e7:df:0a:c9:fd:f6: + dd:87:56:18:d8:b0:2c:55:60:96:fa:08:7e:52:90:f5:4b:a6: + 2e:87:7c:cb:20:db:06:3e:a0:5d:03:77:7d:a2:3c:13:1b:29: + a2:13:55:a0:3d:14:22:af:6f:b8:d0:9a:1b:72:dd:05:01:8d: + 86:60:bf:a4:67:ee:b5:a5:0d:d1:7f:e6:1a:2b:62:66:c3:07: + ba:e7:a0:48:1c:38:c3:e9:45:fb:a7:7f:fc:ed:02:68:1a:ca: + 77:12:77:a6:00:55:28:14:ec:d6:c7:12:a2:1b:65:42:e9:91: + e8:cb:3e:87:89:54:5d:d9:af:9d:97:9c:69:e7:0a:ff:0f:5a: + 78:8b:63:2a:4c:7d:47:94:3f:de:4b:e9:53:d0:30:f1:c5:f6: + 9e:49:df:3b:a0:91:a3:a3:fe:cd:58:cc:ea:df:af:6f:28:3b: + a0:69:9b:8f:ec:ac:ae:2b:54:9d:9b:04:b1:47:20:af:96:12: + 3e:63:94:1d:04:e7:2e:bb:86:c7:0c:9a:88:bf:76:47:ef:f7: + b0:0b:97:66:d2:44:cf:60:52:07:e1:d5:2c:4a:3a:27:61:77: + ca:d7:8f:e7:87:0e:30:ff:0c:bb:04:e2:61:c3:a2:c8:97:61: + 8e:b4:30:6a:3c:6d:c2:07:5f:4a:73:2f:3f:f9:16:8a:01:66: + ef:ba:91:ca:52:57:7b:ae:d4:e6:0f:dd:0b:7a:7f:8b:9e:26: + 20:cf:3b:ef:81:71:83:59 +SHA1 Fingerprint=E9:A8:5D:22:14:52:1C:5B:AA:0A:B4:BE:24:6A:23:8A:C9:BA:E2:A9 +-----BEGIN CERTIFICATE----- +MIIF8zCCA9ugAwIBAgIUDU3FzRYilZYIfrgLfxUGNPt5EDQwDQYJKoZIhvcNAQEL +BQAwgYAxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUt +VHVncmEgRUJHIEEuUy4xHTAbBgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYw +JAYDVQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENBIFJTQSB2MzAeFw0yMDAzMTgw +OTA3MTdaFw00NTAzMTIwOTA3MTdaMIGAMQswCQYDVQQGEwJUUjEPMA0GA1UEBxMG +QW5rYXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1 +Z3JhIFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBD +QSBSU0EgdjMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCiZvCJt3J7 +7gnJY9LTQ91ew6aEOErxjYG7FL1H6EAX8z3DeEVypi6Q3po61CBxyryfHUuXCscx +uj7X/iWpKo429NEvx7epXTPcMHD4QGxLsqYxYdE0PD0xesevxKenhOGXpOhL9hd8 +7jwH7eKKV9y2+/hDJVDqJ4GohryPUkqWOmAalrv9c/SF/YP9f4RtNGx/ardLAQO/ +rWm31zLZ9Vdq6YaCPqVmMbMWPcLzJmAy01IesGykNz709a/r4d+ABs8qQedmCeFL +l+d3vSFtKbZnwy1+7dZ5ZdHPOrbRsV5WYVB6Ws5OUDGAA5hH5+QYfERaxqSzO8bG +wzrwbMOLyKSRBfP12baqBqG3q+Sx6iEUXIOk/P+2UNOMEiaZdnDpwA+mdPy70Bt4 +znKS4iicvObpCdg604nmvi533wEKb5b25Y08TVJ2Glbhc34XrD2tbKNSEhhw5oBO +M/J+JjKsBY04pOZ2PJ8QaQ5tndLBeSBrW88zjdGUdjXnXVXHt6woq0bM5zshtQoK +5EpZ3IE1S0SVEgpnpaH/WwAH0sDM+T/8nzPyAPiMbIedBi3x7+PmBvrFZhNb/FAH +nnGGstpvdDDPk1Po3CLW3iAfYY2jLqN4MpBs3KwytQXk9TwzDdbgh3cXTJ2w2Amo +DVf3RIXwyAS+XF1a4xeOVGNpf0l0ZAWMowIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFLK0ruYt9ybVqnUtdkvAG1Mh0EjvMB0GA1UdDgQWBBSy +tK7mLfcm1ap1LXZLwBtTIdBI7zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEL +BQADggIBAImocn+M684uGMQQgC0QDP/7FM0E4BQ8Tpr7nym/Ip5XuYJzEmMmtcyQ +6dIqKe6cLcwsmb5FJ+Sxce3kOJUxQfJ9emN438o2Fi+CiJ+8EUdPdk3ILY7r3y18 +Tjvarvbj2l0Upq7ohUSdBm6O++96SmotKygY/r+QLHUWnw/qln0F7psTpURs+APQ +3SPh/QMSEgj0GDSz4DcLdxEBSL9htLX4GdnLTeqjjO/98Aa1bZL0SmFQhO3sSdPk +vmjmLuMxC1QLGpLWgti2omU8ZgT5Vdps+9u1FGZNlIM7zR6mK7L+d0CGq+ffCsn9 +9t2HVhjYsCxVYJb6CH5SkPVLpi6HfMsg2wY+oF0Dd32iPBMbKaITVaA9FCKvb7jQ +mhty3QUBjYZgv6Rn7rWlDdF/5horYmbDB7rnoEgcOMPpRfunf/ztAmgayncSd6YA +VSgU7NbHEqIbZULpkejLPoeJVF3Zr52XnGnnCv8PWniLYypMfUeUP95L6VPQMPHF +9p5J3zugkaOj/s1YzOrfr28oO6Bpm4/srK4rVJ2bBLFHIK+WEj5jlB0E5y67hscM +moi/dkfv97ALl2bSRM9gUgfh1SxKOidhd8rXj+eHDjD/DLsE4mHDosiXYY60MGo8 +bcIHX0pzLz/5FooBZu+6kcpSV3uu1OYP3Qt6f4ueJiDPO++BcYNZ +-----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 26:46:19:77:31:e1:4f:6f:28:36:de:39:51:86:e6:d4:97:88:22:c1 + Signature Algorithm: ecdsa-with-SHA384 + Issuer: C = TR, L = Ankara, O = E-Tugra EBG A.S., OU = E-Tugra Trust Center, CN = E-Tugra Global Root CA ECC v3 + Validity + Not Before: Mar 18 09:46:58 2020 GMT + Not After : Mar 12 09:46:58 2045 GMT + Subject: C = TR, L = Ankara, O = E-Tugra EBG A.S., OU = E-Tugra Trust Center, CN = E-Tugra Global Root CA ECC v3 + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (384 bit) + pub: + 04:8e:98:29:bf:c7:10:1e:27:db:ab:03:cc:28:2c: + d8:5e:48:19:10:29:cc:cb:59:81:cc:8c:b8:92:17: + 89:83:2a:92:f6:c3:a4:1d:4c:62:d5:9f:d6:a0:46: + dc:1c:bc:76:c1:e3:47:d0:5b:13:da:e7:a5:b3:66: + 48:e7:21:9a:4a:4f:86:0a:7d:6c:ea:4d:32:80:0a: + b2:7a:09:9b:69:4b:98:81:e2:2e:ec:02:70:96:1f: + fd:f5:46:ce:ca:dc:82 + ASN1 OID: secp384r1 + NIST CURVE: P-384 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Authority Key Identifier: + keyid:FF:82:31:72:3E:F9:C4:66:6C:AD:38:9E:D1:B0:51:88:A5:90:CC:F5 + + X509v3 Subject Key Identifier: + FF:82:31:72:3E:F9:C4:66:6C:AD:38:9E:D1:B0:51:88:A5:90:CC:F5 + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + Signature Algorithm: ecdsa-with-SHA384 + 30:66:02:31:00:e6:05:58:69:61:e5:2d:ca:0d:cb:f1:19:08: + bd:d6:fd:51:92:1a:7e:63:54:04:90:91:9a:35:91:39:99:fa: + 07:a9:66:93:ba:c8:68:d4:8a:3f:fa:ed:6e:16:02:27:b7:02: + 31:00:dd:5a:17:2b:76:1d:65:42:96:a6:ac:5d:8a:79:56:d8: + 8a:1b:df:9a:de:5f:c7:50:8f:b1:5b:71:0c:26:df:6a:40:00: + ec:33:91:21:71:be:68:e4:23:a4:d9:ad:a1:37 +SHA1 Fingerprint=8A:2F:AF:57:53:B1:B0:E6:A1:04:EC:5B:6A:69:71:6D:F6:1C:E2:84 +-----BEGIN CERTIFICATE----- +MIICpTCCAiqgAwIBAgIUJkYZdzHhT28oNt45UYbm1JeIIsEwCgYIKoZIzj0EAwMw +gYAxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUtVHVn +cmEgRUJHIEEuUy4xHTAbBgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYwJAYD +VQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENBIEVDQyB2MzAeFw0yMDAzMTgwOTQ2 +NThaFw00NTAzMTIwOTQ2NThaMIGAMQswCQYDVQQGEwJUUjEPMA0GA1UEBxMGQW5r +YXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1Z3Jh +IFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBDQSBF +Q0MgdjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASOmCm/xxAeJ9urA8woLNheSBkQ +KczLWYHMjLiSF4mDKpL2w6QdTGLVn9agRtwcvHbB40fQWxPa56WzZkjnIZpKT4YK +fWzqTTKACrJ6CZtpS5iB4i7sAnCWH/31Rs7K3IKjYzBhMA8GA1UdEwEB/wQFMAMB +Af8wHwYDVR0jBBgwFoAU/4Ixcj75xGZsrTie0bBRiKWQzPUwHQYDVR0OBBYEFP+C +MXI++cRmbK04ntGwUYilkMz1MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNp +ADBmAjEA5gVYaWHlLcoNy/EZCL3W/VGSGn5jVASQkZo1kTmZ+gepZpO6yGjUij/6 +7W4WAie3AjEA3VoXK3YdZUKWpqxdinlW2Iob35reX8dQj7FbcQwm32pAAOwzkSFx +vmjkI6TZraE3 +-----END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -11212,99 +11658,6 @@ Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= -----END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 0 (0x0) - Signature Algorithm: sha1WithRSAEncryption - Issuer: C = GR, O = Hellenic Academic and Research Institutions Cert. Authority, CN = Hellenic Academic and Research Institutions RootCA 2011 - Validity - Not Before: Dec 6 13:49:52 2011 GMT - Not After : Dec 1 13:49:52 2031 GMT - Subject: C = GR, O = Hellenic Academic and Research Institutions Cert. Authority, CN = Hellenic Academic and Research Institutions RootCA 2011 - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (2048 bit) - Modulus: - 00:a9:53:00:e3:2e:a6:f6:8e:fa:60:d8:2d:95:3e: - f8:2c:2a:54:4e:cd:b9:84:61:94:58:4f:8f:3d:8b: - e4:43:f3:75:89:8d:51:e4:c3:37:d2:8a:88:4d:79: - 1e:b7:12:dd:43:78:4a:8a:92:e6:d7:48:d5:0f:a4: - 3a:29:44:35:b8:07:f6:68:1d:55:cd:38:51:f0:8c: - 24:31:85:af:83:c9:7d:e9:77:af:ed:1a:7b:9d:17: - f9:b3:9d:38:50:0f:a6:5a:79:91:80:af:37:ae:a6: - d3:31:fb:b5:26:09:9d:3c:5a:ef:51:c5:2b:df:96: - 5d:eb:32:1e:02:da:70:49:ec:6e:0c:c8:9a:37:8d: - f7:f1:36:60:4b:26:2c:82:9e:d0:78:f3:0d:0f:63: - a4:51:30:e1:f9:2b:27:12:07:d8:ea:bd:18:62:98: - b0:59:37:7d:be:ee:f3:20:51:42:5a:83:ef:93:ba: - 69:15:f1:62:9d:9f:99:39:82:a1:b7:74:2e:8b:d4: - c5:0b:7b:2f:f0:c8:0a:da:3d:79:0a:9a:93:1c:a5: - 28:72:73:91:43:9a:a7:d1:4d:85:84:b9:a9:74:8f: - 14:40:c7:dc:de:ac:41:64:6c:b4:19:9b:02:63:6d: - 24:64:8f:44:b2:25:ea:ce:5d:74:0c:63:32:5c:8d: - 87:e5 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: critical - CA:TRUE - X509v3 Key Usage: - Certificate Sign, CRL Sign - X509v3 Subject Key Identifier: - A6:91:42:FD:13:61:4A:23:9E:08:A4:29:E5:D8:13:04:23:EE:41:25 - X509v3 Name Constraints: - Permitted: - DNS:.gr - DNS:.eu - DNS:.edu - DNS:.org - email:.gr - email:.eu - email:.edu - email:.org - - Signature Algorithm: sha1WithRSAEncryption - 1f:ef:79:41:e1:7b:6e:3f:b2:8c:86:37:42:4a:4e:1c:37:1e: - 8d:66:ba:24:81:c9:4f:12:0f:21:c0:03:97:86:25:6d:5d:d3: - 22:29:a8:6c:a2:0d:a9:eb:3d:06:5b:99:3a:c7:cc:c3:9a:34: - 7f:ab:0e:c8:4e:1c:e1:fa:e4:dc:cd:0d:be:bf:24:fe:6c:e7: - 6b:c2:0d:c8:06:9e:4e:8d:61:28:a6:6a:fd:e5:f6:62:ea:18: - 3c:4e:a0:53:9d:b2:3a:9c:eb:a5:9c:91:16:b6:4d:82:e0:0c: - 05:48:a9:6c:f5:cc:f8:cb:9d:49:b4:f0:02:a5:fd:70:03:ed: - 8a:21:a5:ae:13:86:49:c3:33:73:be:87:3b:74:8b:17:45:26: - 4c:16:91:83:fe:67:7d:cd:4d:63:67:fa:f3:03:12:96:78:06: - 8d:b1:67:ed:8e:3f:be:9f:4f:02:f5:b3:09:2f:f3:4c:87:df: - 2a:cb:95:7c:01:cc:ac:36:7a:bf:a2:73:7a:f7:8f:c1:b5:9a: - a1:14:b2:8f:33:9f:0d:ef:22:dc:66:7b:84:bd:45:17:06:3d: - 3c:ca:b9:77:34:8f:ca:ea:cf:3f:31:3e:e3:88:e3:80:49:25: - c8:97:b5:9d:9a:99:4d:b0:3c:f8:4a:00:9b:64:dd:9f:39:4b: - d1:27:d7:b8 -SHA1 Fingerprint=FE:45:65:9B:79:03:5B:98:A1:61:B5:51:2E:AC:DA:58:09:48:22:4D ------BEGIN CERTIFICATE----- -MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1Ix -RDBCBgNVBAoTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 -dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1p -YyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIFJvb3RDQSAyMDExMB4XDTExMTIw -NjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYTAkdSMUQwQgYDVQQK -EztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIENl -cnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl -c2VhcmNoIEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBAKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPz -dYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJ -fel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa71HFK9+WXesyHgLacEns -bgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u8yBRQlqD -75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSP -FEDH3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNV -HRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp -5dgTBCPuQSUwRwYDVR0eBEAwPqA8MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQu -b3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQub3JnMA0GCSqGSIb3DQEBBQUA -A4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVtXdMiKahsog2p -6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 -TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7 -dIsXRSZMFpGD/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8Acys -Nnq/onN694/BtZqhFLKPM58N7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXI -l7WdmplNsDz4SgCbZN2fOUvRJ9e4 ------END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -11490,154 +11843,63 @@ Certificate: 53:5f:21:f5:ba:b0:3a:52:39:2c:92:b0:6c:00:c9:ef:ce:20: ef:06:f2:96:9e:e9:a4:74:7f:7a:16:fc:b7:f5:b6:fb:15:1b: 3f:ab:a6:c0:72:5d:10:b1:71:ee:bc:4f:e3:ad:ac:03:6d:2e: - 71:2e:af:c4:e3:ad:a3:bd:0c:11:a7:b4:ff:4a:b2:7b:10:10: - 1f:a7:57:41:b2:c0:ae:f4:2c:59:d6:47:10:88:f3:21:51:29: - 30:ca:60:86:af:46:ab:1d:ed:3a:5b:b0:94:de:44:e3:41:08: - a2:c1:ec:1d:d6:fd:4f:b6:d6:47:d0:14:0b:ca:e6:ca:b5:7b: - 77:7e:41:1f:5e:83:c7:b6:8c:39:96:b0:3f:96:81:41:6f:60: - 90:e2:e8:f9:fb:22:71:d9:7d:b3:3d:46:bf:b4:84:af:90:1c: - 0f:8f:12:6a:af:ef:ee:1e:7a:ae:02:4a:8a:17:2b:76:fe:ac: - 54:89:24:2c:4f:3f:b6:b2:a7:4e:8c:a8:91:97:fb:29:c6:7b: - 5c:2d:b9:cb:66:b6:b7:a8:5b:12:51:85:b5:09:7e:62:78:70: - fe:a9:6a:60:b6:1d:0e:79:0c:fd:ca:ea:24:80:72:c3:97:3f: - f2:77:ab:43:22:0a:c7:eb:b6:0c:84:82:2c:80:6b:41:8a:08: - c0:eb:a5:6b:df:99:12:cb:8a:d5:5e:80:0c:91:e0:26:08:36: - 48:c5:fa:38:11:35:ff:25:83:2d:f2:7a:bf:da:fd:8e:fe:a5: - cb:45:2c:1f:c4:88:53:ae:77:0e:d9:9a:76:c5:8e:2c:1d:a3: - ba:d5:ec:32:ae:c0:aa:ac:f7:d1:7a:4d:eb:d4:07:e2:48:f7: - 22:8e:b0:a4:9f:6a:ce:8e:b2:b2:60:f4:a3:22:d0:23:eb:94: - 5a:7a:69:dd:0f:bf:40:57:ac:6b:59:50:d9:a3:99:e1:6e:fe: - 8d:01:79:27:23:15:de:92:9d:7b:09:4d:5a:e7:4b:48:30:5a: - 18:e6:0a:6d:e6:8f:e0:d2:bb:e6:df:7c:6e:21:82:c1:68:39: - 4d:b4:98:58:66:62:cc:4a:90:5e:c3:fa:27:04:b1:79:15:74: - 99:cc:be:ad:20:de:26:60:1c:eb:56:51:a6:a3:ea:e4:a3:3f: - a7:ff:61:dc:f1:5a:4d:6c:32:23:43:ee:ac:a8:ee:ee:4a:12: - 09:3c:5d:71:c2:be:79:fa:c2:87:68:1d:0b:fd:5c:69:cc:06: - d0:9a:7d:54:99:2a:c9:39:1a:19:af:4b:2a:43:f3:63:5d:5a: - 58:e2:2f:e3:1d:e4:a9:d6:d0:0a:d0:9e:bf:d7:81:09:f1:c9: - c7:26:0d:ac:98:16:56:a0 -SHA1 Fingerprint=49:0A:75:74:DE:87:0A:47:FE:58:EE:F6:C7:6B:EB:C6:0B:12:40:99 ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd -MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg -Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow -TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw -HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB -BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr -6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV -L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 -1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx -MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ -QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB -arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr -Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi -FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS -P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN -9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP -AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz -uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h -9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s -A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t -OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo -+fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 -KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 -DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us -H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ -I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 -5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h -3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz -Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= ------END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 1164660820 (0x456b5054) - Signature Algorithm: sha1WithRSAEncryption - Issuer: C = US, O = "Entrust, Inc.", OU = www.entrust.net/CPS is incorporated by reference, OU = "(c) 2006 Entrust, Inc.", CN = Entrust Root Certification Authority - Validity - Not Before: Nov 27 20:23:42 2006 GMT - Not After : Nov 27 20:53:42 2026 GMT - Subject: C = US, O = "Entrust, Inc.", OU = www.entrust.net/CPS is incorporated by reference, OU = "(c) 2006 Entrust, Inc.", CN = Entrust Root Certification Authority - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - RSA Public-Key: (2048 bit) - Modulus: - 00:b6:95:b6:43:42:fa:c6:6d:2a:6f:48:df:94:4c: - 39:57:05:ee:c3:79:11:41:68:36:ed:ec:fe:9a:01: - 8f:a1:38:28:fc:f7:10:46:66:2e:4d:1e:1a:b1:1a: - 4e:c6:d1:c0:95:88:b0:c9:ff:31:8b:33:03:db:b7: - 83:7b:3e:20:84:5e:ed:b2:56:28:a7:f8:e0:b9:40: - 71:37:c5:cb:47:0e:97:2a:68:c0:22:95:62:15:db: - 47:d9:f5:d0:2b:ff:82:4b:c9:ad:3e:de:4c:db:90: - 80:50:3f:09:8a:84:00:ec:30:0a:3d:18:cd:fb:fd: - 2a:59:9a:23:95:17:2c:45:9e:1f:6e:43:79:6d:0c: - 5c:98:fe:48:a7:c5:23:47:5c:5e:fd:6e:e7:1e:b4: - f6:68:45:d1:86:83:5b:a2:8a:8d:b1:e3:29:80:fe: - 25:71:88:ad:be:bc:8f:ac:52:96:4b:aa:51:8d:e4: - 13:31:19:e8:4e:4d:9f:db:ac:b3:6a:d5:bc:39:54: - 71:ca:7a:7a:7f:90:dd:7d:1d:80:d9:81:bb:59:26: - c2:11:fe:e6:93:e2:f7:80:e4:65:fb:34:37:0e:29: - 80:70:4d:af:38:86:2e:9e:7f:57:af:9e:17:ae:eb: - 1c:cb:28:21:5f:b6:1c:d8:e7:a2:04:22:f9:d3:da: - d8:cb - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Key Usage: critical - Certificate Sign, CRL Sign - X509v3 Basic Constraints: critical - CA:TRUE - X509v3 Private Key Usage Period: - Not Before: Nov 27 20:23:42 2006 GMT, Not After: Nov 27 20:53:42 2026 GMT - X509v3 Authority Key Identifier: - keyid:68:90:E4:67:A4:A6:53:80:C7:86:66:A4:F1:F7:4B:43:FB:84:BD:6D - - X509v3 Subject Key Identifier: - 68:90:E4:67:A4:A6:53:80:C7:86:66:A4:F1:F7:4B:43:FB:84:BD:6D - 1.2.840.113533.7.65.0: - 0...V7.1:4.0.... - Signature Algorithm: sha1WithRSAEncryption - 93:d4:30:b0:d7:03:20:2a:d0:f9:63:e8:91:0c:05:20:a9:5f: - 19:ca:7b:72:4e:d4:b1:db:d0:96:fb:54:5a:19:2c:0c:08:f7: - b2:bc:85:a8:9d:7f:6d:3b:52:b3:2a:db:e7:d4:84:8c:63:f6: - 0f:cb:26:01:91:50:6c:f4:5f:14:e2:93:74:c0:13:9e:30:3a: - 50:e3:b4:60:c5:1c:f0:22:44:8d:71:47:ac:c8:1a:c9:e9:9b: - 9a:00:60:13:ff:70:7e:5f:11:4d:49:1b:b3:15:52:7b:c9:54: - da:bf:9d:95:af:6b:9a:d8:9e:e9:f1:e4:43:8d:e2:11:44:3a: - bf:af:bd:83:42:73:52:8b:aa:bb:a7:29:cf:f5:64:1c:0a:4d: - d1:bc:aa:ac:9f:2a:d0:ff:7f:7f:da:7d:ea:b1:ed:30:25:c1: - 84:da:34:d2:5b:78:83:56:ec:9c:36:c3:26:e2:11:f6:67:49: - 1d:92:ab:8c:fb:eb:ff:7a:ee:85:4a:a7:50:80:f0:a7:5c:4a: - 94:2e:5f:05:99:3c:52:41:e0:cd:b4:63:cf:01:43:ba:9c:83: - dc:8f:60:3b:f3:5a:b4:b4:7b:ae:da:0b:90:38:75:ef:81:1d: - 66:d2:f7:57:70:36:b3:bf:fc:28:af:71:25:85:5b:13:fe:1e: - 7f:5a:b4:3c -SHA1 Fingerprint=B3:1E:B1:B7:40:E3:6C:84:02:DA:DC:37:D4:4D:F5:D4:67:49:52:F9 + 71:2e:af:c4:e3:ad:a3:bd:0c:11:a7:b4:ff:4a:b2:7b:10:10: + 1f:a7:57:41:b2:c0:ae:f4:2c:59:d6:47:10:88:f3:21:51:29: + 30:ca:60:86:af:46:ab:1d:ed:3a:5b:b0:94:de:44:e3:41:08: + a2:c1:ec:1d:d6:fd:4f:b6:d6:47:d0:14:0b:ca:e6:ca:b5:7b: + 77:7e:41:1f:5e:83:c7:b6:8c:39:96:b0:3f:96:81:41:6f:60: + 90:e2:e8:f9:fb:22:71:d9:7d:b3:3d:46:bf:b4:84:af:90:1c: + 0f:8f:12:6a:af:ef:ee:1e:7a:ae:02:4a:8a:17:2b:76:fe:ac: + 54:89:24:2c:4f:3f:b6:b2:a7:4e:8c:a8:91:97:fb:29:c6:7b: + 5c:2d:b9:cb:66:b6:b7:a8:5b:12:51:85:b5:09:7e:62:78:70: + fe:a9:6a:60:b6:1d:0e:79:0c:fd:ca:ea:24:80:72:c3:97:3f: + f2:77:ab:43:22:0a:c7:eb:b6:0c:84:82:2c:80:6b:41:8a:08: + c0:eb:a5:6b:df:99:12:cb:8a:d5:5e:80:0c:91:e0:26:08:36: + 48:c5:fa:38:11:35:ff:25:83:2d:f2:7a:bf:da:fd:8e:fe:a5: + cb:45:2c:1f:c4:88:53:ae:77:0e:d9:9a:76:c5:8e:2c:1d:a3: + ba:d5:ec:32:ae:c0:aa:ac:f7:d1:7a:4d:eb:d4:07:e2:48:f7: + 22:8e:b0:a4:9f:6a:ce:8e:b2:b2:60:f4:a3:22:d0:23:eb:94: + 5a:7a:69:dd:0f:bf:40:57:ac:6b:59:50:d9:a3:99:e1:6e:fe: + 8d:01:79:27:23:15:de:92:9d:7b:09:4d:5a:e7:4b:48:30:5a: + 18:e6:0a:6d:e6:8f:e0:d2:bb:e6:df:7c:6e:21:82:c1:68:39: + 4d:b4:98:58:66:62:cc:4a:90:5e:c3:fa:27:04:b1:79:15:74: + 99:cc:be:ad:20:de:26:60:1c:eb:56:51:a6:a3:ea:e4:a3:3f: + a7:ff:61:dc:f1:5a:4d:6c:32:23:43:ee:ac:a8:ee:ee:4a:12: + 09:3c:5d:71:c2:be:79:fa:c2:87:68:1d:0b:fd:5c:69:cc:06: + d0:9a:7d:54:99:2a:c9:39:1a:19:af:4b:2a:43:f3:63:5d:5a: + 58:e2:2f:e3:1d:e4:a9:d6:d0:0a:d0:9e:bf:d7:81:09:f1:c9: + c7:26:0d:ac:98:16:56:a0 +SHA1 Fingerprint=49:0A:75:74:DE:87:0A:47:FE:58:EE:F6:C7:6B:EB:C6:0B:12:40:99 -----BEGIN CERTIFICATE----- -MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC -VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 -Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW -KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl -cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw -NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw -NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy -ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV -BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo -Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 -4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 -KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI -rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi -94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB -sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi -gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo -kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE -vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA -A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t -O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua -AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP -9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ -eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m -0vdXcDazv/wor3ElhVsT/h5/WrQ8 +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= -----END CERTIFICATE----- Certificate: Data: @@ -11758,6 +12020,97 @@ kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq 4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= -----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1164660820 (0x456b5054) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C = US, O = "Entrust, Inc.", OU = www.entrust.net/CPS is incorporated by reference, OU = "(c) 2006 Entrust, Inc.", CN = Entrust Root Certification Authority + Validity + Not Before: Nov 27 20:23:42 2006 GMT + Not After : Nov 27 20:53:42 2026 GMT + Subject: C = US, O = "Entrust, Inc.", OU = www.entrust.net/CPS is incorporated by reference, OU = "(c) 2006 Entrust, Inc.", CN = Entrust Root Certification Authority + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:b6:95:b6:43:42:fa:c6:6d:2a:6f:48:df:94:4c: + 39:57:05:ee:c3:79:11:41:68:36:ed:ec:fe:9a:01: + 8f:a1:38:28:fc:f7:10:46:66:2e:4d:1e:1a:b1:1a: + 4e:c6:d1:c0:95:88:b0:c9:ff:31:8b:33:03:db:b7: + 83:7b:3e:20:84:5e:ed:b2:56:28:a7:f8:e0:b9:40: + 71:37:c5:cb:47:0e:97:2a:68:c0:22:95:62:15:db: + 47:d9:f5:d0:2b:ff:82:4b:c9:ad:3e:de:4c:db:90: + 80:50:3f:09:8a:84:00:ec:30:0a:3d:18:cd:fb:fd: + 2a:59:9a:23:95:17:2c:45:9e:1f:6e:43:79:6d:0c: + 5c:98:fe:48:a7:c5:23:47:5c:5e:fd:6e:e7:1e:b4: + f6:68:45:d1:86:83:5b:a2:8a:8d:b1:e3:29:80:fe: + 25:71:88:ad:be:bc:8f:ac:52:96:4b:aa:51:8d:e4: + 13:31:19:e8:4e:4d:9f:db:ac:b3:6a:d5:bc:39:54: + 71:ca:7a:7a:7f:90:dd:7d:1d:80:d9:81:bb:59:26: + c2:11:fe:e6:93:e2:f7:80:e4:65:fb:34:37:0e:29: + 80:70:4d:af:38:86:2e:9e:7f:57:af:9e:17:ae:eb: + 1c:cb:28:21:5f:b6:1c:d8:e7:a2:04:22:f9:d3:da: + d8:cb + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Private Key Usage Period: + Not Before: Nov 27 20:23:42 2006 GMT, Not After: Nov 27 20:53:42 2026 GMT + X509v3 Authority Key Identifier: + keyid:68:90:E4:67:A4:A6:53:80:C7:86:66:A4:F1:F7:4B:43:FB:84:BD:6D + + X509v3 Subject Key Identifier: + 68:90:E4:67:A4:A6:53:80:C7:86:66:A4:F1:F7:4B:43:FB:84:BD:6D + 1.2.840.113533.7.65.0: + 0...V7.1:4.0.... + Signature Algorithm: sha1WithRSAEncryption + 93:d4:30:b0:d7:03:20:2a:d0:f9:63:e8:91:0c:05:20:a9:5f: + 19:ca:7b:72:4e:d4:b1:db:d0:96:fb:54:5a:19:2c:0c:08:f7: + b2:bc:85:a8:9d:7f:6d:3b:52:b3:2a:db:e7:d4:84:8c:63:f6: + 0f:cb:26:01:91:50:6c:f4:5f:14:e2:93:74:c0:13:9e:30:3a: + 50:e3:b4:60:c5:1c:f0:22:44:8d:71:47:ac:c8:1a:c9:e9:9b: + 9a:00:60:13:ff:70:7e:5f:11:4d:49:1b:b3:15:52:7b:c9:54: + da:bf:9d:95:af:6b:9a:d8:9e:e9:f1:e4:43:8d:e2:11:44:3a: + bf:af:bd:83:42:73:52:8b:aa:bb:a7:29:cf:f5:64:1c:0a:4d: + d1:bc:aa:ac:9f:2a:d0:ff:7f:7f:da:7d:ea:b1:ed:30:25:c1: + 84:da:34:d2:5b:78:83:56:ec:9c:36:c3:26:e2:11:f6:67:49: + 1d:92:ab:8c:fb:eb:ff:7a:ee:85:4a:a7:50:80:f0:a7:5c:4a: + 94:2e:5f:05:99:3c:52:41:e0:cd:b4:63:cf:01:43:ba:9c:83: + dc:8f:60:3b:f3:5a:b4:b4:7b:ae:da:0b:90:38:75:ef:81:1d: + 66:d2:f7:57:70:36:b3:bf:fc:28:af:71:25:85:5b:13:fe:1e: + 7f:5a:b4:3c +SHA1 Fingerprint=B3:1E:B1:B7:40:E3:6C:84:02:DA:DC:37:D4:4D:F5:D4:67:49:52:F9 +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- Certificate: Data: Version: 3 (0x2) @@ -12733,3 +13086,87 @@ g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN 9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP BSeOE6Fuwg== -----END CERTIFICATE----- +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 6643877497813316402 (0x5c33cb622c5fb332) + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN = Atos TrustedRoot 2011, O = Atos, C = DE + Validity + Not Before: Jul 7 14:58:30 2011 GMT + Not After : Dec 31 23:59:59 2030 GMT + Subject: CN = Atos TrustedRoot 2011, O = Atos, C = DE + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:95:85:3b:97:6f:2a:3b:2e:3b:cf:a6:f3:29:35: + be:cf:18:ac:3e:aa:d9:f8:4d:a0:3e:1a:47:b9:bc: + 9a:df:f2:fe:cc:3e:47:e8:7a:96:c2:24:8e:35:f4: + a9:0c:fc:82:fd:6d:c1:72:62:27:bd:ea:6b:eb:e7: + 8a:cc:54:3e:90:50:cf:80:d4:95:fb:e8:b5:82:d4: + 14:c5:b6:a9:55:25:57:db:b1:50:f6:b0:60:64:59: + 7a:69:cf:03:b7:6f:0d:be:ca:3e:6f:74:72:ea:aa: + 30:2a:73:62:be:49:91:61:c8:11:fe:0e:03:2a:f7: + 6a:20:dc:02:15:0d:5e:15:6a:fc:e3:82:c1:b5:c5: + 9d:64:09:6c:a3:59:98:07:27:c7:1b:96:2b:61:74: + 71:6c:43:f1:f7:35:89:10:e0:9e:ec:55:a1:37:22: + a2:87:04:05:2c:47:7d:b4:1c:b9:62:29:66:28:ca: + b7:e1:93:f5:a4:94:03:99:b9:70:85:b5:e6:48:ea: + 8d:50:fc:d9:de:cc:6f:07:0e:dd:0b:72:9d:80:30: + 16:07:95:3f:28:0e:fd:c5:75:4f:53:d6:74:9a:b4: + 24:2e:8e:02:91:cf:76:c5:9b:1e:55:74:9c:78:21: + b1:f0:2d:f1:0b:9f:c2:d5:96:18:1f:f0:54:22:7a: + 8c:07 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + A7:A5:06:B1:2C:A6:09:60:EE:D1:97:E9:70:AE:BC:3B:19:6C:DB:21 + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Authority Key Identifier: + keyid:A7:A5:06:B1:2C:A6:09:60:EE:D1:97:E9:70:AE:BC:3B:19:6C:DB:21 + + X509v3 Certificate Policies: + Policy: 1.3.6.1.4.1.6189.3.4.1.1 + + X509v3 Key Usage: critical + Digital Signature, Certificate Sign, CRL Sign + Signature Algorithm: sha256WithRSAEncryption + 26:77:34:db:94:48:86:2a:41:9d:2c:3e:06:90:60:c4:8c:ac: + 0b:54:b8:1f:b9:7b:d3:07:39:e4:fa:3e:7b:b2:3d:4e:ed:9f: + 23:bd:97:f3:6b:5c:ef:ee:fd:40:a6:df:a1:93:a1:0a:86:ac: + ef:20:d0:79:01:bd:78:f7:19:d8:24:31:34:04:01:a6:ba:15: + 9a:c3:27:dc:d8:4f:0f:cc:18:63:ff:99:0f:0e:91:6b:75:16: + e1:21:fc:d8:26:c7:47:b7:a6:cf:58:72:71:7e:ba:e1:4d:95: + 47:3b:c9:af:6d:a1:b4:c1:ec:89:f6:b4:0f:38:b5:e2:64:dc: + 25:cf:a6:db:eb:9a:5c:99:a1:c5:08:de:fd:e6:da:d5:d6:5a: + 45:0c:c4:b7:c2:b5:14:ef:b4:11:ff:0e:15:b5:f5:f5:db:c6: + bd:eb:5a:a7:f0:56:22:a9:3c:65:54:c6:15:a8:bd:86:9e:cd: + 83:96:68:7a:71:81:89:e1:0b:e1:ea:11:1b:68:08:cc:69:9e: + ec:9e:41:9e:44:32:26:7a:e2:87:0a:71:3d:eb:e4:5a:a4:d2: + db:c5:cd:c6:de:60:7f:b9:f3:4f:44:92:ef:2a:b7:18:3e:a7: + 19:d9:0b:7d:b1:37:41:42:b0:ba:60:1d:f2:fe:09:11:b0:f0: + 87:7b:a7:9d +SHA1 Fingerprint=2B:B1:F5:3E:55:0C:1D:C5:F1:D4:E6:B7:6A:46:4B:55:06:02:AC:21 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- diff --git a/Base/QTCore/Testing/Cxx/qSlicerExtensionsManagerModelTest.cxx b/Base/QTCore/Testing/Cxx/qSlicerExtensionsManagerModelTest.cxx index 43900557b2a..cf502773fab 100644 --- a/Base/QTCore/Testing/Cxx/qSlicerExtensionsManagerModelTest.cxx +++ b/Base/QTCore/Testing/Cxx/qSlicerExtensionsManagerModelTest.cxx @@ -254,8 +254,6 @@ void qSlicerExtensionsManagerModelTester::installHelper(qSlicerExtensionsManager QVERIFY(model != nullptr); QVERIFY(extensionId >= 0 && extensionId <= 3); - model->setServerQueryWithLimitEnabled(false); - QString inputArchiveFile = QString(":/extension-%1-%2.tar.gz").arg(os).arg(extensionId); QString copiedArchiveFile = tmp + "/" + QFileInfo(inputArchiveFile).fileName(); if (!QFile::exists(copiedArchiveFile)) @@ -582,7 +580,6 @@ void qSlicerExtensionsManagerModelTester::testRetrieveExtensionMetadata() QFETCH(QString, slicerVersion); qSlicerExtensionsManagerModel model; - model.setServerQueryWithLimitEnabled(false); model.setExtensionsSettingsFilePath(QSettings().fileName()); model.setSlicerVersion(slicerVersion); diff --git a/Base/QTCore/qSlicerExtensionsManagerModel.cxx b/Base/QTCore/qSlicerExtensionsManagerModel.cxx index 2d43bb0b5fe..779d7e860d7 100644 --- a/Base/QTCore/qSlicerExtensionsManagerModel.cxx +++ b/Base/QTCore/qSlicerExtensionsManagerModel.cxx @@ -28,11 +28,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include @@ -164,9 +164,6 @@ class qSlicerExtensionsManagerModelPrivate int role(const QByteArray& roleName); - /// This method should only be called after initialization. - void queryExtensionsMetadataFromServer(); - /// Save/load extensions metadata that was retrieved from the server to a local cache /// (in application settings) to avoid too frequent polling of the server. void saveExtensionsMetadataFromServerToCache(); @@ -243,9 +240,7 @@ class qSlicerExtensionsManagerModelPrivate QNetworkAccessManager NetworkManager; qRestAPI ExtensionsMetadataFromServerAPI; - bool ServerQueryWithLimit = true; QMap ExtensionsMetadataFromServer; - QList ExtensionsMetadataFromServerQueryResults; QUuid ExtensionsMetadataFromServerQueryUID; // if not null then it means that query is in progress QHash AvailableUpdates; @@ -398,38 +393,6 @@ int qSlicerExtensionsManagerModelPrivate::role(const QByteArray& roleName) return -1; } -// -------------------------------------------------------------------------- -void qSlicerExtensionsManagerModelPrivate::queryExtensionsMetadataFromServer() -{ - Q_Q(qSlicerExtensionsManagerModel); - - // Build parameters to query server about the extension - qRestAPI::Parameters parameters; - if (q->serverAPI() == qSlicerExtensionsManagerModel::Girder_v1) - { - parameters["app_revision"] = q->slicerRevision(); - parameters["os"] = q->slicerOs(); - parameters["arch"] = q->slicerArch(); - if (this->ServerQueryWithLimit) - { - parameters["offset"] = QString::number(this->ExtensionsMetadataFromServerQueryResults.size()); - // "limit" parameter is not specified as we rely on the server's default value - } - else - { - parameters["limit"] = QString::number(-1); - } - } - else - { - qWarning() << Q_FUNC_INFO << " failed: missing implementation for serverAPI" << q->serverAPI(); - return; - } - - // Issue the query - this->ExtensionsMetadataFromServerQueryUID = this->ExtensionsMetadataFromServerAPI.get("", parameters); -} - // -------------------------------------------------------------------------- void qSlicerExtensionsManagerModelPrivate::saveExtensionsMetadataFromServerToCache() { @@ -1619,8 +1582,9 @@ qSlicerExtensionsManagerModelPrivate::downloadExtensionByName(const QString& ext return nullptr; } - // Retrieve file_id associated with the item + // Retrieve file_id and archive name (extension package filename) associated with the item QString file_id; + QString archivename; this->debug(qSlicerExtensionsManagerModel::tr("Retrieving %1 extension files (extensionId: %2)").arg(extensionName).arg(item_id)); qRestAPI getItemFilesApi; @@ -1639,6 +1603,7 @@ qSlicerExtensionsManagerModelPrivate::downloadExtensionByName(const QString& ext else if (results.count() == 1) { file_id = results.at(0).value("_id").toString(); + archivename = results.at(0).value("name").toString(); } else { @@ -1647,13 +1612,14 @@ qSlicerExtensionsManagerModelPrivate::downloadExtensionByName(const QString& ext } } - if (file_id.isEmpty()) + if (file_id.isEmpty() || archivename.isEmpty()) { return nullptr; } this->debug(qSlicerExtensionsManagerModel::tr("Downloading %1 extension (item_id: %2, file_id: %3)").arg(extensionName).arg(item_id).arg(file_id)); downloadUrl.setPath(downloadUrl.path() + QString("/api/v1/file/%1/download").arg(file_id)); + extensionMetadata.insert("archivename", archivename); } else { @@ -2022,12 +1988,10 @@ bool qSlicerExtensionsManagerModel::updateExtensionsMetadataFromServer(bool forc return true; } - QSignalSpy spy{ this, SIGNAL(updateExtensionsMetadataFromServerCompleted(bool)) }; - if (d->ExtensionsMetadataFromServerQueryUID.isNull()) { // query is not in progress yet, start it - d->ExtensionsMetadataFromServerQueryResults.clear(); + qRestAPI::Parameters parameters; if (this->serverAPI() == qSlicerExtensionsManagerModel::Girder_v1) { QString appID = "5f4474d0e1d8c75dfc705482"; @@ -2038,13 +2002,20 @@ bool qSlicerExtensionsManagerModel::updateExtensionsMetadataFromServer(bool forc return false; } d->ExtensionsMetadataFromServerAPI.setServerUrl(this->serverUrl().toString() + QString("/api/v1/app/%1/extension").arg(appID)); + parameters["app_revision"] = this->slicerRevision(); + parameters["os"] = this->slicerOs(); + parameters["arch"] = this->slicerArch(); + // request all metadata in a single response (it makes synchronous query simpler) + parameters["limit"] = QString::number(0); } else { qWarning() << "Update extension information from server failed: missing implementation for serverAPI" << this->serverAPI(); return false; } - d->queryExtensionsMetadataFromServer(); + + // Issue the query + d->ExtensionsMetadataFromServerQueryUID = d->ExtensionsMetadataFromServerAPI.get("", parameters); } if (!waitForCompletion) @@ -2052,21 +2023,26 @@ bool qSlicerExtensionsManagerModel::updateExtensionsMetadataFromServer(bool forc return true; } - // Wait up to 15 seconds for the update check to complete - // (typically takes only a few seconds) - int timeoutMsec = 15000; - bool completedSignalReceived = spy.wait(timeoutMsec); - if (!completedSignalReceived) + // Temporarily disable onExtensionsMetadataFromServerQueryFinished call via signal/slot + // because we'll call it directly to get returned result. + QObject::disconnect(&d->ExtensionsMetadataFromServerAPI, + SIGNAL(finished(QUuid)), + this, SLOT(onExtensionsMetadataFromServerQueryFinished(QUuid))); + + bool success = this->onExtensionsMetadataFromServerQueryFinished(d->ExtensionsMetadataFromServerQueryUID); + + QObject::connect(&d->ExtensionsMetadataFromServerAPI, + SIGNAL(finished(QUuid)), + this, SLOT(onExtensionsMetadataFromServerQueryFinished(QUuid))); + + if (!success) { d->warning(tr("Update extension information from server failed: timed out while waiting for server response from %1") .arg(d->ExtensionsMetadataFromServerAPI.serverUrl())); } - return completedSignalReceived; -} -// -------------------------------------------------------------------------- -CTK_GET_CPP(qSlicerExtensionsManagerModel, bool, serverQueryWithLimitEnabled, ServerQueryWithLimit); -CTK_SET_CPP(qSlicerExtensionsManagerModel, bool, setServerQueryWithLimitEnabled, ServerQueryWithLimit); + return success; +} // -------------------------------------------------------------------------- QDateTime qSlicerExtensionsManagerModel::lastUpdateTimeExtensionsMetadataFromServer() @@ -2098,7 +2074,7 @@ QStringList qSlicerExtensionsManagerModel::availableUpdateExtensions() const } // -------------------------------------------------------------------------- -void qSlicerExtensionsManagerModel::onExtensionsMetadataFromServerQueryFinished(const QUuid& requestId) +bool qSlicerExtensionsManagerModel::onExtensionsMetadataFromServerQueryFinished(const QUuid& requestId) { Q_UNUSED(requestId); Q_D(qSlicerExtensionsManagerModel); @@ -2117,31 +2093,15 @@ void qSlicerExtensionsManagerModel::onExtensionsMetadataFromServerQueryFinished( { // Query failed d->warning(tr("Failed to download extension metadata from server")); - d->ExtensionsMetadataFromServerQueryResults.clear(); d->ExtensionsMetadataFromServerQueryUID = QUuid(); emit updateExtensionsMetadataFromServerCompleted(false); - return; - } - - d->ExtensionsMetadataFromServerQueryResults.append(restResult->results()); - - if (restResult->results().count() > 0 && d->ServerQueryWithLimit) - { - // May not have received all the items yet. - // To make things simpler, we don't process the data received so far, - // but keep requesting more data until there is no more (we received - // everything) and then process all data at once. - // This should not be an issue because these few queries should be - // all done in a few seconds (with current sever settings, 50 items - // are returned and there are less than 200 extensions). - d->queryExtensionsMetadataFromServer(); - return; + return false; } d->ExtensionsMetadataFromServer.clear(); // Process response - foreach (const QVariantMap& result, d->ExtensionsMetadataFromServerQueryResults) + foreach (const QVariantMap& result, restResult->results()) { // Get extension information from server response ExtensionMetadataType serverExtensionMetadata = qRestAPI::qVariantMapFlattened(result); @@ -2155,13 +2115,13 @@ void qSlicerExtensionsManagerModel::onExtensionsMetadataFromServerQueryFinished( } d->ExtensionsMetadataFromServer[extensionName] = extensionMetadata; } - d->ExtensionsMetadataFromServerQueryResults.clear(); d->ExtensionsMetadataFromServerQueryUID = QUuid(); d->saveExtensionsMetadataFromServerToCache(); this->updateModel(); emit updateExtensionsMetadataFromServerCompleted(true); + return true; } // -------------------------------------------------------------------------- @@ -2302,7 +2262,7 @@ void qSlicerExtensionsManagerModel::onUpdateDownloadFinished( QFile file(archivePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - d->critical(tr("Could not create file for writing: %1").arg(file.errorString())); + d->critical(tr("Could not write file: '%1' (%2)").arg(archivePath).arg(file.errorString())); d->ActiveTasks.remove(task); return; } diff --git a/Base/QTCore/qSlicerExtensionsManagerModel.h b/Base/QTCore/qSlicerExtensionsManagerModel.h index 9f47c9703c6..28202b677f6 100644 --- a/Base/QTCore/qSlicerExtensionsManagerModel.h +++ b/Base/QTCore/qSlicerExtensionsManagerModel.h @@ -399,18 +399,6 @@ public slots: /// Returns false if waitForCompletion is set to true and metadata cannot be retrieved. bool updateExtensionsMetadataFromServer(bool force=false, bool waitForCompletion=false); - /// \brief Enable/disable the use of limit and offset parameters for querying extension metadata from the extensions server. - /// - /// If enabled (the default), the server may return metadata for a limited number of extensions - /// at once (for example, 50) to keep the server's load and response time low. The model automatically - /// submits additional queries until all data is retrieved. - /// - /// IF disabled, the limit is to -1 and all metadata are retrieve at once. - /// - /// \sa updateExtensionsMetadataFromServer() - bool serverQueryWithLimitEnabled()const; - void setServerQueryWithLimitEnabled(bool value); - /// Compares current extensions versions with versions available on the server. /// Emits extensionMetadataUpdated(QString extensionName) and emit extensionUpdatesAvailable(bool found) signals. /// If Extensions/AutoUpdateInstall is enabled in application settings then this will also install the updated extensions. @@ -559,7 +547,7 @@ protected slots: void onUpdateDownloadProgress(qSlicerExtensionDownloadTask* task, qint64 received, qint64 total); - void onExtensionsMetadataFromServerQueryFinished(const QUuid& requestId); + bool onExtensionsMetadataFromServerQueryFinished(const QUuid& requestId); protected: QScopedPointer d_ptr; diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTest.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTest.py index 82c150f00a0..d75e7e6d930 100644 --- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTest.py +++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTest.py @@ -1,16 +1,16 @@ class qSlicerScriptedLoadableModuleNewStyleTest: - def __init__(self, parent): - parent.title = "qSlicerScriptedLoadableModuleNewStyle Test" - parent.categories = ["Testing"] - parent.associatedNodeTypes = ["vtkMRMLModelNode", "vtkMRMLScalarVolumeNode"] - parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware); Max Smolens (Kitware)"] - parent.helpText = """ + def __init__(self, parent): + parent.title = "qSlicerScriptedLoadableModuleNewStyle Test" + parent.categories = ["Testing"] + parent.associatedNodeTypes = ["vtkMRMLModelNode", "vtkMRMLScalarVolumeNode"] + parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware); Max Smolens (Kitware)"] + parent.helpText = """ This module is for testing. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ Based on qSlicerScriptedLoadableModuleTest . """ - self.parent = parent + self.parent = parent - def setup(self): - self.parent.setProperty('setup_called_within_Python', True) + def setup(self): + self.parent.setProperty('setup_called_within_Python', True) diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTestWidget.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTestWidget.py index 5e636bfa350..14f34357896 100644 --- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTestWidget.py +++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTestWidget.py @@ -1,22 +1,22 @@ class qSlicerScriptedLoadableModuleNewStyleTestWidget: - def __init__(self, parent=None): - self.parent = parent + def __init__(self, parent=None): + self.parent = parent - def setup(self): - self.parent.setProperty('setup_called_within_Python', True) + def setup(self): + self.parent.setProperty('setup_called_within_Python', True) - def enter(self): - self.parent.setProperty('enter_called_within_Python', True) + def enter(self): + self.parent.setProperty('enter_called_within_Python', True) - def exit(self): - self.parent.setProperty('exit_called_within_Python', True) + def exit(self): + self.parent.setProperty('exit_called_within_Python', True) - def setEditedNode(self, node, role = '', context = ''): - self.parent.setProperty('editedNodeName', node.GetName() if node is not None else "") - self.parent.setProperty('editedNodeRole', role) - self.parent.setProperty('editedNodeContext', context) - return (node is not None) + def setEditedNode(self, node, role='', context=''): + self.parent.setProperty('editedNodeName', node.GetName() if node is not None else "") + self.parent.setProperty('editedNodeRole', role) + self.parent.setProperty('editedNodeContext', context) + return (node is not None) - def nodeEditable(self, node): - self.parent.setProperty('editableNodeName', node.GetName() if node is not None else "") - return 0.7 if node is not None else 0.3 + def nodeEditable(self, node): + self.parent.setProperty('editableNodeName', node.GetName() if node is not None else "") + return 0.7 if node is not None else 0.3 diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTest.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTest.py index 140670e201c..2d93886e157 100644 --- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTest.py +++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTest.py @@ -1,3 +1,3 @@ class qSlicerScriptedLoadableModuleSyntaxErrorTest: - def __init__(self, parent): - s elf.parent = parent # noqa: E999 + def __init__(self, parent): + s elf.parent = parent # noqa: E999 diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTestWidget.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTestWidget.py index 19b07adaf56..3b3d3cbac9c 100644 --- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTestWidget.py +++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTestWidget.py @@ -1,3 +1,3 @@ class qSlicerScriptedLoadableModuleSyntaxErrorTestWidget: - def __init__(self, parent=None): - s elf.parent = parent # noqa: E999 + def __init__(self, parent=None): + s elf.parent = parent # noqa: E999 diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTest.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTest.py index 8c79907143a..2451c6cf8c2 100644 --- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTest.py +++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTest.py @@ -1,30 +1,30 @@ class qSlicerScriptedLoadableModuleTest: - def __init__(self, parent): - parent.title = "qSlicerScriptedLoadableModule Test" - parent.categories = ["Testing"] - parent.associatedNodeTypes = ["vtkMRMLModelNode", "vtkMRMLScalarVolumeNode"] - parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"] - parent.helpText = """ + def __init__(self, parent): + parent.title = "qSlicerScriptedLoadableModule Test" + parent.categories = ["Testing"] + parent.associatedNodeTypes = ["vtkMRMLModelNode", "vtkMRMLScalarVolumeNode"] + parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"] + parent.helpText = """ This module is used to test qSlicerScriptedLoadableModule and qSlicerScriptedLoadableModuleWidget classes. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. and was partially funded by NIH grant 3P41RR013218-12S1 """ - self.parent = parent + self.parent = parent - def setup(self): - self.parent.setProperty('setup_called_within_Python', True) + def setup(self): + self.parent.setProperty('setup_called_within_Python', True) class qSlicerScriptedLoadableModuleTestWidget: - def __init__(self, parent=None): - self.parent = parent + def __init__(self, parent=None): + self.parent = parent - def setup(self): - self.parent.setProperty('setup_called_within_Python', True) + def setup(self): + self.parent.setProperty('setup_called_within_Python', True) - def enter(self): - self.parent.setProperty('enter_called_within_Python', True) + def enter(self): + self.parent.setProperty('enter_called_within_Python', True) - def exit(self): - self.parent.setProperty('exit_called_within_Python', True) + def exit(self): + self.parent.setProperty('exit_called_within_Python', True) diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTestWidget.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTestWidget.py index 241ec751c65..371b00b4b0b 100644 --- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTestWidget.py +++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTestWidget.py @@ -1,22 +1,22 @@ class qSlicerScriptedLoadableModuleTestWidget: - def __init__(self, parent=None): - self.parent = parent + def __init__(self, parent=None): + self.parent = parent - def setup(self): - self.parent.setProperty('setup_called_within_Python', True) + def setup(self): + self.parent.setProperty('setup_called_within_Python', True) - def enter(self): - self.parent.setProperty('enter_called_within_Python', True) + def enter(self): + self.parent.setProperty('enter_called_within_Python', True) - def exit(self): - self.parent.setProperty('exit_called_within_Python', True) + def exit(self): + self.parent.setProperty('exit_called_within_Python', True) - def setEditedNode(self, node, role = '', context = ''): - self.parent.setProperty('editedNodeName', node.GetName() if node is not None else "") - self.parent.setProperty('editedNodeRole', role) - self.parent.setProperty('editedNodeContext', context) - return (node is not None) + def setEditedNode(self, node, role='', context=''): + self.parent.setProperty('editedNodeName', node.GetName() if node is not None else "") + self.parent.setProperty('editedNodeRole', role) + self.parent.setProperty('editedNodeContext', context) + return (node is not None) - def nodeEditable(self, node): - self.parent.setProperty('editableNodeName', node.GetName() if node is not None else "") - return 0.7 if node is not None else 0.3 + def nodeEditable(self, node): + self.parent.setProperty('editableNodeName', node.GetName() if node is not None else "") + return 0.7 if node is not None else 0.3 diff --git a/Base/QTGUI/qSlicerExportNodeDialog.cxx b/Base/QTGUI/qSlicerExportNodeDialog.cxx index 3b029a768f0..4bac48c55bf 100644 --- a/Base/QTGUI/qSlicerExportNodeDialog.cxx +++ b/Base/QTGUI/qSlicerExportNodeDialog.cxx @@ -1031,6 +1031,10 @@ bool qSlicerExportNodeDialogPrivate::exportNodes() bool success = coreIOManager->exportNodes(savingParameterMaps, this->HardenTransformCheckBox->isChecked(), userMessages); QApplication::restoreOverrideCursor(); + bool warningFound = false; + bool errorFound = false; + QString messagesStr = QString::fromStdString(userMessages->GetAllMessagesAsString(&errorFound, &warningFound)); + if (!success) { // Make sure at least one error message is in userMessages if saving returns with error @@ -1039,7 +1043,7 @@ bool qSlicerExportNodeDialogPrivate::exportNodes() userMessages->AddMessage(vtkCommand::ErrorEvent, tr("Error encountered while exporting.").toStdString()); } - QString messagesStr = QString::fromStdString(userMessages->GetAllMessagesAsString()); + qCritical() << Q_FUNC_INFO << "Data export error:" << messagesStr; // display messagesStr as an error message QMessageBox::critical(this, tr("Export Error"), messagesStr); @@ -1047,23 +1051,21 @@ bool qSlicerExportNodeDialogPrivate::exportNodes() // In case there are any errors or warnings in storage node, make sure to alert even in case of success: else if (userMessages->GetNumberOfMessages() > 0) { - bool warningFound = false; - bool errorFound = false; - QString messagesStr = QString::fromStdString(userMessages->GetAllMessagesAsString(&errorFound, &warningFound)); - if (errorFound) { + qWarning() << Q_FUNC_INFO << "Data export warning: node write returned success, but there were error messages during write." << messagesStr; QMessageBox::critical(this, tr("Export Error"), messagesStr); // If there was an error, this should never have been considered a success. success = false; - qWarning() << Q_FUNC_INFO << " warning: node write returned success, while there were error messages during write."; } else if (warningFound) { + qWarning() << Q_FUNC_INFO << "Data export warning: node write returned success, but there were warning messages during write." << messagesStr; QMessageBox::warning(this, tr("Export Warning"), messagesStr); } else { + qDebug() << Q_FUNC_INFO << "Data export information:" << messagesStr; QMessageBox::information(this, tr("Export Information"), messagesStr); } } diff --git a/Base/QTGUI/qSlicerIOManager.cxx b/Base/QTGUI/qSlicerIOManager.cxx index dbfe55366e1..d567b48b2f8 100644 --- a/Base/QTGUI/qSlicerIOManager.cxx +++ b/Base/QTGUI/qSlicerIOManager.cxx @@ -591,7 +591,9 @@ void qSlicerIOManager::showLoadNodesResultDialog(bool overallSuccess, vtkMRMLMes if (userMessages) { text += tr("Click 'Show details' button and check the application log for more information."); - messageBox->setDetailedText(QString::fromStdString(userMessages->GetAllMessagesAsString())); + QString messagesStr = QString::fromStdString(userMessages->GetAllMessagesAsString()); + messageBox->setDetailedText(messagesStr); + qWarning() << Q_FUNC_INFO << "Errors occurred while loading nodes:" << messagesStr; } else { diff --git a/Base/QTGUI/qSlicerNodeWriter.cxx b/Base/QTGUI/qSlicerNodeWriter.cxx index f8148fc31e6..157d695fcee 100644 --- a/Base/QTGUI/qSlicerNodeWriter.cxx +++ b/Base/QTGUI/qSlicerNodeWriter.cxx @@ -35,6 +35,7 @@ #include #include #include +#include // VTK includes #include @@ -166,6 +167,8 @@ bool qSlicerNodeWriter::write(const qSlicerIO::IOProperties& properties) this->setWrittenNodes(QStringList() << node->GetID()); } + this->userMessages()->AddMessages(snode->GetUserMessages()); + return res; } diff --git a/Base/QTGUI/qSlicerSaveDataDialog.cxx b/Base/QTGUI/qSlicerSaveDataDialog.cxx index dee946612bc..11873a42ed2 100644 --- a/Base/QTGUI/qSlicerSaveDataDialog.cxx +++ b/Base/QTGUI/qSlicerSaveDataDialog.cxx @@ -1288,6 +1288,18 @@ void qSlicerSaveDataDialogPrivate::updateStatusIconFromMessageCollection(int row if (userMessages) { messagesStr = QString::fromStdString(userMessages->GetAllMessagesAsString(&errorFound, &warningFound)); + if (errorFound) + { + qCritical() << Q_FUNC_INFO << "Data save error:" << messagesStr; + } + else if (warningFound) + { + qWarning() << Q_FUNC_INFO << "Data save warning:" << messagesStr; + } + else + { + qDebug() << Q_FUNC_INFO << "Data save information:" << messagesStr; + } } this->setStatusIcon(row, (!success || errorFound) ? this->ErrorIcon : (warningFound ? this->WarningIcon : QIcon()), messagesStr); } diff --git a/Docs/conf.py b/Docs/conf.py index 84c77fc56cc..42a88d2e6b6 100644 --- a/Docs/conf.py +++ b/Docs/conf.py @@ -241,7 +241,7 @@ inputpaths = [ os.path.join(docsfolder, "../Modules/CLI"), os.path.join(docsfolder, "_extracli"), - ] +] # List of modules to be excluded from documentation generation # (for example, testing modules only). @@ -249,7 +249,7 @@ 'CLIROITest.xml', 'TestGridTransformRegistration.xml', 'DiffusionTensorTest.xml', - ] +] # Output folder that contains all generated markdown files. outpath = os.path.join(docsfolder, "_moduledescriptions") @@ -264,7 +264,7 @@ def _generatemd(dom, docsfolder, outpath, xslt, suffix): xsltpath = os.path.join(docsfolder, xslt) transform = ET.XSLT(ET.parse(xsltpath)) content = str(transform(dom)) - with open(os.path.join(outpath, os.path.splitext(name)[0]+suffix+'.md'), 'w', encoding='utf8') as outfile: + with open(os.path.join(outpath, os.path.splitext(name)[0] + suffix + '.md'), 'w', encoding='utf8') as outfile: outfile.write(content) diff --git a/Docs/developer_guide/build_instructions/linux.md b/Docs/developer_guide/build_instructions/linux.md index 0d9607358bf..ea76e696b9d 100644 --- a/Docs/developer_guide/build_instructions/linux.md +++ b/Docs/developer_guide/build_instructions/linux.md @@ -93,9 +93,7 @@ Slicer built on CentOS 7 will be available for many Linux distributions and rele Install Qt and CMake as described in [Any Distribution](./linux.md#any-distribution) section. -Since by default CentOS 7 comes with `gcc 4.8.5` only having [experimental support for C++14](https://gcc.gnu.org/onlinedocs/gcc-4.8.5/gcc/C-Dialect-Options.html#C-Dialect-Options), the following allows to install and activate the `devtoolset-11` [providing](https://access.redhat.com/documentation/en-us/red_hat_developer_toolset/11/html/11.0_release_notes/dts11.0_release#Changes_in_DTS) `gcc 11.2.1` [supporting C++20](https://en.cppreference.com/w/cpp/compiler_support/20). - -CentOS 7 comes with pretty old devtoolset (gcc 4.8.5). Install and activate newer toolset (11 in this case): +Since by default CentOS 7 comes with `gcc 4.8.5` only having [experimental support for C++14](https://gcc.gnu.org/onlinedocs/gcc-4.8.5/gcc/C-Dialect-Options.html#C-Dialect-Options), the following allows to install and activate the `devtoolset-11` [providing](https://access.redhat.com/documentation/en-us/red_hat_developer_toolset/11/html/11.0_release_notes/dts11.0_release#Changes_in_DTS) `gcc 11.2.1` [supporting C++20](https://en.cppreference.com/w/cpp/compiler_support/20): ```console sudo yum install centos-release-scl diff --git a/Docs/developer_guide/extensions.md b/Docs/developer_guide/extensions.md index 87a939192c4..393468e00be 100644 --- a/Docs/developer_guide/extensions.md +++ b/Docs/developer_guide/extensions.md @@ -22,6 +22,8 @@ If developing modules in Python only, then it is not necessary to build the exte ::: +Similarly to the building of Slicer core, multi-configuration builds are not supported: one build tree can be only used for one build mode (Release or Debug or RelWithDebInfo or MinSizeRel). If a release and debug mode build are needed then the same source code folder can be used (e.g., `C:\D\SlicerHeart`) but a separate binary folder must be created for each build mode (e.g., `C:\D\SlicerHeart-R` and `C:\D\SlicerHeart-D` for release and debug modes). + Assuming that the source code of your extension is located in folder `MyExtension`, an extension can be built by the following steps. ### Linux and macOS @@ -69,7 +71,7 @@ Run `CMake (cmake-gui)` from the Windows Start menu. - Click `Configure`. No errors should be displayed. - Click `Generate` button. - Click `Open project` button to open `MyExtension.sln` in Visual Studio. -- Select build configuration (Debug, Release, ...) that matches the build configuration of the chosen Slicer build. +- Select build configuration (Debug, Release, ...) that matches the build configuration (Release, Debug, ...) of the chosen Slicer build. - In the menu choose Build / Build Solution. ## Test an extension @@ -158,7 +160,6 @@ $ make package - Open `MyExtension.sln` in Visual Studio. - Right-click on `PACKAGES` project, then select `Build`. - ## Write documentation for an extension Keep documentation with your extension's source code and keep it up-to-date whenever the software changes. diff --git a/Docs/developer_guide/style_guide.md b/Docs/developer_guide/style_guide.md index 9a47c917ed6..d7177d04555 100644 --- a/Docs/developer_guide/style_guide.md +++ b/Docs/developer_guide/style_guide.md @@ -22,7 +22,7 @@ Line length: Preferably keep lines shorter than 80 characters. Always keep lines #### Python -- Indentation is 2 spaces per level for consistency with the rest of the code base. This may be revisited in the future. Do not use tabs. +- Follow the Slicer repository Flake8 configuration and run `python -m pre_commit run --all-files` to confirm compliance. - Text encoding: UTF-8 is preferred, Latin-1 is acceptable - Comparisons: - To singletons (e.g. None): use 'is' or 'is not', never equality operations. @@ -35,7 +35,6 @@ Line length: Preferably keep lines shorter than 80 characters. Always keep lines - Local apps/library specific imports - Slicer application imports and local/module imports may be grouped independently. - One package per line (with or without multiple function/module/class imports from the package) -- Avoid extraneous whitespaces - Naming conventions: when [PEP 8](https://www.python.org/dev/peps/pep-0008/#package-and-module-names) and Slicer naming conventions conflict, Slicer wins. #### C++ diff --git a/Extensions/Testing/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py b/Extensions/Testing/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py index 338c6891acb..70f8f06df5f 100644 --- a/Extensions/Testing/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py +++ b/Extensions/Testing/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py @@ -13,25 +13,25 @@ class ScriptedLoadableModuleTemplate(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 = "ScriptedLoadableModuleTemplate" # TODO make this more human readable by adding spaces - self.parent.categories = ["Examples"] - self.parent.dependencies = [] - self.parent.contributors = ["John Doe (AnyWare Corp.)"] # replace with "Firstname Lastname (Organization)" - self.parent.helpText = """ + """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 = "ScriptedLoadableModuleTemplate" # TODO make this more human readable by adding spaces + self.parent.categories = ["Examples"] + self.parent.dependencies = [] + self.parent.contributors = ["John Doe (AnyWare Corp.)"] # replace with "Firstname Lastname (Organization)" + 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.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = """ + self.parent.helpText += self.getDefaultModuleDocumentationLink() + 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. +""" # replace with organization, grant and thanks. # # ScriptedLoadableModuleTemplateWidget @@ -39,104 +39,104 @@ def __init__(self, parent): class ScriptedLoadableModuleTemplateWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - # Instantiate and connect widgets ... - - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) - - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - - # - # input volume selector - # - self.inputSelector = slicer.qMRMLNodeComboBox() - self.inputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] - self.inputSelector.selectNodeUponCreation = True - self.inputSelector.addEnabled = False - self.inputSelector.removeEnabled = False - self.inputSelector.noneEnabled = False - self.inputSelector.showHidden = False - self.inputSelector.showChildNodeTypes = False - self.inputSelector.setMRMLScene( slicer.mrmlScene ) - self.inputSelector.setToolTip( "Pick the input to the algorithm." ) - parametersFormLayout.addRow("Input Volume: ", self.inputSelector) - - # - # output volume selector - # - self.outputSelector = slicer.qMRMLNodeComboBox() - self.outputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] - self.outputSelector.selectNodeUponCreation = True - self.outputSelector.addEnabled = True - self.outputSelector.removeEnabled = True - self.outputSelector.noneEnabled = True - self.outputSelector.showHidden = False - self.outputSelector.showChildNodeTypes = False - self.outputSelector.setMRMLScene( slicer.mrmlScene ) - self.outputSelector.setToolTip( "Pick the output to the algorithm." ) - parametersFormLayout.addRow("Output Volume: ", self.outputSelector) - - # - # threshold value - # - self.imageThresholdSliderWidget = ctk.ctkSliderWidget() - self.imageThresholdSliderWidget.singleStep = 0.1 - self.imageThresholdSliderWidget.minimum = -100 - self.imageThresholdSliderWidget.maximum = 100 - self.imageThresholdSliderWidget.value = 0.5 - self.imageThresholdSliderWidget.setToolTip("Set threshold value for computing the output image. Voxels that have intensities lower than this value will set to zero.") - parametersFormLayout.addRow("Image threshold", self.imageThresholdSliderWidget) - - # - # check box to trigger taking screen shots for later use in tutorials - # - self.enableScreenshotsFlagCheckBox = qt.QCheckBox() - self.enableScreenshotsFlagCheckBox.checked = 0 - self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") - parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) - - # - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the algorithm." - self.applyButton.enabled = False - parametersFormLayout.addRow(self.applyButton) - - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) - self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect) - self.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect) - - # Add vertical spacer - self.layout.addStretch(1) - - # Refresh Apply button state - self.onSelect() - - def cleanup(self): - pass - - def onSelect(self): - self.applyButton.enabled = self.inputSelector.currentNode() and self.outputSelector.currentNode() - - def onApplyButton(self): - logic = ScriptedLoadableModuleTemplateLogic() - enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked - imageThreshold = self.imageThresholdSliderWidget.value - logic.run(self.inputSelector.currentNode(), self.outputSelector.currentNode(), imageThreshold, enableScreenshotsFlag) + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + # Instantiate and connect widgets ... + + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) + + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + + # + # input volume selector + # + self.inputSelector = slicer.qMRMLNodeComboBox() + self.inputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] + self.inputSelector.selectNodeUponCreation = True + self.inputSelector.addEnabled = False + self.inputSelector.removeEnabled = False + self.inputSelector.noneEnabled = False + self.inputSelector.showHidden = False + self.inputSelector.showChildNodeTypes = False + self.inputSelector.setMRMLScene(slicer.mrmlScene) + self.inputSelector.setToolTip("Pick the input to the algorithm.") + parametersFormLayout.addRow("Input Volume: ", self.inputSelector) + + # + # output volume selector + # + self.outputSelector = slicer.qMRMLNodeComboBox() + self.outputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] + self.outputSelector.selectNodeUponCreation = True + self.outputSelector.addEnabled = True + self.outputSelector.removeEnabled = True + self.outputSelector.noneEnabled = True + self.outputSelector.showHidden = False + self.outputSelector.showChildNodeTypes = False + self.outputSelector.setMRMLScene(slicer.mrmlScene) + self.outputSelector.setToolTip("Pick the output to the algorithm.") + parametersFormLayout.addRow("Output Volume: ", self.outputSelector) + + # + # threshold value + # + self.imageThresholdSliderWidget = ctk.ctkSliderWidget() + self.imageThresholdSliderWidget.singleStep = 0.1 + self.imageThresholdSliderWidget.minimum = -100 + self.imageThresholdSliderWidget.maximum = 100 + self.imageThresholdSliderWidget.value = 0.5 + self.imageThresholdSliderWidget.setToolTip("Set threshold value for computing the output image. Voxels that have intensities lower than this value will set to zero.") + parametersFormLayout.addRow("Image threshold", self.imageThresholdSliderWidget) + + # + # check box to trigger taking screen shots for later use in tutorials + # + self.enableScreenshotsFlagCheckBox = qt.QCheckBox() + self.enableScreenshotsFlagCheckBox.checked = 0 + self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") + parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) + + # + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the algorithm." + self.applyButton.enabled = False + parametersFormLayout.addRow(self.applyButton) + + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) + self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect) + self.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect) + + # Add vertical spacer + self.layout.addStretch(1) + + # Refresh Apply button state + self.onSelect() + + def cleanup(self): + pass + + def onSelect(self): + self.applyButton.enabled = self.inputSelector.currentNode() and self.outputSelector.currentNode() + + def onApplyButton(self): + logic = ScriptedLoadableModuleTemplateLogic() + enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked + imageThreshold = self.imageThresholdSliderWidget.value + logic.run(self.inputSelector.currentNode(), self.outputSelector.currentNode(), imageThreshold, enableScreenshotsFlag) # # ScriptedLoadableModuleTemplateLogic @@ -144,105 +144,105 @@ def onApplyButton(self): class ScriptedLoadableModuleTemplateLogic(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 - - def run(self, inputVolume, outputVolume, imageThreshold): - """ - Run the actual algorithm + """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 """ - if not self.isValidInputOutputData(inputVolume, outputVolume): - slicer.util.errorDisplay('Input volume is the same as output volume. Choose a different output volume.') - return False - - logging.info('Processing started') - - # Compute the thresholded output volume using the Threshold Scalar Volume CLI module - cliParams = {'InputVolume': inputVolume.GetID(), 'OutputVolume': outputVolume.GetID(), 'ThresholdValue' : imageThreshold, 'ThresholdType' : 'Above'} - cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True) - - logging.info('Processing completed') - - return True + 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 run(self, inputVolume, outputVolume, imageThreshold): + """ + Run the actual algorithm + """ + + if not self.isValidInputOutputData(inputVolume, outputVolume): + slicer.util.errorDisplay('Input volume is the same as output volume. Choose a different output volume.') + return False + + logging.info('Processing started') + + # Compute the thresholded output volume using the Threshold Scalar Volume CLI module + cliParams = {'InputVolume': inputVolume.GetID(), 'OutputVolume': outputVolume.GetID(), 'ThresholdValue': imageThreshold, 'ThresholdType': 'Above'} + cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True) + + logging.info('Processing completed') + + return True class ScriptedLoadableModuleTemplateTest(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_ScriptedLoadableModuleTemplate1() - - def test_ScriptedLoadableModuleTemplate1(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. + 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 """ - self.delayDisplay("Starting the test") - # - # first, get some data - # - import SampleData - volumeNode = SampleData.downloadFromURL( - nodeNames='MRHead', - fileNames='MR-head.nrrd', - uris=TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', - checksums='SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93') - self.delayDisplay('Finished with download and loading') - - logic = ScriptedLoadableModuleTemplateLogic() - self.assertIsNotNone( logic.hasImageData(volumeNode) ) - self.takeScreenshot('ScriptedLoadableModuleTemplateTest-Start','MyScreenshot',-1) - self.delayDisplay('Test passed!') + 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_ScriptedLoadableModuleTemplate1() + + def test_ScriptedLoadableModuleTemplate1(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 SampleData + volumeNode = SampleData.downloadFromURL( + nodeNames='MRHead', + fileNames='MR-head.nrrd', + uris=TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', + checksums='SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93') + self.delayDisplay('Finished with download and loading') + + logic = ScriptedLoadableModuleTemplateLogic() + self.assertIsNotNone(logic.hasImageData(volumeNode)) + self.takeScreenshot('ScriptedLoadableModuleTemplateTest-Start', 'MyScreenshot', -1) + self.delayDisplay('Test passed!') diff --git a/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplate.py b/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplate.py index ea7de2a680d..305c48dbc94 100644 --- a/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplate.py +++ b/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplate.py @@ -6,133 +6,133 @@ class SegmentEditorScriptedSegmentEditorEffectModuleTemplate(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 = "SegmentEditorScriptedSegmentEditorEffectModuleTemplate" - self.parent.categories = ["Segmentation"] - self.parent.dependencies = ["Segmentations"] - self.parent.contributors = ["Andras Lasso (PerkLab)"] - self.parent.hidden = True - self.parent.helpText = "This hidden module registers the segment editor effect" - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = "Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details." - slicer.app.connect("startupCompleted()", self.registerEditorEffect) - - def registerEditorEffect(self): - import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects - instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None) - effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__+'Lib/SegmentEditorEffect.py') - instance.setPythonSource(effectFilename.replace('\\','/')) - instance.self().register() - - -class SegmentEditorScriptedSegmentEditorEffectModuleTemplateTest(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. + """Uses ScriptedLoadableModule base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - slicer.mrmlScene.Clear(0) - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_ScriptedSegmentEditorEffectModuleTemplate1() + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "SegmentEditorScriptedSegmentEditorEffectModuleTemplate" + self.parent.categories = ["Segmentation"] + self.parent.dependencies = ["Segmentations"] + self.parent.contributors = ["Andras Lasso (PerkLab)"] + self.parent.hidden = True + self.parent.helpText = "This hidden module registers the segment editor effect" + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = "Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details." + slicer.app.connect("startupCompleted()", self.registerEditorEffect) + + def registerEditorEffect(self): + import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects + instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None) + effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__ + 'Lib/SegmentEditorEffect.py') + instance.setPythonSource(effectFilename.replace('\\', '/')) + instance.self().register() + - def test_ScriptedSegmentEditorEffectModuleTemplate1(self): +class SegmentEditorScriptedSegmentEditorEffectModuleTemplateTest(ScriptedLoadableModuleTest): """ - Basic automated test of the segmentation method: - - Create segmentation by placing sphere-shaped seeds - - Run segmentation - - Verify results using segment statistics - The test can be executed from SelfTests module (test name: SegmentEditorScriptedSegmentEditorEffectModuleTemplate) + 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 """ - self.delayDisplay("Starting test_ScriptedSegmentEditorEffectModuleTemplate1") - - import vtkSegmentationCorePython as vtkSegmentationCore - import SampleData - from SegmentStatistics import SegmentStatisticsLogic - - ################################## - self.delayDisplay("Load master volume") - - masterVolumeNode = SampleData.downloadSample('MRBrainTumor1') - - ################################## - self.delayDisplay("Create segmentation containing a few spheres") - - segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') - segmentationNode.CreateDefaultDisplayNodes() - segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode) - - # Segments are defined by a list of: name and a list of sphere [radius, posX, posY, posZ] - segmentGeometries = [ - ['Tumor', [[10, -6,30,28]]], - ['Background', [[10, 0,65,22], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0,30,54], [12, 31, 33, 27], [17, -42, 30, 27], [6, -2,-17,71]]], - ['Air', [[10, 76,73,0], [15, -70,74,0]]] ] - for segmentGeometry in segmentGeometries: - segmentName = segmentGeometry[0] - appender = vtk.vtkAppendPolyData() - for sphere in segmentGeometry[1]: - sphereSource = vtk.vtkSphereSource() - sphereSource.SetRadius(sphere[0]) - sphereSource.SetCenter(sphere[1], sphere[2], sphere[3]) - appender.AddInputConnection(sphereSource.GetOutputPort()) - segment = vtkSegmentationCore.vtkSegment() - segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName)) - appender.Update() - segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), appender.GetOutput()) - segmentationNode.GetSegmentation().AddSegment(segment) - - ################################## - self.delayDisplay("Create segment editor") - - segmentEditorWidget = slicer.qMRMLSegmentEditorWidget() - segmentEditorWidget.show() - segmentEditorWidget.setMRMLScene(slicer.mrmlScene) - segmentEditorNode = slicer.vtkMRMLSegmentEditorNode() - slicer.mrmlScene.AddNode(segmentEditorNode) - segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode) - segmentEditorWidget.setSegmentationNode(segmentationNode) - segmentEditorWidget.setMasterVolumeNode(masterVolumeNode) - - ################################## - self.delayDisplay("Run segmentation") - segmentEditorWidget.setActiveEffectByName("ScriptedSegmentEditorEffectModuleTemplate") - effect = segmentEditorWidget.activeEffect() - effect.setParameter("ObjectScaleMm", 3.0) - effect.self().onApply() - - ################################## - self.delayDisplay("Make segmentation results nicely visible in 3D") - segmentationDisplayNode = segmentationNode.GetDisplayNode() - segmentationDisplayNode.SetSegmentVisibility("Air", False) - segmentationDisplayNode.SetSegmentOpacity3D("Background",0.5) - - ################################## - self.delayDisplay("Compute statistics") - - segStatLogic = SegmentStatisticsLogic() - segStatLogic.computeStatistics(segmentationNode, masterVolumeNode) - - # Export results to table (just to see all results) - resultsTableNode = slicer.vtkMRMLTableNode() - slicer.mrmlScene.AddNode(resultsTableNode) - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - - self.delayDisplay("Check a few numerical results") - self.assertEqual( round(segStatLogic.statistics["Tumor","LM volume cc"]), 16) - self.assertEqual( round(segStatLogic.statistics["Background","LM volume cc"]), 3010) - - self.delayDisplay('test_ScriptedSegmentEditorEffectModuleTemplate1 passed') + 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_ScriptedSegmentEditorEffectModuleTemplate1() + + def test_ScriptedSegmentEditorEffectModuleTemplate1(self): + """ + Basic automated test of the segmentation method: + - Create segmentation by placing sphere-shaped seeds + - Run segmentation + - Verify results using segment statistics + The test can be executed from SelfTests module (test name: SegmentEditorScriptedSegmentEditorEffectModuleTemplate) + """ + + self.delayDisplay("Starting test_ScriptedSegmentEditorEffectModuleTemplate1") + + import vtkSegmentationCorePython as vtkSegmentationCore + import SampleData + from SegmentStatistics import SegmentStatisticsLogic + + ################################## + self.delayDisplay("Load master volume") + + masterVolumeNode = SampleData.downloadSample('MRBrainTumor1') + + ################################## + self.delayDisplay("Create segmentation containing a few spheres") + + segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') + segmentationNode.CreateDefaultDisplayNodes() + segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode) + + # Segments are defined by a list of: name and a list of sphere [radius, posX, posY, posZ] + segmentGeometries = [ + ['Tumor', [[10, -6, 30, 28]]], + ['Background', [[10, 0, 65, 22], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 54], [12, 31, 33, 27], [17, -42, 30, 27], [6, -2, -17, 71]]], + ['Air', [[10, 76, 73, 0], [15, -70, 74, 0]]]] + for segmentGeometry in segmentGeometries: + segmentName = segmentGeometry[0] + appender = vtk.vtkAppendPolyData() + for sphere in segmentGeometry[1]: + sphereSource = vtk.vtkSphereSource() + sphereSource.SetRadius(sphere[0]) + sphereSource.SetCenter(sphere[1], sphere[2], sphere[3]) + appender.AddInputConnection(sphereSource.GetOutputPort()) + segment = vtkSegmentationCore.vtkSegment() + segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName)) + appender.Update() + segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), appender.GetOutput()) + segmentationNode.GetSegmentation().AddSegment(segment) + + ################################## + self.delayDisplay("Create segment editor") + + segmentEditorWidget = slicer.qMRMLSegmentEditorWidget() + segmentEditorWidget.show() + segmentEditorWidget.setMRMLScene(slicer.mrmlScene) + segmentEditorNode = slicer.vtkMRMLSegmentEditorNode() + slicer.mrmlScene.AddNode(segmentEditorNode) + segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode) + segmentEditorWidget.setSegmentationNode(segmentationNode) + segmentEditorWidget.setMasterVolumeNode(masterVolumeNode) + + ################################## + self.delayDisplay("Run segmentation") + segmentEditorWidget.setActiveEffectByName("ScriptedSegmentEditorEffectModuleTemplate") + effect = segmentEditorWidget.activeEffect() + effect.setParameter("ObjectScaleMm", 3.0) + effect.self().onApply() + + ################################## + self.delayDisplay("Make segmentation results nicely visible in 3D") + segmentationDisplayNode = segmentationNode.GetDisplayNode() + segmentationDisplayNode.SetSegmentVisibility("Air", False) + segmentationDisplayNode.SetSegmentOpacity3D("Background", 0.5) + + ################################## + self.delayDisplay("Compute statistics") + + segStatLogic = SegmentStatisticsLogic() + segStatLogic.computeStatistics(segmentationNode, masterVolumeNode) + + # Export results to table (just to see all results) + resultsTableNode = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(resultsTableNode) + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + + self.delayDisplay("Check a few numerical results") + self.assertEqual(round(segStatLogic.statistics["Tumor", "LM volume cc"]), 16) + self.assertEqual(round(segStatLogic.statistics["Background", "LM volume cc"]), 3010) + + self.delayDisplay('test_ScriptedSegmentEditorEffectModuleTemplate1 passed') diff --git a/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplateLib/SegmentEditorEffect.py b/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplateLib/SegmentEditorEffect.py index a3b4b54ce48..4dba1b6034b 100644 --- a/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplateLib/SegmentEditorEffect.py +++ b/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplateLib/SegmentEditorEffect.py @@ -10,123 +10,123 @@ class SegmentEditorEffect(AbstractScriptedSegmentEditorEffect): - """This effect uses Watershed algorithm to partition the input volume""" - - def __init__(self, scriptedEffect): - scriptedEffect.name = 'ScriptedSegmentEditorEffectModuleTemplate' - scriptedEffect.perSegment = False # this effect operates on all segments at once (not on a single selected segment) - scriptedEffect.requireSegments = True # this effect requires segment(s) existing in the segmentation - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) - - def clone(self): - # It should not be necessary to modify this method - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - # It should not be necessary to modify this method - iconPath = os.path.join(os.path.dirname(__file__), 'SegmentEditorEffect.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Existing segments are grown to fill the image. + """This effect uses Watershed algorithm to partition the input volume""" + + def __init__(self, scriptedEffect): + scriptedEffect.name = 'ScriptedSegmentEditorEffectModuleTemplate' + scriptedEffect.perSegment = False # this effect operates on all segments at once (not on a single selected segment) + scriptedEffect.requireSegments = True # this effect requires segment(s) existing in the segmentation + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + + def clone(self): + # It should not be necessary to modify this method + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + # It should not be necessary to modify this method + iconPath = os.path.join(os.path.dirname(__file__), 'SegmentEditorEffect.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Existing segments are grown to fill the image. The effect is different from the Grow from seeds effect in that smoothness of structures can be defined, which can prevent leakage. To segment a single object, create a segment and paint inside and create another segment and paint outside on each axis. """ - def setupOptionsFrame(self): - - # Object scale slider - self.objectScaleMmSlider = slicer.qMRMLSliderWidget() - self.objectScaleMmSlider.setMRMLScene(slicer.mrmlScene) - self.objectScaleMmSlider.quantity = "length" # get unit, precision, etc. from MRML unit node - self.objectScaleMmSlider.minimum = 0 - self.objectScaleMmSlider.maximum = 10 - self.objectScaleMmSlider.value = 2.0 - self.objectScaleMmSlider.setToolTip('Increasing this value smooths the segmentation and reduces leaks. This is the sigma used for edge detection.') - self.scriptedEffect.addLabeledOptionsWidget("Object scale:", self.objectScaleMmSlider) - self.objectScaleMmSlider.connect('valueChanged(double)', self.updateMRMLFromGUI) - - # Apply button - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - self.applyButton.setToolTip("Accept previewed result") - self.scriptedEffect.addOptionsWidget(self.applyButton) - self.applyButton.connect('clicked()', self.onApply) - - def createCursor(self, widget): - # Turn off effect-specific cursor for this effect - return slicer.util.mainWindow().cursor - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("ObjectScaleMm", 2.0) - - def updateGUIFromMRML(self): - objectScaleMm = self.scriptedEffect.doubleParameter("ObjectScaleMm") - wasBlocked = self.objectScaleMmSlider.blockSignals(True) - self.objectScaleMmSlider.value = abs(objectScaleMm) - self.objectScaleMmSlider.blockSignals(wasBlocked) - - def updateMRMLFromGUI(self): - self.scriptedEffect.setParameter("ObjectScaleMm", self.objectScaleMmSlider.value) - - def onApply(self): - - # Get list of visible segment IDs, as the effect ignores hidden segments. - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - visibleSegmentIds = vtk.vtkStringArray() - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) - if visibleSegmentIds.GetNumberOfValues() == 0: - logging.info("Smoothing operation skipped: there are no visible segments") - return - - # This can be a long operation - indicate it to the user - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - - # Allow users revert to this state by clicking Undo - self.scriptedEffect.saveStateForUndo() - - # Export master image data to temporary new volume node. - # Note: Although the original master volume node is already in the scene, we do not use it here, - # because the master volume may have been resampled to match segmentation geometry. - masterVolumeNode = slicer.vtkMRMLScalarVolumeNode() - slicer.mrmlScene.AddNode(masterVolumeNode) - masterVolumeNode.SetAndObserveTransformNodeID(segmentationNode.GetTransformNodeID()) - slicer.vtkSlicerSegmentationsModuleLogic.CopyOrientedImageDataToVolumeNode(self.scriptedEffect.masterVolumeImageData(), masterVolumeNode) - # Generate merged labelmap of all visible segments, as the filter expects a single labelmap with all the labels. - mergedLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() - slicer.mrmlScene.AddNode(mergedLabelmapNode) - slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, visibleSegmentIds, mergedLabelmapNode, masterVolumeNode) - - # Run segmentation algorithm - import SimpleITK as sitk - import sitkUtils - # Read input data from Slicer into SimpleITK - labelImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName())) - backgroundImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(masterVolumeNode.GetName())) - # Run watershed filter - featureImage = sitk.GradientMagnitudeRecursiveGaussian(backgroundImage, float(self.scriptedEffect.doubleParameter("ObjectScaleMm"))) - del backgroundImage - f = sitk.MorphologicalWatershedFromMarkersImageFilter() - f.SetMarkWatershedLine(False) - f.SetFullyConnected(False) - labelImage = f.Execute(featureImage, labelImage) - del featureImage - # Pixel type of watershed output is the same as the input. Convert it to int16 now. - if labelImage.GetPixelID() != sitk.sitkInt16: - labelImage = sitk.Cast(labelImage, sitk.sitkInt16) - # Write result from SimpleITK to Slicer. This currently performs a deep copy of the bulk data. - sitk.WriteImage(labelImage, sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName())) - mergedLabelmapNode.GetImageData().Modified() - mergedLabelmapNode.Modified() - - # Update segmentation from labelmap node and remove temporary nodes - slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(mergedLabelmapNode, segmentationNode, visibleSegmentIds) - slicer.mrmlScene.RemoveNode(masterVolumeNode) - slicer.mrmlScene.RemoveNode(mergedLabelmapNode) - - qt.QApplication.restoreOverrideCursor() + def setupOptionsFrame(self): + + # Object scale slider + self.objectScaleMmSlider = slicer.qMRMLSliderWidget() + self.objectScaleMmSlider.setMRMLScene(slicer.mrmlScene) + self.objectScaleMmSlider.quantity = "length" # get unit, precision, etc. from MRML unit node + self.objectScaleMmSlider.minimum = 0 + self.objectScaleMmSlider.maximum = 10 + self.objectScaleMmSlider.value = 2.0 + self.objectScaleMmSlider.setToolTip('Increasing this value smooths the segmentation and reduces leaks. This is the sigma used for edge detection.') + self.scriptedEffect.addLabeledOptionsWidget("Object scale:", self.objectScaleMmSlider) + self.objectScaleMmSlider.connect('valueChanged(double)', self.updateMRMLFromGUI) + + # Apply button + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + self.applyButton.setToolTip("Accept previewed result") + self.scriptedEffect.addOptionsWidget(self.applyButton) + self.applyButton.connect('clicked()', self.onApply) + + def createCursor(self, widget): + # Turn off effect-specific cursor for this effect + return slicer.util.mainWindow().cursor + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("ObjectScaleMm", 2.0) + + def updateGUIFromMRML(self): + objectScaleMm = self.scriptedEffect.doubleParameter("ObjectScaleMm") + wasBlocked = self.objectScaleMmSlider.blockSignals(True) + self.objectScaleMmSlider.value = abs(objectScaleMm) + self.objectScaleMmSlider.blockSignals(wasBlocked) + + def updateMRMLFromGUI(self): + self.scriptedEffect.setParameter("ObjectScaleMm", self.objectScaleMmSlider.value) + + def onApply(self): + + # Get list of visible segment IDs, as the effect ignores hidden segments. + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + visibleSegmentIds = vtk.vtkStringArray() + segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) + if visibleSegmentIds.GetNumberOfValues() == 0: + logging.info("Smoothing operation skipped: there are no visible segments") + return + + # This can be a long operation - indicate it to the user + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + + # Allow users revert to this state by clicking Undo + self.scriptedEffect.saveStateForUndo() + + # Export master image data to temporary new volume node. + # Note: Although the original master volume node is already in the scene, we do not use it here, + # because the master volume may have been resampled to match segmentation geometry. + masterVolumeNode = slicer.vtkMRMLScalarVolumeNode() + slicer.mrmlScene.AddNode(masterVolumeNode) + masterVolumeNode.SetAndObserveTransformNodeID(segmentationNode.GetTransformNodeID()) + slicer.vtkSlicerSegmentationsModuleLogic.CopyOrientedImageDataToVolumeNode(self.scriptedEffect.masterVolumeImageData(), masterVolumeNode) + # Generate merged labelmap of all visible segments, as the filter expects a single labelmap with all the labels. + mergedLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() + slicer.mrmlScene.AddNode(mergedLabelmapNode) + slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, visibleSegmentIds, mergedLabelmapNode, masterVolumeNode) + + # Run segmentation algorithm + import SimpleITK as sitk + import sitkUtils + # Read input data from Slicer into SimpleITK + labelImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName())) + backgroundImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(masterVolumeNode.GetName())) + # Run watershed filter + featureImage = sitk.GradientMagnitudeRecursiveGaussian(backgroundImage, float(self.scriptedEffect.doubleParameter("ObjectScaleMm"))) + del backgroundImage + f = sitk.MorphologicalWatershedFromMarkersImageFilter() + f.SetMarkWatershedLine(False) + f.SetFullyConnected(False) + labelImage = f.Execute(featureImage, labelImage) + del featureImage + # Pixel type of watershed output is the same as the input. Convert it to int16 now. + if labelImage.GetPixelID() != sitk.sitkInt16: + labelImage = sitk.Cast(labelImage, sitk.sitkInt16) + # Write result from SimpleITK to Slicer. This currently performs a deep copy of the bulk data. + sitk.WriteImage(labelImage, sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName())) + mergedLabelmapNode.GetImageData().Modified() + mergedLabelmapNode.Modified() + + # Update segmentation from labelmap node and remove temporary nodes + slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(mergedLabelmapNode, segmentationNode, visibleSegmentIds) + slicer.mrmlScene.RemoveNode(masterVolumeNode) + slicer.mrmlScene.RemoveNode(mergedLabelmapNode) + + qt.QApplication.restoreOverrideCursor() diff --git a/Libs/MRML/Core/Documentation/generate_default_color_node_property_table.py b/Libs/MRML/Core/Documentation/generate_default_color_node_property_table.py index e73ee8525bf..ea0b52e3f60 100644 --- a/Libs/MRML/Core/Documentation/generate_default_color_node_property_table.py +++ b/Libs/MRML/Core/Documentation/generate_default_color_node_property_table.py @@ -59,8 +59,8 @@ print(template.format(**titles)) # Print separator -print(template.format(**{column_name: '-'*column_width for column_name, column_width in max_row_widths.items()})) +print(template.format(**{column_name: '-' * column_width for column_name, column_width in max_row_widths.items()})) # Print content for row in table: - print(template.format(**row) ) + print(template.format(**row)) diff --git a/Libs/MRML/Core/vtkMRMLModelStorageNode.cxx b/Libs/MRML/Core/vtkMRMLModelStorageNode.cxx index b75f08381a7..d62c5601a9d 100644 --- a/Libs/MRML/Core/vtkMRMLModelStorageNode.cxx +++ b/Libs/MRML/Core/vtkMRMLModelStorageNode.cxx @@ -807,6 +807,11 @@ vtkMRMLModelNode* vtkMRMLModelStorageNode::GetAssociatedDataNode() //---------------------------------------------------------------------------- void vtkMRMLModelStorageNode::ConvertBetweenRASAndLPS(vtkPointSet* inputMesh, vtkPointSet* outputMesh) { + if (!inputMesh || !outputMesh) + { + vtkGenericWarningMacro("vtkMRMLModelStorageNode::ConvertBetweenRASAndLPS: invalid input or output mesh"); + return; + } vtkNew transformRasLps; transformRasLps->Scale(-1, -1, 1); // vtkTransformPolyDataFilter preserves texture coordinates, while vtkTransformFilter removes them, diff --git a/Libs/MRML/Core/vtkMRMLSequenceNode.cxx b/Libs/MRML/Core/vtkMRMLSequenceNode.cxx index 9d9b2f4eb05..72522b3d1b0 100644 --- a/Libs/MRML/Core/vtkMRMLSequenceNode.cxx +++ b/Libs/MRML/Core/vtkMRMLSequenceNode.cxx @@ -245,6 +245,9 @@ void vtkMRMLSequenceNode::Copy(vtkMRMLNode *anode) } this->SequenceScene=vtkMRMLScene::New(); + // Get data node ID in the target scene from the data node ID in the source scene + std::map< std::string, std::string > sourceToTargetDataNodeID; + if (snode->SequenceScene) { for (int n = 0; n < snode->SequenceScene->GetNodes()->GetNumberOfItems(); n++) @@ -255,7 +258,8 @@ void vtkMRMLSequenceNode::Copy(vtkMRMLNode *anode) vtkErrorMacro("Invalid node in vtkMRMLSequenceNode"); continue; } - this->DeepCopyNodeToScene(node, this->SequenceScene); + vtkMRMLNode* targetDataNode = this->DeepCopyNodeToScene(node, this->SequenceScene); + sourceToTargetDataNodeID[node->GetID()] = targetDataNode->GetID(); } } @@ -267,13 +271,15 @@ void vtkMRMLSequenceNode::Copy(vtkMRMLNode *anode) seqItem.DataNode = nullptr; if (sourceIndexIt->DataNode!=nullptr) { - seqItem.DataNode=this->SequenceScene->GetNodeByID(sourceIndexIt->DataNode->GetID()); + std::string targetDataNodeID = sourceToTargetDataNodeID[sourceIndexIt->DataNode->GetID()]; + seqItem.DataNode = this->SequenceScene->GetNodeByID(targetDataNodeID); seqItem.DataNodeID.clear(); } if (seqItem.DataNode==nullptr) { // data node was not found, at least copy its ID - seqItem.DataNodeID=sourceIndexIt->DataNodeID; + std::string targetDataNodeID = sourceToTargetDataNodeID[sourceIndexIt->DataNodeID]; + seqItem.DataNodeID = targetDataNodeID; if (seqItem.DataNodeID.empty()) { vtkWarningMacro("vtkMRMLSequenceNode::Copy: node was not found at index value "<Scene->IsBatchProcessing()) - { - return; - } vtkMRMLNode* dataNode = this->GetItemDataNode(itemID); vtkMRMLDisplayableNode* displayableNode = vtkMRMLDisplayableNode::SafeDownCast(dataNode); diff --git a/Libs/MRML/DisplayableManager/Python/mrmlDisplayableManager/vtkScriptedExampleDisplayableManager.py b/Libs/MRML/DisplayableManager/Python/mrmlDisplayableManager/vtkScriptedExampleDisplayableManager.py index b77919432a0..bb5ce9881ec 100644 --- a/Libs/MRML/DisplayableManager/Python/mrmlDisplayableManager/vtkScriptedExampleDisplayableManager.py +++ b/Libs/MRML/DisplayableManager/Python/mrmlDisplayableManager/vtkScriptedExampleDisplayableManager.py @@ -50,39 +50,39 @@ class vtkScriptedExampleDisplayableManager: - def __init__(self, parent): - self.Parent = parent - print("vtkScriptedExampleDisplayableManager - __init__") + def __init__(self, parent): + self.Parent = parent + print("vtkScriptedExampleDisplayableManager - __init__") - def Create(self): - print("vtkScriptedExampleDisplayableManager - Create") - pass + def Create(self): + print("vtkScriptedExampleDisplayableManager - Create") + pass - def GetMRMLSceneEventsToObserve(self): - print("vtkScriptedExampleDisplayableManager - GetMRMLSceneEventsToObserve") - sceneEvents = vtkIntArray() - sceneEvents.InsertNextValue(slicer.vtkMRMLScene.NodeAddedEvent) - sceneEvents.InsertNextValue(slicer.vtkMRMLScene.NodeRemovedEvent) - return sceneEvents + def GetMRMLSceneEventsToObserve(self): + print("vtkScriptedExampleDisplayableManager - GetMRMLSceneEventsToObserve") + sceneEvents = vtkIntArray() + sceneEvents.InsertNextValue(slicer.vtkMRMLScene.NodeAddedEvent) + sceneEvents.InsertNextValue(slicer.vtkMRMLScene.NodeRemovedEvent) + return sceneEvents - def ProcessMRMLSceneEvents(self, scene, eventid, node): - print("vtkScriptedExampleDisplayableManager - ProcessMRMLSceneEvents(eventid,", eventid, ")") - pass + def ProcessMRMLSceneEvents(self, scene, eventid, node): + print("vtkScriptedExampleDisplayableManager - ProcessMRMLSceneEvents(eventid,", eventid, ")") + pass - def ProcessMRMLNodesEvents(self, scene, eventid, callData): - print("vtkScriptedExampleDisplayableManager - ProcessMRMLNodesEvents(eventid,", eventid, ")") - pass + def ProcessMRMLNodesEvents(self, scene, eventid, callData): + print("vtkScriptedExampleDisplayableManager - ProcessMRMLNodesEvents(eventid,", eventid, ")") + pass - def RemoveMRMLObservers(self): - print("vtkScriptedExampleDisplayableManager - RemoveMRMLObservers") - pass + def RemoveMRMLObservers(self): + print("vtkScriptedExampleDisplayableManager - RemoveMRMLObservers") + pass - def UpdateFromMRML(self): - print("vtkScriptedExampleDisplayableManager - UpdateFromMRML") - pass + def UpdateFromMRML(self): + print("vtkScriptedExampleDisplayableManager - UpdateFromMRML") + pass - def OnInteractorStyleEvent(self, eventid): - print("vtkScriptedExampleDisplayableManager - OnInteractorStyleEvent(eventid,", eventid, ")") + def OnInteractorStyleEvent(self, eventid): + print("vtkScriptedExampleDisplayableManager - OnInteractorStyleEvent(eventid,", eventid, ")") - def OnMRMLDisplayableNodeModifiedEvent(self, viewNode): - print("vtkScriptedExampleDisplayableManager - onMRMLDisplayableNodeModifiedEvent") + def OnMRMLDisplayableNodeModifiedEvent(self, viewNode): + print("vtkScriptedExampleDisplayableManager - onMRMLDisplayableNodeModifiedEvent") diff --git a/Libs/vtkITK/Testing/vtkITKArchetypeDiffusionTensorReaderFile.py b/Libs/vtkITK/Testing/vtkITKArchetypeDiffusionTensorReaderFile.py index 8f5dfa4048b..e8336587967 100644 --- a/Libs/vtkITK/Testing/vtkITKArchetypeDiffusionTensorReaderFile.py +++ b/Libs/vtkITK/Testing/vtkITKArchetypeDiffusionTensorReaderFile.py @@ -1,4 +1,4 @@ -#Testing against the NRRD reader +# Testing against the NRRD reader import unittest import numpy @@ -62,14 +62,14 @@ def test_pointdata(self): self.assertTrue(numpy.allclose(self.nrrdArray, self.itkArray)) def runTest(self): - self.setUp() - self.test_measurement_frame() - self.test_pointdata() - self.test_ras_to_ijk() + self.setUp() + self.test_measurement_frame() + self.test_pointdata() + self.test_ras_to_ijk() def compare_vtk_matrix(m1, m2, n=4): - for i in range(0,n): - for j in range(0,n): - assert m1.GetElement(i,j) == m2.GetElement(i,j) + for i in range(0, n): + for j in range(0, n): + assert m1.GetElement(i, j) == m2.GetElement(i, j) return True diff --git a/Libs/vtkITK/Testing/vtkITKArchetypeScalarReaderFile.py b/Libs/vtkITK/Testing/vtkITKArchetypeScalarReaderFile.py index 747ea357f51..d2b4e3c9cf2 100644 --- a/Libs/vtkITK/Testing/vtkITKArchetypeScalarReaderFile.py +++ b/Libs/vtkITK/Testing/vtkITKArchetypeScalarReaderFile.py @@ -1,4 +1,4 @@ -#Testing against the NRRD reader +# Testing against the NRRD reader import unittest import numpy @@ -47,13 +47,13 @@ def test_pointdata(self): self.assertTrue(numpy.allclose(self.nrrdArray, self.itkArray)) def runTest(self): - self.setUp() - self.test_pointdata() - self.test_ras_to_ijk() + self.setUp() + self.test_pointdata() + self.test_ras_to_ijk() def compare_vtk_matrix(m1, m2, n=4): - for i in range(0,n): - for j in range(0,n): - assert m1.GetElement(i,j) == m2.GetElement(i,j) + for i in range(0, n): + for j in range(0, n): + assert m1.GetElement(i, j) == m2.GetElement(i, j) return True diff --git a/Modules/CLI/PETStandardUptakeValueComputation/CMakeLists.txt b/Modules/CLI/PETStandardUptakeValueComputation/CMakeLists.txt index a3dd3008a96..ab8a22ff21e 100644 --- a/Modules/CLI/PETStandardUptakeValueComputation/CMakeLists.txt +++ b/Modules/CLI/PETStandardUptakeValueComputation/CMakeLists.txt @@ -47,7 +47,6 @@ set(${MODULE_NAME}_TARGET_LIBRARIES #----------------------------------------------------------------------------- SEMMacroBuildCLI( NAME ${MODULE_NAME} - ADDITIONAL_SRCS itkDCMTKFileReader.cxx LOGO_HEADER ${Slicer_SOURCE_DIR}/Resources/NAMICLogo.h # LOGO_HEADER ${Slicer_SOURCE_DIR}/Resources/CTSCLogo.h TARGET_LIBRARIES ${${MODULE_NAME}_TARGET_LIBRARIES} diff --git a/Modules/CLI/PETStandardUptakeValueComputation/Testing/CMakeLists.txt b/Modules/CLI/PETStandardUptakeValueComputation/Testing/CMakeLists.txt index f8b3f6ef325..13d4c170376 100644 --- a/Modules/CLI/PETStandardUptakeValueComputation/Testing/CMakeLists.txt +++ b/Modules/CLI/PETStandardUptakeValueComputation/Testing/CMakeLists.txt @@ -9,7 +9,7 @@ if(NOT DEFINED SEM_DATA_MANAGEMENT_TARGET) endif() #----------------------------------------------------------------------------- -ctk_add_executable_utf8(${CLP}Test ${CLP}Test.cxx ../itkDCMTKFileReader.cxx) +ctk_add_executable_utf8(${CLP}Test ${CLP}Test.cxx) add_dependencies(${CLP}Test ${CLP}) target_link_libraries(${CLP}Test ${CLP}Lib diff --git a/Modules/CLI/PETStandardUptakeValueComputation/itkDCMTKFileReader.cxx b/Modules/CLI/PETStandardUptakeValueComputation/itkDCMTKFileReader.cxx deleted file mode 100644 index 5de62cbd88d..00000000000 --- a/Modules/CLI/PETStandardUptakeValueComputation/itkDCMTKFileReader.cxx +++ /dev/null @@ -1,1211 +0,0 @@ -/*========================================================================= - * - * Copyright Insight Software Consortium - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0.txt - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - *=========================================================================*/ - -// XXX # Workaround bug in packaging of DCMTK 3.6.0 on Debian. -// # See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=637687 -#ifndef HAVE_CONFIG_H -#define HAVE_CONFIG_H -#endif - -#include "itkDCMTKFileReader.h" -#undef HAVE_SSTREAM // 'twould be nice if people coded without using -// incredibly generic macro names -#include "dcmtk/config/osconfig.h" // make sure OS specific configuration is included first - -#define INCLUDE_CSTDIO -#define INCLUDE_CSTRING - -#include -#include "dcmtk/dcmdata/dcdict.h" // For DcmDataDictionary -#include "dcmtk/dcmdata/dcsequen.h" /* for DcmSequenceOfItems */ -#include "dcmtk/dcmdata/dcvrcs.h" /* for DcmCodeString */ -#include "dcmtk/dcmdata/dcvrfd.h" /* for DcmFloatingPointDouble */ -#include "dcmtk/dcmdata/dcvrfl.h" /* for DcmFloatingPointDouble */ -#include "dcmtk/dcmdata/dcvrus.h" /* for DcmUnsignedShort */ -#include "dcmtk/dcmdata/dcvris.h" /* for DcmIntegerString */ -#include "dcmtk/dcmdata/dcvrobow.h" /* for DcmOtherByteOtherWord */ -#include "dcmtk/dcmdata/dcvrui.h" /* for DcmUniqueIdentifier */ -#include "dcmtk/dcmdata/dcfilefo.h" /* for DcmFileFormat */ -#include "dcmtk/dcmdata/dcdeftag.h" /* for DCM_NumberOfFrames */ -#include "dcmtk/dcmdata/dcvrlo.h" /* for DcmLongString */ -#include "dcmtk/dcmdata/dcvrtm.h" /* for DCMTime */ -#include "dcmtk/dcmdata/dcvrda.h" /* for DcmDate */ -#include "dcmtk/dcmdata/dcvrpn.h" /* for DcmPersonName */ -// #include "diregist.h" /* include to support color images */ -#include "vnl/vnl_cross.h" - - -namespace itk -{ - -void -DCMTKSequence -::SetDcmSequenceOfItems(DcmSequenceOfItems *seq) -{ - this->m_DcmSequenceOfItems = seq; -} -int -DCMTKSequence -::card() -{ - return this->m_DcmSequenceOfItems->card(); -} - -int -DCMTKSequence -::GetSequence(unsigned long index, - DCMTKSequence &target, - bool throwException) -{ - DcmItem *item = this->m_DcmSequenceOfItems->getItem(index); - DcmSequenceOfItems *sequence = - dynamic_cast(item); - if(sequence == nullptr) - { - DCMTKException(<< "Can't find DCMTKSequence at index " << index); - } - target.SetDcmSequenceOfItems(sequence); - return EXIT_SUCCESS; -} -int -DCMTKSequence -::GetStack(unsigned short group, - unsigned short element, - DcmStack *resultStack, bool throwException) -{ - DcmTagKey tagkey(group,element); - if(this->m_DcmSequenceOfItems->search(tagkey,*resultStack) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex << group << " " - << element << std::dec); - } - return EXIT_SUCCESS; -} - -int -DCMTKSequence -::GetElementCS(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - DcmStack resultStack; - this->GetStack(group,element,&resultStack); - DcmCodeString *codeStringElement = dynamic_cast(resultStack.top()); - if(codeStringElement == nullptr) - { - DCMTKException(<< "Can't get CodeString Element at tag " - << std::hex << group << " " - << element << std::dec); - } - OFString ofString; - if(codeStringElement->getOFStringArray(ofString) != EC_Normal) - { - DCMTKException(<< "Can't get OFString Value at tag " - << std::hex << group << " " - << element << std::dec); - } - target = ""; - for(unsigned j = 0; j < ofString.length(); ++j) - { - target += ofString[j]; - } - return EXIT_SUCCESS; -} - -int -DCMTKSequence:: -GetElementFD(unsigned short group, - unsigned short element, - double * &target, - bool throwException) -{ - DcmStack resultStack; - this->GetStack(group,element,&resultStack); - DcmFloatingPointDouble *fdItem = dynamic_cast(resultStack.top()); - if(fdItem == nullptr) - { - DCMTKException(<< "Can't get CodeString Element at tag " - << std::hex << group << " " - << element << std::dec); - } - if(fdItem->getFloat64Array(target) != EC_Normal) - { - DCMTKException(<< "Can't get floatarray Value at tag " - << std::hex << group << " " - << element << std::dec); - } - return EXIT_SUCCESS; -} - -int -DCMTKSequence -::GetElementFD(unsigned short group, - unsigned short element, - double &target, - bool throwException) -{ - double *array; - this->GetElementFD(group,element,array,throwException); - target = array[0]; - return EXIT_SUCCESS; -} - -int -DCMTKSequence -::GetElementDS(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - DcmStack resultStack; - this->GetStack(group,element,&resultStack); - DcmDecimalString *decimalStringElement = dynamic_cast(resultStack.top()); - if(decimalStringElement == nullptr) - { - DCMTKException(<< "Can't get DecimalString Element at tag " - << std::hex << group << " " - << element << std::dec); - } - OFString ofString; - if(decimalStringElement->getOFStringArray(ofString) != EC_Normal) - { - DCMTKException(<< "Can't get DecimalString Value at tag " - << std::hex << group << " " - << element << std::dec); - } - target = ""; - for(unsigned j = 0; j < ofString.length(); ++j) - { - target += ofString[j]; - } - return EXIT_SUCCESS; -} - -int -DCMTKSequence -::GetElementSQ(unsigned short group, - unsigned short element, - DCMTKSequence &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmStack resultStack; - this->GetStack(group,element,&resultStack); - - DcmSequenceOfItems *seqElement = dynamic_cast(resultStack.top()); - if(seqElement == nullptr) - { - DCMTKException(<< "Can't get at tag " - << std::hex << group << " " - << element << std::dec); - } - target.SetDcmSequenceOfItems(seqElement); - return EXIT_SUCCESS; -} - -int -DCMTKSequence -::GetElementTM(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmStack resultStack; - this->GetStack(group,element,&resultStack); - - DcmTime *dcmTime = dynamic_cast(resultStack.top()); - if(dcmTime == nullptr) - { - DCMTKException(<< "Can't get at tag " - << std::hex << group << " " - << element << std::dec); - } - char *cs; - dcmTime->getString(cs); - target = cs; - return EXIT_SUCCESS; -} - -DCMTKFileReader -::~DCMTKFileReader() -{ - - delete m_DFile; -} - -void -DCMTKFileReader -::SetFileName(const std::string &fileName) -{ - this->m_FileName = fileName; -} - -const std::string & -DCMTKFileReader -::GetFileName() const -{ - return this->m_FileName; -} - -void -DCMTKFileReader -::LoadFile() -{ - if(this->m_FileName == "") - { - itkGenericExceptionMacro(<< "No filename given" ); - } - if(this->m_DFile != nullptr) - { - delete this->m_DFile; - } - this->m_DFile = new DcmFileFormat(); - OFCondition cond = this->m_DFile->loadFile(this->m_FileName.c_str()); - // /* transfer syntax, autodetect */ - // EXS_Unknown, - // /* group length */ - // EGL_noChange, - // /* Max read length */ - // 1024, // should be big - // // enough for - // // header stuff but - // // prevent reading - // // image data. - // /* file read mode */ - // ERM_fileOnly); - if(cond != EC_Normal) - { - itkGenericExceptionMacro(<< cond.text() << ": reading file " << this->m_FileName); - } - this->m_Dataset = this->m_DFile->getDataset(); - this->m_Xfer = this->m_Dataset->getOriginalXfer(); - if(this->m_Dataset->findAndGetSint32(DCM_NumberOfFrames,this->m_FrameCount).bad()) - { - this->m_FrameCount = 1; - } - int fnum; - this->GetElementIS(0x0020,0x0013,fnum); - this->m_FileNumber = fnum; -} - -int -DCMTKFileReader -::GetElementLO(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmLongString *loItem = dynamic_cast(el); - if(loItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - OFString ofString; - if(loItem->getOFStringArray(ofString) != EC_Normal) - { - DCMTKException(<< "Can't get string from element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - target = ""; - for(unsigned i = 0; i < ofString.size(); i++) - { - target += ofString[i]; - } - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetElementLO(unsigned short group, - unsigned short element, - std::vector &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmLongString *loItem = dynamic_cast(el); - if(loItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - target.clear(); - OFString ofString; - for(unsigned long i = 0; loItem->getOFString(ofString,i) == EC_Normal; ++i) - { - std::string targetStr = ""; - for(unsigned j = 0; j < ofString.size(); j++) - { - targetStr += ofString[j]; - } - target.push_back(targetStr); - } - return EXIT_SUCCESS; -} - -/** Get a DecimalString Item as a single string - */ -int -DCMTKFileReader -::GetElementDS(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmDecimalString *dsItem = dynamic_cast(el); - if(dsItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - OFString ofString; - if(dsItem->getOFStringArray(ofString) != EC_Normal) - { - DCMTKException(<< "Can't get DecimalString Value at tag " - << std::hex << group << " " - << element << std::dec); - } - target = ""; - for(unsigned j = 0; j < ofString.length(); ++j) - { - target += ofString[j]; - } - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetElementFD(unsigned short group, - unsigned short element, - double &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmFloatingPointDouble *fdItem = dynamic_cast(el); - if(fdItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - if(fdItem->getFloat64(target) != EC_Normal) - { - DCMTKException(<< "Can't extract Array from DecimalString " << std::hex - << group << " " << std::hex - << element << std::dec); - } - return EXIT_SUCCESS; -} -int -DCMTKFileReader -::GetElementFD(unsigned short group, - unsigned short element, - double * &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmFloatingPointDouble *fdItem = dynamic_cast(el); - if(fdItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - if(fdItem->getFloat64Array(target) != EC_Normal) - { - DCMTKException(<< "Can't extract Array from DecimalString " << std::hex - << group << " " << std::hex - << element << std::dec); - } - return EXIT_SUCCESS; -} -int -DCMTKFileReader -::GetElementFL(unsigned short group, - unsigned short element, - float &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmFloatingPointSingle *flItem = dynamic_cast(el); - if(flItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - if(flItem->getFloat32(target) != EC_Normal) - { - DCMTKException(<< "Can't extract Array from DecimalString " << std::hex - << group << " " << std::hex - << element << std::dec); - } - return EXIT_SUCCESS; -} -int -DCMTKFileReader -::GetElementFLorOB(unsigned short group, - unsigned short element, - float &target, - bool throwException) -{ - if(this->GetElementFL(group,element,target,false) == EXIT_SUCCESS) - { - return EXIT_SUCCESS; - } - std::string val; - if(this->GetElementOB(group,element,val) != EXIT_SUCCESS) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - const char *data = val.c_str(); - const float *fptr = reinterpret_cast(data); - target = *fptr; - switch(this->GetTransferSyntax()) - { - case EXS_LittleEndianImplicit: - case EXS_LittleEndianExplicit: - itk::ByteSwapper::SwapFromSystemToLittleEndian(&target); - break; - case EXS_BigEndianImplicit: - case EXS_BigEndianExplicit: - itk::ByteSwapper::SwapFromSystemToBigEndian(&target); - break; - default: - break; - } - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetElementUS(unsigned short group, - unsigned short element, - unsigned short &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmUnsignedShort *usItem = dynamic_cast(el); - if(usItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - if(usItem->getUint16(target) != EC_Normal) - { - DCMTKException(<< "Can't extract Array from DecimalString " << std::hex - << group << " " << std::hex - << element << std::dec); - } - return EXIT_SUCCESS; -} -int -DCMTKFileReader -::GetElementUS(unsigned short group, - unsigned short element, - unsigned short *&target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmUnsignedShort *usItem = dynamic_cast(el); - if(usItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - if(usItem->getUint16Array(target) != EC_Normal) - { - DCMTKException(<< "Can't extract Array from DecimalString " << std::hex - << group << " " << std::hex - << element << std::dec); - } - return EXIT_SUCCESS; -} -/** Get a DecimalString Item as a single string - */ -int -DCMTKFileReader -::GetElementCS(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmCodeString *csItem = dynamic_cast(el); - if(csItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - OFString ofString; - if(csItem->getOFStringArray(ofString) != EC_Normal) - { - DCMTKException(<< "Can't get DecimalString Value at tag " - << std::hex << group << " " - << element << std::dec); - } - target = ""; - for(unsigned j = 0; j < ofString.length(); ++j) - { - target += ofString[j]; - } - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetElementPN(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmPersonName *pnItem = dynamic_cast(el); - if(pnItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - OFString ofString; - if(pnItem->getOFStringArray(ofString) != EC_Normal) - { - DCMTKException(<< "Can't get DecimalString Value at tag " - << std::hex << group << " " - << element << std::dec); - } - target = ""; - for(unsigned j = 0; j < ofString.length(); ++j) - { - target += ofString[j]; - } - return EXIT_SUCCESS; -} - -/** get an IS (Integer String Item - */ -int -DCMTKFileReader -::GetElementIS(unsigned short group, - unsigned short element, - ::itk::int32_t &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmIntegerString *isItem = dynamic_cast(el); - if(isItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - Sint32 _target; // MSVC seems to have type conversion problems with - // using int32_t as a an argument to getSint32 - if(isItem->getSint32(_target) != EC_Normal) - { - DCMTKException(<< "Can't get DecimalString Value at tag " - << std::hex << group << " " - << element << std::dec); - } - target = static_cast< ::itk::int32_t>(_target); - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetElementISorOB(unsigned short group, - unsigned short element, - ::itk::int32_t &target, - bool throwException) -{ - if(this->GetElementIS(group,element,target,false) == EXIT_SUCCESS) - { - return EXIT_SUCCESS; - } - std::string val; - if(this->GetElementOB(group,element,val,throwException) != EXIT_SUCCESS) - { - return EXIT_FAILURE; - } - const char *data = val.c_str(); - const int *iptr = reinterpret_cast(data); - target = *iptr; - switch(this->GetTransferSyntax()) - { - case EXS_LittleEndianImplicit: - case EXS_LittleEndianExplicit: - itk::ByteSwapper::SwapFromSystemToLittleEndian(&target); - break; - case EXS_BigEndianImplicit: - case EXS_BigEndianExplicit: - itk::ByteSwapper::SwapFromSystemToBigEndian(&target); - break; - default: // no idea what to do - break; - } - - return EXIT_SUCCESS; -} - -/** get an OB OtherByte Item - */ -int -DCMTKFileReader -::GetElementOB(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmOtherByteOtherWord *obItem = dynamic_cast(el); - if(obItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - OFString ofString; - if(obItem->getOFStringArray(ofString) != EC_Normal) - { - DCMTKException(<< "Can't get OFString Value at tag " - << std::hex << group << " " - << element << std::dec); - } - target = Self::ConvertFromOB(ofString); - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetElementSQ(unsigned short group, - unsigned short entry, - DCMTKSequence &sequence, - bool throwException) -{ - DcmSequenceOfItems *seq; - DcmTagKey tagKey(group,entry); - - if(this->m_Dataset->findAndGetSequence(tagKey,seq) != EC_Normal) - { - DCMTKException(<< "Can't find sequence " - << std::hex << group << " " - << std::hex << entry) - } - sequence.SetDcmSequenceOfItems(seq); - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetElementUI(unsigned short group, - unsigned short entry, - std::string &target, - bool throwException) -{ - DcmTagKey tagKey(group,entry); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagKey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << entry << std::dec); - } - DcmUniqueIdentifier *uiItem = dynamic_cast(el); - if(uiItem == nullptr) - { - DCMTKException(<< "Can't convert data item " << group - << "," << entry); - } - OFString ofString; - if(uiItem->getOFStringArray(ofString,false) != EC_Normal) - { - DCMTKException(<< "Can't get UID Value at tag " - << std::hex << group << " " << std::hex - << entry << std::dec); - } - target = ""; - for(unsigned int j = 0; j < ofString.length(); ++j) - { - target.push_back(ofString[j]); - } - return EXIT_SUCCESS; -} - -int DCMTKFileReader:: -GetElementDA(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmDate *dcmDate = dynamic_cast(el); - if(dcmDate == nullptr) - { - DCMTKException(<< "Can't get at tag " - << std::hex << group << " " - << element << std::dec); - } - char *cs; - dcmDate->getString(cs); - target = cs; - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetElementTM(unsigned short group, - unsigned short element, - std::string &target, - bool throwException) -{ - - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmTime *dcmTime = dynamic_cast(el); - if(dcmTime == nullptr) - { - DCMTKException(<< "Can't get at tag " - << std::hex << group << " " - << element << std::dec); - } - char *cs; - dcmTime->getString(cs); - target = cs; - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetDirCosines(vnl_vector &dir1, - vnl_vector &dir2, - vnl_vector &dir3) -{ - double dircos[6]; - DCMTKSequence planeSeq; - if(this->GetElementDS(0x0020,0x0037,6,dircos,false) != EXIT_SUCCESS) - { - if(this->GetElementSQ(0x0020,0x9116,planeSeq,false) == EXIT_SUCCESS) - { - if(planeSeq.GetElementDS(0x0020,0x0037,6,dircos,false) != EXIT_SUCCESS) - { - return EXIT_FAILURE; - } - } - } - dir1[0] = dircos[0]; dir1[1] = dircos[1]; dir1[2] = dircos[2]; - dir2[0] = dircos[3]; dir2[1] = dircos[4]; dir2[2] = dircos[5]; - dir3 = vnl_cross_3d(dir1,dir2); - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetSlopeIntercept(double &slope, double &intercept) -{ - if(this->GetElementDS(0x0028,1053,1,&slope,false) != EXIT_SUCCESS) - { - slope = 1.0; - } - if(this->GetElementDS(0x0028,1052,1,&intercept,false) != EXIT_SUCCESS) - { - intercept = 0.0; - } - return EXIT_SUCCESS; -} - -ImageIOBase::IOPixelType -DCMTKFileReader -::GetImagePixelType() -{ - unsigned short SamplesPerPixel; - if(this->GetElementUS(0x0028,0x0100,SamplesPerPixel,false) != EXIT_SUCCESS) - { - return ImageIOBase::UNKNOWNPIXELTYPE; - } - ImageIOBase::IOPixelType pixelType; - switch(SamplesPerPixel) - { - case 8: - case 16: - pixelType = ImageIOBase::SCALAR; - break; - case 24: - pixelType = ImageIOBase::RGB; - break; - default: - pixelType = ImageIOBase::VECTOR; - } - return pixelType; -} - -ImageIOBase::IOComponentType -DCMTKFileReader -::GetImageDataType() -{ - unsigned short IsSigned; - unsigned short BitsAllocated; - unsigned short BitsStored; - unsigned short HighBit; - ImageIOBase::IOComponentType type = - ImageIOBase::UNKNOWNCOMPONENTTYPE; - - if(this->GetElementUS(0x0028,0x0100,BitsAllocated,false) != EXIT_SUCCESS || - this->GetElementUS(0x0028,0x0101,BitsStored,false) != EXIT_SUCCESS || - this->GetElementUS(0x0028,0x0102,HighBit,false) != EXIT_SUCCESS || - this->GetElementUS(0x0028,0x0103,IsSigned,false) != EXIT_SUCCESS) - { - return type; - } - double slope, intercept; - this->GetSlopeIntercept(slope,intercept); - - switch( BitsAllocated ) - { - case 1: - case 8: - case 24: // RGB? - if(IsSigned) - { - type = ImageIOBase::CHAR; - } - else - { - type = ImageIOBase::UCHAR; - } - break; - case 12: - case 16: - if(IsSigned) - { - type = ImageIOBase::SHORT; - } - else - { - type = ImageIOBase::USHORT; - } - break; - case 32: - case 64: // Don't know what this actually means - if(IsSigned) - { - type = ImageIOBase::LONG; - } - else - { - type = ImageIOBase::ULONG; - } - break; - case 0: - default: - break; - //assert(0); - } - - return type; -} - - -int -DCMTKFileReader -::GetDimensions(unsigned short &rows, unsigned short &columns) -{ - if(this->GetElementUS(0x0028,0x0010,rows,false) != EXIT_SUCCESS || - this->GetElementUS(0x0028,0x0011,columns,false) != EXIT_SUCCESS) - { - return EXIT_FAILURE; - } - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetSpacing(double *spacing) -{ - double _spacing[3]; - // - // There are several tags that can have spacing, and we're going - // from most to least desirable, starting with PixelSpacing, which - // is guaranteed to be in patient space. - // Imager Pixel spacing is inter-pixel spacing at the sensor front plane - // Pixel spacing - if((this->GetElementDS(0x0028,0x0030,2,_spacing,false) != EXIT_SUCCESS && - // imager pixel spacing - this->GetElementDS(0x0018, 0x1164, 2, &_spacing[0],false) != EXIT_SUCCESS && - // Nominal Scanned PixelSpacing - this->GetElementDS(0x0018, 0x2010, 2, &_spacing[0],false) != EXIT_SUCCESS) || - // slice thickness - this->GetElementDS(0x0018,0x0050,1,&_spacing[2],false) != EXIT_SUCCESS) - { - // that method failed, go looking for the spacing sequence - DCMTKSequence spacingSequence; - DCMTKSequence subSequence; - DCMTKSequence subsubSequence; - // first, shared function groups sequence, then - // per-frame groups sequence - if((this->GetElementSQ(0x5200,0x9229,spacingSequence,false) == EXIT_SUCCESS || - this->GetElementSQ(0X5200,0X9230,spacingSequence,false) == EXIT_SUCCESS) && - spacingSequence.GetSequence(0,subSequence,false) == EXIT_SUCCESS && - subSequence.GetElementSQ(0x0028,0x9110,subsubSequence,false) == EXIT_SUCCESS) - { - if(subsubSequence.GetElementDS(0x0028,0x0030,2,_spacing,false) != EXIT_SUCCESS) - { - // Pixel Spacing - _spacing[0] = _spacing[1] = 1.0; - } - if(subsubSequence.GetElementDS(0x0018,0x0050,1,&_spacing[2],false) != EXIT_SUCCESS) - { - // Slice Thickness - _spacing[2] = 1.0; - } - } - else - { - // punt if no info found. - _spacing[0] = _spacing[1] = _spacing[2] = 1.0; - } - } - // - // spacing is row spacing\column spacing - // but a slice is width-first, i.e. columns increase fastest. - // - spacing[0] = _spacing[1]; - spacing[1] = _spacing[0]; - spacing[2] = _spacing[2]; - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetOrigin(double *origin) -{ - DCMTKSequence originSequence; - DCMTKSequence subSequence; - DCMTKSequence subsubSequence; - - if((this->GetElementSQ(0x5200,0x9229,originSequence,false) == EXIT_SUCCESS || - this->GetElementSQ(0X5200,0X9239,originSequence,false) == EXIT_SUCCESS) && - originSequence.GetSequence(0,subSequence,false) == EXIT_SUCCESS && - subSequence.GetElementSQ(0x0028,0x9113,subsubSequence,false) == EXIT_SUCCESS) - { - subsubSequence.GetElementDS(0x0020,0x0032,3,origin,true); - return EXIT_SUCCESS; - } - this->GetElementDS(0x0020,0x0032,3,origin,true); - return EXIT_SUCCESS; -} - -int -DCMTKFileReader -::GetFrameCount() const -{ - return this->m_FrameCount; -} - -E_TransferSyntax -DCMTKFileReader -::GetTransferSyntax() const -{ - return m_Xfer; -} - -long -DCMTKFileReader -::GetFileNumber() const -{ - return m_FileNumber; -} - -void -DCMTKFileReader -::AddDictEntry(DcmDictEntry *entry) -{ - DcmDataDictionary &dict = dcmDataDict.wrlock(); - dict.addEntry(entry); -#if OFFIS_DCMTK_VERSION_NUMBER < 364 - dcmDataDict.unlock(); -#else - dcmDataDict.rdunlock(); -#endif -} - -unsigned -DCMTKFileReader -::ascii2hex(char c) -{ - switch(c) - { - case '0': return 0; - case '1': return 1; - case '2': return 2; - case '3': return 3; - case '4': return 4; - case '5': return 5; - case '6': return 6; - case '7': return 7; - case '8': return 8; - case '9': return 9; - case 'a': - case 'A': return 10; - case 'b': - case 'B': return 11; - case 'c': - case 'C': return 12; - case 'd': - case 'D': return 13; - case 'e': - case 'E': return 14; - case 'f': - case 'F': return 15; - } - return 255; // should never happen -} - -std::string -DCMTKFileReader -::ConvertFromOB(OFString &toConvert) -{ - // string format is nn\nn\nn... - std::string rval; - for(size_t pos = 0; pos < toConvert.size(); pos += 3) - { - unsigned char convert[2]; - convert[0] = Self::ascii2hex(toConvert[pos]); - convert[1] = Self::ascii2hex(toConvert[pos+1]); - unsigned char conv = convert[0] << 4; - conv += convert[1]; - rval.push_back(static_cast(conv)); - } - return rval; -} - -bool CompareDCMTKFileReaders(DCMTKFileReader *a, DCMTKFileReader *b) -{ - return a->GetFileNumber() < b->GetFileNumber(); -} - -} diff --git a/Modules/CLI/PETStandardUptakeValueComputation/itkDCMTKFileReader.h b/Modules/CLI/PETStandardUptakeValueComputation/itkDCMTKFileReader.h deleted file mode 100644 index 2cb3964e645..00000000000 --- a/Modules/CLI/PETStandardUptakeValueComputation/itkDCMTKFileReader.h +++ /dev/null @@ -1,371 +0,0 @@ -/*========================================================================= - * - * Copyright Insight Software Consortium - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0.txt - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - *=========================================================================*/ -#ifndef itkDCMTKFileReader_h -#define itkDCMTKFileReader_h - -// XXX # Workaround bug in packaging of DCMTK 3.6.0 on Debian. -// # See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=637687 -#ifndef HAVE_CONFIG_H -#define HAVE_CONFIG_H -#endif - -#include -#include -#include "itkByteSwapper.h" -#include "itkIntTypes.h" -#include "vnl/vnl_vector.h" -#include "dcmtk/dcmdata/dcxfer.h" -#include "dcmtk/dcmdata/dcvrds.h" -#include "dcmtk/dcmdata/dcstack.h" -#include "dcmtk/dcmdata/dcdatset.h" -#include "itkMacro.h" -#include "itkImageIOBase.h" - -class DcmSequenceOfItems; -class DcmFileFormat; -class DcmDictEntry; - -// Don't print error messages if you're not throwing -// an exception -// std::cerr body; -#define DCMTKException(body) \ - { \ - if(throwException) \ - { \ - itkGenericExceptionMacro(body); \ - } \ - else \ - { \ - return EXIT_FAILURE; \ - } \ - } - -namespace itk -{ -class DCMTKSequence -{ -public: - DCMTKSequence() = default; - void SetDcmSequenceOfItems(DcmSequenceOfItems *seq); - int card(); - int GetSequence(unsigned long index, - DCMTKSequence &target,bool throwException = true); - int GetStack(unsigned short group, - unsigned short element, - DcmStack *resultStack, bool throwException = true); - int GetElementCS(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - int GetElementFD(unsigned short group, - unsigned short element, - double * &target, - bool throwException = true); - int GetElementFD(unsigned short group, - unsigned short element, - double &target, - bool throwException = true); - int GetElementDS(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - int GetElementTM(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - /** Get an array of data values, as contained in a DICOM - * DecimalString Item - */ - template - int GetElementDS(unsigned short group, - unsigned short element, - unsigned short count, - TType *target, - bool throwException) - { - DcmStack resultStack; - this->GetStack(group,element,&resultStack); - DcmDecimalString *dsItem = - dynamic_cast(resultStack.top()); - if(dsItem == nullptr) - { - DCMTKException(<< "Can't get DecimalString Element at tag " - << std::hex << group << " " - << element << std::dec); - } - - OFVector doubleVals; - if(dsItem->getFloat64Vector(doubleVals) != EC_Normal) - { - DCMTKException(<< "Can't extract Array from DecimalString " << std::hex - << group << " " << std::hex - << element << std::dec); - } - if(doubleVals.size() != count) - { - DCMTKException(<< "DecimalString " << std::hex - << group << " " << std::hex - << element << " expected " - << count << "items, but found " - << doubleVals.size() << std::dec); - - } - for(unsigned i = 0; i < count; i++) - { - target[i] = static_cast(doubleVals[i]); - } - return EXIT_SUCCESS; - } - int GetElementSQ(unsigned short group, - unsigned short element, - DCMTKSequence &target, - bool throwException = true); -private: - DcmSequenceOfItems *m_DcmSequenceOfItems{nullptr}; -}; - -class DCMTKFileReader -{ -public: - typedef DCMTKFileReader Self; - - DCMTKFileReader() = default; - ~DCMTKFileReader(); - - void SetFileName(const std::string &fileName); - - const std::string &GetFileName() const; - - void LoadFile(); - - int GetElementLO(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - int GetElementLO(unsigned short group, - unsigned short element, - std::vector &target, - bool throwException = true); - - /** Get an array of data values, as contained in a DICOM - * DecimalString Item - */ - template - int GetElementDS(unsigned short group, - unsigned short element, - unsigned short count, - TType *target, - bool throwException = true) - { - DcmTagKey tagkey(group,element); - DcmElement *el; - if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal) - { - DCMTKException(<< "Can't find tag " << std::hex - << group << " " << std::hex - << element << std::dec); - } - DcmDecimalString *dsItem = dynamic_cast(el); - if(dsItem == nullptr) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - OFVector doubleVals; - if(dsItem->getFloat64Vector(doubleVals) != EC_Normal) - { - DCMTKException(<< "Can't extract Array from DecimalString " << std::hex - << group << " " << std::hex - << element << std::dec); - } - if(doubleVals.size() != count) - { - DCMTKException(<< "DecimalString " << std::hex - << group << " " << std::hex - << element << " expected " - << count << "items, but found " - << doubleVals.size() << std::dec); - - } - for(unsigned i = 0; i < count; i++) - { - target[i] = static_cast(doubleVals[i]); - } - return EXIT_SUCCESS; - } - - template - int GetElementDSorOB(unsigned short group, - unsigned short element, - TType &target, - bool throwException = true) - { - if(this->GetElementDS(group,element,1,&target,false) == EXIT_SUCCESS) - { - return EXIT_SUCCESS; - } - std::string val; - if(this->GetElementOB(group,element,val) != EXIT_SUCCESS) - { - DCMTKException(<< "Can't find DecimalString element " << std::hex - << group << " " << std::hex - << element << std::dec); - } - const char *data = val.c_str(); - const TType *fptr = reinterpret_cast(data); - target = *fptr; - switch(this->GetTransferSyntax()) - { - case EXS_LittleEndianImplicit: - case EXS_LittleEndianExplicit: - itk::ByteSwapper::SwapFromSystemToLittleEndian(&target); - break; - case EXS_BigEndianImplicit: - case EXS_BigEndianExplicit: - itk::ByteSwapper::SwapFromSystemToBigEndian(&target); - break; - default: - break; - } - return EXIT_SUCCESS; - - } - /** Get a DecimalString Item as a single string - */ - int GetElementDS(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - int GetElementFD(unsigned short group, - unsigned short element, - double &target, - bool throwException = true); - int GetElementFD(unsigned short group, - unsigned short element, - double * &target, - bool throwException = true); - int GetElementFL(unsigned short group, - unsigned short element, - float &target, - bool throwException = true); - int GetElementFLorOB(unsigned short group, - unsigned short element, - float &target, - bool throwException = true); - - int GetElementUS(unsigned short group, - unsigned short element, - unsigned short &target, - bool throwException = true); - int GetElementUS(unsigned short group, - unsigned short element, - unsigned short *&target, - bool throwException = true); - /** Get a DecimalString Item as a single string - */ - int GetElementCS(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - - /** Get a PersonName Item as a single string - */ - int GetElementPN(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - - /** get an IS (Integer String Item - */ - int GetElementIS(unsigned short group, - unsigned short element, - ::itk::int32_t &target, - bool throwException = true); - - int GetElementISorOB(unsigned short group, - unsigned short element, - ::itk::int32_t &target, - bool throwException = true); - /** get an OB OtherByte Item - */ - int GetElementOB(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - - int GetElementSQ(unsigned short group, - unsigned short entry, - DCMTKSequence &sequence, - bool throwException = true); - - int GetElementUI(unsigned short group, - unsigned short entry, - std::string &target, - bool throwException = true); - - int GetElementDA(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - - int GetElementTM(unsigned short group, - unsigned short element, - std::string &target, - bool throwException = true); - - int GetDirCosines(vnl_vector &dir1, - vnl_vector &dir2, - vnl_vector &dir3); - - int GetFrameCount() const; - - int GetSlopeIntercept(double &slope, double &intercept); - - int GetDimensions(unsigned short &rows, unsigned short &columns); - - ImageIOBase::IOComponentType GetImageDataType(); - ImageIOBase::IOPixelType GetImagePixelType(); - - int GetSpacing(double *spacing); - int GetOrigin(double *origin); - - E_TransferSyntax GetTransferSyntax() const; - - long GetFileNumber() const; - - static void - AddDictEntry(DcmDictEntry *entry); - -private: - static unsigned ascii2hex(char c); - - static std::string ConvertFromOB(OFString &toConvert); - - std::string m_FileName; - DcmFileFormat* m_DFile{nullptr}; - DcmDataset * m_Dataset{nullptr}; - E_TransferSyntax m_Xfer{EXS_Unknown}; - Sint32 m_FrameCount{0}; - long m_FileNumber{-1L}; -}; - -extern bool CompareDCMTKFileReaders(DCMTKFileReader *a, DCMTKFileReader *b); -} - -#endif // itkDCMTKFileReader_h diff --git a/Modules/Loadable/Annotations/SubjectHierarchyPlugins/AnnotationsSubjectHierarchyPlugin.py b/Modules/Loadable/Annotations/SubjectHierarchyPlugins/AnnotationsSubjectHierarchyPlugin.py index bf51fd403d2..4c5fec78a0a 100644 --- a/Modules/Loadable/Annotations/SubjectHierarchyPlugins/AnnotationsSubjectHierarchyPlugin.py +++ b/Modules/Loadable/Annotations/SubjectHierarchyPlugins/AnnotationsSubjectHierarchyPlugin.py @@ -6,122 +6,122 @@ class AnnotationsSubjectHierarchyPlugin(AbstractScriptedSubjectHierarchyPlugin): - """ Scripted subject hierarchy plugin for the Annotations module. - - This is also an example for scripted plugins, so includes all possible methods. - The methods that are not needed (i.e. the default implementation in - qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be - omitted in plugins created based on this one. - - The plugin registers itself on creation, but needs to be initialized from the - module or application as follows: - from SubjectHierarchyPlugins import AnnotationsSubjectHierarchyPlugin - scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None) - scriptedPlugin.setPythonSource(AnnotationsSubjectHierarchyPlugin.filePath) - """ - - # Necessary static member to be able to set python source to scripted subject hierarchy plugin - filePath = __file__ - - def __init__(self, scriptedPlugin): - scriptedPlugin.name = 'Annotations' - AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin) - - def canAddNodeToSubjectHierarchy(self, node, parentItemID): - if node is not None: - if node.IsA("vtkMRMLAnnotationROINode") or node.IsA("vtkMRMLAnnotationRulerNode"): - return 1.0 - return 0.0 - - def canOwnSubjectHierarchyItem(self, itemID): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - shNode = pluginHandlerSingleton.subjectHierarchyNode() - associatedNode = shNode.GetItemDataNode(itemID) - # ROI or Ruler - if associatedNode is not None: - if associatedNode.IsA("vtkMRMLAnnotationROINode") or associatedNode.IsA("vtkMRMLAnnotationRulerNode"): - return 1.0 - return 0.0 - - def roleForPlugin(self): - return "Annotation" - - def helpText(self): - # return ("

" - # "" - # "SegmentEditor module subject hierarchy help text" - # "" - # "

" - # "

" - # "" - # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin." - # "" - # "

\n") - return "" - - def icon(self, itemID): - import os - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - shNode = pluginHandlerSingleton.subjectHierarchyNode() - associatedNode = shNode.GetItemDataNode(itemID) - if associatedNode is not None: - # ROI - if associatedNode.IsA("vtkMRMLAnnotationROINode"): - roiIconPath = os.path.join(os.path.dirname(__file__), '../Resources/Icons/AnnotationROI.png') - if os.path.exists(roiIconPath): - return qt.QIcon(roiIconPath) - # Ruler - if associatedNode.IsA("vtkMRMLAnnotationRulerNode"): - rulerIconPath = os.path.join(os.path.dirname(__file__), '../Resources/Icons/AnnotationDistance.png') - if os.path.exists(rulerIconPath): - return qt.QIcon(rulerIconPath) - # Item unknown by plugin - return qt.QIcon() - - def visibilityIcon(self, visible): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible) - - def editProperties(self, itemID): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - pluginHandlerSingleton.pluginByName('Default').editProperties(itemID) - - def itemContextMenuActions(self): - return [] - - def sceneContextMenuActions(self): - return [] - - def showContextMenuActionsForItem(self, itemID): - pass - - def viewContextMenuActions(self): - """ Important note: - In order to use view menus in scripted plugins, it needs to be registered differently, - so that the Python API can be fully built by the time this function is called. - - The following changes are necessary: - 1. Remove or comment out the following line from constructor - AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin) - 2. In addition to the initialization where the scripted plugin is instantialized and - the source set, the plugin also needs to be registered manually: - pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - pluginHandler.registerPlugin(scriptedPlugin) + """ Scripted subject hierarchy plugin for the Annotations module. + + This is also an example for scripted plugins, so includes all possible methods. + The methods that are not needed (i.e. the default implementation in + qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be + omitted in plugins created based on this one. + + The plugin registers itself on creation, but needs to be initialized from the + module or application as follows: + from SubjectHierarchyPlugins import AnnotationsSubjectHierarchyPlugin + scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None) + scriptedPlugin.setPythonSource(AnnotationsSubjectHierarchyPlugin.filePath) """ - return [] - def showViewContextMenuActionsForItem(self, itemID, eventData): - pass - - def tooltip(self, itemID): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - tooltip = pluginHandlerSingleton.pluginByName('Default').tooltip(itemID) - return str(tooltip) - - def setDisplayVisibility(self, itemID, visible): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible) - - def getDisplayVisibility(self, itemID): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID) + # Necessary static member to be able to set python source to scripted subject hierarchy plugin + filePath = __file__ + + def __init__(self, scriptedPlugin): + scriptedPlugin.name = 'Annotations' + AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin) + + def canAddNodeToSubjectHierarchy(self, node, parentItemID): + if node is not None: + if node.IsA("vtkMRMLAnnotationROINode") or node.IsA("vtkMRMLAnnotationRulerNode"): + return 1.0 + return 0.0 + + def canOwnSubjectHierarchyItem(self, itemID): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + shNode = pluginHandlerSingleton.subjectHierarchyNode() + associatedNode = shNode.GetItemDataNode(itemID) + # ROI or Ruler + if associatedNode is not None: + if associatedNode.IsA("vtkMRMLAnnotationROINode") or associatedNode.IsA("vtkMRMLAnnotationRulerNode"): + return 1.0 + return 0.0 + + def roleForPlugin(self): + return "Annotation" + + def helpText(self): + # return ("

" + # "" + # "SegmentEditor module subject hierarchy help text" + # "" + # "

" + # "

" + # "" + # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin." + # "" + # "

\n") + return "" + + def icon(self, itemID): + import os + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + shNode = pluginHandlerSingleton.subjectHierarchyNode() + associatedNode = shNode.GetItemDataNode(itemID) + if associatedNode is not None: + # ROI + if associatedNode.IsA("vtkMRMLAnnotationROINode"): + roiIconPath = os.path.join(os.path.dirname(__file__), '../Resources/Icons/AnnotationROI.png') + if os.path.exists(roiIconPath): + return qt.QIcon(roiIconPath) + # Ruler + if associatedNode.IsA("vtkMRMLAnnotationRulerNode"): + rulerIconPath = os.path.join(os.path.dirname(__file__), '../Resources/Icons/AnnotationDistance.png') + if os.path.exists(rulerIconPath): + return qt.QIcon(rulerIconPath) + # Item unknown by plugin + return qt.QIcon() + + def visibilityIcon(self, visible): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible) + + def editProperties(self, itemID): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + pluginHandlerSingleton.pluginByName('Default').editProperties(itemID) + + def itemContextMenuActions(self): + return [] + + def sceneContextMenuActions(self): + return [] + + def showContextMenuActionsForItem(self, itemID): + pass + + def viewContextMenuActions(self): + """ Important note: + In order to use view menus in scripted plugins, it needs to be registered differently, + so that the Python API can be fully built by the time this function is called. + + The following changes are necessary: + 1. Remove or comment out the following line from constructor + AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin) + 2. In addition to the initialization where the scripted plugin is instantialized and + the source set, the plugin also needs to be registered manually: + pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + pluginHandler.registerPlugin(scriptedPlugin) + """ + return [] + + def showViewContextMenuActionsForItem(self, itemID, eventData): + pass + + def tooltip(self, itemID): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + tooltip = pluginHandlerSingleton.pluginByName('Default').tooltip(itemID) + return str(tooltip) + + def setDisplayVisibility(self, itemID, visible): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible) + + def getDisplayVisibility(self, itemID): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID) diff --git a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyFiducials.py b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyFiducials.py index ab16863977c..8a484eb0bb2 100644 --- a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyFiducials.py +++ b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyFiducials.py @@ -4,48 +4,48 @@ def TestFiducialAdd(renameFlag=1, visibilityFlag=1, numToAdd=20): - print("numToAdd = ", numToAdd) - if renameFlag > 0: - print("Index\tTime to add fid\tDelta between adds\tTime to rename fid\tDelta between renames") - print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt") - else: - print("Index\tTime to add fid\tDelta between adds") - print("%(index)04s\t" % {'index': "i"}, "t\tdt") - r = 0 - a = 0 - s = 0 - t1 = 0 - t2 = 0 - t3 = 0 - t4 = 0 - timeToAddThisFid = 0 - timeToAddLastFid = 0 - timeToRenameThisFid = 0 - timeToRenameLastFid = 0 - # iterate over the number of fiducials to add - for i in range(numToAdd): -# print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s - fidNode = slicer.vtkMRMLAnnotationFiducialNode() - fidNode.SetFiducialCoordinates(r, a, s) - t1 = time.process_time() - fidNode.Initialize(slicer.mrmlScene) - t2 = time.process_time() - timeToAddThisFid = t2 - t1 - dt = timeToAddThisFid - timeToAddLastFid + print("numToAdd = ", numToAdd) if renameFlag > 0: - t3 = time.process_time() - fidNode.SetName(str(i)) - t4 = time.process_time() - timeToRenameThisFid = t4 - t3 - dt2 = timeToRenameThisFid - timeToRenameLastFid - print('%(index)04d\t' % {'index': i}, timeToAddThisFid, "\t", dt, "\t", timeToRenameThisFid, "\t", dt2) - timeToRenameLastFid = timeToRenameThisFid + print("Index\tTime to add fid\tDelta between adds\tTime to rename fid\tDelta between renames") + print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt") else: - print('%(index)04d\t' % {'index': i}, timeToAddThisFid, "\t", dt) - r = r + 1.0 - a = a + 1.0 - s = s + 1.0 - timeToAddLastFid = timeToAddThisFid + print("Index\tTime to add fid\tDelta between adds") + print("%(index)04s\t" % {'index': "i"}, "t\tdt") + r = 0 + a = 0 + s = 0 + t1 = 0 + t2 = 0 + t3 = 0 + t4 = 0 + timeToAddThisFid = 0 + timeToAddLastFid = 0 + timeToRenameThisFid = 0 + timeToRenameLastFid = 0 + # iterate over the number of fiducials to add + for i in range(numToAdd): + # print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s + fidNode = slicer.vtkMRMLAnnotationFiducialNode() + fidNode.SetFiducialCoordinates(r, a, s) + t1 = time.process_time() + fidNode.Initialize(slicer.mrmlScene) + t2 = time.process_time() + timeToAddThisFid = t2 - t1 + dt = timeToAddThisFid - timeToAddLastFid + if renameFlag > 0: + t3 = time.process_time() + fidNode.SetName(str(i)) + t4 = time.process_time() + timeToRenameThisFid = t4 - t3 + dt2 = timeToRenameThisFid - timeToRenameLastFid + print('%(index)04d\t' % {'index': i}, timeToAddThisFid, "\t", dt, "\t", timeToRenameThisFid, "\t", dt2) + timeToRenameLastFid = timeToRenameThisFid + else: + print('%(index)04d\t' % {'index': i}, timeToAddThisFid, "\t", dt) + r = r + 1.0 + a = a + 1.0 + s = s + 1.0 + timeToAddLastFid = timeToAddThisFid testStartTime = time.process_time() diff --git a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyROIs.py b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyROIs.py index f953e582a69..eca9bc91a86 100644 --- a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyROIs.py +++ b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyROIs.py @@ -2,55 +2,55 @@ def TestROIAdd(renameFlag=1, visibilityFlag=1, numToAdd=20): - print("numToAdd = ", numToAdd) - if renameFlag > 0: - print("Index\tTime to add roi\tDelta between adds\tTime to rename roi\tDelta between renames") - print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt") - else: - print("Index\tTime to add roi\tDelta between adds") - print("%(index)04s\t" % {'index': "i"}, "t\tdt") - cx = 0 - cy = 0 - cz = 0 - rx = 1 - ry = 1 - rz = 1 - t1 = 0 - t2 = 0 - t3 = 0 - t4 = 0 - timeToAddThisROI = 0 - timeToAddLastROI = 0 - timeToRenameThisROI = 0 - timeToRenameLastROI = 0 - # iterate over the number of rois to add - for i in range(numToAdd): -# print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s - roiNode = slicer.vtkMRMLAnnotationROINode() - roiNode.SetXYZ(cx, cy, cz) - roiNode.SetRadiusXYZ(rx, ry, rz) - t1 = time.process_time() - roiNode.Initialize(slicer.mrmlScene) - t2 = time.process_time() - timeToAddThisROI = t2 - t1 - dt = timeToAddThisROI - timeToAddLastROI + print("numToAdd = ", numToAdd) if renameFlag > 0: - t3 = time.process_time() - roiNode.SetName(str(i)) - t4 = time.process_time() - timeToRenameThisROI = t4 - t3 - dt2 = timeToRenameThisROI - timeToRenameLastROI - print('%(index)04d\t' % {'index': i}, timeToAddThisROI, "\t", dt, "\t", timeToRenameThisROI, "\t", dt2) - timeToRenameLastROI = timeToRenameThisROI + print("Index\tTime to add roi\tDelta between adds\tTime to rename roi\tDelta between renames") + print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt") else: - print('%(index)04d\t' % {'index': i}, timeToAddThisROI, "\t", dt) - rx = rx + 0.5 - ry = ry + 0.5 - rz = rz + 0.5 - cx = cx + 2.0 - cy = cy + 2.0 - cz = cz + 2.0 - timeToAddLastROI = timeToAddThisROI + print("Index\tTime to add roi\tDelta between adds") + print("%(index)04s\t" % {'index': "i"}, "t\tdt") + cx = 0 + cy = 0 + cz = 0 + rx = 1 + ry = 1 + rz = 1 + t1 = 0 + t2 = 0 + t3 = 0 + t4 = 0 + timeToAddThisROI = 0 + timeToAddLastROI = 0 + timeToRenameThisROI = 0 + timeToRenameLastROI = 0 + # iterate over the number of rois to add + for i in range(numToAdd): + # print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s + roiNode = slicer.vtkMRMLAnnotationROINode() + roiNode.SetXYZ(cx, cy, cz) + roiNode.SetRadiusXYZ(rx, ry, rz) + t1 = time.process_time() + roiNode.Initialize(slicer.mrmlScene) + t2 = time.process_time() + timeToAddThisROI = t2 - t1 + dt = timeToAddThisROI - timeToAddLastROI + if renameFlag > 0: + t3 = time.process_time() + roiNode.SetName(str(i)) + t4 = time.process_time() + timeToRenameThisROI = t4 - t3 + dt2 = timeToRenameThisROI - timeToRenameLastROI + print('%(index)04d\t' % {'index': i}, timeToAddThisROI, "\t", dt, "\t", timeToRenameThisROI, "\t", dt2) + timeToRenameLastROI = timeToRenameThisROI + else: + print('%(index)04d\t' % {'index': i}, timeToAddThisROI, "\t", dt) + rx = rx + 0.5 + ry = ry + 0.5 + rz = rz + 0.5 + cx = cx + 2.0 + cy = cy + 2.0 + cz = cz + 2.0 + timeToAddLastROI = timeToAddThisROI testStartTime = time.process_time() diff --git a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyRulers.py b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyRulers.py index bc8a8575918..ae40f755656 100644 --- a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyRulers.py +++ b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyRulers.py @@ -4,55 +4,55 @@ def TestRulerAdd(renameFlag=1, visibilityFlag=1, numToAdd=20): - print("numToAdd = ", numToAdd) - if renameFlag > 0: - print("Index\tTime to add ruler\tDelta between adds\tTime to rename ruler\tDelta between renames") - print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt") - else: - print("Index\tTime to add ruler\tDelta between adds") - print("%(index)04s\t" % {'index': "i"}, "t\tdt") - r1 = 0 - a1 = 0 - s1 = 0 - r2 = 1 - a2 = 1 - s2 = 1 - t1 = 0 - t2 = 0 - t3 = 0 - t4 = 0 - timeToAddThisRuler = 0 - timeToAddLastRuler = 0 - timeToRenameThisRuler = 0 - timeToRenameLastRuler = 0 - # iterate over the number of rulers to add - for i in range(numToAdd): -# print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s - rulerNode = slicer.vtkMRMLAnnotationRulerNode() - rulerNode.SetPosition1(r1, a1, s1) - rulerNode.SetPosition2(r2, a2, s2) - t1 = time.process_time() - rulerNode.Initialize(slicer.mrmlScene) - t2 = time.process_time() - timeToAddThisRuler = t2 - t1 - dt = timeToAddThisRuler - timeToAddLastRuler + print("numToAdd = ", numToAdd) if renameFlag > 0: - t3 = time.process_time() - rulerNode.SetName(str(i)) - t4 = time.process_time() - timeToRenameThisRuler = t4 - t3 - dt2 = timeToRenameThisRuler - timeToRenameLastRuler - print('%(index)04d\t' % {'index': i}, timeToAddThisRuler, "\t", dt, "\t", timeToRenameThisRuler, "\t", dt2) - timeToRenameLastRuler = timeToRenameThisRuler + print("Index\tTime to add ruler\tDelta between adds\tTime to rename ruler\tDelta between renames") + print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt") else: - print('%(index)04d\t' % {'index': i}, timeToAddThisRuler, "\t", dt) - r1 = r1 + 1.0 - a1 = a1 + 1.0 - s1 = s1 + 1.0 - r2 = r2 + 1.5 - a2 = a2 + 1.5 - s2 = s2 + 1.5 - timeToAddLastRuler = timeToAddThisRuler + print("Index\tTime to add ruler\tDelta between adds") + print("%(index)04s\t" % {'index': "i"}, "t\tdt") + r1 = 0 + a1 = 0 + s1 = 0 + r2 = 1 + a2 = 1 + s2 = 1 + t1 = 0 + t2 = 0 + t3 = 0 + t4 = 0 + timeToAddThisRuler = 0 + timeToAddLastRuler = 0 + timeToRenameThisRuler = 0 + timeToRenameLastRuler = 0 + # iterate over the number of rulers to add + for i in range(numToAdd): + # print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s + rulerNode = slicer.vtkMRMLAnnotationRulerNode() + rulerNode.SetPosition1(r1, a1, s1) + rulerNode.SetPosition2(r2, a2, s2) + t1 = time.process_time() + rulerNode.Initialize(slicer.mrmlScene) + t2 = time.process_time() + timeToAddThisRuler = t2 - t1 + dt = timeToAddThisRuler - timeToAddLastRuler + if renameFlag > 0: + t3 = time.process_time() + rulerNode.SetName(str(i)) + t4 = time.process_time() + timeToRenameThisRuler = t4 - t3 + dt2 = timeToRenameThisRuler - timeToRenameLastRuler + print('%(index)04d\t' % {'index': i}, timeToAddThisRuler, "\t", dt, "\t", timeToRenameThisRuler, "\t", dt2) + timeToRenameLastRuler = timeToRenameThisRuler + else: + print('%(index)04d\t' % {'index': i}, timeToAddThisRuler, "\t", dt) + r1 = r1 + 1.0 + a1 = a1 + 1.0 + s1 = s1 + 1.0 + r2 = r2 + 1.5 + a2 = a2 + 1.5 + s2 = s2 + 1.5 + timeToAddLastRuler = timeToAddThisRuler testStartTime = time.process_time() diff --git a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingFiducialWithSceneViewRestore.py b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingFiducialWithSceneViewRestore.py index dd07484bf2a..41c0ca3f29d 100644 --- a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingFiducialWithSceneViewRestore.py +++ b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingFiducialWithSceneViewRestore.py @@ -9,26 +9,26 @@ fid.CreateAnnotationPointDisplayNode() startCoords = [1.0, 2.0, 3.0] -fid.AddControlPoint(startCoords,0,1) +fid.AddControlPoint(startCoords, 0, 1) slicer.mrmlScene.AddNode(fid) fid.GetFiducialCoordinates(startCoords) -print("Starting fiducial coordinates = ",startCoords) +print("Starting fiducial coordinates = ", startCoords) sv = slicer.mrmlScene.AddNode(slicer.vtkMRMLSceneViewNode()) sv.StoreScene() fid.SetFiducialCoordinates(11.1, 22.2, 33.3) -afterStoreSceneCoords = [0,0,0] +afterStoreSceneCoords = [0, 0, 0] fid.GetFiducialCoordinates(afterStoreSceneCoords) -print("After storing the scene, set fiducial coords to ",afterStoreSceneCoords) +print("After storing the scene, set fiducial coords to ", afterStoreSceneCoords) sv.RestoreScene() -fidAfterRestore = slicer.mrmlScene.GetNodeByID("vtkMRMLAnnotationFiducialNode1") +fidAfterRestore = slicer.mrmlScene.GetNodeByID("vtkMRMLAnnotationFiducialNode1") -coords = [0,0,0] +coords = [0, 0, 0] fidAfterRestore.GetFiducialCoordinates(coords) print("After restoring the scene, fiducial coordinates = ", coords) @@ -41,5 +41,5 @@ diffTotal = xdiff + ydiff + zdiff if diffTotal > 0.1: - exceptionMessage = "Difference between coordinate values total = " + str(diffTotal) - raise Exception(exceptionMessage) + exceptionMessage = "Difference between coordinate values total = " + str(diffTotal) + raise Exception(exceptionMessage) diff --git a/Modules/Loadable/Annotations/Testing/Python/LoadAnnotationRulerScene.py b/Modules/Loadable/Annotations/Testing/Python/LoadAnnotationRulerScene.py index 1e29a91052c..205f4b1abd0 100644 --- a/Modules/Loadable/Annotations/Testing/Python/LoadAnnotationRulerScene.py +++ b/Modules/Loadable/Annotations/Testing/Python/LoadAnnotationRulerScene.py @@ -7,10 +7,10 @@ # try get the path of the ruler scene file from the arguments numArgs = len(sys.argv) if numArgs > 1: - scenePath = sys.argv[1] + scenePath = sys.argv[1] else: - # set the url as best guess from SLICER_HOME - scenePath = os.path.join(os.environ['SLICER_HOME'], "../../Slicer4/Modules/Loadable/Annotations/Testing/Data/Input/ruler.mrml") + # set the url as best guess from SLICER_HOME + scenePath = os.path.join(os.environ['SLICER_HOME'], "../../Slicer4/Modules/Loadable/Annotations/Testing/Data/Input/ruler.mrml") scenePath = os.path.normpath(scenePath) print("Trying to load ruler mrml file", scenePath) diff --git a/Modules/Loadable/Annotations/Testing/Python/RemoveAnnotationsIDFromSelectionNode.py b/Modules/Loadable/Annotations/Testing/Python/RemoveAnnotationsIDFromSelectionNode.py index 7dcb33ced03..9fe0d5c1c88 100644 --- a/Modules/Loadable/Annotations/Testing/Python/RemoveAnnotationsIDFromSelectionNode.py +++ b/Modules/Loadable/Annotations/Testing/Python/RemoveAnnotationsIDFromSelectionNode.py @@ -5,32 +5,32 @@ selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton") if selectionNode: - print(selectionNode) - annotClassName = "vtkMRMLAnnotationRulerNode" - startIndex = selectionNode.PlaceNodeClassNameInList(annotClassName) - print("Removing ", annotClassName) - selectionNode.RemovePlaceNodeClassNameFromList(annotClassName) - endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName) - print(selectionNode) - print("Start index for ", annotClassName, " = ", startIndex, ", end index after removing it = ", endIndex) - if endIndex != -1: - raise Exception(f"Failed to remove annotation {annotClassName} from list, end index = {endIndex} should be -1") + print(selectionNode) + annotClassName = "vtkMRMLAnnotationRulerNode" + startIndex = selectionNode.PlaceNodeClassNameInList(annotClassName) + print("Removing ", annotClassName) + selectionNode.RemovePlaceNodeClassNameFromList(annotClassName) + endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName) + print(selectionNode) + print("Start index for ", annotClassName, " = ", startIndex, ", end index after removing it = ", endIndex) + if endIndex != -1: + raise Exception(f"Failed to remove annotation {annotClassName} from list, end index = {endIndex} should be -1") - # now make one active and remove it - annotClassName = "vtkMRMLAnnotationFiducialNode" - selectionNode.SetActivePlaceNodeClassName(annotClassName) - interactionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLInteractionNodeSingleton") - interactionNode.SwitchToSinglePlaceMode() - print("Removing", annotClassName) - selectionNode.RemovePlaceNodeClassNameFromList(annotClassName) - endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName) - if endIndex != -1: - raise Exception(f"Failed to remove active annotation {annotClassName} from list, end index = {endIndex} should be -1") + # now make one active and remove it + annotClassName = "vtkMRMLAnnotationFiducialNode" + selectionNode.SetActivePlaceNodeClassName(annotClassName) + interactionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLInteractionNodeSingleton") + interactionNode.SwitchToSinglePlaceMode() + print("Removing", annotClassName) + selectionNode.RemovePlaceNodeClassNameFromList(annotClassName) + endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName) + if endIndex != -1: + raise Exception(f"Failed to remove active annotation {annotClassName} from list, end index = {endIndex} should be -1") - # re-add the ruler one - annotClassName = "vtkMRMLAnnotationRulerNode" - print("Adding back the ruler node") - selectionNode.AddNewPlaceNodeClassNameToList("vtkMRMLAnnotationRulerNode", ":/Icons/AnnotationDistanceWithArrow.png") - endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName) - if endIndex == -1: - raise Exception(f"Failed to re-add {annotClassName}, end index = {endIndex}") + # re-add the ruler one + annotClassName = "vtkMRMLAnnotationRulerNode" + print("Adding back the ruler node") + selectionNode.AddNewPlaceNodeClassNameToList("vtkMRMLAnnotationRulerNode", ":/Icons/AnnotationDistanceWithArrow.png") + endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName) + if endIndex == -1: + raise Exception(f"Failed to re-add {annotClassName}, end index = {endIndex}") diff --git a/Modules/Loadable/Colors/Testing/Python/ColorLegendSelfTest.py b/Modules/Loadable/Colors/Testing/Python/ColorLegendSelfTest.py index f30a44c77c3..4bd54e65ad1 100644 --- a/Modules/Loadable/Colors/Testing/Python/ColorLegendSelfTest.py +++ b/Modules/Loadable/Colors/Testing/Python/ColorLegendSelfTest.py @@ -10,20 +10,20 @@ class ColorLegendSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "ColorLegendSelfTest" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = [] - self.parent.contributors = ["Kevin Wang (PMH), Nicole Aucoin (BWH), Mikhail Polkovnikov (IHEP)"] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "ColorLegendSelfTest" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = [] + self.parent.contributors = ["Kevin Wang (PMH), Nicole Aucoin (BWH), Mikhail Polkovnikov (IHEP)"] + self.parent.helpText = """ This is a test case for the new vtkSlicerScalarBarActor class. It iterates through all the color nodes and sets them active in the Colors module while the color legend widget is displayed. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ This file was originally developed by Kevin Wang, PMH and was funded by CCO and OCAIRO. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # # ColorLegendSelfTestWidget @@ -32,140 +32,140 @@ def __init__(self, parent): class ColorLegendSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + # Instantiate and connect widgets ... - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the algorithm." - self.applyButton.enabled = True - parametersFormLayout.addRow(self.applyButton) + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the algorithm." + self.applyButton.enabled = True + parametersFormLayout.addRow(self.applyButton) - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - def cleanup(self): - pass + def cleanup(self): + pass - def onApplyButton(self): - test = ColorLegendSelfTestTest() - print("Run the test algorithm") - test.test_ColorLegendSelfTest1() + def onApplyButton(self): + test = ColorLegendSelfTestTest() + print("Run the test algorithm") + test.test_ColorLegendSelfTest1() class ColorLegendSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ - slicer.mrmlScene.Clear(0) - # Timeout delay - self.delayMs = 700 - - def runTest(self): - """Run as few or as many tests as needed here. + This is the test case for your scripted module. """ - self.setUp() - self.test_ColorLegendSelfTest1() - - def test_ColorLegendSelfTest1(self): - - self.delayDisplay("Starting test_ColorLegendSelfTest1") - - self.delayDisplay('Load CTChest sample volume') - import SampleData - sampleDataLogic = SampleData.SampleDataLogic() - ctVolumeNode = sampleDataLogic.downloadCTChest() - self.assertIsNotNone( ctVolumeNode ) - - self.delayDisplay('Switch to Colors module') - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('Colors') - - # Get widgets for testing via GUI - colorWidget = slicer.modules.colors.widgetRepresentation() - activeColorNodeSelector = slicer.util.findChildren(colorWidget, 'ColorTableComboBox')[0] - self.assertIsNotNone(activeColorNodeSelector) - activeDisplayableNodeSelector = slicer.util.findChildren(colorWidget, 'DisplayableNodeComboBox')[0] - self.assertIsNotNone(activeDisplayableNodeSelector) - createColorLegendButton = slicer.util.findChildren(colorWidget, 'CreateColorLegendButton')[0] - self.assertIsNotNone(createColorLegendButton) - useCurrentColorsButton = slicer.util.findChildren(colorWidget, 'UseCurrentColorsButton')[0] - self.assertIsNotNone(useCurrentColorsButton) - colorLegendDisplayNodeWidget = slicer.util.findChildren(colorWidget, 'ColorLegendDisplayNodeWidget')[0] - self.assertIsNotNone(colorLegendDisplayNodeWidget) - colorLegendVisibilityCheckBox = slicer.util.findChildren(colorLegendDisplayNodeWidget, 'ColorLegendVisibilityCheckBox')[0] - self.assertIsNotNone(colorLegendVisibilityCheckBox) - - self.delayDisplay('Show color legend on all views and slices', self.delayMs) - activeDisplayableNodeSelector.setCurrentNode(ctVolumeNode) - createColorLegendButton.click() - self.assertTrue(colorLegendVisibilityCheckBox.checked) - - self.delayDisplay('Iterate over the color nodes and set each one active', self.delayMs) - shortDelayMs = 5 - # There are many color nodes, we don't test each to make the test complete faster - testedColorNodeIndices = list(range(0, 60, 3)) - for ind, n in enumerate(testedColorNodeIndices): - colorNode = slicer.mrmlScene.GetNthNodeByClass(n, 'vtkMRMLColorNode') - self.delayDisplay(f"Setting color node {colorNode.GetName()} ({ind}/{len(testedColorNodeIndices)}) for the displayable node", shortDelayMs) - activeColorNodeSelector.setCurrentNodeID(colorNode.GetID()) - # use the delay display here to ensure a render - useCurrentColorsButton.click() - - self.delayDisplay('Test color legend visibility', self.delayMs) - colorLegend = slicer.modules.colors.logic().GetColorLegendDisplayNode(ctVolumeNode) - self.assertIsNotNone(colorLegend) - - self.delayDisplay('Exercise color legend updates via MRML', self.delayMs) - # signal to displayable manager to show a created color legend - colorLegend.SetMaxNumberOfColors(256) - colorLegend.SetVisibility(True) - - self.delayDisplay('Show color legend in Red slice and 3D views only', self.delayMs) - sliceNodeRed = slicer.app.layoutManager().sliceWidget('Red').mrmlSliceNode() - self.assertIsNotNone(sliceNodeRed) - threeDViewNode = slicer.app.layoutManager().threeDWidget(0).mrmlViewNode() - self.assertIsNotNone(threeDViewNode) - colorLegend.SetViewNodeIDs([sliceNodeRed.GetID(), threeDViewNode.GetID()]) - - self.delayDisplay('Show color legend in the 3D view only', self.delayMs) - colorLegend.SetViewNodeIDs([threeDViewNode.GetID()]) - self.delayDisplay('Test color legend on 3D view finished!', self.delayMs) - - # Test showing color legend only in a single slice node - sliceNameColor = { - 'Red': [ 1., 0., 0. ], - 'Green': [ 0., 1., 0. ], - 'Yellow': [ 1., 1., 0.] - } - for sliceName, titleColor in sliceNameColor.items(): - self.delayDisplay('Test color legend on the ' + sliceName + ' slice view', self.delayMs) - sliceNode = slicer.app.layoutManager().sliceWidget(sliceName).mrmlSliceNode() - colorLegend.SetViewNodeIDs([sliceNode.GetID()]) - colorLegend.SetTitleText(sliceName) - colorLegend.GetTitleTextProperty().SetColor(titleColor) - self.delayDisplay('Test color legend on the ' + sliceName + ' slice view finished!',self.delayMs*2) - - colorLegend.SetVisibility(False) - - self.delayDisplay('Test passed!') + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) + # Timeout delay + self.delayMs = 700 + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_ColorLegendSelfTest1() + + def test_ColorLegendSelfTest1(self): + + self.delayDisplay("Starting test_ColorLegendSelfTest1") + + self.delayDisplay('Load CTChest sample volume') + import SampleData + sampleDataLogic = SampleData.SampleDataLogic() + ctVolumeNode = sampleDataLogic.downloadCTChest() + self.assertIsNotNone(ctVolumeNode) + + self.delayDisplay('Switch to Colors module') + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('Colors') + + # Get widgets for testing via GUI + colorWidget = slicer.modules.colors.widgetRepresentation() + activeColorNodeSelector = slicer.util.findChildren(colorWidget, 'ColorTableComboBox')[0] + self.assertIsNotNone(activeColorNodeSelector) + activeDisplayableNodeSelector = slicer.util.findChildren(colorWidget, 'DisplayableNodeComboBox')[0] + self.assertIsNotNone(activeDisplayableNodeSelector) + createColorLegendButton = slicer.util.findChildren(colorWidget, 'CreateColorLegendButton')[0] + self.assertIsNotNone(createColorLegendButton) + useCurrentColorsButton = slicer.util.findChildren(colorWidget, 'UseCurrentColorsButton')[0] + self.assertIsNotNone(useCurrentColorsButton) + colorLegendDisplayNodeWidget = slicer.util.findChildren(colorWidget, 'ColorLegendDisplayNodeWidget')[0] + self.assertIsNotNone(colorLegendDisplayNodeWidget) + colorLegendVisibilityCheckBox = slicer.util.findChildren(colorLegendDisplayNodeWidget, 'ColorLegendVisibilityCheckBox')[0] + self.assertIsNotNone(colorLegendVisibilityCheckBox) + + self.delayDisplay('Show color legend on all views and slices', self.delayMs) + activeDisplayableNodeSelector.setCurrentNode(ctVolumeNode) + createColorLegendButton.click() + self.assertTrue(colorLegendVisibilityCheckBox.checked) + + self.delayDisplay('Iterate over the color nodes and set each one active', self.delayMs) + shortDelayMs = 5 + # There are many color nodes, we don't test each to make the test complete faster + testedColorNodeIndices = list(range(0, 60, 3)) + for ind, n in enumerate(testedColorNodeIndices): + colorNode = slicer.mrmlScene.GetNthNodeByClass(n, 'vtkMRMLColorNode') + self.delayDisplay(f"Setting color node {colorNode.GetName()} ({ind}/{len(testedColorNodeIndices)}) for the displayable node", shortDelayMs) + activeColorNodeSelector.setCurrentNodeID(colorNode.GetID()) + # use the delay display here to ensure a render + useCurrentColorsButton.click() + + self.delayDisplay('Test color legend visibility', self.delayMs) + colorLegend = slicer.modules.colors.logic().GetColorLegendDisplayNode(ctVolumeNode) + self.assertIsNotNone(colorLegend) + + self.delayDisplay('Exercise color legend updates via MRML', self.delayMs) + # signal to displayable manager to show a created color legend + colorLegend.SetMaxNumberOfColors(256) + colorLegend.SetVisibility(True) + + self.delayDisplay('Show color legend in Red slice and 3D views only', self.delayMs) + sliceNodeRed = slicer.app.layoutManager().sliceWidget('Red').mrmlSliceNode() + self.assertIsNotNone(sliceNodeRed) + threeDViewNode = slicer.app.layoutManager().threeDWidget(0).mrmlViewNode() + self.assertIsNotNone(threeDViewNode) + colorLegend.SetViewNodeIDs([sliceNodeRed.GetID(), threeDViewNode.GetID()]) + + self.delayDisplay('Show color legend in the 3D view only', self.delayMs) + colorLegend.SetViewNodeIDs([threeDViewNode.GetID()]) + self.delayDisplay('Test color legend on 3D view finished!', self.delayMs) + + # Test showing color legend only in a single slice node + sliceNameColor = { + 'Red': [1., 0., 0.], + 'Green': [0., 1., 0.], + 'Yellow': [1., 1., 0.] + } + for sliceName, titleColor in sliceNameColor.items(): + self.delayDisplay('Test color legend on the ' + sliceName + ' slice view', self.delayMs) + sliceNode = slicer.app.layoutManager().sliceWidget(sliceName).mrmlSliceNode() + colorLegend.SetViewNodeIDs([sliceNode.GetID()]) + colorLegend.SetTitleText(sliceName) + colorLegend.GetTitleTextProperty().SetColor(titleColor) + self.delayDisplay('Test color legend on the ' + sliceName + ' slice view finished!', self.delayMs * 2) + + colorLegend.SetVisibility(False) + + self.delayDisplay('Test passed!') diff --git a/Modules/Loadable/Colors/Testing/Python/CustomColorTableSceneViewRestoreTestBug3992.py b/Modules/Loadable/Colors/Testing/Python/CustomColorTableSceneViewRestoreTestBug3992.py index aeadf19e74e..3b771a62e88 100644 --- a/Modules/Loadable/Colors/Testing/Python/CustomColorTableSceneViewRestoreTestBug3992.py +++ b/Modules/Loadable/Colors/Testing/Python/CustomColorTableSceneViewRestoreTestBug3992.py @@ -12,7 +12,7 @@ colorNode.NamesInitialisedOff() colorNode.SetNumberOfColors(3) if colorNode.GetLookupTable() is not None: - colorNode.GetLookupTable().SetTableRange(0,2) + colorNode.GetLookupTable().SetTableRange(0, 2) colorNode.SetColor(0, 'zero', 0.0, 0.0, 0.0, 0.0) colorNode.SetColor(1, 'one', 1.0, 1.0, 1.0, 1.0) @@ -32,9 +32,9 @@ slicer.mrmlScene.AddNode(colorStorageNode) colorNode.SetAndObserveStorageNodeID(colorStorageNode.GetID()) -startCol2 = [0.,0.,0.,0.] +startCol2 = [0., 0., 0., 0.] colorNode.GetColor(2, startCol2) -print("Starting color 2 =\n\t",startCol2) +print("Starting color 2 =\n\t", startCol2) sv = slicer.mrmlScene.AddNode(slicer.vtkMRMLSceneViewNode()) sv.SetName('Scene View Custom Color Test') @@ -43,16 +43,16 @@ mainSceneCol2 = [0.3, 0.3, 0.3, 1.0] colorNode.SetColor(2, mainSceneCol2[0], mainSceneCol2[1], mainSceneCol2[2], mainSceneCol2[3]) colorNode.GetColor(2, mainSceneCol2) -print('After saving the scene view, set the main scene color 2 to\n\t',mainSceneCol2) +print('After saving the scene view, set the main scene color 2 to\n\t', mainSceneCol2) url = slicer.app.temporaryPath + "/customColorTableSceneViewRestore.mrml" slicer.mrmlScene.SetURL(url) slicer.mrmlScene.Commit() -print("Saved to ",url) +print("Saved to ", url) # make sure it writes the color table writeFlag = colorStorageNode.WriteData(colorNode) if writeFlag == 0: - print("Error writing out file ",colorStorageNode.GetFileName()) + print("Error writing out file ", colorStorageNode.GetFileName()) # clear out the scene and re-read from disk @@ -65,25 +65,25 @@ afterReadSceneCol2 = [0., 0., 0., 0.] readColorNode.GetColor(2, afterReadSceneCol2) -print('After reading in the scene again, have color 2 =\n\t',afterReadSceneCol2) +print('After reading in the scene again, have color 2 =\n\t', afterReadSceneCol2) readSceneView = slicer.util.getFirstNodeByName('Scene View Custom Color Test') # Current implementation is a hack to not delete the whole color table on restore, but it also won't restore the color value to the original as it's bypassing the copy since the color table in the scene view is empty. readSceneView.RestoreScene() -colorNodeAfterRestore = slicer.util.getFirstNodeByName('CustomTest') +colorNodeAfterRestore = slicer.util.getFirstNodeByName('CustomTest') # mrmlScene.GetNodeByID("vtkMRMLColorTableNode1") if colorNodeAfterRestore is None: - exceptionMessage = "Unable to find vtkMRMLColorTableNode1 in scene after restore" - raise Exception(exceptionMessage) + exceptionMessage = "Unable to find vtkMRMLColorTableNode1 in scene after restore" + raise Exception(exceptionMessage) numColors = colorNodeAfterRestore.GetNumberOfColors() if numColors != 3: - exceptionMessage = "Color node doesn't have 3 colors, instead has " + str(numColors) - raise Exception(exceptionMessage) + exceptionMessage = "Color node doesn't have 3 colors, instead has " + str(numColors) + raise Exception(exceptionMessage) afterRestoreSceneCol2 = [0., 0., 0., 0.0] colorNodeAfterRestore.GetColor(2, afterRestoreSceneCol2) @@ -104,5 +104,5 @@ print("Difference between colors after restored the scene and value from when it was read in from disk:\n\t", rdiff, gdiff, bdiff, adiff, "\n\tsummed absolute diff = ", diffTotal) if diffTotal > 0.1: - exceptionMessage = "Difference between color values total = " + str(diffTotal) - raise Exception(exceptionMessage) + exceptionMessage = "Difference between color values total = " + str(diffTotal) + raise Exception(exceptionMessage) diff --git a/Modules/Loadable/CropVolume/Testing/Python/CropVolumeSelfTest.py b/Modules/Loadable/CropVolume/Testing/Python/CropVolumeSelfTest.py index f741eaf3b1e..86e140c6983 100644 --- a/Modules/Loadable/CropVolume/Testing/Python/CropVolumeSelfTest.py +++ b/Modules/Loadable/CropVolume/Testing/Python/CropVolumeSelfTest.py @@ -7,17 +7,17 @@ class CropVolumeSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "CropVolumeSelfTest" # TODO make this more human readable by adding spaces - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Andrey Fedorov (BWH)"] # replace with "Firstname Lastname (Org)" - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "CropVolumeSelfTest" # TODO make this more human readable by adding spaces + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Andrey Fedorov (BWH)"] # replace with "Firstname Lastname (Org)" + parent.helpText = """ This module was developed as a self test to perform the operations needed for crop volume. """ - parent.acknowledgementText = """ -""" # replace with organization, grant and thanks. + parent.acknowledgementText = """ +""" # replace with organization, grant and thanks. # @@ -25,69 +25,69 @@ def __init__(self, parent): # class CropVolumeSelfTestWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) class CropVolumeSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ + """ + This is the test case for your scripted module. + """ - def setUp(self): - slicer.mrmlScene.Clear(0) + def setUp(self): + slicer.mrmlScene.Clear(0) - def runTest(self): - self.setUp() - self.test_CropVolumeSelfTest() + def runTest(self): + self.setUp() + self.test_CropVolumeSelfTest() - def test_CropVolumeSelfTest(self): - """ - Replicate the crashe in issue 3117 - """ + def test_CropVolumeSelfTest(self): + """ + Replicate the crashe in issue 3117 + """ - print("Running CropVolumeSelfTest Test case:") + print("Running CropVolumeSelfTest Test case:") - import SampleData + import SampleData - vol = SampleData.downloadSample("MRHead") - roi = slicer.vtkMRMLMarkupsROINode() + vol = SampleData.downloadSample("MRHead") + roi = slicer.vtkMRMLMarkupsROINode() - mainWindow = slicer.util.mainWindow() - mainWindow.moduleSelector().selectModule('CropVolume') + mainWindow = slicer.util.mainWindow() + mainWindow.moduleSelector().selectModule('CropVolume') - cropVolumeNode = slicer.vtkMRMLCropVolumeParametersNode() - cropVolumeNode.SetScene(slicer.mrmlScene) - cropVolumeNode.SetName('ChangeTracker_CropVolume_node') - cropVolumeNode.SetIsotropicResampling(True) - cropVolumeNode.SetSpacingScalingConst(0.5) - slicer.mrmlScene.AddNode(cropVolumeNode) + cropVolumeNode = slicer.vtkMRMLCropVolumeParametersNode() + cropVolumeNode.SetScene(slicer.mrmlScene) + cropVolumeNode.SetName('ChangeTracker_CropVolume_node') + cropVolumeNode.SetIsotropicResampling(True) + cropVolumeNode.SetSpacingScalingConst(0.5) + slicer.mrmlScene.AddNode(cropVolumeNode) - cropVolumeNode.SetInputVolumeNodeID(vol.GetID()) - cropVolumeNode.SetROINodeID(roi.GetID()) + cropVolumeNode.SetInputVolumeNodeID(vol.GetID()) + cropVolumeNode.SetROINodeID(roi.GetID()) - cropVolumeLogic = slicer.modules.cropvolume.logic() - cropVolumeLogic.Apply(cropVolumeNode) + cropVolumeLogic = slicer.modules.cropvolume.logic() + cropVolumeLogic.Apply(cropVolumeNode) - self.delayDisplay('First test passed, closing the scene and running again') - # test clearing the scene and running a second time - slicer.mrmlScene.Clear(0) - # the module will re-add the removed parameters node - mainWindow.moduleSelector().selectModule('Transforms') - mainWindow.moduleSelector().selectModule('CropVolume') - cropVolumeNode = slicer.mrmlScene.GetNodeByID('vtkMRMLCropVolumeParametersNode1') - vol = SampleData.downloadSample("MRHead") - roi = slicer.vtkMRMLMarkupsROINode() - cropVolumeNode.SetInputVolumeNodeID(vol.GetID()) - cropVolumeNode.SetROINodeID(roi.GetID()) - cropVolumeLogic.Apply(cropVolumeNode) + self.delayDisplay('First test passed, closing the scene and running again') + # test clearing the scene and running a second time + slicer.mrmlScene.Clear(0) + # the module will re-add the removed parameters node + mainWindow.moduleSelector().selectModule('Transforms') + mainWindow.moduleSelector().selectModule('CropVolume') + cropVolumeNode = slicer.mrmlScene.GetNodeByID('vtkMRMLCropVolumeParametersNode1') + vol = SampleData.downloadSample("MRHead") + roi = slicer.vtkMRMLMarkupsROINode() + cropVolumeNode.SetInputVolumeNodeID(vol.GetID()) + cropVolumeNode.SetROINodeID(roi.GetID()) + cropVolumeLogic.Apply(cropVolumeNode) - self.delayDisplay('Test passed') + self.delayDisplay('Test passed') diff --git a/Modules/Loadable/Markups/Testing/Python/AddManyMarkupsFiducialTest.py b/Modules/Loadable/Markups/Testing/Python/AddManyMarkupsFiducialTest.py index 21712d1e9e2..9b4241428e6 100644 --- a/Modules/Loadable/Markups/Testing/Python/AddManyMarkupsFiducialTest.py +++ b/Modules/Loadable/Markups/Testing/Python/AddManyMarkupsFiducialTest.py @@ -13,18 +13,18 @@ # class AddManyMarkupsFiducialTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "AddManyMarkupsFiducialTest" - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Nicole Aucoin (BWH)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "AddManyMarkupsFiducialTest" + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Nicole Aucoin (BWH)"] + parent.helpText = """ This is a test case that adds many Markup Fiducials to the scene and times the operation. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218-12S1. -""" # replace with organization, grant and thanks. +""" # replace with organization, grant and thanks. # @@ -32,111 +32,111 @@ def __init__(self, parent): # class AddManyMarkupsFiducialTestWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... - - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) - - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - - # - # node type to add - # - self.nodeTypeComboBox = qt.QComboBox() - self.nodeTypeComboBox.addItem("vtkMRMLMarkupsFiducialNode") - self.nodeTypeComboBox.addItem("vtkMRMLMarkupsLineNode") - self.nodeTypeComboBox.addItem("vtkMRMLMarkupsAngleNode") - self.nodeTypeComboBox.addItem("vtkMRMLMarkupsCurveNode") - self.nodeTypeComboBox.addItem("vtkMRMLMarkupsClosedCurveNode") - self.nodeTypeComboBox.addItem("vtkMRMLMarkupsROINode") - parametersFormLayout.addRow("Node type: ", self.nodeTypeComboBox) - - # - # number of nodes to add - # - self.numberOfNodesSliderWidget = ctk.ctkSliderWidget() - self.numberOfNodesSliderWidget.singleStep = 1.0 - self.numberOfNodesSliderWidget.decimals = 0 - self.numberOfNodesSliderWidget.minimum = 0.0 - self.numberOfNodesSliderWidget.maximum = 1000.0 - self.numberOfNodesSliderWidget.value = 1.0 - self.numberOfNodesSliderWidget.toolTip = "Set the number of nodes to add." - parametersFormLayout.addRow("Number of nodes: ", self.numberOfNodesSliderWidget) - - # - # number of fiducials to add - # - self.numberOfControlPointsSliderWidget = ctk.ctkSliderWidget() - self.numberOfControlPointsSliderWidget.singleStep = 1.0 - self.numberOfControlPointsSliderWidget.decimals = 0 - self.numberOfControlPointsSliderWidget.minimum = 0.0 - self.numberOfControlPointsSliderWidget.maximum = 10000.0 - self.numberOfControlPointsSliderWidget.value = 500.0 - self.numberOfControlPointsSliderWidget.toolTip = "Set the number of control points to add per node." - parametersFormLayout.addRow("Number of control points: ", self.numberOfControlPointsSliderWidget) - - # - # check box to trigger fewer modify events, adding all the new points - # is wrapped inside of a StartModify/EndModify block - # - self.fewerModifyFlagCheckBox = qt.QCheckBox() - self.fewerModifyFlagCheckBox.checked = 0 - self.fewerModifyFlagCheckBox.toolTip = 'If checked, wrap adding points inside of a StartModify - EndModify block' - parametersFormLayout.addRow("Fewer modify events: ", self.fewerModifyFlagCheckBox) - - # - # markups locked - # - self.lockedFlagCheckBox = qt.QCheckBox() - self.lockedFlagCheckBox.checked = 0 - self.lockedFlagCheckBox.toolTip = 'If checked, markups will be locked for editing' - parametersFormLayout.addRow("Locked nodes: ", self.lockedFlagCheckBox) - - # - # markups labels hidden - # - self.labelsHiddenFlagCheckBox = qt.QCheckBox() - self.labelsHiddenFlagCheckBox.checked = 0 - self.labelsHiddenFlagCheckBox.toolTip = 'If checked, markups labels will be forced to be hidden, regardless of default markups properties' - parametersFormLayout.addRow("Labels hidden: ", self.labelsHiddenFlagCheckBox) - - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the algorithm." - self.applyButton.enabled = True - parametersFormLayout.addRow(self.applyButton) - - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) - - # Add vertical spacer - self.layout.addStretch(1) - - def cleanup(self): - pass - - def onApplyButton(self): - logic = AddManyMarkupsFiducialTestLogic() - nodeType = self.nodeTypeComboBox.currentText - numberOfNodes = int(self.numberOfNodesSliderWidget.value) - numberOfControlPoints = int(self.numberOfControlPointsSliderWidget.value) - fewerModifyFlag = self.fewerModifyFlagCheckBox.checked - labelsHiddenFlag = self.labelsHiddenFlagCheckBox.checked - locked = self.lockedFlagCheckBox.checked - print(f"Run the logic method to add {numberOfNodes} nodes with {numberOfControlPoints} control points each") - logic.run(nodeType, numberOfNodes, numberOfControlPoints,0, fewerModifyFlag, locked, labelsHiddenFlag) + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... + + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) + + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + + # + # node type to add + # + self.nodeTypeComboBox = qt.QComboBox() + self.nodeTypeComboBox.addItem("vtkMRMLMarkupsFiducialNode") + self.nodeTypeComboBox.addItem("vtkMRMLMarkupsLineNode") + self.nodeTypeComboBox.addItem("vtkMRMLMarkupsAngleNode") + self.nodeTypeComboBox.addItem("vtkMRMLMarkupsCurveNode") + self.nodeTypeComboBox.addItem("vtkMRMLMarkupsClosedCurveNode") + self.nodeTypeComboBox.addItem("vtkMRMLMarkupsROINode") + parametersFormLayout.addRow("Node type: ", self.nodeTypeComboBox) + + # + # number of nodes to add + # + self.numberOfNodesSliderWidget = ctk.ctkSliderWidget() + self.numberOfNodesSliderWidget.singleStep = 1.0 + self.numberOfNodesSliderWidget.decimals = 0 + self.numberOfNodesSliderWidget.minimum = 0.0 + self.numberOfNodesSliderWidget.maximum = 1000.0 + self.numberOfNodesSliderWidget.value = 1.0 + self.numberOfNodesSliderWidget.toolTip = "Set the number of nodes to add." + parametersFormLayout.addRow("Number of nodes: ", self.numberOfNodesSliderWidget) + + # + # number of fiducials to add + # + self.numberOfControlPointsSliderWidget = ctk.ctkSliderWidget() + self.numberOfControlPointsSliderWidget.singleStep = 1.0 + self.numberOfControlPointsSliderWidget.decimals = 0 + self.numberOfControlPointsSliderWidget.minimum = 0.0 + self.numberOfControlPointsSliderWidget.maximum = 10000.0 + self.numberOfControlPointsSliderWidget.value = 500.0 + self.numberOfControlPointsSliderWidget.toolTip = "Set the number of control points to add per node." + parametersFormLayout.addRow("Number of control points: ", self.numberOfControlPointsSliderWidget) + + # + # check box to trigger fewer modify events, adding all the new points + # is wrapped inside of a StartModify/EndModify block + # + self.fewerModifyFlagCheckBox = qt.QCheckBox() + self.fewerModifyFlagCheckBox.checked = 0 + self.fewerModifyFlagCheckBox.toolTip = 'If checked, wrap adding points inside of a StartModify - EndModify block' + parametersFormLayout.addRow("Fewer modify events: ", self.fewerModifyFlagCheckBox) + + # + # markups locked + # + self.lockedFlagCheckBox = qt.QCheckBox() + self.lockedFlagCheckBox.checked = 0 + self.lockedFlagCheckBox.toolTip = 'If checked, markups will be locked for editing' + parametersFormLayout.addRow("Locked nodes: ", self.lockedFlagCheckBox) + + # + # markups labels hidden + # + self.labelsHiddenFlagCheckBox = qt.QCheckBox() + self.labelsHiddenFlagCheckBox.checked = 0 + self.labelsHiddenFlagCheckBox.toolTip = 'If checked, markups labels will be forced to be hidden, regardless of default markups properties' + parametersFormLayout.addRow("Labels hidden: ", self.labelsHiddenFlagCheckBox) + + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the algorithm." + self.applyButton.enabled = True + parametersFormLayout.addRow(self.applyButton) + + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) + + # Add vertical spacer + self.layout.addStretch(1) + + def cleanup(self): + pass + + def onApplyButton(self): + logic = AddManyMarkupsFiducialTestLogic() + nodeType = self.nodeTypeComboBox.currentText + numberOfNodes = int(self.numberOfNodesSliderWidget.value) + numberOfControlPoints = int(self.numberOfControlPointsSliderWidget.value) + fewerModifyFlag = self.fewerModifyFlagCheckBox.checked + labelsHiddenFlag = self.labelsHiddenFlagCheckBox.checked + locked = self.lockedFlagCheckBox.checked + print(f"Run the logic method to add {numberOfNodes} nodes with {numberOfControlPoints} control points each") + logic.run(nodeType, numberOfNodes, numberOfControlPoints, 0, fewerModifyFlag, locked, labelsHiddenFlag) # @@ -145,104 +145,104 @@ def onApplyButton(self): class AddManyMarkupsFiducialTestLogic(ScriptedLoadableModuleLogic): - def run(self, nodeType, numberOfNodes = 10, numberOfControlPoints=10, rOffset=0,usefewerModifyCalls=False,locked=False, labelsHidden=False): - """ - Run the actual algorithm - """ - print(f'Running test to add {numberOfNodes} nodes markups with {numberOfControlPoints} control points') - print('Index\tTime to add fid\tDelta between adds') - print("%(index)04s\t" % {'index': "i"}, "t\tdt'") - r = rOffset - a = 0 - s = 0 - t1 = 0 - t2 = 0 - t3 = 0 - t4 = 0 - timeToAddThisFid = 0 - timeToAddLastFid = 0 - - testStartTime = time.process_time() - - import random - - if usefewerModifyCalls: - print("Pause render") - slicer.app.pauseRender() - - for nodeIndex in range(numberOfNodes): - - markupsNode = slicer.mrmlScene.AddNewNodeByClass(nodeType) - markupsNode.CreateDefaultDisplayNodes() - if locked: - markupsNode.SetLocked(True) - - if labelsHidden: - markupsNode.GetDisplayNode().SetPropertiesLabelVisibility(False) - markupsNode.GetDisplayNode().SetPointLabelsVisibility(False) - - if usefewerModifyCalls: - print("Start modify") - mod = markupsNode.StartModify() - - for controlPointIndex in range(numberOfControlPoints): - # print "controlPointIndex = ", controlPointIndex, "/", numberOfControlPoints, ", r = ", r, ", a = ", a, ", s = ", s - t1 = time.process_time() - markupsNode.AddControlPoint(vtk.vtkVector3d(r,a,s)) - t2 = time.process_time() - timeToAddThisFid = t2 - t1 - dt = timeToAddThisFid - timeToAddLastFid - #print '%(index)04d\t' % {'index': controlPointIndex}, timeToAddThisFid, "\t", dt - r = float(controlPointIndex)/numberOfControlPoints * 100.0 - 50.0 + random.uniform(-20.0, 20.0) - a = float(controlPointIndex)/numberOfControlPoints * 100.0 - 50.0 + random.uniform(-20.0, 20.0) - s = random.uniform(-20.0, 20.0) - timeToAddLastFid = timeToAddThisFid - - if usefewerModifyCalls: - markupsNode.EndModify(mod) - - if usefewerModifyCalls: - print("Resume render") - slicer.app.resumeRender() - - testEndTime = time.process_time() - testTime = testEndTime - testStartTime - print("Total time to add ",numberOfControlPoints," = ", testTime) - - return True + def run(self, nodeType, numberOfNodes=10, numberOfControlPoints=10, rOffset=0, usefewerModifyCalls=False, locked=False, labelsHidden=False): + """ + Run the actual algorithm + """ + print(f'Running test to add {numberOfNodes} nodes markups with {numberOfControlPoints} control points') + print('Index\tTime to add fid\tDelta between adds') + print("%(index)04s\t" % {'index': "i"}, "t\tdt'") + r = rOffset + a = 0 + s = 0 + t1 = 0 + t2 = 0 + t3 = 0 + t4 = 0 + timeToAddThisFid = 0 + timeToAddLastFid = 0 + + testStartTime = time.process_time() + + import random + + if usefewerModifyCalls: + print("Pause render") + slicer.app.pauseRender() + + for nodeIndex in range(numberOfNodes): + + markupsNode = slicer.mrmlScene.AddNewNodeByClass(nodeType) + markupsNode.CreateDefaultDisplayNodes() + if locked: + markupsNode.SetLocked(True) + + if labelsHidden: + markupsNode.GetDisplayNode().SetPropertiesLabelVisibility(False) + markupsNode.GetDisplayNode().SetPointLabelsVisibility(False) + + if usefewerModifyCalls: + print("Start modify") + mod = markupsNode.StartModify() + + for controlPointIndex in range(numberOfControlPoints): + # print "controlPointIndex = ", controlPointIndex, "/", numberOfControlPoints, ", r = ", r, ", a = ", a, ", s = ", s + t1 = time.process_time() + markupsNode.AddControlPoint(vtk.vtkVector3d(r, a, s)) + t2 = time.process_time() + timeToAddThisFid = t2 - t1 + dt = timeToAddThisFid - timeToAddLastFid + # print '%(index)04d\t' % {'index': controlPointIndex}, timeToAddThisFid, "\t", dt + r = float(controlPointIndex) / numberOfControlPoints * 100.0 - 50.0 + random.uniform(-20.0, 20.0) + a = float(controlPointIndex) / numberOfControlPoints * 100.0 - 50.0 + random.uniform(-20.0, 20.0) + s = random.uniform(-20.0, 20.0) + timeToAddLastFid = timeToAddThisFid + + if usefewerModifyCalls: + markupsNode.EndModify(mod) + + if usefewerModifyCalls: + print("Resume render") + slicer.app.resumeRender() + + testEndTime = time.process_time() + testTime = testEndTime - testStartTime + print("Total time to add ", numberOfControlPoints, " = ", testTime) + + return True class AddManyMarkupsFiducialTestTest(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. + 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 """ - self.setUp() - self.test_AddManyMarkupsFiducialTest1() - def test_AddManyMarkupsFiducialTest1(self): + 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_AddManyMarkupsFiducialTest1() + + def test_AddManyMarkupsFiducialTest1(self): - self.delayDisplay("Starting the add many Markups fiducials test") + self.delayDisplay("Starting the add many Markups fiducials test") - # start in the welcome module - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('Welcome') + # start in the welcome module + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('Welcome') - logic = AddManyMarkupsFiducialTestLogic() - logic.run('vtkMRMLMarkupsFiducialNode', numberOfNodes = 1, numberOfControlPoints=100, rOffset=0) + logic = AddManyMarkupsFiducialTestLogic() + logic.run('vtkMRMLMarkupsFiducialNode', numberOfNodes=1, numberOfControlPoints=100, rOffset=0) - self.delayDisplay("Now running it while the Markups Module is open") - m.moduleSelector().selectModule('Markups') - logic.run('vtkMRMLMarkupsFiducialNode', numberOfNodes = 1, numberOfControlPoints=100, rOffset=100) + self.delayDisplay("Now running it while the Markups Module is open") + m.moduleSelector().selectModule('Markups') + logic.run('vtkMRMLMarkupsFiducialNode', numberOfNodes=1, numberOfControlPoints=100, rOffset=100) - self.delayDisplay('Test passed!') + self.delayDisplay('Test passed!') diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsCurveCoordinateFrameTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsCurveCoordinateFrameTest.py index d861efcea76..c1bb5b784ef 100644 --- a/Modules/Loadable/Markups/Testing/Python/MarkupsCurveCoordinateFrameTest.py +++ b/Modules/Loadable/Markups/Testing/Python/MarkupsCurveCoordinateFrameTest.py @@ -12,68 +12,68 @@ def updateCoordinateSystemsModel(updateInfo): - model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer = updateInfo - coordinateSystemAppender.RemoveAllInputs() - numberOfCurvePoints = curve.GetCurvePointsWorld().GetNumberOfPoints() - for curvePointIndex in range(numberOfCurvePoints): - result = curve.GetCurvePointToWorldTransformAtPointIndex(curvePointIndex, curvePointToWorldTransform) - transform.SetMatrix(curvePointToWorldTransform) - transformer.Update() - coordinateSystemInWorld = vtk.vtkPolyData() - coordinateSystemInWorld.DeepCopy(transformer.GetOutput()) - coordinateSystemAppender.AddInputData(coordinateSystemInWorld) - coordinateSystemAppender.Update() - model.SetAndObservePolyData(coordinateSystemAppender.GetOutput()) + model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer = updateInfo + coordinateSystemAppender.RemoveAllInputs() + numberOfCurvePoints = curve.GetCurvePointsWorld().GetNumberOfPoints() + for curvePointIndex in range(numberOfCurvePoints): + result = curve.GetCurvePointToWorldTransformAtPointIndex(curvePointIndex, curvePointToWorldTransform) + transform.SetMatrix(curvePointToWorldTransform) + transformer.Update() + coordinateSystemInWorld = vtk.vtkPolyData() + coordinateSystemInWorld.DeepCopy(transformer.GetOutput()) + coordinateSystemAppender.AddInputData(coordinateSystemInWorld) + coordinateSystemAppender.Update() + model.SetAndObservePolyData(coordinateSystemAppender.GetOutput()) def createCoordinateSystemsModel(curve, axisLength=5): - """Add a coordinate system model at each curve point. - :param curve: input curve - :param axisLength: length of normal axis is `axisLength*2`, length of binormal and tangent axes are `axisLength` - :return model, coordinateSystemAppender, curvePointToWorldTransform, transform - """ - # Create coordinate system polydata - axisAppender = vtk.vtkAppendPolyData() - xAxis = vtk.vtkLineSource() - xAxis.SetPoint1(0,0,0) - xAxis.SetPoint2(axisLength * 2, 0, 0) - yAxis = vtk.vtkLineSource() - yAxis.SetPoint1(0,0,0) - yAxis.SetPoint2(0, axisLength, 0) - zAxis = vtk.vtkLineSource() - zAxis.SetPoint1(0,0,0) - zAxis.SetPoint2(0, 0, axisLength) - axisAppender.AddInputConnection(xAxis.GetOutputPort()) - axisAppender.AddInputConnection(yAxis.GetOutputPort()) - axisAppender.AddInputConnection(zAxis.GetOutputPort()) - # Initialize transformer that will place the coordinate system polydata along the curve - curvePointToWorldTransform = vtk.vtkMatrix4x4() - transformer = vtk.vtkTransformPolyDataFilter() - transform = vtk.vtkTransform() - transformer.SetTransform(transform) - transformer.SetInputConnection(axisAppender.GetOutputPort()) - # Create model appender that assembles the model that contains all the coordinate systems - coordinateSystemAppender = vtk.vtkAppendPolyData() - #model = slicer.modules.models.logic().AddModel(coordinateSystemAppender.GetOutputPort()) - model = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLModelNode') - model.CreateDefaultDisplayNodes() - # prevent picking by markups so that the control points can be moved without sticking to the generated model - model.SetSelectable(False) - updateInfo = [model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer] - updateCoordinateSystemsModel(updateInfo) - return updateInfo + """Add a coordinate system model at each curve point. + :param curve: input curve + :param axisLength: length of normal axis is `axisLength*2`, length of binormal and tangent axes are `axisLength` + :return model, coordinateSystemAppender, curvePointToWorldTransform, transform + """ + # Create coordinate system polydata + axisAppender = vtk.vtkAppendPolyData() + xAxis = vtk.vtkLineSource() + xAxis.SetPoint1(0, 0, 0) + xAxis.SetPoint2(axisLength * 2, 0, 0) + yAxis = vtk.vtkLineSource() + yAxis.SetPoint1(0, 0, 0) + yAxis.SetPoint2(0, axisLength, 0) + zAxis = vtk.vtkLineSource() + zAxis.SetPoint1(0, 0, 0) + zAxis.SetPoint2(0, 0, axisLength) + axisAppender.AddInputConnection(xAxis.GetOutputPort()) + axisAppender.AddInputConnection(yAxis.GetOutputPort()) + axisAppender.AddInputConnection(zAxis.GetOutputPort()) + # Initialize transformer that will place the coordinate system polydata along the curve + curvePointToWorldTransform = vtk.vtkMatrix4x4() + transformer = vtk.vtkTransformPolyDataFilter() + transform = vtk.vtkTransform() + transformer.SetTransform(transform) + transformer.SetInputConnection(axisAppender.GetOutputPort()) + # Create model appender that assembles the model that contains all the coordinate systems + coordinateSystemAppender = vtk.vtkAppendPolyData() + # model = slicer.modules.models.logic().AddModel(coordinateSystemAppender.GetOutputPort()) + model = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLModelNode') + model.CreateDefaultDisplayNodes() + # prevent picking by markups so that the control points can be moved without sticking to the generated model + model.SetSelectable(False) + updateInfo = [model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer] + updateCoordinateSystemsModel(updateInfo) + return updateInfo def addCoordinateSystemUpdater(updateInfo): - model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer = updateInfo - observation = curve.AddObserver(slicer.vtkMRMLMarkupsNode.PointModifiedEvent, - lambda caller, eventData, updateInfo=updateInfo: updateCoordinateSystemsModel(updateInfo)) - return [curve, observation] + model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer = updateInfo + observation = curve.AddObserver(slicer.vtkMRMLMarkupsNode.PointModifiedEvent, + lambda caller, eventData, updateInfo=updateInfo: updateCoordinateSystemsModel(updateInfo)) + return [curve, observation] def removeCoordinateSystemUpdaters(curveObservations): - for curve, observer in curveObservations: - curve.RemoveObserver(observer) + for curve, observer in curveObservations: + curve.RemoveObserver(observer) # @@ -83,7 +83,7 @@ def removeCoordinateSystemUpdaters(curveObservations): curveMeasurementsTestDir = slicer.app.temporaryPath + '/curveMeasurementsTest' print('Test directory: ', curveMeasurementsTestDir) if not os.access(curveMeasurementsTestDir, os.F_OK): - os.mkdir(curveMeasurementsTestDir) + os.mkdir(curveMeasurementsTestDir) curvePointToWorldTransform = vtk.vtkMatrix4x4() @@ -95,9 +95,9 @@ def removeCoordinateSystemUpdaters(curveObservations): testSceneFilePath = curveMeasurementsTestDir + '/MarkupsCurvatureTestScene.mrb' slicer.util.downloadFile( - TESTING_DATA_URL + 'SHA256/5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32', - testSceneFilePath, - checksum='SHA256:5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32') + TESTING_DATA_URL + 'SHA256/5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32', + testSceneFilePath, + checksum='SHA256:5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32') slicer.util.loadScene(testSceneFilePath) planarCurveNode = slicer.util.getNode('C') @@ -109,17 +109,17 @@ def removeCoordinateSystemUpdaters(curveObservations): # Check quantitative results if not planarCurveNode.GetCurvePointToWorldTransformAtPointIndex(6, curvePointToWorldTransform): - raise Exception("Test1 GetCurvePointToWorldTransformAtPointIndex failed") + raise Exception("Test1 GetCurvePointToWorldTransformAtPointIndex failed") -curvePointToWorldMatrix= slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform) +curvePointToWorldMatrix = slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform) expectedCurvePointToWorldMatrix = np.array( - [[ 2.15191499e-01, 0.00000000e+00, -9.76571871e-01, -3.03394470e+01], - [ 0.00000000e+00, -1.00000000e+00, 0.00000000e+00, 3.63797881e-09], - [-9.76571871e-01, 0.00000000e+00, -2.15191499e-01, 8.10291061e+01], - [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) + [[2.15191499e-01, 0.00000000e+00, -9.76571871e-01, -3.03394470e+01], + [0.00000000e+00, -1.00000000e+00, 0.00000000e+00, 3.63797881e-09], + [-9.76571871e-01, 0.00000000e+00, -2.15191499e-01, 8.10291061e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) if not np.isclose(curvePointToWorldMatrix, expectedCurvePointToWorldMatrix).all(): - raise Exception(f"Test1 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.") + raise Exception(f"Test1 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.") # # Test2. Test free-form closed curve with 6 control points, all points in one plane @@ -128,8 +128,8 @@ def removeCoordinateSystemUpdaters(curveObservations): closedCurveNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsClosedCurveNode') pos = np.zeros(3) for i in range(planarCurveNode.GetNumberOfControlPoints()): - planarCurveNode.GetNthControlPointPosition(i, pos) - pointIndex = closedCurveNode.AddControlPoint(vtk.vtkVector3d(pos)) + planarCurveNode.GetNthControlPointPosition(i, pos) + pointIndex = closedCurveNode.AddControlPoint(vtk.vtkVector3d(pos)) # Visualize @@ -139,17 +139,17 @@ def removeCoordinateSystemUpdaters(curveObservations): # Check quantitative results if not closedCurveNode.GetCurvePointToWorldTransformAtPointIndex(6, curvePointToWorldTransform): - raise Exception("Test2 GetCurvePointToWorldTransformAtPointIndex failed") + raise Exception("Test2 GetCurvePointToWorldTransformAtPointIndex failed") -curvePointToWorldMatrix= slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform) +curvePointToWorldMatrix = slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform) expectedCurvePointToWorldMatrix = np.array( - [[-3.85813409e-01, 0.00000000e+00, -9.22576833e-01, -3.71780586e+01], - [ 0.00000000e+00, 1.00000000e+00, 0.00000000e+00, 3.63797881e-09], - [ 9.22576833e-01, 0.00000000e+00, -3.85813409e-01, 8.78303909e+01], - [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) + [[-3.85813409e-01, 0.00000000e+00, -9.22576833e-01, -3.71780586e+01], + [0.00000000e+00, 1.00000000e+00, 0.00000000e+00, 3.63797881e-09], + [9.22576833e-01, 0.00000000e+00, -3.85813409e-01, 8.78303909e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) if not np.isclose(curvePointToWorldMatrix, expectedCurvePointToWorldMatrix).all(): - raise Exception(f"Test2 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.") + raise Exception(f"Test2 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.") # # Test3. Test on a vessel centerline curve @@ -161,9 +161,9 @@ def removeCoordinateSystemUpdaters(curveObservations): testSceneFilePath = curveMeasurementsTestDir + '/MarkupsControlPointMeasurementInterpolationTestScene.mrb' slicer.util.downloadFile( - TESTING_DATA_URL + 'SHA256/b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344', - testSceneFilePath, - checksum='SHA256:b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344') + TESTING_DATA_URL + 'SHA256/b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344', + testSceneFilePath, + checksum='SHA256:b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344') # Import test scene slicer.util.loadScene(testSceneFilePath) @@ -177,7 +177,7 @@ def removeCoordinateSystemUpdaters(curveObservations): centerlineCurve.SetName('CenterlineCurve') for i in range(centerlinePolyData.GetNumberOfPoints()): - pointIndex = centerlineCurve.AddControlPoint(vtk.vtkVector3d(centerlinePolyData.GetPoint(i))) + pointIndex = centerlineCurve.AddControlPoint(vtk.vtkVector3d(centerlinePolyData.GetPoint(i))) # Spacing between control points is not uniform. Applying b-spline interpolation to it directly would # cause discontinuities (overshoots where a large gap between points is followed by points close to each other). @@ -194,17 +194,17 @@ def removeCoordinateSystemUpdaters(curveObservations): # Check quantitative results if not centerlineCurve.GetCurvePointToWorldTransformAtPointIndex(6, curvePointToWorldTransform): - raise Exception("Test3 GetCurvePointToWorldTransformAtPointIndex failed") + raise Exception("Test3 GetCurvePointToWorldTransformAtPointIndex failed") -curvePointToWorldMatrix= slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform) +curvePointToWorldMatrix = slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform) expectedCurvePointToWorldMatrix = np.array( - [[ 9.85648052e-01, 8.80625424e-03, -1.68583415e-01, -3.08991909e+00], - [-6.35257659e-02, 9.44581803e-01, -3.22070946e-01, 2.22146526e-01], - [ 1.56404587e-01, 3.28157991e-01, 9.31584638e-01, -7.34501495e+01], - [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) + [[9.85648052e-01, 8.80625424e-03, -1.68583415e-01, -3.08991909e+00], + [-6.35257659e-02, 9.44581803e-01, -3.22070946e-01, 2.22146526e-01], + [1.56404587e-01, 3.28157991e-01, 9.31584638e-01, -7.34501495e+01], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) if not np.isclose(curvePointToWorldMatrix, expectedCurvePointToWorldMatrix).all(): - raise Exception(f"Test3 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.") + raise Exception(f"Test3 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.") # # Test4. curvature computation for a circle-shaped closed curve @@ -215,7 +215,7 @@ def removeCoordinateSystemUpdaters(curveObservations): import math circleCurveNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsClosedCurveNode") for controlPointIndex in range(numberOfControlPoints): - angle = 2.0*math.pi * controlPointIndex/numberOfControlPoints + angle = 2.0 * math.pi * controlPointIndex / numberOfControlPoints pointIndex = circleCurveNode.AddControlPoint(vtk.vtkVector3d(radius * math.sin(angle), radius * math.cos(angle), 0.0)) # Visualize @@ -226,17 +226,17 @@ def removeCoordinateSystemUpdaters(curveObservations): # Check quantitative results if not circleCurveNode.GetCurvePointToWorldTransformAtPointIndex(6, curvePointToWorldTransform): - raise Exception("Test4. GetCurvePointToWorldTransformAtPointIndex failed") + raise Exception("Test4. GetCurvePointToWorldTransformAtPointIndex failed") -curvePointToWorldMatrix= slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform) +curvePointToWorldMatrix = slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform) expectedCurvePointToWorldMatrix = np.array( - [[ 0.10190135, 0. , 0.99479451, 3.29378772], - [ 0.99479451, 0. , -0.10190135, 34.84461594], - [ 0. , 1. , 0. , 0. ], - [ 0. , 0. , 0. , 1. ]]) + [[0.10190135, 0., 0.99479451, 3.29378772], + [0.99479451, 0., -0.10190135, 34.84461594], + [0., 1., 0., 0.], + [0., 0., 0., 1.]]) if not np.isclose(curvePointToWorldMatrix, expectedCurvePointToWorldMatrix).all(): - raise Exception(f"Test4. CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.") + raise Exception(f"Test4. CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.") # # Remove observations diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsCurveMeasurementsTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsCurveMeasurementsTest.py index 1cd5e1110f9..5b13f927174 100644 --- a/Modules/Loadable/Markups/Testing/Python/MarkupsCurveMeasurementsTest.py +++ b/Modules/Loadable/Markups/Testing/Python/MarkupsCurveMeasurementsTest.py @@ -10,14 +10,14 @@ curveMeasurementsTestDir = slicer.app.temporaryPath + '/curveMeasurementsTest' print('Test directory: ', curveMeasurementsTestDir) if not os.access(curveMeasurementsTestDir, os.F_OK): - os.mkdir(curveMeasurementsTestDir) + os.mkdir(curveMeasurementsTestDir) testSceneFilePath = curveMeasurementsTestDir + '/MarkupsCurvatureTestScene.mrb' slicer.util.downloadFile( - TESTING_DATA_URL + 'SHA256/5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32', - testSceneFilePath, - checksum='SHA256:5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32') + TESTING_DATA_URL + 'SHA256/5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32', + testSceneFilePath, + checksum='SHA256:5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32') # Import test scene slicer.util.loadScene(testSceneFilePath) @@ -26,8 +26,8 @@ # Check number of arrays in the curve node curvePointData = curveNode.GetCurveWorld().GetPointData() if curvePointData.GetNumberOfArrays() != 1: - exceptionMessage = f"Unexpected number of data arrays in curve: {curvePointData.GetNumberOfArrays()} (expected 1)" - raise Exception(exceptionMessage) + exceptionMessage = f"Unexpected number of data arrays in curve: {curvePointData.GetNumberOfArrays()} (expected 1)" + raise Exception(exceptionMessage) # Turn on curvature calculation in curve node curveNode.GetMeasurement("curvature max").SetEnabled(True) @@ -35,31 +35,31 @@ # Check curvature computation result curvePointData = curveNode.GetCurveWorld().GetPointData() if curvePointData.GetNumberOfArrays() != 2: - exceptionMessage = f"Unexpected number of data arrays in curve: {curvePointData.GetNumberOfArrays()} (expected 2)" - raise Exception(exceptionMessage) + exceptionMessage = f"Unexpected number of data arrays in curve: {curvePointData.GetNumberOfArrays()} (expected 2)" + raise Exception(exceptionMessage) if curvePointData.GetArrayName(1) != 'Curvature': - exceptionMessage = f"Unexpected data array name in curve: {curvePointData.GetArrayName(1)} (expected 'Curvature')" - raise Exception(exceptionMessage) + exceptionMessage = f"Unexpected data array name in curve: {curvePointData.GetArrayName(1)} (expected 'Curvature')" + raise Exception(exceptionMessage) curvatureArray = curvePointData.GetArray(1) -if curvatureArray.GetMaxId() != curvePointData.GetNumberOfTuples()-1: - exceptionMessage = "Unexpected number of values in curvature data array: %d (expected %d)" % (curvatureArray.GetMaxId(), curvePointData.GetNumberOfTuples()-1) - raise Exception(exceptionMessage) +if curvatureArray.GetMaxId() != curvePointData.GetNumberOfTuples() - 1: + exceptionMessage = "Unexpected number of values in curvature data array: %d (expected %d)" % (curvatureArray.GetMaxId(), curvePointData.GetNumberOfTuples() - 1) + raise Exception(exceptionMessage) if abs(curvatureArray.GetRange()[0] - 0.0) > 0.0001: - exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0]) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0]) + raise Exception(exceptionMessage) if abs(curvatureArray.GetRange()[1] - 0.9816015970208652) > 0.0001: - exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1]) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1]) + raise Exception(exceptionMessage) # Turn off curvature computation curveNode.GetMeasurement("curvature max").SetEnabled(False) curvePointData = curveNode.GetCurveWorld().GetPointData() if curvePointData.GetNumberOfArrays() != 1: - exceptionMessage = "Unexpected number of data arrays in curve: " + str(curvePointData.GetNumberOfArrays()) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected number of data arrays in curve: " + str(curvePointData.GetNumberOfArrays()) + raise Exception(exceptionMessage) print('Open curve curvature test finished successfully') @@ -70,30 +70,30 @@ closedCurveNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsClosedCurveNode') pos = np.zeros(3) for i in range(curveNode.GetNumberOfControlPoints()): - curveNode.GetNthControlPointPosition(i, pos) - closedCurveNode.AddControlPoint(vtk.vtkVector3d(pos)) + curveNode.GetNthControlPointPosition(i, pos) + closedCurveNode.AddControlPoint(vtk.vtkVector3d(pos)) closedCurveNode.GetMeasurement("curvature mean").SetEnabled(True) curvePointData = closedCurveNode.GetCurveWorld().GetPointData() if curvePointData.GetNumberOfArrays() != 2: - exceptionMessage = "Unexpected number of data arrays in curve: " + str(curvePointData.GetNumberOfArrays()) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected number of data arrays in curve: " + str(curvePointData.GetNumberOfArrays()) + raise Exception(exceptionMessage) if curvePointData.GetArrayName(1) != 'Curvature': - exceptionMessage = "Unexpected data array name in curve: " + str(curvePointData.GetArrayName(1)) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected data array name in curve: " + str(curvePointData.GetArrayName(1)) + raise Exception(exceptionMessage) curvatureArray = curvePointData.GetArray(1) -if curvatureArray.GetMaxId() != curvePointData.GetNumberOfTuples()-1: - exceptionMessage = "Unexpected number of values in curvature data array: %d (expected %d)" % (curvatureArray.GetMaxId(), curvePointData.GetNumberOfTuples()-1) - raise Exception(exceptionMessage) +if curvatureArray.GetMaxId() != curvePointData.GetNumberOfTuples() - 1: + exceptionMessage = "Unexpected number of values in curvature data array: %d (expected %d)" % (curvatureArray.GetMaxId(), curvePointData.GetNumberOfTuples() - 1) + raise Exception(exceptionMessage) if abs(curvatureArray.GetRange()[0] - 0.0) > 0.0001: - exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0]) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0]) + raise Exception(exceptionMessage) if abs(curvatureArray.GetRange()[1] - 0.26402460470400924) > 0.0001: - exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1]) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1]) + raise Exception(exceptionMessage) print('Closed curve curvature test finished successfully') @@ -109,9 +109,9 @@ testSceneFilePath = curveMeasurementsTestDir + '/MarkupsControlPointMeasurementInterpolationTestScene.mrb' slicer.util.downloadFile( - TESTING_DATA_URL + 'SHA256/b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344', - testSceneFilePath, - checksum='SHA256:b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344') + TESTING_DATA_URL + 'SHA256/b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344', + testSceneFilePath, + checksum='SHA256:b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344') # Import test scene slicer.util.loadScene(testSceneFilePath) @@ -124,7 +124,7 @@ centerlineCurve = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsCurveNode') centerlineCurve.SetName('CenterlineCurve') for i in range(centerlinePolyData.GetNumberOfPoints()): - centerlineCurve.AddControlPoint(vtk.vtkVector3d(centerlinePolyData.GetPoint(i))) + centerlineCurve.AddControlPoint(vtk.vtkVector3d(centerlinePolyData.GetPoint(i))) # Add radius data to centerline curve as measurement radiusMeasurement = slicer.vtkMRMLStaticMeasurement() @@ -138,36 +138,36 @@ # Check interpolation computation result if centerlineCurvePointData.GetNumberOfArrays() != 2: - exceptionMessage = "Unexpected number of data arrays in curve: " + str(centerlineCurvePointData.GetNumberOfArrays()) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected number of data arrays in curve: " + str(centerlineCurvePointData.GetNumberOfArrays()) + raise Exception(exceptionMessage) if centerlineCurvePointData.GetArrayName(1) != 'Radius': - exceptionMessage = "Unexpected data array name in curve: " + str(centerlineCurvePointData.GetArrayName(1)) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected data array name in curve: " + str(centerlineCurvePointData.GetArrayName(1)) + raise Exception(exceptionMessage) interpolatedRadiusArray = centerlineCurvePointData.GetArray(1) if interpolatedRadiusArray.GetNumberOfTuples() != 571: - exceptionMessage = "Unexpected number of data points in interpolated radius array: " + str(interpolatedRadiusArray.GetNumberOfTuples()) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected number of data points in interpolated radius array: " + str(interpolatedRadiusArray.GetNumberOfTuples()) + raise Exception(exceptionMessage) if abs(interpolatedRadiusArray.GetRange()[0] - 12.322814731747465) > 0.0001: - exceptionMessage = "Unexpected minimum in curvature data array: " + str(interpolatedRadiusArray.GetRange()[0]) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected minimum in curvature data array: " + str(interpolatedRadiusArray.GetRange()[0]) + raise Exception(exceptionMessage) if abs(interpolatedRadiusArray.GetRange()[1] - 42.9542138185081) > 0.0001: - exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetRange()[1]) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetRange()[1]) + raise Exception(exceptionMessage) if abs(interpolatedRadiusArray.GetValue(9) - 42.92838813390291) > 0.0001: - exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(9)) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(9)) + raise Exception(exceptionMessage) if abs(interpolatedRadiusArray.GetValue(10) - 42.9542138185081) > 0.0001: - exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(10)) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(10)) + raise Exception(exceptionMessage) if abs(interpolatedRadiusArray.GetValue(569) - 12.904227531040913) > 0.0001: - exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(569)) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(569)) + raise Exception(exceptionMessage) if abs(interpolatedRadiusArray.GetValue(570) - 12.765926543271583) > 0.0001: - exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(570)) - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(570)) + raise Exception(exceptionMessage) print('Control point measurement interpolation test finished successfully') @@ -180,7 +180,7 @@ import math closedCurveNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsClosedCurveNode") for controlPointIndex in range(numberOfControlPoints): - angle = 2.0*math.pi * controlPointIndex/numberOfControlPoints + angle = 2.0 * math.pi * controlPointIndex / numberOfControlPoints closedCurveNode.AddControlPoint(vtk.vtkVector3d(radius * math.sin(angle), radius * math.cos(angle), 0.0)) # Turn on curvature calculation in curve node @@ -188,36 +188,36 @@ closedCurveNode.GetMeasurement("curvature max").SetEnabled(True) curvatureArray = closedCurveNode.GetCurveWorld().GetPointData().GetArray('Curvature') if curvatureArray.GetNumberOfValues() < 10: - exceptionMessage = "Many values are expected in the curvature array, instead found just %d" % curvatureArray.GetNumberOfValues() - raise Exception(exceptionMessage) - -if abs(curvatureArray.GetRange()[0] - 1/radius) > 1e-4: - exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0]) - raise Exception(exceptionMessage) -if abs(curvatureArray.GetRange()[1] - 1/radius) > 1e-4: - exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1]) - raise Exception(exceptionMessage) -if abs(curvatureArray.GetRange()[1] - 1/radius) > 1e-4: - exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1]) - raise Exception(exceptionMessage) -if abs(closedCurveNode.GetMeasurement("curvature mean").GetValue() - 1/radius) > 1e-4: - exceptionMessage = "Unexpected curvature mean value: " + str(closedCurveNode.GetMeasurement("curvature mean").GetValue()) - raise Exception(exceptionMessage) -if abs(closedCurveNode.GetMeasurement("curvature max").GetValue() - 1/radius) > 1e-4: - exceptionMessage = "Unexpected curvature max value: " + str(closedCurveNode.GetMeasurement("curvature max").GetValue()) - raise Exception(exceptionMessage) + exceptionMessage = "Many values are expected in the curvature array, instead found just %d" % curvatureArray.GetNumberOfValues() + raise Exception(exceptionMessage) + +if abs(curvatureArray.GetRange()[0] - 1 / radius) > 1e-4: + exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0]) + raise Exception(exceptionMessage) +if abs(curvatureArray.GetRange()[1] - 1 / radius) > 1e-4: + exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1]) + raise Exception(exceptionMessage) +if abs(curvatureArray.GetRange()[1] - 1 / radius) > 1e-4: + exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1]) + raise Exception(exceptionMessage) +if abs(closedCurveNode.GetMeasurement("curvature mean").GetValue() - 1 / radius) > 1e-4: + exceptionMessage = "Unexpected curvature mean value: " + str(closedCurveNode.GetMeasurement("curvature mean").GetValue()) + raise Exception(exceptionMessage) +if abs(closedCurveNode.GetMeasurement("curvature max").GetValue() - 1 / radius) > 1e-4: + exceptionMessage = "Unexpected curvature max value: " + str(closedCurveNode.GetMeasurement("curvature max").GetValue()) + raise Exception(exceptionMessage) # Check length and area closedCurveNode.GetMeasurement("length").SetEnabled(True) if closedCurveNode.GetMeasurement("length").GetValueWithUnitsAsPrintableString() != '219.9mm': - exceptionMessage = "Unexpected curve length value: " + closedCurveNode.GetMeasurement("length").GetValueWithUnitsAsPrintableString() - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected curve length value: " + closedCurveNode.GetMeasurement("length").GetValueWithUnitsAsPrintableString() + raise Exception(exceptionMessage) closedCurveNode.GetMeasurement("area").SetEnabled(True) if closedCurveNode.GetMeasurement("area").GetValueWithUnitsAsPrintableString() != '38.48cm2': - exceptionMessage = "Unexpected curve area value: " + closedCurveNode.GetMeasurement("area").GetValueWithUnitsAsPrintableString() - raise Exception(exceptionMessage) + exceptionMessage = "Unexpected curve area value: " + closedCurveNode.GetMeasurement("area").GetValueWithUnitsAsPrintableString() + raise Exception(exceptionMessage) # Display surface area as a model. # Useful for manual testing of surface quality. diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsInCompareViewersSelfTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsInCompareViewersSelfTest.py index bcdee8bf05a..f0511777ed0 100644 --- a/Modules/Loadable/Markups/Testing/Python/MarkupsInCompareViewersSelfTest.py +++ b/Modules/Loadable/Markups/Testing/Python/MarkupsInCompareViewersSelfTest.py @@ -10,16 +10,16 @@ # class MarkupsInCompareViewersSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "MarkupsInCompareViewersSelfTest" - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Nicole Aucoin (BWH)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "MarkupsInCompareViewersSelfTest" + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Nicole Aucoin (BWH)"] + parent.helpText = """ This is a test case that exercises the control points lists with compare viewers. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218-12S1. """ @@ -30,40 +30,40 @@ def __init__(self, parent): class MarkupsInCompareViewersSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + # Instantiate and connect widgets ... - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the algorithm." - self.applyButton.enabled = True - parametersFormLayout.addRow(self.applyButton) + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the algorithm." + self.applyButton.enabled = True + parametersFormLayout.addRow(self.applyButton) - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - def cleanup(self): - pass + def cleanup(self): + pass - def onApplyButton(self): - logic = MarkupsInCompareViewersSelfTestLogic() - logic.run() + def onApplyButton(self): + logic = MarkupsInCompareViewersSelfTestLogic() + logic.run() # @@ -72,141 +72,141 @@ def onApplyButton(self): class MarkupsInCompareViewersSelfTestLogic(ScriptedLoadableModuleLogic): - def run(self): - """ - Run the actual algorithm - """ - print('Running test of the markups in compare viewers') - - # - # first load the data - # - print("Getting MR Head Volume") - import SampleData - mrHeadVolume = SampleData.downloadSample("MRHead") - - # - # link the viewers - # - sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic() - compositeNode = sliceLogic.GetSliceCompositeNode() - compositeNode.SetLinkedControl(1) - - # - # MR Head in the background - # - sliceLogic.StartSliceCompositeNodeInteraction(1) - compositeNode.SetBackgroundVolumeID(mrHeadVolume.GetID()) - sliceLogic.EndSliceCompositeNodeInteraction() - - # - # switch to conventional layout - # - lm = slicer.app.layoutManager() - lm.setLayout(2) - - # create a control points list - displayNode = slicer.vtkMRMLMarkupsDisplayNode() - slicer.mrmlScene.AddNode(displayNode) - fidNode = slicer.vtkMRMLMarkupsFiducialNode() - slicer.mrmlScene.AddNode(fidNode) - fidNode.SetAndObserveDisplayNodeID(displayNode.GetID()) - - # make it active - selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton") - if (selectionNode is not None): - selectionNode.SetReferenceActivePlaceNodeID(fidNode.GetID()) - - # add some known points to it - eye1 = [33.4975, 79.4042, -10.2143] - eye2 = [-31.283, 80.9652, -16.2143] - nose = [4.61944, 114.526, -33.2143] - index = fidNode.AddControlPoint(eye1) - fidNode.SetNthControlPointLabel(index, "eye-1") - index = fidNode.AddControlPoint(eye2) - fidNode.SetNthControlPointLabel(index, "eye-2") - index = fidNode.AddControlPoint(nose) - fidNode.SetNthControlPointLabel(index, "nose") - - slicer.util.delayDisplay("Placed 3 control points") - - # - # switch to 2 viewers compare layout - # - lm.setLayout(12) - slicer.util.delayDisplay("Switched to Compare 2 viewers") - - # - # get compare slice composite node - # - compareLogic1 = slicer.app.layoutManager().sliceWidget('Compare1').sliceLogic() - compareCompositeNode1 = compareLogic1.GetSliceCompositeNode() - - # set MRHead in the background - compareLogic1.StartSliceCompositeNodeInteraction(1) - compareCompositeNode1.SetBackgroundVolumeID(mrHeadVolume.GetID()) - compareLogic1.EndSliceCompositeNodeInteraction() - compareLogic1.FitSliceToAll() - # make it visible in 3D - compareLogic1.GetSliceNode().SetSliceVisible(1) - - # scroll to a control point location - compareLogic1.StartSliceOffsetInteraction() - compareLogic1.SetSliceOffset(eye1[2]) - compareLogic1.EndSliceOffsetInteraction() - slicer.util.delayDisplay("MH Head in background, scrolled to a control point") - - # scroll around through the range of points - offset = nose[2] - while offset < eye1[2]: - compareLogic1.StartSliceOffsetInteraction() - compareLogic1.SetSliceOffset(offset) - compareLogic1.EndSliceOffsetInteraction() - msg = "Scrolled to " + str(offset) - slicer.util.delayDisplay(msg,250) - offset += 1.0 - - # switch back to conventional - lm.setLayout(2) - slicer.util.delayDisplay("Switched back to conventional layout") - - # switch to compare grid - lm.setLayout(23) - compareLogic1.FitSliceToAll() - slicer.util.delayDisplay("Switched to Compare grid") - - # switch back to conventional - lm.setLayout(2) - slicer.util.delayDisplay("Switched back to conventional layout") - - return True + def run(self): + """ + Run the actual algorithm + """ + print('Running test of the markups in compare viewers') + + # + # first load the data + # + print("Getting MR Head Volume") + import SampleData + mrHeadVolume = SampleData.downloadSample("MRHead") + + # + # link the viewers + # + sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic() + compositeNode = sliceLogic.GetSliceCompositeNode() + compositeNode.SetLinkedControl(1) + + # + # MR Head in the background + # + sliceLogic.StartSliceCompositeNodeInteraction(1) + compositeNode.SetBackgroundVolumeID(mrHeadVolume.GetID()) + sliceLogic.EndSliceCompositeNodeInteraction() + + # + # switch to conventional layout + # + lm = slicer.app.layoutManager() + lm.setLayout(2) + + # create a control points list + displayNode = slicer.vtkMRMLMarkupsDisplayNode() + slicer.mrmlScene.AddNode(displayNode) + fidNode = slicer.vtkMRMLMarkupsFiducialNode() + slicer.mrmlScene.AddNode(fidNode) + fidNode.SetAndObserveDisplayNodeID(displayNode.GetID()) + + # make it active + selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton") + if (selectionNode is not None): + selectionNode.SetReferenceActivePlaceNodeID(fidNode.GetID()) + + # add some known points to it + eye1 = [33.4975, 79.4042, -10.2143] + eye2 = [-31.283, 80.9652, -16.2143] + nose = [4.61944, 114.526, -33.2143] + index = fidNode.AddControlPoint(eye1) + fidNode.SetNthControlPointLabel(index, "eye-1") + index = fidNode.AddControlPoint(eye2) + fidNode.SetNthControlPointLabel(index, "eye-2") + index = fidNode.AddControlPoint(nose) + fidNode.SetNthControlPointLabel(index, "nose") + + slicer.util.delayDisplay("Placed 3 control points") + + # + # switch to 2 viewers compare layout + # + lm.setLayout(12) + slicer.util.delayDisplay("Switched to Compare 2 viewers") + + # + # get compare slice composite node + # + compareLogic1 = slicer.app.layoutManager().sliceWidget('Compare1').sliceLogic() + compareCompositeNode1 = compareLogic1.GetSliceCompositeNode() + + # set MRHead in the background + compareLogic1.StartSliceCompositeNodeInteraction(1) + compareCompositeNode1.SetBackgroundVolumeID(mrHeadVolume.GetID()) + compareLogic1.EndSliceCompositeNodeInteraction() + compareLogic1.FitSliceToAll() + # make it visible in 3D + compareLogic1.GetSliceNode().SetSliceVisible(1) + + # scroll to a control point location + compareLogic1.StartSliceOffsetInteraction() + compareLogic1.SetSliceOffset(eye1[2]) + compareLogic1.EndSliceOffsetInteraction() + slicer.util.delayDisplay("MH Head in background, scrolled to a control point") + + # scroll around through the range of points + offset = nose[2] + while offset < eye1[2]: + compareLogic1.StartSliceOffsetInteraction() + compareLogic1.SetSliceOffset(offset) + compareLogic1.EndSliceOffsetInteraction() + msg = "Scrolled to " + str(offset) + slicer.util.delayDisplay(msg, 250) + offset += 1.0 + + # switch back to conventional + lm.setLayout(2) + slicer.util.delayDisplay("Switched back to conventional layout") + + # switch to compare grid + lm.setLayout(23) + compareLogic1.FitSliceToAll() + slicer.util.delayDisplay("Switched to Compare grid") + + # switch back to conventional + lm.setLayout(2) + slicer.util.delayDisplay("Switched back to conventional layout") + + return True class MarkupsInCompareViewersSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - 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. + This is the test case for your scripted module. """ - self.setUp() - self.test_MarkupsInCompareViewersSelfTest1() - def test_MarkupsInCompareViewersSelfTest1(self): + 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_MarkupsInCompareViewersSelfTest1() + + def test_MarkupsInCompareViewersSelfTest1(self): - self.delayDisplay("Starting the Markups in compare viewers test") + self.delayDisplay("Starting the Markups in compare viewers test") - # start in the welcome module - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('Welcome') + # start in the welcome module + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('Welcome') - logic = MarkupsInCompareViewersSelfTestLogic() - logic.run() + logic = MarkupsInCompareViewersSelfTestLogic() + logic.run() - self.delayDisplay('Test passed!') + self.delayDisplay('Test passed!') diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsInViewsSelfTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsInViewsSelfTest.py index 9e1c2f25141..6cadd4bb2ff 100644 --- a/Modules/Loadable/Markups/Testing/Python/MarkupsInViewsSelfTest.py +++ b/Modules/Loadable/Markups/Testing/Python/MarkupsInViewsSelfTest.py @@ -11,16 +11,16 @@ # class MarkupsInViewsSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "MarkupsInViewsSelfTest" - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Nicole Aucoin (BWH)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "MarkupsInViewsSelfTest" + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Nicole Aucoin (BWH)"] + parent.helpText = """ This is a test case that exercises the control points nodes with different settings on the display node to show only in certain views. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218-12S1. """ @@ -31,42 +31,42 @@ def __init__(self, parent): class MarkupsInViewsSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + # Instantiate and connect widgets ... - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the algorithm." - self.applyButton.enabled = True - parametersFormLayout.addRow(self.applyButton) + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the algorithm." + self.applyButton.enabled = True + parametersFormLayout.addRow(self.applyButton) - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - def cleanup(self): - pass + def cleanup(self): + pass - def onApplyButton(self): - # note difference from running from command line: does not switch to the - # markups module - logic = MarkupsInViewsSelfTestLogic() - logic.run() + def onApplyButton(self): + # note difference from running from command line: does not switch to the + # markups module + logic = MarkupsInViewsSelfTestLogic() + logic.run() # @@ -75,273 +75,273 @@ def onApplyButton(self): class MarkupsInViewsSelfTestLogic(ScriptedLoadableModuleLogic): - def controlPointVisible3D(self, fidNode, viewNodeID, controlPointIndex): - lm = slicer.app.layoutManager() - for v in range(lm.threeDViewCount): - td = lm.threeDWidget(v) - if td.viewLogic().GetViewNode().GetID() != viewNodeID: - continue - td.threeDView().forceRender() - slicer.app.processEvents() - ms = vtk.vtkCollection() - td.getDisplayableManagers(ms) - for i in range(ms.GetNumberOfItems()): - m = ms.GetItemAsObject(i) - if m.GetClassName() == "vtkMRMLMarkupsDisplayableManager": - markupsWidget = m.GetWidget(fidNode.GetDisplayNode()) - return markupsWidget.GetMarkupsRepresentation().GetNthControlPointViewVisibility(controlPointIndex) - return False - - def controlPointVisibleSlice(self, fidNode, sliceNodeID, controlPointIndex): - lm = slicer.app.layoutManager() - sliceNames = lm.sliceViewNames() - for sliceName in sliceNames: - sliceWidget = lm.sliceWidget(sliceName) - sliceView = sliceWidget.sliceView() - sliceNode = sliceView.mrmlSliceNode() - if sliceNode.GetID() != sliceNodeID: - continue - sliceView.forceRender() - slicer.app.processEvents() - ms = vtk.vtkCollection() - sliceView.getDisplayableManagers(ms) - for i in range(ms.GetNumberOfItems()): - m = ms.GetItemAsObject(i) - if m.GetClassName() == 'vtkMRMLMarkupsDisplayableManager': - markupsWidget = m.GetWidget(fidNode.GetDisplayNode()) - return markupsWidget.GetMarkupsRepresentation().GetNthControlPointViewVisibility(controlPointIndex) - return False - - def printViewNodeIDs(self, displayNode): - numIDs = displayNode.GetNumberOfViewNodeIDs() - if numIDs == 0: - print('No view node ids for display node',displayNode.GetID()) - return - print('View node ids for display node',displayNode.GetID()) - for i in range(numIDs): - id = displayNode.GetNthViewNodeID(i) - print(id) - - def printViewAndSliceNodes(self): - numViewNodes = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLViewNode') - print('Number of view nodes = ', numViewNodes) - for vn in range(numViewNodes): - viewNode = slicer.mrmlScene.GetNthNodeByClass(vn, 'vtkMRMLViewNode') - print('\t',viewNode.GetName(),"id =",viewNode.GetID()) - - numSliceNodes = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSliceNode') - print('Number of slice nodes = ', numSliceNodes) - for sn in range(numSliceNodes): - sliceNode = slicer.mrmlScene.GetNthNodeByClass(sn, 'vtkMRMLSliceNode') - print('\t',sliceNode.GetName(),"id =",sliceNode.GetID()) - - def onRecordNodeEvent(self, caller, event, eventId): - self.nodeEvents.append(eventId) - - def run(self): - """ - Run the actual algorithm - """ - print('Running test of the markups in different views') - - # - # first load the data - # - print("Getting MR Head Volume") - import SampleData - mrHeadVolume = SampleData.downloadSample("MRHead") - - # - # link the viewers - # - sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic() - compositeNode = sliceLogic.GetSliceCompositeNode() - compositeNode.SetLinkedControl(1) - - # - # MR Head in the background - # - sliceLogic.StartSliceCompositeNodeInteraction(1) - compositeNode.SetBackgroundVolumeID(mrHeadVolume.GetID()) - sliceLogic.EndSliceCompositeNodeInteraction() - - # - # switch to conventional layout - # - lm = slicer.app.layoutManager() - lm.setLayout(2) - - # create a control points list - fidNode = slicer.vtkMRMLMarkupsFiducialNode() - slicer.mrmlScene.AddNode(fidNode) - fidNode.CreateDefaultDisplayNodes() - displayNode = fidNode.GetDisplayNode() - - # make it active - selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton") - if (selectionNode is not None): - selectionNode.SetReferenceActivePlaceNodeID(fidNode.GetID()) - - fidNodeObserverTags = [] - self.nodeEvents = [] - observedEvents = [ - slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent, - slicer.vtkMRMLMarkupsNode.PointPositionUndefinedEvent ] - for eventId in observedEvents: - fidNodeObserverTags.append(fidNode.AddObserver(eventId, lambda caller, event, eventId=eventId: self.onRecordNodeEvent(caller, event, eventId))) - - # add some known points to it - eye1 = [33.4975, 79.4042, -10.2143] - eye2 = [-31.283, 80.9652, -16.2143] - nose = [4.61944, 114.526, -33.2143] - controlPointIndex = fidNode.AddControlPoint(eye1) - slicer.nodeEvents = self.nodeEvents - assert(len(self.nodeEvents) == 1) - assert(self.nodeEvents[0] == slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent) - fidNode.SetNthControlPointLabel(controlPointIndex, "eye-1") - controlPointIndex = fidNode.AddControlPoint(eye2) - fidNode.SetNthControlPointLabel(controlPointIndex, "eye-2") - # hide the second eye as a test of visibility flags - fidNode.SetNthControlPointVisibility(controlPointIndex, controlPointIndex) - controlPointIndex = fidNode.AddControlPoint(nose) - fidNode.SetNthControlPointLabel(controlPointIndex, "nose") - - for tag in fidNodeObserverTags: - fidNode.RemoveObserver(tag) - - slicer.util.delayDisplay("Placed 3 control points") - - # self.printViewAndSliceNodes() - - if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex): - slicer.util.delayDisplay("Test failed: widget is not visible in view 1") - # self.printViewNodeIDs(displayNode) - return False - - # - # switch to 2 3D views layout - # - lm.setLayout(15) - slicer.util.delayDisplay("Switched to 2 3D views") - # self.printViewAndSliceNodes() - - controlPointIndex = 0 - - slicer.modules.markups.logic().FocusCamerasOnNthPointInMarkup(fidNode.GetID(), controlPointIndex) - - if (not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex) - or not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex)): - slicer.util.delayDisplay("Test failed: widget is not visible in view 1 and 2") - # self.printViewNodeIDs(displayNode) - return False - - # - # show only in view 2 - # - displayNode.AddViewNodeID("vtkMRMLViewNode2") - slicer.util.delayDisplay("Showing only in view 2") - if self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex): - slicer.util.delayDisplay("Test failed: widget is not supposed to be visible in view 1") - # self.printViewNodeIDs(displayNode) - return False - if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex): - slicer.util.delayDisplay("Test failed: widget is not visible in view 2") - # self.printViewNodeIDs(displayNode) - return False - - # - # remove it so show in all - # - displayNode.RemoveAllViewNodeIDs() - slicer.util.delayDisplay("Showing in both views") - if (not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex) - or not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex)): - slicer.util.delayDisplay("Test failed: widget is not visible in view 1 and 2") - self.printViewNodeIDs(displayNode) - return False - - # - # show only in view 1 - # - displayNode.AddViewNodeID("vtkMRMLViewNode1") - slicer.util.delayDisplay("Showing only in view 1") - if self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex): - slicer.util.delayDisplay("Test failed: widget is not supposed to be visible in view 2") - # self.printViewNodeIDs(displayNode) - return False - if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex): - slicer.util.delayDisplay("Test failed: widget is not visible in view 1") - # self.printViewNodeIDs(displayNode) - return False - - # switch back to conventional - lm.setLayout(2) - slicer.util.delayDisplay("Switched back to conventional layout") - # self.printViewAndSliceNodes() - - # test of the visibility in slice views - displayNode.RemoveAllViewNodeIDs() - - # jump to the last control point - slicer.modules.markups.logic().JumpSlicesToNthPointInMarkup(fidNode.GetID(), controlPointIndex, True) - # refocus the 3D cameras as well - slicer.modules.markups.logic().FocusCamerasOnNthPointInMarkup(fidNode.GetID(), controlPointIndex) - - # show only in red - displayNode.AddViewNodeID('vtkMRMLSliceNodeRed') - slicer.util.delayDisplay("Show only in red slice") - if not self.controlPointVisibleSlice(fidNode,'vtkMRMLSliceNodeRed', controlPointIndex): - slicer.util.delayDisplay("Test failed: widget not displayed on red slice") - # self.printViewNodeIDs(displayNode) - return False - - # remove all, add green - # print 'before remove all, after added red' - # self.printViewNodeIDs(displayNode) - displayNode.RemoveAllViewNodeIDs() - # print 'after removed all' - # self.printViewNodeIDs(displayNode) - displayNode.AddViewNodeID('vtkMRMLSliceNodeGreen') - slicer.util.delayDisplay('Show only in green slice') - if (self.controlPointVisibleSlice(fidNode,'vtkMRMLSliceNodeRed', controlPointIndex) - or not self.controlPointVisibleSlice(fidNode,'vtkMRMLSliceNodeGreen', controlPointIndex)): - slicer.util.delayDisplay("Test failed: widget not displayed only on green slice") - print('\tred = ',self.controlPointVisibleSlice(fidNode,'vtkMRMLSliceNodeRed', controlPointIndex)) - print('\tgreen =',self.controlPointVisibleSlice(fidNode,'vtkMRMLSliceNodeGreen', controlPointIndex)) - self.printViewNodeIDs(displayNode) - return False - - return True + def controlPointVisible3D(self, fidNode, viewNodeID, controlPointIndex): + lm = slicer.app.layoutManager() + for v in range(lm.threeDViewCount): + td = lm.threeDWidget(v) + if td.viewLogic().GetViewNode().GetID() != viewNodeID: + continue + td.threeDView().forceRender() + slicer.app.processEvents() + ms = vtk.vtkCollection() + td.getDisplayableManagers(ms) + for i in range(ms.GetNumberOfItems()): + m = ms.GetItemAsObject(i) + if m.GetClassName() == "vtkMRMLMarkupsDisplayableManager": + markupsWidget = m.GetWidget(fidNode.GetDisplayNode()) + return markupsWidget.GetMarkupsRepresentation().GetNthControlPointViewVisibility(controlPointIndex) + return False + + def controlPointVisibleSlice(self, fidNode, sliceNodeID, controlPointIndex): + lm = slicer.app.layoutManager() + sliceNames = lm.sliceViewNames() + for sliceName in sliceNames: + sliceWidget = lm.sliceWidget(sliceName) + sliceView = sliceWidget.sliceView() + sliceNode = sliceView.mrmlSliceNode() + if sliceNode.GetID() != sliceNodeID: + continue + sliceView.forceRender() + slicer.app.processEvents() + ms = vtk.vtkCollection() + sliceView.getDisplayableManagers(ms) + for i in range(ms.GetNumberOfItems()): + m = ms.GetItemAsObject(i) + if m.GetClassName() == 'vtkMRMLMarkupsDisplayableManager': + markupsWidget = m.GetWidget(fidNode.GetDisplayNode()) + return markupsWidget.GetMarkupsRepresentation().GetNthControlPointViewVisibility(controlPointIndex) + return False + + def printViewNodeIDs(self, displayNode): + numIDs = displayNode.GetNumberOfViewNodeIDs() + if numIDs == 0: + print('No view node ids for display node', displayNode.GetID()) + return + print('View node ids for display node', displayNode.GetID()) + for i in range(numIDs): + id = displayNode.GetNthViewNodeID(i) + print(id) + + def printViewAndSliceNodes(self): + numViewNodes = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLViewNode') + print('Number of view nodes = ', numViewNodes) + for vn in range(numViewNodes): + viewNode = slicer.mrmlScene.GetNthNodeByClass(vn, 'vtkMRMLViewNode') + print('\t', viewNode.GetName(), "id =", viewNode.GetID()) + + numSliceNodes = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSliceNode') + print('Number of slice nodes = ', numSliceNodes) + for sn in range(numSliceNodes): + sliceNode = slicer.mrmlScene.GetNthNodeByClass(sn, 'vtkMRMLSliceNode') + print('\t', sliceNode.GetName(), "id =", sliceNode.GetID()) + + def onRecordNodeEvent(self, caller, event, eventId): + self.nodeEvents.append(eventId) + + def run(self): + """ + Run the actual algorithm + """ + print('Running test of the markups in different views') + + # + # first load the data + # + print("Getting MR Head Volume") + import SampleData + mrHeadVolume = SampleData.downloadSample("MRHead") + + # + # link the viewers + # + sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic() + compositeNode = sliceLogic.GetSliceCompositeNode() + compositeNode.SetLinkedControl(1) + + # + # MR Head in the background + # + sliceLogic.StartSliceCompositeNodeInteraction(1) + compositeNode.SetBackgroundVolumeID(mrHeadVolume.GetID()) + sliceLogic.EndSliceCompositeNodeInteraction() + + # + # switch to conventional layout + # + lm = slicer.app.layoutManager() + lm.setLayout(2) + + # create a control points list + fidNode = slicer.vtkMRMLMarkupsFiducialNode() + slicer.mrmlScene.AddNode(fidNode) + fidNode.CreateDefaultDisplayNodes() + displayNode = fidNode.GetDisplayNode() + + # make it active + selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton") + if (selectionNode is not None): + selectionNode.SetReferenceActivePlaceNodeID(fidNode.GetID()) + + fidNodeObserverTags = [] + self.nodeEvents = [] + observedEvents = [ + slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent, + slicer.vtkMRMLMarkupsNode.PointPositionUndefinedEvent] + for eventId in observedEvents: + fidNodeObserverTags.append(fidNode.AddObserver(eventId, lambda caller, event, eventId=eventId: self.onRecordNodeEvent(caller, event, eventId))) + + # add some known points to it + eye1 = [33.4975, 79.4042, -10.2143] + eye2 = [-31.283, 80.9652, -16.2143] + nose = [4.61944, 114.526, -33.2143] + controlPointIndex = fidNode.AddControlPoint(eye1) + slicer.nodeEvents = self.nodeEvents + assert(len(self.nodeEvents) == 1) + assert(self.nodeEvents[0] == slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent) + fidNode.SetNthControlPointLabel(controlPointIndex, "eye-1") + controlPointIndex = fidNode.AddControlPoint(eye2) + fidNode.SetNthControlPointLabel(controlPointIndex, "eye-2") + # hide the second eye as a test of visibility flags + fidNode.SetNthControlPointVisibility(controlPointIndex, controlPointIndex) + controlPointIndex = fidNode.AddControlPoint(nose) + fidNode.SetNthControlPointLabel(controlPointIndex, "nose") + + for tag in fidNodeObserverTags: + fidNode.RemoveObserver(tag) + + slicer.util.delayDisplay("Placed 3 control points") + + # self.printViewAndSliceNodes() + + if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex): + slicer.util.delayDisplay("Test failed: widget is not visible in view 1") + # self.printViewNodeIDs(displayNode) + return False + + # + # switch to 2 3D views layout + # + lm.setLayout(15) + slicer.util.delayDisplay("Switched to 2 3D views") + # self.printViewAndSliceNodes() + + controlPointIndex = 0 + + slicer.modules.markups.logic().FocusCamerasOnNthPointInMarkup(fidNode.GetID(), controlPointIndex) + + if (not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex) + or not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex)): + slicer.util.delayDisplay("Test failed: widget is not visible in view 1 and 2") + # self.printViewNodeIDs(displayNode) + return False + + # + # show only in view 2 + # + displayNode.AddViewNodeID("vtkMRMLViewNode2") + slicer.util.delayDisplay("Showing only in view 2") + if self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex): + slicer.util.delayDisplay("Test failed: widget is not supposed to be visible in view 1") + # self.printViewNodeIDs(displayNode) + return False + if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex): + slicer.util.delayDisplay("Test failed: widget is not visible in view 2") + # self.printViewNodeIDs(displayNode) + return False + + # + # remove it so show in all + # + displayNode.RemoveAllViewNodeIDs() + slicer.util.delayDisplay("Showing in both views") + if (not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex) + or not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex)): + slicer.util.delayDisplay("Test failed: widget is not visible in view 1 and 2") + self.printViewNodeIDs(displayNode) + return False + + # + # show only in view 1 + # + displayNode.AddViewNodeID("vtkMRMLViewNode1") + slicer.util.delayDisplay("Showing only in view 1") + if self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex): + slicer.util.delayDisplay("Test failed: widget is not supposed to be visible in view 2") + # self.printViewNodeIDs(displayNode) + return False + if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex): + slicer.util.delayDisplay("Test failed: widget is not visible in view 1") + # self.printViewNodeIDs(displayNode) + return False + + # switch back to conventional + lm.setLayout(2) + slicer.util.delayDisplay("Switched back to conventional layout") + # self.printViewAndSliceNodes() + + # test of the visibility in slice views + displayNode.RemoveAllViewNodeIDs() + + # jump to the last control point + slicer.modules.markups.logic().JumpSlicesToNthPointInMarkup(fidNode.GetID(), controlPointIndex, True) + # refocus the 3D cameras as well + slicer.modules.markups.logic().FocusCamerasOnNthPointInMarkup(fidNode.GetID(), controlPointIndex) + + # show only in red + displayNode.AddViewNodeID('vtkMRMLSliceNodeRed') + slicer.util.delayDisplay("Show only in red slice") + if not self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeRed', controlPointIndex): + slicer.util.delayDisplay("Test failed: widget not displayed on red slice") + # self.printViewNodeIDs(displayNode) + return False + + # remove all, add green + # print 'before remove all, after added red' + # self.printViewNodeIDs(displayNode) + displayNode.RemoveAllViewNodeIDs() + # print 'after removed all' + # self.printViewNodeIDs(displayNode) + displayNode.AddViewNodeID('vtkMRMLSliceNodeGreen') + slicer.util.delayDisplay('Show only in green slice') + if (self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeRed', controlPointIndex) + or not self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeGreen', controlPointIndex)): + slicer.util.delayDisplay("Test failed: widget not displayed only on green slice") + print('\tred = ', self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeRed', controlPointIndex)) + print('\tgreen =', self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeGreen', controlPointIndex)) + self.printViewNodeIDs(displayNode) + return False + + return True class MarkupsInViewsSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - 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. + This is the test case for your scripted module. """ - self.setUp() - self.test_MarkupsInViewsSelfTest1() - def test_MarkupsInViewsSelfTest1(self): + 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_MarkupsInViewsSelfTest1() + + def test_MarkupsInViewsSelfTest1(self): - self.delayDisplay("Starting the Markups in viewers test") + self.delayDisplay("Starting the Markups in viewers test") - # start in the Markups module - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('Markups') + # start in the Markups module + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('Markups') - logic = MarkupsInViewsSelfTestLogic() - retval = logic.run() + logic = MarkupsInViewsSelfTestLogic() + retval = logic.run() - if retval == True: - self.delayDisplay('Test passed!') - else: - self.delayDisplay('Test failed!') + if retval is True: + self.delayDisplay('Test passed!') + else: + self.delayDisplay('Test failed!') diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsMeasurementsTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsMeasurementsTest.py index f870770a10e..4b410cd0f9c 100644 --- a/Modules/Loadable/Markups/Testing/Python/MarkupsMeasurementsTest.py +++ b/Modules/Loadable/Markups/Testing/Python/MarkupsMeasurementsTest.py @@ -15,7 +15,7 @@ markupsMeasurementsTestDir = slicer.app.temporaryPath + '/markupsMeasurementsTest' print('Test directory: ', markupsMeasurementsTestDir) if not os.access(markupsMeasurementsTestDir, os.F_OK): - os.mkdir(markupsMeasurementsTestDir) + os.mkdir(markupsMeasurementsTestDir) preserveFiles = True @@ -27,18 +27,18 @@ length = 34.12 direction = np.array([0.3, -0.4, 0.8]) pos1 = np.array([20, -12.4, 3.8]) -pos2 = pos1 + direction/np.linalg.norm(direction) * length +pos2 = pos1 + direction / np.linalg.norm(direction) * length markupsNode.AddControlPoint(vtk.vtkVector3d(pos1)) markupsNode.AddControlPoint(vtk.vtkVector3d(pos2)) measurement = markupsNode.GetMeasurement("length") if abs(measurement.GetValue() - length) > 1e-4: - raise Exception("Unexpected length value: " + str(measurement.GetValue())) + raise Exception("Unexpected length value: " + str(measurement.GetValue())) if measurement.GetValueWithUnitsAsPrintableString() != '34.12mm': - raise Exception("Unexpected length measurement result: " + measurement.GetValueWithUnitsAsPrintableString()) + raise Exception("Unexpected length measurement result: " + measurement.GetValueWithUnitsAsPrintableString()) -markupsFilename = markupsMeasurementsTestDir+'/line.mkp.json' +markupsFilename = markupsMeasurementsTestDir + '/line.mkp.json' slicer.util.saveNode(markupsNode, markupsFilename) with open(markupsFilename) as f: @@ -46,7 +46,7 @@ result = [{'name': 'length', 'enabled': True, 'value': 34.12, 'units': 'mm', 'printFormat': '%-#4.4gmm'}] if markupsJson['markups'][0]['measurements'] != result: - raise Exception("Unexpected length measurement result in file: " + str(markupsJson['markups'][0]['measurements'])) + raise Exception("Unexpected length measurement result in file: " + str(markupsJson['markups'][0]['measurements'])) if not preserveFiles: os.remove(markupsFilename) @@ -62,12 +62,12 @@ measurement = markupsNode.GetMeasurement("angle") if abs(measurement.GetValue() - 117.4) > 0.1: - raise Exception("Unexpected angle value: " + str(measurement.GetValue())) + raise Exception("Unexpected angle value: " + str(measurement.GetValue())) if measurement.GetValueWithUnitsAsPrintableString() != '117.4deg': - raise Exception("Unexpected angle measurement result: " + measurement.GetValueWithUnitsAsPrintableString()) + raise Exception("Unexpected angle measurement result: " + measurement.GetValueWithUnitsAsPrintableString()) -markupsFilename = markupsMeasurementsTestDir+'/angle.mkp.json' +markupsFilename = markupsMeasurementsTestDir + '/angle.mkp.json' slicer.util.saveNode(markupsNode, markupsFilename) with open(markupsFilename) as f: @@ -75,20 +75,20 @@ result = [{'name': 'angle', 'enabled': True, 'value': 117.36891896165277, 'units': 'deg', 'printFormat': '%3.1f%s'}] if markupsJson['markups'][0]['measurements'] != result: - raise Exception("Unexpected angle measurement result in file: " + str(markupsJson['markups'][0]['measurements'])) + raise Exception("Unexpected angle measurement result in file: " + str(markupsJson['markups'][0]['measurements'])) markupsNode.SetAngleMeasurementModeToOrientedPositive() measurement = markupsNode.GetMeasurement("angle") if abs(measurement.GetValue() - 242.6) > 0.1: - raise Exception("Unexpected angle value: " + str(measurement.GetValue())) + raise Exception("Unexpected angle value: " + str(measurement.GetValue())) markupsNode.SetAngleMeasurementModeToOrientedSigned() measurement = markupsNode.GetMeasurement("angle") if abs(measurement.GetValue() - (-117.36891896165277)) > 0.1: - raise Exception("Unexpected angle value: " + str(measurement.GetValue())) + raise Exception("Unexpected angle value: " + str(measurement.GetValue())) if not preserveFiles: - os.remove(markupsFilename) + os.remove(markupsFilename) # # Plane @@ -103,9 +103,9 @@ measurement = markupsNode.GetMeasurement("area") measurement.SetEnabled(True) if measurement.GetValueWithUnitsAsPrintableString() != '32.00cm2': - raise Exception("Unexpected area measurement result: " + measurement.GetValueWithUnitsAsPrintableString()) + raise Exception("Unexpected area measurement result: " + measurement.GetValueWithUnitsAsPrintableString()) -markupsFilename = markupsMeasurementsTestDir+'/plane.mkp.json' +markupsFilename = markupsMeasurementsTestDir + '/plane.mkp.json' slicer.util.saveNode(markupsNode, markupsFilename) with open(markupsFilename) as f: @@ -113,10 +113,10 @@ result = [{'name': 'area', 'enabled': True, 'value': 32.00000000000001, 'units': 'cm2', 'printFormat': '%-#4.4gcm2'}] if markupsJson['markups'][0]['measurements'] != result: - raise Exception("Unexpected area measurement result in file: " + str(markupsJson['markups'][0]['measurements'])) + raise Exception("Unexpected area measurement result in file: " + str(markupsJson['markups'][0]['measurements'])) if not preserveFiles: - os.remove(markupsFilename) + os.remove(markupsFilename) # # ROI @@ -124,14 +124,14 @@ markupsNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsROINode') markupsNode.AddControlPoint(vtk.vtkVector3d(61, -12.4, 13.8)) -markupsNode.SetSize(20,30,40) +markupsNode.SetSize(20, 30, 40) measurement = markupsNode.GetMeasurement("volume") measurement.SetEnabled(True) if measurement.GetValueWithUnitsAsPrintableString() != '24.000cm3': - raise Exception("Unexpected volume measurement result: " + measurement.GetValueWithUnitsAsPrintableString()) + raise Exception("Unexpected volume measurement result: " + measurement.GetValueWithUnitsAsPrintableString()) -markupsFilename = markupsMeasurementsTestDir+'/roi.mkp.json' +markupsFilename = markupsMeasurementsTestDir + '/roi.mkp.json' slicer.util.saveNode(markupsNode, markupsFilename) with open(markupsFilename) as f: @@ -139,9 +139,9 @@ result = [{'name': 'volume', 'enabled': True, 'value': 24.000000000000007, 'units': 'cm3', 'printFormat': '%-#4.5gcm3'}] if markupsJson['markups'][0]['measurements'] != result: - raise Exception("Unexpected volume measurement result in file: " + str(markupsJson['markups'][0]['measurements'])) + raise Exception("Unexpected volume measurement result in file: " + str(markupsJson['markups'][0]['measurements'])) if not preserveFiles: - os.remove(markupsFilename) + os.remove(markupsFilename) print('Markups computation is verified successfully') diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestManyLists.py b/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestManyLists.py index 5d410e2f959..a20ca0ba044 100644 --- a/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestManyLists.py +++ b/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestManyLists.py @@ -8,20 +8,20 @@ coords = [0.0, 0.0, 0.0] numFidsInList1 = 5 for i in range(numFidsInList1): - fidNode1.AddControlPoint(coords) - coords[0] += 1.0 - coords[1] += 2.0 - coords[2] += 1.0 + fidNode1.AddControlPoint(coords) + coords[0] += 1.0 + coords[1] += 2.0 + coords[2] += 1.0 # second control point list fidNode2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", "FidNode2") fidNode2.CreateDefaultDisplayNodes() numFidsInList2 = 10 for i in range(numFidsInList2): - fidNode2.AddControlPoint(coords) - coords[0] += 1.0 - coords[1] += 1.0 - coords[2] += 3.0 + fidNode2.AddControlPoint(coords) + coords[0] += 1.0 + coords[1] += 1.0 + coords[2] += 3.0 # Create scene view numFidNodesBeforeStore = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialNode') @@ -34,69 +34,69 @@ fidNode3.CreateDefaultDisplayNodes() numFidsInList3 = 2 for i in range(numFidsInList3): - fidNode3.AddControlPoint(coords) - coords[0] += 1.0 - coords[1] += 2.0 - coords[2] += 3.0 + fidNode3.AddControlPoint(coords) + coords[0] += 1.0 + coords[1] += 2.0 + coords[2] += 3.0 # Restore scene view sv.RestoreScene() numFidNodesAfterRestore = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialNode') if numFidNodesAfterRestore != numFidNodesBeforeStore: - print("After restoring the scene, expected ", numFidNodesBeforeStore, " control points nodes, but have ", numFidNodesAfterRestore) - exceptionMessage = "After restoring the scene, expected " + str(numFidNodesBeforeStore) + " control points nodes, but have " + str(numFidNodesAfterRestore) - raise Exception(exceptionMessage) + print("After restoring the scene, expected ", numFidNodesBeforeStore, " control points nodes, but have ", numFidNodesAfterRestore) + exceptionMessage = "After restoring the scene, expected " + str(numFidNodesBeforeStore) + " control points nodes, but have " + str(numFidNodesAfterRestore) + raise Exception(exceptionMessage) fid1AfterRestore = slicer.mrmlScene.GetFirstNodeByName("FidNode1") numFidsInList1AfterRestore = fid1AfterRestore.GetNumberOfControlPoints() print("After restore, list with name FidNode1 has id ", fid1AfterRestore.GetID(), " and num fids = ", numFidsInList1AfterRestore) if numFidsInList1AfterRestore != numFidsInList1: - exceptionMessage = "After restoring list 1, id = " + fid1AfterRestore.GetID() - exceptionMessage += ", expected " + str(numFidsInList1) + " but got " - exceptionMessage += str(numFidsInList1AfterRestore) - raise Exception(exceptionMessage) + exceptionMessage = "After restoring list 1, id = " + fid1AfterRestore.GetID() + exceptionMessage += ", expected " + str(numFidsInList1) + " but got " + exceptionMessage += str(numFidsInList1AfterRestore) + raise Exception(exceptionMessage) fid2AfterRestore = slicer.mrmlScene.GetFirstNodeByName("FidNode2") numFidsInList2AfterRestore = fid2AfterRestore.GetNumberOfControlPoints() print("After restore, list with name FidNode2 has id ", fid2AfterRestore.GetID(), " and num fids = ", numFidsInList2AfterRestore) if numFidsInList2AfterRestore != numFidsInList2: - exceptionMessage = "After restoring list 2, id = " + fid2AfterRestore.GetID() - exceptionMessage += ", expected " + str(numFidsInList2) + " but got " - exceptionMessage += str(numFidsInList2AfterRestore) - raise Exception(exceptionMessage) + exceptionMessage = "After restoring list 2, id = " + fid2AfterRestore.GetID() + exceptionMessage += ", expected " + str(numFidsInList2) + " but got " + exceptionMessage += str(numFidsInList2AfterRestore) + raise Exception(exceptionMessage) # check the displayable manager for the right number of widgets/seeds lm = slicer.app.layoutManager() td = lm.threeDWidget(0) mfm = td.threeDView().displayableManagerByClassName("vtkMRMLMarkupsDisplayableManager") h = mfm.GetHelper() -print('Helper = ',h) +print('Helper = ', h) for markupsNode in [fid1AfterRestore, fid2AfterRestore]: - markupsWidget = h.GetWidget(markupsNode) - rep = markupsWidget.GetRepresentation() - controlPointsPoly = rep.GetControlPointsPolyData(rep.Selected) - numberOfControlPoints = controlPointsPoly.GetNumberOfPoints() - print(f"Markups widget {markupsNode.GetName()} has number of control points = {numberOfControlPoints}") - if numberOfControlPoints != markupsNode.GetNumberOfControlPoints(): - exceptionMessage = "After restoring " + markupsNode.GetName() + ", expected widget to have " - exceptionMessage += str(markupsNode.GetNumberOfControlPoints()) + " points, but it has " - exceptionMessage += str(numberOfControlPoints) - raise Exception(exceptionMessage) - # check positions - for s in range(markupsNode.GetNumberOfControlPoints()): - worldPos = controlPointsPoly.GetPoint(s) - print("control point ",s," world position = ",worldPos) - fidPos = [0.0,0.0,0.0] - markupsNode.GetNthControlPointPosition(s,fidPos) - xdiff = fidPos[0] - worldPos[0] - ydiff = fidPos[1] - worldPos[1] - zdiff = fidPos[2] - worldPos[2] - diffTotal = xdiff + ydiff + zdiff - if diffTotal > 0.1: - exceptionMessage = markupsNode.GetName() + ": Difference between control point position " + str(s) - exceptionMessage += " and representation point position totals = " + str(diffTotal) - raise Exception(exceptionMessage) - # Release reference to VTK widget, otherwise application could crash on exit - del markupsWidget + markupsWidget = h.GetWidget(markupsNode) + rep = markupsWidget.GetRepresentation() + controlPointsPoly = rep.GetControlPointsPolyData(rep.Selected) + numberOfControlPoints = controlPointsPoly.GetNumberOfPoints() + print(f"Markups widget {markupsNode.GetName()} has number of control points = {numberOfControlPoints}") + if numberOfControlPoints != markupsNode.GetNumberOfControlPoints(): + exceptionMessage = "After restoring " + markupsNode.GetName() + ", expected widget to have " + exceptionMessage += str(markupsNode.GetNumberOfControlPoints()) + " points, but it has " + exceptionMessage += str(numberOfControlPoints) + raise Exception(exceptionMessage) + # check positions + for s in range(markupsNode.GetNumberOfControlPoints()): + worldPos = controlPointsPoly.GetPoint(s) + print("control point ", s, " world position = ", worldPos) + fidPos = [0.0, 0.0, 0.0] + markupsNode.GetNthControlPointPosition(s, fidPos) + xdiff = fidPos[0] - worldPos[0] + ydiff = fidPos[1] - worldPos[1] + zdiff = fidPos[2] - worldPos[2] + diffTotal = xdiff + ydiff + zdiff + if diffTotal > 0.1: + exceptionMessage = markupsNode.GetName() + ": Difference between control point position " + str(s) + exceptionMessage += " and representation point position totals = " + str(diffTotal) + raise Exception(exceptionMessage) + # Release reference to VTK widget, otherwise application could crash on exit + del markupsWidget diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestSimple.py b/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestSimple.py index 57152ca669d..a603874f230 100644 --- a/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestSimple.py +++ b/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestSimple.py @@ -12,25 +12,25 @@ startCoords = [1.0, 2.0, 3.0] fid.AddControlPoint(startCoords) -fid.GetNthControlPointPosition(0,startCoords) +fid.GetNthControlPointPosition(0, startCoords) print(f"Starting control point coordinates = {startCoords}") sv = slicer.mrmlScene.AddNode(slicer.vtkMRMLSceneViewNode()) sv.StoreScene() -afterStoreSceneCoords = [11.1,22.2,33.3] +afterStoreSceneCoords = [11.1, 22.2, 33.3] fid.SetNthControlPointPosition(0, afterStoreSceneCoords) -fid.GetNthControlPointPosition(0,afterStoreSceneCoords) +fid.GetNthControlPointPosition(0, afterStoreSceneCoords) print(f"After storing the scene, set control point coords to {afterStoreSceneCoords}") sv.RestoreScene() -fidAfterRestore = slicer.mrmlScene.GetNodeByID("vtkMRMLMarkupsFiducialNode1") +fidAfterRestore = slicer.mrmlScene.GetNodeByID("vtkMRMLMarkupsFiducialNode1") -coords = [0,0,0] -fidAfterRestore.GetNthControlPointPosition(0,coords) +coords = [0, 0, 0] +fidAfterRestore.GetNthControlPointPosition(0, coords) print("After restoring the scene, control point coordinates = ", coords) xdiff = coords[0] - startCoords[0] @@ -42,5 +42,5 @@ diffTotal = xdiff + ydiff + zdiff if diffTotal > 0.1: - exceptionMessage = "Difference between coordinate values total = " + str(diffTotal) - raise Exception(exceptionMessage) + exceptionMessage = "Difference between coordinate values total = " + str(diffTotal) + raise Exception(exceptionMessage) diff --git a/Modules/Loadable/Markups/Testing/Python/NeurosurgicalPlanningTutorialMarkupsSelfTest.py b/Modules/Loadable/Markups/Testing/Python/NeurosurgicalPlanningTutorialMarkupsSelfTest.py index 4d765994dcd..4eeb403fe1f 100644 --- a/Modules/Loadable/Markups/Testing/Python/NeurosurgicalPlanningTutorialMarkupsSelfTest.py +++ b/Modules/Loadable/Markups/Testing/Python/NeurosurgicalPlanningTutorialMarkupsSelfTest.py @@ -13,14 +13,14 @@ # class NeurosurgicalPlanningTutorialMarkupsSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "NeurosurgicalPlanningTutorialMarkupsSelfTest" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = ["Segmentations"] - self.parent.contributors = ["Nicole Aucoin (BWH), Andras Lasso (PerkLab, Queen's)"] - self.parent.helpText = """This is a test case that exercises the fiducials used in the Neurosurgical Planning tutorial.""" - parent.acknowledgementText = """This file was originally developed by Nicole Aucoin, BWH + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "NeurosurgicalPlanningTutorialMarkupsSelfTest" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = ["Segmentations"] + self.parent.contributors = ["Nicole Aucoin (BWH), Andras Lasso (PerkLab, Queen's)"] + self.parent.helpText = """This is a test case that exercises the fiducials used in the Neurosurgical Planning tutorial.""" + parent.acknowledgementText = """This file was originally developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218-12S1. The test was updated to use Segment editor by Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care""" @@ -31,62 +31,62 @@ def __init__(self, parent): # class NeurosurgicalPlanningTutorialMarkupsSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - # Instantiate and connect widgets ... - - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) - - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - - # - # check box to trigger taking screen shots for later use in tutorials - # - self.enableScreenshotsFlagCheckBox = qt.QCheckBox() - self.enableScreenshotsFlagCheckBox.checked = 0 - self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") - parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) - - # - # scale factor for screen shots - # - self.screenshotScaleFactorSliderWidget = ctk.ctkSliderWidget() - self.screenshotScaleFactorSliderWidget.singleStep = 1.0 - self.screenshotScaleFactorSliderWidget.minimum = 1.0 - self.screenshotScaleFactorSliderWidget.maximum = 50.0 - self.screenshotScaleFactorSliderWidget.value = 1.0 - self.screenshotScaleFactorSliderWidget.setToolTip("Set scale factor for the screen shots.") - parametersFormLayout.addRow("Screenshot scale factor", self.screenshotScaleFactorSliderWidget) - - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the algorithm." - self.applyButton.enabled = True - parametersFormLayout.addRow(self.applyButton) - - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) - - # Add vertical spacer - self.layout.addStretch(1) - - def cleanup(self): - pass - - def onApplyButton(self): - logging.debug("Execute logic.run() method") - logic = NeurosurgicalPlanningTutorialMarkupsSelfTestLogic() - logic.enableScreenshots = self.enableScreenshotsFlagCheckBox.checked - logic.screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) - logic.run() + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + # Instantiate and connect widgets ... + + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) + + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + + # + # check box to trigger taking screen shots for later use in tutorials + # + self.enableScreenshotsFlagCheckBox = qt.QCheckBox() + self.enableScreenshotsFlagCheckBox.checked = 0 + self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") + parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) + + # + # scale factor for screen shots + # + self.screenshotScaleFactorSliderWidget = ctk.ctkSliderWidget() + self.screenshotScaleFactorSliderWidget.singleStep = 1.0 + self.screenshotScaleFactorSliderWidget.minimum = 1.0 + self.screenshotScaleFactorSliderWidget.maximum = 50.0 + self.screenshotScaleFactorSliderWidget.value = 1.0 + self.screenshotScaleFactorSliderWidget.setToolTip("Set scale factor for the screen shots.") + parametersFormLayout.addRow("Screenshot scale factor", self.screenshotScaleFactorSliderWidget) + + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the algorithm." + self.applyButton.enabled = True + parametersFormLayout.addRow(self.applyButton) + + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) + + # Add vertical spacer + self.layout.addStretch(1) + + def cleanup(self): + pass + + def onApplyButton(self): + logging.debug("Execute logic.run() method") + logic = NeurosurgicalPlanningTutorialMarkupsSelfTestLogic() + logic.enableScreenshots = self.enableScreenshotsFlagCheckBox.checked + logic.screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value) + logic.run() # @@ -94,295 +94,295 @@ def onApplyButton(self): # class NeurosurgicalPlanningTutorialMarkupsSelfTestLogic(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 - """ - - def __init__(self): - ScriptedLoadableModuleLogic.__init__(self) - - # - # for the red slice widget, convert the background volume's RAS - # coordinates to display coordinates for painting - # - def rasToDisplay(self, r, a, s): - displayCoords = [0, 0, 0, 1] - - # get the slice node - lm = slicer.app.layoutManager() - sliceWidget = lm.sliceWidget('Red') - sliceLogic = sliceWidget.sliceLogic() - sliceNode = sliceLogic.GetSliceNode() - - xyToRASMatrix = sliceNode.GetXYToRAS() - rasToXyMatrix = vtk.vtkMatrix4x4() - rasToXyMatrix.Invert(xyToRASMatrix, rasToXyMatrix) - - worldCoords = [r, a, s, 1.0] - rasToXyMatrix.MultiplyPoint(worldCoords, displayCoords) - - return (int(displayCoords[0]), int(displayCoords[1])) - - -class NeurosurgicalPlanningTutorialMarkupsSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. - """ - slicer.mrmlScene.Clear(0) - # reset to conventional layout - lm = slicer.app.layoutManager() - lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - - def runTest(self): - """Run as few or as many tests as needed here. + """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 """ - self.setUp() - self.test_NeurosurgicalPlanningTutorialMarkupsSelfTest1() - - def test_NeurosurgicalPlanningTutorialMarkupsSelfTest1(self): - - self.delayDisplay("Starting the Neurosurgical Planning Tutorial Markups test") - - # start in the welcome module - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('Welcome') - - logic = NeurosurgicalPlanningTutorialMarkupsSelfTestLogic() - self.delayDisplay('Running test of the Neurosurgical Planning tutorial') - - # conventional layout - lm = slicer.app.layoutManager() - lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - # - # first load the data - # - if self.enableScreenshots: - # for the tutorial, do it through the welcome module - slicer.util.selectModule('Welcome') - self.delayDisplay("Screenshot") - self.takeScreenshot('NeurosurgicalPlanning-Welcome','Welcome module') - else: - # otherwise show the sample data module - slicer.util.selectModule('SampleData') - - # use the sample data module logic to load data for the self test - self.delayDisplay("Getting Baseline volume") - import SampleData - baselineVolume = SampleData.downloadSample('BaselineVolume') - - self.takeScreenshot('NeurosurgicalPlanning-Loaded','Data loaded') + def __init__(self): + ScriptedLoadableModuleLogic.__init__(self) # - # link the viewers + # for the red slice widget, convert the background volume's RAS + # coordinates to display coordinates for painting # + def rasToDisplay(self, r, a, s): + displayCoords = [0, 0, 0, 1] - if self.enableScreenshots: - # for the tutorial, pop up the linking control - sliceController = slicer.app.layoutManager().sliceWidget("Red").sliceController() - popupWidget = sliceController.findChild("ctkPopupWidget") - if popupWidget is not None: - popupWidget.pinPopup(1) - self.takeScreenshot('NeurosurgicalPlanning-Link','Link slice viewers') - popupWidget.pinPopup(0) + # get the slice node + lm = slicer.app.layoutManager() + sliceWidget = lm.sliceWidget('Red') + sliceLogic = sliceWidget.sliceLogic() + sliceNode = sliceLogic.GetSliceNode() - sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic() - compositeNode = sliceLogic.GetSliceCompositeNode() - compositeNode.SetLinkedControl(1) + xyToRASMatrix = sliceNode.GetXYToRAS() + rasToXyMatrix = vtk.vtkMatrix4x4() + rasToXyMatrix.Invert(xyToRASMatrix, rasToXyMatrix) - # - # baseline in the background - # - sliceLogic.StartSliceCompositeNodeInteraction(1) - compositeNode.SetBackgroundVolumeID(baselineVolume.GetID()) - slicer.app.processEvents() - sliceLogic.FitSliceToAll() - sliceLogic.EndSliceCompositeNodeInteraction() - self.takeScreenshot('NeurosurgicalPlanning-Baseline','Baseline in background') + worldCoords = [r, a, s, 1.0] + rasToXyMatrix.MultiplyPoint(worldCoords, displayCoords) - # - # adjust window level on baseline - # - slicer.util.selectModule('Volumes') - baselineDisplay = baselineVolume.GetDisplayNode() - baselineDisplay.SetAutoWindowLevel(0) - baselineDisplay.SetWindow(2600) - baselineDisplay.SetLevel(1206) - self.takeScreenshot('NeurosurgicalPlanning-WindowLevel','Set W/L on baseline') + return (int(displayCoords[0]), int(displayCoords[1])) - # - # switch to red slice only - # - lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView) - slicer.app.processEvents() - sliceLogic.FitSliceToAll() - self.takeScreenshot('NeurosurgicalPlanning-RedSliceOnly','Set layout to Red Slice only') - - # - # segmentation of tumor - # - # Create segmentation - segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", baselineVolume.GetName() + '-segmentation') - segmentationNode.CreateDefaultDisplayNodes() - segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(baselineVolume) - - # - # segment editor module - # - slicer.util.selectModule('SegmentEditor') - segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor - segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode() - segmentEditorNode.SetAndObserveSegmentationNode(segmentationNode) - segmentEditorNode.SetAndObserveMasterVolumeNode(baselineVolume) - - # create segments - region1SegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Tumor-cystic") - region2SegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Tumor-solid") - backgroundSegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Background") - ventriclesSegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Ventricles") - segmentEditorNode.SetSelectedSegmentID(region1SegmentId) - # Make segmentation results visible in 3D - segmentationNode.CreateClosedSurfaceRepresentation() - - self.takeScreenshot('NeurosurgicalPlanning-Editor','Showing Editor Module') - - # set the slice offset so drawing is right - sliceNode = sliceLogic.GetSliceNode() - sliceOffset = 58.7 - sliceNode.SetSliceOffset(sliceOffset) - - # - # paint - # - segmentEditorWidget.setActiveEffectByName("Paint") - paintEffect = segmentEditorWidget.activeEffect() - paintEffect.setParameter("BrushDiameterIsRelative", 0) - paintEffect.setParameter("BrushAbsoluteDiameter", 4.0) - - self.takeScreenshot('NeurosurgicalPlanning-Paint','Paint tool in Editor Module') - - # - # paint in cystic part of tumor, using conversion from RAS coords to - # avoid slice widget size differences - # - segmentEditorNode.SetSelectedSegmentID(region1SegmentId) - - clickCoordsList = [ - [-7.4, 71, sliceOffset], - [-11, 73, sliceOffset], - [-12, 85, sliceOffset], - [-13, 91, sliceOffset], - [-15, 78, sliceOffset]] - sliceWidget = lm.sliceWidget('Red') - currentCoords = None - for clickCoords in clickCoordsList: - if currentCoords: - slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(*currentCoords), end=logic.rasToDisplay(*clickCoords), steps=10) - currentCoords = clickCoords - - self.takeScreenshot('NeurosurgicalPlanning-PaintCystic','Paint cystic part of tumor') - - # - # paint in solid part of tumor - # - segmentEditorNode.SetSelectedSegmentID(region2SegmentId) - slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(-0.5, 118.5, sliceOffset), end=logic.rasToDisplay(-7.4, 116, sliceOffset), steps=10) - self.takeScreenshot('NeurosurgicalPlanning-PaintSolid','Paint solid part of tumor') - - # - # paint around the tumor - # - segmentEditorNode.SetSelectedSegmentID(backgroundSegmentId) - clickCoordsList = [ - [-40, 50, sliceOffset], - [ 30, 50, sliceOffset], - [ 30, 145, sliceOffset], - [-40, 145, sliceOffset], - [-40, 50, sliceOffset]] - sliceWidget = lm.sliceWidget('Red') - currentCoords = None - for clickCoords in clickCoordsList: - if currentCoords: - slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(*currentCoords), end=logic.rasToDisplay(*clickCoords), steps=30) - currentCoords = clickCoords - self.takeScreenshot('NeurosurgicalPlanning-PaintAround','Paint around tumor') - - # - # Grow cut - # - segmentEditorWidget.setActiveEffectByName("Grow from seeds") - effect = segmentEditorWidget.activeEffect() - effect.self().onPreview() - effect.self().onApply() - self.takeScreenshot('NeurosurgicalPlanning-Growcut','Growcut') - - #segmentationNode.RemoveSegment(backgroundSegmentId) - - # - # go to the data module - # - slicer.util.selectModule('Data') - self.takeScreenshot('NeurosurgicalPlanning-GrowCutData','GrowCut segmentation results in Data') - - # - # Ventricles Segmentation - # - - slicer.util.selectModule('SegmentEditor') - - segmentEditorNode.SetSelectedSegmentID(ventriclesSegmentId) - segmentEditorNode.SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteNone) - - # Thresholding - segmentEditorWidget.setActiveEffectByName("Threshold") - effect = segmentEditorWidget.activeEffect() - effect.setParameter("MinimumThreshold","1700") - #effect.setParameter("MaximumThreshold","695") - effect.self().onApply() - self.takeScreenshot('NeurosurgicalPlanning-Ventricles','Ventricles segmentation') - - # - # Save Islands - # - segmentEditorWidget.setActiveEffectByName("Islands") - effect = segmentEditorWidget.activeEffect() - effect.setParameter("Operation","KEEP_SELECTED_ISLAND") - slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(25.3, 5.8, sliceOffset), end=logic.rasToDisplay(25.3, 5.8, sliceOffset), steps=1) - self.takeScreenshot('NeurosurgicalPlanning-SaveIsland','Ventricles save island') - - # - # switch to conventional layout - # - lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - self.takeScreenshot('NeurosurgicalPlanning-MergeAndBuild','Merged and built models') - - # - # Smoothing - # - segmentEditorNode.SetSelectedSegmentID(region2SegmentId) - segmentEditorWidget.setActiveEffectByName("Smoothing") - effect = segmentEditorWidget.activeEffect() - effect.setParameter("SmoothingMethod", "MEDIAN") - effect.setParameter("KernelSizeMm", 5) - effect.self().onApply() - self.takeScreenshot('NeurosurgicalPlanning-Smoothed','Smoothed cystic region') +class NeurosurgicalPlanningTutorialMarkupsSelfTestTest(ScriptedLoadableModuleTest): + """ + This is the test case for your scripted module. + """ - # - # Dilation - # - segmentEditorNode.SetSelectedSegmentID(region1SegmentId) - segmentEditorWidget.setActiveEffectByName("Margin") - effect = segmentEditorWidget.activeEffect() - effect.setParameter("MarginSizeMm", 3.0) - effect.self().onApply() - self.takeScreenshot('NeurosurgicalPlanning-Dilated','Dilated tumor') - - self.delayDisplay('Test passed!') + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) + # reset to conventional layout + lm = slicer.app.layoutManager() + lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_NeurosurgicalPlanningTutorialMarkupsSelfTest1() + + def test_NeurosurgicalPlanningTutorialMarkupsSelfTest1(self): + + self.delayDisplay("Starting the Neurosurgical Planning Tutorial Markups test") + + # start in the welcome module + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('Welcome') + + logic = NeurosurgicalPlanningTutorialMarkupsSelfTestLogic() + self.delayDisplay('Running test of the Neurosurgical Planning tutorial') + + # conventional layout + lm = slicer.app.layoutManager() + lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + + # + # first load the data + # + if self.enableScreenshots: + # for the tutorial, do it through the welcome module + slicer.util.selectModule('Welcome') + self.delayDisplay("Screenshot") + self.takeScreenshot('NeurosurgicalPlanning-Welcome', 'Welcome module') + else: + # otherwise show the sample data module + slicer.util.selectModule('SampleData') + + # use the sample data module logic to load data for the self test + self.delayDisplay("Getting Baseline volume") + import SampleData + baselineVolume = SampleData.downloadSample('BaselineVolume') + + self.takeScreenshot('NeurosurgicalPlanning-Loaded', 'Data loaded') + + # + # link the viewers + # + + if self.enableScreenshots: + # for the tutorial, pop up the linking control + sliceController = slicer.app.layoutManager().sliceWidget("Red").sliceController() + popupWidget = sliceController.findChild("ctkPopupWidget") + if popupWidget is not None: + popupWidget.pinPopup(1) + self.takeScreenshot('NeurosurgicalPlanning-Link', 'Link slice viewers') + popupWidget.pinPopup(0) + + sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic() + compositeNode = sliceLogic.GetSliceCompositeNode() + compositeNode.SetLinkedControl(1) + + # + # baseline in the background + # + sliceLogic.StartSliceCompositeNodeInteraction(1) + compositeNode.SetBackgroundVolumeID(baselineVolume.GetID()) + slicer.app.processEvents() + sliceLogic.FitSliceToAll() + sliceLogic.EndSliceCompositeNodeInteraction() + self.takeScreenshot('NeurosurgicalPlanning-Baseline', 'Baseline in background') + + # + # adjust window level on baseline + # + slicer.util.selectModule('Volumes') + baselineDisplay = baselineVolume.GetDisplayNode() + baselineDisplay.SetAutoWindowLevel(0) + baselineDisplay.SetWindow(2600) + baselineDisplay.SetLevel(1206) + self.takeScreenshot('NeurosurgicalPlanning-WindowLevel', 'Set W/L on baseline') + + # + # switch to red slice only + # + lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView) + slicer.app.processEvents() + sliceLogic.FitSliceToAll() + self.takeScreenshot('NeurosurgicalPlanning-RedSliceOnly', 'Set layout to Red Slice only') + + # + # segmentation of tumor + # + + # Create segmentation + segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", baselineVolume.GetName() + '-segmentation') + segmentationNode.CreateDefaultDisplayNodes() + segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(baselineVolume) + + # + # segment editor module + # + slicer.util.selectModule('SegmentEditor') + segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor + segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode() + segmentEditorNode.SetAndObserveSegmentationNode(segmentationNode) + segmentEditorNode.SetAndObserveMasterVolumeNode(baselineVolume) + + # create segments + region1SegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Tumor-cystic") + region2SegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Tumor-solid") + backgroundSegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Background") + ventriclesSegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Ventricles") + segmentEditorNode.SetSelectedSegmentID(region1SegmentId) + # Make segmentation results visible in 3D + segmentationNode.CreateClosedSurfaceRepresentation() + + self.takeScreenshot('NeurosurgicalPlanning-Editor', 'Showing Editor Module') + + # set the slice offset so drawing is right + sliceNode = sliceLogic.GetSliceNode() + sliceOffset = 58.7 + sliceNode.SetSliceOffset(sliceOffset) + + # + # paint + # + segmentEditorWidget.setActiveEffectByName("Paint") + paintEffect = segmentEditorWidget.activeEffect() + paintEffect.setParameter("BrushDiameterIsRelative", 0) + paintEffect.setParameter("BrushAbsoluteDiameter", 4.0) + + self.takeScreenshot('NeurosurgicalPlanning-Paint', 'Paint tool in Editor Module') + + # + # paint in cystic part of tumor, using conversion from RAS coords to + # avoid slice widget size differences + # + segmentEditorNode.SetSelectedSegmentID(region1SegmentId) + + clickCoordsList = [ + [-7.4, 71, sliceOffset], + [-11, 73, sliceOffset], + [-12, 85, sliceOffset], + [-13, 91, sliceOffset], + [-15, 78, sliceOffset]] + sliceWidget = lm.sliceWidget('Red') + currentCoords = None + for clickCoords in clickCoordsList: + if currentCoords: + slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(*currentCoords), end=logic.rasToDisplay(*clickCoords), steps=10) + currentCoords = clickCoords + + self.takeScreenshot('NeurosurgicalPlanning-PaintCystic', 'Paint cystic part of tumor') + + # + # paint in solid part of tumor + # + segmentEditorNode.SetSelectedSegmentID(region2SegmentId) + slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(-0.5, 118.5, sliceOffset), end=logic.rasToDisplay(-7.4, 116, sliceOffset), steps=10) + self.takeScreenshot('NeurosurgicalPlanning-PaintSolid', 'Paint solid part of tumor') + + # + # paint around the tumor + # + segmentEditorNode.SetSelectedSegmentID(backgroundSegmentId) + clickCoordsList = [ + [-40, 50, sliceOffset], + [30, 50, sliceOffset], + [30, 145, sliceOffset], + [-40, 145, sliceOffset], + [-40, 50, sliceOffset]] + sliceWidget = lm.sliceWidget('Red') + currentCoords = None + for clickCoords in clickCoordsList: + if currentCoords: + slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(*currentCoords), end=logic.rasToDisplay(*clickCoords), steps=30) + currentCoords = clickCoords + self.takeScreenshot('NeurosurgicalPlanning-PaintAround', 'Paint around tumor') + + # + # Grow cut + # + segmentEditorWidget.setActiveEffectByName("Grow from seeds") + effect = segmentEditorWidget.activeEffect() + effect.self().onPreview() + effect.self().onApply() + self.takeScreenshot('NeurosurgicalPlanning-Growcut', 'Growcut') + + # segmentationNode.RemoveSegment(backgroundSegmentId) + + # + # go to the data module + # + slicer.util.selectModule('Data') + self.takeScreenshot('NeurosurgicalPlanning-GrowCutData', 'GrowCut segmentation results in Data') + + # + # Ventricles Segmentation + # + + slicer.util.selectModule('SegmentEditor') + + segmentEditorNode.SetSelectedSegmentID(ventriclesSegmentId) + segmentEditorNode.SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteNone) + + # Thresholding + segmentEditorWidget.setActiveEffectByName("Threshold") + effect = segmentEditorWidget.activeEffect() + effect.setParameter("MinimumThreshold", "1700") + # effect.setParameter("MaximumThreshold","695") + effect.self().onApply() + self.takeScreenshot('NeurosurgicalPlanning-Ventricles', 'Ventricles segmentation') + + # + # Save Islands + # + segmentEditorWidget.setActiveEffectByName("Islands") + effect = segmentEditorWidget.activeEffect() + effect.setParameter("Operation", "KEEP_SELECTED_ISLAND") + slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(25.3, 5.8, sliceOffset), end=logic.rasToDisplay(25.3, 5.8, sliceOffset), steps=1) + self.takeScreenshot('NeurosurgicalPlanning-SaveIsland', 'Ventricles save island') + + # + # switch to conventional layout + # + lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + self.takeScreenshot('NeurosurgicalPlanning-MergeAndBuild', 'Merged and built models') + + # + # Smoothing + # + segmentEditorNode.SetSelectedSegmentID(region2SegmentId) + segmentEditorWidget.setActiveEffectByName("Smoothing") + effect = segmentEditorWidget.activeEffect() + effect.setParameter("SmoothingMethod", "MEDIAN") + effect.setParameter("KernelSizeMm", 5) + effect.self().onApply() + self.takeScreenshot('NeurosurgicalPlanning-Smoothed', 'Smoothed cystic region') + + # + # Dilation + # + segmentEditorNode.SetSelectedSegmentID(region1SegmentId) + segmentEditorWidget.setActiveEffectByName("Margin") + effect = segmentEditorWidget.activeEffect() + effect.setParameter("MarginSizeMm", 3.0) + effect.self().onApply() + self.takeScreenshot('NeurosurgicalPlanning-Dilated', 'Dilated tumor') + + self.delayDisplay('Test passed!') diff --git a/Modules/Loadable/Markups/Testing/Python/PluggableMarkupsSelfTest.py b/Modules/Loadable/Markups/Testing/Python/PluggableMarkupsSelfTest.py index 15140437fd4..9125874539f 100644 --- a/Modules/Loadable/Markups/Testing/Python/PluggableMarkupsSelfTest.py +++ b/Modules/Loadable/Markups/Testing/Python/PluggableMarkupsSelfTest.py @@ -11,18 +11,18 @@ # PluggableMarkupsSelfTest # class PluggableMarkupsSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "PluggableMarkupsSelfTest" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = [] - self.parent.contributors = ["Rafael Palomar (OUS)"] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "PluggableMarkupsSelfTest" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = [] + self.parent.contributors = ["Rafael Palomar (OUS)"] + self.parent.helpText = """ This is a test case for the markups pluggable architecture. It unregisters the markups provided by the Markups module and registers them again. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ This file was originally developed by Rafael Palomar (OUS) and was funded by the Research Council of Norway (grant nr. 311393). """ @@ -33,240 +33,240 @@ def __init__(self, parent): # class PluggableMarkupsSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + # Instantiate and connect widgets ... - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the test." - self.applyButton.enabled = True - parametersFormLayout.addRow(self.applyButton) + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the test." + self.applyButton.enabled = True + parametersFormLayout.addRow(self.applyButton) - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - def cleanup(self): - pass + def cleanup(self): + pass - def onApplyButton(self): - logging.debug("Execute logic.run() method") - logic = PluggableMarkupsSelfTestLogic() - logic.run() + def onApplyButton(self): + logging.debug("Execute logic.run() method") + logic = PluggableMarkupsSelfTestLogic() + logic.run() class PluggableMarkupsSelfTestLogic(ScriptedLoadableModuleLogic): - def __init__(self): - ScriptedLoadableModuleLogic.__init__(self) - - def setUp(self): - # - # Step 1: Register all available markups nodes - # - - markupsWidget = slicer.modules.markups.widgetRepresentation() - if markupsWidget is None: - raise Exception("Couldn't get the Markups module widget") - - markupsLogic = slicer.modules.markups.logic() - if markupsLogic is None: - raise Exception("Couldn't get the Markups module logic") - - markupsNodes = self.markupsNodes() - - #Check Markups module standard nodes are registered - for markupNode in markupsNodes: - markupsLogic.RegisterMarkupsNode(markupNode, markupsNodes[markupNode]) - - # - # Step 2: Register all available additional options widgets - # - additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance() - for additionalOptionsWidget in self.additionalOptionsWidgets(): - additionalOptionsWidgetsFactory.registerOptionsWidget(additionalOptionsWidget) - - def __checkPushButtonExists(self, widget, name): - pushButtonObjectName = "Create%sPushButton" % name - #slicer.util.delayDisplay("Checking whether '%s' exists" % pushButtonObjectName) - if widget.findChild(qt.QPushButton, pushButtonObjectName): - return True - return False - - def __checkWidgetExists(self, widget, name): - #slicer.util.delayDisplay("Checking whether '%s' exists" % name) - if widget.findChild(qt.QWidget, name): - return True - return False - - def __checkWidgetVisibility(self, widget, name): - #slicer.util.delayDisplay("Checking whether '%s' is visible" % pushButtonObjectName) - w = widget.findChild(qt.QWidget, name) - return w.isVisible() - - def markupsNodes(self): - return { - slicer.vtkMRMLMarkupsAngleNode(): slicer.vtkSlicerAngleWidget(), - slicer.vtkMRMLMarkupsClosedCurveNode(): slicer.vtkSlicerCurveWidget(), - slicer.vtkMRMLMarkupsCurveNode(): slicer.vtkSlicerCurveWidget(), - slicer.vtkMRMLMarkupsFiducialNode(): slicer.vtkSlicerPointsWidget(), - slicer.vtkMRMLMarkupsLineNode(): slicer.vtkSlicerLineWidget(), - slicer.vtkMRMLMarkupsPlaneNode(): slicer.vtkSlicerPlaneWidget(), - slicer.vtkMRMLMarkupsROINode(): slicer.vtkSlicerROIWidget(), - slicer.vtkMRMLMarkupsTestLineNode(): slicer.vtkSlicerTestLineWidget() - } - - def additionalOptionsWidgets(self): - return [ - slicer.qMRMLMarkupsCurveSettingsWidget(), - slicer.qMRMLMarkupsAngleMeasurementsWidget(), - slicer.qMRMLMarkupsPlaneWidget(), - slicer.qMRMLMarkupsROIWidget(), - slicer.qMRMLMarkupsTestLineWidget() - ] - - def test_unregister_existing_markups(self): - """ - This unregisters existing registered markups - """ - - markupsWidget = slicer.modules.markups.widgetRepresentation() - if markupsWidget is None: - raise Exception("Couldn't get the Markups module widget") - - #Check Markups module standard nodes are registered - for markupNode in self.markupsNodes(): - if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()) is None: - raise Exception("Create PushButton for %s is not present" % markupNode.GetMarkupType()) - - markupsLogic = slicer.modules.markups.logic() - if markupsLogic is None: - raise Exception("Couldn't get the Markups module logic") - - # Unregister Markups and check the buttons are gone - for markupNode in self.markupsNodes(): - slicer.util.delayDisplay("Unregistering %s" % markupNode.GetMarkupType()) - markupsLogic.UnregisterMarkupsNode(markupNode) - if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()): - raise Exception("Create PushButton for %s is present after unregistration" % markupNode.GetMarkupType()) - - def test_register_markups(self): - """ - This registers all known markups - """ - markupsWidget = slicer.modules.markups.widgetRepresentation() - if markupsWidget is None: - raise Exception("Couldn't get the Markups module widget") - - markupsLogic = slicer.modules.markups.logic() - if markupsLogic is None: - raise Exception("Couldn't get the Markups module logic") - - markupsNodes = self.markupsNodes() - - #Check Markups module standard nodes are registered - for markupNode in markupsNodes: - slicer.util.delayDisplay("Registering %s" % markupNode.GetMarkupType()) - markupsLogic.RegisterMarkupsNode(markupNode, markupsNodes[markupNode]) - if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()) is None: - raise Exception("Create PushButton for %s is not present" % markupNode.GetMarkupType()) - - def test_unregister_additional_options_widgets(self): - """ - This unregisters all the additional options widgets - """ - markupsWidget = slicer.modules.markups.widgetRepresentation() - if markupsWidget is None: - raise Exception("Couldn't get the Markups module widget") - - additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance() - for additionalOptionsWidget in self.additionalOptionsWidgets(): + def __init__(self): + ScriptedLoadableModuleLogic.__init__(self) + + def setUp(self): + # + # Step 1: Register all available markups nodes + # + + markupsWidget = slicer.modules.markups.widgetRepresentation() + if markupsWidget is None: + raise Exception("Couldn't get the Markups module widget") + + markupsLogic = slicer.modules.markups.logic() + if markupsLogic is None: + raise Exception("Couldn't get the Markups module logic") + + markupsNodes = self.markupsNodes() + + # Check Markups module standard nodes are registered + for markupNode in markupsNodes: + markupsLogic.RegisterMarkupsNode(markupNode, markupsNodes[markupNode]) + + # + # Step 2: Register all available additional options widgets + # + additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance() + for additionalOptionsWidget in self.additionalOptionsWidgets(): + additionalOptionsWidgetsFactory.registerOptionsWidget(additionalOptionsWidget) + + def __checkPushButtonExists(self, widget, name): + pushButtonObjectName = "Create%sPushButton" % name + # slicer.util.delayDisplay("Checking whether '%s' exists" % pushButtonObjectName) + if widget.findChild(qt.QPushButton, pushButtonObjectName): + return True + return False + + def __checkWidgetExists(self, widget, name): + # slicer.util.delayDisplay("Checking whether '%s' exists" % name) + if widget.findChild(qt.QWidget, name): + return True + return False + + def __checkWidgetVisibility(self, widget, name): + # slicer.util.delayDisplay("Checking whether '%s' is visible" % pushButtonObjectName) + w = widget.findChild(qt.QWidget, name) + return w.isVisible() + + def markupsNodes(self): + return { + slicer.vtkMRMLMarkupsAngleNode(): slicer.vtkSlicerAngleWidget(), + slicer.vtkMRMLMarkupsClosedCurveNode(): slicer.vtkSlicerCurveWidget(), + slicer.vtkMRMLMarkupsCurveNode(): slicer.vtkSlicerCurveWidget(), + slicer.vtkMRMLMarkupsFiducialNode(): slicer.vtkSlicerPointsWidget(), + slicer.vtkMRMLMarkupsLineNode(): slicer.vtkSlicerLineWidget(), + slicer.vtkMRMLMarkupsPlaneNode(): slicer.vtkSlicerPlaneWidget(), + slicer.vtkMRMLMarkupsROINode(): slicer.vtkSlicerROIWidget(), + slicer.vtkMRMLMarkupsTestLineNode(): slicer.vtkSlicerTestLineWidget() + } + + def additionalOptionsWidgets(self): + return [ + slicer.qMRMLMarkupsCurveSettingsWidget(), + slicer.qMRMLMarkupsAngleMeasurementsWidget(), + slicer.qMRMLMarkupsPlaneWidget(), + slicer.qMRMLMarkupsROIWidget(), + slicer.qMRMLMarkupsTestLineWidget() + ] + + def test_unregister_existing_markups(self): + """ + This unregisters existing registered markups + """ + + markupsWidget = slicer.modules.markups.widgetRepresentation() + if markupsWidget is None: + raise Exception("Couldn't get the Markups module widget") + + # Check Markups module standard nodes are registered + for markupNode in self.markupsNodes(): + if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()) is None: + raise Exception("Create PushButton for %s is not present" % markupNode.GetMarkupType()) + + markupsLogic = slicer.modules.markups.logic() + if markupsLogic is None: + raise Exception("Couldn't get the Markups module logic") + + # Unregister Markups and check the buttons are gone + for markupNode in self.markupsNodes(): + slicer.util.delayDisplay("Unregistering %s" % markupNode.GetMarkupType()) + markupsLogic.UnregisterMarkupsNode(markupNode) + if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()): + raise Exception("Create PushButton for %s is present after unregistration" % markupNode.GetMarkupType()) + + def test_register_markups(self): + """ + This registers all known markups + """ + markupsWidget = slicer.modules.markups.widgetRepresentation() + if markupsWidget is None: + raise Exception("Couldn't get the Markups module widget") + + markupsLogic = slicer.modules.markups.logic() + if markupsLogic is None: + raise Exception("Couldn't get the Markups module logic") + + markupsNodes = self.markupsNodes() + + # Check Markups module standard nodes are registered + for markupNode in markupsNodes: + slicer.util.delayDisplay("Registering %s" % markupNode.GetMarkupType()) + markupsLogic.RegisterMarkupsNode(markupNode, markupsNodes[markupNode]) + if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()) is None: + raise Exception("Create PushButton for %s is not present" % markupNode.GetMarkupType()) + + def test_unregister_additional_options_widgets(self): + """ + This unregisters all the additional options widgets + """ + markupsWidget = slicer.modules.markups.widgetRepresentation() + if markupsWidget is None: + raise Exception("Couldn't get the Markups module widget") + + additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance() + for additionalOptionsWidget in self.additionalOptionsWidgets(): + + # Check the widget exists + if not self.__checkWidgetExists(markupsWidget, additionalOptionsWidget.objectName): + raise Exception("%s does not exist" % additionalOptionsWidget.objectName) + + # NOTE: since the widget will get destroyed, we take note of the name for the checking step + objectName = additionalOptionsWidget.objectName + + # Unregister widget + additionalOptionsWidgetsFactory.unregisterOptionsWidget(additionalOptionsWidget.className) + + # Check the widget does not exist + if self.__checkWidgetExists(markupsWidget, objectName): + raise Exception("%s does still exist" % objectName) + + def test_register_additional_options_widgets(self): + """ + This reigisters additional options widgets + """ + + additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance() + + markupsWidget = slicer.modules.markups.widgetRepresentation() + if markupsWidget is None: + raise Exception("Couldn't get the Markups module widget") + + for additionalOptionsWidget in self.additionalOptionsWidgets(): + name = additionalOptionsWidget.objectName + slicer.util.delayDisplay("Registering %s" % additionalOptionsWidget.objectName) + additionalOptionsWidgetsFactory.registerOptionsWidget(additionalOptionsWidget) + + # Check the widget exists + if not self.__checkWidgetExists(markupsWidget, name): + raise Exception("%s does not exist" % additionalOptionsWidget.objectName) + + def run(self): + """ + Run the tests + """ + slicer.util.delayDisplay('Running integration tests for Pluggable Markups') + + self.test_unregister_existing_markups() + self.test_register_markups() + # self.test_unregister_additional_options_widgets() + self.test_register_additional_options_widgets() + + logging.info('Process completed') - # Check the widget exists - if not self.__checkWidgetExists(markupsWidget, additionalOptionsWidget.objectName): - raise Exception("%s does not exist" % additionalOptionsWidget.objectName) - #NOTE: since the widget will get destroyed, we take note of the name for the checking step - objectName = additionalOptionsWidget.objectName - - # Unregister widget - additionalOptionsWidgetsFactory.unregisterOptionsWidget(additionalOptionsWidget.className) - - # Check the widget does not exist - if self.__checkWidgetExists(markupsWidget, objectName): - raise Exception("%s does still exist" % objectName) - - def test_register_additional_options_widgets(self): - """ - This reigisters additional options widgets - """ - - additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance() - - markupsWidget = slicer.modules.markups.widgetRepresentation() - if markupsWidget is None: - raise Exception("Couldn't get the Markups module widget") - - for additionalOptionsWidget in self.additionalOptionsWidgets(): - name = additionalOptionsWidget.objectName - slicer.util.delayDisplay("Registering %s" % additionalOptionsWidget.objectName) - additionalOptionsWidgetsFactory.registerOptionsWidget(additionalOptionsWidget) - - # Check the widget exists - if not self.__checkWidgetExists(markupsWidget, name): - raise Exception("%s does not exist" % additionalOptionsWidget.objectName) - - def run(self): +class PluggableMarkupsSelfTestTest(ScriptedLoadableModuleTest): """ - Run the tests + This is the test case """ - slicer.util.delayDisplay('Running integration tests for Pluggable Markups') - - self.test_unregister_existing_markups() - self.test_register_markups() - # self.test_unregister_additional_options_widgets() - self.test_register_additional_options_widgets() - - logging.info('Process completed') - - -class PluggableMarkupsSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case - """ - def setUp(self): - logic = PluggableMarkupsSelfTestLogic() - logic.setUp() + def setUp(self): + logic = PluggableMarkupsSelfTestLogic() + logic.setUp() - def runTest(self): - self.setUp() - self.test_PluggableMarkupsSelfTest1() + def runTest(self): + self.setUp() + self.test_PluggableMarkupsSelfTest1() - def test_PluggableMarkupsSelfTest1(self): + def test_PluggableMarkupsSelfTest1(self): - self.delayDisplay("Starting the Pluggable Markups Test") + self.delayDisplay("Starting the Pluggable Markups Test") - # Open the markups module - slicer.util.mainWindow().moduleSelector().selectModule('Markups') - self.delayDisplay('In Markups module') + # Open the markups module + slicer.util.mainWindow().moduleSelector().selectModule('Markups') + self.delayDisplay('In Markups module') - logic = PluggableMarkupsSelfTestLogic() - logic.run() + logic = PluggableMarkupsSelfTestLogic() + logic.run() - self.delayDisplay('Test passed!') + self.delayDisplay('Test passed!') diff --git a/Modules/Loadable/Markups/Widgets/Testing/Python/MarkupsWidgetsSelfTest.py b/Modules/Loadable/Markups/Widgets/Testing/Python/MarkupsWidgetsSelfTest.py index c1b21bc2e22..95d41823312 100644 --- a/Modules/Loadable/Markups/Widgets/Testing/Python/MarkupsWidgetsSelfTest.py +++ b/Modules/Loadable/Markups/Widgets/Testing/Python/MarkupsWidgetsSelfTest.py @@ -9,14 +9,14 @@ # class MarkupsWidgetsSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "MarkupsWidgetsSelfTest" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = ["Markups"] - self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"] - self.parent.helpText = """This is a self test for Markups widgets.""" - self.parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care""" + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "MarkupsWidgetsSelfTest" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = ["Markups"] + self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"] + self.parent.helpText = """This is a self test for Markups widgets.""" + self.parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care""" # @@ -24,8 +24,8 @@ def __init__(self, parent): # class MarkupsWidgetsSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -33,170 +33,170 @@ def setup(self): # class MarkupsWidgetsSelfTestLogic(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 - """ + """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 + """ - def __init__(self): - pass + def __init__(self): + pass class MarkupsWidgetsSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ - slicer.mrmlScene.Clear(0) - - self.delayMs = 700 - - def runTest(self): - """Run as few or as many tests as needed here. + This is the test case for your scripted module. """ - self.setUp() - self.test_MarkupsWidgetsSelfTest_FullTest1() - - # ------------------------------------------------------------------------------ - def test_MarkupsWidgetsSelfTest_FullTest1(self): - # Check for Tables module - self.assertTrue( slicer.modules.tables ) - - self.section_SetupPathsAndNames() - self.section_CreateMarkups() - self.section_SimpleMarkupsWidget() - self.section_MarkupsPlaceWidget() - self.delayDisplay("Test passed",self.delayMs) - - # ------------------------------------------------------------------------------ - def section_SetupPathsAndNames(self): - # Set constants - self.sampleMarkupsNodeName1 = 'SampleMarkups1' - self.sampleMarkupsNodeName2 = 'SampleMarkups2' - - # ------------------------------------------------------------------------------ - def section_CreateMarkups(self): - self.delayDisplay("Create markup nodes",self.delayMs) - - self.markupsLogic = slicer.modules.markups.logic() - - # Create sample markups node - self.markupsNode1 = slicer.mrmlScene.GetNodeByID(self.markupsLogic.AddNewFiducialNode()) - self.markupsNode1.SetName(self.sampleMarkupsNodeName1) - - self.markupsNode2 = slicer.mrmlScene.GetNodeByID(self.markupsLogic.AddNewFiducialNode()) - self.markupsNode2.SetName(self.sampleMarkupsNodeName2) - - # ------------------------------------------------------------------------------ - def section_SimpleMarkupsWidget(self): - self.delayDisplay("Test SimpleMarkupsWidget",self.delayMs) - - simpleMarkupsWidget = slicer.qSlicerSimpleMarkupsWidget() - nodeSelector = slicer.util.findChildren(simpleMarkupsWidget,"MarkupsNodeComboBox")[0] - self.assertIsNone(simpleMarkupsWidget.interactionNode()) - simpleMarkupsWidget.setMRMLScene(slicer.mrmlScene) - simpleMarkupsWidget.show() - - placeWidget = simpleMarkupsWidget.markupsPlaceWidget() - self.assertIsNotNone(placeWidget) - - simpleMarkupsWidget.setCurrentNode(None) - simpleMarkupsWidget.enterPlaceModeOnNodeChange = False - placeWidget.placeModeEnabled = False - nodeSelector.setCurrentNode(self.markupsNode1) - self.assertFalse(placeWidget.placeModeEnabled) - - simpleMarkupsWidget.enterPlaceModeOnNodeChange = True - nodeSelector.setCurrentNode(self.markupsNode2) - self.assertTrue(placeWidget.placeModeEnabled) - - simpleMarkupsWidget.jumpToSliceEnabled = True - self.assertTrue(simpleMarkupsWidget.jumpToSliceEnabled) - simpleMarkupsWidget.jumpToSliceEnabled = False - self.assertFalse(simpleMarkupsWidget.jumpToSliceEnabled) - - simpleMarkupsWidget.nodeSelectorVisible = False - self.assertFalse(simpleMarkupsWidget.nodeSelectorVisible) - simpleMarkupsWidget.nodeSelectorVisible = True - self.assertTrue(simpleMarkupsWidget.nodeSelectorVisible) - - simpleMarkupsWidget.optionsVisible = False - self.assertFalse(simpleMarkupsWidget.optionsVisible) - simpleMarkupsWidget.optionsVisible = True - self.assertTrue(simpleMarkupsWidget.optionsVisible) - - defaultColor = qt.QColor(0,255,0) - simpleMarkupsWidget.defaultNodeColor = defaultColor - self.assertEqual(simpleMarkupsWidget.defaultNodeColor, defaultColor) - - self.markupsNode3 = nodeSelector.addNode() - displayNode3 = self.markupsNode3.GetDisplayNode() - color3 = displayNode3.GetColor() - self.assertEqual(color3[0]*255, defaultColor.red()) - self.assertEqual(color3[1]*255, defaultColor.green()) - self.assertEqual(color3[2]*255, defaultColor.blue()) - - numberOfFiducialsAdded = 5 - for i in range(numberOfFiducialsAdded): - self.markupsNode3.AddControlPoint([i*20, i*15, i*5]) - - tableWidget = simpleMarkupsWidget.tableWidget() - self.assertEqual(tableWidget.rowCount, numberOfFiducialsAdded) - - self.assertEqual(simpleMarkupsWidget.interactionNode(), slicer.app.applicationLogic().GetInteractionNode()) - otherInteractionNode = slicer.vtkMRMLInteractionNode() - otherInteractionNode.SetSingletonOff() - slicer.mrmlScene.AddNode(otherInteractionNode) - simpleMarkupsWidget.setInteractionNode(otherInteractionNode) - self.assertEqual(simpleMarkupsWidget.interactionNode(), otherInteractionNode) - - # ------------------------------------------------------------------------------ - def section_MarkupsPlaceWidget(self): - self.delayDisplay("Test MarkupsPlaceWidget",self.delayMs) - - placeWidget = slicer.qSlicerMarkupsPlaceWidget() - self.assertIsNone(placeWidget.interactionNode()) - placeWidget.setMRMLScene(slicer.mrmlScene) - placeWidget.setCurrentNode(self.markupsNode1) - placeWidget.show() - - placeWidget.buttonsVisible = False - self.assertFalse(placeWidget.buttonsVisible) - placeWidget.buttonsVisible = True - self.assertTrue(placeWidget.buttonsVisible) - - placeWidget.deleteAllControlPointsOptionVisible = False - self.assertFalse(placeWidget.deleteAllControlPointsOptionVisible) - placeWidget.deleteAllControlPointsOptionVisible = True - self.assertTrue(placeWidget.deleteAllControlPointsOptionVisible) - - placeWidget.unsetLastControlPointOptionVisible = False - self.assertFalse(placeWidget.unsetLastControlPointOptionVisible) - placeWidget.unsetLastControlPointOptionVisible = True - self.assertTrue(placeWidget.unsetLastControlPointOptionVisible) - - placeWidget.unsetAllControlPointsOptionVisible = False - self.assertFalse(placeWidget.unsetAllControlPointsOptionVisible) - placeWidget.unsetAllControlPointsOptionVisible = True - self.assertTrue(placeWidget.unsetAllControlPointsOptionVisible) - - placeWidget.placeMultipleMarkups = slicer.qSlicerMarkupsPlaceWidget.ForcePlaceSingleMarkup - placeWidget.placeModeEnabled = True - self.assertFalse(placeWidget.placeModePersistency) - - placeWidget.placeMultipleMarkups = slicer.qSlicerMarkupsPlaceWidget.ForcePlaceMultipleMarkups - placeWidget.placeModeEnabled = False - placeWidget.placeModeEnabled = True - self.assertTrue(placeWidget.placeModePersistency) - - self.assertEqual(placeWidget.interactionNode(), slicer.app.applicationLogic().GetInteractionNode()) - otherInteractionNode = slicer.vtkMRMLInteractionNode() - otherInteractionNode.SetSingletonOff() - slicer.mrmlScene.AddNode(otherInteractionNode) - placeWidget.setInteractionNode(otherInteractionNode) - self.assertEqual(placeWidget.interactionNode(), otherInteractionNode) + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) + + self.delayMs = 700 + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_MarkupsWidgetsSelfTest_FullTest1() + + # ------------------------------------------------------------------------------ + def test_MarkupsWidgetsSelfTest_FullTest1(self): + # Check for Tables module + self.assertTrue(slicer.modules.tables) + + self.section_SetupPathsAndNames() + self.section_CreateMarkups() + self.section_SimpleMarkupsWidget() + self.section_MarkupsPlaceWidget() + self.delayDisplay("Test passed", self.delayMs) + + # ------------------------------------------------------------------------------ + def section_SetupPathsAndNames(self): + # Set constants + self.sampleMarkupsNodeName1 = 'SampleMarkups1' + self.sampleMarkupsNodeName2 = 'SampleMarkups2' + + # ------------------------------------------------------------------------------ + def section_CreateMarkups(self): + self.delayDisplay("Create markup nodes", self.delayMs) + + self.markupsLogic = slicer.modules.markups.logic() + + # Create sample markups node + self.markupsNode1 = slicer.mrmlScene.GetNodeByID(self.markupsLogic.AddNewFiducialNode()) + self.markupsNode1.SetName(self.sampleMarkupsNodeName1) + + self.markupsNode2 = slicer.mrmlScene.GetNodeByID(self.markupsLogic.AddNewFiducialNode()) + self.markupsNode2.SetName(self.sampleMarkupsNodeName2) + + # ------------------------------------------------------------------------------ + def section_SimpleMarkupsWidget(self): + self.delayDisplay("Test SimpleMarkupsWidget", self.delayMs) + + simpleMarkupsWidget = slicer.qSlicerSimpleMarkupsWidget() + nodeSelector = slicer.util.findChildren(simpleMarkupsWidget, "MarkupsNodeComboBox")[0] + self.assertIsNone(simpleMarkupsWidget.interactionNode()) + simpleMarkupsWidget.setMRMLScene(slicer.mrmlScene) + simpleMarkupsWidget.show() + + placeWidget = simpleMarkupsWidget.markupsPlaceWidget() + self.assertIsNotNone(placeWidget) + + simpleMarkupsWidget.setCurrentNode(None) + simpleMarkupsWidget.enterPlaceModeOnNodeChange = False + placeWidget.placeModeEnabled = False + nodeSelector.setCurrentNode(self.markupsNode1) + self.assertFalse(placeWidget.placeModeEnabled) + + simpleMarkupsWidget.enterPlaceModeOnNodeChange = True + nodeSelector.setCurrentNode(self.markupsNode2) + self.assertTrue(placeWidget.placeModeEnabled) + + simpleMarkupsWidget.jumpToSliceEnabled = True + self.assertTrue(simpleMarkupsWidget.jumpToSliceEnabled) + simpleMarkupsWidget.jumpToSliceEnabled = False + self.assertFalse(simpleMarkupsWidget.jumpToSliceEnabled) + + simpleMarkupsWidget.nodeSelectorVisible = False + self.assertFalse(simpleMarkupsWidget.nodeSelectorVisible) + simpleMarkupsWidget.nodeSelectorVisible = True + self.assertTrue(simpleMarkupsWidget.nodeSelectorVisible) + + simpleMarkupsWidget.optionsVisible = False + self.assertFalse(simpleMarkupsWidget.optionsVisible) + simpleMarkupsWidget.optionsVisible = True + self.assertTrue(simpleMarkupsWidget.optionsVisible) + + defaultColor = qt.QColor(0, 255, 0) + simpleMarkupsWidget.defaultNodeColor = defaultColor + self.assertEqual(simpleMarkupsWidget.defaultNodeColor, defaultColor) + + self.markupsNode3 = nodeSelector.addNode() + displayNode3 = self.markupsNode3.GetDisplayNode() + color3 = displayNode3.GetColor() + self.assertEqual(color3[0] * 255, defaultColor.red()) + self.assertEqual(color3[1] * 255, defaultColor.green()) + self.assertEqual(color3[2] * 255, defaultColor.blue()) + + numberOfFiducialsAdded = 5 + for i in range(numberOfFiducialsAdded): + self.markupsNode3.AddControlPoint([i * 20, i * 15, i * 5]) + + tableWidget = simpleMarkupsWidget.tableWidget() + self.assertEqual(tableWidget.rowCount, numberOfFiducialsAdded) + + self.assertEqual(simpleMarkupsWidget.interactionNode(), slicer.app.applicationLogic().GetInteractionNode()) + otherInteractionNode = slicer.vtkMRMLInteractionNode() + otherInteractionNode.SetSingletonOff() + slicer.mrmlScene.AddNode(otherInteractionNode) + simpleMarkupsWidget.setInteractionNode(otherInteractionNode) + self.assertEqual(simpleMarkupsWidget.interactionNode(), otherInteractionNode) + + # ------------------------------------------------------------------------------ + def section_MarkupsPlaceWidget(self): + self.delayDisplay("Test MarkupsPlaceWidget", self.delayMs) + + placeWidget = slicer.qSlicerMarkupsPlaceWidget() + self.assertIsNone(placeWidget.interactionNode()) + placeWidget.setMRMLScene(slicer.mrmlScene) + placeWidget.setCurrentNode(self.markupsNode1) + placeWidget.show() + + placeWidget.buttonsVisible = False + self.assertFalse(placeWidget.buttonsVisible) + placeWidget.buttonsVisible = True + self.assertTrue(placeWidget.buttonsVisible) + + placeWidget.deleteAllControlPointsOptionVisible = False + self.assertFalse(placeWidget.deleteAllControlPointsOptionVisible) + placeWidget.deleteAllControlPointsOptionVisible = True + self.assertTrue(placeWidget.deleteAllControlPointsOptionVisible) + + placeWidget.unsetLastControlPointOptionVisible = False + self.assertFalse(placeWidget.unsetLastControlPointOptionVisible) + placeWidget.unsetLastControlPointOptionVisible = True + self.assertTrue(placeWidget.unsetLastControlPointOptionVisible) + + placeWidget.unsetAllControlPointsOptionVisible = False + self.assertFalse(placeWidget.unsetAllControlPointsOptionVisible) + placeWidget.unsetAllControlPointsOptionVisible = True + self.assertTrue(placeWidget.unsetAllControlPointsOptionVisible) + + placeWidget.placeMultipleMarkups = slicer.qSlicerMarkupsPlaceWidget.ForcePlaceSingleMarkup + placeWidget.placeModeEnabled = True + self.assertFalse(placeWidget.placeModePersistency) + + placeWidget.placeMultipleMarkups = slicer.qSlicerMarkupsPlaceWidget.ForcePlaceMultipleMarkups + placeWidget.placeModeEnabled = False + placeWidget.placeModeEnabled = True + self.assertTrue(placeWidget.placeModePersistency) + + self.assertEqual(placeWidget.interactionNode(), slicer.app.applicationLogic().GetInteractionNode()) + otherInteractionNode = slicer.vtkMRMLInteractionNode() + otherInteractionNode.SetSingletonOff() + slicer.mrmlScene.AddNode(otherInteractionNode) + placeWidget.setInteractionNode(otherInteractionNode) + self.assertEqual(placeWidget.interactionNode(), otherInteractionNode) diff --git a/Modules/Loadable/Plots/Testing/Python/PlotsSelfTest.py b/Modules/Loadable/Plots/Testing/Python/PlotsSelfTest.py index 449f732bd26..7e1616e7e1d 100644 --- a/Modules/Loadable/Plots/Testing/Python/PlotsSelfTest.py +++ b/Modules/Loadable/Plots/Testing/Python/PlotsSelfTest.py @@ -9,14 +9,14 @@ # class PlotsSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "PlotsSelfTest" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = ["Plots"] - self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"] - self.parent.helpText = """This is a self test for plot nodes and widgets.""" - parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "PlotsSelfTest" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = ["Plots"] + self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"] + self.parent.helpText = """This is a self test for plot nodes and widgets.""" + parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University and was supported through Canada CANARIE's Research Software Program.""" @@ -25,8 +25,8 @@ def __init__(self, parent): # class PlotsSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -34,141 +34,141 @@ def setup(self): # class PlotsSelfTestLogic(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 - """ + """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 + """ - def __init__(self): - pass + def __init__(self): + pass class PlotsSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - 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. + This is the test case for your scripted module. """ - self.setUp() - self.test_PlotsSelfTest_FullTest1() - - # ------------------------------------------------------------------------------ - def test_PlotsSelfTest_FullTest1(self): - # Check for Plots module - self.assertTrue( slicer.modules.plots ) - - self.section_SetupPathsAndNames() - self.section_CreateTable() - self.section_CreatePlots() - self.section_TestPlotView() - self.delayDisplay("Test passed") - - # ------------------------------------------------------------------------------ - def section_SetupPathsAndNames(self): - # Set constants - self.tableName = 'SampleTable' - self.xColumnName = 'x' - self.y1ColumnName = 'cos' - self.y2ColumnName = 'sin' - - self.series1Name = "Cosine" - self.series2Name = "Sine" - - self.chartName = "My Chart" - - # ------------------------------------------------------------------------------ - def section_CreateTable(self): - self.delayDisplay("Create table") - - tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", self.tableName) - self.assertIsNotNone(tableNode) - table = tableNode.GetTable() - self.assertIsNotNone(table) - - # Create X, Y1, and Y2 series - - arrX = vtk.vtkFloatArray() - arrX.SetName(self.xColumnName) - table.AddColumn(arrX) - - arrY1 = vtk.vtkFloatArray() - arrY1.SetName(self.y1ColumnName) - table.AddColumn(arrY1) - - arrY2 = vtk.vtkFloatArray() - arrY2.SetName(self.y2ColumnName) - table.AddColumn(arrY2) - - # Fill in the table with some example values - import math - numPoints = 69 - inc = 7.5 / (numPoints - 1) - table.SetNumberOfRows(numPoints) - for i in range(numPoints): - table.SetValue(i, 0, i * inc ) - table.SetValue(i, 1, math.cos(i * inc)) - table.SetValue(i, 2, math.sin(i * inc)) - - # ------------------------------------------------------------------------------ - def section_CreatePlots(self): - self.delayDisplay("Create plots") - - tableNode = slicer.util.getNode(self.tableName) - - # Create plot data series nodes - - plotSeriesNode1 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", self.series1Name) - plotSeriesNode1.SetAndObserveTableNodeID(tableNode.GetID()) - plotSeriesNode1.SetXColumnName(self.xColumnName) - plotSeriesNode1.SetYColumnName(self.y1ColumnName) - plotSeriesNode1.SetLineStyle(slicer.vtkMRMLPlotSeriesNode.LineStyleDash) - plotSeriesNode1.SetMarkerStyle(slicer.vtkMRMLPlotSeriesNode.MarkerStyleSquare) - - plotSeriesNode2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", self.series2Name) - plotSeriesNode2.SetAndObserveTableNodeID(tableNode.GetID()) - plotSeriesNode2.SetXColumnName(self.xColumnName) - plotSeriesNode2.SetYColumnName(self.y2ColumnName) - plotSeriesNode2.SetUniqueColor() - - # Create plot chart node - plotChartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode", self.chartName) - plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode1.GetID()) - plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode2.GetID()) - plotChartNode.SetTitle('A simple plot with 2 curves') - plotChartNode.SetXAxisTitle('A simple plot with 2 curves') - plotChartNode.SetYAxisTitle('This is the Y axis') - - # ------------------------------------------------------------------------------ - def section_TestPlotView(self): - self.delayDisplay("Test plot view") - - plotChartNode = slicer.util.getNode(self.chartName) - - # Create plot view node - plotViewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotViewNode") - plotViewNode.SetPlotChartNodeID(plotChartNode.GetID()) - - # Create plotWidget - plotWidget = slicer.qMRMLPlotWidget() - plotWidget.setMRMLScene(slicer.mrmlScene) - plotWidget.setMRMLPlotViewNode(plotViewNode) - plotWidget.show() - - # Create plotView - plotView = slicer.qMRMLPlotView() - plotView.setMRMLScene(slicer.mrmlScene) - plotView.setMRMLPlotViewNode(plotViewNode) - plotView.show() - - # Save variables into slicer namespace for debugging - slicer.plotWidget = plotWidget - slicer.plotView = plotView + + 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_PlotsSelfTest_FullTest1() + + # ------------------------------------------------------------------------------ + def test_PlotsSelfTest_FullTest1(self): + # Check for Plots module + self.assertTrue(slicer.modules.plots) + + self.section_SetupPathsAndNames() + self.section_CreateTable() + self.section_CreatePlots() + self.section_TestPlotView() + self.delayDisplay("Test passed") + + # ------------------------------------------------------------------------------ + def section_SetupPathsAndNames(self): + # Set constants + self.tableName = 'SampleTable' + self.xColumnName = 'x' + self.y1ColumnName = 'cos' + self.y2ColumnName = 'sin' + + self.series1Name = "Cosine" + self.series2Name = "Sine" + + self.chartName = "My Chart" + + # ------------------------------------------------------------------------------ + def section_CreateTable(self): + self.delayDisplay("Create table") + + tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", self.tableName) + self.assertIsNotNone(tableNode) + table = tableNode.GetTable() + self.assertIsNotNone(table) + + # Create X, Y1, and Y2 series + + arrX = vtk.vtkFloatArray() + arrX.SetName(self.xColumnName) + table.AddColumn(arrX) + + arrY1 = vtk.vtkFloatArray() + arrY1.SetName(self.y1ColumnName) + table.AddColumn(arrY1) + + arrY2 = vtk.vtkFloatArray() + arrY2.SetName(self.y2ColumnName) + table.AddColumn(arrY2) + + # Fill in the table with some example values + import math + numPoints = 69 + inc = 7.5 / (numPoints - 1) + table.SetNumberOfRows(numPoints) + for i in range(numPoints): + table.SetValue(i, 0, i * inc) + table.SetValue(i, 1, math.cos(i * inc)) + table.SetValue(i, 2, math.sin(i * inc)) + + # ------------------------------------------------------------------------------ + def section_CreatePlots(self): + self.delayDisplay("Create plots") + + tableNode = slicer.util.getNode(self.tableName) + + # Create plot data series nodes + + plotSeriesNode1 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", self.series1Name) + plotSeriesNode1.SetAndObserveTableNodeID(tableNode.GetID()) + plotSeriesNode1.SetXColumnName(self.xColumnName) + plotSeriesNode1.SetYColumnName(self.y1ColumnName) + plotSeriesNode1.SetLineStyle(slicer.vtkMRMLPlotSeriesNode.LineStyleDash) + plotSeriesNode1.SetMarkerStyle(slicer.vtkMRMLPlotSeriesNode.MarkerStyleSquare) + + plotSeriesNode2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", self.series2Name) + plotSeriesNode2.SetAndObserveTableNodeID(tableNode.GetID()) + plotSeriesNode2.SetXColumnName(self.xColumnName) + plotSeriesNode2.SetYColumnName(self.y2ColumnName) + plotSeriesNode2.SetUniqueColor() + + # Create plot chart node + plotChartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode", self.chartName) + plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode1.GetID()) + plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode2.GetID()) + plotChartNode.SetTitle('A simple plot with 2 curves') + plotChartNode.SetXAxisTitle('A simple plot with 2 curves') + plotChartNode.SetYAxisTitle('This is the Y axis') + + # ------------------------------------------------------------------------------ + def section_TestPlotView(self): + self.delayDisplay("Test plot view") + + plotChartNode = slicer.util.getNode(self.chartName) + + # Create plot view node + plotViewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotViewNode") + plotViewNode.SetPlotChartNodeID(plotChartNode.GetID()) + + # Create plotWidget + plotWidget = slicer.qMRMLPlotWidget() + plotWidget.setMRMLScene(slicer.mrmlScene) + plotWidget.setMRMLPlotViewNode(plotViewNode) + plotWidget.show() + + # Create plotView + plotView = slicer.qMRMLPlotView() + plotView.setMRMLScene(slicer.mrmlScene) + plotView.setMRMLPlotViewNode(plotViewNode) + plotView.show() + + # Save variables into slicer namespace for debugging + slicer.plotWidget = plotWidget + slicer.plotView = plotView diff --git a/Modules/Loadable/SceneViews/Testing/Python/AddStorableDataAfterSceneViewTest.py b/Modules/Loadable/SceneViews/Testing/Python/AddStorableDataAfterSceneViewTest.py index f108f38e74f..dddf5cb5e39 100644 --- a/Modules/Loadable/SceneViews/Testing/Python/AddStorableDataAfterSceneViewTest.py +++ b/Modules/Loadable/SceneViews/Testing/Python/AddStorableDataAfterSceneViewTest.py @@ -12,23 +12,23 @@ # class AddStorableDataAfterSceneViewTest(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 = "Add Storable Data After Scene View Test" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = [] - self.parent.contributors = ["Nicole Aucoin (BWH)"] - self.parent.helpText = """ + """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 = "Add Storable Data After Scene View Test" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = [] + self.parent.contributors = ["Nicole Aucoin (BWH)"] + self.parent.helpText = """ This self test adds some data, creates a scene view, then adds more storable data. It tests Slicer's functionality after the scene view is restored, is the new storable node still present? With the current implementation it only passes if the new storable node is NOT present. """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ This file was originally developed by Nicole Aucoin, BWH, and was partially funded by NIH grant 3P41RR013218-12S1. """ @@ -38,54 +38,54 @@ def __init__(self, parent): # class AddStorableDataAfterSceneViewTestWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + # Instantiate and connect widgets ... - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - # - # check box to trigger taking screen shots for later use in tutorials - # - self.enableScreenshotsFlagCheckBox = qt.QCheckBox() - self.enableScreenshotsFlagCheckBox.checked = 0 - self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") - parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) + # + # check box to trigger taking screen shots for later use in tutorials + # + self.enableScreenshotsFlagCheckBox = qt.QCheckBox() + self.enableScreenshotsFlagCheckBox.checked = 0 + self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.") + parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox) - # - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the test." - self.applyButton.enabled = True - parametersFormLayout.addRow(self.applyButton) + # + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the test." + self.applyButton.enabled = True + parametersFormLayout.addRow(self.applyButton) - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - def cleanup(self): - pass + def cleanup(self): + pass - def onApplyButton(self): - logic = AddStorableDataAfterSceneViewTestLogic() - enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked - logic.run(enableScreenshotsFlag) + def onApplyButton(self): + logic = AddStorableDataAfterSceneViewTestLogic() + enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked + logic.run(enableScreenshotsFlag) # @@ -93,112 +93,112 @@ def onApplyButton(self): # class AddStorableDataAfterSceneViewTestLogic(ScriptedLoadableModuleLogic): - """ - Uses ScriptedLoadableModuleLogic base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def run(self, enableScreenshots=0): """ - Run the test via GUI + Uses ScriptedLoadableModuleLogic base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - logging.info('Processing started') + def run(self, enableScreenshots=0): + """ + Run the test via GUI + """ - try: - evalString = 'AddStorableDataAfterSceneViewTestTest()' - tester = eval(evalString) - tester.runTest() - except Exception as e: - import traceback - traceback.print_exc() - errorMessage = "Add storable data after scene view test: Exception!\n\n" + str(e) + "\n\nSee Python Console for Stack Trace" - slicer.util.errorDisplay(errorMessage) + logging.info('Processing started') - logging.info('Processing completed') + try: + evalString = 'AddStorableDataAfterSceneViewTestTest()' + tester = eval(evalString) + tester.runTest() + except Exception as e: + import traceback + traceback.print_exc() + errorMessage = "Add storable data after scene view test: Exception!\n\n" + str(e) + "\n\nSee Python Console for Stack Trace" + slicer.util.errorDisplay(errorMessage) - return True + logging.info('Processing completed') + + return True class AddStorableDataAfterSceneViewTestTest(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. + 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 """ - self.setUp() - self.test_AddStorableDataAfterSceneViewTest1() - - def test_AddStorableDataAfterSceneViewTest1(self): - - slicer.util.delayDisplay("Starting the test") - - # - # add a markups control point list - # - - pointList = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsFiducialNode') - pointList.AddControlPoint([10,20,15]) - - # - # save a scene view - # - sv = slicer.mrmlScene.AddNode(slicer.vtkMRMLSceneViewNode()) - sv.StoreScene() - - # - # add another storable node, a volume - # - slicer.util.delayDisplay("Adding a new storable node, after creating a scene view") - import SampleData - mrHeadVolume = SampleData.downloadSample("MRHead") - mrHeadID = mrHeadVolume.GetID() - - # - # restore the scene view - # - slicer.util.delayDisplay("Restoring the scene view") - sv.RestoreScene() - - # - # Is the new storable data still present? - # - restoredData = slicer.mrmlScene.GetNodeByID(mrHeadID) - - # for now, the non scene view storable data is removed - self.assertIsNone( restoredData ) - slicer.util.delayDisplay('Success: extra storable node removed with scene view restore') - - # - # add new storable again - mrHeadVolume = SampleData.downloadSample("MRHead") - mrHeadID = mrHeadVolume.GetID() - - # - # restore the scene view, but error on removing nodes - # - slicer.util.delayDisplay("Restoring the scene view with check for removed nodes") - sv.RestoreScene(0) - - # - # Is the new storable data still present? - # - restoredData = slicer.mrmlScene.GetNodeByID(mrHeadID) - - # in this case the non scene view storable data is kept' scene is not changed - self.assertIsNotNone( restoredData ) - slicer.util.delayDisplay('Success: extra storable node NOT removed with scene view restore') - - print('Scene error code = ' + str(slicer.mrmlScene.GetErrorCode())) - print('\t' + slicer.mrmlScene.GetErrorMessage()) - - slicer.util.delayDisplay('Test passed!') + + 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_AddStorableDataAfterSceneViewTest1() + + def test_AddStorableDataAfterSceneViewTest1(self): + + slicer.util.delayDisplay("Starting the test") + + # + # add a markups control point list + # + + pointList = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsFiducialNode') + pointList.AddControlPoint([10, 20, 15]) + + # + # save a scene view + # + sv = slicer.mrmlScene.AddNode(slicer.vtkMRMLSceneViewNode()) + sv.StoreScene() + + # + # add another storable node, a volume + # + slicer.util.delayDisplay("Adding a new storable node, after creating a scene view") + import SampleData + mrHeadVolume = SampleData.downloadSample("MRHead") + mrHeadID = mrHeadVolume.GetID() + + # + # restore the scene view + # + slicer.util.delayDisplay("Restoring the scene view") + sv.RestoreScene() + + # + # Is the new storable data still present? + # + restoredData = slicer.mrmlScene.GetNodeByID(mrHeadID) + + # for now, the non scene view storable data is removed + self.assertIsNone(restoredData) + slicer.util.delayDisplay('Success: extra storable node removed with scene view restore') + + # + # add new storable again + mrHeadVolume = SampleData.downloadSample("MRHead") + mrHeadID = mrHeadVolume.GetID() + + # + # restore the scene view, but error on removing nodes + # + slicer.util.delayDisplay("Restoring the scene view with check for removed nodes") + sv.RestoreScene(0) + + # + # Is the new storable data still present? + # + restoredData = slicer.mrmlScene.GetNodeByID(mrHeadID) + + # in this case the non scene view storable data is kept' scene is not changed + self.assertIsNotNone(restoredData) + slicer.util.delayDisplay('Success: extra storable node NOT removed with scene view restore') + + print('Scene error code = ' + str(slicer.mrmlScene.GetErrorCode())) + print('\t' + slicer.mrmlScene.GetErrorMessage()) + + slicer.util.delayDisplay('Test passed!') diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorAutoCompleteEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorAutoCompleteEffect.py index 2919277d4a5..f15e21c3470 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorAutoCompleteEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorAutoCompleteEffect.py @@ -19,541 +19,541 @@ # class AbstractScriptedSegmentEditorAutoCompleteEffect(AbstractScriptedSegmentEditorEffect): - """ AutoCompleteEffect is an effect that can create a full segmentation - from a partial segmentation (not all slices are segmented or only - part of the target structures are painted). - """ - - def __init__(self, scriptedEffect): - # Indicates that effect does not operate on one segment, but the whole segmentation. - # This means that while this effect is active, no segment can be selected - scriptedEffect.perSegment = False - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) - - self.minimumNumberOfSegments = 1 - self.clippedMasterImageDataRequired = False - self.clippedMaskImageDataRequired = False - - # Stores merged labelmap image geometry (voxel data is not allocated) - self.mergedLabelmapGeometryImage = None - self.selectedSegmentIds = None - self.selectedSegmentModifiedTimes = {} # map from segment ID to ModifiedTime - self.clippedMasterImageData = None - self.clippedMaskImageData = None - - # Observation for auto-update - self.observedSegmentation = None - self.segmentationNodeObserverTags = [] - - # Wait this much after the last modified event before starting aut-update: - autoUpdateDelaySec = 1.0 - self.delayedAutoUpdateTimer = qt.QTimer() - self.delayedAutoUpdateTimer.setSingleShot(True) - self.delayedAutoUpdateTimer.interval = autoUpdateDelaySec * 1000 - self.delayedAutoUpdateTimer.connect('timeout()', self.onPreview) - - self.extentGrowthRatio = 0.1 # extent of seed region will be grown outside by this much - self.minimumExtentMargin = 3 - - self.previewComputationInProgress = False - - def __del__(self, scriptedEffect): - super(SegmentEditorAutoCompleteEffect,self).__del__() - self.delayedAutoUpdateTimer.stop() - self.observeSegmentation(False) - - @staticmethod - def isBackgroundLabelmap(labelmapOrientedImageData, label=None): - if labelmapOrientedImageData is None: - return False - # If five or more corner voxels of the image contain non-zero, then it is background - extent = labelmapOrientedImageData.GetExtent() - if extent[0] > extent[1] or extent[2] > extent[3] or extent[4] > extent[5]: - return False - numberOfFilledCorners = 0 - for i in [0,1]: - for j in [2,3]: - for k in [4,5]: - voxelValue = labelmapOrientedImageData.GetScalarComponentAsFloat(extent[i],extent[j],extent[k],0) - if label is None: - if voxelValue > 0: - numberOfFilledCorners += 1 - else: - if voxelValue == label: - numberOfFilledCorners += 1 - if numberOfFilledCorners > 4: - return True - return False - - def setupOptionsFrame(self): - self.autoUpdateCheckBox = qt.QCheckBox("Auto-update") - self.autoUpdateCheckBox.setToolTip("Auto-update results preview when input segments change.") - self.autoUpdateCheckBox.setChecked(True) - self.autoUpdateCheckBox.setEnabled(False) - - self.previewButton = qt.QPushButton("Initialize") - self.previewButton.objectName = self.__class__.__name__ + 'Preview' - self.previewButton.setToolTip("Preview complete segmentation") - # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) - # fails on some systems, therefore set the policies using separate method calls - qSize = qt.QSizePolicy() - qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding) - self.previewButton.setSizePolicy(qSize) - - previewFrame = qt.QHBoxLayout() - previewFrame.addWidget(self.autoUpdateCheckBox) - previewFrame.addWidget(self.previewButton) - self.scriptedEffect.addLabeledOptionsWidget("Preview:", previewFrame) - - self.previewOpacitySlider = ctk.ctkSliderWidget() - self.previewOpacitySlider.setToolTip("Adjust visibility of results preview.") - self.previewOpacitySlider.minimum = 0 - self.previewOpacitySlider.maximum = 1.0 - self.previewOpacitySlider.value = 0.0 - self.previewOpacitySlider.singleStep = 0.05 - self.previewOpacitySlider.pageStep = 0.1 - self.previewOpacitySlider.spinBoxVisible = False - - self.previewShow3DButton = qt.QPushButton("Show 3D") - self.previewShow3DButton.setToolTip("Preview results in 3D.") - self.previewShow3DButton.setCheckable(True) - - displayFrame = qt.QHBoxLayout() - displayFrame.addWidget(qt.QLabel("inputs")) - displayFrame.addWidget(self.previewOpacitySlider) - displayFrame.addWidget(qt.QLabel("results")) - displayFrame.addWidget(self.previewShow3DButton) - self.scriptedEffect.addLabeledOptionsWidget("Display:", displayFrame) - - self.cancelButton = qt.QPushButton("Cancel") - self.cancelButton.objectName = self.__class__.__name__ + 'Cancel' - self.cancelButton.setToolTip("Clear preview and cancel auto-complete") - - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - self.applyButton.setToolTip("Replace segments by previewed result") - - finishFrame = qt.QHBoxLayout() - finishFrame.addWidget(self.cancelButton) - finishFrame.addWidget(self.applyButton) - self.scriptedEffect.addOptionsWidget(finishFrame) - - self.previewButton.connect('clicked()', self.onPreview) - self.cancelButton.connect('clicked()', self.onCancel) - self.applyButton.connect('clicked()', self.onApply) - self.previewOpacitySlider.connect("valueChanged(double)", self.updateMRMLFromGUI) - self.previewShow3DButton.connect("toggled(bool)", self.updateMRMLFromGUI) - self.autoUpdateCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) - - def createCursor(self, widget): - # Turn off effect-specific cursor for this effect - return slicer.util.mainWindow().cursor - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("AutoUpdate", "1") - - def onSegmentationModified(self, caller, event): - if not self.autoUpdateCheckBox.isChecked(): - # just in case a queued request comes through - return - - import vtkSegmentationCorePython as vtkSegmentationCore - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - segmentation = segmentationNode.GetSegmentation() - - updateNeeded = False - for segmentIndex in range(self.selectedSegmentIds.GetNumberOfValues()): - segmentID = self.selectedSegmentIds.GetValue(segmentIndex) - segment = segmentation.GetSegment(segmentID) - if not segment: - # selected segment was deleted, cancel segmentation - logging.debug("Segmentation cancelled because an input segment was deleted") - self.onCancel() - return - segmentLabelmap = segment.GetRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()) - if segmentID in self.selectedSegmentModifiedTimes \ - and segmentLabelmap and segmentLabelmap.GetMTime() == self.selectedSegmentModifiedTimes[segmentID]: - # this segment has not changed since last update - continue - if segmentLabelmap: - self.selectedSegmentModifiedTimes[segmentID] = segmentLabelmap.GetMTime() - elif segmentID in self.selectedSegmentModifiedTimes: - self.selectedSegmentModifiedTimes.pop(segmentID) - updateNeeded = True - # continue so that all segment modified times are updated - - if not updateNeeded: - return - - logging.debug("Segmentation update requested") - # There could be multiple update events for a single paint operation (e.g., one segment overwrites the other) - # therefore don't update directly, just set up/reset a timer that will perform the update when it elapses. - if not self.previewComputationInProgress: - self.delayedAutoUpdateTimer.start() - - def observeSegmentation(self, observationEnabled): - import vtkSegmentationCorePython as vtkSegmentationCore - - parameterSetNode = self.scriptedEffect.parameterSetNode() - segmentationNode = None - if parameterSetNode: - segmentationNode = parameterSetNode.GetSegmentationNode() - - segmentation = None - if segmentationNode: - segmentation = segmentationNode.GetSegmentation() - - if observationEnabled and self.observedSegmentation == segmentation: - return - if not observationEnabled and not self.observedSegmentation: - return - # Need to update the observer - # Remove old observer - if self.observedSegmentation: - for tag in self.segmentationNodeObserverTags: - self.observedSegmentation.RemoveObserver(tag) - self.segmentationNodeObserverTags = [] - self.observedSegmentation = None - # Add new observer - if observationEnabled and segmentation is not None: - self.observedSegmentation = segmentation - observedEvents = [ - vtkSegmentationCore.vtkSegmentation.SegmentAdded, - vtkSegmentationCore.vtkSegmentation.SegmentRemoved, - vtkSegmentationCore.vtkSegmentation.SegmentModified, - vtkSegmentationCore.vtkSegmentation.MasterRepresentationModified ] - for eventId in observedEvents: - self.segmentationNodeObserverTags.append(self.observedSegmentation.AddObserver(eventId, self.onSegmentationModified)) - - def getPreviewNode(self): - previewNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ResultPreviewNodeReferenceRole) - if previewNode and self.scriptedEffect.parameter("SegmentationResultPreviewOwnerEffect") != self.scriptedEffect.name: - # another effect owns this preview node - return None - return previewNode - - def updateGUIFromMRML(self): - - previewNode = self.getPreviewNode() - - self.cancelButton.setEnabled(previewNode is not None) - self.applyButton.setEnabled(previewNode is not None) - - self.previewOpacitySlider.setEnabled(previewNode is not None) - if previewNode: - wasBlocked = self.previewOpacitySlider.blockSignals(True) - self.previewOpacitySlider.value = self.getPreviewOpacity() - self.previewOpacitySlider.blockSignals(wasBlocked) - self.previewButton.text = "Update" - self.previewShow3DButton.setEnabled(True) - self.previewShow3DButton.setChecked(self.getPreviewShow3D()) - self.autoUpdateCheckBox.setEnabled(True) - self.observeSegmentation(self.autoUpdateCheckBox.isChecked()) - else: - self.previewButton.text = "Initialize" - self.autoUpdateCheckBox.setEnabled(False) - self.previewShow3DButton.setEnabled(False) - self.delayedAutoUpdateTimer.stop() - self.observeSegmentation(False) - - autoUpdate = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("AutoUpdate") == 0 else qt.Qt.Checked - wasBlocked = self.autoUpdateCheckBox.blockSignals(True) - self.autoUpdateCheckBox.setCheckState(autoUpdate) - self.autoUpdateCheckBox.blockSignals(wasBlocked) - - def updateMRMLFromGUI(self): - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - previewNode = self.getPreviewNode() - if previewNode: - self.setPreviewOpacity(self.previewOpacitySlider.value) - self.setPreviewShow3D(self.previewShow3DButton.checked) - - autoUpdate = 1 if self.autoUpdateCheckBox.isChecked() else 0 - self.scriptedEffect.setParameter("AutoUpdate", autoUpdate) - - def onPreview(self): - if self.previewComputationInProgress: - return - self.previewComputationInProgress = True - - slicer.util.showStatusMessage(f"Running {self.scriptedEffect.name} auto-complete...", 2000) - try: - # This can be a long operation - indicate it to the user - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - self.preview() - finally: - qt.QApplication.restoreOverrideCursor() - - self.previewComputationInProgress = False - - def reset(self): - self.delayedAutoUpdateTimer.stop() - self.observeSegmentation(False) - previewNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ResultPreviewNodeReferenceRole) - if previewNode: - self.scriptedEffect.parameterSetNode().SetNodeReferenceID(ResultPreviewNodeReferenceRole, None) - slicer.mrmlScene.RemoveNode(previewNode) - self.scriptedEffect.setCommonParameter("SegmentationResultPreviewOwnerEffect", "") - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - segmentationNode.GetDisplayNode().SetOpacity(1.0) - self.mergedLabelmapGeometryImage = None - self.selectedSegmentIds = None - self.selectedSegmentModifiedTimes = {} - self.clippedMasterImageData = None - self.clippedMaskImageData = None - self.updateGUIFromMRML() - - def onCancel(self): - self.reset() - - def onApply(self): - self.delayedAutoUpdateTimer.stop() - self.observeSegmentation(False) - - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - segmentationDisplayNode = segmentationNode.GetDisplayNode() - previewNode = self.getPreviewNode() - - self.scriptedEffect.saveStateForUndo() - - previewContainsClosedSurfaceRepresentation = previewNode.GetSegmentation().ContainsRepresentation( - slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()) - - # Move segments from preview into current segmentation - segmentIDs = vtk.vtkStringArray() - previewNode.GetSegmentation().GetSegmentIDs(segmentIDs) - for index in range(segmentIDs.GetNumberOfValues()): - segmentID = segmentIDs.GetValue(index) - previewSegmentLabelmap = slicer.vtkOrientedImageData() - previewNode.GetBinaryLabelmapRepresentation(segmentID, previewSegmentLabelmap) - self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, previewSegmentLabelmap, - slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) - if segmentationDisplayNode is not None and self.isBackgroundLabelmap(previewSegmentLabelmap): - # Automatically hide result segments that are background (all eight corners are non-zero) - segmentationDisplayNode.SetSegmentVisibility(segmentID, False) - previewNode.GetSegmentation().RemoveSegment(segmentID) # delete now to limit memory usage - - if previewContainsClosedSurfaceRepresentation: - segmentationNode.CreateClosedSurfaceRepresentation() - - self.reset() - - def setPreviewOpacity(self, opacity): - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - segmentationNode.GetDisplayNode().SetOpacity(1.0-opacity) - previewNode = self.getPreviewNode() - if previewNode: - previewNode.GetDisplayNode().SetOpacity(opacity) - previewNode.GetDisplayNode().SetOpacity3D(opacity) - - # Make sure the GUI is up-to-date - wasBlocked = self.previewOpacitySlider.blockSignals(True) - self.previewOpacitySlider.value = opacity - self.previewOpacitySlider.blockSignals(wasBlocked) - - def getPreviewOpacity(self): - previewNode = self.getPreviewNode() - return previewNode.GetDisplayNode().GetOpacity() if previewNode else 0.6 # default opacity for preview - - def setPreviewShow3D(self, show): - previewNode = self.getPreviewNode() - if previewNode: - if show: - previewNode.CreateClosedSurfaceRepresentation() - else: - previewNode.RemoveClosedSurfaceRepresentation() - - # Make sure the GUI is up-to-date - wasBlocked = self.previewShow3DButton.blockSignals(True) - self.previewShow3DButton.checked = show - self.previewShow3DButton.blockSignals(wasBlocked) - - def getPreviewShow3D(self): - previewNode = self.getPreviewNode() - if not previewNode: - return False - containsClosedSurfaceRepresentation = previewNode.GetSegmentation().ContainsRepresentation( - slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()) - return containsClosedSurfaceRepresentation - - def effectiveExtentChanged(self): - if self.getPreviewNode() is None: - return True - if self.mergedLabelmapGeometryImage is None: - return True - if self.selectedSegmentIds is None: - return True - - import vtkSegmentationCorePython as vtkSegmentationCore - - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - - # The effective extent for the current input segments - effectiveGeometryImage = slicer.vtkOrientedImageData() - effectiveGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry( - vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.selectedSegmentIds) - if effectiveGeometryString is None: - return True - vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(effectiveGeometryString, effectiveGeometryImage) - - masterImageData = self.scriptedEffect.masterVolumeImageData() - masterImageExtent = masterImageData.GetExtent() - - # The effective extent of the selected segments - effectiveLabelExtent = effectiveGeometryImage.GetExtent() - # Current extent used for auto-complete preview - currentLabelExtent = self.mergedLabelmapGeometryImage.GetExtent() - - # Determine if the current merged labelmap extent has less than a 3 voxel margin around the effective segment extent (limited by the master image extent) - return ((masterImageExtent[0] != currentLabelExtent[0] and currentLabelExtent[0] > effectiveLabelExtent[0] - self.minimumExtentMargin) or - (masterImageExtent[1] != currentLabelExtent[1] and currentLabelExtent[1] < effectiveLabelExtent[1] + self.minimumExtentMargin) or - (masterImageExtent[2] != currentLabelExtent[2] and currentLabelExtent[2] > effectiveLabelExtent[2] - self.minimumExtentMargin) or - (masterImageExtent[3] != currentLabelExtent[3] and currentLabelExtent[3] < effectiveLabelExtent[3] + self.minimumExtentMargin) or - (masterImageExtent[4] != currentLabelExtent[4] and currentLabelExtent[4] > effectiveLabelExtent[4] - self.minimumExtentMargin) or - (masterImageExtent[5] != currentLabelExtent[5] and currentLabelExtent[5] < effectiveLabelExtent[5] + self.minimumExtentMargin)) - - def preview(self): - # Get master volume image data - import vtkSegmentationCorePython as vtkSegmentationCore - - # Get segmentation - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - - previewNode = self.getPreviewNode() - previewOpacity = self.getPreviewOpacity() - previewShow3D = self.getPreviewShow3D() - - # If the selectedSegmentIds have been specified, then they shouldn't be overwritten here - currentSelectedSegmentIds = self.selectedSegmentIds - - if self.effectiveExtentChanged(): - self.reset() - - # Restore the selectedSegmentIds - self.selectedSegmentIds = currentSelectedSegmentIds - if self.selectedSegmentIds is None: - self.selectedSegmentIds = vtk.vtkStringArray() - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(self.selectedSegmentIds) - if self.selectedSegmentIds.GetNumberOfValues() < self.minimumNumberOfSegments: - logging.error(f"Auto-complete operation skipped: at least {self.minimumNumberOfSegments} visible segments are required") + """ AutoCompleteEffect is an effect that can create a full segmentation + from a partial segmentation (not all slices are segmented or only + part of the target structures are painted). + """ + + def __init__(self, scriptedEffect): + # Indicates that effect does not operate on one segment, but the whole segmentation. + # This means that while this effect is active, no segment can be selected + scriptedEffect.perSegment = False + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + + self.minimumNumberOfSegments = 1 + self.clippedMasterImageDataRequired = False + self.clippedMaskImageDataRequired = False + + # Stores merged labelmap image geometry (voxel data is not allocated) + self.mergedLabelmapGeometryImage = None + self.selectedSegmentIds = None + self.selectedSegmentModifiedTimes = {} # map from segment ID to ModifiedTime + self.clippedMasterImageData = None + self.clippedMaskImageData = None + + # Observation for auto-update + self.observedSegmentation = None + self.segmentationNodeObserverTags = [] + + # Wait this much after the last modified event before starting aut-update: + autoUpdateDelaySec = 1.0 + self.delayedAutoUpdateTimer = qt.QTimer() + self.delayedAutoUpdateTimer.setSingleShot(True) + self.delayedAutoUpdateTimer.interval = autoUpdateDelaySec * 1000 + self.delayedAutoUpdateTimer.connect('timeout()', self.onPreview) + + self.extentGrowthRatio = 0.1 # extent of seed region will be grown outside by this much + self.minimumExtentMargin = 3 + + self.previewComputationInProgress = False + + def __del__(self, scriptedEffect): + super(SegmentEditorAutoCompleteEffect, self).__del__() + self.delayedAutoUpdateTimer.stop() + self.observeSegmentation(False) + + @staticmethod + def isBackgroundLabelmap(labelmapOrientedImageData, label=None): + if labelmapOrientedImageData is None: + return False + # If five or more corner voxels of the image contain non-zero, then it is background + extent = labelmapOrientedImageData.GetExtent() + if extent[0] > extent[1] or extent[2] > extent[3] or extent[4] > extent[5]: + return False + numberOfFilledCorners = 0 + for i in [0, 1]: + for j in [2, 3]: + for k in [4, 5]: + voxelValue = labelmapOrientedImageData.GetScalarComponentAsFloat(extent[i], extent[j], extent[k], 0) + if label is None: + if voxelValue > 0: + numberOfFilledCorners += 1 + else: + if voxelValue == label: + numberOfFilledCorners += 1 + if numberOfFilledCorners > 4: + return True + return False + + def setupOptionsFrame(self): + self.autoUpdateCheckBox = qt.QCheckBox("Auto-update") + self.autoUpdateCheckBox.setToolTip("Auto-update results preview when input segments change.") + self.autoUpdateCheckBox.setChecked(True) + self.autoUpdateCheckBox.setEnabled(False) + + self.previewButton = qt.QPushButton("Initialize") + self.previewButton.objectName = self.__class__.__name__ + 'Preview' + self.previewButton.setToolTip("Preview complete segmentation") + # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + # fails on some systems, therefore set the policies using separate method calls + qSize = qt.QSizePolicy() + qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding) + self.previewButton.setSizePolicy(qSize) + + previewFrame = qt.QHBoxLayout() + previewFrame.addWidget(self.autoUpdateCheckBox) + previewFrame.addWidget(self.previewButton) + self.scriptedEffect.addLabeledOptionsWidget("Preview:", previewFrame) + + self.previewOpacitySlider = ctk.ctkSliderWidget() + self.previewOpacitySlider.setToolTip("Adjust visibility of results preview.") + self.previewOpacitySlider.minimum = 0 + self.previewOpacitySlider.maximum = 1.0 + self.previewOpacitySlider.value = 0.0 + self.previewOpacitySlider.singleStep = 0.05 + self.previewOpacitySlider.pageStep = 0.1 + self.previewOpacitySlider.spinBoxVisible = False + + self.previewShow3DButton = qt.QPushButton("Show 3D") + self.previewShow3DButton.setToolTip("Preview results in 3D.") + self.previewShow3DButton.setCheckable(True) + + displayFrame = qt.QHBoxLayout() + displayFrame.addWidget(qt.QLabel("inputs")) + displayFrame.addWidget(self.previewOpacitySlider) + displayFrame.addWidget(qt.QLabel("results")) + displayFrame.addWidget(self.previewShow3DButton) + self.scriptedEffect.addLabeledOptionsWidget("Display:", displayFrame) + + self.cancelButton = qt.QPushButton("Cancel") + self.cancelButton.objectName = self.__class__.__name__ + 'Cancel' + self.cancelButton.setToolTip("Clear preview and cancel auto-complete") + + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + self.applyButton.setToolTip("Replace segments by previewed result") + + finishFrame = qt.QHBoxLayout() + finishFrame.addWidget(self.cancelButton) + finishFrame.addWidget(self.applyButton) + self.scriptedEffect.addOptionsWidget(finishFrame) + + self.previewButton.connect('clicked()', self.onPreview) + self.cancelButton.connect('clicked()', self.onCancel) + self.applyButton.connect('clicked()', self.onApply) + self.previewOpacitySlider.connect("valueChanged(double)", self.updateMRMLFromGUI) + self.previewShow3DButton.connect("toggled(bool)", self.updateMRMLFromGUI) + self.autoUpdateCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) + + def createCursor(self, widget): + # Turn off effect-specific cursor for this effect + return slicer.util.mainWindow().cursor + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("AutoUpdate", "1") + + def onSegmentationModified(self, caller, event): + if not self.autoUpdateCheckBox.isChecked(): + # just in case a queued request comes through + return + + import vtkSegmentationCorePython as vtkSegmentationCore + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + segmentation = segmentationNode.GetSegmentation() + + updateNeeded = False + for segmentIndex in range(self.selectedSegmentIds.GetNumberOfValues()): + segmentID = self.selectedSegmentIds.GetValue(segmentIndex) + segment = segmentation.GetSegment(segmentID) + if not segment: + # selected segment was deleted, cancel segmentation + logging.debug("Segmentation cancelled because an input segment was deleted") + self.onCancel() + return + segmentLabelmap = segment.GetRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()) + if segmentID in self.selectedSegmentModifiedTimes \ + and segmentLabelmap and segmentLabelmap.GetMTime() == self.selectedSegmentModifiedTimes[segmentID]: + # this segment has not changed since last update + continue + if segmentLabelmap: + self.selectedSegmentModifiedTimes[segmentID] = segmentLabelmap.GetMTime() + elif segmentID in self.selectedSegmentModifiedTimes: + self.selectedSegmentModifiedTimes.pop(segmentID) + updateNeeded = True + # continue so that all segment modified times are updated + + if not updateNeeded: + return + + logging.debug("Segmentation update requested") + # There could be multiple update events for a single paint operation (e.g., one segment overwrites the other) + # therefore don't update directly, just set up/reset a timer that will perform the update when it elapses. + if not self.previewComputationInProgress: + self.delayedAutoUpdateTimer.start() + + def observeSegmentation(self, observationEnabled): + import vtkSegmentationCorePython as vtkSegmentationCore + + parameterSetNode = self.scriptedEffect.parameterSetNode() + segmentationNode = None + if parameterSetNode: + segmentationNode = parameterSetNode.GetSegmentationNode() + + segmentation = None + if segmentationNode: + segmentation = segmentationNode.GetSegmentation() + + if observationEnabled and self.observedSegmentation == segmentation: + return + if not observationEnabled and not self.observedSegmentation: + return + # Need to update the observer + # Remove old observer + if self.observedSegmentation: + for tag in self.segmentationNodeObserverTags: + self.observedSegmentation.RemoveObserver(tag) + self.segmentationNodeObserverTags = [] + self.observedSegmentation = None + # Add new observer + if observationEnabled and segmentation is not None: + self.observedSegmentation = segmentation + observedEvents = [ + vtkSegmentationCore.vtkSegmentation.SegmentAdded, + vtkSegmentationCore.vtkSegmentation.SegmentRemoved, + vtkSegmentationCore.vtkSegmentation.SegmentModified, + vtkSegmentationCore.vtkSegmentation.MasterRepresentationModified] + for eventId in observedEvents: + self.segmentationNodeObserverTags.append(self.observedSegmentation.AddObserver(eventId, self.onSegmentationModified)) + + def getPreviewNode(self): + previewNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ResultPreviewNodeReferenceRole) + if previewNode and self.scriptedEffect.parameter("SegmentationResultPreviewOwnerEffect") != self.scriptedEffect.name: + # another effect owns this preview node + return None + return previewNode + + def updateGUIFromMRML(self): + + previewNode = self.getPreviewNode() + + self.cancelButton.setEnabled(previewNode is not None) + self.applyButton.setEnabled(previewNode is not None) + + self.previewOpacitySlider.setEnabled(previewNode is not None) + if previewNode: + wasBlocked = self.previewOpacitySlider.blockSignals(True) + self.previewOpacitySlider.value = self.getPreviewOpacity() + self.previewOpacitySlider.blockSignals(wasBlocked) + self.previewButton.text = "Update" + self.previewShow3DButton.setEnabled(True) + self.previewShow3DButton.setChecked(self.getPreviewShow3D()) + self.autoUpdateCheckBox.setEnabled(True) + self.observeSegmentation(self.autoUpdateCheckBox.isChecked()) + else: + self.previewButton.text = "Initialize" + self.autoUpdateCheckBox.setEnabled(False) + self.previewShow3DButton.setEnabled(False) + self.delayedAutoUpdateTimer.stop() + self.observeSegmentation(False) + + autoUpdate = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("AutoUpdate") == 0 else qt.Qt.Checked + wasBlocked = self.autoUpdateCheckBox.blockSignals(True) + self.autoUpdateCheckBox.setCheckState(autoUpdate) + self.autoUpdateCheckBox.blockSignals(wasBlocked) + + def updateMRMLFromGUI(self): + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + previewNode = self.getPreviewNode() + if previewNode: + self.setPreviewOpacity(self.previewOpacitySlider.value) + self.setPreviewShow3D(self.previewShow3DButton.checked) + + autoUpdate = 1 if self.autoUpdateCheckBox.isChecked() else 0 + self.scriptedEffect.setParameter("AutoUpdate", autoUpdate) + + def onPreview(self): + if self.previewComputationInProgress: + return + self.previewComputationInProgress = True + + slicer.util.showStatusMessage(f"Running {self.scriptedEffect.name} auto-complete...", 2000) + try: + # This can be a long operation - indicate it to the user + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + self.preview() + finally: + qt.QApplication.restoreOverrideCursor() + + self.previewComputationInProgress = False + + def reset(self): + self.delayedAutoUpdateTimer.stop() + self.observeSegmentation(False) + previewNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ResultPreviewNodeReferenceRole) + if previewNode: + self.scriptedEffect.parameterSetNode().SetNodeReferenceID(ResultPreviewNodeReferenceRole, None) + slicer.mrmlScene.RemoveNode(previewNode) + self.scriptedEffect.setCommonParameter("SegmentationResultPreviewOwnerEffect", "") + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + segmentationNode.GetDisplayNode().SetOpacity(1.0) + self.mergedLabelmapGeometryImage = None self.selectedSegmentIds = None - return - - # Compute merged labelmap extent (effective extent slightly expanded) - if not self.mergedLabelmapGeometryImage: - self.mergedLabelmapGeometryImage = slicer.vtkOrientedImageData() - commonGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry( - vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.selectedSegmentIds) - if not commonGeometryString: - logging.info("Auto-complete operation skipped: all visible segments are empty") - return - vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(commonGeometryString, self.mergedLabelmapGeometryImage) - - masterImageData = self.scriptedEffect.masterVolumeImageData() - masterImageExtent = masterImageData.GetExtent() - labelsEffectiveExtent = self.mergedLabelmapGeometryImage.GetExtent() - # Margin size is relative to combined seed region size, but minimum of 3 voxels - print(f"self.extentGrowthRatio = {self.extentGrowthRatio}") - margin = [ - int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[1]-labelsEffectiveExtent[0]))), - int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[3]-labelsEffectiveExtent[2]))), - int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[5]-labelsEffectiveExtent[4]))) ] - labelsExpandedExtent = [ - max(masterImageExtent[0], labelsEffectiveExtent[0]-margin[0]), - min(masterImageExtent[1], labelsEffectiveExtent[1]+margin[0]), - max(masterImageExtent[2], labelsEffectiveExtent[2]-margin[1]), - min(masterImageExtent[3], labelsEffectiveExtent[3]+margin[1]), - max(masterImageExtent[4], labelsEffectiveExtent[4]-margin[2]), - min(masterImageExtent[5], labelsEffectiveExtent[5]+margin[2]) ] - print("masterImageExtent = "+repr(masterImageExtent)) - print("labelsEffectiveExtent = "+repr(labelsEffectiveExtent)) - print("labelsExpandedExtent = "+repr(labelsExpandedExtent)) - self.mergedLabelmapGeometryImage.SetExtent(labelsExpandedExtent) - - # Create and setup preview node - previewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") - previewNode.CreateDefaultDisplayNodes() - previewNode.GetDisplayNode().SetVisibility2DOutline(False) - if segmentationNode.GetParentTransformNode(): - previewNode.SetAndObserveTransformNodeID(segmentationNode.GetParentTransformNode().GetID()) - self.scriptedEffect.parameterSetNode().SetNodeReferenceID(ResultPreviewNodeReferenceRole, previewNode.GetID()) - self.scriptedEffect.setCommonParameter("SegmentationResultPreviewOwnerEffect", self.scriptedEffect.name) - self.setPreviewOpacity(0.6) - - # Disable smoothing for closed surface generation to make it fast - previewNode.GetSegmentation().SetConversionParameter( - slicer.vtkBinaryLabelmapToClosedSurfaceConversionRule.GetSmoothingFactorParameterName(), - "-0.5") - - inputContainsClosedSurfaceRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation( - slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()) - - self.setPreviewShow3D(inputContainsClosedSurfaceRepresentation) - - if self.clippedMasterImageDataRequired: - self.clippedMasterImageData = slicer.vtkOrientedImageData() - masterImageClipper = vtk.vtkImageConstantPad() - masterImageClipper.SetInputData(masterImageData) - masterImageClipper.SetOutputWholeExtent(self.mergedLabelmapGeometryImage.GetExtent()) - masterImageClipper.Update() - self.clippedMasterImageData.ShallowCopy(masterImageClipper.GetOutput()) - self.clippedMasterImageData.CopyDirections(self.mergedLabelmapGeometryImage) - - self.clippedMaskImageData = None - if self.clippedMaskImageDataRequired: - self.clippedMaskImageData = slicer.vtkOrientedImageData() - intensityBasedMasking = self.scriptedEffect.parameterSetNode().GetMasterVolumeIntensityMask() - success = segmentationNode.GenerateEditMask(self.clippedMaskImageData, - self.scriptedEffect.parameterSetNode().GetMaskMode(), - self.clippedMasterImageData, # reference geometry - "", # edited segment ID - self.scriptedEffect.parameterSetNode().GetMaskSegmentID() if self.scriptedEffect.parameterSetNode().GetMaskSegmentID() else "", - self.clippedMasterImageData if intensityBasedMasking else None, - self.scriptedEffect.parameterSetNode().GetMasterVolumeIntensityMaskRange() if intensityBasedMasking else None) - if not success: - logging.error("Failed to create edit mask") - self.clippedMaskImageData = None - - previewNode.SetName(segmentationNode.GetName()+" preview") - previewNode.RemoveClosedSurfaceRepresentation() # Force the closed surface representation to update - # TODO: This will no longer be required when we can use the segment editor to set multiple segments - # as the closed surfaces will be converted as necessary by the segmentation logic. - - mergedImage = slicer.vtkOrientedImageData() - segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage, - vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.mergedLabelmapGeometryImage, self.selectedSegmentIds) - - outputLabelmap = slicer.vtkOrientedImageData() - self.computePreviewLabelmap(mergedImage, outputLabelmap) - - if previewNode.GetSegmentation().GetNumberOfSegments() != self.selectedSegmentIds.GetNumberOfValues(): - # first update (or number of segments changed), need a full reinitialization - previewNode.GetSegmentation().RemoveAllSegments() - - for index in range(self.selectedSegmentIds.GetNumberOfValues()): - segmentID = self.selectedSegmentIds.GetValue(index) - - previewSegment = previewNode.GetSegmentation().GetSegment(segmentID) - if not previewSegment: - inputSegment = segmentationNode.GetSegmentation().GetSegment(segmentID) - - previewSegment = vtkSegmentationCore.vtkSegment() - previewSegment.SetName(inputSegment.GetName()) - previewSegment.SetColor(inputSegment.GetColor()) - previewNode.GetSegmentation().AddSegment(previewSegment, segmentID) - - labelValue = index + 1 # n-th segment label value = n + 1 (background label value is 0) - previewSegment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(), outputLabelmap) - previewSegment.SetLabelValue(labelValue) - - # Automatically hide result segments that are background (all eight corners are non-zero) - previewNode.GetDisplayNode().SetSegmentVisibility3D(segmentID, not self.isBackgroundLabelmap(outputLabelmap, labelValue)) - - # If the preview was reset, we need to restore the visibility options - self.setPreviewOpacity(previewOpacity) - self.setPreviewShow3D(previewShow3D) - - self.updateGUIFromMRML() + self.selectedSegmentModifiedTimes = {} + self.clippedMasterImageData = None + self.clippedMaskImageData = None + self.updateGUIFromMRML() + + def onCancel(self): + self.reset() + + def onApply(self): + self.delayedAutoUpdateTimer.stop() + self.observeSegmentation(False) + + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + segmentationDisplayNode = segmentationNode.GetDisplayNode() + previewNode = self.getPreviewNode() + + self.scriptedEffect.saveStateForUndo() + + previewContainsClosedSurfaceRepresentation = previewNode.GetSegmentation().ContainsRepresentation( + slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()) + + # Move segments from preview into current segmentation + segmentIDs = vtk.vtkStringArray() + previewNode.GetSegmentation().GetSegmentIDs(segmentIDs) + for index in range(segmentIDs.GetNumberOfValues()): + segmentID = segmentIDs.GetValue(index) + previewSegmentLabelmap = slicer.vtkOrientedImageData() + previewNode.GetBinaryLabelmapRepresentation(segmentID, previewSegmentLabelmap) + self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, previewSegmentLabelmap, + slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) + if segmentationDisplayNode is not None and self.isBackgroundLabelmap(previewSegmentLabelmap): + # Automatically hide result segments that are background (all eight corners are non-zero) + segmentationDisplayNode.SetSegmentVisibility(segmentID, False) + previewNode.GetSegmentation().RemoveSegment(segmentID) # delete now to limit memory usage + + if previewContainsClosedSurfaceRepresentation: + segmentationNode.CreateClosedSurfaceRepresentation() + + self.reset() + + def setPreviewOpacity(self, opacity): + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + segmentationNode.GetDisplayNode().SetOpacity(1.0 - opacity) + previewNode = self.getPreviewNode() + if previewNode: + previewNode.GetDisplayNode().SetOpacity(opacity) + previewNode.GetDisplayNode().SetOpacity3D(opacity) + + # Make sure the GUI is up-to-date + wasBlocked = self.previewOpacitySlider.blockSignals(True) + self.previewOpacitySlider.value = opacity + self.previewOpacitySlider.blockSignals(wasBlocked) + + def getPreviewOpacity(self): + previewNode = self.getPreviewNode() + return previewNode.GetDisplayNode().GetOpacity() if previewNode else 0.6 # default opacity for preview + + def setPreviewShow3D(self, show): + previewNode = self.getPreviewNode() + if previewNode: + if show: + previewNode.CreateClosedSurfaceRepresentation() + else: + previewNode.RemoveClosedSurfaceRepresentation() + + # Make sure the GUI is up-to-date + wasBlocked = self.previewShow3DButton.blockSignals(True) + self.previewShow3DButton.checked = show + self.previewShow3DButton.blockSignals(wasBlocked) + + def getPreviewShow3D(self): + previewNode = self.getPreviewNode() + if not previewNode: + return False + containsClosedSurfaceRepresentation = previewNode.GetSegmentation().ContainsRepresentation( + slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()) + return containsClosedSurfaceRepresentation + + def effectiveExtentChanged(self): + if self.getPreviewNode() is None: + return True + if self.mergedLabelmapGeometryImage is None: + return True + if self.selectedSegmentIds is None: + return True + + import vtkSegmentationCorePython as vtkSegmentationCore + + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + + # The effective extent for the current input segments + effectiveGeometryImage = slicer.vtkOrientedImageData() + effectiveGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry( + vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.selectedSegmentIds) + if effectiveGeometryString is None: + return True + vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(effectiveGeometryString, effectiveGeometryImage) + + masterImageData = self.scriptedEffect.masterVolumeImageData() + masterImageExtent = masterImageData.GetExtent() + + # The effective extent of the selected segments + effectiveLabelExtent = effectiveGeometryImage.GetExtent() + # Current extent used for auto-complete preview + currentLabelExtent = self.mergedLabelmapGeometryImage.GetExtent() + + # Determine if the current merged labelmap extent has less than a 3 voxel margin around the effective segment extent (limited by the master image extent) + return ((masterImageExtent[0] != currentLabelExtent[0] and currentLabelExtent[0] > effectiveLabelExtent[0] - self.minimumExtentMargin) or + (masterImageExtent[1] != currentLabelExtent[1] and currentLabelExtent[1] < effectiveLabelExtent[1] + self.minimumExtentMargin) or + (masterImageExtent[2] != currentLabelExtent[2] and currentLabelExtent[2] > effectiveLabelExtent[2] - self.minimumExtentMargin) or + (masterImageExtent[3] != currentLabelExtent[3] and currentLabelExtent[3] < effectiveLabelExtent[3] + self.minimumExtentMargin) or + (masterImageExtent[4] != currentLabelExtent[4] and currentLabelExtent[4] > effectiveLabelExtent[4] - self.minimumExtentMargin) or + (masterImageExtent[5] != currentLabelExtent[5] and currentLabelExtent[5] < effectiveLabelExtent[5] + self.minimumExtentMargin)) + + def preview(self): + # Get master volume image data + import vtkSegmentationCorePython as vtkSegmentationCore + + # Get segmentation + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + + previewNode = self.getPreviewNode() + previewOpacity = self.getPreviewOpacity() + previewShow3D = self.getPreviewShow3D() + + # If the selectedSegmentIds have been specified, then they shouldn't be overwritten here + currentSelectedSegmentIds = self.selectedSegmentIds + + if self.effectiveExtentChanged(): + self.reset() + + # Restore the selectedSegmentIds + self.selectedSegmentIds = currentSelectedSegmentIds + if self.selectedSegmentIds is None: + self.selectedSegmentIds = vtk.vtkStringArray() + segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(self.selectedSegmentIds) + if self.selectedSegmentIds.GetNumberOfValues() < self.minimumNumberOfSegments: + logging.error(f"Auto-complete operation skipped: at least {self.minimumNumberOfSegments} visible segments are required") + self.selectedSegmentIds = None + return + + # Compute merged labelmap extent (effective extent slightly expanded) + if not self.mergedLabelmapGeometryImage: + self.mergedLabelmapGeometryImage = slicer.vtkOrientedImageData() + commonGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry( + vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.selectedSegmentIds) + if not commonGeometryString: + logging.info("Auto-complete operation skipped: all visible segments are empty") + return + vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(commonGeometryString, self.mergedLabelmapGeometryImage) + + masterImageData = self.scriptedEffect.masterVolumeImageData() + masterImageExtent = masterImageData.GetExtent() + labelsEffectiveExtent = self.mergedLabelmapGeometryImage.GetExtent() + # Margin size is relative to combined seed region size, but minimum of 3 voxels + print(f"self.extentGrowthRatio = {self.extentGrowthRatio}") + margin = [ + int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[1] - labelsEffectiveExtent[0]))), + int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[3] - labelsEffectiveExtent[2]))), + int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[5] - labelsEffectiveExtent[4])))] + labelsExpandedExtent = [ + max(masterImageExtent[0], labelsEffectiveExtent[0] - margin[0]), + min(masterImageExtent[1], labelsEffectiveExtent[1] + margin[0]), + max(masterImageExtent[2], labelsEffectiveExtent[2] - margin[1]), + min(masterImageExtent[3], labelsEffectiveExtent[3] + margin[1]), + max(masterImageExtent[4], labelsEffectiveExtent[4] - margin[2]), + min(masterImageExtent[5], labelsEffectiveExtent[5] + margin[2])] + print("masterImageExtent = " + repr(masterImageExtent)) + print("labelsEffectiveExtent = " + repr(labelsEffectiveExtent)) + print("labelsExpandedExtent = " + repr(labelsExpandedExtent)) + self.mergedLabelmapGeometryImage.SetExtent(labelsExpandedExtent) + + # Create and setup preview node + previewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") + previewNode.CreateDefaultDisplayNodes() + previewNode.GetDisplayNode().SetVisibility2DOutline(False) + if segmentationNode.GetParentTransformNode(): + previewNode.SetAndObserveTransformNodeID(segmentationNode.GetParentTransformNode().GetID()) + self.scriptedEffect.parameterSetNode().SetNodeReferenceID(ResultPreviewNodeReferenceRole, previewNode.GetID()) + self.scriptedEffect.setCommonParameter("SegmentationResultPreviewOwnerEffect", self.scriptedEffect.name) + self.setPreviewOpacity(0.6) + + # Disable smoothing for closed surface generation to make it fast + previewNode.GetSegmentation().SetConversionParameter( + slicer.vtkBinaryLabelmapToClosedSurfaceConversionRule.GetSmoothingFactorParameterName(), + "-0.5") + + inputContainsClosedSurfaceRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation( + slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()) + + self.setPreviewShow3D(inputContainsClosedSurfaceRepresentation) + + if self.clippedMasterImageDataRequired: + self.clippedMasterImageData = slicer.vtkOrientedImageData() + masterImageClipper = vtk.vtkImageConstantPad() + masterImageClipper.SetInputData(masterImageData) + masterImageClipper.SetOutputWholeExtent(self.mergedLabelmapGeometryImage.GetExtent()) + masterImageClipper.Update() + self.clippedMasterImageData.ShallowCopy(masterImageClipper.GetOutput()) + self.clippedMasterImageData.CopyDirections(self.mergedLabelmapGeometryImage) + + self.clippedMaskImageData = None + if self.clippedMaskImageDataRequired: + self.clippedMaskImageData = slicer.vtkOrientedImageData() + intensityBasedMasking = self.scriptedEffect.parameterSetNode().GetMasterVolumeIntensityMask() + success = segmentationNode.GenerateEditMask(self.clippedMaskImageData, + self.scriptedEffect.parameterSetNode().GetMaskMode(), + self.clippedMasterImageData, # reference geometry + "", # edited segment ID + self.scriptedEffect.parameterSetNode().GetMaskSegmentID() if self.scriptedEffect.parameterSetNode().GetMaskSegmentID() else "", + self.clippedMasterImageData if intensityBasedMasking else None, + self.scriptedEffect.parameterSetNode().GetMasterVolumeIntensityMaskRange() if intensityBasedMasking else None) + if not success: + logging.error("Failed to create edit mask") + self.clippedMaskImageData = None + + previewNode.SetName(segmentationNode.GetName() + " preview") + previewNode.RemoveClosedSurfaceRepresentation() # Force the closed surface representation to update + # TODO: This will no longer be required when we can use the segment editor to set multiple segments + # as the closed surfaces will be converted as necessary by the segmentation logic. + + mergedImage = slicer.vtkOrientedImageData() + segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage, + vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.mergedLabelmapGeometryImage, self.selectedSegmentIds) + + outputLabelmap = slicer.vtkOrientedImageData() + self.computePreviewLabelmap(mergedImage, outputLabelmap) + + if previewNode.GetSegmentation().GetNumberOfSegments() != self.selectedSegmentIds.GetNumberOfValues(): + # first update (or number of segments changed), need a full reinitialization + previewNode.GetSegmentation().RemoveAllSegments() + + for index in range(self.selectedSegmentIds.GetNumberOfValues()): + segmentID = self.selectedSegmentIds.GetValue(index) + + previewSegment = previewNode.GetSegmentation().GetSegment(segmentID) + if not previewSegment: + inputSegment = segmentationNode.GetSegmentation().GetSegment(segmentID) + + previewSegment = vtkSegmentationCore.vtkSegment() + previewSegment.SetName(inputSegment.GetName()) + previewSegment.SetColor(inputSegment.GetColor()) + previewNode.GetSegmentation().AddSegment(previewSegment, segmentID) + + labelValue = index + 1 # n-th segment label value = n + 1 (background label value is 0) + previewSegment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(), outputLabelmap) + previewSegment.SetLabelValue(labelValue) + + # Automatically hide result segments that are background (all eight corners are non-zero) + previewNode.GetDisplayNode().SetSegmentVisibility3D(segmentID, not self.isBackgroundLabelmap(outputLabelmap, labelValue)) + + # If the preview was reset, we need to restore the visibility options + self.setPreviewOpacity(previewOpacity) + self.setPreviewShow3D(previewShow3D) + + self.updateGUIFromMRML() ResultPreviewNodeReferenceRole = "SegmentationResultPreview" diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorEffect.py index b8d4d915b1f..8cfd7d59b5f 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorEffect.py @@ -8,86 +8,86 @@ # class AbstractScriptedSegmentEditorEffect: - """ Abstract scripted segment editor effects for effects implemented in python - - USAGE: - 1. Instantiation and registration - Instantiate segment editor effect adaptor class from - module (e.g. from setup function), and set python source: - > import qSlicerSegmentationsEditorEffectsPythonQt as effects - > scriptedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - > scriptedEffect.setPythonSource(MyEffect.filePath) - > scriptedEffect.self().register() - If effect name is added to slicer.modules.segmenteditorscriptedeffectnames - list then the above instantiation and registration steps are not necessary, - as the SegmentEditor module do all these. - - 2. Call host C++ implementation using - > self.scriptedEffect.functionName() - - 2.a. Most frequently used such methods are: - Parameter get/set: parameter, integerParameter, doubleParameter, setParameter - Add options widget: addOptionsWidget - Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk - Convenience getters: renderWindow, renderer, viewNode - - 2.b. Always call API functions (the ones that are defined in the adaptor - class qSlicerSegmentEditorScriptedEffect) using the adaptor accessor: - > self.scriptedEffect.updateGUIFromMRML() - - 3. To prevent deactivation of an effect by clicking place fiducial toolbar button, - override interactionNodeModified(self, interactionNode) - - An example for a generic effect is the ThresholdEffect - - """ - - def __init__(self, scriptedEffect): - self.scriptedEffect = scriptedEffect - - def register(self): - effectFactorySingleton = slicer.qSlicerSegmentEditorEffectFactory.instance() - effectFactorySingleton.registerEffect(self.scriptedEffect) - - # - # Utility functions for convenient coordinate transformations - # - def rasToXy(self, ras, viewWidget): - rasVector = qt.QVector3D(ras[0], ras[1], ras[2]) - xyPoint = self.scriptedEffect.rasToXy(rasVector, viewWidget) - return [xyPoint.x(), xyPoint.y()] - - def xyzToRas(self, xyz, viewWidget): - xyzVector = qt.QVector3D(xyz[0], xyz[1], xyz[2]) - rasVector = self.scriptedEffect.xyzToRas(xyzVector, viewWidget) - return [rasVector.x(), rasVector.y(), rasVector.z()] - - def xyToRas(self, xy, viewWidget): - xyPoint = qt.QPoint(xy[0], xy[1]) - rasVector = self.scriptedEffect.xyToRas(xyPoint, viewWidget) - return [rasVector.x(), rasVector.y(), rasVector.z()] - - def xyzToIjk(self, xyz, viewWidget, image, parentTransformNode=None): - xyzVector = qt.QVector3D(xyz[0], xyz[1], xyz[2]) - ijkVector = self.scriptedEffect.xyzToIjk(xyzVector, viewWidget, image, parentTransformNode) - return [int(ijkVector.x()), int(ijkVector.y()), int(ijkVector.z())] - - def xyToIjk(self, xy, viewWidget, image, parentTransformNode=None): - xyPoint = qt.QPoint(xy[0], xy[1]) - ijkVector = self.scriptedEffect.xyToIjk(xyPoint, viewWidget, image, parentTransformNode) - return [int(ijkVector.x()), int(ijkVector.y()), int(ijkVector.z())] - - def setWidgetMinMaxStepFromImageSpacing(self, spinbox, imageData): - # Set spinbox minimum, maximum, and step size from vtkImageData spacing: - # Set widget minimum spacing and step size to be 1/10th or less than minimum spacing - # Set widget minimum spacing to be 100x or more than minimum spacing - if not imageData: - return - import math - spinbox.unitAwareProperties &= ~(slicer.qMRMLSpinBox.MinimumValue | slicer.qMRMLSpinBox.MaximumValue | slicer.qMRMLSpinBox.Precision) - stepSize = 10**(math.floor(math.log10(min(imageData.GetSpacing())/10.0))) - spinbox.minimum = stepSize - spinbox.maximum = 10**(math.ceil(math.log10(max(imageData.GetSpacing())*100.0))) - spinbox.singleStep = stepSize - # number of decimals is set to be able to show the step size (e.g., stepSize = 0.01 => decimals = 2) - spinbox.decimals = max(int(-math.floor(math.log10(stepSize))),0) + """ Abstract scripted segment editor effects for effects implemented in python + + USAGE: + 1. Instantiation and registration + Instantiate segment editor effect adaptor class from + module (e.g. from setup function), and set python source: + > import qSlicerSegmentationsEditorEffectsPythonQt as effects + > scriptedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + > scriptedEffect.setPythonSource(MyEffect.filePath) + > scriptedEffect.self().register() + If effect name is added to slicer.modules.segmenteditorscriptedeffectnames + list then the above instantiation and registration steps are not necessary, + as the SegmentEditor module do all these. + + 2. Call host C++ implementation using + > self.scriptedEffect.functionName() + + 2.a. Most frequently used such methods are: + Parameter get/set: parameter, integerParameter, doubleParameter, setParameter + Add options widget: addOptionsWidget + Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk + Convenience getters: renderWindow, renderer, viewNode + + 2.b. Always call API functions (the ones that are defined in the adaptor + class qSlicerSegmentEditorScriptedEffect) using the adaptor accessor: + > self.scriptedEffect.updateGUIFromMRML() + + 3. To prevent deactivation of an effect by clicking place fiducial toolbar button, + override interactionNodeModified(self, interactionNode) + + An example for a generic effect is the ThresholdEffect + + """ + + def __init__(self, scriptedEffect): + self.scriptedEffect = scriptedEffect + + def register(self): + effectFactorySingleton = slicer.qSlicerSegmentEditorEffectFactory.instance() + effectFactorySingleton.registerEffect(self.scriptedEffect) + + # + # Utility functions for convenient coordinate transformations + # + def rasToXy(self, ras, viewWidget): + rasVector = qt.QVector3D(ras[0], ras[1], ras[2]) + xyPoint = self.scriptedEffect.rasToXy(rasVector, viewWidget) + return [xyPoint.x(), xyPoint.y()] + + def xyzToRas(self, xyz, viewWidget): + xyzVector = qt.QVector3D(xyz[0], xyz[1], xyz[2]) + rasVector = self.scriptedEffect.xyzToRas(xyzVector, viewWidget) + return [rasVector.x(), rasVector.y(), rasVector.z()] + + def xyToRas(self, xy, viewWidget): + xyPoint = qt.QPoint(xy[0], xy[1]) + rasVector = self.scriptedEffect.xyToRas(xyPoint, viewWidget) + return [rasVector.x(), rasVector.y(), rasVector.z()] + + def xyzToIjk(self, xyz, viewWidget, image, parentTransformNode=None): + xyzVector = qt.QVector3D(xyz[0], xyz[1], xyz[2]) + ijkVector = self.scriptedEffect.xyzToIjk(xyzVector, viewWidget, image, parentTransformNode) + return [int(ijkVector.x()), int(ijkVector.y()), int(ijkVector.z())] + + def xyToIjk(self, xy, viewWidget, image, parentTransformNode=None): + xyPoint = qt.QPoint(xy[0], xy[1]) + ijkVector = self.scriptedEffect.xyToIjk(xyPoint, viewWidget, image, parentTransformNode) + return [int(ijkVector.x()), int(ijkVector.y()), int(ijkVector.z())] + + def setWidgetMinMaxStepFromImageSpacing(self, spinbox, imageData): + # Set spinbox minimum, maximum, and step size from vtkImageData spacing: + # Set widget minimum spacing and step size to be 1/10th or less than minimum spacing + # Set widget minimum spacing to be 100x or more than minimum spacing + if not imageData: + return + import math + spinbox.unitAwareProperties &= ~(slicer.qMRMLSpinBox.MinimumValue | slicer.qMRMLSpinBox.MaximumValue | slicer.qMRMLSpinBox.Precision) + stepSize = 10**(math.floor(math.log10(min(imageData.GetSpacing()) / 10.0))) + spinbox.minimum = stepSize + spinbox.maximum = 10**(math.ceil(math.log10(max(imageData.GetSpacing()) * 100.0))) + spinbox.singleStep = stepSize + # number of decimals is set to be able to show the step size (e.g., stepSize = 0.01 => decimals = 2) + spinbox.decimals = max(int(-math.floor(math.log10(stepSize))), 0) diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorLabelEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorLabelEffect.py index dd7d1806f30..8549ff5d31f 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorLabelEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorLabelEffect.py @@ -11,34 +11,34 @@ # class AbstractScriptedSegmentEditorLabelEffect(AbstractScriptedSegmentEditorEffect): - """ Abstract scripted segment editor label effects for effects implemented in python + """ Abstract scripted segment editor label effects for effects implemented in python - USAGE: - 1. Instantiation and registration - Instantiate segment editor label effect adaptor class from - module (e.g. from setup function), and set python source: - > import qSlicerSegmentationsEditorEffectsPythonQt as effects - > scriptedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None) - > scriptedEffect.setPythonSource(MyLabelEffect.filePath) - Registration is automatic + USAGE: + 1. Instantiation and registration + Instantiate segment editor label effect adaptor class from + module (e.g. from setup function), and set python source: + > import qSlicerSegmentationsEditorEffectsPythonQt as effects + > scriptedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None) + > scriptedEffect.setPythonSource(MyLabelEffect.filePath) + Registration is automatic - 2. Call host C++ implementation using - > self.scriptedEffect.functionName() + 2. Call host C++ implementation using + > self.scriptedEffect.functionName() - 2.a. Most frequently used such methods are: - Parameter get/set: parameter, integerParameter, doubleParameter, setParameter - Add options widget: addOptionsWidget - Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk - Convenience getters: renderWindow, renderer, viewNode - Geometry getters: imageToWorldMatrix (for volume node and for oriented image data with segmentation) + 2.a. Most frequently used such methods are: + Parameter get/set: parameter, integerParameter, doubleParameter, setParameter + Add options widget: addOptionsWidget + Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk + Convenience getters: renderWindow, renderer, viewNode + Geometry getters: imageToWorldMatrix (for volume node and for oriented image data with segmentation) - 2.b. Always call API functions (the ones that are defined in the adaptor - class qSlicerSegmentEditorScriptedLabelEffect) using the adaptor accessor: - > self.scriptedEffect.updateGUIFromMRML() + 2.b. Always call API functions (the ones that are defined in the adaptor + class qSlicerSegmentEditorScriptedLabelEffect) using the adaptor accessor: + > self.scriptedEffect.updateGUIFromMRML() - An example for a generic effect is the DrawEffect + An example for a generic effect is the DrawEffect - """ + """ - def __init__(self, scriptedEffect): - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + def __init__(self, scriptedEffect): + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorPaintEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorPaintEffect.py index 6349624f96c..b5b207cbe94 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorPaintEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorPaintEffect.py @@ -11,34 +11,34 @@ # class AbstractScriptedSegmentEditorPaintEffect(AbstractScriptedSegmentEditorEffect): - """ Abstract scripted segment editor Paint effects for effects implemented in python + """ Abstract scripted segment editor Paint effects for effects implemented in python - USAGE: - 1. Instantiation and registration - Instantiate segment editor paint effect adaptor class from - module (e.g. from setup function), and set python source: - > import qSlicerSegmentationsEditorEffectsPythonQt as effects - > scriptedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(None) - > scriptedEffect.setPythonSource(MyPaintEffect.filePath) - Registration is automatic + USAGE: + 1. Instantiation and registration + Instantiate segment editor paint effect adaptor class from + module (e.g. from setup function), and set python source: + > import qSlicerSegmentationsEditorEffectsPythonQt as effects + > scriptedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(None) + > scriptedEffect.setPythonSource(MyPaintEffect.filePath) + Registration is automatic - 2. Call host C++ implementation using - > self.scriptedEffect.functionName() + 2. Call host C++ implementation using + > self.scriptedEffect.functionName() - 2.a. Most frequently used such methods are: - Parameter get/set: parameter, integerParameter, doubleParameter, setParameter - Add options widget: addOptionsWidget - Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk - Convenience getters: renderWindow, renderer, viewNode - Geometry getters: imageToWorldMatrix (for volume node and for oriented image data with segmentation) + 2.a. Most frequently used such methods are: + Parameter get/set: parameter, integerParameter, doubleParameter, setParameter + Add options widget: addOptionsWidget + Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk + Convenience getters: renderWindow, renderer, viewNode + Geometry getters: imageToWorldMatrix (for volume node and for oriented image data with segmentation) - 2.b. Always call API functions (the ones that are defined in the adaptor - class qSlicerSegmentEditorScriptedPaintEffect) using the adaptor accessor: - > self.scriptedEffect.updateGUIFromMRML() + 2.b. Always call API functions (the ones that are defined in the adaptor + class qSlicerSegmentEditorScriptedPaintEffect) using the adaptor accessor: + > self.scriptedEffect.updateGUIFromMRML() - An example for a generic effect is the DrawEffect + An example for a generic effect is the DrawEffect - """ + """ - def __init__(self, scriptedEffect): - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + def __init__(self, scriptedEffect): + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorDrawEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorDrawEffect.py index f11b835c759..e4f1e444e0c 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorDrawEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorDrawEffect.py @@ -10,29 +10,29 @@ class SegmentEditorDrawEffect(AbstractScriptedSegmentEditorLabelEffect): - """ DrawEffect is a LabelEffect implementing the interactive draw - tool in the segment editor - """ - - def __init__(self, scriptedEffect): - scriptedEffect.name = 'Draw' - self.drawPipelines = {} - AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect) - - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Draw.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Draw segment outline in slice viewers
. + """ DrawEffect is a LabelEffect implementing the interactive draw + tool in the segment editor + """ + + def __init__(self, scriptedEffect): + scriptedEffect.name = 'Draw' + self.drawPipelines = {} + AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect) + + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Draw.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Draw segment outline in slice viewers
.

  • Left-click: add point.
  • Left-button drag-and-drop: add multiple points.
  • @@ -40,283 +40,284 @@ def helpText(self):
  • Double-left-click or right-click or a or enter: apply outline.

""" - def deactivate(self): - # Clear draw pipelines - for sliceWidget, pipeline in self.drawPipelines.items(): - self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor) - self.drawPipelines = {} - - def setupOptionsFrame(self): - pass - - def processInteractionEvents(self, callerInteractor, eventId, viewWidget): - abortEvent = False - - # Only allow for slice views - if viewWidget.className() != "qMRMLSliceWidget": - return abortEvent - # Get draw pipeline for current slice - pipeline = self.pipelineForWidget(viewWidget) - if pipeline is None: - return abortEvent - - anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey() - - if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed: - # Make sure the user wants to do the operation, even if the segment is not visible - confirmedEditingAllowed = self.scriptedEffect.confirmCurrentSegmentVisible() - if confirmedEditingAllowed == self.scriptedEffect.NotConfirmed or confirmedEditingAllowed == self.scriptedEffect.ConfirmedWithDialog: - # If user had to move the mouse to click on the popup, so we cannot continue with painting - # from the current mouse position. User will need to click again. - # The dialog is not displayed again for the same segment. + def deactivate(self): + # Clear draw pipelines + for sliceWidget, pipeline in self.drawPipelines.items(): + self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor) + self.drawPipelines = {} + + def setupOptionsFrame(self): + pass + + def processInteractionEvents(self, callerInteractor, eventId, viewWidget): + abortEvent = False + + # Only allow for slice views + if viewWidget.className() != "qMRMLSliceWidget": + return abortEvent + # Get draw pipeline for current slice + pipeline = self.pipelineForWidget(viewWidget) + if pipeline is None: + return abortEvent + + anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey() + + if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed: + # Make sure the user wants to do the operation, even if the segment is not visible + confirmedEditingAllowed = self.scriptedEffect.confirmCurrentSegmentVisible() + if confirmedEditingAllowed == self.scriptedEffect.NotConfirmed or confirmedEditingAllowed == self.scriptedEffect.ConfirmedWithDialog: + # If user had to move the mouse to click on the popup, so we cannot continue with painting + # from the current mouse position. User will need to click again. + # The dialog is not displayed again for the same segment. + return abortEvent + pipeline.actionState = "drawing" + self.scriptedEffect.cursorOff(viewWidget) + xy = callerInteractor.GetEventPosition() + ras = self.xyToRas(xy, viewWidget) + pipeline.addPoint(ras) + abortEvent = True + elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent: + if pipeline.actionState == "drawing": + pipeline.actionState = "moving" + self.scriptedEffect.cursorOn(viewWidget) + abortEvent = True + elif eventId == vtk.vtkCommand.RightButtonPressEvent and not anyModifierKeyPressed: + pipeline.actionState = "finishing" + sliceNode = viewWidget.sliceLogic().GetSliceNode() + pipeline.lastInsertSliceNodeMTime = sliceNode.GetMTime() + abortEvent = True + elif (eventId == vtk.vtkCommand.RightButtonReleaseEvent and pipeline.actionState == "finishing") or (eventId == vtk.vtkCommand.LeftButtonDoubleClickEvent and not anyModifierKeyPressed): + abortEvent = (pipeline.rasPoints.GetNumberOfPoints() > 1) + sliceNode = viewWidget.sliceLogic().GetSliceNode() + if abs(pipeline.lastInsertSliceNodeMTime - sliceNode.GetMTime()) < 2: + pipeline.apply() + pipeline.actionState = "" + elif eventId == vtk.vtkCommand.MouseMoveEvent: + if pipeline.actionState == "drawing": + xy = callerInteractor.GetEventPosition() + ras = self.xyToRas(xy, viewWidget) + pipeline.addPoint(ras) + abortEvent = True + elif eventId == vtk.vtkCommand.KeyPressEvent: + key = callerInteractor.GetKeySym() + if key == 'a' or key == 'Return': + pipeline.apply() + abortEvent = True + if key == 'x': + pipeline.deleteLastPoint() + abortEvent = True + else: + pass + + pipeline.positionActors() return abortEvent - pipeline.actionState = "drawing" - self.scriptedEffect.cursorOff(viewWidget) - xy = callerInteractor.GetEventPosition() - ras = self.xyToRas(xy, viewWidget) - pipeline.addPoint(ras) - abortEvent = True - elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent: - if pipeline.actionState == "drawing": - pipeline.actionState = "moving" - self.scriptedEffect.cursorOn(viewWidget) - abortEvent = True - elif eventId == vtk.vtkCommand.RightButtonPressEvent and not anyModifierKeyPressed: - pipeline.actionState = "finishing" - sliceNode = viewWidget.sliceLogic().GetSliceNode() - pipeline.lastInsertSliceNodeMTime = sliceNode.GetMTime() - abortEvent = True - elif (eventId == vtk.vtkCommand.RightButtonReleaseEvent and pipeline.actionState == "finishing") or (eventId==vtk.vtkCommand.LeftButtonDoubleClickEvent and not anyModifierKeyPressed): - abortEvent = (pipeline.rasPoints.GetNumberOfPoints() > 1) - sliceNode = viewWidget.sliceLogic().GetSliceNode() - if abs(pipeline.lastInsertSliceNodeMTime - sliceNode.GetMTime()) < 2: - pipeline.apply() - pipeline.actionState = "" - elif eventId == vtk.vtkCommand.MouseMoveEvent: - if pipeline.actionState == "drawing": - xy = callerInteractor.GetEventPosition() - ras = self.xyToRas(xy, viewWidget) - pipeline.addPoint(ras) - abortEvent = True - elif eventId == vtk.vtkCommand.KeyPressEvent: - key = callerInteractor.GetKeySym() - if key == 'a' or key == 'Return': - pipeline.apply() - abortEvent = True - if key == 'x': - pipeline.deleteLastPoint() - abortEvent = True - else: - pass - - pipeline.positionActors() - return abortEvent - - def processViewNodeEvents(self, callerViewNode, eventId, viewWidget): - if callerViewNode and callerViewNode.IsA('vtkMRMLSliceNode'): - # Get draw pipeline for current slice - pipeline = self.pipelineForWidget(viewWidget) - if pipeline is None: - logging.error('processViewNodeEvents: Invalid pipeline') - return - - # Make sure all points are on the current slice plane. - # If the SliceToRAS has been modified, then we're on a different plane - sliceLogic = viewWidget.sliceLogic() - lineMode = "solid" - currentSliceOffset = sliceLogic.GetSliceOffset() - if pipeline.activeSliceOffset: - offset = abs(currentSliceOffset - pipeline.activeSliceOffset) - if offset > 0.01: - lineMode = "dashed" - pipeline.setLineMode(lineMode) - pipeline.positionActors() - - def pipelineForWidget(self, sliceWidget): - if sliceWidget in self.drawPipelines: - return self.drawPipelines[sliceWidget] - - # Create pipeline if does not yet exist - pipeline = DrawPipeline(self.scriptedEffect, sliceWidget) - - # Add actor - renderer = self.scriptedEffect.renderer(sliceWidget) - if renderer is None: - logging.error("pipelineForWidget: Failed to get renderer!") - return None - self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor) - - self.drawPipelines[sliceWidget] = pipeline - return pipeline + + def processViewNodeEvents(self, callerViewNode, eventId, viewWidget): + if callerViewNode and callerViewNode.IsA('vtkMRMLSliceNode'): + # Get draw pipeline for current slice + pipeline = self.pipelineForWidget(viewWidget) + if pipeline is None: + logging.error('processViewNodeEvents: Invalid pipeline') + return + + # Make sure all points are on the current slice plane. + # If the SliceToRAS has been modified, then we're on a different plane + sliceLogic = viewWidget.sliceLogic() + lineMode = "solid" + currentSliceOffset = sliceLogic.GetSliceOffset() + if pipeline.activeSliceOffset: + offset = abs(currentSliceOffset - pipeline.activeSliceOffset) + if offset > 0.01: + lineMode = "dashed" + pipeline.setLineMode(lineMode) + pipeline.positionActors() + + def pipelineForWidget(self, sliceWidget): + if sliceWidget in self.drawPipelines: + return self.drawPipelines[sliceWidget] + + # Create pipeline if does not yet exist + pipeline = DrawPipeline(self.scriptedEffect, sliceWidget) + + # Add actor + renderer = self.scriptedEffect.renderer(sliceWidget) + if renderer is None: + logging.error("pipelineForWidget: Failed to get renderer!") + return None + self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor) + + self.drawPipelines[sliceWidget] = pipeline + return pipeline # # DrawPipeline # class DrawPipeline: - """ Visualization objects and pipeline for each slice view for drawing - """ - - def __init__(self, scriptedEffect, sliceWidget): - self.scriptedEffect = scriptedEffect - self.sliceWidget = sliceWidget - self.activeSliceOffset = None - self.lastInsertSliceNodeMTime = None - self.actionState = None - - self.xyPoints = vtk.vtkPoints() - self.rasPoints = vtk.vtkPoints() - self.polyData = self.createPolyData() - - self.mapper = vtk.vtkPolyDataMapper2D() - self.actor = vtk.vtkTexturedActor2D() - self.mapper.SetInputData(self.polyData) - self.actor.SetMapper(self.mapper) - actorProperty = self.actor.GetProperty() - actorProperty.SetColor(1,1,0) - actorProperty.SetLineWidth(1) - - self.createStippleTexture(0xAAAA, 8) - - def createStippleTexture(self, lineStipplePattern, lineStippleRepeat): - self.tcoords = vtk.vtkDoubleArray() - self.texture = vtk.vtkTexture() - - # Create texture - dimension = 16 * lineStippleRepeat - - image = vtk.vtkImageData() - image.SetDimensions(dimension, 1, 1) - image.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 4) - image.SetExtent(0, dimension - 1, 0, 0, 0, 0) - on = 255 - off = 0 - i_dim = 0 - while i_dim < dimension: - for i in range(0, 16): - mask = (1 << i) - bit = (lineStipplePattern & mask) >> i - value = bit - if value == 0: - for j in range(0, lineStippleRepeat): - image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on) - image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on) - image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on) - image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, off) - i_dim += 1 - else: - for j in range(0, lineStippleRepeat): - image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on) - image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on) - image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on) - image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, on) - i_dim += 1 - self.texture.SetInputData(image) - self.texture.InterpolateOff() - self.texture.RepeatOn() - - def createPolyData(self): - # Make an empty single-polyline polydata - polyData = vtk.vtkPolyData() - polyData.SetPoints(self.xyPoints) - lines = vtk.vtkCellArray() - polyData.SetLines(lines) - return polyData - - def addPoint(self,ras): - # Add a world space point to the current outline - - # Store active slice when first point is added - sliceLogic = self.sliceWidget.sliceLogic() - currentSliceOffset = sliceLogic.GetSliceOffset() - if not self.activeSliceOffset: - self.activeSliceOffset = currentSliceOffset - self.setLineMode("solid") - - # Don't allow adding points on except on the active slice - # (where first point was laid down) - if self.activeSliceOffset != currentSliceOffset: return - - # Keep track of node state (in case of pan/zoom) - sliceNode = sliceLogic.GetSliceNode() - self.lastInsertSliceNodeMTime = sliceNode.GetMTime() - - p = self.rasPoints.InsertNextPoint(ras) - if p > 0: - idList = vtk.vtkIdList() - idList.InsertNextId(p-1) - idList.InsertNextId(p) - self.polyData.InsertNextCell(vtk.VTK_LINE, idList) - - def setLineMode(self,mode="solid"): - actorProperty = self.actor.GetProperty() - if mode == "solid": - self.polyData.GetPointData().SetTCoords(None) - self.actor.SetTexture(None) - elif mode == "dashed": - # Create texture coordinates - self.tcoords.SetNumberOfComponents(1) - self.tcoords.SetNumberOfTuples(self.polyData.GetNumberOfPoints()) - for i in range(0, self.polyData.GetNumberOfPoints()): - value = i * 0.5 - self.tcoords.SetTypedTuple(i, [value]) - self.polyData.GetPointData().SetTCoords(self.tcoords) - self.actor.SetTexture(self.texture) - - def positionActors(self): - # Update draw feedback to follow slice node - sliceLogic = self.sliceWidget.sliceLogic() - sliceNode = sliceLogic.GetSliceNode() - rasToXY = vtk.vtkTransform() - rasToXY.SetMatrix(sliceNode.GetXYToRAS()) - rasToXY.Inverse() - self.xyPoints.Reset() - rasToXY.TransformPoints(self.rasPoints, self.xyPoints) - self.polyData.Modified() - self.sliceWidget.sliceView().scheduleRender() - - def apply(self): - lines = self.polyData.GetLines() - lineExists = lines.GetNumberOfCells() > 0 - if lineExists: - # Close the polyline back to the first point - idList = vtk.vtkIdList() - idList.InsertNextId(self.polyData.GetNumberOfPoints()-1) - idList.InsertNextId(0) - self.polyData.InsertNextCell(vtk.VTK_LINE, idList) - - # Get modifier labelmap - modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() - - # Apply poly data on modifier labelmap - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - self.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode) - - self.resetPolyData() - if lineExists: - self.scriptedEffect.saveStateForUndo() - self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd) - - def resetPolyData(self): - # Return the polyline to initial state with no points - lines = self.polyData.GetLines() - lines.Initialize() - self.xyPoints.Reset() - self.rasPoints.Reset() - self.activeSliceOffset = None - - def deleteLastPoint(self): - # Unwind through addPoint list back to empty polydata - pcount = self.rasPoints.GetNumberOfPoints() - if pcount <= 0: - return - - pcount = pcount - 1 - self.rasPoints.SetNumberOfPoints(pcount) - - cellCount = self.polyData.GetNumberOfCells() - if cellCount > 0: - self.polyData.DeleteCell(cellCount - 1) - self.polyData.RemoveDeletedCells() - - self.positionActors() + """ Visualization objects and pipeline for each slice view for drawing + """ + + def __init__(self, scriptedEffect, sliceWidget): + self.scriptedEffect = scriptedEffect + self.sliceWidget = sliceWidget + self.activeSliceOffset = None + self.lastInsertSliceNodeMTime = None + self.actionState = None + + self.xyPoints = vtk.vtkPoints() + self.rasPoints = vtk.vtkPoints() + self.polyData = self.createPolyData() + + self.mapper = vtk.vtkPolyDataMapper2D() + self.actor = vtk.vtkTexturedActor2D() + self.mapper.SetInputData(self.polyData) + self.actor.SetMapper(self.mapper) + actorProperty = self.actor.GetProperty() + actorProperty.SetColor(1, 1, 0) + actorProperty.SetLineWidth(1) + + self.createStippleTexture(0xAAAA, 8) + + def createStippleTexture(self, lineStipplePattern, lineStippleRepeat): + self.tcoords = vtk.vtkDoubleArray() + self.texture = vtk.vtkTexture() + + # Create texture + dimension = 16 * lineStippleRepeat + + image = vtk.vtkImageData() + image.SetDimensions(dimension, 1, 1) + image.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 4) + image.SetExtent(0, dimension - 1, 0, 0, 0, 0) + on = 255 + off = 0 + i_dim = 0 + while i_dim < dimension: + for i in range(0, 16): + mask = (1 << i) + bit = (lineStipplePattern & mask) >> i + value = bit + if value == 0: + for j in range(0, lineStippleRepeat): + image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on) + image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on) + image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on) + image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, off) + i_dim += 1 + else: + for j in range(0, lineStippleRepeat): + image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on) + image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on) + image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on) + image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, on) + i_dim += 1 + self.texture.SetInputData(image) + self.texture.InterpolateOff() + self.texture.RepeatOn() + + def createPolyData(self): + # Make an empty single-polyline polydata + polyData = vtk.vtkPolyData() + polyData.SetPoints(self.xyPoints) + lines = vtk.vtkCellArray() + polyData.SetLines(lines) + return polyData + + def addPoint(self, ras): + # Add a world space point to the current outline + + # Store active slice when first point is added + sliceLogic = self.sliceWidget.sliceLogic() + currentSliceOffset = sliceLogic.GetSliceOffset() + if not self.activeSliceOffset: + self.activeSliceOffset = currentSliceOffset + self.setLineMode("solid") + + # Don't allow adding points on except on the active slice + # (where first point was laid down) + if self.activeSliceOffset != currentSliceOffset: + return + + # Keep track of node state (in case of pan/zoom) + sliceNode = sliceLogic.GetSliceNode() + self.lastInsertSliceNodeMTime = sliceNode.GetMTime() + + p = self.rasPoints.InsertNextPoint(ras) + if p > 0: + idList = vtk.vtkIdList() + idList.InsertNextId(p - 1) + idList.InsertNextId(p) + self.polyData.InsertNextCell(vtk.VTK_LINE, idList) + + def setLineMode(self, mode="solid"): + actorProperty = self.actor.GetProperty() + if mode == "solid": + self.polyData.GetPointData().SetTCoords(None) + self.actor.SetTexture(None) + elif mode == "dashed": + # Create texture coordinates + self.tcoords.SetNumberOfComponents(1) + self.tcoords.SetNumberOfTuples(self.polyData.GetNumberOfPoints()) + for i in range(0, self.polyData.GetNumberOfPoints()): + value = i * 0.5 + self.tcoords.SetTypedTuple(i, [value]) + self.polyData.GetPointData().SetTCoords(self.tcoords) + self.actor.SetTexture(self.texture) + + def positionActors(self): + # Update draw feedback to follow slice node + sliceLogic = self.sliceWidget.sliceLogic() + sliceNode = sliceLogic.GetSliceNode() + rasToXY = vtk.vtkTransform() + rasToXY.SetMatrix(sliceNode.GetXYToRAS()) + rasToXY.Inverse() + self.xyPoints.Reset() + rasToXY.TransformPoints(self.rasPoints, self.xyPoints) + self.polyData.Modified() + self.sliceWidget.sliceView().scheduleRender() + + def apply(self): + lines = self.polyData.GetLines() + lineExists = lines.GetNumberOfCells() > 0 + if lineExists: + # Close the polyline back to the first point + idList = vtk.vtkIdList() + idList.InsertNextId(self.polyData.GetNumberOfPoints() - 1) + idList.InsertNextId(0) + self.polyData.InsertNextCell(vtk.VTK_LINE, idList) + + # Get modifier labelmap + modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() + + # Apply poly data on modifier labelmap + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + self.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode) + + self.resetPolyData() + if lineExists: + self.scriptedEffect.saveStateForUndo() + self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd) + + def resetPolyData(self): + # Return the polyline to initial state with no points + lines = self.polyData.GetLines() + lines.Initialize() + self.xyPoints.Reset() + self.rasPoints.Reset() + self.activeSliceOffset = None + + def deleteLastPoint(self): + # Unwind through addPoint list back to empty polydata + pcount = self.rasPoints.GetNumberOfPoints() + if pcount <= 0: + return + + pcount = pcount - 1 + self.rasPoints.SetNumberOfPoints(pcount) + + cellCount = self.polyData.GetNumberOfCells() + if cellCount > 0: + self.polyData.DeleteCell(cellCount - 1) + self.polyData.RemoveDeletedCells() + + self.positionActors() diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorFillBetweenSlicesEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorFillBetweenSlicesEffect.py index 9baeff5044c..7b881a8ec32 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorFillBetweenSlicesEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorFillBetweenSlicesEffect.py @@ -7,29 +7,29 @@ class SegmentEditorFillBetweenSlicesEffect(AbstractScriptedSegmentEditorAutoCompleteEffect): - """ AutoCompleteEffect is an effect that can create a full segmentation - from a partial segmentation (not all slices are segmented or only - part of the target structures are painted). - """ - - def __init__(self, scriptedEffect): - AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect) - scriptedEffect.name = 'Fill between slices' - - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/FillBetweenSlices.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Interpolate segmentation between slices
. Instructions: + """ AutoCompleteEffect is an effect that can create a full segmentation + from a partial segmentation (not all slices are segmented or only + part of the target structures are painted). + """ + + def __init__(self, scriptedEffect): + AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect) + scriptedEffect.name = 'Fill between slices' + + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/FillBetweenSlices.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Interpolate segmentation between slices
. Instructions:

  • Create complete segmentation on selected slices using any editor effect. Segmentation will only expanded if a slice is segmented but none of the direct neighbors are segmented, therefore @@ -41,12 +41,12 @@ def helpText(self): The effect uses morphological contour interpolation method.

    """ - def computePreviewLabelmap(self, mergedImage, outputLabelmap): - import vtkITK - interpolator = vtkITK.vtkITKMorphologicalContourInterpolator() - interpolator.SetInputData(mergedImage) - interpolator.Update() - outputLabelmap.DeepCopy(interpolator.GetOutput()) - imageToWorld = vtk.vtkMatrix4x4() - mergedImage.GetImageToWorldMatrix(imageToWorld) - outputLabelmap.SetImageToWorldMatrix(imageToWorld) + def computePreviewLabelmap(self, mergedImage, outputLabelmap): + import vtkITK + interpolator = vtkITK.vtkITKMorphologicalContourInterpolator() + interpolator.SetInputData(mergedImage) + interpolator.Update() + outputLabelmap.DeepCopy(interpolator.GetOutput()) + imageToWorld = vtk.vtkMatrix4x4() + mergedImage.GetImageToWorldMatrix(imageToWorld) + outputLabelmap.SetImageToWorldMatrix(imageToWorld) diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorGrowFromSeedsEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorGrowFromSeedsEffect.py index aa51e910a23..4f038469892 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorGrowFromSeedsEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorGrowFromSeedsEffect.py @@ -11,33 +11,33 @@ class SegmentEditorGrowFromSeedsEffect(AbstractScriptedSegmentEditorAutoCompleteEffect): - """ AutoCompleteEffect is an effect that can create a full segmentation - from a partial segmentation (not all slices are segmented or only - part of the target structures are painted). - """ - - def __init__(self, scriptedEffect): - AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect) - scriptedEffect.name = 'Grow from seeds' - self.minimumNumberOfSegments = 2 - self.clippedMasterImageDataRequired = True # master volume intensities are used by this effect - self.clippedMaskImageDataRequired = True # masking is used - self.growCutFilter = None - - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/GrowFromSeeds.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Growing segments to create complete segmentation
    . + """ AutoCompleteEffect is an effect that can create a full segmentation + from a partial segmentation (not all slices are segmented or only + part of the target structures are painted). + """ + + def __init__(self, scriptedEffect): + AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect) + scriptedEffect.name = 'Grow from seeds' + self.minimumNumberOfSegments = 2 + self.clippedMasterImageDataRequired = True # master volume intensities are used by this effect + self.clippedMaskImageDataRequired = True # masking is used + self.growCutFilter = None + + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/GrowFromSeeds.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Growing segments to create complete segmentation
    . Location, size, and shape of initial segments and content of master volume are taken into account. Final segment boundaries will be placed where master volume brightness changes abruptly. Instructions:

      @@ -53,86 +53,86 @@ def helpText(self): The effect uses fast grow-cut method.

      """ - def reset(self): - self.growCutFilter = None - AbstractScriptedSegmentEditorAutoCompleteEffect.reset(self) - self.updateGUIFromMRML() - - def setupOptionsFrame(self): - AbstractScriptedSegmentEditorAutoCompleteEffect.setupOptionsFrame(self) - - # Object scale slider - self.seedLocalityFactorSlider = slicer.qMRMLSliderWidget() - self.seedLocalityFactorSlider.setMRMLScene(slicer.mrmlScene) - self.seedLocalityFactorSlider.minimum = 0 - self.seedLocalityFactorSlider.maximum = 10 - self.seedLocalityFactorSlider.value = 0.0 - self.seedLocalityFactorSlider.decimals = 1 - self.seedLocalityFactorSlider.singleStep = 0.1 - self.seedLocalityFactorSlider.pageStep = 1.0 - self.seedLocalityFactorSlider.setToolTip('Increasing this value makes the effect of seeds more localized,' - ' thereby reducing leaks, but requires seed regions to be more evenly distributed in the image.' - ' The value is specified as an additional "intensity level difference" per "unit distance."') - self.scriptedEffect.addLabeledOptionsWidget("Seed locality:", self.seedLocalityFactorSlider) - self.seedLocalityFactorSlider.connect('valueChanged(double)', self.updateAlgorithmParameterFromGUI) - - def setMRMLDefaults(self): - AbstractScriptedSegmentEditorAutoCompleteEffect.setMRMLDefaults(self) - self.scriptedEffect.setParameterDefault("SeedLocalityFactor", 0.0) - - def updateGUIFromMRML(self): - AbstractScriptedSegmentEditorAutoCompleteEffect.updateGUIFromMRML(self) - if self.scriptedEffect.parameterDefined("SeedLocalityFactor"): - seedLocalityFactor = self.scriptedEffect.doubleParameter("SeedLocalityFactor") - else: - seedLocalityFactor = 0.0 - wasBlocked = self.seedLocalityFactorSlider.blockSignals(True) - self.seedLocalityFactorSlider.value = abs(seedLocalityFactor) - self.seedLocalityFactorSlider.blockSignals(wasBlocked) - - def updateMRMLFromGUI(self): - AbstractScriptedSegmentEditorAutoCompleteEffect.updateMRMLFromGUI(self) - self.scriptedEffect.setParameter("SeedLocalityFactor", self.seedLocalityFactorSlider.value) - - def updateAlgorithmParameterFromGUI(self): - self.updateMRMLFromGUI() - - # Trigger preview update - if self.getPreviewNode(): - self.delayedAutoUpdateTimer.start() - - def computePreviewLabelmap(self, mergedImage, outputLabelmap): - import vtkSlicerSegmentationsModuleLogicPython as vtkSlicerSegmentationsModuleLogic - - if not self.growCutFilter: - self.growCutFilter = vtkSlicerSegmentationsModuleLogic.vtkImageGrowCutSegment() - self.growCutFilter.SetIntensityVolume(self.clippedMasterImageData) - self.growCutFilter.SetMaskVolume(self.clippedMaskImageData) - maskExtent = self.clippedMaskImageData.GetExtent() if self.clippedMaskImageData else None - if maskExtent is not None and maskExtent[0] <= maskExtent[1] and maskExtent[2] <= maskExtent[3] and maskExtent[4] <= maskExtent[5]: - # Mask is used. - # Grow the extent more, as background segment does not surround region of interest. - self.extentGrowthRatio = 0.50 - else: - # No masking is used. - # Background segment is expected to surround region of interest, so narrower margin is enough. - self.extentGrowthRatio = 0.20 - - if self.scriptedEffect.parameterDefined("SeedLocalityFactor"): - seedLocalityFactor = self.scriptedEffect.doubleParameter("SeedLocalityFactor") - else: - seedLocalityFactor = 0.0 - self.growCutFilter.SetDistancePenalty(seedLocalityFactor) - self.growCutFilter.SetSeedLabelVolume(mergedImage) - startTime = time.time() - self.growCutFilter.Update() - logging.info('Grow-cut operation on volume of {}x{}x{} voxels was completed in {:3.1f} seconds.'.format( - self.clippedMasterImageData.GetDimensions()[0], - self.clippedMasterImageData.GetDimensions()[1], - self.clippedMasterImageData.GetDimensions()[2], - time.time() - startTime)) - - outputLabelmap.DeepCopy(self.growCutFilter.GetOutput()) - imageToWorld = vtk.vtkMatrix4x4() - mergedImage.GetImageToWorldMatrix(imageToWorld) - outputLabelmap.SetImageToWorldMatrix(imageToWorld) + def reset(self): + self.growCutFilter = None + AbstractScriptedSegmentEditorAutoCompleteEffect.reset(self) + self.updateGUIFromMRML() + + def setupOptionsFrame(self): + AbstractScriptedSegmentEditorAutoCompleteEffect.setupOptionsFrame(self) + + # Object scale slider + self.seedLocalityFactorSlider = slicer.qMRMLSliderWidget() + self.seedLocalityFactorSlider.setMRMLScene(slicer.mrmlScene) + self.seedLocalityFactorSlider.minimum = 0 + self.seedLocalityFactorSlider.maximum = 10 + self.seedLocalityFactorSlider.value = 0.0 + self.seedLocalityFactorSlider.decimals = 1 + self.seedLocalityFactorSlider.singleStep = 0.1 + self.seedLocalityFactorSlider.pageStep = 1.0 + self.seedLocalityFactorSlider.setToolTip('Increasing this value makes the effect of seeds more localized,' + ' thereby reducing leaks, but requires seed regions to be more evenly distributed in the image.' + ' The value is specified as an additional "intensity level difference" per "unit distance."') + self.scriptedEffect.addLabeledOptionsWidget("Seed locality:", self.seedLocalityFactorSlider) + self.seedLocalityFactorSlider.connect('valueChanged(double)', self.updateAlgorithmParameterFromGUI) + + def setMRMLDefaults(self): + AbstractScriptedSegmentEditorAutoCompleteEffect.setMRMLDefaults(self) + self.scriptedEffect.setParameterDefault("SeedLocalityFactor", 0.0) + + def updateGUIFromMRML(self): + AbstractScriptedSegmentEditorAutoCompleteEffect.updateGUIFromMRML(self) + if self.scriptedEffect.parameterDefined("SeedLocalityFactor"): + seedLocalityFactor = self.scriptedEffect.doubleParameter("SeedLocalityFactor") + else: + seedLocalityFactor = 0.0 + wasBlocked = self.seedLocalityFactorSlider.blockSignals(True) + self.seedLocalityFactorSlider.value = abs(seedLocalityFactor) + self.seedLocalityFactorSlider.blockSignals(wasBlocked) + + def updateMRMLFromGUI(self): + AbstractScriptedSegmentEditorAutoCompleteEffect.updateMRMLFromGUI(self) + self.scriptedEffect.setParameter("SeedLocalityFactor", self.seedLocalityFactorSlider.value) + + def updateAlgorithmParameterFromGUI(self): + self.updateMRMLFromGUI() + + # Trigger preview update + if self.getPreviewNode(): + self.delayedAutoUpdateTimer.start() + + def computePreviewLabelmap(self, mergedImage, outputLabelmap): + import vtkSlicerSegmentationsModuleLogicPython as vtkSlicerSegmentationsModuleLogic + + if not self.growCutFilter: + self.growCutFilter = vtkSlicerSegmentationsModuleLogic.vtkImageGrowCutSegment() + self.growCutFilter.SetIntensityVolume(self.clippedMasterImageData) + self.growCutFilter.SetMaskVolume(self.clippedMaskImageData) + maskExtent = self.clippedMaskImageData.GetExtent() if self.clippedMaskImageData else None + if maskExtent is not None and maskExtent[0] <= maskExtent[1] and maskExtent[2] <= maskExtent[3] and maskExtent[4] <= maskExtent[5]: + # Mask is used. + # Grow the extent more, as background segment does not surround region of interest. + self.extentGrowthRatio = 0.50 + else: + # No masking is used. + # Background segment is expected to surround region of interest, so narrower margin is enough. + self.extentGrowthRatio = 0.20 + + if self.scriptedEffect.parameterDefined("SeedLocalityFactor"): + seedLocalityFactor = self.scriptedEffect.doubleParameter("SeedLocalityFactor") + else: + seedLocalityFactor = 0.0 + self.growCutFilter.SetDistancePenalty(seedLocalityFactor) + self.growCutFilter.SetSeedLabelVolume(mergedImage) + startTime = time.time() + self.growCutFilter.Update() + logging.info('Grow-cut operation on volume of {}x{}x{} voxels was completed in {:3.1f} seconds.'.format( + self.clippedMasterImageData.GetDimensions()[0], + self.clippedMasterImageData.GetDimensions()[1], + self.clippedMasterImageData.GetDimensions()[2], + time.time() - startTime)) + + outputLabelmap.DeepCopy(self.growCutFilter.GetOutput()) + imageToWorld = vtk.vtkMatrix4x4() + mergedImage.GetImageToWorldMatrix(imageToWorld) + outputLabelmap.SetImageToWorldMatrix(imageToWorld) diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorHollowEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorHollowEffect.py index 2791d9f23d5..371f59d2a0d 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorHollowEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorHollowEffect.py @@ -11,249 +11,249 @@ class SegmentEditorHollowEffect(AbstractScriptedSegmentEditorEffect): - """This effect makes a segment hollow by replacing it with a shell at the segment boundary""" - - def __init__(self, scriptedEffect): - scriptedEffect.name = 'Hollow' - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) - - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Hollow.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Make the selected segment hollow by replacing the segment with a uniform-thickness shell defined by the segment boundary.""" - - def setupOptionsFrame(self): - - operationLayout = qt.QVBoxLayout() - - self.insideSurfaceOptionRadioButton = qt.QRadioButton("inside surface") - self.medialSurfaceOptionRadioButton = qt.QRadioButton("medial surface") - self.outsideSurfaceOptionRadioButton = qt.QRadioButton("outside surface") - operationLayout.addWidget(self.insideSurfaceOptionRadioButton) - operationLayout.addWidget(self.medialSurfaceOptionRadioButton) - operationLayout.addWidget(self.outsideSurfaceOptionRadioButton) - self.insideSurfaceOptionRadioButton.setChecked(True) - - self.scriptedEffect.addLabeledOptionsWidget("Use current segment as:", operationLayout) - - self.shellThicknessMMSpinBox = slicer.qMRMLSpinBox() - self.shellThicknessMMSpinBox.setMRMLScene(slicer.mrmlScene) - self.shellThicknessMMSpinBox.setToolTip("Thickness of the hollow shell.") - self.shellThicknessMMSpinBox.quantity = "length" - self.shellThicknessMMSpinBox.minimum = 0.0 - self.shellThicknessMMSpinBox.value = 3.0 - self.shellThicknessMMSpinBox.singleStep = 1.0 - - self.shellThicknessLabel = qt.QLabel() - self.shellThicknessLabel.setToolTip("Closest achievable thickness. Constrained by the segmentation's binary labelmap representation spacing.") - - shellThicknessFrame = qt.QHBoxLayout() - shellThicknessFrame.addWidget(self.shellThicknessMMSpinBox) - self.shellThicknessMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Shell thickness:", shellThicknessFrame) - self.scriptedEffect.addLabeledOptionsWidget("", self.shellThicknessLabel) - - self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox() - self.applyToAllVisibleSegmentsCheckBox.setToolTip("Apply hollow effect to all visible segments in this segmentation node. \ + """This effect makes a segment hollow by replacing it with a shell at the segment boundary""" + + def __init__(self, scriptedEffect): + scriptedEffect.name = 'Hollow' + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Hollow.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Make the selected segment hollow by replacing the segment with a uniform-thickness shell defined by the segment boundary.""" + + def setupOptionsFrame(self): + + operationLayout = qt.QVBoxLayout() + + self.insideSurfaceOptionRadioButton = qt.QRadioButton("inside surface") + self.medialSurfaceOptionRadioButton = qt.QRadioButton("medial surface") + self.outsideSurfaceOptionRadioButton = qt.QRadioButton("outside surface") + operationLayout.addWidget(self.insideSurfaceOptionRadioButton) + operationLayout.addWidget(self.medialSurfaceOptionRadioButton) + operationLayout.addWidget(self.outsideSurfaceOptionRadioButton) + self.insideSurfaceOptionRadioButton.setChecked(True) + + self.scriptedEffect.addLabeledOptionsWidget("Use current segment as:", operationLayout) + + self.shellThicknessMMSpinBox = slicer.qMRMLSpinBox() + self.shellThicknessMMSpinBox.setMRMLScene(slicer.mrmlScene) + self.shellThicknessMMSpinBox.setToolTip("Thickness of the hollow shell.") + self.shellThicknessMMSpinBox.quantity = "length" + self.shellThicknessMMSpinBox.minimum = 0.0 + self.shellThicknessMMSpinBox.value = 3.0 + self.shellThicknessMMSpinBox.singleStep = 1.0 + + self.shellThicknessLabel = qt.QLabel() + self.shellThicknessLabel.setToolTip("Closest achievable thickness. Constrained by the segmentation's binary labelmap representation spacing.") + + shellThicknessFrame = qt.QHBoxLayout() + shellThicknessFrame.addWidget(self.shellThicknessMMSpinBox) + self.shellThicknessMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Shell thickness:", shellThicknessFrame) + self.scriptedEffect.addLabeledOptionsWidget("", self.shellThicknessLabel) + + self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox() + self.applyToAllVisibleSegmentsCheckBox.setToolTip("Apply hollow effect to all visible segments in this segmentation node. \ This operation may take a while.") - self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments' - self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox) - - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - self.applyButton.setToolTip("Makes the segment hollow by replacing it with a thick shell at the segment boundary.") - self.scriptedEffect.addOptionsWidget(self.applyButton) - - self.applyButton.connect('clicked()', self.onApply) - self.shellThicknessMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI) - self.insideSurfaceOptionRadioButton.connect("toggled(bool)", self.insideSurfaceModeToggled) - self.medialSurfaceOptionRadioButton.connect("toggled(bool)", self.medialSurfaceModeToggled) - self.outsideSurfaceOptionRadioButton.connect("toggled(bool)", self.outsideSurfaceModeToggled) - self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) - - def createCursor(self, widget): - # Turn off effect-specific cursor for this effect - return slicer.util.mainWindow().cursor - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0) - self.scriptedEffect.setParameterDefault("ShellMode", INSIDE_SURFACE) - self.scriptedEffect.setParameterDefault("ShellThicknessMm", 3.0) - - def getShellThicknessPixel(self): - selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - if selectedSegmentLabelmap: - selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() - - shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm")) - shellThicknessPixel = [int(math.floor(shellThicknessMM / selectedSegmentLabelmapSpacing[componentIndex])) for componentIndex in range(3)] - return shellThicknessPixel - - def updateGUIFromMRML(self): - shellThicknessMM = self.scriptedEffect.doubleParameter("ShellThicknessMm") - wasBlocked = self.shellThicknessMMSpinBox.blockSignals(True) - self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) - self.shellThicknessMMSpinBox.value = abs(shellThicknessMM) - self.shellThicknessMMSpinBox.blockSignals(wasBlocked) - - wasBlocked = self.insideSurfaceOptionRadioButton.blockSignals(True) - self.insideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == INSIDE_SURFACE) - self.insideSurfaceOptionRadioButton.blockSignals(wasBlocked) - - wasBlocked = self.medialSurfaceOptionRadioButton.blockSignals(True) - self.medialSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == MEDIAL_SURFACE) - self.medialSurfaceOptionRadioButton.blockSignals(wasBlocked) - - wasBlocked = self.outsideSurfaceOptionRadioButton.blockSignals(True) - self.outsideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == OUTSIDE_SURFACE) - self.outsideSurfaceOptionRadioButton.blockSignals(wasBlocked) - - selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - if selectedSegmentLabelmap: - selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() - shellThicknessPixel = self.getShellThicknessPixel() - if shellThicknessPixel[0] < 1 or shellThicknessPixel[1] < 1 or shellThicknessPixel[2] < 1: - self.shellThicknessLabel.text = "Not feasible at current resolution." - self.applyButton.setEnabled(False) - else: - thicknessMM = self.getShellThicknessMM() - self.shellThicknessLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*thicknessMM, *shellThicknessPixel) - self.applyButton.setEnabled(True) - else: - self.shellThicknessLabel.text = "Empty segment" - - self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) - - applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked - wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True) - self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments) - self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked) - - def updateMRMLFromGUI(self): - # Operation is managed separately - self.scriptedEffect.setParameter("ShellThicknessMm", self.shellThicknessMMSpinBox.value) - applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0 - self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments) - - def insideSurfaceModeToggled(self, toggled): - if toggled: - self.scriptedEffect.setParameter("ShellMode", INSIDE_SURFACE) - - def medialSurfaceModeToggled(self, toggled): - if toggled: - self.scriptedEffect.setParameter("ShellMode", MEDIAL_SURFACE) - - def outsideSurfaceModeToggled(self, toggled): - if toggled: - self.scriptedEffect.setParameter("ShellMode", OUTSIDE_SURFACE) - - def getShellThicknessMM(self): - selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - if selectedSegmentLabelmap: - selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() - - shellThicknessPixel = self.getShellThicknessPixel() - shellThicknessMM = [abs((shellThicknessPixel[i])*selectedSegmentLabelmapSpacing[i]) for i in range(3)] - for i in range(3): - if shellThicknessMM[i] > 0: - shellThicknessMM[i] = round(shellThicknessMM[i], max(int(-math.floor(math.log10(shellThicknessMM[i]))),1)) - return shellThicknessMM - - def showStatusMessage(self, msg, timeoutMsec=500): + self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments' + self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox) + + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + self.applyButton.setToolTip("Makes the segment hollow by replacing it with a thick shell at the segment boundary.") + self.scriptedEffect.addOptionsWidget(self.applyButton) + + self.applyButton.connect('clicked()', self.onApply) + self.shellThicknessMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI) + self.insideSurfaceOptionRadioButton.connect("toggled(bool)", self.insideSurfaceModeToggled) + self.medialSurfaceOptionRadioButton.connect("toggled(bool)", self.medialSurfaceModeToggled) + self.outsideSurfaceOptionRadioButton.connect("toggled(bool)", self.outsideSurfaceModeToggled) + self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) + + def createCursor(self, widget): + # Turn off effect-specific cursor for this effect + return slicer.util.mainWindow().cursor + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0) + self.scriptedEffect.setParameterDefault("ShellMode", INSIDE_SURFACE) + self.scriptedEffect.setParameterDefault("ShellThicknessMm", 3.0) + + def getShellThicknessPixel(self): + selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + if selectedSegmentLabelmap: + selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() + + shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm")) + shellThicknessPixel = [int(math.floor(shellThicknessMM / selectedSegmentLabelmapSpacing[componentIndex])) for componentIndex in range(3)] + return shellThicknessPixel + + def updateGUIFromMRML(self): + shellThicknessMM = self.scriptedEffect.doubleParameter("ShellThicknessMm") + wasBlocked = self.shellThicknessMMSpinBox.blockSignals(True) + self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) + self.shellThicknessMMSpinBox.value = abs(shellThicknessMM) + self.shellThicknessMMSpinBox.blockSignals(wasBlocked) + + wasBlocked = self.insideSurfaceOptionRadioButton.blockSignals(True) + self.insideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == INSIDE_SURFACE) + self.insideSurfaceOptionRadioButton.blockSignals(wasBlocked) + + wasBlocked = self.medialSurfaceOptionRadioButton.blockSignals(True) + self.medialSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == MEDIAL_SURFACE) + self.medialSurfaceOptionRadioButton.blockSignals(wasBlocked) + + wasBlocked = self.outsideSurfaceOptionRadioButton.blockSignals(True) + self.outsideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == OUTSIDE_SURFACE) + self.outsideSurfaceOptionRadioButton.blockSignals(wasBlocked) + + selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + if selectedSegmentLabelmap: + selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() + shellThicknessPixel = self.getShellThicknessPixel() + if shellThicknessPixel[0] < 1 or shellThicknessPixel[1] < 1 or shellThicknessPixel[2] < 1: + self.shellThicknessLabel.text = "Not feasible at current resolution." + self.applyButton.setEnabled(False) + else: + thicknessMM = self.getShellThicknessMM() + self.shellThicknessLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*thicknessMM, *shellThicknessPixel) + self.applyButton.setEnabled(True) + else: + self.shellThicknessLabel.text = "Empty segment" + + self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) + + applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked + wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True) + self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments) + self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked) + + def updateMRMLFromGUI(self): + # Operation is managed separately + self.scriptedEffect.setParameter("ShellThicknessMm", self.shellThicknessMMSpinBox.value) + applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0 + self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments) + + def insideSurfaceModeToggled(self, toggled): + if toggled: + self.scriptedEffect.setParameter("ShellMode", INSIDE_SURFACE) + + def medialSurfaceModeToggled(self, toggled): + if toggled: + self.scriptedEffect.setParameter("ShellMode", MEDIAL_SURFACE) + + def outsideSurfaceModeToggled(self, toggled): + if toggled: + self.scriptedEffect.setParameter("ShellMode", OUTSIDE_SURFACE) + + def getShellThicknessMM(self): + selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + if selectedSegmentLabelmap: + selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() + + shellThicknessPixel = self.getShellThicknessPixel() + shellThicknessMM = [abs((shellThicknessPixel[i]) * selectedSegmentLabelmapSpacing[i]) for i in range(3)] + for i in range(3): + if shellThicknessMM[i] > 0: + shellThicknessMM[i] = round(shellThicknessMM[i], max(int(-math.floor(math.log10(shellThicknessMM[i]))), 1)) + return shellThicknessMM + + def showStatusMessage(self, msg, timeoutMsec=500): slicer.util.showStatusMessage(msg, timeoutMsec) slicer.app.processEvents() - def processHollowing(self): - # Get modifier labelmap and parameters - modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value - labelValue = 1 - backgroundValue = 0 - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(selectedSegmentLabelmap) - thresh.ThresholdByLower(0) - thresh.SetInValue(backgroundValue) - thresh.SetOutValue(labelValue) - thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) - - shellMode = self.scriptedEffect.parameter("ShellMode") - shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm")) - import vtkITK - margin = vtkITK.vtkITKImageMargin() - margin.SetInputConnection(thresh.GetOutputPort()) - margin.CalculateMarginInMMOn() - - spacing = selectedSegmentLabelmap.GetSpacing() - voxelDiameter = min(selectedSegmentLabelmap.GetSpacing()) - if shellMode == MEDIAL_SURFACE: - margin.SetOuterMarginMM( 0.5 * shellThicknessMM) - margin.SetInnerMarginMM(-0.5 * shellThicknessMM + 0.5*voxelDiameter) - elif shellMode == INSIDE_SURFACE: - margin.SetOuterMarginMM(shellThicknessMM + 0.1*voxelDiameter) - margin.SetInnerMarginMM(0.0 + 0.1*voxelDiameter) # Don't include the original border (0.0) - elif shellMode == OUTSIDE_SURFACE: - margin.SetOuterMarginMM(0.0) - margin.SetInnerMarginMM(-shellThicknessMM + voxelDiameter) - - modifierLabelmap.DeepCopy(margin.GetOutput()) - - margin.Update() - modifierLabelmap.ShallowCopy(margin.GetOutput()) - - # Apply changes - self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) - - def onApply(self): - # Make sure the user wants to do the operation, even if the segment is not visible - if not self.scriptedEffect.confirmCurrentSegmentVisible(): - return - - try: - # This can be a long operation - indicate it to the user - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - self.scriptedEffect.saveStateForUndo() - - applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) !=0 \ - if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False - - if applyToAllVisibleSegments: - # Process all visible segments - inputSegmentIDs = vtk.vtkStringArray() - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs) - segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor - segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode() - # store which segment was selected before operation - selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID() - if inputSegmentIDs.GetNumberOfValues() == 0: - logging.info("Hollow operation skipped: there are no visible segments.") - return - # select input segments one by one, process - for index in range(inputSegmentIDs.GetNumberOfValues()): - segmentID = inputSegmentIDs.GetValue(index) - self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...') - segmentEditorNode.SetSelectedSegmentID(segmentID) - self.processHollowing() - # restore segment selection - segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID) - else: - self.processHollowing() - - finally: - qt.QApplication.restoreOverrideCursor() + def processHollowing(self): + # Get modifier labelmap and parameters + modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value + labelValue = 1 + backgroundValue = 0 + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(selectedSegmentLabelmap) + thresh.ThresholdByLower(0) + thresh.SetInValue(backgroundValue) + thresh.SetOutValue(labelValue) + thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) + + shellMode = self.scriptedEffect.parameter("ShellMode") + shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm")) + import vtkITK + margin = vtkITK.vtkITKImageMargin() + margin.SetInputConnection(thresh.GetOutputPort()) + margin.CalculateMarginInMMOn() + + spacing = selectedSegmentLabelmap.GetSpacing() + voxelDiameter = min(selectedSegmentLabelmap.GetSpacing()) + if shellMode == MEDIAL_SURFACE: + margin.SetOuterMarginMM(0.5 * shellThicknessMM) + margin.SetInnerMarginMM(-0.5 * shellThicknessMM + 0.5 * voxelDiameter) + elif shellMode == INSIDE_SURFACE: + margin.SetOuterMarginMM(shellThicknessMM + 0.1 * voxelDiameter) + margin.SetInnerMarginMM(0.0 + 0.1 * voxelDiameter) # Don't include the original border (0.0) + elif shellMode == OUTSIDE_SURFACE: + margin.SetOuterMarginMM(0.0) + margin.SetInnerMarginMM(-shellThicknessMM + voxelDiameter) + + modifierLabelmap.DeepCopy(margin.GetOutput()) + + margin.Update() + modifierLabelmap.ShallowCopy(margin.GetOutput()) + + # Apply changes + self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) + + def onApply(self): + # Make sure the user wants to do the operation, even if the segment is not visible + if not self.scriptedEffect.confirmCurrentSegmentVisible(): + return + + try: + # This can be a long operation - indicate it to the user + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + self.scriptedEffect.saveStateForUndo() + + applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \ + if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False + + if applyToAllVisibleSegments: + # Process all visible segments + inputSegmentIDs = vtk.vtkStringArray() + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs) + segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor + segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode() + # store which segment was selected before operation + selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID() + if inputSegmentIDs.GetNumberOfValues() == 0: + logging.info("Hollow operation skipped: there are no visible segments.") + return + # select input segments one by one, process + for index in range(inputSegmentIDs.GetNumberOfValues()): + segmentID = inputSegmentIDs.GetValue(index) + self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...') + segmentEditorNode.SetSelectedSegmentID(segmentID) + self.processHollowing() + # restore segment selection + segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID) + else: + self.processHollowing() + + finally: + qt.QApplication.restoreOverrideCursor() INSIDE_SURFACE = 'INSIDE_SURFACE' diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorIslandsEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorIslandsEffect.py index e6e2181e62d..1bb3f0884ec 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorIslandsEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorIslandsEffect.py @@ -11,390 +11,390 @@ class SegmentEditorIslandsEffect(AbstractScriptedSegmentEditorEffect): - """ Operate on connected components (islands) within a segment - """ - - def __init__(self, scriptedEffect): - scriptedEffect.name = 'Islands' - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) - self.widgetToOperationNameMap = {} - - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Islands.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Edit islands (connected components) in a segment
      . To get more information + """ Operate on connected components (islands) within a segment + """ + + def __init__(self, scriptedEffect): + scriptedEffect.name = 'Islands' + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + self.widgetToOperationNameMap = {} + + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Islands.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Edit islands (connected components) in a segment
      . To get more information about each operation, hover the mouse over the option and wait for the tooltip to appear.""" - def setupOptionsFrame(self): - self.operationRadioButtons = [] - - self.keepLargestOptionRadioButton = qt.QRadioButton("Keep largest island") - self.keepLargestOptionRadioButton.setToolTip( - "Keep only the largest island in selected segment, remove all other islands in the segment.") - self.operationRadioButtons.append(self.keepLargestOptionRadioButton) - self.widgetToOperationNameMap[self.keepLargestOptionRadioButton] = KEEP_LARGEST_ISLAND - - self.keepSelectedOptionRadioButton = qt.QRadioButton("Keep selected island") - self.keepSelectedOptionRadioButton.setToolTip( - "Click on an island in a slice view to keep that island and remove all other islands in selected segment.") - self.operationRadioButtons.append(self.keepSelectedOptionRadioButton) - self.widgetToOperationNameMap[self.keepSelectedOptionRadioButton] = KEEP_SELECTED_ISLAND - - self.removeSmallOptionRadioButton = qt.QRadioButton("Remove small islands") - self.removeSmallOptionRadioButton.setToolTip( - "Remove all islands from the selected segment that are smaller than the specified minimum size.") - self.operationRadioButtons.append(self.removeSmallOptionRadioButton) - self.widgetToOperationNameMap[self.removeSmallOptionRadioButton] = REMOVE_SMALL_ISLANDS - - self.removeSelectedOptionRadioButton = qt.QRadioButton("Remove selected island") - self.removeSelectedOptionRadioButton.setToolTip( - "Click on an island in a slice view to remove it from selected segment.") - self.operationRadioButtons.append(self.removeSelectedOptionRadioButton) - self.widgetToOperationNameMap[self.removeSelectedOptionRadioButton] = REMOVE_SELECTED_ISLAND - - self.addSelectedOptionRadioButton = qt.QRadioButton("Add selected island") - self.addSelectedOptionRadioButton.setToolTip( - "Click on a region in a slice view to add it to selected segment.") - self.operationRadioButtons.append(self.addSelectedOptionRadioButton) - self.widgetToOperationNameMap[self.addSelectedOptionRadioButton] = ADD_SELECTED_ISLAND - - self.splitAllOptionRadioButton = qt.QRadioButton("Split islands to segments") - self.splitAllOptionRadioButton.setToolTip( - "Create a new segment for each island of selected segment. Islands smaller than minimum size will be removed. "+ - "Segments will be ordered by island size.") - self.operationRadioButtons.append(self.splitAllOptionRadioButton) - self.widgetToOperationNameMap[self.splitAllOptionRadioButton] = SPLIT_ISLANDS_TO_SEGMENTS - - operationLayout = qt.QGridLayout() - operationLayout.addWidget(self.keepLargestOptionRadioButton,0,0) - operationLayout.addWidget(self.removeSmallOptionRadioButton,1,0) - operationLayout.addWidget(self.splitAllOptionRadioButton,2,0) - operationLayout.addWidget(self.keepSelectedOptionRadioButton,0,1) - operationLayout.addWidget(self.removeSelectedOptionRadioButton,1,1) - operationLayout.addWidget(self.addSelectedOptionRadioButton,2,1) - - self.operationRadioButtons[0].setChecked(True) - self.scriptedEffect.addOptionsWidget(operationLayout) - - self.minimumSizeSpinBox = qt.QSpinBox() - self.minimumSizeSpinBox.setToolTip("Islands consisting of less voxels than this minimum size, will be deleted.") - self.minimumSizeSpinBox.setMinimum(0) - self.minimumSizeSpinBox.setMaximum(vtk.VTK_INT_MAX) - self.minimumSizeSpinBox.setValue(1000) - self.minimumSizeSpinBox.suffix = " voxels" - self.minimumSizeLabel = self.scriptedEffect.addLabeledOptionsWidget("Minimum size:", self.minimumSizeSpinBox) - - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - self.scriptedEffect.addOptionsWidget(self.applyButton) - - for operationRadioButton in self.operationRadioButtons: - operationRadioButton.connect('toggled(bool)', - lambda toggle, widget=self.widgetToOperationNameMap[operationRadioButton]: self.onOperationSelectionChanged(widget, toggle)) - - self.minimumSizeSpinBox.connect('valueChanged(int)', self.updateMRMLFromGUI) - - self.applyButton.connect('clicked()', self.onApply) - - def onOperationSelectionChanged(self, operationName, toggle): - if not toggle: - return - self.scriptedEffect.setParameter("Operation", operationName) - - def currentOperationRequiresSegmentSelection(self): - operationName = self.scriptedEffect.parameter("Operation") - return operationName in [KEEP_SELECTED_ISLAND, REMOVE_SELECTED_ISLAND, ADD_SELECTED_ISLAND] - - def onApply(self): - # Make sure the user wants to do the operation, even if the segment is not visible - if not self.scriptedEffect.confirmCurrentSegmentVisible(): - return - operationName = self.scriptedEffect.parameter("Operation") - minimumSize = self.scriptedEffect.integerParameter("MinimumSize") - if operationName == KEEP_LARGEST_ISLAND: - self.splitSegments(minimumSize = minimumSize, maxNumberOfSegments = 1) - elif operationName == REMOVE_SMALL_ISLANDS: - self.splitSegments(minimumSize = minimumSize, split = False) - elif operationName == SPLIT_ISLANDS_TO_SEGMENTS: - self.splitSegments(minimumSize = minimumSize) - - def splitSegments(self, minimumSize = 0, maxNumberOfSegments = 0, split = True): - """ - minimumSize: if 0 then it means that all islands are kept, regardless of size - maxNumberOfSegments: if 0 then it means that all islands are kept, regardless of how many - """ - # This can be a long operation - indicate it to the user - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - - self.scriptedEffect.saveStateForUndo() - - # Get modifier labelmap - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - - castIn = vtk.vtkImageCast() - castIn.SetInputData(selectedSegmentLabelmap) - castIn.SetOutputScalarTypeToUnsignedInt() - - # Identify the islands in the inverted volume and - # find the pixel that corresponds to the background - islandMath = vtkITK.vtkITKIslandMath() - islandMath.SetInputConnection(castIn.GetOutputPort()) - islandMath.SetFullyConnected(False) - islandMath.SetMinimumSize(minimumSize) - islandMath.Update() - - islandImage = slicer.vtkOrientedImageData() - islandImage.ShallowCopy(islandMath.GetOutput()) - selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4() - selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) - islandImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) - - islandCount = islandMath.GetNumberOfIslands() - islandOrigCount = islandMath.GetOriginalNumberOfIslands() - ignoredIslands = islandOrigCount - islandCount - logging.info( "%d islands created (%d ignored)" % (islandCount, ignoredIslands) ) - - baseSegmentName = "Label" - selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - with slicer.util.NodeModify(segmentationNode): - segmentation = segmentationNode.GetSegmentation() - selectedSegment = segmentation.GetSegment(selectedSegmentID) - selectedSegmentName = selectedSegment.GetName() - if selectedSegmentName is not None and selectedSegmentName != "": - baseSegmentName = selectedSegmentName - - labelValues = vtk.vtkIntArray() - slicer.vtkSlicerSegmentationsModuleLogic.GetAllLabelValues(labelValues, islandImage) - - # Erase segment from in original labelmap. - # Individuall islands will be added back later. - threshold = vtk.vtkImageThreshold() - threshold.SetInputData(selectedSegmentLabelmap) - threshold.ThresholdBetween(0, 0) - threshold.SetInValue(0) - threshold.SetOutValue(0) - threshold.Update() - emptyLabelmap = slicer.vtkOrientedImageData() - emptyLabelmap.ShallowCopy(threshold.GetOutput()) - emptyLabelmap.CopyDirections(selectedSegmentLabelmap) - self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, selectedSegmentID, emptyLabelmap, - slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) - - for i in range(labelValues.GetNumberOfTuples()): - if (maxNumberOfSegments > 0 and i >= maxNumberOfSegments): - # We only care about the segments up to maxNumberOfSegments. - # If we do not want to split segments, we only care about the first. - break - - labelValue = int(labelValues.GetTuple1(i)) - segment = selectedSegment - segmentID = selectedSegmentID - if i != 0 and split: - segment = slicer.vtkSegment() - name = baseSegmentName + "_" + str(i+1) - segment.SetName(name) - segment.AddRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(), - selectedSegment.GetRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName())) - segmentation.AddSegment(segment) - segmentID = segmentation.GetSegmentIdBySegment(segment) - segment.SetLabelValue(segmentation.GetUniqueLabelValueForSharedLabelmap(selectedSegmentID)) - - threshold = vtk.vtkImageThreshold() - threshold.SetInputData(islandMath.GetOutput()) - if not split and maxNumberOfSegments <= 0: - # no need to split segments and no limit on number of segments, so we can lump all islands into one segment - threshold.ThresholdByLower(0) - threshold.SetInValue(0) - threshold.SetOutValue(1) - else: - # copy only selected islands; or copy islands into different segments - threshold.ThresholdBetween(labelValue, labelValue) - threshold.SetInValue(1) - threshold.SetOutValue(0) - threshold.Update() - - modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd - if i == 0: - modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet - - # Create oriented image data from output - modifierImage = slicer.vtkOrientedImageData() - modifierImage.DeepCopy(threshold.GetOutput()) + def setupOptionsFrame(self): + self.operationRadioButtons = [] + + self.keepLargestOptionRadioButton = qt.QRadioButton("Keep largest island") + self.keepLargestOptionRadioButton.setToolTip( + "Keep only the largest island in selected segment, remove all other islands in the segment.") + self.operationRadioButtons.append(self.keepLargestOptionRadioButton) + self.widgetToOperationNameMap[self.keepLargestOptionRadioButton] = KEEP_LARGEST_ISLAND + + self.keepSelectedOptionRadioButton = qt.QRadioButton("Keep selected island") + self.keepSelectedOptionRadioButton.setToolTip( + "Click on an island in a slice view to keep that island and remove all other islands in selected segment.") + self.operationRadioButtons.append(self.keepSelectedOptionRadioButton) + self.widgetToOperationNameMap[self.keepSelectedOptionRadioButton] = KEEP_SELECTED_ISLAND + + self.removeSmallOptionRadioButton = qt.QRadioButton("Remove small islands") + self.removeSmallOptionRadioButton.setToolTip( + "Remove all islands from the selected segment that are smaller than the specified minimum size.") + self.operationRadioButtons.append(self.removeSmallOptionRadioButton) + self.widgetToOperationNameMap[self.removeSmallOptionRadioButton] = REMOVE_SMALL_ISLANDS + + self.removeSelectedOptionRadioButton = qt.QRadioButton("Remove selected island") + self.removeSelectedOptionRadioButton.setToolTip( + "Click on an island in a slice view to remove it from selected segment.") + self.operationRadioButtons.append(self.removeSelectedOptionRadioButton) + self.widgetToOperationNameMap[self.removeSelectedOptionRadioButton] = REMOVE_SELECTED_ISLAND + + self.addSelectedOptionRadioButton = qt.QRadioButton("Add selected island") + self.addSelectedOptionRadioButton.setToolTip( + "Click on a region in a slice view to add it to selected segment.") + self.operationRadioButtons.append(self.addSelectedOptionRadioButton) + self.widgetToOperationNameMap[self.addSelectedOptionRadioButton] = ADD_SELECTED_ISLAND + + self.splitAllOptionRadioButton = qt.QRadioButton("Split islands to segments") + self.splitAllOptionRadioButton.setToolTip( + "Create a new segment for each island of selected segment. Islands smaller than minimum size will be removed. " + + "Segments will be ordered by island size.") + self.operationRadioButtons.append(self.splitAllOptionRadioButton) + self.widgetToOperationNameMap[self.splitAllOptionRadioButton] = SPLIT_ISLANDS_TO_SEGMENTS + + operationLayout = qt.QGridLayout() + operationLayout.addWidget(self.keepLargestOptionRadioButton, 0, 0) + operationLayout.addWidget(self.removeSmallOptionRadioButton, 1, 0) + operationLayout.addWidget(self.splitAllOptionRadioButton, 2, 0) + operationLayout.addWidget(self.keepSelectedOptionRadioButton, 0, 1) + operationLayout.addWidget(self.removeSelectedOptionRadioButton, 1, 1) + operationLayout.addWidget(self.addSelectedOptionRadioButton, 2, 1) + + self.operationRadioButtons[0].setChecked(True) + self.scriptedEffect.addOptionsWidget(operationLayout) + + self.minimumSizeSpinBox = qt.QSpinBox() + self.minimumSizeSpinBox.setToolTip("Islands consisting of less voxels than this minimum size, will be deleted.") + self.minimumSizeSpinBox.setMinimum(0) + self.minimumSizeSpinBox.setMaximum(vtk.VTK_INT_MAX) + self.minimumSizeSpinBox.setValue(1000) + self.minimumSizeSpinBox.suffix = " voxels" + self.minimumSizeLabel = self.scriptedEffect.addLabeledOptionsWidget("Minimum size:", self.minimumSizeSpinBox) + + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + self.scriptedEffect.addOptionsWidget(self.applyButton) + + for operationRadioButton in self.operationRadioButtons: + operationRadioButton.connect('toggled(bool)', + lambda toggle, widget=self.widgetToOperationNameMap[operationRadioButton]: self.onOperationSelectionChanged(widget, toggle)) + + self.minimumSizeSpinBox.connect('valueChanged(int)', self.updateMRMLFromGUI) + + self.applyButton.connect('clicked()', self.onApply) + + def onOperationSelectionChanged(self, operationName, toggle): + if not toggle: + return + self.scriptedEffect.setParameter("Operation", operationName) + + def currentOperationRequiresSegmentSelection(self): + operationName = self.scriptedEffect.parameter("Operation") + return operationName in [KEEP_SELECTED_ISLAND, REMOVE_SELECTED_ISLAND, ADD_SELECTED_ISLAND] + + def onApply(self): + # Make sure the user wants to do the operation, even if the segment is not visible + if not self.scriptedEffect.confirmCurrentSegmentVisible(): + return + operationName = self.scriptedEffect.parameter("Operation") + minimumSize = self.scriptedEffect.integerParameter("MinimumSize") + if operationName == KEEP_LARGEST_ISLAND: + self.splitSegments(minimumSize=minimumSize, maxNumberOfSegments=1) + elif operationName == REMOVE_SMALL_ISLANDS: + self.splitSegments(minimumSize=minimumSize, split=False) + elif operationName == SPLIT_ISLANDS_TO_SEGMENTS: + self.splitSegments(minimumSize=minimumSize) + + def splitSegments(self, minimumSize=0, maxNumberOfSegments=0, split=True): + """ + minimumSize: if 0 then it means that all islands are kept, regardless of size + maxNumberOfSegments: if 0 then it means that all islands are kept, regardless of how many + """ + # This can be a long operation - indicate it to the user + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + + self.scriptedEffect.saveStateForUndo() + + # Get modifier labelmap + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + + castIn = vtk.vtkImageCast() + castIn.SetInputData(selectedSegmentLabelmap) + castIn.SetOutputScalarTypeToUnsignedInt() + + # Identify the islands in the inverted volume and + # find the pixel that corresponds to the background + islandMath = vtkITK.vtkITKIslandMath() + islandMath.SetInputConnection(castIn.GetOutputPort()) + islandMath.SetFullyConnected(False) + islandMath.SetMinimumSize(minimumSize) + islandMath.Update() + + islandImage = slicer.vtkOrientedImageData() + islandImage.ShallowCopy(islandMath.GetOutput()) selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4() selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) - modifierImage.SetGeometryFromImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) - # We could use a single slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode - # method call to import all the resulting segments at once but that would put all the imported segments - # in a new layer. By using modifySegmentByLabelmap, the number of layers will not increase. - self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, modifierImage, modificationMode) + islandImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) + + islandCount = islandMath.GetNumberOfIslands() + islandOrigCount = islandMath.GetOriginalNumberOfIslands() + ignoredIslands = islandOrigCount - islandCount + logging.info("%d islands created (%d ignored)" % (islandCount, ignoredIslands)) + + baseSegmentName = "Label" + selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + with slicer.util.NodeModify(segmentationNode): + segmentation = segmentationNode.GetSegmentation() + selectedSegment = segmentation.GetSegment(selectedSegmentID) + selectedSegmentName = selectedSegment.GetName() + if selectedSegmentName is not None and selectedSegmentName != "": + baseSegmentName = selectedSegmentName + + labelValues = vtk.vtkIntArray() + slicer.vtkSlicerSegmentationsModuleLogic.GetAllLabelValues(labelValues, islandImage) + + # Erase segment from in original labelmap. + # Individuall islands will be added back later. + threshold = vtk.vtkImageThreshold() + threshold.SetInputData(selectedSegmentLabelmap) + threshold.ThresholdBetween(0, 0) + threshold.SetInValue(0) + threshold.SetOutValue(0) + threshold.Update() + emptyLabelmap = slicer.vtkOrientedImageData() + emptyLabelmap.ShallowCopy(threshold.GetOutput()) + emptyLabelmap.CopyDirections(selectedSegmentLabelmap) + self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, selectedSegmentID, emptyLabelmap, + slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) + + for i in range(labelValues.GetNumberOfTuples()): + if (maxNumberOfSegments > 0 and i >= maxNumberOfSegments): + # We only care about the segments up to maxNumberOfSegments. + # If we do not want to split segments, we only care about the first. + break + + labelValue = int(labelValues.GetTuple1(i)) + segment = selectedSegment + segmentID = selectedSegmentID + if i != 0 and split: + segment = slicer.vtkSegment() + name = baseSegmentName + "_" + str(i + 1) + segment.SetName(name) + segment.AddRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(), + selectedSegment.GetRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName())) + segmentation.AddSegment(segment) + segmentID = segmentation.GetSegmentIdBySegment(segment) + segment.SetLabelValue(segmentation.GetUniqueLabelValueForSharedLabelmap(selectedSegmentID)) + + threshold = vtk.vtkImageThreshold() + threshold.SetInputData(islandMath.GetOutput()) + if not split and maxNumberOfSegments <= 0: + # no need to split segments and no limit on number of segments, so we can lump all islands into one segment + threshold.ThresholdByLower(0) + threshold.SetInValue(0) + threshold.SetOutValue(1) + else: + # copy only selected islands; or copy islands into different segments + threshold.ThresholdBetween(labelValue, labelValue) + threshold.SetInValue(1) + threshold.SetOutValue(0) + threshold.Update() + + modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd + if i == 0: + modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet + + # Create oriented image data from output + modifierImage = slicer.vtkOrientedImageData() + modifierImage.DeepCopy(threshold.GetOutput()) + selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4() + selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) + modifierImage.SetGeometryFromImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) + # We could use a single slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode + # method call to import all the resulting segments at once but that would put all the imported segments + # in a new layer. By using modifySegmentByLabelmap, the number of layers will not increase. + self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, modifierImage, modificationMode) + + if not split and maxNumberOfSegments <= 0: + # all islands lumped into one segment, so we are done + break - if not split and maxNumberOfSegments <= 0: - # all islands lumped into one segment, so we are done - break + qt.QApplication.restoreOverrideCursor() - qt.QApplication.restoreOverrideCursor() + def processInteractionEvents(self, callerInteractor, eventId, viewWidget): + import vtkSegmentationCorePython as vtkSegmentationCore - def processInteractionEvents(self, callerInteractor, eventId, viewWidget): - import vtkSegmentationCorePython as vtkSegmentationCore + abortEvent = False - abortEvent = False + # Only allow in modes where segment selection is needed + if not self.currentOperationRequiresSegmentSelection(): + return False - # Only allow in modes where segment selection is needed - if not self.currentOperationRequiresSegmentSelection(): - return False + # Only allow for slice views + if viewWidget.className() != "qMRMLSliceWidget": + return abortEvent - # Only allow for slice views - if viewWidget.className() != "qMRMLSliceWidget": - return abortEvent + if eventId != vtk.vtkCommand.LeftButtonPressEvent or callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey(): + return abortEvent - if eventId != vtk.vtkCommand.LeftButtonPressEvent or callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey(): - return abortEvent + # Make sure the user wants to do the operation, even if the segment is not visible + if not self.scriptedEffect.confirmCurrentSegmentVisible(): + return abortEvent - # Make sure the user wants to do the operation, even if the segment is not visible - if not self.scriptedEffect.confirmCurrentSegmentVisible(): - return abortEvent + abortEvent = True - abortEvent = True + # Generate merged labelmap of all visible segments + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + visibleSegmentIds = vtk.vtkStringArray() + segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) + if visibleSegmentIds.GetNumberOfValues() == 0: + logging.info("Island operation skipped: there are no visible segments") + return abortEvent - # Generate merged labelmap of all visible segments - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - visibleSegmentIds = vtk.vtkStringArray() - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) - if visibleSegmentIds.GetNumberOfValues() == 0: - logging.info("Island operation skipped: there are no visible segments") - return abortEvent + self.scriptedEffect.saveStateForUndo() - self.scriptedEffect.saveStateForUndo() + # This can be a long operation - indicate it to the user + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - # This can be a long operation - indicate it to the user - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + operationName = self.scriptedEffect.parameter("Operation") - operationName = self.scriptedEffect.parameter("Operation") + if operationName == ADD_SELECTED_ISLAND: + inputLabelImage = slicer.vtkOrientedImageData() + if not segmentationNode.GenerateMergedLabelmapForAllSegments(inputLabelImage, + vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED, + None, visibleSegmentIds): + logging.error('Failed to apply island operation: cannot get list of visible segments') + qt.QApplication.restoreOverrideCursor() + return abortEvent + else: + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value + labelValue = 1 + backgroundValue = 0 + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(selectedSegmentLabelmap) + thresh.ThresholdByLower(0) + thresh.SetInValue(backgroundValue) + thresh.SetOutValue(labelValue) + thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) + thresh.Update() + # Create oriented image data from output + import vtkSegmentationCorePython as vtkSegmentationCore + inputLabelImage = slicer.vtkOrientedImageData() + inputLabelImage.ShallowCopy(thresh.GetOutput()) + selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4() + selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) + inputLabelImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) + + xy = callerInteractor.GetEventPosition() + ijk = self.xyToIjk(xy, viewWidget, inputLabelImage, segmentationNode.GetParentTransformNode()) + pixelValue = inputLabelImage.GetScalarComponentAsFloat(ijk[0], ijk[1], ijk[2], 0) + + try: + floodFillingFilter = vtk.vtkImageThresholdConnectivity() + floodFillingFilter.SetInputData(inputLabelImage) + seedPoints = vtk.vtkPoints() + origin = inputLabelImage.GetOrigin() + spacing = inputLabelImage.GetSpacing() + seedPoints.InsertNextPoint(origin[0] + ijk[0] * spacing[0], origin[1] + ijk[1] * spacing[1], origin[2] + ijk[2] * spacing[2]) + floodFillingFilter.SetSeedPoints(seedPoints) + floodFillingFilter.ThresholdBetween(pixelValue, pixelValue) + + if operationName == ADD_SELECTED_ISLAND: + floodFillingFilter.SetInValue(1) + floodFillingFilter.SetOutValue(0) + floodFillingFilter.Update() + modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() + modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput()) + self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd) + + elif pixelValue != 0: # if clicked on empty part then there is nothing to remove or keep + + if operationName == KEEP_SELECTED_ISLAND: + floodFillingFilter.SetInValue(1) + floodFillingFilter.SetOutValue(0) + else: # operationName == REMOVE_SELECTED_ISLAND: + floodFillingFilter.SetInValue(1) + floodFillingFilter.SetOutValue(0) + + floodFillingFilter.Update() + modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() + modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput()) + + if operationName == KEEP_SELECTED_ISLAND: + self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) + else: # operationName == REMOVE_SELECTED_ISLAND: + self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove) + + except IndexError: + logging.error('apply: Failed to threshold master volume!') + finally: + qt.QApplication.restoreOverrideCursor() - if operationName == ADD_SELECTED_ISLAND: - inputLabelImage = slicer.vtkOrientedImageData() - if not segmentationNode.GenerateMergedLabelmapForAllSegments(inputLabelImage, - vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED, - None, visibleSegmentIds): - logging.error('Failed to apply island operation: cannot get list of visible segments') - qt.QApplication.restoreOverrideCursor() return abortEvent - else: - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value - labelValue = 1 - backgroundValue = 0 - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(selectedSegmentLabelmap) - thresh.ThresholdByLower(0) - thresh.SetInValue(backgroundValue) - thresh.SetOutValue(labelValue) - thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) - thresh.Update() - # Create oriented image data from output - import vtkSegmentationCorePython as vtkSegmentationCore - inputLabelImage = slicer.vtkOrientedImageData() - inputLabelImage.ShallowCopy(thresh.GetOutput()) - selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4() - selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) - inputLabelImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix) - - xy = callerInteractor.GetEventPosition() - ijk = self.xyToIjk(xy, viewWidget, inputLabelImage, segmentationNode.GetParentTransformNode()) - pixelValue = inputLabelImage.GetScalarComponentAsFloat(ijk[0], ijk[1], ijk[2], 0) - - try: - floodFillingFilter = vtk.vtkImageThresholdConnectivity() - floodFillingFilter.SetInputData(inputLabelImage) - seedPoints = vtk.vtkPoints() - origin = inputLabelImage.GetOrigin() - spacing = inputLabelImage.GetSpacing() - seedPoints.InsertNextPoint(origin[0]+ijk[0]*spacing[0], origin[1]+ijk[1]*spacing[1], origin[2]+ijk[2]*spacing[2]) - floodFillingFilter.SetSeedPoints(seedPoints) - floodFillingFilter.ThresholdBetween(pixelValue, pixelValue) - - if operationName == ADD_SELECTED_ISLAND: - floodFillingFilter.SetInValue(1) - floodFillingFilter.SetOutValue(0) - floodFillingFilter.Update() - modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() - modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput()) - self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd) - - elif pixelValue != 0: # if clicked on empty part then there is nothing to remove or keep - - if operationName == KEEP_SELECTED_ISLAND: - floodFillingFilter.SetInValue(1) - floodFillingFilter.SetOutValue(0) - else: # operationName == REMOVE_SELECTED_ISLAND: - floodFillingFilter.SetInValue(1) - floodFillingFilter.SetOutValue(0) - - floodFillingFilter.Update() - modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() - modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput()) - - if operationName == KEEP_SELECTED_ISLAND: - self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) - else: # operationName == REMOVE_SELECTED_ISLAND: - self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove) - - except IndexError: - logging.error('apply: Failed to threshold master volume!') - finally: - qt.QApplication.restoreOverrideCursor() - - return abortEvent - - def processViewNodeEvents(self, callerViewNode, eventId, viewWidget): - pass # For the sake of example - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("Operation", KEEP_LARGEST_ISLAND) - self.scriptedEffect.setParameterDefault("MinimumSize", 1000) - - def updateGUIFromMRML(self): - for operationRadioButton in self.operationRadioButtons: - operationRadioButton.blockSignals(True) - operationName = self.scriptedEffect.parameter("Operation") - currentOperationRadioButton = list(self.widgetToOperationNameMap.keys())[list(self.widgetToOperationNameMap.values()).index(operationName)] - currentOperationRadioButton.setChecked(True) - for operationRadioButton in self.operationRadioButtons: - operationRadioButton.blockSignals(False) - - segmentSelectionRequired = self.currentOperationRequiresSegmentSelection() - self.applyButton.setEnabled(not segmentSelectionRequired) - if segmentSelectionRequired: - self.applyButton.setToolTip("Click in a slice view to select an island.") - else: - self.applyButton.setToolTip("") - - # TODO: this call has no effect now - # qSlicerSegmentEditorAbstractEffect should be improved so that it triggers a cursor update - # self.scriptedEffect.showEffectCursorInSliceView = segmentSelectionRequired - - showMinimumSizeOption = (operationName in [KEEP_LARGEST_ISLAND, REMOVE_SMALL_ISLANDS, SPLIT_ISLANDS_TO_SEGMENTS]) - self.minimumSizeSpinBox.setEnabled(showMinimumSizeOption) - self.minimumSizeLabel.setEnabled(showMinimumSizeOption) - - self.minimumSizeSpinBox.blockSignals(True) - self.minimumSizeSpinBox.value = self.scriptedEffect.integerParameter("MinimumSize") - self.minimumSizeSpinBox.blockSignals(False) - - def updateMRMLFromGUI(self): - # Operation is managed separately - self.scriptedEffect.setParameter("MinimumSize", self.minimumSizeSpinBox.value) + + def processViewNodeEvents(self, callerViewNode, eventId, viewWidget): + pass # For the sake of example + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("Operation", KEEP_LARGEST_ISLAND) + self.scriptedEffect.setParameterDefault("MinimumSize", 1000) + + def updateGUIFromMRML(self): + for operationRadioButton in self.operationRadioButtons: + operationRadioButton.blockSignals(True) + operationName = self.scriptedEffect.parameter("Operation") + currentOperationRadioButton = list(self.widgetToOperationNameMap.keys())[list(self.widgetToOperationNameMap.values()).index(operationName)] + currentOperationRadioButton.setChecked(True) + for operationRadioButton in self.operationRadioButtons: + operationRadioButton.blockSignals(False) + + segmentSelectionRequired = self.currentOperationRequiresSegmentSelection() + self.applyButton.setEnabled(not segmentSelectionRequired) + if segmentSelectionRequired: + self.applyButton.setToolTip("Click in a slice view to select an island.") + else: + self.applyButton.setToolTip("") + + # TODO: this call has no effect now + # qSlicerSegmentEditorAbstractEffect should be improved so that it triggers a cursor update + # self.scriptedEffect.showEffectCursorInSliceView = segmentSelectionRequired + + showMinimumSizeOption = (operationName in [KEEP_LARGEST_ISLAND, REMOVE_SMALL_ISLANDS, SPLIT_ISLANDS_TO_SEGMENTS]) + self.minimumSizeSpinBox.setEnabled(showMinimumSizeOption) + self.minimumSizeLabel.setEnabled(showMinimumSizeOption) + + self.minimumSizeSpinBox.blockSignals(True) + self.minimumSizeSpinBox.value = self.scriptedEffect.integerParameter("MinimumSize") + self.minimumSizeSpinBox.blockSignals(False) + + def updateMRMLFromGUI(self): + # Operation is managed separately + self.scriptedEffect.setParameter("MinimumSize", self.minimumSizeSpinBox.value) KEEP_LARGEST_ISLAND = 'KEEP_LARGEST_ISLAND' diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLevelTracingEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLevelTracingEffect.py index 6f0912cfc4a..8d7ca9161cd 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLevelTracingEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLevelTracingEffect.py @@ -11,232 +11,232 @@ class SegmentEditorLevelTracingEffect(AbstractScriptedSegmentEditorLabelEffect): - """ LevelTracingEffect is a LabelEffect implementing level tracing fill - using intensity-based isolines - """ - - def __init__(self, scriptedEffect): - scriptedEffect.name = 'Level tracing' - AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect) - - # Effect-specific members - self.levelTracingPipelines = {} - self.lastXY = None - - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/LevelTracing.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Add uniform intensity region to selected segment
      . + """ LevelTracingEffect is a LabelEffect implementing level tracing fill + using intensity-based isolines + """ + + def __init__(self, scriptedEffect): + scriptedEffect.name = 'Level tracing' + AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect) + + # Effect-specific members + self.levelTracingPipelines = {} + self.lastXY = None + + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/LevelTracing.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Add uniform intensity region to selected segment
      .

      • Mouse move: current background voxel is used to find a closed path that follows the same intensity value back to the starting point within the current slice.
      • Left-click: add the previewed region to the current segment.

      """ - def setupOptionsFrame(self): - self.sliceRotatedErrorLabel = qt.QLabel() - # This widget displays an error message if the slice view is not aligned - # with the segmentation's axes. - self.scriptedEffect.addOptionsWidget(self.sliceRotatedErrorLabel) - - def activate(self): - self.sliceRotatedErrorLabel.text = "" - - def deactivate(self): - # Clear draw pipelines - for sliceWidget, pipeline in self.levelTracingPipelines.items(): - self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor) - self.levelTracingPipelines = {} - self.lastXY = None - - def processInteractionEvents(self, callerInteractor, eventId, viewWidget): - abortEvent = False - - # Only allow for slice views - if viewWidget.className() != "qMRMLSliceWidget": - return abortEvent - # Get draw pipeline for current slice - pipeline = self.pipelineForWidget(viewWidget) - if pipeline is None: - return abortEvent - - anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey() - - if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed: - # Make sure the user wants to do the operation, even if the segment is not visible - if not self.scriptedEffect.confirmCurrentSegmentVisible(): + def setupOptionsFrame(self): + self.sliceRotatedErrorLabel = qt.QLabel() + # This widget displays an error message if the slice view is not aligned + # with the segmentation's axes. + self.scriptedEffect.addOptionsWidget(self.sliceRotatedErrorLabel) + + def activate(self): + self.sliceRotatedErrorLabel.text = "" + + def deactivate(self): + # Clear draw pipelines + for sliceWidget, pipeline in self.levelTracingPipelines.items(): + self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor) + self.levelTracingPipelines = {} + self.lastXY = None + + def processInteractionEvents(self, callerInteractor, eventId, viewWidget): + abortEvent = False + + # Only allow for slice views + if viewWidget.className() != "qMRMLSliceWidget": + return abortEvent + # Get draw pipeline for current slice + pipeline = self.pipelineForWidget(viewWidget) + if pipeline is None: + return abortEvent + + anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey() + + if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed: + # Make sure the user wants to do the operation, even if the segment is not visible + if not self.scriptedEffect.confirmCurrentSegmentVisible(): + return abortEvent + + self.scriptedEffect.saveStateForUndo() + + # Get modifier labelmap + modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() + + # Apply poly data on modifier labelmap + pipeline.appendPolyMask(modifierLabelmap) + # TODO: it would be nice to reduce extent + self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd) + abortEvent = True + elif eventId == vtk.vtkCommand.MouseMoveEvent: + if pipeline.actionState == '': + xy = callerInteractor.GetEventPosition() + if pipeline.preview(xy): + self.sliceRotatedErrorLabel.text = "" + else: + self.sliceRotatedErrorLabel.text = ("" + + "Slice view is not aligned with segmentation axis.
      To use this effect, click the 'Slice views orientation' warning button." + + "
      ") + abortEvent = True + self.lastXY = xy + elif eventId == vtk.vtkCommand.EnterEvent: + self.sliceRotatedErrorLabel.text = "" + pipeline.actor.VisibilityOn() + elif eventId == vtk.vtkCommand.LeaveEvent: + self.sliceRotatedErrorLabel.text = "" + pipeline.actor.VisibilityOff() + self.lastXY = None + return abortEvent - self.scriptedEffect.saveStateForUndo() - - # Get modifier labelmap - modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() - - # Apply poly data on modifier labelmap - pipeline.appendPolyMask(modifierLabelmap) - # TODO: it would be nice to reduce extent - self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd) - abortEvent = True - elif eventId == vtk.vtkCommand.MouseMoveEvent: - if pipeline.actionState == '': - xy = callerInteractor.GetEventPosition() - if pipeline.preview(xy): - self.sliceRotatedErrorLabel.text = "" - else: - self.sliceRotatedErrorLabel.text = ("" - + "Slice view is not aligned with segmentation axis.
      To use this effect, click the 'Slice views orientation' warning button." - + "
      ") - abortEvent = True - self.lastXY = xy - elif eventId == vtk.vtkCommand.EnterEvent: - self.sliceRotatedErrorLabel.text = "" - pipeline.actor.VisibilityOn() - elif eventId == vtk.vtkCommand.LeaveEvent: - self.sliceRotatedErrorLabel.text = "" - pipeline.actor.VisibilityOff() - self.lastXY = None - - return abortEvent - - def processViewNodeEvents(self, callerViewNode, eventId, viewWidget): - if callerViewNode and callerViewNode.IsA('vtkMRMLSliceNode'): - # Get draw pipeline for current slice - pipeline = self.pipelineForWidget(viewWidget) - if pipeline is None: - logging.error('processViewNodeEvents: Invalid pipeline') - return - - # Update the preview to the new slice - if pipeline.actionState == '' and self.lastXY: - pipeline.preview(self.lastXY) - - def pipelineForWidget(self, sliceWidget): - if sliceWidget in self.levelTracingPipelines: - return self.levelTracingPipelines[sliceWidget] - - # Create pipeline if does not yet exist - pipeline = LevelTracingPipeline(self, sliceWidget) - - # Add actor - renderer = self.scriptedEffect.renderer(sliceWidget) - if renderer is None: - logging.error("setupPreviewDisplay: Failed to get renderer!") - return None - self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor) - - self.levelTracingPipelines[sliceWidget] = pipeline - return pipeline + def processViewNodeEvents(self, callerViewNode, eventId, viewWidget): + if callerViewNode and callerViewNode.IsA('vtkMRMLSliceNode'): + # Get draw pipeline for current slice + pipeline = self.pipelineForWidget(viewWidget) + if pipeline is None: + logging.error('processViewNodeEvents: Invalid pipeline') + return + + # Update the preview to the new slice + if pipeline.actionState == '' and self.lastXY: + pipeline.preview(self.lastXY) + + def pipelineForWidget(self, sliceWidget): + if sliceWidget in self.levelTracingPipelines: + return self.levelTracingPipelines[sliceWidget] + + # Create pipeline if does not yet exist + pipeline = LevelTracingPipeline(self, sliceWidget) + + # Add actor + renderer = self.scriptedEffect.renderer(sliceWidget) + if renderer is None: + logging.error("setupPreviewDisplay: Failed to get renderer!") + return None + self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor) + + self.levelTracingPipelines[sliceWidget] = pipeline + return pipeline # # LevelTracingPipeline # class LevelTracingPipeline: - """ Visualization objects and pipeline for each slice view for level tracing - """ - - def __init__(self, effect, sliceWidget): - self.effect = effect - self.sliceWidget = sliceWidget - self.actionState = '' - - self.xyPoints = vtk.vtkPoints() - self.rasPoints = vtk.vtkPoints() - self.polyData = vtk.vtkPolyData() - - self.tracingFilter = vtkITK.vtkITKLevelTracingImageFilter() - self.ijkToXY = vtk.vtkGeneralTransform() - - self.mapper = vtk.vtkPolyDataMapper2D() - self.actor = vtk.vtkActor2D() - actorProperty = self.actor.GetProperty() - actorProperty.SetColor( 107/255., 190/255., 99/255. ) - actorProperty.SetLineWidth( 1 ) - self.mapper.SetInputData(self.polyData) - self.actor.SetMapper(self.mapper) - actorProperty = self.actor.GetProperty() - actorProperty.SetColor(1,1,0) - actorProperty.SetLineWidth(1) - - def preview(self,xy): - """Calculate the current level trace view if the mouse is inside the volume extent - Returns False if slice views are rotated. + """ Visualization objects and pipeline for each slice view for level tracing """ - # Get master volume image data - masterImageData = self.effect.scriptedEffect.masterVolumeImageData() - - segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode() - parentTransformNode = None - if segmentationNode: - parentTransformNode = segmentationNode.GetParentTransformNode() - - self.xyPoints.Reset() - ijk = self.effect.xyToIjk(xy, self.sliceWidget, masterImageData, parentTransformNode) - dimensions = masterImageData.GetDimensions() - - self.tracingFilter.SetInputData(masterImageData) - self.tracingFilter.SetSeed(ijk) - - # Select the plane corresponding to current slice orientation - # for the input volume - sliceNode = self.effect.scriptedEffect.viewNode(self.sliceWidget) - offset = max(sliceNode.GetDimensions()) - - i0,j0,k0 = self.effect.xyToIjk((0,0), self.sliceWidget, masterImageData, parentTransformNode) - i1,j1,k1 = self.effect.xyToIjk((offset,offset), self.sliceWidget, masterImageData, parentTransformNode) - if i0 == i1: - self.tracingFilter.SetPlaneToJK() - elif j0 == j1: - self.tracingFilter.SetPlaneToIK() - elif k0 == k1: - self.tracingFilter.SetPlaneToIJ() - else: - self.polyData.Reset() - self.sliceWidget.sliceView().scheduleRender() - return False - - self.tracingFilter.Update() - polyData = self.tracingFilter.GetOutput() - - # Get master volume IJK to slice XY transform - xyToRas = sliceNode.GetXYToRAS() - rasToIjk = vtk.vtkMatrix4x4() - masterImageData.GetImageToWorldMatrix(rasToIjk) - rasToIjk.Invert() - xyToIjk = vtk.vtkGeneralTransform() - xyToIjk.PostMultiply() - xyToIjk.Concatenate(xyToRas) - if parentTransformNode: - worldToSegmentation = vtk.vtkMatrix4x4() - parentTransformNode.GetMatrixTransformFromWorld(worldToSegmentation) - xyToIjk.Concatenate(worldToSegmentation) - xyToIjk.Concatenate(rasToIjk) - ijkToXy = xyToIjk.GetInverse() - if polyData.GetPoints(): - ijkToXy.TransformPoints(polyData.GetPoints(), self.xyPoints) - self.polyData.DeepCopy(polyData) - self.polyData.GetPoints().DeepCopy(self.xyPoints) - else: - self.polyData.Reset() - self.sliceWidget.sliceView().scheduleRender() - return True - - def appendPolyMask(self, modifierLabelmap): - lines = self.polyData.GetLines() - if lines.GetNumberOfCells() == 0: - return - - # Apply poly data on modifier labelmap - segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode() - self.effect.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode) + def __init__(self, effect, sliceWidget): + self.effect = effect + self.sliceWidget = sliceWidget + self.actionState = '' + + self.xyPoints = vtk.vtkPoints() + self.rasPoints = vtk.vtkPoints() + self.polyData = vtk.vtkPolyData() + + self.tracingFilter = vtkITK.vtkITKLevelTracingImageFilter() + self.ijkToXY = vtk.vtkGeneralTransform() + + self.mapper = vtk.vtkPolyDataMapper2D() + self.actor = vtk.vtkActor2D() + actorProperty = self.actor.GetProperty() + actorProperty.SetColor(107 / 255., 190 / 255., 99 / 255.) + actorProperty.SetLineWidth(1) + self.mapper.SetInputData(self.polyData) + self.actor.SetMapper(self.mapper) + actorProperty = self.actor.GetProperty() + actorProperty.SetColor(1, 1, 0) + actorProperty.SetLineWidth(1) + + def preview(self, xy): + """Calculate the current level trace view if the mouse is inside the volume extent + Returns False if slice views are rotated. + """ + + # Get master volume image data + masterImageData = self.effect.scriptedEffect.masterVolumeImageData() + + segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode() + parentTransformNode = None + if segmentationNode: + parentTransformNode = segmentationNode.GetParentTransformNode() + + self.xyPoints.Reset() + ijk = self.effect.xyToIjk(xy, self.sliceWidget, masterImageData, parentTransformNode) + dimensions = masterImageData.GetDimensions() + + self.tracingFilter.SetInputData(masterImageData) + self.tracingFilter.SetSeed(ijk) + + # Select the plane corresponding to current slice orientation + # for the input volume + sliceNode = self.effect.scriptedEffect.viewNode(self.sliceWidget) + offset = max(sliceNode.GetDimensions()) + + i0, j0, k0 = self.effect.xyToIjk((0, 0), self.sliceWidget, masterImageData, parentTransformNode) + i1, j1, k1 = self.effect.xyToIjk((offset, offset), self.sliceWidget, masterImageData, parentTransformNode) + if i0 == i1: + self.tracingFilter.SetPlaneToJK() + elif j0 == j1: + self.tracingFilter.SetPlaneToIK() + elif k0 == k1: + self.tracingFilter.SetPlaneToIJ() + else: + self.polyData.Reset() + self.sliceWidget.sliceView().scheduleRender() + return False + + self.tracingFilter.Update() + polyData = self.tracingFilter.GetOutput() + + # Get master volume IJK to slice XY transform + xyToRas = sliceNode.GetXYToRAS() + rasToIjk = vtk.vtkMatrix4x4() + masterImageData.GetImageToWorldMatrix(rasToIjk) + rasToIjk.Invert() + xyToIjk = vtk.vtkGeneralTransform() + xyToIjk.PostMultiply() + xyToIjk.Concatenate(xyToRas) + if parentTransformNode: + worldToSegmentation = vtk.vtkMatrix4x4() + parentTransformNode.GetMatrixTransformFromWorld(worldToSegmentation) + xyToIjk.Concatenate(worldToSegmentation) + xyToIjk.Concatenate(rasToIjk) + ijkToXy = xyToIjk.GetInverse() + if polyData.GetPoints(): + ijkToXy.TransformPoints(polyData.GetPoints(), self.xyPoints) + self.polyData.DeepCopy(polyData) + self.polyData.GetPoints().DeepCopy(self.xyPoints) + else: + self.polyData.Reset() + self.sliceWidget.sliceView().scheduleRender() + return True + + def appendPolyMask(self, modifierLabelmap): + lines = self.polyData.GetLines() + if lines.GetNumberOfCells() == 0: + return + + # Apply poly data on modifier labelmap + segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode() + self.effect.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode) diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLogicalEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLogicalEffect.py index 383853527b9..b0901ddd74f 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLogicalEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLogicalEffect.py @@ -10,28 +10,28 @@ class SegmentEditorLogicalEffect(AbstractScriptedSegmentEditorEffect): - """ LogicalEffect is an MorphologyEffect to erode a layer of pixels from a segment - """ - - def __init__(self, scriptedEffect): - scriptedEffect.name = 'Logical operators' - self.operationsRequireModifierSegment = [LOGICAL_COPY, LOGICAL_UNION, LOGICAL_SUBTRACT, LOGICAL_INTERSECT] - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) - - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Logical.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Apply logical operators or combine segments
      . Available operations:

      + """ LogicalEffect is an MorphologyEffect to erode a layer of pixels from a segment + """ + + def __init__(self, scriptedEffect): + scriptedEffect.name = 'Logical operators' + self.operationsRequireModifierSegment = [LOGICAL_COPY, LOGICAL_UNION, LOGICAL_SUBTRACT, LOGICAL_INTERSECT] + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Logical.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Apply logical operators or combine segments
      . Available operations:

      • Copy: replace the selected segment by the modifier segment.
      • Add: add modifier segment to current segment.
      • @@ -44,231 +44,231 @@ def helpText(self): Selected segment: segment selected in the segment list - above. Modifier segment: segment chosen in segment list in effect options - below.

        """ - def setupOptionsFrame(self): - - self.methodSelectorComboBox = qt.QComboBox() - self.methodSelectorComboBox.addItem("Copy", LOGICAL_COPY) - self.methodSelectorComboBox.addItem("Add", LOGICAL_UNION) - self.methodSelectorComboBox.addItem("Subtract", LOGICAL_SUBTRACT) - self.methodSelectorComboBox.addItem("Intersect", LOGICAL_INTERSECT) - self.methodSelectorComboBox.addItem("Invert", LOGICAL_INVERT) - self.methodSelectorComboBox.addItem("Clear", LOGICAL_CLEAR) - self.methodSelectorComboBox.addItem("Fill", LOGICAL_FILL) - self.methodSelectorComboBox.setToolTip('Click Show details link above for description of operations.') - - self.bypassMaskingCheckBox = qt.QCheckBox("Bypass masking") - self.bypassMaskingCheckBox.setToolTip("Ignore all masking options and only modify the selected segment.") - self.bypassMaskingCheckBox.objectName = self.__class__.__name__ + 'BypassMasking' - - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - - operationFrame = qt.QHBoxLayout() - operationFrame.addWidget(self.methodSelectorComboBox) - operationFrame.addWidget(self.applyButton) - operationFrame.addWidget(self.bypassMaskingCheckBox) - self.marginSizeMmLabel = self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationFrame) - - self.modifierSegmentSelectorLabel = qt.QLabel("Modifier segment:") - self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelectorLabel) - - self.modifierSegmentSelector = slicer.qMRMLSegmentsTableView() - self.modifierSegmentSelector.selectionMode = qt.QAbstractItemView.SingleSelection - self.modifierSegmentSelector.headerVisible = False - self.modifierSegmentSelector.visibilityColumnVisible = False - self.modifierSegmentSelector.opacityColumnVisible = False - - self.modifierSegmentSelector.setMRMLScene(slicer.mrmlScene) - self.modifierSegmentSelector.setToolTip('Contents of this segment will be used for modifying the selected segment. This segment itself will not be changed.') - self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelector) - - self.applyButton.connect('clicked()', self.onApply) - self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI) - self.modifierSegmentSelector.connect("selectionChanged(QItemSelection, QItemSelection)", self.updateMRMLFromGUI) - self.bypassMaskingCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) - - def createCursor(self, widget): - # Turn off effect-specific cursor for this effect - return slicer.util.mainWindow().cursor - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("Operation", LOGICAL_COPY) - self.scriptedEffect.setParameterDefault("ModifierSegmentID", "") - self.scriptedEffect.setParameterDefault("BypassMasking", 1) - - def modifierSegmentID(self): - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - if not segmentationNode: - return "" - if not self.scriptedEffect.parameterDefined("ModifierSegmentID"): - # Avoid logging warning - return "" - modifierSegmentIDs = self.scriptedEffect.parameter("ModifierSegmentID").split(';') - if not modifierSegmentIDs: - return "" - return modifierSegmentIDs[0] - - def updateGUIFromMRML(self): - operation = self.scriptedEffect.parameter("Operation") - operationIndex = self.methodSelectorComboBox.findData(operation) - wasBlocked = self.methodSelectorComboBox.blockSignals(True) - self.methodSelectorComboBox.setCurrentIndex(operationIndex) - self.methodSelectorComboBox.blockSignals(wasBlocked) - - modifierSegmentID = self.modifierSegmentID() - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - wasBlocked = self.modifierSegmentSelector.blockSignals(True) - self.modifierSegmentSelector.setSegmentationNode(segmentationNode) - self.modifierSegmentSelector.setSelectedSegmentIDs([modifierSegmentID]) - self.modifierSegmentSelector.blockSignals(wasBlocked) - - modifierSegmentRequired = (operation in self.operationsRequireModifierSegment) - self.modifierSegmentSelectorLabel.setVisible(modifierSegmentRequired) - self.modifierSegmentSelector.setVisible(modifierSegmentRequired) - - if operation == LOGICAL_COPY: - self.modifierSegmentSelectorLabel.text = "Copy from segment:" - elif operation == LOGICAL_UNION: - self.modifierSegmentSelectorLabel.text = "Add segment:" - elif operation == LOGICAL_SUBTRACT: - self.modifierSegmentSelectorLabel.text = "Subtract segment:" - elif operation == LOGICAL_INTERSECT: - self.modifierSegmentSelectorLabel.text = "Intersect with segment:" - else: - self.modifierSegmentSelectorLabel.text = "Modifier segment:" - - if modifierSegmentRequired and not modifierSegmentID: - self.applyButton.setToolTip("Please select a modifier segment in the list below.") - self.applyButton.enabled = False - else: - self.applyButton.setToolTip("") - self.applyButton.enabled = True - - bypassMasking = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("BypassMasking") == 0 else qt.Qt.Checked - wasBlocked = self.bypassMaskingCheckBox.blockSignals(True) - self.bypassMaskingCheckBox.setCheckState(bypassMasking) - self.bypassMaskingCheckBox.blockSignals(wasBlocked) - - def updateMRMLFromGUI(self): - operationIndex = self.methodSelectorComboBox.currentIndex - operation = self.methodSelectorComboBox.itemData(operationIndex) - self.scriptedEffect.setParameter("Operation", operation) - - bypassMasking = 1 if self.bypassMaskingCheckBox.isChecked() else 0 - self.scriptedEffect.setParameter("BypassMasking", bypassMasking) - - modifierSegmentIDs = ';'.join(self.modifierSegmentSelector.selectedSegmentIDs()) # semicolon-separated list of segment IDs - self.scriptedEffect.setParameter("ModifierSegmentID", modifierSegmentIDs) - - def getInvertedBinaryLabelmap(self, modifierLabelmap): - fillValue = 1 - eraseValue = 0 - inverter = vtk.vtkImageThreshold() - inverter.SetInputData(modifierLabelmap) - inverter.SetInValue(fillValue) - inverter.SetOutValue(eraseValue) - inverter.ReplaceInOn() - inverter.ThresholdByLower(0) - inverter.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) - inverter.Update() - - invertedModifierLabelmap = slicer.vtkOrientedImageData() - invertedModifierLabelmap.ShallowCopy(inverter.GetOutput()) - imageToWorldMatrix = vtk.vtkMatrix4x4() - modifierLabelmap.GetImageToWorldMatrix(imageToWorldMatrix) - invertedModifierLabelmap.SetGeometryFromImageToWorldMatrix(imageToWorldMatrix) - return invertedModifierLabelmap - - def onApply(self): - # Make sure the user wants to do the operation, even if the segment is not visible - if not self.scriptedEffect.confirmCurrentSegmentVisible(): - return - - import vtkSegmentationCorePython as vtkSegmentationCore - - self.scriptedEffect.saveStateForUndo() - - # Get modifier labelmap and parameters - - operation = self.scriptedEffect.parameter("Operation") - bypassMasking = (self.scriptedEffect.integerParameter("BypassMasking") != 0) - - selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() - - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - segmentation = segmentationNode.GetSegmentation() - - if operation in self.operationsRequireModifierSegment: - - # Get modifier segment - modifierSegmentID = self.modifierSegmentID() - if not modifierSegmentID: - logging.error(f"Operation {operation} requires a selected modifier segment") - return - modifierSegment = segmentation.GetSegment(modifierSegmentID) - modifierSegmentLabelmap = slicer.vtkOrientedImageData() - segmentationNode.GetBinaryLabelmapRepresentation(modifierSegmentID, modifierSegmentLabelmap) - - # Get common geometry - commonGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry( - vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS, None) - if not commonGeometryString: - logging.info("Logical operation skipped: all segments are empty") - return - commonGeometryImage = slicer.vtkOrientedImageData() - vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(commonGeometryString, commonGeometryImage, False) - - # Make sure modifier segment has correct geometry - # (if modifier segment has been just copied over from another segment then its geometry may be different) - if not vtkSegmentationCore.vtkOrientedImageDataResample.DoGeometriesMatch(commonGeometryImage, modifierSegmentLabelmap): - modifierSegmentLabelmap_CommonGeometry = slicer.vtkOrientedImageData() - vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - modifierSegmentLabelmap, commonGeometryImage, modifierSegmentLabelmap_CommonGeometry, - False, # nearest neighbor interpolation, - True # make sure resampled modifier segment is not cropped - ) - modifierSegmentLabelmap = modifierSegmentLabelmap_CommonGeometry - - if operation == LOGICAL_COPY: - self.scriptedEffect.modifySelectedSegmentByLabelmap( - modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking) - elif operation == LOGICAL_UNION: - self.scriptedEffect.modifySelectedSegmentByLabelmap( - modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd, bypassMasking) - elif operation == LOGICAL_SUBTRACT: - self.scriptedEffect.modifySelectedSegmentByLabelmap( - modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove, bypassMasking) - elif operation == LOGICAL_INTERSECT: - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - intersectionLabelmap = slicer.vtkOrientedImageData() - vtkSegmentationCore.vtkOrientedImageDataResample.MergeImage( - selectedSegmentLabelmap, modifierSegmentLabelmap, intersectionLabelmap, - vtkSegmentationCore.vtkOrientedImageDataResample.OPERATION_MINIMUM, selectedSegmentLabelmap.GetExtent()) - selectedSegmentLabelmapExtent = selectedSegmentLabelmap.GetExtent() - modifierSegmentLabelmapExtent = modifierSegmentLabelmap.GetExtent() - commonExtent = [max(selectedSegmentLabelmapExtent[0], modifierSegmentLabelmapExtent[0]), - min(selectedSegmentLabelmapExtent[1], modifierSegmentLabelmapExtent[1]), - max(selectedSegmentLabelmapExtent[2], modifierSegmentLabelmapExtent[2]), - min(selectedSegmentLabelmapExtent[3], modifierSegmentLabelmapExtent[3]), - max(selectedSegmentLabelmapExtent[4], modifierSegmentLabelmapExtent[4]), - min(selectedSegmentLabelmapExtent[5], modifierSegmentLabelmapExtent[5])] - self.scriptedEffect.modifySelectedSegmentByLabelmap( - intersectionLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, commonExtent, bypassMasking) - - elif operation == LOGICAL_INVERT: - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - invertedSelectedSegmentLabelmap = self.getInvertedBinaryLabelmap(selectedSegmentLabelmap) - self.scriptedEffect.modifySelectedSegmentByLabelmap( - invertedSelectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking) - - elif operation == LOGICAL_CLEAR or operation == LOGICAL_FILL: - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(selectedSegmentLabelmap, 1 if operation == LOGICAL_FILL else 0, selectedSegmentLabelmap.GetExtent()) - self.scriptedEffect.modifySelectedSegmentByLabelmap( - selectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking) - - else: - logging.error(f"Unknown operation: {operation}") + def setupOptionsFrame(self): + + self.methodSelectorComboBox = qt.QComboBox() + self.methodSelectorComboBox.addItem("Copy", LOGICAL_COPY) + self.methodSelectorComboBox.addItem("Add", LOGICAL_UNION) + self.methodSelectorComboBox.addItem("Subtract", LOGICAL_SUBTRACT) + self.methodSelectorComboBox.addItem("Intersect", LOGICAL_INTERSECT) + self.methodSelectorComboBox.addItem("Invert", LOGICAL_INVERT) + self.methodSelectorComboBox.addItem("Clear", LOGICAL_CLEAR) + self.methodSelectorComboBox.addItem("Fill", LOGICAL_FILL) + self.methodSelectorComboBox.setToolTip('Click Show details link above for description of operations.') + + self.bypassMaskingCheckBox = qt.QCheckBox("Bypass masking") + self.bypassMaskingCheckBox.setToolTip("Ignore all masking options and only modify the selected segment.") + self.bypassMaskingCheckBox.objectName = self.__class__.__name__ + 'BypassMasking' + + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + + operationFrame = qt.QHBoxLayout() + operationFrame.addWidget(self.methodSelectorComboBox) + operationFrame.addWidget(self.applyButton) + operationFrame.addWidget(self.bypassMaskingCheckBox) + self.marginSizeMmLabel = self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationFrame) + + self.modifierSegmentSelectorLabel = qt.QLabel("Modifier segment:") + self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelectorLabel) + + self.modifierSegmentSelector = slicer.qMRMLSegmentsTableView() + self.modifierSegmentSelector.selectionMode = qt.QAbstractItemView.SingleSelection + self.modifierSegmentSelector.headerVisible = False + self.modifierSegmentSelector.visibilityColumnVisible = False + self.modifierSegmentSelector.opacityColumnVisible = False + + self.modifierSegmentSelector.setMRMLScene(slicer.mrmlScene) + self.modifierSegmentSelector.setToolTip('Contents of this segment will be used for modifying the selected segment. This segment itself will not be changed.') + self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelector) + + self.applyButton.connect('clicked()', self.onApply) + self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI) + self.modifierSegmentSelector.connect("selectionChanged(QItemSelection, QItemSelection)", self.updateMRMLFromGUI) + self.bypassMaskingCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) + + def createCursor(self, widget): + # Turn off effect-specific cursor for this effect + return slicer.util.mainWindow().cursor + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("Operation", LOGICAL_COPY) + self.scriptedEffect.setParameterDefault("ModifierSegmentID", "") + self.scriptedEffect.setParameterDefault("BypassMasking", 1) + + def modifierSegmentID(self): + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + if not segmentationNode: + return "" + if not self.scriptedEffect.parameterDefined("ModifierSegmentID"): + # Avoid logging warning + return "" + modifierSegmentIDs = self.scriptedEffect.parameter("ModifierSegmentID").split(';') + if not modifierSegmentIDs: + return "" + return modifierSegmentIDs[0] + + def updateGUIFromMRML(self): + operation = self.scriptedEffect.parameter("Operation") + operationIndex = self.methodSelectorComboBox.findData(operation) + wasBlocked = self.methodSelectorComboBox.blockSignals(True) + self.methodSelectorComboBox.setCurrentIndex(operationIndex) + self.methodSelectorComboBox.blockSignals(wasBlocked) + + modifierSegmentID = self.modifierSegmentID() + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + wasBlocked = self.modifierSegmentSelector.blockSignals(True) + self.modifierSegmentSelector.setSegmentationNode(segmentationNode) + self.modifierSegmentSelector.setSelectedSegmentIDs([modifierSegmentID]) + self.modifierSegmentSelector.blockSignals(wasBlocked) + + modifierSegmentRequired = (operation in self.operationsRequireModifierSegment) + self.modifierSegmentSelectorLabel.setVisible(modifierSegmentRequired) + self.modifierSegmentSelector.setVisible(modifierSegmentRequired) + + if operation == LOGICAL_COPY: + self.modifierSegmentSelectorLabel.text = "Copy from segment:" + elif operation == LOGICAL_UNION: + self.modifierSegmentSelectorLabel.text = "Add segment:" + elif operation == LOGICAL_SUBTRACT: + self.modifierSegmentSelectorLabel.text = "Subtract segment:" + elif operation == LOGICAL_INTERSECT: + self.modifierSegmentSelectorLabel.text = "Intersect with segment:" + else: + self.modifierSegmentSelectorLabel.text = "Modifier segment:" + + if modifierSegmentRequired and not modifierSegmentID: + self.applyButton.setToolTip("Please select a modifier segment in the list below.") + self.applyButton.enabled = False + else: + self.applyButton.setToolTip("") + self.applyButton.enabled = True + + bypassMasking = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("BypassMasking") == 0 else qt.Qt.Checked + wasBlocked = self.bypassMaskingCheckBox.blockSignals(True) + self.bypassMaskingCheckBox.setCheckState(bypassMasking) + self.bypassMaskingCheckBox.blockSignals(wasBlocked) + + def updateMRMLFromGUI(self): + operationIndex = self.methodSelectorComboBox.currentIndex + operation = self.methodSelectorComboBox.itemData(operationIndex) + self.scriptedEffect.setParameter("Operation", operation) + + bypassMasking = 1 if self.bypassMaskingCheckBox.isChecked() else 0 + self.scriptedEffect.setParameter("BypassMasking", bypassMasking) + + modifierSegmentIDs = ';'.join(self.modifierSegmentSelector.selectedSegmentIDs()) # semicolon-separated list of segment IDs + self.scriptedEffect.setParameter("ModifierSegmentID", modifierSegmentIDs) + + def getInvertedBinaryLabelmap(self, modifierLabelmap): + fillValue = 1 + eraseValue = 0 + inverter = vtk.vtkImageThreshold() + inverter.SetInputData(modifierLabelmap) + inverter.SetInValue(fillValue) + inverter.SetOutValue(eraseValue) + inverter.ReplaceInOn() + inverter.ThresholdByLower(0) + inverter.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) + inverter.Update() + + invertedModifierLabelmap = slicer.vtkOrientedImageData() + invertedModifierLabelmap.ShallowCopy(inverter.GetOutput()) + imageToWorldMatrix = vtk.vtkMatrix4x4() + modifierLabelmap.GetImageToWorldMatrix(imageToWorldMatrix) + invertedModifierLabelmap.SetGeometryFromImageToWorldMatrix(imageToWorldMatrix) + return invertedModifierLabelmap + + def onApply(self): + # Make sure the user wants to do the operation, even if the segment is not visible + if not self.scriptedEffect.confirmCurrentSegmentVisible(): + return + + import vtkSegmentationCorePython as vtkSegmentationCore + + self.scriptedEffect.saveStateForUndo() + + # Get modifier labelmap and parameters + + operation = self.scriptedEffect.parameter("Operation") + bypassMasking = (self.scriptedEffect.integerParameter("BypassMasking") != 0) + + selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() + + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + segmentation = segmentationNode.GetSegmentation() + + if operation in self.operationsRequireModifierSegment: + + # Get modifier segment + modifierSegmentID = self.modifierSegmentID() + if not modifierSegmentID: + logging.error(f"Operation {operation} requires a selected modifier segment") + return + modifierSegment = segmentation.GetSegment(modifierSegmentID) + modifierSegmentLabelmap = slicer.vtkOrientedImageData() + segmentationNode.GetBinaryLabelmapRepresentation(modifierSegmentID, modifierSegmentLabelmap) + + # Get common geometry + commonGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry( + vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS, None) + if not commonGeometryString: + logging.info("Logical operation skipped: all segments are empty") + return + commonGeometryImage = slicer.vtkOrientedImageData() + vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(commonGeometryString, commonGeometryImage, False) + + # Make sure modifier segment has correct geometry + # (if modifier segment has been just copied over from another segment then its geometry may be different) + if not vtkSegmentationCore.vtkOrientedImageDataResample.DoGeometriesMatch(commonGeometryImage, modifierSegmentLabelmap): + modifierSegmentLabelmap_CommonGeometry = slicer.vtkOrientedImageData() + vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + modifierSegmentLabelmap, commonGeometryImage, modifierSegmentLabelmap_CommonGeometry, + False, # nearest neighbor interpolation, + True # make sure resampled modifier segment is not cropped + ) + modifierSegmentLabelmap = modifierSegmentLabelmap_CommonGeometry + + if operation == LOGICAL_COPY: + self.scriptedEffect.modifySelectedSegmentByLabelmap( + modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking) + elif operation == LOGICAL_UNION: + self.scriptedEffect.modifySelectedSegmentByLabelmap( + modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd, bypassMasking) + elif operation == LOGICAL_SUBTRACT: + self.scriptedEffect.modifySelectedSegmentByLabelmap( + modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove, bypassMasking) + elif operation == LOGICAL_INTERSECT: + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + intersectionLabelmap = slicer.vtkOrientedImageData() + vtkSegmentationCore.vtkOrientedImageDataResample.MergeImage( + selectedSegmentLabelmap, modifierSegmentLabelmap, intersectionLabelmap, + vtkSegmentationCore.vtkOrientedImageDataResample.OPERATION_MINIMUM, selectedSegmentLabelmap.GetExtent()) + selectedSegmentLabelmapExtent = selectedSegmentLabelmap.GetExtent() + modifierSegmentLabelmapExtent = modifierSegmentLabelmap.GetExtent() + commonExtent = [max(selectedSegmentLabelmapExtent[0], modifierSegmentLabelmapExtent[0]), + min(selectedSegmentLabelmapExtent[1], modifierSegmentLabelmapExtent[1]), + max(selectedSegmentLabelmapExtent[2], modifierSegmentLabelmapExtent[2]), + min(selectedSegmentLabelmapExtent[3], modifierSegmentLabelmapExtent[3]), + max(selectedSegmentLabelmapExtent[4], modifierSegmentLabelmapExtent[4]), + min(selectedSegmentLabelmapExtent[5], modifierSegmentLabelmapExtent[5])] + self.scriptedEffect.modifySelectedSegmentByLabelmap( + intersectionLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, commonExtent, bypassMasking) + + elif operation == LOGICAL_INVERT: + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + invertedSelectedSegmentLabelmap = self.getInvertedBinaryLabelmap(selectedSegmentLabelmap) + self.scriptedEffect.modifySelectedSegmentByLabelmap( + invertedSelectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking) + + elif operation == LOGICAL_CLEAR or operation == LOGICAL_FILL: + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(selectedSegmentLabelmap, 1 if operation == LOGICAL_FILL else 0, selectedSegmentLabelmap.GetExtent()) + self.scriptedEffect.modifySelectedSegmentByLabelmap( + selectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking) + + else: + logging.error(f"Unknown operation: {operation}") LOGICAL_COPY = 'COPY' diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMarginEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMarginEffect.py index 2e1cc134106..f9ec140c25f 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMarginEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMarginEffect.py @@ -11,238 +11,238 @@ class SegmentEditorMarginEffect(AbstractScriptedSegmentEditorEffect): - """ MaringEffect grows or shrinks the segment by a specified margin - """ + """ MaringEffect grows or shrinks the segment by a specified margin + """ - def __init__(self, scriptedEffect): - scriptedEffect.name = 'Margin' - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + def __init__(self, scriptedEffect): + scriptedEffect.name = 'Margin' + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Margin.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Margin.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() - def helpText(self): - return "Grow or shrink selected segment by specified margin size." + def helpText(self): + return "Grow or shrink selected segment by specified margin size." - def setupOptionsFrame(self): + def setupOptionsFrame(self): - operationLayout = qt.QVBoxLayout() + operationLayout = qt.QVBoxLayout() - self.shrinkOptionRadioButton = qt.QRadioButton("Shrink") - self.growOptionRadioButton = qt.QRadioButton("Grow") - operationLayout.addWidget(self.shrinkOptionRadioButton) - operationLayout.addWidget(self.growOptionRadioButton) - self.growOptionRadioButton.setChecked(True) + self.shrinkOptionRadioButton = qt.QRadioButton("Shrink") + self.growOptionRadioButton = qt.QRadioButton("Grow") + operationLayout.addWidget(self.shrinkOptionRadioButton) + operationLayout.addWidget(self.growOptionRadioButton) + self.growOptionRadioButton.setChecked(True) - self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout) + self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout) - self.marginSizeMMSpinBox = slicer.qMRMLSpinBox() - self.marginSizeMMSpinBox.setMRMLScene(slicer.mrmlScene) - self.marginSizeMMSpinBox.setToolTip("Segment boundaries will be shifted by this distance. Positive value means the segments will grow, negative value means segment will shrink.") - self.marginSizeMMSpinBox.quantity = "length" - self.marginSizeMMSpinBox.value = 3.0 - self.marginSizeMMSpinBox.singleStep = 1.0 + self.marginSizeMMSpinBox = slicer.qMRMLSpinBox() + self.marginSizeMMSpinBox.setMRMLScene(slicer.mrmlScene) + self.marginSizeMMSpinBox.setToolTip("Segment boundaries will be shifted by this distance. Positive value means the segments will grow, negative value means segment will shrink.") + self.marginSizeMMSpinBox.quantity = "length" + self.marginSizeMMSpinBox.value = 3.0 + self.marginSizeMMSpinBox.singleStep = 1.0 - self.marginSizeLabel = qt.QLabel() - self.marginSizeLabel.setToolTip("Size change in pixel. Computed from the segment's spacing and the specified margin size.") + self.marginSizeLabel = qt.QLabel() + self.marginSizeLabel.setToolTip("Size change in pixel. Computed from the segment's spacing and the specified margin size.") - marginSizeFrame = qt.QHBoxLayout() - marginSizeFrame.addWidget(self.marginSizeMMSpinBox) - self.marginSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Margin size:", marginSizeFrame) - self.scriptedEffect.addLabeledOptionsWidget("", self.marginSizeLabel) + marginSizeFrame = qt.QHBoxLayout() + marginSizeFrame.addWidget(self.marginSizeMMSpinBox) + self.marginSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Margin size:", marginSizeFrame) + self.scriptedEffect.addLabeledOptionsWidget("", self.marginSizeLabel) - self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox() - self.applyToAllVisibleSegmentsCheckBox.setToolTip("Grow or shrink all visible segments in this segmentation node. \ + self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox() + self.applyToAllVisibleSegmentsCheckBox.setToolTip("Grow or shrink all visible segments in this segmentation node. \ This operation may take a while.") - self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments' - self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox) - - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - self.applyButton.setToolTip("Grows or shrinks selected segment /default) or all segments (checkbox) by the specified margin.") - self.scriptedEffect.addOptionsWidget(self.applyButton) - - self.applyButton.connect('clicked()', self.onApply) - self.marginSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI) - self.growOptionRadioButton.connect("toggled(bool)", self.growOperationToggled) - self.shrinkOptionRadioButton.connect("toggled(bool)", self.shrinkOperationToggled) - self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) - - def createCursor(self, widget): - # Turn off effect-specific cursor for this effect - return slicer.util.mainWindow().cursor - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0) - self.scriptedEffect.setParameterDefault("MarginSizeMm", 3) - - def getMarginSizePixel(self): - selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - if selectedSegmentLabelmap: - selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() - - marginSizeMM = abs(self.scriptedEffect.doubleParameter("MarginSizeMm")) - marginSizePixel = [int(math.floor(marginSizeMM / spacing)) for spacing in selectedSegmentLabelmapSpacing] - return marginSizePixel - - def updateGUIFromMRML(self): - marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm") - wasBlocked = self.marginSizeMMSpinBox.blockSignals(True) - self.marginSizeMMSpinBox.value = abs(marginSizeMM) - self.marginSizeMMSpinBox.blockSignals(wasBlocked) - - wasBlocked = self.growOptionRadioButton.blockSignals(True) - self.growOptionRadioButton.setChecked(marginSizeMM > 0) - self.growOptionRadioButton.blockSignals(wasBlocked) - - wasBlocked = self.shrinkOptionRadioButton.blockSignals(True) - self.shrinkOptionRadioButton.setChecked(marginSizeMM < 0) - self.shrinkOptionRadioButton.blockSignals(wasBlocked) - - selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - if selectedSegmentLabelmap: - selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() - marginSizePixel = self.getMarginSizePixel() - if marginSizePixel[0] < 1 or marginSizePixel[1] < 1 or marginSizePixel[2] < 1: - self.marginSizeLabel.text = "Not feasible at current resolution." - self.applyButton.setEnabled(False) - else: - marginSizeMM = self.getMarginSizeMM() - self.marginSizeLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*marginSizeMM, *marginSizePixel) - self.applyButton.setEnabled(True) - else: - self.marginSizeLabel.text = "Empty segment" - - applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked - wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True) - self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments) - self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked) - - self.setWidgetMinMaxStepFromImageSpacing(self.marginSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) - - def growOperationToggled(self, toggled): - if toggled: - self.scriptedEffect.setParameter("MarginSizeMm", self.marginSizeMMSpinBox.value) - - def shrinkOperationToggled(self, toggled): - if toggled: - self.scriptedEffect.setParameter("MarginSizeMm", -self.marginSizeMMSpinBox.value) - - def updateMRMLFromGUI(self): - marginSizeMM = (self.marginSizeMMSpinBox.value) if self.growOptionRadioButton.checked else (-self.marginSizeMMSpinBox.value) - self.scriptedEffect.setParameter("MarginSizeMm", marginSizeMM) - applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0 - self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments) - - def getMarginSizeMM(self): - selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - if selectedSegmentLabelmap: - selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() - - marginSizePixel = self.getMarginSizePixel() - marginSizeMM = [abs((marginSizePixel[i])*selectedSegmentLabelmapSpacing[i]) for i in range(3)] - for i in range(3): - if marginSizeMM[i] > 0: - marginSizeMM[i] = round(marginSizeMM[i], max(int(-math.floor(math.log10(marginSizeMM[i]))),1)) - return marginSizeMM - - def showStatusMessage(self, msg, timeoutMsec=500): + self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments' + self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox) + + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + self.applyButton.setToolTip("Grows or shrinks selected segment /default) or all segments (checkbox) by the specified margin.") + self.scriptedEffect.addOptionsWidget(self.applyButton) + + self.applyButton.connect('clicked()', self.onApply) + self.marginSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI) + self.growOptionRadioButton.connect("toggled(bool)", self.growOperationToggled) + self.shrinkOptionRadioButton.connect("toggled(bool)", self.shrinkOperationToggled) + self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) + + def createCursor(self, widget): + # Turn off effect-specific cursor for this effect + return slicer.util.mainWindow().cursor + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0) + self.scriptedEffect.setParameterDefault("MarginSizeMm", 3) + + def getMarginSizePixel(self): + selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + if selectedSegmentLabelmap: + selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() + + marginSizeMM = abs(self.scriptedEffect.doubleParameter("MarginSizeMm")) + marginSizePixel = [int(math.floor(marginSizeMM / spacing)) for spacing in selectedSegmentLabelmapSpacing] + return marginSizePixel + + def updateGUIFromMRML(self): + marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm") + wasBlocked = self.marginSizeMMSpinBox.blockSignals(True) + self.marginSizeMMSpinBox.value = abs(marginSizeMM) + self.marginSizeMMSpinBox.blockSignals(wasBlocked) + + wasBlocked = self.growOptionRadioButton.blockSignals(True) + self.growOptionRadioButton.setChecked(marginSizeMM > 0) + self.growOptionRadioButton.blockSignals(wasBlocked) + + wasBlocked = self.shrinkOptionRadioButton.blockSignals(True) + self.shrinkOptionRadioButton.setChecked(marginSizeMM < 0) + self.shrinkOptionRadioButton.blockSignals(wasBlocked) + + selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + if selectedSegmentLabelmap: + selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() + marginSizePixel = self.getMarginSizePixel() + if marginSizePixel[0] < 1 or marginSizePixel[1] < 1 or marginSizePixel[2] < 1: + self.marginSizeLabel.text = "Not feasible at current resolution." + self.applyButton.setEnabled(False) + else: + marginSizeMM = self.getMarginSizeMM() + self.marginSizeLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*marginSizeMM, *marginSizePixel) + self.applyButton.setEnabled(True) + else: + self.marginSizeLabel.text = "Empty segment" + + applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked + wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True) + self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments) + self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked) + + self.setWidgetMinMaxStepFromImageSpacing(self.marginSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) + + def growOperationToggled(self, toggled): + if toggled: + self.scriptedEffect.setParameter("MarginSizeMm", self.marginSizeMMSpinBox.value) + + def shrinkOperationToggled(self, toggled): + if toggled: + self.scriptedEffect.setParameter("MarginSizeMm", -self.marginSizeMMSpinBox.value) + + def updateMRMLFromGUI(self): + marginSizeMM = (self.marginSizeMMSpinBox.value) if self.growOptionRadioButton.checked else (-self.marginSizeMMSpinBox.value) + self.scriptedEffect.setParameter("MarginSizeMm", marginSizeMM) + applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0 + self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments) + + def getMarginSizeMM(self): + selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + if selectedSegmentLabelmap: + selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() + + marginSizePixel = self.getMarginSizePixel() + marginSizeMM = [abs((marginSizePixel[i]) * selectedSegmentLabelmapSpacing[i]) for i in range(3)] + for i in range(3): + if marginSizeMM[i] > 0: + marginSizeMM[i] = round(marginSizeMM[i], max(int(-math.floor(math.log10(marginSizeMM[i]))), 1)) + return marginSizeMM + + def showStatusMessage(self, msg, timeoutMsec=500): slicer.util.showStatusMessage(msg, timeoutMsec) slicer.app.processEvents() - def processMargin(self): - # Get modifier labelmap and parameters - modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - - marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm") - - # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value - labelValue = 1 - backgroundValue = 0 - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(selectedSegmentLabelmap) - thresh.ThresholdByLower(0) - thresh.SetInValue(backgroundValue) - thresh.SetOutValue(labelValue) - thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) - if (marginSizeMM < 0): - # The distance filter used in the margin filter starts at zero at the border voxels, - # so if we need to shrink the margin, it is more accurate to invert the labelmap and - # use positive distance when calculating the margin - thresh.SetInValue(labelValue) - thresh.SetOutValue(backgroundValue) - - import vtkITK - margin = vtkITK.vtkITKImageMargin() - margin.SetInputConnection(thresh.GetOutputPort()) - margin.CalculateMarginInMMOn() - margin.SetOuterMarginMM(abs(marginSizeMM)) - margin.Update() - - if marginSizeMM >= 0: - modifierLabelmap.ShallowCopy(margin.GetOutput()) - else: - # If we are shrinking then the result needs to be inverted. - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(margin.GetOutput()) - thresh.ThresholdByLower(0) - thresh.SetInValue(labelValue) - thresh.SetOutValue(backgroundValue) - thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) - thresh.Update() - modifierLabelmap.ShallowCopy(thresh.GetOutput()) - - # Apply changes - self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) - - def onApply(self): - # Make sure the user wants to do the operation, even if the segment is not visible - if not self.scriptedEffect.confirmCurrentSegmentVisible(): - return - - try: - # This can be a long operation - indicate it to the user - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - self.scriptedEffect.saveStateForUndo() - - applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) !=0 \ - if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False - - if applyToAllVisibleSegments: - # Smooth all visible segments - inputSegmentIDs = vtk.vtkStringArray() - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs) - segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor - segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode() - # store which segment was selected before operation - selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID() - if inputSegmentIDs.GetNumberOfValues() == 0: - logging.info("Margin operation skipped: there are no visible segments.") - return - # select input segments one by one, process - for index in range(inputSegmentIDs.GetNumberOfValues()): - segmentID = inputSegmentIDs.GetValue(index) - self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...') - segmentEditorNode.SetSelectedSegmentID(segmentID) - self.processMargin() - # restore segment selection - segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID) - else: - self.processMargin() - - finally: - qt.QApplication.restoreOverrideCursor() + def processMargin(self): + # Get modifier labelmap and parameters + modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + + marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm") + + # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value + labelValue = 1 + backgroundValue = 0 + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(selectedSegmentLabelmap) + thresh.ThresholdByLower(0) + thresh.SetInValue(backgroundValue) + thresh.SetOutValue(labelValue) + thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) + if (marginSizeMM < 0): + # The distance filter used in the margin filter starts at zero at the border voxels, + # so if we need to shrink the margin, it is more accurate to invert the labelmap and + # use positive distance when calculating the margin + thresh.SetInValue(labelValue) + thresh.SetOutValue(backgroundValue) + + import vtkITK + margin = vtkITK.vtkITKImageMargin() + margin.SetInputConnection(thresh.GetOutputPort()) + margin.CalculateMarginInMMOn() + margin.SetOuterMarginMM(abs(marginSizeMM)) + margin.Update() + + if marginSizeMM >= 0: + modifierLabelmap.ShallowCopy(margin.GetOutput()) + else: + # If we are shrinking then the result needs to be inverted. + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(margin.GetOutput()) + thresh.ThresholdByLower(0) + thresh.SetInValue(labelValue) + thresh.SetOutValue(backgroundValue) + thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) + thresh.Update() + modifierLabelmap.ShallowCopy(thresh.GetOutput()) + + # Apply changes + self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) + + def onApply(self): + # Make sure the user wants to do the operation, even if the segment is not visible + if not self.scriptedEffect.confirmCurrentSegmentVisible(): + return + + try: + # This can be a long operation - indicate it to the user + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + self.scriptedEffect.saveStateForUndo() + + applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \ + if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False + + if applyToAllVisibleSegments: + # Smooth all visible segments + inputSegmentIDs = vtk.vtkStringArray() + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs) + segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor + segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode() + # store which segment was selected before operation + selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID() + if inputSegmentIDs.GetNumberOfValues() == 0: + logging.info("Margin operation skipped: there are no visible segments.") + return + # select input segments one by one, process + for index in range(inputSegmentIDs.GetNumberOfValues()): + segmentID = inputSegmentIDs.GetValue(index) + self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...') + segmentEditorNode.SetSelectedSegmentID(segmentID) + self.processMargin() + # restore segment selection + segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID) + else: + self.processMargin() + + finally: + qt.QApplication.restoreOverrideCursor() diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMaskVolumeEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMaskVolumeEffect.py index 651a4ca4c45..c10610aaa84 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMaskVolumeEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMaskVolumeEffect.py @@ -5,368 +5,368 @@ class SegmentEditorMaskVolumeEffect(AbstractScriptedSegmentEditorEffect): - """This effect fills a selected volume node inside and/or outside a segment with a chosen value. - """ - - def __init__(self, scriptedEffect): - scriptedEffect.name = 'Mask volume' - scriptedEffect.perSegment = True # this effect operates on a single selected segment - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) - - #Effect-specific members - self.buttonToOperationNameMap = {} - - def clone(self): - # It should not be necessary to modify this method - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - # It should not be necessary to modify this method - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MaskVolume.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Use the currently selected segment as a mask to blank out regions in a volume.
        The mask is applied to the master volume by default.

        + """This effect fills a selected volume node inside and/or outside a segment with a chosen value. + """ + + def __init__(self, scriptedEffect): + scriptedEffect.name = 'Mask volume' + scriptedEffect.perSegment = True # this effect operates on a single selected segment + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + + # Effect-specific members + self.buttonToOperationNameMap = {} + + def clone(self): + # It should not be necessary to modify this method + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + # It should not be necessary to modify this method + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MaskVolume.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Use the currently selected segment as a mask to blank out regions in a volume.
        The mask is applied to the master volume by default.

        Fill inside and outside operation creates a binary labelmap volume as output, with the inside and outside fill values modifiable. """ - def setupOptionsFrame(self): - self.operationRadioButtons = [] - self.updatingGUIFromMRML = False - self.visibleIcon = qt.QIcon(":/Icons/Small/SlicerVisible.png") - self.invisibleIcon = qt.QIcon(":/Icons/Small/SlicerInvisible.png") - - # Fill operation buttons - self.fillInsideButton = qt.QRadioButton("Fill inside") - self.operationRadioButtons.append(self.fillInsideButton) - self.buttonToOperationNameMap[self.fillInsideButton] = 'FILL_INSIDE' - - self.fillOutsideButton = qt.QRadioButton("Fill outside") - self.operationRadioButtons.append(self.fillOutsideButton) - self.buttonToOperationNameMap[self.fillOutsideButton] = 'FILL_OUTSIDE' - - self.binaryMaskFillButton = qt.QRadioButton("Fill inside and outside") - self.binaryMaskFillButton.setToolTip("Create a labelmap volume with specified inside and outside fill values.") - self.operationRadioButtons.append(self.binaryMaskFillButton) - self.buttonToOperationNameMap[self.binaryMaskFillButton] = 'FILL_INSIDE_AND_OUTSIDE' - - # Operation buttons layout - operationLayout = qt.QGridLayout() - operationLayout.addWidget(self.fillInsideButton, 0, 0) - operationLayout.addWidget(self.fillOutsideButton, 1, 0) - operationLayout.addWidget(self.binaryMaskFillButton, 0, 1) - self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout) - - # fill value - self.fillValueEdit = ctk.ctkDoubleSpinBox() - self.fillValueEdit.setToolTip("Choose the voxel intensity that will be used to fill the masked region.") - self.fillValueLabel = qt.QLabel("Fill value: ") - - # Binary mask fill outside value - self.binaryMaskFillOutsideEdit = ctk.ctkDoubleSpinBox() - self.binaryMaskFillOutsideEdit.setToolTip("Choose the voxel intensity that will be used to fill outside the mask.") - self.fillOutsideLabel = qt.QLabel("Outside fill value: ") - - # Binary mask fill outside value - self.binaryMaskFillInsideEdit = ctk.ctkDoubleSpinBox() - self.binaryMaskFillInsideEdit.setToolTip("Choose the voxel intensity that will be used to fill inside the mask.") - self.fillInsideLabel = qt.QLabel(" Inside fill value: ") - - for fillValueEdit in [self.fillValueEdit, self.binaryMaskFillOutsideEdit, self.binaryMaskFillInsideEdit]: - fillValueEdit.decimalsOption = ctk.ctkDoubleSpinBox.DecimalsByValue + ctk.ctkDoubleSpinBox.DecimalsByKey + ctk.ctkDoubleSpinBox.InsertDecimals - fillValueEdit.minimum = vtk.vtkDoubleArray().GetDataTypeMin(vtk.VTK_DOUBLE) - fillValueEdit.maximum = vtk.vtkDoubleArray().GetDataTypeMax(vtk.VTK_DOUBLE) - fillValueEdit.connect("valueChanged(double)", self.fillValueChanged) - - # Fill value layouts - fillValueLayout = qt.QFormLayout() - fillValueLayout.addRow(self.fillValueLabel, self.fillValueEdit) - - fillOutsideLayout = qt.QFormLayout() - fillOutsideLayout.addRow(self.fillOutsideLabel, self.binaryMaskFillOutsideEdit) - - fillInsideLayout = qt.QFormLayout() - fillInsideLayout.addRow(self.fillInsideLabel, self.binaryMaskFillInsideEdit) - - binaryMaskFillLayout = qt.QHBoxLayout() - binaryMaskFillLayout.addLayout(fillOutsideLayout) - binaryMaskFillLayout.addLayout(fillInsideLayout) - fillValuesSpinBoxLayout = qt.QFormLayout() - fillValuesSpinBoxLayout.addRow(binaryMaskFillLayout) - fillValuesSpinBoxLayout.addRow(fillValueLayout) - self.scriptedEffect.addOptionsWidget(fillValuesSpinBoxLayout) - - # input volume selector - self.inputVolumeSelector = slicer.qMRMLNodeComboBox() - self.inputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] - self.inputVolumeSelector.selectNodeUponCreation = True - self.inputVolumeSelector.addEnabled = True - self.inputVolumeSelector.removeEnabled = True - self.inputVolumeSelector.noneEnabled = True - self.inputVolumeSelector.noneDisplay = "(Master volume)" - self.inputVolumeSelector.showHidden = False - self.inputVolumeSelector.setMRMLScene(slicer.mrmlScene) - self.inputVolumeSelector.setToolTip("Volume to mask. Default is current master volume node.") - self.inputVolumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onInputVolumeChanged) - - self.inputVisibilityButton = qt.QToolButton() - self.inputVisibilityButton.setIcon(self.invisibleIcon) - self.inputVisibilityButton.connect('clicked()', self.onInputVisibilityButtonClicked) - inputLayout = qt.QHBoxLayout() - inputLayout.addWidget(self.inputVisibilityButton) - inputLayout.addWidget(self.inputVolumeSelector) - self.scriptedEffect.addLabeledOptionsWidget("Input Volume: ", inputLayout) - - # output volume selector - self.outputVolumeSelector = slicer.qMRMLNodeComboBox() - self.outputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"] - self.outputVolumeSelector.selectNodeUponCreation = True - self.outputVolumeSelector.addEnabled = True - self.outputVolumeSelector.removeEnabled = True - self.outputVolumeSelector.renameEnabled = True - self.outputVolumeSelector.noneEnabled = True - self.outputVolumeSelector.noneDisplay = "(Create new Volume)" - self.outputVolumeSelector.showHidden = False - self.outputVolumeSelector.setMRMLScene( slicer.mrmlScene ) - self.outputVolumeSelector.setToolTip("Masked output volume. It may be the same as the input volume for cumulative masking.") - self.outputVolumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onOutputVolumeChanged) - - self.outputVisibilityButton = qt.QToolButton() - self.outputVisibilityButton.setIcon(self.invisibleIcon) - self.outputVisibilityButton.connect('clicked()', self.onOutputVisibilityButtonClicked) - outputLayout = qt.QHBoxLayout() - outputLayout.addWidget(self.outputVisibilityButton) - outputLayout.addWidget(self.outputVolumeSelector) - self.scriptedEffect.addLabeledOptionsWidget("Output Volume: ", outputLayout) - - # Apply button - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - self.applyButton.setToolTip("Apply segment as volume mask. No undo operation available once applied.") - self.scriptedEffect.addOptionsWidget(self.applyButton) - self.applyButton.connect('clicked()', self.onApply) - - for button in self.operationRadioButtons: - button.connect('toggled(bool)', - lambda toggle, widget=self.buttonToOperationNameMap[button]: self.onOperationSelectionChanged(widget, toggle)) - - def createCursor(self, widget): - # Turn off effect-specific cursor for this effect - return slicer.util.mainWindow().cursor - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("FillValue", "0") - self.scriptedEffect.setParameterDefault("BinaryMaskFillValueInside", "1") - self.scriptedEffect.setParameterDefault("BinaryMaskFillValueOutside", "0") - self.scriptedEffect.setParameterDefault("Operation", "FILL_OUTSIDE") - - def isVolumeVisible(self, volumeNode): - if not volumeNode: - return False - volumeNodeID = volumeNode.GetID() - lm = slicer.app.layoutManager() - sliceViewNames = lm.sliceViewNames() - for sliceViewName in sliceViewNames: - sliceWidget = lm.sliceWidget(sliceViewName) - if volumeNodeID == sliceWidget.mrmlSliceCompositeNode().GetBackgroundVolumeID(): - return True - return False - - def updateGUIFromMRML(self): - self.updatingGUIFromMRML = True - - self.fillValueEdit.setValue(float(self.scriptedEffect.parameter("FillValue")) if self.scriptedEffect.parameter("FillValue") else 0) - self.binaryMaskFillOutsideEdit.setValue(float(self.scriptedEffect.parameter("BinaryMaskFillValueOutside")) - if self.scriptedEffect.parameter("BinaryMaskFillValueOutside") else 0) - self.binaryMaskFillInsideEdit.setValue(float(self.scriptedEffect.parameter("BinaryMaskFillValueInside")) - if self.scriptedEffect.parameter("BinaryMaskFillValueInside") else 1) - operationName = self.scriptedEffect.parameter("Operation") - if operationName: - operationButton = list(self.buttonToOperationNameMap.keys())[list(self.buttonToOperationNameMap.values()).index(operationName)] - operationButton.setChecked(True) - - inputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.InputVolume") - self.inputVolumeSelector.setCurrentNode(inputVolume) - outputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.OutputVolume") - self.outputVolumeSelector.setCurrentNode(outputVolume) - - masterVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode() - if inputVolume is None: - inputVolume = masterVolume - - self.fillValueEdit.setVisible(operationName in ["FILL_INSIDE", "FILL_OUTSIDE"]) - self.fillValueLabel.setVisible(operationName in ["FILL_INSIDE", "FILL_OUTSIDE"]) - self.binaryMaskFillInsideEdit.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE") - self.fillInsideLabel.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE") - self.binaryMaskFillOutsideEdit.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE") - self.fillOutsideLabel.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE") - if operationName in ["FILL_INSIDE", "FILL_OUTSIDE"]: - if self.outputVolumeSelector.noneDisplay != "(Create new Volume)": - self.outputVolumeSelector.noneDisplay = "(Create new Volume)" + def setupOptionsFrame(self): + self.operationRadioButtons = [] + self.updatingGUIFromMRML = False + self.visibleIcon = qt.QIcon(":/Icons/Small/SlicerVisible.png") + self.invisibleIcon = qt.QIcon(":/Icons/Small/SlicerInvisible.png") + + # Fill operation buttons + self.fillInsideButton = qt.QRadioButton("Fill inside") + self.operationRadioButtons.append(self.fillInsideButton) + self.buttonToOperationNameMap[self.fillInsideButton] = 'FILL_INSIDE' + + self.fillOutsideButton = qt.QRadioButton("Fill outside") + self.operationRadioButtons.append(self.fillOutsideButton) + self.buttonToOperationNameMap[self.fillOutsideButton] = 'FILL_OUTSIDE' + + self.binaryMaskFillButton = qt.QRadioButton("Fill inside and outside") + self.binaryMaskFillButton.setToolTip("Create a labelmap volume with specified inside and outside fill values.") + self.operationRadioButtons.append(self.binaryMaskFillButton) + self.buttonToOperationNameMap[self.binaryMaskFillButton] = 'FILL_INSIDE_AND_OUTSIDE' + + # Operation buttons layout + operationLayout = qt.QGridLayout() + operationLayout.addWidget(self.fillInsideButton, 0, 0) + operationLayout.addWidget(self.fillOutsideButton, 1, 0) + operationLayout.addWidget(self.binaryMaskFillButton, 0, 1) + self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout) + + # fill value + self.fillValueEdit = ctk.ctkDoubleSpinBox() + self.fillValueEdit.setToolTip("Choose the voxel intensity that will be used to fill the masked region.") + self.fillValueLabel = qt.QLabel("Fill value: ") + + # Binary mask fill outside value + self.binaryMaskFillOutsideEdit = ctk.ctkDoubleSpinBox() + self.binaryMaskFillOutsideEdit.setToolTip("Choose the voxel intensity that will be used to fill outside the mask.") + self.fillOutsideLabel = qt.QLabel("Outside fill value: ") + + # Binary mask fill outside value + self.binaryMaskFillInsideEdit = ctk.ctkDoubleSpinBox() + self.binaryMaskFillInsideEdit.setToolTip("Choose the voxel intensity that will be used to fill inside the mask.") + self.fillInsideLabel = qt.QLabel(" Inside fill value: ") + + for fillValueEdit in [self.fillValueEdit, self.binaryMaskFillOutsideEdit, self.binaryMaskFillInsideEdit]: + fillValueEdit.decimalsOption = ctk.ctkDoubleSpinBox.DecimalsByValue + ctk.ctkDoubleSpinBox.DecimalsByKey + ctk.ctkDoubleSpinBox.InsertDecimals + fillValueEdit.minimum = vtk.vtkDoubleArray().GetDataTypeMin(vtk.VTK_DOUBLE) + fillValueEdit.maximum = vtk.vtkDoubleArray().GetDataTypeMax(vtk.VTK_DOUBLE) + fillValueEdit.connect("valueChanged(double)", self.fillValueChanged) + + # Fill value layouts + fillValueLayout = qt.QFormLayout() + fillValueLayout.addRow(self.fillValueLabel, self.fillValueEdit) + + fillOutsideLayout = qt.QFormLayout() + fillOutsideLayout.addRow(self.fillOutsideLabel, self.binaryMaskFillOutsideEdit) + + fillInsideLayout = qt.QFormLayout() + fillInsideLayout.addRow(self.fillInsideLabel, self.binaryMaskFillInsideEdit) + + binaryMaskFillLayout = qt.QHBoxLayout() + binaryMaskFillLayout.addLayout(fillOutsideLayout) + binaryMaskFillLayout.addLayout(fillInsideLayout) + fillValuesSpinBoxLayout = qt.QFormLayout() + fillValuesSpinBoxLayout.addRow(binaryMaskFillLayout) + fillValuesSpinBoxLayout.addRow(fillValueLayout) + self.scriptedEffect.addOptionsWidget(fillValuesSpinBoxLayout) + + # input volume selector + self.inputVolumeSelector = slicer.qMRMLNodeComboBox() + self.inputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] + self.inputVolumeSelector.selectNodeUponCreation = True + self.inputVolumeSelector.addEnabled = True + self.inputVolumeSelector.removeEnabled = True + self.inputVolumeSelector.noneEnabled = True + self.inputVolumeSelector.noneDisplay = "(Master volume)" + self.inputVolumeSelector.showHidden = False + self.inputVolumeSelector.setMRMLScene(slicer.mrmlScene) + self.inputVolumeSelector.setToolTip("Volume to mask. Default is current master volume node.") + self.inputVolumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onInputVolumeChanged) + + self.inputVisibilityButton = qt.QToolButton() + self.inputVisibilityButton.setIcon(self.invisibleIcon) + self.inputVisibilityButton.connect('clicked()', self.onInputVisibilityButtonClicked) + inputLayout = qt.QHBoxLayout() + inputLayout.addWidget(self.inputVisibilityButton) + inputLayout.addWidget(self.inputVolumeSelector) + self.scriptedEffect.addLabeledOptionsWidget("Input Volume: ", inputLayout) + + # output volume selector + self.outputVolumeSelector = slicer.qMRMLNodeComboBox() self.outputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"] - else: - if self.outputVolumeSelector.noneDisplay != "(Create new Labelmap Volume)": - self.outputVolumeSelector.noneDisplay = "(Create new Labelmap Volume)" - self.outputVolumeSelector.nodeTypes = ["vtkMRMLLabelMapVolumeNode", "vtkMRMLScalarVolumeNode"] - - self.inputVisibilityButton.setIcon(self.visibleIcon if self.isVolumeVisible(inputVolume) else self.invisibleIcon) - self.outputVisibilityButton.setIcon(self.visibleIcon if self.isVolumeVisible(outputVolume) else self.invisibleIcon) - - self.updatingGUIFromMRML = False - - def updateMRMLFromGUI(self): - if self.updatingGUIFromMRML: - return - self.scriptedEffect.setParameter("FillValue", self.fillValueEdit.value) - self.scriptedEffect.setParameter("BinaryMaskFillValueInside", self.binaryMaskFillInsideEdit.value) - self.scriptedEffect.setParameter("BinaryMaskFillValueOutside", self.binaryMaskFillOutsideEdit.value) - self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.InputVolume", self.inputVolumeSelector.currentNodeID) - self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.OutputVolume", self.outputVolumeSelector.currentNodeID) - - def activate(self): - self.scriptedEffect.setParameter("InputVisibility", "True") - - def deactivate(self): - if self.outputVolumeSelector.currentNode() is not self.scriptedEffect.parameterSetNode().GetMasterVolumeNode(): - self.scriptedEffect.setParameter("OutputVisibility", "False") - slicer.util.setSliceViewerLayers(background=self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()) - - def onOperationSelectionChanged(self, operationName, toggle): - if not toggle: - return - self.scriptedEffect.setParameter("Operation", operationName) - - def getInputVolume(self): - inputVolume = self.inputVolumeSelector.currentNode() - if inputVolume is None: - inputVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode() - return inputVolume - - def onInputVisibilityButtonClicked(self): - inputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.InputVolume") - masterVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode() - if inputVolume is None: - inputVolume = masterVolume - if inputVolume: - slicer.util.setSliceViewerLayers(background=inputVolume) - self.updateGUIFromMRML() - - def onOutputVisibilityButtonClicked(self): - outputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.OutputVolume") - if outputVolume: - slicer.util.setSliceViewerLayers(background=outputVolume) - self.updateGUIFromMRML() - - def onInputVolumeChanged(self): - self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.InputVolume", self.inputVolumeSelector.currentNodeID) - self.updateGUIFromMRML() # node reference changes are not observed, update GUI manually - - def onOutputVolumeChanged(self): - self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.OutputVolume", self.outputVolumeSelector.currentNodeID) - self.updateGUIFromMRML() # node reference changes are not observed, update GUI manually - - def fillValueChanged(self): - self.updateMRMLFromGUI() - - def onApply(self): - inputVolume = self.getInputVolume() - outputVolume = self.outputVolumeSelector.currentNode() - operationMode = self.scriptedEffect.parameter("Operation") - if not outputVolume: - # Create new node for output - volumesLogic = slicer.modules.volumes.logic() - scene = inputVolume.GetScene() - if operationMode == "FILL_INSIDE_AND_OUTSIDE": - outputVolumeName = inputVolume.GetName()+" label" - outputVolume = volumesLogic.CreateAndAddLabelVolume(inputVolume, outputVolumeName) - else: - outputVolumeName = inputVolume.GetName()+" masked" - outputVolume = volumesLogic.CloneVolumeGeneric(scene, inputVolume, outputVolumeName, False) - self.outputVolumeSelector.setCurrentNode(outputVolume) - - if operationMode in ["FILL_INSIDE", "FILL_OUTSIDE"]: - fillValues = [self.fillValueEdit.value] - else: - fillValues = [self.binaryMaskFillInsideEdit.value, self.binaryMaskFillOutsideEdit.value] - - segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - - slicer.app.setOverrideCursor(qt.Qt.WaitCursor) - SegmentEditorMaskVolumeEffect.maskVolumeWithSegment(segmentationNode, segmentID, operationMode, fillValues, inputVolume, outputVolume) - - slicer.util.setSliceViewerLayers(background=outputVolume) - qt.QApplication.restoreOverrideCursor() - - self.updateGUIFromMRML() - - @staticmethod - def maskVolumeWithSegment(segmentationNode, segmentID, operationMode, fillValues, inputVolumeNode, outputVolumeNode, maskExtent=None): - """ - Fill voxels of the input volume inside/outside the masking model with the provided fill value - maskExtent: optional output to return computed mask extent (expected input is a 6-element list) - fillValues: list containing one or two fill values. If fill mode is inside or outside then only one value is specified in the list. - If fill mode is inside&outside then the list must contain two values: first is the inside fill, second is the outside fill value. - """ - - segmentIDs = vtk.vtkStringArray() - segmentIDs.InsertNextValue(segmentID) - maskVolumeNode = slicer.modules.volumes.logic().CreateAndAddLabelVolume(inputVolumeNode, "TemporaryVolumeMask") - if not maskVolumeNode: - logging.error("maskVolumeWithSegment failed: invalid maskVolumeNode") - return False - - if not slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, segmentIDs, maskVolumeNode, inputVolumeNode): - logging.error("maskVolumeWithSegment failed: ExportSegmentsToLabelmapNode error") - slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode().GetColorNode()) - slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode()) - slicer.mrmlScene.RemoveNode(maskVolumeNode) - return False - - if maskExtent: - img = slicer.modules.segmentations.logic().CreateOrientedImageDataFromVolumeNode(maskVolumeNode) - img.UnRegister(None) - import vtkSegmentationCorePython as vtkSegmentationCore - vtkSegmentationCore.vtkOrientedImageDataResample.CalculateEffectiveExtent(img, maskExtent, 0) - - maskToStencil = vtk.vtkImageToImageStencil() - maskToStencil.ThresholdByLower(0) - maskToStencil.SetInputData(maskVolumeNode.GetImageData()) - - stencil = vtk.vtkImageStencil() - - if operationMode == "FILL_INSIDE_AND_OUTSIDE": - # Set input to constant value - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(inputVolumeNode.GetImageData()) - thresh.ThresholdByLower(0) - thresh.SetInValue(fillValues[1]) - thresh.SetOutValue(fillValues[1]) - thresh.SetOutputScalarType(inputVolumeNode.GetImageData().GetScalarType()) - thresh.Update() - stencil.SetInputData(thresh.GetOutput()) - else: - stencil.SetInputData(inputVolumeNode.GetImageData()) - - stencil.SetStencilConnection(maskToStencil.GetOutputPort()) - stencil.SetReverseStencil(operationMode == "FILL_OUTSIDE") - stencil.SetBackgroundValue(fillValues[0]) - stencil.Update() - - outputVolumeNode.SetAndObserveImageData(stencil.GetOutput()) - - # Set the same geometry and parent transform as the input volume - ijkToRas = vtk.vtkMatrix4x4() - inputVolumeNode.GetIJKToRASMatrix(ijkToRas) - outputVolumeNode.SetIJKToRASMatrix(ijkToRas) - inputVolumeNode.SetAndObserveTransformNodeID(inputVolumeNode.GetTransformNodeID()) - - slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode().GetColorNode()) - slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode()) - slicer.mrmlScene.RemoveNode(maskVolumeNode) - return True + self.outputVolumeSelector.selectNodeUponCreation = True + self.outputVolumeSelector.addEnabled = True + self.outputVolumeSelector.removeEnabled = True + self.outputVolumeSelector.renameEnabled = True + self.outputVolumeSelector.noneEnabled = True + self.outputVolumeSelector.noneDisplay = "(Create new Volume)" + self.outputVolumeSelector.showHidden = False + self.outputVolumeSelector.setMRMLScene(slicer.mrmlScene) + self.outputVolumeSelector.setToolTip("Masked output volume. It may be the same as the input volume for cumulative masking.") + self.outputVolumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onOutputVolumeChanged) + + self.outputVisibilityButton = qt.QToolButton() + self.outputVisibilityButton.setIcon(self.invisibleIcon) + self.outputVisibilityButton.connect('clicked()', self.onOutputVisibilityButtonClicked) + outputLayout = qt.QHBoxLayout() + outputLayout.addWidget(self.outputVisibilityButton) + outputLayout.addWidget(self.outputVolumeSelector) + self.scriptedEffect.addLabeledOptionsWidget("Output Volume: ", outputLayout) + + # Apply button + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + self.applyButton.setToolTip("Apply segment as volume mask. No undo operation available once applied.") + self.scriptedEffect.addOptionsWidget(self.applyButton) + self.applyButton.connect('clicked()', self.onApply) + + for button in self.operationRadioButtons: + button.connect('toggled(bool)', + lambda toggle, widget=self.buttonToOperationNameMap[button]: self.onOperationSelectionChanged(widget, toggle)) + + def createCursor(self, widget): + # Turn off effect-specific cursor for this effect + return slicer.util.mainWindow().cursor + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("FillValue", "0") + self.scriptedEffect.setParameterDefault("BinaryMaskFillValueInside", "1") + self.scriptedEffect.setParameterDefault("BinaryMaskFillValueOutside", "0") + self.scriptedEffect.setParameterDefault("Operation", "FILL_OUTSIDE") + + def isVolumeVisible(self, volumeNode): + if not volumeNode: + return False + volumeNodeID = volumeNode.GetID() + lm = slicer.app.layoutManager() + sliceViewNames = lm.sliceViewNames() + for sliceViewName in sliceViewNames: + sliceWidget = lm.sliceWidget(sliceViewName) + if volumeNodeID == sliceWidget.mrmlSliceCompositeNode().GetBackgroundVolumeID(): + return True + return False + + def updateGUIFromMRML(self): + self.updatingGUIFromMRML = True + + self.fillValueEdit.setValue(float(self.scriptedEffect.parameter("FillValue")) if self.scriptedEffect.parameter("FillValue") else 0) + self.binaryMaskFillOutsideEdit.setValue(float(self.scriptedEffect.parameter("BinaryMaskFillValueOutside")) + if self.scriptedEffect.parameter("BinaryMaskFillValueOutside") else 0) + self.binaryMaskFillInsideEdit.setValue(float(self.scriptedEffect.parameter("BinaryMaskFillValueInside")) + if self.scriptedEffect.parameter("BinaryMaskFillValueInside") else 1) + operationName = self.scriptedEffect.parameter("Operation") + if operationName: + operationButton = list(self.buttonToOperationNameMap.keys())[list(self.buttonToOperationNameMap.values()).index(operationName)] + operationButton.setChecked(True) + + inputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.InputVolume") + self.inputVolumeSelector.setCurrentNode(inputVolume) + outputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.OutputVolume") + self.outputVolumeSelector.setCurrentNode(outputVolume) + + masterVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode() + if inputVolume is None: + inputVolume = masterVolume + + self.fillValueEdit.setVisible(operationName in ["FILL_INSIDE", "FILL_OUTSIDE"]) + self.fillValueLabel.setVisible(operationName in ["FILL_INSIDE", "FILL_OUTSIDE"]) + self.binaryMaskFillInsideEdit.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE") + self.fillInsideLabel.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE") + self.binaryMaskFillOutsideEdit.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE") + self.fillOutsideLabel.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE") + if operationName in ["FILL_INSIDE", "FILL_OUTSIDE"]: + if self.outputVolumeSelector.noneDisplay != "(Create new Volume)": + self.outputVolumeSelector.noneDisplay = "(Create new Volume)" + self.outputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"] + else: + if self.outputVolumeSelector.noneDisplay != "(Create new Labelmap Volume)": + self.outputVolumeSelector.noneDisplay = "(Create new Labelmap Volume)" + self.outputVolumeSelector.nodeTypes = ["vtkMRMLLabelMapVolumeNode", "vtkMRMLScalarVolumeNode"] + + self.inputVisibilityButton.setIcon(self.visibleIcon if self.isVolumeVisible(inputVolume) else self.invisibleIcon) + self.outputVisibilityButton.setIcon(self.visibleIcon if self.isVolumeVisible(outputVolume) else self.invisibleIcon) + + self.updatingGUIFromMRML = False + + def updateMRMLFromGUI(self): + if self.updatingGUIFromMRML: + return + self.scriptedEffect.setParameter("FillValue", self.fillValueEdit.value) + self.scriptedEffect.setParameter("BinaryMaskFillValueInside", self.binaryMaskFillInsideEdit.value) + self.scriptedEffect.setParameter("BinaryMaskFillValueOutside", self.binaryMaskFillOutsideEdit.value) + self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.InputVolume", self.inputVolumeSelector.currentNodeID) + self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.OutputVolume", self.outputVolumeSelector.currentNodeID) + + def activate(self): + self.scriptedEffect.setParameter("InputVisibility", "True") + + def deactivate(self): + if self.outputVolumeSelector.currentNode() is not self.scriptedEffect.parameterSetNode().GetMasterVolumeNode(): + self.scriptedEffect.setParameter("OutputVisibility", "False") + slicer.util.setSliceViewerLayers(background=self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()) + + def onOperationSelectionChanged(self, operationName, toggle): + if not toggle: + return + self.scriptedEffect.setParameter("Operation", operationName) + + def getInputVolume(self): + inputVolume = self.inputVolumeSelector.currentNode() + if inputVolume is None: + inputVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode() + return inputVolume + + def onInputVisibilityButtonClicked(self): + inputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.InputVolume") + masterVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode() + if inputVolume is None: + inputVolume = masterVolume + if inputVolume: + slicer.util.setSliceViewerLayers(background=inputVolume) + self.updateGUIFromMRML() + + def onOutputVisibilityButtonClicked(self): + outputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.OutputVolume") + if outputVolume: + slicer.util.setSliceViewerLayers(background=outputVolume) + self.updateGUIFromMRML() + + def onInputVolumeChanged(self): + self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.InputVolume", self.inputVolumeSelector.currentNodeID) + self.updateGUIFromMRML() # node reference changes are not observed, update GUI manually + + def onOutputVolumeChanged(self): + self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.OutputVolume", self.outputVolumeSelector.currentNodeID) + self.updateGUIFromMRML() # node reference changes are not observed, update GUI manually + + def fillValueChanged(self): + self.updateMRMLFromGUI() + + def onApply(self): + inputVolume = self.getInputVolume() + outputVolume = self.outputVolumeSelector.currentNode() + operationMode = self.scriptedEffect.parameter("Operation") + if not outputVolume: + # Create new node for output + volumesLogic = slicer.modules.volumes.logic() + scene = inputVolume.GetScene() + if operationMode == "FILL_INSIDE_AND_OUTSIDE": + outputVolumeName = inputVolume.GetName() + " label" + outputVolume = volumesLogic.CreateAndAddLabelVolume(inputVolume, outputVolumeName) + else: + outputVolumeName = inputVolume.GetName() + " masked" + outputVolume = volumesLogic.CloneVolumeGeneric(scene, inputVolume, outputVolumeName, False) + self.outputVolumeSelector.setCurrentNode(outputVolume) + + if operationMode in ["FILL_INSIDE", "FILL_OUTSIDE"]: + fillValues = [self.fillValueEdit.value] + else: + fillValues = [self.binaryMaskFillInsideEdit.value, self.binaryMaskFillOutsideEdit.value] + + segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + + slicer.app.setOverrideCursor(qt.Qt.WaitCursor) + SegmentEditorMaskVolumeEffect.maskVolumeWithSegment(segmentationNode, segmentID, operationMode, fillValues, inputVolume, outputVolume) + + slicer.util.setSliceViewerLayers(background=outputVolume) + qt.QApplication.restoreOverrideCursor() + + self.updateGUIFromMRML() + + @staticmethod + def maskVolumeWithSegment(segmentationNode, segmentID, operationMode, fillValues, inputVolumeNode, outputVolumeNode, maskExtent=None): + """ + Fill voxels of the input volume inside/outside the masking model with the provided fill value + maskExtent: optional output to return computed mask extent (expected input is a 6-element list) + fillValues: list containing one or two fill values. If fill mode is inside or outside then only one value is specified in the list. + If fill mode is inside&outside then the list must contain two values: first is the inside fill, second is the outside fill value. + """ + + segmentIDs = vtk.vtkStringArray() + segmentIDs.InsertNextValue(segmentID) + maskVolumeNode = slicer.modules.volumes.logic().CreateAndAddLabelVolume(inputVolumeNode, "TemporaryVolumeMask") + if not maskVolumeNode: + logging.error("maskVolumeWithSegment failed: invalid maskVolumeNode") + return False + + if not slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, segmentIDs, maskVolumeNode, inputVolumeNode): + logging.error("maskVolumeWithSegment failed: ExportSegmentsToLabelmapNode error") + slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode().GetColorNode()) + slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode()) + slicer.mrmlScene.RemoveNode(maskVolumeNode) + return False + + if maskExtent: + img = slicer.modules.segmentations.logic().CreateOrientedImageDataFromVolumeNode(maskVolumeNode) + img.UnRegister(None) + import vtkSegmentationCorePython as vtkSegmentationCore + vtkSegmentationCore.vtkOrientedImageDataResample.CalculateEffectiveExtent(img, maskExtent, 0) + + maskToStencil = vtk.vtkImageToImageStencil() + maskToStencil.ThresholdByLower(0) + maskToStencil.SetInputData(maskVolumeNode.GetImageData()) + + stencil = vtk.vtkImageStencil() + + if operationMode == "FILL_INSIDE_AND_OUTSIDE": + # Set input to constant value + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(inputVolumeNode.GetImageData()) + thresh.ThresholdByLower(0) + thresh.SetInValue(fillValues[1]) + thresh.SetOutValue(fillValues[1]) + thresh.SetOutputScalarType(inputVolumeNode.GetImageData().GetScalarType()) + thresh.Update() + stencil.SetInputData(thresh.GetOutput()) + else: + stencil.SetInputData(inputVolumeNode.GetImageData()) + + stencil.SetStencilConnection(maskToStencil.GetOutputPort()) + stencil.SetReverseStencil(operationMode == "FILL_OUTSIDE") + stencil.SetBackgroundValue(fillValues[0]) + stencil.Update() + + outputVolumeNode.SetAndObserveImageData(stencil.GetOutput()) + + # Set the same geometry and parent transform as the input volume + ijkToRas = vtk.vtkMatrix4x4() + inputVolumeNode.GetIJKToRASMatrix(ijkToRas) + outputVolumeNode.SetIJKToRASMatrix(ijkToRas) + inputVolumeNode.SetAndObserveTransformNodeID(inputVolumeNode.GetTransformNodeID()) + + slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode().GetColorNode()) + slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode()) + slicer.mrmlScene.RemoveNode(maskVolumeNode) + return True diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorSmoothingEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorSmoothingEffect.py index d05c8291ff5..4b5f1f2ddfb 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorSmoothingEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorSmoothingEffect.py @@ -11,27 +11,27 @@ class SegmentEditorSmoothingEffect(AbstractScriptedSegmentEditorPaintEffect): - """ SmoothingEffect is an Effect that smoothes a selected segment - """ - - def __init__(self, scriptedEffect): - scriptedEffect.name = 'Smoothing' - AbstractScriptedSegmentEditorPaintEffect.__init__(self, scriptedEffect) - - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Smoothing.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Make segment boundaries smoother
        by removing extrusions and filling small holes. The effect can be either applied locally + """ SmoothingEffect is an Effect that smoothes a selected segment + """ + + def __init__(self, scriptedEffect): + scriptedEffect.name = 'Smoothing' + AbstractScriptedSegmentEditorPaintEffect.__init__(self, scriptedEffect) + + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Smoothing.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Make segment boundaries smoother
        by removing extrusions and filling small holes. The effect can be either applied locally (by painting in viewers) or to the whole segment (by clicking Apply button). Available methods:

        • Median: removes small details while keeps smooth contours mostly unchanged. Applied to selected segment only.
        • @@ -42,450 +42,450 @@ def helpText(self): If segments overlap, segment higher in the segments table will have priority. Applied to all visible segments.

        """ - def setupOptionsFrame(self): - - self.methodSelectorComboBox = qt.QComboBox() - self.methodSelectorComboBox.addItem("Median", MEDIAN) - self.methodSelectorComboBox.addItem("Opening (remove extrusions)", MORPHOLOGICAL_OPENING) - self.methodSelectorComboBox.addItem("Closing (fill holes)", MORPHOLOGICAL_CLOSING) - self.methodSelectorComboBox.addItem("Gaussian", GAUSSIAN) - self.methodSelectorComboBox.addItem("Joint smoothing", JOINT_TAUBIN) - self.scriptedEffect.addLabeledOptionsWidget("Smoothing method:", self.methodSelectorComboBox) - - self.kernelSizeMMSpinBox = slicer.qMRMLSpinBox() - self.kernelSizeMMSpinBox.setMRMLScene(slicer.mrmlScene) - self.kernelSizeMMSpinBox.setToolTip("Diameter of the neighborhood that will be considered around each voxel. Higher value makes smoothing stronger (more details are suppressed).") - self.kernelSizeMMSpinBox.quantity = "length" - self.kernelSizeMMSpinBox.minimum = 0.0 - self.kernelSizeMMSpinBox.value = 3.0 - self.kernelSizeMMSpinBox.singleStep = 1.0 - - self.kernelSizePixel = qt.QLabel() - self.kernelSizePixel.setToolTip("Diameter of the neighborhood in pixel. Computed from the segment's spacing and the specified kernel size.") - - kernelSizeFrame = qt.QHBoxLayout() - kernelSizeFrame.addWidget(self.kernelSizeMMSpinBox) - kernelSizeFrame.addWidget(self.kernelSizePixel) - self.kernelSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Kernel size:", kernelSizeFrame) - - self.gaussianStandardDeviationMMSpinBox = slicer.qMRMLSpinBox() - self.gaussianStandardDeviationMMSpinBox.setMRMLScene(slicer.mrmlScene) - self.gaussianStandardDeviationMMSpinBox.setToolTip("Standard deviation of the Gaussian smoothing filter coefficients. Higher value makes smoothing stronger (more details are suppressed).") - self.gaussianStandardDeviationMMSpinBox.quantity = "length" - self.gaussianStandardDeviationMMSpinBox.value = 3.0 - self.gaussianStandardDeviationMMSpinBox.singleStep = 1.0 - self.gaussianStandardDeviationMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Standard deviation:", self.gaussianStandardDeviationMMSpinBox) - - self.jointTaubinSmoothingFactorSlider = ctk.ctkSliderWidget() - self.jointTaubinSmoothingFactorSlider.setToolTip("Higher value means stronger smoothing.") - self.jointTaubinSmoothingFactorSlider.minimum = 0.01 - self.jointTaubinSmoothingFactorSlider.maximum = 1.0 - self.jointTaubinSmoothingFactorSlider.value = 0.5 - self.jointTaubinSmoothingFactorSlider.singleStep = 0.01 - self.jointTaubinSmoothingFactorSlider.pageStep = 0.1 - self.jointTaubinSmoothingFactorLabel = self.scriptedEffect.addLabeledOptionsWidget("Smoothing factor:", self.jointTaubinSmoothingFactorSlider) - - self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox() - self.applyToAllVisibleSegmentsCheckBox.setToolTip("Apply smoothing effect to all visible segments in this segmentation node. \ + def setupOptionsFrame(self): + + self.methodSelectorComboBox = qt.QComboBox() + self.methodSelectorComboBox.addItem("Median", MEDIAN) + self.methodSelectorComboBox.addItem("Opening (remove extrusions)", MORPHOLOGICAL_OPENING) + self.methodSelectorComboBox.addItem("Closing (fill holes)", MORPHOLOGICAL_CLOSING) + self.methodSelectorComboBox.addItem("Gaussian", GAUSSIAN) + self.methodSelectorComboBox.addItem("Joint smoothing", JOINT_TAUBIN) + self.scriptedEffect.addLabeledOptionsWidget("Smoothing method:", self.methodSelectorComboBox) + + self.kernelSizeMMSpinBox = slicer.qMRMLSpinBox() + self.kernelSizeMMSpinBox.setMRMLScene(slicer.mrmlScene) + self.kernelSizeMMSpinBox.setToolTip("Diameter of the neighborhood that will be considered around each voxel. Higher value makes smoothing stronger (more details are suppressed).") + self.kernelSizeMMSpinBox.quantity = "length" + self.kernelSizeMMSpinBox.minimum = 0.0 + self.kernelSizeMMSpinBox.value = 3.0 + self.kernelSizeMMSpinBox.singleStep = 1.0 + + self.kernelSizePixel = qt.QLabel() + self.kernelSizePixel.setToolTip("Diameter of the neighborhood in pixel. Computed from the segment's spacing and the specified kernel size.") + + kernelSizeFrame = qt.QHBoxLayout() + kernelSizeFrame.addWidget(self.kernelSizeMMSpinBox) + kernelSizeFrame.addWidget(self.kernelSizePixel) + self.kernelSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Kernel size:", kernelSizeFrame) + + self.gaussianStandardDeviationMMSpinBox = slicer.qMRMLSpinBox() + self.gaussianStandardDeviationMMSpinBox.setMRMLScene(slicer.mrmlScene) + self.gaussianStandardDeviationMMSpinBox.setToolTip("Standard deviation of the Gaussian smoothing filter coefficients. Higher value makes smoothing stronger (more details are suppressed).") + self.gaussianStandardDeviationMMSpinBox.quantity = "length" + self.gaussianStandardDeviationMMSpinBox.value = 3.0 + self.gaussianStandardDeviationMMSpinBox.singleStep = 1.0 + self.gaussianStandardDeviationMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Standard deviation:", self.gaussianStandardDeviationMMSpinBox) + + self.jointTaubinSmoothingFactorSlider = ctk.ctkSliderWidget() + self.jointTaubinSmoothingFactorSlider.setToolTip("Higher value means stronger smoothing.") + self.jointTaubinSmoothingFactorSlider.minimum = 0.01 + self.jointTaubinSmoothingFactorSlider.maximum = 1.0 + self.jointTaubinSmoothingFactorSlider.value = 0.5 + self.jointTaubinSmoothingFactorSlider.singleStep = 0.01 + self.jointTaubinSmoothingFactorSlider.pageStep = 0.1 + self.jointTaubinSmoothingFactorLabel = self.scriptedEffect.addLabeledOptionsWidget("Smoothing factor:", self.jointTaubinSmoothingFactorSlider) + + self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox() + self.applyToAllVisibleSegmentsCheckBox.setToolTip("Apply smoothing effect to all visible segments in this segmentation node. \ This operation may take a while.") - self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments' - self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox) - - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - self.applyButton.setToolTip("Apply smoothing to selected segment") - self.scriptedEffect.addOptionsWidget(self.applyButton) - - self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI) - self.kernelSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI) - self.gaussianStandardDeviationMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI) - self.jointTaubinSmoothingFactorSlider.connect("valueChanged(double)", self.updateMRMLFromGUI) - self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) - self.applyButton.connect('clicked()', self.onApply) - - # Customize smoothing brush - self.scriptedEffect.setColorSmudgeCheckboxVisible(False) - self.paintOptionsGroupBox = ctk.ctkCollapsibleGroupBox() - self.paintOptionsGroupBox.setTitle("Smoothing brush options") - self.paintOptionsGroupBox.setLayout(qt.QVBoxLayout()) - self.paintOptionsGroupBox.layout().addWidget(self.scriptedEffect.paintOptionsFrame()) - self.paintOptionsGroupBox.collapsed = True - self.scriptedEffect.addOptionsWidget(self.paintOptionsGroupBox) - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0) - self.scriptedEffect.setParameterDefault("GaussianStandardDeviationMm", 3) - self.scriptedEffect.setParameterDefault("JointTaubinSmoothingFactor", 0.5) - self.scriptedEffect.setParameterDefault("KernelSizeMm", 3) - self.scriptedEffect.setParameterDefault("SmoothingMethod", MEDIAN) - - def updateParameterWidgetsVisibility(self): - methodIndex = self.methodSelectorComboBox.currentIndex - smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex) - morphologicalMethod = (smoothingMethod==MEDIAN or smoothingMethod==MORPHOLOGICAL_OPENING or smoothingMethod==MORPHOLOGICAL_CLOSING) - self.kernelSizeMMLabel.setVisible(morphologicalMethod) - self.kernelSizeMMSpinBox.setVisible(morphologicalMethod) - self.kernelSizePixel.setVisible(morphologicalMethod) - self.gaussianStandardDeviationMMLabel.setVisible(smoothingMethod==GAUSSIAN) - self.gaussianStandardDeviationMMSpinBox.setVisible(smoothingMethod==GAUSSIAN) - self.jointTaubinSmoothingFactorLabel.setVisible(smoothingMethod==JOINT_TAUBIN) - self.jointTaubinSmoothingFactorSlider.setVisible(smoothingMethod==JOINT_TAUBIN) - self.applyToAllVisibleSegmentsLabel.setVisible(smoothingMethod!=JOINT_TAUBIN) - self.applyToAllVisibleSegmentsCheckBox.setVisible(smoothingMethod!=JOINT_TAUBIN) - - def getKernelSizePixel(self): - selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - if selectedSegmentLabelmap: - selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() - - # size rounded to nearest odd number. If kernel size is even then image gets shifted. - kernelSizeMM = self.scriptedEffect.doubleParameter("KernelSizeMm") - kernelSizePixel = [int(round((kernelSizeMM / selectedSegmentLabelmapSpacing[componentIndex]+1)/2)*2-1) for componentIndex in range(3)] - return kernelSizePixel - - def updateGUIFromMRML(self): - methodIndex = self.methodSelectorComboBox.findData(self.scriptedEffect.parameter("SmoothingMethod")) - wasBlocked = self.methodSelectorComboBox.blockSignals(True) - self.methodSelectorComboBox.setCurrentIndex(methodIndex) - self.methodSelectorComboBox.blockSignals(wasBlocked) - - wasBlocked = self.kernelSizeMMSpinBox.blockSignals(True) - self.setWidgetMinMaxStepFromImageSpacing(self.kernelSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) - self.kernelSizeMMSpinBox.value = self.scriptedEffect.doubleParameter("KernelSizeMm") - self.kernelSizeMMSpinBox.blockSignals(wasBlocked) - kernelSizePixel = self.getKernelSizePixel() - self.kernelSizePixel.text = f"{kernelSizePixel[0]}x{kernelSizePixel[1]}x{kernelSizePixel[2]} pixel" - - wasBlocked = self.gaussianStandardDeviationMMSpinBox.blockSignals(True) - self.setWidgetMinMaxStepFromImageSpacing(self.gaussianStandardDeviationMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) - self.gaussianStandardDeviationMMSpinBox.value = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm") - self.gaussianStandardDeviationMMSpinBox.blockSignals(wasBlocked) - - wasBlocked = self.jointTaubinSmoothingFactorSlider.blockSignals(True) - self.jointTaubinSmoothingFactorSlider.value = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor") - self.jointTaubinSmoothingFactorSlider.blockSignals(wasBlocked) - - applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked - wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True) - self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments) - self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked) - - self.updateParameterWidgetsVisibility() - - def updateMRMLFromGUI(self): - methodIndex = self.methodSelectorComboBox.currentIndex - smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex) - self.scriptedEffect.setParameter("SmoothingMethod", smoothingMethod) - self.scriptedEffect.setParameter("KernelSizeMm", self.kernelSizeMMSpinBox.value) - self.scriptedEffect.setParameter("GaussianStandardDeviationMm", self.gaussianStandardDeviationMMSpinBox.value) - self.scriptedEffect.setParameter("JointTaubinSmoothingFactor", self.jointTaubinSmoothingFactorSlider.value) - applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0 - self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments) - - self.updateParameterWidgetsVisibility() - - # - # Effect specific methods (the above ones are the API methods to override) - # - - def showStatusMessage(self, msg, timeoutMsec=500): - slicer.util.showStatusMessage(msg, timeoutMsec) - slicer.app.processEvents() + self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments' + self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox) + + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + self.applyButton.setToolTip("Apply smoothing to selected segment") + self.scriptedEffect.addOptionsWidget(self.applyButton) + + self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI) + self.kernelSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI) + self.gaussianStandardDeviationMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI) + self.jointTaubinSmoothingFactorSlider.connect("valueChanged(double)", self.updateMRMLFromGUI) + self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI) + self.applyButton.connect('clicked()', self.onApply) + + # Customize smoothing brush + self.scriptedEffect.setColorSmudgeCheckboxVisible(False) + self.paintOptionsGroupBox = ctk.ctkCollapsibleGroupBox() + self.paintOptionsGroupBox.setTitle("Smoothing brush options") + self.paintOptionsGroupBox.setLayout(qt.QVBoxLayout()) + self.paintOptionsGroupBox.layout().addWidget(self.scriptedEffect.paintOptionsFrame()) + self.paintOptionsGroupBox.collapsed = True + self.scriptedEffect.addOptionsWidget(self.paintOptionsGroupBox) + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0) + self.scriptedEffect.setParameterDefault("GaussianStandardDeviationMm", 3) + self.scriptedEffect.setParameterDefault("JointTaubinSmoothingFactor", 0.5) + self.scriptedEffect.setParameterDefault("KernelSizeMm", 3) + self.scriptedEffect.setParameterDefault("SmoothingMethod", MEDIAN) + + def updateParameterWidgetsVisibility(self): + methodIndex = self.methodSelectorComboBox.currentIndex + smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex) + morphologicalMethod = (smoothingMethod == MEDIAN or smoothingMethod == MORPHOLOGICAL_OPENING or smoothingMethod == MORPHOLOGICAL_CLOSING) + self.kernelSizeMMLabel.setVisible(morphologicalMethod) + self.kernelSizeMMSpinBox.setVisible(morphologicalMethod) + self.kernelSizePixel.setVisible(morphologicalMethod) + self.gaussianStandardDeviationMMLabel.setVisible(smoothingMethod == GAUSSIAN) + self.gaussianStandardDeviationMMSpinBox.setVisible(smoothingMethod == GAUSSIAN) + self.jointTaubinSmoothingFactorLabel.setVisible(smoothingMethod == JOINT_TAUBIN) + self.jointTaubinSmoothingFactorSlider.setVisible(smoothingMethod == JOINT_TAUBIN) + self.applyToAllVisibleSegmentsLabel.setVisible(smoothingMethod != JOINT_TAUBIN) + self.applyToAllVisibleSegmentsCheckBox.setVisible(smoothingMethod != JOINT_TAUBIN) + + def getKernelSizePixel(self): + selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0] + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + if selectedSegmentLabelmap: + selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing() - def onApply(self, maskImage=None, maskExtent=None): - """maskImage: contains nonzero where smoothing will be applied - """ - smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod") - applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) !=0 \ - if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False - - if smoothingMethod != JOINT_TAUBIN: - # Make sure the user wants to do the operation, even if the segment is not visible - if not self.scriptedEffect.confirmCurrentSegmentVisible(): - return - - try: - # This can be a long operation - indicate it to the user - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - self.scriptedEffect.saveStateForUndo() - - if smoothingMethod == JOINT_TAUBIN: - self.smoothMultipleSegments(maskImage, maskExtent) - elif applyToAllVisibleSegments: - # Smooth all visible segments - inputSegmentIDs = vtk.vtkStringArray() - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs) - segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor - segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode() - # store which segment was selected before operation - selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID() - if inputSegmentIDs.GetNumberOfValues() == 0: - logging.info("Smoothing operation skipped: there are no visible segments.") - return - for index in range(inputSegmentIDs.GetNumberOfValues()): - segmentID = inputSegmentIDs.GetValue(index) - self.showStatusMessage(f'Smoothing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...') - segmentEditorNode.SetSelectedSegmentID(segmentID) - self.smoothSelectedSegment(maskImage, maskExtent) - # restore segment selection - segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID) - else: - self.smoothSelectedSegment(maskImage, maskExtent) - finally: - qt.QApplication.restoreOverrideCursor() - - def clipImage(self, inputImage, maskExtent, margin): - clipper = vtk.vtkImageClip() - clipper.SetOutputWholeExtent(maskExtent[0] - margin[0], maskExtent[1] + margin[0], - maskExtent[2] - margin[1], maskExtent[3] + margin[1], - maskExtent[4] - margin[2], maskExtent[5] + margin[2]) - clipper.SetInputData(inputImage) - clipper.SetClipData(True) - clipper.Update() - clippedImage = slicer.vtkOrientedImageData() - clippedImage.ShallowCopy(clipper.GetOutput()) - clippedImage.CopyDirections(inputImage) - return clippedImage - - def modifySelectedSegmentByLabelmap(self, smoothedImage, selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent): - if maskImage: - smoothedClippedSelectedSegmentLabelmap = slicer.vtkOrientedImageData() - smoothedClippedSelectedSegmentLabelmap.ShallowCopy(smoothedImage) - smoothedClippedSelectedSegmentLabelmap.CopyDirections(modifierLabelmap) - - # fill smoothed selected segment outside the painted region to 1 so that in the end the image is not modified by OPERATION_MINIMUM - fillValue = 1.0 - slicer.vtkOrientedImageDataResample.ApplyImageMask(smoothedClippedSelectedSegmentLabelmap, maskImage, fillValue, False) - # set original segment labelmap outside painted region, solid 1 inside painted region - slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, selectedSegmentLabelmap, - slicer.vtkOrientedImageDataResample.OPERATION_MAXIMUM) - slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, smoothedClippedSelectedSegmentLabelmap, - slicer.vtkOrientedImageDataResample.OPERATION_MINIMUM) - - updateExtent = [0, -1, 0, -1, 0, -1] - modifierExtent = modifierLabelmap.GetExtent() - for i in range(3): - updateExtent[2 * i] = min(maskExtent[2 * i], modifierExtent[2 * i]) - updateExtent[2 * i + 1] = max(maskExtent[2 * i + 1], modifierExtent[2 * i + 1]) - - self.scriptedEffect.modifySelectedSegmentByLabelmap(maskImage, - slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, - updateExtent) - else: - modifierLabelmap.DeepCopy(smoothedImage) - self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) - - def smoothSelectedSegment(self, maskImage=None, maskExtent=None): - try: - # Get modifier labelmap - modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() - selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() - - smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod") - - if smoothingMethod == GAUSSIAN: - maxValue = 255 - radiusFactor = 4.0 - standardDeviationMM = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm") - spacing = modifierLabelmap.GetSpacing() - standardDeviationPixel = [1.0, 1.0, 1.0] - radiusPixel = [3, 3, 3] - for idx in range(3): - standardDeviationPixel[idx] = standardDeviationMM / spacing[idx] - radiusPixel[idx] = int(standardDeviationPixel[idx] * radiusFactor) + 1 - if maskExtent: - clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, radiusPixel) - else: - clippedSelectedSegmentLabelmap = selectedSegmentLabelmap - - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(clippedSelectedSegmentLabelmap) - thresh.ThresholdByLower(0) - thresh.SetInValue(0) - thresh.SetOutValue(maxValue) - thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) - - gaussianFilter = vtk.vtkImageGaussianSmooth() - gaussianFilter.SetInputConnection(thresh.GetOutputPort()) - gaussianFilter.SetStandardDeviation(*standardDeviationPixel) - gaussianFilter.SetRadiusFactor(radiusFactor) - - thresh2 = vtk.vtkImageThreshold() - thresh2.SetInputConnection(gaussianFilter.GetOutputPort()) - thresh2.ThresholdByUpper(int(maxValue / 2)) - thresh2.SetInValue(1) - thresh2.SetOutValue(0) - thresh2.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) - thresh2.Update() - - self.modifySelectedSegmentByLabelmap(thresh2.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent) - - else: # size rounded to nearest odd number. If kernel size is even then image gets shifted. + kernelSizeMM = self.scriptedEffect.doubleParameter("KernelSizeMm") + kernelSizePixel = [int(round((kernelSizeMM / selectedSegmentLabelmapSpacing[componentIndex] + 1) / 2) * 2 - 1) for componentIndex in range(3)] + return kernelSizePixel + + def updateGUIFromMRML(self): + methodIndex = self.methodSelectorComboBox.findData(self.scriptedEffect.parameter("SmoothingMethod")) + wasBlocked = self.methodSelectorComboBox.blockSignals(True) + self.methodSelectorComboBox.setCurrentIndex(methodIndex) + self.methodSelectorComboBox.blockSignals(wasBlocked) + + wasBlocked = self.kernelSizeMMSpinBox.blockSignals(True) + self.setWidgetMinMaxStepFromImageSpacing(self.kernelSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) + self.kernelSizeMMSpinBox.value = self.scriptedEffect.doubleParameter("KernelSizeMm") + self.kernelSizeMMSpinBox.blockSignals(wasBlocked) kernelSizePixel = self.getKernelSizePixel() + self.kernelSizePixel.text = f"{kernelSizePixel[0]}x{kernelSizePixel[1]}x{kernelSizePixel[2]} pixel" - if maskExtent: - clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, kernelSizePixel) - else: - clippedSelectedSegmentLabelmap = selectedSegmentLabelmap + wasBlocked = self.gaussianStandardDeviationMMSpinBox.blockSignals(True) + self.setWidgetMinMaxStepFromImageSpacing(self.gaussianStandardDeviationMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap()) + self.gaussianStandardDeviationMMSpinBox.value = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm") + self.gaussianStandardDeviationMMSpinBox.blockSignals(wasBlocked) + + wasBlocked = self.jointTaubinSmoothingFactorSlider.blockSignals(True) + self.jointTaubinSmoothingFactorSlider.value = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor") + self.jointTaubinSmoothingFactorSlider.blockSignals(wasBlocked) + + applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked + wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True) + self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments) + self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked) + + self.updateParameterWidgetsVisibility() + + def updateMRMLFromGUI(self): + methodIndex = self.methodSelectorComboBox.currentIndex + smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex) + self.scriptedEffect.setParameter("SmoothingMethod", smoothingMethod) + self.scriptedEffect.setParameter("KernelSizeMm", self.kernelSizeMMSpinBox.value) + self.scriptedEffect.setParameter("GaussianStandardDeviationMm", self.gaussianStandardDeviationMMSpinBox.value) + self.scriptedEffect.setParameter("JointTaubinSmoothingFactor", self.jointTaubinSmoothingFactorSlider.value) + applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0 + self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments) - if smoothingMethod == MEDIAN: - # Median filter does not require a particular label value - smoothingFilter = vtk.vtkImageMedian3D() - smoothingFilter.SetInputData(clippedSelectedSegmentLabelmap) + self.updateParameterWidgetsVisibility() + # + # Effect specific methods (the above ones are the API methods to override) + # + + def showStatusMessage(self, msg, timeoutMsec=500): + slicer.util.showStatusMessage(msg, timeoutMsec) + slicer.app.processEvents() + + def onApply(self, maskImage=None, maskExtent=None): + """maskImage: contains nonzero where smoothing will be applied + """ + smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod") + applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \ + if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False + + if smoothingMethod != JOINT_TAUBIN: + # Make sure the user wants to do the operation, even if the segment is not visible + if not self.scriptedEffect.confirmCurrentSegmentVisible(): + return + + try: + # This can be a long operation - indicate it to the user + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + self.scriptedEffect.saveStateForUndo() + + if smoothingMethod == JOINT_TAUBIN: + self.smoothMultipleSegments(maskImage, maskExtent) + elif applyToAllVisibleSegments: + # Smooth all visible segments + inputSegmentIDs = vtk.vtkStringArray() + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs) + segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor + segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode() + # store which segment was selected before operation + selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID() + if inputSegmentIDs.GetNumberOfValues() == 0: + logging.info("Smoothing operation skipped: there are no visible segments.") + return + for index in range(inputSegmentIDs.GetNumberOfValues()): + segmentID = inputSegmentIDs.GetValue(index) + self.showStatusMessage(f'Smoothing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...') + segmentEditorNode.SetSelectedSegmentID(segmentID) + self.smoothSelectedSegment(maskImage, maskExtent) + # restore segment selection + segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID) + else: + self.smoothSelectedSegment(maskImage, maskExtent) + finally: + qt.QApplication.restoreOverrideCursor() + + def clipImage(self, inputImage, maskExtent, margin): + clipper = vtk.vtkImageClip() + clipper.SetOutputWholeExtent(maskExtent[0] - margin[0], maskExtent[1] + margin[0], + maskExtent[2] - margin[1], maskExtent[3] + margin[1], + maskExtent[4] - margin[2], maskExtent[5] + margin[2]) + clipper.SetInputData(inputImage) + clipper.SetClipData(True) + clipper.Update() + clippedImage = slicer.vtkOrientedImageData() + clippedImage.ShallowCopy(clipper.GetOutput()) + clippedImage.CopyDirections(inputImage) + return clippedImage + + def modifySelectedSegmentByLabelmap(self, smoothedImage, selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent): + if maskImage: + smoothedClippedSelectedSegmentLabelmap = slicer.vtkOrientedImageData() + smoothedClippedSelectedSegmentLabelmap.ShallowCopy(smoothedImage) + smoothedClippedSelectedSegmentLabelmap.CopyDirections(modifierLabelmap) + + # fill smoothed selected segment outside the painted region to 1 so that in the end the image is not modified by OPERATION_MINIMUM + fillValue = 1.0 + slicer.vtkOrientedImageDataResample.ApplyImageMask(smoothedClippedSelectedSegmentLabelmap, maskImage, fillValue, False) + # set original segment labelmap outside painted region, solid 1 inside painted region + slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, selectedSegmentLabelmap, + slicer.vtkOrientedImageDataResample.OPERATION_MAXIMUM) + slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, smoothedClippedSelectedSegmentLabelmap, + slicer.vtkOrientedImageDataResample.OPERATION_MINIMUM) + + updateExtent = [0, -1, 0, -1, 0, -1] + modifierExtent = modifierLabelmap.GetExtent() + for i in range(3): + updateExtent[2 * i] = min(maskExtent[2 * i], modifierExtent[2 * i]) + updateExtent[2 * i + 1] = max(maskExtent[2 * i + 1], modifierExtent[2 * i + 1]) + + self.scriptedEffect.modifySelectedSegmentByLabelmap(maskImage, + slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, + updateExtent) else: - # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value - labelValue = 1 - backgroundValue = 0 - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(clippedSelectedSegmentLabelmap) - thresh.ThresholdByLower(0) - thresh.SetInValue(backgroundValue) - thresh.SetOutValue(labelValue) - thresh.SetOutputScalarType(clippedSelectedSegmentLabelmap.GetScalarType()) - - smoothingFilter = vtk.vtkImageOpenClose3D() - smoothingFilter.SetInputConnection(thresh.GetOutputPort()) - if smoothingMethod == MORPHOLOGICAL_OPENING: - smoothingFilter.SetOpenValue(labelValue) - smoothingFilter.SetCloseValue(backgroundValue) - else: # must be smoothingMethod == MORPHOLOGICAL_CLOSING: - smoothingFilter.SetOpenValue(backgroundValue) - smoothingFilter.SetCloseValue(labelValue) - - smoothingFilter.SetKernelSize(kernelSizePixel[0],kernelSizePixel[1],kernelSizePixel[2]) - smoothingFilter.Update() - - self.modifySelectedSegmentByLabelmap(smoothingFilter.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent) - - except IndexError: - logging.error('apply: Failed to apply smoothing') - - def smoothMultipleSegments(self, maskImage=None, maskExtent=None): - import vtkSegmentationCorePython as vtkSegmentationCore - - self.showStatusMessage(f'Joint smoothing ...') - # Generate merged labelmap of all visible segments - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - visibleSegmentIds = vtk.vtkStringArray() - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) - if visibleSegmentIds.GetNumberOfValues() == 0: - logging.info("Smoothing operation skipped: there are no visible segments") - return - - mergedImage = slicer.vtkOrientedImageData() - if not segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage, - vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED, - None, visibleSegmentIds): - logging.error('Failed to apply smoothing: cannot get list of visible segments') - return - - segmentLabelValues = [] # list of [segmentId, labelValue] - for i in range(visibleSegmentIds.GetNumberOfValues()): - segmentId = visibleSegmentIds.GetValue(i) - segmentLabelValues.append([segmentId, i+1]) - - # Perform smoothing in voxel space - ici = vtk.vtkImageChangeInformation() - ici.SetInputData(mergedImage) - ici.SetOutputSpacing(1, 1, 1) - ici.SetOutputOrigin(0, 0, 0) - - # Convert labelmap to combined polydata - # vtkDiscreteFlyingEdges3D cannot be used here, as in the output of that filter, - # each labeled region is completely disconnected from neighboring regions, and - # for joint smoothing it is essential for the points to move together. - convertToPolyData = vtk.vtkDiscreteMarchingCubes() - convertToPolyData.SetInputConnection(ici.GetOutputPort()) - convertToPolyData.SetNumberOfContours(len(segmentLabelValues)) - - contourIndex = 0 - for segmentId, labelValue in segmentLabelValues: - convertToPolyData.SetValue(contourIndex, labelValue) - contourIndex += 1 - - # Low-pass filtering using Taubin's method - smoothingFactor = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor") - smoothingIterations = 100 # according to VTK documentation 10-20 iterations could be enough but we use a higher value to reduce chance of shrinking - passBand = pow(10.0, -4.0*smoothingFactor) # gives a nice range of 1-0.0001 from a user input of 0-1 - smoother = vtk.vtkWindowedSincPolyDataFilter() - smoother.SetInputConnection(convertToPolyData.GetOutputPort()) - smoother.SetNumberOfIterations(smoothingIterations) - smoother.BoundarySmoothingOff() - smoother.FeatureEdgeSmoothingOff() - smoother.SetFeatureAngle(90.0) - smoother.SetPassBand(passBand) - smoother.NonManifoldSmoothingOn() - smoother.NormalizeCoordinatesOn() - - # Extract a label - threshold = vtk.vtkThreshold() - threshold.SetInputConnection(smoother.GetOutputPort()) - - # Convert to polydata - geometryFilter = vtk.vtkGeometryFilter() - geometryFilter.SetInputConnection(threshold.GetOutputPort()) - - # Convert polydata to stencil - polyDataToImageStencil = vtk.vtkPolyDataToImageStencil() - polyDataToImageStencil.SetInputConnection(geometryFilter.GetOutputPort()) - polyDataToImageStencil.SetOutputSpacing(1,1,1) - polyDataToImageStencil.SetOutputOrigin(0,0,0) - polyDataToImageStencil.SetOutputWholeExtent(mergedImage.GetExtent()) - - # Convert stencil to image - stencil = vtk.vtkImageStencil() - emptyBinaryLabelMap = vtk.vtkImageData() - emptyBinaryLabelMap.SetExtent(mergedImage.GetExtent()) - emptyBinaryLabelMap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) - vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(emptyBinaryLabelMap, 0) - stencil.SetInputData(emptyBinaryLabelMap) - stencil.SetStencilConnection(polyDataToImageStencil.GetOutputPort()) - stencil.ReverseStencilOn() - stencil.SetBackgroundValue(1) # General foreground value is 1 (background value because of reverse stencil) - - imageToWorldMatrix = vtk.vtkMatrix4x4() - mergedImage.GetImageToWorldMatrix(imageToWorldMatrix) - - # TODO: Temporarily setting the overwrite mode to OverwriteVisibleSegments is an approach that should be change once additional - # layer control options have been implemented. Users may wish to keep segments on separate layers, and not allow them to be separated/merged automatically. - # This effect could leverage those options once they have been implemented. - oldOverwriteMode = self.scriptedEffect.parameterSetNode().GetOverwriteMode() - self.scriptedEffect.parameterSetNode().SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteVisibleSegments) - for segmentId, labelValue in segmentLabelValues: - threshold.ThresholdBetween(labelValue, labelValue) - stencil.Update() - smoothedBinaryLabelMap = slicer.vtkOrientedImageData() - smoothedBinaryLabelMap.ShallowCopy(stencil.GetOutput()) - smoothedBinaryLabelMap.SetImageToWorldMatrix(imageToWorldMatrix) - self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentId, smoothedBinaryLabelMap, - slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, False) - self.scriptedEffect.parameterSetNode().SetOverwriteMode(oldOverwriteMode) - - def paintApply(self, viewWidget): - - # Current limitation: smoothing brush is not implemented for joint smoothing - smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod") - if smoothingMethod == JOINT_TAUBIN: - self.scriptedEffect.clearBrushes() - self.scriptedEffect.forceRender(viewWidget) - slicer.util.messageBox("Smoothing brush is not available for 'joint smoothing' method.") - return - - modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() - maskImage = slicer.vtkOrientedImageData() - maskImage.DeepCopy(modifierLabelmap) - maskExtent = self.scriptedEffect.paintBrushesIntoLabelmap(maskImage, viewWidget) - self.scriptedEffect.clearBrushes() - self.scriptedEffect.forceRender(viewWidget) - if maskExtent[0]>maskExtent[1] or maskExtent[2]>maskExtent[3] or maskExtent[4]>maskExtent[5]: - return - - self.scriptedEffect.saveStateForUndo() - self.onApply(maskImage, maskExtent) + modifierLabelmap.DeepCopy(smoothedImage) + self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) + + def smoothSelectedSegment(self, maskImage=None, maskExtent=None): + try: + # Get modifier labelmap + modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() + selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap() + + smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod") + + if smoothingMethod == GAUSSIAN: + maxValue = 255 + radiusFactor = 4.0 + standardDeviationMM = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm") + spacing = modifierLabelmap.GetSpacing() + standardDeviationPixel = [1.0, 1.0, 1.0] + radiusPixel = [3, 3, 3] + for idx in range(3): + standardDeviationPixel[idx] = standardDeviationMM / spacing[idx] + radiusPixel[idx] = int(standardDeviationPixel[idx] * radiusFactor) + 1 + if maskExtent: + clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, radiusPixel) + else: + clippedSelectedSegmentLabelmap = selectedSegmentLabelmap + + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(clippedSelectedSegmentLabelmap) + thresh.ThresholdByLower(0) + thresh.SetInValue(0) + thresh.SetOutValue(maxValue) + thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) + + gaussianFilter = vtk.vtkImageGaussianSmooth() + gaussianFilter.SetInputConnection(thresh.GetOutputPort()) + gaussianFilter.SetStandardDeviation(*standardDeviationPixel) + gaussianFilter.SetRadiusFactor(radiusFactor) + + thresh2 = vtk.vtkImageThreshold() + thresh2.SetInputConnection(gaussianFilter.GetOutputPort()) + thresh2.ThresholdByUpper(int(maxValue / 2)) + thresh2.SetInValue(1) + thresh2.SetOutValue(0) + thresh2.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType()) + thresh2.Update() + + self.modifySelectedSegmentByLabelmap(thresh2.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent) + + else: + # size rounded to nearest odd number. If kernel size is even then image gets shifted. + kernelSizePixel = self.getKernelSizePixel() + + if maskExtent: + clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, kernelSizePixel) + else: + clippedSelectedSegmentLabelmap = selectedSegmentLabelmap + + if smoothingMethod == MEDIAN: + # Median filter does not require a particular label value + smoothingFilter = vtk.vtkImageMedian3D() + smoothingFilter.SetInputData(clippedSelectedSegmentLabelmap) + + else: + # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value + labelValue = 1 + backgroundValue = 0 + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(clippedSelectedSegmentLabelmap) + thresh.ThresholdByLower(0) + thresh.SetInValue(backgroundValue) + thresh.SetOutValue(labelValue) + thresh.SetOutputScalarType(clippedSelectedSegmentLabelmap.GetScalarType()) + + smoothingFilter = vtk.vtkImageOpenClose3D() + smoothingFilter.SetInputConnection(thresh.GetOutputPort()) + if smoothingMethod == MORPHOLOGICAL_OPENING: + smoothingFilter.SetOpenValue(labelValue) + smoothingFilter.SetCloseValue(backgroundValue) + else: # must be smoothingMethod == MORPHOLOGICAL_CLOSING: + smoothingFilter.SetOpenValue(backgroundValue) + smoothingFilter.SetCloseValue(labelValue) + + smoothingFilter.SetKernelSize(kernelSizePixel[0], kernelSizePixel[1], kernelSizePixel[2]) + smoothingFilter.Update() + + self.modifySelectedSegmentByLabelmap(smoothingFilter.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent) + + except IndexError: + logging.error('apply: Failed to apply smoothing') + + def smoothMultipleSegments(self, maskImage=None, maskExtent=None): + import vtkSegmentationCorePython as vtkSegmentationCore + + self.showStatusMessage(f'Joint smoothing ...') + # Generate merged labelmap of all visible segments + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + visibleSegmentIds = vtk.vtkStringArray() + segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) + if visibleSegmentIds.GetNumberOfValues() == 0: + logging.info("Smoothing operation skipped: there are no visible segments") + return + + mergedImage = slicer.vtkOrientedImageData() + if not segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage, + vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED, + None, visibleSegmentIds): + logging.error('Failed to apply smoothing: cannot get list of visible segments') + return + + segmentLabelValues = [] # list of [segmentId, labelValue] + for i in range(visibleSegmentIds.GetNumberOfValues()): + segmentId = visibleSegmentIds.GetValue(i) + segmentLabelValues.append([segmentId, i + 1]) + + # Perform smoothing in voxel space + ici = vtk.vtkImageChangeInformation() + ici.SetInputData(mergedImage) + ici.SetOutputSpacing(1, 1, 1) + ici.SetOutputOrigin(0, 0, 0) + + # Convert labelmap to combined polydata + # vtkDiscreteFlyingEdges3D cannot be used here, as in the output of that filter, + # each labeled region is completely disconnected from neighboring regions, and + # for joint smoothing it is essential for the points to move together. + convertToPolyData = vtk.vtkDiscreteMarchingCubes() + convertToPolyData.SetInputConnection(ici.GetOutputPort()) + convertToPolyData.SetNumberOfContours(len(segmentLabelValues)) + + contourIndex = 0 + for segmentId, labelValue in segmentLabelValues: + convertToPolyData.SetValue(contourIndex, labelValue) + contourIndex += 1 + + # Low-pass filtering using Taubin's method + smoothingFactor = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor") + smoothingIterations = 100 # according to VTK documentation 10-20 iterations could be enough but we use a higher value to reduce chance of shrinking + passBand = pow(10.0, -4.0 * smoothingFactor) # gives a nice range of 1-0.0001 from a user input of 0-1 + smoother = vtk.vtkWindowedSincPolyDataFilter() + smoother.SetInputConnection(convertToPolyData.GetOutputPort()) + smoother.SetNumberOfIterations(smoothingIterations) + smoother.BoundarySmoothingOff() + smoother.FeatureEdgeSmoothingOff() + smoother.SetFeatureAngle(90.0) + smoother.SetPassBand(passBand) + smoother.NonManifoldSmoothingOn() + smoother.NormalizeCoordinatesOn() + + # Extract a label + threshold = vtk.vtkThreshold() + threshold.SetInputConnection(smoother.GetOutputPort()) + + # Convert to polydata + geometryFilter = vtk.vtkGeometryFilter() + geometryFilter.SetInputConnection(threshold.GetOutputPort()) + + # Convert polydata to stencil + polyDataToImageStencil = vtk.vtkPolyDataToImageStencil() + polyDataToImageStencil.SetInputConnection(geometryFilter.GetOutputPort()) + polyDataToImageStencil.SetOutputSpacing(1, 1, 1) + polyDataToImageStencil.SetOutputOrigin(0, 0, 0) + polyDataToImageStencil.SetOutputWholeExtent(mergedImage.GetExtent()) + + # Convert stencil to image + stencil = vtk.vtkImageStencil() + emptyBinaryLabelMap = vtk.vtkImageData() + emptyBinaryLabelMap.SetExtent(mergedImage.GetExtent()) + emptyBinaryLabelMap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) + vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(emptyBinaryLabelMap, 0) + stencil.SetInputData(emptyBinaryLabelMap) + stencil.SetStencilConnection(polyDataToImageStencil.GetOutputPort()) + stencil.ReverseStencilOn() + stencil.SetBackgroundValue(1) # General foreground value is 1 (background value because of reverse stencil) + + imageToWorldMatrix = vtk.vtkMatrix4x4() + mergedImage.GetImageToWorldMatrix(imageToWorldMatrix) + + # TODO: Temporarily setting the overwrite mode to OverwriteVisibleSegments is an approach that should be change once additional + # layer control options have been implemented. Users may wish to keep segments on separate layers, and not allow them to be separated/merged automatically. + # This effect could leverage those options once they have been implemented. + oldOverwriteMode = self.scriptedEffect.parameterSetNode().GetOverwriteMode() + self.scriptedEffect.parameterSetNode().SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteVisibleSegments) + for segmentId, labelValue in segmentLabelValues: + threshold.ThresholdBetween(labelValue, labelValue) + stencil.Update() + smoothedBinaryLabelMap = slicer.vtkOrientedImageData() + smoothedBinaryLabelMap.ShallowCopy(stencil.GetOutput()) + smoothedBinaryLabelMap.SetImageToWorldMatrix(imageToWorldMatrix) + self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentId, smoothedBinaryLabelMap, + slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, False) + self.scriptedEffect.parameterSetNode().SetOverwriteMode(oldOverwriteMode) + + def paintApply(self, viewWidget): + + # Current limitation: smoothing brush is not implemented for joint smoothing + smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod") + if smoothingMethod == JOINT_TAUBIN: + self.scriptedEffect.clearBrushes() + self.scriptedEffect.forceRender(viewWidget) + slicer.util.messageBox("Smoothing brush is not available for 'joint smoothing' method.") + return + + modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() + maskImage = slicer.vtkOrientedImageData() + maskImage.DeepCopy(modifierLabelmap) + maskExtent = self.scriptedEffect.paintBrushesIntoLabelmap(maskImage, viewWidget) + self.scriptedEffect.clearBrushes() + self.scriptedEffect.forceRender(viewWidget) + if maskExtent[0] > maskExtent[1] or maskExtent[2] > maskExtent[3] or maskExtent[4] > maskExtent[5]: + return + + self.scriptedEffect.saveStateForUndo() + self.onApply(maskImage, maskExtent) MEDIAN = 'MEDIAN' diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorThresholdEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorThresholdEffect.py index 440b39ab662..926fdc12450 100644 --- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorThresholdEffect.py +++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorThresholdEffect.py @@ -10,962 +10,962 @@ class SegmentEditorThresholdEffect(AbstractScriptedSegmentEditorEffect): - """ ThresholdEffect is an Effect implementing the global threshold - operation in the segment editor - - This is also an example for scripted effects, and some methods have no - function. The methods that are not needed (i.e. the default implementation in - qSlicerSegmentEditorAbstractEffect is satisfactory) can simply be omitted. - """ - - def __init__(self, scriptedEffect): - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) - scriptedEffect.name = 'Threshold' - - self.segment2DFillOpacity = None - self.segment2DOutlineOpacity = None - self.previewedSegmentID = None - - # Effect-specific members - import vtkITK - self.autoThresholdCalculator = vtkITK.vtkITKImageThresholdCalculator() - - self.timer = qt.QTimer() - self.previewState = 0 - self.previewStep = 1 - self.previewSteps = 5 - self.timer.connect('timeout()', self.preview) - - self.previewPipelines = {} - self.histogramPipeline = None - self.setupPreviewDisplay() - - # Histogram stencil setup - self.stencil = vtk.vtkPolyDataToImageStencil() - - # Histogram reslice setup - self.reslice = vtk.vtkImageReslice() - self.reslice.AutoCropOutputOff() - self.reslice.SetOptimization(1) - self.reslice.SetOutputOrigin(0, 0, 0) - self.reslice.SetOutputSpacing(1, 1, 1) - self.reslice.SetOutputDimensionality(3) - self.reslice.GenerateStencilOutputOn() - - self.imageAccumulate = vtk.vtkImageAccumulate() - self.imageAccumulate.SetInputConnection(0, self.reslice.GetOutputPort()) - self.imageAccumulate.SetInputConnection(1, self.stencil.GetOutputPort()) - - self.selectionStartPosition = None - self.selectionEndPosition = None - - def clone(self): - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Threshold.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Fill segment based on master volume intensity range
        . Options:

        + """ ThresholdEffect is an Effect implementing the global threshold + operation in the segment editor + + This is also an example for scripted effects, and some methods have no + function. The methods that are not needed (i.e. the default implementation in + qSlicerSegmentEditorAbstractEffect is satisfactory) can simply be omitted. + """ + + def __init__(self, scriptedEffect): + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + scriptedEffect.name = 'Threshold' + + self.segment2DFillOpacity = None + self.segment2DOutlineOpacity = None + self.previewedSegmentID = None + + # Effect-specific members + import vtkITK + self.autoThresholdCalculator = vtkITK.vtkITKImageThresholdCalculator() + + self.timer = qt.QTimer() + self.previewState = 0 + self.previewStep = 1 + self.previewSteps = 5 + self.timer.connect('timeout()', self.preview) + + self.previewPipelines = {} + self.histogramPipeline = None + self.setupPreviewDisplay() + + # Histogram stencil setup + self.stencil = vtk.vtkPolyDataToImageStencil() + + # Histogram reslice setup + self.reslice = vtk.vtkImageReslice() + self.reslice.AutoCropOutputOff() + self.reslice.SetOptimization(1) + self.reslice.SetOutputOrigin(0, 0, 0) + self.reslice.SetOutputSpacing(1, 1, 1) + self.reslice.SetOutputDimensionality(3) + self.reslice.GenerateStencilOutputOn() + + self.imageAccumulate = vtk.vtkImageAccumulate() + self.imageAccumulate.SetInputConnection(0, self.reslice.GetOutputPort()) + self.imageAccumulate.SetInputConnection(1, self.stencil.GetOutputPort()) + + self.selectionStartPosition = None + self.selectionEndPosition = None + + def clone(self): + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Threshold.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Fill segment based on master volume intensity range
        . Options:

        • Use for masking: set the selected intensity range as Editable intensity range and switch to Paint effect.
        • Apply: set the previewed segmentation in the selected segment. Previous contents of the segment is overwritten.

        """ - def activate(self): - self.setCurrentSegmentTransparent() - - # Update intensity range - self.masterVolumeNodeChanged() - - # Setup and start preview pulse - self.setupPreviewDisplay() - self.timer.start(200) - - def deactivate(self): - self.restorePreviewedSegmentTransparency() - - # Clear preview pipeline and stop timer - self.clearPreviewDisplay() - self.clearHistogramDisplay() - self.timer.stop() + def activate(self): + self.setCurrentSegmentTransparent() + + # Update intensity range + self.masterVolumeNodeChanged() + + # Setup and start preview pulse + self.setupPreviewDisplay() + self.timer.start(200) + + def deactivate(self): + self.restorePreviewedSegmentTransparency() + + # Clear preview pipeline and stop timer + self.clearPreviewDisplay() + self.clearHistogramDisplay() + self.timer.stop() + + def setCurrentSegmentTransparent(self): + """Save current segment opacity and set it to zero + to temporarily hide the segment so that threshold preview + can be seen better. + It also restores opacity of previously previewed segment. + Call restorePreviewedSegmentTransparency() to restore original + opacity. + """ + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + if not segmentationNode: + return + displayNode = segmentationNode.GetDisplayNode() + if not displayNode: + return + segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() + + if segmentID == self.previewedSegmentID: + # already previewing the current segment + return + + # If an other segment was previewed before, restore that. + if self.previewedSegmentID: + self.restorePreviewedSegmentTransparency() + + # Make current segment fully transparent + if segmentID: + self.segment2DFillOpacity = displayNode.GetSegmentOpacity2DFill(segmentID) + self.segment2DOutlineOpacity = displayNode.GetSegmentOpacity2DOutline(segmentID) + self.previewedSegmentID = segmentID + displayNode.SetSegmentOpacity2DFill(segmentID, 0) + displayNode.SetSegmentOpacity2DOutline(segmentID, 0) + + def restorePreviewedSegmentTransparency(self): + """Restore previewed segment's opacity that was temporarily + made transparen by calling setCurrentSegmentTransparent().""" + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + if not segmentationNode: + return + displayNode = segmentationNode.GetDisplayNode() + if not displayNode: + return + if not self.previewedSegmentID: + # already previewing the current segment + return + displayNode.SetSegmentOpacity2DFill(self.previewedSegmentID, self.segment2DFillOpacity) + displayNode.SetSegmentOpacity2DOutline(self.previewedSegmentID, self.segment2DOutlineOpacity) + self.previewedSegmentID = None + + def setupOptionsFrame(self): + self.thresholdSliderLabel = qt.QLabel("Threshold Range:") + self.thresholdSliderLabel.setToolTip("Set the range of the background values that should be labeled.") + self.scriptedEffect.addOptionsWidget(self.thresholdSliderLabel) + + self.thresholdSlider = ctk.ctkRangeWidget() + self.thresholdSlider.spinBoxAlignment = qt.Qt.AlignTop + self.thresholdSlider.singleStep = 0.01 + self.scriptedEffect.addOptionsWidget(self.thresholdSlider) + + self.autoThresholdModeSelectorComboBox = qt.QComboBox() + self.autoThresholdModeSelectorComboBox.addItem("threshold above", MODE_SET_LOWER_MAX) + self.autoThresholdModeSelectorComboBox.addItem("threshold below", MODE_SET_MIN_UPPER) + self.autoThresholdModeSelectorComboBox.addItem("set as lower value", MODE_SET_LOWER) + self.autoThresholdModeSelectorComboBox.addItem("set as upper value", MODE_SET_UPPER) + self.autoThresholdModeSelectorComboBox.setToolTip("How to set lower and upper values of the threshold range." + " Threshold above/below: sets the range from the computed value to maximum/minimum." + " Set as lower/upper value: only modifies one side of the threshold range.") + + self.autoThresholdMethodSelectorComboBox = qt.QComboBox() + self.autoThresholdMethodSelectorComboBox.addItem("Otsu", METHOD_OTSU) + self.autoThresholdMethodSelectorComboBox.addItem("Huang", METHOD_HUANG) + self.autoThresholdMethodSelectorComboBox.addItem("IsoData", METHOD_ISO_DATA) + # Kittler-Illingworth sometimes fails with an exception, but it does not cause any major issue, + # it just logs an error message and does not compute a new threshold value + self.autoThresholdMethodSelectorComboBox.addItem("Kittler-Illingworth", METHOD_KITTLER_ILLINGWORTH) + # Li sometimes crashes (index out of range error in + # ITK/Modules/Filtering/Thresholding/include/itkLiThresholdCalculator.hxx#L94) + # We can add this method back when issue is fixed in ITK. + # self.autoThresholdMethodSelectorComboBox.addItem("Li", METHOD_LI) + self.autoThresholdMethodSelectorComboBox.addItem("Maximum entropy", METHOD_MAXIMUM_ENTROPY) + self.autoThresholdMethodSelectorComboBox.addItem("Moments", METHOD_MOMENTS) + self.autoThresholdMethodSelectorComboBox.addItem("Renyi entropy", METHOD_RENYI_ENTROPY) + self.autoThresholdMethodSelectorComboBox.addItem("Shanbhag", METHOD_SHANBHAG) + self.autoThresholdMethodSelectorComboBox.addItem("Triangle", METHOD_TRIANGLE) + self.autoThresholdMethodSelectorComboBox.addItem("Yen", METHOD_YEN) + self.autoThresholdMethodSelectorComboBox.setToolTip("Select method to compute threshold value automatically.") + + self.selectPreviousAutoThresholdButton = qt.QToolButton() + self.selectPreviousAutoThresholdButton.text = "<" + self.selectPreviousAutoThresholdButton.setToolTip("Select previous thresholding method and set thresholds." + + " Useful for iterating through all available methods.") + + self.selectNextAutoThresholdButton = qt.QToolButton() + self.selectNextAutoThresholdButton.text = ">" + self.selectNextAutoThresholdButton.setToolTip("Select next thresholding method and set thresholds." + + " Useful for iterating through all available methods.") + + self.setAutoThresholdButton = qt.QPushButton("Set") + self.setAutoThresholdButton.setToolTip("Set threshold using selected method.") + # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + # fails on some systems, therefore set the policies using separate method calls + qSize = qt.QSizePolicy() + qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding) + self.setAutoThresholdButton.setSizePolicy(qSize) + + autoThresholdFrame = qt.QGridLayout() + autoThresholdFrame.addWidget(self.autoThresholdMethodSelectorComboBox, 0, 0, 1, 1) + autoThresholdFrame.addWidget(self.selectPreviousAutoThresholdButton, 0, 1, 1, 1) + autoThresholdFrame.addWidget(self.selectNextAutoThresholdButton, 0, 2, 1, 1) + autoThresholdFrame.addWidget(self.autoThresholdModeSelectorComboBox, 1, 0, 1, 3) + autoThresholdFrame.addWidget(self.setAutoThresholdButton, 2, 0, 1, 3) + + autoThresholdGroupBox = ctk.ctkCollapsibleGroupBox() + autoThresholdGroupBox.setTitle("Automatic threshold") + autoThresholdGroupBox.setLayout(autoThresholdFrame) + autoThresholdGroupBox.collapsed = True + self.scriptedEffect.addOptionsWidget(autoThresholdGroupBox) + + histogramFrame = qt.QVBoxLayout() + + histogramBrushFrame = qt.QHBoxLayout() + histogramFrame.addLayout(histogramBrushFrame) + + self.regionLabel = qt.QLabel("Region shape:") + histogramBrushFrame.addWidget(self.regionLabel) + + self.histogramBrushButtonGroup = qt.QButtonGroup() + self.histogramBrushButtonGroup.setExclusive(True) + + self.boxROIButton = qt.QToolButton() + self.boxROIButton.setText("Box") + self.boxROIButton.setCheckable(True) + self.boxROIButton.clicked.connect(self.updateMRMLFromGUI) + histogramBrushFrame.addWidget(self.boxROIButton) + self.histogramBrushButtonGroup.addButton(self.boxROIButton) + + self.circleROIButton = qt.QToolButton() + self.circleROIButton.setText("Circle") + self.circleROIButton.setCheckable(True) + self.circleROIButton.clicked.connect(self.updateMRMLFromGUI) + histogramBrushFrame.addWidget(self.circleROIButton) + self.histogramBrushButtonGroup.addButton(self.circleROIButton) + + self.drawROIButton = qt.QToolButton() + self.drawROIButton.setText("Draw") + self.drawROIButton.setCheckable(True) + self.drawROIButton.clicked.connect(self.updateMRMLFromGUI) + histogramBrushFrame.addWidget(self.drawROIButton) + self.histogramBrushButtonGroup.addButton(self.drawROIButton) + + self.lineROIButton = qt.QToolButton() + self.lineROIButton.setText("Line") + self.lineROIButton.setCheckable(True) + self.lineROIButton.clicked.connect(self.updateMRMLFromGUI) + histogramBrushFrame.addWidget(self.lineROIButton) + self.histogramBrushButtonGroup.addButton(self.lineROIButton) + + histogramBrushFrame.addStretch() + + self.histogramView = ctk.ctkTransferFunctionView() + self.histogramView = self.histogramView + histogramFrame.addWidget(self.histogramView) + scene = self.histogramView.scene() + + self.histogramFunction = vtk.vtkPiecewiseFunction() + self.histogramFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect) + self.histogramFunctionContainer.setPiecewiseFunction(self.histogramFunction) + self.histogramFunctionItem = ctk.ctkTransferFunctionBarsItem(self.histogramFunctionContainer) + self.histogramFunctionItem.barWidth = 1.0 + self.histogramFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog + self.histogramFunctionItem.setZValue(1) + scene.addItem(self.histogramFunctionItem) + + self.histogramEventFilter = HistogramEventFilter() + self.histogramEventFilter.setThresholdEffect(self) + self.histogramFunctionItem.installEventFilter(self.histogramEventFilter) + + self.minMaxFunction = vtk.vtkPiecewiseFunction() + self.minMaxFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect) + self.minMaxFunctionContainer.setPiecewiseFunction(self.minMaxFunction) + self.minMaxFunctionItem = ctk.ctkTransferFunctionBarsItem(self.minMaxFunctionContainer) + self.minMaxFunctionItem.barWidth = 0.03 + self.minMaxFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog + self.minMaxFunctionItem.barColor = qt.QColor(200, 0, 0) + self.minMaxFunctionItem.setZValue(0) + scene.addItem(self.minMaxFunctionItem) + + self.averageFunction = vtk.vtkPiecewiseFunction() + self.averageFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect) + self.averageFunctionContainer.setPiecewiseFunction(self.averageFunction) + self.averageFunctionItem = ctk.ctkTransferFunctionBarsItem(self.averageFunctionContainer) + self.averageFunctionItem.barWidth = 0.03 + self.averageFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog + self.averageFunctionItem.barColor = qt.QColor(225, 150, 0) + self.averageFunctionItem.setZValue(-1) + scene.addItem(self.averageFunctionItem) + + # Window level gradient + self.backgroundColor = [1.0, 1.0, 0.7] + self.backgroundFunction = vtk.vtkColorTransferFunction() + self.backgroundFunctionContainer = ctk.ctkVTKColorTransferFunction(self.scriptedEffect) + self.backgroundFunctionContainer.setColorTransferFunction(self.backgroundFunction) + self.backgroundFunctionItem = ctk.ctkTransferFunctionGradientItem(self.backgroundFunctionContainer) + self.backgroundFunctionItem.setZValue(-2) + scene.addItem(self.backgroundFunctionItem) + + histogramItemFrame = qt.QHBoxLayout() + histogramFrame.addLayout(histogramItemFrame) + + ### + # Lower histogram threshold buttons + + lowerGroupBox = qt.QGroupBox("Lower") + lowerHistogramLayout = qt.QHBoxLayout() + lowerHistogramLayout.setContentsMargins(0, 3, 0, 3) + lowerGroupBox.setLayout(lowerHistogramLayout) + histogramItemFrame.addWidget(lowerGroupBox) + self.histogramLowerMethodButtonGroup = qt.QButtonGroup() + self.histogramLowerMethodButtonGroup.setExclusive(True) + + self.histogramLowerThresholdMinimumButton = qt.QToolButton() + self.histogramLowerThresholdMinimumButton.setText("Min") + self.histogramLowerThresholdMinimumButton.setToolTip("Minimum") + self.histogramLowerThresholdMinimumButton.setCheckable(True) + self.histogramLowerThresholdMinimumButton.clicked.connect(self.updateMRMLFromGUI) + lowerHistogramLayout.addWidget(self.histogramLowerThresholdMinimumButton) + self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdMinimumButton) + + self.histogramLowerThresholdLowerButton = qt.QToolButton() + self.histogramLowerThresholdLowerButton.setText("Lower") + self.histogramLowerThresholdLowerButton.setCheckable(True) + self.histogramLowerThresholdLowerButton.clicked.connect(self.updateMRMLFromGUI) + lowerHistogramLayout.addWidget(self.histogramLowerThresholdLowerButton) + self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdLowerButton) + + self.histogramLowerThresholdAverageButton = qt.QToolButton() + self.histogramLowerThresholdAverageButton.setText("Mean") + self.histogramLowerThresholdAverageButton.setCheckable(True) + self.histogramLowerThresholdAverageButton.clicked.connect(self.updateMRMLFromGUI) + lowerHistogramLayout.addWidget(self.histogramLowerThresholdAverageButton) + self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdAverageButton) + + ### + # Upper histogram threshold buttons + + upperGroupBox = qt.QGroupBox("Upper") + upperHistogramLayout = qt.QHBoxLayout() + upperHistogramLayout.setContentsMargins(0, 3, 0, 3) + upperGroupBox.setLayout(upperHistogramLayout) + histogramItemFrame.addWidget(upperGroupBox) + self.histogramUpperMethodButtonGroup = qt.QButtonGroup() + self.histogramUpperMethodButtonGroup.setExclusive(True) + + self.histogramUpperThresholdAverageButton = qt.QToolButton() + self.histogramUpperThresholdAverageButton.setText("Mean") + self.histogramUpperThresholdAverageButton.setCheckable(True) + self.histogramUpperThresholdAverageButton.clicked.connect(self.updateMRMLFromGUI) + upperHistogramLayout.addWidget(self.histogramUpperThresholdAverageButton) + self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdAverageButton) + + self.histogramUpperThresholdUpperButton = qt.QToolButton() + self.histogramUpperThresholdUpperButton.setText("Upper") + self.histogramUpperThresholdUpperButton.setCheckable(True) + self.histogramUpperThresholdUpperButton.clicked.connect(self.updateMRMLFromGUI) + upperHistogramLayout.addWidget(self.histogramUpperThresholdUpperButton) + self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdUpperButton) + + self.histogramUpperThresholdMaximumButton = qt.QToolButton() + self.histogramUpperThresholdMaximumButton.setText("Max") + self.histogramUpperThresholdMaximumButton.setToolTip("Maximum") + self.histogramUpperThresholdMaximumButton.setCheckable(True) + self.histogramUpperThresholdMaximumButton.clicked.connect(self.updateMRMLFromGUI) + upperHistogramLayout.addWidget(self.histogramUpperThresholdMaximumButton) + self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdMaximumButton) + + histogramGroupBox = ctk.ctkCollapsibleGroupBox() + histogramGroupBox.setTitle("Local histogram") + histogramGroupBox.setLayout(histogramFrame) + histogramGroupBox.collapsed = True + self.scriptedEffect.addOptionsWidget(histogramGroupBox) + + self.useForPaintButton = qt.QPushButton("Use for masking") + self.useForPaintButton.setToolTip("Use specified intensity range for masking and switch to Paint effect.") + self.scriptedEffect.addOptionsWidget(self.useForPaintButton) + + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + self.applyButton.setToolTip("Fill selected segment in regions that are in the specified intensity range.") + self.scriptedEffect.addOptionsWidget(self.applyButton) + + self.useForPaintButton.connect('clicked()', self.onUseForPaint) + self.thresholdSlider.connect('valuesChanged(double,double)', self.onThresholdValuesChanged) + self.autoThresholdMethodSelectorComboBox.connect("activated(int)", self.onSelectedAutoThresholdMethod) + self.autoThresholdModeSelectorComboBox.connect("activated(int)", self.onSelectedAutoThresholdMethod) + self.selectPreviousAutoThresholdButton.connect('clicked()', self.onSelectPreviousAutoThresholdMethod) + self.selectNextAutoThresholdButton.connect('clicked()', self.onSelectNextAutoThresholdMethod) + self.setAutoThresholdButton.connect('clicked()', self.onAutoThreshold) + self.applyButton.connect('clicked()', self.onApply) + + def masterVolumeNodeChanged(self): + # Set scalar range of master volume image data to threshold slider + masterImageData = self.scriptedEffect.masterVolumeImageData() + if masterImageData: + lo, hi = masterImageData.GetScalarRange() + self.thresholdSlider.setRange(lo, hi) + self.thresholdSlider.singleStep = (hi - lo) / 1000. + if (self.scriptedEffect.doubleParameter("MinimumThreshold") == self.scriptedEffect.doubleParameter("MaximumThreshold")): + # has not been initialized yet + self.scriptedEffect.setParameter("MinimumThreshold", lo + (hi - lo) * 0.25) + self.scriptedEffect.setParameter("MaximumThreshold", hi) + + def layoutChanged(self): + self.setupPreviewDisplay() + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("MinimumThreshold", 0.) + self.scriptedEffect.setParameterDefault("MaximumThreshold", 0) + self.scriptedEffect.setParameterDefault("AutoThresholdMethod", METHOD_OTSU) + self.scriptedEffect.setParameterDefault("AutoThresholdMode", MODE_SET_LOWER_MAX) + self.scriptedEffect.setParameterDefault(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, HISTOGRAM_BRUSH_TYPE_CIRCLE) + self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_LOWER_PARAMETER_NAME, HISTOGRAM_SET_LOWER) + self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_UPPER_PARAMETER_NAME, HISTOGRAM_SET_UPPER) + + def updateGUIFromMRML(self): + self.thresholdSlider.blockSignals(True) + self.thresholdSlider.setMinimumValue(self.scriptedEffect.doubleParameter("MinimumThreshold")) + self.thresholdSlider.setMaximumValue(self.scriptedEffect.doubleParameter("MaximumThreshold")) + self.thresholdSlider.blockSignals(False) + + autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMethod")) + wasBlocked = self.autoThresholdMethodSelectorComboBox.blockSignals(True) + self.autoThresholdMethodSelectorComboBox.setCurrentIndex(autoThresholdMethod) + self.autoThresholdMethodSelectorComboBox.blockSignals(wasBlocked) + + autoThresholdMode = self.autoThresholdModeSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMode")) + wasBlocked = self.autoThresholdModeSelectorComboBox.blockSignals(True) + self.autoThresholdModeSelectorComboBox.setCurrentIndex(autoThresholdMode) + self.autoThresholdModeSelectorComboBox.blockSignals(wasBlocked) + + histogramBrushType = self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME) + self.boxROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_BOX) + self.circleROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_CIRCLE) + self.drawROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_DRAW) + self.lineROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_LINE) + + histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME) + self.histogramLowerThresholdMinimumButton.checked = (histogramSetModeLower == HISTOGRAM_SET_MINIMUM) + self.histogramLowerThresholdLowerButton.checked = (histogramSetModeLower == HISTOGRAM_SET_LOWER) + self.histogramLowerThresholdAverageButton.checked = (histogramSetModeLower == HISTOGRAM_SET_AVERAGE) + + histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME) + self.histogramUpperThresholdAverageButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_AVERAGE) + self.histogramUpperThresholdUpperButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_UPPER) + self.histogramUpperThresholdMaximumButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM) + + self.updateHistogramBackground() + + def updateMRMLFromGUI(self): + with slicer.util.NodeModify(self.scriptedEffect.parameterSetNode()): + self.scriptedEffect.setParameter("MinimumThreshold", self.thresholdSlider.minimumValue) + self.scriptedEffect.setParameter("MaximumThreshold", self.thresholdSlider.maximumValue) + + methodIndex = self.autoThresholdMethodSelectorComboBox.currentIndex + autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.itemData(methodIndex) + self.scriptedEffect.setParameter("AutoThresholdMethod", autoThresholdMethod) + + modeIndex = self.autoThresholdModeSelectorComboBox.currentIndex + autoThresholdMode = self.autoThresholdModeSelectorComboBox.itemData(modeIndex) + self.scriptedEffect.setParameter("AutoThresholdMode", autoThresholdMode) + + histogramParameterChanged = False + + histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE + if self.boxROIButton.checked: + histogramBrushType = HISTOGRAM_BRUSH_TYPE_BOX + elif self.circleROIButton.checked: + histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE + elif self.drawROIButton.checked: + histogramBrushType = HISTOGRAM_BRUSH_TYPE_DRAW + elif self.lineROIButton.checked: + histogramBrushType = HISTOGRAM_BRUSH_TYPE_LINE + + if histogramBrushType != self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME): + self.scriptedEffect.setParameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, histogramBrushType) + histogramParameterChanged = True + + histogramSetModeLower = HISTOGRAM_SET_LOWER + if self.histogramLowerThresholdMinimumButton.checked: + histogramSetModeLower = HISTOGRAM_SET_MINIMUM + elif self.histogramLowerThresholdLowerButton.checked: + histogramSetModeLower = HISTOGRAM_SET_LOWER + elif self.histogramLowerThresholdAverageButton.checked: + histogramSetModeLower = HISTOGRAM_SET_AVERAGE + if histogramSetModeLower != self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME): + self.scriptedEffect.setParameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME, histogramSetModeLower) + histogramParameterChanged = True + + histogramSetModeUpper = HISTOGRAM_SET_UPPER + if self.histogramUpperThresholdAverageButton.checked: + histogramSetModeUpper = HISTOGRAM_SET_AVERAGE + elif self.histogramUpperThresholdUpperButton.checked: + histogramSetModeUpper = HISTOGRAM_SET_UPPER + elif self.histogramUpperThresholdMaximumButton.checked: + histogramSetModeUpper = HISTOGRAM_SET_MAXIMUM + if histogramSetModeUpper != self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME): + self.scriptedEffect.setParameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME, histogramSetModeUpper) + histogramParameterChanged = True + + if histogramParameterChanged: + self.updateHistogram() + + # + # Effect specific methods (the above ones are the API methods to override) + # + def onThresholdValuesChanged(self, min, max): + self.scriptedEffect.updateMRMLFromGUI() + + def onUseForPaint(self): + parameterSetNode = self.scriptedEffect.parameterSetNode() + parameterSetNode.MasterVolumeIntensityMaskOn() + parameterSetNode.SetMasterVolumeIntensityMaskRange(self.thresholdSlider.minimumValue, self.thresholdSlider.maximumValue) + # Switch to paint effect + self.scriptedEffect.selectEffect("Paint") + + def onSelectPreviousAutoThresholdMethod(self): + self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex - 1) \ + % self.autoThresholdMethodSelectorComboBox.count + self.onSelectedAutoThresholdMethod() + + def onSelectNextAutoThresholdMethod(self): + self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex + 1) \ + % self.autoThresholdMethodSelectorComboBox.count + self.onSelectedAutoThresholdMethod() + + def onSelectedAutoThresholdMethod(self): + self.updateMRMLFromGUI() + self.onAutoThreshold() + self.updateGUIFromMRML() + + def onAutoThreshold(self): + autoThresholdMethod = self.scriptedEffect.parameter("AutoThresholdMethod") + autoThresholdMode = self.scriptedEffect.parameter("AutoThresholdMode") + self.autoThreshold(autoThresholdMethod, autoThresholdMode) + + def autoThreshold(self, autoThresholdMethod, autoThresholdMode): + if autoThresholdMethod == METHOD_HUANG: + self.autoThresholdCalculator.SetMethodToHuang() + elif autoThresholdMethod == METHOD_INTERMODES: + self.autoThresholdCalculator.SetMethodToIntermodes() + elif autoThresholdMethod == METHOD_ISO_DATA: + self.autoThresholdCalculator.SetMethodToIsoData() + elif autoThresholdMethod == METHOD_KITTLER_ILLINGWORTH: + self.autoThresholdCalculator.SetMethodToKittlerIllingworth() + elif autoThresholdMethod == METHOD_LI: + self.autoThresholdCalculator.SetMethodToLi() + elif autoThresholdMethod == METHOD_MAXIMUM_ENTROPY: + self.autoThresholdCalculator.SetMethodToMaximumEntropy() + elif autoThresholdMethod == METHOD_MOMENTS: + self.autoThresholdCalculator.SetMethodToMoments() + elif autoThresholdMethod == METHOD_OTSU: + self.autoThresholdCalculator.SetMethodToOtsu() + elif autoThresholdMethod == METHOD_RENYI_ENTROPY: + self.autoThresholdCalculator.SetMethodToRenyiEntropy() + elif autoThresholdMethod == METHOD_SHANBHAG: + self.autoThresholdCalculator.SetMethodToShanbhag() + elif autoThresholdMethod == METHOD_TRIANGLE: + self.autoThresholdCalculator.SetMethodToTriangle() + elif autoThresholdMethod == METHOD_YEN: + self.autoThresholdCalculator.SetMethodToYen() + else: + logging.error(f"Unknown AutoThresholdMethod {autoThresholdMethod}") + + masterImageData = self.scriptedEffect.masterVolumeImageData() + self.autoThresholdCalculator.SetInputData(masterImageData) + + self.autoThresholdCalculator.Update() + computedThreshold = self.autoThresholdCalculator.GetThreshold() + + masterVolumeMin, masterVolumeMax = masterImageData.GetScalarRange() + + if autoThresholdMode == MODE_SET_UPPER: + self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold) + elif autoThresholdMode == MODE_SET_LOWER: + self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold) + elif autoThresholdMode == MODE_SET_MIN_UPPER: + self.scriptedEffect.setParameter("MinimumThreshold", masterVolumeMin) + self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold) + elif autoThresholdMode == MODE_SET_LOWER_MAX: + self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold) + self.scriptedEffect.setParameter("MaximumThreshold", masterVolumeMax) + else: + logging.error(f"Unknown AutoThresholdMode {autoThresholdMode}") + + def onApply(self): + if not self.scriptedEffect.confirmCurrentSegmentVisible(): + return + + try: + # Get master volume image data + masterImageData = self.scriptedEffect.masterVolumeImageData() + # Get modifier labelmap + modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() + originalImageToWorldMatrix = vtk.vtkMatrix4x4() + modifierLabelmap.GetImageToWorldMatrix(originalImageToWorldMatrix) + # Get parameters + min = self.scriptedEffect.doubleParameter("MinimumThreshold") + max = self.scriptedEffect.doubleParameter("MaximumThreshold") + + self.scriptedEffect.saveStateForUndo() + + # Perform thresholding + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(masterImageData) + thresh.ThresholdBetween(min, max) + thresh.SetInValue(1) + thresh.SetOutValue(0) + thresh.SetOutputScalarType(modifierLabelmap.GetScalarType()) + thresh.Update() + modifierLabelmap.DeepCopy(thresh.GetOutput()) + except IndexError: + logging.error('apply: Failed to threshold master volume!') + pass + + # Apply changes + self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) + + # De-select effect + self.scriptedEffect.selectEffect("") + + def clearPreviewDisplay(self): + for sliceWidget, pipeline in self.previewPipelines.items(): + self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor) + self.previewPipelines = {} + + def clearHistogramDisplay(self): + if self.histogramPipeline is None: + return + self.histogramPipeline.removeActors() + self.histogramPipeline = None + + def setupPreviewDisplay(self): + # Clear previous pipelines before setting up the new ones + self.clearPreviewDisplay() + + layoutManager = slicer.app.layoutManager() + if layoutManager is None: + return + + # Add a pipeline for each 2D slice view + for sliceViewName in layoutManager.sliceViewNames(): + sliceWidget = layoutManager.sliceWidget(sliceViewName) + if not self.scriptedEffect.segmentationDisplayableInView(sliceWidget.mrmlSliceNode()): + continue + renderer = self.scriptedEffect.renderer(sliceWidget) + if renderer is None: + logging.error("setupPreviewDisplay: Failed to get renderer!") + continue + + # Create pipeline + pipeline = PreviewPipeline() + self.previewPipelines[sliceWidget] = pipeline + + # Add actor + self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor) + + def preview(self): + + opacity = 0.5 + self.previewState / (2. * self.previewSteps) + min = self.scriptedEffect.doubleParameter("MinimumThreshold") + max = self.scriptedEffect.doubleParameter("MaximumThreshold") + + # Get color of edited segment + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + if not segmentationNode: + # scene was closed while preview was active + return + displayNode = segmentationNode.GetDisplayNode() + if displayNode is None: + logging.error("preview: Invalid segmentation display node!") + color = [0.5, 0.5, 0.5] + segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() + + # Make sure we keep the currently selected segment hidden (the user may have changed selection) + if segmentID != self.previewedSegmentID: + self.setCurrentSegmentTransparent() + + r, g, b = segmentationNode.GetSegmentation().GetSegment(segmentID).GetColor() + + # Set values to pipelines + for sliceWidget in self.previewPipelines: + pipeline = self.previewPipelines[sliceWidget] + pipeline.lookupTable.SetTableValue(1, r, g, b, opacity) + layerLogic = self.getMasterVolumeLayerLogic(sliceWidget) + pipeline.thresholdFilter.SetInputConnection(layerLogic.GetReslice().GetOutputPort()) + pipeline.thresholdFilter.ThresholdBetween(min, max) + pipeline.actor.VisibilityOn() + sliceWidget.sliceView().scheduleRender() + + self.previewState += self.previewStep + if self.previewState >= self.previewSteps: + self.previewStep = -1 + if self.previewState <= 0: + self.previewStep = 1 + + def processInteractionEvents(self, callerInteractor, eventId, viewWidget): + abortEvent = False + + masterImageData = self.scriptedEffect.masterVolumeImageData() + if masterImageData is None: + return abortEvent + + # Only allow for slice views + if viewWidget.className() != "qMRMLSliceWidget": + return abortEvent + + anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey() + + # Clicking in a view should remove all previous pipelines + if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed: + self.clearHistogramDisplay() + + if self.histogramPipeline is None: + self.createHistogramPipeline(viewWidget) + + xy = callerInteractor.GetEventPosition() + ras = self.xyToRas(xy, viewWidget) + + if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed: + self.histogramPipeline.state = HISTOGRAM_STATE_MOVING + self.histogramPipeline.addPoint(ras) + self.updateHistogram() + abortEvent = True + elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent: + if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING: + self.histogramPipeline.state = HISTOGRAM_STATE_PLACED + abortEvent = True + elif eventId == vtk.vtkCommand.MouseMoveEvent: + if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING: + self.histogramPipeline.addPoint(ras) + self.updateHistogram() + return abortEvent + + def createHistogramPipeline(self, sliceWidget): + brushType = HISTOGRAM_BRUSH_TYPE_CIRCLE + if self.boxROIButton.checked: + brushType = HISTOGRAM_BRUSH_TYPE_BOX + elif self.drawROIButton.checked: + brushType = HISTOGRAM_BRUSH_TYPE_DRAW + elif self.lineROIButton.checked: + brushType = HISTOGRAM_BRUSH_TYPE_LINE + pipeline = HistogramPipeline(self, self.scriptedEffect, sliceWidget, brushType) + self.histogramPipeline = pipeline + + def processViewNodeEvents(self, callerViewNode, eventId, viewWidget): + if self.histogramPipeline is not None: + self.histogramPipeline.updateBrushModel() + + def onHistogramMouseClick(self, pos, button): + self.selectionStartPosition = pos + self.selectionEndPosition = pos + if (button == qt.Qt.RightButton): + self.selectionStartPosition = None + self.selectionEndPosition = None + self.minMaxFunction.RemoveAllPoints() + self.averageFunction.RemoveAllPoints() + self.updateHistogram() - def setCurrentSegmentTransparent(self): - """Save current segment opacity and set it to zero - to temporarily hide the segment so that threshold preview - can be seen better. - It also restores opacity of previously previewed segment. - Call restorePreviewedSegmentTransparency() to restore original - opacity. - """ - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - if not segmentationNode: - return - displayNode = segmentationNode.GetDisplayNode() - if not displayNode: - return - segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() - - if segmentID == self.previewedSegmentID: - # already previewing the current segment - return - - # If an other segment was previewed before, restore that. - if self.previewedSegmentID: - self.restorePreviewedSegmentTransparency() - - # Make current segment fully transparent - if segmentID: - self.segment2DFillOpacity = displayNode.GetSegmentOpacity2DFill(segmentID) - self.segment2DOutlineOpacity = displayNode.GetSegmentOpacity2DOutline(segmentID) - self.previewedSegmentID = segmentID - displayNode.SetSegmentOpacity2DFill(segmentID, 0) - displayNode.SetSegmentOpacity2DOutline(segmentID, 0) - - def restorePreviewedSegmentTransparency(self): - """Restore previewed segment's opacity that was temporarily - made transparen by calling setCurrentSegmentTransparent().""" - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - if not segmentationNode: - return - displayNode = segmentationNode.GetDisplayNode() - if not displayNode: - return - if not self.previewedSegmentID: - # already previewing the current segment - return - displayNode.SetSegmentOpacity2DFill(self.previewedSegmentID, self.segment2DFillOpacity) - displayNode.SetSegmentOpacity2DOutline(self.previewedSegmentID, self.segment2DOutlineOpacity) - self.previewedSegmentID = None - - def setupOptionsFrame(self): - self.thresholdSliderLabel = qt.QLabel("Threshold Range:") - self.thresholdSliderLabel.setToolTip("Set the range of the background values that should be labeled.") - self.scriptedEffect.addOptionsWidget(self.thresholdSliderLabel) - - self.thresholdSlider = ctk.ctkRangeWidget() - self.thresholdSlider.spinBoxAlignment = qt.Qt.AlignTop - self.thresholdSlider.singleStep = 0.01 - self.scriptedEffect.addOptionsWidget(self.thresholdSlider) - - self.autoThresholdModeSelectorComboBox = qt.QComboBox() - self.autoThresholdModeSelectorComboBox.addItem("threshold above", MODE_SET_LOWER_MAX) - self.autoThresholdModeSelectorComboBox.addItem("threshold below", MODE_SET_MIN_UPPER) - self.autoThresholdModeSelectorComboBox.addItem("set as lower value", MODE_SET_LOWER) - self.autoThresholdModeSelectorComboBox.addItem("set as upper value", MODE_SET_UPPER) - self.autoThresholdModeSelectorComboBox.setToolTip("How to set lower and upper values of the threshold range." - " Threshold above/below: sets the range from the computed value to maximum/minimum." - " Set as lower/upper value: only modifies one side of the threshold range.") - - self.autoThresholdMethodSelectorComboBox = qt.QComboBox() - self.autoThresholdMethodSelectorComboBox.addItem("Otsu", METHOD_OTSU) - self.autoThresholdMethodSelectorComboBox.addItem("Huang", METHOD_HUANG) - self.autoThresholdMethodSelectorComboBox.addItem("IsoData", METHOD_ISO_DATA) - # Kittler-Illingworth sometimes fails with an exception, but it does not cause any major issue, - # it just logs an error message and does not compute a new threshold value - self.autoThresholdMethodSelectorComboBox.addItem("Kittler-Illingworth", METHOD_KITTLER_ILLINGWORTH) - # Li sometimes crashes (index out of range error in - # ITK/Modules/Filtering/Thresholding/include/itkLiThresholdCalculator.hxx#L94) - # We can add this method back when issue is fixed in ITK. - #self.autoThresholdMethodSelectorComboBox.addItem("Li", METHOD_LI) - self.autoThresholdMethodSelectorComboBox.addItem("Maximum entropy", METHOD_MAXIMUM_ENTROPY) - self.autoThresholdMethodSelectorComboBox.addItem("Moments", METHOD_MOMENTS) - self.autoThresholdMethodSelectorComboBox.addItem("Renyi entropy", METHOD_RENYI_ENTROPY) - self.autoThresholdMethodSelectorComboBox.addItem("Shanbhag", METHOD_SHANBHAG) - self.autoThresholdMethodSelectorComboBox.addItem("Triangle", METHOD_TRIANGLE) - self.autoThresholdMethodSelectorComboBox.addItem("Yen", METHOD_YEN) - self.autoThresholdMethodSelectorComboBox.setToolTip("Select method to compute threshold value automatically.") - - self.selectPreviousAutoThresholdButton = qt.QToolButton() - self.selectPreviousAutoThresholdButton.text = "<" - self.selectPreviousAutoThresholdButton.setToolTip("Select previous thresholding method and set thresholds." - +" Useful for iterating through all available methods.") - - self.selectNextAutoThresholdButton = qt.QToolButton() - self.selectNextAutoThresholdButton.text = ">" - self.selectNextAutoThresholdButton.setToolTip("Select next thresholding method and set thresholds." - +" Useful for iterating through all available methods.") - - self.setAutoThresholdButton = qt.QPushButton("Set") - self.setAutoThresholdButton.setToolTip("Set threshold using selected method.") - # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) - # fails on some systems, therefore set the policies using separate method calls - qSize = qt.QSizePolicy() - qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding) - self.setAutoThresholdButton.setSizePolicy(qSize) - - autoThresholdFrame = qt.QGridLayout() - autoThresholdFrame.addWidget(self.autoThresholdMethodSelectorComboBox, 0, 0, 1, 1) - autoThresholdFrame.addWidget(self.selectPreviousAutoThresholdButton, 0, 1, 1, 1) - autoThresholdFrame.addWidget(self.selectNextAutoThresholdButton, 0, 2, 1, 1) - autoThresholdFrame.addWidget(self.autoThresholdModeSelectorComboBox, 1, 0, 1, 3) - autoThresholdFrame.addWidget(self.setAutoThresholdButton, 2, 0, 1, 3) - - autoThresholdGroupBox = ctk.ctkCollapsibleGroupBox() - autoThresholdGroupBox.setTitle("Automatic threshold") - autoThresholdGroupBox.setLayout(autoThresholdFrame) - autoThresholdGroupBox.collapsed = True - self.scriptedEffect.addOptionsWidget(autoThresholdGroupBox) - - histogramFrame = qt.QVBoxLayout() - - histogramBrushFrame = qt.QHBoxLayout() - histogramFrame.addLayout(histogramBrushFrame) - - self.regionLabel = qt.QLabel("Region shape:") - histogramBrushFrame.addWidget(self.regionLabel) - - self.histogramBrushButtonGroup = qt.QButtonGroup() - self.histogramBrushButtonGroup.setExclusive(True) - - self.boxROIButton = qt.QToolButton() - self.boxROIButton.setText("Box") - self.boxROIButton.setCheckable(True) - self.boxROIButton.clicked.connect(self.updateMRMLFromGUI) - histogramBrushFrame.addWidget(self.boxROIButton) - self.histogramBrushButtonGroup.addButton(self.boxROIButton) - - self.circleROIButton = qt.QToolButton() - self.circleROIButton.setText("Circle") - self.circleROIButton.setCheckable(True) - self.circleROIButton.clicked.connect(self.updateMRMLFromGUI) - histogramBrushFrame.addWidget(self.circleROIButton) - self.histogramBrushButtonGroup.addButton(self.circleROIButton) - - self.drawROIButton = qt.QToolButton() - self.drawROIButton.setText("Draw") - self.drawROIButton.setCheckable(True) - self.drawROIButton.clicked.connect(self.updateMRMLFromGUI) - histogramBrushFrame.addWidget(self.drawROIButton) - self.histogramBrushButtonGroup.addButton(self.drawROIButton) - - self.lineROIButton = qt.QToolButton() - self.lineROIButton.setText("Line") - self.lineROIButton.setCheckable(True) - self.lineROIButton.clicked.connect(self.updateMRMLFromGUI) - histogramBrushFrame.addWidget(self.lineROIButton) - self.histogramBrushButtonGroup.addButton(self.lineROIButton) - - histogramBrushFrame.addStretch() - - self.histogramView = ctk.ctkTransferFunctionView() - self.histogramView = self.histogramView - histogramFrame.addWidget(self.histogramView) - scene = self.histogramView.scene() - - self.histogramFunction = vtk.vtkPiecewiseFunction() - self.histogramFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect) - self.histogramFunctionContainer.setPiecewiseFunction(self.histogramFunction) - self.histogramFunctionItem = ctk.ctkTransferFunctionBarsItem(self.histogramFunctionContainer) - self.histogramFunctionItem.barWidth = 1.0 - self.histogramFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog - self.histogramFunctionItem.setZValue(1) - scene.addItem(self.histogramFunctionItem) - - self.histogramEventFilter = HistogramEventFilter() - self.histogramEventFilter.setThresholdEffect(self) - self.histogramFunctionItem.installEventFilter(self.histogramEventFilter) - - self.minMaxFunction = vtk.vtkPiecewiseFunction() - self.minMaxFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect) - self.minMaxFunctionContainer.setPiecewiseFunction(self.minMaxFunction) - self.minMaxFunctionItem = ctk.ctkTransferFunctionBarsItem(self.minMaxFunctionContainer) - self.minMaxFunctionItem.barWidth = 0.03 - self.minMaxFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog - self.minMaxFunctionItem.barColor = qt.QColor(200, 0, 0) - self.minMaxFunctionItem.setZValue(0) - scene.addItem(self.minMaxFunctionItem) - - self.averageFunction = vtk.vtkPiecewiseFunction() - self.averageFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect) - self.averageFunctionContainer.setPiecewiseFunction(self.averageFunction) - self.averageFunctionItem = ctk.ctkTransferFunctionBarsItem(self.averageFunctionContainer) - self.averageFunctionItem.barWidth = 0.03 - self.averageFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog - self.averageFunctionItem.barColor = qt.QColor(225, 150, 0) - self.averageFunctionItem.setZValue(-1) - scene.addItem(self.averageFunctionItem) - - # Window level gradient - self.backgroundColor = [1.0, 1.0, 0.7] - self.backgroundFunction = vtk.vtkColorTransferFunction() - self.backgroundFunctionContainer = ctk.ctkVTKColorTransferFunction(self.scriptedEffect) - self.backgroundFunctionContainer.setColorTransferFunction(self.backgroundFunction) - self.backgroundFunctionItem = ctk.ctkTransferFunctionGradientItem(self.backgroundFunctionContainer) - self.backgroundFunctionItem.setZValue(-2) - scene.addItem(self.backgroundFunctionItem) - - histogramItemFrame = qt.QHBoxLayout() - histogramFrame.addLayout(histogramItemFrame) - - ### - # Lower histogram threshold buttons - - lowerGroupBox = qt.QGroupBox("Lower") - lowerHistogramLayout = qt.QHBoxLayout() - lowerHistogramLayout.setContentsMargins(0,3,0,3) - lowerGroupBox.setLayout(lowerHistogramLayout) - histogramItemFrame.addWidget(lowerGroupBox) - self.histogramLowerMethodButtonGroup = qt.QButtonGroup() - self.histogramLowerMethodButtonGroup.setExclusive(True) - - self.histogramLowerThresholdMinimumButton = qt.QToolButton() - self.histogramLowerThresholdMinimumButton.setText("Min") - self.histogramLowerThresholdMinimumButton.setToolTip("Minimum") - self.histogramLowerThresholdMinimumButton.setCheckable(True) - self.histogramLowerThresholdMinimumButton.clicked.connect(self.updateMRMLFromGUI) - lowerHistogramLayout.addWidget(self.histogramLowerThresholdMinimumButton) - self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdMinimumButton) - - self.histogramLowerThresholdLowerButton = qt.QToolButton() - self.histogramLowerThresholdLowerButton.setText("Lower") - self.histogramLowerThresholdLowerButton.setCheckable(True) - self.histogramLowerThresholdLowerButton.clicked.connect(self.updateMRMLFromGUI) - lowerHistogramLayout.addWidget(self.histogramLowerThresholdLowerButton) - self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdLowerButton) - - self.histogramLowerThresholdAverageButton = qt.QToolButton() - self.histogramLowerThresholdAverageButton.setText("Mean") - self.histogramLowerThresholdAverageButton.setCheckable(True) - self.histogramLowerThresholdAverageButton.clicked.connect(self.updateMRMLFromGUI) - lowerHistogramLayout.addWidget(self.histogramLowerThresholdAverageButton) - self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdAverageButton) - - ### - # Upper histogram threshold buttons - - upperGroupBox = qt.QGroupBox("Upper") - upperHistogramLayout = qt.QHBoxLayout() - upperHistogramLayout.setContentsMargins(0,3,0,3) - upperGroupBox.setLayout(upperHistogramLayout) - histogramItemFrame.addWidget(upperGroupBox) - self.histogramUpperMethodButtonGroup = qt.QButtonGroup() - self.histogramUpperMethodButtonGroup.setExclusive(True) - - self.histogramUpperThresholdAverageButton = qt.QToolButton() - self.histogramUpperThresholdAverageButton.setText("Mean") - self.histogramUpperThresholdAverageButton.setCheckable(True) - self.histogramUpperThresholdAverageButton.clicked.connect(self.updateMRMLFromGUI) - upperHistogramLayout.addWidget(self.histogramUpperThresholdAverageButton) - self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdAverageButton) - - self.histogramUpperThresholdUpperButton = qt.QToolButton() - self.histogramUpperThresholdUpperButton.setText("Upper") - self.histogramUpperThresholdUpperButton.setCheckable(True) - self.histogramUpperThresholdUpperButton.clicked.connect(self.updateMRMLFromGUI) - upperHistogramLayout.addWidget(self.histogramUpperThresholdUpperButton) - self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdUpperButton) - - self.histogramUpperThresholdMaximumButton = qt.QToolButton() - self.histogramUpperThresholdMaximumButton.setText("Max") - self.histogramUpperThresholdMaximumButton.setToolTip("Maximum") - self.histogramUpperThresholdMaximumButton.setCheckable(True) - self.histogramUpperThresholdMaximumButton.clicked.connect(self.updateMRMLFromGUI) - upperHistogramLayout.addWidget(self.histogramUpperThresholdMaximumButton) - self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdMaximumButton) - - histogramGroupBox = ctk.ctkCollapsibleGroupBox() - histogramGroupBox.setTitle("Local histogram") - histogramGroupBox.setLayout(histogramFrame) - histogramGroupBox.collapsed = True - self.scriptedEffect.addOptionsWidget(histogramGroupBox) - - self.useForPaintButton = qt.QPushButton("Use for masking") - self.useForPaintButton.setToolTip("Use specified intensity range for masking and switch to Paint effect.") - self.scriptedEffect.addOptionsWidget(self.useForPaintButton) - - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - self.applyButton.setToolTip("Fill selected segment in regions that are in the specified intensity range.") - self.scriptedEffect.addOptionsWidget(self.applyButton) - - self.useForPaintButton.connect('clicked()', self.onUseForPaint) - self.thresholdSlider.connect('valuesChanged(double,double)', self.onThresholdValuesChanged) - self.autoThresholdMethodSelectorComboBox.connect("activated(int)", self.onSelectedAutoThresholdMethod) - self.autoThresholdModeSelectorComboBox.connect("activated(int)", self.onSelectedAutoThresholdMethod) - self.selectPreviousAutoThresholdButton.connect('clicked()', self.onSelectPreviousAutoThresholdMethod) - self.selectNextAutoThresholdButton.connect('clicked()', self.onSelectNextAutoThresholdMethod) - self.setAutoThresholdButton.connect('clicked()', self.onAutoThreshold) - self.applyButton.connect('clicked()', self.onApply) - - def masterVolumeNodeChanged(self): - # Set scalar range of master volume image data to threshold slider - masterImageData = self.scriptedEffect.masterVolumeImageData() - if masterImageData: - lo, hi = masterImageData.GetScalarRange() - self.thresholdSlider.setRange(lo, hi) - self.thresholdSlider.singleStep = (hi - lo) / 1000. - if (self.scriptedEffect.doubleParameter("MinimumThreshold") == self.scriptedEffect.doubleParameter("MaximumThreshold")): - # has not been initialized yet - self.scriptedEffect.setParameter("MinimumThreshold", lo+(hi-lo)*0.25) - self.scriptedEffect.setParameter("MaximumThreshold", hi) - - def layoutChanged(self): - self.setupPreviewDisplay() - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("MinimumThreshold", 0.) - self.scriptedEffect.setParameterDefault("MaximumThreshold", 0) - self.scriptedEffect.setParameterDefault("AutoThresholdMethod", METHOD_OTSU) - self.scriptedEffect.setParameterDefault("AutoThresholdMode", MODE_SET_LOWER_MAX) - self.scriptedEffect.setParameterDefault(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, HISTOGRAM_BRUSH_TYPE_CIRCLE) - self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_LOWER_PARAMETER_NAME, HISTOGRAM_SET_LOWER) - self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_UPPER_PARAMETER_NAME, HISTOGRAM_SET_UPPER) - - def updateGUIFromMRML(self): - self.thresholdSlider.blockSignals(True) - self.thresholdSlider.setMinimumValue(self.scriptedEffect.doubleParameter("MinimumThreshold")) - self.thresholdSlider.setMaximumValue(self.scriptedEffect.doubleParameter("MaximumThreshold")) - self.thresholdSlider.blockSignals(False) - - autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMethod")) - wasBlocked = self.autoThresholdMethodSelectorComboBox.blockSignals(True) - self.autoThresholdMethodSelectorComboBox.setCurrentIndex(autoThresholdMethod) - self.autoThresholdMethodSelectorComboBox.blockSignals(wasBlocked) - - autoThresholdMode = self.autoThresholdModeSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMode")) - wasBlocked = self.autoThresholdModeSelectorComboBox.blockSignals(True) - self.autoThresholdModeSelectorComboBox.setCurrentIndex(autoThresholdMode) - self.autoThresholdModeSelectorComboBox.blockSignals(wasBlocked) - - histogramBrushType = self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME) - self.boxROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_BOX) - self.circleROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_CIRCLE) - self.drawROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_DRAW) - self.lineROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_LINE) - - histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME) - self.histogramLowerThresholdMinimumButton.checked = (histogramSetModeLower == HISTOGRAM_SET_MINIMUM) - self.histogramLowerThresholdLowerButton.checked = (histogramSetModeLower == HISTOGRAM_SET_LOWER) - self.histogramLowerThresholdAverageButton.checked = (histogramSetModeLower == HISTOGRAM_SET_AVERAGE) - - histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME) - self.histogramUpperThresholdAverageButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_AVERAGE) - self.histogramUpperThresholdUpperButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_UPPER) - self.histogramUpperThresholdMaximumButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM) - - self.updateHistogramBackground() - - def updateMRMLFromGUI(self): - with slicer.util.NodeModify(self.scriptedEffect.parameterSetNode()): - self.scriptedEffect.setParameter("MinimumThreshold", self.thresholdSlider.minimumValue) - self.scriptedEffect.setParameter("MaximumThreshold", self.thresholdSlider.maximumValue) - - methodIndex = self.autoThresholdMethodSelectorComboBox.currentIndex - autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.itemData(methodIndex) - self.scriptedEffect.setParameter("AutoThresholdMethod", autoThresholdMethod) - - modeIndex = self.autoThresholdModeSelectorComboBox.currentIndex - autoThresholdMode = self.autoThresholdModeSelectorComboBox.itemData(modeIndex) - self.scriptedEffect.setParameter("AutoThresholdMode", autoThresholdMode) - - histogramParameterChanged = False - - histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE - if self.boxROIButton.checked: - histogramBrushType = HISTOGRAM_BRUSH_TYPE_BOX - elif self.circleROIButton.checked: - histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE - elif self.drawROIButton.checked: - histogramBrushType = HISTOGRAM_BRUSH_TYPE_DRAW - elif self.lineROIButton.checked: - histogramBrushType = HISTOGRAM_BRUSH_TYPE_LINE - - if histogramBrushType != self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME): - self.scriptedEffect.setParameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, histogramBrushType) - histogramParameterChanged = True - - histogramSetModeLower = HISTOGRAM_SET_LOWER - if self.histogramLowerThresholdMinimumButton.checked: - histogramSetModeLower = HISTOGRAM_SET_MINIMUM - elif self.histogramLowerThresholdLowerButton.checked: - histogramSetModeLower = HISTOGRAM_SET_LOWER - elif self.histogramLowerThresholdAverageButton.checked: - histogramSetModeLower = HISTOGRAM_SET_AVERAGE - if histogramSetModeLower != self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME): - self.scriptedEffect.setParameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME, histogramSetModeLower) - histogramParameterChanged = True - - histogramSetModeUpper = HISTOGRAM_SET_UPPER - if self.histogramUpperThresholdAverageButton.checked: - histogramSetModeUpper = HISTOGRAM_SET_AVERAGE - elif self.histogramUpperThresholdUpperButton.checked: - histogramSetModeUpper = HISTOGRAM_SET_UPPER - elif self.histogramUpperThresholdMaximumButton.checked: - histogramSetModeUpper = HISTOGRAM_SET_MAXIMUM - if histogramSetModeUpper != self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME): - self.scriptedEffect.setParameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME, histogramSetModeUpper) - histogramParameterChanged = True - - if histogramParameterChanged: + def onHistogramMouseMove(self, pos, button): + self.selectionEndPosition = pos + if (button == qt.Qt.RightButton): + return self.updateHistogram() - # - # Effect specific methods (the above ones are the API methods to override) - # - def onThresholdValuesChanged(self,min,max): - self.scriptedEffect.updateMRMLFromGUI() - - def onUseForPaint(self): - parameterSetNode = self.scriptedEffect.parameterSetNode() - parameterSetNode.MasterVolumeIntensityMaskOn() - parameterSetNode.SetMasterVolumeIntensityMaskRange(self.thresholdSlider.minimumValue, self.thresholdSlider.maximumValue) - # Switch to paint effect - self.scriptedEffect.selectEffect("Paint") - - def onSelectPreviousAutoThresholdMethod(self): - self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex - 1) \ - % self.autoThresholdMethodSelectorComboBox.count - self.onSelectedAutoThresholdMethod() - - def onSelectNextAutoThresholdMethod(self): - self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex + 1) \ - % self.autoThresholdMethodSelectorComboBox.count - self.onSelectedAutoThresholdMethod() - - def onSelectedAutoThresholdMethod(self): - self.updateMRMLFromGUI() - self.onAutoThreshold() - self.updateGUIFromMRML() - - def onAutoThreshold(self): - autoThresholdMethod = self.scriptedEffect.parameter("AutoThresholdMethod") - autoThresholdMode = self.scriptedEffect.parameter("AutoThresholdMode") - self.autoThreshold(autoThresholdMethod, autoThresholdMode) - - def autoThreshold(self, autoThresholdMethod, autoThresholdMode): - if autoThresholdMethod == METHOD_HUANG: - self.autoThresholdCalculator.SetMethodToHuang() - elif autoThresholdMethod == METHOD_INTERMODES: - self.autoThresholdCalculator.SetMethodToIntermodes() - elif autoThresholdMethod == METHOD_ISO_DATA: - self.autoThresholdCalculator.SetMethodToIsoData() - elif autoThresholdMethod == METHOD_KITTLER_ILLINGWORTH: - self.autoThresholdCalculator.SetMethodToKittlerIllingworth() - elif autoThresholdMethod == METHOD_LI: - self.autoThresholdCalculator.SetMethodToLi() - elif autoThresholdMethod == METHOD_MAXIMUM_ENTROPY: - self.autoThresholdCalculator.SetMethodToMaximumEntropy() - elif autoThresholdMethod == METHOD_MOMENTS: - self.autoThresholdCalculator.SetMethodToMoments() - elif autoThresholdMethod == METHOD_OTSU: - self.autoThresholdCalculator.SetMethodToOtsu() - elif autoThresholdMethod == METHOD_RENYI_ENTROPY: - self.autoThresholdCalculator.SetMethodToRenyiEntropy() - elif autoThresholdMethod == METHOD_SHANBHAG: - self.autoThresholdCalculator.SetMethodToShanbhag() - elif autoThresholdMethod == METHOD_TRIANGLE: - self.autoThresholdCalculator.SetMethodToTriangle() - elif autoThresholdMethod == METHOD_YEN: - self.autoThresholdCalculator.SetMethodToYen() - else: - logging.error(f"Unknown AutoThresholdMethod {autoThresholdMethod}") - - masterImageData = self.scriptedEffect.masterVolumeImageData() - self.autoThresholdCalculator.SetInputData(masterImageData) - - self.autoThresholdCalculator.Update() - computedThreshold = self.autoThresholdCalculator.GetThreshold() - - masterVolumeMin, masterVolumeMax = masterImageData.GetScalarRange() - - if autoThresholdMode == MODE_SET_UPPER: - self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold) - elif autoThresholdMode == MODE_SET_LOWER: - self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold) - elif autoThresholdMode == MODE_SET_MIN_UPPER: - self.scriptedEffect.setParameter("MinimumThreshold", masterVolumeMin) - self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold) - elif autoThresholdMode == MODE_SET_LOWER_MAX: - self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold) - self.scriptedEffect.setParameter("MaximumThreshold", masterVolumeMax) - else: - logging.error(f"Unknown AutoThresholdMode {autoThresholdMode}") - - def onApply(self): - if not self.scriptedEffect.confirmCurrentSegmentVisible(): - return - - try: - # Get master volume image data - masterImageData = self.scriptedEffect.masterVolumeImageData() - # Get modifier labelmap - modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap() - originalImageToWorldMatrix = vtk.vtkMatrix4x4() - modifierLabelmap.GetImageToWorldMatrix(originalImageToWorldMatrix) - # Get parameters - min = self.scriptedEffect.doubleParameter("MinimumThreshold") - max = self.scriptedEffect.doubleParameter("MaximumThreshold") - - self.scriptedEffect.saveStateForUndo() - - # Perform thresholding - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(masterImageData) - thresh.ThresholdBetween(min, max) - thresh.SetInValue(1) - thresh.SetOutValue(0) - thresh.SetOutputScalarType(modifierLabelmap.GetScalarType()) - thresh.Update() - modifierLabelmap.DeepCopy(thresh.GetOutput()) - except IndexError: - logging.error('apply: Failed to threshold master volume!') - pass - - # Apply changes - self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet) - - # De-select effect - self.scriptedEffect.selectEffect("") - - def clearPreviewDisplay(self): - for sliceWidget, pipeline in self.previewPipelines.items(): - self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor) - self.previewPipelines = {} - - def clearHistogramDisplay(self): - if self.histogramPipeline is None: - return - self.histogramPipeline.removeActors() - self.histogramPipeline = None - - def setupPreviewDisplay(self): - # Clear previous pipelines before setting up the new ones - self.clearPreviewDisplay() - - layoutManager = slicer.app.layoutManager() - if layoutManager is None: - return - - # Add a pipeline for each 2D slice view - for sliceViewName in layoutManager.sliceViewNames(): - sliceWidget = layoutManager.sliceWidget(sliceViewName) - if not self.scriptedEffect.segmentationDisplayableInView(sliceWidget.mrmlSliceNode()): - continue - renderer = self.scriptedEffect.renderer(sliceWidget) - if renderer is None: - logging.error("setupPreviewDisplay: Failed to get renderer!") - continue - - # Create pipeline - pipeline = PreviewPipeline() - self.previewPipelines[sliceWidget] = pipeline - - # Add actor - self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor) - - def preview(self): - - opacity = 0.5 + self.previewState / (2. * self.previewSteps) - min = self.scriptedEffect.doubleParameter("MinimumThreshold") - max = self.scriptedEffect.doubleParameter("MaximumThreshold") - - # Get color of edited segment - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - if not segmentationNode: - # scene was closed while preview was active - return - displayNode = segmentationNode.GetDisplayNode() - if displayNode is None: - logging.error("preview: Invalid segmentation display node!") - color = [0.5,0.5,0.5] - segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID() - - # Make sure we keep the currently selected segment hidden (the user may have changed selection) - if segmentID != self.previewedSegmentID: - self.setCurrentSegmentTransparent() - - r,g,b = segmentationNode.GetSegmentation().GetSegment(segmentID).GetColor() - - # Set values to pipelines - for sliceWidget in self.previewPipelines: - pipeline = self.previewPipelines[sliceWidget] - pipeline.lookupTable.SetTableValue(1, r, g, b, opacity) - layerLogic = self.getMasterVolumeLayerLogic(sliceWidget) - pipeline.thresholdFilter.SetInputConnection(layerLogic.GetReslice().GetOutputPort()) - pipeline.thresholdFilter.ThresholdBetween(min, max) - pipeline.actor.VisibilityOn() - sliceWidget.sliceView().scheduleRender() - - self.previewState += self.previewStep - if self.previewState >= self.previewSteps: - self.previewStep = -1 - if self.previewState <= 0: - self.previewStep = 1 - - def processInteractionEvents(self, callerInteractor, eventId, viewWidget): - abortEvent = False - - masterImageData = self.scriptedEffect.masterVolumeImageData() - if masterImageData is None: - return abortEvent - - # Only allow for slice views - if viewWidget.className() != "qMRMLSliceWidget": - return abortEvent - - anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey() - - # Clicking in a view should remove all previous pipelines - if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed: - self.clearHistogramDisplay() - - if self.histogramPipeline is None: - self.createHistogramPipeline(viewWidget) - - xy = callerInteractor.GetEventPosition() - ras = self.xyToRas(xy, viewWidget) - - if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed: - self.histogramPipeline.state = HISTOGRAM_STATE_MOVING - self.histogramPipeline.addPoint(ras) - self.updateHistogram() - abortEvent = True - elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent: - if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING: - self.histogramPipeline.state = HISTOGRAM_STATE_PLACED - abortEvent = True - elif eventId == vtk.vtkCommand.MouseMoveEvent: - if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING: - self.histogramPipeline.addPoint(ras) + def onHistogramMouseRelease(self, pos, button): + self.selectionEndPosition = pos + if (button == qt.Qt.RightButton): + return self.updateHistogram() - return abortEvent - - def createHistogramPipeline(self, sliceWidget): - brushType = HISTOGRAM_BRUSH_TYPE_CIRCLE - if self.boxROIButton.checked: - brushType = HISTOGRAM_BRUSH_TYPE_BOX - elif self.drawROIButton.checked: - brushType = HISTOGRAM_BRUSH_TYPE_DRAW - elif self.lineROIButton.checked: - brushType = HISTOGRAM_BRUSH_TYPE_LINE - pipeline = HistogramPipeline(self, self.scriptedEffect, sliceWidget, brushType) - self.histogramPipeline = pipeline - - def processViewNodeEvents(self, callerViewNode, eventId, viewWidget): - if self.histogramPipeline is not None: - self.histogramPipeline.updateBrushModel() - - def onHistogramMouseClick(self, pos, button): - self.selectionStartPosition = pos - self.selectionEndPosition = pos - if (button == qt.Qt.RightButton): - self.selectionStartPosition = None - self.selectionEndPosition = None - self.minMaxFunction.RemoveAllPoints() - self.averageFunction.RemoveAllPoints() - self.updateHistogram() - - def onHistogramMouseMove(self, pos, button): - self.selectionEndPosition = pos - if (button == qt.Qt.RightButton): - return - self.updateHistogram() - - def onHistogramMouseRelease(self, pos, button): - self.selectionEndPosition = pos - if (button == qt.Qt.RightButton): - return - self.updateHistogram() - - def getMasterVolumeLayerLogic(self, sliceWidget): - masterVolumeNode = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode() - sliceLogic = sliceWidget.sliceLogic() - - backgroundLogic = sliceLogic.GetBackgroundLayer() - backgroundVolumeNode = backgroundLogic.GetVolumeNode() - if masterVolumeNode == backgroundVolumeNode: - return backgroundLogic - - foregroundLogic = sliceLogic.GetForegroundLayer() - foregroundVolumeNode = foregroundLogic.GetVolumeNode() - if masterVolumeNode == foregroundVolumeNode: - return foregroundLogic - - logging.warning("Master volume is not set as either the foreground or background") - - foregroundOpacity = 0.0 - if foregroundVolumeNode: - compositeNode = sliceLogic.GetSliceCompositeNode() - foregroundOpacity = compositeNode.GetForegroundOpacity() - - if foregroundOpacity > 0.5: - return foregroundLogic - - return backgroundLogic - - def updateHistogram(self): - masterImageData = self.scriptedEffect.masterVolumeImageData() - if masterImageData is None or self.histogramPipeline is None: - self.histogramFunction.RemoveAllPoints() - return - - # Ensure that the brush is in the correct location - self.histogramPipeline.updateBrushModel() - - self.stencil.SetInputConnection(self.histogramPipeline.worldToSliceTransformer.GetOutputPort()) - - self.histogramPipeline.worldToSliceTransformer.Update() - brushPolydata = self.histogramPipeline.worldToSliceTransformer.GetOutput() - brushBounds = brushPolydata.GetBounds() - brushExtent = [0, -1, 0, -1, 0, -1] - for i in range(3): - brushExtent[2*i] = vtk.vtkMath.Floor(brushBounds[2*i]) - brushExtent[2*i+1] = vtk.vtkMath.Ceil(brushBounds[2*i+1]) - if brushExtent[0] > brushExtent[1] or brushExtent[2] > brushExtent[3] or brushExtent[4] > brushExtent[5]: - self.histogramFunction.RemoveAllPoints() - return - - layerLogic = self.getMasterVolumeLayerLogic(self.histogramPipeline.sliceWidget) - self.reslice.SetInputConnection(layerLogic.GetReslice().GetInputConnection(0, 0)) - self.reslice.SetResliceTransform(layerLogic.GetReslice().GetResliceTransform()) - self.reslice.SetInterpolationMode(layerLogic.GetReslice().GetInterpolationMode()) - self.reslice.SetOutputExtent(brushExtent) - - maxNumberOfBins = 1000 - masterImageData = self.scriptedEffect.masterVolumeImageData() - scalarRange = masterImageData.GetScalarRange() - scalarType = masterImageData.GetScalarType() - if scalarType == vtk.VTK_FLOAT or scalarType == vtk.VTK_DOUBLE: - numberOfBins = maxNumberOfBins - else: - numberOfBins = int(scalarRange[1] - scalarRange[0]) + 1 - if numberOfBins > maxNumberOfBins: - numberOfBins = maxNumberOfBins - binSpacing = (scalarRange[1] - scalarRange[0] + 1) / numberOfBins - - self.imageAccumulate.SetComponentExtent(0, numberOfBins - 1, 0, 0, 0, 0) - self.imageAccumulate.SetComponentSpacing(binSpacing, binSpacing, binSpacing) - self.imageAccumulate.SetComponentOrigin(scalarRange[0], scalarRange[0], scalarRange[0]) - - self.imageAccumulate.Update() - - self.histogramFunction.RemoveAllPoints() - tableSize = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetNumberOfTuples() - for i in range(tableSize): - value = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetTuple1(i) - self.histogramFunction.AddPoint(binSpacing * i + scalarRange[0], value) - self.histogramFunction.AdjustRange(scalarRange) - - lower = self.imageAccumulate.GetMin()[0] - average = self.imageAccumulate.GetMean()[0] - upper = self.imageAccumulate.GetMax()[0] - - # If there is a selection, then set the threshold based on that - if self.selectionStartPosition is not None and self.selectionEndPosition is not None: - - # Clamp selection based on scalar range - startX = min(scalarRange[1], max(scalarRange[0], self.selectionStartPosition[0])) - endX = min(scalarRange[1], max(scalarRange[0], self.selectionEndPosition[0])) - - lower = min(startX, endX) - average = (startX + endX) / 2.0 - upper = max(startX, endX) - - epsilon = 0.00001 - self.minMaxFunction.RemoveAllPoints() - self.minMaxFunction.AddPoint(lower - epsilon, 0.0) - self.minMaxFunction.AddPoint(lower, 1.0) - self.minMaxFunction.AddPoint(lower + epsilon, 0.0) - self.minMaxFunction.AddPoint(upper - epsilon, 0.0) - self.minMaxFunction.AddPoint(upper, 1.0) - self.minMaxFunction.AddPoint(upper + epsilon, 0.0) - self.minMaxFunction.AdjustRange(scalarRange) - - self.averageFunction.RemoveAllPoints() - self.averageFunction.AddPoint(average - epsilon, 0.0) - self.averageFunction.AddPoint(average, 1.0) - self.averageFunction.AddPoint(average + epsilon, 0.0) - self.averageFunction.AdjustRange(scalarRange) - - minimumThreshold = lower - maximumThreshold = upper - - histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME) - if histogramSetModeLower == HISTOGRAM_SET_MINIMUM: - minimumThreshold = scalarRange[0] - elif histogramSetModeLower == HISTOGRAM_SET_LOWER: - minimumThreshold = lower - elif histogramSetModeLower == HISTOGRAM_SET_AVERAGE: - minimumThreshold = average - - histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME) - if histogramSetModeUpper == HISTOGRAM_SET_AVERAGE: - maximumThreshold = average - elif histogramSetModeUpper == HISTOGRAM_SET_UPPER: - maximumThreshold = upper - elif histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM: - maximumThreshold = scalarRange[1] - - self.scriptedEffect.setParameter("MinimumThreshold", minimumThreshold) - self.scriptedEffect.setParameter("MaximumThreshold", maximumThreshold) - - def updateHistogramBackground(self): - self.backgroundFunction.RemoveAllPoints() - - masterImageData = self.scriptedEffect.masterVolumeImageData() - if masterImageData is None: - return - - scalarRange = masterImageData.GetScalarRange() - - epsilon = 0.00001 - low = self.scriptedEffect.doubleParameter("MinimumThreshold") - upper = self.scriptedEffect.doubleParameter("MaximumThreshold") - low = max(scalarRange[0] + epsilon, low) - upper = min(scalarRange[1] - epsilon, upper) - - self.backgroundFunction.AddRGBPoint(scalarRange[0], 1, 1, 1) - self.backgroundFunction.AddRGBPoint(low - epsilon, 1, 1, 1) - self.backgroundFunction.AddRGBPoint(low, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2]) - self.backgroundFunction.AddRGBPoint(upper, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2]) - self.backgroundFunction.AddRGBPoint(upper + epsilon, 1, 1, 1) - self.backgroundFunction.AddRGBPoint(scalarRange[1], 1, 1, 1) - self.backgroundFunction.SetAlpha(1.0) - self.backgroundFunction.Build() + + def getMasterVolumeLayerLogic(self, sliceWidget): + masterVolumeNode = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode() + sliceLogic = sliceWidget.sliceLogic() + + backgroundLogic = sliceLogic.GetBackgroundLayer() + backgroundVolumeNode = backgroundLogic.GetVolumeNode() + if masterVolumeNode == backgroundVolumeNode: + return backgroundLogic + + foregroundLogic = sliceLogic.GetForegroundLayer() + foregroundVolumeNode = foregroundLogic.GetVolumeNode() + if masterVolumeNode == foregroundVolumeNode: + return foregroundLogic + + logging.warning("Master volume is not set as either the foreground or background") + + foregroundOpacity = 0.0 + if foregroundVolumeNode: + compositeNode = sliceLogic.GetSliceCompositeNode() + foregroundOpacity = compositeNode.GetForegroundOpacity() + + if foregroundOpacity > 0.5: + return foregroundLogic + + return backgroundLogic + + def updateHistogram(self): + masterImageData = self.scriptedEffect.masterVolumeImageData() + if masterImageData is None or self.histogramPipeline is None: + self.histogramFunction.RemoveAllPoints() + return + + # Ensure that the brush is in the correct location + self.histogramPipeline.updateBrushModel() + + self.stencil.SetInputConnection(self.histogramPipeline.worldToSliceTransformer.GetOutputPort()) + + self.histogramPipeline.worldToSliceTransformer.Update() + brushPolydata = self.histogramPipeline.worldToSliceTransformer.GetOutput() + brushBounds = brushPolydata.GetBounds() + brushExtent = [0, -1, 0, -1, 0, -1] + for i in range(3): + brushExtent[2 * i] = vtk.vtkMath.Floor(brushBounds[2 * i]) + brushExtent[2 * i + 1] = vtk.vtkMath.Ceil(brushBounds[2 * i + 1]) + if brushExtent[0] > brushExtent[1] or brushExtent[2] > brushExtent[3] or brushExtent[4] > brushExtent[5]: + self.histogramFunction.RemoveAllPoints() + return + + layerLogic = self.getMasterVolumeLayerLogic(self.histogramPipeline.sliceWidget) + self.reslice.SetInputConnection(layerLogic.GetReslice().GetInputConnection(0, 0)) + self.reslice.SetResliceTransform(layerLogic.GetReslice().GetResliceTransform()) + self.reslice.SetInterpolationMode(layerLogic.GetReslice().GetInterpolationMode()) + self.reslice.SetOutputExtent(brushExtent) + + maxNumberOfBins = 1000 + masterImageData = self.scriptedEffect.masterVolumeImageData() + scalarRange = masterImageData.GetScalarRange() + scalarType = masterImageData.GetScalarType() + if scalarType == vtk.VTK_FLOAT or scalarType == vtk.VTK_DOUBLE: + numberOfBins = maxNumberOfBins + else: + numberOfBins = int(scalarRange[1] - scalarRange[0]) + 1 + if numberOfBins > maxNumberOfBins: + numberOfBins = maxNumberOfBins + binSpacing = (scalarRange[1] - scalarRange[0] + 1) / numberOfBins + + self.imageAccumulate.SetComponentExtent(0, numberOfBins - 1, 0, 0, 0, 0) + self.imageAccumulate.SetComponentSpacing(binSpacing, binSpacing, binSpacing) + self.imageAccumulate.SetComponentOrigin(scalarRange[0], scalarRange[0], scalarRange[0]) + + self.imageAccumulate.Update() + + self.histogramFunction.RemoveAllPoints() + tableSize = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetNumberOfTuples() + for i in range(tableSize): + value = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetTuple1(i) + self.histogramFunction.AddPoint(binSpacing * i + scalarRange[0], value) + self.histogramFunction.AdjustRange(scalarRange) + + lower = self.imageAccumulate.GetMin()[0] + average = self.imageAccumulate.GetMean()[0] + upper = self.imageAccumulate.GetMax()[0] + + # If there is a selection, then set the threshold based on that + if self.selectionStartPosition is not None and self.selectionEndPosition is not None: + + # Clamp selection based on scalar range + startX = min(scalarRange[1], max(scalarRange[0], self.selectionStartPosition[0])) + endX = min(scalarRange[1], max(scalarRange[0], self.selectionEndPosition[0])) + + lower = min(startX, endX) + average = (startX + endX) / 2.0 + upper = max(startX, endX) + + epsilon = 0.00001 + self.minMaxFunction.RemoveAllPoints() + self.minMaxFunction.AddPoint(lower - epsilon, 0.0) + self.minMaxFunction.AddPoint(lower, 1.0) + self.minMaxFunction.AddPoint(lower + epsilon, 0.0) + self.minMaxFunction.AddPoint(upper - epsilon, 0.0) + self.minMaxFunction.AddPoint(upper, 1.0) + self.minMaxFunction.AddPoint(upper + epsilon, 0.0) + self.minMaxFunction.AdjustRange(scalarRange) + + self.averageFunction.RemoveAllPoints() + self.averageFunction.AddPoint(average - epsilon, 0.0) + self.averageFunction.AddPoint(average, 1.0) + self.averageFunction.AddPoint(average + epsilon, 0.0) + self.averageFunction.AdjustRange(scalarRange) + + minimumThreshold = lower + maximumThreshold = upper + + histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME) + if histogramSetModeLower == HISTOGRAM_SET_MINIMUM: + minimumThreshold = scalarRange[0] + elif histogramSetModeLower == HISTOGRAM_SET_LOWER: + minimumThreshold = lower + elif histogramSetModeLower == HISTOGRAM_SET_AVERAGE: + minimumThreshold = average + + histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME) + if histogramSetModeUpper == HISTOGRAM_SET_AVERAGE: + maximumThreshold = average + elif histogramSetModeUpper == HISTOGRAM_SET_UPPER: + maximumThreshold = upper + elif histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM: + maximumThreshold = scalarRange[1] + + self.scriptedEffect.setParameter("MinimumThreshold", minimumThreshold) + self.scriptedEffect.setParameter("MaximumThreshold", maximumThreshold) + + def updateHistogramBackground(self): + self.backgroundFunction.RemoveAllPoints() + + masterImageData = self.scriptedEffect.masterVolumeImageData() + if masterImageData is None: + return + + scalarRange = masterImageData.GetScalarRange() + + epsilon = 0.00001 + low = self.scriptedEffect.doubleParameter("MinimumThreshold") + upper = self.scriptedEffect.doubleParameter("MaximumThreshold") + low = max(scalarRange[0] + epsilon, low) + upper = min(scalarRange[1] - epsilon, upper) + + self.backgroundFunction.AddRGBPoint(scalarRange[0], 1, 1, 1) + self.backgroundFunction.AddRGBPoint(low - epsilon, 1, 1, 1) + self.backgroundFunction.AddRGBPoint(low, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2]) + self.backgroundFunction.AddRGBPoint(upper, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2]) + self.backgroundFunction.AddRGBPoint(upper + epsilon, 1, 1, 1) + self.backgroundFunction.AddRGBPoint(scalarRange[1], 1, 1, 1) + self.backgroundFunction.SetAlpha(1.0) + self.backgroundFunction.Build() # # PreviewPipeline # class PreviewPipeline: - """ Visualization objects and pipeline for each slice view for threshold preview - """ - - def __init__(self): - self.lookupTable = vtk.vtkLookupTable() - self.lookupTable.SetRampToLinear() - self.lookupTable.SetNumberOfTableValues(2) - self.lookupTable.SetTableRange(0, 1) - self.lookupTable.SetTableValue(0, 0, 0, 0, 0) - self.colorMapper = vtk.vtkImageMapToRGBA() - self.colorMapper.SetOutputFormatToRGBA() - self.colorMapper.SetLookupTable(self.lookupTable) - self.thresholdFilter = vtk.vtkImageThreshold() - self.thresholdFilter.SetInValue(1) - self.thresholdFilter.SetOutValue(0) - self.thresholdFilter.SetOutputScalarTypeToUnsignedChar() - - # Feedback actor - self.mapper = vtk.vtkImageMapper() - self.dummyImage = vtk.vtkImageData() - self.dummyImage.AllocateScalars(vtk.VTK_UNSIGNED_INT, 1) - self.mapper.SetInputData(self.dummyImage) - self.actor = vtk.vtkActor2D() - self.actor.VisibilityOff() - self.actor.SetMapper(self.mapper) - self.mapper.SetColorWindow(255) - self.mapper.SetColorLevel(128) - - # Setup pipeline - self.colorMapper.SetInputConnection(self.thresholdFilter.GetOutputPort()) - self.mapper.SetInputConnection(self.colorMapper.GetOutputPort()) + """ Visualization objects and pipeline for each slice view for threshold preview + """ + + def __init__(self): + self.lookupTable = vtk.vtkLookupTable() + self.lookupTable.SetRampToLinear() + self.lookupTable.SetNumberOfTableValues(2) + self.lookupTable.SetTableRange(0, 1) + self.lookupTable.SetTableValue(0, 0, 0, 0, 0) + self.colorMapper = vtk.vtkImageMapToRGBA() + self.colorMapper.SetOutputFormatToRGBA() + self.colorMapper.SetLookupTable(self.lookupTable) + self.thresholdFilter = vtk.vtkImageThreshold() + self.thresholdFilter.SetInValue(1) + self.thresholdFilter.SetOutValue(0) + self.thresholdFilter.SetOutputScalarTypeToUnsignedChar() + + # Feedback actor + self.mapper = vtk.vtkImageMapper() + self.dummyImage = vtk.vtkImageData() + self.dummyImage.AllocateScalars(vtk.VTK_UNSIGNED_INT, 1) + self.mapper.SetInputData(self.dummyImage) + self.actor = vtk.vtkActor2D() + self.actor.VisibilityOff() + self.actor.SetMapper(self.mapper) + self.mapper.SetColorWindow(255) + self.mapper.SetColorLevel(128) + + # Setup pipeline + self.colorMapper.SetInputConnection(self.thresholdFilter.GetOutputPort()) + self.mapper.SetInputConnection(self.colorMapper.GetOutputPort()) ### @@ -973,238 +973,238 @@ def __init__(self): # Histogram threshold # class HistogramEventFilter(qt.QObject): - thresholdEffect = None + thresholdEffect = None - def setThresholdEffect(self, thresholdEffect): - self.thresholdEffect = thresholdEffect + def setThresholdEffect(self, thresholdEffect): + self.thresholdEffect = thresholdEffect - def eventFilter(self, object, event): - if self.thresholdEffect is None: - return + def eventFilter(self, object, event): + if self.thresholdEffect is None: + return - if (event.type() == qt.QEvent.GraphicsSceneMousePress or - event.type() == qt.QEvent.GraphicsSceneMouseMove or - event.type() == qt.QEvent.GraphicsSceneMouseRelease): - transferFunction = object.transferFunction() - if transferFunction is None: - return + if (event.type() == qt.QEvent.GraphicsSceneMousePress or + event.type() == qt.QEvent.GraphicsSceneMouseMove or + event.type() == qt.QEvent.GraphicsSceneMouseRelease): + transferFunction = object.transferFunction() + if transferFunction is None: + return - representation = transferFunction.representation() - x = representation.mapXFromScene(event.pos().x()) - y = representation.mapYFromScene(event.pos().y()) - position = (x, y) + representation = transferFunction.representation() + x = representation.mapXFromScene(event.pos().x()) + y = representation.mapYFromScene(event.pos().y()) + position = (x, y) - if event.type() == qt.QEvent.GraphicsSceneMousePress: - self.thresholdEffect.onHistogramMouseClick(position, event.button()) - elif event.type() == qt.QEvent.GraphicsSceneMouseMove: - self.thresholdEffect.onHistogramMouseMove(position, event.button()) - elif event.type() == qt.QEvent.GraphicsSceneMouseRelease: - self.thresholdEffect.onHistogramMouseRelease(position, event.button()) - return True - return False + if event.type() == qt.QEvent.GraphicsSceneMousePress: + self.thresholdEffect.onHistogramMouseClick(position, event.button()) + elif event.type() == qt.QEvent.GraphicsSceneMouseMove: + self.thresholdEffect.onHistogramMouseMove(position, event.button()) + elif event.type() == qt.QEvent.GraphicsSceneMouseRelease: + self.thresholdEffect.onHistogramMouseRelease(position, event.button()) + return True + return False class HistogramPipeline: - def __init__(self, thresholdEffect, scriptedEffect, sliceWidget, brushMode): - self.thresholdEffect = thresholdEffect - self.scriptedEffect = scriptedEffect - self.sliceWidget = sliceWidget - self.brushMode = brushMode - self.state = HISTOGRAM_STATE_OFF - - self.point1 = None - self.point2 = None - - # Actor setup - self.brushCylinderSource = vtk.vtkCylinderSource() - self.brushCylinderSource.SetResolution(32) - - self.brushCubeSource = vtk.vtkCubeSource() - - self.brushLineSource = vtk.vtkLineSource() - self.brushTubeSource = vtk.vtkTubeFilter() - self.brushTubeSource.SetInputConnection(self.brushLineSource.GetOutputPort()) - self.brushTubeSource.SetNumberOfSides(50) - self.brushTubeSource.SetCapping(True) - - self.brushToWorldOriginTransform = vtk.vtkTransform() - self.brushToWorldOriginTransformer = vtk.vtkTransformPolyDataFilter() - self.brushToWorldOriginTransformer.SetTransform(self.brushToWorldOriginTransform) - self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort()) - - self.normalFilter = vtk.vtkPolyDataNormals() - self.normalFilter.AutoOrientNormalsOn() - self.normalFilter.SetInputConnection(self.brushToWorldOriginTransformer.GetOutputPort()) - - # Brush to RAS transform - self.worldOriginToWorldTransform = vtk.vtkTransform() - self.worldOriginToWorldTransformer = vtk.vtkTransformPolyDataFilter() - self.worldOriginToWorldTransformer.SetTransform(self.worldOriginToWorldTransform) - self.worldOriginToWorldTransformer.SetInputConnection(self.normalFilter.GetOutputPort()) - - # RAS to XY transform - self.worldToSliceTransform = vtk.vtkTransform() - self.worldToSliceTransformer = vtk.vtkTransformPolyDataFilter() - self.worldToSliceTransformer.SetTransform(self.worldToSliceTransform) - self.worldToSliceTransformer.SetInputConnection(self.worldOriginToWorldTransformer.GetOutputPort()) - - # Cutting takes place in XY coordinates - self.slicePlane = vtk.vtkPlane() - self.slicePlane.SetNormal(0, 0, 1) - self.slicePlane.SetOrigin(0, 0, 0) - self.cutter = vtk.vtkCutter() - self.cutter.SetCutFunction(self.slicePlane) - self.cutter.SetInputConnection(self.worldToSliceTransformer.GetOutputPort()) - - self.rasPoints = vtk.vtkPoints() - lines = vtk.vtkCellArray() - self.polyData = vtk.vtkPolyData() - self.polyData.SetPoints(self.rasPoints) - self.polyData.SetLines(lines) - - # Thin line - self.thinRASPoints = vtk.vtkPoints() - thinLines = vtk.vtkCellArray() - self.thinPolyData = vtk.vtkPolyData() - self.thinPolyData.SetPoints(self.rasPoints) - self.thinPolyData.SetLines(thinLines) - - self.mapper = vtk.vtkPolyDataMapper2D() - self.mapper.SetInputConnection(self.cutter.GetOutputPort()) - - # Add actor - self.actor = vtk.vtkActor2D() - self.actor.SetMapper(self.mapper) - actorProperty = self.actor.GetProperty() - actorProperty.SetColor(1,1,0) - actorProperty.SetLineWidth(2) - renderer = self.scriptedEffect.renderer(sliceWidget) - if renderer is None: - logging.error("pipelineForWidget: Failed to get renderer!") - return None - self.scriptedEffect.addActor2D(sliceWidget, self.actor) - - self.thinActor = None - if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW: - self.worldToSliceTransformer.SetInputData(self.polyData) - self.mapper.SetInputConnection(self.worldToSliceTransformer.GetOutputPort()) - - self.thinWorldToSliceTransformer = vtk.vtkTransformPolyDataFilter() - self.thinWorldToSliceTransformer.SetInputData(self.thinPolyData) - self.thinWorldToSliceTransformer.SetTransform(self.worldToSliceTransform) - - self.thinMapper = vtk.vtkPolyDataMapper2D() - self.thinMapper.SetInputConnection(self.thinWorldToSliceTransformer.GetOutputPort()) - - self.thinActor = vtk.vtkActor2D() - self.thinActor.SetMapper(self.thinMapper) - thinActorProperty = self.thinActor.GetProperty() - thinActorProperty.SetColor(1,1,0) - thinActorProperty.SetLineWidth(1) - self.scriptedEffect.addActor2D(sliceWidget, self.thinActor) - elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE: - self.worldToSliceTransformer.SetInputConnection(self.brushTubeSource.GetOutputPort()) - - def removeActors(self): - if self.actor is not None: - self.scriptedEffect.removeActor2D(self.sliceWidget, self.actor) - if self.thinActor is not None: - self.scriptedEffect.removeActor2D(self.sliceWidget, self.thinActor) - - def setPoint1(self, ras): - self.point1 = ras - self.updateBrushModel() - - def setPoint2(self, ras): - self.point2 = ras - self.updateBrushModel() - - def addPoint(self, ras): - if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW: - newPointIndex = self.rasPoints.InsertNextPoint(ras) - previousPointIndex = newPointIndex - 1 - if (previousPointIndex >= 0): - idList = vtk.vtkIdList() - idList.InsertNextId(previousPointIndex) - idList.InsertNextId(newPointIndex) - self.polyData.InsertNextCell(vtk.VTK_LINE, idList) - - thinLines = self.thinPolyData.GetLines() - thinLines.Initialize() - idList = vtk.vtkIdList() - idList.InsertNextId(newPointIndex) - idList.InsertNextId(0) - self.thinPolyData.InsertNextCell(vtk.VTK_LINE, idList) - - else: - if self.point1 is None: - self.setPoint1(ras) - self.setPoint2(ras) - - def updateBrushModel(self): - if self.brushMode != HISTOGRAM_BRUSH_TYPE_DRAW and (self.point1 is None or self.point2 is None): - return - - # Update slice cutting plane position and orientation - sliceXyToRas = self.sliceWidget.sliceLogic().GetSliceNode().GetXYToRAS() - rasToSliceXy = vtk.vtkMatrix4x4() - vtk.vtkMatrix4x4.Invert(sliceXyToRas, rasToSliceXy) - self.worldToSliceTransform.SetMatrix(rasToSliceXy) - - # brush is rotated to the slice widget plane - brushToWorldOriginTransformMatrix = vtk.vtkMatrix4x4() - brushToWorldOriginTransformMatrix.DeepCopy(self.sliceWidget.sliceLogic().GetSliceNode().GetSliceToRAS()) - brushToWorldOriginTransformMatrix.SetElement(0,3, 0) - brushToWorldOriginTransformMatrix.SetElement(1,3, 0) - brushToWorldOriginTransformMatrix.SetElement(2,3, 0) - - self.brushToWorldOriginTransform.Identity() - self.brushToWorldOriginTransform.Concatenate(brushToWorldOriginTransformMatrix) - self.brushToWorldOriginTransform.RotateX(90) # cylinder's long axis is the Y axis, we need to rotate it to Z axis - - sliceSpacingMm = self.scriptedEffect.sliceSpacing(self.sliceWidget) - - center = [0,0,0] - if self.brushMode == HISTOGRAM_BRUSH_TYPE_CIRCLE: - center = self.point1 - - point1ToPoint2 = [0,0,0] - vtk.vtkMath.Subtract(self.point1, self.point2, point1ToPoint2) - radius = vtk.vtkMath.Normalize(point1ToPoint2) - - self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort()) - self.brushCylinderSource.SetRadius(radius) - self.brushCylinderSource.SetHeight(sliceSpacingMm) - - elif self.brushMode == HISTOGRAM_BRUSH_TYPE_BOX: - self.brushToWorldOriginTransformer.SetInputConnection(self.brushCubeSource.GetOutputPort()) - - length = [0,0,0] - for i in range(3): - center[i] = (self.point1[i] + self.point2[i]) / 2.0 - length[i] = abs(self.point1[i] - self.point2[i]) - - xVector = [1,0,0,0] - self.brushToWorldOriginTransform.MultiplyPoint(xVector, xVector) - xLength = abs(vtk.vtkMath.Dot(xVector[:3], length)) - self.brushCubeSource.SetXLength(xLength) - - zVector = [0,0,1,0] - self.brushToWorldOriginTransform.MultiplyPoint(zVector, zVector) - zLength = abs(vtk.vtkMath.Dot(zVector[:3], length)) - self.brushCubeSource.SetZLength(zLength) - self.brushCubeSource.SetYLength(sliceSpacingMm) - - elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE: - self.brushLineSource.SetPoint1(self.point1) - self.brushLineSource.SetPoint2(self.point2) - self.brushTubeSource.SetRadius(sliceSpacingMm) - - self.worldOriginToWorldTransform.Identity() - self.worldOriginToWorldTransform.Translate(center) - - self.sliceWidget.sliceView().scheduleRender() + def __init__(self, thresholdEffect, scriptedEffect, sliceWidget, brushMode): + self.thresholdEffect = thresholdEffect + self.scriptedEffect = scriptedEffect + self.sliceWidget = sliceWidget + self.brushMode = brushMode + self.state = HISTOGRAM_STATE_OFF + + self.point1 = None + self.point2 = None + + # Actor setup + self.brushCylinderSource = vtk.vtkCylinderSource() + self.brushCylinderSource.SetResolution(32) + + self.brushCubeSource = vtk.vtkCubeSource() + + self.brushLineSource = vtk.vtkLineSource() + self.brushTubeSource = vtk.vtkTubeFilter() + self.brushTubeSource.SetInputConnection(self.brushLineSource.GetOutputPort()) + self.brushTubeSource.SetNumberOfSides(50) + self.brushTubeSource.SetCapping(True) + + self.brushToWorldOriginTransform = vtk.vtkTransform() + self.brushToWorldOriginTransformer = vtk.vtkTransformPolyDataFilter() + self.brushToWorldOriginTransformer.SetTransform(self.brushToWorldOriginTransform) + self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort()) + + self.normalFilter = vtk.vtkPolyDataNormals() + self.normalFilter.AutoOrientNormalsOn() + self.normalFilter.SetInputConnection(self.brushToWorldOriginTransformer.GetOutputPort()) + + # Brush to RAS transform + self.worldOriginToWorldTransform = vtk.vtkTransform() + self.worldOriginToWorldTransformer = vtk.vtkTransformPolyDataFilter() + self.worldOriginToWorldTransformer.SetTransform(self.worldOriginToWorldTransform) + self.worldOriginToWorldTransformer.SetInputConnection(self.normalFilter.GetOutputPort()) + + # RAS to XY transform + self.worldToSliceTransform = vtk.vtkTransform() + self.worldToSliceTransformer = vtk.vtkTransformPolyDataFilter() + self.worldToSliceTransformer.SetTransform(self.worldToSliceTransform) + self.worldToSliceTransformer.SetInputConnection(self.worldOriginToWorldTransformer.GetOutputPort()) + + # Cutting takes place in XY coordinates + self.slicePlane = vtk.vtkPlane() + self.slicePlane.SetNormal(0, 0, 1) + self.slicePlane.SetOrigin(0, 0, 0) + self.cutter = vtk.vtkCutter() + self.cutter.SetCutFunction(self.slicePlane) + self.cutter.SetInputConnection(self.worldToSliceTransformer.GetOutputPort()) + + self.rasPoints = vtk.vtkPoints() + lines = vtk.vtkCellArray() + self.polyData = vtk.vtkPolyData() + self.polyData.SetPoints(self.rasPoints) + self.polyData.SetLines(lines) + + # Thin line + self.thinRASPoints = vtk.vtkPoints() + thinLines = vtk.vtkCellArray() + self.thinPolyData = vtk.vtkPolyData() + self.thinPolyData.SetPoints(self.rasPoints) + self.thinPolyData.SetLines(thinLines) + + self.mapper = vtk.vtkPolyDataMapper2D() + self.mapper.SetInputConnection(self.cutter.GetOutputPort()) + + # Add actor + self.actor = vtk.vtkActor2D() + self.actor.SetMapper(self.mapper) + actorProperty = self.actor.GetProperty() + actorProperty.SetColor(1, 1, 0) + actorProperty.SetLineWidth(2) + renderer = self.scriptedEffect.renderer(sliceWidget) + if renderer is None: + logging.error("pipelineForWidget: Failed to get renderer!") + return None + self.scriptedEffect.addActor2D(sliceWidget, self.actor) + + self.thinActor = None + if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW: + self.worldToSliceTransformer.SetInputData(self.polyData) + self.mapper.SetInputConnection(self.worldToSliceTransformer.GetOutputPort()) + + self.thinWorldToSliceTransformer = vtk.vtkTransformPolyDataFilter() + self.thinWorldToSliceTransformer.SetInputData(self.thinPolyData) + self.thinWorldToSliceTransformer.SetTransform(self.worldToSliceTransform) + + self.thinMapper = vtk.vtkPolyDataMapper2D() + self.thinMapper.SetInputConnection(self.thinWorldToSliceTransformer.GetOutputPort()) + + self.thinActor = vtk.vtkActor2D() + self.thinActor.SetMapper(self.thinMapper) + thinActorProperty = self.thinActor.GetProperty() + thinActorProperty.SetColor(1, 1, 0) + thinActorProperty.SetLineWidth(1) + self.scriptedEffect.addActor2D(sliceWidget, self.thinActor) + elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE: + self.worldToSliceTransformer.SetInputConnection(self.brushTubeSource.GetOutputPort()) + + def removeActors(self): + if self.actor is not None: + self.scriptedEffect.removeActor2D(self.sliceWidget, self.actor) + if self.thinActor is not None: + self.scriptedEffect.removeActor2D(self.sliceWidget, self.thinActor) + + def setPoint1(self, ras): + self.point1 = ras + self.updateBrushModel() + + def setPoint2(self, ras): + self.point2 = ras + self.updateBrushModel() + + def addPoint(self, ras): + if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW: + newPointIndex = self.rasPoints.InsertNextPoint(ras) + previousPointIndex = newPointIndex - 1 + if (previousPointIndex >= 0): + idList = vtk.vtkIdList() + idList.InsertNextId(previousPointIndex) + idList.InsertNextId(newPointIndex) + self.polyData.InsertNextCell(vtk.VTK_LINE, idList) + + thinLines = self.thinPolyData.GetLines() + thinLines.Initialize() + idList = vtk.vtkIdList() + idList.InsertNextId(newPointIndex) + idList.InsertNextId(0) + self.thinPolyData.InsertNextCell(vtk.VTK_LINE, idList) + + else: + if self.point1 is None: + self.setPoint1(ras) + self.setPoint2(ras) + + def updateBrushModel(self): + if self.brushMode != HISTOGRAM_BRUSH_TYPE_DRAW and (self.point1 is None or self.point2 is None): + return + + # Update slice cutting plane position and orientation + sliceXyToRas = self.sliceWidget.sliceLogic().GetSliceNode().GetXYToRAS() + rasToSliceXy = vtk.vtkMatrix4x4() + vtk.vtkMatrix4x4.Invert(sliceXyToRas, rasToSliceXy) + self.worldToSliceTransform.SetMatrix(rasToSliceXy) + + # brush is rotated to the slice widget plane + brushToWorldOriginTransformMatrix = vtk.vtkMatrix4x4() + brushToWorldOriginTransformMatrix.DeepCopy(self.sliceWidget.sliceLogic().GetSliceNode().GetSliceToRAS()) + brushToWorldOriginTransformMatrix.SetElement(0, 3, 0) + brushToWorldOriginTransformMatrix.SetElement(1, 3, 0) + brushToWorldOriginTransformMatrix.SetElement(2, 3, 0) + + self.brushToWorldOriginTransform.Identity() + self.brushToWorldOriginTransform.Concatenate(brushToWorldOriginTransformMatrix) + self.brushToWorldOriginTransform.RotateX(90) # cylinder's long axis is the Y axis, we need to rotate it to Z axis + + sliceSpacingMm = self.scriptedEffect.sliceSpacing(self.sliceWidget) + + center = [0, 0, 0] + if self.brushMode == HISTOGRAM_BRUSH_TYPE_CIRCLE: + center = self.point1 + + point1ToPoint2 = [0, 0, 0] + vtk.vtkMath.Subtract(self.point1, self.point2, point1ToPoint2) + radius = vtk.vtkMath.Normalize(point1ToPoint2) + + self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort()) + self.brushCylinderSource.SetRadius(radius) + self.brushCylinderSource.SetHeight(sliceSpacingMm) + + elif self.brushMode == HISTOGRAM_BRUSH_TYPE_BOX: + self.brushToWorldOriginTransformer.SetInputConnection(self.brushCubeSource.GetOutputPort()) + + length = [0, 0, 0] + for i in range(3): + center[i] = (self.point1[i] + self.point2[i]) / 2.0 + length[i] = abs(self.point1[i] - self.point2[i]) + + xVector = [1, 0, 0, 0] + self.brushToWorldOriginTransform.MultiplyPoint(xVector, xVector) + xLength = abs(vtk.vtkMath.Dot(xVector[:3], length)) + self.brushCubeSource.SetXLength(xLength) + + zVector = [0, 0, 1, 0] + self.brushToWorldOriginTransform.MultiplyPoint(zVector, zVector) + zLength = abs(vtk.vtkMath.Dot(zVector[:3], length)) + self.brushCubeSource.SetZLength(zLength) + self.brushCubeSource.SetYLength(sliceSpacingMm) + + elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE: + self.brushLineSource.SetPoint1(self.point1) + self.brushLineSource.SetPoint2(self.point2) + self.brushTubeSource.SetRadius(sliceSpacingMm) + + self.worldOriginToWorldTransform.Identity() + self.worldOriginToWorldTransform.Translate(center) + + self.sliceWidget.sliceView().scheduleRender() HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME = "BrushType" diff --git a/Modules/Loadable/Segmentations/Testing/Python/SegmentationWidgetsTest1.py b/Modules/Loadable/Segmentations/Testing/Python/SegmentationWidgetsTest1.py index 892f88c6989..301a4c47eb6 100644 --- a/Modules/Loadable/Segmentations/Testing/Python/SegmentationWidgetsTest1.py +++ b/Modules/Loadable/Segmentations/Testing/Python/SegmentationWidgetsTest1.py @@ -9,403 +9,403 @@ class SegmentationWidgetsTest1(ScriptedLoadableModuleTest): - 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_SegmentationWidgetsTest1() - - #------------------------------------------------------------------------------ - def test_SegmentationWidgetsTest1(self): - # Check for modules - self.assertIsNotNone( slicer.modules.segmentations ) - - self.TestSection_00_SetupPathsAndNames() - self.TestSection_01_GenerateInputData() - self.TestSection_02_qMRMLSegmentsTableView() - self.TestSection_03_qMRMLSegmentationGeometryWidget() - self.TestSection_04_qMRMLSegmentEditorWidget() - - logging.info('Test finished') - - #------------------------------------------------------------------------------ - def TestSection_00_SetupPathsAndNames(self): - logging.info('Test section 0: SetupPathsAndNames') - self.inputSegmentationNode = None - - #------------------------------------------------------------------------------ - def TestSection_01_GenerateInputData(self): - logging.info('Test section 1: GenerateInputData') - self.inputSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') - - # Create new segments - import random - for segmentName in ['first', 'second', 'third']: - sphereSegment = slicer.vtkSegment() - sphereSegment.SetName(segmentName) - sphereSegment.SetColor(random.uniform(0.0,1.0), random.uniform(0.0,1.0), random.uniform(0.0,1.0)) - - sphere = vtk.vtkSphereSource() - sphere.SetCenter(random.uniform(0,100),random.uniform(0,100),random.uniform(0,100)) - sphere.SetRadius(random.uniform(20,30)) - sphere.Update() - spherePolyData = sphere.GetOutput() - sphereSegment.AddRepresentation( - slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), - spherePolyData) - - self.inputSegmentationNode.GetSegmentation().AddSegment(sphereSegment) - - self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3) - - self.inputSegmentationNode.CreateDefaultDisplayNodes() - displayNode = self.inputSegmentationNode.GetDisplayNode() - self.assertIsNotNone(displayNode) - - #------------------------------------------------------------------------------ - def TestSection_02_qMRMLSegmentsTableView(self): - logging.info('Test section 2: qMRMLSegmentsTableView') - - displayNode = self.inputSegmentationNode.GetDisplayNode() - self.assertIsNotNone(displayNode) - - segmentsTableView = slicer.qMRMLSegmentsTableView() - segmentsTableView.setMRMLScene(slicer.mrmlScene) - segmentsTableView.setSegmentationNode(self.inputSegmentationNode) - self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 3) - segmentsTableView.show() - slicer.app.processEvents() - slicer.util.delayDisplay("All shown") - - segmentsTableView.setHideSegments(['second']) - self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 2) - slicer.app.processEvents() - slicer.util.delayDisplay("Hidden 'second'") - - segmentsTableView.setHideSegments([]) - segmentsTableView.filterBarVisible = True - segmentsTableView.textFilter = "third" - self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 1) - slicer.app.processEvents() - slicer.util.delayDisplay("All but 'third' filtered") - - segmentsTableView.textFilter = "" - firstSegment = self.inputSegmentationNode.GetSegmentation().GetSegment("first") - logic = slicer.modules.segmentations.logic() - logic.SetSegmentStatus(firstSegment, logic.InProgress) - sortFilterProxyModel = segmentsTableView.sortFilterProxyModel() - sortFilterProxyModel.setShowStatus(logic.NotStarted, True) - self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 2) - slicer.app.processEvents() - slicer.util.delayDisplay("'NotStarted' shown") - - segmentsTableView.setSelectedSegmentIDs(["third"]) - segmentsTableView.setHideSegments(['second']) - segmentsTableView.showOnlySelectedSegments() - self.assertEqual(displayNode.GetSegmentVisibility("first"), False) - self.assertEqual(displayNode.GetSegmentVisibility("second"), True) - self.assertEqual(displayNode.GetSegmentVisibility("third"), True) - slicer.app.processEvents() - slicer.util.delayDisplay("Show only selected segments") - - displayNode.SetSegmentVisibility("first", True) - displayNode.SetSegmentVisibility("second", True) - displayNode.SetSegmentVisibility("third", True) - segmentsTableView.filterBarVisible = False - - # Reset the filtering parameters in the segmentation node to avoid interference with other tests that - # use this segmentation node - self.inputSegmentationNode.SetSegmentListFilterEnabled(False) - self.inputSegmentationNode.SetSegmentListFilterOptions("") - - #------------------------------------------------------------------------------ - def compareOutputGeometry(self, orientedImageData, spacing, origin, directions): - if orientedImageData is None: - logging.error('Invalid input oriented image data') - return False - if (not isinstance(spacing, list) and not isinstance(spacing, tuple)) \ - or (not isinstance(origin, list) and not isinstance(origin, tuple)) \ - or not isinstance(directions, list): - logging.error('Invalid baseline object types - need lists') - return False - if len(spacing) != 3 or len(origin) != 3 or len(directions) != 3 \ - or len(directions[0]) != 3 or len(directions[1]) != 3 or len(directions[2]) != 3: - logging.error('Baseline lists need to contain 3 elements each, the directions 3 lists of 3') - return False - import numpy - tolerance = 0.0001 - actualSpacing = orientedImageData.GetSpacing() - actualOrigin = orientedImageData.GetOrigin() - actualDirections = [[0]*3,[0]*3,[0]*3] - orientedImageData.GetDirections(actualDirections) - for i in [0,1,2]: - if not numpy.isclose(spacing[i], actualSpacing[i], tolerance): - logging.warning('Spacing discrepancy: ' + str(spacing) + ' != ' + str(actualSpacing)) - return False - if not numpy.isclose(origin[i], actualOrigin[i], tolerance): - logging.warning('Origin discrepancy: ' + str(origin) + ' != ' + str(actualOrigin)) - return False - for j in [0,1,2]: - if not numpy.isclose(directions[i][j], actualDirections[i][j], tolerance): - logging.warning('Directions discrepancy: ' + str(directions) + ' != ' + str(actualDirections)) - return False - return True - - #------------------------------------------------------------------------------ - def getForegroundVoxelCount(self, imageData): - if imageData is None: - logging.error('Invalid input image data') - return False - imageAccumulate = vtk.vtkImageAccumulate() - imageAccumulate.SetInputData(imageData) - imageAccumulate.SetIgnoreZero(1) - imageAccumulate.Update() - return imageAccumulate.GetVoxelCount() - - #------------------------------------------------------------------------------ - def TestSection_03_qMRMLSegmentationGeometryWidget(self): - logging.info('Test section 2: qMRMLSegmentationGeometryWidget') - - binaryLabelmapReprName = slicer.vtkSegmentationConverter.GetBinaryLabelmapRepresentationName() - closedSurfaceReprName = slicer.vtkSegmentationConverter.GetClosedSurfaceRepresentationName() - - # Use MRHead and Tinypatient for testing - import SampleData - mrVolumeNode = SampleData.downloadSample("MRHead") - [tinyVolumeNode, tinySegmentationNode] = SampleData.downloadSamples('TinyPatient') - - # Convert MRHead to oriented image data - import vtkSlicerSegmentationsModuleLogicPython as vtkSlicerSegmentationsModuleLogic - mrOrientedImageData = vtkSlicerSegmentationsModuleLogic.vtkSlicerSegmentationsModuleLogic.CreateOrientedImageDataFromVolumeNode(mrVolumeNode) - mrOrientedImageData.UnRegister(None) - - # Create segmentation node with binary labelmap master and one segment with MRHead geometry - segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') - segmentationNode.GetSegmentation().SetMasterRepresentationName(binaryLabelmapReprName) - geometryStr = slicer.vtkSegmentationConverter.SerializeImageGeometry(mrOrientedImageData) - segmentationNode.GetSegmentation().SetConversionParameter( - slicer.vtkSegmentationConverter.GetReferenceImageGeometryParameterName(), geometryStr) - - threshold = vtk.vtkImageThreshold() - threshold.SetInputData(mrOrientedImageData) - threshold.ThresholdByUpper(16.0) - threshold.SetInValue(1) - threshold.SetOutValue(0) - threshold.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) - threshold.Update() - segmentOrientedImageData = slicer.vtkOrientedImageData() - segmentOrientedImageData.DeepCopy(threshold.GetOutput()) - mrImageToWorldMatrix = vtk.vtkMatrix4x4() - mrOrientedImageData.GetImageToWorldMatrix(mrImageToWorldMatrix) - segmentOrientedImageData.SetImageToWorldMatrix(mrImageToWorldMatrix) - segment = slicer.vtkSegment() - segment.SetName('Brain') - segment.SetColor(0.0,0.0,1.0) - segment.AddRepresentation(binaryLabelmapReprName, segmentOrientedImageData) - segmentationNode.GetSegmentation().AddSegment(segment) - - # Create geometry widget - geometryWidget = slicer.qMRMLSegmentationGeometryWidget() - geometryWidget.setSegmentationNode(segmentationNode) - geometryWidget.editEnabled = True - geometryImageData = slicer.vtkOrientedImageData() # To contain the output later - - # Volume source with no transforms - geometryWidget.setSourceNode(tinyVolumeNode) - geometryWidget.geometryImageData(geometryImageData) - self.assertTrue(self.compareOutputGeometry(geometryImageData, - (49,49,23), (248.8439, 248.2890, -123.75), - [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) - slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentOrientedImageData, geometryImageData, geometryImageData, False, True) - self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 92) - - # Transformed volume source - translationTransformMatrix = vtk.vtkMatrix4x4() - translationTransformMatrix.SetElement(0,3,24.5) - translationTransformMatrix.SetElement(1,3,24.5) - translationTransformMatrix.SetElement(2,3,11.5) - translationTransformNode = slicer.vtkMRMLLinearTransformNode() - translationTransformNode.SetName('TestTranslation') - slicer.mrmlScene.AddNode(translationTransformNode) - translationTransformNode.SetMatrixTransformToParent(translationTransformMatrix) - - tinyVolumeNode.SetAndObserveTransformNodeID(translationTransformNode.GetID()) - geometryWidget.geometryImageData(geometryImageData) - self.assertTrue(self.compareOutputGeometry(geometryImageData, - (49,49,23), (273.3439, 272.7890, -112.25), - [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) - slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentOrientedImageData, geometryImageData, geometryImageData, False, True) - self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 94) - - # Volume source with isotropic spacing - tinyVolumeNode.SetAndObserveTransformNodeID(None) - geometryWidget.setIsotropicSpacing(True) - geometryWidget.geometryImageData(geometryImageData) - self.assertTrue(self.compareOutputGeometry(geometryImageData, - (23,23,23), (248.8439, 248.2890, -123.75), - [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) - slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentOrientedImageData, geometryImageData, geometryImageData, False, True) - self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 414) - - # Volume source with oversampling - geometryWidget.setIsotropicSpacing(False) - geometryWidget.setOversamplingFactor(2.0) - geometryWidget.geometryImageData(geometryImageData) - self.assertTrue(self.compareOutputGeometry(geometryImageData, - (24.5, 24.5, 11.5), (261.0939, 260.5390, -129.5), - [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) - slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentOrientedImageData, geometryImageData, geometryImageData, False, True) - self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 751) - slicer.util.delayDisplay('Volume source cases - OK') - - # Segmentation source with binary labelmap master - geometryWidget.setOversamplingFactor(1.0) - geometryWidget.setSourceNode(tinySegmentationNode) - geometryWidget.geometryImageData(geometryImageData) - self.assertTrue(self.compareOutputGeometry(geometryImageData, - (49,49,23), (248.8439, 248.2890, -123.75), - [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) - slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentOrientedImageData, geometryImageData, geometryImageData, False, True) - self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 92) - - # Segmentation source with closed surface master - tinySegmentationNode.GetSegmentation().SetConversionParameter('Smoothing factor', '0.0') - self.assertTrue(tinySegmentationNode.GetSegmentation().CreateRepresentation(closedSurfaceReprName)) - tinySegmentationNode.GetSegmentation().SetMasterRepresentationName(closedSurfaceReprName) - tinySegmentationNode.Modified() # Trigger re-calculation of geometry (only generic Modified event is observed) - geometryWidget.geometryImageData(geometryImageData) - self.assertTrue(self.compareOutputGeometry(geometryImageData, - (1,1,1), (-86.645, 133.929, 116.786), # current origin of the segmentation is kept - [[0.0, 0.0, 1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]])) - slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentOrientedImageData, geometryImageData, geometryImageData, False, True) - self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040) - slicer.util.delayDisplay('Segmentation source cases - OK') - - # Model source with no transform - shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - outputFolderId = shNode.CreateFolderItem(shNode.GetSceneItemID(), 'ModelsFolder') - success = vtkSlicerSegmentationsModuleLogic.vtkSlicerSegmentationsModuleLogic.ExportVisibleSegmentsToModels( - tinySegmentationNode, outputFolderId ) - self.assertTrue(success) - modelNode = slicer.util.getNode('Body_Contour') - geometryWidget.setSourceNode(modelNode) - geometryWidget.geometryImageData(geometryImageData) - self.assertTrue(self.compareOutputGeometry(geometryImageData, - (1,1,1), (-86.645, 133.929, 116.786), # current origin of the segmentation is kept - [[0.0, 0.0, 1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]])) - slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentOrientedImageData, geometryImageData, geometryImageData, False, True) - self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040) - slicer.util.delayDisplay('Model source - OK') - - # Transformed model source - rotationTransform = vtk.vtkTransform() - rotationTransform.RotateX(45) - rotationTransformMatrix = vtk.vtkMatrix4x4() - rotationTransform.GetMatrix(rotationTransformMatrix) - rotationTransformNode = slicer.vtkMRMLLinearTransformNode() - rotationTransformNode.SetName('TestRotation') - slicer.mrmlScene.AddNode(rotationTransformNode) - rotationTransformNode.SetMatrixTransformToParent(rotationTransformMatrix) - - modelNode.SetAndObserveTransformNodeID(rotationTransformNode.GetID()) - modelNode.Modified() - geometryWidget.geometryImageData(geometryImageData) - self.assertTrue(self.compareOutputGeometry(geometryImageData, - (1,1,1), (-86.645, 177.282, -12.122), - [[0.0, 0.0, 1.0], [-0.7071, -0.7071, 0.0], [0.7071, -0.7071, 0.0]])) - slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentOrientedImageData, geometryImageData, geometryImageData, False, True) - self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5229164) - slicer.util.delayDisplay('Transformed model source - OK') - - # ROI source - roiNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsROINode", 'SourceROI') - rasDimensions = [0.0, 0.0, 0.0] - rasCenter = [0.0, 0.0, 0.0] - slicer.vtkMRMLSliceLogic.GetVolumeRASBox(tinyVolumeNode, rasDimensions, rasCenter) - print(f"rasDimensions={rasDimensions}, rasCenter={rasCenter}") - rasRadius = [x/2.0 for x in rasDimensions] - roiNode.SetCenter(rasCenter) - roiNode.SetRadiusXYZ(rasRadius) - geometryWidget.setSourceNode(roiNode) - geometryWidget.geometryImageData(geometryImageData) - print(f"geometryImageData: {geometryImageData}") - self.assertTrue(self.compareOutputGeometry(geometryImageData, - (1,1,1), (28.344, 27.789, -20.25), - [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])) - slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentOrientedImageData, geometryImageData, geometryImageData, False, True) - self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040) - slicer.util.delayDisplay('ROI source - OK') - - slicer.util.delayDisplay('Segmentation geometry widget test passed') - - #------------------------------------------------------------------------------ - def TestSection_04_qMRMLSegmentEditorWidget(self): - logging.info('Test section 4: qMRMLSegmentEditorWidget') - - self.segmentEditorNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentEditorNode') - self.assertIsNotNone(self.segmentEditorNode) - - self.inputSegmentationNode.SetSegmentListFilterEnabled(False) - self.inputSegmentationNode.SetSegmentListFilterOptions("") - - displayNode = self.inputSegmentationNode.GetDisplayNode() - self.assertIsNotNone(displayNode) - - segmentEditorWidget = slicer.qMRMLSegmentEditorWidget() - segmentEditorWidget.setMRMLSegmentEditorNode(self.segmentEditorNode) - segmentEditorWidget.setMRMLScene(slicer.mrmlScene) - segmentEditorWidget.setSegmentationNode(self.inputSegmentationNode) - segmentEditorWidget.installKeyboardShortcuts(segmentEditorWidget) - segmentEditorWidget.setFocus(qt.Qt.OtherFocusReason) - segmentEditorWidget.show() - - self.segmentEditorNode.SetSelectedSegmentID('first') - self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') - slicer.app.processEvents() - slicer.util.delayDisplay("First selected") - - segmentEditorWidget.selectNextSegment() - self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'second') - slicer.app.processEvents() - slicer.util.delayDisplay("Next segment") - - segmentEditorWidget.selectPreviousSegment() - self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') - slicer.app.processEvents() - slicer.util.delayDisplay("Previous segment") - - displayNode.SetSegmentVisibility('second', False) - segmentEditorWidget.selectNextSegment() - self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'third') - slicer.app.processEvents() - slicer.util.delayDisplay("Next segment (with second segment hidden)") - - # Trying to go out of bounds past first segment - segmentEditorWidget.selectPreviousSegment() #First - self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') - segmentEditorWidget.selectPreviousSegment() #First - self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') - segmentEditorWidget.selectPreviousSegment() #First - self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') - slicer.app.processEvents() - slicer.util.delayDisplay("Multiple previous segment") - - # Wrap around - self.segmentEditorNode.SetSelectedSegmentID('third') - segmentEditorWidget.selectNextSegment() - self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') - slicer.util.delayDisplay("Wrap around segments") + 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_SegmentationWidgetsTest1() + + # ------------------------------------------------------------------------------ + def test_SegmentationWidgetsTest1(self): + # Check for modules + self.assertIsNotNone(slicer.modules.segmentations) + + self.TestSection_00_SetupPathsAndNames() + self.TestSection_01_GenerateInputData() + self.TestSection_02_qMRMLSegmentsTableView() + self.TestSection_03_qMRMLSegmentationGeometryWidget() + self.TestSection_04_qMRMLSegmentEditorWidget() + + logging.info('Test finished') + + # ------------------------------------------------------------------------------ + def TestSection_00_SetupPathsAndNames(self): + logging.info('Test section 0: SetupPathsAndNames') + self.inputSegmentationNode = None + + # ------------------------------------------------------------------------------ + def TestSection_01_GenerateInputData(self): + logging.info('Test section 1: GenerateInputData') + self.inputSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') + + # Create new segments + import random + for segmentName in ['first', 'second', 'third']: + sphereSegment = slicer.vtkSegment() + sphereSegment.SetName(segmentName) + sphereSegment.SetColor(random.uniform(0.0, 1.0), random.uniform(0.0, 1.0), random.uniform(0.0, 1.0)) + + sphere = vtk.vtkSphereSource() + sphere.SetCenter(random.uniform(0, 100), random.uniform(0, 100), random.uniform(0, 100)) + sphere.SetRadius(random.uniform(20, 30)) + sphere.Update() + spherePolyData = sphere.GetOutput() + sphereSegment.AddRepresentation( + slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), + spherePolyData) + + self.inputSegmentationNode.GetSegmentation().AddSegment(sphereSegment) + + self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3) + + self.inputSegmentationNode.CreateDefaultDisplayNodes() + displayNode = self.inputSegmentationNode.GetDisplayNode() + self.assertIsNotNone(displayNode) + + # ------------------------------------------------------------------------------ + def TestSection_02_qMRMLSegmentsTableView(self): + logging.info('Test section 2: qMRMLSegmentsTableView') + + displayNode = self.inputSegmentationNode.GetDisplayNode() + self.assertIsNotNone(displayNode) + + segmentsTableView = slicer.qMRMLSegmentsTableView() + segmentsTableView.setMRMLScene(slicer.mrmlScene) + segmentsTableView.setSegmentationNode(self.inputSegmentationNode) + self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 3) + segmentsTableView.show() + slicer.app.processEvents() + slicer.util.delayDisplay("All shown") + + segmentsTableView.setHideSegments(['second']) + self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 2) + slicer.app.processEvents() + slicer.util.delayDisplay("Hidden 'second'") + + segmentsTableView.setHideSegments([]) + segmentsTableView.filterBarVisible = True + segmentsTableView.textFilter = "third" + self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 1) + slicer.app.processEvents() + slicer.util.delayDisplay("All but 'third' filtered") + + segmentsTableView.textFilter = "" + firstSegment = self.inputSegmentationNode.GetSegmentation().GetSegment("first") + logic = slicer.modules.segmentations.logic() + logic.SetSegmentStatus(firstSegment, logic.InProgress) + sortFilterProxyModel = segmentsTableView.sortFilterProxyModel() + sortFilterProxyModel.setShowStatus(logic.NotStarted, True) + self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 2) + slicer.app.processEvents() + slicer.util.delayDisplay("'NotStarted' shown") + + segmentsTableView.setSelectedSegmentIDs(["third"]) + segmentsTableView.setHideSegments(['second']) + segmentsTableView.showOnlySelectedSegments() + self.assertEqual(displayNode.GetSegmentVisibility("first"), False) + self.assertEqual(displayNode.GetSegmentVisibility("second"), True) + self.assertEqual(displayNode.GetSegmentVisibility("third"), True) + slicer.app.processEvents() + slicer.util.delayDisplay("Show only selected segments") + + displayNode.SetSegmentVisibility("first", True) + displayNode.SetSegmentVisibility("second", True) + displayNode.SetSegmentVisibility("third", True) + segmentsTableView.filterBarVisible = False + + # Reset the filtering parameters in the segmentation node to avoid interference with other tests that + # use this segmentation node + self.inputSegmentationNode.SetSegmentListFilterEnabled(False) + self.inputSegmentationNode.SetSegmentListFilterOptions("") + + # ------------------------------------------------------------------------------ + def compareOutputGeometry(self, orientedImageData, spacing, origin, directions): + if orientedImageData is None: + logging.error('Invalid input oriented image data') + return False + if (not isinstance(spacing, list) and not isinstance(spacing, tuple)) \ + or (not isinstance(origin, list) and not isinstance(origin, tuple)) \ + or not isinstance(directions, list): + logging.error('Invalid baseline object types - need lists') + return False + if len(spacing) != 3 or len(origin) != 3 or len(directions) != 3 \ + or len(directions[0]) != 3 or len(directions[1]) != 3 or len(directions[2]) != 3: + logging.error('Baseline lists need to contain 3 elements each, the directions 3 lists of 3') + return False + import numpy + tolerance = 0.0001 + actualSpacing = orientedImageData.GetSpacing() + actualOrigin = orientedImageData.GetOrigin() + actualDirections = [[0] * 3, [0] * 3, [0] * 3] + orientedImageData.GetDirections(actualDirections) + for i in [0, 1, 2]: + if not numpy.isclose(spacing[i], actualSpacing[i], tolerance): + logging.warning('Spacing discrepancy: ' + str(spacing) + ' != ' + str(actualSpacing)) + return False + if not numpy.isclose(origin[i], actualOrigin[i], tolerance): + logging.warning('Origin discrepancy: ' + str(origin) + ' != ' + str(actualOrigin)) + return False + for j in [0, 1, 2]: + if not numpy.isclose(directions[i][j], actualDirections[i][j], tolerance): + logging.warning('Directions discrepancy: ' + str(directions) + ' != ' + str(actualDirections)) + return False + return True + + # ------------------------------------------------------------------------------ + def getForegroundVoxelCount(self, imageData): + if imageData is None: + logging.error('Invalid input image data') + return False + imageAccumulate = vtk.vtkImageAccumulate() + imageAccumulate.SetInputData(imageData) + imageAccumulate.SetIgnoreZero(1) + imageAccumulate.Update() + return imageAccumulate.GetVoxelCount() + + # ------------------------------------------------------------------------------ + def TestSection_03_qMRMLSegmentationGeometryWidget(self): + logging.info('Test section 2: qMRMLSegmentationGeometryWidget') + + binaryLabelmapReprName = slicer.vtkSegmentationConverter.GetBinaryLabelmapRepresentationName() + closedSurfaceReprName = slicer.vtkSegmentationConverter.GetClosedSurfaceRepresentationName() + + # Use MRHead and Tinypatient for testing + import SampleData + mrVolumeNode = SampleData.downloadSample("MRHead") + [tinyVolumeNode, tinySegmentationNode] = SampleData.downloadSamples('TinyPatient') + + # Convert MRHead to oriented image data + import vtkSlicerSegmentationsModuleLogicPython as vtkSlicerSegmentationsModuleLogic + mrOrientedImageData = vtkSlicerSegmentationsModuleLogic.vtkSlicerSegmentationsModuleLogic.CreateOrientedImageDataFromVolumeNode(mrVolumeNode) + mrOrientedImageData.UnRegister(None) + + # Create segmentation node with binary labelmap master and one segment with MRHead geometry + segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') + segmentationNode.GetSegmentation().SetMasterRepresentationName(binaryLabelmapReprName) + geometryStr = slicer.vtkSegmentationConverter.SerializeImageGeometry(mrOrientedImageData) + segmentationNode.GetSegmentation().SetConversionParameter( + slicer.vtkSegmentationConverter.GetReferenceImageGeometryParameterName(), geometryStr) + + threshold = vtk.vtkImageThreshold() + threshold.SetInputData(mrOrientedImageData) + threshold.ThresholdByUpper(16.0) + threshold.SetInValue(1) + threshold.SetOutValue(0) + threshold.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) + threshold.Update() + segmentOrientedImageData = slicer.vtkOrientedImageData() + segmentOrientedImageData.DeepCopy(threshold.GetOutput()) + mrImageToWorldMatrix = vtk.vtkMatrix4x4() + mrOrientedImageData.GetImageToWorldMatrix(mrImageToWorldMatrix) + segmentOrientedImageData.SetImageToWorldMatrix(mrImageToWorldMatrix) + segment = slicer.vtkSegment() + segment.SetName('Brain') + segment.SetColor(0.0, 0.0, 1.0) + segment.AddRepresentation(binaryLabelmapReprName, segmentOrientedImageData) + segmentationNode.GetSegmentation().AddSegment(segment) + + # Create geometry widget + geometryWidget = slicer.qMRMLSegmentationGeometryWidget() + geometryWidget.setSegmentationNode(segmentationNode) + geometryWidget.editEnabled = True + geometryImageData = slicer.vtkOrientedImageData() # To contain the output later + + # Volume source with no transforms + geometryWidget.setSourceNode(tinyVolumeNode) + geometryWidget.geometryImageData(geometryImageData) + self.assertTrue(self.compareOutputGeometry(geometryImageData, + (49, 49, 23), (248.8439, 248.2890, -123.75), + [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) + slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentOrientedImageData, geometryImageData, geometryImageData, False, True) + self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 92) + + # Transformed volume source + translationTransformMatrix = vtk.vtkMatrix4x4() + translationTransformMatrix.SetElement(0, 3, 24.5) + translationTransformMatrix.SetElement(1, 3, 24.5) + translationTransformMatrix.SetElement(2, 3, 11.5) + translationTransformNode = slicer.vtkMRMLLinearTransformNode() + translationTransformNode.SetName('TestTranslation') + slicer.mrmlScene.AddNode(translationTransformNode) + translationTransformNode.SetMatrixTransformToParent(translationTransformMatrix) + + tinyVolumeNode.SetAndObserveTransformNodeID(translationTransformNode.GetID()) + geometryWidget.geometryImageData(geometryImageData) + self.assertTrue(self.compareOutputGeometry(geometryImageData, + (49, 49, 23), (273.3439, 272.7890, -112.25), + [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) + slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentOrientedImageData, geometryImageData, geometryImageData, False, True) + self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 94) + + # Volume source with isotropic spacing + tinyVolumeNode.SetAndObserveTransformNodeID(None) + geometryWidget.setIsotropicSpacing(True) + geometryWidget.geometryImageData(geometryImageData) + self.assertTrue(self.compareOutputGeometry(geometryImageData, + (23, 23, 23), (248.8439, 248.2890, -123.75), + [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) + slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentOrientedImageData, geometryImageData, geometryImageData, False, True) + self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 414) + + # Volume source with oversampling + geometryWidget.setIsotropicSpacing(False) + geometryWidget.setOversamplingFactor(2.0) + geometryWidget.geometryImageData(geometryImageData) + self.assertTrue(self.compareOutputGeometry(geometryImageData, + (24.5, 24.5, 11.5), (261.0939, 260.5390, -129.5), + [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) + slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentOrientedImageData, geometryImageData, geometryImageData, False, True) + self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 751) + slicer.util.delayDisplay('Volume source cases - OK') + + # Segmentation source with binary labelmap master + geometryWidget.setOversamplingFactor(1.0) + geometryWidget.setSourceNode(tinySegmentationNode) + geometryWidget.geometryImageData(geometryImageData) + self.assertTrue(self.compareOutputGeometry(geometryImageData, + (49, 49, 23), (248.8439, 248.2890, -123.75), + [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]])) + slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentOrientedImageData, geometryImageData, geometryImageData, False, True) + self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 92) + + # Segmentation source with closed surface master + tinySegmentationNode.GetSegmentation().SetConversionParameter('Smoothing factor', '0.0') + self.assertTrue(tinySegmentationNode.GetSegmentation().CreateRepresentation(closedSurfaceReprName)) + tinySegmentationNode.GetSegmentation().SetMasterRepresentationName(closedSurfaceReprName) + tinySegmentationNode.Modified() # Trigger re-calculation of geometry (only generic Modified event is observed) + geometryWidget.geometryImageData(geometryImageData) + self.assertTrue(self.compareOutputGeometry(geometryImageData, + (1, 1, 1), (-86.645, 133.929, 116.786), # current origin of the segmentation is kept + [[0.0, 0.0, 1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]])) + slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentOrientedImageData, geometryImageData, geometryImageData, False, True) + self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040) + slicer.util.delayDisplay('Segmentation source cases - OK') + + # Model source with no transform + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + outputFolderId = shNode.CreateFolderItem(shNode.GetSceneItemID(), 'ModelsFolder') + success = vtkSlicerSegmentationsModuleLogic.vtkSlicerSegmentationsModuleLogic.ExportVisibleSegmentsToModels( + tinySegmentationNode, outputFolderId) + self.assertTrue(success) + modelNode = slicer.util.getNode('Body_Contour') + geometryWidget.setSourceNode(modelNode) + geometryWidget.geometryImageData(geometryImageData) + self.assertTrue(self.compareOutputGeometry(geometryImageData, + (1, 1, 1), (-86.645, 133.929, 116.786), # current origin of the segmentation is kept + [[0.0, 0.0, 1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]])) + slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentOrientedImageData, geometryImageData, geometryImageData, False, True) + self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040) + slicer.util.delayDisplay('Model source - OK') + + # Transformed model source + rotationTransform = vtk.vtkTransform() + rotationTransform.RotateX(45) + rotationTransformMatrix = vtk.vtkMatrix4x4() + rotationTransform.GetMatrix(rotationTransformMatrix) + rotationTransformNode = slicer.vtkMRMLLinearTransformNode() + rotationTransformNode.SetName('TestRotation') + slicer.mrmlScene.AddNode(rotationTransformNode) + rotationTransformNode.SetMatrixTransformToParent(rotationTransformMatrix) + + modelNode.SetAndObserveTransformNodeID(rotationTransformNode.GetID()) + modelNode.Modified() + geometryWidget.geometryImageData(geometryImageData) + self.assertTrue(self.compareOutputGeometry(geometryImageData, + (1, 1, 1), (-86.645, 177.282, -12.122), + [[0.0, 0.0, 1.0], [-0.7071, -0.7071, 0.0], [0.7071, -0.7071, 0.0]])) + slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentOrientedImageData, geometryImageData, geometryImageData, False, True) + self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5229164) + slicer.util.delayDisplay('Transformed model source - OK') + + # ROI source + roiNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsROINode", 'SourceROI') + rasDimensions = [0.0, 0.0, 0.0] + rasCenter = [0.0, 0.0, 0.0] + slicer.vtkMRMLSliceLogic.GetVolumeRASBox(tinyVolumeNode, rasDimensions, rasCenter) + print(f"rasDimensions={rasDimensions}, rasCenter={rasCenter}") + rasRadius = [x / 2.0 for x in rasDimensions] + roiNode.SetCenter(rasCenter) + roiNode.SetRadiusXYZ(rasRadius) + geometryWidget.setSourceNode(roiNode) + geometryWidget.geometryImageData(geometryImageData) + print(f"geometryImageData: {geometryImageData}") + self.assertTrue(self.compareOutputGeometry(geometryImageData, + (1, 1, 1), (28.344, 27.789, -20.25), + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])) + slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentOrientedImageData, geometryImageData, geometryImageData, False, True) + self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040) + slicer.util.delayDisplay('ROI source - OK') + + slicer.util.delayDisplay('Segmentation geometry widget test passed') + + # ------------------------------------------------------------------------------ + def TestSection_04_qMRMLSegmentEditorWidget(self): + logging.info('Test section 4: qMRMLSegmentEditorWidget') + + self.segmentEditorNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentEditorNode') + self.assertIsNotNone(self.segmentEditorNode) + + self.inputSegmentationNode.SetSegmentListFilterEnabled(False) + self.inputSegmentationNode.SetSegmentListFilterOptions("") + + displayNode = self.inputSegmentationNode.GetDisplayNode() + self.assertIsNotNone(displayNode) + + segmentEditorWidget = slicer.qMRMLSegmentEditorWidget() + segmentEditorWidget.setMRMLSegmentEditorNode(self.segmentEditorNode) + segmentEditorWidget.setMRMLScene(slicer.mrmlScene) + segmentEditorWidget.setSegmentationNode(self.inputSegmentationNode) + segmentEditorWidget.installKeyboardShortcuts(segmentEditorWidget) + segmentEditorWidget.setFocus(qt.Qt.OtherFocusReason) + segmentEditorWidget.show() + + self.segmentEditorNode.SetSelectedSegmentID('first') + self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') + slicer.app.processEvents() + slicer.util.delayDisplay("First selected") + + segmentEditorWidget.selectNextSegment() + self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'second') + slicer.app.processEvents() + slicer.util.delayDisplay("Next segment") + + segmentEditorWidget.selectPreviousSegment() + self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') + slicer.app.processEvents() + slicer.util.delayDisplay("Previous segment") + + displayNode.SetSegmentVisibility('second', False) + segmentEditorWidget.selectNextSegment() + self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'third') + slicer.app.processEvents() + slicer.util.delayDisplay("Next segment (with second segment hidden)") + + # Trying to go out of bounds past first segment + segmentEditorWidget.selectPreviousSegment() # First + self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') + segmentEditorWidget.selectPreviousSegment() # First + self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') + segmentEditorWidget.selectPreviousSegment() # First + self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') + slicer.app.processEvents() + slicer.util.delayDisplay("Multiple previous segment") + + # Wrap around + self.segmentEditorNode.SetSelectedSegmentID('third') + segmentEditorWidget.selectNextSegment() + self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first') + slicer.util.delayDisplay("Wrap around segments") diff --git a/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest1.py b/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest1.py index be35833d22c..7bbe0a04b04 100644 --- a/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest1.py +++ b/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest1.py @@ -12,497 +12,497 @@ class SegmentationsModuleTest1(unittest.TestCase): - 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_SegmentationsModuleTest1() - - #------------------------------------------------------------------------------ - def test_SegmentationsModuleTest1(self): - # Check for modules - self.assertIsNotNone( slicer.modules.segmentations ) - - self.TestSection_SetupPathsAndNames() - self.TestSection_RetrieveInputData() - self.TestSection_LoadInputData() - self.TestSection_AddRemoveSegment() - self.TestSection_MergeLabelmapWithDifferentGeometries() - self.TestSection_ImportExportSegment() - self.TestSection_ImportExportSegment2() - self.TestSection_SubjectHierarchy() - - logging.info('Test finished') - - #------------------------------------------------------------------------------ - def TestSection_SetupPathsAndNames(self): - # Set up paths used for this test - self.segmentationsModuleTestDir = slicer.app.temporaryPath + '/SegmentationsModuleTest' - if not os.access(self.segmentationsModuleTestDir, os.F_OK): - os.mkdir(self.segmentationsModuleTestDir) - - self.dataDir = self.segmentationsModuleTestDir + '/TinyPatient_Seg' - if not os.access(self.dataDir, os.F_OK): - os.mkdir(self.dataDir) - self.dataSegDir = self.dataDir + '/TinyPatient_Structures.seg' - - self.dataZipFilePath = self.segmentationsModuleTestDir + '/TinyPatient_Seg.zip' - - # Define variables - self.expectedNumOfFilesInDataDir = 4 - self.expectedNumOfFilesInDataSegDir = 2 - self.inputSegmentationNode = None - self.bodySegmentName = 'Body_Contour' - self.tumorSegmentName = 'Tumor_Contour' - self.secondSegmentationNode = None - self.sphereSegment = None - self.sphereSegmentName = 'Sphere' - self.closedSurfaceReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName() - self.binaryLabelmapReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName() - - #------------------------------------------------------------------------------ - def TestSection_RetrieveInputData(self): - try: - slicer.util.downloadAndExtractArchive( - TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7', - self.dataZipFilePath, self.segmentationsModuleTestDir, - checksum='SHA256:b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7') - - numOfFilesInDataDirTest = len([name for name in os.listdir(self.dataDir) if os.path.isfile(self.dataDir + '/' + name)]) - self.assertEqual( numOfFilesInDataDirTest, self.expectedNumOfFilesInDataDir ) - self.assertTrue( os.access(self.dataSegDir, os.F_OK) ) - numOfFilesInDataSegDirTest = len([name for name in os.listdir(self.dataSegDir) if os.path.isfile(self.dataSegDir + '/' + name)]) - self.assertEqual( numOfFilesInDataSegDirTest, self.expectedNumOfFilesInDataSegDir ) - - except Exception as e: - import traceback - traceback.print_exc() - logging.error('Test caused exception!\n' + str(e)) - - #------------------------------------------------------------------------------ - def TestSection_LoadInputData(self): - # Load into Slicer - slicer.util.loadVolume(self.dataDir + '/TinyPatient_CT.nrrd') - slicer.util.loadNodeFromFile(self.dataDir + '/TinyPatient_Structures.seg.vtm', "SegmentationFile", {}) - - # Change master representation to closed surface (so that conversion is possible when adding segment) - self.inputSegmentationNode = slicer.util.getNode('vtkMRMLSegmentationNode1') - self.inputSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName) - - #------------------------------------------------------------------------------ - def TestSection_AddRemoveSegment(self): - # Add/remove segment from segmentation (check display properties, color table, etc.) - logging.info('Test section: Add/remove segment') - - # Get baseline values - displayNode = self.inputSegmentationNode.GetDisplayNode() - self.assertIsNotNone(displayNode) - # If segments are not found then the returned color is the pre-defined invalid color - bodyColor = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName).GetColor() - logging.info(f"bodyColor: {bodyColor}") - self.assertEqual(int(bodyColor[0]*100), 33) - self.assertEqual(int(bodyColor[1]*100), 66) - self.assertEqual(int(bodyColor[2]*100), 0) - tumorColor = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName).GetColor() - logging.info(f"tumorColor: {tumorColor}") - self.assertEqual(int(tumorColor[0]*100), 100) - self.assertEqual(int(tumorColor[1]*100), 0) - self.assertEqual(int(tumorColor[2]*100), 0) - - # Create new segment - sphere = vtk.vtkSphereSource() - sphere.SetCenter(0,50,0) - sphere.SetRadius(80) - sphere.Update() - spherePolyData = vtk.vtkPolyData() - spherePolyData.DeepCopy(sphere.GetOutput()) - - self.sphereSegment = vtkSegmentationCore.vtkSegment() - self.sphereSegment.SetName(self.sphereSegmentName) - self.sphereSegment.SetColor(0.0,0.0,1.0) - self.sphereSegment.AddRepresentation(self.closedSurfaceReprName, spherePolyData) - - # Add segment to segmentation - self.inputSegmentationNode.GetSegmentation().AddSegment(self.sphereSegment) - self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3) - - # Check merged labelmap - mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData() - self.inputSegmentationNode.GetSegmentation().CreateRepresentation(self.binaryLabelmapReprName) - self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0) - imageStat = vtk.vtkImageAccumulate() - imageStat.SetInputData(mergedLabelmap) - imageStat.SetComponentExtent(0,4,0,0,0,0) - imageStat.SetComponentOrigin(0,0,0) - imageStat.SetComponentSpacing(1,1,1) - imageStat.Update() - imageStatResult = imageStat.GetOutput() - for i in range(4): - logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}") - self.assertEqual(imageStat.GetVoxelCount(), 1000) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0,0,0,0), 786) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1,0,0,0), 170) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2,0,0,0), 4) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3,0,0,0), 40) - - # Check if segment reorder is taken into account in merged labelmap generation - # Change segment order - sphereSegmentId = self.inputSegmentationNode.GetSegmentation().GetSegmentIdBySegment(self.sphereSegment) - self.inputSegmentationNode.GetSegmentation().SetSegmentIndex(sphereSegmentId, 1) - # Re-generate merged labelmap - self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0) - imageStat.SetInputData(mergedLabelmap) - imageStat.Update() - imageStatResult = imageStat.GetOutput() - for i in range(4): - logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}") - self.assertEqual(imageStat.GetVoxelCount(), 1000) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0,0,0,0), 786) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1,0,0,0), 170) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2,0,0,0), 39) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3,0,0,0), 5) - - # Remove segment from segmentation - self.inputSegmentationNode.GetSegmentation().RemoveSegment(self.sphereSegmentName) - self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2) - - #------------------------------------------------------------------------------ - def TestSection_MergeLabelmapWithDifferentGeometries(self): - # Merge labelmap when segments containing labelmaps with different geometries (both same directions, different directions) - logging.info('Test section: Merge labelmap with different geometries') - - self.assertIsNotNone(self.sphereSegment) - self.sphereSegment.RemoveRepresentation(self.binaryLabelmapReprName) - self.assertIsNone(self.sphereSegment.GetRepresentation(self.binaryLabelmapReprName)) - - # Create new segmentation with sphere segment - self.secondSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'Second') - self.secondSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName) - - self.secondSegmentationNode.GetSegmentation().AddSegment(self.sphereSegment) - - # Check automatically converted labelmap. It is supposed to have the default geometry - # (which is different than the one in the input segmentation) - sphereLabelmap = self.sphereSegment.GetRepresentation(self.binaryLabelmapReprName) - self.assertIsNotNone(sphereLabelmap) - sphereLabelmapSpacing = sphereLabelmap.GetSpacing() - self.assertAlmostEqual(sphereLabelmapSpacing[0], 0.629257364931788, 8) - self.assertAlmostEqual(sphereLabelmapSpacing[1], 0.629257364931788, 8) - self.assertAlmostEqual(sphereLabelmapSpacing[2], 0.629257364931788, 8) - - # Create binary labelmap in segmentation that will create the merged labelmap from - # different geometries so that labelmap is not removed from sphere segment when adding - self.inputSegmentationNode.GetSegmentation().CreateRepresentation(self.binaryLabelmapReprName) - - # Copy segment to input segmentation - self.inputSegmentationNode.GetSegmentation().CopySegmentFromSegmentation(self.secondSegmentationNode.GetSegmentation(), self.sphereSegmentName) - self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3) - - # Check merged labelmap - # Reference geometry has the tiny patient spacing, and it is oversampled to have similar - # voxel size as the sphere labelmap with the uniform 0.629mm spacing - mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData() - self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0) - mergedLabelmapSpacing = mergedLabelmap.GetSpacing() - self.assertAlmostEqual(mergedLabelmapSpacing[0], 0.80327868852459, 8) - self.assertAlmostEqual(mergedLabelmapSpacing[1], 0.80327868852459, 8) - self.assertAlmostEqual(mergedLabelmapSpacing[2], 0.377049180327869, 8) - - imageStat = vtk.vtkImageAccumulate() - imageStat.SetInputData(mergedLabelmap) - imageStat.SetComponentExtent(0,5,0,0,0,0) - imageStat.SetComponentOrigin(0,0,0) - imageStat.SetComponentSpacing(1,1,1) - imageStat.Update() - imageStatResult = imageStat.GetOutput() - for i in range(5): - logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}") - self.assertEqual(imageStat.GetVoxelCount(), 226981000) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0,0,0,0), 178838889) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1,0,0,0), 39705288) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2,0,0,0), 890883) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3,0,0,0), 7545940) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(4,0,0,0), 0) # Built from color table and color four is removed in previous test section - - #------------------------------------------------------------------------------ - def TestSection_ImportExportSegment(self): - # Import/export, both one label and all labels - logging.info('Test section: Import/export segment') - - # Export single segment to model node - bodyModelNode = slicer.vtkMRMLModelNode() - bodyModelNode.SetName('BodyModel') - slicer.mrmlScene.AddNode(bodyModelNode) - - bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName) - result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyModelNode) - self.assertTrue(result) - self.assertIsNotNone(bodyModelNode.GetPolyData()) - #TODO: Number of points increased to 1677 due to end-capping, need to investigate! - #self.assertEqual(bodyModelNode.GetPolyData().GetNumberOfPoints(), 302) - #TODO: On Linux and Windows it is 588, on Mac it is 580. Need to investigate - # self.assertEqual(bodyModelNode.GetPolyData().GetNumberOfCells(), 588) - #self.assertTrue(bodyModelNode.GetPolyData().GetNumberOfCells() == 588 or bodyModelNode.GetPolyData().GetNumberOfCells() == 580) - - # Export single segment to volume node - bodyLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() - bodyLabelmapNode.SetName('BodyLabelmap') - slicer.mrmlScene.AddNode(bodyLabelmapNode) - result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNode) - self.assertTrue(result) - bodyImageData = bodyLabelmapNode.GetImageData() - self.assertIsNotNone(bodyImageData) - imageStat = vtk.vtkImageAccumulate() - imageStat.SetInputData(bodyImageData) - imageStat.Update() - self.assertEqual(imageStat.GetVoxelCount(), 792) - self.assertEqual(imageStat.GetMin()[0], 0) - self.assertEqual(imageStat.GetMax()[0], 1) - - # Export multiple segments to volume node - allSegmentsLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() - allSegmentsLabelmapNode.SetName('AllSegmentsLabelmap') - slicer.mrmlScene.AddNode(allSegmentsLabelmapNode) - result = slicer.vtkSlicerSegmentationsModuleLogic.ExportAllSegmentsToLabelmapNode(self.inputSegmentationNode, allSegmentsLabelmapNode) - self.assertTrue(result) - allSegmentsImageData = allSegmentsLabelmapNode.GetImageData() - self.assertIsNotNone(allSegmentsImageData) - imageStat = vtk.vtkImageAccumulate() - imageStat.SetInputData(allSegmentsImageData) - imageStat.SetComponentExtent(0,5,0,0,0,0) - imageStat.SetComponentOrigin(0,0,0) - imageStat.SetComponentSpacing(1,1,1) - imageStat.Update() - imageStatResult = imageStat.GetOutput() - for i in range(4): - logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}") - self.assertEqual(imageStat.GetVoxelCount(), 127109360) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0,0,0,0), 78967249) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1,0,0,0), 39705288) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2,0,0,0), 890883) - self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3,0,0,0), 7545940) - # Import model to segment - modelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'ModelImport') - modelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName) - modelSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromModelNode(bodyModelNode) - modelSegment.UnRegister(None) # Need to release ownership - self.assertIsNotNone(modelSegment) - self.assertIsNotNone(modelSegment.GetRepresentation(self.closedSurfaceReprName)) - - # Import multi-label labelmap to segmentation - multiLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'MultiLabelImport') - multiLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName) - result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(allSegmentsLabelmapNode, multiLabelImportSegmentationNode) - self.assertTrue(result) - self.assertEqual(multiLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3) - - # Import labelmap into single segment - singleLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'SingleLabelImport') - singleLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName) - # Should not import multi-label labelmap to segment - nullSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromLabelmapVolumeNode(allSegmentsLabelmapNode) - self.assertIsNone(nullSegment) - logging.info('(This error message is a result of testing an impossible scenario, it is supposed to appear)') - # Make labelmap single-label and import again - threshold = vtk.vtkImageThreshold() - threshold.SetInValue(0) - threshold.SetOutValue(1) - threshold.ReplaceInOn() - threshold.ThresholdByLower(0) - threshold.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) - threshold.SetInputData(allSegmentsLabelmapNode.GetImageData()) - threshold.Update() - allSegmentsLabelmapNode.GetImageData().ShallowCopy(threshold.GetOutput()) - labelSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromLabelmapVolumeNode(allSegmentsLabelmapNode) - labelSegment.UnRegister(None) # Need to release ownership - self.assertIsNotNone(labelSegment) - self.assertIsNotNone(labelSegment.GetRepresentation(self.binaryLabelmapReprName)) - - # Import/export with transforms - logging.info('Test subsection: Import/export with transforms') - - # Create transform node that will be used to transform the tested nodes - bodyModelTransformNode = slicer.vtkMRMLLinearTransformNode() - slicer.mrmlScene.AddNode(bodyModelTransformNode) - bodyModelTransform = vtk.vtkTransform() - bodyModelTransform.Translate(1000.0, 0.0, 0.0) - bodyModelTransformNode.ApplyTransformMatrix(bodyModelTransform.GetMatrix()) - - # Set transform as parent to input segmentation node - self.inputSegmentationNode.SetAndObserveTransformNodeID(bodyModelTransformNode.GetID()) - - # Export single segment to model node from transformed segmentation - bodyModelNodeTransformed = slicer.vtkMRMLModelNode() - bodyModelNodeTransformed.SetName('BodyModelTransformed') - slicer.mrmlScene.AddNode(bodyModelNodeTransformed) - bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName) - result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyModelNodeTransformed) - self.assertTrue(result) - self.assertIsNotNone(bodyModelNodeTransformed.GetParentTransformNode()) - - # Export single segment to volume node from transformed segmentation - bodyLabelmapNodeTransformed = slicer.vtkMRMLLabelMapVolumeNode() - bodyLabelmapNodeTransformed.SetName('BodyLabelmapTransformed') - slicer.mrmlScene.AddNode(bodyLabelmapNodeTransformed) - result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNodeTransformed) - self.assertTrue(result) - self.assertIsNotNone(bodyLabelmapNodeTransformed.GetParentTransformNode()) - - # Create transform node that will be used to transform the tested nodes - modelTransformedImportSegmentationTransformNode = slicer.vtkMRMLLinearTransformNode() - slicer.mrmlScene.AddNode(modelTransformedImportSegmentationTransformNode) - modelTransformedImportSegmentationTransform = vtk.vtkTransform() - modelTransformedImportSegmentationTransform.Translate(-500.0, 0.0, 0.0) - modelTransformedImportSegmentationTransformNode.ApplyTransformMatrix(modelTransformedImportSegmentationTransform.GetMatrix()) - - # Import transformed model to segment in transformed segmentation - modelTransformedImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'ModelImportTransformed') - modelTransformedImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName) - modelTransformedImportSegmentationNode.SetAndObserveTransformNodeID(modelTransformedImportSegmentationTransformNode.GetID()) - modelSegmentTranformed = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromModelNode(bodyModelNodeTransformed, modelTransformedImportSegmentationNode) - modelSegmentTranformed.UnRegister(None) # Need to release ownership - self.assertIsNotNone(modelSegmentTranformed) - modelSegmentTransformedPolyData = modelSegmentTranformed.GetRepresentation(self.closedSurfaceReprName) - self.assertIsNotNone(modelSegmentTransformedPolyData) - self.assertEqual(int(modelSegmentTransformedPolyData.GetBounds()[0]), 1332) - self.assertEqual(int(modelSegmentTransformedPolyData.GetBounds()[1]), 1675) - - # Clean up temporary nodes - slicer.mrmlScene.RemoveNode(bodyModelNode) - slicer.mrmlScene.RemoveNode(bodyLabelmapNode) - slicer.mrmlScene.RemoveNode(allSegmentsLabelmapNode) - slicer.mrmlScene.RemoveNode(modelImportSegmentationNode) - slicer.mrmlScene.RemoveNode(multiLabelImportSegmentationNode) - slicer.mrmlScene.RemoveNode(singleLabelImportSegmentationNode) - slicer.mrmlScene.RemoveNode(bodyModelTransformNode) - slicer.mrmlScene.RemoveNode(bodyModelNodeTransformed) - slicer.mrmlScene.RemoveNode(bodyLabelmapNodeTransformed) - slicer.mrmlScene.RemoveNode(modelTransformedImportSegmentationNode) - - def TestSection_ImportExportSegment2(self): - # Testing sequential add of individual segments to a segmentation through ImportLabelmapToSegmentationNode - logging.info('Test section: Import/export segment 2') - - # Export body segment to volume node - bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName) - bodyLabelmapNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode', 'BodyLabelmap') - result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNode) - self.assertTrue(result) - bodyImageData = bodyLabelmapNode.GetImageData() - self.assertIsNotNone(bodyImageData) - imageStat = vtk.vtkImageAccumulate() - imageStat.SetInputData(bodyImageData) - imageStat.Update() - self.assertEqual(imageStat.GetVoxelCount(), 792) - self.assertEqual(imageStat.GetMin()[0], 0) - self.assertEqual(imageStat.GetMax()[0], 1) - - # Export tumor segment to volume node - tumorSegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName) - tumorLabelmapNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode', 'TumorLabelmap') - result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(tumorSegment, tumorLabelmapNode) - self.assertTrue(result) - tumorImageData = tumorLabelmapNode.GetImageData() - self.assertIsNotNone(tumorImageData) - imageStat = vtk.vtkImageAccumulate() - imageStat.SetInputData(tumorImageData) - imageStat.Update() - self.assertEqual(imageStat.GetVoxelCount(), 12) - self.assertEqual(imageStat.GetMin()[0], 0) - self.assertEqual(imageStat.GetMax()[0], 1) - - # Import single-label labelmap to segmentation - singleLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'SingleLabelImport') - singleLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName) - - bodySegmentID = singleLabelImportSegmentationNode.GetSegmentation().AddEmptySegment('BodyLabelmap') - bodySegmentIDArray = vtk.vtkStringArray() - bodySegmentIDArray.SetNumberOfValues(1) - bodySegmentIDArray.SetValue(0, bodySegmentID) - result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(bodyLabelmapNode, singleLabelImportSegmentationNode, bodySegmentIDArray) - - self.assertTrue(result) - self.assertEqual(singleLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 1) - - tumorSegmentID = singleLabelImportSegmentationNode.GetSegmentation().AddEmptySegment('TumorLabelmap') - tumorSegmentIDArray = vtk.vtkStringArray() - tumorSegmentIDArray.SetNumberOfValues(1) - tumorSegmentIDArray.SetValue(0, tumorSegmentID) - result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(tumorLabelmapNode, singleLabelImportSegmentationNode, tumorSegmentIDArray) - self.assertTrue(result) - self.assertEqual(singleLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2) - - bodyLabelmap = slicer.vtkOrientedImageData() - singleLabelImportSegmentationNode.GetBinaryLabelmapRepresentation(bodySegmentID, bodyLabelmap) - imageStat = vtk.vtkImageAccumulate() - imageStat.SetInputData(bodyLabelmap) - imageStat.Update() - self.assertEqual(imageStat.GetVoxelCount(), 792) - self.assertEqual(imageStat.GetMin()[0], 0) - self.assertEqual(imageStat.GetMax()[0], 1) - - tumorLabelmap = slicer.vtkOrientedImageData() - singleLabelImportSegmentationNode.GetBinaryLabelmapRepresentation(tumorSegmentID, tumorLabelmap) - self.assertIsNotNone(tumorLabelmap) - imageStat = vtk.vtkImageAccumulate() - imageStat.SetInputData(tumorLabelmap) - imageStat.Update() - self.assertEqual(imageStat.GetVoxelCount(), 12) - self.assertEqual(imageStat.GetMin()[0], 0) - self.assertEqual(imageStat.GetMax()[0], 1) - - # Clean up temporary nodes - slicer.mrmlScene.RemoveNode(bodyLabelmapNode) - slicer.mrmlScene.RemoveNode(tumorLabelmapNode) - slicer.mrmlScene.RemoveNode(singleLabelImportSegmentationNode) - - #------------------------------------------------------------------------------ - def TestSection_SubjectHierarchy(self): - # Subject hierarchy plugin: item creation, removal, renaming - logging.info('Test section: Subject hierarchy') - - shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - self.assertIsNotNone( shNode ) - - # Check if subject hierarchy items have been created - segmentationShItemID = shNode.GetItemByDataNode(self.inputSegmentationNode) - self.assertIsNotNone( segmentationShItemID ) - - bodyItemID = shNode.GetItemChildWithName(segmentationShItemID, self.bodySegmentName) - self.assertIsNotNone( bodyItemID ) - tumorItemID = shNode.GetItemChildWithName(segmentationShItemID, self.tumorSegmentName) - self.assertIsNotNone( tumorItemID ) - sphereItemID = shNode.GetItemChildWithName(segmentationShItemID, self.sphereSegmentName) - self.assertIsNotNone( sphereItemID ) - - # Rename segment - bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName) - bodySegment.SetName('Body') - qt.QApplication.processEvents() - self.assertEqual( shNode.GetItemName(bodyItemID), 'Body') - - tumorSegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName) - shNode.SetItemName(tumorItemID, 'Tumor') - qt.QApplication.processEvents() - self.assertEqual( tumorSegment.GetName(), 'Tumor') - - # Remove segment - self.inputSegmentationNode.GetSegmentation().RemoveSegment(bodySegment) - qt.QApplication.processEvents() - logging.info('(The error messages below are results of testing invalidity of objects, they are supposed to appear)') - self.assertEqual( shNode.GetItemChildWithName(segmentationShItemID, 'Body'), 0) - self.assertEqual( self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2) - - shNode.RemoveItem(tumorItemID) - qt.QApplication.processEvents() - self.assertEqual( self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 1 ) - - # Remove segmentation - slicer.mrmlScene.RemoveNode(self.inputSegmentationNode) - self.assertEqual( shNode.GetItemName(segmentationShItemID), '') - self.assertEqual( shNode.GetItemName(sphereItemID), '') + 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_SegmentationsModuleTest1() + + # ------------------------------------------------------------------------------ + def test_SegmentationsModuleTest1(self): + # Check for modules + self.assertIsNotNone(slicer.modules.segmentations) + + self.TestSection_SetupPathsAndNames() + self.TestSection_RetrieveInputData() + self.TestSection_LoadInputData() + self.TestSection_AddRemoveSegment() + self.TestSection_MergeLabelmapWithDifferentGeometries() + self.TestSection_ImportExportSegment() + self.TestSection_ImportExportSegment2() + self.TestSection_SubjectHierarchy() + + logging.info('Test finished') + + # ------------------------------------------------------------------------------ + def TestSection_SetupPathsAndNames(self): + # Set up paths used for this test + self.segmentationsModuleTestDir = slicer.app.temporaryPath + '/SegmentationsModuleTest' + if not os.access(self.segmentationsModuleTestDir, os.F_OK): + os.mkdir(self.segmentationsModuleTestDir) + + self.dataDir = self.segmentationsModuleTestDir + '/TinyPatient_Seg' + if not os.access(self.dataDir, os.F_OK): + os.mkdir(self.dataDir) + self.dataSegDir = self.dataDir + '/TinyPatient_Structures.seg' + + self.dataZipFilePath = self.segmentationsModuleTestDir + '/TinyPatient_Seg.zip' + + # Define variables + self.expectedNumOfFilesInDataDir = 4 + self.expectedNumOfFilesInDataSegDir = 2 + self.inputSegmentationNode = None + self.bodySegmentName = 'Body_Contour' + self.tumorSegmentName = 'Tumor_Contour' + self.secondSegmentationNode = None + self.sphereSegment = None + self.sphereSegmentName = 'Sphere' + self.closedSurfaceReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName() + self.binaryLabelmapReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName() + + # ------------------------------------------------------------------------------ + def TestSection_RetrieveInputData(self): + try: + slicer.util.downloadAndExtractArchive( + TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7', + self.dataZipFilePath, self.segmentationsModuleTestDir, + checksum='SHA256:b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7') + + numOfFilesInDataDirTest = len([name for name in os.listdir(self.dataDir) if os.path.isfile(self.dataDir + '/' + name)]) + self.assertEqual(numOfFilesInDataDirTest, self.expectedNumOfFilesInDataDir) + self.assertTrue(os.access(self.dataSegDir, os.F_OK)) + numOfFilesInDataSegDirTest = len([name for name in os.listdir(self.dataSegDir) if os.path.isfile(self.dataSegDir + '/' + name)]) + self.assertEqual(numOfFilesInDataSegDirTest, self.expectedNumOfFilesInDataSegDir) + + except Exception as e: + import traceback + traceback.print_exc() + logging.error('Test caused exception!\n' + str(e)) + + # ------------------------------------------------------------------------------ + def TestSection_LoadInputData(self): + # Load into Slicer + slicer.util.loadVolume(self.dataDir + '/TinyPatient_CT.nrrd') + slicer.util.loadNodeFromFile(self.dataDir + '/TinyPatient_Structures.seg.vtm', "SegmentationFile", {}) + + # Change master representation to closed surface (so that conversion is possible when adding segment) + self.inputSegmentationNode = slicer.util.getNode('vtkMRMLSegmentationNode1') + self.inputSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName) + + # ------------------------------------------------------------------------------ + def TestSection_AddRemoveSegment(self): + # Add/remove segment from segmentation (check display properties, color table, etc.) + logging.info('Test section: Add/remove segment') + + # Get baseline values + displayNode = self.inputSegmentationNode.GetDisplayNode() + self.assertIsNotNone(displayNode) + # If segments are not found then the returned color is the pre-defined invalid color + bodyColor = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName).GetColor() + logging.info(f"bodyColor: {bodyColor}") + self.assertEqual(int(bodyColor[0] * 100), 33) + self.assertEqual(int(bodyColor[1] * 100), 66) + self.assertEqual(int(bodyColor[2] * 100), 0) + tumorColor = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName).GetColor() + logging.info(f"tumorColor: {tumorColor}") + self.assertEqual(int(tumorColor[0] * 100), 100) + self.assertEqual(int(tumorColor[1] * 100), 0) + self.assertEqual(int(tumorColor[2] * 100), 0) + + # Create new segment + sphere = vtk.vtkSphereSource() + sphere.SetCenter(0, 50, 0) + sphere.SetRadius(80) + sphere.Update() + spherePolyData = vtk.vtkPolyData() + spherePolyData.DeepCopy(sphere.GetOutput()) + + self.sphereSegment = vtkSegmentationCore.vtkSegment() + self.sphereSegment.SetName(self.sphereSegmentName) + self.sphereSegment.SetColor(0.0, 0.0, 1.0) + self.sphereSegment.AddRepresentation(self.closedSurfaceReprName, spherePolyData) + + # Add segment to segmentation + self.inputSegmentationNode.GetSegmentation().AddSegment(self.sphereSegment) + self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3) + + # Check merged labelmap + mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData() + self.inputSegmentationNode.GetSegmentation().CreateRepresentation(self.binaryLabelmapReprName) + self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0) + imageStat = vtk.vtkImageAccumulate() + imageStat.SetInputData(mergedLabelmap) + imageStat.SetComponentExtent(0, 4, 0, 0, 0, 0) + imageStat.SetComponentOrigin(0, 0, 0) + imageStat.SetComponentSpacing(1, 1, 1) + imageStat.Update() + imageStatResult = imageStat.GetOutput() + for i in range(4): + logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}") + self.assertEqual(imageStat.GetVoxelCount(), 1000) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 786) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 170) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 4) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 40) + + # Check if segment reorder is taken into account in merged labelmap generation + # Change segment order + sphereSegmentId = self.inputSegmentationNode.GetSegmentation().GetSegmentIdBySegment(self.sphereSegment) + self.inputSegmentationNode.GetSegmentation().SetSegmentIndex(sphereSegmentId, 1) + # Re-generate merged labelmap + self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0) + imageStat.SetInputData(mergedLabelmap) + imageStat.Update() + imageStatResult = imageStat.GetOutput() + for i in range(4): + logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}") + self.assertEqual(imageStat.GetVoxelCount(), 1000) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 786) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 170) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 39) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 5) + + # Remove segment from segmentation + self.inputSegmentationNode.GetSegmentation().RemoveSegment(self.sphereSegmentName) + self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2) + + # ------------------------------------------------------------------------------ + def TestSection_MergeLabelmapWithDifferentGeometries(self): + # Merge labelmap when segments containing labelmaps with different geometries (both same directions, different directions) + logging.info('Test section: Merge labelmap with different geometries') + + self.assertIsNotNone(self.sphereSegment) + self.sphereSegment.RemoveRepresentation(self.binaryLabelmapReprName) + self.assertIsNone(self.sphereSegment.GetRepresentation(self.binaryLabelmapReprName)) + + # Create new segmentation with sphere segment + self.secondSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'Second') + self.secondSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName) + + self.secondSegmentationNode.GetSegmentation().AddSegment(self.sphereSegment) + + # Check automatically converted labelmap. It is supposed to have the default geometry + # (which is different than the one in the input segmentation) + sphereLabelmap = self.sphereSegment.GetRepresentation(self.binaryLabelmapReprName) + self.assertIsNotNone(sphereLabelmap) + sphereLabelmapSpacing = sphereLabelmap.GetSpacing() + self.assertAlmostEqual(sphereLabelmapSpacing[0], 0.629257364931788, 8) + self.assertAlmostEqual(sphereLabelmapSpacing[1], 0.629257364931788, 8) + self.assertAlmostEqual(sphereLabelmapSpacing[2], 0.629257364931788, 8) + + # Create binary labelmap in segmentation that will create the merged labelmap from + # different geometries so that labelmap is not removed from sphere segment when adding + self.inputSegmentationNode.GetSegmentation().CreateRepresentation(self.binaryLabelmapReprName) + + # Copy segment to input segmentation + self.inputSegmentationNode.GetSegmentation().CopySegmentFromSegmentation(self.secondSegmentationNode.GetSegmentation(), self.sphereSegmentName) + self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3) + + # Check merged labelmap + # Reference geometry has the tiny patient spacing, and it is oversampled to have similar + # voxel size as the sphere labelmap with the uniform 0.629mm spacing + mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData() + self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0) + mergedLabelmapSpacing = mergedLabelmap.GetSpacing() + self.assertAlmostEqual(mergedLabelmapSpacing[0], 0.80327868852459, 8) + self.assertAlmostEqual(mergedLabelmapSpacing[1], 0.80327868852459, 8) + self.assertAlmostEqual(mergedLabelmapSpacing[2], 0.377049180327869, 8) + + imageStat = vtk.vtkImageAccumulate() + imageStat.SetInputData(mergedLabelmap) + imageStat.SetComponentExtent(0, 5, 0, 0, 0, 0) + imageStat.SetComponentOrigin(0, 0, 0) + imageStat.SetComponentSpacing(1, 1, 1) + imageStat.Update() + imageStatResult = imageStat.GetOutput() + for i in range(5): + logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}") + self.assertEqual(imageStat.GetVoxelCount(), 226981000) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 178838889) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 39705288) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 890883) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 7545940) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(4, 0, 0, 0), 0) # Built from color table and color four is removed in previous test section + + # ------------------------------------------------------------------------------ + def TestSection_ImportExportSegment(self): + # Import/export, both one label and all labels + logging.info('Test section: Import/export segment') + + # Export single segment to model node + bodyModelNode = slicer.vtkMRMLModelNode() + bodyModelNode.SetName('BodyModel') + slicer.mrmlScene.AddNode(bodyModelNode) + + bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName) + result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyModelNode) + self.assertTrue(result) + self.assertIsNotNone(bodyModelNode.GetPolyData()) + # TODO: Number of points increased to 1677 due to end-capping, need to investigate! + # self.assertEqual(bodyModelNode.GetPolyData().GetNumberOfPoints(), 302) + # TODO: On Linux and Windows it is 588, on Mac it is 580. Need to investigate + # self.assertEqual(bodyModelNode.GetPolyData().GetNumberOfCells(), 588) + # self.assertTrue(bodyModelNode.GetPolyData().GetNumberOfCells() == 588 or bodyModelNode.GetPolyData().GetNumberOfCells() == 580) + + # Export single segment to volume node + bodyLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() + bodyLabelmapNode.SetName('BodyLabelmap') + slicer.mrmlScene.AddNode(bodyLabelmapNode) + result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNode) + self.assertTrue(result) + bodyImageData = bodyLabelmapNode.GetImageData() + self.assertIsNotNone(bodyImageData) + imageStat = vtk.vtkImageAccumulate() + imageStat.SetInputData(bodyImageData) + imageStat.Update() + self.assertEqual(imageStat.GetVoxelCount(), 792) + self.assertEqual(imageStat.GetMin()[0], 0) + self.assertEqual(imageStat.GetMax()[0], 1) + + # Export multiple segments to volume node + allSegmentsLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() + allSegmentsLabelmapNode.SetName('AllSegmentsLabelmap') + slicer.mrmlScene.AddNode(allSegmentsLabelmapNode) + result = slicer.vtkSlicerSegmentationsModuleLogic.ExportAllSegmentsToLabelmapNode(self.inputSegmentationNode, allSegmentsLabelmapNode) + self.assertTrue(result) + allSegmentsImageData = allSegmentsLabelmapNode.GetImageData() + self.assertIsNotNone(allSegmentsImageData) + imageStat = vtk.vtkImageAccumulate() + imageStat.SetInputData(allSegmentsImageData) + imageStat.SetComponentExtent(0, 5, 0, 0, 0, 0) + imageStat.SetComponentOrigin(0, 0, 0) + imageStat.SetComponentSpacing(1, 1, 1) + imageStat.Update() + imageStatResult = imageStat.GetOutput() + for i in range(4): + logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}") + self.assertEqual(imageStat.GetVoxelCount(), 127109360) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 78967249) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 39705288) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 890883) + self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 7545940) + # Import model to segment + modelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'ModelImport') + modelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName) + modelSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromModelNode(bodyModelNode) + modelSegment.UnRegister(None) # Need to release ownership + self.assertIsNotNone(modelSegment) + self.assertIsNotNone(modelSegment.GetRepresentation(self.closedSurfaceReprName)) + + # Import multi-label labelmap to segmentation + multiLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'MultiLabelImport') + multiLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName) + result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(allSegmentsLabelmapNode, multiLabelImportSegmentationNode) + self.assertTrue(result) + self.assertEqual(multiLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3) + + # Import labelmap into single segment + singleLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'SingleLabelImport') + singleLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName) + # Should not import multi-label labelmap to segment + nullSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromLabelmapVolumeNode(allSegmentsLabelmapNode) + self.assertIsNone(nullSegment) + logging.info('(This error message is a result of testing an impossible scenario, it is supposed to appear)') + # Make labelmap single-label and import again + threshold = vtk.vtkImageThreshold() + threshold.SetInValue(0) + threshold.SetOutValue(1) + threshold.ReplaceInOn() + threshold.ThresholdByLower(0) + threshold.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) + threshold.SetInputData(allSegmentsLabelmapNode.GetImageData()) + threshold.Update() + allSegmentsLabelmapNode.GetImageData().ShallowCopy(threshold.GetOutput()) + labelSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromLabelmapVolumeNode(allSegmentsLabelmapNode) + labelSegment.UnRegister(None) # Need to release ownership + self.assertIsNotNone(labelSegment) + self.assertIsNotNone(labelSegment.GetRepresentation(self.binaryLabelmapReprName)) + + # Import/export with transforms + logging.info('Test subsection: Import/export with transforms') + + # Create transform node that will be used to transform the tested nodes + bodyModelTransformNode = slicer.vtkMRMLLinearTransformNode() + slicer.mrmlScene.AddNode(bodyModelTransformNode) + bodyModelTransform = vtk.vtkTransform() + bodyModelTransform.Translate(1000.0, 0.0, 0.0) + bodyModelTransformNode.ApplyTransformMatrix(bodyModelTransform.GetMatrix()) + + # Set transform as parent to input segmentation node + self.inputSegmentationNode.SetAndObserveTransformNodeID(bodyModelTransformNode.GetID()) + + # Export single segment to model node from transformed segmentation + bodyModelNodeTransformed = slicer.vtkMRMLModelNode() + bodyModelNodeTransformed.SetName('BodyModelTransformed') + slicer.mrmlScene.AddNode(bodyModelNodeTransformed) + bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName) + result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyModelNodeTransformed) + self.assertTrue(result) + self.assertIsNotNone(bodyModelNodeTransformed.GetParentTransformNode()) + + # Export single segment to volume node from transformed segmentation + bodyLabelmapNodeTransformed = slicer.vtkMRMLLabelMapVolumeNode() + bodyLabelmapNodeTransformed.SetName('BodyLabelmapTransformed') + slicer.mrmlScene.AddNode(bodyLabelmapNodeTransformed) + result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNodeTransformed) + self.assertTrue(result) + self.assertIsNotNone(bodyLabelmapNodeTransformed.GetParentTransformNode()) + + # Create transform node that will be used to transform the tested nodes + modelTransformedImportSegmentationTransformNode = slicer.vtkMRMLLinearTransformNode() + slicer.mrmlScene.AddNode(modelTransformedImportSegmentationTransformNode) + modelTransformedImportSegmentationTransform = vtk.vtkTransform() + modelTransformedImportSegmentationTransform.Translate(-500.0, 0.0, 0.0) + modelTransformedImportSegmentationTransformNode.ApplyTransformMatrix(modelTransformedImportSegmentationTransform.GetMatrix()) + + # Import transformed model to segment in transformed segmentation + modelTransformedImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'ModelImportTransformed') + modelTransformedImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName) + modelTransformedImportSegmentationNode.SetAndObserveTransformNodeID(modelTransformedImportSegmentationTransformNode.GetID()) + modelSegmentTranformed = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromModelNode(bodyModelNodeTransformed, modelTransformedImportSegmentationNode) + modelSegmentTranformed.UnRegister(None) # Need to release ownership + self.assertIsNotNone(modelSegmentTranformed) + modelSegmentTransformedPolyData = modelSegmentTranformed.GetRepresentation(self.closedSurfaceReprName) + self.assertIsNotNone(modelSegmentTransformedPolyData) + self.assertEqual(int(modelSegmentTransformedPolyData.GetBounds()[0]), 1332) + self.assertEqual(int(modelSegmentTransformedPolyData.GetBounds()[1]), 1675) + + # Clean up temporary nodes + slicer.mrmlScene.RemoveNode(bodyModelNode) + slicer.mrmlScene.RemoveNode(bodyLabelmapNode) + slicer.mrmlScene.RemoveNode(allSegmentsLabelmapNode) + slicer.mrmlScene.RemoveNode(modelImportSegmentationNode) + slicer.mrmlScene.RemoveNode(multiLabelImportSegmentationNode) + slicer.mrmlScene.RemoveNode(singleLabelImportSegmentationNode) + slicer.mrmlScene.RemoveNode(bodyModelTransformNode) + slicer.mrmlScene.RemoveNode(bodyModelNodeTransformed) + slicer.mrmlScene.RemoveNode(bodyLabelmapNodeTransformed) + slicer.mrmlScene.RemoveNode(modelTransformedImportSegmentationNode) + + def TestSection_ImportExportSegment2(self): + # Testing sequential add of individual segments to a segmentation through ImportLabelmapToSegmentationNode + logging.info('Test section: Import/export segment 2') + + # Export body segment to volume node + bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName) + bodyLabelmapNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode', 'BodyLabelmap') + result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNode) + self.assertTrue(result) + bodyImageData = bodyLabelmapNode.GetImageData() + self.assertIsNotNone(bodyImageData) + imageStat = vtk.vtkImageAccumulate() + imageStat.SetInputData(bodyImageData) + imageStat.Update() + self.assertEqual(imageStat.GetVoxelCount(), 792) + self.assertEqual(imageStat.GetMin()[0], 0) + self.assertEqual(imageStat.GetMax()[0], 1) + + # Export tumor segment to volume node + tumorSegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName) + tumorLabelmapNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode', 'TumorLabelmap') + result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(tumorSegment, tumorLabelmapNode) + self.assertTrue(result) + tumorImageData = tumorLabelmapNode.GetImageData() + self.assertIsNotNone(tumorImageData) + imageStat = vtk.vtkImageAccumulate() + imageStat.SetInputData(tumorImageData) + imageStat.Update() + self.assertEqual(imageStat.GetVoxelCount(), 12) + self.assertEqual(imageStat.GetMin()[0], 0) + self.assertEqual(imageStat.GetMax()[0], 1) + + # Import single-label labelmap to segmentation + singleLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'SingleLabelImport') + singleLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName) + + bodySegmentID = singleLabelImportSegmentationNode.GetSegmentation().AddEmptySegment('BodyLabelmap') + bodySegmentIDArray = vtk.vtkStringArray() + bodySegmentIDArray.SetNumberOfValues(1) + bodySegmentIDArray.SetValue(0, bodySegmentID) + result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(bodyLabelmapNode, singleLabelImportSegmentationNode, bodySegmentIDArray) + + self.assertTrue(result) + self.assertEqual(singleLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 1) + + tumorSegmentID = singleLabelImportSegmentationNode.GetSegmentation().AddEmptySegment('TumorLabelmap') + tumorSegmentIDArray = vtk.vtkStringArray() + tumorSegmentIDArray.SetNumberOfValues(1) + tumorSegmentIDArray.SetValue(0, tumorSegmentID) + result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(tumorLabelmapNode, singleLabelImportSegmentationNode, tumorSegmentIDArray) + self.assertTrue(result) + self.assertEqual(singleLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2) + + bodyLabelmap = slicer.vtkOrientedImageData() + singleLabelImportSegmentationNode.GetBinaryLabelmapRepresentation(bodySegmentID, bodyLabelmap) + imageStat = vtk.vtkImageAccumulate() + imageStat.SetInputData(bodyLabelmap) + imageStat.Update() + self.assertEqual(imageStat.GetVoxelCount(), 792) + self.assertEqual(imageStat.GetMin()[0], 0) + self.assertEqual(imageStat.GetMax()[0], 1) + + tumorLabelmap = slicer.vtkOrientedImageData() + singleLabelImportSegmentationNode.GetBinaryLabelmapRepresentation(tumorSegmentID, tumorLabelmap) + self.assertIsNotNone(tumorLabelmap) + imageStat = vtk.vtkImageAccumulate() + imageStat.SetInputData(tumorLabelmap) + imageStat.Update() + self.assertEqual(imageStat.GetVoxelCount(), 12) + self.assertEqual(imageStat.GetMin()[0], 0) + self.assertEqual(imageStat.GetMax()[0], 1) + + # Clean up temporary nodes + slicer.mrmlScene.RemoveNode(bodyLabelmapNode) + slicer.mrmlScene.RemoveNode(tumorLabelmapNode) + slicer.mrmlScene.RemoveNode(singleLabelImportSegmentationNode) + + # ------------------------------------------------------------------------------ + def TestSection_SubjectHierarchy(self): + # Subject hierarchy plugin: item creation, removal, renaming + logging.info('Test section: Subject hierarchy') + + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + self.assertIsNotNone(shNode) + + # Check if subject hierarchy items have been created + segmentationShItemID = shNode.GetItemByDataNode(self.inputSegmentationNode) + self.assertIsNotNone(segmentationShItemID) + + bodyItemID = shNode.GetItemChildWithName(segmentationShItemID, self.bodySegmentName) + self.assertIsNotNone(bodyItemID) + tumorItemID = shNode.GetItemChildWithName(segmentationShItemID, self.tumorSegmentName) + self.assertIsNotNone(tumorItemID) + sphereItemID = shNode.GetItemChildWithName(segmentationShItemID, self.sphereSegmentName) + self.assertIsNotNone(sphereItemID) + + # Rename segment + bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName) + bodySegment.SetName('Body') + qt.QApplication.processEvents() + self.assertEqual(shNode.GetItemName(bodyItemID), 'Body') + + tumorSegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName) + shNode.SetItemName(tumorItemID, 'Tumor') + qt.QApplication.processEvents() + self.assertEqual(tumorSegment.GetName(), 'Tumor') + + # Remove segment + self.inputSegmentationNode.GetSegmentation().RemoveSegment(bodySegment) + qt.QApplication.processEvents() + logging.info('(The error messages below are results of testing invalidity of objects, they are supposed to appear)') + self.assertEqual(shNode.GetItemChildWithName(segmentationShItemID, 'Body'), 0) + self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2) + + shNode.RemoveItem(tumorItemID) + qt.QApplication.processEvents() + self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 1) + + # Remove segmentation + slicer.mrmlScene.RemoveNode(self.inputSegmentationNode) + self.assertEqual(shNode.GetItemName(segmentationShItemID), '') + self.assertEqual(shNode.GetItemName(sphereItemID), '') diff --git a/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest2.py b/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest2.py index ff919dc8bb8..1ca22dbd13e 100644 --- a/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest2.py +++ b/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest2.py @@ -18,501 +18,501 @@ class SegmentationsModuleTest2(unittest.TestCase): - #------------------------------------------------------------------------------ - 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_SegmentationsModuleTest2() - - #------------------------------------------------------------------------------ - def test_SegmentationsModuleTest2(self): - # Check for modules - self.assertIsNotNone( slicer.modules.segmentations ) - self.assertIsNotNone( slicer.modules.segmenteditor ) - - # Run tests - self.TestSection_SetupPathsAndNames() - self.TestSection_RetrieveInputData() - self.TestSection_SetupScene() - self.TestSection_SharedLabelmapMultipleLayerEditing() - self.TestSection_IslandEffects() - self.TestSection_MarginEffects() - self.TestSection_MaskingSettings() - logging.info('Test finished') - - #------------------------------------------------------------------------------ - def TestSection_SetupPathsAndNames(self): - # Set up paths used for this test - self.segmentationsModuleTestDir = slicer.app.temporaryPath + '/SegmentationsModuleTest' - if not os.access(self.segmentationsModuleTestDir, os.F_OK): - os.mkdir(self.segmentationsModuleTestDir) - - self.dataDir = self.segmentationsModuleTestDir + '/TinyPatient_Seg' - if not os.access(self.dataDir, os.F_OK): - os.mkdir(self.dataDir) - self.dataSegDir = self.dataDir + '/TinyPatient_Structures.seg' - - self.dataZipFilePath = self.segmentationsModuleTestDir + '/TinyPatient_Seg.zip' - - # Define variables - self.expectedNumOfFilesInDataDir = 4 - self.expectedNumOfFilesInDataSegDir = 2 - self.closedSurfaceReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName() - self.binaryLabelmapReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName() - - #------------------------------------------------------------------------------ - def TestSection_RetrieveInputData(self): - try: - slicer.util.downloadAndExtractArchive( - TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7', - self.dataZipFilePath, self.segmentationsModuleTestDir, - checksum='SHA256:b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7') - - numOfFilesInDataDirTest = len([name for name in os.listdir(self.dataDir) if os.path.isfile(self.dataDir + '/' + name)]) - self.assertEqual( numOfFilesInDataDirTest, self.expectedNumOfFilesInDataDir ) - self.assertTrue( os.access(self.dataSegDir, os.F_OK) ) - numOfFilesInDataSegDirTest = len([name for name in os.listdir(self.dataSegDir) if os.path.isfile(self.dataSegDir + '/' + name)]) - self.assertEqual( numOfFilesInDataSegDirTest, self.expectedNumOfFilesInDataSegDir ) - - except Exception as e: - import traceback - traceback.print_exc() - logging.error('Test caused exception!\n' + str(e)) - - #------------------------------------------------------------------------------ - def TestSection_SetupScene(self): - self.paintEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Paint") - self.eraseEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Erase") - self.islandEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Islands") - self.thresholdEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Threshold") - - self.segmentEditorNode = slicer.util.getNode("SegmentEditor") - self.assertIsNotNone(self.segmentEditorNode) - - self.segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") - self.assertIsNotNone(self.segmentationNode) - self.segmentEditorNode.SetAndObserveSegmentationNode(self.segmentationNode) - - self.masterVolumeNode = slicer.util.loadVolume(self.dataDir + '/TinyPatient_CT.nrrd') - self.assertIsNotNone(self.masterVolumeNode) - self.segmentEditorNode.SetAndObserveMasterVolumeNode(self.masterVolumeNode) - - self.segmentation = self.segmentationNode.GetSegmentation() - self.segmentation.SetMasterRepresentationName(self.binaryLabelmapReprName) - self.assertIsNotNone(self.segmentation) - - #------------------------------------------------------------------------------ - def TestSection_SharedLabelmapMultipleLayerEditing(self): - self.segmentation.RemoveAllSegments() - self.segmentation.AddEmptySegment("Segment_1") - self.segmentation.AddEmptySegment("Segment_2") - - defaultModifierLabelmap = self.paintEffect.defaultModifierLabelmap() - self.ijkToRas = vtk.vtkMatrix4x4() - defaultModifierLabelmap.GetImageToWorldMatrix(self.ijkToRas) - - mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData() - mergedLabelmap.SetImageToWorldMatrix(self.ijkToRas) - mergedLabelmap.SetExtent(0, 10, 0, 10, 0, 10) - mergedLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) - mergedLabelmap.GetPointData().GetScalars().Fill(1) - - oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode() - - self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteAllSegments) - self.segmentEditorNode.SetSelectedSegmentID("Segment_1") - self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd) - self.segmentEditorNode.SetSelectedSegmentID("Segment_2") - self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd) - - layerCount = self.segmentation.GetNumberOfLayers() - self.assertEqual(layerCount, 1) - - self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteNone) - self.segmentEditorNode.SetSelectedSegmentID("Segment_1") - self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd) - layerCount = self.segmentation.GetNumberOfLayers() - self.assertEqual(layerCount, 2) - - self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode) - logging.info('Multiple layer editing successful') - - #------------------------------------------------------------------------------ - def TestSection_IslandEffects(self): - islandSizes = [1, 26, 11, 6, 8, 6, 2] - islandSizes.sort(reverse=True) - - minimumSize = 3 - self.resetIslandSegments(islandSizes) - self.islandEffect.setParameter('MinimumSize', minimumSize) - self.islandEffect.setParameter('Operation','KEEP_LARGEST_ISLAND') - self.islandEffect.self().onApply() - layerCount = self.segmentation.GetNumberOfLayers() - self.assertEqual(layerCount, 1) - - voxelCount = 0 - for size in islandSizes: - if size < minimumSize: - continue - voxelCount = max(voxelCount, size) - self.checkSegmentVoxelCount(0, voxelCount) - - minimumSize = 7 - self.resetIslandSegments(islandSizes) - self.islandEffect.setParameter('MinimumSize', minimumSize) - self.islandEffect.setParameter('Operation','REMOVE_SMALL_ISLANDS') - self.islandEffect.self().onApply() - layerCount = self.segmentation.GetNumberOfLayers() - self.assertEqual(layerCount, 1) - - voxelCount = 0 - for size in islandSizes: - if size < minimumSize: - continue - voxelCount += size - self.checkSegmentVoxelCount(0, voxelCount) - - self.resetIslandSegments(islandSizes) - minimumSize = 3 - self.islandEffect.setParameter('MinimumSize', minimumSize) - self.islandEffect.setParameter('Operation','SPLIT_ISLANDS_TO_SEGMENTS') - self.islandEffect.self().onApply() - layerCount = self.segmentation.GetNumberOfLayers() - self.assertEqual(layerCount, 1) - - for i in range(len(islandSizes)): - size = islandSizes[i] - if size < minimumSize: - continue - self.checkSegmentVoxelCount(i, size) - - #------------------------------------------------------------------------------ - def resetIslandSegments(self, islandSizes): - self.segmentation.RemoveAllSegments() - - totalSize = 0 - voxelSizeSum = 0 - for size in islandSizes: - totalSize += size + 1 - voxelSizeSum += size - - mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData() - mergedLabelmap.SetImageToWorldMatrix(self.ijkToRas) - mergedLabelmapExtent = [0, totalSize-1, 0, 0, 0, 0] - self.setupIslandLabelmap(mergedLabelmap, mergedLabelmapExtent, 0) - - emptySegment = slicer.vtkSegment() - emptySegment.SetName("Segment_1") - emptySegment.AddRepresentation(self.binaryLabelmapReprName, mergedLabelmap) - self.segmentation.AddSegment(emptySegment) - self.segmentEditorNode.SetSelectedSegmentID("Segment_1") - - startExtent = 0 - for size in islandSizes: - islandLabelmap = vtkSegmentationCore.vtkOrientedImageData() - islandLabelmap.SetImageToWorldMatrix(self.ijkToRas) - islandExtent = [startExtent, startExtent+size-1, 0, 0, 0, 0] - self.setupIslandLabelmap(islandLabelmap, islandExtent) - self.paintEffect.modifySelectedSegmentByLabelmap(islandLabelmap, self.paintEffect.ModificationModeAdd) - startExtent += size + 1 - self.checkSegmentVoxelCount(0, voxelSizeSum) - - layerCount = self.segmentation.GetNumberOfLayers() - self.assertEqual(layerCount, 1) - - #------------------------------------------------------------------------------ - def checkSegmentVoxelCount(self, segmentIndex, expectedVoxelCount): - segment = self.segmentation.GetNthSegment(segmentIndex) - self.assertIsNotNone(segment) - - labelmap = slicer.vtkOrientedImageData() - labelmap.SetImageToWorldMatrix(self.ijkToRas) - segmentID = self.segmentation.GetNthSegmentID(segmentIndex) - self.segmentationNode.GetBinaryLabelmapRepresentation(segmentID, labelmap) - - imageStat = vtk.vtkImageAccumulate() - imageStat.SetInputData(labelmap) - imageStat.SetComponentExtent(0,4,0,0,0,0) - imageStat.SetComponentOrigin(0,0,0) - imageStat.SetComponentSpacing(1,1,1) - imageStat.IgnoreZeroOn() - imageStat.Update() - - self.assertEqual(imageStat.GetVoxelCount(), expectedVoxelCount) - - #------------------------------------------------------------------------------ - def setupIslandLabelmap(self, labelmap, extent, value=1): - labelmap.SetExtent(extent) - labelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) - labelmap.GetPointData().GetScalars().Fill(value) - - #------------------------------------------------------------------------------ - def TestSection_MarginEffects(self): - logging.info("Running test on margin effect") - - slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Margin") - - self.segmentation.RemoveAllSegments() - segment1Id = self.segmentation.AddEmptySegment("Segment_1") - segment1 = self.segmentation.GetSegment(segment1Id) - segment1.SetLabelValue(1) - self.segmentEditorNode.SetSelectedSegmentID("Segment_1") - - segment2Id = self.segmentation.AddEmptySegment("Segment_2") - segment2 = self.segmentation.GetSegment(segment2Id) - segment2.SetLabelValue(2) - - binaryLabelmapRepresentationName = slicer.vtkSegmentationConverter.GetBinaryLabelmapRepresentationName() - dataTypes = [ - vtk.VTK_CHAR, - vtk.VTK_SIGNED_CHAR, - vtk.VTK_UNSIGNED_CHAR, - vtk.VTK_SHORT, - vtk.VTK_UNSIGNED_SHORT, - vtk.VTK_INT, - vtk.VTK_UNSIGNED_INT, - #vtk.VTK_LONG, # On linux, VTK_LONG has the same size as VTK_LONG_LONG. This causes issues in vtkImageThreshold. - #vtk.VTK_UNSIGNED_LONG, See https://github.com/Slicer/Slicer/issues/5427 - #vtk.VTK_FLOAT, # Since float can't represent all int, we jump straight to double. - vtk.VTK_DOUBLE, - #vtk.VTK_LONG_LONG, # These types are unsupported in ITK - #vtk.VTK_UNSIGNED_LONG_LONG, - ] - logging.info("Testing shared labelmaps") - for dataType in dataTypes: - initialLabelmap = slicer.vtkOrientedImageData() - initialLabelmap.SetImageToWorldMatrix(self.ijkToRas) - initialLabelmap.SetExtent(0, 10, 0, 10, 0, 10) - initialLabelmap.AllocateScalars(dataType, 1) - initialLabelmap.GetPointData().GetScalars().Fill(0) - segment1.AddRepresentation(binaryLabelmapRepresentationName, initialLabelmap) - segment2.AddRepresentation(binaryLabelmapRepresentationName, initialLabelmap) - - self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteAllSegments) - self.assertEqual(self.segmentation.GetNumberOfLayers(), 1) - self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteNone) - self.assertEqual(self.segmentation.GetNumberOfLayers(), 2) - - logging.info("Testing separate labelmaps") - for dataType in dataTypes: - segment1Labelmap = slicer.vtkOrientedImageData() - segment1Labelmap.SetImageToWorldMatrix(self.ijkToRas) - segment1Labelmap.SetExtent(0, 10, 0, 10, 0, 10) - segment1Labelmap.AllocateScalars(dataType, 1) - segment1Labelmap.GetPointData().GetScalars().Fill(0) - segment1.AddRepresentation(binaryLabelmapRepresentationName, segment1Labelmap) - - segment2Labelmap = slicer.vtkOrientedImageData() - segment2Labelmap.DeepCopy(segment1Labelmap) - segment2.AddRepresentation(binaryLabelmapRepresentationName, segment2Labelmap) - - self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteAllSegments) - self.assertEqual(self.segmentation.GetNumberOfLayers(), 2) - self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteNone) - self.assertEqual(self.segmentation.GetNumberOfLayers(), 2) - - #------------------------------------------------------------------------------ - def runMarginEffect(self, segment1, segment2, dataType, overwriteMode): - logging.info(f"Running margin effect with data type: {dataType}, and overwriteMode {overwriteMode}") - marginEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Margin") - - marginEffect.setParameter("MarginSizeMm", 50.0) - - oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode() - self.segmentEditorNode.SetOverwriteMode(overwriteMode) - - segment1Labelmap = segment1.GetRepresentation(self.binaryLabelmapReprName) - segment1Labelmap.AllocateScalars(dataType, 1) - segment1Labelmap.GetPointData().GetScalars().Fill(0) - - segment2Labelmap = segment2.GetRepresentation(self.binaryLabelmapReprName) - segment2Labelmap.AllocateScalars(dataType, 1) - segment2Labelmap.GetPointData().GetScalars().Fill(0) - - segment1Position_IJK = [5, 5, 5] - segment1Labelmap.SetScalarComponentFromDouble(segment1Position_IJK[0], segment1Position_IJK[1], segment1Position_IJK[2], 0, segment1.GetLabelValue()) - segment2Position_IJK = [6, 5, 6] - segment2Labelmap.SetScalarComponentFromDouble(segment2Position_IJK[0], segment2Position_IJK[1], segment2Position_IJK[2], 0, segment2.GetLabelValue()) - - self.checkSegmentVoxelCount(0, 1) - self.checkSegmentVoxelCount(1, 1) - - marginEffect.self().onApply() - self.checkSegmentVoxelCount(0, 9) # Margin grow - self.checkSegmentVoxelCount(1, 1) - - marginEffect.self().onApply() - self.checkSegmentVoxelCount(0, 37) # Margin grow - if overwriteMode == slicer.vtkMRMLSegmentEditorNode.OverwriteAllSegments: - self.checkSegmentVoxelCount(1, 0) # Overwritten - else: - self.checkSegmentVoxelCount(1, 1) # Not overwritten - - marginEffect.setParameter("MarginSizeMm", -50.0) - marginEffect.self().onApply() - - self.checkSegmentVoxelCount(0, 9) # Margin shrink - if overwriteMode == slicer.vtkMRMLSegmentEditorNode.OverwriteAllSegments: - self.checkSegmentVoxelCount(1, 0) # Overwritten - else: - self.checkSegmentVoxelCount(1, 1) # Not overwritten - - self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode) - - #------------------------------------------------------------------------------ - def TestSection_MaskingSettings(self): - self.segmentation.RemoveAllSegments() - segment1Id = self.segmentation.AddEmptySegment("Segment_1") - segment2Id = self.segmentation.AddEmptySegment("Segment_2") - segment3Id = self.segmentation.AddEmptySegment("Segment_3") - segment4Id = self.segmentation.AddEmptySegment("Segment_4") - - oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode() - - #------------------- - # Test applying threshold with no masking - self.segmentEditorNode.SetSelectedSegmentID(segment1Id) - self.thresholdEffect.setParameter("MinimumThreshold","-17") - self.thresholdEffect.setParameter("MaximumThreshold","848") - self.thresholdEffect.self().onApply() - self.checkSegmentVoxelCount(0, 204) # Segment_1 - self.checkSegmentVoxelCount(1, 0) # Segment_2 - - #------------------- - # Add paint to segment 2. No overwrite - paintModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData() - paintModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas) - paintModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5) - paintModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) - paintModifierLabelmap.GetPointData().GetScalars().Fill(1) - - self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteNone) - self.segmentEditorNode.SetSelectedSegmentID(segment2Id) - self.paintEffect.modifySelectedSegmentByLabelmap(paintModifierLabelmap, self.paintEffect.ModificationModeAdd) - - self.checkSegmentVoxelCount(0, 204) # Segment_1 - self.checkSegmentVoxelCount(1, 64) # Segment_2 - - #------------------- - # Test erasing with no masking - eraseModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData() - eraseModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas) - eraseModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5) - eraseModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) - eraseModifierLabelmap.GetPointData().GetScalars().Fill(1) - - self.segmentEditorNode.SetSelectedSegmentID(segment1Id) - self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) - self.checkSegmentVoxelCount(0, 177) # Segment_1 - self.checkSegmentVoxelCount(1, 64) # Segment_2 - - #------------------- - # Test erasing with masking on empty segment - self.segmentEditorNode.SetSelectedSegmentID(segment1Id) - self.thresholdEffect.self().onApply() # Reset Segment_1 - self.checkSegmentVoxelCount(0, 204) # Segment_1 - self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedInsideSingleSegment) - self.segmentEditorNode.SetMaskSegmentID(segment2Id) - self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) - self.checkSegmentVoxelCount(0, 177) # We expect to be able to erase the current segment regardless of masking - self.checkSegmentVoxelCount(1, 64) # Segment_2 - - #------------------- - # Test erasing with masking on the same segment - self.segmentEditorNode.SetSelectedSegmentID(segment1Id) - self.thresholdEffect.self().onApply() # Reset Segment_1 - self.checkSegmentVoxelCount(0, 204) # Segment_1 - self.segmentEditorNode.SetMaskSegmentID(segment1Id) - self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) - self.checkSegmentVoxelCount(0, 177) # Segment_1 - self.checkSegmentVoxelCount(1, 64) # Segment_2 - - #------------------- - # Test erasing all segments - self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedEverywhere) - self.thresholdEffect.self().onApply() # Reset Segment_1 - self.checkSegmentVoxelCount(0, 204) # Segment_1 - self.segmentEditorNode.SetSelectedSegmentID(segment1Id) - self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemoveAll) - self.checkSegmentVoxelCount(0, 177) # Segment_1 - self.checkSegmentVoxelCount(1, 0) # Segment_2 - - #------------------- - # Test adding back segments - self.thresholdEffect.self().onApply() # Reset Segment_1 - self.checkSegmentVoxelCount(0, 204) # Segment_1 - self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedInsideSingleSegment) - self.segmentEditorNode.SetMaskSegmentID(segment2Id) - self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) - self.checkSegmentVoxelCount(0, 177) # Segment_1 - self.checkSegmentVoxelCount(1, 27) # Segment_2 - - #------------------- - # Test threshold effect segment mask - self.segmentEditorNode.SetMaskSegmentID(segment2Id) # Erase Segment_2 - self.segmentEditorNode.SetSelectedSegmentID(segment2Id) - self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) - self.segmentEditorNode.SetMaskSegmentID(segment1Id) - self.segmentEditorNode.SetSelectedSegmentID(segment2Id) - self.thresholdEffect.self().onApply() # Threshold Segment_2 within Segment_1 - self.checkSegmentVoxelCount(0, 177) # Segment_1 - self.checkSegmentVoxelCount(1, 177) # Segment_2 - - #------------------- - # Test intensity masking with segment mask - self.segmentEditorNode.MasterVolumeIntensityMaskOn() - self.segmentEditorNode.SetMasterVolumeIntensityMaskRange(-17, 848) - self.thresholdEffect.setParameter("MinimumThreshold","-99999") - self.thresholdEffect.setParameter("MaximumThreshold","99999") - self.segmentEditorNode.SetSelectedSegmentID(segment3Id) - self.thresholdEffect.self().onApply() # Threshold Segment_3 - self.checkSegmentVoxelCount(2, 177) # Segment_3 - - #------------------- - # Test intensity masking with islands - self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedEverywhere) - self.segmentEditorNode.MasterVolumeIntensityMaskOff() - self.segmentEditorNode.SetSelectedSegmentID(segment4Id) - - island1ModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData() - island1ModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas) - island1ModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5) - island1ModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) - island1ModifierLabelmap.GetPointData().GetScalars().Fill(1) - self.paintEffect.modifySelectedSegmentByLabelmap(island1ModifierLabelmap, self.paintEffect.ModificationModeAdd) - - island2ModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData() - island2ModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas) - island2ModifierLabelmap.SetExtent(7, 9, 7, 9, 7, 9) - island2ModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) - island2ModifierLabelmap.GetPointData().GetScalars().Fill(1) - self.paintEffect.modifySelectedSegmentByLabelmap(island2ModifierLabelmap, self.paintEffect.ModificationModeAdd) - self.checkSegmentVoxelCount(3, 91) # Segment_4 - - # Test that no masking works as expected - minimumSize = 3 - self.islandEffect.setParameter('MinimumSize', minimumSize) - self.islandEffect.setParameter('Operation','KEEP_LARGEST_ISLAND') - self.islandEffect.self().onApply() - self.checkSegmentVoxelCount(3, 64) # Segment_4 - - # Reset Segment_4 islands - self.paintEffect.modifySelectedSegmentByLabelmap(island1ModifierLabelmap, self.paintEffect.ModificationModeAdd) - self.paintEffect.modifySelectedSegmentByLabelmap(island2ModifierLabelmap, self.paintEffect.ModificationModeAdd) - - # Test intensity masking - self.segmentEditorNode.MasterVolumeIntensityMaskOn() - self.segmentEditorNode.SetMasterVolumeIntensityMaskRange(-17, 848) - self.islandEffect.self().onApply() - self.checkSegmentVoxelCount(3, 87) # Segment_4 - - # Restore old overwrite setting - self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode) - self.segmentEditorNode.MasterVolumeIntensityMaskOff() + # ------------------------------------------------------------------------------ + 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_SegmentationsModuleTest2() + + # ------------------------------------------------------------------------------ + def test_SegmentationsModuleTest2(self): + # Check for modules + self.assertIsNotNone(slicer.modules.segmentations) + self.assertIsNotNone(slicer.modules.segmenteditor) + + # Run tests + self.TestSection_SetupPathsAndNames() + self.TestSection_RetrieveInputData() + self.TestSection_SetupScene() + self.TestSection_SharedLabelmapMultipleLayerEditing() + self.TestSection_IslandEffects() + self.TestSection_MarginEffects() + self.TestSection_MaskingSettings() + logging.info('Test finished') + + # ------------------------------------------------------------------------------ + def TestSection_SetupPathsAndNames(self): + # Set up paths used for this test + self.segmentationsModuleTestDir = slicer.app.temporaryPath + '/SegmentationsModuleTest' + if not os.access(self.segmentationsModuleTestDir, os.F_OK): + os.mkdir(self.segmentationsModuleTestDir) + + self.dataDir = self.segmentationsModuleTestDir + '/TinyPatient_Seg' + if not os.access(self.dataDir, os.F_OK): + os.mkdir(self.dataDir) + self.dataSegDir = self.dataDir + '/TinyPatient_Structures.seg' + + self.dataZipFilePath = self.segmentationsModuleTestDir + '/TinyPatient_Seg.zip' + + # Define variables + self.expectedNumOfFilesInDataDir = 4 + self.expectedNumOfFilesInDataSegDir = 2 + self.closedSurfaceReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName() + self.binaryLabelmapReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName() + + # ------------------------------------------------------------------------------ + def TestSection_RetrieveInputData(self): + try: + slicer.util.downloadAndExtractArchive( + TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7', + self.dataZipFilePath, self.segmentationsModuleTestDir, + checksum='SHA256:b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7') + + numOfFilesInDataDirTest = len([name for name in os.listdir(self.dataDir) if os.path.isfile(self.dataDir + '/' + name)]) + self.assertEqual(numOfFilesInDataDirTest, self.expectedNumOfFilesInDataDir) + self.assertTrue(os.access(self.dataSegDir, os.F_OK)) + numOfFilesInDataSegDirTest = len([name for name in os.listdir(self.dataSegDir) if os.path.isfile(self.dataSegDir + '/' + name)]) + self.assertEqual(numOfFilesInDataSegDirTest, self.expectedNumOfFilesInDataSegDir) + + except Exception as e: + import traceback + traceback.print_exc() + logging.error('Test caused exception!\n' + str(e)) + + # ------------------------------------------------------------------------------ + def TestSection_SetupScene(self): + self.paintEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Paint") + self.eraseEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Erase") + self.islandEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Islands") + self.thresholdEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Threshold") + + self.segmentEditorNode = slicer.util.getNode("SegmentEditor") + self.assertIsNotNone(self.segmentEditorNode) + + self.segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") + self.assertIsNotNone(self.segmentationNode) + self.segmentEditorNode.SetAndObserveSegmentationNode(self.segmentationNode) + + self.masterVolumeNode = slicer.util.loadVolume(self.dataDir + '/TinyPatient_CT.nrrd') + self.assertIsNotNone(self.masterVolumeNode) + self.segmentEditorNode.SetAndObserveMasterVolumeNode(self.masterVolumeNode) + + self.segmentation = self.segmentationNode.GetSegmentation() + self.segmentation.SetMasterRepresentationName(self.binaryLabelmapReprName) + self.assertIsNotNone(self.segmentation) + + # ------------------------------------------------------------------------------ + def TestSection_SharedLabelmapMultipleLayerEditing(self): + self.segmentation.RemoveAllSegments() + self.segmentation.AddEmptySegment("Segment_1") + self.segmentation.AddEmptySegment("Segment_2") + + defaultModifierLabelmap = self.paintEffect.defaultModifierLabelmap() + self.ijkToRas = vtk.vtkMatrix4x4() + defaultModifierLabelmap.GetImageToWorldMatrix(self.ijkToRas) + + mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData() + mergedLabelmap.SetImageToWorldMatrix(self.ijkToRas) + mergedLabelmap.SetExtent(0, 10, 0, 10, 0, 10) + mergedLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) + mergedLabelmap.GetPointData().GetScalars().Fill(1) + + oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode() + + self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteAllSegments) + self.segmentEditorNode.SetSelectedSegmentID("Segment_1") + self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd) + self.segmentEditorNode.SetSelectedSegmentID("Segment_2") + self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd) + + layerCount = self.segmentation.GetNumberOfLayers() + self.assertEqual(layerCount, 1) + + self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteNone) + self.segmentEditorNode.SetSelectedSegmentID("Segment_1") + self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd) + layerCount = self.segmentation.GetNumberOfLayers() + self.assertEqual(layerCount, 2) + + self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode) + logging.info('Multiple layer editing successful') + + # ------------------------------------------------------------------------------ + def TestSection_IslandEffects(self): + islandSizes = [1, 26, 11, 6, 8, 6, 2] + islandSizes.sort(reverse=True) + + minimumSize = 3 + self.resetIslandSegments(islandSizes) + self.islandEffect.setParameter('MinimumSize', minimumSize) + self.islandEffect.setParameter('Operation', 'KEEP_LARGEST_ISLAND') + self.islandEffect.self().onApply() + layerCount = self.segmentation.GetNumberOfLayers() + self.assertEqual(layerCount, 1) + + voxelCount = 0 + for size in islandSizes: + if size < minimumSize: + continue + voxelCount = max(voxelCount, size) + self.checkSegmentVoxelCount(0, voxelCount) + + minimumSize = 7 + self.resetIslandSegments(islandSizes) + self.islandEffect.setParameter('MinimumSize', minimumSize) + self.islandEffect.setParameter('Operation', 'REMOVE_SMALL_ISLANDS') + self.islandEffect.self().onApply() + layerCount = self.segmentation.GetNumberOfLayers() + self.assertEqual(layerCount, 1) + + voxelCount = 0 + for size in islandSizes: + if size < minimumSize: + continue + voxelCount += size + self.checkSegmentVoxelCount(0, voxelCount) + + self.resetIslandSegments(islandSizes) + minimumSize = 3 + self.islandEffect.setParameter('MinimumSize', minimumSize) + self.islandEffect.setParameter('Operation', 'SPLIT_ISLANDS_TO_SEGMENTS') + self.islandEffect.self().onApply() + layerCount = self.segmentation.GetNumberOfLayers() + self.assertEqual(layerCount, 1) + + for i in range(len(islandSizes)): + size = islandSizes[i] + if size < minimumSize: + continue + self.checkSegmentVoxelCount(i, size) + + # ------------------------------------------------------------------------------ + def resetIslandSegments(self, islandSizes): + self.segmentation.RemoveAllSegments() + + totalSize = 0 + voxelSizeSum = 0 + for size in islandSizes: + totalSize += size + 1 + voxelSizeSum += size + + mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData() + mergedLabelmap.SetImageToWorldMatrix(self.ijkToRas) + mergedLabelmapExtent = [0, totalSize - 1, 0, 0, 0, 0] + self.setupIslandLabelmap(mergedLabelmap, mergedLabelmapExtent, 0) + + emptySegment = slicer.vtkSegment() + emptySegment.SetName("Segment_1") + emptySegment.AddRepresentation(self.binaryLabelmapReprName, mergedLabelmap) + self.segmentation.AddSegment(emptySegment) + self.segmentEditorNode.SetSelectedSegmentID("Segment_1") + + startExtent = 0 + for size in islandSizes: + islandLabelmap = vtkSegmentationCore.vtkOrientedImageData() + islandLabelmap.SetImageToWorldMatrix(self.ijkToRas) + islandExtent = [startExtent, startExtent + size - 1, 0, 0, 0, 0] + self.setupIslandLabelmap(islandLabelmap, islandExtent) + self.paintEffect.modifySelectedSegmentByLabelmap(islandLabelmap, self.paintEffect.ModificationModeAdd) + startExtent += size + 1 + self.checkSegmentVoxelCount(0, voxelSizeSum) + + layerCount = self.segmentation.GetNumberOfLayers() + self.assertEqual(layerCount, 1) + + # ------------------------------------------------------------------------------ + def checkSegmentVoxelCount(self, segmentIndex, expectedVoxelCount): + segment = self.segmentation.GetNthSegment(segmentIndex) + self.assertIsNotNone(segment) + + labelmap = slicer.vtkOrientedImageData() + labelmap.SetImageToWorldMatrix(self.ijkToRas) + segmentID = self.segmentation.GetNthSegmentID(segmentIndex) + self.segmentationNode.GetBinaryLabelmapRepresentation(segmentID, labelmap) + + imageStat = vtk.vtkImageAccumulate() + imageStat.SetInputData(labelmap) + imageStat.SetComponentExtent(0, 4, 0, 0, 0, 0) + imageStat.SetComponentOrigin(0, 0, 0) + imageStat.SetComponentSpacing(1, 1, 1) + imageStat.IgnoreZeroOn() + imageStat.Update() + + self.assertEqual(imageStat.GetVoxelCount(), expectedVoxelCount) + + # ------------------------------------------------------------------------------ + def setupIslandLabelmap(self, labelmap, extent, value=1): + labelmap.SetExtent(extent) + labelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) + labelmap.GetPointData().GetScalars().Fill(value) + + # ------------------------------------------------------------------------------ + def TestSection_MarginEffects(self): + logging.info("Running test on margin effect") + + slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Margin") + + self.segmentation.RemoveAllSegments() + segment1Id = self.segmentation.AddEmptySegment("Segment_1") + segment1 = self.segmentation.GetSegment(segment1Id) + segment1.SetLabelValue(1) + self.segmentEditorNode.SetSelectedSegmentID("Segment_1") + + segment2Id = self.segmentation.AddEmptySegment("Segment_2") + segment2 = self.segmentation.GetSegment(segment2Id) + segment2.SetLabelValue(2) + + binaryLabelmapRepresentationName = slicer.vtkSegmentationConverter.GetBinaryLabelmapRepresentationName() + dataTypes = [ + vtk.VTK_CHAR, + vtk.VTK_SIGNED_CHAR, + vtk.VTK_UNSIGNED_CHAR, + vtk.VTK_SHORT, + vtk.VTK_UNSIGNED_SHORT, + vtk.VTK_INT, + vtk.VTK_UNSIGNED_INT, + # vtk.VTK_LONG, # On linux, VTK_LONG has the same size as VTK_LONG_LONG. This causes issues in vtkImageThreshold. + # vtk.VTK_UNSIGNED_LONG, See https://github.com/Slicer/Slicer/issues/5427 + # vtk.VTK_FLOAT, # Since float can't represent all int, we jump straight to double. + vtk.VTK_DOUBLE, + # vtk.VTK_LONG_LONG, # These types are unsupported in ITK + # vtk.VTK_UNSIGNED_LONG_LONG, + ] + logging.info("Testing shared labelmaps") + for dataType in dataTypes: + initialLabelmap = slicer.vtkOrientedImageData() + initialLabelmap.SetImageToWorldMatrix(self.ijkToRas) + initialLabelmap.SetExtent(0, 10, 0, 10, 0, 10) + initialLabelmap.AllocateScalars(dataType, 1) + initialLabelmap.GetPointData().GetScalars().Fill(0) + segment1.AddRepresentation(binaryLabelmapRepresentationName, initialLabelmap) + segment2.AddRepresentation(binaryLabelmapRepresentationName, initialLabelmap) + + self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteAllSegments) + self.assertEqual(self.segmentation.GetNumberOfLayers(), 1) + self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteNone) + self.assertEqual(self.segmentation.GetNumberOfLayers(), 2) + + logging.info("Testing separate labelmaps") + for dataType in dataTypes: + segment1Labelmap = slicer.vtkOrientedImageData() + segment1Labelmap.SetImageToWorldMatrix(self.ijkToRas) + segment1Labelmap.SetExtent(0, 10, 0, 10, 0, 10) + segment1Labelmap.AllocateScalars(dataType, 1) + segment1Labelmap.GetPointData().GetScalars().Fill(0) + segment1.AddRepresentation(binaryLabelmapRepresentationName, segment1Labelmap) + + segment2Labelmap = slicer.vtkOrientedImageData() + segment2Labelmap.DeepCopy(segment1Labelmap) + segment2.AddRepresentation(binaryLabelmapRepresentationName, segment2Labelmap) + + self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteAllSegments) + self.assertEqual(self.segmentation.GetNumberOfLayers(), 2) + self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteNone) + self.assertEqual(self.segmentation.GetNumberOfLayers(), 2) + + # ------------------------------------------------------------------------------ + def runMarginEffect(self, segment1, segment2, dataType, overwriteMode): + logging.info(f"Running margin effect with data type: {dataType}, and overwriteMode {overwriteMode}") + marginEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Margin") + + marginEffect.setParameter("MarginSizeMm", 50.0) + + oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode() + self.segmentEditorNode.SetOverwriteMode(overwriteMode) + + segment1Labelmap = segment1.GetRepresentation(self.binaryLabelmapReprName) + segment1Labelmap.AllocateScalars(dataType, 1) + segment1Labelmap.GetPointData().GetScalars().Fill(0) + + segment2Labelmap = segment2.GetRepresentation(self.binaryLabelmapReprName) + segment2Labelmap.AllocateScalars(dataType, 1) + segment2Labelmap.GetPointData().GetScalars().Fill(0) + + segment1Position_IJK = [5, 5, 5] + segment1Labelmap.SetScalarComponentFromDouble(segment1Position_IJK[0], segment1Position_IJK[1], segment1Position_IJK[2], 0, segment1.GetLabelValue()) + segment2Position_IJK = [6, 5, 6] + segment2Labelmap.SetScalarComponentFromDouble(segment2Position_IJK[0], segment2Position_IJK[1], segment2Position_IJK[2], 0, segment2.GetLabelValue()) + + self.checkSegmentVoxelCount(0, 1) + self.checkSegmentVoxelCount(1, 1) + + marginEffect.self().onApply() + self.checkSegmentVoxelCount(0, 9) # Margin grow + self.checkSegmentVoxelCount(1, 1) + + marginEffect.self().onApply() + self.checkSegmentVoxelCount(0, 37) # Margin grow + if overwriteMode == slicer.vtkMRMLSegmentEditorNode.OverwriteAllSegments: + self.checkSegmentVoxelCount(1, 0) # Overwritten + else: + self.checkSegmentVoxelCount(1, 1) # Not overwritten + + marginEffect.setParameter("MarginSizeMm", -50.0) + marginEffect.self().onApply() + + self.checkSegmentVoxelCount(0, 9) # Margin shrink + if overwriteMode == slicer.vtkMRMLSegmentEditorNode.OverwriteAllSegments: + self.checkSegmentVoxelCount(1, 0) # Overwritten + else: + self.checkSegmentVoxelCount(1, 1) # Not overwritten + + self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode) + + # ------------------------------------------------------------------------------ + def TestSection_MaskingSettings(self): + self.segmentation.RemoveAllSegments() + segment1Id = self.segmentation.AddEmptySegment("Segment_1") + segment2Id = self.segmentation.AddEmptySegment("Segment_2") + segment3Id = self.segmentation.AddEmptySegment("Segment_3") + segment4Id = self.segmentation.AddEmptySegment("Segment_4") + + oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode() + + # ------------------- + # Test applying threshold with no masking + self.segmentEditorNode.SetSelectedSegmentID(segment1Id) + self.thresholdEffect.setParameter("MinimumThreshold", "-17") + self.thresholdEffect.setParameter("MaximumThreshold", "848") + self.thresholdEffect.self().onApply() + self.checkSegmentVoxelCount(0, 204) # Segment_1 + self.checkSegmentVoxelCount(1, 0) # Segment_2 + + # ------------------- + # Add paint to segment 2. No overwrite + paintModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData() + paintModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas) + paintModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5) + paintModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) + paintModifierLabelmap.GetPointData().GetScalars().Fill(1) + + self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteNone) + self.segmentEditorNode.SetSelectedSegmentID(segment2Id) + self.paintEffect.modifySelectedSegmentByLabelmap(paintModifierLabelmap, self.paintEffect.ModificationModeAdd) + + self.checkSegmentVoxelCount(0, 204) # Segment_1 + self.checkSegmentVoxelCount(1, 64) # Segment_2 + + # ------------------- + # Test erasing with no masking + eraseModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData() + eraseModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas) + eraseModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5) + eraseModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) + eraseModifierLabelmap.GetPointData().GetScalars().Fill(1) + + self.segmentEditorNode.SetSelectedSegmentID(segment1Id) + self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) + self.checkSegmentVoxelCount(0, 177) # Segment_1 + self.checkSegmentVoxelCount(1, 64) # Segment_2 + + # ------------------- + # Test erasing with masking on empty segment + self.segmentEditorNode.SetSelectedSegmentID(segment1Id) + self.thresholdEffect.self().onApply() # Reset Segment_1 + self.checkSegmentVoxelCount(0, 204) # Segment_1 + self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedInsideSingleSegment) + self.segmentEditorNode.SetMaskSegmentID(segment2Id) + self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) + self.checkSegmentVoxelCount(0, 177) # We expect to be able to erase the current segment regardless of masking + self.checkSegmentVoxelCount(1, 64) # Segment_2 + + # ------------------- + # Test erasing with masking on the same segment + self.segmentEditorNode.SetSelectedSegmentID(segment1Id) + self.thresholdEffect.self().onApply() # Reset Segment_1 + self.checkSegmentVoxelCount(0, 204) # Segment_1 + self.segmentEditorNode.SetMaskSegmentID(segment1Id) + self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) + self.checkSegmentVoxelCount(0, 177) # Segment_1 + self.checkSegmentVoxelCount(1, 64) # Segment_2 + + # ------------------- + # Test erasing all segments + self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedEverywhere) + self.thresholdEffect.self().onApply() # Reset Segment_1 + self.checkSegmentVoxelCount(0, 204) # Segment_1 + self.segmentEditorNode.SetSelectedSegmentID(segment1Id) + self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemoveAll) + self.checkSegmentVoxelCount(0, 177) # Segment_1 + self.checkSegmentVoxelCount(1, 0) # Segment_2 + + # ------------------- + # Test adding back segments + self.thresholdEffect.self().onApply() # Reset Segment_1 + self.checkSegmentVoxelCount(0, 204) # Segment_1 + self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedInsideSingleSegment) + self.segmentEditorNode.SetMaskSegmentID(segment2Id) + self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) + self.checkSegmentVoxelCount(0, 177) # Segment_1 + self.checkSegmentVoxelCount(1, 27) # Segment_2 + + # ------------------- + # Test threshold effect segment mask + self.segmentEditorNode.SetMaskSegmentID(segment2Id) # Erase Segment_2 + self.segmentEditorNode.SetSelectedSegmentID(segment2Id) + self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove) + self.segmentEditorNode.SetMaskSegmentID(segment1Id) + self.segmentEditorNode.SetSelectedSegmentID(segment2Id) + self.thresholdEffect.self().onApply() # Threshold Segment_2 within Segment_1 + self.checkSegmentVoxelCount(0, 177) # Segment_1 + self.checkSegmentVoxelCount(1, 177) # Segment_2 + + # ------------------- + # Test intensity masking with segment mask + self.segmentEditorNode.MasterVolumeIntensityMaskOn() + self.segmentEditorNode.SetMasterVolumeIntensityMaskRange(-17, 848) + self.thresholdEffect.setParameter("MinimumThreshold", "-99999") + self.thresholdEffect.setParameter("MaximumThreshold", "99999") + self.segmentEditorNode.SetSelectedSegmentID(segment3Id) + self.thresholdEffect.self().onApply() # Threshold Segment_3 + self.checkSegmentVoxelCount(2, 177) # Segment_3 + + # ------------------- + # Test intensity masking with islands + self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedEverywhere) + self.segmentEditorNode.MasterVolumeIntensityMaskOff() + self.segmentEditorNode.SetSelectedSegmentID(segment4Id) + + island1ModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData() + island1ModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas) + island1ModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5) + island1ModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) + island1ModifierLabelmap.GetPointData().GetScalars().Fill(1) + self.paintEffect.modifySelectedSegmentByLabelmap(island1ModifierLabelmap, self.paintEffect.ModificationModeAdd) + + island2ModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData() + island2ModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas) + island2ModifierLabelmap.SetExtent(7, 9, 7, 9, 7, 9) + island2ModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) + island2ModifierLabelmap.GetPointData().GetScalars().Fill(1) + self.paintEffect.modifySelectedSegmentByLabelmap(island2ModifierLabelmap, self.paintEffect.ModificationModeAdd) + self.checkSegmentVoxelCount(3, 91) # Segment_4 + + # Test that no masking works as expected + minimumSize = 3 + self.islandEffect.setParameter('MinimumSize', minimumSize) + self.islandEffect.setParameter('Operation', 'KEEP_LARGEST_ISLAND') + self.islandEffect.self().onApply() + self.checkSegmentVoxelCount(3, 64) # Segment_4 + + # Reset Segment_4 islands + self.paintEffect.modifySelectedSegmentByLabelmap(island1ModifierLabelmap, self.paintEffect.ModificationModeAdd) + self.paintEffect.modifySelectedSegmentByLabelmap(island2ModifierLabelmap, self.paintEffect.ModificationModeAdd) + + # Test intensity masking + self.segmentEditorNode.MasterVolumeIntensityMaskOn() + self.segmentEditorNode.SetMasterVolumeIntensityMaskRange(-17, 848) + self.islandEffect.self().onApply() + self.checkSegmentVoxelCount(3, 87) # Segment_4 + + # Restore old overwrite setting + self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode) + self.segmentEditorNode.MasterVolumeIntensityMaskOff() diff --git a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyCorePluginsSelfTest.py b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyCorePluginsSelfTest.py index 2756db23e0d..68a3c1a27f2 100644 --- a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyCorePluginsSelfTest.py +++ b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyCorePluginsSelfTest.py @@ -9,32 +9,32 @@ # class SubjectHierarchyCorePluginsSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "SubjectHierarchyCorePluginsSelfTest" - parent.categories = ["Testing.TestCases"] - parent.dependencies = ["SubjectHierarchy"] - parent.contributors = ["Csaba Pinter (Queen's)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "SubjectHierarchyCorePluginsSelfTest" + parent.categories = ["Testing.TestCases"] + parent.dependencies = ["SubjectHierarchy"] + parent.contributors = ["Csaba Pinter (Queen's)"] + parent.helpText = """ This is a self test for the Subject hierarchy core plugins. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Csaba Pinter, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care""" - self.parent = parent + self.parent = parent - # Add this test to the SelfTest module's list for discovery when the module - # is created. Since this module may be discovered before SelfTests itself, - # create the list if it doesn't already exist. - try: - slicer.selfTests - except AttributeError: - slicer.selfTests = {} - slicer.selfTests['SubjectHierarchyCorePluginsSelfTest'] = self.runTest + # Add this test to the SelfTest module's list for discovery when the module + # is created. Since this module may be discovered before SelfTests itself, + # create the list if it doesn't already exist. + try: + slicer.selfTests + except AttributeError: + slicer.selfTests = {} + slicer.selfTests['SubjectHierarchyCorePluginsSelfTest'] = self.runTest - def runTest(self, msec=100, **kwargs): - tester = SubjectHierarchyCorePluginsSelfTestTest() - tester.runTest() + def runTest(self, msec=100, **kwargs): + tester = SubjectHierarchyCorePluginsSelfTestTest() + tester.runTest() # @@ -42,8 +42,8 @@ def runTest(self, msec=100, **kwargs): # class SubjectHierarchyCorePluginsSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -51,180 +51,180 @@ def setup(self): # class SubjectHierarchyCorePluginsSelfTestLogic(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 - """ + """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 + """ - def __init__(self): - pass + def __init__(self): + pass class SubjectHierarchyCorePluginsSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ - slicer.mrmlScene.Clear(0) - - self.delayMs = 700 - - #TODO: Comment out (sample code for debugging) - #logFile = open('d:/pyTestLog.txt', 'w') - #logFile.write(repr(slicer.modules.SubjectHierarchyCorePluginsSelfTest) + '\n') - #logFile.write(repr(slicer.modules.subjecthierarchy) + '\n') - #logFile.close() - - def runTest(self): - """Run as few or as many tests as needed here. + This is the test case for your scripted module. """ - self.setUp() - self.test_SubjectHierarchyCorePluginsSelfTest_FullTest1() - - # ------------------------------------------------------------------------------ - def test_SubjectHierarchyCorePluginsSelfTest_FullTest1(self): - # Check for SubjectHierarchy module - self.assertTrue( slicer.modules.subjecthierarchy ) - - # Switch to subject hierarchy module so that the changes can be seen as the test goes - slicer.util.selectModule('SubjectHierarchy') - - self.section_SetupPathsAndNames() - self.section_MarkupRole() - self.section_CloneNode() - self.section_SegmentEditor() - - # ------------------------------------------------------------------------------ - def section_SetupPathsAndNames(self): - # Set constants - self.invalidItemID = slicer.vtkMRMLSubjectHierarchyNode.GetInvalidItemID() - self.sampleMarkupName = 'SampleMarkup' - self.studyItemID = self.invalidItemID - self.cloneNodeNamePostfix = slicer.qSlicerSubjectHierarchyCloneNodePlugin().getCloneNodeNamePostfix() - - # Test printing of all context menu actions and their section numbers - pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance(); - print(pluginHandler.dumpContextMenuActions()) - - # ------------------------------------------------------------------------------ - def section_MarkupRole(self): - self.delayDisplay("Markup role",self.delayMs) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - # Create sample markups node - markupsNode = slicer.vtkMRMLMarkupsFiducialNode() - slicer.mrmlScene.AddNode(markupsNode) - markupsNode.SetName(self.sampleMarkupName) - fiducialPosition = [100.0, 0.0, 0.0] - markupsNode.AddControlPoint(fiducialPosition) - markupsShItemID = shNode.GetItemByDataNode(markupsNode) - self.assertIsNotNone( markupsShItemID ) - self.assertEqual( shNode.GetItemOwnerPluginName(markupsShItemID), 'Markups' ) - - # Create patient and study - patientItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), 'Patient') - self.studyItemID = shNode.CreateStudyItem(patientItemID, 'Study') - - # Add markups under study - markupsShItemID2 = shNode.CreateItem(self.studyItemID, markupsNode) - self.assertEqual( markupsShItemID, markupsShItemID2 ) - self.assertEqual( shNode.GetItemParent(markupsShItemID), self.studyItemID ) - self.assertEqual( shNode.GetItemOwnerPluginName(markupsShItemID), 'Markups' ) - - # ------------------------------------------------------------------------------ - def section_CloneNode(self): - self.delayDisplay("Clone node",self.delayMs) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - markupsNode = slicer.util.getNode(self.sampleMarkupName) - markupsShItemID = shNode.GetItemByDataNode(markupsNode) - - self.assertIsNotNone( markupsShItemID ) - self.assertIsNotNone( shNode.GetItemDataNode(markupsShItemID) ) - - # Add storage node for markups node to test cloning those - markupsStorageNode = slicer.vtkMRMLMarkupsFiducialStorageNode() - slicer.mrmlScene.AddNode(markupsStorageNode) - markupsNode.SetAndObserveStorageNodeID(markupsStorageNode.GetID()) - - # Get clone node plugin - pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance() - self.assertIsNotNone( pluginHandler ) - - cloneNodePlugin = pluginHandler.pluginByName('CloneNode') - self.assertIsNotNone( cloneNodePlugin ) - - # Set markup node as current (i.e. selected in the tree) for clone - pluginHandler.setCurrentItem(markupsShItemID) - - # Get clone node context menu action and trigger - cloneNodePlugin.itemContextMenuActions()[0].activate(qt.QAction.Trigger) - - self.assertEqual( slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialNode'), 2 ) - self.assertEqual( slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsDisplayNode'), 2 ) - self.assertEqual( slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialStorageNode'), 2 ) - - clonedMarkupsName = self.sampleMarkupName + self.cloneNodeNamePostfix - clonedMarkupsNode = slicer.util.getNode(clonedMarkupsName) - self.assertIsNotNone( clonedMarkupsNode ) - clonedMarkupsShItemID = shNode.GetItemChildWithName(self.studyItemID, clonedMarkupsName) - self.assertIsNotNone( clonedMarkupsShItemID ) - self.assertIsNotNone( clonedMarkupsNode.GetDisplayNode() ) - self.assertIsNotNone( clonedMarkupsNode.GetStorageNode() ) - - inSameStudy = slicer.vtkSlicerSubjectHierarchyModuleLogic.AreItemsInSameBranch( - shNode, markupsShItemID, clonedMarkupsShItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy()) - self.assertTrue( inSameStudy ) - - # ------------------------------------------------------------------------------ - def section_SegmentEditor(self): - self.delayDisplay("Segment Editor",self.delayMs) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - import SampleData - mrHeadNode = SampleData.SampleDataLogic().downloadMRHead() - - # Make sure Data module is initialized because the use case tested below - # (https://github.com/Slicer/Slicer/issues/4877) needs an initialized SH - # tree view so that applyReferenceHighlightForItems is run - slicer.util.selectModule('Data') - - folderItem = shNode.CreateFolderItem(shNode.GetSceneItemID(), 'TestFolder') - - mrHeadItem = shNode.GetItemByDataNode(mrHeadNode) - shNode.SetItemParent(mrHeadItem, folderItem) - - dataModuleWidget = slicer.modules.data.widgetRepresentation() - treeView = slicer.util.findChildren(dataModuleWidget, className='qMRMLSubjectHierarchyTreeView')[0] - treeView.setCurrentItem(mrHeadItem) - - pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - segmentEditorPlugin = pluginHandler.pluginByName('SegmentEditor').self() - segmentEditorPlugin.segmentEditorAction.trigger() - - # Get segmentation node automatically created by "Segment this..." action - segmentationNode = None - segmentationNodes = slicer.mrmlScene.GetNodesByClass('vtkMRMLSegmentationNode') - segmentationNodes.UnRegister(None) - for i in range(segmentationNodes.GetNumberOfItems()): - currentSegNode = segmentationNodes.GetItemAsObject(i) - if currentSegNode.GetNodeReferenceID(currentSegNode.GetReferenceImageGeometryReferenceRole()) == mrHeadNode.GetID(): - segmentationNode = currentSegNode - break - - self.assertIsNotNone(segmentationNode) - - segmentationItem = shNode.GetItemByDataNode(segmentationNode) - self.assertEqual( shNode.GetItemParent(segmentationItem), shNode.GetItemParent(mrHeadItem) ) - self.assertEqual( segmentationNode.GetName()[:len(mrHeadNode.GetName())], mrHeadNode.GetName() ) + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) + + self.delayMs = 700 + + # TODO: Comment out (sample code for debugging) + # logFile = open('d:/pyTestLog.txt', 'w') + # logFile.write(repr(slicer.modules.SubjectHierarchyCorePluginsSelfTest) + '\n') + # logFile.write(repr(slicer.modules.subjecthierarchy) + '\n') + # logFile.close() + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_SubjectHierarchyCorePluginsSelfTest_FullTest1() + + # ------------------------------------------------------------------------------ + def test_SubjectHierarchyCorePluginsSelfTest_FullTest1(self): + # Check for SubjectHierarchy module + self.assertTrue(slicer.modules.subjecthierarchy) + + # Switch to subject hierarchy module so that the changes can be seen as the test goes + slicer.util.selectModule('SubjectHierarchy') + + self.section_SetupPathsAndNames() + self.section_MarkupRole() + self.section_CloneNode() + self.section_SegmentEditor() + + # ------------------------------------------------------------------------------ + def section_SetupPathsAndNames(self): + # Set constants + self.invalidItemID = slicer.vtkMRMLSubjectHierarchyNode.GetInvalidItemID() + self.sampleMarkupName = 'SampleMarkup' + self.studyItemID = self.invalidItemID + self.cloneNodeNamePostfix = slicer.qSlicerSubjectHierarchyCloneNodePlugin().getCloneNodeNamePostfix() + + # Test printing of all context menu actions and their section numbers + pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance() + print(pluginHandler.dumpContextMenuActions()) + + # ------------------------------------------------------------------------------ + def section_MarkupRole(self): + self.delayDisplay("Markup role", self.delayMs) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + # Create sample markups node + markupsNode = slicer.vtkMRMLMarkupsFiducialNode() + slicer.mrmlScene.AddNode(markupsNode) + markupsNode.SetName(self.sampleMarkupName) + fiducialPosition = [100.0, 0.0, 0.0] + markupsNode.AddControlPoint(fiducialPosition) + markupsShItemID = shNode.GetItemByDataNode(markupsNode) + self.assertIsNotNone(markupsShItemID) + self.assertEqual(shNode.GetItemOwnerPluginName(markupsShItemID), 'Markups') + + # Create patient and study + patientItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), 'Patient') + self.studyItemID = shNode.CreateStudyItem(patientItemID, 'Study') + + # Add markups under study + markupsShItemID2 = shNode.CreateItem(self.studyItemID, markupsNode) + self.assertEqual(markupsShItemID, markupsShItemID2) + self.assertEqual(shNode.GetItemParent(markupsShItemID), self.studyItemID) + self.assertEqual(shNode.GetItemOwnerPluginName(markupsShItemID), 'Markups') + + # ------------------------------------------------------------------------------ + def section_CloneNode(self): + self.delayDisplay("Clone node", self.delayMs) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + markupsNode = slicer.util.getNode(self.sampleMarkupName) + markupsShItemID = shNode.GetItemByDataNode(markupsNode) + + self.assertIsNotNone(markupsShItemID) + self.assertIsNotNone(shNode.GetItemDataNode(markupsShItemID)) + + # Add storage node for markups node to test cloning those + markupsStorageNode = slicer.vtkMRMLMarkupsFiducialStorageNode() + slicer.mrmlScene.AddNode(markupsStorageNode) + markupsNode.SetAndObserveStorageNodeID(markupsStorageNode.GetID()) + + # Get clone node plugin + pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance() + self.assertIsNotNone(pluginHandler) + + cloneNodePlugin = pluginHandler.pluginByName('CloneNode') + self.assertIsNotNone(cloneNodePlugin) + + # Set markup node as current (i.e. selected in the tree) for clone + pluginHandler.setCurrentItem(markupsShItemID) + + # Get clone node context menu action and trigger + cloneNodePlugin.itemContextMenuActions()[0].activate(qt.QAction.Trigger) + + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialNode'), 2) + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsDisplayNode'), 2) + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialStorageNode'), 2) + + clonedMarkupsName = self.sampleMarkupName + self.cloneNodeNamePostfix + clonedMarkupsNode = slicer.util.getNode(clonedMarkupsName) + self.assertIsNotNone(clonedMarkupsNode) + clonedMarkupsShItemID = shNode.GetItemChildWithName(self.studyItemID, clonedMarkupsName) + self.assertIsNotNone(clonedMarkupsShItemID) + self.assertIsNotNone(clonedMarkupsNode.GetDisplayNode()) + self.assertIsNotNone(clonedMarkupsNode.GetStorageNode()) + + inSameStudy = slicer.vtkSlicerSubjectHierarchyModuleLogic.AreItemsInSameBranch( + shNode, markupsShItemID, clonedMarkupsShItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy()) + self.assertTrue(inSameStudy) + + # ------------------------------------------------------------------------------ + def section_SegmentEditor(self): + self.delayDisplay("Segment Editor", self.delayMs) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + import SampleData + mrHeadNode = SampleData.SampleDataLogic().downloadMRHead() + + # Make sure Data module is initialized because the use case tested below + # (https://github.com/Slicer/Slicer/issues/4877) needs an initialized SH + # tree view so that applyReferenceHighlightForItems is run + slicer.util.selectModule('Data') + + folderItem = shNode.CreateFolderItem(shNode.GetSceneItemID(), 'TestFolder') + + mrHeadItem = shNode.GetItemByDataNode(mrHeadNode) + shNode.SetItemParent(mrHeadItem, folderItem) + + dataModuleWidget = slicer.modules.data.widgetRepresentation() + treeView = slicer.util.findChildren(dataModuleWidget, className='qMRMLSubjectHierarchyTreeView')[0] + treeView.setCurrentItem(mrHeadItem) + + pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + segmentEditorPlugin = pluginHandler.pluginByName('SegmentEditor').self() + segmentEditorPlugin.segmentEditorAction.trigger() + + # Get segmentation node automatically created by "Segment this..." action + segmentationNode = None + segmentationNodes = slicer.mrmlScene.GetNodesByClass('vtkMRMLSegmentationNode') + segmentationNodes.UnRegister(None) + for i in range(segmentationNodes.GetNumberOfItems()): + currentSegNode = segmentationNodes.GetItemAsObject(i) + if currentSegNode.GetNodeReferenceID(currentSegNode.GetReferenceImageGeometryReferenceRole()) == mrHeadNode.GetID(): + segmentationNode = currentSegNode + break + + self.assertIsNotNone(segmentationNode) + + segmentationItem = shNode.GetItemByDataNode(segmentationNode) + self.assertEqual(shNode.GetItemParent(segmentationItem), shNode.GetItemParent(mrHeadItem)) + self.assertEqual(segmentationNode.GetName()[:len(mrHeadNode.GetName())], mrHeadNode.GetName()) diff --git a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyFoldersTest1.py b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyFoldersTest1.py index 8b0f210998a..eb3661971fe 100644 --- a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyFoldersTest1.py +++ b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyFoldersTest1.py @@ -10,274 +10,274 @@ class SubjectHierarchyFoldersTest1(unittest.TestCase): - 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_SubjectHierarchyFoldersTest1() - - #------------------------------------------------------------------------------ - def test_SubjectHierarchyFoldersTest1(self): - # Check for modules - self.assertIsNotNone( slicer.modules.subjecthierarchy ) - - self.TestSection_InitializeTest() - self.TestSection_LoadTestData() - self.TestSection_FolderVisibility() - self.TestSection_ApplyDisplayPropertiesOnBranch() - self.TestSection_FolderDisplayOverrideAllowed() - - logging.info('Test finished') - - #------------------------------------------------------------------------------ - def TestSection_InitializeTest(self): - # - # Define variables - # - - # A certain model to test that is both in the brain and the midbrain folders - self.testModelNodeName = 'Model_79_left_red_nucleus' - self.testModelNode = None - self.overrideColor = [255, 255, 0] - - # Get subject hierarchy node - self.shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - self.assertIsNotNone(self.shNode) - # Get folder plugin - pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance() - self.assertIsNotNone(pluginHandler) - self.folderPlugin = pluginHandler.pluginByName('Folder') - self.assertIsNotNone(self.folderPlugin) - - # - # Initialize test - # - - # Create 3D view - self.layoutName = "Test3DView" - # ownerNode manages this view instead of the layout manager (it can be any node in the scene) - self.viewOwnerNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScriptedModuleNode") - self.viewNode = slicer.vtkMRMLViewNode() - self.viewNode.SetName(self.layoutName) - self.viewNode.SetLayoutName(self.layoutName) - self.viewNode.SetLayoutColor(1, 1, 0) - self.viewNode.SetAndObserveParentLayoutNodeID(self.viewOwnerNode.GetID()) - self.viewNode = slicer.mrmlScene.AddNode(self.viewNode) - self.viewWidget = slicer.qMRMLThreeDWidget() - self.viewWidget.setMRMLScene(slicer.mrmlScene) - self.viewWidget.setMRMLViewNode(self.viewNode) - self.viewWidget.show() - - # Get model displayable manager for the 3D view - self.modelDisplayableManager = self.getModelDisplayableManager() - self.assertIsNotNone(self.modelDisplayableManager) - - #------------------------------------------------------------------------------ - def TestSection_LoadTestData(self): - # Load NAC Brain Atlas 2015 (https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f) - logging.info('Test section: Load NAC Brain Atlas 2015') - - import SampleData - sceneFile = SampleData.downloadFromURL( - fileNames='NACBrainAtlas2015.mrb', - # Note: this data set is from SlicerDataStore (not from SlicerTestingData) repository - uris=DATA_STORE_URL + 'SHA256/d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f', - checksums='SHA256:d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f')[0] - - ioManager = slicer.app.ioManager() - ioManager.loadFile(sceneFile) - - # Check number of models to see if atlas was fully loaded - self.assertEqual(298, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelNode')) # 301 with main window due to the slice views - - # Check number of model hierarchy nodes to make sure all of them were converted - self.assertEqual(0, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelHierarchyNode')) - # Check number of folder display nodes, which is zero until branch display related functions are used - self.assertEqual(0, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLFolderDisplayNode')) - - # Check number of folder items - numberOfFolderItems = 0 - allItems = vtk.vtkIdList() - self.shNode.GetItemChildren(self.shNode.GetSceneItemID(), allItems, True) - for index in range(allItems.GetNumberOfIds()): - currentItem = allItems.GetId(index) - if self.shNode.IsItemLevel(currentItem, slicer.vtkMRMLSubjectHierarchyConstants.GetSubjectHierarchyLevelFolder()): - numberOfFolderItems += 1 - self.assertEqual(80, numberOfFolderItems) - - #------------------------------------------------------------------------------ - def TestSection_FolderVisibility(self): - # Test apply display properties on branch feature - logging.info('Test section: Folder visibility') - - # Get folder that contains the whole brain - brainFolderItem = self.shNode.GetItemByName('Brain') - self.assertNotEqual(brainFolderItem, 0) - - # Check number of visible models - modelNodes = slicer.util.getNodes('vtkMRMLModelNode*') - modelNodesList = list(modelNodes.values()) - numberOfVisibleModels = 0 - for modelNode in modelNodesList: - displayNode = modelNode.GetDisplayNode() - actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID()) - if actor.GetVisibility() > 0: - numberOfVisibleModels += 1 - self.assertEqual(225, numberOfVisibleModels) - - # Check model node hierarchy visibility - self.testModelNode = slicer.util.getNode(self.testModelNodeName) - self.assertIsNotNone(self.testModelNode) - - testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode) - self.assertTrue(testModelHierarchyVisibility) - - # Hide branch using the folder plugin - self.startTiming() - self.folderPlugin.setDisplayVisibility(brainFolderItem, 0) - logging.info('Time of hiding whole brain: ' + str(self.stopTiming()/1000) + ' s') - - # Check if a folder display node was indeed created when changing display property on the folder - self.assertEqual(1, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLFolderDisplayNode')) - brainFolderDisplayNode = self.shNode.GetItemDataNode(brainFolderItem) - self.assertIsNotNone(brainFolderDisplayNode) - - # Check model node hierarchy visibility - testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode) - self.assertFalse(testModelHierarchyVisibility) - - # Check if brain models were indeed hidden - numberOfVisibleModels = 0 - for modelNode in modelNodesList: - displayNode = modelNode.GetDisplayNode() - actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID()) - if actor.GetVisibility() > 0: - numberOfVisibleModels += 1 - self.assertEqual(3, numberOfVisibleModels) - - # Show folder again - self.startTiming() - self.folderPlugin.setDisplayVisibility(brainFolderItem, 1) - logging.info('Time of showing whole brain: ' + str(self.stopTiming()/1000) + ' s') - - # Check number of visible models - numberOfVisibleModels = 0 - for modelNode in modelNodesList: - displayNode = modelNode.GetDisplayNode() - actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID()) - if actor.GetVisibility() > 0: - numberOfVisibleModels += 1 - self.assertEqual(225, numberOfVisibleModels) - - # Check model node hierarchy visibility - testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode) - self.assertTrue(testModelHierarchyVisibility) - - #------------------------------------------------------------------------------ - def TestSection_ApplyDisplayPropertiesOnBranch(self): - # Test apply display properties on branch feature - logging.info('Test section: Apply display properties on branch') - - # Get folder that contains the midbrain - midbrainFolderItem = self.shNode.GetItemByName('midbrain') - self.assertNotEqual(midbrainFolderItem, 0) - - # Test simple color override, check actor color - overrideColorQt = qt.QColor(self.overrideColor[0], self.overrideColor[1], self.overrideColor[2]) - self.startTiming() - self.folderPlugin.setDisplayColor(midbrainFolderItem, overrideColorQt, {}) - logging.info('Time of setting override color on midbrain branch: ' + str(self.stopTiming()/1000) + ' s') - - # Check number of models with overridden color - numberOfOverriddenMidbrainModels = 0 - testModelNodeOverridden = False - midbrainModelItems = vtk.vtkIdList() - self.shNode.GetItemChildren(midbrainFolderItem, midbrainModelItems, True) - for index in range(midbrainModelItems.GetNumberOfIds()): - currentMidbrainModelItem = midbrainModelItems.GetId(index) - currentMidbrainModelNode = self.shNode.GetItemDataNode(currentMidbrainModelItem) - if currentMidbrainModelNode: # The child item can be a folder as well, in which case there is no model node - displayNode = currentMidbrainModelNode.GetDisplayNode() - actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID()) - currentColor = actor.GetProperty().GetColor() - if (currentColor[0] == self.overrideColor[0]/255 and - currentColor[1] == self.overrideColor[1]/255 and - currentColor[2] == self.overrideColor[2]/255): - if currentMidbrainModelNode is self.testModelNode: - testModelNodeOverridden = True - numberOfOverriddenMidbrainModels += 1 - self.assertEqual(6, numberOfOverriddenMidbrainModels) - self.assertTrue(testModelNodeOverridden) - - # Test hierarchy opacity - testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode) - self.assertEqual(testModelHierarchyOpacity, 1.0) - - midbrainFolderDisplayNode = self.shNode.GetItemDataNode(midbrainFolderItem) - self.assertIsNotNone(midbrainFolderDisplayNode) - midbrainFolderDisplayNode.SetOpacity(0.5) - - testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode) - self.assertEqual(testModelHierarchyOpacity, 0.5) - - brainFolderItem = self.shNode.GetItemByName('Brain') - self.assertNotEqual(brainFolderItem, 0) - brainFolderDisplayNode = self.shNode.GetItemDataNode(brainFolderItem) - self.assertIsNotNone(brainFolderDisplayNode) - brainFolderDisplayNode.SetOpacity(0.5) - - testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode) - self.assertEqual(testModelHierarchyOpacity, 0.25) - - #------------------------------------------------------------------------------ - def TestSection_FolderDisplayOverrideAllowed(self): - # Test exclusion of a node from the apply display properties feature - logging.info('Test section: Disable apply display properties using FolderDisplayOverrideAllowed') - - testModelDisplayNode = self.testModelNode.GetDisplayNode() - self.assertTrue(testModelDisplayNode.GetFolderDisplayOverrideAllowed()) - - # Turn of override allowed - testModelDisplayNode.SetFolderDisplayOverrideAllowed(False) - - # Check that color and opacity are not overridden - testModelActor = self.modelDisplayableManager.GetActorByID(testModelDisplayNode.GetID()) - - testModelCurrentColor = testModelActor.GetProperty().GetColor() - colorOverridden = False - if (testModelCurrentColor[0] == self.overrideColor[0]/255 and - testModelCurrentColor[1] == self.overrideColor[1]/255 and - testModelCurrentColor[2] == self.overrideColor[2]/255): - colorOverridden = True - self.assertFalse(colorOverridden) - - testModelCurrentOpacity = testModelActor.GetProperty().GetOpacity() - self.assertEqual(testModelCurrentOpacity, 1.0) - - #------------------------------------------------------------------------------ - def startTiming(self): - self.timer = qt.QTime() - self.timer.start() - - #------------------------------------------------------------------------------ - def stopTiming(self): - return self.timer.elapsed() - - #------------------------------------------------------------------------------ - def getModelDisplayableManager(self): - if self.viewWidget is None: - logging.error('View widget is not created') - return None - managers = vtk.vtkCollection() - self.viewWidget.getDisplayableManagers(managers) - for i in range(managers.GetNumberOfItems()): - obj = managers.GetItemAsObject(i) - if obj.IsA('vtkMRMLModelDisplayableManager'): - return obj - logging.error('Failed to find the model displayable manager') - return None + 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_SubjectHierarchyFoldersTest1() + + # ------------------------------------------------------------------------------ + def test_SubjectHierarchyFoldersTest1(self): + # Check for modules + self.assertIsNotNone(slicer.modules.subjecthierarchy) + + self.TestSection_InitializeTest() + self.TestSection_LoadTestData() + self.TestSection_FolderVisibility() + self.TestSection_ApplyDisplayPropertiesOnBranch() + self.TestSection_FolderDisplayOverrideAllowed() + + logging.info('Test finished') + + # ------------------------------------------------------------------------------ + def TestSection_InitializeTest(self): + # + # Define variables + # + + # A certain model to test that is both in the brain and the midbrain folders + self.testModelNodeName = 'Model_79_left_red_nucleus' + self.testModelNode = None + self.overrideColor = [255, 255, 0] + + # Get subject hierarchy node + self.shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + self.assertIsNotNone(self.shNode) + # Get folder plugin + pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance() + self.assertIsNotNone(pluginHandler) + self.folderPlugin = pluginHandler.pluginByName('Folder') + self.assertIsNotNone(self.folderPlugin) + + # + # Initialize test + # + + # Create 3D view + self.layoutName = "Test3DView" + # ownerNode manages this view instead of the layout manager (it can be any node in the scene) + self.viewOwnerNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScriptedModuleNode") + self.viewNode = slicer.vtkMRMLViewNode() + self.viewNode.SetName(self.layoutName) + self.viewNode.SetLayoutName(self.layoutName) + self.viewNode.SetLayoutColor(1, 1, 0) + self.viewNode.SetAndObserveParentLayoutNodeID(self.viewOwnerNode.GetID()) + self.viewNode = slicer.mrmlScene.AddNode(self.viewNode) + self.viewWidget = slicer.qMRMLThreeDWidget() + self.viewWidget.setMRMLScene(slicer.mrmlScene) + self.viewWidget.setMRMLViewNode(self.viewNode) + self.viewWidget.show() + + # Get model displayable manager for the 3D view + self.modelDisplayableManager = self.getModelDisplayableManager() + self.assertIsNotNone(self.modelDisplayableManager) + + # ------------------------------------------------------------------------------ + def TestSection_LoadTestData(self): + # Load NAC Brain Atlas 2015 (https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f) + logging.info('Test section: Load NAC Brain Atlas 2015') + + import SampleData + sceneFile = SampleData.downloadFromURL( + fileNames='NACBrainAtlas2015.mrb', + # Note: this data set is from SlicerDataStore (not from SlicerTestingData) repository + uris=DATA_STORE_URL + 'SHA256/d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f', + checksums='SHA256:d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f')[0] + + ioManager = slicer.app.ioManager() + ioManager.loadFile(sceneFile) + + # Check number of models to see if atlas was fully loaded + self.assertEqual(298, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelNode')) # 301 with main window due to the slice views + + # Check number of model hierarchy nodes to make sure all of them were converted + self.assertEqual(0, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelHierarchyNode')) + # Check number of folder display nodes, which is zero until branch display related functions are used + self.assertEqual(0, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLFolderDisplayNode')) + + # Check number of folder items + numberOfFolderItems = 0 + allItems = vtk.vtkIdList() + self.shNode.GetItemChildren(self.shNode.GetSceneItemID(), allItems, True) + for index in range(allItems.GetNumberOfIds()): + currentItem = allItems.GetId(index) + if self.shNode.IsItemLevel(currentItem, slicer.vtkMRMLSubjectHierarchyConstants.GetSubjectHierarchyLevelFolder()): + numberOfFolderItems += 1 + self.assertEqual(80, numberOfFolderItems) + + # ------------------------------------------------------------------------------ + def TestSection_FolderVisibility(self): + # Test apply display properties on branch feature + logging.info('Test section: Folder visibility') + + # Get folder that contains the whole brain + brainFolderItem = self.shNode.GetItemByName('Brain') + self.assertNotEqual(brainFolderItem, 0) + + # Check number of visible models + modelNodes = slicer.util.getNodes('vtkMRMLModelNode*') + modelNodesList = list(modelNodes.values()) + numberOfVisibleModels = 0 + for modelNode in modelNodesList: + displayNode = modelNode.GetDisplayNode() + actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID()) + if actor.GetVisibility() > 0: + numberOfVisibleModels += 1 + self.assertEqual(225, numberOfVisibleModels) + + # Check model node hierarchy visibility + self.testModelNode = slicer.util.getNode(self.testModelNodeName) + self.assertIsNotNone(self.testModelNode) + + testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode) + self.assertTrue(testModelHierarchyVisibility) + + # Hide branch using the folder plugin + self.startTiming() + self.folderPlugin.setDisplayVisibility(brainFolderItem, 0) + logging.info('Time of hiding whole brain: ' + str(self.stopTiming() / 1000) + ' s') + + # Check if a folder display node was indeed created when changing display property on the folder + self.assertEqual(1, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLFolderDisplayNode')) + brainFolderDisplayNode = self.shNode.GetItemDataNode(brainFolderItem) + self.assertIsNotNone(brainFolderDisplayNode) + + # Check model node hierarchy visibility + testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode) + self.assertFalse(testModelHierarchyVisibility) + + # Check if brain models were indeed hidden + numberOfVisibleModels = 0 + for modelNode in modelNodesList: + displayNode = modelNode.GetDisplayNode() + actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID()) + if actor.GetVisibility() > 0: + numberOfVisibleModels += 1 + self.assertEqual(3, numberOfVisibleModels) + + # Show folder again + self.startTiming() + self.folderPlugin.setDisplayVisibility(brainFolderItem, 1) + logging.info('Time of showing whole brain: ' + str(self.stopTiming() / 1000) + ' s') + + # Check number of visible models + numberOfVisibleModels = 0 + for modelNode in modelNodesList: + displayNode = modelNode.GetDisplayNode() + actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID()) + if actor.GetVisibility() > 0: + numberOfVisibleModels += 1 + self.assertEqual(225, numberOfVisibleModels) + + # Check model node hierarchy visibility + testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode) + self.assertTrue(testModelHierarchyVisibility) + + # ------------------------------------------------------------------------------ + def TestSection_ApplyDisplayPropertiesOnBranch(self): + # Test apply display properties on branch feature + logging.info('Test section: Apply display properties on branch') + + # Get folder that contains the midbrain + midbrainFolderItem = self.shNode.GetItemByName('midbrain') + self.assertNotEqual(midbrainFolderItem, 0) + + # Test simple color override, check actor color + overrideColorQt = qt.QColor(self.overrideColor[0], self.overrideColor[1], self.overrideColor[2]) + self.startTiming() + self.folderPlugin.setDisplayColor(midbrainFolderItem, overrideColorQt, {}) + logging.info('Time of setting override color on midbrain branch: ' + str(self.stopTiming() / 1000) + ' s') + + # Check number of models with overridden color + numberOfOverriddenMidbrainModels = 0 + testModelNodeOverridden = False + midbrainModelItems = vtk.vtkIdList() + self.shNode.GetItemChildren(midbrainFolderItem, midbrainModelItems, True) + for index in range(midbrainModelItems.GetNumberOfIds()): + currentMidbrainModelItem = midbrainModelItems.GetId(index) + currentMidbrainModelNode = self.shNode.GetItemDataNode(currentMidbrainModelItem) + if currentMidbrainModelNode: # The child item can be a folder as well, in which case there is no model node + displayNode = currentMidbrainModelNode.GetDisplayNode() + actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID()) + currentColor = actor.GetProperty().GetColor() + if (currentColor[0] == self.overrideColor[0] / 255 and + currentColor[1] == self.overrideColor[1] / 255 and + currentColor[2] == self.overrideColor[2] / 255): + if currentMidbrainModelNode is self.testModelNode: + testModelNodeOverridden = True + numberOfOverriddenMidbrainModels += 1 + self.assertEqual(6, numberOfOverriddenMidbrainModels) + self.assertTrue(testModelNodeOverridden) + + # Test hierarchy opacity + testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode) + self.assertEqual(testModelHierarchyOpacity, 1.0) + + midbrainFolderDisplayNode = self.shNode.GetItemDataNode(midbrainFolderItem) + self.assertIsNotNone(midbrainFolderDisplayNode) + midbrainFolderDisplayNode.SetOpacity(0.5) + + testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode) + self.assertEqual(testModelHierarchyOpacity, 0.5) + + brainFolderItem = self.shNode.GetItemByName('Brain') + self.assertNotEqual(brainFolderItem, 0) + brainFolderDisplayNode = self.shNode.GetItemDataNode(brainFolderItem) + self.assertIsNotNone(brainFolderDisplayNode) + brainFolderDisplayNode.SetOpacity(0.5) + + testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode) + self.assertEqual(testModelHierarchyOpacity, 0.25) + + # ------------------------------------------------------------------------------ + def TestSection_FolderDisplayOverrideAllowed(self): + # Test exclusion of a node from the apply display properties feature + logging.info('Test section: Disable apply display properties using FolderDisplayOverrideAllowed') + + testModelDisplayNode = self.testModelNode.GetDisplayNode() + self.assertTrue(testModelDisplayNode.GetFolderDisplayOverrideAllowed()) + + # Turn of override allowed + testModelDisplayNode.SetFolderDisplayOverrideAllowed(False) + + # Check that color and opacity are not overridden + testModelActor = self.modelDisplayableManager.GetActorByID(testModelDisplayNode.GetID()) + + testModelCurrentColor = testModelActor.GetProperty().GetColor() + colorOverridden = False + if (testModelCurrentColor[0] == self.overrideColor[0] / 255 and + testModelCurrentColor[1] == self.overrideColor[1] / 255 and + testModelCurrentColor[2] == self.overrideColor[2] / 255): + colorOverridden = True + self.assertFalse(colorOverridden) + + testModelCurrentOpacity = testModelActor.GetProperty().GetOpacity() + self.assertEqual(testModelCurrentOpacity, 1.0) + + # ------------------------------------------------------------------------------ + def startTiming(self): + self.timer = qt.QTime() + self.timer.start() + + # ------------------------------------------------------------------------------ + def stopTiming(self): + return self.timer.elapsed() + + # ------------------------------------------------------------------------------ + def getModelDisplayableManager(self): + if self.viewWidget is None: + logging.error('View widget is not created') + return None + managers = vtk.vtkCollection() + self.viewWidget.getDisplayableManagers(managers) + for i in range(managers.GetNumberOfItems()): + obj = managers.GetItemAsObject(i) + if obj.IsA('vtkMRMLModelDisplayableManager'): + return obj + logging.error('Failed to find the model displayable manager') + return None diff --git a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyGenericSelfTest.py b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyGenericSelfTest.py index 7e617717245..b566e53e016 100644 --- a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyGenericSelfTest.py +++ b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyGenericSelfTest.py @@ -15,32 +15,32 @@ # class SubjectHierarchyGenericSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "SubjectHierarchyGenericSelfTest" - parent.categories = ["Testing.TestCases"] - parent.dependencies = ["SubjectHierarchy", "DICOM"] - parent.contributors = ["Csaba Pinter (Queen's)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "SubjectHierarchyGenericSelfTest" + parent.categories = ["Testing.TestCases"] + parent.dependencies = ["SubjectHierarchy", "DICOM"] + parent.contributors = ["Csaba Pinter (Queen's)"] + parent.helpText = """ This is a self test for the Subject hierarchy module generic features. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was originally developed by Csaba Pinter, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care""" - self.parent = parent + self.parent = parent - # Add this test to the SelfTest module's list for discovery when the module - # is created. Since this module may be discovered before SelfTests itself, - # create the list if it doesn't already exist. - try: - slicer.selfTests - except AttributeError: - slicer.selfTests = {} - slicer.selfTests['SubjectHierarchyGenericSelfTest'] = self.runTest + # Add this test to the SelfTest module's list for discovery when the module + # is created. Since this module may be discovered before SelfTests itself, + # create the list if it doesn't already exist. + try: + slicer.selfTests + except AttributeError: + slicer.selfTests = {} + slicer.selfTests['SubjectHierarchyGenericSelfTest'] = self.runTest - def runTest(self, msec=100, **kwargs): - tester = SubjectHierarchyGenericSelfTestTest() - tester.runTest() + def runTest(self, msec=100, **kwargs): + tester = SubjectHierarchyGenericSelfTestTest() + tester.runTest() # @@ -48,8 +48,8 @@ def runTest(self, msec=100, **kwargs): # class SubjectHierarchyGenericSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -57,623 +57,623 @@ def setup(self): # class SubjectHierarchyGenericSelfTestLogic(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 - """ + """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 + """ - def __init__(self): - pass + def __init__(self): + pass class SubjectHierarchyGenericSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - def setUp(self): - """ Do whatever is needed to reset the state - typically a scene clear will be enough. """ - slicer.mrmlScene.Clear() - - self.delayMs = 700 - - #TODO: Comment out (sample code for debugging) - #logFile = open('d:/pyTestLog.txt', 'w') - #logFile.write(repr(slicer.modules.subjecthierarchygenericselftest) + '\n') - #logFile.write(repr(slicer.modules.subjecthierarchy) + '\n') - #logFile.close() - - def runTest(self): - """Run as few or as many tests as needed here. + This is the test case for your scripted module. """ - self.setUp() - self.test_SubjectHierarchyGenericSelfTest_FullTest1() - - # ------------------------------------------------------------------------------ - def test_SubjectHierarchyGenericSelfTest_FullTest1(self): - # Check for SubjectHierarchy module - self.assertIsNotNone( slicer.modules.subjecthierarchy ) - - # Switch to subject hierarchy module so that the changes can be seen as the test goes - slicer.util.selectModule('SubjectHierarchy') - - self.section_SetupPathsAndNames() - self.section_ClearScene() - self.section_LoadDicomDataWitchBatchProcessing() - self.section_SaveScene() - self.section_AddNodeToSubjectHierarchy() - self.section_CLI() - self.section_CreateSecondBranch() - self.section_ReparentNodeInSubjectHierarchy() - self.section_LoadScene() - self.section_TestCircularParenthood() - self.section_AttributeFilters() - self.section_ComboboxFeatures() - - logging.info('Test finished') - - # ------------------------------------------------------------------------------ - def section_SetupPathsAndNames(self): - # Set constants - subjectHierarchyGenericSelfTestDir = slicer.app.temporaryPath + '/SubjectHierarchyGenericSelfTest' - print('Test directory: ' + subjectHierarchyGenericSelfTestDir) - if not os.access(subjectHierarchyGenericSelfTestDir, os.F_OK): - os.mkdir(subjectHierarchyGenericSelfTestDir) - - self.dicomDataDir = subjectHierarchyGenericSelfTestDir + '/DicomData' - if not os.access(self.dicomDataDir, os.F_OK): - os.mkdir(self.dicomDataDir) - - self.dicomDatabaseDir = subjectHierarchyGenericSelfTestDir + '/CtkDicomDatabase' - self.dicomZipFileUrl = TESTING_DATA_URL + 'SHA256/1aa0bb177bbf6471ca5f2192340a6cecdedb81b33506b03ff316c6b5f624e863' - self.dicomZipChecksum = 'SHA256:1aa0bb177bbf6471ca5f2192340a6cecdedb81b33506b03ff316c6b5f624e863' - self.dicomZipFilePath = subjectHierarchyGenericSelfTestDir + '/TestDicomCT.zip' - self.expectedNumOfFilesInDicomDataDir = 10 - self.tempDir = subjectHierarchyGenericSelfTestDir + '/Temp' - self.genericTestSceneFileName = self.tempDir + '/SubjectHierarchyGenericSelfTestScene.mrml' - - self.attributeFilterTestSceneFileUrl = TESTING_DATA_URL + 'SHA256/83e0df42d178405dccaf5a87d0661dd4bad71b535c6f15457344a71c4c0b7984' - self.attributeFilterTestSceneChecksum = 'SHA256:83e0df42d178405dccaf5a87d0661dd4bad71b535c6f15457344a71c4c0b7984' - self.attributeFilterTestSceneFileName = 'SubjectHierarchyAttributeFilterTestScene.mrb' - - self.invalidItemID = slicer.vtkMRMLSubjectHierarchyNode.GetInvalidItemID() - - self.loadedDicomStudyName = 'No study description (20110101)' - self.loadedDicomVolumeName = '303: Unnamed Series' - self.patientItemID = self.invalidItemID # To be filled in after adding - self.patientOriginalName = '' - self.patientNewName = 'TestPatient_1' - self.studyItemID = self.invalidItemID - self.studyOriginalName = '' - self.studyNewName = 'No study description (20170107)' - self.ctVolumeShItemID = self.invalidItemID - self.ctVolumeOriginalName = '' - self.ctVolumeNewName = '404: Unnamed Series' - self.sampleLabelmapName = 'SampleLabelmap' - self.sampleLabelmapNode = None - self.sampleLabelmapShItemID = self.invalidItemID - self.sampleModelName = 'SampleModel' - self.sampleModelNode = None - self.sampleModelShItemID = self.invalidItemID - self.patient2Name = 'Patient2' - self.patient2ItemID = self.invalidItemID - self.study2Name = 'Study2' - self.study2ItemID = self.invalidItemID - self.folderName = 'Folder' - self.folderItemID = self.invalidItemID - - # ------------------------------------------------------------------------------ - def section_ClearScene(self): - self.delayDisplay("Clear scene",self.delayMs) - - # Clear the scene to make sure there is no crash (closing scene is a sensitive operation) - slicer.mrmlScene.Clear() - - # Make sure there is only one subject hierarchy node after closing the scene - self.assertEqual( slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSubjectHierarchyNode'), 1 ) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - # ------------------------------------------------------------------------------ - def section_LoadDicomDataWitchBatchProcessing(self): - try: - # Open Data module so that a subject hierarchy scene model is active - # (which caused problems with batch processing) - slicer.util.selectModule('Data') - - # Open test database and empty it - with DICOMUtils.TemporaryDICOMDatabase(self.dicomDatabaseDir) as db: - self.assertTrue( db.isOpen ) - self.assertEqual( slicer.dicomDatabase, db) - - slicer.mrmlScene.StartState(slicer.vtkMRMLScene.BatchProcessState) - - # Download, unzip, import, and load data. Verify loaded nodes. - loadedNodes = {'vtkMRMLScalarVolumeNode':1} - with DICOMUtils.LoadDICOMFilesToDatabase( \ - self.dicomZipFileUrl, self.dicomZipFilePath, \ - self.dicomDataDir, self.expectedNumOfFilesInDicomDataDir, \ - {}, loadedNodes, checksum=self.dicomZipChecksum) as success: - self.assertTrue(success) - - slicer.mrmlScene.EndState(slicer.vtkMRMLScene.BatchProcessState) - - self.assertEqual( len( slicer.util.getNodes('vtkMRMLSubjectHierarchyNode*') ), 1 ) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - loadedDicomVolumeItemID = shNode.GetItemByName(self.loadedDicomVolumeName) - loadedDicomStudyItemID = shNode.GetItemByName(self.loadedDicomStudyName) - self.assertEqual( shNode.GetItemParent(loadedDicomVolumeItemID), loadedDicomStudyItemID ) - - except Exception as e: - import traceback - traceback.print_exc() - self.delayDisplay('Test caused exception!\n' + str(e),self.delayMs*2) - - # ------------------------------------------------------------------------------ - def section_SaveScene(self): - self.delayDisplay("Save scene",self.delayMs) - - if not os.access(self.tempDir, os.F_OK): - os.mkdir(self.tempDir) - - if os.access(self.genericTestSceneFileName, os.F_OK): - os.remove(self.genericTestSceneFileName) - - # Save MRML scene into file - slicer.mrmlScene.Commit(self.genericTestSceneFileName) - logging.info('Scene saved into ' + self.genericTestSceneFileName) - - readable = os.access(self.genericTestSceneFileName, os.R_OK) - self.assertTrue( readable ) - - # ------------------------------------------------------------------------------ - def section_AddNodeToSubjectHierarchy(self): - self.delayDisplay("Add node to subject hierarchy",self.delayMs) - - # Get volume previously loaded from DICOM - volumeNodes = list(slicer.util.getNodes('vtkMRMLScalarVolumeNode*').values()) - ctVolumeNode = volumeNodes[len(volumeNodes)-1] - self.assertIsNotNone( ctVolumeNode ) - - # Create sample labelmap and model and add them in subject hierarchy - self.sampleLabelmapNode = self.createSampleLabelmapVolumeNode(ctVolumeNode, self.sampleLabelmapName, 2) - sampleModelColor = [0.0, 1.0, 0.0] - self.sampleModelNode = self.createSampleModelNode(self.sampleModelName, sampleModelColor, ctVolumeNode) - - # Get subject hierarchy scene model and node - dataWidget = slicer.modules.data.widgetRepresentation() - self.assertIsNotNone( dataWidget ) - shTreeView = slicer.util.findChild(dataWidget, name='SubjectHierarchyTreeView') - self.assertIsNotNone( shTreeView ) - shModel = shTreeView.model() - self.assertIsNotNone( shModel ) - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - # Get and check subject hierarchy items for the data nodes - self.ctVolumeShItemID = shNode.GetItemByDataNode(ctVolumeNode) - self.ctVolumeOriginalName = shNode.GetItemName(self.ctVolumeShItemID) - self.assertIsNotNone( self.ctVolumeShItemID ) - - self.sampleLabelmapShItemID = shNode.GetItemByDataNode(self.sampleLabelmapNode) - self.assertIsNotNone( self.sampleLabelmapShItemID ) - self.assertEqual( shNode.GetItemOwnerPluginName(self.sampleLabelmapShItemID), 'LabelMaps' ) - - self.sampleModelShItemID = shNode.GetItemByDataNode(self.sampleModelNode) - self.assertIsNotNone( self.sampleModelShItemID ) - self.assertEqual( shNode.GetItemOwnerPluginName(self.sampleModelShItemID), 'Models' ) - - # Save item IDs for scene load testing - self.studyItemID = shNode.GetItemParent(self.ctVolumeShItemID) - self.studyOriginalName = shNode.GetItemName(self.studyItemID) - self.assertIsNotNone( self.studyItemID ) - - self.patientItemID = shNode.GetItemParent(self.studyItemID) - self.patientOriginalName = shNode.GetItemName(self.patientItemID) - self.assertIsNotNone( self.patientItemID ) - - # Verify DICOM levels - self.assertEqual( shNode.GetItemLevel(self.patientItemID), slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelPatient() ) - self.assertEqual( shNode.GetItemLevel(self.studyItemID), slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy() ) - self.assertEqual( shNode.GetItemLevel(self.ctVolumeShItemID), "" ) - - # Add model and labelmap to the created study - retVal1 = shModel.reparent(self.sampleLabelmapShItemID, self.studyItemID) - self.assertTrue(retVal1) - retVal2 = shModel.reparent(self.sampleModelShItemID, self.studyItemID) - self.assertTrue(retVal2) - qt.QApplication.processEvents() - - # ------------------------------------------------------------------------------ - def section_CLI(self): - self.delayDisplay("Test command-line interface",self.delayMs) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - # Get CT volume - ctVolumeNode = shNode.GetItemDataNode(self.ctVolumeShItemID) - self.assertIsNotNone( ctVolumeNode ) - - # Create output volume - resampledVolumeNode = slicer.vtkMRMLScalarVolumeNode() - resampledVolumeNode.SetName(ctVolumeNode.GetName() + '_Resampled_10x10x10mm') - slicer.mrmlScene.AddNode(resampledVolumeNode) - - # Resample - resampleParameters = { 'outputPixelSpacing':'24.5,24.5,11.5', 'interpolationType':'lanczos', - 'InputVolume':ctVolumeNode.GetID(), 'OutputVolume':resampledVolumeNode.GetID() } - slicer.cli.run(slicer.modules.resamplescalarvolume, None, resampleParameters, wait_for_completion=True) - self.delayDisplay("Wait for CLI logic to add result to same branch",self.delayMs) - - # Check if output is also under the same study node - resampledVolumeItemID = shNode.GetItemByDataNode(resampledVolumeNode) - self.assertIsNotNone( resampledVolumeItemID ) - self.assertEqual( shNode.GetItemParent(resampledVolumeItemID), self.studyItemID ) - - # ------------------------------------------------------------------------------ - def section_CreateSecondBranch(self): - self.delayDisplay("Create second branch in subject hierarchy",self.delayMs) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - # Create second patient, study, and a folder - self.patient2ItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), self.patient2Name) - self.study2ItemID = shNode.CreateStudyItem(self.patient2ItemID, self.study2Name) - self.folderItemID = shNode.CreateFolderItem(self.study2ItemID, self.folderName) - - # Check if the items have the right parents - self.assertEqual( shNode.GetItemParent(self.patient2ItemID), shNode.GetSceneItemID() ) - self.assertEqual( shNode.GetItemParent(self.study2ItemID), self.patient2ItemID ) - self.assertEqual( shNode.GetItemParent(self.folderItemID), self.study2ItemID ) - - # ------------------------------------------------------------------------------ - def section_ReparentNodeInSubjectHierarchy(self): - self.delayDisplay("Reparent node in subject hierarchy",self.delayMs) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - # Get subject hierarchy scene model - dataWidget = slicer.modules.data.widgetRepresentation() - self.assertIsNotNone( dataWidget ) - shTreeView = slicer.util.findChild(dataWidget, name='SubjectHierarchyTreeView') - self.assertIsNotNone( shTreeView ) - shModel = shTreeView.model() - self.assertIsNotNone( shModel ) - - # Reparent using the item model - shModel.reparent(self.sampleLabelmapShItemID, self.studyItemID) - self.assertEqual( shNode.GetItemParent(self.sampleLabelmapShItemID), self.studyItemID ) - self.assertEqual( shNode.GetItemOwnerPluginName(self.sampleLabelmapShItemID), 'LabelMaps' ) - - # Reparent using the node's set parent function - shNode.SetItemParent(self.ctVolumeShItemID, self.study2ItemID) - self.assertEqual( shNode.GetItemParent(self.ctVolumeShItemID), self.study2ItemID ) - self.assertEqual( shNode.GetItemOwnerPluginName(self.ctVolumeShItemID), 'Volumes' ) - - # Reparent using the node's create item function - shNode.CreateItem(self.folderItemID, self.sampleModelNode) - self.assertEqual( shNode.GetItemParent(self.sampleModelShItemID), self.folderItemID ) - self.assertEqual( shNode.GetItemOwnerPluginName(self.sampleModelShItemID), 'Models' ) - - # ------------------------------------------------------------------------------ - def section_LoadScene(self): - self.delayDisplay("Load scene",self.delayMs) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - # Rename existing items so that when the scene is loaded again they are different - shNode.SetItemName(self.patientItemID, self.patientNewName) - shNode.SetItemName(self.studyItemID, self.studyNewName) - shNode.SetItemName(self.ctVolumeShItemID, self.ctVolumeNewName) - - # Load the saved scene - slicer.util.loadScene(self.genericTestSceneFileName) - - # Check number of nodes in the scene - self.assertEqual( slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLScalarVolumeNode'), 4 ) - self.assertEqual( slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelNode'), 4 ) # Including the three slice view models - self.assertEqual( slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSubjectHierarchyNode'), 1 ) - - # Check if the items are in the right hierarchy with the right names - self.assertEqual( shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patientNewName), self.patientItemID ) - self.assertEqual( shNode.GetItemChildWithName(self.patientItemID, self.studyNewName), self.studyItemID ) - self.assertEqual( shNode.GetItemChildWithName(self.studyItemID, self.sampleLabelmapName), self.sampleLabelmapShItemID ) - - self.assertEqual( shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patient2Name), self.patient2ItemID ) - self.assertEqual( shNode.GetItemChildWithName(self.patient2ItemID, self.study2Name), self.study2ItemID ) - self.assertEqual( shNode.GetItemChildWithName(self.study2ItemID, self.folderName), self.folderItemID ) - self.assertEqual( shNode.GetItemChildWithName(self.folderItemID, self.sampleModelName), self.sampleModelShItemID ) - self.assertEqual( shNode.GetItemChildWithName(self.study2ItemID, self.ctVolumeNewName), self.ctVolumeShItemID ) - - loadedPatientItemID = shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patientOriginalName) - self.assertIsNotNone( loadedPatientItemID ) - loadedStudyItemID = shNode.GetItemChildWithName(loadedPatientItemID, self.studyOriginalName) - self.assertIsNotNone( loadedStudyItemID ) - loadedCtVolumeShItemID = shNode.GetItemChildWithName(loadedStudyItemID, self.ctVolumeOriginalName) - self.assertIsNotNone( loadedCtVolumeShItemID ) - - # Print subject hierarchy after the test - logging.info(shNode) - - # ------------------------------------------------------------------------------ - def section_TestCircularParenthood(self): - # Test case for https://issues.slicer.org/view.php?id=4713 - self.delayDisplay("Test circular parenthood",self.delayMs) - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertIsNotNone( shNode ) - - sceneItemID = shNode.GetSceneItemID() - mainfolder_ID = shNode.CreateFolderItem(sceneItemID, "Main Folder") - subfolder_ID = shNode.CreateFolderItem(sceneItemID, "Sub Folder") - shNode.SetItemParent(subfolder_ID, mainfolder_ID) # Regular hierarchy setting - shNode.SetItemParent(mainfolder_ID, subfolder_ID) # Makes slicer crash instead of returning an error - - # ------------------------------------------------------------------------------ - def section_AttributeFilters(self): - self.delayDisplay("Attribute filters",self.delayMs) - - import SampleData - sceneFile = SampleData.downloadFromURL( - fileNames=self.attributeFilterTestSceneFileName, - uris=self.attributeFilterTestSceneFileUrl, - # loadFiles=True, - checksums=self.attributeFilterTestSceneChecksum)[0] - if not os.path.exists(sceneFile): - logging.error('Failed to download attribute filter test scene to path ' + str(sceneFile)) - self.assertTrue(os.path.exists(sceneFile)) - - slicer.mrmlScene.Clear() - ioManager = slicer.app.ioManager() - ioManager.loadFile(sceneFile) - - # The loaded scene contains the following items and data nodes - # - # Scene - # +----- NewFolder - # | +----------- MarkupsAngle (DataNode:vtkMRMLMarkupsAngleNode1) - # | | (ItemAttributes: ItemAttribute1:'1') - # | | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1') - # | +----------- MarkupsAngle_1 (DataNode:vtkMRMLMarkupsAngleNode2) - # | | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1') - # | +----------- MarkupsAngle_2 (DataNode:vtkMRMLMarkupsAngleNode3) - # | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1', ParentAttribute:'') - # | +----------- MarkupsAngle_2 (DataNode:vtkMRMLMarkupsAngleNode3) - # | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1', ChildAttribute:'') - # +----- NewFolder_1 - # | (ItemAttributes: FolderAttribute1:'1') - # | +----------- MarkupsCurve_1 (DataNode:vtkMRMLMarkupsCurveNode2) - # | | (NodeAttributes: Markups.MovingMarkupIndex:'3', Sajt:'Green') - # | +----------- MarkupsCurve (DataNode:vtkMRMLMarkupsCurveNode1) - # | (ItemAttributes: ItemAttribute2:'2') - # | (NodeAttributes: Markups.MovingMarkupIndex:'2', Sajt:'Green') - # +----- MarkupsCurve_2 (DataNode:vtkMRMLMarkupsCurveNode1) - # (NodeAttributes: Markups.MovingMarkupIndex:'3', Sajt:'Green') - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - - # Check scene validity - self.assertEqual(9, shNode.GetNumberOfItems()) - self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsNode'), 7) - - # Create test SH tree view - shTreeView = slicer.qMRMLSubjectHierarchyTreeView() - shTreeView.setMRMLScene(slicer.mrmlScene) - shTreeView.show() - - shProxyModel = shTreeView.sortFilterProxyModel() - - def testAttributeFilters(filteredObject, proxyModel): - # Check include node attribute name filter - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - filteredObject.includeNodeAttributeNamesFilter = ['Markups.MovingInSliceView'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5) - filteredObject.addNodeAttributeFilter('Sajt') - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - filteredObject.includeNodeAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - # Check attribute value filter - filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', 3) - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3) - filteredObject.includeNodeAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', '3') - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3) - filteredObject.includeNodeAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - - # Check exclude node attribute name filter (overrides include node attribute name filter) - filteredObject.excludeNodeAttributeNamesFilter = ['Markups.MovingInSliceView'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5) - filteredObject.excludeNodeAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - # Check if exclude indeed overrides include node attribute name filter - filteredObject.includeNodeAttributeNamesFilter = ['Markups.MovingMarkupIndex'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - filteredObject.excludeNodeAttributeNamesFilter = ['Markups.MovingInSliceView'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) - filteredObject.includeNodeAttributeNamesFilter = [] - filteredObject.excludeNodeAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - - # Check include item attribute name filter - filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2) - filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1', 'FolderAttribute1'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3) - filteredObject.addItemAttributeFilter('ItemAttribute2') - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) - filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1', 'ItemAttribute2'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) - filteredObject.includeItemAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - - # Check legacy (item) attribute value filter - filteredObject.attributeNameFilter = 'ItemAttribute1' - filteredObject.attributeValueFilter = '1' - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2) - filteredObject.attributeNameFilter = '' - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - - # Check exclude item attribute name filter (overrides include item attribute filter) - filteredObject.excludeItemAttributeNamesFilter = ['ItemAttribute1'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 8) - filteredObject.excludeItemAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - # Check if exclude indeed overrides include item attribute filter - filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2) - filteredObject.excludeItemAttributeNamesFilter = ['ItemAttribute1'] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 0) - filteredObject.includeItemAttributeNamesFilter = [] - filteredObject.excludeItemAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - filteredObject.excludeItemAttributeNamesFilter = ['FolderAttribute1'] - # Note: Shown only 6 because accepted children of rejected parents are not shown - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 8) - filteredObject.excludeItemAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - - # Check attribute filtering with class name and attribute value - filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', 3, True, 'vtkMRMLMarkupsCurveNode') - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3) - filteredObject.addNodeAttributeFilter('ParentAttribute', '', True, 'vtkMRMLMarkupsAngleNode') - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5) - filteredObject.addNodeAttributeFilter('ChildAttribute', '', True, 'vtkMRMLMarkupsAngleNode') - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 6) - filteredObject.includeNodeAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - # Check with empty attribute value - filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', '', True, 'vtkMRMLMarkupsCurveNode') - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) - filteredObject.includeNodeAttributeNamesFilter = [] - self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) - - logging.info('Test attribute filters on proxy model directly') - testAttributeFilters(shProxyModel, shProxyModel) - logging.info('Test attribute filters on tree view') - testAttributeFilters(shTreeView, shProxyModel) - - # ------------------------------------------------------------------------------ - def section_ComboboxFeatures(self): - self.delayDisplay("Combobox features",self.delayMs) - - comboBox = slicer.qMRMLSubjectHierarchyComboBox() - comboBox.setMRMLScene(slicer.mrmlScene) - comboBox.show() - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - self.assertEqual(comboBox.sortFilterProxyModel().acceptedItemCount(shNode.GetSceneItemID()), 9) - - # Enable None item, number of accepted SH items is the same (None does not have a corresponding accepted SH item) - comboBox.noneEnabled = True - self.assertEqual(comboBox.sortFilterProxyModel().acceptedItemCount(shNode.GetSceneItemID()), 9) - - # Default text - self.assertEqual(comboBox.defaultText, 'Select subject hierarchy item') - - # Select node, include parent names in current item text (when collapsed) - markupsCurve1ItemID = shNode.GetItemByName('MarkupsCurve_1') - comboBox.setCurrentItem(markupsCurve1ItemID) - self.assertEqual(comboBox.defaultText, 'NewFolder_1 / MarkupsCurve_1') - - # Select None item - comboBox.setCurrentItem(0) - self.assertEqual(comboBox.defaultText, comboBox.noneDisplay) - - # Do not show parent names in current item text - comboBox.showCurrentItemParents = False - comboBox.setCurrentItem(markupsCurve1ItemID) - self.assertEqual(comboBox.defaultText, 'MarkupsCurve_1') - - # Change None item name - comboBox.noneDisplay = 'No selection' - comboBox.setCurrentItem(0) - self.assertEqual(comboBox.defaultText, comboBox.noneDisplay) - - # ------------------------------------------------------------------------------ - # Utility functions - - # ------------------------------------------------------------------------------ - # Create sample labelmap with same geometry as input volume - def createSampleLabelmapVolumeNode(self, volumeNode, name, label, colorNode=None): - self.assertIsNotNone( volumeNode ) - self.assertTrue( volumeNode.IsA('vtkMRMLScalarVolumeNode') ) - self.assertTrue( label > 0 ) - - sampleLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() - sampleLabelmapNode.SetName(name) - sampleLabelmapNode = slicer.mrmlScene.AddNode(sampleLabelmapNode) - sampleLabelmapNode.Copy(volumeNode) - imageData = sampleLabelmapNode.GetImageData() - extent = imageData.GetExtent() - for x in range(extent[0], extent[1]+1): - for y in range(extent[2], extent[3]+1): - for z in range(extent[4], extent[5]+1): - if ( (x >= (extent[1]/4) and x <= (extent[1]/4) * 3) and - (y >= (extent[3]/4) and y <= (extent[3]/4) * 3) and - (z >= (extent[5]/4) and z <= (extent[5]/4) * 3) ): - imageData.SetScalarComponentFromDouble(x,y,z,0,label) - else: - imageData.SetScalarComponentFromDouble(x,y,z,0,0) - - # Display labelmap - labelmapVolumeDisplayNode = slicer.vtkMRMLLabelMapVolumeDisplayNode() - slicer.mrmlScene.AddNode(labelmapVolumeDisplayNode) - if colorNode is None: - colorNode = slicer.util.getNode('GenericAnatomyColors') - self.assertIsNotNone( colorNode ) - labelmapVolumeDisplayNode.SetAndObserveColorNodeID(colorNode.GetID()) - labelmapVolumeDisplayNode.VisibilityOn() - sampleLabelmapName = slicer.mrmlScene.GenerateUniqueName(name) - sampleLabelmapNode.SetName(sampleLabelmapName) - sampleLabelmapNode.SetAndObserveDisplayNodeID(labelmapVolumeDisplayNode.GetID()) - - return sampleLabelmapNode - - #------------------------------------------------------------------------------ - # Create sphere model at the centre of an input volume - def createSampleModelNode(self, name, color, volumeNode=None): - if volumeNode: - self.assertTrue( volumeNode.IsA('vtkMRMLScalarVolumeNode') ) - bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - volumeNode.GetRASBounds(bounds) - x = (bounds[0] + bounds[1])/2 - y = (bounds[2] + bounds[3])/2 - z = (bounds[4] + bounds[5])/2 - radius = min(bounds[1]-bounds[0],bounds[3]-bounds[2],bounds[5]-bounds[4]) / 3.0 - else: - radius = 50 - x = y = z = 0 - - # Taken from: https://mantisarchive.slicer.org/view.php?id=1536 - sphere = vtk.vtkSphereSource() - sphere.SetCenter(x, y, z) - sphere.SetRadius(radius) - - modelNode = slicer.vtkMRMLModelNode() - modelNode.SetName(name) - modelNode = slicer.mrmlScene.AddNode(modelNode) - modelNode.SetPolyDataConnection(sphere.GetOutputPort()) - modelNode.SetHideFromEditors(0) - - displayNode = slicer.vtkMRMLModelDisplayNode() - slicer.mrmlScene.AddNode(displayNode) - displayNode.Visibility2DOn() - displayNode.VisibilityOn() - displayNode.SetColor(color[0], color[1], color[2]) - modelNode.SetAndObserveDisplayNodeID(displayNode.GetID()) - - return modelNode + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear() + + self.delayMs = 700 + + # TODO: Comment out (sample code for debugging) + # logFile = open('d:/pyTestLog.txt', 'w') + # logFile.write(repr(slicer.modules.subjecthierarchygenericselftest) + '\n') + # logFile.write(repr(slicer.modules.subjecthierarchy) + '\n') + # logFile.close() + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_SubjectHierarchyGenericSelfTest_FullTest1() + + # ------------------------------------------------------------------------------ + def test_SubjectHierarchyGenericSelfTest_FullTest1(self): + # Check for SubjectHierarchy module + self.assertIsNotNone(slicer.modules.subjecthierarchy) + + # Switch to subject hierarchy module so that the changes can be seen as the test goes + slicer.util.selectModule('SubjectHierarchy') + + self.section_SetupPathsAndNames() + self.section_ClearScene() + self.section_LoadDicomDataWitchBatchProcessing() + self.section_SaveScene() + self.section_AddNodeToSubjectHierarchy() + self.section_CLI() + self.section_CreateSecondBranch() + self.section_ReparentNodeInSubjectHierarchy() + self.section_LoadScene() + self.section_TestCircularParenthood() + self.section_AttributeFilters() + self.section_ComboboxFeatures() + + logging.info('Test finished') + + # ------------------------------------------------------------------------------ + def section_SetupPathsAndNames(self): + # Set constants + subjectHierarchyGenericSelfTestDir = slicer.app.temporaryPath + '/SubjectHierarchyGenericSelfTest' + print('Test directory: ' + subjectHierarchyGenericSelfTestDir) + if not os.access(subjectHierarchyGenericSelfTestDir, os.F_OK): + os.mkdir(subjectHierarchyGenericSelfTestDir) + + self.dicomDataDir = subjectHierarchyGenericSelfTestDir + '/DicomData' + if not os.access(self.dicomDataDir, os.F_OK): + os.mkdir(self.dicomDataDir) + + self.dicomDatabaseDir = subjectHierarchyGenericSelfTestDir + '/CtkDicomDatabase' + self.dicomZipFileUrl = TESTING_DATA_URL + 'SHA256/1aa0bb177bbf6471ca5f2192340a6cecdedb81b33506b03ff316c6b5f624e863' + self.dicomZipChecksum = 'SHA256:1aa0bb177bbf6471ca5f2192340a6cecdedb81b33506b03ff316c6b5f624e863' + self.dicomZipFilePath = subjectHierarchyGenericSelfTestDir + '/TestDicomCT.zip' + self.expectedNumOfFilesInDicomDataDir = 10 + self.tempDir = subjectHierarchyGenericSelfTestDir + '/Temp' + self.genericTestSceneFileName = self.tempDir + '/SubjectHierarchyGenericSelfTestScene.mrml' + + self.attributeFilterTestSceneFileUrl = TESTING_DATA_URL + 'SHA256/83e0df42d178405dccaf5a87d0661dd4bad71b535c6f15457344a71c4c0b7984' + self.attributeFilterTestSceneChecksum = 'SHA256:83e0df42d178405dccaf5a87d0661dd4bad71b535c6f15457344a71c4c0b7984' + self.attributeFilterTestSceneFileName = 'SubjectHierarchyAttributeFilterTestScene.mrb' + + self.invalidItemID = slicer.vtkMRMLSubjectHierarchyNode.GetInvalidItemID() + + self.loadedDicomStudyName = 'No study description (20110101)' + self.loadedDicomVolumeName = '303: Unnamed Series' + self.patientItemID = self.invalidItemID # To be filled in after adding + self.patientOriginalName = '' + self.patientNewName = 'TestPatient_1' + self.studyItemID = self.invalidItemID + self.studyOriginalName = '' + self.studyNewName = 'No study description (20170107)' + self.ctVolumeShItemID = self.invalidItemID + self.ctVolumeOriginalName = '' + self.ctVolumeNewName = '404: Unnamed Series' + self.sampleLabelmapName = 'SampleLabelmap' + self.sampleLabelmapNode = None + self.sampleLabelmapShItemID = self.invalidItemID + self.sampleModelName = 'SampleModel' + self.sampleModelNode = None + self.sampleModelShItemID = self.invalidItemID + self.patient2Name = 'Patient2' + self.patient2ItemID = self.invalidItemID + self.study2Name = 'Study2' + self.study2ItemID = self.invalidItemID + self.folderName = 'Folder' + self.folderItemID = self.invalidItemID + + # ------------------------------------------------------------------------------ + def section_ClearScene(self): + self.delayDisplay("Clear scene", self.delayMs) + + # Clear the scene to make sure there is no crash (closing scene is a sensitive operation) + slicer.mrmlScene.Clear() + + # Make sure there is only one subject hierarchy node after closing the scene + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSubjectHierarchyNode'), 1) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + # ------------------------------------------------------------------------------ + def section_LoadDicomDataWitchBatchProcessing(self): + try: + # Open Data module so that a subject hierarchy scene model is active + # (which caused problems with batch processing) + slicer.util.selectModule('Data') + + # Open test database and empty it + with DICOMUtils.TemporaryDICOMDatabase(self.dicomDatabaseDir) as db: + self.assertTrue(db.isOpen) + self.assertEqual(slicer.dicomDatabase, db) + + slicer.mrmlScene.StartState(slicer.vtkMRMLScene.BatchProcessState) + + # Download, unzip, import, and load data. Verify loaded nodes. + loadedNodes = {'vtkMRMLScalarVolumeNode': 1} + with DICOMUtils.LoadDICOMFilesToDatabase( \ + self.dicomZipFileUrl, self.dicomZipFilePath, \ + self.dicomDataDir, self.expectedNumOfFilesInDicomDataDir, \ + {}, loadedNodes, checksum=self.dicomZipChecksum) as success: + self.assertTrue(success) + + slicer.mrmlScene.EndState(slicer.vtkMRMLScene.BatchProcessState) + + self.assertEqual(len(slicer.util.getNodes('vtkMRMLSubjectHierarchyNode*')), 1) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + loadedDicomVolumeItemID = shNode.GetItemByName(self.loadedDicomVolumeName) + loadedDicomStudyItemID = shNode.GetItemByName(self.loadedDicomStudyName) + self.assertEqual(shNode.GetItemParent(loadedDicomVolumeItemID), loadedDicomStudyItemID) + + except Exception as e: + import traceback + traceback.print_exc() + self.delayDisplay('Test caused exception!\n' + str(e), self.delayMs * 2) + + # ------------------------------------------------------------------------------ + def section_SaveScene(self): + self.delayDisplay("Save scene", self.delayMs) + + if not os.access(self.tempDir, os.F_OK): + os.mkdir(self.tempDir) + + if os.access(self.genericTestSceneFileName, os.F_OK): + os.remove(self.genericTestSceneFileName) + + # Save MRML scene into file + slicer.mrmlScene.Commit(self.genericTestSceneFileName) + logging.info('Scene saved into ' + self.genericTestSceneFileName) + + readable = os.access(self.genericTestSceneFileName, os.R_OK) + self.assertTrue(readable) + + # ------------------------------------------------------------------------------ + def section_AddNodeToSubjectHierarchy(self): + self.delayDisplay("Add node to subject hierarchy", self.delayMs) + + # Get volume previously loaded from DICOM + volumeNodes = list(slicer.util.getNodes('vtkMRMLScalarVolumeNode*').values()) + ctVolumeNode = volumeNodes[len(volumeNodes) - 1] + self.assertIsNotNone(ctVolumeNode) + + # Create sample labelmap and model and add them in subject hierarchy + self.sampleLabelmapNode = self.createSampleLabelmapVolumeNode(ctVolumeNode, self.sampleLabelmapName, 2) + sampleModelColor = [0.0, 1.0, 0.0] + self.sampleModelNode = self.createSampleModelNode(self.sampleModelName, sampleModelColor, ctVolumeNode) + + # Get subject hierarchy scene model and node + dataWidget = slicer.modules.data.widgetRepresentation() + self.assertIsNotNone(dataWidget) + shTreeView = slicer.util.findChild(dataWidget, name='SubjectHierarchyTreeView') + self.assertIsNotNone(shTreeView) + shModel = shTreeView.model() + self.assertIsNotNone(shModel) + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + # Get and check subject hierarchy items for the data nodes + self.ctVolumeShItemID = shNode.GetItemByDataNode(ctVolumeNode) + self.ctVolumeOriginalName = shNode.GetItemName(self.ctVolumeShItemID) + self.assertIsNotNone(self.ctVolumeShItemID) + + self.sampleLabelmapShItemID = shNode.GetItemByDataNode(self.sampleLabelmapNode) + self.assertIsNotNone(self.sampleLabelmapShItemID) + self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleLabelmapShItemID), 'LabelMaps') + + self.sampleModelShItemID = shNode.GetItemByDataNode(self.sampleModelNode) + self.assertIsNotNone(self.sampleModelShItemID) + self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleModelShItemID), 'Models') + + # Save item IDs for scene load testing + self.studyItemID = shNode.GetItemParent(self.ctVolumeShItemID) + self.studyOriginalName = shNode.GetItemName(self.studyItemID) + self.assertIsNotNone(self.studyItemID) + + self.patientItemID = shNode.GetItemParent(self.studyItemID) + self.patientOriginalName = shNode.GetItemName(self.patientItemID) + self.assertIsNotNone(self.patientItemID) + + # Verify DICOM levels + self.assertEqual(shNode.GetItemLevel(self.patientItemID), slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelPatient()) + self.assertEqual(shNode.GetItemLevel(self.studyItemID), slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy()) + self.assertEqual(shNode.GetItemLevel(self.ctVolumeShItemID), "") + + # Add model and labelmap to the created study + retVal1 = shModel.reparent(self.sampleLabelmapShItemID, self.studyItemID) + self.assertTrue(retVal1) + retVal2 = shModel.reparent(self.sampleModelShItemID, self.studyItemID) + self.assertTrue(retVal2) + qt.QApplication.processEvents() + + # ------------------------------------------------------------------------------ + def section_CLI(self): + self.delayDisplay("Test command-line interface", self.delayMs) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + # Get CT volume + ctVolumeNode = shNode.GetItemDataNode(self.ctVolumeShItemID) + self.assertIsNotNone(ctVolumeNode) + + # Create output volume + resampledVolumeNode = slicer.vtkMRMLScalarVolumeNode() + resampledVolumeNode.SetName(ctVolumeNode.GetName() + '_Resampled_10x10x10mm') + slicer.mrmlScene.AddNode(resampledVolumeNode) + + # Resample + resampleParameters = {'outputPixelSpacing': '24.5,24.5,11.5', 'interpolationType': 'lanczos', + 'InputVolume': ctVolumeNode.GetID(), 'OutputVolume': resampledVolumeNode.GetID()} + slicer.cli.run(slicer.modules.resamplescalarvolume, None, resampleParameters, wait_for_completion=True) + self.delayDisplay("Wait for CLI logic to add result to same branch", self.delayMs) + + # Check if output is also under the same study node + resampledVolumeItemID = shNode.GetItemByDataNode(resampledVolumeNode) + self.assertIsNotNone(resampledVolumeItemID) + self.assertEqual(shNode.GetItemParent(resampledVolumeItemID), self.studyItemID) + + # ------------------------------------------------------------------------------ + def section_CreateSecondBranch(self): + self.delayDisplay("Create second branch in subject hierarchy", self.delayMs) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + # Create second patient, study, and a folder + self.patient2ItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), self.patient2Name) + self.study2ItemID = shNode.CreateStudyItem(self.patient2ItemID, self.study2Name) + self.folderItemID = shNode.CreateFolderItem(self.study2ItemID, self.folderName) + + # Check if the items have the right parents + self.assertEqual(shNode.GetItemParent(self.patient2ItemID), shNode.GetSceneItemID()) + self.assertEqual(shNode.GetItemParent(self.study2ItemID), self.patient2ItemID) + self.assertEqual(shNode.GetItemParent(self.folderItemID), self.study2ItemID) + + # ------------------------------------------------------------------------------ + def section_ReparentNodeInSubjectHierarchy(self): + self.delayDisplay("Reparent node in subject hierarchy", self.delayMs) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + # Get subject hierarchy scene model + dataWidget = slicer.modules.data.widgetRepresentation() + self.assertIsNotNone(dataWidget) + shTreeView = slicer.util.findChild(dataWidget, name='SubjectHierarchyTreeView') + self.assertIsNotNone(shTreeView) + shModel = shTreeView.model() + self.assertIsNotNone(shModel) + + # Reparent using the item model + shModel.reparent(self.sampleLabelmapShItemID, self.studyItemID) + self.assertEqual(shNode.GetItemParent(self.sampleLabelmapShItemID), self.studyItemID) + self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleLabelmapShItemID), 'LabelMaps') + + # Reparent using the node's set parent function + shNode.SetItemParent(self.ctVolumeShItemID, self.study2ItemID) + self.assertEqual(shNode.GetItemParent(self.ctVolumeShItemID), self.study2ItemID) + self.assertEqual(shNode.GetItemOwnerPluginName(self.ctVolumeShItemID), 'Volumes') + + # Reparent using the node's create item function + shNode.CreateItem(self.folderItemID, self.sampleModelNode) + self.assertEqual(shNode.GetItemParent(self.sampleModelShItemID), self.folderItemID) + self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleModelShItemID), 'Models') + + # ------------------------------------------------------------------------------ + def section_LoadScene(self): + self.delayDisplay("Load scene", self.delayMs) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + # Rename existing items so that when the scene is loaded again they are different + shNode.SetItemName(self.patientItemID, self.patientNewName) + shNode.SetItemName(self.studyItemID, self.studyNewName) + shNode.SetItemName(self.ctVolumeShItemID, self.ctVolumeNewName) + + # Load the saved scene + slicer.util.loadScene(self.genericTestSceneFileName) + + # Check number of nodes in the scene + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLScalarVolumeNode'), 4) + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelNode'), 4) # Including the three slice view models + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSubjectHierarchyNode'), 1) + + # Check if the items are in the right hierarchy with the right names + self.assertEqual(shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patientNewName), self.patientItemID) + self.assertEqual(shNode.GetItemChildWithName(self.patientItemID, self.studyNewName), self.studyItemID) + self.assertEqual(shNode.GetItemChildWithName(self.studyItemID, self.sampleLabelmapName), self.sampleLabelmapShItemID) + + self.assertEqual(shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patient2Name), self.patient2ItemID) + self.assertEqual(shNode.GetItemChildWithName(self.patient2ItemID, self.study2Name), self.study2ItemID) + self.assertEqual(shNode.GetItemChildWithName(self.study2ItemID, self.folderName), self.folderItemID) + self.assertEqual(shNode.GetItemChildWithName(self.folderItemID, self.sampleModelName), self.sampleModelShItemID) + self.assertEqual(shNode.GetItemChildWithName(self.study2ItemID, self.ctVolumeNewName), self.ctVolumeShItemID) + + loadedPatientItemID = shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patientOriginalName) + self.assertIsNotNone(loadedPatientItemID) + loadedStudyItemID = shNode.GetItemChildWithName(loadedPatientItemID, self.studyOriginalName) + self.assertIsNotNone(loadedStudyItemID) + loadedCtVolumeShItemID = shNode.GetItemChildWithName(loadedStudyItemID, self.ctVolumeOriginalName) + self.assertIsNotNone(loadedCtVolumeShItemID) + + # Print subject hierarchy after the test + logging.info(shNode) + + # ------------------------------------------------------------------------------ + def section_TestCircularParenthood(self): + # Test case for https://issues.slicer.org/view.php?id=4713 + self.delayDisplay("Test circular parenthood", self.delayMs) + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertIsNotNone(shNode) + + sceneItemID = shNode.GetSceneItemID() + mainfolder_ID = shNode.CreateFolderItem(sceneItemID, "Main Folder") + subfolder_ID = shNode.CreateFolderItem(sceneItemID, "Sub Folder") + shNode.SetItemParent(subfolder_ID, mainfolder_ID) # Regular hierarchy setting + shNode.SetItemParent(mainfolder_ID, subfolder_ID) # Makes slicer crash instead of returning an error + + # ------------------------------------------------------------------------------ + def section_AttributeFilters(self): + self.delayDisplay("Attribute filters", self.delayMs) + + import SampleData + sceneFile = SampleData.downloadFromURL( + fileNames=self.attributeFilterTestSceneFileName, + uris=self.attributeFilterTestSceneFileUrl, + # loadFiles=True, + checksums=self.attributeFilterTestSceneChecksum)[0] + if not os.path.exists(sceneFile): + logging.error('Failed to download attribute filter test scene to path ' + str(sceneFile)) + self.assertTrue(os.path.exists(sceneFile)) + + slicer.mrmlScene.Clear() + ioManager = slicer.app.ioManager() + ioManager.loadFile(sceneFile) + + # The loaded scene contains the following items and data nodes + # + # Scene + # +----- NewFolder + # | +----------- MarkupsAngle (DataNode:vtkMRMLMarkupsAngleNode1) + # | | (ItemAttributes: ItemAttribute1:'1') + # | | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1') + # | +----------- MarkupsAngle_1 (DataNode:vtkMRMLMarkupsAngleNode2) + # | | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1') + # | +----------- MarkupsAngle_2 (DataNode:vtkMRMLMarkupsAngleNode3) + # | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1', ParentAttribute:'') + # | +----------- MarkupsAngle_2 (DataNode:vtkMRMLMarkupsAngleNode3) + # | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1', ChildAttribute:'') + # +----- NewFolder_1 + # | (ItemAttributes: FolderAttribute1:'1') + # | +----------- MarkupsCurve_1 (DataNode:vtkMRMLMarkupsCurveNode2) + # | | (NodeAttributes: Markups.MovingMarkupIndex:'3', Sajt:'Green') + # | +----------- MarkupsCurve (DataNode:vtkMRMLMarkupsCurveNode1) + # | (ItemAttributes: ItemAttribute2:'2') + # | (NodeAttributes: Markups.MovingMarkupIndex:'2', Sajt:'Green') + # +----- MarkupsCurve_2 (DataNode:vtkMRMLMarkupsCurveNode1) + # (NodeAttributes: Markups.MovingMarkupIndex:'3', Sajt:'Green') + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + + # Check scene validity + self.assertEqual(9, shNode.GetNumberOfItems()) + self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsNode'), 7) + + # Create test SH tree view + shTreeView = slicer.qMRMLSubjectHierarchyTreeView() + shTreeView.setMRMLScene(slicer.mrmlScene) + shTreeView.show() + + shProxyModel = shTreeView.sortFilterProxyModel() + + def testAttributeFilters(filteredObject, proxyModel): + # Check include node attribute name filter + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + filteredObject.includeNodeAttributeNamesFilter = ['Markups.MovingInSliceView'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5) + filteredObject.addNodeAttributeFilter('Sajt') + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + filteredObject.includeNodeAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + # Check attribute value filter + filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', 3) + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3) + filteredObject.includeNodeAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', '3') + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3) + filteredObject.includeNodeAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + + # Check exclude node attribute name filter (overrides include node attribute name filter) + filteredObject.excludeNodeAttributeNamesFilter = ['Markups.MovingInSliceView'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5) + filteredObject.excludeNodeAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + # Check if exclude indeed overrides include node attribute name filter + filteredObject.includeNodeAttributeNamesFilter = ['Markups.MovingMarkupIndex'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + filteredObject.excludeNodeAttributeNamesFilter = ['Markups.MovingInSliceView'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) + filteredObject.includeNodeAttributeNamesFilter = [] + filteredObject.excludeNodeAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + + # Check include item attribute name filter + filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2) + filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1', 'FolderAttribute1'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3) + filteredObject.addItemAttributeFilter('ItemAttribute2') + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) + filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1', 'ItemAttribute2'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) + filteredObject.includeItemAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + + # Check legacy (item) attribute value filter + filteredObject.attributeNameFilter = 'ItemAttribute1' + filteredObject.attributeValueFilter = '1' + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2) + filteredObject.attributeNameFilter = '' + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + + # Check exclude item attribute name filter (overrides include item attribute filter) + filteredObject.excludeItemAttributeNamesFilter = ['ItemAttribute1'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 8) + filteredObject.excludeItemAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + # Check if exclude indeed overrides include item attribute filter + filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2) + filteredObject.excludeItemAttributeNamesFilter = ['ItemAttribute1'] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 0) + filteredObject.includeItemAttributeNamesFilter = [] + filteredObject.excludeItemAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + filteredObject.excludeItemAttributeNamesFilter = ['FolderAttribute1'] + # Note: Shown only 6 because accepted children of rejected parents are not shown + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 8) + filteredObject.excludeItemAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + + # Check attribute filtering with class name and attribute value + filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', 3, True, 'vtkMRMLMarkupsCurveNode') + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3) + filteredObject.addNodeAttributeFilter('ParentAttribute', '', True, 'vtkMRMLMarkupsAngleNode') + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5) + filteredObject.addNodeAttributeFilter('ChildAttribute', '', True, 'vtkMRMLMarkupsAngleNode') + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 6) + filteredObject.includeNodeAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + # Check with empty attribute value + filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', '', True, 'vtkMRMLMarkupsCurveNode') + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4) + filteredObject.includeNodeAttributeNamesFilter = [] + self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9) + + logging.info('Test attribute filters on proxy model directly') + testAttributeFilters(shProxyModel, shProxyModel) + logging.info('Test attribute filters on tree view') + testAttributeFilters(shTreeView, shProxyModel) + + # ------------------------------------------------------------------------------ + def section_ComboboxFeatures(self): + self.delayDisplay("Combobox features", self.delayMs) + + comboBox = slicer.qMRMLSubjectHierarchyComboBox() + comboBox.setMRMLScene(slicer.mrmlScene) + comboBox.show() + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + self.assertEqual(comboBox.sortFilterProxyModel().acceptedItemCount(shNode.GetSceneItemID()), 9) + + # Enable None item, number of accepted SH items is the same (None does not have a corresponding accepted SH item) + comboBox.noneEnabled = True + self.assertEqual(comboBox.sortFilterProxyModel().acceptedItemCount(shNode.GetSceneItemID()), 9) + + # Default text + self.assertEqual(comboBox.defaultText, 'Select subject hierarchy item') + + # Select node, include parent names in current item text (when collapsed) + markupsCurve1ItemID = shNode.GetItemByName('MarkupsCurve_1') + comboBox.setCurrentItem(markupsCurve1ItemID) + self.assertEqual(comboBox.defaultText, 'NewFolder_1 / MarkupsCurve_1') + + # Select None item + comboBox.setCurrentItem(0) + self.assertEqual(comboBox.defaultText, comboBox.noneDisplay) + + # Do not show parent names in current item text + comboBox.showCurrentItemParents = False + comboBox.setCurrentItem(markupsCurve1ItemID) + self.assertEqual(comboBox.defaultText, 'MarkupsCurve_1') + + # Change None item name + comboBox.noneDisplay = 'No selection' + comboBox.setCurrentItem(0) + self.assertEqual(comboBox.defaultText, comboBox.noneDisplay) + + # ------------------------------------------------------------------------------ + # Utility functions + + # ------------------------------------------------------------------------------ + # Create sample labelmap with same geometry as input volume + def createSampleLabelmapVolumeNode(self, volumeNode, name, label, colorNode=None): + self.assertIsNotNone(volumeNode) + self.assertTrue(volumeNode.IsA('vtkMRMLScalarVolumeNode')) + self.assertTrue(label > 0) + + sampleLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() + sampleLabelmapNode.SetName(name) + sampleLabelmapNode = slicer.mrmlScene.AddNode(sampleLabelmapNode) + sampleLabelmapNode.Copy(volumeNode) + imageData = sampleLabelmapNode.GetImageData() + extent = imageData.GetExtent() + for x in range(extent[0], extent[1] + 1): + for y in range(extent[2], extent[3] + 1): + for z in range(extent[4], extent[5] + 1): + if ((x >= (extent[1] / 4) and x <= (extent[1] / 4) * 3) and + (y >= (extent[3] / 4) and y <= (extent[3] / 4) * 3) and + (z >= (extent[5] / 4) and z <= (extent[5] / 4) * 3)): + imageData.SetScalarComponentFromDouble(x, y, z, 0, label) + else: + imageData.SetScalarComponentFromDouble(x, y, z, 0, 0) + + # Display labelmap + labelmapVolumeDisplayNode = slicer.vtkMRMLLabelMapVolumeDisplayNode() + slicer.mrmlScene.AddNode(labelmapVolumeDisplayNode) + if colorNode is None: + colorNode = slicer.util.getNode('GenericAnatomyColors') + self.assertIsNotNone(colorNode) + labelmapVolumeDisplayNode.SetAndObserveColorNodeID(colorNode.GetID()) + labelmapVolumeDisplayNode.VisibilityOn() + sampleLabelmapName = slicer.mrmlScene.GenerateUniqueName(name) + sampleLabelmapNode.SetName(sampleLabelmapName) + sampleLabelmapNode.SetAndObserveDisplayNodeID(labelmapVolumeDisplayNode.GetID()) + + return sampleLabelmapNode + + # ------------------------------------------------------------------------------ + # Create sphere model at the centre of an input volume + def createSampleModelNode(self, name, color, volumeNode=None): + if volumeNode: + self.assertTrue(volumeNode.IsA('vtkMRMLScalarVolumeNode')) + bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + volumeNode.GetRASBounds(bounds) + x = (bounds[0] + bounds[1]) / 2 + y = (bounds[2] + bounds[3]) / 2 + z = (bounds[4] + bounds[5]) / 2 + radius = min(bounds[1] - bounds[0], bounds[3] - bounds[2], bounds[5] - bounds[4]) / 3.0 + else: + radius = 50 + x = y = z = 0 + + # Taken from: https://mantisarchive.slicer.org/view.php?id=1536 + sphere = vtk.vtkSphereSource() + sphere.SetCenter(x, y, z) + sphere.SetRadius(radius) + + modelNode = slicer.vtkMRMLModelNode() + modelNode.SetName(name) + modelNode = slicer.mrmlScene.AddNode(modelNode) + modelNode.SetPolyDataConnection(sphere.GetOutputPort()) + modelNode.SetHideFromEditors(0) + + displayNode = slicer.vtkMRMLModelDisplayNode() + slicer.mrmlScene.AddNode(displayNode) + displayNode.Visibility2DOn() + displayNode.VisibilityOn() + displayNode.SetColor(color[0], color[1], color[2]) + modelNode.SetAndObserveDisplayNodeID(displayNode.GetID()) + + return modelNode diff --git a/Modules/Loadable/SubjectHierarchy/Widgets/Python/AbstractScriptedSubjectHierarchyPlugin.py b/Modules/Loadable/SubjectHierarchy/Widgets/Python/AbstractScriptedSubjectHierarchyPlugin.py index fda608b6752..f0b696bd196 100644 --- a/Modules/Loadable/SubjectHierarchy/Widgets/Python/AbstractScriptedSubjectHierarchyPlugin.py +++ b/Modules/Loadable/SubjectHierarchy/Widgets/Python/AbstractScriptedSubjectHierarchyPlugin.py @@ -8,31 +8,31 @@ # class AbstractScriptedSubjectHierarchyPlugin: - """ Abstract scripted subject hierarchy plugin for python scripted plugins + """ Abstract scripted subject hierarchy plugin for python scripted plugins - USAGE: Instantiate scripted subject hierarchy plugin adaptor class from - module (e.g. from setup function), and set python source: + USAGE: Instantiate scripted subject hierarchy plugin adaptor class from + module (e.g. from setup function), and set python source: - from SubjectHierarchyPlugins import * - ... - class [Module]Widget(ScriptedLoadableModuleWidget): + from SubjectHierarchyPlugins import * ... - def setup(self): - ... - scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None) - scriptedPlugin.setPythonSource(VolumeClipSubjectHierarchyPlugin.filePath) + class [Module]Widget(ScriptedLoadableModuleWidget): ... + def setup(self): + ... + scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None) + scriptedPlugin.setPythonSource(VolumeClipSubjectHierarchyPlugin.filePath) + ... - Example can be found here: https://slicer.readthedocs.io/en/latest/developer_guide/script_repository.html#subject-hierarchy-plugin-offering-view-context-menu-action - """ + Example can be found here: https://slicer.readthedocs.io/en/latest/developer_guide/script_repository.html#subject-hierarchy-plugin-offering-view-context-menu-action + """ - def __init__(self, scriptedPlugin): - self.scriptedPlugin = scriptedPlugin + def __init__(self, scriptedPlugin): + self.scriptedPlugin = scriptedPlugin - # Register plugin on initialization - self.register() + # Register plugin on initialization + self.register() - def register(self): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - pluginHandlerSingleton.registerPlugin(self.scriptedPlugin) - logging.debug('Scripted subject hierarchy plugin registered: ' + self.scriptedPlugin.name) + def register(self): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + pluginHandlerSingleton.registerPlugin(self.scriptedPlugin) + logging.debug('Scripted subject hierarchy plugin registered: ' + self.scriptedPlugin.name) diff --git a/Modules/Loadable/SubjectHierarchy/Widgets/Python/__init__.py b/Modules/Loadable/SubjectHierarchy/Widgets/Python/__init__.py index 8f917e6b0a7..e9b1ef41755 100644 --- a/Modules/Loadable/SubjectHierarchy/Widgets/Python/__init__.py +++ b/Modules/Loadable/SubjectHierarchy/Widgets/Python/__init__.py @@ -6,11 +6,11 @@ currentDir = os.path.dirname(os.path.realpath(__file__)) sys.path.append(currentDir) for fileName in os.listdir(currentDir): - fileNameNoExtension = os.path.splitext(fileName)[0] - fileExtension = os.path.splitext(fileName)[1] - if fileExtension == '.py' and fileNameNoExtension != '__init__': - importStr = 'from ' + fileNameNoExtension + ' import *' - try: - exec(importStr) - except Exception as e: - logging.error('Failed to import ' + fileNameNoExtension + ': ' + traceback.format_exc()) + fileNameNoExtension = os.path.splitext(fileName)[0] + fileExtension = os.path.splitext(fileName)[1] + if fileExtension == '.py' and fileNameNoExtension != '__init__': + importStr = 'from ' + fileNameNoExtension + ' import *' + try: + exec(importStr) + except Exception as e: + logging.error('Failed to import ' + fileNameNoExtension + ': ' + traceback.format_exc()) diff --git a/Modules/Loadable/Tables/Testing/Python/TablesSelfTest.py b/Modules/Loadable/Tables/Testing/Python/TablesSelfTest.py index 23b820ffaa6..76df6772ac4 100644 --- a/Modules/Loadable/Tables/Testing/Python/TablesSelfTest.py +++ b/Modules/Loadable/Tables/Testing/Python/TablesSelfTest.py @@ -10,14 +10,14 @@ # class TablesSelfTest(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "TablesSelfTest" - self.parent.categories = ["Testing.TestCases"] - self.parent.dependencies = ["Tables"] - self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"] - self.parent.helpText = """This is a self test for Table node and widgets.""" - parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care""" + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "TablesSelfTest" + self.parent.categories = ["Testing.TestCases"] + self.parent.dependencies = ["Tables"] + self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"] + self.parent.helpText = """This is a self test for Table node and widgets.""" + parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care""" # @@ -25,8 +25,8 @@ def __init__(self, parent): # class TablesSelfTestWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) # @@ -34,254 +34,254 @@ def setup(self): # class TablesSelfTestLogic(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 - """ + """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 + """ - def __init__(self): - pass + def __init__(self): + pass class TablesSelfTestTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - 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. + This is the test case for your scripted module. """ - self.setUp() - self.test_TablesSelfTest_FullTest1() - - # ------------------------------------------------------------------------------ - def test_TablesSelfTest_FullTest1(self): - # Check for Tables module - self.assertTrue( slicer.modules.tables ) - - self.section_SetupPathsAndNames() - self.section_CreateTable() - self.section_TableProperties() - self.section_TableWidgetButtons() - self.section_CliTableInputOutput() - self.delayDisplay("Test passed") - - # ------------------------------------------------------------------------------ - def section_SetupPathsAndNames(self): - # Set constants - self.sampleTableName = 'SampleTable' - - # ------------------------------------------------------------------------------ - def section_CreateTable(self): - self.delayDisplay("Create table") - - # Create sample table node - tableNode = slicer.vtkMRMLTableNode() - slicer.mrmlScene.AddNode(tableNode) - tableNode.SetName(self.sampleTableName) - # Add a new column - column = tableNode.AddColumn() - self.assertTrue( column is not None ) - column.InsertNextValue("some") - column.InsertNextValue("data") - column.InsertNextValue("in this") - column.InsertNextValue("column") - tableNode.Modified() + 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_TablesSelfTest_FullTest1() + + # ------------------------------------------------------------------------------ + def test_TablesSelfTest_FullTest1(self): + # Check for Tables module + self.assertTrue(slicer.modules.tables) + + self.section_SetupPathsAndNames() + self.section_CreateTable() + self.section_TableProperties() + self.section_TableWidgetButtons() + self.section_CliTableInputOutput() + self.delayDisplay("Test passed") + + # ------------------------------------------------------------------------------ + def section_SetupPathsAndNames(self): + # Set constants + self.sampleTableName = 'SampleTable' + + # ------------------------------------------------------------------------------ + def section_CreateTable(self): + self.delayDisplay("Create table") + + # Create sample table node + tableNode = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(tableNode) + tableNode.SetName(self.sampleTableName) - # Check table - table = tableNode.GetTable() - self.assertTrue( table is not None ) - self.assertTrue( table.GetNumberOfRows() == 4 ) - self.assertTrue( table.GetNumberOfColumns() == 1 ) + # Add a new column + column = tableNode.AddColumn() + self.assertTrue(column is not None) + column.InsertNextValue("some") + column.InsertNextValue("data") + column.InsertNextValue("in this") + column.InsertNextValue("column") + tableNode.Modified() - # ------------------------------------------------------------------------------ - def section_TableProperties(self): - self.delayDisplay("Table properties") + # Check table + table = tableNode.GetTable() + self.assertTrue(table is not None) + self.assertTrue(table.GetNumberOfRows() == 4) + self.assertTrue(table.GetNumberOfColumns() == 1) - tableNode = slicer.util.getNode(self.sampleTableName) + # ------------------------------------------------------------------------------ + def section_TableProperties(self): + self.delayDisplay("Table properties") - tableNode.SetColumnLongName("Column 1", "First column") - tableNode.SetColumnUnitLabel("Column 1", "mm") - tableNode.SetColumnDescription("Column 1", "This a long description of the first column") + tableNode = slicer.util.getNode(self.sampleTableName) - tableNode.SetColumnUnitLabel("Column 2", "{SUVbw}g/ml") - tableNode.SetColumnDescription("Column 2", "Second column") + tableNode.SetColumnLongName("Column 1", "First column") + tableNode.SetColumnUnitLabel("Column 1", "mm") + tableNode.SetColumnDescription("Column 1", "This a long description of the first column") - # ------------------------------------------------------------------------------ - def section_TableWidgetButtons(self): - self.delayDisplay("Test widget buttons") + tableNode.SetColumnUnitLabel("Column 2", "{SUVbw}g/ml") + tableNode.SetColumnDescription("Column 2", "Second column") - slicer.util.selectModule('Tables') + # ------------------------------------------------------------------------------ + def section_TableWidgetButtons(self): + self.delayDisplay("Test widget buttons") - # Make sure subject hierarchy auto-creation is on for this test - tablesWidget = slicer.modules.tables.widgetRepresentation() - self.assertTrue( tablesWidget is not None ) + slicer.util.selectModule('Tables') - tableNode = slicer.util.getNode(self.sampleTableName) + # Make sure subject hierarchy auto-creation is on for this test + tablesWidget = slicer.modules.tables.widgetRepresentation() + self.assertTrue(tablesWidget is not None) - tablesWidget.setCurrentTableNode(tableNode) + tableNode = slicer.util.getNode(self.sampleTableName) - lockTableButton = slicer.util.findChildren(widget=tablesWidget,name='LockTableButton')[0] - copyButton = slicer.util.findChildren(widget=tablesWidget,name='CopyButton')[0] - pasteButton = slicer.util.findChildren(widget=tablesWidget,name='PasteButton')[0] - addRowButton = slicer.util.findChildren(widget=tablesWidget,name='RowInsertButton')[0] - deleteRowButton = slicer.util.findChildren(widget=tablesWidget,name='RowDeleteButton')[0] - lockFirstRowButton = slicer.util.findChildren(widget=tablesWidget,name='LockFirstRowButton')[0] - addColumnButton = slicer.util.findChildren(widget=tablesWidget,name='ColumnInsertButton')[0] - deleteColumnButton = slicer.util.findChildren(widget=tablesWidget,name='ColumnDeleteButton')[0] - lockFirstColumnButton = slicer.util.findChildren(widget=tablesWidget,name='LockFirstColumnButton')[0] - tableView = slicer.util.findChildren(widget=tablesWidget,name='TableView')[0] + tablesWidget.setCurrentTableNode(tableNode) - tableModel = tableView.model() + lockTableButton = slicer.util.findChildren(widget=tablesWidget, name='LockTableButton')[0] + copyButton = slicer.util.findChildren(widget=tablesWidget, name='CopyButton')[0] + pasteButton = slicer.util.findChildren(widget=tablesWidget, name='PasteButton')[0] + addRowButton = slicer.util.findChildren(widget=tablesWidget, name='RowInsertButton')[0] + deleteRowButton = slicer.util.findChildren(widget=tablesWidget, name='RowDeleteButton')[0] + lockFirstRowButton = slicer.util.findChildren(widget=tablesWidget, name='LockFirstRowButton')[0] + addColumnButton = slicer.util.findChildren(widget=tablesWidget, name='ColumnInsertButton')[0] + deleteColumnButton = slicer.util.findChildren(widget=tablesWidget, name='ColumnDeleteButton')[0] + lockFirstColumnButton = slicer.util.findChildren(widget=tablesWidget, name='LockFirstColumnButton')[0] + tableView = slicer.util.findChildren(widget=tablesWidget, name='TableView')[0] - initialNumberOfColumns = tableNode.GetNumberOfColumns() - initialNumberOfRows = tableNode.GetNumberOfRows() + tableModel = tableView.model() - ############# - self.delayDisplay("Test add rows/columns") + initialNumberOfColumns = tableNode.GetNumberOfColumns() + initialNumberOfRows = tableNode.GetNumberOfRows() - addRowButton.click() - self.assertTrue( tableNode.GetNumberOfRows() == initialNumberOfRows+1 ) + ############# + self.delayDisplay("Test add rows/columns") - addColumnButton.click() - self.assertTrue( tableNode.GetNumberOfColumns() == initialNumberOfColumns+1 ) + addRowButton.click() + self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows + 1) + + addColumnButton.click() + self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns + 1) - ############# - self.delayDisplay("Test lock first row/column") + ############# + self.delayDisplay("Test lock first row/column") - self.assertTrue( tableModel.data(tableModel.index(0,0)) == 'Column 1' ) - lockFirstRowButton.click() - self.assertTrue( tableModel.data(tableModel.index(0,0)) == 'some' ) - lockFirstColumnButton.click() - self.assertTrue( tableModel.data(tableModel.index(0,0)) == '' ) - lockFirstRowButton.click() - lockFirstColumnButton.click() - - ############# - self.delayDisplay("Test delete row/column") - - tableView.selectionModel().select(tableModel.index(1,1),qt.QItemSelectionModel.Select) # Select second item in second column - deleteColumnButton.click() - self.assertTrue( tableNode.GetNumberOfColumns() == initialNumberOfColumns ) - - tableView.selectionModel().select(tableModel.index(4,0),qt.QItemSelectionModel.Select) # Select 5th item in first column - deleteRowButton.click() - self.assertTrue( tableNode.GetNumberOfRows() == initialNumberOfRows ) - - ############# - self.delayDisplay("Test if buttons are disabled") - - lockTableButton.click() - - addRowButton.click() - self.assertTrue( tableNode.GetNumberOfRows() == initialNumberOfRows ) - - addColumnButton.click() - self.assertTrue( tableNode.GetNumberOfColumns() == initialNumberOfColumns ) - - tableView.selectionModel().select(tableView.model().index(0,0),qt.QItemSelectionModel.Select) - - deleteColumnButton.click() - self.assertTrue( tableNode.GetNumberOfColumns() == initialNumberOfColumns ) - - deleteRowButton.click() - self.assertTrue( tableNode.GetNumberOfRows() == initialNumberOfRows ) - - lockFirstRowButton.click() - self.assertTrue( tableModel.data(tableModel.index(0,0)) == 'Column 1' ) - - lockFirstColumnButton.click() - self.assertTrue( tableModel.data(tableModel.index(0,0)) == 'Column 1' ) - - lockTableButton.click() - - ############# - self.delayDisplay("Test copy/paste") - - tableView.selectColumn(0) - copyButton.click() - tableView.clearSelection() - - # Paste first column into a newly created second column - addColumnButton.click() - - tableView.setCurrentIndex(tableModel.index(0,1)) - pasteButton.click() - - # Check if first and second column content is the same - for rowIndex in range(5): - self.assertEqual( tableModel.data(tableModel.index(rowIndex,0)), tableModel.data(tableModel.index(rowIndex,1)) ) - - # ------------------------------------------------------------------------------ - def section_CliTableInputOutput(self): - self.delayDisplay("Test table writing and reading by CLI module") - - # Create input and output nodes - - inputTableNode = slicer.vtkMRMLTableNode() - slicer.mrmlScene.AddNode(inputTableNode) - inputTableNode.AddColumn() - inputTableNode.AddColumn() - inputTableNode.AddColumn() - inputTableNode.AddEmptyRow() - inputTableNode.AddEmptyRow() - inputTableNode.AddEmptyRow() - for row in range(3): - for col in range(3): - inputTableNode.SetCellText(row,col,str((row+1)*(col+1))) - inputTableNode.SetCellText(0,0,"source") - - outputTableNode = slicer.vtkMRMLTableNode() - slicer.mrmlScene.AddNode(outputTableNode) - - # Run CLI module - - self.delayDisplay("Run CLI module") - parameters = {} - parameters["arg0"] = self.createDummyVolume().GetID() - parameters["arg1"] = self.createDummyVolume().GetID() - parameters["transform1"] = self.createDummyTransform().GetID() - parameters["transform2"] = self.createDummyTransform().GetID() - parameters["inputDT"] = inputTableNode.GetID() - parameters["outputDT"] = outputTableNode.GetID() - slicer.cli.run(slicer.modules.executionmodeltour, None, parameters, wait_for_completion=True) - - # Verify the output table content - - self.delayDisplay("Verify results") - # the ExecutionModelTour module copies the input table to the output exxcept the first two rows - # of the first column, which is set to "Computed first" and "Computed second" strings - for row in range(3): - for col in range(3): - if row==0 and col==0: - self.assertTrue( outputTableNode.GetCellText(row, col) == "Computed first") - elif row==1 and col==0: - self.assertTrue( outputTableNode.GetCellText(row, col) == "Computed second") - else: - self.assertTrue( outputTableNode.GetCellText(row, col) == inputTableNode.GetCellText(row, col) ) - - def createDummyTransform(self): - transformNode = slicer.vtkMRMLLinearTransformNode() - slicer.mrmlScene.AddNode(transformNode) - return transformNode - - def createDummyVolume(self): - imageData = vtk.vtkImageData() - imageData.SetDimensions(10,10,10) - imageData.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) - volumeNode = slicer.vtkMRMLScalarVolumeNode() - volumeNode.SetAndObserveImageData(imageData) - displayNode = slicer.vtkMRMLScalarVolumeDisplayNode() - slicer.mrmlScene.AddNode(volumeNode) - slicer.mrmlScene.AddNode(displayNode) - volumeNode.SetAndObserveDisplayNodeID(displayNode.GetID()) - displayNode.SetAndObserveColorNodeID('vtkMRMLColorTableNodeGrey') - return volumeNode + self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'Column 1') + lockFirstRowButton.click() + self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'some') + lockFirstColumnButton.click() + self.assertTrue(tableModel.data(tableModel.index(0, 0)) == '') + lockFirstRowButton.click() + lockFirstColumnButton.click() + + ############# + self.delayDisplay("Test delete row/column") + + tableView.selectionModel().select(tableModel.index(1, 1), qt.QItemSelectionModel.Select) # Select second item in second column + deleteColumnButton.click() + self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns) + + tableView.selectionModel().select(tableModel.index(4, 0), qt.QItemSelectionModel.Select) # Select 5th item in first column + deleteRowButton.click() + self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows) + + ############# + self.delayDisplay("Test if buttons are disabled") + + lockTableButton.click() + + addRowButton.click() + self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows) + + addColumnButton.click() + self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns) + + tableView.selectionModel().select(tableView.model().index(0, 0), qt.QItemSelectionModel.Select) + + deleteColumnButton.click() + self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns) + + deleteRowButton.click() + self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows) + + lockFirstRowButton.click() + self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'Column 1') + + lockFirstColumnButton.click() + self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'Column 1') + + lockTableButton.click() + + ############# + self.delayDisplay("Test copy/paste") + + tableView.selectColumn(0) + copyButton.click() + tableView.clearSelection() + + # Paste first column into a newly created second column + addColumnButton.click() + + tableView.setCurrentIndex(tableModel.index(0, 1)) + pasteButton.click() + + # Check if first and second column content is the same + for rowIndex in range(5): + self.assertEqual(tableModel.data(tableModel.index(rowIndex, 0)), tableModel.data(tableModel.index(rowIndex, 1))) + + # ------------------------------------------------------------------------------ + def section_CliTableInputOutput(self): + self.delayDisplay("Test table writing and reading by CLI module") + + # Create input and output nodes + + inputTableNode = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(inputTableNode) + inputTableNode.AddColumn() + inputTableNode.AddColumn() + inputTableNode.AddColumn() + inputTableNode.AddEmptyRow() + inputTableNode.AddEmptyRow() + inputTableNode.AddEmptyRow() + for row in range(3): + for col in range(3): + inputTableNode.SetCellText(row, col, str((row + 1) * (col + 1))) + inputTableNode.SetCellText(0, 0, "source") + + outputTableNode = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(outputTableNode) + + # Run CLI module + + self.delayDisplay("Run CLI module") + parameters = {} + parameters["arg0"] = self.createDummyVolume().GetID() + parameters["arg1"] = self.createDummyVolume().GetID() + parameters["transform1"] = self.createDummyTransform().GetID() + parameters["transform2"] = self.createDummyTransform().GetID() + parameters["inputDT"] = inputTableNode.GetID() + parameters["outputDT"] = outputTableNode.GetID() + slicer.cli.run(slicer.modules.executionmodeltour, None, parameters, wait_for_completion=True) + + # Verify the output table content + + self.delayDisplay("Verify results") + # the ExecutionModelTour module copies the input table to the output exxcept the first two rows + # of the first column, which is set to "Computed first" and "Computed second" strings + for row in range(3): + for col in range(3): + if row == 0 and col == 0: + self.assertTrue(outputTableNode.GetCellText(row, col) == "Computed first") + elif row == 1 and col == 0: + self.assertTrue(outputTableNode.GetCellText(row, col) == "Computed second") + else: + self.assertTrue(outputTableNode.GetCellText(row, col) == inputTableNode.GetCellText(row, col)) + + def createDummyTransform(self): + transformNode = slicer.vtkMRMLLinearTransformNode() + slicer.mrmlScene.AddNode(transformNode) + return transformNode + + def createDummyVolume(self): + imageData = vtk.vtkImageData() + imageData.SetDimensions(10, 10, 10) + imageData.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) + volumeNode = slicer.vtkMRMLScalarVolumeNode() + volumeNode.SetAndObserveImageData(imageData) + displayNode = slicer.vtkMRMLScalarVolumeDisplayNode() + slicer.mrmlScene.AddNode(volumeNode) + slicer.mrmlScene.AddNode(displayNode) + volumeNode.SetAndObserveDisplayNodeID(displayNode.GetID()) + displayNode.SetAndObserveColorNodeID('vtkMRMLColorTableNodeGrey') + return volumeNode diff --git a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRenderingSceneClose.py b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRenderingSceneClose.py index c8673fbfc36..ee6008b3194 100644 --- a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRenderingSceneClose.py +++ b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRenderingSceneClose.py @@ -10,21 +10,21 @@ # class VolumeRenderingSceneClose(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) - parent.title = "VolumeRenderingSceneClose" - parent.categories = ["Testing.TestCases"] - parent.dependencies = [] - parent.contributors = ["Nicole Aucoin (BWH)"] - parent.helpText = """ + """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) + parent.title = "VolumeRenderingSceneClose" + parent.categories = ["Testing.TestCases"] + parent.dependencies = [] + parent.contributors = ["Nicole Aucoin (BWH)"] + parent.helpText = """ This is a scripted self test to check that scene close works while in the volume rendering module. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was contributed by Nicole Aucoin, BWH, and was partially funded by NIH grant 3P41RR013218-12S1. """ @@ -34,45 +34,45 @@ def __init__(self, parent): # class VolumeRenderingSceneCloseWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - # - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the algorithm." - self.applyButton.enabled = True - parametersFormLayout.addRow(self.applyButton) + # + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the algorithm." + self.applyButton.enabled = True + parametersFormLayout.addRow(self.applyButton) - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) - # Add vertical spacer - self.layout.addStretch(1) + # Add vertical spacer + self.layout.addStretch(1) - def cleanup(self): - pass + def cleanup(self): + pass - def onApplyButton(self): - logic = VolumeRenderingSceneCloseLogic() - print("Run the algorithm") - logic.run() + def onApplyButton(self): + logic = VolumeRenderingSceneCloseLogic() + print("Run the algorithm") + logic.run() # @@ -80,74 +80,74 @@ def onApplyButton(self): # class VolumeRenderingSceneCloseLogic(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 run(self): - """ - Run the actual algorithm + """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 """ - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) + def run(self): + """ + Run the actual algorithm + """ - slicer.util.delayDisplay('Running the aglorithm') + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView) - import SampleData - ctVolume = SampleData.downloadSample('CTChest') - slicer.util.delayDisplay('Downloaded CT sample data') + slicer.util.delayDisplay('Running the aglorithm') - # go to the volume rendering module - slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering') - slicer.util.delayDisplay('Volume Rendering module') + import SampleData + ctVolume = SampleData.downloadSample('CTChest') + slicer.util.delayDisplay('Downloaded CT sample data') - # turn it on - volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation() - volumeRenderingWidgetRep.setMRMLVolumeNode(ctVolume) - volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering') - volumeRenderingNode.SetVisibility(1) - slicer.util.delayDisplay('Volume Rendering') + # go to the volume rendering module + slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering') + slicer.util.delayDisplay('Volume Rendering module') - # set up a cropping ROI - volumeRenderingNode.SetCroppingEnabled(1) - markupsROI = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLMarkupsROINode') - slicer.util.delayDisplay('Cropping') + # turn it on + volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation() + volumeRenderingWidgetRep.setMRMLVolumeNode(ctVolume) + volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering') + volumeRenderingNode.SetVisibility(1) + slicer.util.delayDisplay('Volume Rendering') - # close the scene - slicer.mrmlScene.Clear(0) + # set up a cropping ROI + volumeRenderingNode.SetCroppingEnabled(1) + markupsROI = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLMarkupsROINode') + slicer.util.delayDisplay('Cropping') - return True + # close the scene + slicer.mrmlScene.Clear(0) + + return True class VolumeRenderingSceneCloseTest(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. + 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 """ - self.setUp() - self.test_VolumeRenderingSceneClose1() - def test_VolumeRenderingSceneClose1(self): + 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_VolumeRenderingSceneClose1() + + def test_VolumeRenderingSceneClose1(self): - self.delayDisplay("Starting the test") + self.delayDisplay("Starting the test") - logic = VolumeRenderingSceneCloseLogic() - logic.run() + logic = VolumeRenderingSceneCloseLogic() + logic.run() - self.delayDisplay('Test passed!') + self.delayDisplay('Test passed!') diff --git a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomenTutorial.py b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomenTutorial.py index d5f343e0217..49968a9f769 100644 --- a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomenTutorial.py +++ b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomenTutorial.py @@ -4,5 +4,5 @@ filepath = datapath.input + '/VolumeRendering_CTAbdomenTutorial.xml' testUtility = slicer.app.testingUtility() success = testUtility.playTests(filepath) -if not success : - raise Exception('Failed to finished properly the play back !') +if not success: + raise Exception('Failed to finished properly the play back !') diff --git a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomen_AppleTutorial.py b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomen_AppleTutorial.py index ae4a92e4abf..0141aedd046 100644 --- a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomen_AppleTutorial.py +++ b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomen_AppleTutorial.py @@ -4,5 +4,5 @@ filepath = datapath.input + '/VolumeRendering_CTAbdomen_AppleTutorial.xml' testUtility = slicer.app.testingUtility() success = testUtility.playTests(filepath) -if not success : - raise Exception('Failed to finished properly the play back !') +if not success: + raise Exception('Failed to finished properly the play back !') diff --git a/Modules/Loadable/Volumes/Testing/Python/LoadVolumeDisplaybleSceneModelClose.py b/Modules/Loadable/Volumes/Testing/Python/LoadVolumeDisplaybleSceneModelClose.py index 6f9beb06d1a..e9e5ec91c0f 100644 --- a/Modules/Loadable/Volumes/Testing/Python/LoadVolumeDisplaybleSceneModelClose.py +++ b/Modules/Loadable/Volumes/Testing/Python/LoadVolumeDisplaybleSceneModelClose.py @@ -4,31 +4,31 @@ class VolumesLoadSceneCloseTesting(ScriptedLoadableModuleTest): - def setUp(self): - pass - - def test_LoadVolumeCloseScene(self): - """ - Load a volume, go to a module that has a displayable scene model set for the tree view, then close the scene. - Tests the case of closing a scene with a displayable node in it while a GUI is up that is showing a tree view with a displayable scene model (display nodes are set to null during scene closing and can trigger events). - """ - self.delayDisplay("Starting the test") - - # - # first, get some sample data - # - import SampleData - SampleData.downloadSample("MRHead") - - # - # enter the models module - # - mainWindow = slicer.util.mainWindow() - mainWindow.moduleSelector().selectModule('Models') - - # - # close the scene - # - slicer.mrmlScene.Clear(0) - - self.delayDisplay('Test passed') + def setUp(self): + pass + + def test_LoadVolumeCloseScene(self): + """ + Load a volume, go to a module that has a displayable scene model set for the tree view, then close the scene. + Tests the case of closing a scene with a displayable node in it while a GUI is up that is showing a tree view with a displayable scene model (display nodes are set to null during scene closing and can trigger events). + """ + self.delayDisplay("Starting the test") + + # + # first, get some sample data + # + import SampleData + SampleData.downloadSample("MRHead") + + # + # enter the models module + # + mainWindow = slicer.util.mainWindow() + mainWindow.moduleSelector().selectModule('Models') + + # + # close the scene + # + slicer.mrmlScene.Clear(0) + + self.delayDisplay('Test passed') diff --git a/Modules/Loadable/Volumes/Testing/Python/VolumesLogicCompareVolumeGeometry.py b/Modules/Loadable/Volumes/Testing/Python/VolumesLogicCompareVolumeGeometry.py index eeb70657e61..70a1d995bee 100644 --- a/Modules/Loadable/Volumes/Testing/Python/VolumesLogicCompareVolumeGeometry.py +++ b/Modules/Loadable/Volumes/Testing/Python/VolumesLogicCompareVolumeGeometry.py @@ -6,118 +6,118 @@ class VolumesLogicCompareVolumeGeometryTesting(ScriptedLoadableModuleTest): - def setUp(self): - pass - - def test_VolumesLogicCompareVolumeGeometry(self): - """ - Load a volume, then call the compare volume geometry test with - different values of epsilon and precision. - """ - self.delayDisplay("Starting the test") - - # - # first, get some sample data - # - import SampleData - head = SampleData.downloadSample("MRHead") - - # - # get the volumes logic and print out default epsilon and precision - # - volumesLogic = slicer.modules.volumes.logic() - print('Compare volume geometry epsilon: ', volumesLogic.GetCompareVolumeGeometryEpsilon()) - print('Compare volume geometry precision: ', volumesLogic.GetCompareVolumeGeometryPrecision()) - self.assertAlmostEqual(volumesLogic.GetCompareVolumeGeometryEpsilon(), 1e-6) - self.assertEqual(volumesLogic.GetCompareVolumeGeometryPrecision(), 6) - - # - # compare the head against itself, this shouldn't produce any warning - # string - # - warningString = volumesLogic.CompareVolumeGeometry(head, head) - if len(warningString) != 0: - print('Error in checking MRHead geometry against itself') - print(warningString) - return False - else: - print('Success in comparing MRHead vs itself with epsilon',volumesLogic.GetCompareVolumeGeometryEpsilon()) - - # - # see if you can get it to fail with a tighter epsilon - # - volumesLogic.SetCompareVolumeGeometryEpsilon(1e-10) - precision = volumesLogic.GetCompareVolumeGeometryPrecision() - if precision != 10: - print('Error in calculating precision from epsilon of ', volumesLogic.GetCompareVolumeGeometryEpsilon(), ', expected 10, got ', precision) - return False - warningString = volumesLogic.CompareVolumeGeometry(head, head) - if len(warningString) != 0: - print('Error in checking MRHead geometry against itself with strict epsilon') - print(warningString) - return False - else: - print('Success in comparing MRHead vs itself with epsilon',volumesLogic.GetCompareVolumeGeometryEpsilon()) - - # - # clone the volume so can test for mismatches in geometry with - # that operation - # - head2 = volumesLogic.CloneVolume(head, 'head2') - - warningString = volumesLogic.CompareVolumeGeometry(head, head2) - if len(warningString) != 0: - print('Error in checking MRHead geometry against itself with epsilon ', volumesLogic.GetCompareVolumeGeometryEpsilon()) - print(warningString) - return False - else: - print('Success in comparing MRHead vs clone with epsilon',volumesLogic.GetCompareVolumeGeometryEpsilon()) - - # - # now try with a label map volume - # - headLabel = volumesLogic.CreateAndAddLabelVolume(head, "label vol") - warningString = volumesLogic.CompareVolumeGeometry(head, headLabel) - if len(warningString) != 0: - print('Error in comparing MRHead geometry against a label map of itself with epsilon',volumesLogic.GetCompareVolumeGeometryEpsilon()) - print(warningString) - return False - else: - print('Success in comparing MRHead vs label map with epsilon',volumesLogic.GetCompareVolumeGeometryEpsilon()) - - # - # adjust the geometry and make it fail - # - head2Matrix = vtk.vtkMatrix4x4() - head2.GetRASToIJKMatrix(head2Matrix) - val = head2Matrix.GetElement(2,0) - head2Matrix.SetElement(2,0,val+0.25) - head2.SetRASToIJKMatrix(head2Matrix) - head2.SetSpacing(0.12345678901234567890, 2.0, 3.4) - - warningString = volumesLogic.CompareVolumeGeometry(head,head2) - if len(warningString) == 0: - print('Error in comparing MRHead geometry against an updated clone, with epsilon',volumesLogic.GetCompareVolumeGeometryEpsilon()) - return False - else: - print('Success in making the comparison fail, with with epsilon',volumesLogic.GetCompareVolumeGeometryEpsilon()) - print(warningString) - - # - # reset the epsilon with an invalid negative number - # - volumesLogic.SetCompareVolumeGeometryEpsilon(-0.01) - epsilon = volumesLogic.GetCompareVolumeGeometryEpsilon() - if epsilon != 0.01: - print('Failed to use the absolute value for an epsilon of -0.01: ', epsilon) - return False - precision = volumesLogic.GetCompareVolumeGeometryPrecision() - if precision != 2: - print('Failed to set the precision to 2: ',precision) - return False - warningString = volumesLogic.CompareVolumeGeometry(head,head2) - print(warningString) - - self.delayDisplay('Test passed') - - return True + def setUp(self): + pass + + def test_VolumesLogicCompareVolumeGeometry(self): + """ + Load a volume, then call the compare volume geometry test with + different values of epsilon and precision. + """ + self.delayDisplay("Starting the test") + + # + # first, get some sample data + # + import SampleData + head = SampleData.downloadSample("MRHead") + + # + # get the volumes logic and print out default epsilon and precision + # + volumesLogic = slicer.modules.volumes.logic() + print('Compare volume geometry epsilon: ', volumesLogic.GetCompareVolumeGeometryEpsilon()) + print('Compare volume geometry precision: ', volumesLogic.GetCompareVolumeGeometryPrecision()) + self.assertAlmostEqual(volumesLogic.GetCompareVolumeGeometryEpsilon(), 1e-6) + self.assertEqual(volumesLogic.GetCompareVolumeGeometryPrecision(), 6) + + # + # compare the head against itself, this shouldn't produce any warning + # string + # + warningString = volumesLogic.CompareVolumeGeometry(head, head) + if len(warningString) != 0: + print('Error in checking MRHead geometry against itself') + print(warningString) + return False + else: + print('Success in comparing MRHead vs itself with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon()) + + # + # see if you can get it to fail with a tighter epsilon + # + volumesLogic.SetCompareVolumeGeometryEpsilon(1e-10) + precision = volumesLogic.GetCompareVolumeGeometryPrecision() + if precision != 10: + print('Error in calculating precision from epsilon of ', volumesLogic.GetCompareVolumeGeometryEpsilon(), ', expected 10, got ', precision) + return False + warningString = volumesLogic.CompareVolumeGeometry(head, head) + if len(warningString) != 0: + print('Error in checking MRHead geometry against itself with strict epsilon') + print(warningString) + return False + else: + print('Success in comparing MRHead vs itself with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon()) + + # + # clone the volume so can test for mismatches in geometry with + # that operation + # + head2 = volumesLogic.CloneVolume(head, 'head2') + + warningString = volumesLogic.CompareVolumeGeometry(head, head2) + if len(warningString) != 0: + print('Error in checking MRHead geometry against itself with epsilon ', volumesLogic.GetCompareVolumeGeometryEpsilon()) + print(warningString) + return False + else: + print('Success in comparing MRHead vs clone with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon()) + + # + # now try with a label map volume + # + headLabel = volumesLogic.CreateAndAddLabelVolume(head, "label vol") + warningString = volumesLogic.CompareVolumeGeometry(head, headLabel) + if len(warningString) != 0: + print('Error in comparing MRHead geometry against a label map of itself with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon()) + print(warningString) + return False + else: + print('Success in comparing MRHead vs label map with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon()) + + # + # adjust the geometry and make it fail + # + head2Matrix = vtk.vtkMatrix4x4() + head2.GetRASToIJKMatrix(head2Matrix) + val = head2Matrix.GetElement(2, 0) + head2Matrix.SetElement(2, 0, val + 0.25) + head2.SetRASToIJKMatrix(head2Matrix) + head2.SetSpacing(0.12345678901234567890, 2.0, 3.4) + + warningString = volumesLogic.CompareVolumeGeometry(head, head2) + if len(warningString) == 0: + print('Error in comparing MRHead geometry against an updated clone, with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon()) + return False + else: + print('Success in making the comparison fail, with with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon()) + print(warningString) + + # + # reset the epsilon with an invalid negative number + # + volumesLogic.SetCompareVolumeGeometryEpsilon(-0.01) + epsilon = volumesLogic.GetCompareVolumeGeometryEpsilon() + if epsilon != 0.01: + print('Failed to use the absolute value for an epsilon of -0.01: ', epsilon) + return False + precision = volumesLogic.GetCompareVolumeGeometryPrecision() + if precision != 2: + print('Failed to set the precision to 2: ', precision) + return False + warningString = volumesLogic.CompareVolumeGeometry(head, head2) + print(warningString) + + self.delayDisplay('Test passed') + + return True diff --git a/Modules/Scripted/CropVolumeSequence/CropVolumeSequence.py b/Modules/Scripted/CropVolumeSequence/CropVolumeSequence.py index 19808ef794e..6482567396e 100644 --- a/Modules/Scripted/CropVolumeSequence/CropVolumeSequence.py +++ b/Modules/Scripted/CropVolumeSequence/CropVolumeSequence.py @@ -13,19 +13,19 @@ # class CropVolumeSequence(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 = "Crop volume sequence" - self.parent.categories = ["Sequences"] - self.parent.dependencies = [] - self.parent.contributors = ["Andras Lasso (PerkLab, Queen's University)"] - self.parent.helpText = """This module can crop and resample a volume sequence to reduce its size for faster rendering and processing.""" - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = """ + """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 = "Crop volume sequence" + self.parent.categories = ["Sequences"] + self.parent.dependencies = [] + self.parent.contributors = ["Andras Lasso (PerkLab, Queen's University)"] + self.parent.helpText = """This module can crop and resample a volume sequence to reduce its size for faster rendering and processing.""" + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = """ This file was originally developed by Andras Lasso """ @@ -35,121 +35,121 @@ def __init__(self, parent): # class CropVolumeSequenceWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - # Instantiate and connect widgets ... - - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) - - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - - # - # input volume selector - # - self.inputSelector = slicer.qMRMLNodeComboBox() - self.inputSelector.nodeTypes = ["vtkMRMLSequenceNode"] - self.inputSelector.addEnabled = False - self.inputSelector.removeEnabled = False - self.inputSelector.noneEnabled = False - self.inputSelector.showHidden = False - self.inputSelector.showChildNodeTypes = False - self.inputSelector.setMRMLScene( slicer.mrmlScene ) - self.inputSelector.setToolTip( "Pick a sequence node of volumes that will be cropped and resampled." ) - parametersFormLayout.addRow("Input volume sequence: ", self.inputSelector) - - # - # output volume selector - # - self.outputSelector = slicer.qMRMLNodeComboBox() - self.outputSelector.nodeTypes = ["vtkMRMLSequenceNode"] - self.outputSelector.selectNodeUponCreation = True - self.outputSelector.addEnabled = True - self.outputSelector.removeEnabled = True - self.outputSelector.noneEnabled = True - self.outputSelector.noneDisplay = "(Overwrite input)" - self.outputSelector.showHidden = False - self.outputSelector.showChildNodeTypes = False - self.outputSelector.setMRMLScene( slicer.mrmlScene ) - self.outputSelector.setToolTip( "Pick a sequence node where the cropped and resampled volumes will be stored." ) - parametersFormLayout.addRow("Output volume sequence: ", self.outputSelector) - - # - # Crop parameters selector - # - self.cropParametersSelector = slicer.qMRMLNodeComboBox() - self.cropParametersSelector.nodeTypes = ["vtkMRMLCropVolumeParametersNode"] - self.cropParametersSelector.selectNodeUponCreation = True - self.cropParametersSelector.addEnabled = True - self.cropParametersSelector.removeEnabled = True - self.cropParametersSelector.renameEnabled = True - self.cropParametersSelector.noneEnabled = False - self.cropParametersSelector.showHidden = True - self.cropParametersSelector.showChildNodeTypes = False - self.cropParametersSelector.setMRMLScene( slicer.mrmlScene ) - self.cropParametersSelector.setToolTip("Select a crop volumes parameters.") - - self.editCropParametersButton = qt.QPushButton() - self.editCropParametersButton.setIcon(qt.QIcon(':Icons/Go.png')) - #self.editCropParametersButton.setMaximumWidth(60) - self.editCropParametersButton.enabled = True - self.editCropParametersButton.toolTip = "Go to Crop Volume module to edit cropping parameters." - hbox = qt.QHBoxLayout() - hbox.addWidget(self.cropParametersSelector) - hbox.addWidget(self.editCropParametersButton) - parametersFormLayout.addRow("Crop volume settings: ", hbox) - - # - # Apply Button - # - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run the algorithm." - self.applyButton.enabled = False - parametersFormLayout.addRow(self.applyButton) - - # connections - self.applyButton.connect('clicked(bool)', self.onApplyButton) - self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect) - self.cropParametersSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect) - self.editCropParametersButton.connect("clicked()", self.onEditCropParameters) - - # Add vertical spacer - self.layout.addStretch(1) - - # Refresh Apply button state - self.onSelect() - - def cleanup(self): - pass - - def onSelect(self): - self.applyButton.enabled = (self.inputSelector.currentNode() and self.cropParametersSelector.currentNode()) - - def onEditCropParameters(self): - if not self.cropParametersSelector.currentNode(): - cropParametersNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLCropVolumeParametersNode") - self.cropParametersSelector.setCurrentNode(cropParametersNode) - if self.inputSelector.currentNode(): - inputVolSeq = self.inputSelector.currentNode() - seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq) - inputVolume = seqBrowser.GetProxyNode(inputVolSeq) - if inputVolume: - self.cropParametersSelector.currentNode().SetInputVolumeNodeID(inputVolume.GetID()) - slicer.app.openNodeModule(self.cropParametersSelector.currentNode()) - - def onApplyButton(self): - logic = CropVolumeSequenceLogic() - logic.run(self.inputSelector.currentNode(), self.outputSelector.currentNode(), self.cropParametersSelector.currentNode()) + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + # Instantiate and connect widgets ... + + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) + + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + + # + # input volume selector + # + self.inputSelector = slicer.qMRMLNodeComboBox() + self.inputSelector.nodeTypes = ["vtkMRMLSequenceNode"] + self.inputSelector.addEnabled = False + self.inputSelector.removeEnabled = False + self.inputSelector.noneEnabled = False + self.inputSelector.showHidden = False + self.inputSelector.showChildNodeTypes = False + self.inputSelector.setMRMLScene(slicer.mrmlScene) + self.inputSelector.setToolTip("Pick a sequence node of volumes that will be cropped and resampled.") + parametersFormLayout.addRow("Input volume sequence: ", self.inputSelector) + + # + # output volume selector + # + self.outputSelector = slicer.qMRMLNodeComboBox() + self.outputSelector.nodeTypes = ["vtkMRMLSequenceNode"] + self.outputSelector.selectNodeUponCreation = True + self.outputSelector.addEnabled = True + self.outputSelector.removeEnabled = True + self.outputSelector.noneEnabled = True + self.outputSelector.noneDisplay = "(Overwrite input)" + self.outputSelector.showHidden = False + self.outputSelector.showChildNodeTypes = False + self.outputSelector.setMRMLScene(slicer.mrmlScene) + self.outputSelector.setToolTip("Pick a sequence node where the cropped and resampled volumes will be stored.") + parametersFormLayout.addRow("Output volume sequence: ", self.outputSelector) + + # + # Crop parameters selector + # + self.cropParametersSelector = slicer.qMRMLNodeComboBox() + self.cropParametersSelector.nodeTypes = ["vtkMRMLCropVolumeParametersNode"] + self.cropParametersSelector.selectNodeUponCreation = True + self.cropParametersSelector.addEnabled = True + self.cropParametersSelector.removeEnabled = True + self.cropParametersSelector.renameEnabled = True + self.cropParametersSelector.noneEnabled = False + self.cropParametersSelector.showHidden = True + self.cropParametersSelector.showChildNodeTypes = False + self.cropParametersSelector.setMRMLScene(slicer.mrmlScene) + self.cropParametersSelector.setToolTip("Select a crop volumes parameters.") + + self.editCropParametersButton = qt.QPushButton() + self.editCropParametersButton.setIcon(qt.QIcon(':Icons/Go.png')) + # self.editCropParametersButton.setMaximumWidth(60) + self.editCropParametersButton.enabled = True + self.editCropParametersButton.toolTip = "Go to Crop Volume module to edit cropping parameters." + hbox = qt.QHBoxLayout() + hbox.addWidget(self.cropParametersSelector) + hbox.addWidget(self.editCropParametersButton) + parametersFormLayout.addRow("Crop volume settings: ", hbox) + + # + # Apply Button + # + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run the algorithm." + self.applyButton.enabled = False + parametersFormLayout.addRow(self.applyButton) + + # connections + self.applyButton.connect('clicked(bool)', self.onApplyButton) + self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect) + self.cropParametersSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect) + self.editCropParametersButton.connect("clicked()", self.onEditCropParameters) + + # Add vertical spacer + self.layout.addStretch(1) + + # Refresh Apply button state + self.onSelect() + + def cleanup(self): + pass + + def onSelect(self): + self.applyButton.enabled = (self.inputSelector.currentNode() and self.cropParametersSelector.currentNode()) + + def onEditCropParameters(self): + if not self.cropParametersSelector.currentNode(): + cropParametersNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLCropVolumeParametersNode") + self.cropParametersSelector.setCurrentNode(cropParametersNode) + if self.inputSelector.currentNode(): + inputVolSeq = self.inputSelector.currentNode() + seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq) + inputVolume = seqBrowser.GetProxyNode(inputVolSeq) + if inputVolume: + self.cropParametersSelector.currentNode().SetInputVolumeNodeID(inputVolume.GetID()) + slicer.app.openNodeModule(self.cropParametersSelector.currentNode()) + + def onApplyButton(self): + logic = CropVolumeSequenceLogic() + logic.run(self.inputSelector.currentNode(), self.outputSelector.currentNode(), self.cropParametersSelector.currentNode()) # @@ -157,188 +157,188 @@ def onApplyButton(self): # class CropVolumeSequenceLogic(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 transformForSequence(self, volumeSeq): - seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(volumeSeq) - if not seqBrowser: - return None - proxyVolume = seqBrowser.GetProxyNode(volumeSeq) - if not proxyVolume: - return None - return proxyVolume.GetTransformNodeID() - - def run(self, inputVolSeq, outputVolSeq, cropParameters): - """ - Run the actual algorithm + """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 """ - logging.info('Processing started') - - # Get original parent transform, if any (before creating the new sequence browser) - inputVolTransformNodeID = self.transformForSequence(inputVolSeq) - outputVolTransformNodeID = None - - seqBrowser = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceBrowserNode") - seqBrowser.SetAndObserveMasterSequenceNodeID(inputVolSeq.GetID()) - seqBrowser.SetSaveChanges(inputVolSeq, True) # allow modifying node in the sequence - - seqBrowser.SetSelectedItemNumber(0) - slicer.modules.sequences.logic().UpdateAllProxyNodes() - slicer.app.processEvents() - inputVolume = seqBrowser.GetProxyNode(inputVolSeq) - inputVolume.SetAndObserveTransformNodeID(inputVolTransformNodeID) - cropParameters.SetInputVolumeNodeID(inputVolume.GetID()) - - if outputVolSeq == inputVolSeq: - outputVolSeq = None - - if outputVolSeq: - # Get original parent transform, if any (before erasing all the proxy nodes) - outputVolTransformNodeID = self.transformForSequence(outputVolSeq) - - # Initialize output sequence - outputVolSeq.RemoveAllDataNodes() - outputVolSeq.SetIndexType(inputVolSeq.GetIndexType()) - outputVolSeq.SetIndexName(inputVolSeq.GetIndexName()) - outputVolSeq.SetIndexUnit(inputVolSeq.GetIndexUnit()) - outputVolume = slicer.mrmlScene.AddNewNodeByClass(inputVolume.GetClassName()) - outputVolume.SetAndObserveTransformNodeID(outputVolTransformNodeID) - cropParameters.SetOutputVolumeNodeID(outputVolume.GetID()) - else: - outputVolume = None - cropParameters.SetOutputVolumeNodeID(inputVolume.GetID()) - - # Make sure we can record data into the output sequence is not overwritten by any browser nodes - browserNodesForOutputSequence = vtk.vtkCollection() - playSuspendedForBrowserNodes = [] - slicer.modules.sequences.logic().GetBrowserNodesForSequenceNode(outputVolSeq, browserNodesForOutputSequence) - for i in range(browserNodesForOutputSequence.GetNumberOfItems()): - browserNodeForOutputSequence = browserNodesForOutputSequence.GetItemAsObject(i) - if browserNodeForOutputSequence == seqBrowser: - continue - if browserNodeForOutputSequence.GetPlayback(outputVolSeq): - browserNodeForOutputSequence.SetPlayback(outputVolSeq, False) - playSuspendedForBrowserNodes.append(browserNodeForOutputSequence) - - try: - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - numberOfDataNodes = inputVolSeq.GetNumberOfDataNodes() - for seqItemNumber in range(numberOfDataNodes): - slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents) - seqBrowser.SetSelectedItemNumber(seqItemNumber) - slicer.modules.sequences.logic().UpdateProxyNodesFromSequences(seqBrowser) - slicer.modules.cropvolume.logic().Apply(cropParameters) - if outputVolSeq: - # Saved cropped result - outputVolSeq.SetDataNodeAtValue(outputVolume, inputVolSeq.GetNthIndexValue(seqItemNumber)) - - finally: - qt.QApplication.restoreOverrideCursor() - - # Temporary result node - if outputVolume: - slicer.mrmlScene.RemoveNode(outputVolume) - # Temporary input browser node - slicer.mrmlScene.RemoveNode(seqBrowser) - # Temporary input volume proxy node - slicer.mrmlScene.RemoveNode(inputVolume) - - # Move output sequence node in the same browser node as the input volume sequence - # if not in a sequence browser node already. - if outputVolSeq: - - if slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(outputVolSeq) is None: - # Add output sequence to a sequence browser - seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq) - if seqBrowser: - seqBrowser.AddSynchronizedSequenceNode(outputVolSeq) - else: - seqBrowser = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceBrowserNode") - seqBrowser.SetAndObserveMasterSequenceNodeID(outputVolSeq.GetID()) - seqBrowser.SetOverwriteProxyName(outputVolSeq, True) - - # Show output in slice views - slicer.modules.sequences.logic().UpdateAllProxyNodes() - slicer.app.processEvents() - outputVolume = seqBrowser.GetProxyNode(outputVolSeq) - outputVolume.SetAndObserveTransformNodeID(outputVolTransformNodeID) - slicer.util.setSliceViewerLayers(background=outputVolume) + def transformForSequence(self, volumeSeq): + seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(volumeSeq) + if not seqBrowser: + return None + proxyVolume = seqBrowser.GetProxyNode(volumeSeq) + if not proxyVolume: + return None + return proxyVolume.GetTransformNodeID() - else: - # Restore play enabled states - for playSuspendedForBrowserNode in playSuspendedForBrowserNodes: - playSuspendedForBrowserNode.SetPlayback(outputVolSeq, True) + def run(self, inputVolSeq, outputVolSeq, cropParameters): + """ + Run the actual algorithm + """ + + logging.info('Processing started') - else: - # Refresh proxy node - seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq) - slicer.modules.sequences.logic().UpdateProxyNodesFromSequences(seqBrowser) + # Get original parent transform, if any (before creating the new sequence browser) + inputVolTransformNodeID = self.transformForSequence(inputVolSeq) + outputVolTransformNodeID = None - logging.info('Processing completed') + seqBrowser = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceBrowserNode") + seqBrowser.SetAndObserveMasterSequenceNodeID(inputVolSeq.GetID()) + seqBrowser.SetSaveChanges(inputVolSeq, True) # allow modifying node in the sequence + + seqBrowser.SetSelectedItemNumber(0) + slicer.modules.sequences.logic().UpdateAllProxyNodes() + slicer.app.processEvents() + inputVolume = seqBrowser.GetProxyNode(inputVolSeq) + inputVolume.SetAndObserveTransformNodeID(inputVolTransformNodeID) + cropParameters.SetInputVolumeNodeID(inputVolume.GetID()) + + if outputVolSeq == inputVolSeq: + outputVolSeq = None + + if outputVolSeq: + # Get original parent transform, if any (before erasing all the proxy nodes) + outputVolTransformNodeID = self.transformForSequence(outputVolSeq) + + # Initialize output sequence + outputVolSeq.RemoveAllDataNodes() + outputVolSeq.SetIndexType(inputVolSeq.GetIndexType()) + outputVolSeq.SetIndexName(inputVolSeq.GetIndexName()) + outputVolSeq.SetIndexUnit(inputVolSeq.GetIndexUnit()) + outputVolume = slicer.mrmlScene.AddNewNodeByClass(inputVolume.GetClassName()) + outputVolume.SetAndObserveTransformNodeID(outputVolTransformNodeID) + cropParameters.SetOutputVolumeNodeID(outputVolume.GetID()) + else: + outputVolume = None + cropParameters.SetOutputVolumeNodeID(inputVolume.GetID()) + + # Make sure we can record data into the output sequence is not overwritten by any browser nodes + browserNodesForOutputSequence = vtk.vtkCollection() + playSuspendedForBrowserNodes = [] + slicer.modules.sequences.logic().GetBrowserNodesForSequenceNode(outputVolSeq, browserNodesForOutputSequence) + for i in range(browserNodesForOutputSequence.GetNumberOfItems()): + browserNodeForOutputSequence = browserNodesForOutputSequence.GetItemAsObject(i) + if browserNodeForOutputSequence == seqBrowser: + continue + if browserNodeForOutputSequence.GetPlayback(outputVolSeq): + browserNodeForOutputSequence.SetPlayback(outputVolSeq, False) + playSuspendedForBrowserNodes.append(browserNodeForOutputSequence) + + try: + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + numberOfDataNodes = inputVolSeq.GetNumberOfDataNodes() + for seqItemNumber in range(numberOfDataNodes): + slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents) + seqBrowser.SetSelectedItemNumber(seqItemNumber) + slicer.modules.sequences.logic().UpdateProxyNodesFromSequences(seqBrowser) + slicer.modules.cropvolume.logic().Apply(cropParameters) + if outputVolSeq: + # Saved cropped result + outputVolSeq.SetDataNodeAtValue(outputVolume, inputVolSeq.GetNthIndexValue(seqItemNumber)) + + finally: + qt.QApplication.restoreOverrideCursor() + + # Temporary result node + if outputVolume: + slicer.mrmlScene.RemoveNode(outputVolume) + # Temporary input browser node + slicer.mrmlScene.RemoveNode(seqBrowser) + # Temporary input volume proxy node + slicer.mrmlScene.RemoveNode(inputVolume) + + # Move output sequence node in the same browser node as the input volume sequence + # if not in a sequence browser node already. + if outputVolSeq: + + if slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(outputVolSeq) is None: + # Add output sequence to a sequence browser + seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq) + if seqBrowser: + seqBrowser.AddSynchronizedSequenceNode(outputVolSeq) + else: + seqBrowser = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceBrowserNode") + seqBrowser.SetAndObserveMasterSequenceNodeID(outputVolSeq.GetID()) + seqBrowser.SetOverwriteProxyName(outputVolSeq, True) + + # Show output in slice views + slicer.modules.sequences.logic().UpdateAllProxyNodes() + slicer.app.processEvents() + outputVolume = seqBrowser.GetProxyNode(outputVolSeq) + outputVolume.SetAndObserveTransformNodeID(outputVolTransformNodeID) + slicer.util.setSliceViewerLayers(background=outputVolume) + + else: + # Restore play enabled states + for playSuspendedForBrowserNode in playSuspendedForBrowserNodes: + playSuspendedForBrowserNode.SetPlayback(outputVolSeq, True) + + else: + # Refresh proxy node + seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq) + slicer.modules.sequences.logic().UpdateProxyNodesFromSequences(seqBrowser) + + logging.info('Processing completed') class CropVolumeSequenceTest(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. + 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 """ - self.setUp() - self.test_CropVolumeSequence1() - def test_CropVolumeSequence1(self): + 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_CropVolumeSequence1() + + def test_CropVolumeSequence1(self): - self.delayDisplay("Starting the test") + self.delayDisplay("Starting the test") - # Load volume sequence - import SampleData - sequenceNode = SampleData.downloadSample('CTCardioSeq') - sequenceBrowserNode = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(sequenceNode) + # Load volume sequence + import SampleData + sequenceNode = SampleData.downloadSample('CTCardioSeq') + sequenceBrowserNode = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(sequenceNode) - # Set cropping parameters - croppedSequenceNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSequenceNode') - cropVolumeNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLCropVolumeParametersNode') - cropVolumeNode.SetIsotropicResampling(True) - cropVolumeNode.SetSpacingScalingConst(3.0) - volumeNode = sequenceBrowserNode.GetProxyNode(sequenceNode) + # Set cropping parameters + croppedSequenceNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSequenceNode') + cropVolumeNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLCropVolumeParametersNode') + cropVolumeNode.SetIsotropicResampling(True) + cropVolumeNode.SetSpacingScalingConst(3.0) + volumeNode = sequenceBrowserNode.GetProxyNode(sequenceNode) - # Set cropping region - roiNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsROINode') - cropVolumeNode.SetROINodeID(roiNode.GetID()) - cropVolumeNode.SetInputVolumeNodeID(volumeNode.GetID()) - slicer.modules.cropvolume.logic().FitROIToInputVolume(cropVolumeNode) + # Set cropping region + roiNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsROINode') + cropVolumeNode.SetROINodeID(roiNode.GetID()) + cropVolumeNode.SetInputVolumeNodeID(volumeNode.GetID()) + slicer.modules.cropvolume.logic().FitROIToInputVolume(cropVolumeNode) - # Crop volume sequence - CropVolumeSequenceLogic().run(sequenceNode,croppedSequenceNode,cropVolumeNode) + # Crop volume sequence + CropVolumeSequenceLogic().run(sequenceNode, croppedSequenceNode, cropVolumeNode) - # Verify results + # Verify results - self.assertEqual(croppedSequenceNode.GetNumberOfDataNodes(), - sequenceNode.GetNumberOfDataNodes()) + self.assertEqual(croppedSequenceNode.GetNumberOfDataNodes(), + sequenceNode.GetNumberOfDataNodes()) - cropVolumeNode = sequenceBrowserNode.GetProxyNode(croppedSequenceNode) - self.assertIsNotNone(cropVolumeNode) + cropVolumeNode = sequenceBrowserNode.GetProxyNode(croppedSequenceNode) + self.assertIsNotNone(cropVolumeNode) - # We downsampled by a factor of 3 therefore the image size must be decreased by about factor of 3 - # (less along z axis due to anisotropic input volume and isotropic output volume) - self.assertEqual(volumeNode.GetImageData().GetExtent(), (0, 127, 0, 103, 0, 71)) - self.assertEqual(cropVolumeNode.GetImageData().GetExtent(), (0, 41, 0, 33, 0, 40)) + # We downsampled by a factor of 3 therefore the image size must be decreased by about factor of 3 + # (less along z axis due to anisotropic input volume and isotropic output volume) + self.assertEqual(volumeNode.GetImageData().GetExtent(), (0, 127, 0, 103, 0, 71)) + self.assertEqual(cropVolumeNode.GetImageData().GetExtent(), (0, 41, 0, 33, 0, 40)) - self.delayDisplay('Test passed!') + self.delayDisplay('Test passed!') diff --git a/Modules/Scripted/DICOM/DICOM.py b/Modules/Scripted/DICOM/DICOM.py index 38912e47144..db5b3e1fa05 100644 --- a/Modules/Scripted/DICOM/DICOM.py +++ b/Modules/Scripted/DICOM/DICOM.py @@ -22,536 +22,537 @@ class DICOM(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "DICOM" - self.parent.categories = ["", "Informatics"] # top level module - self.parent.contributors = ["Steve Pieper (Isomics)", "Andras Lasso (PerkLab)"] - self.parent.helpText = """ + self.parent.title = "DICOM" + self.parent.categories = ["", "Informatics"] # top level module + self.parent.contributors = ["Steve Pieper (Isomics)", "Andras Lasso (PerkLab)"] + self.parent.helpText = """ This module allows importing, loading, and exporting DICOM files, and sending receiving data using DICOM networking. """ - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = """ + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = """ This work is supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. """ - self.parent.icon = qt.QIcon(':Icons/Medium/SlicerLoadDICOM.png') - self.parent.dependencies = ["SubjectHierarchy"] - - self.viewWidget = None # Widget used in the layout manager (contains just label and browser widget) - self.browserWidget = None # SlicerDICOMBrowser instance (ctkDICOMBrowser with additional section for loading the selected items) - self.browserSettingsWidget = None - self.currentViewArrangement = 0 - # This variable is set to true if we temporarily - # hide the data probe (and so we need to restore its visibility). - self.dataProbeHasBeenTemporarilyHidden = False - - self.postModuleDiscoveryTasksPerformed = False - - def setup(self): - # Tasks to execute after the application has started up - slicer.app.connect("startupCompleted()", self.performPostModuleDiscoveryTasks) - slicer.app.connect("urlReceived(QString)", self.onURLReceived) - - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - pluginHandlerSingleton.registerPlugin(slicer.qSlicerSubjectHierarchyDICOMPlugin()) - - self.viewFactory = slicer.qSlicerSingletonViewFactory() - self.viewFactory.setTagName("dicombrowser") - if slicer.app.layoutManager() is not None: - slicer.app.layoutManager().registerViewFactory(self.viewFactory) - - def performPostModuleDiscoveryTasks(self): - """Since dicom plugins are discovered while the application - is initialized, they may be found after the DICOM module - itself if initialized. This method is tied to a singleShot - that will be called once the event loop is ready to start. - """ + self.parent.icon = qt.QIcon(':Icons/Medium/SlicerLoadDICOM.png') + self.parent.dependencies = ["SubjectHierarchy"] + + self.viewWidget = None # Widget used in the layout manager (contains just label and browser widget) + self.browserWidget = None # SlicerDICOMBrowser instance (ctkDICOMBrowser with additional section for loading the selected items) + self.browserSettingsWidget = None + self.currentViewArrangement = 0 + # This variable is set to true if we temporarily + # hide the data probe (and so we need to restore its visibility). + self.dataProbeHasBeenTemporarilyHidden = False - if self.postModuleDiscoveryTasksPerformed: - return - self.postModuleDiscoveryTasksPerformed = True - - if slicer.mrmlScene.GetTagByClassName( "vtkMRMLScriptedModuleNode" ) != 'ScriptedModule': - slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode()) - - self.initializeDICOMDatabase() - - settings = qt.QSettings() - if settings.contains('DICOM/RunListenerAtStart') and not slicer.app.commandOptions().testingEnabled: - if settings.value('DICOM/RunListenerAtStart') == 'true': - self.startListener() - - if not slicer.app.commandOptions().noMainWindow: - # add to the main app file menu - self.addMenu() - # add the settings options - self.settingsPanel = DICOMSettingsPanel() - slicer.app.settingsDialog().addPanel("DICOM", self.settingsPanel) - - layoutManager = slicer.app.layoutManager() - layoutManager.layoutChanged.connect(self.onLayoutChanged) - layout = ( - "" - " " - " " - " " - "" - ) - layoutNode = slicer.app.layoutManager().layoutLogic().GetLayoutNode() - layoutNode.AddLayoutDescription( - slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView, layout) - self.currentViewArrangement = layoutNode.GetViewArrangement() - self.previousViewArrangement = layoutNode.GetViewArrangement() - - slicer.app.moduleManager().connect( - 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded) - - def onURLReceived(self, urlString): - """Process DICOM view requests. Example: - slicer://viewer/?studyUID=2.16.840.1.113669.632.20.121711.10000158860 - &access_token=k0zR6WAPpNbVguQ8gGUHp6 - &dicomweb_endpoint=http%3A%2F%2Fdemo.kheops.online%2Fapi - &dicomweb_uri_endpoint=%20http%3A%2F%2Fdemo.kheops.online%2Fapi%2Fwado - """ + self.postModuleDiscoveryTasksPerformed = False + + def setup(self): + # Tasks to execute after the application has started up + slicer.app.connect("startupCompleted()", self.performPostModuleDiscoveryTasks) + slicer.app.connect("urlReceived(QString)", self.onURLReceived) + + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + pluginHandlerSingleton.registerPlugin(slicer.qSlicerSubjectHierarchyDICOMPlugin()) + + self.viewFactory = slicer.qSlicerSingletonViewFactory() + self.viewFactory.setTagName("dicombrowser") + if slicer.app.layoutManager() is not None: + slicer.app.layoutManager().registerViewFactory(self.viewFactory) + + def performPostModuleDiscoveryTasks(self): + """Since dicom plugins are discovered while the application + is initialized, they may be found after the DICOM module + itself if initialized. This method is tied to a singleShot + that will be called once the event loop is ready to start. + """ + + if self.postModuleDiscoveryTasksPerformed: + return + self.postModuleDiscoveryTasksPerformed = True + + if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != 'ScriptedModule': + slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode()) + + self.initializeDICOMDatabase() + + settings = qt.QSettings() + if settings.contains('DICOM/RunListenerAtStart') and not slicer.app.commandOptions().testingEnabled: + if settings.value('DICOM/RunListenerAtStart') == 'true': + self.startListener() + + if not slicer.app.commandOptions().noMainWindow: + slicer.util.mainWindow().findChild(qt.QAction, "LoadDICOMAction").setVisible(True) + # add to the main app file menu + self.addMenu() + # add the settings options + self.settingsPanel = DICOMSettingsPanel() + slicer.app.settingsDialog().addPanel("DICOM", self.settingsPanel) + + layoutManager = slicer.app.layoutManager() + layoutManager.layoutChanged.connect(self.onLayoutChanged) + layout = ( + "" + " " + " " + " " + "" + ) + layoutNode = slicer.app.layoutManager().layoutLogic().GetLayoutNode() + layoutNode.AddLayoutDescription( + slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView, layout) + self.currentViewArrangement = layoutNode.GetViewArrangement() + self.previousViewArrangement = layoutNode.GetViewArrangement() + + slicer.app.moduleManager().connect( + 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded) + + def onURLReceived(self, urlString): + """Process DICOM view requests. Example: + slicer://viewer/?studyUID=2.16.840.1.113669.632.20.121711.10000158860 + &access_token=k0zR6WAPpNbVguQ8gGUHp6 + &dicomweb_endpoint=http%3A%2F%2Fdemo.kheops.online%2Fapi + &dicomweb_uri_endpoint=%20http%3A%2F%2Fdemo.kheops.online%2Fapi%2Fwado + """ + + url = qt.QUrl(urlString) + if (url.authority().lower() != "viewer"): + logging.debug("DICOM module ignores non-viewer URL: " + urlString) + return + query = qt.QUrlQuery(url) + queryMap = {} + for key, value in query.queryItems(qt.QUrl.FullyDecoded): + queryMap[key] = qt.QUrl.fromPercentEncoding(value) + + if "dicomweb_endpoint" not in queryMap: + logging.debug("DICOM module ignores URL without dicomweb_endpoint query parameter: " + urlString) + return + if "studyUID" not in queryMap: + logging.debug("DICOM module ignores URL without studyUID query parameter: " + urlString) + return + + logging.info("DICOM module received URL: " + urlString) + + accessToken = None + if "access_token" in queryMap: + accessToken = queryMap["access_token"] + + slicer.util.selectModule("DICOM") + slicer.app.processEvents() + from DICOMLib import DICOMUtils + importedSeriesInstanceUIDs = DICOMUtils.importFromDICOMWeb( + dicomWebEndpoint=queryMap["dicomweb_endpoint"], + studyInstanceUID=queryMap["studyUID"], + accessToken=accessToken) + + # Select newly loaded items to make it easier to load them + self.browserWidget.dicomBrowser.setSelectedItems(ctk.ctkDICOMModel.SeriesType, importedSeriesInstanceUIDs) + + def initializeDICOMDatabase(self): + # Create alias for convenience + slicer.dicomDatabase = slicer.app.dicomDatabase() + + # Set the dicom pre-cache tags once all plugin classes have been initialized. + # Pre-caching tags is very important for fast DICOM loading because tags that are + # not pre-cached during DICOM import in bulk, will be cached during Examine step one-by-one + # (which takes magnitudes more time). + tagsToPrecache = list(slicer.dicomDatabase.tagsToPrecache) + for pluginClass in slicer.modules.dicomPlugins: + plugin = slicer.modules.dicomPlugins[pluginClass]() + tagsToPrecache += list(plugin.tags.values()) + tagsToPrecache = sorted(set(tagsToPrecache)) # remove duplicates + slicer.dicomDatabase.tagsToPrecache = tagsToPrecache + + # Try to initialize the database using the location stored in settings + if slicer.app.commandOptions().testingEnabled: + # For automatic tests (use a separate DICOM database for testing) + slicer.dicomDatabaseDirectorySettingsKey = 'DatabaseDirectoryTest_' + ctk.ctkDICOMDatabase().schemaVersion() + databaseDirectory = os.path.join(slicer.app.temporaryPath, + 'temp' + slicer.app.applicationName + 'DICOMDatabase_' + ctk.ctkDICOMDatabase().schemaVersion()) + else: + # For production + slicer.dicomDatabaseDirectorySettingsKey = 'DatabaseDirectory_' + ctk.ctkDICOMDatabase().schemaVersion() + settings = qt.QSettings() + databaseDirectory = settings.value(slicer.dicomDatabaseDirectorySettingsKey) + if not databaseDirectory: + documentsLocation = qt.QStandardPaths.DocumentsLocation + documents = qt.QStandardPaths.writableLocation(documentsLocation) + databaseDirectory = os.path.join(documents, slicer.app.applicationName + "DICOMDatabase") + settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, databaseDirectory) + + # Attempt to open the database. If it fails then user will have to configure it using DICOM module. + databaseFileName = databaseDirectory + "/ctkDICOM.sql" + slicer.dicomDatabase.openDatabase(databaseFileName) + if slicer.dicomDatabase.isOpen: + # There is an existing database at the current location + if slicer.dicomDatabase.schemaVersionLoaded() != slicer.dicomDatabase.schemaVersion(): + # Schema does not match, do not use it + slicer.dicomDatabase.closeDatabase() + + def startListener(self): + + if not slicer.dicomDatabase.isOpen: + logging.error("Failed to start DICOM listener. DICOM database is not open.") + return False - url = qt.QUrl(urlString) - if (url.authority().lower() != "viewer"): - logging.debug("DICOM module ignores non-viewer URL: "+urlString) - return - query = qt.QUrlQuery(url) - queryMap = {} - for key, value in query.queryItems(qt.QUrl.FullyDecoded): - queryMap[key] = qt.QUrl.fromPercentEncoding(value) - - if not "dicomweb_endpoint" in queryMap: - logging.debug("DICOM module ignores URL without dicomweb_endpoint query parameter: "+urlString) - return - if not "studyUID" in queryMap: - logging.debug("DICOM module ignores URL without studyUID query parameter: "+urlString) - return - - logging.info("DICOM module received URL: "+urlString) - - accessToken = None - if "access_token" in queryMap: - accessToken = queryMap["access_token"] - - slicer.util.selectModule("DICOM") - slicer.app.processEvents() - from DICOMLib import DICOMUtils - importedSeriesInstanceUIDs = DICOMUtils.importFromDICOMWeb( - dicomWebEndpoint=queryMap["dicomweb_endpoint"], - studyInstanceUID=queryMap["studyUID"], - accessToken=accessToken) - - # Select newly loaded items to make it easier to load them - self.browserWidget.dicomBrowser.setSelectedItems(ctk.ctkDICOMModel.SeriesType, importedSeriesInstanceUIDs) - - def initializeDICOMDatabase(self): - # Create alias for convenience - slicer.dicomDatabase = slicer.app.dicomDatabase() - - # Set the dicom pre-cache tags once all plugin classes have been initialized. - # Pre-caching tags is very important for fast DICOM loading because tags that are - # not pre-cached during DICOM import in bulk, will be cached during Examine step one-by-one - # (which takes magnitudes more time). - tagsToPrecache = list(slicer.dicomDatabase.tagsToPrecache) - for pluginClass in slicer.modules.dicomPlugins: - plugin = slicer.modules.dicomPlugins[pluginClass]() - tagsToPrecache += list(plugin.tags.values()) - tagsToPrecache = sorted(set(tagsToPrecache)) # remove duplicates - slicer.dicomDatabase.tagsToPrecache = tagsToPrecache - - # Try to initialize the database using the location stored in settings - if slicer.app.commandOptions().testingEnabled: - # For automatic tests (use a separate DICOM database for testing) - slicer.dicomDatabaseDirectorySettingsKey = 'DatabaseDirectoryTest_'+ctk.ctkDICOMDatabase().schemaVersion() - databaseDirectory = os.path.join(slicer.app.temporaryPath, - 'temp'+slicer.app.applicationName+'DICOMDatabase_'+ctk.ctkDICOMDatabase().schemaVersion()) - else: - # For production - slicer.dicomDatabaseDirectorySettingsKey = 'DatabaseDirectory_'+ctk.ctkDICOMDatabase().schemaVersion() - settings = qt.QSettings() - databaseDirectory = settings.value(slicer.dicomDatabaseDirectorySettingsKey) - if not databaseDirectory: - documentsLocation = qt.QStandardPaths.DocumentsLocation - documents = qt.QStandardPaths.writableLocation(documentsLocation) - databaseDirectory = os.path.join(documents, slicer.app.applicationName+"DICOMDatabase") - settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, databaseDirectory) - - # Attempt to open the database. If it fails then user will have to configure it using DICOM module. - databaseFileName = databaseDirectory + "/ctkDICOM.sql" - slicer.dicomDatabase.openDatabase(databaseFileName) - if slicer.dicomDatabase.isOpen: - # There is an existing database at the current location - if slicer.dicomDatabase.schemaVersionLoaded() != slicer.dicomDatabase.schemaVersion(): - # Schema does not match, do not use it - slicer.dicomDatabase.closeDatabase() - - def startListener(self): - - if not slicer.dicomDatabase.isOpen: - logging.error("Failed to start DICOM listener. DICOM database is not open.") - return False - - if not hasattr(slicer, 'dicomListener'): - dicomListener = DICOMLib.DICOMListener(slicer.dicomDatabase) - - try: - dicomListener.start() - except (UserWarning, OSError) as message: - logging.error('Problem trying to start DICOM listener:\n %s' % message) - return False - if not dicomListener.process: - logging.error("Failed to start DICOM listener. Process start failed.") - return False - slicer.dicomListener = dicomListener - logging.info("DICOM C-Store SCP service started at port "+str(slicer.dicomListener.port)) - - def stopListener(self): - if hasattr(slicer, 'dicomListener'): - logging.info("DICOM C-Store SCP service stopping") - slicer.dicomListener.stop() - del slicer.dicomListener - - def addMenu(self): - """Add an action to the File menu that will go into - the DICOM module by selecting the module. Note that - once the module is constructed (below in setup) another - connection is made that will also cause the instance-created - DICOM browser to be raised by this menu action""" - a = self.parent.action() - a.setText(a.tr("Add DICOM Data")) - fileMenu = slicer.util.lookupTopLevelWidget('FileMenu') - if fileMenu: - for child in fileMenu.children(): - if child.objectName == "RecentlyLoadedMenu": - fileMenu.insertAction(child.menuAction(), a) # insert action before RecentlyLoadedMenu - - def setBrowserWidgetInDICOMLayout(self, browserWidget): - """Set DICOM browser widget in the custom view layout""" - if self.browserWidget == browserWidget: - return - - if self.browserWidget is not None: - self.browserWidget.closed.disconnect(self.onBrowserWidgetClosed) - - oldBrowserWidget = self.browserWidget - self.browserWidget = browserWidget - self.browserWidget.setAutoFillBackground(True) - if slicer.util.mainWindow(): - # For some reason, we cannot disconnect this event connection if - # main window, and not disconnecting would cause crash on application shutdown, - # so we only connect when main window is present. - self.browserWidget.closed.connect(self.onBrowserWidgetClosed) - - if self.viewWidget is None: - self.viewWidget = qt.QWidget() - self.viewWidget.setAutoFillBackground(True) - self.viewFactory.setWidget(self.viewWidget) - layout = qt.QVBoxLayout() - self.viewWidget.setLayout(layout) - - label = qt.QLabel("DICOM database") - label.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - layout.addWidget(label) - font = qt.QFont() - font.setBold(True) - font.setPointSize(12) - label.setFont(font) - - if oldBrowserWidget is not None: - self.viewWidget.layout().removeWidget(oldBrowserWidget) - - if self.browserWidget: - self.viewWidget.layout().addWidget(self.browserWidget) - - def onLayoutChanged(self, viewArrangement): - if viewArrangement == self.currentViewArrangement: - return - - if (self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutNone and - self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView): - self.previousViewArrangement = self.currentViewArrangement - self.currentViewArrangement = viewArrangement - - if self.browserWidget is None: - return - mw = slicer.util.mainWindow() - dataProbe = mw.findChild("QWidget", "DataProbeCollapsibleWidget") if mw else None - if self.currentViewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView: - # View has been changed to the DICOM browser view - self.browserWidget.show() - # If we are in DICOM module, hide the Data Probe to have more space for the module - try: - inDicomModule = slicer.modules.dicom.widgetRepresentation().isEntered - except AttributeError: - # Slicer is shutting down - inDicomModule = False - if inDicomModule and dataProbe and dataProbe.isVisible(): - dataProbe.setVisible(False) - self.dataProbeHasBeenTemporarilyHidden = True - else: - # View has been changed from the DICOM browser view - if self.dataProbeHasBeenTemporarilyHidden: - # DataProbe was temporarily hidden, restore its visibility now - dataProbe.setVisible(True) - self.dataProbeHasBeenTemporarilyHidden = False + if not hasattr(slicer, 'dicomListener'): + dicomListener = DICOMLib.DICOMListener(slicer.dicomDatabase) - def onBrowserWidgetClosed(self): - if (self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView and - self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutNone): - # current layout is a valid layout that is not the DICOM browser view, so nothing to do - return + try: + dicomListener.start() + except (UserWarning, OSError) as message: + logging.error('Problem trying to start DICOM listener:\n %s' % message) + return False + if not dicomListener.process: + logging.error("Failed to start DICOM listener. Process start failed.") + return False + slicer.dicomListener = dicomListener + logging.info("DICOM C-Store SCP service started at port " + str(slicer.dicomListener.port)) + + def stopListener(self): + if hasattr(slicer, 'dicomListener'): + logging.info("DICOM C-Store SCP service stopping") + slicer.dicomListener.stop() + del slicer.dicomListener + + def addMenu(self): + """Add an action to the File menu that will go into + the DICOM module by selecting the module. Note that + once the module is constructed (below in setup) another + connection is made that will also cause the instance-created + DICOM browser to be raised by this menu action""" + a = self.parent.action() + a.setText(a.tr("Add DICOM Data")) + fileMenu = slicer.util.lookupTopLevelWidget('FileMenu') + if fileMenu: + for child in fileMenu.children(): + if child.objectName == "RecentlyLoadedMenu": + fileMenu.insertAction(child.menuAction(), a) # insert action before RecentlyLoadedMenu + + def setBrowserWidgetInDICOMLayout(self, browserWidget): + """Set DICOM browser widget in the custom view layout""" + if self.browserWidget == browserWidget: + return + + if self.browserWidget is not None: + self.browserWidget.closed.disconnect(self.onBrowserWidgetClosed) + + oldBrowserWidget = self.browserWidget + self.browserWidget = browserWidget + self.browserWidget.setAutoFillBackground(True) + if slicer.util.mainWindow(): + # For some reason, we cannot disconnect this event connection if + # main window, and not disconnecting would cause crash on application shutdown, + # so we only connect when main window is present. + self.browserWidget.closed.connect(self.onBrowserWidgetClosed) + + if self.viewWidget is None: + self.viewWidget = qt.QWidget() + self.viewWidget.setAutoFillBackground(True) + self.viewFactory.setWidget(self.viewWidget) + layout = qt.QVBoxLayout() + self.viewWidget.setLayout(layout) + + label = qt.QLabel("DICOM database") + label.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) + layout.addWidget(label) + font = qt.QFont() + font.setBold(True) + font.setPointSize(12) + label.setFont(font) + + if oldBrowserWidget is not None: + self.viewWidget.layout().removeWidget(oldBrowserWidget) + + if self.browserWidget: + self.viewWidget.layout().addWidget(self.browserWidget) + + def onLayoutChanged(self, viewArrangement): + if viewArrangement == self.currentViewArrangement: + return + + if (self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutNone and + self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView): + self.previousViewArrangement = self.currentViewArrangement + self.currentViewArrangement = viewArrangement + + if self.browserWidget is None: + return + mw = slicer.util.mainWindow() + dataProbe = mw.findChild("QWidget", "DataProbeCollapsibleWidget") if mw else None + if self.currentViewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView: + # View has been changed to the DICOM browser view + self.browserWidget.show() + # If we are in DICOM module, hide the Data Probe to have more space for the module + try: + inDicomModule = slicer.modules.dicom.widgetRepresentation().isEntered + except AttributeError: + # Slicer is shutting down + inDicomModule = False + if inDicomModule and dataProbe and dataProbe.isVisible(): + dataProbe.setVisible(False) + self.dataProbeHasBeenTemporarilyHidden = True + else: + # View has been changed from the DICOM browser view + if self.dataProbeHasBeenTemporarilyHidden: + # DataProbe was temporarily hidden, restore its visibility now + dataProbe.setVisible(True) + self.dataProbeHasBeenTemporarilyHidden = False - layoutId = self.previousViewArrangement + def onBrowserWidgetClosed(self): + if (self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView and + self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutNone): + # current layout is a valid layout that is not the DICOM browser view, so nothing to do + return - # Use a default layout if this layout is not valid - if (layoutId == slicer.vtkMRMLLayoutNode.SlicerLayoutNone - or layoutId == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView): - layoutId = qt.QSettings().value("MainWindow/layout", slicer.vtkMRMLLayoutNode.SlicerLayoutInitialView) + layoutId = self.previousViewArrangement - slicer.app.layoutManager().setLayout(layoutId) + # Use a default layout if this layout is not valid + if (layoutId == slicer.vtkMRMLLayoutNode.SlicerLayoutNone + or layoutId == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView): + layoutId = qt.QSettings().value("MainWindow/layout", slicer.vtkMRMLLayoutNode.SlicerLayoutInitialView) - def _onModuleAboutToBeUnloaded(self, moduleName): - # Application is shutting down. Stop the listener. - if moduleName == "DICOM": - self.stopListener() - slicer.app.moduleManager().disconnect( - 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded) + slicer.app.layoutManager().setLayout(layoutId) + + def _onModuleAboutToBeUnloaded(self, moduleName): + # Application is shutting down. Stop the listener. + if moduleName == "DICOM": + self.stopListener() + slicer.app.moduleManager().disconnect( + 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded) class _ui_DICOMSettingsPanel: - def __init__(self, parent): - vBoxLayout = qt.QVBoxLayout(parent) - # Add generic settings - genericGroupBox = ctk.ctkCollapsibleGroupBox() - genericGroupBox.title = "Generic DICOM settings" - genericGroupBoxFormLayout = qt.QFormLayout(genericGroupBox) - - directoryButton = ctk.ctkDirectoryButton() - genericGroupBoxFormLayout.addRow("Database location:", directoryButton) - parent.registerProperty(slicer.dicomDatabaseDirectorySettingsKey, directoryButton, - "directory", str(qt.SIGNAL("directoryChanged(QString)")), - "DICOM general settings", ctk.ctkSettingsPanel.OptionRequireRestart) - # Restart is forced because no mechanism is implemented that would reopen the DICOM database after - # folder location is changed. It is easier to restart the application than implementing an update - # mechanism. - - loadReferencesComboBox = ctk.ctkComboBox() - loadReferencesComboBox.toolTip = "Determines whether referenced DICOM series are " \ - "offered when loading DICOM, or the automatic behavior if interaction is disabled. " \ - "Interactive selection of referenced series is the default selection" - loadReferencesComboBox.addItem("Ask user", qt.QMessageBox.InvalidRole) - loadReferencesComboBox.addItem("Always", qt.QMessageBox.Yes) - loadReferencesComboBox.addItem("Never", qt.QMessageBox.No) - loadReferencesComboBox.currentIndex = 0 - genericGroupBoxFormLayout.addRow("Load referenced series:", loadReferencesComboBox) - parent.registerProperty( - "DICOM/automaticallyLoadReferences", loadReferencesComboBox, - "currentUserDataAsString", str(qt.SIGNAL("currentIndexChanged(int)"))) - - detailedLoggingCheckBox = qt.QCheckBox() - detailedLoggingCheckBox.toolTip = ("Log more details during DICOM operations." - " Useful for investigating DICOM loading issues but may impact performance.") - genericGroupBoxFormLayout.addRow("Detailed logging:", detailedLoggingCheckBox) - detailedLoggingMapper = ctk.ctkBooleanMapper(detailedLoggingCheckBox, "checked", str(qt.SIGNAL("toggled(bool)"))) - parent.registerProperty( - "DICOM/detailedLogging", detailedLoggingMapper, - "valueAsInt", str(qt.SIGNAL("valueAsIntChanged(int)"))) - - vBoxLayout.addWidget(genericGroupBox) - - # Add settings panel for the plugins - plugins = slicer.modules.dicomPlugins - for pluginName in plugins.keys(): - if hasattr(plugins[pluginName], 'settingsPanelEntry'): - pluginGroupBox = ctk.ctkCollapsibleGroupBox() - pluginGroupBox.title = pluginName - vBoxLayout.addWidget(pluginGroupBox) - plugins[pluginName].settingsPanelEntry(parent, pluginGroupBox) - vBoxLayout.addStretch(1) + def __init__(self, parent): + vBoxLayout = qt.QVBoxLayout(parent) + # Add generic settings + genericGroupBox = ctk.ctkCollapsibleGroupBox() + genericGroupBox.title = "Generic DICOM settings" + genericGroupBoxFormLayout = qt.QFormLayout(genericGroupBox) + + directoryButton = ctk.ctkDirectoryButton() + genericGroupBoxFormLayout.addRow("Database location:", directoryButton) + parent.registerProperty(slicer.dicomDatabaseDirectorySettingsKey, directoryButton, + "directory", str(qt.SIGNAL("directoryChanged(QString)")), + "DICOM general settings", ctk.ctkSettingsPanel.OptionRequireRestart) + # Restart is forced because no mechanism is implemented that would reopen the DICOM database after + # folder location is changed. It is easier to restart the application than implementing an update + # mechanism. + + loadReferencesComboBox = ctk.ctkComboBox() + loadReferencesComboBox.toolTip = "Determines whether referenced DICOM series are " \ + "offered when loading DICOM, or the automatic behavior if interaction is disabled. " \ + "Interactive selection of referenced series is the default selection" + loadReferencesComboBox.addItem("Ask user", qt.QMessageBox.InvalidRole) + loadReferencesComboBox.addItem("Always", qt.QMessageBox.Yes) + loadReferencesComboBox.addItem("Never", qt.QMessageBox.No) + loadReferencesComboBox.currentIndex = 0 + genericGroupBoxFormLayout.addRow("Load referenced series:", loadReferencesComboBox) + parent.registerProperty( + "DICOM/automaticallyLoadReferences", loadReferencesComboBox, + "currentUserDataAsString", str(qt.SIGNAL("currentIndexChanged(int)"))) + + detailedLoggingCheckBox = qt.QCheckBox() + detailedLoggingCheckBox.toolTip = ("Log more details during DICOM operations." + " Useful for investigating DICOM loading issues but may impact performance.") + genericGroupBoxFormLayout.addRow("Detailed logging:", detailedLoggingCheckBox) + detailedLoggingMapper = ctk.ctkBooleanMapper(detailedLoggingCheckBox, "checked", str(qt.SIGNAL("toggled(bool)"))) + parent.registerProperty( + "DICOM/detailedLogging", detailedLoggingMapper, + "valueAsInt", str(qt.SIGNAL("valueAsIntChanged(int)"))) + + vBoxLayout.addWidget(genericGroupBox) + + # Add settings panel for the plugins + plugins = slicer.modules.dicomPlugins + for pluginName in plugins.keys(): + if hasattr(plugins[pluginName], 'settingsPanelEntry'): + pluginGroupBox = ctk.ctkCollapsibleGroupBox() + pluginGroupBox.title = pluginName + vBoxLayout.addWidget(pluginGroupBox) + plugins[pluginName].settingsPanelEntry(parent, pluginGroupBox) + vBoxLayout.addStretch(1) class DICOMSettingsPanel(ctk.ctkSettingsPanel): - def __init__(self, *args, **kwargs): - ctk.ctkSettingsPanel.__init__(self, *args, **kwargs) - self.ui = _ui_DICOMSettingsPanel(self) + def __init__(self, *args, **kwargs): + ctk.ctkSettingsPanel.__init__(self, *args, **kwargs) + self.ui = _ui_DICOMSettingsPanel(self) # # DICOM file dialog # class DICOMFileDialog: - """This specially named class is detected by the scripted loadable - module and is the target for optional drag and drop operations. - See: Base/QTGUI/qSlicerScriptedFileDialog.h - and commit http://svn.slicer.org/Slicer4/trunk@21951 and issue #3081 - """ - - def __init__(self,qSlicerFileDialog): - self.qSlicerFileDialog = qSlicerFileDialog - qSlicerFileDialog.fileType = 'DICOM Directory' - qSlicerFileDialog.description = 'Load directory into DICOM database' - qSlicerFileDialog.action = slicer.qSlicerFileDialog.Read - self.directoriesToAdd = [] - - def execDialog(self): - """Not used""" - logging.debug('execDialog called on %s' % self) - - def isMimeDataAccepted(self): - """Checks the dropped data and returns true if it is one or - more directories""" - self.directoriesToAdd, _ = DICOMFileDialog.pathsFromMimeData(self.qSlicerFileDialog.mimeData()) - self.qSlicerFileDialog.acceptMimeData(len(self.directoriesToAdd) != 0) - - @staticmethod - def pathsFromMimeData(mimeData): - directoriesToAdd = [] - filesToAdd = [] - if mimeData.hasFormat('text/uri-list'): - urls = mimeData.urls() - for url in urls: - localPath = url.toLocalFile() # convert QUrl to local path - pathInfo = qt.QFileInfo() - pathInfo.setFile(localPath) # information about the path - if pathInfo.isDir(): # if it is a directory we add the files to the dialog - directoriesToAdd.append(localPath) - else: - filesToAdd.append(localPath) - return directoriesToAdd, filesToAdd - - @staticmethod - def isAscii(s): - """Return True if string only contains ASCII characters. + """This specially named class is detected by the scripted loadable + module and is the target for optional drag and drop operations. + See: Base/QTGUI/qSlicerScriptedFileDialog.h + and commit http://svn.slicer.org/Slicer4/trunk@21951 and issue #3081 """ - if isinstance(s, str): - try: - s.encode('ascii') - except UnicodeEncodeError: - # encoding as ascii failed, therefore it was not an ascii string - return False - else: - try: - s.decode('ascii') - except UnicodeDecodeError: - # decoding to ascii failed, therefore it was not an ascii string + + def __init__(self, qSlicerFileDialog): + self.qSlicerFileDialog = qSlicerFileDialog + qSlicerFileDialog.fileType = 'DICOM Directory' + qSlicerFileDialog.description = 'Load directory into DICOM database' + qSlicerFileDialog.action = slicer.qSlicerFileDialog.Read + self.directoriesToAdd = [] + + def execDialog(self): + """Not used""" + logging.debug('execDialog called on %s' % self) + + def isMimeDataAccepted(self): + """Checks the dropped data and returns true if it is one or + more directories""" + self.directoriesToAdd, _ = DICOMFileDialog.pathsFromMimeData(self.qSlicerFileDialog.mimeData()) + self.qSlicerFileDialog.acceptMimeData(len(self.directoriesToAdd) != 0) + + @staticmethod + def pathsFromMimeData(mimeData): + directoriesToAdd = [] + filesToAdd = [] + if mimeData.hasFormat('text/uri-list'): + urls = mimeData.urls() + for url in urls: + localPath = url.toLocalFile() # convert QUrl to local path + pathInfo = qt.QFileInfo() + pathInfo.setFile(localPath) # information about the path + if pathInfo.isDir(): # if it is a directory we add the files to the dialog + directoriesToAdd.append(localPath) + else: + filesToAdd.append(localPath) + return directoriesToAdd, filesToAdd + + @staticmethod + def isAscii(s): + """Return True if string only contains ASCII characters. + """ + if isinstance(s, str): + try: + s.encode('ascii') + except UnicodeEncodeError: + # encoding as ascii failed, therefore it was not an ascii string + return False + else: + try: + s.decode('ascii') + except UnicodeDecodeError: + # decoding to ascii failed, therefore it was not an ascii string + return False + return True + + @staticmethod + def validDirectories(directoriesToAdd): + """Return True if the directory names are acceptable for input. + If path contains non-ASCII characters then they are rejected because + DICOM module cannot reliable read files form folders that contain + special characters in the name. + """ + if slicer.app.isCodePageUtf8(): + return True + + for directoryName in directoriesToAdd: + for root, dirs, files in os.walk(directoryName): + if not DICOMFileDialog.isAscii(root): + return False + for name in files: + if not DICOMFileDialog.isAscii(name): + return False + for name in dirs: + if not DICOMFileDialog.isAscii(name): + return False + + return True + + @staticmethod + def createDefaultDatabase(): + """If DICOM database is invalid then try to create a default one. If fails then show an error message. + This method should only be used when user initiates DICOM import on the GUI, because the error message is + shown in a popup, which would block execution of auomated processing scripts. + Returns True if a valid DICOM database is available (has been created succussfully or it was already available). + """ + if slicer.dicomDatabase and slicer.dicomDatabase.isOpen: + # Valid DICOM database already exists + return True + + # Try to create a database with default settings + if slicer.modules.DICOMInstance.browserWidget is None: + slicer.util.selectModule('DICOM') + slicer.modules.DICOMInstance.browserWidget.dicomBrowser.createNewDatabaseDirectory() + if slicer.dicomDatabase and slicer.dicomDatabase.isOpen: + # DICOM database created successfully + return True + + # Failed to create database + # Make sure the browser is visible then display error message + slicer.util.selectModule('DICOM') + slicer.modules.dicom.widgetRepresentation().self().onOpenBrowserWidget() + slicer.util.warningDisplay("Could not create a DICOM database with default settings. Please create a new database or" + " update the existing incompatible database using options shown in DICOM browser.") return False - return True - - @staticmethod - def validDirectories(directoriesToAdd): - """Return True if the directory names are acceptable for input. - If path contains non-ASCII characters then they are rejected because - DICOM module cannot reliable read files form folders that contain - special characters in the name. - """ - if slicer.app.isCodePageUtf8(): - return True - - for directoryName in directoriesToAdd: - for root, dirs, files in os.walk(directoryName): - if not DICOMFileDialog.isAscii(root): - return False - for name in files: - if not DICOMFileDialog.isAscii(name): - return False - for name in dirs: - if not DICOMFileDialog.isAscii(name): - return False - return True + def dropEvent(self): + if not DICOMFileDialog.createDefaultDatabase(): + return - @staticmethod - def createDefaultDatabase(): - """If DICOM database is invalid then try to create a default one. If fails then show an error message. - This method should only be used when user initiates DICOM import on the GUI, because the error message is - shown in a popup, which would block execution of auomated processing scripts. - Returns True if a valid DICOM database is available (has been created succussfully or it was already available). - """ - if slicer.dicomDatabase and slicer.dicomDatabase.isOpen: - # Valid DICOM database already exists - return True - - # Try to create a database with default settings - if slicer.modules.DICOMInstance.browserWidget is None: - slicer.util.selectModule('DICOM') - slicer.modules.DICOMInstance.browserWidget.dicomBrowser.createNewDatabaseDirectory() - if slicer.dicomDatabase and slicer.dicomDatabase.isOpen: - # DICOM database created successfully - return True - - # Failed to create database - # Make sure the browser is visible then display error message - slicer.util.selectModule('DICOM') - slicer.modules.dicom.widgetRepresentation().self().onOpenBrowserWidget() - slicer.util.warningDisplay("Could not create a DICOM database with default settings. Please create a new database or" - " update the existing incompatible database using options shown in DICOM browser.") - return False - - def dropEvent(self): - if not DICOMFileDialog.createDefaultDatabase(): - return - - if not DICOMFileDialog.validDirectories(self.directoriesToAdd): - if not slicer.util.confirmYesNoDisplay("Import of files that have special (non-ASCII) characters in their names is not supported." - " It is recommended to move files into a different folder and retry. Try to import from current location anyway?"): - self.directoriesToAdd = [] - return + if not DICOMFileDialog.validDirectories(self.directoriesToAdd): + if not slicer.util.confirmYesNoDisplay("Import of files that have special (non-ASCII) characters in their names is not supported." + " It is recommended to move files into a different folder and retry. Try to import from current location anyway?"): + self.directoriesToAdd = [] + return - slicer.util.selectModule('DICOM') - slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importDirectories(self.directoriesToAdd) - self.directoriesToAdd = [] + slicer.util.selectModule('DICOM') + slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importDirectories(self.directoriesToAdd) + self.directoriesToAdd = [] class DICOMLoadingByDragAndDropEventFilter(qt.QWidget): - """This event filter is used for overriding drag-and-drop behavior while - the DICOM module is active. To simplify DICOM import, while DICOM module is active - then files or folders that are drag-and-dropped to the application window - are always interpreted as DICOM data. - """ - - def eventFilter(self, object, event): + """This event filter is used for overriding drag-and-drop behavior while + the DICOM module is active. To simplify DICOM import, while DICOM module is active + then files or folders that are drag-and-dropped to the application window + are always interpreted as DICOM data. """ - Custom event filter for Slicer Main Window. - Inputs: Object (QObject), Event (QEvent) - """ - if event.type() == qt.QEvent.DragEnter: - self.dragEnterEvent(event) - return True - if event.type() == qt.QEvent.Drop: - self.dropEvent(event) - return True - return False - - def dragEnterEvent(self, event): - """ - Actions to do when a drag enter event occurs in the Main Window. + def eventFilter(self, object, event): + """ + Custom event filter for Slicer Main Window. + + Inputs: Object (QObject), Event (QEvent) + """ + if event.type() == qt.QEvent.DragEnter: + self.dragEnterEvent(event) + return True + if event.type() == qt.QEvent.Drop: + self.dropEvent(event) + return True + return False - Read up on https://doc.qt.io/qt-5.12/dnd.html#dropping - Input: Event (QEvent) - """ - self.directoriesToAdd, self.filesToAdd = DICOMFileDialog.pathsFromMimeData(event.mimeData()) - if self.directoriesToAdd or self.filesToAdd: - event.acceptProposedAction() # allows drop event to proceed - else: - event.ignore() - - def dropEvent(self, event): - if not DICOMFileDialog.createDefaultDatabase(): - return - - if not DICOMFileDialog.validDirectories(self.directoriesToAdd) or not DICOMFileDialog.validDirectories(self.filesToAdd): - if not slicer.util.confirmYesNoDisplay("Import from folders with special (non-ASCII) characters in the name is not supported." - " It is recommended to move files into a different folder and retry. Try to import from current location anyway?"): - self.directoriesToAdd = [] - return + def dragEnterEvent(self, event): + """ + Actions to do when a drag enter event occurs in the Main Window. + + Read up on https://doc.qt.io/qt-5.12/dnd.html#dropping + Input: Event (QEvent) + """ + self.directoriesToAdd, self.filesToAdd = DICOMFileDialog.pathsFromMimeData(event.mimeData()) + if self.directoriesToAdd or self.filesToAdd: + event.acceptProposedAction() # allows drop event to proceed + else: + event.ignore() + + def dropEvent(self, event): + if not DICOMFileDialog.createDefaultDatabase(): + return + + if not DICOMFileDialog.validDirectories(self.directoriesToAdd) or not DICOMFileDialog.validDirectories(self.filesToAdd): + if not slicer.util.confirmYesNoDisplay("Import from folders with special (non-ASCII) characters in the name is not supported." + " It is recommended to move files into a different folder and retry. Try to import from current location anyway?"): + self.directoriesToAdd = [] + return - slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importDirectories(self.directoriesToAdd) - slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importFiles(self.filesToAdd) + slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importDirectories(self.directoriesToAdd) + slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importFiles(self.filesToAdd) # @@ -559,337 +560,337 @@ def dropEvent(self, event): # class DICOMWidget(ScriptedLoadableModuleWidget): - """ - Slicer module that creates the Qt GUI for interacting with DICOM - """ + """ + Slicer module that creates the Qt GUI for interacting with DICOM + """ + + # sets up the widget + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - # sets up the widget - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + # If DICOM module is the startup module then this widget will be shown + # before startup completes, therefore we need to ensure here that + # module discovery happens before proceeding. + slicer.modules.DICOMInstance.performPostModuleDiscoveryTasks() - # If DICOM module is the startup module then this widget will be shown - # before startup completes, therefore we need to ensure here that - # module discovery happens before proceeding. - slicer.modules.DICOMInstance.performPostModuleDiscoveryTasks() + # This module is often used in developer mode, therefore + # collapse reload & test section by default. + if hasattr(self, "reloadCollapsibleButton"): + self.reloadCollapsibleButton.collapsed = True - # This module is often used in developer mode, therefore - # collapse reload & test section by default. - if hasattr(self, "reloadCollapsibleButton"): - self.reloadCollapsibleButton.collapsed = True + self.dragAndDropEventFilter = DICOMLoadingByDragAndDropEventFilter() - self.dragAndDropEventFilter = DICOMLoadingByDragAndDropEventFilter() + globals()['d'] = self - globals()['d'] = self + self.testingServer = None + self.browserWidget = None - self.testingServer = None - self.browserWidget = None + # Load widget from .ui file (created by Qt Designer) + uiWidget = slicer.util.loadUI(self.resourcePath('UI/DICOM.ui')) + self.layout.addWidget(uiWidget) + self.ui = slicer.util.childWidgetVariables(uiWidget) - # Load widget from .ui file (created by Qt Designer) - uiWidget = slicer.util.loadUI(self.resourcePath('UI/DICOM.ui')) - self.layout.addWidget(uiWidget) - self.ui = slicer.util.childWidgetVariables(uiWidget) + self.browserWidget = DICOMLib.SlicerDICOMBrowser() + self.browserWidget.objectName = 'SlicerDICOMBrowser' - self.browserWidget = DICOMLib.SlicerDICOMBrowser() - self.browserWidget.objectName = 'SlicerDICOMBrowser' + slicer.modules.DICOMInstance.setBrowserWidgetInDICOMLayout(self.browserWidget) - slicer.modules.DICOMInstance.setBrowserWidgetInDICOMLayout(self.browserWidget) + layoutManager = slicer.app.layoutManager() + if layoutManager is not None: + layoutManager.layoutChanged.connect(self.onLayoutChanged) + viewArrangement = slicer.app.layoutManager().layoutLogic().GetLayoutNode().GetViewArrangement() + self.ui.showBrowserButton.checked = (viewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView) - layoutManager = slicer.app.layoutManager() - if layoutManager is not None: - layoutManager.layoutChanged.connect(self.onLayoutChanged) - viewArrangement = slicer.app.layoutManager().layoutLogic().GetLayoutNode().GetViewArrangement() - self.ui.showBrowserButton.checked = (viewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView) - - # connect to the 'Show DICOM Browser' button - self.ui.showBrowserButton.connect('clicked()', self.toggleBrowserWidget) - - self.ui.importButton.connect('clicked()', self.importFolder) - - self.ui.subjectHierarchyTree.setMRMLScene(slicer.mrmlScene) - self.ui.subjectHierarchyTree.currentItemChanged.connect(self.onCurrentItemChanged) - self.ui.subjectHierarchyTree.currentItemModified.connect(self.onCurrentItemModified) - self.subjectHierarchyCurrentVisibility = False - self.ui.subjectHierarchyTree.setColumnHidden(self.ui.subjectHierarchyTree.model().idColumn, True) - - # - # DICOM networking - # - - self.ui.networkingFrame.collapsed = True - self.ui.queryServerButton.connect('clicked()', self.browserWidget.dicomBrowser, "openQueryDialog()") - - self.ui.toggleListener.connect('toggled(bool)', self.onToggleListener) - - settings = qt.QSettings() - self.ui.runListenerAtStart.checked = settingsValue('DICOM/RunListenerAtStart', False, converter=toBool) - self.ui.runListenerAtStart.connect('toggled(bool)', self.onRunListenerAtStart) - - # Testing server - not exposed (used for development) - - self.toggleServer = qt.QPushButton("Start Testing Server") - self.ui.networkingFrame.layout().addWidget(self.toggleServer) - self.toggleServer.connect('clicked()', self.onToggleServer) - - self.verboseServer = qt.QCheckBox("Verbose") - self.ui.networkingFrame.layout().addWidget(self.verboseServer) - - # advanced options - not exposed to end users - # developers can uncomment these lines to access testing server - self.toggleServer.hide() - self.verboseServer.hide() - - # - # Browser settings - # - - self.ui.browserSettingsFrame.collapsed = True - - self.updateDatabaseDirectoryFromBrowser(self.browserWidget.dicomBrowser.databaseDirectory) - # Synchronize database selection between browser and this widget - self.ui.directoryButton.directoryChanged.connect(self.updateDatabaseDirectoryFromWidget) - self.ui.directoryButton.sizePolicy = qt.QSizePolicy(qt.QSizePolicy.Ignored, qt.QSizePolicy.Fixed) - self.browserWidget.dicomBrowser.databaseDirectoryChanged.connect(self.updateDatabaseDirectoryFromBrowser) - - self.ui.browserAutoHideCheckBox.checked = not settingsValue('DICOM/BrowserPersistent', False, converter=toBool) - self.ui.browserAutoHideCheckBox.stateChanged.connect(self.onBrowserAutoHideStateChanged) - - self.ui.repairDatabaseButton.connect('clicked()', self.browserWidget.dicomBrowser, "onRepairAction()") - self.ui.clearDatabaseButton.connect('clicked()', self.onClearDatabase) - - # connect to the main window's dicom button - mw = slicer.util.mainWindow() - if mw: - try: - action = slicer.util.findChildren(mw,name='LoadDICOMAction')[0] - action.connect('triggered()',self.onOpenBrowserWidget) - except IndexError: - logging.error('Could not connect to the main window DICOM button') - - self.databaseRefreshRequestTimer = qt.QTimer() - self.databaseRefreshRequestTimer.setSingleShot(True) - # If not receiving new file for 2 seconds then a database update is triggered. - self.databaseRefreshRequestTimer.setInterval(2000) - self.databaseRefreshRequestTimer.connect('timeout()', self.requestDatabaseRefresh) - - # - # DICOM Plugins selection widget - # - self.ui.dicomPluginsFrame.collapsed = True - self.pluginSelector = DICOMLib.DICOMPluginSelector(self.ui.dicomPluginsFrame) - self.ui.dicomPluginsFrame.layout().addWidget(self.pluginSelector) - self.checkBoxByPlugins = [] - - for pluginClass in slicer.modules.dicomPlugins: - self.checkBox = self.pluginSelector.checkBoxByPlugin[pluginClass] - self.checkBox.connect('stateChanged(int)', self.onPluginStateChanged) - self.checkBoxByPlugins.append(self.checkBox) - - def onPluginStateChanged(self, state): - settings = qt.QSettings() - settings.beginWriteArray('DICOM/disabledPlugins') - - for key in settings.allKeys(): - settings.remove(key) - - plugins = self.pluginSelector.selectedPlugins() - arrayIndex = 0 - for pluginClass in slicer.modules.dicomPlugins: - if pluginClass not in plugins: - settings.setArrayIndex(arrayIndex) - settings.setValue(pluginClass, 'disabled') - arrayIndex += 1 - - settings.endArray() - - def enter(self): - self.onOpenBrowserWidget() - self.addListenerObservers() - self.onListenerStateChanged() - # While DICOM module is active, drag-and-drop always performs DICOM import - mw = slicer.util.mainWindow() - if mw: - mw.installEventFilter(self.dragAndDropEventFilter) - - def exit(self): - mw = slicer.util.mainWindow() - if mw: - mw.removeEventFilter(self.dragAndDropEventFilter) - self.removeListenerObservers() - self.browserWidget.close() - - def addListenerObservers(self): - if not hasattr(slicer, 'dicomListener'): - return - if slicer.dicomListener.process is not None: - slicer.dicomListener.process.connect('stateChanged(QProcess::ProcessState)', self.onListenerStateChanged) - slicer.dicomListener.fileToBeAddedCallback = self.onListenerToAddFile - slicer.dicomListener.fileAddedCallback = self.onListenerAddedFile - - def removeListenerObservers(self): - if not hasattr(slicer, 'dicomListener'): - return - if slicer.dicomListener.process is not None: - slicer.dicomListener.process.disconnect('stateChanged(QProcess::ProcessState)', self.onListenerStateChanged) - slicer.dicomListener.fileToBeAddedCallback = None - slicer.dicomListener.fileAddedCallback = None - - def updateGUIFromMRML(self, caller, event): - pass - - def onLayoutChanged(self, viewArrangement): - self.ui.showBrowserButton.checked = (viewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView) - - def onCurrentItemChanged(self, id): - plugin = slicer.qSlicerSubjectHierarchyPluginHandler.instance().getOwnerPluginForSubjectHierarchyItem(id) - if not plugin: - self.subjectHierarchyCurrentVisibility = False - return - self.subjectHierarchyCurrentVisibility = plugin.getDisplayVisibility(id) - - def onCurrentItemModified(self, id): - oldSubjectHierarchyCurrentVisibility = self.subjectHierarchyCurrentVisibility - - plugin = slicer.qSlicerSubjectHierarchyPluginHandler.instance().getOwnerPluginForSubjectHierarchyItem(id) - if not plugin: - self.subjectHierarchyCurrentVisibility = False - else: - self.subjectHierarchyCurrentVisibility = plugin.getDisplayVisibility(id) - - if self.browserWidget is None: - return - - if (oldSubjectHierarchyCurrentVisibility != self.subjectHierarchyCurrentVisibility and - self.subjectHierarchyCurrentVisibility): - self.browserWidget.close() - - def toggleBrowserWidget(self): - if self.ui.showBrowserButton.checked: - self.onOpenBrowserWidget() - else: - if self.browserWidget: + # connect to the 'Show DICOM Browser' button + self.ui.showBrowserButton.connect('clicked()', self.toggleBrowserWidget) + + self.ui.importButton.connect('clicked()', self.importFolder) + + self.ui.subjectHierarchyTree.setMRMLScene(slicer.mrmlScene) + self.ui.subjectHierarchyTree.currentItemChanged.connect(self.onCurrentItemChanged) + self.ui.subjectHierarchyTree.currentItemModified.connect(self.onCurrentItemModified) + self.subjectHierarchyCurrentVisibility = False + self.ui.subjectHierarchyTree.setColumnHidden(self.ui.subjectHierarchyTree.model().idColumn, True) + + # + # DICOM networking + # + + self.ui.networkingFrame.collapsed = True + self.ui.queryServerButton.connect('clicked()', self.browserWidget.dicomBrowser, "openQueryDialog()") + + self.ui.toggleListener.connect('toggled(bool)', self.onToggleListener) + + settings = qt.QSettings() + self.ui.runListenerAtStart.checked = settingsValue('DICOM/RunListenerAtStart', False, converter=toBool) + self.ui.runListenerAtStart.connect('toggled(bool)', self.onRunListenerAtStart) + + # Testing server - not exposed (used for development) + + self.toggleServer = qt.QPushButton("Start Testing Server") + self.ui.networkingFrame.layout().addWidget(self.toggleServer) + self.toggleServer.connect('clicked()', self.onToggleServer) + + self.verboseServer = qt.QCheckBox("Verbose") + self.ui.networkingFrame.layout().addWidget(self.verboseServer) + + # advanced options - not exposed to end users + # developers can uncomment these lines to access testing server + self.toggleServer.hide() + self.verboseServer.hide() + + # + # Browser settings + # + + self.ui.browserSettingsFrame.collapsed = True + + self.updateDatabaseDirectoryFromBrowser(self.browserWidget.dicomBrowser.databaseDirectory) + # Synchronize database selection between browser and this widget + self.ui.directoryButton.directoryChanged.connect(self.updateDatabaseDirectoryFromWidget) + self.ui.directoryButton.sizePolicy = qt.QSizePolicy(qt.QSizePolicy.Ignored, qt.QSizePolicy.Fixed) + self.browserWidget.dicomBrowser.databaseDirectoryChanged.connect(self.updateDatabaseDirectoryFromBrowser) + + self.ui.browserAutoHideCheckBox.checked = not settingsValue('DICOM/BrowserPersistent', False, converter=toBool) + self.ui.browserAutoHideCheckBox.stateChanged.connect(self.onBrowserAutoHideStateChanged) + + self.ui.repairDatabaseButton.connect('clicked()', self.browserWidget.dicomBrowser, "onRepairAction()") + self.ui.clearDatabaseButton.connect('clicked()', self.onClearDatabase) + + # connect to the main window's dicom button + mw = slicer.util.mainWindow() + if mw: + try: + action = slicer.util.findChildren(mw, name='LoadDICOMAction')[0] + action.connect('triggered()', self.onOpenBrowserWidget) + except IndexError: + logging.error('Could not connect to the main window DICOM button') + + self.databaseRefreshRequestTimer = qt.QTimer() + self.databaseRefreshRequestTimer.setSingleShot(True) + # If not receiving new file for 2 seconds then a database update is triggered. + self.databaseRefreshRequestTimer.setInterval(2000) + self.databaseRefreshRequestTimer.connect('timeout()', self.requestDatabaseRefresh) + + # + # DICOM Plugins selection widget + # + self.ui.dicomPluginsFrame.collapsed = True + self.pluginSelector = DICOMLib.DICOMPluginSelector(self.ui.dicomPluginsFrame) + self.ui.dicomPluginsFrame.layout().addWidget(self.pluginSelector) + self.checkBoxByPlugins = [] + + for pluginClass in slicer.modules.dicomPlugins: + self.checkBox = self.pluginSelector.checkBoxByPlugin[pluginClass] + self.checkBox.connect('stateChanged(int)', self.onPluginStateChanged) + self.checkBoxByPlugins.append(self.checkBox) + + def onPluginStateChanged(self, state): + settings = qt.QSettings() + settings.beginWriteArray('DICOM/disabledPlugins') + + for key in settings.allKeys(): + settings.remove(key) + + plugins = self.pluginSelector.selectedPlugins() + arrayIndex = 0 + for pluginClass in slicer.modules.dicomPlugins: + if pluginClass not in plugins: + settings.setArrayIndex(arrayIndex) + settings.setValue(pluginClass, 'disabled') + arrayIndex += 1 + + settings.endArray() + + def enter(self): + self.onOpenBrowserWidget() + self.addListenerObservers() + self.onListenerStateChanged() + # While DICOM module is active, drag-and-drop always performs DICOM import + mw = slicer.util.mainWindow() + if mw: + mw.installEventFilter(self.dragAndDropEventFilter) + + def exit(self): + mw = slicer.util.mainWindow() + if mw: + mw.removeEventFilter(self.dragAndDropEventFilter) + self.removeListenerObservers() self.browserWidget.close() - def importFolder(self): - if not DICOMFileDialog.createDefaultDatabase(): - return - self.browserWidget.dicomBrowser.openImportDialog() - - def onOpenBrowserWidget(self): - slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView) - - def onToggleListener(self, toggled): - if hasattr(slicer, 'dicomListener'): - self.removeListenerObservers() - slicer.modules.DICOMInstance.stopListener() - if toggled: - slicer.modules.DICOMInstance.startListener() - self.addListenerObservers() - self.onListenerStateChanged() - - def onListenerStateChanged(self, newState=None): - """ Called when the indexer process state changes - so we can provide feedback to the user - """ - if hasattr(slicer, 'dicomListener') and slicer.dicomListener.process is not None: - newState = slicer.dicomListener.process.state() - else: - newState = 0 - - if newState == 0: - self.ui.listenerStateLabel.text = "not started" - wasBlocked = self.ui.toggleListener.blockSignals(True) - self.ui.toggleListener.checked = False - self.ui.toggleListener.blockSignals(wasBlocked) - if hasattr(slicer.modules, 'DICOMInstance'): # custom applications may not have the standard DICOM module - slicer.modules.DICOMInstance.stopListener() - if newState == 1: - self.ui.listenerStateLabel.text = "starting" - if newState == 2: - port = str(slicer.dicomListener.port) if hasattr(slicer, 'dicomListener') else "unknown" - self.ui.listenerStateLabel.text = "running at port "+port - self.ui.toggleListener.checked = True - - def onListenerToAddFile(self): - """ Called when the indexer is about to add a file to the database. - Works around issue where ctkDICOMModel has open queries that keep the - database locked. - """ - pass + def addListenerObservers(self): + if not hasattr(slicer, 'dicomListener'): + return + if slicer.dicomListener.process is not None: + slicer.dicomListener.process.connect('stateChanged(QProcess::ProcessState)', self.onListenerStateChanged) + slicer.dicomListener.fileToBeAddedCallback = self.onListenerToAddFile + slicer.dicomListener.fileAddedCallback = self.onListenerAddedFile + + def removeListenerObservers(self): + if not hasattr(slicer, 'dicomListener'): + return + if slicer.dicomListener.process is not None: + slicer.dicomListener.process.disconnect('stateChanged(QProcess::ProcessState)', self.onListenerStateChanged) + slicer.dicomListener.fileToBeAddedCallback = None + slicer.dicomListener.fileAddedCallback = None + + def updateGUIFromMRML(self, caller, event): + pass + + def onLayoutChanged(self, viewArrangement): + self.ui.showBrowserButton.checked = (viewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView) + + def onCurrentItemChanged(self, id): + plugin = slicer.qSlicerSubjectHierarchyPluginHandler.instance().getOwnerPluginForSubjectHierarchyItem(id) + if not plugin: + self.subjectHierarchyCurrentVisibility = False + return + self.subjectHierarchyCurrentVisibility = plugin.getDisplayVisibility(id) + + def onCurrentItemModified(self, id): + oldSubjectHierarchyCurrentVisibility = self.subjectHierarchyCurrentVisibility + + plugin = slicer.qSlicerSubjectHierarchyPluginHandler.instance().getOwnerPluginForSubjectHierarchyItem(id) + if not plugin: + self.subjectHierarchyCurrentVisibility = False + else: + self.subjectHierarchyCurrentVisibility = plugin.getDisplayVisibility(id) - def onListenerAddedFile(self): - """Called after the listener has added a file. - Restore and refresh the app model - """ - newFile = slicer.dicomListener.lastFileAdded - if newFile: - slicer.util.showStatusMessage("Received DICOM file: %s" % newFile, 1000) - self.databaseRefreshRequestTimer.start() - - def requestDatabaseRefresh(self): - logging.debug("Database refresh is requested") - if slicer.dicomDatabase.isOpen: - slicer.dicomDatabase.databaseChanged() - - def onToggleServer(self): - if self.testingServer and self.testingServer.qrRunning(): - self.testingServer.stop() - self.toggleServer.text = "Start Testing Server" - else: - # - # create&configure the testingServer if needed, start the server, and populate it - # - if not self.testingServer: - # find the helper executables (only works on build trees - # with standard naming conventions) - self.exeDir = slicer.app.slicerHome - if slicer.app.intDir: - self.exeDir = self.exeDir + '/' + slicer.app.intDir - self.exeDir = self.exeDir + '/../CTK-build/DCMTK-build' - - # TODO: deal with Debug/RelWithDebInfo on windows - - # set up temp dir - tmpDir = slicer.app.temporaryPath - if not os.path.exists(tmpDir): - os.mkdir(tmpDir) - self.tmpDir = tmpDir + '/DICOM' - if not os.path.exists(self.tmpDir): - os.mkdir(self.tmpDir) - self.testingServer = DICOMLib.DICOMTestingQRServer(exeDir=self.exeDir,tmpDir=self.tmpDir) - - # look for the sample data to load (only works on build trees - # with standard naming conventions) - self.dataDir = slicer.app.slicerHome + '/../../Slicer4/Testing/Data/Input/CTHeadAxialDicom' - files = glob.glob(self.dataDir+'/*.dcm') - - # now start the server - self.testingServer.start(verbose=self.verboseServer.checked,initialFiles=files) - #self.toggleServer.text = "Stop Testing Server" - - def onRunListenerAtStart(self, toggled): - settings = qt.QSettings() - settings.setValue('DICOM/RunListenerAtStart', toggled) - - def updateDatabaseDirectoryFromWidget(self, databaseDirectory): - self.browserWidget.dicomBrowser.databaseDirectory = databaseDirectory - - def updateDatabaseDirectoryFromBrowser(self,databaseDirectory): - wasBlocked = self.ui.directoryButton.blockSignals(True) - self.ui.directoryButton.directory = databaseDirectory - self.ui.directoryButton.blockSignals(wasBlocked) - - def onBrowserAutoHideStateChanged(self, autoHideState): - if self.browserWidget: - self.browserWidget.setBrowserPersistence(autoHideState != qt.Qt.Checked) - - def onClearDatabase(self): - patientIds = slicer.dicomDatabase.patients() - if len(patientIds) == 0: - slicer.util.infoDisplay("DICOM database is already empty.") - elif not slicer.util.confirmYesNoDisplay( - 'Are you sure you want to delete all data and files copied into the database (%d patients)?' % len(patientIds), - windowTitle='Clear entire DICOM database'): - return - slicer.app.setOverrideCursor(qt.Qt.WaitCursor) - DICOMLib.clearDatabase(slicer.dicomDatabase) - slicer.app.restoreOverrideCursor() + if self.browserWidget is None: + return + + if (oldSubjectHierarchyCurrentVisibility != self.subjectHierarchyCurrentVisibility and + self.subjectHierarchyCurrentVisibility): + self.browserWidget.close() + + def toggleBrowserWidget(self): + if self.ui.showBrowserButton.checked: + self.onOpenBrowserWidget() + else: + if self.browserWidget: + self.browserWidget.close() + + def importFolder(self): + if not DICOMFileDialog.createDefaultDatabase(): + return + self.browserWidget.dicomBrowser.openImportDialog() + + def onOpenBrowserWidget(self): + slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView) + + def onToggleListener(self, toggled): + if hasattr(slicer, 'dicomListener'): + self.removeListenerObservers() + slicer.modules.DICOMInstance.stopListener() + if toggled: + slicer.modules.DICOMInstance.startListener() + self.addListenerObservers() + self.onListenerStateChanged() + + def onListenerStateChanged(self, newState=None): + """ Called when the indexer process state changes + so we can provide feedback to the user + """ + if hasattr(slicer, 'dicomListener') and slicer.dicomListener.process is not None: + newState = slicer.dicomListener.process.state() + else: + newState = 0 + + if newState == 0: + self.ui.listenerStateLabel.text = "not started" + wasBlocked = self.ui.toggleListener.blockSignals(True) + self.ui.toggleListener.checked = False + self.ui.toggleListener.blockSignals(wasBlocked) + if hasattr(slicer.modules, 'DICOMInstance'): # custom applications may not have the standard DICOM module + slicer.modules.DICOMInstance.stopListener() + if newState == 1: + self.ui.listenerStateLabel.text = "starting" + if newState == 2: + port = str(slicer.dicomListener.port) if hasattr(slicer, 'dicomListener') else "unknown" + self.ui.listenerStateLabel.text = "running at port " + port + self.ui.toggleListener.checked = True + + def onListenerToAddFile(self): + """ Called when the indexer is about to add a file to the database. + Works around issue where ctkDICOMModel has open queries that keep the + database locked. + """ + pass + + def onListenerAddedFile(self): + """Called after the listener has added a file. + Restore and refresh the app model + """ + newFile = slicer.dicomListener.lastFileAdded + if newFile: + slicer.util.showStatusMessage("Received DICOM file: %s" % newFile, 1000) + self.databaseRefreshRequestTimer.start() + + def requestDatabaseRefresh(self): + logging.debug("Database refresh is requested") + if slicer.dicomDatabase.isOpen: + slicer.dicomDatabase.databaseChanged() + + def onToggleServer(self): + if self.testingServer and self.testingServer.qrRunning(): + self.testingServer.stop() + self.toggleServer.text = "Start Testing Server" + else: + # + # create&configure the testingServer if needed, start the server, and populate it + # + if not self.testingServer: + # find the helper executables (only works on build trees + # with standard naming conventions) + self.exeDir = slicer.app.slicerHome + if slicer.app.intDir: + self.exeDir = self.exeDir + '/' + slicer.app.intDir + self.exeDir = self.exeDir + '/../CTK-build/DCMTK-build' + + # TODO: deal with Debug/RelWithDebInfo on windows + + # set up temp dir + tmpDir = slicer.app.temporaryPath + if not os.path.exists(tmpDir): + os.mkdir(tmpDir) + self.tmpDir = tmpDir + '/DICOM' + if not os.path.exists(self.tmpDir): + os.mkdir(self.tmpDir) + self.testingServer = DICOMLib.DICOMTestingQRServer(exeDir=self.exeDir, tmpDir=self.tmpDir) + + # look for the sample data to load (only works on build trees + # with standard naming conventions) + self.dataDir = slicer.app.slicerHome + '/../../Slicer4/Testing/Data/Input/CTHeadAxialDicom' + files = glob.glob(self.dataDir + '/*.dcm') + + # now start the server + self.testingServer.start(verbose=self.verboseServer.checked, initialFiles=files) + # self.toggleServer.text = "Stop Testing Server" + + def onRunListenerAtStart(self, toggled): + settings = qt.QSettings() + settings.setValue('DICOM/RunListenerAtStart', toggled) + + def updateDatabaseDirectoryFromWidget(self, databaseDirectory): + self.browserWidget.dicomBrowser.databaseDirectory = databaseDirectory + + def updateDatabaseDirectoryFromBrowser(self, databaseDirectory): + wasBlocked = self.ui.directoryButton.blockSignals(True) + self.ui.directoryButton.directory = databaseDirectory + self.ui.directoryButton.blockSignals(wasBlocked) + + def onBrowserAutoHideStateChanged(self, autoHideState): + if self.browserWidget: + self.browserWidget.setBrowserPersistence(autoHideState != qt.Qt.Checked) + + def onClearDatabase(self): + patientIds = slicer.dicomDatabase.patients() + if len(patientIds) == 0: + slicer.util.infoDisplay("DICOM database is already empty.") + elif not slicer.util.confirmYesNoDisplay( + 'Are you sure you want to delete all data and files copied into the database (%d patients)?' % len(patientIds), + windowTitle='Clear entire DICOM database'): + return + slicer.app.setOverrideCursor(qt.Qt.WaitCursor) + DICOMLib.clearDatabase(slicer.dicomDatabase) + slicer.app.restoreOverrideCursor() diff --git a/Modules/Scripted/DICOMLib/DICOMBrowser.py b/Modules/Scripted/DICOMLib/DICOMBrowser.py index ac306fdcda2..65d7cfc3f62 100644 --- a/Modules/Scripted/DICOMLib/DICOMBrowser.py +++ b/Modules/Scripted/DICOMLib/DICOMBrowser.py @@ -30,697 +30,697 @@ class SlicerDICOMBrowser(VTKObservationMixin, qt.QWidget): - """Implement the Qt window showing details and possible - operations to perform on the selected dicom list item. - This is a helper used in the DICOMWidget class. - """ - - closed = qt.Signal() # Invoked when the dicom widget is closed using the close method - - def __init__(self, dicomBrowser=None, parent="mainWindow"): - VTKObservationMixin.__init__(self) - qt.QWidget.__init__(self, slicer.util.mainWindow() if parent == "mainWindow" else parent) - - self.pluginInstances = {} - self.fileLists = [] - self.extensionCheckPending = False - - self.settings = qt.QSettings() - - self.dicomBrowser = dicomBrowser if dicomBrowser is not None else slicer.app.createDICOMBrowserForMainDatabase() - - self.browserPersistent = settingsValue('DICOM/BrowserPersistent', False, converter=toBool) - self.advancedView = settingsValue('DICOM/advancedView', 0, converter=int) - self.horizontalTables = settingsValue('DICOM/horizontalTables', 0, converter=int) - - self.setup() - - self.dicomBrowser.connect('directoryImported()', self.onDirectoryImported) - self.dicomBrowser.connect('sendRequested(QStringList)', self.onSend) - - # Load when double-clicked on an item in the browser - self.dicomBrowser.dicomTableManager().connect('patientsDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked) - self.dicomBrowser.dicomTableManager().connect('studiesDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked) - self.dicomBrowser.dicomTableManager().connect('seriesDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked) - - def open(self): - self.show() - - def close(self): - self.hide() - self.closed.emit() - - def onSend(self, fileList): - if len(fileList): - sendDialog = DICOMLib.DICOMSendDialog(fileList, self) - - def setup(self, showPreview=False): - """ - main window is a frame with widgets from the app - widget repacked into it along with slicer-specific - extra widgets + """Implement the Qt window showing details and possible + operations to perform on the selected dicom list item. + This is a helper used in the DICOMWidget class. """ - self.setWindowTitle('DICOM Browser') - self.setLayout(qt.QVBoxLayout()) - - self.dicomBrowser.databaseDirectorySelectorVisible = False - self.dicomBrowser.toolbarVisible = False - self.dicomBrowser.sendActionVisible = True - self.dicomBrowser.databaseDirectorySettingsKey = slicer.dicomDatabaseDirectorySettingsKey - self.dicomBrowser.dicomTableManager().dynamicTableLayout = False - horizontal = self.settings.setValue('DICOM/horizontalTables', 0) - self.dicomBrowser.dicomTableManager().tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical - self.layout().addWidget(self.dicomBrowser) - - self.userFrame = qt.QWidget() - self.preview = qt.QWidget() - - # - # preview related column - # - self.previewLayout = qt.QVBoxLayout() - if showPreview: - self.previewLayout.addWidget(self.preview) - else: - self.preview.hide() - - # - # action related column (interacting with slicer) - # - self.loadableTableFrame = qt.QWidget() - self.loadableTableFrame.setMaximumHeight(200) - self.loadableTableLayout = qt.QVBoxLayout(self.loadableTableFrame) - self.layout().addWidget(self.loadableTableFrame) - - self.loadableTableLayout.addWidget(self.userFrame) - self.userFrame.hide() - - self.loadableTable = DICOMLoadableTable(self.userFrame) - self.loadableTable.itemChanged.connect(self.onLoadableTableItemChanged) - - # - # button row for action column - # - self.actionButtonsFrame = qt.QWidget() - self.actionButtonsFrame.setMaximumHeight(40) - self.actionButtonsFrame.objectName = 'ActionButtonsFrame' - self.layout().addWidget(self.actionButtonsFrame) - - self.actionButtonLayout = qt.QHBoxLayout() - self.actionButtonsFrame.setLayout(self.actionButtonLayout) - - self.uncheckAllButton = qt.QPushButton('Uncheck All') - self.actionButtonLayout.addWidget(self.uncheckAllButton) - self.uncheckAllButton.connect('clicked()', self.uncheckAllLoadables) - - self.actionButtonLayout.addStretch(0.05) - - self.examineButton = qt.QPushButton('Examine') - self.examineButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - self.actionButtonLayout.addWidget(self.examineButton) - self.examineButton.enabled = False - self.examineButton.connect('clicked()', self.examineForLoading) - - self.loadButton = qt.QPushButton('Load') - self.loadButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - self.loadButton.toolTip = 'Load selected items into the scene' - self.actionButtonLayout.addWidget(self.loadButton) - self.loadButton.connect('clicked()', self.loadCheckedLoadables) - - self.actionButtonLayout.addStretch(0.05) - - self.advancedViewButton = qt.QCheckBox('Advanced') - self.advancedViewButton.objectName = 'AdvancedViewCheckBox' - self.actionButtonLayout.addWidget(self.advancedViewButton) - self.advancedViewButton.checked = self.advancedView - self.advancedViewButton.toggled.connect(self.onAdvancedViewButton) - - if self.advancedView: - self.loadableTableFrame.visible = True - else: - self.loadableTableFrame.visible = False - self.examineButton.visible = False - self.uncheckAllButton.visible = False - - # - # Series selection - # - self.dicomBrowser.dicomTableManager().connect('seriesSelectionChanged(QStringList)', self.onSeriesSelected) - - # - # Loadable table widget (advanced) - # DICOM Plugins selection widget is moved to module panel - # - self.loadableTableLayout.addWidget(self.loadableTable) - self.updateButtonStates() - - def updateButtonStates(self): - if self.advancedView: - #self.loadButton.enabled = loadEnabled = loadEnabled or loadablesByPlugin[plugin] != [] - loadablesChecked = self.loadableTable.getNumberOfCheckedItems() > 0 - self.loadButton.enabled = loadablesChecked - self.examineButton.enabled = len(self.fileLists) != 0 - self.uncheckAllButton.enabled = loadablesChecked - else: - #seriesSelected = self.dicomBrowser.dicomTableManager().seriesTable().tableView().selectedIndexes() - self.loadButton.enabled = self.fileLists - - def onDirectoryImported(self): - """The dicom browser will emit multiple directoryImported - signals during the same operation, so we collapse them - into a single check for compatible extensions.""" - if not hasattr(slicer.app, 'extensionsManagerModel'): - # Slicer may not be built with extensions manager support - return - if not self.extensionCheckPending: - self.extensionCheckPending = True - - def timerCallback(): - # Prompting for extension may be undesirable in custom applications. - # DICOM/PromptForExtensions key can be used to disable this feature. - promptForExtensionsEnabled = settingsValue('DICOM/PromptForExtensions', True, converter=toBool) - if promptForExtensionsEnabled: - self.promptForExtensions() - self.extensionCheckPending = False + closed = qt.Signal() # Invoked when the dicom widget is closed using the close method - qt.QTimer.singleShot(0, timerCallback) - - def promptForExtensions(self): - extensionsToOffer = self.checkForExtensions() - if len(extensionsToOffer) != 0: - if len(extensionsToOffer) == 1: - pluralOrNot = " is" - else: - pluralOrNot = "s are" - message = "The following data type%s in your database:\n\n" % pluralOrNot - displayedTypeDescriptions = [] - for extension in extensionsToOffer: - typeDescription = extension['typeDescription'] - if not typeDescription in displayedTypeDescriptions: - # only display each data type only once - message += ' ' + typeDescription + '\n' - displayedTypeDescriptions.append(typeDescription) - message += "\nThe following extension%s not installed, but may help you work with this data:\n\n" % pluralOrNot - displayedExtensionNames = [] - for extension in extensionsToOffer: - extensionName = extension['name'] - if not extensionName in displayedExtensionNames: - # only display each extension name only once - message += ' ' + extensionName + '\n' - displayedExtensionNames.append(extensionName) - message += "\nYou can install extensions using the Extensions Manager option from the View menu." - slicer.util.infoDisplay(message, parent=self, windowTitle='DICOM') - - def checkForExtensions(self): - """Check to see if there - are any registered extensions that might be available to - help the user work with data in the database. - - 1) load extension json description - 2) load info for each series - 3) check if data matches - - then return matches - - See - https://mantisarchive.slicer.org/view.php?id=4146 - """ + def __init__(self, dicomBrowser=None, parent="mainWindow"): + VTKObservationMixin.__init__(self) + qt.QWidget.__init__(self, slicer.util.mainWindow() if parent == "mainWindow" else parent) - # 1 - load json - import logging, os, json - logging.info('Imported a DICOM directory, checking for extensions') - modulePath = os.path.dirname(slicer.modules.dicom.path) - extensionDescriptorPath = os.path.join(modulePath, 'DICOMExtensions.json') - try: - with open(extensionDescriptorPath) as extensionDescriptorFP: - extensionDescriptor = extensionDescriptorFP.read() - dicomExtensions = json.loads(extensionDescriptor) - except: - logging.error('Cannot access DICOMExtensions.json file') - return - - # 2 - get series info - # - iterate though metadata - should be fast even with large database - # - the fileValue call checks the tag cache so it's fast - modalityTag = "0008,0060" - sopClassUIDTag = "0008,0016" - sopClassUIDs = set() - modalities = set() - for patient in slicer.dicomDatabase.patients(): - for study in slicer.dicomDatabase.studiesForPatient(patient): - for series in slicer.dicomDatabase.seriesForStudy(study): - instance0 = slicer.dicomDatabase.filesForSeries(series, 1)[0] - modality = slicer.dicomDatabase.fileValue(instance0, modalityTag) - sopClassUID = slicer.dicomDatabase.fileValue(instance0, sopClassUIDTag) - modalities.add(modality) - sopClassUIDs.add(sopClassUID) - - # 3 - check if data matches - extensionsManagerModel = slicer.app.extensionsManagerModel() - installedExtensions = extensionsManagerModel.installedExtensions - extensionsToOffer = [] - for extension in dicomExtensions['extensions']: - extensionName = extension['name'] - if extensionName not in installedExtensions: - tagValues = extension['tagValues'] - if 'Modality' in tagValues: - for modality in tagValues['Modality']: - if modality in modalities: - extensionsToOffer.append(extension) - if 'SOPClassUID' in tagValues: - for sopClassUID in tagValues['SOPClassUID']: - if sopClassUID in sopClassUIDs: - extensionsToOffer.append(extension) - return extensionsToOffer - - def setBrowserPersistence(self, state): - self.browserPersistent = state - self.settings.setValue('DICOM/BrowserPersistent', bool(self.browserPersistent)) - - def onAdvancedViewButton(self, checked): - self.advancedView = checked - advancedWidgets = [self.loadableTableFrame, self.examineButton, self.uncheckAllButton] - for widget in advancedWidgets: - widget.visible = self.advancedView - self.updateButtonStates() - - self.settings.setValue('DICOM/advancedView', int(self.advancedView)) - - def onHorizontalViewCheckBox(self): - horizontal = self.horizontalViewCheckBox.checked - self.dicomBrowser.dicomTableManager().tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical - self.settings.setValue('DICOM/horizontalTables', int(horizontal)) - - def onSeriesSelected(self, seriesUIDList): - self.loadableTable.setLoadables([]) - self.fileLists = self.getFileListsForRole(seriesUIDList, "SeriesUIDList") - self.updateButtonStates() - - def getFileListsForRole(self, uidArgument, role): - fileLists = [] - if role == "Series": - fileLists.append(slicer.dicomDatabase.filesForSeries(uidArgument)) - if role == "SeriesUIDList": - for uid in uidArgument: - uid = uid.replace("'", "") - fileLists.append(slicer.dicomDatabase.filesForSeries(uid)) - if role == "Study": - series = slicer.dicomDatabase.seriesForStudy(uidArgument) - for serie in series: - fileLists.append(slicer.dicomDatabase.filesForSeries(serie)) - if role == "Patient": - studies = slicer.dicomDatabase.studiesForPatient(uidArgument) - for study in studies: - series = slicer.dicomDatabase.seriesForStudy(study) - for serie in series: - fileList = slicer.dicomDatabase.filesForSeries(serie) - fileLists.append(fileList) - return fileLists - - def uncheckAllLoadables(self): - self.loadableTable.uncheckAll() - - def onLoadableTableItemChanged(self, item): - self.updateButtonStates() - - def examineForLoading(self): - """For selected plugins, give user the option - of what to load""" - - (self.loadablesByPlugin, loadEnabled) = self.getLoadablesFromFileLists(self.fileLists) - DICOMLib.selectHighestConfidenceLoadables(self.loadablesByPlugin) - self.loadableTable.setLoadables(self.loadablesByPlugin) - self.updateButtonStates() - - def getLoadablesFromFileLists(self, fileLists): - """Take list of file lists, return loadables by plugin dictionary - """ + self.pluginInstances = {} + self.fileLists = [] + self.extensionCheckPending = False - loadablesByPlugin = {} - loadEnabled = False - - # Get selected plugins from application settings - # Settings are filled in DICOMWidget using DICOMPluginSelector - settings = qt.QSettings() - selectedPlugins = [] - if settings.contains('DICOM/disabledPlugins/size'): - size = settings.beginReadArray('DICOM/disabledPlugins') - disabledPlugins = [] - - for i in range(size): - settings.setArrayIndex(i) - disabledPlugins.append(str(settings.allKeys()[0])) - settings.endArray() - - for pluginClass in slicer.modules.dicomPlugins: - if pluginClass not in disabledPlugins: - selectedPlugins.append(pluginClass) - else: - # All DICOM plugins would be enabled by default - for pluginClass in slicer.modules.dicomPlugins: - selectedPlugins.append(pluginClass) - - allFileCount = missingFileCount = 0 - for fileList in fileLists: - for filePath in fileList: - allFileCount += 1 - if not os.path.exists(filePath): - missingFileCount += 1 - - messages = [] - if missingFileCount > 0: - messages.append("Warning: %d of %d selected files listed in the database cannot be found on disk." % (missingFileCount, allFileCount)) - - if missingFileCount < allFileCount: - progressDialog = slicer.util.createProgressDialog(parent=self, value=0, maximum=100) - - def progressCallback(progressDialog, progressLabel, progressValue): - progressDialog.labelText = '\nChecking %s' % progressLabel - slicer.app.processEvents() - progressDialog.setValue(progressValue) - slicer.app.processEvents() - cancelled = progressDialog.wasCanceled - return cancelled - - loadablesByPlugin, loadEnabled = DICOMLib.getLoadablesFromFileLists(fileLists, selectedPlugins, messages, - lambda progressLabel, progressValue, progressDialog=progressDialog: progressCallback(progressDialog, progressLabel, progressValue), - self.pluginInstances) - - progressDialog.close() - - if messages: - slicer.util.warningDisplay("Warning: %s\n\nSee python console for error message." % ' '.join(messages), - windowTitle="DICOM", parent=self) - - return loadablesByPlugin, loadEnabled - - def isFileListInCheckedLoadables(self, fileList): - for plugin in self.loadablesByPlugin: - for loadable in self.loadablesByPlugin[plugin]: - if len(loadable.files) != len(fileList) or len(loadable.files) == 0: - continue - inputFileListCopy = copy.deepcopy(fileList) - loadableFileListCopy = copy.deepcopy(loadable.files) + self.settings = qt.QSettings() + + self.dicomBrowser = dicomBrowser if dicomBrowser is not None else slicer.app.createDICOMBrowserForMainDatabase() + + self.browserPersistent = settingsValue('DICOM/BrowserPersistent', False, converter=toBool) + self.advancedView = settingsValue('DICOM/advancedView', 0, converter=int) + self.horizontalTables = settingsValue('DICOM/horizontalTables', 0, converter=int) + + self.setup() + + self.dicomBrowser.connect('directoryImported()', self.onDirectoryImported) + self.dicomBrowser.connect('sendRequested(QStringList)', self.onSend) + + # Load when double-clicked on an item in the browser + self.dicomBrowser.dicomTableManager().connect('patientsDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked) + self.dicomBrowser.dicomTableManager().connect('studiesDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked) + self.dicomBrowser.dicomTableManager().connect('seriesDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked) + + def open(self): + self.show() + + def close(self): + self.hide() + self.closed.emit() + + def onSend(self, fileList): + if len(fileList): + sendDialog = DICOMLib.DICOMSendDialog(fileList, self) + + def setup(self, showPreview=False): + """ + main window is a frame with widgets from the app + widget repacked into it along with slicer-specific + extra widgets + """ + + self.setWindowTitle('DICOM Browser') + self.setLayout(qt.QVBoxLayout()) + + self.dicomBrowser.databaseDirectorySelectorVisible = False + self.dicomBrowser.toolbarVisible = False + self.dicomBrowser.sendActionVisible = True + self.dicomBrowser.databaseDirectorySettingsKey = slicer.dicomDatabaseDirectorySettingsKey + self.dicomBrowser.dicomTableManager().dynamicTableLayout = False + horizontal = self.settings.setValue('DICOM/horizontalTables', 0) + self.dicomBrowser.dicomTableManager().tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical + self.layout().addWidget(self.dicomBrowser) + + self.userFrame = qt.QWidget() + self.preview = qt.QWidget() + + # + # preview related column + # + self.previewLayout = qt.QVBoxLayout() + if showPreview: + self.previewLayout.addWidget(self.preview) + else: + self.preview.hide() + + # + # action related column (interacting with slicer) + # + self.loadableTableFrame = qt.QWidget() + self.loadableTableFrame.setMaximumHeight(200) + self.loadableTableLayout = qt.QVBoxLayout(self.loadableTableFrame) + self.layout().addWidget(self.loadableTableFrame) + + self.loadableTableLayout.addWidget(self.userFrame) + self.userFrame.hide() + + self.loadableTable = DICOMLoadableTable(self.userFrame) + self.loadableTable.itemChanged.connect(self.onLoadableTableItemChanged) + + # + # button row for action column + # + self.actionButtonsFrame = qt.QWidget() + self.actionButtonsFrame.setMaximumHeight(40) + self.actionButtonsFrame.objectName = 'ActionButtonsFrame' + self.layout().addWidget(self.actionButtonsFrame) + + self.actionButtonLayout = qt.QHBoxLayout() + self.actionButtonsFrame.setLayout(self.actionButtonLayout) + + self.uncheckAllButton = qt.QPushButton('Uncheck All') + self.actionButtonLayout.addWidget(self.uncheckAllButton) + self.uncheckAllButton.connect('clicked()', self.uncheckAllLoadables) + + self.actionButtonLayout.addStretch(0.05) + + self.examineButton = qt.QPushButton('Examine') + self.examineButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) + self.actionButtonLayout.addWidget(self.examineButton) + self.examineButton.enabled = False + self.examineButton.connect('clicked()', self.examineForLoading) + + self.loadButton = qt.QPushButton('Load') + self.loadButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) + self.loadButton.toolTip = 'Load selected items into the scene' + self.actionButtonLayout.addWidget(self.loadButton) + self.loadButton.connect('clicked()', self.loadCheckedLoadables) + + self.actionButtonLayout.addStretch(0.05) + + self.advancedViewButton = qt.QCheckBox('Advanced') + self.advancedViewButton.objectName = 'AdvancedViewCheckBox' + self.actionButtonLayout.addWidget(self.advancedViewButton) + self.advancedViewButton.checked = self.advancedView + self.advancedViewButton.toggled.connect(self.onAdvancedViewButton) + + if self.advancedView: + self.loadableTableFrame.visible = True + else: + self.loadableTableFrame.visible = False + self.examineButton.visible = False + self.uncheckAllButton.visible = False + + # + # Series selection + # + self.dicomBrowser.dicomTableManager().connect('seriesSelectionChanged(QStringList)', self.onSeriesSelected) + + # + # Loadable table widget (advanced) + # DICOM Plugins selection widget is moved to module panel + # + self.loadableTableLayout.addWidget(self.loadableTable) + self.updateButtonStates() + + def updateButtonStates(self): + if self.advancedView: + # self.loadButton.enabled = loadEnabled = loadEnabled or loadablesByPlugin[plugin] != [] + loadablesChecked = self.loadableTable.getNumberOfCheckedItems() > 0 + self.loadButton.enabled = loadablesChecked + self.examineButton.enabled = len(self.fileLists) != 0 + self.uncheckAllButton.enabled = loadablesChecked + else: + # seriesSelected = self.dicomBrowser.dicomTableManager().seriesTable().tableView().selectedIndexes() + self.loadButton.enabled = self.fileLists + + def onDirectoryImported(self): + """The dicom browser will emit multiple directoryImported + signals during the same operation, so we collapse them + into a single check for compatible extensions.""" + if not hasattr(slicer.app, 'extensionsManagerModel'): + # Slicer may not be built with extensions manager support + return + if not self.extensionCheckPending: + self.extensionCheckPending = True + + def timerCallback(): + # Prompting for extension may be undesirable in custom applications. + # DICOM/PromptForExtensions key can be used to disable this feature. + promptForExtensionsEnabled = settingsValue('DICOM/PromptForExtensions', True, converter=toBool) + if promptForExtensionsEnabled: + self.promptForExtensions() + self.extensionCheckPending = False + + qt.QTimer.singleShot(0, timerCallback) + + def promptForExtensions(self): + extensionsToOffer = self.checkForExtensions() + if len(extensionsToOffer) != 0: + if len(extensionsToOffer) == 1: + pluralOrNot = " is" + else: + pluralOrNot = "s are" + message = "The following data type%s in your database:\n\n" % pluralOrNot + displayedTypeDescriptions = [] + for extension in extensionsToOffer: + typeDescription = extension['typeDescription'] + if typeDescription not in displayedTypeDescriptions: + # only display each data type only once + message += ' ' + typeDescription + '\n' + displayedTypeDescriptions.append(typeDescription) + message += "\nThe following extension%s not installed, but may help you work with this data:\n\n" % pluralOrNot + displayedExtensionNames = [] + for extension in extensionsToOffer: + extensionName = extension['name'] + if extensionName not in displayedExtensionNames: + # only display each extension name only once + message += ' ' + extensionName + '\n' + displayedExtensionNames.append(extensionName) + message += "\nYou can install extensions using the Extensions Manager option from the View menu." + slicer.util.infoDisplay(message, parent=self, windowTitle='DICOM') + + def checkForExtensions(self): + """Check to see if there + are any registered extensions that might be available to + help the user work with data in the database. + + 1) load extension json description + 2) load info for each series + 3) check if data matches + + then return matches + + See + https://mantisarchive.slicer.org/view.php?id=4146 + """ + + # 1 - load json + import logging, os, json + logging.info('Imported a DICOM directory, checking for extensions') + modulePath = os.path.dirname(slicer.modules.dicom.path) + extensionDescriptorPath = os.path.join(modulePath, 'DICOMExtensions.json') try: - inputFileListCopy.sort() - loadableFileListCopy.sort() - except Exception: - pass - isEqual = True - for pair in zip(inputFileListCopy, loadableFileListCopy): - if pair[0] != pair[1]: - print(f"{pair[0]} != {pair[1]}") - isEqual = False - break - if not isEqual: - continue - return True - return False - - def patientStudySeriesDoubleClicked(self): - if self.advancedViewButton.checkState() == 0: - # basic mode - self.loadCheckedLoadables() - else: - # advanced mode, just examine the double-clicked item, do not load - self.examineForLoading() - - def loadCheckedLoadables(self): - """Invoke the load method on each plugin for the loadable - (DICOMLoadable or qSlicerDICOMLoadable) instances that are selected""" - if self.advancedViewButton.checkState() == 0: - self.examineForLoading() - - self.loadableTable.updateSelectedFromCheckstate() - - # TODO: add check that disables all referenced stuff to be considered? - # get all the references from the checked loadables - referencedFileLists = [] - for plugin in self.loadablesByPlugin: - for loadable in self.loadablesByPlugin[plugin]: - if hasattr(loadable, 'referencedInstanceUIDs'): - instanceFileList = [] - for instance in loadable.referencedInstanceUIDs: - instanceFile = slicer.dicomDatabase.fileForInstance(instance) - if instanceFile != '': - instanceFileList.append(instanceFile) - if len(instanceFileList) and not self.isFileListInCheckedLoadables(instanceFileList): - referencedFileLists.append(instanceFileList) - - # if applicable, find all loadables from the file lists - loadEnabled = False - if len(referencedFileLists): - (self.referencedLoadables, loadEnabled) = self.getLoadablesFromFileLists(referencedFileLists) - - automaticallyLoadReferences = int(slicer.util.settingsValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.InvalidRole)) - if slicer.app.commandOptions().testingEnabled: - automaticallyLoadReferences = qt.QMessageBox.No - if loadEnabled and automaticallyLoadReferences == qt.QMessageBox.InvalidRole: - self.showReferenceDialogAndProceed() - elif loadEnabled and automaticallyLoadReferences == qt.QMessageBox.Yes: - self.addReferencesAndProceed() - else: - self.proceedWithReferencedLoadablesSelection() - - return - - def showReferenceDialogAndProceed(self): - referencesDialog = DICOMReferencesDialog(self, loadables=self.referencedLoadables) - answer = referencesDialog.exec_() - if referencesDialog.rememberChoiceAndStopAskingCheckbox.checked == True: - if answer == qt.QMessageBox.Yes: - qt.QSettings().setValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.Yes) - if answer == qt.QMessageBox.No: - qt.QSettings().setValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.No) - if answer == qt.QMessageBox.Yes: - # each check box corresponds to a referenced loadable that was selected by examine; - # if the user confirmed that reference should be loaded, add it to the self.loadablesByPlugin dictionary - for plugin in self.referencedLoadables: - for loadable in [l for l in self.referencedLoadables[plugin] if l.selected]: - if referencesDialog.checkboxes[loadable].checked: - self.loadablesByPlugin[plugin].append(loadable) - self.loadablesByPlugin[plugin] = list(set(self.loadablesByPlugin[plugin])) - self.proceedWithReferencedLoadablesSelection() - elif answer == qt.QMessageBox.No: - self.proceedWithReferencedLoadablesSelection() - - def addReferencesAndProceed(self): - for plugin in self.referencedLoadables: - for loadable in [l for l in self.referencedLoadables[plugin] if l.selected]: - self.loadablesByPlugin[plugin].append(loadable) - self.loadablesByPlugin[plugin] = list(set(self.loadablesByPlugin[plugin])) - self.proceedWithReferencedLoadablesSelection() - - def proceedWithReferencedLoadablesSelection(self): - if not self.warnUserIfLoadableWarningsAndProceed(): - return - - progressDialog = slicer.util.createProgressDialog(parent=self, value=0, maximum=100) - - def progressCallback(progressDialog, progressLabel, progressValue): - progressDialog.labelText = '\nLoading %s' % progressLabel - slicer.app.processEvents() - progressDialog.setValue(progressValue) - slicer.app.processEvents() - cancelled = progressDialog.wasCanceled - return cancelled - - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - - messages = [] - loadedNodeIDs = DICOMLib.loadLoadables(self.loadablesByPlugin, messages, - lambda progressLabel, progressValue, progressDialog=progressDialog: progressCallback(progressDialog, progressLabel, progressValue)) - - loadedFileParameters = {} - loadedFileParameters['nodeIDs'] = loadedNodeIDs - slicer.app.ioManager().emitNewFileLoaded(loadedFileParameters) - - qt.QApplication.restoreOverrideCursor() - - progressDialog.close() - - if messages: - slicer.util.warningDisplay('\n'.join(messages), windowTitle='DICOM loading') - - self.onLoadingFinished() - - def warnUserIfLoadableWarningsAndProceed(self): - warningsInSelectedLoadables = False - details = "" - for plugin in self.loadablesByPlugin: - for loadable in self.loadablesByPlugin[plugin]: - if loadable.selected and loadable.warning != "": - warningsInSelectedLoadables = True - logging.warning('Warning in DICOM plugin ' + plugin.loadType + ' when examining loadable ' + loadable.name + - ': ' + loadable.warning) - details += loadable.name + " [" + plugin.loadType + "]: " + loadable.warning + "\n" - if warningsInSelectedLoadables: - warning = "Warnings detected during load. Examine data in Advanced mode for details. Load anyway?" - if not slicer.util.confirmOkCancelDisplay(warning, parent=self, detailedText=details): + with open(extensionDescriptorPath) as extensionDescriptorFP: + extensionDescriptor = extensionDescriptorFP.read() + dicomExtensions = json.loads(extensionDescriptor) + except: + logging.error('Cannot access DICOMExtensions.json file') + return + + # 2 - get series info + # - iterate though metadata - should be fast even with large database + # - the fileValue call checks the tag cache so it's fast + modalityTag = "0008,0060" + sopClassUIDTag = "0008,0016" + sopClassUIDs = set() + modalities = set() + for patient in slicer.dicomDatabase.patients(): + for study in slicer.dicomDatabase.studiesForPatient(patient): + for series in slicer.dicomDatabase.seriesForStudy(study): + instance0 = slicer.dicomDatabase.filesForSeries(series, 1)[0] + modality = slicer.dicomDatabase.fileValue(instance0, modalityTag) + sopClassUID = slicer.dicomDatabase.fileValue(instance0, sopClassUIDTag) + modalities.add(modality) + sopClassUIDs.add(sopClassUID) + + # 3 - check if data matches + extensionsManagerModel = slicer.app.extensionsManagerModel() + installedExtensions = extensionsManagerModel.installedExtensions + extensionsToOffer = [] + for extension in dicomExtensions['extensions']: + extensionName = extension['name'] + if extensionName not in installedExtensions: + tagValues = extension['tagValues'] + if 'Modality' in tagValues: + for modality in tagValues['Modality']: + if modality in modalities: + extensionsToOffer.append(extension) + if 'SOPClassUID' in tagValues: + for sopClassUID in tagValues['SOPClassUID']: + if sopClassUID in sopClassUIDs: + extensionsToOffer.append(extension) + return extensionsToOffer + + def setBrowserPersistence(self, state): + self.browserPersistent = state + self.settings.setValue('DICOM/BrowserPersistent', bool(self.browserPersistent)) + + def onAdvancedViewButton(self, checked): + self.advancedView = checked + advancedWidgets = [self.loadableTableFrame, self.examineButton, self.uncheckAllButton] + for widget in advancedWidgets: + widget.visible = self.advancedView + self.updateButtonStates() + + self.settings.setValue('DICOM/advancedView', int(self.advancedView)) + + def onHorizontalViewCheckBox(self): + horizontal = self.horizontalViewCheckBox.checked + self.dicomBrowser.dicomTableManager().tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical + self.settings.setValue('DICOM/horizontalTables', int(horizontal)) + + def onSeriesSelected(self, seriesUIDList): + self.loadableTable.setLoadables([]) + self.fileLists = self.getFileListsForRole(seriesUIDList, "SeriesUIDList") + self.updateButtonStates() + + def getFileListsForRole(self, uidArgument, role): + fileLists = [] + if role == "Series": + fileLists.append(slicer.dicomDatabase.filesForSeries(uidArgument)) + if role == "SeriesUIDList": + for uid in uidArgument: + uid = uid.replace("'", "") + fileLists.append(slicer.dicomDatabase.filesForSeries(uid)) + if role == "Study": + series = slicer.dicomDatabase.seriesForStudy(uidArgument) + for serie in series: + fileLists.append(slicer.dicomDatabase.filesForSeries(serie)) + if role == "Patient": + studies = slicer.dicomDatabase.studiesForPatient(uidArgument) + for study in studies: + series = slicer.dicomDatabase.seriesForStudy(study) + for serie in series: + fileList = slicer.dicomDatabase.filesForSeries(serie) + fileLists.append(fileList) + return fileLists + + def uncheckAllLoadables(self): + self.loadableTable.uncheckAll() + + def onLoadableTableItemChanged(self, item): + self.updateButtonStates() + + def examineForLoading(self): + """For selected plugins, give user the option + of what to load""" + + (self.loadablesByPlugin, loadEnabled) = self.getLoadablesFromFileLists(self.fileLists) + DICOMLib.selectHighestConfidenceLoadables(self.loadablesByPlugin) + self.loadableTable.setLoadables(self.loadablesByPlugin) + self.updateButtonStates() + + def getLoadablesFromFileLists(self, fileLists): + """Take list of file lists, return loadables by plugin dictionary + """ + + loadablesByPlugin = {} + loadEnabled = False + + # Get selected plugins from application settings + # Settings are filled in DICOMWidget using DICOMPluginSelector + settings = qt.QSettings() + selectedPlugins = [] + if settings.contains('DICOM/disabledPlugins/size'): + size = settings.beginReadArray('DICOM/disabledPlugins') + disabledPlugins = [] + + for i in range(size): + settings.setArrayIndex(i) + disabledPlugins.append(str(settings.allKeys()[0])) + settings.endArray() + + for pluginClass in slicer.modules.dicomPlugins: + if pluginClass not in disabledPlugins: + selectedPlugins.append(pluginClass) + else: + # All DICOM plugins would be enabled by default + for pluginClass in slicer.modules.dicomPlugins: + selectedPlugins.append(pluginClass) + + allFileCount = missingFileCount = 0 + for fileList in fileLists: + for filePath in fileList: + allFileCount += 1 + if not os.path.exists(filePath): + missingFileCount += 1 + + messages = [] + if missingFileCount > 0: + messages.append("Warning: %d of %d selected files listed in the database cannot be found on disk." % (missingFileCount, allFileCount)) + + if missingFileCount < allFileCount: + progressDialog = slicer.util.createProgressDialog(parent=self, value=0, maximum=100) + + def progressCallback(progressDialog, progressLabel, progressValue): + progressDialog.labelText = '\nChecking %s' % progressLabel + slicer.app.processEvents() + progressDialog.setValue(progressValue) + slicer.app.processEvents() + cancelled = progressDialog.wasCanceled + return cancelled + + loadablesByPlugin, loadEnabled = DICOMLib.getLoadablesFromFileLists(fileLists, selectedPlugins, messages, + lambda progressLabel, progressValue, progressDialog=progressDialog: progressCallback(progressDialog, progressLabel, progressValue), + self.pluginInstances) + + progressDialog.close() + + if messages: + slicer.util.warningDisplay("Warning: %s\n\nSee python console for error message." % ' '.join(messages), + windowTitle="DICOM", parent=self) + + return loadablesByPlugin, loadEnabled + + def isFileListInCheckedLoadables(self, fileList): + for plugin in self.loadablesByPlugin: + for loadable in self.loadablesByPlugin[plugin]: + if len(loadable.files) != len(fileList) or len(loadable.files) == 0: + continue + inputFileListCopy = copy.deepcopy(fileList) + loadableFileListCopy = copy.deepcopy(loadable.files) + try: + inputFileListCopy.sort() + loadableFileListCopy.sort() + except Exception: + pass + isEqual = True + for pair in zip(inputFileListCopy, loadableFileListCopy): + if pair[0] != pair[1]: + print(f"{pair[0]} != {pair[1]}") + isEqual = False + break + if not isEqual: + continue + return True return False - return True - def onLoadingFinished(self): - if not self.browserPersistent: - self.close() + def patientStudySeriesDoubleClicked(self): + if self.advancedViewButton.checkState() == 0: + # basic mode + self.loadCheckedLoadables() + else: + # advanced mode, just examine the double-clicked item, do not load + self.examineForLoading() + + def loadCheckedLoadables(self): + """Invoke the load method on each plugin for the loadable + (DICOMLoadable or qSlicerDICOMLoadable) instances that are selected""" + if self.advancedViewButton.checkState() == 0: + self.examineForLoading() + + self.loadableTable.updateSelectedFromCheckstate() + + # TODO: add check that disables all referenced stuff to be considered? + # get all the references from the checked loadables + referencedFileLists = [] + for plugin in self.loadablesByPlugin: + for loadable in self.loadablesByPlugin[plugin]: + if hasattr(loadable, 'referencedInstanceUIDs'): + instanceFileList = [] + for instance in loadable.referencedInstanceUIDs: + instanceFile = slicer.dicomDatabase.fileForInstance(instance) + if instanceFile != '': + instanceFileList.append(instanceFile) + if len(instanceFileList) and not self.isFileListInCheckedLoadables(instanceFileList): + referencedFileLists.append(instanceFileList) + + # if applicable, find all loadables from the file lists + loadEnabled = False + if len(referencedFileLists): + (self.referencedLoadables, loadEnabled) = self.getLoadablesFromFileLists(referencedFileLists) + + automaticallyLoadReferences = int(slicer.util.settingsValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.InvalidRole)) + if slicer.app.commandOptions().testingEnabled: + automaticallyLoadReferences = qt.QMessageBox.No + if loadEnabled and automaticallyLoadReferences == qt.QMessageBox.InvalidRole: + self.showReferenceDialogAndProceed() + elif loadEnabled and automaticallyLoadReferences == qt.QMessageBox.Yes: + self.addReferencesAndProceed() + else: + self.proceedWithReferencedLoadablesSelection() + + return + + def showReferenceDialogAndProceed(self): + referencesDialog = DICOMReferencesDialog(self, loadables=self.referencedLoadables) + answer = referencesDialog.exec_() + if referencesDialog.rememberChoiceAndStopAskingCheckbox.checked is True: + if answer == qt.QMessageBox.Yes: + qt.QSettings().setValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.Yes) + if answer == qt.QMessageBox.No: + qt.QSettings().setValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.No) + if answer == qt.QMessageBox.Yes: + # each check box corresponds to a referenced loadable that was selected by examine; + # if the user confirmed that reference should be loaded, add it to the self.loadablesByPlugin dictionary + for plugin in self.referencedLoadables: + for loadable in [loadable_item for loadable_item in self.referencedLoadables[plugin] if loadable_item.selected]: + if referencesDialog.checkboxes[loadable].checked: + self.loadablesByPlugin[plugin].append(loadable) + self.loadablesByPlugin[plugin] = list(set(self.loadablesByPlugin[plugin])) + self.proceedWithReferencedLoadablesSelection() + elif answer == qt.QMessageBox.No: + self.proceedWithReferencedLoadablesSelection() + + def addReferencesAndProceed(self): + for plugin in self.referencedLoadables: + for loadable in [loadable_item for loadable_item in self.referencedLoadables[plugin] if loadable_item.selected]: + self.loadablesByPlugin[plugin].append(loadable) + self.loadablesByPlugin[plugin] = list(set(self.loadablesByPlugin[plugin])) + self.proceedWithReferencedLoadablesSelection() + + def proceedWithReferencedLoadablesSelection(self): + if not self.warnUserIfLoadableWarningsAndProceed(): + return + + progressDialog = slicer.util.createProgressDialog(parent=self, value=0, maximum=100) + + def progressCallback(progressDialog, progressLabel, progressValue): + progressDialog.labelText = '\nLoading %s' % progressLabel + slicer.app.processEvents() + progressDialog.setValue(progressValue) + slicer.app.processEvents() + cancelled = progressDialog.wasCanceled + return cancelled + + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + + messages = [] + loadedNodeIDs = DICOMLib.loadLoadables(self.loadablesByPlugin, messages, + lambda progressLabel, progressValue, progressDialog=progressDialog: progressCallback(progressDialog, progressLabel, progressValue)) + + loadedFileParameters = {} + loadedFileParameters['nodeIDs'] = loadedNodeIDs + slicer.app.ioManager().emitNewFileLoaded(loadedFileParameters) + + qt.QApplication.restoreOverrideCursor() + + progressDialog.close() + + if messages: + slicer.util.warningDisplay('\n'.join(messages), windowTitle='DICOM loading') + + self.onLoadingFinished() + + def warnUserIfLoadableWarningsAndProceed(self): + warningsInSelectedLoadables = False + details = "" + for plugin in self.loadablesByPlugin: + for loadable in self.loadablesByPlugin[plugin]: + if loadable.selected and loadable.warning != "": + warningsInSelectedLoadables = True + logging.warning('Warning in DICOM plugin ' + plugin.loadType + ' when examining loadable ' + loadable.name + + ': ' + loadable.warning) + details += loadable.name + " [" + plugin.loadType + "]: " + loadable.warning + "\n" + if warningsInSelectedLoadables: + warning = "Warnings detected during load. Examine data in Advanced mode for details. Load anyway?" + if not slicer.util.confirmOkCancelDisplay(warning, parent=self, detailedText=details): + return False + return True + + def onLoadingFinished(self): + if not self.browserPersistent: + self.close() class DICOMReferencesDialog(qt.QMessageBox): - WINDOW_TITLE = "Referenced datasets found" - WINDOW_TEXT = "The loaded DICOM objects contain references to other datasets you did not select for loading. Please " \ - "select Yes if you would like to load the following referenced datasets, No if you only want to load the " \ - "originally selected series, or Cancel to abort loading." - - def __init__(self, parent, loadables): - super().__init__(parent) - self.loadables = loadables - self.checkboxes = dict() - self.setup() - - def setup(self): - self._setBasicProperties() - self._addTextLabel() - self._addLoadableCheckboxes() - self.rememberChoiceAndStopAskingCheckbox = qt.QCheckBox('Remember choice and stop asking') - self.rememberChoiceAndStopAskingCheckbox.toolTip = 'Can be changed later in Application Settings / DICOM' - self.yesButton = self.addButton(self.Yes) - self.yesButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)) - self.noButton = self.addButton(self.No) - self.noButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)) - self.cancelButton = self.addButton(self.Cancel) - self.cancelButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)) - self.layout().addWidget(self.yesButton, 3, 0, 1, 1) - self.layout().addWidget(self.noButton, 3, 1, 1, 1) - self.layout().addWidget(self.cancelButton, 3, 2, 1, 1) - self.layout().addWidget(self.rememberChoiceAndStopAskingCheckbox, 2, 0, 1, 3) - - def _setBasicProperties(self): - self.layout().setSpacing(9) - self.setWindowTitle(self.WINDOW_TITLE) - fontMetrics = qt.QFontMetrics(qt.QApplication.font(self)) - try: - self.setMinimumWidth(fontMetrics.horizontalAdvance(self.WINDOW_TITLE)) - except AttributeError: - # Support Qt < 5.11 lacking QFontMetrics::horizontalAdvance() - self.setMinimumWidth(fontMetrics.width(self.WINDOW_TITLE)) - - def _addTextLabel(self): - label = qt.QLabel(self.WINDOW_TEXT) - label.wordWrap = True - self.layout().addWidget(label, 0, 0, 1, 3) - - def _addLoadableCheckboxes(self): - self.checkBoxGroupBox = qt.QGroupBox("References") - self.checkBoxGroupBox.setLayout(qt.QFormLayout()) - for plugin in self.loadables: - for loadable in [l for l in self.loadables[plugin] if l.selected]: - checkBoxText = loadable.name + ' (' + plugin.loadType + ') ' - cb = qt.QCheckBox(checkBoxText, self) - cb.checked = True - cb.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)) - self.checkboxes[loadable] = cb - self.checkBoxGroupBox.layout().addWidget(cb) - self.layout().addWidget(self.checkBoxGroupBox, 1, 0, 1, 3) + WINDOW_TITLE = "Referenced datasets found" + WINDOW_TEXT = "The loaded DICOM objects contain references to other datasets you did not select for loading. Please " \ + "select Yes if you would like to load the following referenced datasets, No if you only want to load the " \ + "originally selected series, or Cancel to abort loading." + + def __init__(self, parent, loadables): + super().__init__(parent) + self.loadables = loadables + self.checkboxes = dict() + self.setup() + + def setup(self): + self._setBasicProperties() + self._addTextLabel() + self._addLoadableCheckboxes() + self.rememberChoiceAndStopAskingCheckbox = qt.QCheckBox('Remember choice and stop asking') + self.rememberChoiceAndStopAskingCheckbox.toolTip = 'Can be changed later in Application Settings / DICOM' + self.yesButton = self.addButton(self.Yes) + self.yesButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)) + self.noButton = self.addButton(self.No) + self.noButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)) + self.cancelButton = self.addButton(self.Cancel) + self.cancelButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)) + self.layout().addWidget(self.yesButton, 3, 0, 1, 1) + self.layout().addWidget(self.noButton, 3, 1, 1, 1) + self.layout().addWidget(self.cancelButton, 3, 2, 1, 1) + self.layout().addWidget(self.rememberChoiceAndStopAskingCheckbox, 2, 0, 1, 3) + + def _setBasicProperties(self): + self.layout().setSpacing(9) + self.setWindowTitle(self.WINDOW_TITLE) + fontMetrics = qt.QFontMetrics(qt.QApplication.font(self)) + try: + self.setMinimumWidth(fontMetrics.horizontalAdvance(self.WINDOW_TITLE)) + except AttributeError: + # Support Qt < 5.11 lacking QFontMetrics::horizontalAdvance() + self.setMinimumWidth(fontMetrics.width(self.WINDOW_TITLE)) + + def _addTextLabel(self): + label = qt.QLabel(self.WINDOW_TEXT) + label.wordWrap = True + self.layout().addWidget(label, 0, 0, 1, 3) + + def _addLoadableCheckboxes(self): + self.checkBoxGroupBox = qt.QGroupBox("References") + self.checkBoxGroupBox.setLayout(qt.QFormLayout()) + for plugin in self.loadables: + for loadable in [loadable_item for loadable_item in self.loadables[plugin] if loadable_item.selected]: + checkBoxText = loadable.name + ' (' + plugin.loadType + ') ' + cb = qt.QCheckBox(checkBoxText, self) + cb.checked = True + cb.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)) + self.checkboxes[loadable] = cb + self.checkBoxGroupBox.layout().addWidget(cb) + self.layout().addWidget(self.checkBoxGroupBox, 1, 0, 1, 3) class DICOMLoadableTable(qt.QTableWidget): - """Implement the Qt code for a table of - selectable slicer data to be made from - the given dicom files - """ - - def __init__(self, parent, width=350, height=100): - super().__init__(parent) - self.setMinimumHeight(height) - self.setMinimumWidth(width) - self.loadables = {} - self.setLoadables([]) - self.configure() - slicer.app.connect('aboutToQuit()', self.deleteLater) - - def getNumberOfCheckedItems(self): - return sum(1 for row in range(self.rowCount) if self.item(row, 0).checkState() == qt.Qt.Checked) - - def configure(self): - self.setColumnCount(3) - self.setHorizontalHeaderLabels(['DICOM Data', 'Reader', 'Warnings']) - self.setSelectionBehavior(qt.QTableView.SelectRows) - self.horizontalHeader().setSectionResizeMode(qt.QHeaderView.Stretch) - self.horizontalHeader().setSectionResizeMode(0, qt.QHeaderView.Interactive) - self.horizontalHeader().setSectionResizeMode(1, qt.QHeaderView.ResizeToContents) - self.horizontalHeader().setSectionResizeMode(2, qt.QHeaderView.Stretch) - self.horizontalScrollMode = qt.QAbstractItemView.ScrollPerPixel - - def addLoadableRow(self, loadable, row, reader): - self.insertRow(row) - self.loadables[row] = loadable - item = qt.QTableWidgetItem(loadable.name) - self.setItem(row, 0, item) - self.setCheckState(item, loadable) - self.addReaderColumn(item, reader, row) - self.addWarningColumn(item, loadable, row) - - def setCheckState(self, item, loadable): - item.setCheckState(qt.Qt.Checked if loadable.selected else qt.Qt.Unchecked) - item.setToolTip(loadable.tooltip) - - def addReaderColumn(self, item, reader, row): - if not reader: - return - readerItem = qt.QTableWidgetItem(reader) - readerItem.setFlags(readerItem.flags() ^ qt.Qt.ItemIsEditable) - self.setItem(row, 1, readerItem) - readerItem.setToolTip(item.toolTip()) - - def addWarningColumn(self, item, loadable, row): - warning = loadable.warning if loadable.warning else '' - warnItem = qt.QTableWidgetItem(warning) - warnItem.setFlags(warnItem.flags() ^ qt.Qt.ItemIsEditable) - self.setItem(row, 2, warnItem) - item.setToolTip(item.toolTip() + "\n" + warning) - warnItem.setToolTip(item.toolTip()) - - def setLoadables(self, loadablesByPlugin): - """Load the table widget with a list - of volume options (of class DICOMVolume) + """Implement the Qt code for a table of + selectable slicer data to be made from + the given dicom files """ - self.clearContents() - self.setRowCount(0) - self.loadables = {} - - # For each plugin, keep only a single loadable selected for the same file set - # to prevent loading same data multiple times. - for plugin in loadablesByPlugin: - for thisLoadableId in range(len(loadablesByPlugin[plugin])): - for prevLoadableId in range(0, thisLoadableId): - thisLoadable = loadablesByPlugin[plugin][thisLoadableId] - prevLoadable = loadablesByPlugin[plugin][prevLoadableId] - # fileDifferences will contain all the files that only present in one or the other list (or tuple) - fileDifferences = set(thisLoadable.files).symmetric_difference(set(prevLoadable.files)) - if (not fileDifferences) and (prevLoadable.selected): - thisLoadable.selected = False - break - - row = 0 - for selectState in (True, False): - for plugin in loadablesByPlugin: - for loadable in loadablesByPlugin[plugin]: - if loadable.selected == selectState: - self.addLoadableRow(loadable, row, plugin.loadType) - row += 1 - - self.setVerticalHeaderLabels(row * [""]) - - def uncheckAll(self): - for row in range(self.rowCount): - item = self.item(row, 0) - item.setCheckState(False) - - def updateSelectedFromCheckstate(self): - for row in range(self.rowCount): - item = self.item(row, 0) - self.loadables[row].selected = (item.checkState() != 0) - # updating the names - self.loadables[row].name = item.text() + + def __init__(self, parent, width=350, height=100): + super().__init__(parent) + self.setMinimumHeight(height) + self.setMinimumWidth(width) + self.loadables = {} + self.setLoadables([]) + self.configure() + slicer.app.connect('aboutToQuit()', self.deleteLater) + + def getNumberOfCheckedItems(self): + return sum(1 for row in range(self.rowCount) if self.item(row, 0).checkState() == qt.Qt.Checked) + + def configure(self): + self.setColumnCount(3) + self.setHorizontalHeaderLabels(['DICOM Data', 'Reader', 'Warnings']) + self.setSelectionBehavior(qt.QTableView.SelectRows) + self.horizontalHeader().setSectionResizeMode(qt.QHeaderView.Stretch) + self.horizontalHeader().setSectionResizeMode(0, qt.QHeaderView.Interactive) + self.horizontalHeader().setSectionResizeMode(1, qt.QHeaderView.ResizeToContents) + self.horizontalHeader().setSectionResizeMode(2, qt.QHeaderView.Stretch) + self.horizontalScrollMode = qt.QAbstractItemView.ScrollPerPixel + + def addLoadableRow(self, loadable, row, reader): + self.insertRow(row) + self.loadables[row] = loadable + item = qt.QTableWidgetItem(loadable.name) + self.setItem(row, 0, item) + self.setCheckState(item, loadable) + self.addReaderColumn(item, reader, row) + self.addWarningColumn(item, loadable, row) + + def setCheckState(self, item, loadable): + item.setCheckState(qt.Qt.Checked if loadable.selected else qt.Qt.Unchecked) + item.setToolTip(loadable.tooltip) + + def addReaderColumn(self, item, reader, row): + if not reader: + return + readerItem = qt.QTableWidgetItem(reader) + readerItem.setFlags(readerItem.flags() ^ qt.Qt.ItemIsEditable) + self.setItem(row, 1, readerItem) + readerItem.setToolTip(item.toolTip()) + + def addWarningColumn(self, item, loadable, row): + warning = loadable.warning if loadable.warning else '' + warnItem = qt.QTableWidgetItem(warning) + warnItem.setFlags(warnItem.flags() ^ qt.Qt.ItemIsEditable) + self.setItem(row, 2, warnItem) + item.setToolTip(item.toolTip() + "\n" + warning) + warnItem.setToolTip(item.toolTip()) + + def setLoadables(self, loadablesByPlugin): + """Load the table widget with a list + of volume options (of class DICOMVolume) + """ + self.clearContents() + self.setRowCount(0) + self.loadables = {} + + # For each plugin, keep only a single loadable selected for the same file set + # to prevent loading same data multiple times. + for plugin in loadablesByPlugin: + for thisLoadableId in range(len(loadablesByPlugin[plugin])): + for prevLoadableId in range(0, thisLoadableId): + thisLoadable = loadablesByPlugin[plugin][thisLoadableId] + prevLoadable = loadablesByPlugin[plugin][prevLoadableId] + # fileDifferences will contain all the files that only present in one or the other list (or tuple) + fileDifferences = set(thisLoadable.files).symmetric_difference(set(prevLoadable.files)) + if (not fileDifferences) and (prevLoadable.selected): + thisLoadable.selected = False + break + + row = 0 + for selectState in (True, False): + for plugin in loadablesByPlugin: + for loadable in loadablesByPlugin[plugin]: + if loadable.selected == selectState: + self.addLoadableRow(loadable, row, plugin.loadType) + row += 1 + + self.setVerticalHeaderLabels(row * [""]) + + def uncheckAll(self): + for row in range(self.rowCount): + item = self.item(row, 0) + item.setCheckState(False) + + def updateSelectedFromCheckstate(self): + for row in range(self.rowCount): + item = self.item(row, 0) + self.loadables[row].selected = (item.checkState() != 0) + # updating the names + self.loadables[row].name = item.text() diff --git a/Modules/Scripted/DICOMLib/DICOMExportScalarVolume.py b/Modules/Scripted/DICOMLib/DICOMExportScalarVolume.py index b50c1939059..7c44a2a9830 100644 --- a/Modules/Scripted/DICOMLib/DICOMExportScalarVolume.py +++ b/Modules/Scripted/DICOMLib/DICOMExportScalarVolume.py @@ -19,32 +19,32 @@ class DICOMExportScalarVolume: - """Code to export slicer data to dicom database - TODO: delete temp directories and files - """ - - def __init__(self,studyUID,volumeNode,tags,directory,filenamePrefix=None): - """ - studyUID parameter is not used (studyUID is retrieved from tags). + """Code to export slicer data to dicom database + TODO: delete temp directories and files """ - self.studyUID = studyUID - self.volumeNode = volumeNode - self.tags = tags - self.directory = directory - self.filenamePrefix = filenamePrefix if filenamePrefix else "IMG" - #self.referenceFile = None - - #TODO: May come in use when appending to existing study - # def parametersFromStudy(self,studyUID=None): - # """Return a dictionary of the required conversion parameters - # based on the studyUID found in the dicom dictionary (empty if - # not well defined""" - # if not studyUID: - # studyUID = self.studyUID - - # # TODO: we should install dicom.dic with slicer and use it to - # # define the tag to name mapping - # tags = { + + def __init__(self, studyUID, volumeNode, tags, directory, filenamePrefix=None): + """ + studyUID parameter is not used (studyUID is retrieved from tags). + """ + self.studyUID = studyUID + self.volumeNode = volumeNode + self.tags = tags + self.directory = directory + self.filenamePrefix = filenamePrefix if filenamePrefix else "IMG" + # self.referenceFile = None + + # TODO: May come in use when appending to existing study + # def parametersFromStudy(self,studyUID=None): + # """Return a dictionary of the required conversion parameters + # based on the studyUID found in the dicom dictionary (empty if + # not well defined""" + # if not studyUID: + # studyUID = self.studyUID + + # # TODO: we should install dicom.dic with slicer and use it to + # # define the tag to name mapping + # tags = { # "0010,0010": "Patient Name", # "0010,0020": "Patient ID", # "0010,4000": "Patient Comments", @@ -54,114 +54,114 @@ def __init__(self,studyUID,volumeNode,tags,directory,filenamePrefix=None): # "0008,0060": "Modality", # "0008,0070": "Manufacturer", # "0008,1090": "Model", - # } - # seriesNumbers = [] - # p = {} - # if studyUID: - # series = slicer.dicomDatabase.seriesForStudy(studyUID) - # # first find a unique series number - # for serie in series: - # files = slicer.dicomDatabase.filesForSeries(serie, 1) - # if len(files): - # slicer.dicomDatabase.loadFileHeader(files[0]) - # dump = slicer.dicomDatabase.headerValue('0020,0011') - # try: - # value = dump[dump.index('[')+1:dump.index(']')] - # seriesNumbers.append(int(value)) - # except ValueError: - # pass - # for i in xrange(len(series)+1): - # if not i in seriesNumbers: - # p['Series Number'] = i - # break - - # # now find the other values from any file (use first file in first series) - # if len(series): - # p['Series Number'] = str(len(series)+1) # doesn't need to be unique, but we try - # files = slicer.dicomDatabase.filesForSeries(series[0], 1) - # if len(files): - # self.referenceFile = files[0] - # slicer.dicomDatabase.loadFileHeader(self.referenceFile) - # for tag in tags.keys(): - # dump = slicer.dicomDatabase.headerValue(tag) - # try: - # value = dump[dump.index('[')+1:dump.index(']')] - # except ValueError: - # value = "Unknown" - # p[tags[tag]] = value - # return p - - def progress(self,string): - # TODO: make this a callback for a gui progress dialog - print(string) - - def export(self): - """ - Export the volume data using the ITK-based utility - TODO: confirm that resulting file is valid - may need to change the CLI - to include more parameters or do a new implementation ctk/DCMTK - See: - https://sourceforge.net/apps/mediawiki/gdcm/index.php?title=Writing_DICOM - TODO: add more parameters to the CLI and/or find a different - mechanism for creating the DICOM files - """ - cliparameters = {} - # Patient - cliparameters['patientName'] = self.tags['Patient Name'] - cliparameters['patientID'] = self.tags['Patient ID'] - cliparameters['patientBirthDate'] = self.tags['Patient Birth Date'] - cliparameters['patientSex'] = self.tags['Patient Sex'] if self.tags['Patient Sex'] else "[unknown]" - cliparameters['patientComments'] = self.tags['Patient Comments'] - # Study - cliparameters['studyID'] = self.tags['Study ID'] - cliparameters['studyDate'] = self.tags['Study Date'] - cliparameters['studyTime'] = self.tags['Study Time'] - cliparameters['studyDescription'] = self.tags['Study Description'] - cliparameters['modality'] = self.tags['Modality'] - cliparameters['manufacturer'] = self.tags['Manufacturer'] - cliparameters['model'] = self.tags['Model'] - # Series - cliparameters['seriesDescription'] = self.tags['Series Description'] - cliparameters['seriesNumber'] = self.tags['Series Number'] - cliparameters['seriesDate'] = self.tags['Series Date'] - cliparameters['seriesTime'] = self.tags['Series Time'] - # Image - displayNode = self.volumeNode.GetDisplayNode() - if displayNode: - if displayNode.IsA('vtkMRMLScalarVolumeDisplayNode'): - cliparameters['windowCenter'] = str(displayNode.GetLevel()) - cliparameters['windowWidth'] = str(displayNode.GetWindow()) - else: - # labelmap volume - scalarRange = displayNode.GetScalarRange() - cliparameters['windowCenter'] = str((scalarRange[0]+scalarRange[0])/2.0) - cliparameters['windowWidth'] = str(scalarRange[1]-scalarRange[0]) - cliparameters['contentDate'] = self.tags['Content Date'] - cliparameters['contentTime'] = self.tags['Content Time'] - - # UIDs - cliparameters['studyInstanceUID'] = self.tags['Study Instance UID'] - cliparameters['seriesInstanceUID'] = self.tags['Series Instance UID'] - if 'Frame of Reference UID' in self.tags: - cliparameters['frameOfReferenceUID'] = self.tags['Frame of Reference UID'] - elif 'Frame of Reference Instance UID' in self.tags: - logging.warning('Usage of "Frame of Reference Instance UID" is deprecated, use "Frame of Reference UID" instead.') - cliparameters['frameOfReferenceUID'] = self.tags['Frame of Reference UID'] - cliparameters['inputVolume'] = self.volumeNode.GetID() - - cliparameters['dicomDirectory'] = self.directory - cliparameters['dicomPrefix'] = self.filenamePrefix - - # - # run the task (in the background) - # - use the GUI to provide progress feedback - # - use the GUI's Logic to invoke the task - # - if not hasattr(slicer.modules, 'createdicomseries'): - logging.error("CreateDICOMSeries module is not found") - return False - dicomWrite = slicer.modules.createdicomseries - cliNode = slicer.cli.run(dicomWrite, None, cliparameters, wait_for_completion=True) - success = (cliNode.GetStatus() == cliNode.Completed) - slicer.mrmlScene.RemoveNode(cliNode) - return success + # } + # seriesNumbers = [] + # p = {} + # if studyUID: + # series = slicer.dicomDatabase.seriesForStudy(studyUID) + # # first find a unique series number + # for serie in series: + # files = slicer.dicomDatabase.filesForSeries(serie, 1) + # if len(files): + # slicer.dicomDatabase.loadFileHeader(files[0]) + # dump = slicer.dicomDatabase.headerValue('0020,0011') + # try: + # value = dump[dump.index('[')+1:dump.index(']')] + # seriesNumbers.append(int(value)) + # except ValueError: + # pass + # for i in xrange(len(series)+1): + # if not i in seriesNumbers: + # p['Series Number'] = i + # break + + # # now find the other values from any file (use first file in first series) + # if len(series): + # p['Series Number'] = str(len(series)+1) # doesn't need to be unique, but we try + # files = slicer.dicomDatabase.filesForSeries(series[0], 1) + # if len(files): + # self.referenceFile = files[0] + # slicer.dicomDatabase.loadFileHeader(self.referenceFile) + # for tag in tags.keys(): + # dump = slicer.dicomDatabase.headerValue(tag) + # try: + # value = dump[dump.index('[')+1:dump.index(']')] + # except ValueError: + # value = "Unknown" + # p[tags[tag]] = value + # return p + + def progress(self, string): + # TODO: make this a callback for a gui progress dialog + print(string) + + def export(self): + """ + Export the volume data using the ITK-based utility + TODO: confirm that resulting file is valid - may need to change the CLI + to include more parameters or do a new implementation ctk/DCMTK + See: + https://sourceforge.net/apps/mediawiki/gdcm/index.php?title=Writing_DICOM + TODO: add more parameters to the CLI and/or find a different + mechanism for creating the DICOM files + """ + cliparameters = {} + # Patient + cliparameters['patientName'] = self.tags['Patient Name'] + cliparameters['patientID'] = self.tags['Patient ID'] + cliparameters['patientBirthDate'] = self.tags['Patient Birth Date'] + cliparameters['patientSex'] = self.tags['Patient Sex'] if self.tags['Patient Sex'] else "[unknown]" + cliparameters['patientComments'] = self.tags['Patient Comments'] + # Study + cliparameters['studyID'] = self.tags['Study ID'] + cliparameters['studyDate'] = self.tags['Study Date'] + cliparameters['studyTime'] = self.tags['Study Time'] + cliparameters['studyDescription'] = self.tags['Study Description'] + cliparameters['modality'] = self.tags['Modality'] + cliparameters['manufacturer'] = self.tags['Manufacturer'] + cliparameters['model'] = self.tags['Model'] + # Series + cliparameters['seriesDescription'] = self.tags['Series Description'] + cliparameters['seriesNumber'] = self.tags['Series Number'] + cliparameters['seriesDate'] = self.tags['Series Date'] + cliparameters['seriesTime'] = self.tags['Series Time'] + # Image + displayNode = self.volumeNode.GetDisplayNode() + if displayNode: + if displayNode.IsA('vtkMRMLScalarVolumeDisplayNode'): + cliparameters['windowCenter'] = str(displayNode.GetLevel()) + cliparameters['windowWidth'] = str(displayNode.GetWindow()) + else: + # labelmap volume + scalarRange = displayNode.GetScalarRange() + cliparameters['windowCenter'] = str((scalarRange[0] + scalarRange[0]) / 2.0) + cliparameters['windowWidth'] = str(scalarRange[1] - scalarRange[0]) + cliparameters['contentDate'] = self.tags['Content Date'] + cliparameters['contentTime'] = self.tags['Content Time'] + + # UIDs + cliparameters['studyInstanceUID'] = self.tags['Study Instance UID'] + cliparameters['seriesInstanceUID'] = self.tags['Series Instance UID'] + if 'Frame of Reference UID' in self.tags: + cliparameters['frameOfReferenceUID'] = self.tags['Frame of Reference UID'] + elif 'Frame of Reference Instance UID' in self.tags: + logging.warning('Usage of "Frame of Reference Instance UID" is deprecated, use "Frame of Reference UID" instead.') + cliparameters['frameOfReferenceUID'] = self.tags['Frame of Reference UID'] + cliparameters['inputVolume'] = self.volumeNode.GetID() + + cliparameters['dicomDirectory'] = self.directory + cliparameters['dicomPrefix'] = self.filenamePrefix + + # + # run the task (in the background) + # - use the GUI to provide progress feedback + # - use the GUI's Logic to invoke the task + # + if not hasattr(slicer.modules, 'createdicomseries'): + logging.error("CreateDICOMSeries module is not found") + return False + dicomWrite = slicer.modules.createdicomseries + cliNode = slicer.cli.run(dicomWrite, None, cliparameters, wait_for_completion=True) + success = (cliNode.GetStatus() == cliNode.Completed) + slicer.mrmlScene.RemoveNode(cliNode) + return success diff --git a/Modules/Scripted/DICOMLib/DICOMExportScene.py b/Modules/Scripted/DICOMLib/DICOMExportScene.py index a9211313730..7814d0f2793 100644 --- a/Modules/Scripted/DICOMLib/DICOMExportScene.py +++ b/Modules/Scripted/DICOMLib/DICOMExportScene.py @@ -27,142 +27,142 @@ class DICOMExportScene: - """Export slicer scene to dicom database - """ - - def __init__(self, referenceFile, saveDirectoryPath=None): - # File used as reference for DICOM export. Provides most of the DICOM tags. - # If not specified, the first file in the DICOM database is used. - self.referenceFile = referenceFile - # Directory where all the intermediate files are saved. - self.saveDirectoryPath = saveDirectoryPath - # Path and filename of the Slicer Data Bundle DICOM file - self.sdbFile = None - # Path to the screenshot image file that is saved with the scene and in the Secondary Capture. - # If not specified, then the default scene saving method is used to generate the image. - self.imageFile = None - # Study description string to save in the tags. Default is "Slicer Scene Export" - self.studyDescription = None - # Series description string to save in the tags. Default is "Slicer Data Bundle" - self.seriesDescription = None - # Optional tags. - # Dictionary where the keys are the tag names (such as StudyInstanceUID), and the values are the tag values - self.optionalTags = {} - - def progress(self,string): - # TODO: make this a callback for a gui progress dialog - logging.info(string) - - def export(self): - # Perform export - success = self.createDICOMFileForScene() - return success - - def createDICOMFileForScene(self): - """ - Export the scene data: - - first to a directory using the utility in the mrmlScene - - create a zip file using the application logic - - create secondary capture based on the sample dataset - - add the zip file as a private creator tag - TODO: confirm that resulting file is valid - may need to change the CLI - to include more parameters or do a new implementation ctk/DCMTK - See: - https://sourceforge.net/apps/mediawiki/gdcm/index.php?title=Writing_DICOM + """Export slicer scene to dicom database """ - # set up temp directories and files - if self.saveDirectoryPath is None: - self.saveDirectoryPath = tempfile.mkdtemp('', 'dicomExport', slicer.app.temporaryPath) - self.zipFile = os.path.join(self.saveDirectoryPath, "scene.zip") - self.dumpFile = os.path.join(self.saveDirectoryPath, "dump.dcm") - self.templateFile = os.path.join(self.saveDirectoryPath, "template.dcm") - self.sdbFile = os.path.join(self.saveDirectoryPath, "SlicerDataBundle.dcm") - if self.studyDescription is None: - self.studyDescription = 'Slicer Scene Export' - if self.seriesDescription is None: - self.seriesDescription = 'Slicer Data Bundle' - - # get the screen image if not specified - if self.imageFile is None: - self.progress('Saving Image...') - self.imageFile = os.path.join(self.saveDirectoryPath, "scene.jpg") - image = ctk.ctkWidgetsUtils.grabWidget(slicer.util.mainWindow()) - image.save(self.imageFile) - imageReader = vtk.vtkJPEGReader() - imageReader.SetFileName(self.imageFile) - imageReader.Update() - - # Clean up paths on Windows (some commands and operations are not performed properly with mixed slash and backslash) - self.saveDirectoryPath = self.saveDirectoryPath.replace('\\','/') - self.imageFile = self.imageFile.replace('\\','/') - self.zipFile = self.zipFile.replace('\\','/') - self.dumpFile = self.dumpFile.replace('\\','/') - self.templateFile = self.templateFile.replace('\\','/') - self.sdbFile = self.sdbFile.replace('\\','/') - - # save the scene to the temp dir - self.progress('Saving scene into MRB...') - if not slicer.mrmlScene.WriteToMRB(self.zipFile, imageReader.GetOutput()): - logging.error('Failed to save scene into MRB file: ' + self.zipFile) - return False - - zipSize = os.path.getsize(self.zipFile) - - # now create the dicom file - # - create the dump (capture stdout) - # cmd = "dcmdump --print-all --write-pixel %s %s" % (self.saveDirectoryPath, self.referenceFile) - self.progress('Making dicom reference file...') - logging.info('Using reference file ' + str(self.referenceFile)) - args = ['--print-all', '--write-pixel', self.saveDirectoryPath, self.referenceFile] - dumpByteArray = DICOMLib.DICOMCommand('dcmdump', args).start() - dump = str(dumpByteArray.data(), encoding='utf-8') - - # append this to the dumped output and save the result as self.saveDirectoryPath/dcm.dump - # with %s as self.zipFile and %d being its size in bytes - zipSizeString = "%d" % zipSize - - # hack: encode the file zip file size as part of the creator string - # because none of the normal types (UL, DS, LO) seem to survive - # the dump2dcm step (possibly due to the Unknown nature of the private tag) - creatorString = "3D Slicer %s" % zipSizeString - candygram = """(cadb,0010) LO [%s] # %d, 1 PrivateCreator + def __init__(self, referenceFile, saveDirectoryPath=None): + # File used as reference for DICOM export. Provides most of the DICOM tags. + # If not specified, the first file in the DICOM database is used. + self.referenceFile = referenceFile + # Directory where all the intermediate files are saved. + self.saveDirectoryPath = saveDirectoryPath + # Path and filename of the Slicer Data Bundle DICOM file + self.sdbFile = None + # Path to the screenshot image file that is saved with the scene and in the Secondary Capture. + # If not specified, then the default scene saving method is used to generate the image. + self.imageFile = None + # Study description string to save in the tags. Default is "Slicer Scene Export" + self.studyDescription = None + # Series description string to save in the tags. Default is "Slicer Data Bundle" + self.seriesDescription = None + # Optional tags. + # Dictionary where the keys are the tag names (such as StudyInstanceUID), and the values are the tag values + self.optionalTags = {} + + def progress(self, string): + # TODO: make this a callback for a gui progress dialog + logging.info(string) + + def export(self): + # Perform export + success = self.createDICOMFileForScene() + return success + + def createDICOMFileForScene(self): + """ + Export the scene data: + - first to a directory using the utility in the mrmlScene + - create a zip file using the application logic + - create secondary capture based on the sample dataset + - add the zip file as a private creator tag + TODO: confirm that resulting file is valid - may need to change the CLI + to include more parameters or do a new implementation ctk/DCMTK + See: + https://sourceforge.net/apps/mediawiki/gdcm/index.php?title=Writing_DICOM + """ + + # set up temp directories and files + if self.saveDirectoryPath is None: + self.saveDirectoryPath = tempfile.mkdtemp('', 'dicomExport', slicer.app.temporaryPath) + self.zipFile = os.path.join(self.saveDirectoryPath, "scene.zip") + self.dumpFile = os.path.join(self.saveDirectoryPath, "dump.dcm") + self.templateFile = os.path.join(self.saveDirectoryPath, "template.dcm") + self.sdbFile = os.path.join(self.saveDirectoryPath, "SlicerDataBundle.dcm") + if self.studyDescription is None: + self.studyDescription = 'Slicer Scene Export' + if self.seriesDescription is None: + self.seriesDescription = 'Slicer Data Bundle' + + # get the screen image if not specified + if self.imageFile is None: + self.progress('Saving Image...') + self.imageFile = os.path.join(self.saveDirectoryPath, "scene.jpg") + image = ctk.ctkWidgetsUtils.grabWidget(slicer.util.mainWindow()) + image.save(self.imageFile) + imageReader = vtk.vtkJPEGReader() + imageReader.SetFileName(self.imageFile) + imageReader.Update() + + # Clean up paths on Windows (some commands and operations are not performed properly with mixed slash and backslash) + self.saveDirectoryPath = self.saveDirectoryPath.replace('\\', '/') + self.imageFile = self.imageFile.replace('\\', '/') + self.zipFile = self.zipFile.replace('\\', '/') + self.dumpFile = self.dumpFile.replace('\\', '/') + self.templateFile = self.templateFile.replace('\\', '/') + self.sdbFile = self.sdbFile.replace('\\', '/') + + # save the scene to the temp dir + self.progress('Saving scene into MRB...') + if not slicer.mrmlScene.WriteToMRB(self.zipFile, imageReader.GetOutput()): + logging.error('Failed to save scene into MRB file: ' + self.zipFile) + return False + + zipSize = os.path.getsize(self.zipFile) + + # now create the dicom file + # - create the dump (capture stdout) + # cmd = "dcmdump --print-all --write-pixel %s %s" % (self.saveDirectoryPath, self.referenceFile) + self.progress('Making dicom reference file...') + logging.info('Using reference file ' + str(self.referenceFile)) + args = ['--print-all', '--write-pixel', self.saveDirectoryPath, self.referenceFile] + dumpByteArray = DICOMLib.DICOMCommand('dcmdump', args).start() + dump = str(dumpByteArray.data(), encoding='utf-8') + + # append this to the dumped output and save the result as self.saveDirectoryPath/dcm.dump + # with %s as self.zipFile and %d being its size in bytes + zipSizeString = "%d" % zipSize + + # hack: encode the file zip file size as part of the creator string + # because none of the normal types (UL, DS, LO) seem to survive + # the dump2dcm step (possibly due to the Unknown nature of the private tag) + creatorString = "3D Slicer %s" % zipSizeString + candygram = """(cadb,0010) LO [%s] # %d, 1 PrivateCreator (cadb,1008) LO [%s] # 4, 1 Unknown Tag & Data (cadb,1010) OB =%s # %d, 1 Unknown Tag & Data """ % (creatorString, len(creatorString), zipSizeString, self.zipFile, zipSize) - dump = dump + candygram - - logging.debug('dumping to: %s' % self.dumpFile) - fp = open(self.dumpFile, 'w') - fp.write(dump) - fp.close() - - self.progress('Encapsulating scene in DICOM dump...') - args = [ self.dumpFile, self.templateFile, '--generate-new-uids', '--overwrite-uids', '--ignore-errors' ] - DICOMLib.DICOMCommand('dump2dcm', args).start() - - # now create the Secondary Capture data set - # cmd = "img2dcm -k 'InstanceNumber=1' -k 'SeriesDescription=Slicer Data Bundle' -df %s/template.dcm %s %s" % (self.saveDirectoryPath, self.imageFile, self.sdbFile) - args = [ - '-k', 'InstanceNumber=1', - '-k', 'StudyDescription=%s' % str(self.studyDescription), - '-k', 'SeriesDescription=%s' % str(self.seriesDescription), - '--dataset-from', self.templateFile, - self.imageFile, self.sdbFile ] - argIndex = 6 - for key, value in self.optionalTags.items(): - args.insert(argIndex, '-k') - tagNameValue = f'{str(key)}={str(value)}' - args.insert(argIndex+1, tagNameValue) - argIndex += 2 - self.progress('Creating DICOM binary file...') - DICOMLib.DICOMCommand('img2dcm', args).start() - - self.progress('Deleting temporary files...') - os.remove(self.zipFile) - os.remove(self.dumpFile) - os.remove(self.templateFile) - - self.progress('Done') - return True + dump = dump + candygram + + logging.debug('dumping to: %s' % self.dumpFile) + fp = open(self.dumpFile, 'w') + fp.write(dump) + fp.close() + + self.progress('Encapsulating scene in DICOM dump...') + args = [self.dumpFile, self.templateFile, '--generate-new-uids', '--overwrite-uids', '--ignore-errors'] + DICOMLib.DICOMCommand('dump2dcm', args).start() + + # now create the Secondary Capture data set + # cmd = "img2dcm -k 'InstanceNumber=1' -k 'SeriesDescription=Slicer Data Bundle' -df %s/template.dcm %s %s" % (self.saveDirectoryPath, self.imageFile, self.sdbFile) + args = [ + '-k', 'InstanceNumber=1', + '-k', 'StudyDescription=%s' % str(self.studyDescription), + '-k', 'SeriesDescription=%s' % str(self.seriesDescription), + '--dataset-from', self.templateFile, + self.imageFile, self.sdbFile] + argIndex = 6 + for key, value in self.optionalTags.items(): + args.insert(argIndex, '-k') + tagNameValue = f'{str(key)}={str(value)}' + args.insert(argIndex + 1, tagNameValue) + argIndex += 2 + self.progress('Creating DICOM binary file...') + DICOMLib.DICOMCommand('img2dcm', args).start() + + self.progress('Deleting temporary files...') + os.remove(self.zipFile) + os.remove(self.dumpFile) + os.remove(self.templateFile) + + self.progress('Done') + return True diff --git a/Modules/Scripted/DICOMLib/DICOMPlugin.py b/Modules/Scripted/DICOMLib/DICOMPlugin.py index e2bfdc13b4e..eb9d7a0f995 100644 --- a/Modules/Scripted/DICOMLib/DICOMPlugin.py +++ b/Modules/Scripted/DICOMLib/DICOMPlugin.py @@ -22,46 +22,46 @@ # class DICOMLoadable: - """Container class for things that can be - loaded from dicom files into slicer. - Each plugin returns a list of instances from its - evaluate method and accepts a list of these - in its load method corresponding to the things - the user has selected for loading - NOTE: This class is deprecated, use qSlicerDICOMLoadable - instead. - """ - - def __init__(self, qLoadable=None): - if qLoadable is None: - # the file list of the data to be loaded - self.files = [] - # name exposed to the user for the node - self.name = "Unknown" - # extra information the user sees on mouse over of the thing - self.tooltip = "No further information available" - # things the user should know before loading this data - self.warning = "" - # is the object checked for loading by default - self.selected = False - # confidence - from 0 to 1 where 0 means low chance - # that the user actually wants to load their data this - # way up to 1, which means that the plugin is very confident - # that this is the best way to load the data. - # When more than one plugin marks the same series as - # selected, the one with the highest confidence is - # actually selected by default. In the case of a tie, - # both series are selected for loading. - self.confidence = 0.5 - else: - self.name = qLoadable.name - self.tooltip = qLoadable.tooltip - self.warning = qLoadable.warning - self.files = [] - for file in qLoadable.files: - self.files.append(file) - self.selected = qLoadable.selected - self.confidence = qLoadable.confidence + """Container class for things that can be + loaded from dicom files into slicer. + Each plugin returns a list of instances from its + evaluate method and accepts a list of these + in its load method corresponding to the things + the user has selected for loading + NOTE: This class is deprecated, use qSlicerDICOMLoadable + instead. + """ + + def __init__(self, qLoadable=None): + if qLoadable is None: + # the file list of the data to be loaded + self.files = [] + # name exposed to the user for the node + self.name = "Unknown" + # extra information the user sees on mouse over of the thing + self.tooltip = "No further information available" + # things the user should know before loading this data + self.warning = "" + # is the object checked for loading by default + self.selected = False + # confidence - from 0 to 1 where 0 means low chance + # that the user actually wants to load their data this + # way up to 1, which means that the plugin is very confident + # that this is the best way to load the data. + # When more than one plugin marks the same series as + # selected, the one with the highest confidence is + # actually selected by default. In the case of a tie, + # both series are selected for loading. + self.confidence = 0.5 + else: + self.name = qLoadable.name + self.tooltip = qLoadable.tooltip + self.warning = qLoadable.warning + self.files = [] + for file in qLoadable.files: + self.files.append(file) + self.selected = qLoadable.selected + self.confidence = qLoadable.confidence # @@ -69,316 +69,316 @@ def __init__(self, qLoadable=None): # class DICOMPlugin: - """ Base class for DICOM plugins - """ - - def __init__(self): - # displayed for the user as the plugin handling the load - self.loadType = "Generic DICOM" - # a dictionary that maps a list of files to a list of loadables - # (so that subsequent requests for the same info can be - # serviced quickly) - self.loadableCache = {} - # tags is a dictionary of symbolic name keys mapping to - # hex tag number values (as in {'pixelData': '7fe0,0010'}). - # Each subclass should define the tags it will be using in - # calls to the DICOM database so that any needed values - # can be efficiently pre-fetched if possible. - self.tags = {} - self.tags['seriesDescription'] = "0008,103E" - self.tags['seriesNumber'] = "0020,0011" - self.tags['frameOfReferenceUID'] = "0020,0052" - - def findPrivateTag(self, ds, group, element, privateCreator): - """Helper function to get private tag from private creator name. - Example: - ds = pydicom.read_file(...) - tag = self.findPrivateTag(ds, 0x0021, 0x40, "General Electric Company 01") - value = ds[tag].value - """ - for tag, data_element in ds.items(): - if (tag.group == group) and (tag.element < 0x0100): - data_element_value = data_element.value - if type(data_element.value) == bytes: - data_element_value = data_element_value.decode() - if data_element_value.rstrip() == privateCreator: - import pydicom as dicom - return dicom.tag.Tag(group, (tag.element << 8) + element) - return None - - def isDetailedLogging(self): - """Helper function that returns True if detailed DICOM logging is enabled. - If enabled then the plugin can log as many details as it wants, even if it - makes loading slower or adds lots of information to the application log. - """ - return slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool) - - def hashFiles(self,files): - """Create a hash key for a list of files""" - try: - import hashlib - except: - return None - m = hashlib.md5() - for f in files: - # Unicode-objects must be encoded before hashing - m.update(f.encode('UTF-8', 'ignore')) - return(m.digest()) - - def getCachedLoadables(self,files): - """ Helper method to access the results of a previous - examination of a list of files""" - key = self.hashFiles(files) - if key in self.loadableCache: - return self.loadableCache[key] - return None - - def cacheLoadables(self,files,loadables): - """ Helper method to store the results of examining a list - of files for later quick access""" - key = self.hashFiles(files) - self.loadableCache[key] = loadables - - def examineForImport(self,fileList): - """Look at the list of lists of filenames and return - a list of DICOMLoadables that are options for loading - Virtual: should be overridden by the subclass - """ - return [] - - def examine(self,fileList): - """Backwards compatibility function for examineForImport - (renamed on introducing examineForExport to avoid confusion) + """ Base class for DICOM plugins """ - return self.examineForImport(fileList) - def load(self,loadable): - """Accept a DICOMLoadable and perform the operation to convert - the referenced data into MRML nodes - Virtual: should be overridden by the subclass - """ - return True - - def examineForExport(self,subjectHierarchyItemID): - """Return a list of DICOMExportable instances that describe the - available techniques that this plugin offers to convert MRML - data associated to a subject hierarchy item into DICOM data - Virtual: should be overridden by the subclass - """ - return [] - - def export(self,exportable): - """Export an exportable (one series) to file(s) - Return error message, empty if success - Virtual: should be overridden by the subclass - """ - return "" - - def defaultSeriesNodeName(self,seriesUID): - """Generate a name suitable for use as a mrml node name based - on the series level data in the database""" - instanceFilePaths = slicer.dicomDatabase.filesForSeries(seriesUID, 1) - if len(instanceFilePaths) == 0: - return "Unnamed Series" - seriesDescription = slicer.dicomDatabase.fileValue(instanceFilePaths[0],self.tags['seriesDescription']) - seriesNumber = slicer.dicomDatabase.fileValue(instanceFilePaths[0],self.tags['seriesNumber']) - name = seriesDescription - if seriesDescription == "": - name = "Unnamed Series" - if seriesNumber != "": - name = seriesNumber + ": " + name - return name - - def addSeriesInSubjectHierarchy(self,loadable,dataNode): - """Add loaded DICOM series into subject hierarchy. - The DICOM tags are read from the first file referenced by the - given loadable. The dataNode argument is associated to the created - series node and provides fallback name in case of empty series - description. - This function should be called from the load() function of - each subclass of the DICOMPlugin class. - """ - tags = {} - tags['seriesInstanceUID'] = "0020,000E" - tags['seriesModality'] = "0008,0060" - tags['seriesNumber'] = "0020,0011" - tags['frameOfReferenceUID'] = "0020,0052" - tags['studyInstanceUID'] = "0020,000D" - tags['studyID'] = "0020,0010" - tags['studyDescription'] = "0008,1030" - tags['studyDate'] = "0008,0020" - tags['studyTime'] = "0008,0030" - tags['patientID'] = "0010,0020" - tags['patientName'] = "0010,0010" - tags['patientSex'] = "0010,0040" - tags['patientBirthDate'] = "0010,0030" - tags['patientComments'] = "0010,4000" - tags['classUID'] = "0008,0016" - tags['instanceUID'] = "0008,0018" - - # Import and check dependencies - try: - slicer.vtkSlicerSubjectHierarchyModuleLogic - except AttributeError: - logging.error('Unable to create subject hierarchy: Subject Hierarchy module logic not found') - return - - # Validate dataNode argument - if dataNode is None or not dataNode.IsA('vtkMRMLNode'): - logging.error('Unable to create subject hierarchy items: invalid data node provided') - return - - # Get first file to access DICOM tags from it - firstFile = loadable.files[0] - - # Get subject hierarchy node and basic IDs - shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - sceneItemID = shn.GetSceneItemID() - - # Set up subject hierarchy item - seriesItemID = shn.CreateItem(sceneItemID, dataNode) - - # Specify details of series item - seriesInstanceUid = slicer.dicomDatabase.fileValue(firstFile,tags['seriesInstanceUID']) - shn.SetItemUID(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), seriesInstanceUid) - shn.SetItemAttribute( seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMSeriesModalityAttributeName(), - slicer.dicomDatabase.fileValue(firstFile, tags['seriesModality']) ) - shn.SetItemAttribute( seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMSeriesNumberAttributeName(), - slicer.dicomDatabase.fileValue(firstFile, tags['seriesNumber']) ) - shn.SetItemAttribute( seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMFrameOfReferenceUIDAttributeName(), - slicer.dicomDatabase.fileValue(firstFile, tags['frameOfReferenceUID']) ) - # Set instance UIDs - instanceUIDs = "" - for file in loadable.files: - uid = slicer.dicomDatabase.fileValue(file,tags['instanceUID']) - if uid == "": - uid = "Unknown" - instanceUIDs += uid + " " - instanceUIDs = instanceUIDs[:-1] # strip last space - shn.SetItemUID(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMInstanceUIDName(), instanceUIDs) - - # Set referenced instance UIDs from loadable to series - referencedInstanceUIDs = "" - if hasattr(loadable,'referencedInstanceUIDs'): - for instanceUID in loadable.referencedInstanceUIDs: - referencedInstanceUIDs += instanceUID + " " - referencedInstanceUIDs = referencedInstanceUIDs[:-1] # strip last space - shn.SetItemAttribute( seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMReferencedInstanceUIDsAttributeName(), - referencedInstanceUIDs ) - - # Add series item to hierarchy under the right study and patient items. If they are present then used, if not, then created - studyInstanceUid = slicer.dicomDatabase.fileValue(firstFile, tags['studyInstanceUID']) - patientId = slicer.dicomDatabase.fileValue(firstFile, tags['patientID']) - if not patientId: - # Patient ID tag is required DICOM tag and it cannot be empty. Unfortunately, we may get DICOM files that do not follow - # the standard (e.g., incorrectly anonymized) and have empty patient tag. We generate a unique ID from the study instance UID. - # The DICOM browser uses the study instance UID as patient ID directly, but this would not work in the subject hierarchy, because - # then the DICOM UID of the patient and study tag would be the same, so we add a prefix ("Patient-"). - patientId = "Patient-"+studyInstanceUid - patientItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), patientId) - studyId = slicer.dicomDatabase.fileValue(firstFile, tags['studyID']) - studyItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), studyInstanceUid) - slicer.vtkSlicerSubjectHierarchyModuleLogic.InsertDicomSeriesInHierarchy(shn, patientId, studyInstanceUid, seriesInstanceUid) - - if not patientItemID: - patientItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), patientId) - if patientItemID: - # Add attributes for DICOM tags - patientName = slicer.dicomDatabase.fileValue(firstFile,tags['patientName']) - if patientName == '': - patientName = 'No name' - - shn.SetItemAttribute( patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameAttributeName(), - patientName ) - shn.SetItemAttribute( patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDAttributeName(), - patientId ) - shn.SetItemAttribute( patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexAttributeName(), - slicer.dicomDatabase.fileValue(firstFile, tags['patientSex']) ) - patientBirthDate = slicer.dicomDatabase.fileValue(firstFile, tags['patientBirthDate']) - shn.SetItemAttribute( patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateAttributeName(), - patientBirthDate ) - shn.SetItemAttribute( patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsAttributeName(), - slicer.dicomDatabase.fileValue(firstFile, tags['patientComments']) ) - # Set item name - patientItemName = patientName - if pluginHandlerSingleton.displayPatientIDInSubjectHierarchyItemName: - patientItemName += ' (' + str(patientId) + ')' - if pluginHandlerSingleton.displayPatientBirthDateInSubjectHierarchyItemName and patientBirthDate != '': - patientItemName += ' (' + str(patientBirthDate) + ')' - shn.SetItemName(patientItemID, patientItemName) - - if not studyItemID: - studyItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), studyInstanceUid) - if studyItemID: - # Add attributes for DICOM tags - studyDescription = slicer.dicomDatabase.fileValue(firstFile,tags['studyDescription']) - if studyDescription == '': - studyDescription = 'No study description' - - shn.SetItemAttribute( studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionAttributeName(), - studyDescription ) - studyDate = slicer.dicomDatabase.fileValue(firstFile,tags['studyDate']) - shn.SetItemAttribute( studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyInstanceUIDAttributeName(), - studyInstanceUid ) - shn.SetItemAttribute( studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDAttributeName(), - studyId ) - shn.SetItemAttribute( studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateAttributeName(), - studyDate ) - shn.SetItemAttribute( studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeAttributeName(), - slicer.dicomDatabase.fileValue(firstFile, tags['studyTime']) ) - # Set item name - studyItemName = studyDescription - if pluginHandlerSingleton.displayStudyIDInSubjectHierarchyItemName: - studyItemName += ' (' + str(studyId) + ')' - if pluginHandlerSingleton.displayStudyDateInSubjectHierarchyItemName and studyDate != '': - studyItemName += ' (' + str(studyDate) + ')' - shn.SetItemName(studyItemID, studyItemName) - - def mapSOPClassUIDToModality(self, sopClassUID): - # Note more specialized definitions can be specified for MR by more - # specialized plugins, see codes 110800 and on in - # https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_D.html - MRname2UID = { - "MR Image Storage": "1.2.840.10008.5.1.4.1.1.4", - "Enhanced MR Image Storage": "1.2.840.10008.5.1.4.1.1.4.1", - "Legacy Converted Enhanced MR Image Storage": "1.2.840.10008.5.1.4.1.1.4.4" - } - CTname2UID = { - "CT Image Storage": "1.2.840.10008.5.1.4.1.1.2", - "Enhanced CT Image Storage": "1.2.840.10008.5.1.4.1.1.2.1", - "Legacy Converted Enhanced CT Image Storage": "1.2.840.10008.5.1.4.1.1.2.2" - } - PETname2UID = { - "Positron Emission Tomography Image Storage": "1.2.840.10008.5.1.4.1.1.128", - "Enhanced PET Image Storage": "1.2.840.10008.5.1.4.1.1.130", - "Legacy Converted Enhanced PET Image Storage": "1.2.840.10008.5.1.4.1.1.128.1" - } - - if sopClassUID in MRname2UID.values(): - return "MR" - elif sopClassUID in CTname2UID.values(): - return "CT" - elif sopClassUID in PETname2UID.values(): - return "PT" - else: - return None - - def mapSOPClassUIDToDICOMQuantityAndUnits(self, sopClassUID): - - quantity = None - units = None - - modality = self.mapSOPClassUIDToModality(sopClassUID) - if modality == "MR": - quantity = slicer.vtkCodedEntry() - quantity.SetValueSchemeMeaning("110852", "DCM", "MR signal intensity") - units = slicer.vtkCodedEntry() - units.SetValueSchemeMeaning("1", "UCUM", "no units") - elif modality == "CT": - quantity = slicer.vtkCodedEntry() - quantity.SetValueSchemeMeaning("112031", "DCM", "Attenuation Coefficient") - units = slicer.vtkCodedEntry() - units.SetValueSchemeMeaning("[hnsf'U]", "UCUM", "Hounsfield unit") - - return (quantity, units) + def __init__(self): + # displayed for the user as the plugin handling the load + self.loadType = "Generic DICOM" + # a dictionary that maps a list of files to a list of loadables + # (so that subsequent requests for the same info can be + # serviced quickly) + self.loadableCache = {} + # tags is a dictionary of symbolic name keys mapping to + # hex tag number values (as in {'pixelData': '7fe0,0010'}). + # Each subclass should define the tags it will be using in + # calls to the DICOM database so that any needed values + # can be efficiently pre-fetched if possible. + self.tags = {} + self.tags['seriesDescription'] = "0008,103E" + self.tags['seriesNumber'] = "0020,0011" + self.tags['frameOfReferenceUID'] = "0020,0052" + + def findPrivateTag(self, ds, group, element, privateCreator): + """Helper function to get private tag from private creator name. + Example: + ds = pydicom.read_file(...) + tag = self.findPrivateTag(ds, 0x0021, 0x40, "General Electric Company 01") + value = ds[tag].value + """ + for tag, data_element in ds.items(): + if (tag.group == group) and (tag.element < 0x0100): + data_element_value = data_element.value + if type(data_element.value) == bytes: + data_element_value = data_element_value.decode() + if data_element_value.rstrip() == privateCreator: + import pydicom as dicom + return dicom.tag.Tag(group, (tag.element << 8) + element) + return None + + def isDetailedLogging(self): + """Helper function that returns True if detailed DICOM logging is enabled. + If enabled then the plugin can log as many details as it wants, even if it + makes loading slower or adds lots of information to the application log. + """ + return slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool) + + def hashFiles(self, files): + """Create a hash key for a list of files""" + try: + import hashlib + except: + return None + m = hashlib.md5() + for f in files: + # Unicode-objects must be encoded before hashing + m.update(f.encode('UTF-8', 'ignore')) + return(m.digest()) + + def getCachedLoadables(self, files): + """ Helper method to access the results of a previous + examination of a list of files""" + key = self.hashFiles(files) + if key in self.loadableCache: + return self.loadableCache[key] + return None + + def cacheLoadables(self, files, loadables): + """ Helper method to store the results of examining a list + of files for later quick access""" + key = self.hashFiles(files) + self.loadableCache[key] = loadables + + def examineForImport(self, fileList): + """Look at the list of lists of filenames and return + a list of DICOMLoadables that are options for loading + Virtual: should be overridden by the subclass + """ + return [] + + def examine(self, fileList): + """Backwards compatibility function for examineForImport + (renamed on introducing examineForExport to avoid confusion) + """ + return self.examineForImport(fileList) + + def load(self, loadable): + """Accept a DICOMLoadable and perform the operation to convert + the referenced data into MRML nodes + Virtual: should be overridden by the subclass + """ + return True + + def examineForExport(self, subjectHierarchyItemID): + """Return a list of DICOMExportable instances that describe the + available techniques that this plugin offers to convert MRML + data associated to a subject hierarchy item into DICOM data + Virtual: should be overridden by the subclass + """ + return [] + + def export(self, exportable): + """Export an exportable (one series) to file(s) + Return error message, empty if success + Virtual: should be overridden by the subclass + """ + return "" + + def defaultSeriesNodeName(self, seriesUID): + """Generate a name suitable for use as a mrml node name based + on the series level data in the database""" + instanceFilePaths = slicer.dicomDatabase.filesForSeries(seriesUID, 1) + if len(instanceFilePaths) == 0: + return "Unnamed Series" + seriesDescription = slicer.dicomDatabase.fileValue(instanceFilePaths[0], self.tags['seriesDescription']) + seriesNumber = slicer.dicomDatabase.fileValue(instanceFilePaths[0], self.tags['seriesNumber']) + name = seriesDescription + if seriesDescription == "": + name = "Unnamed Series" + if seriesNumber != "": + name = seriesNumber + ": " + name + return name + + def addSeriesInSubjectHierarchy(self, loadable, dataNode): + """Add loaded DICOM series into subject hierarchy. + The DICOM tags are read from the first file referenced by the + given loadable. The dataNode argument is associated to the created + series node and provides fallback name in case of empty series + description. + This function should be called from the load() function of + each subclass of the DICOMPlugin class. + """ + tags = {} + tags['seriesInstanceUID'] = "0020,000E" + tags['seriesModality'] = "0008,0060" + tags['seriesNumber'] = "0020,0011" + tags['frameOfReferenceUID'] = "0020,0052" + tags['studyInstanceUID'] = "0020,000D" + tags['studyID'] = "0020,0010" + tags['studyDescription'] = "0008,1030" + tags['studyDate'] = "0008,0020" + tags['studyTime'] = "0008,0030" + tags['patientID'] = "0010,0020" + tags['patientName'] = "0010,0010" + tags['patientSex'] = "0010,0040" + tags['patientBirthDate'] = "0010,0030" + tags['patientComments'] = "0010,4000" + tags['classUID'] = "0008,0016" + tags['instanceUID'] = "0008,0018" + + # Import and check dependencies + try: + slicer.vtkSlicerSubjectHierarchyModuleLogic + except AttributeError: + logging.error('Unable to create subject hierarchy: Subject Hierarchy module logic not found') + return + + # Validate dataNode argument + if dataNode is None or not dataNode.IsA('vtkMRMLNode'): + logging.error('Unable to create subject hierarchy items: invalid data node provided') + return + + # Get first file to access DICOM tags from it + firstFile = loadable.files[0] + + # Get subject hierarchy node and basic IDs + shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + sceneItemID = shn.GetSceneItemID() + + # Set up subject hierarchy item + seriesItemID = shn.CreateItem(sceneItemID, dataNode) + + # Specify details of series item + seriesInstanceUid = slicer.dicomDatabase.fileValue(firstFile, tags['seriesInstanceUID']) + shn.SetItemUID(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), seriesInstanceUid) + shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMSeriesModalityAttributeName(), + slicer.dicomDatabase.fileValue(firstFile, tags['seriesModality'])) + shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMSeriesNumberAttributeName(), + slicer.dicomDatabase.fileValue(firstFile, tags['seriesNumber'])) + shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMFrameOfReferenceUIDAttributeName(), + slicer.dicomDatabase.fileValue(firstFile, tags['frameOfReferenceUID'])) + # Set instance UIDs + instanceUIDs = "" + for file in loadable.files: + uid = slicer.dicomDatabase.fileValue(file, tags['instanceUID']) + if uid == "": + uid = "Unknown" + instanceUIDs += uid + " " + instanceUIDs = instanceUIDs[:-1] # strip last space + shn.SetItemUID(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMInstanceUIDName(), instanceUIDs) + + # Set referenced instance UIDs from loadable to series + referencedInstanceUIDs = "" + if hasattr(loadable, 'referencedInstanceUIDs'): + for instanceUID in loadable.referencedInstanceUIDs: + referencedInstanceUIDs += instanceUID + " " + referencedInstanceUIDs = referencedInstanceUIDs[:-1] # strip last space + shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMReferencedInstanceUIDsAttributeName(), + referencedInstanceUIDs) + + # Add series item to hierarchy under the right study and patient items. If they are present then used, if not, then created + studyInstanceUid = slicer.dicomDatabase.fileValue(firstFile, tags['studyInstanceUID']) + patientId = slicer.dicomDatabase.fileValue(firstFile, tags['patientID']) + if not patientId: + # Patient ID tag is required DICOM tag and it cannot be empty. Unfortunately, we may get DICOM files that do not follow + # the standard (e.g., incorrectly anonymized) and have empty patient tag. We generate a unique ID from the study instance UID. + # The DICOM browser uses the study instance UID as patient ID directly, but this would not work in the subject hierarchy, because + # then the DICOM UID of the patient and study tag would be the same, so we add a prefix ("Patient-"). + patientId = "Patient-" + studyInstanceUid + patientItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), patientId) + studyId = slicer.dicomDatabase.fileValue(firstFile, tags['studyID']) + studyItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), studyInstanceUid) + slicer.vtkSlicerSubjectHierarchyModuleLogic.InsertDicomSeriesInHierarchy(shn, patientId, studyInstanceUid, seriesInstanceUid) + + if not patientItemID: + patientItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), patientId) + if patientItemID: + # Add attributes for DICOM tags + patientName = slicer.dicomDatabase.fileValue(firstFile, tags['patientName']) + if patientName == '': + patientName = 'No name' + + shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameAttributeName(), + patientName) + shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDAttributeName(), + patientId) + shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexAttributeName(), + slicer.dicomDatabase.fileValue(firstFile, tags['patientSex'])) + patientBirthDate = slicer.dicomDatabase.fileValue(firstFile, tags['patientBirthDate']) + shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateAttributeName(), + patientBirthDate) + shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsAttributeName(), + slicer.dicomDatabase.fileValue(firstFile, tags['patientComments'])) + # Set item name + patientItemName = patientName + if pluginHandlerSingleton.displayPatientIDInSubjectHierarchyItemName: + patientItemName += ' (' + str(patientId) + ')' + if pluginHandlerSingleton.displayPatientBirthDateInSubjectHierarchyItemName and patientBirthDate != '': + patientItemName += ' (' + str(patientBirthDate) + ')' + shn.SetItemName(patientItemID, patientItemName) + + if not studyItemID: + studyItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), studyInstanceUid) + if studyItemID: + # Add attributes for DICOM tags + studyDescription = slicer.dicomDatabase.fileValue(firstFile, tags['studyDescription']) + if studyDescription == '': + studyDescription = 'No study description' + + shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionAttributeName(), + studyDescription) + studyDate = slicer.dicomDatabase.fileValue(firstFile, tags['studyDate']) + shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyInstanceUIDAttributeName(), + studyInstanceUid) + shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDAttributeName(), + studyId) + shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateAttributeName(), + studyDate) + shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeAttributeName(), + slicer.dicomDatabase.fileValue(firstFile, tags['studyTime'])) + # Set item name + studyItemName = studyDescription + if pluginHandlerSingleton.displayStudyIDInSubjectHierarchyItemName: + studyItemName += ' (' + str(studyId) + ')' + if pluginHandlerSingleton.displayStudyDateInSubjectHierarchyItemName and studyDate != '': + studyItemName += ' (' + str(studyDate) + ')' + shn.SetItemName(studyItemID, studyItemName) + + def mapSOPClassUIDToModality(self, sopClassUID): + # Note more specialized definitions can be specified for MR by more + # specialized plugins, see codes 110800 and on in + # https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_D.html + MRname2UID = { + "MR Image Storage": "1.2.840.10008.5.1.4.1.1.4", + "Enhanced MR Image Storage": "1.2.840.10008.5.1.4.1.1.4.1", + "Legacy Converted Enhanced MR Image Storage": "1.2.840.10008.5.1.4.1.1.4.4" + } + CTname2UID = { + "CT Image Storage": "1.2.840.10008.5.1.4.1.1.2", + "Enhanced CT Image Storage": "1.2.840.10008.5.1.4.1.1.2.1", + "Legacy Converted Enhanced CT Image Storage": "1.2.840.10008.5.1.4.1.1.2.2" + } + PETname2UID = { + "Positron Emission Tomography Image Storage": "1.2.840.10008.5.1.4.1.1.128", + "Enhanced PET Image Storage": "1.2.840.10008.5.1.4.1.1.130", + "Legacy Converted Enhanced PET Image Storage": "1.2.840.10008.5.1.4.1.1.128.1" + } + + if sopClassUID in MRname2UID.values(): + return "MR" + elif sopClassUID in CTname2UID.values(): + return "CT" + elif sopClassUID in PETname2UID.values(): + return "PT" + else: + return None + + def mapSOPClassUIDToDICOMQuantityAndUnits(self, sopClassUID): + + quantity = None + units = None + + modality = self.mapSOPClassUIDToModality(sopClassUID) + if modality == "MR": + quantity = slicer.vtkCodedEntry() + quantity.SetValueSchemeMeaning("110852", "DCM", "MR signal intensity") + units = slicer.vtkCodedEntry() + units.SetValueSchemeMeaning("1", "UCUM", "no units") + elif modality == "CT": + quantity = slicer.vtkCodedEntry() + quantity.SetValueSchemeMeaning("112031", "DCM", "Attenuation Coefficient") + units = slicer.vtkCodedEntry() + units.SetValueSchemeMeaning("[hnsf'U]", "UCUM", "Hounsfield unit") + + return (quantity, units) diff --git a/Modules/Scripted/DICOMLib/DICOMPluginSelector.py b/Modules/Scripted/DICOMLib/DICOMPluginSelector.py index 106d56353fb..26971cef4e7 100644 --- a/Modules/Scripted/DICOMLib/DICOMPluginSelector.py +++ b/Modules/Scripted/DICOMLib/DICOMPluginSelector.py @@ -4,58 +4,58 @@ class DICOMPluginSelector(qt.QWidget): - """Implement the Qt code for a table of - selectable DICOM Plugins that determine - which mappings from DICOM to slicer datatypes - will be considered. - """ - - def __init__(self, parent, width=50, height=100): - super().__init__(parent) - self.setMinimumHeight(height) - self.setMinimumWidth(width) - verticalBox = qt.QVBoxLayout() - self.checkBoxByPlugin = {} - - for pluginClass in slicer.modules.dicomPlugins: - self.checkBoxByPlugin[pluginClass] = qt.QCheckBox(pluginClass) - verticalBox.addWidget(self.checkBoxByPlugin[pluginClass]) - - # Pack vertical box with plugins into a scroll area widget - verticalBoxWidget = qt.QWidget() - scrollAreaBox = qt.QVBoxLayout() - verticalBoxWidget.setLayout(verticalBox) - scrollArea = qt.QScrollArea() - scrollArea.setWidget(verticalBoxWidget) - scrollAreaBox.addWidget(scrollArea) - self.setLayout(scrollAreaBox) - settings = qt.QSettings() - - if settings.contains('DICOM/disabledPlugins/size'): - size = settings.beginReadArray('DICOM/disabledPlugins') - disabledPlugins = [] - - for i in range(size): - settings.setArrayIndex(i) - disabledPlugins.append(str(settings.allKeys()[0])) - settings.endArray() - - for pluginClass in slicer.modules.dicomPlugins: - if pluginClass in disabledPlugins: - self.checkBoxByPlugin[pluginClass].checked = False + """Implement the Qt code for a table of + selectable DICOM Plugins that determine + which mappings from DICOM to slicer datatypes + will be considered. + """ + + def __init__(self, parent, width=50, height=100): + super().__init__(parent) + self.setMinimumHeight(height) + self.setMinimumWidth(width) + verticalBox = qt.QVBoxLayout() + self.checkBoxByPlugin = {} + + for pluginClass in slicer.modules.dicomPlugins: + self.checkBoxByPlugin[pluginClass] = qt.QCheckBox(pluginClass) + verticalBox.addWidget(self.checkBoxByPlugin[pluginClass]) + + # Pack vertical box with plugins into a scroll area widget + verticalBoxWidget = qt.QWidget() + scrollAreaBox = qt.QVBoxLayout() + verticalBoxWidget.setLayout(verticalBox) + scrollArea = qt.QScrollArea() + scrollArea.setWidget(verticalBoxWidget) + scrollAreaBox.addWidget(scrollArea) + self.setLayout(scrollAreaBox) + settings = qt.QSettings() + + if settings.contains('DICOM/disabledPlugins/size'): + size = settings.beginReadArray('DICOM/disabledPlugins') + disabledPlugins = [] + + for i in range(size): + settings.setArrayIndex(i) + disabledPlugins.append(str(settings.allKeys()[0])) + settings.endArray() + + for pluginClass in slicer.modules.dicomPlugins: + if pluginClass in disabledPlugins: + self.checkBoxByPlugin[pluginClass].checked = False + else: + # Activate plugins for the ones who are not in the disabled list + # and also plugins installed with extensions + self.checkBoxByPlugin[pluginClass].checked = True else: - # Activate plugins for the ones who are not in the disabled list - # and also plugins installed with extensions - self.checkBoxByPlugin[pluginClass].checked = True - else: - # All DICOM plugins would be enabled by default - for pluginClass in slicer.modules.dicomPlugins: - self.checkBoxByPlugin[pluginClass].checked = True - - def selectedPlugins(self): - """Return a list of selected plugins""" - selectedPlugins = [] - for pluginClass in slicer.modules.dicomPlugins: - if self.checkBoxByPlugin[pluginClass].checked: - selectedPlugins.append(pluginClass) - return selectedPlugins + # All DICOM plugins would be enabled by default + for pluginClass in slicer.modules.dicomPlugins: + self.checkBoxByPlugin[pluginClass].checked = True + + def selectedPlugins(self): + """Return a list of selected plugins""" + selectedPlugins = [] + for pluginClass in slicer.modules.dicomPlugins: + if self.checkBoxByPlugin[pluginClass].checked: + selectedPlugins.append(pluginClass) + return selectedPlugins diff --git a/Modules/Scripted/DICOMLib/DICOMProcesses.py b/Modules/Scripted/DICOMLib/DICOMProcesses.py index f1f1d08c733..a2f8e83f692 100644 --- a/Modules/Scripted/DICOMLib/DICOMProcesses.py +++ b/Modules/Scripted/DICOMLib/DICOMProcesses.py @@ -30,596 +30,596 @@ class DICOMProcess: - """helper class to run dcmtk's executables - Code here depends only on python and DCMTK executables - """ - - def __init__(self): - self.process = None - self.connections = {} - pathOptions = ( - '/../DCMTK-build/bin/Debug', - '/../DCMTK-build/bin/Release', - '/../DCMTK-build/bin/RelWithDebInfo', - '/../DCMTK-build/bin/MinSizeRel', - '/../DCMTK-build/bin', - '/../CTK-build/CMakeExternals/Install/bin', - '/bin' - ) + """helper class to run dcmtk's executables + Code here depends only on python and DCMTK executables + """ - self.exeDir = None - for path in pathOptions: - testPath = slicer.app.slicerHome + path - if os.path.exists(testPath): - self.exeDir = testPath - break - if not self.exeDir: - raise UserWarning("Could not find a valid path to DICOM helper applications") - - self.exeExtension = "" - if os.name == 'nt': - self.exeExtension = '.exe' - - self.QProcessState = {0: 'NotRunning', 1: 'Starting', 2: 'Running',} - - def __del__(self): - self.stop() - - def start(self, cmd, args): - if self.process is not None: - self.stop() - self.cmd = cmd - self.args = args - - # start the server! - self.process = qt.QProcess() - self.process.connect('stateChanged(QProcess::ProcessState)', self.onStateChanged) - logging.debug(("Starting %s with " % cmd, args)) - self.process.start(cmd, args) - - def onStateChanged(self, newState): - logging.debug(f"Process {self.cmd} now in state {self.QProcessState[newState]}") - if newState == 0 and self.process: - stdout = self.process.readAllStandardOutput() - stderr = self.process.readAllStandardError() - logging.debug('DICOM process error code is: %d' % self.process.error()) - logging.debug('DICOM process standard out is: %s' % stdout) - logging.debug('DICOM process standard error is: %s' % stderr) - return stdout, stderr - return None, None - - def stop(self): - if hasattr(self,'process'): - if self.process: - logging.debug("stopping DICOM process") - self.process.kill() - # Wait up to 3 seconds for the process to stop - self.process.waitForFinished(3000) + def __init__(self): self.process = None + self.connections = {} + pathOptions = ( + '/../DCMTK-build/bin/Debug', + '/../DCMTK-build/bin/Release', + '/../DCMTK-build/bin/RelWithDebInfo', + '/../DCMTK-build/bin/MinSizeRel', + '/../DCMTK-build/bin', + '/../CTK-build/CMakeExternals/Install/bin', + '/bin' + ) + + self.exeDir = None + for path in pathOptions: + testPath = slicer.app.slicerHome + path + if os.path.exists(testPath): + self.exeDir = testPath + break + if not self.exeDir: + raise UserWarning("Could not find a valid path to DICOM helper applications") + + self.exeExtension = "" + if os.name == 'nt': + self.exeExtension = '.exe' + + self.QProcessState = {0: 'NotRunning', 1: 'Starting', 2: 'Running', } + + def __del__(self): + self.stop() + + def start(self, cmd, args): + if self.process is not None: + self.stop() + self.cmd = cmd + self.args = args + + # start the server! + self.process = qt.QProcess() + self.process.connect('stateChanged(QProcess::ProcessState)', self.onStateChanged) + logging.debug(("Starting %s with " % cmd, args)) + self.process.start(cmd, args) + + def onStateChanged(self, newState): + logging.debug(f"Process {self.cmd} now in state {self.QProcessState[newState]}") + if newState == 0 and self.process: + stdout = self.process.readAllStandardOutput() + stderr = self.process.readAllStandardError() + logging.debug('DICOM process error code is: %d' % self.process.error()) + logging.debug('DICOM process standard out is: %s' % stdout) + logging.debug('DICOM process standard error is: %s' % stderr) + return stdout, stderr + return None, None + + def stop(self): + if hasattr(self, 'process'): + if self.process: + logging.debug("stopping DICOM process") + self.process.kill() + # Wait up to 3 seconds for the process to stop + self.process.waitForFinished(3000) + self.process = None class DICOMCommand(DICOMProcess): - """ - Run a generic dcmtk command and return the stdout - """ - - def __init__(self,cmd,args): - super().__init__() - self.executable = self.exeDir+'/'+cmd+self.exeExtension - self.args = args - - def __del__(self): - super().__del__() - - def start(self): - # run the process! - self.process = qt.QProcess() - logging.debug(('DICOM process running: ', self.executable, self.args)) - self.process.start(self.executable, self.args) - self.process.waitForFinished() - if self.process.exitStatus() == qt.QProcess.CrashExit or self.process.exitCode() != 0: - stdout = self.process.readAllStandardOutput() - stderr = self.process.readAllStandardError() - logging.debug('DICOM process exit status is: %d' % self.process.exitStatus()) - logging.debug('DICOM process exit code is: %d' % self.process.exitCode()) - logging.debug('DICOM process error is: %d' % self.process.error()) - logging.debug('DICOM process standard out is: %s' % stdout) - logging.debug('DICOM process standard error is: %s' % stderr) - raise UserWarning(f"Could not run {self.executable} with {self.args}") - stdout = self.process.readAllStandardOutput() - return stdout + """ + Run a generic dcmtk command and return the stdout + """ + + def __init__(self, cmd, args): + super().__init__() + self.executable = self.exeDir + '/' + cmd + self.exeExtension + self.args = args + + def __del__(self): + super().__del__() + + def start(self): + # run the process! + self.process = qt.QProcess() + logging.debug(('DICOM process running: ', self.executable, self.args)) + self.process.start(self.executable, self.args) + self.process.waitForFinished() + if self.process.exitStatus() == qt.QProcess.CrashExit or self.process.exitCode() != 0: + stdout = self.process.readAllStandardOutput() + stderr = self.process.readAllStandardError() + logging.debug('DICOM process exit status is: %d' % self.process.exitStatus()) + logging.debug('DICOM process exit code is: %d' % self.process.exitCode()) + logging.debug('DICOM process error is: %d' % self.process.error()) + logging.debug('DICOM process standard out is: %s' % stdout) + logging.debug('DICOM process standard error is: %s' % stderr) + raise UserWarning(f"Could not run {self.executable} with {self.args}") + stdout = self.process.readAllStandardOutput() + return stdout class DICOMStoreSCPProcess(DICOMProcess): - """helper class to run dcmtk's storescp - Code here depends only on python and DCMTK executables - TODO: it might make sense to refactor this as a generic tool - for interacting with DCMTK - """ - - STORESCP_PROCESS_FILE_NAME = "storescp" - - def __init__(self, incomingDataDir, incomingPort=None): - super().__init__() - - self.incomingDataDir = incomingDataDir - if not os.path.exists(self.incomingDataDir): - os.mkdir(self.incomingDataDir) - - if incomingPort: - assert isinstance(incomingPort, int) - self.port = str(incomingPort) - else: - settings = qt.QSettings() - self.port = settings.value('StoragePort') - if not self.port: - settings.setValue('StoragePort', '11112') - self.port = settings.value('StoragePort') - - self.storescpExecutable = os.path.join(self.exeDir, self.STORESCP_PROCESS_FILE_NAME + self.exeExtension) - self.dcmdumpExecutable = os.path.join(self.exeDir,'dcmdump'+self.exeExtension) - - def __del__(self): - super().__del__() - - def onStateChanged(self, newState): - stdout, stderr = super().onStateChanged(newState) - if stderr and stderr.size(): - slicer.util.errorDisplay("An error occurred. For further information click 'Show Details...'", - windowTitle=self.__class__.__name__, detailedText=str(stderr)) - return stdout, stderr - - def start(self, cmd=None, args=None): - # Offer to terminate running SCP processes. - # They may be started by other applications, listening on other ports, so we try to start ours anyway. - self.killStoreSCPProcesses() - onReceptionCallback = '%s --load-short --print-short --print-filename --search PatientName "%s/#f"' \ - % (self.dcmdumpExecutable, self.incomingDataDir) - args = [str(self.port), '--accept-all', '--output-directory' , self.incomingDataDir, '--exec-sync', - '--exec-on-reception', onReceptionCallback] - logging.debug("Starting storescp process") - super().start(self.storescpExecutable, args) - self.process.connect('readyReadStandardOutput()', self.readFromStandardOutput) - - def killStoreSCPProcesses(self): - uniqueListener = True - if os.name == 'nt': - uniqueListener = self.killStoreSCPProcessesNT(uniqueListener) - elif os.name == 'posix': - uniqueListener = self.killStoreSCPProcessesPosix(uniqueListener) - return uniqueListener - - def killStoreSCPProcessesPosix(self, uniqueListener): - p = subprocess.Popen(['ps', '-A'], stdout=subprocess.PIPE) - out, err = p.communicate() - for line in out.splitlines(): - line = line.decode() - if self.STORESCP_PROCESS_FILE_NAME in line: - pid = int(line.split(None, 1)[0]) - uniqueListener = self.notifyUserAboutRunningStoreSCP(pid) - return uniqueListener - - def findAndKillProcessNT(self, processName, killProcess): - """Find (and optionally terminate) processes by the specified name. - Returns true if process by that name exists (after attempting to - terminate the process). + """helper class to run dcmtk's storescp + Code here depends only on python and DCMTK executables + TODO: it might make sense to refactor this as a generic tool + for interacting with DCMTK """ - import ctypes - import ctypes.wintypes - import os.path - - psapi = ctypes.WinDLL('Psapi.dll') - enum_processes = psapi.EnumProcesses - enum_processes.restype = ctypes.wintypes.BOOL - get_process_image_file_name = psapi.GetProcessImageFileNameA - get_process_image_file_name.restype = ctypes.wintypes.DWORD - - kernel32 = ctypes.WinDLL('kernel32.dll') - open_process = kernel32.OpenProcess - open_process.restype = ctypes.wintypes.HANDLE - terminate_process = kernel32.TerminateProcess - terminate_process.restype = ctypes.wintypes.BOOL - close_handle = kernel32.CloseHandle - - MAX_PATH = 260 - PROCESS_TERMINATE = 0x0001 - PROCESS_QUERY_INFORMATION = 0x0400 - - count = 512 - while True: - process_ids = (ctypes.wintypes.DWORD * count)() - cb = ctypes.sizeof(process_ids) - bytes_returned = ctypes.wintypes.DWORD() - if enum_processes(ctypes.byref(process_ids), cb, ctypes.byref(bytes_returned)): - if bytes_returned.value < cb: - break + + STORESCP_PROCESS_FILE_NAME = "storescp" + + def __init__(self, incomingDataDir, incomingPort=None): + super().__init__() + + self.incomingDataDir = incomingDataDir + if not os.path.exists(self.incomingDataDir): + os.mkdir(self.incomingDataDir) + + if incomingPort: + assert isinstance(incomingPort, int) + self.port = str(incomingPort) else: - count *= 2 - else: - logging.error("Call to EnumProcesses failed") + settings = qt.QSettings() + self.port = settings.value('StoragePort') + if not self.port: + settings.setValue('StoragePort', '11112') + self.port = settings.value('StoragePort') + + self.storescpExecutable = os.path.join(self.exeDir, self.STORESCP_PROCESS_FILE_NAME + self.exeExtension) + self.dcmdumpExecutable = os.path.join(self.exeDir, 'dcmdump' + self.exeExtension) + + def __del__(self): + super().__del__() + + def onStateChanged(self, newState): + stdout, stderr = super().onStateChanged(newState) + if stderr and stderr.size(): + slicer.util.errorDisplay("An error occurred. For further information click 'Show Details...'", + windowTitle=self.__class__.__name__, detailedText=str(stderr)) + return stdout, stderr + + def start(self, cmd=None, args=None): + # Offer to terminate running SCP processes. + # They may be started by other applications, listening on other ports, so we try to start ours anyway. + self.killStoreSCPProcesses() + onReceptionCallback = '%s --load-short --print-short --print-filename --search PatientName "%s/#f"' \ + % (self.dcmdumpExecutable, self.incomingDataDir) + args = [str(self.port), '--accept-all', '--output-directory', self.incomingDataDir, '--exec-sync', + '--exec-on-reception', onReceptionCallback] + logging.debug("Starting storescp process") + super().start(self.storescpExecutable, args) + self.process.connect('readyReadStandardOutput()', self.readFromStandardOutput) + + def killStoreSCPProcesses(self): + uniqueListener = True + if os.name == 'nt': + uniqueListener = self.killStoreSCPProcessesNT(uniqueListener) + elif os.name == 'posix': + uniqueListener = self.killStoreSCPProcessesPosix(uniqueListener) + return uniqueListener + + def killStoreSCPProcessesPosix(self, uniqueListener): + p = subprocess.Popen(['ps', '-A'], stdout=subprocess.PIPE) + out, err = p.communicate() + for line in out.splitlines(): + line = line.decode() + if self.STORESCP_PROCESS_FILE_NAME in line: + pid = int(line.split(None, 1)[0]) + uniqueListener = self.notifyUserAboutRunningStoreSCP(pid) + return uniqueListener + + def findAndKillProcessNT(self, processName, killProcess): + """Find (and optionally terminate) processes by the specified name. + Returns true if process by that name exists (after attempting to + terminate the process). + """ + import ctypes + import ctypes.wintypes + import os.path + + psapi = ctypes.WinDLL('Psapi.dll') + enum_processes = psapi.EnumProcesses + enum_processes.restype = ctypes.wintypes.BOOL + get_process_image_file_name = psapi.GetProcessImageFileNameA + get_process_image_file_name.restype = ctypes.wintypes.DWORD + + kernel32 = ctypes.WinDLL('kernel32.dll') + open_process = kernel32.OpenProcess + open_process.restype = ctypes.wintypes.HANDLE + terminate_process = kernel32.TerminateProcess + terminate_process.restype = ctypes.wintypes.BOOL + close_handle = kernel32.CloseHandle + + MAX_PATH = 260 + PROCESS_TERMINATE = 0x0001 + PROCESS_QUERY_INFORMATION = 0x0400 + + count = 512 + while True: + process_ids = (ctypes.wintypes.DWORD * count)() + cb = ctypes.sizeof(process_ids) + bytes_returned = ctypes.wintypes.DWORD() + if enum_processes(ctypes.byref(process_ids), cb, ctypes.byref(bytes_returned)): + if bytes_returned.value < cb: + break + else: + count *= 2 + else: + logging.error("Call to EnumProcesses failed") + return False + + processMayBeStillRunning = False + + for index in range(int(bytes_returned.value / ctypes.sizeof(ctypes.wintypes.DWORD))): + process_id = process_ids[index] + h_process = open_process(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION, False, process_id) + if h_process: + image_file_name = (ctypes.c_char * MAX_PATH)() + if get_process_image_file_name(h_process, image_file_name, MAX_PATH) > 0: + filename = os.path.basename(image_file_name.value) + if filename.decode() == processName: + # Found the process we are looking for + if not killProcess: + # we don't need to kill the process, just indicate that there is a process to kill + res = close_handle(h_process) + return True + if not terminate_process(h_process, 1): + # failed to terminate process, it may be still running + processMayBeStillRunning = True + + res = close_handle(h_process) + + return processMayBeStillRunning + + def isStoreSCPProcessesRunningNT(self): + return self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, False) + + def killStoreSCPProcessesNT(self, uniqueListener): + if self.isStoreSCPProcessesRunningNT(): + uniqueListener = self.notifyUserAboutRunningStoreSCP() + return uniqueListener + + def readFromStandardOutput(self, readLineCallback=None): + lines = [] + while self.process.canReadLine(): + line = str(self.process.readLine()) + lines.append(line) + logging.debug("Output from {}: {}".format(self.__class__.__name__, "\n".join(lines))) + if readLineCallback: + for line in lines: + # Remove stray newline and single-quote characters + clearLine = line.replace('\\r', '').replace('\\n', '').replace('\'', '').strip() + readLineCallback(clearLine) + self.readFromStandardError() + + def readFromStandardError(self): + stdErr = str(self.process.readAllStandardError()) + if stdErr: + logging.debug(f"Error output from {self.__class__.__name__}: {stdErr}") + + def notifyUserAboutRunningStoreSCP(self, pid=None): + if slicer.util.confirmYesNoDisplay('There are other DICOM listeners running.\n Do you want to end them?'): + if os.name == 'nt': + self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, True) + # Killing processes can take a while, so we retry a couple of times until we confirm that there + # are no more listeners. + retryAttempts = 5 + while retryAttempts: + if not self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, False): + break + retryAttempts -= 1 + time.sleep(1) + elif os.name == 'posix': + import signal + os.kill(pid, signal.SIGKILL) + return True return False - processMayBeStillRunning = False - - for index in range(int(bytes_returned.value / ctypes.sizeof(ctypes.wintypes.DWORD))): - process_id = process_ids[index] - h_process = open_process(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION, False, process_id) - if h_process: - image_file_name = (ctypes.c_char * MAX_PATH)() - if get_process_image_file_name(h_process, image_file_name, MAX_PATH) > 0: - filename = os.path.basename(image_file_name.value) - if filename.decode() == processName: - # Found the process we are looking for - if not killProcess: - # we don't need to kill the process, just indicate that there is a process to kill - res = close_handle(h_process) - return True - if not terminate_process(h_process, 1): - # failed to terminate process, it may be still running - processMayBeStillRunning = True - - res = close_handle(h_process) - - return processMayBeStillRunning - - def isStoreSCPProcessesRunningNT(self): - return self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, False) - - def killStoreSCPProcessesNT(self, uniqueListener): - if self.isStoreSCPProcessesRunningNT(): - uniqueListener = self.notifyUserAboutRunningStoreSCP() - return uniqueListener - - def readFromStandardOutput(self, readLineCallback=None): - lines = [] - while self.process.canReadLine(): - line = str(self.process.readLine()) - lines.append(line) - logging.debug("Output from {}: {}".format(self.__class__.__name__, "\n".join(lines))) - if readLineCallback: - for line in lines: - # Remove stray newline and single-quote characters - clearLine = line.replace('\\r', '').replace('\\n', '').replace('\'', '').strip() - readLineCallback(clearLine) - self.readFromStandardError() - - def readFromStandardError(self): - stdErr = str(self.process.readAllStandardError()) - if stdErr: - logging.debug(f"Error output from {self.__class__.__name__}: {stdErr}") - - def notifyUserAboutRunningStoreSCP(self, pid=None): - if slicer.util.confirmYesNoDisplay('There are other DICOM listeners running.\n Do you want to end them?'): - if os.name == 'nt': - self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, True) - # Killing processes can take a while, so we retry a couple of times until we confirm that there - # are no more listeners. - retryAttempts = 5 - while retryAttempts: - if not self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, False): - break - retryAttempts -= 1 - time.sleep(1) - elif os.name == 'posix': - import signal - os.kill(pid, signal.SIGKILL) - return True - return False - class DICOMListener(DICOMStoreSCPProcess): - """helper class that uses storscp process including indexing - into Slicer DICOMdatabase. - TODO: down the line we might have ctkDICOMListener perform - this task as a QObject callable from PythonQt - """ - - def __init__(self, database, fileToBeAddedCallback=None, fileAddedCallback=None): - self.dicomDatabase = database - self.indexer = ctk.ctkDICOMIndexer() - # Enable background indexing to improve performance. - self.indexer.backgroundImportEnabled = True - self.fileToBeAddedCallback = fileToBeAddedCallback - self.fileAddedCallback = fileAddedCallback - self.lastFileAdded = None - - # A timer is used to ensure that indexing is completed after new files come in, - # but without enforcing completing the indexing after each file (because - # waiting for indexing to be completed has an overhead). - autoUpdateDelaySec = 10.0 - self.delayedAutoUpdateTimer = qt.QTimer() - self.delayedAutoUpdateTimer.setSingleShot(True) - self.delayedAutoUpdateTimer.interval = autoUpdateDelaySec * 1000 - self.delayedAutoUpdateTimer.connect('timeout()', self.completeIncomingFilesIndexing) - - # List of received files that are being indexed - self.incomingFiles = [] - # After self.incomingFiles reaches maximumIncomingFiles, indexing will be forced - # to limit disk space usage (until indexing is completed, the file is present both - # in the incoming folder and in the database) and make sure some updates are visible - # in the DICOM browser (even if files are continuously coming in). - # Smaller values result in more frequent updates, slightly less disk space usage, - # slightly slower import. - self.maximumIncomingFiles = 400 - - databaseDirectory = self.dicomDatabase.databaseDirectory - if not databaseDirectory: - raise UserWarning('Database directory not set: cannot start DICOMListener') - if not os.path.exists(databaseDirectory): - os.mkdir(databaseDirectory) - incomingDir = databaseDirectory + "/incoming" - super().__init__(incomingDataDir=incomingDir) - - def __del__(self): - super().__del__() - - def readFromStandardOutput(self): - super().readFromStandardOutput(readLineCallback=self.processStdoutLine) - - def completeIncomingFilesIndexing(self): - """Complete indexing of all incoming files and remove them from the incoming folder.""" - logging.debug(f"Complete indexing for indexing to complete for {len(self.incomingFiles)} files.") - import os - self.indexer.waitForImportFinished() - for dicomFilePath in self.incomingFiles: - os.remove(dicomFilePath) - self.incomingFiles = [] - - def processStdoutLine(self, line): - searchTag = '# dcmdump (1/1): ' - tagStart = line.find(searchTag) - if tagStart != -1: - dicomFilePath = line[tagStart + len(searchTag):].strip() - slicer.dicomFilePath = dicomFilePath - logging.debug("indexing: %s " % dicomFilePath) - if self.fileToBeAddedCallback: - self.fileToBeAddedCallback() - self.indexer.addFile(self.dicomDatabase, dicomFilePath, True) - self.incomingFiles.append(dicomFilePath) - if len(self.incomingFiles) < self.maximumIncomingFiles: - self.delayedAutoUpdateTimer.start() - else: - # Limit of pending incoming files is reached, complete indexing of files - # that we have received so far. - self.delayedAutoUpdateTimer.stop() - self.completeIncomingFilesIndexing() - self.lastFileAdded = dicomFilePath - if self.fileAddedCallback: - logging.debug("calling callback...") - self.fileAddedCallback() - logging.debug("callback done") - else: - logging.debug("no callback") + """helper class that uses storscp process including indexing + into Slicer DICOMdatabase. + TODO: down the line we might have ctkDICOMListener perform + this task as a QObject callable from PythonQt + """ + + def __init__(self, database, fileToBeAddedCallback=None, fileAddedCallback=None): + self.dicomDatabase = database + self.indexer = ctk.ctkDICOMIndexer() + # Enable background indexing to improve performance. + self.indexer.backgroundImportEnabled = True + self.fileToBeAddedCallback = fileToBeAddedCallback + self.fileAddedCallback = fileAddedCallback + self.lastFileAdded = None + + # A timer is used to ensure that indexing is completed after new files come in, + # but without enforcing completing the indexing after each file (because + # waiting for indexing to be completed has an overhead). + autoUpdateDelaySec = 10.0 + self.delayedAutoUpdateTimer = qt.QTimer() + self.delayedAutoUpdateTimer.setSingleShot(True) + self.delayedAutoUpdateTimer.interval = autoUpdateDelaySec * 1000 + self.delayedAutoUpdateTimer.connect('timeout()', self.completeIncomingFilesIndexing) + + # List of received files that are being indexed + self.incomingFiles = [] + # After self.incomingFiles reaches maximumIncomingFiles, indexing will be forced + # to limit disk space usage (until indexing is completed, the file is present both + # in the incoming folder and in the database) and make sure some updates are visible + # in the DICOM browser (even if files are continuously coming in). + # Smaller values result in more frequent updates, slightly less disk space usage, + # slightly slower import. + self.maximumIncomingFiles = 400 + + databaseDirectory = self.dicomDatabase.databaseDirectory + if not databaseDirectory: + raise UserWarning('Database directory not set: cannot start DICOMListener') + if not os.path.exists(databaseDirectory): + os.mkdir(databaseDirectory) + incomingDir = databaseDirectory + "/incoming" + super().__init__(incomingDataDir=incomingDir) + + def __del__(self): + super().__del__() + + def readFromStandardOutput(self): + super().readFromStandardOutput(readLineCallback=self.processStdoutLine) + + def completeIncomingFilesIndexing(self): + """Complete indexing of all incoming files and remove them from the incoming folder.""" + logging.debug(f"Complete indexing for indexing to complete for {len(self.incomingFiles)} files.") + import os + self.indexer.waitForImportFinished() + for dicomFilePath in self.incomingFiles: + os.remove(dicomFilePath) + self.incomingFiles = [] + + def processStdoutLine(self, line): + searchTag = '# dcmdump (1/1): ' + tagStart = line.find(searchTag) + if tagStart != -1: + dicomFilePath = line[tagStart + len(searchTag):].strip() + slicer.dicomFilePath = dicomFilePath + logging.debug("indexing: %s " % dicomFilePath) + if self.fileToBeAddedCallback: + self.fileToBeAddedCallback() + self.indexer.addFile(self.dicomDatabase, dicomFilePath, True) + self.incomingFiles.append(dicomFilePath) + if len(self.incomingFiles) < self.maximumIncomingFiles: + self.delayedAutoUpdateTimer.start() + else: + # Limit of pending incoming files is reached, complete indexing of files + # that we have received so far. + self.delayedAutoUpdateTimer.stop() + self.completeIncomingFilesIndexing() + self.lastFileAdded = dicomFilePath + if self.fileAddedCallback: + logging.debug("calling callback...") + self.fileAddedCallback() + logging.debug("callback done") + else: + logging.debug("no callback") class DICOMSender(DICOMProcess): - """ Code to send files to a remote host. - (Uses storescu from dcmtk.) - """ - extended_dicom_config_path = 'DICOM/dcmtk/storescu-seg.cfg' - - def __init__(self,files,address,protocol=None,progressCallback=None,aeTitle=None): - """protocol: can be DIMSE (default) or DICOMweb - port: optional (if not specified then address URL should contain it) - """ - super().__init__() - self.files = files - self.destinationUrl = qt.QUrl().fromUserInput(address) - if aeTitle: - self.aeTitle = aeTitle - else: - self.aeTitle = "CTK" - self.protocol = protocol if protocol is not None else "DIMSE" - self.progressCallback = progressCallback - if not self.progressCallback: - self.progressCallback = self.defaultProgressCallback - self.send() - - def __del__(self): - super().__del__() - - def defaultProgressCallback(self,s): - logging.debug(s) - - def send(self): - self.progressCallback("Starting send to %s using self.protocol" % self.destinationUrl.toString()) - - if self.protocol == "DICOMweb": - # DICOMweb - # Ensure that correct version of dicomweb-client Python package is installed - needRestart = False - needInstall = False - minimumDicomwebClientVersion = "0.51" - try: - import dicomweb_client - from packaging import version - if version.parse(dicomweb_client.__version__) < version.parse(minimumDicomwebClientVersion): - if not slicer.util.confirmOkCancelDisplay(f"DICOMweb sending requires installation of dicomweb-client (version {minimumDicomwebClientVersion} or later).\nClick OK to upgrade dicomweb-client and restart the application."): - self.showBrowserOnEnter = False - return - needRestart = True - needInstall = True - except ModuleNotFoundError: - needInstall = True - - if needInstall: - # pythonweb-client 0.50 was broken (https://github.com/MGHComputationalPathology/dicomweb-client/issues/41) - progressDialog = slicer.util.createProgressDialog(labelText='Upgrading dicomweb-client. This may take a minute...', maximum=0) - slicer.app.processEvents() - slicer.util.pip_install(f'dicomweb-client>={minimumDicomwebClientVersion}') - import dicomweb_client - progressDialog.close() - if needRestart: - slicer.util.restart() - - # Establish connection - import dicomweb_client.log - dicomweb_client.log.configure_logging(2) - from dicomweb_client.api import DICOMwebClient - effectiveServerUrl = self.destinationUrl.toString() - session = None - headers = {} - # Setting up of the DICOMweb client from various server parameters can be done - # in plugins in the future, but for now just hardcode special initialization - # steps for a few server types. - if "kheops" in effectiveServerUrl: - # Kheops DICOMweb API endpoint from browser view URL - url = qt.QUrl(effectiveServerUrl) - if url.path().startswith('/view/'): - # This is a Kheops viewer URL. - # Retrieve the token from the viewer URL and use the Kheops API URL to connect to the server. - token = url.path().replace('/view/','') - effectiveServerUrl = "https://demo.kheops.online/api" - from requests.auth import HTTPBasicAuth - from dicomweb_client.session_utils import create_session_from_auth - auth = HTTPBasicAuth('token', token) - session = create_session_from_auth(auth) - - client = DICOMwebClient(url=effectiveServerUrl, session=session, headers=headers) - - for file in self.files: - if not self.progressCallback(f"Sending {file} to {self.destinationUrl.toString()} using {self.protocol}"): - raise UserWarning("Sending was cancelled, upload is incomplete.") - import pydicom - dataset = pydicom.dcmread(file) - client.store_instances(datasets=[dataset]) - else: - # DIMSE (traditional DICOM networking) - for file in self.files: - self.start(file) - if not self.progressCallback(f"Sent {file} to {self.destinationUrl.host()}:{self.destinationUrl.port()}"): - raise UserWarning("Sending was cancelled, upload is incomplete.") - - def dicomSend(self, file, config=None, config_profile='Default'): - """Send DICOM file to the specified modality.""" - self.storeSCUExecutable = self.exeDir+'/storescu'+self.exeExtension - - ### TODO: maybe use dcmsend (is smarter about the compress/decompress) - - args = [] - - # Utilize custom configuration - if config and os.path.exists(config): - args.extend(('-xf', config, config_profile)) - - # Core arguments: hostname, port, AEC, file - args.extend((self.destinationUrl.host(), str(self.destinationUrl.port()), "-aec", self.aeTitle, file)) - - # Execute SCU CLI program and wait for termination. Uses super().start() to access the - # to initialize the background process and wait for completion of the transfer. - super().start(self.storeSCUExecutable, args) - self.process.waitForFinished() - return not (self.process.ExitStatus() == qt.QProcess.CrashExit or self.process.exitCode() != 0) - - def start(self, file): - """ Send DICOM file to the specified modality. If the transfer fails due to - an unsupported presentation context, attempt the transfer a second time using - a custom configuration that provides. + """ Code to send files to a remote host. + (Uses storescu from dcmtk.) """ + extended_dicom_config_path = 'DICOM/dcmtk/storescu-seg.cfg' + + def __init__(self, files, address, protocol=None, progressCallback=None, aeTitle=None): + """protocol: can be DIMSE (default) or DICOMweb + port: optional (if not specified then address URL should contain it) + """ + super().__init__() + self.files = files + self.destinationUrl = qt.QUrl().fromUserInput(address) + if aeTitle: + self.aeTitle = aeTitle + else: + self.aeTitle = "CTK" + self.protocol = protocol if protocol is not None else "DIMSE" + self.progressCallback = progressCallback + if not self.progressCallback: + self.progressCallback = self.defaultProgressCallback + self.send() + + def __del__(self): + super().__del__() + + def defaultProgressCallback(self, s): + logging.debug(s) + + def send(self): + self.progressCallback("Starting send to %s using self.protocol" % self.destinationUrl.toString()) + + if self.protocol == "DICOMweb": + # DICOMweb + # Ensure that correct version of dicomweb-client Python package is installed + needRestart = False + needInstall = False + minimumDicomwebClientVersion = "0.51" + try: + import dicomweb_client + from packaging import version + if version.parse(dicomweb_client.__version__) < version.parse(minimumDicomwebClientVersion): + if not slicer.util.confirmOkCancelDisplay(f"DICOMweb sending requires installation of dicomweb-client (version {minimumDicomwebClientVersion} or later).\nClick OK to upgrade dicomweb-client and restart the application."): + self.showBrowserOnEnter = False + return + needRestart = True + needInstall = True + except ModuleNotFoundError: + needInstall = True + + if needInstall: + # pythonweb-client 0.50 was broken (https://github.com/MGHComputationalPathology/dicomweb-client/issues/41) + progressDialog = slicer.util.createProgressDialog(labelText='Upgrading dicomweb-client. This may take a minute...', maximum=0) + slicer.app.processEvents() + slicer.util.pip_install(f'dicomweb-client>={minimumDicomwebClientVersion}') + import dicomweb_client + progressDialog.close() + if needRestart: + slicer.util.restart() + + # Establish connection + import dicomweb_client.log + dicomweb_client.log.configure_logging(2) + from dicomweb_client.api import DICOMwebClient + effectiveServerUrl = self.destinationUrl.toString() + session = None + headers = {} + # Setting up of the DICOMweb client from various server parameters can be done + # in plugins in the future, but for now just hardcode special initialization + # steps for a few server types. + if "kheops" in effectiveServerUrl: + # Kheops DICOMweb API endpoint from browser view URL + url = qt.QUrl(effectiveServerUrl) + if url.path().startswith('/view/'): + # This is a Kheops viewer URL. + # Retrieve the token from the viewer URL and use the Kheops API URL to connect to the server. + token = url.path().replace('/view/', '') + effectiveServerUrl = "https://demo.kheops.online/api" + from requests.auth import HTTPBasicAuth + from dicomweb_client.session_utils import create_session_from_auth + auth = HTTPBasicAuth('token', token) + session = create_session_from_auth(auth) + + client = DICOMwebClient(url=effectiveServerUrl, session=session, headers=headers) + + for file in self.files: + if not self.progressCallback(f"Sending {file} to {self.destinationUrl.toString()} using {self.protocol}"): + raise UserWarning("Sending was cancelled, upload is incomplete.") + import pydicom + dataset = pydicom.dcmread(file) + client.store_instances(datasets=[dataset]) + else: + # DIMSE (traditional DICOM networking) + for file in self.files: + self.start(file) + if not self.progressCallback(f"Sent {file} to {self.destinationUrl.host()}:{self.destinationUrl.port()}"): + raise UserWarning("Sending was cancelled, upload is incomplete.") + + def dicomSend(self, file, config=None, config_profile='Default'): + """Send DICOM file to the specified modality.""" + self.storeSCUExecutable = self.exeDir + '/storescu' + self.exeExtension + + # TODO: maybe use dcmsend (is smarter about the compress/decompress) - if self.dicomSend(file): - # success - return True + args = [] - stdout = self.process.readAllStandardOutput() - stderr = self.process.readAllStandardError() - logging.debug('DICOM send using standard configuration failed: process error code is %d' % self.process.error()) - logging.debug('DICOM send process standard out is: %s' % stdout) - logging.debug('DICOM send process standard error is: %s' % stderr) + # Utilize custom configuration + if config and os.path.exists(config): + args.extend(('-xf', config, config_profile)) - # Retry transfer with alternative configuration with presentation contexts which support SEG/SR. - # A common cause of failure is an incomplete set of dcmtk/DCMSCU presentation context UIDS. - # Refer to https://book.orthanc-server.com/faq/dcmtk-tricks.html#id2 for additional detail. - logging.info('Retry transfer with alternative dicomscu configuration: %s' % self.extended_dicom_config_path) + # Core arguments: hostname, port, AEC, file + args.extend((self.destinationUrl.host(), str(self.destinationUrl.port()), "-aec", self.aeTitle, file)) - # Terminate transfer and notify user of failure - if self.dicomSend(file, config=os.path.join(RESOURCE_ROOT, self.extended_dicom_config_path)): - # success - return True + # Execute SCU CLI program and wait for termination. Uses super().start() to access the + # to initialize the background process and wait for completion of the transfer. + super().start(self.storeSCUExecutable, args) + self.process.waitForFinished() + return not (self.process.ExitStatus() == qt.QProcess.CrashExit or self.process.exitCode() != 0) - stdout = self.process.readAllStandardOutput() - stderr = self.process.readAllStandardError() - logging.debug('DICOM send using extended configuration failed: process error code is %d' % self.process.error()) - logging.debug('DICOM send process standard out is: %s' % stdout) - logging.debug('DICOM send process standard error is: %s' % stderr) + def start(self, file): + """ Send DICOM file to the specified modality. If the transfer fails due to + an unsupported presentation context, attempt the transfer a second time using + a custom configuration that provides. + """ - userMsg = f"Could not send {file} to {self.destinationUrl.host()}:{self.destinationUrl.port()}" - raise UserWarning(userMsg) + if self.dicomSend(file): + # success + return True + + stdout = self.process.readAllStandardOutput() + stderr = self.process.readAllStandardError() + logging.debug('DICOM send using standard configuration failed: process error code is %d' % self.process.error()) + logging.debug('DICOM send process standard out is: %s' % stdout) + logging.debug('DICOM send process standard error is: %s' % stderr) + + # Retry transfer with alternative configuration with presentation contexts which support SEG/SR. + # A common cause of failure is an incomplete set of dcmtk/DCMSCU presentation context UIDS. + # Refer to https://book.orthanc-server.com/faq/dcmtk-tricks.html#id2 for additional detail. + logging.info('Retry transfer with alternative dicomscu configuration: %s' % self.extended_dicom_config_path) + + # Terminate transfer and notify user of failure + if self.dicomSend(file, config=os.path.join(RESOURCE_ROOT, self.extended_dicom_config_path)): + # success + return True + + stdout = self.process.readAllStandardOutput() + stderr = self.process.readAllStandardError() + logging.debug('DICOM send using extended configuration failed: process error code is %d' % self.process.error()) + logging.debug('DICOM send process standard out is: %s' % stdout) + logging.debug('DICOM send process standard error is: %s' % stderr) + + userMsg = f"Could not send {file} to {self.destinationUrl.host()}:{self.destinationUrl.port()}" + raise UserWarning(userMsg) class DICOMTestingQRServer: - """helper class to set up the DICOM servers - Code here depends only on python and DCMTK executables - TODO: it might make sense to refactor this as a generic tool - for interacting with DCMTK - """ - # TODO: make this use DICOMProcess superclass - - def __init__(self,exeDir=".",tmpDir="./DICOM"): - self.qrProcess = None - self.tmpDir = tmpDir - self.exeDir = exeDir - - def __del__(self): - self.stop() - - def qrRunning(self): - return self.qrProcess is not None - - def start(self,verbose=False,initialFiles=None): - if self.qrRunning(): - self.stop() - - self.dcmqrscpExecutable = self.exeDir+'/dcmqrdb/apps/dcmqrscp' - self.storeSCUExecutable = self.exeDir+'/dcmnet/apps/storescu' - - # make the config file - cfg = self.tmpDir+"/dcmqrscp.cfg" - self.makeConfigFile(cfg, storageDirectory=self.tmpDir) - - # start the server! - cmdLine = [self.dcmqrscpExecutable] - if verbose: - cmdLine.append('--verbose') - cmdLine.append('--config') - cmdLine.append(cfg) - self.qrProcess = subprocess.Popen(cmdLine) - # TODO: handle output - #stdin=subprocess.PIPE, - #stdout=subprocess.PIPE, - #stderr=subprocess.PIPE) - - # push the data to the server! - if initialFiles: - cmdLine = [self.storeSCUExecutable] - if verbose: - cmdLine.append('--verbose') - cmdLine.append('-aec') - cmdLine.append('CTK_AE') - cmdLine.append('-aet') - cmdLine.append('CTK_AE') - cmdLine.append('localhost') - cmdLine.append('11112') - cmdLine += initialFiles - p = subprocess.Popen(cmdLine) - p.wait() - - def stop(self): - self.qrProcess.kill() - self.qrProcess.communicate() - self.qrProcess.wait() - self.qrProcess = None - - def makeConfigFile(self,configFile,storageDirectory='.'): - """ make a config file for the local instance with just - the parts we need (comments and examples removed). - For examples and the full syntax - see dcmqrdb/etc/dcmqrscp.cfg and - dcmqrdb/docs/dcmqrcnf.txt in the dcmtk source - available from dcmtk.org or the ctk distribution + """helper class to set up the DICOM servers + Code here depends only on python and DCMTK executables + TODO: it might make sense to refactor this as a generic tool + for interacting with DCMTK """ - - template = """ + # TODO: make this use DICOMProcess superclass + + def __init__(self, exeDir=".", tmpDir="./DICOM"): + self.qrProcess = None + self.tmpDir = tmpDir + self.exeDir = exeDir + + def __del__(self): + self.stop() + + def qrRunning(self): + return self.qrProcess is not None + + def start(self, verbose=False, initialFiles=None): + if self.qrRunning(): + self.stop() + + self.dcmqrscpExecutable = self.exeDir + '/dcmqrdb/apps/dcmqrscp' + self.storeSCUExecutable = self.exeDir + '/dcmnet/apps/storescu' + + # make the config file + cfg = self.tmpDir + "/dcmqrscp.cfg" + self.makeConfigFile(cfg, storageDirectory=self.tmpDir) + + # start the server! + cmdLine = [self.dcmqrscpExecutable] + if verbose: + cmdLine.append('--verbose') + cmdLine.append('--config') + cmdLine.append(cfg) + self.qrProcess = subprocess.Popen(cmdLine) + # TODO: handle output + # stdin=subprocess.PIPE, + # stdout=subprocess.PIPE, + # stderr=subprocess.PIPE) + + # push the data to the server! + if initialFiles: + cmdLine = [self.storeSCUExecutable] + if verbose: + cmdLine.append('--verbose') + cmdLine.append('-aec') + cmdLine.append('CTK_AE') + cmdLine.append('-aet') + cmdLine.append('CTK_AE') + cmdLine.append('localhost') + cmdLine.append('11112') + cmdLine += initialFiles + p = subprocess.Popen(cmdLine) + p.wait() + + def stop(self): + self.qrProcess.kill() + self.qrProcess.communicate() + self.qrProcess.wait() + self.qrProcess = None + + def makeConfigFile(self, configFile, storageDirectory='.'): + """ make a config file for the local instance with just + the parts we need (comments and examples removed). + For examples and the full syntax + see dcmqrdb/etc/dcmqrscp.cfg and + dcmqrdb/docs/dcmqrcnf.txt in the dcmtk source + available from dcmtk.org or the ctk distribution + """ + + template = """ # Global Configuration Parameters NetworkType = "tcp" NetworkTCPPort = 11112 @@ -639,8 +639,8 @@ def makeConfigFile(self,configFile,storageDirectory='.'): CTK_AE %s RW (200, 1024mb) ANY AETable END """ - config = template % storageDirectory + config = template % storageDirectory - fp = open(configFile,'w') - fp.write(config) - fp.close() + fp = open(configFile, 'w') + fp.write(config) + fp.close() diff --git a/Modules/Scripted/DICOMLib/DICOMRecentActivityWidget.py b/Modules/Scripted/DICOMLib/DICOMRecentActivityWidget.py index 58121e8c1cd..b39ab7fd9be 100644 --- a/Modules/Scripted/DICOMLib/DICOMRecentActivityWidget.py +++ b/Modules/Scripted/DICOMLib/DICOMRecentActivityWidget.py @@ -7,130 +7,130 @@ class DICOMRecentActivityWidget(qt.QWidget): - """Display the recent activity of the slicer DICOM database - Example: - slicer.util.selectModule('DICOM') - import DICOMLib - w = DICOMLib.DICOMRecentActivityWidget(None, slicer.dicomDatabase, slicer.modules.DICOMInstance.browserWidget) - w.update() - w.show() - """ - - def __init__(self, parent, dicomDatabase=None, browserWidget=None): - """If browserWidget is specified (e.g., set to slicer.modules.DICOMInstance.browserWidget) - then clicking on an item selects the series in that browserWidget. + """Display the recent activity of the slicer DICOM database + Example: + slicer.util.selectModule('DICOM') + import DICOMLib + w = DICOMLib.DICOMRecentActivityWidget(None, slicer.dicomDatabase, slicer.modules.DICOMInstance.browserWidget) + w.update() + w.show() """ - super().__init__(parent) - if dicomDatabase: - self.dicomDatabase = dicomDatabase - else: - self.dicomDatabase = slicer.dicomDatabase - self.browserWidget = browserWidget - self.recentSeries = [] - self.name = 'recentActivityWidget' - self.setLayout(qt.QVBoxLayout()) - self.statusLabel = qt.QLabel() - self.layout().addWidget(self.statusLabel) - self.statusLabel.text = '' + def __init__(self, parent, dicomDatabase=None, browserWidget=None): + """If browserWidget is specified (e.g., set to slicer.modules.DICOMInstance.browserWidget) + then clicking on an item selects the series in that browserWidget. + """ + super().__init__(parent) + if dicomDatabase: + self.dicomDatabase = dicomDatabase + else: + self.dicomDatabase = slicer.dicomDatabase + self.browserWidget = browserWidget + self.recentSeries = [] + self.name = 'recentActivityWidget' + self.setLayout(qt.QVBoxLayout()) - self.scrollArea = qt.QScrollArea() - self.layout().addWidget(self.scrollArea) - self.listWidget = qt.QListWidget() - self.listWidget.name = 'recentActivityListWidget' - self.scrollArea.setWidget(self.listWidget) - self.scrollArea.setWidgetResizable(True) - self.listWidget.setProperty('SH_ItemView_ActivateItemOnSingleClick', 1) - self.listWidget.connect('activated(QModelIndex)', self.onActivated) + self.statusLabel = qt.QLabel() + self.layout().addWidget(self.statusLabel) + self.statusLabel.text = '' - self.refreshButton = qt.QPushButton() - self.layout().addWidget(self.refreshButton) - self.refreshButton.text = 'Refresh' - self.refreshButton.connect('clicked()', self.update) + self.scrollArea = qt.QScrollArea() + self.layout().addWidget(self.scrollArea) + self.listWidget = qt.QListWidget() + self.listWidget.name = 'recentActivityListWidget' + self.scrollArea.setWidget(self.listWidget) + self.scrollArea.setWidgetResizable(True) + self.listWidget.setProperty('SH_ItemView_ActivateItemOnSingleClick', 1) + self.listWidget.connect('activated(QModelIndex)', self.onActivated) - self.tags = {} - self.tags['seriesDescription'] = "0008,103e" - self.tags['patientName'] = "0010,0010" + self.refreshButton = qt.QPushButton() + self.layout().addWidget(self.refreshButton) + self.refreshButton.text = 'Refresh' + self.refreshButton.connect('clicked()', self.update) - class seriesWithTime: - """helper class to track series and time...""" + self.tags = {} + self.tags['seriesDescription'] = "0008,103e" + self.tags['patientName'] = "0010,0010" - def __init__(self, series, elapsedSinceInsert, insertDateTime, text): - self.series = series - self.elapsedSinceInsert = elapsedSinceInsert - self.insertDateTime = insertDateTime - self.text = text + class seriesWithTime: + """helper class to track series and time...""" - @staticmethod - def compareSeriesTimes(a, b): - if a.elapsedSinceInsert > b.elapsedSinceInsert: - return 1 - else: - return -1 + def __init__(self, series, elapsedSinceInsert, insertDateTime, text): + self.series = series + self.elapsedSinceInsert = elapsedSinceInsert + self.insertDateTime = insertDateTime + self.text = text - def recentSeriesList(self): - """Return a list of series sorted by insert time - (counting backwards from today) - Assume that first insert time of series is valid - for entire series (should be close enough for this purpose) - """ - recentSeries = [] - now = qt.QDateTime.currentDateTime() - for patient in self.dicomDatabase.patients(): - for study in self.dicomDatabase.studiesForPatient(patient): - for series in self.dicomDatabase.seriesForStudy(study): - files = self.dicomDatabase.filesForSeries(series, 1) - if len(files) > 0: - instance = self.dicomDatabase.instanceForFile(files[0]) - seriesTime = self.dicomDatabase.insertDateTimeForInstance(instance) - try: - patientName = self.dicomDatabase.instanceValue(instance, self.tags['patientName']) - except RuntimeError: - # this indicates that the particular instance is no longer - # accessible to the dicom database, so we should ignore it here - continue - seriesDescription = self.dicomDatabase.instanceValue(instance, self.tags['seriesDescription']) - elapsed = seriesTime.secsTo(now) - secondsPerHour = 60 * 60 - secondsPerDay = secondsPerHour * 24 - timeNote = None - if elapsed < secondsPerDay: - timeNote = 'Today' - elif elapsed < 7 * secondsPerDay: - timeNote = 'Past Week' - elif elapsed < 30 * 7 * secondsPerDay: - timeNote = 'Past Month' - if timeNote: - text = f"{timeNote}: {seriesDescription} for {patientName}" - recentSeries.append(self.seriesWithTime(series, elapsed, seriesTime, text)) - recentSeries.sort(key=cmp_to_key(self.compareSeriesTimes)) - return recentSeries + @staticmethod + def compareSeriesTimes(a, b): + if a.elapsedSinceInsert > b.elapsedSinceInsert: + return 1 + else: + return -1 - def update(self): - """Load the table widget with header values for the file - """ - self.listWidget.clear() - secondsPerHour = 60 * 60 - insertsPastHour = 0 - self.recentSeries = self.recentSeriesList() - for series in self.recentSeries: - self.listWidget.addItem(series.text) - if series.elapsedSinceInsert < secondsPerHour: - insertsPastHour += 1 - self.statusLabel.text = '%d series added to database in the past hour' % insertsPastHour - if len(self.recentSeries) > 0: - statusMessage = "Most recent DICOM Database addition: %s" % self.recentSeries[0].insertDateTime.toString() - slicer.util.showStatusMessage(statusMessage, 10000) + def recentSeriesList(self): + """Return a list of series sorted by insert time + (counting backwards from today) + Assume that first insert time of series is valid + for entire series (should be close enough for this purpose) + """ + recentSeries = [] + now = qt.QDateTime.currentDateTime() + for patient in self.dicomDatabase.patients(): + for study in self.dicomDatabase.studiesForPatient(patient): + for series in self.dicomDatabase.seriesForStudy(study): + files = self.dicomDatabase.filesForSeries(series, 1) + if len(files) > 0: + instance = self.dicomDatabase.instanceForFile(files[0]) + seriesTime = self.dicomDatabase.insertDateTimeForInstance(instance) + try: + patientName = self.dicomDatabase.instanceValue(instance, self.tags['patientName']) + except RuntimeError: + # this indicates that the particular instance is no longer + # accessible to the dicom database, so we should ignore it here + continue + seriesDescription = self.dicomDatabase.instanceValue(instance, self.tags['seriesDescription']) + elapsed = seriesTime.secsTo(now) + secondsPerHour = 60 * 60 + secondsPerDay = secondsPerHour * 24 + timeNote = None + if elapsed < secondsPerDay: + timeNote = 'Today' + elif elapsed < 7 * secondsPerDay: + timeNote = 'Past Week' + elif elapsed < 30 * 7 * secondsPerDay: + timeNote = 'Past Month' + if timeNote: + text = f"{timeNote}: {seriesDescription} for {patientName}" + recentSeries.append(self.seriesWithTime(series, elapsed, seriesTime, text)) + recentSeries.sort(key=cmp_to_key(self.compareSeriesTimes)) + return recentSeries + + def update(self): + """Load the table widget with header values for the file + """ + self.listWidget.clear() + secondsPerHour = 60 * 60 + insertsPastHour = 0 + self.recentSeries = self.recentSeriesList() + for series in self.recentSeries: + self.listWidget.addItem(series.text) + if series.elapsedSinceInsert < secondsPerHour: + insertsPastHour += 1 + self.statusLabel.text = '%d series added to database in the past hour' % insertsPastHour + if len(self.recentSeries) > 0: + statusMessage = "Most recent DICOM Database addition: %s" % self.recentSeries[0].insertDateTime.toString() + slicer.util.showStatusMessage(statusMessage, 10000) - def onActivated(self, modelIndex): - logging.debug('Recent activity widget selected row: %d (%s)' % (modelIndex.row(), self.recentSeries[modelIndex.row()].text)) - if not self.browserWidget: - return - # Select series in the series table - series = self.recentSeries[modelIndex.row()] - seriesUID = series.series - seriesTableView = self.browserWidget.dicomBrowser.dicomTableManager().seriesTable().tableView() - foundModelIndex = seriesTableView.model().match(seriesTableView.model().index(0,0), qt.Qt.ItemDataRole(), seriesUID, 1) - if foundModelIndex: - row = foundModelIndex[0].row() - seriesTableView.selectRow(row) + def onActivated(self, modelIndex): + logging.debug('Recent activity widget selected row: %d (%s)' % (modelIndex.row(), self.recentSeries[modelIndex.row()].text)) + if not self.browserWidget: + return + # Select series in the series table + series = self.recentSeries[modelIndex.row()] + seriesUID = series.series + seriesTableView = self.browserWidget.dicomBrowser.dicomTableManager().seriesTable().tableView() + foundModelIndex = seriesTableView.model().match(seriesTableView.model().index(0, 0), qt.Qt.ItemDataRole(), seriesUID, 1) + if foundModelIndex: + row = foundModelIndex[0].row() + seriesTableView.selectRow(row) diff --git a/Modules/Scripted/DICOMLib/DICOMSendDialog.py b/Modules/Scripted/DICOMLib/DICOMSendDialog.py index 703e16212f9..564e4dd454e 100644 --- a/Modules/Scripted/DICOMLib/DICOMSendDialog.py +++ b/Modules/Scripted/DICOMLib/DICOMSendDialog.py @@ -7,101 +7,101 @@ class DICOMSendDialog(qt.QDialog): - """Implement the Qt dialog for doing a DICOM Send (storage SCU) - """ - - def __init__(self, files, parent="mainWindow"): - super().__init__(slicer.util.mainWindow() if parent == "mainWindow" else parent) - self.setWindowTitle('Send DICOM Study') - self.setWindowModality(1) - self.setLayout(qt.QVBoxLayout()) - self.files = files - self.cancelRequested = False - self.sendingIsInProgress = False - self.setMinimumWidth(200) - self.open() - - def open(self): - self.studyLabel = qt.QLabel('Send %d items to destination' % len(self.files)) - self.layout().addWidget(self.studyLabel) - - # Send Parameters - self.dicomFrame = qt.QFrame(self) - self.dicomFormLayout = qt.QFormLayout() - self.dicomFrame.setLayout(self.dicomFormLayout) - - self.settings = qt.QSettings() - - self.protocolSelectorCombobox = qt.QComboBox() - self.protocolSelectorCombobox.addItems(["DIMSE","DICOMweb"]) - self.protocolSelectorCombobox.setCurrentText(self.settings.value('DICOM/Send/Protocol', 'DIMSE')) - self.protocolSelectorCombobox.currentIndexChanged.connect(self.onProtocolSelectorChange) - self.dicomFormLayout.addRow("Protocol: ", self.protocolSelectorCombobox) - - self.serverAETitleEdit = qt.QLineEdit() - self.serverAETitleEdit.setToolTip("AE Title") - self.serverAETitleEdit.text = self.settings.value('DICOM/Send/AETitle', 'CTK') - self.dicomFormLayout.addRow("AE Title: ", self.serverAETitleEdit) - # Enable AET only for DIMSE - self.serverAETitleEdit.enabled = self.protocolSelectorCombobox.currentText == 'DIMSE' - - self.serverAddressLineEdit = qt.QLineEdit() - self.serverAddressLineEdit.setToolTip("Address includes hostname and port number in standard URL format (hostname:port).") - self.serverAddressLineEdit.text = self.settings.value('DICOM/Send/URL', '') - self.dicomFormLayout.addRow("Destination Address: ", self.serverAddressLineEdit) - - self.layout().addWidget(self.dicomFrame) - - # button box - self.bbox = qt.QDialogButtonBox(self) - self.bbox.addButton(self.bbox.Ok) - self.bbox.addButton(self.bbox.Cancel) - self.bbox.accepted.connect(self.onOk) - self.bbox.rejected.connect(self.onCancel) - self.layout().addWidget(self.bbox) - - self.progressBar = qt.QProgressBar(self.parent().window()) - self.progressBar.hide() - self.dicomFormLayout.addRow(self.progressBar) - - qt.QDialog.open(self) - - def onProtocolSelectorChange(self): - # Enable AET only for DIMSE - self.serverAETitleEdit.enabled = self.protocolSelectorCombobox.currentText == 'DIMSE' - - def onOk(self): - self.sendingIsInProgress = True - address = self.serverAddressLineEdit.text - aeTitle = self.serverAETitleEdit.text - protocol = self.protocolSelectorCombobox.currentText - self.settings.setValue('DICOM/Send/URL', address) - self.settings.setValue('DICOM/Send/AETitle', aeTitle) - self.settings.setValue('DICOM/Send/Protocol', protocol) - self.progressBar.value = 0 - self.progressBar.maximum = len(self.files)+1 - self.progressBar.show() - self.cancelRequested = False - okButton = self.bbox.button(self.bbox.Ok) - - with slicer.util.tryWithErrorDisplay("DICOM sending failed."): - okButton.enabled = False - DICOMLib.DICOMSender(self.files, address, protocol, aeTitle=aeTitle, progressCallback=self.onProgress) - logging.debug("DICOM sending of %s files succeeded" % len(self.files)) - self.close() - - okButton.enabled = True - self.sendingIsInProgress = False - - def onCancel(self): - if self.sendingIsInProgress: - self.cancelRequested = True - else: - self.close() - - def onProgress(self, message): - self.progressBar.value += 1 - # message can be long, do not display it, but still log it (might be useful for troubleshooting) - logging.debug("DICOM send: " + message) - slicer.app.processEvents() - return not self.cancelRequested + """Implement the Qt dialog for doing a DICOM Send (storage SCU) + """ + + def __init__(self, files, parent="mainWindow"): + super().__init__(slicer.util.mainWindow() if parent == "mainWindow" else parent) + self.setWindowTitle('Send DICOM Study') + self.setWindowModality(1) + self.setLayout(qt.QVBoxLayout()) + self.files = files + self.cancelRequested = False + self.sendingIsInProgress = False + self.setMinimumWidth(200) + self.open() + + def open(self): + self.studyLabel = qt.QLabel('Send %d items to destination' % len(self.files)) + self.layout().addWidget(self.studyLabel) + + # Send Parameters + self.dicomFrame = qt.QFrame(self) + self.dicomFormLayout = qt.QFormLayout() + self.dicomFrame.setLayout(self.dicomFormLayout) + + self.settings = qt.QSettings() + + self.protocolSelectorCombobox = qt.QComboBox() + self.protocolSelectorCombobox.addItems(["DIMSE", "DICOMweb"]) + self.protocolSelectorCombobox.setCurrentText(self.settings.value('DICOM/Send/Protocol', 'DIMSE')) + self.protocolSelectorCombobox.currentIndexChanged.connect(self.onProtocolSelectorChange) + self.dicomFormLayout.addRow("Protocol: ", self.protocolSelectorCombobox) + + self.serverAETitleEdit = qt.QLineEdit() + self.serverAETitleEdit.setToolTip("AE Title") + self.serverAETitleEdit.text = self.settings.value('DICOM/Send/AETitle', 'CTK') + self.dicomFormLayout.addRow("AE Title: ", self.serverAETitleEdit) + # Enable AET only for DIMSE + self.serverAETitleEdit.enabled = self.protocolSelectorCombobox.currentText == 'DIMSE' + + self.serverAddressLineEdit = qt.QLineEdit() + self.serverAddressLineEdit.setToolTip("Address includes hostname and port number in standard URL format (hostname:port).") + self.serverAddressLineEdit.text = self.settings.value('DICOM/Send/URL', '') + self.dicomFormLayout.addRow("Destination Address: ", self.serverAddressLineEdit) + + self.layout().addWidget(self.dicomFrame) + + # button box + self.bbox = qt.QDialogButtonBox(self) + self.bbox.addButton(self.bbox.Ok) + self.bbox.addButton(self.bbox.Cancel) + self.bbox.accepted.connect(self.onOk) + self.bbox.rejected.connect(self.onCancel) + self.layout().addWidget(self.bbox) + + self.progressBar = qt.QProgressBar(self.parent().window()) + self.progressBar.hide() + self.dicomFormLayout.addRow(self.progressBar) + + qt.QDialog.open(self) + + def onProtocolSelectorChange(self): + # Enable AET only for DIMSE + self.serverAETitleEdit.enabled = self.protocolSelectorCombobox.currentText == 'DIMSE' + + def onOk(self): + self.sendingIsInProgress = True + address = self.serverAddressLineEdit.text + aeTitle = self.serverAETitleEdit.text + protocol = self.protocolSelectorCombobox.currentText + self.settings.setValue('DICOM/Send/URL', address) + self.settings.setValue('DICOM/Send/AETitle', aeTitle) + self.settings.setValue('DICOM/Send/Protocol', protocol) + self.progressBar.value = 0 + self.progressBar.maximum = len(self.files) + 1 + self.progressBar.show() + self.cancelRequested = False + okButton = self.bbox.button(self.bbox.Ok) + + with slicer.util.tryWithErrorDisplay("DICOM sending failed."): + okButton.enabled = False + DICOMLib.DICOMSender(self.files, address, protocol, aeTitle=aeTitle, progressCallback=self.onProgress) + logging.debug("DICOM sending of %s files succeeded" % len(self.files)) + self.close() + + okButton.enabled = True + self.sendingIsInProgress = False + + def onCancel(self): + if self.sendingIsInProgress: + self.cancelRequested = True + else: + self.close() + + def onProgress(self, message): + self.progressBar.value += 1 + # message can be long, do not display it, but still log it (might be useful for troubleshooting) + logging.debug("DICOM send: " + message) + slicer.app.processEvents() + return not self.cancelRequested diff --git a/Modules/Scripted/DICOMLib/DICOMUtils.py b/Modules/Scripted/DICOMLib/DICOMUtils.py index 05826cd5821..75a0c012d0d 100644 --- a/Modules/Scripted/DICOMLib/DICOMUtils.py +++ b/Modules/Scripted/DICOMLib/DICOMUtils.py @@ -21,954 +21,954 @@ ######################################################### -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def loadPatientByUID(patientUID): - """ Load patient by patient UID from DICOM database. - Returns list of loaded node ids. - - Example: load all data from a DICOM folder (using a temporary DICOM database) - - dicomDataDir = "c:/my/folder/with/dicom-files" # input folder with DICOM files - loadedNodeIDs = [] # this list will contain the list of all loaded node IDs - - from DICOMLib import DICOMUtils - with DICOMUtils.TemporaryDICOMDatabase() as db: - DICOMUtils.importDicom(dicomDataDir, db) - patientUIDs = db.patients() - for patientUID in patientUIDs: - loadedNodeIDs.extend(DICOMUtils.loadPatientByUID(patientUID)) - - This method expecs a patientUID in the form returned by - db.patients(), which are (integer) strings unique for the current database. - The actual contents of these strings are implementation specific - and should not be relied on (may be changed). - See ctkDICOMDatabasePrivate::insertPatient for more details. - - See loadPatientByPatientID to use the PatientID field - of the dicom header. - - Note that we have these methods because - there is no way to have a globally unique patient ID. - They are issued by different institutions so there may be - name clashes. This is is in contrast to other places where UID is - used in dicom and in this code (studyUID, seriesUID, instanceUID), - where it is common to assume that - the IDs are unique because the dicom standard provides - mechanisms to support generating unique values - (although there is no way to know for sure that - the creator of an instance actually created a unique value - rather than just copying an existing one). - """ - if not slicer.dicomDatabase.isOpen: - raise OSError('DICOM module or database cannot be accessed') - - patientUIDstr = str(patientUID) - if not patientUIDstr in slicer.dicomDatabase.patients(): - raise OSError('No patient found with DICOM database UID %s' % patientUIDstr) - - # Select all series in selected patient - studies = slicer.dicomDatabase.studiesForPatient(patientUIDstr) - if len(studies) == 0: - raise OSError('No studies found in patient with DICOM database UID ' + patientUIDstr) - - series = [slicer.dicomDatabase.seriesForStudy(study) for study in studies] - seriesUIDs = [uid for uidList in series for uid in uidList] - if len(seriesUIDs) == 0: - raise OSError('No series found in patient with DICOM database UID ' + patientUIDstr) - - return loadSeriesByUID(seriesUIDs) - - -#------------------------------------------------------------------------------ -def getDatabasePatientUIDByPatientName(name): - """ Get patient UID by patient name for easy loading of a patient - """ - if not slicer.dicomDatabase.isOpen: - raise OSError('DICOM module or database cannot be accessed') + """ Load patient by patient UID from DICOM database. + Returns list of loaded node ids. + + Example: load all data from a DICOM folder (using a temporary DICOM database) + + dicomDataDir = "c:/my/folder/with/dicom-files" # input folder with DICOM files + loadedNodeIDs = [] # this list will contain the list of all loaded node IDs + + from DICOMLib import DICOMUtils + with DICOMUtils.TemporaryDICOMDatabase() as db: + DICOMUtils.importDicom(dicomDataDir, db) + patientUIDs = db.patients() + for patientUID in patientUIDs: + loadedNodeIDs.extend(DICOMUtils.loadPatientByUID(patientUID)) + + This method expecs a patientUID in the form returned by + db.patients(), which are (integer) strings unique for the current database. + The actual contents of these strings are implementation specific + and should not be relied on (may be changed). + See ctkDICOMDatabasePrivate::insertPatient for more details. + + See loadPatientByPatientID to use the PatientID field + of the dicom header. + + Note that we have these methods because + there is no way to have a globally unique patient ID. + They are issued by different institutions so there may be + name clashes. This is is in contrast to other places where UID is + used in dicom and in this code (studyUID, seriesUID, instanceUID), + where it is common to assume that + the IDs are unique because the dicom standard provides + mechanisms to support generating unique values + (although there is no way to know for sure that + the creator of an instance actually created a unique value + rather than just copying an existing one). + """ + if not slicer.dicomDatabase.isOpen: + raise OSError('DICOM module or database cannot be accessed') + + patientUIDstr = str(patientUID) + if patientUIDstr not in slicer.dicomDatabase.patients(): + raise OSError('No patient found with DICOM database UID %s' % patientUIDstr) + + # Select all series in selected patient + studies = slicer.dicomDatabase.studiesForPatient(patientUIDstr) + if len(studies) == 0: + raise OSError('No studies found in patient with DICOM database UID ' + patientUIDstr) + + series = [slicer.dicomDatabase.seriesForStudy(study) for study in studies] + seriesUIDs = [uid for uidList in series for uid in uidList] + if len(seriesUIDs) == 0: + raise OSError('No series found in patient with DICOM database UID ' + patientUIDstr) - patients = slicer.dicomDatabase.patients() - for patientUID in patients: - currentName = slicer.dicomDatabase.nameForPatient(patientUID) - if currentName == name: - return patientUID - return None + return loadSeriesByUID(seriesUIDs) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +def getDatabasePatientUIDByPatientName(name): + """ Get patient UID by patient name for easy loading of a patient + """ + if not slicer.dicomDatabase.isOpen: + raise OSError('DICOM module or database cannot be accessed') + + patients = slicer.dicomDatabase.patients() + for patientUID in patients: + currentName = slicer.dicomDatabase.nameForPatient(patientUID) + if currentName == name: + return patientUID + return None + + +# ------------------------------------------------------------------------------ def loadPatientByName(patientName): - """ Load patient by patient name from DICOM database. - Returns list of loaded node ids. - """ - patientUID = getDatabasePatientUIDByPatientName(patientName) - if patientUID is None: - raise OSError('Patient not found by name %s' % patientName) - return loadPatientByUID(patientUID) + """ Load patient by patient name from DICOM database. + Returns list of loaded node ids. + """ + patientUID = getDatabasePatientUIDByPatientName(patientName) + if patientUID is None: + raise OSError('Patient not found by name %s' % patientName) + return loadPatientByUID(patientUID) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def getDatabasePatientUIDByPatientID(patientID): - """ Get database patient UID by DICOM patient ID for easy loading of a patient - """ - if not slicer.dicomDatabase.isOpen: - raise OSError('DICOM module or database cannot be accessed') - - patients = slicer.dicomDatabase.patients() - for patientUID in patients: - # Get first file of first series - studies = slicer.dicomDatabase.studiesForPatient(patientUID) - series = [slicer.dicomDatabase.seriesForStudy(study) for study in studies] - seriesUIDs = [uid for uidList in series for uid in uidList] - if len(seriesUIDs) == 0: - continue - filePaths = slicer.dicomDatabase.filesForSeries(seriesUIDs[0], 1) - if len(filePaths) == 0: - continue - firstFile = filePaths[0] - # Get PatientID from first file - currentPatientID = slicer.dicomDatabase.fileValue(slicer.util.longPath(firstFile), "0010,0020") - if currentPatientID == patientID: - return patientUID - return None + """ Get database patient UID by DICOM patient ID for easy loading of a patient + """ + if not slicer.dicomDatabase.isOpen: + raise OSError('DICOM module or database cannot be accessed') + + patients = slicer.dicomDatabase.patients() + for patientUID in patients: + # Get first file of first series + studies = slicer.dicomDatabase.studiesForPatient(patientUID) + series = [slicer.dicomDatabase.seriesForStudy(study) for study in studies] + seriesUIDs = [uid for uidList in series for uid in uidList] + if len(seriesUIDs) == 0: + continue + filePaths = slicer.dicomDatabase.filesForSeries(seriesUIDs[0], 1) + if len(filePaths) == 0: + continue + firstFile = filePaths[0] + # Get PatientID from first file + currentPatientID = slicer.dicomDatabase.fileValue(slicer.util.longPath(firstFile), "0010,0020") + if currentPatientID == patientID: + return patientUID + return None -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def loadPatientByPatientID(patientID): - """ Load patient from DICOM database by DICOM PatientID. - Returns list of loaded node ids. - """ - patientUID = getDatabasePatientUIDByPatientID(patientID) - if patientUID is None: - raise OSError('Patient not found by PatientID %s' % patientID) - return loadPatientByUID(patientUID) + """ Load patient from DICOM database by DICOM PatientID. + Returns list of loaded node ids. + """ + patientUID = getDatabasePatientUIDByPatientID(patientID) + if patientUID is None: + raise OSError('Patient not found by PatientID %s' % patientID) + return loadPatientByUID(patientUID) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def loadPatient(uid=None, name=None, patientID=None): - """ Load patient from DICOM database fr uid, name, or patient ID. - Returns list of loaded node ids. - """ - if uid is not None: - return loadPatientByUID(uid) - elif name is not None: - return loadPatientByName(name) - elif patientID is not None: - return loadPatientByPatientID(patientID) + """ Load patient from DICOM database fr uid, name, or patient ID. + Returns list of loaded node ids. + """ + if uid is not None: + return loadPatientByUID(uid) + elif name is not None: + return loadPatientByName(name) + elif patientID is not None: + return loadPatientByPatientID(patientID) - raise ValueError('One of the following arguments needs to be specified: uid, name, patientID') + raise ValueError('One of the following arguments needs to be specified: uid, name, patientID') -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def loadSeriesByUID(seriesUIDs): - """ Load multiple series by UID from DICOM database. - Returns list of loaded node ids. - """ - if not isinstance(seriesUIDs, list): - raise ValueError('SeriesUIDs must contain a list') - if seriesUIDs is None or len(seriesUIDs) == 0: - raise ValueError('No series UIDs given') + """ Load multiple series by UID from DICOM database. + Returns list of loaded node ids. + """ + if not isinstance(seriesUIDs, list): + raise ValueError('SeriesUIDs must contain a list') + if seriesUIDs is None or len(seriesUIDs) == 0: + raise ValueError('No series UIDs given') - if not slicer.dicomDatabase.isOpen: - raise OSError('DICOM module or database cannot be accessed') + if not slicer.dicomDatabase.isOpen: + raise OSError('DICOM module or database cannot be accessed') - fileLists = [] - for seriesUID in seriesUIDs: - fileLists.append(slicer.dicomDatabase.filesForSeries(seriesUID)) - if len(fileLists) == 0: - # No files found for DICOM series list - return [] + fileLists = [] + for seriesUID in seriesUIDs: + fileLists.append(slicer.dicomDatabase.filesForSeries(seriesUID)) + if len(fileLists) == 0: + # No files found for DICOM series list + return [] - loadablesByPlugin, loadEnabled = getLoadablesFromFileLists(fileLists) - selectHighestConfidenceLoadables(loadablesByPlugin) - return loadLoadables(loadablesByPlugin) + loadablesByPlugin, loadEnabled = getLoadablesFromFileLists(fileLists) + selectHighestConfidenceLoadables(loadablesByPlugin) + return loadLoadables(loadablesByPlugin) def selectHighestConfidenceLoadables(loadablesByPlugin): - """Review the selected state and confidence of the loadables - across plugins so that the options the user is most likely - to want are listed at the top of the table and are selected - by default. Only offer one pre-selected loadable per series - unless both plugins mark it as selected and they have equal - confidence.""" - - # first, get all loadables corresponding to a series - seriesUIDTag = "0020,000E" - loadablesBySeries = {} - for plugin in loadablesByPlugin: - for loadable in loadablesByPlugin[plugin]: - seriesUID = slicer.dicomDatabase.fileValue(loadable.files[0], seriesUIDTag) - if seriesUID not in loadablesBySeries: - loadablesBySeries[seriesUID] = [loadable] - else: - loadablesBySeries[seriesUID].append(loadable) - - # now for each series, find the highest confidence selected loadables - # and set all others to be unselected. - # If there are several loadables that tie for the - # highest confidence value, select them all - # on the assumption that they represent alternate interpretations - # of the data or subparts of it. The user can either use - # advanced mode to deselect, or simply delete the - # unwanted interpretations. - for series in loadablesBySeries: - highestConfidenceValue = -1 - for loadable in loadablesBySeries[series]: - if loadable.confidence > highestConfidenceValue: - highestConfidenceValue = loadable.confidence - for loadable in loadablesBySeries[series]: - loadable.selected = loadable.confidence == highestConfidenceValue - - -#------------------------------------------------------------------------------ + """Review the selected state and confidence of the loadables + across plugins so that the options the user is most likely + to want are listed at the top of the table and are selected + by default. Only offer one pre-selected loadable per series + unless both plugins mark it as selected and they have equal + confidence.""" + + # first, get all loadables corresponding to a series + seriesUIDTag = "0020,000E" + loadablesBySeries = {} + for plugin in loadablesByPlugin: + for loadable in loadablesByPlugin[plugin]: + seriesUID = slicer.dicomDatabase.fileValue(loadable.files[0], seriesUIDTag) + if seriesUID not in loadablesBySeries: + loadablesBySeries[seriesUID] = [loadable] + else: + loadablesBySeries[seriesUID].append(loadable) + + # now for each series, find the highest confidence selected loadables + # and set all others to be unselected. + # If there are several loadables that tie for the + # highest confidence value, select them all + # on the assumption that they represent alternate interpretations + # of the data or subparts of it. The user can either use + # advanced mode to deselect, or simply delete the + # unwanted interpretations. + for series in loadablesBySeries: + highestConfidenceValue = -1 + for loadable in loadablesBySeries[series]: + if loadable.confidence > highestConfidenceValue: + highestConfidenceValue = loadable.confidence + for loadable in loadablesBySeries[series]: + loadable.selected = loadable.confidence == highestConfidenceValue + + +# ------------------------------------------------------------------------------ def loadByInstanceUID(instanceUID): - """ Load with the most confident loadable that contains the instanceUID from DICOM database. - This helps in the case where an instance is part of a series which may offer multiple - loadables, such as when a series has multiple time points where - each corresponds to a scalar volume and you only want to load the correct one. - Returns list of loaded node ids (typically one node). - - For example: - >>> uid = '1.3.6.1.4.1.14519.5.2.1.3098.5025.172915611048593327557054469973' - >>> import DICOMLib - >>> nodeIDs = DICOMLib.DICOMUtils.loadByInstanceUID(uid) - """ - - if not slicer.dicomDatabase.isOpen: - raise OSError('DICOM module or database cannot be accessed') - - # get the loadables corresponding to this instance's series - filePath = slicer.dicomDatabase.fileForInstance(instanceUID) - seriesUID = slicer.dicomDatabase.seriesForFile(filePath) - fileList = slicer.dicomDatabase.filesForSeries(seriesUID) - loadablesByPlugin, loadEnabled = getLoadablesFromFileLists([fileList]) - # keep only the loadables that include this instance's file and is highest confidence - highestConfidence = { - 'confidence': 0, - 'plugin': None, - 'loadable': None - } - for plugin in loadablesByPlugin.keys(): - loadablesWithInstance = [] - for loadable in loadablesByPlugin[plugin]: - if filePath in loadable.files: - if loadable.confidence > highestConfidence['confidence']: - loadable.selected = True - highestConfidence = { - 'confidence': loadable.confidence, - 'plugin': plugin, - 'loadable': loadable - } - filteredLoadablesByPlugin = {} - filteredLoadablesByPlugin[highestConfidence['plugin']] = [highestConfidence['loadable'],] - # load the results - return loadLoadables(filteredLoadablesByPlugin) - - -#------------------------------------------------------------------------------ + """ Load with the most confident loadable that contains the instanceUID from DICOM database. + This helps in the case where an instance is part of a series which may offer multiple + loadables, such as when a series has multiple time points where + each corresponds to a scalar volume and you only want to load the correct one. + Returns list of loaded node ids (typically one node). + + For example: + >>> uid = '1.3.6.1.4.1.14519.5.2.1.3098.5025.172915611048593327557054469973' + >>> import DICOMLib + >>> nodeIDs = DICOMLib.DICOMUtils.loadByInstanceUID(uid) + """ + + if not slicer.dicomDatabase.isOpen: + raise OSError('DICOM module or database cannot be accessed') + + # get the loadables corresponding to this instance's series + filePath = slicer.dicomDatabase.fileForInstance(instanceUID) + seriesUID = slicer.dicomDatabase.seriesForFile(filePath) + fileList = slicer.dicomDatabase.filesForSeries(seriesUID) + loadablesByPlugin, loadEnabled = getLoadablesFromFileLists([fileList]) + # keep only the loadables that include this instance's file and is highest confidence + highestConfidence = { + 'confidence': 0, + 'plugin': None, + 'loadable': None + } + for plugin in loadablesByPlugin.keys(): + loadablesWithInstance = [] + for loadable in loadablesByPlugin[plugin]: + if filePath in loadable.files: + if loadable.confidence > highestConfidence['confidence']: + loadable.selected = True + highestConfidence = { + 'confidence': loadable.confidence, + 'plugin': plugin, + 'loadable': loadable + } + filteredLoadablesByPlugin = {} + filteredLoadablesByPlugin[highestConfidence['plugin']] = [highestConfidence['loadable'], ] + # load the results + return loadLoadables(filteredLoadablesByPlugin) + + +# ------------------------------------------------------------------------------ def openDatabase(databaseDir): - """Open DICOM database in the specified folder""" - if not os.access(databaseDir, os.F_OK): - logging.error('Specified database directory ' + repr(databaseDir) + ' cannot be found') - return False - databaseFileName = databaseDir + "/ctkDICOM.sql" - slicer.dicomDatabase.openDatabase(databaseFileName) - if not slicer.dicomDatabase.isOpen: - logging.error('Unable to open DICOM database ' + databaseDir) - return False - return True - - -#------------------------------------------------------------------------------ + """Open DICOM database in the specified folder""" + if not os.access(databaseDir, os.F_OK): + logging.error('Specified database directory ' + repr(databaseDir) + ' cannot be found') + return False + databaseFileName = databaseDir + "/ctkDICOM.sql" + slicer.dicomDatabase.openDatabase(databaseFileName) + if not slicer.dicomDatabase.isOpen: + logging.error('Unable to open DICOM database ' + databaseDir) + return False + return True + + +# ------------------------------------------------------------------------------ def clearDatabase(dicomDatabase=None): - """Delete entire content (index and copied files) of the DICOM database""" - # Remove files from index and copied files from disk - if dicomDatabase is None: - dicomDatabase = slicer.dicomDatabase - patientIds = dicomDatabase.patients() - for patientId in patientIds: - dicomDatabase.removePatient(patientId) - # Delete empty folders remaining after removing copied files - removeEmptyDirs(dicomDatabase.databaseDirectory+'/dicom') - dicomDatabase.databaseChanged() + """Delete entire content (index and copied files) of the DICOM database""" + # Remove files from index and copied files from disk + if dicomDatabase is None: + dicomDatabase = slicer.dicomDatabase + patientIds = dicomDatabase.patients() + for patientId in patientIds: + dicomDatabase.removePatient(patientId) + # Delete empty folders remaining after removing copied files + removeEmptyDirs(dicomDatabase.databaseDirectory + '/dicom') + dicomDatabase.databaseChanged() def removeEmptyDirs(path): - for root, dirnames, filenames in os.walk(path, topdown=False): - for dirname in dirnames: - removeEmptyDirs(os.path.realpath(os.path.join(root, dirname))) - try: - os.rmdir(os.path.realpath(os.path.join(root, dirname))) - except OSError as e: - logging.error("Removing directory failed: " + str(e)) + for root, dirnames, filenames in os.walk(path, topdown=False): + for dirname in dirnames: + removeEmptyDirs(os.path.realpath(os.path.join(root, dirname))) + try: + os.rmdir(os.path.realpath(os.path.join(root, dirname))) + except OSError as e: + logging.error("Removing directory failed: " + str(e)) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def openTemporaryDatabase(directory=None): - """ Temporarily change the main DICOM database folder location, - return current database directory. Useful for tests and demos. - Call closeTemporaryDatabase to restore the original database folder. - """ - # Specify temporary directory - if not directory or directory == '': - from time import gmtime, strftime - directory = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'TempDICOMDatabase' - if os.path.isabs(directory): - tempDatabaseDir = directory - else: - tempDatabaseDir = slicer.app.temporaryPath + '/' + directory - logging.info('Switching to temporary DICOM database: ' + tempDatabaseDir) - if not os.access(tempDatabaseDir, os.F_OK): - qt.QDir().mkpath(tempDatabaseDir) - - # Get original database directory to be able to restore it later - settings = qt.QSettings() - originalDatabaseDir = settings.value(slicer.dicomDatabaseDirectorySettingsKey) - settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, tempDatabaseDir) - - openDatabase(tempDatabaseDir) - - # Clear the entire database - slicer.dicomDatabase.initializeDatabase() - - return originalDatabaseDir - - -#------------------------------------------------------------------------------ + """ Temporarily change the main DICOM database folder location, + return current database directory. Useful for tests and demos. + Call closeTemporaryDatabase to restore the original database folder. + """ + # Specify temporary directory + if not directory or directory == '': + from time import gmtime, strftime + directory = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'TempDICOMDatabase' + if os.path.isabs(directory): + tempDatabaseDir = directory + else: + tempDatabaseDir = slicer.app.temporaryPath + '/' + directory + logging.info('Switching to temporary DICOM database: ' + tempDatabaseDir) + if not os.access(tempDatabaseDir, os.F_OK): + qt.QDir().mkpath(tempDatabaseDir) + + # Get original database directory to be able to restore it later + settings = qt.QSettings() + originalDatabaseDir = settings.value(slicer.dicomDatabaseDirectorySettingsKey) + settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, tempDatabaseDir) + + openDatabase(tempDatabaseDir) + + # Clear the entire database + slicer.dicomDatabase.initializeDatabase() + + return originalDatabaseDir + + +# ------------------------------------------------------------------------------ def closeTemporaryDatabase(originalDatabaseDir, cleanup=True): - """ Close temporary DICOM database and remove its directory if requested - """ - if slicer.dicomDatabase.isOpen: - if cleanup: - slicer.dicomDatabase.initializeDatabase() - # TODO: The database files cannot be deleted even if the database is closed. - # Not critical, as it will be empty, so will not take measurable disk space. - # import shutil - # databaseDir = os.path.split(slicer.dicomDatabase.databaseFilename)[0] - # shutil.rmtree(databaseDir) - # if os.access(databaseDir, os.F_OK): - # logging.error('Failed to delete DICOM database ' + databaseDir) - slicer.dicomDatabase.closeDatabase() - else: - logging.error('Unable to close DICOM database ' + slicer.dicomDatabase.databaseFilename) - - if originalDatabaseDir is None: - # Only log debug if there was no original database, as it is a valid use case, - # see openTemporaryDatabase - logging.debug('No original database directory was specified') - return True + """ Close temporary DICOM database and remove its directory if requested + """ + if slicer.dicomDatabase.isOpen: + if cleanup: + slicer.dicomDatabase.initializeDatabase() + # TODO: The database files cannot be deleted even if the database is closed. + # Not critical, as it will be empty, so will not take measurable disk space. + # import shutil + # databaseDir = os.path.split(slicer.dicomDatabase.databaseFilename)[0] + # shutil.rmtree(databaseDir) + # if os.access(databaseDir, os.F_OK): + # logging.error('Failed to delete DICOM database ' + databaseDir) + slicer.dicomDatabase.closeDatabase() + else: + logging.error('Unable to close DICOM database ' + slicer.dicomDatabase.databaseFilename) - settings = qt.QSettings() - settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, originalDatabaseDir) + if originalDatabaseDir is None: + # Only log debug if there was no original database, as it is a valid use case, + # see openTemporaryDatabase + logging.debug('No original database directory was specified') + return True - # Attempt to re-open original database only if it exists - if os.access(originalDatabaseDir, os.F_OK): - success = openDatabase(originalDatabaseDir) - if not success: - logging.error('Unable to open DICOM database ' + originalDatabaseDir) - return False + settings = qt.QSettings() + settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, originalDatabaseDir) - return True + # Attempt to re-open original database only if it exists + if os.access(originalDatabaseDir, os.F_OK): + success = openDatabase(originalDatabaseDir) + if not success: + logging.error('Unable to open DICOM database ' + originalDatabaseDir) + return False + return True -#------------------------------------------------------------------------------ + +# ------------------------------------------------------------------------------ def createTemporaryDatabase(directory=None): - """ Open temporary DICOM database, return new database object - """ - # Specify temporary directory - if not directory or directory == '': - from time import gmtime, strftime - directory = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'TempDICOMDatabase' - if os.path.isabs(directory): - tempDatabaseDir = directory - else: - tempDatabaseDir = slicer.app.temporaryPath + '/' + directory - logging.info('Switching to temporary DICOM database: ' + tempDatabaseDir) - if not os.access(tempDatabaseDir, os.F_OK): - qt.QDir().mkpath(tempDatabaseDir) - - databaseFileName = tempDatabaseDir + "/ctkDICOM.sql" - dicomDatabase = ctk.ctkDICOMDatabase() - dicomDatabase.openDatabase(databaseFileName) - if dicomDatabase.isOpen: - if dicomDatabase.schemaVersionLoaded() != dicomDatabase.schemaVersion(): - dicomDatabase.closeDatabase() - - if dicomDatabase.isOpen: - return dicomDatabase - else: - return None + """ Open temporary DICOM database, return new database object + """ + # Specify temporary directory + if not directory or directory == '': + from time import gmtime, strftime + directory = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'TempDICOMDatabase' + if os.path.isabs(directory): + tempDatabaseDir = directory + else: + tempDatabaseDir = slicer.app.temporaryPath + '/' + directory + logging.info('Switching to temporary DICOM database: ' + tempDatabaseDir) + if not os.access(tempDatabaseDir, os.F_OK): + qt.QDir().mkpath(tempDatabaseDir) + + databaseFileName = tempDatabaseDir + "/ctkDICOM.sql" + dicomDatabase = ctk.ctkDICOMDatabase() + dicomDatabase.openDatabase(databaseFileName) + if dicomDatabase.isOpen: + if dicomDatabase.schemaVersionLoaded() != dicomDatabase.schemaVersion(): + dicomDatabase.closeDatabase() + + if dicomDatabase.isOpen: + return dicomDatabase + else: + return None -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def deleteTemporaryDatabase(dicomDatabase, cleanup=True): - """ Close temporary DICOM database and remove its directory if requested - """ - dicomDatabase.closeDatabase() + """ Close temporary DICOM database and remove its directory if requested + """ + dicomDatabase.closeDatabase() - if cleanup: - import shutil - databaseDir = os.path.split(dicomDatabase.databaseFilename)[0] - shutil.rmtree(databaseDir) - if os.access(databaseDir, os.F_OK): - logging.error('Failed to delete DICOM database ' + databaseDir) - # Database is still in use, at least clear its content - dicomDatabase.initializeDatabase() + if cleanup: + import shutil + databaseDir = os.path.split(dicomDatabase.databaseFilename)[0] + shutil.rmtree(databaseDir) + if os.access(databaseDir, os.F_OK): + logging.error('Failed to delete DICOM database ' + databaseDir) + # Database is still in use, at least clear its content + dicomDatabase.initializeDatabase() - return True + return True -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ class TemporaryDICOMDatabase: - """Context manager to conveniently use temporary DICOM database. - It creates a new DICOM database and temporarily sets it as the main - DICOM database in the application (slicer.dicomDatabase). - """ + """Context manager to conveniently use temporary DICOM database. + It creates a new DICOM database and temporarily sets it as the main + DICOM database in the application (slicer.dicomDatabase). + """ - def __init__(self, directory=None): - self.temporaryDatabaseDir = directory - self.originalDatabaseDir = None + def __init__(self, directory=None): + self.temporaryDatabaseDir = directory + self.originalDatabaseDir = None - def __enter__(self): - self.originalDatabaseDir = openTemporaryDatabase(self.temporaryDatabaseDir) - return slicer.dicomDatabase + def __enter__(self): + self.originalDatabaseDir = openTemporaryDatabase(self.temporaryDatabaseDir) + return slicer.dicomDatabase - def __exit__(self, type, value, traceback): - closeTemporaryDatabase(self.originalDatabaseDir) + def __exit__(self, type, value, traceback): + closeTemporaryDatabase(self.originalDatabaseDir) -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ def importDicom(dicomDataDir, dicomDatabase=None, copyFiles=False): - """ Import DICOM files from folder into Slicer database - """ - try: - indexer = ctk.ctkDICOMIndexer() - assert indexer is not None - if dicomDatabase is None: - dicomDatabase = slicer.dicomDatabase - indexer.addDirectory(dicomDatabase, dicomDataDir, copyFiles) - indexer.waitForImportFinished() - except Exception as e: - import traceback - traceback.print_exc() - logging.error('Failed to import DICOM folder ' + dicomDataDir) - return False - return True - - -#------------------------------------------------------------------------------ -def loadSeriesWithVerification(seriesUIDs, expectedSelectedPlugins=None, expectedLoadedNodes=None): - """ Load series by UID, and verify loadable selection and loaded nodes. - - ``selectedPlugins`` example: { 'Scalar Volume':1, 'RT':2 } - ``expectedLoadedNodes`` example: { 'vtkMRMLScalarVolumeNode':2, 'vtkMRMLSegmentationNode':1 } - """ - if not slicer.dicomDatabase.isOpen: - logging.error('DICOM module or database cannot be accessed') - return False - if seriesUIDs is None or len(seriesUIDs) == 0: - logging.error('No series UIDs given') - return False - - fileLists = [] - for seriesUID in seriesUIDs: - fileLists.append(slicer.dicomDatabase.filesForSeries(seriesUID)) - - if len(fileLists) == 0: - logging.error('No files found for DICOM series list') - return False - - loadablesByPlugin, loadEnabled = getLoadablesFromFileLists(fileLists) - success = True - - # Verify loadables if baseline is given - if expectedSelectedPlugins is not None and len(expectedSelectedPlugins.keys()) > 0: - actualSelectedPlugins = {} - for plugin in loadablesByPlugin: - for loadable in loadablesByPlugin[plugin]: - if loadable.selected: - if plugin.loadType in actualSelectedPlugins: - count = int(actualSelectedPlugins[plugin.loadType]) - actualSelectedPlugins[plugin.loadType] = count+1 - else: - actualSelectedPlugins[plugin.loadType] = 1 - for pluginName in expectedSelectedPlugins.keys(): - if pluginName not in actualSelectedPlugins: - logging.error("Expected DICOM plugin '%s' was not selected" % (pluginName)) - success = False - elif actualSelectedPlugins[pluginName] != expectedSelectedPlugins[pluginName]: - logging.error("DICOM plugin '%s' was expected to be selected in %d loadables, but was selected in %d" % \ - (pluginName, expectedSelectedPlugins[pluginName], actualSelectedPlugins[pluginName])) - success = False - - # Count relevant node types in scene - actualLoadedNodes = {} - if expectedLoadedNodes is not None: - for nodeType in expectedLoadedNodes.keys(): - nodeCollection = slicer.mrmlScene.GetNodesByClass(nodeType) - nodeCollection.UnRegister(None) - actualLoadedNodes[nodeType] = nodeCollection.GetNumberOfItems() - - # Load selected data - loadedNodeIDs = loadLoadables(loadablesByPlugin) - - if expectedLoadedNodes is not None: - for nodeType in expectedLoadedNodes.keys(): - nodeCollection = slicer.mrmlScene.GetNodesByClass(nodeType) - nodeCollection.UnRegister(None) - numOfLoadedNodes = nodeCollection.GetNumberOfItems()-actualLoadedNodes[nodeType] - if numOfLoadedNodes != expectedLoadedNodes[nodeType]: - logging.error("Number of loaded %s nodes was %d, but %d was expected" % \ - (nodeType, numOfLoadedNodes, expectedLoadedNodes[nodeType]) ) - success = False - - return success - - -#------------------------------------------------------------------------------ -def allSeriesUIDsInDatabase(database=None): - """ Collect all series instance UIDs in a DICOM database (the Slicer one by default) - - Useful to get list of just imported series UIDs, for example: - newSeriesUIDs = [x for x in seriesUIDsAfter if x not in seriesUIDsBefore] - """ - if database is None: - database = slicer.dicomDatabase - dicomWidget = slicer.modules.dicom.widgetRepresentation().self() - allSeriesUIDs = [] - for patient in database.patients(): - studies = database.studiesForPatient(patient) - series = [database.seriesForStudy(study) for study in studies] - seriesUIDs = [uid for uidList in series for uid in uidList] - allSeriesUIDs.extend(seriesUIDs) - return allSeriesUIDs + """ Import DICOM files from folder into Slicer database + """ + try: + indexer = ctk.ctkDICOMIndexer() + assert indexer is not None + if dicomDatabase is None: + dicomDatabase = slicer.dicomDatabase + indexer.addDirectory(dicomDatabase, dicomDataDir, copyFiles) + indexer.waitForImportFinished() + except Exception as e: + import traceback + traceback.print_exc() + logging.error('Failed to import DICOM folder ' + dicomDataDir) + return False + return True -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +def loadSeriesWithVerification(seriesUIDs, expectedSelectedPlugins=None, expectedLoadedNodes=None): + """ Load series by UID, and verify loadable selection and loaded nodes. + + ``selectedPlugins`` example: { 'Scalar Volume':1, 'RT':2 } + ``expectedLoadedNodes`` example: { 'vtkMRMLScalarVolumeNode':2, 'vtkMRMLSegmentationNode':1 } + """ + if not slicer.dicomDatabase.isOpen: + logging.error('DICOM module or database cannot be accessed') + return False + if seriesUIDs is None or len(seriesUIDs) == 0: + logging.error('No series UIDs given') + return False + + fileLists = [] + for seriesUID in seriesUIDs: + fileLists.append(slicer.dicomDatabase.filesForSeries(seriesUID)) + + if len(fileLists) == 0: + logging.error('No files found for DICOM series list') + return False + + loadablesByPlugin, loadEnabled = getLoadablesFromFileLists(fileLists) + success = True + + # Verify loadables if baseline is given + if expectedSelectedPlugins is not None and len(expectedSelectedPlugins.keys()) > 0: + actualSelectedPlugins = {} + for plugin in loadablesByPlugin: + for loadable in loadablesByPlugin[plugin]: + if loadable.selected: + if plugin.loadType in actualSelectedPlugins: + count = int(actualSelectedPlugins[plugin.loadType]) + actualSelectedPlugins[plugin.loadType] = count + 1 + else: + actualSelectedPlugins[plugin.loadType] = 1 + for pluginName in expectedSelectedPlugins.keys(): + if pluginName not in actualSelectedPlugins: + logging.error("Expected DICOM plugin '%s' was not selected" % (pluginName)) + success = False + elif actualSelectedPlugins[pluginName] != expectedSelectedPlugins[pluginName]: + logging.error("DICOM plugin '%s' was expected to be selected in %d loadables, but was selected in %d" % \ + (pluginName, expectedSelectedPlugins[pluginName], actualSelectedPlugins[pluginName])) + success = False + + # Count relevant node types in scene + actualLoadedNodes = {} + if expectedLoadedNodes is not None: + for nodeType in expectedLoadedNodes.keys(): + nodeCollection = slicer.mrmlScene.GetNodesByClass(nodeType) + nodeCollection.UnRegister(None) + actualLoadedNodes[nodeType] = nodeCollection.GetNumberOfItems() + + # Load selected data + loadedNodeIDs = loadLoadables(loadablesByPlugin) + + if expectedLoadedNodes is not None: + for nodeType in expectedLoadedNodes.keys(): + nodeCollection = slicer.mrmlScene.GetNodesByClass(nodeType) + nodeCollection.UnRegister(None) + numOfLoadedNodes = nodeCollection.GetNumberOfItems() - actualLoadedNodes[nodeType] + if numOfLoadedNodes != expectedLoadedNodes[nodeType]: + logging.error("Number of loaded %s nodes was %d, but %d was expected" % \ + (nodeType, numOfLoadedNodes, expectedLoadedNodes[nodeType])) + success = False + + return success + + +# ------------------------------------------------------------------------------ +def allSeriesUIDsInDatabase(database=None): + """ Collect all series instance UIDs in a DICOM database (the Slicer one by default) + + Useful to get list of just imported series UIDs, for example: + newSeriesUIDs = [x for x in seriesUIDsAfter if x not in seriesUIDsBefore] + """ + if database is None: + database = slicer.dicomDatabase + dicomWidget = slicer.modules.dicom.widgetRepresentation().self() + allSeriesUIDs = [] + for patient in database.patients(): + studies = database.studiesForPatient(patient) + series = [database.seriesForStudy(study) for study in studies] + seriesUIDs = [uid for uidList in series for uid in uidList] + allSeriesUIDs.extend(seriesUIDs) + return allSeriesUIDs + + +# ------------------------------------------------------------------------------ def seriesUIDsForFiles(files): - """ Collect series instance UIDs belonging to a list of files - """ - seriesUIDs = set() - for file in files: - seriesUID = slicer.dicomDatabase.seriesForFile(file) - if seriesUID != '': - seriesUIDs.add(seriesUID) - return seriesUIDs + """ Collect series instance UIDs belonging to a list of files + """ + seriesUIDs = set() + for file in files: + seriesUID = slicer.dicomDatabase.seriesForFile(file) + if seriesUID != '': + seriesUIDs.add(seriesUID) + return seriesUIDs -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ class LoadDICOMFilesToDatabase: - """Context manager to conveniently load DICOM files downloaded zipped from the internet - """ - def __init__( self, url, archiveFilePath=None, dicomDataDir=None, \ - expectedNumberOfFiles=None, selectedPlugins=None, loadedNodes=None, checksum=None): - from time import gmtime, strftime - if archiveFilePath is None: - fileName = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'LoadDICOMFilesToDatabase.zip' - archiveFilePath = slicer.app.temporaryPath + '/' + fileName - if dicomDataDir is None: - directoryName = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'LoadDICOMFilesToDatabase' - dicomDataDir = slicer.app.temporaryPath + '/' + directoryName - - self.url = url - self.checksum = checksum - self.archiveFilePath = archiveFilePath - self.dicomDataDir = dicomDataDir - self.expectedNumberOfExtractedFiles = expectedNumberOfFiles - self.selectedPlugins = selectedPlugins - self.loadedNodes = loadedNodes - - def __enter__(self): - if slicer.util.downloadAndExtractArchive( self.url, self.archiveFilePath, \ - self.dicomDataDir, self.expectedNumberOfExtractedFiles, - checksum=self.checksum): - dicomFiles = slicer.util.getFilesInDirectory(self.dicomDataDir) - if importDicom(self.dicomDataDir): - seriesUIDs = seriesUIDsForFiles(dicomFiles) - return loadSeriesWithVerification(seriesUIDs, self.selectedPlugins, self.loadedNodes) - return False - - def __exit__(self, type, value, traceback): - pass - - -#------------------------------------------------------------------------------ + """Context manager to conveniently load DICOM files downloaded zipped from the internet + """ + def __init__(self, url, archiveFilePath=None, dicomDataDir=None, \ + expectedNumberOfFiles=None, selectedPlugins=None, loadedNodes=None, checksum=None): + from time import gmtime, strftime + if archiveFilePath is None: + fileName = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'LoadDICOMFilesToDatabase.zip' + archiveFilePath = slicer.app.temporaryPath + '/' + fileName + if dicomDataDir is None: + directoryName = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'LoadDICOMFilesToDatabase' + dicomDataDir = slicer.app.temporaryPath + '/' + directoryName + + self.url = url + self.checksum = checksum + self.archiveFilePath = archiveFilePath + self.dicomDataDir = dicomDataDir + self.expectedNumberOfExtractedFiles = expectedNumberOfFiles + self.selectedPlugins = selectedPlugins + self.loadedNodes = loadedNodes + + def __enter__(self): + if slicer.util.downloadAndExtractArchive(self.url, self.archiveFilePath, \ + self.dicomDataDir, self.expectedNumberOfExtractedFiles, + checksum=self.checksum): + dicomFiles = slicer.util.getFilesInDirectory(self.dicomDataDir) + if importDicom(self.dicomDataDir): + seriesUIDs = seriesUIDsForFiles(dicomFiles) + return loadSeriesWithVerification(seriesUIDs, self.selectedPlugins, self.loadedNodes) + return False + + def __exit__(self, type, value, traceback): + pass + + +# ------------------------------------------------------------------------------ # TODO: more consistency checks: # - is there gantry tilt? # - are the orientations the same for all slices? def getSortedImageFiles(filePaths, epsilon=0.01): - """ Sort DICOM image files in increasing slice order (IS direction) corresponding to a series - - Use the first file to get the ImageOrientationPatient for the - series and calculate the scan direction (assumed to be perpendicular - to the acquisition plane) - - epsilon: Maximum difference in distance between slices to consider spacing uniform - """ - warningText = '' - if len(filePaths) == 0: - return filePaths, [], warningText - - # Define DICOM tags used in this function - tags = {} - tags['position'] = "0020,0032" - tags['orientation'] = "0020,0037" - tags['numberOfFrames'] = "0028,0008" - tags['seriesUID'] = "0020,000E" - - seriesUID = slicer.dicomDatabase.fileValue(filePaths[0], tags['seriesUID']) - - if slicer.dicomDatabase.fileValue(filePaths[0], tags['numberOfFrames']) not in ["", "1"]: - warningText += "Multi-frame image. If slice orientation or spacing is non-uniform then the image may be displayed incorrectly. Use with caution.\n" - - # Make sure first file contains valid geometry - ref = {} - for tag in [tags['position'], tags['orientation']]: - value = slicer.dicomDatabase.fileValue(filePaths[0], tag) - if not value or value == "": - warningText += "Reference image in series does not contain geometry information. Please use caution.\n" - return filePaths, [], warningText - ref[tag] = value - - # Determine out-of-plane direction for first slice - import numpy as np - sliceAxes = [float(zz) for zz in ref[tags['orientation']].split('\\')] - x = np.array(sliceAxes[:3]) - y = np.array(sliceAxes[3:]) - scanAxis = np.cross(x,y) - scanOrigin = np.array([float(zz) for zz in ref[tags['position']].split('\\')]) - - # For each file in series, calculate the distance along the scan axis, sort files by this - sortList = [] - missingGeometry = False - for file in filePaths: - positionStr = slicer.dicomDatabase.fileValue(file,tags['position']) - orientationStr = slicer.dicomDatabase.fileValue(file,tags['orientation']) - if not positionStr or positionStr == "" or not orientationStr or orientationStr == "": - missingGeometry = True - break - position = np.array([float(zz) for zz in positionStr.split('\\')]) - vec = position - scanOrigin - dist = vec.dot(scanAxis) - sortList.append((file, dist)) - - if missingGeometry: - warningText += "One or more images is missing geometry information in series. Please use caution.\n" - return filePaths, [], warningText - - # Sort files names by distance from reference slice - sortedFiles = sorted(sortList, key=lambda x: x[1]) - files = [] - distances = {} - for file,dist in sortedFiles: - files.append(file) - distances[file] = dist - - # Get acquisition geometry regularization setting value - settings = qt.QSettings() - acquisitionGeometryRegularizationEnabled = (settings.value("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "default") == "transform") - - # Confirm equal spacing between slices - # - use variable 'epsilon' to determine the tolerance - spaceWarnings = 0 - if len(files) > 1: - file0 = files[0] - file1 = files[1] - dist0 = distances[file0] - dist1 = distances[file1] - spacing0 = dist1 - dist0 - n = 1 - for fileN in files[1:]: - fileNminus1 = files[n-1] - distN = distances[fileN] - distNminus1 = distances[fileNminus1] - spacingN = distN - distNminus1 - spaceError = spacingN - spacing0 - if abs(spaceError) > epsilon: - spaceWarnings += 1 - warningText += f"Images are not equally spaced (a difference of {spaceError:g} vs {spacing0:g} in spacings was detected)." - if acquisitionGeometryRegularizationEnabled: - warningText += " Slicer will apply a transform to this series trying to regularize the volume. Please use caution.\n" - else: - warningText += (" If loaded image appears distorted, enable 'Acquisition geometry regularization'" - " in Application settings / DICOM / DICOMScalarVolumePlugin. Please use caution.\n") - break - n += 1 - - if spaceWarnings != 0: - logging.warning("Geometric issues were found with %d of the series. Please use caution.\n" % spaceWarnings) - - return files, distances, warningText + """ Sort DICOM image files in increasing slice order (IS direction) corresponding to a series + Use the first file to get the ImageOrientationPatient for the + series and calculate the scan direction (assumed to be perpendicular + to the acquisition plane) -#------------------------------------------------------------------------------ + epsilon: Maximum difference in distance between slices to consider spacing uniform + """ + warningText = '' + if len(filePaths) == 0: + return filePaths, [], warningText + + # Define DICOM tags used in this function + tags = {} + tags['position'] = "0020,0032" + tags['orientation'] = "0020,0037" + tags['numberOfFrames'] = "0028,0008" + tags['seriesUID'] = "0020,000E" + + seriesUID = slicer.dicomDatabase.fileValue(filePaths[0], tags['seriesUID']) + + if slicer.dicomDatabase.fileValue(filePaths[0], tags['numberOfFrames']) not in ["", "1"]: + warningText += "Multi-frame image. If slice orientation or spacing is non-uniform then the image may be displayed incorrectly. Use with caution.\n" + + # Make sure first file contains valid geometry + ref = {} + for tag in [tags['position'], tags['orientation']]: + value = slicer.dicomDatabase.fileValue(filePaths[0], tag) + if not value or value == "": + warningText += "Reference image in series does not contain geometry information. Please use caution.\n" + return filePaths, [], warningText + ref[tag] = value + + # Determine out-of-plane direction for first slice + import numpy as np + sliceAxes = [float(zz) for zz in ref[tags['orientation']].split('\\')] + x = np.array(sliceAxes[:3]) + y = np.array(sliceAxes[3:]) + scanAxis = np.cross(x, y) + scanOrigin = np.array([float(zz) for zz in ref[tags['position']].split('\\')]) + + # For each file in series, calculate the distance along the scan axis, sort files by this + sortList = [] + missingGeometry = False + for file in filePaths: + positionStr = slicer.dicomDatabase.fileValue(file, tags['position']) + orientationStr = slicer.dicomDatabase.fileValue(file, tags['orientation']) + if not positionStr or positionStr == "" or not orientationStr or orientationStr == "": + missingGeometry = True + break + position = np.array([float(zz) for zz in positionStr.split('\\')]) + vec = position - scanOrigin + dist = vec.dot(scanAxis) + sortList.append((file, dist)) + + if missingGeometry: + warningText += "One or more images is missing geometry information in series. Please use caution.\n" + return filePaths, [], warningText + + # Sort files names by distance from reference slice + sortedFiles = sorted(sortList, key=lambda x: x[1]) + files = [] + distances = {} + for file, dist in sortedFiles: + files.append(file) + distances[file] = dist + + # Get acquisition geometry regularization setting value + settings = qt.QSettings() + acquisitionGeometryRegularizationEnabled = (settings.value("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "default") == "transform") + + # Confirm equal spacing between slices + # - use variable 'epsilon' to determine the tolerance + spaceWarnings = 0 + if len(files) > 1: + file0 = files[0] + file1 = files[1] + dist0 = distances[file0] + dist1 = distances[file1] + spacing0 = dist1 - dist0 + n = 1 + for fileN in files[1:]: + fileNminus1 = files[n - 1] + distN = distances[fileN] + distNminus1 = distances[fileNminus1] + spacingN = distN - distNminus1 + spaceError = spacingN - spacing0 + if abs(spaceError) > epsilon: + spaceWarnings += 1 + warningText += f"Images are not equally spaced (a difference of {spaceError:g} vs {spacing0:g} in spacings was detected)." + if acquisitionGeometryRegularizationEnabled: + warningText += " Slicer will apply a transform to this series trying to regularize the volume. Please use caution.\n" + else: + warningText += (" If loaded image appears distorted, enable 'Acquisition geometry regularization'" + " in Application settings / DICOM / DICOMScalarVolumePlugin. Please use caution.\n") + break + n += 1 + + if spaceWarnings != 0: + logging.warning("Geometric issues were found with %d of the series. Please use caution.\n" % spaceWarnings) + + return files, distances, warningText + + +# ------------------------------------------------------------------------------ def refreshDICOMWidget(): - """ Refresh DICOM browser from database. - It is useful when the database is changed via a database object that is - different from the one stored in the DICOM browser. There may be multiple - database connection (through different database objects) in the same process. - """ - try: - slicer.modules.DICOMInstance.browserWidget.dicomBrowser.dicomTableManager().updateTableViews() - except AttributeError: - logging.error('DICOM module or browser cannot be accessed') - return False - return True + """ Refresh DICOM browser from database. + It is useful when the database is changed via a database object that is + different from the one stored in the DICOM browser. There may be multiple + database connection (through different database objects) in the same process. + """ + try: + slicer.modules.DICOMInstance.browserWidget.dicomBrowser.dicomTableManager().updateTableViews() + except AttributeError: + logging.error('DICOM module or browser cannot be accessed') + return False + return True def getLoadablesFromFileLists(fileLists, pluginClassNames=None, messages=None, progressCallback=None, pluginInstances=None): - """Take list of file lists, return loadables by plugin dictionary - """ - detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool) - loadablesByPlugin = {} - loadEnabled = False - if not isinstance(fileLists, list) or len(fileLists) == 0 or not type(fileLists[0]) in [tuple, list]: - logging.error('File lists must contain a non-empty list of tuples/lists') + """Take list of file lists, return loadables by plugin dictionary + """ + detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool) + loadablesByPlugin = {} + loadEnabled = False + if not isinstance(fileLists, list) or len(fileLists) == 0 or not type(fileLists[0]) in [tuple, list]: + logging.error('File lists must contain a non-empty list of tuples/lists') + return loadablesByPlugin, loadEnabled + + if pluginClassNames is None: + pluginClassNames = list(slicer.modules.dicomPlugins.keys()) + + if pluginInstances is None: + pluginInstances = {} + + for step, pluginClassName in enumerate(pluginClassNames): + if pluginClassName not in pluginInstances: + pluginInstances[pluginClassName] = slicer.modules.dicomPlugins[pluginClassName]() + plugin = pluginInstances[pluginClassName] + if progressCallback: + cancelled = progressCallback(pluginClassName, step * 100 / len(pluginClassNames)) + if cancelled: + break + try: + if detailedLogging: + logging.debug("Examine for import using " + pluginClassName) + loadablesByPlugin[plugin] = plugin.examineForImport(fileLists) + # If regular method is not overridden (so returns empty list), try old function + # Ensuring backwards compatibility: examineForImport used to be called examine + if not loadablesByPlugin[plugin]: + loadablesByPlugin[plugin] = plugin.examine(fileLists) + loadEnabled = loadEnabled or loadablesByPlugin[plugin] != [] + except Exception as e: + import traceback + traceback.print_exc() + logging.error("DICOM Plugin failed: %s" % str(e)) + if messages: + messages.append("Plugin failed: %s." % pluginClass) + return loadablesByPlugin, loadEnabled - if pluginClassNames is None: - pluginClassNames = list(slicer.modules.dicomPlugins.keys()) - if pluginInstances is None: - pluginInstances = {} +def loadLoadables(loadablesByPlugin, messages=None, progressCallback=None): + """Load each DICOM loadable item. + Returns loaded node IDs. + """ - for step, pluginClassName in enumerate(pluginClassNames): - if pluginClassName not in pluginInstances: - pluginInstances[pluginClassName] = slicer.modules.dicomPlugins[pluginClassName]() - plugin = pluginInstances[pluginClassName] - if progressCallback: - cancelled = progressCallback(pluginClassName, step*100/len(pluginClassNames)) - if cancelled: - break - try: - if detailedLogging: - logging.debug("Examine for import using " + pluginClassName) - loadablesByPlugin[plugin] = plugin.examineForImport(fileLists) - # If regular method is not overridden (so returns empty list), try old function - # Ensuring backwards compatibility: examineForImport used to be called examine - if not loadablesByPlugin[plugin]: - loadablesByPlugin[plugin] = plugin.examine(fileLists) - loadEnabled = loadEnabled or loadablesByPlugin[plugin] != [] - except Exception as e: - import traceback - traceback.print_exc() - logging.error("DICOM Plugin failed: %s" % str(e)) - if messages: - messages.append("Plugin failed: %s." % pluginClass) + # Find a plugin for each loadable that will load it + # (the last plugin that has that loadable selected wins) + selectedLoadables = {} + for plugin in loadablesByPlugin: + for loadable in loadablesByPlugin[plugin]: + if loadable.selected: + selectedLoadables[loadable] = plugin - return loadablesByPlugin, loadEnabled + loadedNodeIDs = [] + @vtk.calldata_type(vtk.VTK_OBJECT) + def onNodeAdded(caller, event, calldata): + node = calldata + if not isinstance(node, slicer.vtkMRMLStorageNode) and not isinstance(node, slicer.vtkMRMLDisplayNode): + loadedNodeIDs.append(node.GetID()) -def loadLoadables(loadablesByPlugin, messages=None, progressCallback=None): - """Load each DICOM loadable item. - Returns loaded node IDs. - """ - - # Find a plugin for each loadable that will load it - # (the last plugin that has that loadable selected wins) - selectedLoadables = {} - for plugin in loadablesByPlugin: - for loadable in loadablesByPlugin[plugin]: - if loadable.selected: - selectedLoadables[loadable] = plugin - - loadedNodeIDs = [] - - @vtk.calldata_type(vtk.VTK_OBJECT) - def onNodeAdded(caller, event, calldata): - node = calldata - if not isinstance(node, slicer.vtkMRMLStorageNode) and not isinstance(node, slicer.vtkMRMLDisplayNode): - loadedNodeIDs.append(node.GetID()) - - sceneObserverTag = slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent, onNodeAdded) - - for step, (loadable, plugin) in enumerate(selectedLoadables.items(), start=1): - if progressCallback: - cancelled = progressCallback(loadable.name, step*100/len(selectedLoadables)) - if cancelled: - break + sceneObserverTag = slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent, onNodeAdded) - try: - loadSuccess = plugin.load(loadable) - except: - loadSuccess = False - import traceback - logging.error("DICOM plugin failed to load '" - + loadable.name + "' as a '" + plugin.loadType + "'.\n" - + traceback.format_exc()) - if (not loadSuccess) and (messages is not None): - messages.append(f'Could not load: {loadable.name} as a {plugin.loadType}') - - cancelled = False - try: - # DICOM reader plugins (for example, in PETDICOM extension) may generate additional DICOM files - # during loading. These must be added to the database. - for derivedItem in loadable.derivedItems: - indexer = ctk.ctkDICOMIndexer() + for step, (loadable, plugin) in enumerate(selectedLoadables.items(), start=1): if progressCallback: - cancelled = progressCallback(f"{loadable.name} ({derivedItem})", step*100/len(selectedLoadables)) - if cancelled: + cancelled = progressCallback(loadable.name, step * 100 / len(selectedLoadables)) + if cancelled: + break + + try: + loadSuccess = plugin.load(loadable) + except: + loadSuccess = False + import traceback + logging.error("DICOM plugin failed to load '" + + loadable.name + "' as a '" + plugin.loadType + "'.\n" + + traceback.format_exc()) + if (not loadSuccess) and (messages is not None): + messages.append(f'Could not load: {loadable.name} as a {plugin.loadType}') + + cancelled = False + try: + # DICOM reader plugins (for example, in PETDICOM extension) may generate additional DICOM files + # during loading. These must be added to the database. + for derivedItem in loadable.derivedItems: + indexer = ctk.ctkDICOMIndexer() + if progressCallback: + cancelled = progressCallback(f"{loadable.name} ({derivedItem})", step * 100 / len(selectedLoadables)) + if cancelled: + break + indexer.addFile(slicer.dicomDatabase, derivedItem) + except AttributeError: + # no derived items or some other attribute error + pass + if cancelled: break - indexer.addFile(slicer.dicomDatabase, derivedItem) - except AttributeError: - # no derived items or some other attribute error - pass - if cancelled: - break - slicer.mrmlScene.RemoveObserver(sceneObserverTag) + slicer.mrmlScene.RemoveObserver(sceneObserverTag) - return loadedNodeIDs + return loadedNodeIDs def importFromDICOMWeb(dicomWebEndpoint, studyInstanceUID, seriesInstanceUID=None, accessToken=None): - """ - Downloads and imports DICOM series from a DICOMweb instance. - Progress is displayed and if errors occur then they are displayed in a popup window in the end. - If all the instances in a series are already imported then the series will not be retrieved and imported again. - - :param dicomWebEndpoint: Endpoint URL for retrieving the study/series from DICOMweb - :param studyInstanceUID: UID for the study to be downloaded - :param seriesInstanceUID: UID for the series to be downloaded. If not specified, all series will be downloaded from the study - :param accessToken: Optional access token for the query - :return: List of imported study UIDs + """ + Downloads and imports DICOM series from a DICOMweb instance. + Progress is displayed and if errors occur then they are displayed in a popup window in the end. + If all the instances in a series are already imported then the series will not be retrieved and imported again. - Example: calling from PythonSlicer console + :param dicomWebEndpoint: Endpoint URL for retrieving the study/series from DICOMweb + :param studyInstanceUID: UID for the study to be downloaded + :param seriesInstanceUID: UID for the series to be downloaded. If not specified, all series will be downloaded from the study + :param accessToken: Optional access token for the query + :return: List of imported study UIDs - .. code-block:: python + Example: calling from PythonSlicer console - from DICOMLib import DICOMUtils - loadedUIDs = DICOMUtils.importFromDICOMWeb(dicomWebEndpoint="https://yourdicomweburl/dicomWebEndpoint", - studyInstanceUID="2.16.840.1.113669.632.20.1211.10000509338") - accessToken="YOUR_ACCESS_TOKEN") + .. code-block:: python - """ + from DICOMLib import DICOMUtils + loadedUIDs = DICOMUtils.importFromDICOMWeb(dicomWebEndpoint="https://yourdicomweburl/dicomWebEndpoint", + studyInstanceUID="2.16.840.1.113669.632.20.1211.10000509338") + accessToken="YOUR_ACCESS_TOKEN") - from dicomweb_client.api import DICOMwebClient + """ - seriesImported = [] - errors = [] - clientLogger = logging.getLogger('dicomweb_client.api') - originalClientLogLevel = clientLogger.level - - progressDialog = slicer.util.createProgressDialog(parent=slicer.util.mainWindow(), value=0, maximum=100) - try: - progressDialog.labelText = f'Retrieving series list...' - slicer.app.processEvents() - - if accessToken is None: - client = DICOMwebClient(url = dicomWebEndpoint) - else: - client = DICOMwebClient( - url = dicomWebEndpoint, - headers = { "Authorization": f"Bearer {accessToken}" }, - ) - - seriesList = client.search_for_series(study_instance_uid=studyInstanceUID) - seriesInstanceUIDs = [] - if not seriesInstanceUID is None: - seriesInstanceUIDs = [seriesInstanceUID] - else: - for series in seriesList: - currentSeriesInstanceUID = series['0020000E']['Value'][0] - seriesInstanceUIDs.append(currentSeriesInstanceUID) - - # Turn off detailed logging, because it would slow down the file transfer - clientLogger.setLevel(logging.WARNING) - - fileNumber = 0 - cancelled = False - for seriesIndex, currentSeriesInstanceUID in enumerate(seriesInstanceUIDs): - progressDialog.labelText = f'Retrieving series {seriesIndex+1} of {len(seriesInstanceUIDs)}...' - slicer.app.processEvents() - - try: - seriesInfo = client.retrieve_series_metadata( - study_instance_uid=studyInstanceUID, - series_instance_uid=currentSeriesInstanceUID) - numberOfInstances = len(seriesInfo) - - # Skip retrieve and import of this series if it is already imported - alreadyImportedInstances = slicer.dicomDatabase.instancesForSeries(currentSeriesInstanceUID) - seriesAlreadyImported = True - for serieInfo in seriesInfo: - sopInstanceUID = serieInfo['00080018']['Value'][0] - if sopInstanceUID not in alreadyImportedInstances: - seriesAlreadyImported = False - break - if seriesAlreadyImported: - seriesImported.append(currentSeriesInstanceUID) - continue + from dicomweb_client.api import DICOMwebClient - instances = client.iter_series( - study_instance_uid=studyInstanceUID, - series_instance_uid=currentSeriesInstanceUID) + seriesImported = [] + errors = [] + clientLogger = logging.getLogger('dicomweb_client.api') + originalClientLogLevel = clientLogger.level + progressDialog = slicer.util.createProgressDialog(parent=slicer.util.mainWindow(), value=0, maximum=100) + try: + progressDialog.labelText = f'Retrieving series list...' slicer.app.processEvents() - cancelled = progressDialog.wasCanceled - if cancelled: - break - - outputDirectoryBase = slicer.dicomDatabase.databaseDirectory + "/DICOMweb" - if not os.access(outputDirectoryBase, os.F_OK): - os.makedirs(outputDirectoryBase) - outputDirectoryBase += "/" + qt.QDateTime.currentDateTime().toString("yyyyMMdd-hhmmss") - outputDirectory = qt.QTemporaryDir(outputDirectoryBase) # Add unique substring to directory - outputDirectory.setAutoRemove(False) - outputDirectoryPath = outputDirectory.path() - - for instanceIndex, instance in enumerate(instances): - progressDialog.setValue(int(100*instanceIndex/numberOfInstances)) - slicer.app.processEvents() - cancelled = progressDialog.wasCanceled - if cancelled: - break - filename = outputDirectoryPath + "/" + str(fileNumber) + ".dcm" - instance.save_as(filename) - fileNumber += 1 - - if cancelled: - # cancel was requested in instance retrieve loop, - # stop the entire import process - break - importDicom(outputDirectoryPath) - seriesImported.append(currentSeriesInstanceUID) + if accessToken is None: + client = DICOMwebClient(url=dicomWebEndpoint) + else: + client = DICOMwebClient( + url=dicomWebEndpoint, + headers={"Authorization": f"Bearer {accessToken}"}, + ) + + seriesList = client.search_for_series(study_instance_uid=studyInstanceUID) + seriesInstanceUIDs = [] + if seriesInstanceUID is not None: + seriesInstanceUIDs = [seriesInstanceUID] + else: + for series in seriesList: + currentSeriesInstanceUID = series['0020000E']['Value'][0] + seriesInstanceUIDs.append(currentSeriesInstanceUID) + + # Turn off detailed logging, because it would slow down the file transfer + clientLogger.setLevel(logging.WARNING) + + fileNumber = 0 + cancelled = False + for seriesIndex, currentSeriesInstanceUID in enumerate(seriesInstanceUIDs): + progressDialog.labelText = f'Retrieving series {seriesIndex+1} of {len(seriesInstanceUIDs)}...' + slicer.app.processEvents() + + try: + seriesInfo = client.retrieve_series_metadata( + study_instance_uid=studyInstanceUID, + series_instance_uid=currentSeriesInstanceUID) + numberOfInstances = len(seriesInfo) + + # Skip retrieve and import of this series if it is already imported + alreadyImportedInstances = slicer.dicomDatabase.instancesForSeries(currentSeriesInstanceUID) + seriesAlreadyImported = True + for serieInfo in seriesInfo: + sopInstanceUID = serieInfo['00080018']['Value'][0] + if sopInstanceUID not in alreadyImportedInstances: + seriesAlreadyImported = False + break + if seriesAlreadyImported: + seriesImported.append(currentSeriesInstanceUID) + continue + + instances = client.iter_series( + study_instance_uid=studyInstanceUID, + series_instance_uid=currentSeriesInstanceUID) + + slicer.app.processEvents() + cancelled = progressDialog.wasCanceled + if cancelled: + break + + outputDirectoryBase = slicer.dicomDatabase.databaseDirectory + "/DICOMweb" + if not os.access(outputDirectoryBase, os.F_OK): + os.makedirs(outputDirectoryBase) + outputDirectoryBase += "/" + qt.QDateTime.currentDateTime().toString("yyyyMMdd-hhmmss") + outputDirectory = qt.QTemporaryDir(outputDirectoryBase) # Add unique substring to directory + outputDirectory.setAutoRemove(False) + outputDirectoryPath = outputDirectory.path() + + for instanceIndex, instance in enumerate(instances): + progressDialog.setValue(int(100 * instanceIndex / numberOfInstances)) + slicer.app.processEvents() + cancelled = progressDialog.wasCanceled + if cancelled: + break + filename = outputDirectoryPath + "/" + str(fileNumber) + ".dcm" + instance.save_as(filename) + fileNumber += 1 + + if cancelled: + # cancel was requested in instance retrieve loop, + # stop the entire import process + break + + importDicom(outputDirectoryPath) + seriesImported.append(currentSeriesInstanceUID) + + except Exception as e: + import traceback + errors.append(f"Error importing series {currentSeriesInstanceUID}: {str(e)} ({traceback.format_exc()})") - except Exception as e: + except Exception as e: import traceback - errors.append(f"Error importing series {currentSeriesInstanceUID}: {str(e)} ({traceback.format_exc()})") + errors.append(f"{str(e)} ({traceback.format_exc()})") - except Exception as e: - import traceback - errors.append(f"{str(e)} ({traceback.format_exc()})") + finally: + progressDialog.close() + clientLogger.setLevel(originalClientLogLevel) - finally: - progressDialog.close() - clientLogger.setLevel(originalClientLogLevel) + if errors: + slicer.util.errorDisplay(f"Errors occurred during DICOMweb import of {len(errors)} series.", detailedText="\n\n".join(errors)) + elif cancelled and (len(seriesImported) < len(seriesInstanceUIDs)): + slicer.util.infoDisplay(f"DICOMweb import has been interrupted after completing {len(seriesImported)} out of {len(seriesInstanceUIDs)} series.") - if errors: - slicer.util.errorDisplay(f"Errors occurred during DICOMweb import of {len(errors)} series.", detailedText="\n\n".join(errors)) - elif cancelled and (len(seriesImported) < len(seriesInstanceUIDs)): - slicer.util.infoDisplay(f"DICOMweb import has been interrupted after completing {len(seriesImported)} out of {len(seriesInstanceUIDs)} series.") - - return seriesImported + return seriesImported def registerSlicerURLHandler(): - """ - Registers file associations and applicationName:// protocol (e.g., Slicer://) - with this executable. This allows Kheops (https://demo.kheops.online) open - images selected in the web browser directly in Slicer. - For now, only implemented on Windows. - """ - if os.name == 'nt': - launcherPath = qt.QDir.toNativeSeparators(qt.QFileInfo(slicer.app.launcherExecutableFilePath).absoluteFilePath()) - reg = qt.QSettings(f"HKEY_CURRENT_USER\\Software\\Classes", qt.QSettings.NativeFormat) - reg.setValue(f"{slicer.app.applicationName}/.",f"{slicer.app.applicationName} supported file") - reg.setValue(f"{slicer.app.applicationName}/URL protocol","") - reg.setValue(f"{slicer.app.applicationName}/shell/open/command/.",f"\"{launcherPath}\" \"%1\"") - reg.setValue(f"{slicer.app.applicationName}/DefaultIcon/.",f"{slicer.app.applicationName}.exe,0") - for ext in ['mrml', 'mrb']: - reg.setValue(f".{ext}/.",f"{slicer.app.applicationName}") - reg.setValue(f".{ext}/Content Type", f"application/x-{ext}") - else: - raise NotImplementedError() + """ + Registers file associations and applicationName:// protocol (e.g., Slicer://) + with this executable. This allows Kheops (https://demo.kheops.online) open + images selected in the web browser directly in Slicer. + For now, only implemented on Windows. + """ + if os.name == 'nt': + launcherPath = qt.QDir.toNativeSeparators(qt.QFileInfo(slicer.app.launcherExecutableFilePath).absoluteFilePath()) + reg = qt.QSettings(f"HKEY_CURRENT_USER\\Software\\Classes", qt.QSettings.NativeFormat) + reg.setValue(f"{slicer.app.applicationName}/.", f"{slicer.app.applicationName} supported file") + reg.setValue(f"{slicer.app.applicationName}/URL protocol", "") + reg.setValue(f"{slicer.app.applicationName}/shell/open/command/.", f"\"{launcherPath}\" \"%1\"") + reg.setValue(f"{slicer.app.applicationName}/DefaultIcon/.", f"{slicer.app.applicationName}.exe,0") + for ext in ['mrml', 'mrb']: + reg.setValue(f".{ext}/.", f"{slicer.app.applicationName}") + reg.setValue(f".{ext}/Content Type", f"application/x-{ext}") + else: + raise NotImplementedError() diff --git a/Modules/Scripted/DICOMPatcher/DICOMPatcher.py b/Modules/Scripted/DICOMPatcher/DICOMPatcher.py index e4351f22da0..82c3e83c13a 100644 --- a/Modules/Scripted/DICOMPatcher/DICOMPatcher.py +++ b/Modules/Scripted/DICOMPatcher/DICOMPatcher.py @@ -13,19 +13,19 @@ # class DICOMPatcher(ScriptedLoadableModule): - """Uses ScriptedLoadableModule base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """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 = "DICOM Patcher" - self.parent.categories = ["Utilities"] - self.parent.dependencies = ["DICOM"] - self.parent.contributors = ["Andras Lasso (PerkLab)"] - self.parent.helpText = """Fix common issues in DICOM files. This module may help fixing DICOM files that Slicer fails to import.""" - self.parent.helpText += parent.defaultDocumentationLink - self.parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab.""" + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "DICOM Patcher" + self.parent.categories = ["Utilities"] + self.parent.dependencies = ["DICOM"] + self.parent.contributors = ["Andras Lasso (PerkLab)"] + self.parent.helpText = """Fix common issues in DICOM files. This module may help fixing DICOM files that Slicer fails to import.""" + self.parent.helpText += parent.defaultDocumentationLink + self.parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab.""" # @@ -33,141 +33,141 @@ def __init__(self, parent): # class DICOMPatcherWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - # Instantiate and connect widgets ... - - # - # Parameters Area - # - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Parameters" - self.layout.addWidget(parametersCollapsibleButton) - - # Layout within the dummy collapsible button - parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) - - self.inputDirSelector = ctk.ctkPathLineEdit() - self.inputDirSelector.filters = ctk.ctkPathLineEdit.Dirs - self.inputDirSelector.settingKey = 'DICOMPatcherInputDir' - parametersFormLayout.addRow("Input DICOM directory:", self.inputDirSelector) - - self.outputDirSelector = ctk.ctkPathLineEdit() - self.outputDirSelector.filters = ctk.ctkPathLineEdit.Dirs - self.outputDirSelector.settingKey = 'DICOMPatcherOutputDir' - parametersFormLayout.addRow("Output DICOM directory:", self.outputDirSelector) - - self.normalizeFileNamesCheckBox = qt.QCheckBox() - self.normalizeFileNamesCheckBox.checked = True - self.normalizeFileNamesCheckBox.setToolTip("Replace file and folder names with automatically generated names." - " Fixes errors caused by file path containins special characters or being too long.") - parametersFormLayout.addRow("Normalize file names", self.normalizeFileNamesCheckBox) - - self.forceSamePatientNameIdInEachDirectoryCheckBox = qt.QCheckBox() - self.forceSamePatientNameIdInEachDirectoryCheckBox.checked = False - self.forceSamePatientNameIdInEachDirectoryCheckBox.setToolTip("Generate patient name and ID from the first file in a directory" - " and force all other files in the same directory to have the same patient name and ID." - " Enable this option if a separate patient directory is created for each patched file.") - parametersFormLayout.addRow("Force same patient name and ID in each directory", self.forceSamePatientNameIdInEachDirectoryCheckBox) - - self.forceSameSeriesInstanceUidInEachDirectoryCheckBox = qt.QCheckBox() - self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.checked = False - self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.setToolTip("Generate a new series instance UID for each directory" - " and set it in all files in that same directory." - " Enable this option to force placing all frames in a folder into a single volume.") - parametersFormLayout.addRow("Force same series instance UID in each directory", self.forceSameSeriesInstanceUidInEachDirectoryCheckBox) - - self.generateMissingIdsCheckBox = qt.QCheckBox() - self.generateMissingIdsCheckBox.checked = True - self.generateMissingIdsCheckBox.setToolTip("Generate missing patient, study, series IDs. It is assumed that" - " all files in a directory belong to the same series. Fixes error caused by too aggressive anonymization" - " or incorrect DICOM image converters.") - parametersFormLayout.addRow("Generate missing patient/study/series IDs", self.generateMissingIdsCheckBox) - - self.generateImagePositionFromSliceThicknessCheckBox = qt.QCheckBox() - self.generateImagePositionFromSliceThicknessCheckBox.checked = True - self.generateImagePositionFromSliceThicknessCheckBox.setToolTip("Generate 'image position sequence' for" - " multi-frame files that only have 'SliceThickness' field. Fixes error in Dolphin 3D CBCT scanners.") - parametersFormLayout.addRow("Generate slice position for multi-frame volumes", self.generateImagePositionFromSliceThicknessCheckBox) - - self.anonymizeDicomCheckBox = qt.QCheckBox() - self.anonymizeDicomCheckBox.checked = False - self.anonymizeDicomCheckBox.setToolTip("If checked, then some patient identifiable information will be removed" - " from the patched DICOM files. There are many fields that can identify a patient, this function does not remove all of them.") - parametersFormLayout.addRow("Partially anonymize", self.anonymizeDicomCheckBox) - - # - # Patch Button - # - self.patchButton = qt.QPushButton("Patch") - self.patchButton.toolTip = "Fix DICOM files in input directory and write them to output directory" - parametersFormLayout.addRow(self.patchButton) - - # - # Import Button - # - self.importButton = qt.QPushButton("Import to DICOM database") - self.importButton.toolTip = "Import DICOM files in output directory into the application's DICOM database" - parametersFormLayout.addRow(self.importButton) - - # connections - self.patchButton.connect('clicked(bool)', self.onPatchButton) - self.importButton.connect('clicked(bool)', self.onImportButton) - - self.statusLabel = qt.QPlainTextEdit() - self.statusLabel.setTextInteractionFlags(qt.Qt.TextSelectableByMouse) - parametersFormLayout.addRow(self.statusLabel) - - # Add vertical spacer - self.layout.addStretch(1) - - self.logic = DICOMPatcherLogic() - self.logic.logCallback = self.addLog - - def cleanup(self): - pass - - def onPatchButton(self): - with slicer.util.tryWithErrorDisplay("Unexpected error.", waitCursor=True): - - import tempfile - if not self.outputDirSelector.currentPath: - self.outputDirSelector.currentPath = tempfile.mkdtemp(prefix="DICOMPatcher-", dir=slicer.app.temporaryPath) - - self.inputDirSelector.addCurrentPathToHistory() - self.outputDirSelector.addCurrentPathToHistory() - self.statusLabel.plainText = '' - - self.logic.clearRules() - if self.forceSamePatientNameIdInEachDirectoryCheckBox.checked: - self.logic.addRule("ForceSamePatientNameIdInEachDirectory") - if self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.checked: - self.logic.addRule("ForceSameSeriesInstanceUidInEachDirectory") - if self.generateMissingIdsCheckBox.checked: - self.logic.addRule("GenerateMissingIDs") - self.logic.addRule("RemoveDICOMDIR") - self.logic.addRule("FixPrivateMediaStorageSOPClassUID") - if self.generateImagePositionFromSliceThicknessCheckBox.checked: - self.logic.addRule("AddMissingSliceSpacingToMultiframe") - if self.anonymizeDicomCheckBox.checked: - self.logic.addRule("Anonymize") - if self.normalizeFileNamesCheckBox.checked: - self.logic.addRule("NormalizeFileNames") - self.logic.patchDicomDir(self.inputDirSelector.currentPath, self.outputDirSelector.currentPath) - - def onImportButton(self): - self.logic.importDicomDir(self.outputDirSelector.currentPath) - - def addLog(self, text): - """Append text to log window + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - self.statusLabel.appendPlainText(text) - slicer.app.processEvents() # force update + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + # Instantiate and connect widgets ... + + # + # Parameters Area + # + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Parameters" + self.layout.addWidget(parametersCollapsibleButton) + + # Layout within the dummy collapsible button + parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) + + self.inputDirSelector = ctk.ctkPathLineEdit() + self.inputDirSelector.filters = ctk.ctkPathLineEdit.Dirs + self.inputDirSelector.settingKey = 'DICOMPatcherInputDir' + parametersFormLayout.addRow("Input DICOM directory:", self.inputDirSelector) + + self.outputDirSelector = ctk.ctkPathLineEdit() + self.outputDirSelector.filters = ctk.ctkPathLineEdit.Dirs + self.outputDirSelector.settingKey = 'DICOMPatcherOutputDir' + parametersFormLayout.addRow("Output DICOM directory:", self.outputDirSelector) + + self.normalizeFileNamesCheckBox = qt.QCheckBox() + self.normalizeFileNamesCheckBox.checked = True + self.normalizeFileNamesCheckBox.setToolTip("Replace file and folder names with automatically generated names." + " Fixes errors caused by file path containins special characters or being too long.") + parametersFormLayout.addRow("Normalize file names", self.normalizeFileNamesCheckBox) + + self.forceSamePatientNameIdInEachDirectoryCheckBox = qt.QCheckBox() + self.forceSamePatientNameIdInEachDirectoryCheckBox.checked = False + self.forceSamePatientNameIdInEachDirectoryCheckBox.setToolTip("Generate patient name and ID from the first file in a directory" + " and force all other files in the same directory to have the same patient name and ID." + " Enable this option if a separate patient directory is created for each patched file.") + parametersFormLayout.addRow("Force same patient name and ID in each directory", self.forceSamePatientNameIdInEachDirectoryCheckBox) + + self.forceSameSeriesInstanceUidInEachDirectoryCheckBox = qt.QCheckBox() + self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.checked = False + self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.setToolTip("Generate a new series instance UID for each directory" + " and set it in all files in that same directory." + " Enable this option to force placing all frames in a folder into a single volume.") + parametersFormLayout.addRow("Force same series instance UID in each directory", self.forceSameSeriesInstanceUidInEachDirectoryCheckBox) + + self.generateMissingIdsCheckBox = qt.QCheckBox() + self.generateMissingIdsCheckBox.checked = True + self.generateMissingIdsCheckBox.setToolTip("Generate missing patient, study, series IDs. It is assumed that" + " all files in a directory belong to the same series. Fixes error caused by too aggressive anonymization" + " or incorrect DICOM image converters.") + parametersFormLayout.addRow("Generate missing patient/study/series IDs", self.generateMissingIdsCheckBox) + + self.generateImagePositionFromSliceThicknessCheckBox = qt.QCheckBox() + self.generateImagePositionFromSliceThicknessCheckBox.checked = True + self.generateImagePositionFromSliceThicknessCheckBox.setToolTip("Generate 'image position sequence' for" + " multi-frame files that only have 'SliceThickness' field. Fixes error in Dolphin 3D CBCT scanners.") + parametersFormLayout.addRow("Generate slice position for multi-frame volumes", self.generateImagePositionFromSliceThicknessCheckBox) + + self.anonymizeDicomCheckBox = qt.QCheckBox() + self.anonymizeDicomCheckBox.checked = False + self.anonymizeDicomCheckBox.setToolTip("If checked, then some patient identifiable information will be removed" + " from the patched DICOM files. There are many fields that can identify a patient, this function does not remove all of them.") + parametersFormLayout.addRow("Partially anonymize", self.anonymizeDicomCheckBox) + + # + # Patch Button + # + self.patchButton = qt.QPushButton("Patch") + self.patchButton.toolTip = "Fix DICOM files in input directory and write them to output directory" + parametersFormLayout.addRow(self.patchButton) + + # + # Import Button + # + self.importButton = qt.QPushButton("Import to DICOM database") + self.importButton.toolTip = "Import DICOM files in output directory into the application's DICOM database" + parametersFormLayout.addRow(self.importButton) + + # connections + self.patchButton.connect('clicked(bool)', self.onPatchButton) + self.importButton.connect('clicked(bool)', self.onImportButton) + + self.statusLabel = qt.QPlainTextEdit() + self.statusLabel.setTextInteractionFlags(qt.Qt.TextSelectableByMouse) + parametersFormLayout.addRow(self.statusLabel) + + # Add vertical spacer + self.layout.addStretch(1) + + self.logic = DICOMPatcherLogic() + self.logic.logCallback = self.addLog + + def cleanup(self): + pass + + def onPatchButton(self): + with slicer.util.tryWithErrorDisplay("Unexpected error.", waitCursor=True): + + import tempfile + if not self.outputDirSelector.currentPath: + self.outputDirSelector.currentPath = tempfile.mkdtemp(prefix="DICOMPatcher-", dir=slicer.app.temporaryPath) + + self.inputDirSelector.addCurrentPathToHistory() + self.outputDirSelector.addCurrentPathToHistory() + self.statusLabel.plainText = '' + + self.logic.clearRules() + if self.forceSamePatientNameIdInEachDirectoryCheckBox.checked: + self.logic.addRule("ForceSamePatientNameIdInEachDirectory") + if self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.checked: + self.logic.addRule("ForceSameSeriesInstanceUidInEachDirectory") + if self.generateMissingIdsCheckBox.checked: + self.logic.addRule("GenerateMissingIDs") + self.logic.addRule("RemoveDICOMDIR") + self.logic.addRule("FixPrivateMediaStorageSOPClassUID") + if self.generateImagePositionFromSliceThicknessCheckBox.checked: + self.logic.addRule("AddMissingSliceSpacingToMultiframe") + if self.anonymizeDicomCheckBox.checked: + self.logic.addRule("Anonymize") + if self.normalizeFileNamesCheckBox.checked: + self.logic.addRule("NormalizeFileNames") + self.logic.patchDicomDir(self.inputDirSelector.currentPath, self.outputDirSelector.currentPath) + + def onImportButton(self): + self.logic.importDicomDir(self.outputDirSelector.currentPath) + + def addLog(self, text): + """Append text to log window + """ + self.statusLabel.appendPlainText(text) + slicer.app.processEvents() # force update # @@ -175,28 +175,28 @@ def addLog(self, text): # class DICOMPatcherRule: - def __init__(self): - self.logCallback = None + def __init__(self): + self.logCallback = None - def addLog(self, text): - logging.info(text) - if self.logCallback: - self.logCallback(text) + def addLog(self, text): + logging.info(text) + if self.logCallback: + self.logCallback(text) - def processStart(self, inputRootDir, outputRootDir): - pass + def processStart(self, inputRootDir, outputRootDir): + pass - def processDirectory(self, currentSubDir): - pass + def processDirectory(self, currentSubDir): + pass - def skipFile(self, filepath): - return False + def skipFile(self, filepath): + return False - def processDataSet(self, ds): - pass + def processDataSet(self, ds): + pass - def generateOutputFilePath(self, ds, filepath): - return filepath + def generateOutputFilePath(self, ds, filepath): + return filepath # @@ -204,105 +204,105 @@ def generateOutputFilePath(self, ds, filepath): # class ForceSamePatientNameIdInEachDirectory(DICOMPatcherRule): - def __init__(self): - self.requiredTags = ['PatientName', 'PatientID'] - self.eachFileIsSeparateSeries = False - - def processStart(self, inputRootDir, outputRootDir): - self.patientIndex = 0 - - def processDirectory(self, currentSubDir): - self.firstFileInDirectory = True - self.patientIndex += 1 - - def processDataSet(self, ds): - import pydicom - if self.firstFileInDirectory: - # Get patient name and ID for this folder and save it - self.firstFileInDirectory = False - if ds.PatientName: - self.patientName = ds.PatientName - else: - self.patientName = "Unspecified Patient " + str(self.patientIndex) - if ds.PatientID: - self.patientID = ds.PatientID - else: - self.patientID = pydicom.uid.generate_uid(None) - # Set the same patient name and ID as the first file in the directory - ds.PatientName = self.patientName - ds.PatientID = self.patientID + def __init__(self): + self.requiredTags = ['PatientName', 'PatientID'] + self.eachFileIsSeparateSeries = False + + def processStart(self, inputRootDir, outputRootDir): + self.patientIndex = 0 + + def processDirectory(self, currentSubDir): + self.firstFileInDirectory = True + self.patientIndex += 1 + + def processDataSet(self, ds): + import pydicom + if self.firstFileInDirectory: + # Get patient name and ID for this folder and save it + self.firstFileInDirectory = False + if ds.PatientName: + self.patientName = ds.PatientName + else: + self.patientName = "Unspecified Patient " + str(self.patientIndex) + if ds.PatientID: + self.patientID = ds.PatientID + else: + self.patientID = pydicom.uid.generate_uid(None) + # Set the same patient name and ID as the first file in the directory + ds.PatientName = self.patientName + ds.PatientID = self.patientID class ForceSameSeriesInstanceUidInEachDirectory(DICOMPatcherRule): - def __init__(self): - self.requiredTags = ['SeriesInstanceUID'] + def __init__(self): + self.requiredTags = ['SeriesInstanceUID'] - def processStart(self, inputRootDir, outputRootDir): - self.seriesIndex = 0 + def processStart(self, inputRootDir, outputRootDir): + self.seriesIndex = 0 - def processDirectory(self, currentSubDir): - self.firstFileInDirectory = True - self.seriesIndex += 1 + def processDirectory(self, currentSubDir): + self.firstFileInDirectory = True + self.seriesIndex += 1 - def processDataSet(self, ds): - import pydicom - if self.firstFileInDirectory: - # Get seriesInstanceUID for this folder and save it - self.firstFileInDirectory = False - self.seriesInstanceUID = pydicom.uid.generate_uid(None) - # Set the same patient name and ID as the first file in the directory - ds.SeriesInstanceUID = self.seriesInstanceUID + def processDataSet(self, ds): + import pydicom + if self.firstFileInDirectory: + # Get seriesInstanceUID for this folder and save it + self.firstFileInDirectory = False + self.seriesInstanceUID = pydicom.uid.generate_uid(None) + # Set the same patient name and ID as the first file in the directory + ds.SeriesInstanceUID = self.seriesInstanceUID class GenerateMissingIDs(DICOMPatcherRule): - def __init__(self): - self.requiredTags = ['PatientName', 'PatientID', 'StudyInstanceUID', 'SeriesInstanceUID', 'SeriesNumber'] - self.eachFileIsSeparateSeries = False - - def processStart(self, inputRootDir, outputRootDir): - import pydicom - self.patientIDToRandomIDMap = {} - self.studyUIDToRandomUIDMap = {} - self.seriesUIDToRandomUIDMap = {} - self.numberOfSeriesInStudyMap = {} - # All files without a patient ID will be assigned to the same patient - self.randomPatientID = pydicom.uid.generate_uid(None) - - def processDirectory(self, currentSubDir): - import pydicom - # Assume that all files in a directory belongs to the same study - self.randomStudyUID = pydicom.uid.generate_uid(None) - # Assume that all files in a directory belongs to the same series - self.randomSeriesInstanceUID = pydicom.uid.generate_uid(None) - - def processDataSet(self, ds): - import pydicom - - for tag in self.requiredTags: - if not hasattr(ds,tag): - setattr(ds,tag,'') - - # Generate a new SOPInstanceUID to avoid different files having the same SOPInstanceUID - ds.SOPInstanceUID = pydicom.uid.generate_uid(None) - - if ds.PatientName == '': - ds.PatientName = "Unspecified Patient" - if ds.PatientID == '': - ds.PatientID = self.randomPatientID - if ds.StudyInstanceUID == '': - ds.StudyInstanceUID = self.randomStudyUID - if ds.SeriesInstanceUID == '': - if self.eachFileIsSeparateSeries: - ds.SeriesInstanceUID = pydicom.uid.generate_uid(None) - else: - ds.SeriesInstanceUID = self.randomSeriesInstanceUID - - # Generate series number to make it easier to identify a sequence within a study - if ds.SeriesNumber == '': - if ds.StudyInstanceUID not in self.numberOfSeriesInStudyMap: - self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] = 0 - self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] = self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] + 1 - ds.SeriesNumber = self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] + def __init__(self): + self.requiredTags = ['PatientName', 'PatientID', 'StudyInstanceUID', 'SeriesInstanceUID', 'SeriesNumber'] + self.eachFileIsSeparateSeries = False + + def processStart(self, inputRootDir, outputRootDir): + import pydicom + self.patientIDToRandomIDMap = {} + self.studyUIDToRandomUIDMap = {} + self.seriesUIDToRandomUIDMap = {} + self.numberOfSeriesInStudyMap = {} + # All files without a patient ID will be assigned to the same patient + self.randomPatientID = pydicom.uid.generate_uid(None) + + def processDirectory(self, currentSubDir): + import pydicom + # Assume that all files in a directory belongs to the same study + self.randomStudyUID = pydicom.uid.generate_uid(None) + # Assume that all files in a directory belongs to the same series + self.randomSeriesInstanceUID = pydicom.uid.generate_uid(None) + + def processDataSet(self, ds): + import pydicom + + for tag in self.requiredTags: + if not hasattr(ds, tag): + setattr(ds, tag, '') + + # Generate a new SOPInstanceUID to avoid different files having the same SOPInstanceUID + ds.SOPInstanceUID = pydicom.uid.generate_uid(None) + + if ds.PatientName == '': + ds.PatientName = "Unspecified Patient" + if ds.PatientID == '': + ds.PatientID = self.randomPatientID + if ds.StudyInstanceUID == '': + ds.StudyInstanceUID = self.randomStudyUID + if ds.SeriesInstanceUID == '': + if self.eachFileIsSeparateSeries: + ds.SeriesInstanceUID = pydicom.uid.generate_uid(None) + else: + ds.SeriesInstanceUID = self.randomSeriesInstanceUID + + # Generate series number to make it easier to identify a sequence within a study + if ds.SeriesNumber == '': + if ds.StudyInstanceUID not in self.numberOfSeriesInStudyMap: + self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] = 0 + self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] = self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] + 1 + ds.SeriesNumber = self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] # @@ -310,11 +310,11 @@ def processDataSet(self, ds): # class RemoveDICOMDIR(DICOMPatcherRule): - def skipFile(self, filepath): - if os.path.basename(filepath) != 'DICOMDIR': - return False - self.addLog('DICOMDIR file is ignored (its contents may be inconsistent with the contents of the indexed DICOM files, therefore it is safer not to use it)') - return True + def skipFile(self, filepath): + if os.path.basename(filepath) != 'DICOMDIR': + return False + self.addLog('DICOMDIR file is ignored (its contents may be inconsistent with the contents of the indexed DICOM files, therefore it is safer not to use it)') + return True # @@ -322,19 +322,19 @@ def skipFile(self, filepath): # class FixPrivateMediaStorageSOPClassUID(DICOMPatcherRule): - def processDataSet(self, ds): - # DCMTK uses a specific UID for if storage SOP class UID is not specified. - # GDCM refuses to load images with a private SOP class UID, so we change it to CT storage - # (as that is the most commonly used imaging modality). - # We could make things nicer by allowing the user to specify a modality. - DCMTKPrivateMediaStorageSOPClassUID = "1.2.276.0.7230010.3.1.0.1" - CTImageStorageSOPClassUID = "1.2.840.10008.5.1.4.1.1.2" - if not hasattr(ds.file_meta, 'MediaStorageSOPClassUID') or ds.file_meta.MediaStorageSOPClassUID == DCMTKPrivateMediaStorageSOPClassUID: - self.addLog("DCMTK private MediaStorageSOPClassUID found. Replace it with CT media storage SOP class UID.") - ds.file_meta.MediaStorageSOPClassUID = CTImageStorageSOPClassUID + def processDataSet(self, ds): + # DCMTK uses a specific UID for if storage SOP class UID is not specified. + # GDCM refuses to load images with a private SOP class UID, so we change it to CT storage + # (as that is the most commonly used imaging modality). + # We could make things nicer by allowing the user to specify a modality. + DCMTKPrivateMediaStorageSOPClassUID = "1.2.276.0.7230010.3.1.0.1" + CTImageStorageSOPClassUID = "1.2.840.10008.5.1.4.1.1.2" + if not hasattr(ds.file_meta, 'MediaStorageSOPClassUID') or ds.file_meta.MediaStorageSOPClassUID == DCMTKPrivateMediaStorageSOPClassUID: + self.addLog("DCMTK private MediaStorageSOPClassUID found. Replace it with CT media storage SOP class UID.") + ds.file_meta.MediaStorageSOPClassUID = CTImageStorageSOPClassUID - if hasattr(ds, 'SOPClassUID') and ds.SOPClassUID == DCMTKPrivateMediaStorageSOPClassUID: - ds.SOPClassUID = CTImageStorageSOPClassUID + if hasattr(ds, 'SOPClassUID') and ds.SOPClassUID == DCMTKPrivateMediaStorageSOPClassUID: + ds.SOPClassUID = CTImageStorageSOPClassUID # @@ -342,83 +342,83 @@ def processDataSet(self, ds): # class AddMissingSliceSpacingToMultiframe(DICOMPatcherRule): - """Add missing slice spacing info to multiframe files""" - - def processDataSet(self, ds): - import pydicom - - if not hasattr(ds,'NumberOfFrames'): - return - numberOfFrames = ds.NumberOfFrames - if numberOfFrames <= 1: - return - - # Multi-frame sequence, we may need to add slice positions - - # Error in Dolphin 3D CBCT scanners, they store multiple frames but they keep using CTImageStorage as storage class - if ds.SOPClassUID == '1.2.840.10008.5.1.4.1.1.2': # Computed Tomography Image IOD - ds.SOPClassUID = '1.2.840.10008.5.1.4.1.1.2.1' # Enhanced CT Image IOD - - sliceStartPosition = ds.ImagePositionPatient if hasattr(ds,'ImagePositionPatient') else [0,0,0] - sliceAxes = ds.ImageOrientationPatient if hasattr(ds,'ImageOrientationPatient') else [1,0,0,0,1,0] - x = sliceAxes[:3] - y = sliceAxes[3:] - z = [x[1] * y[2] - x[2] * y[1], x[2] * y[0] - x[0] * y[2], x[0] * y[1] - x[1] * y[0]] # cross(x,y) - sliceSpacing = ds.SliceThickness if hasattr(ds,'SliceThickness') else 1.0 - pixelSpacing = ds.PixelSpacing if hasattr(ds,'PixelSpacing') else [1.0, 1.0] - - if not (pydicom.tag.Tag(0x5200,0x9229) in ds): - - # (5200,9229) SQ (Sequence with undefined length #=1) # u/l, 1 SharedFunctionalGroupsSequence - # (0020,9116) SQ (Sequence with undefined length #=1) # u/l, 1 PlaneOrientationSequence - # (0020,0037) DS [1.00000\0.00000\0.00000\0.00000\1.00000\0.00000] # 48, 6 ImageOrientationPatient - # (0028,9110) SQ (Sequence with undefined length #=1) # u/l, 1 PixelMeasuresSequence - # (0018,0050) DS [3.00000] # 8, 1 SliceThickness - # (0028,0030) DS [0.597656\0.597656] # 18, 2 PixelSpacing - - planeOrientationDataSet = pydicom.dataset.Dataset() - planeOrientationDataSet.ImageOrientationPatient = sliceAxes - planeOrientationSequence = pydicom.sequence.Sequence() - planeOrientationSequence.insert(pydicom.tag.Tag(0x0020,0x9116),planeOrientationDataSet) - - pixelMeasuresDataSet = pydicom.dataset.Dataset() - pixelMeasuresDataSet.SliceThickness = sliceSpacing - pixelMeasuresDataSet.PixelSpacing = pixelSpacing - pixelMeasuresSequence = pydicom.sequence.Sequence() - pixelMeasuresSequence.insert(pydicom.tag.Tag(0x0028,0x9110),pixelMeasuresDataSet) - - sharedFunctionalGroupsDataSet = pydicom.dataset.Dataset() - sharedFunctionalGroupsDataSet.PlaneOrientationSequence = planeOrientationSequence - sharedFunctionalGroupsDataSet.PixelMeasuresSequence = pixelMeasuresSequence - sharedFunctionalGroupsSequence = pydicom.sequence.Sequence() - sharedFunctionalGroupsSequence.insert(pydicom.tag.Tag(0x5200,0x9229),sharedFunctionalGroupsDataSet) - ds.SharedFunctionalGroupsSequence = sharedFunctionalGroupsSequence - - if not (pydicom.tag.Tag(0x5200,0x9230) in ds): - - #(5200,9230) SQ (Sequence with undefined length #=54) # u/l, 1 PerFrameFunctionalGroupsSequence - # (0020,9113) SQ (Sequence with undefined length #=1) # u/l, 1 PlanePositionSequence - # (0020,0032) DS [-94.7012\-312.701\-806.500] # 26, 3 ImagePositionPatient - # (0020,9113) SQ (Sequence with undefined length #=1) # u/l, 1 PlanePositionSequence - # (0020,0032) DS [-94.7012\-312.701\-809.500] # 26, 3 ImagePositionPatient - # ... - - perFrameFunctionalGroupsSequence = pydicom.sequence.Sequence() - - for frameIndex in range(numberOfFrames): - planePositionDataSet = pydicom.dataset.Dataset() - slicePosition = [ - sliceStartPosition[0]+frameIndex*z[0]*sliceSpacing, - sliceStartPosition[1]+frameIndex*z[1]*sliceSpacing, - sliceStartPosition[2]+frameIndex*z[2]*sliceSpacing] - planePositionDataSet.ImagePositionPatient = slicePosition - planePositionSequence = pydicom.sequence.Sequence() - planePositionSequence.insert(pydicom.tag.Tag(0x0020,0x9113),planePositionDataSet) - perFrameFunctionalGroupsDataSet = pydicom.dataset.Dataset() - perFrameFunctionalGroupsDataSet.PlanePositionSequence = planePositionSequence - perFrameFunctionalGroupsSequence.insert(pydicom.tag.Tag(0x5200,0x9230),perFrameFunctionalGroupsDataSet) - - ds.PerFrameFunctionalGroupsSequence = perFrameFunctionalGroupsSequence + """Add missing slice spacing info to multiframe files""" + + def processDataSet(self, ds): + import pydicom + + if not hasattr(ds, 'NumberOfFrames'): + return + numberOfFrames = ds.NumberOfFrames + if numberOfFrames <= 1: + return + + # Multi-frame sequence, we may need to add slice positions + + # Error in Dolphin 3D CBCT scanners, they store multiple frames but they keep using CTImageStorage as storage class + if ds.SOPClassUID == '1.2.840.10008.5.1.4.1.1.2': # Computed Tomography Image IOD + ds.SOPClassUID = '1.2.840.10008.5.1.4.1.1.2.1' # Enhanced CT Image IOD + + sliceStartPosition = ds.ImagePositionPatient if hasattr(ds, 'ImagePositionPatient') else [0, 0, 0] + sliceAxes = ds.ImageOrientationPatient if hasattr(ds, 'ImageOrientationPatient') else [1, 0, 0, 0, 1, 0] + x = sliceAxes[:3] + y = sliceAxes[3:] + z = [x[1] * y[2] - x[2] * y[1], x[2] * y[0] - x[0] * y[2], x[0] * y[1] - x[1] * y[0]] # cross(x,y) + sliceSpacing = ds.SliceThickness if hasattr(ds, 'SliceThickness') else 1.0 + pixelSpacing = ds.PixelSpacing if hasattr(ds, 'PixelSpacing') else [1.0, 1.0] + + if not (pydicom.tag.Tag(0x5200, 0x9229) in ds): + + # (5200,9229) SQ (Sequence with undefined length #=1) # u/l, 1 SharedFunctionalGroupsSequence + # (0020,9116) SQ (Sequence with undefined length #=1) # u/l, 1 PlaneOrientationSequence + # (0020,0037) DS [1.00000\0.00000\0.00000\0.00000\1.00000\0.00000] # 48, 6 ImageOrientationPatient + # (0028,9110) SQ (Sequence with undefined length #=1) # u/l, 1 PixelMeasuresSequence + # (0018,0050) DS [3.00000] # 8, 1 SliceThickness + # (0028,0030) DS [0.597656\0.597656] # 18, 2 PixelSpacing + + planeOrientationDataSet = pydicom.dataset.Dataset() + planeOrientationDataSet.ImageOrientationPatient = sliceAxes + planeOrientationSequence = pydicom.sequence.Sequence() + planeOrientationSequence.insert(pydicom.tag.Tag(0x0020, 0x9116), planeOrientationDataSet) + + pixelMeasuresDataSet = pydicom.dataset.Dataset() + pixelMeasuresDataSet.SliceThickness = sliceSpacing + pixelMeasuresDataSet.PixelSpacing = pixelSpacing + pixelMeasuresSequence = pydicom.sequence.Sequence() + pixelMeasuresSequence.insert(pydicom.tag.Tag(0x0028, 0x9110), pixelMeasuresDataSet) + + sharedFunctionalGroupsDataSet = pydicom.dataset.Dataset() + sharedFunctionalGroupsDataSet.PlaneOrientationSequence = planeOrientationSequence + sharedFunctionalGroupsDataSet.PixelMeasuresSequence = pixelMeasuresSequence + sharedFunctionalGroupsSequence = pydicom.sequence.Sequence() + sharedFunctionalGroupsSequence.insert(pydicom.tag.Tag(0x5200, 0x9229), sharedFunctionalGroupsDataSet) + ds.SharedFunctionalGroupsSequence = sharedFunctionalGroupsSequence + + if not (pydicom.tag.Tag(0x5200, 0x9230) in ds): + + # (5200,9230) SQ (Sequence with undefined length #=54) # u/l, 1 PerFrameFunctionalGroupsSequence + # (0020,9113) SQ (Sequence with undefined length #=1) # u/l, 1 PlanePositionSequence + # (0020,0032) DS [-94.7012\-312.701\-806.500] # 26, 3 ImagePositionPatient + # (0020,9113) SQ (Sequence with undefined length #=1) # u/l, 1 PlanePositionSequence + # (0020,0032) DS [-94.7012\-312.701\-809.500] # 26, 3 ImagePositionPatient + # ... + + perFrameFunctionalGroupsSequence = pydicom.sequence.Sequence() + + for frameIndex in range(numberOfFrames): + planePositionDataSet = pydicom.dataset.Dataset() + slicePosition = [ + sliceStartPosition[0] + frameIndex * z[0] * sliceSpacing, + sliceStartPosition[1] + frameIndex * z[1] * sliceSpacing, + sliceStartPosition[2] + frameIndex * z[2] * sliceSpacing] + planePositionDataSet.ImagePositionPatient = slicePosition + planePositionSequence = pydicom.sequence.Sequence() + planePositionSequence.insert(pydicom.tag.Tag(0x0020, 0x9113), planePositionDataSet) + perFrameFunctionalGroupsDataSet = pydicom.dataset.Dataset() + perFrameFunctionalGroupsDataSet.PlanePositionSequence = planePositionSequence + perFrameFunctionalGroupsSequence.insert(pydicom.tag.Tag(0x5200, 0x9230), perFrameFunctionalGroupsDataSet) + + ds.PerFrameFunctionalGroupsSequence = perFrameFunctionalGroupsSequence # @@ -426,49 +426,49 @@ def processDataSet(self, ds): # class Anonymize(DICOMPatcherRule): - def __init__(self): - self.requiredTags = ['PatientName', 'PatientID', 'StudyInstanceUID', 'SeriesInstanceUID', 'SeriesNumber'] - - def processStart(self, inputRootDir, outputRootDir): - import pydicom - self.patientIDToRandomIDMap = {} - self.studyUIDToRandomUIDMap = {} - self.seriesUIDToRandomUIDMap = {} - self.numberOfSeriesInStudyMap = {} - # All files without a patient ID will be assigned to the same patient - self.randomPatientID = pydicom.uid.generate_uid(None) - - def processDirectory(self, currentSubDir): - import pydicom - # Assume that all files in a directory belongs to the same study - self.randomStudyUID = pydicom.uid.generate_uid(None) - # Assume that all files in a directory belongs to the same series - self.randomSeriesInstanceUID = pydicom.uid.generate_uid(None) - - def processDataSet(self, ds): - import pydicom - - ds.StudyDate = '' - ds.StudyTime = '' - ds.ContentDate = '' - ds.ContentTime = '' - ds.AccessionNumber = '' - ds.ReferringPhysiciansName = '' - ds.PatientsBirthDate = '' - ds.PatientsSex = '' - ds.StudyID = '' - ds.PatientName = "Unspecified Patient" - - # replace ids with random values - re-use if we have seen them before - if ds.PatientID not in self.patientIDToRandomIDMap: - self.patientIDToRandomIDMap[ds.PatientID] = pydicom.uid.generate_uid(None) - ds.PatientID = self.patientIDToRandomIDMap[ds.PatientID] - if ds.StudyInstanceUID not in self.studyUIDToRandomUIDMap: - self.studyUIDToRandomUIDMap[ds.StudyInstanceUID] = pydicom.uid.generate_uid(None) - ds.StudyInstanceUID = self.studyUIDToRandomUIDMap[ds.StudyInstanceUID] - if ds.SeriesInstanceUID not in self.seriesUIDToRandomUIDMap: - self.seriesUIDToRandomUIDMap[ds.SeriesInstanceUID] = pydicom.uid.generate_uid(None) - ds.SeriesInstanceUID = self.seriesUIDToRandomUIDMap[ds.SeriesInstanceUID] + def __init__(self): + self.requiredTags = ['PatientName', 'PatientID', 'StudyInstanceUID', 'SeriesInstanceUID', 'SeriesNumber'] + + def processStart(self, inputRootDir, outputRootDir): + import pydicom + self.patientIDToRandomIDMap = {} + self.studyUIDToRandomUIDMap = {} + self.seriesUIDToRandomUIDMap = {} + self.numberOfSeriesInStudyMap = {} + # All files without a patient ID will be assigned to the same patient + self.randomPatientID = pydicom.uid.generate_uid(None) + + def processDirectory(self, currentSubDir): + import pydicom + # Assume that all files in a directory belongs to the same study + self.randomStudyUID = pydicom.uid.generate_uid(None) + # Assume that all files in a directory belongs to the same series + self.randomSeriesInstanceUID = pydicom.uid.generate_uid(None) + + def processDataSet(self, ds): + import pydicom + + ds.StudyDate = '' + ds.StudyTime = '' + ds.ContentDate = '' + ds.ContentTime = '' + ds.AccessionNumber = '' + ds.ReferringPhysiciansName = '' + ds.PatientsBirthDate = '' + ds.PatientsSex = '' + ds.StudyID = '' + ds.PatientName = "Unspecified Patient" + + # replace ids with random values - re-use if we have seen them before + if ds.PatientID not in self.patientIDToRandomIDMap: + self.patientIDToRandomIDMap[ds.PatientID] = pydicom.uid.generate_uid(None) + ds.PatientID = self.patientIDToRandomIDMap[ds.PatientID] + if ds.StudyInstanceUID not in self.studyUIDToRandomUIDMap: + self.studyUIDToRandomUIDMap[ds.StudyInstanceUID] = pydicom.uid.generate_uid(None) + ds.StudyInstanceUID = self.studyUIDToRandomUIDMap[ds.StudyInstanceUID] + if ds.SeriesInstanceUID not in self.seriesUIDToRandomUIDMap: + self.seriesUIDToRandomUIDMap[ds.SeriesInstanceUID] = pydicom.uid.generate_uid(None) + ds.SeriesInstanceUID = self.seriesUIDToRandomUIDMap[ds.SeriesInstanceUID] # @@ -476,35 +476,35 @@ def processDataSet(self, ds): # class NormalizeFileNames(DICOMPatcherRule): - def processStart(self, inputRootDir, outputRootDir): - self.inputRootDir = inputRootDir - self.outputRootDir = outputRootDir - self.patientNameIDToFolderMap = {} - self.studyUIDToFolderMap = {} - self.seriesUIDToFolderMap = {} - # Number of files or folder in the specified folder - self.numberOfItemsInFolderMap = {} - - def getNextItemName(self, prefix, root): - numberOfFilesInFolder = self.numberOfItemsInFolderMap[root] if root in self.numberOfItemsInFolderMap else 0 - self.numberOfItemsInFolderMap[root] = numberOfFilesInFolder+1 - return f"{prefix}{numberOfFilesInFolder:03d}" - - def generateOutputFilePath(self, ds, filepath): - folderName = "" - patientNameID = str(ds.PatientName)+"*"+ds.PatientID - if patientNameID not in self.patientNameIDToFolderMap: - self.patientNameIDToFolderMap[patientNameID] = self.getNextItemName("pa", folderName) - folderName += self.patientNameIDToFolderMap[patientNameID] - if ds.StudyInstanceUID not in self.studyUIDToFolderMap: - self.studyUIDToFolderMap[ds.StudyInstanceUID] = self.getNextItemName("st", folderName) - folderName += "/" + self.studyUIDToFolderMap[ds.StudyInstanceUID] - if ds.SeriesInstanceUID not in self.seriesUIDToFolderMap: - self.seriesUIDToFolderMap[ds.SeriesInstanceUID] = self.getNextItemName("se", folderName) - folderName += "/" +self.seriesUIDToFolderMap[ds.SeriesInstanceUID] - prefix = ds.Modality.lower() if hasattr(ds, 'Modality') else "" - filePath = self.outputRootDir + "/" + folderName + "/" + self.getNextItemName(prefix, folderName)+".dcm" - return filePath + def processStart(self, inputRootDir, outputRootDir): + self.inputRootDir = inputRootDir + self.outputRootDir = outputRootDir + self.patientNameIDToFolderMap = {} + self.studyUIDToFolderMap = {} + self.seriesUIDToFolderMap = {} + # Number of files or folder in the specified folder + self.numberOfItemsInFolderMap = {} + + def getNextItemName(self, prefix, root): + numberOfFilesInFolder = self.numberOfItemsInFolderMap[root] if root in self.numberOfItemsInFolderMap else 0 + self.numberOfItemsInFolderMap[root] = numberOfFilesInFolder + 1 + return f"{prefix}{numberOfFilesInFolder:03d}" + + def generateOutputFilePath(self, ds, filepath): + folderName = "" + patientNameID = str(ds.PatientName) + "*" + ds.PatientID + if patientNameID not in self.patientNameIDToFolderMap: + self.patientNameIDToFolderMap[patientNameID] = self.getNextItemName("pa", folderName) + folderName += self.patientNameIDToFolderMap[patientNameID] + if ds.StudyInstanceUID not in self.studyUIDToFolderMap: + self.studyUIDToFolderMap[ds.StudyInstanceUID] = self.getNextItemName("st", folderName) + folderName += "/" + self.studyUIDToFolderMap[ds.StudyInstanceUID] + if ds.SeriesInstanceUID not in self.seriesUIDToFolderMap: + self.seriesUIDToFolderMap[ds.SeriesInstanceUID] = self.getNextItemName("se", folderName) + folderName += "/" + self.seriesUIDToFolderMap[ds.SeriesInstanceUID] + prefix = ds.Modality.lower() if hasattr(ds, 'Modality') else "" + filePath = self.outputRootDir + "/" + folderName + "/" + self.getNextItemName(prefix, folderName) + ".dcm" + return filePath # @@ -512,119 +512,119 @@ def generateOutputFilePath(self, ds, filepath): # class DICOMPatcherLogic(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): - ScriptedLoadableModuleLogic.__init__(self) - self.logCallback = None - self.patchingRules = [] - - def clearRules(self): - self.patchingRules = [] - - def addRule(self, ruleName): - import importlib - ruleModule = importlib.import_module("DICOMPatcher") - ruleClass = getattr(ruleModule, ruleName) - ruleInstance = ruleClass() - self.patchingRules.append(ruleInstance) - - def addLog(self, text): - logging.info(text) - if self.logCallback: - self.logCallback(text) - - def patchDicomDir(self, inputDirPath, outputDirPath): + """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 """ - Since CTK (rightly) requires certain basic information [1] before it can import - data files that purport to be dicom, this code patches the files in a directory - with some needed fields. - Calling this function with a directory path will make a patched copy of each file. - Importing the old files to CTK should still fail, but the new ones should work. + def __init__(self): + ScriptedLoadableModuleLogic.__init__(self) + self.logCallback = None + self.patchingRules = [] - The directory is assumed to have a set of instances that are all from the - same study of the same patient. Also that each instance (file) is an - independent (multiframe) series. + def clearRules(self): + self.patchingRules = [] - [1] https://github.com/commontk/CTK/blob/16aa09540dcb59c6eafde4d9a88dfee1f0948edc/Libs/DICOM/Core/ctkDICOMDatabase.cpp#L1283-L1287 - """ + def addRule(self, ruleName): + import importlib + ruleModule = importlib.import_module("DICOMPatcher") + ruleClass = getattr(ruleModule, ruleName) + ruleInstance = ruleClass() + self.patchingRules.append(ruleInstance) - import pydicom + def addLog(self, text): + logging.info(text) + if self.logCallback: + self.logCallback(text) - self.addLog('DICOM patching started...') - logging.debug('DICOM patch input directory: '+inputDirPath) - logging.debug('DICOM patch output directory: '+outputDirPath) + def patchDicomDir(self, inputDirPath, outputDirPath): + """ + Since CTK (rightly) requires certain basic information [1] before it can import + data files that purport to be dicom, this code patches the files in a directory + with some needed fields. - for rule in self.patchingRules: - rule.logCallback = self.addLog - rule.processStart(inputDirPath, outputDirPath) + Calling this function with a directory path will make a patched copy of each file. + Importing the old files to CTK should still fail, but the new ones should work. - for root, subFolders, files in os.walk(inputDirPath): + The directory is assumed to have a set of instances that are all from the + same study of the same patient. Also that each instance (file) is an + independent (multiframe) series. - currentSubDir = os.path.relpath(root, inputDirPath) - rootOutput = os.path.join(outputDirPath, currentSubDir) + [1] https://github.com/commontk/CTK/blob/16aa09540dcb59c6eafde4d9a88dfee1f0948edc/Libs/DICOM/Core/ctkDICOMDatabase.cpp#L1283-L1287 + """ - # Notify rules that processing of a new subdirectory started - for rule in self.patchingRules: - rule.processDirectory(currentSubDir) + import pydicom - for file in files: - filePath = os.path.join(root,file) - self.addLog('Examining %s...' % os.path.join(currentSubDir,file)) + self.addLog('DICOM patching started...') + logging.debug('DICOM patch input directory: ' + inputDirPath) + logging.debug('DICOM patch output directory: ' + outputDirPath) - skipFileRequestingRule = None for rule in self.patchingRules: - if rule.skipFile(currentSubDir): - skipFileRequestingRule = rule - break - if skipFileRequestingRule: - self.addLog(' Rule '+rule.__class__.__name__+' requested to skip this file.') - continue + rule.logCallback = self.addLog + rule.processStart(inputDirPath, outputDirPath) - try: - ds = pydicom.read_file(filePath) - except (OSError, pydicom.filereader.InvalidDicomError): - self.addLog(' Not DICOM file. Skipped.') - continue + for root, subFolders, files in os.walk(inputDirPath): - self.addLog(' Patching...') + currentSubDir = os.path.relpath(root, inputDirPath) + rootOutput = os.path.join(outputDirPath, currentSubDir) - for rule in self.patchingRules: - rule.processDataSet(ds) + # Notify rules that processing of a new subdirectory started + for rule in self.patchingRules: + rule.processDirectory(currentSubDir) - patchedFilePath = os.path.abspath(os.path.join(rootOutput,file)) - for rule in self.patchingRules: - patchedFilePath = rule.generateOutputFilePath(ds, patchedFilePath) + for file in files: + filePath = os.path.join(root, file) + self.addLog('Examining %s...' % os.path.join(currentSubDir, file)) - ###################################################### - # Write + skipFileRequestingRule = None + for rule in self.patchingRules: + if rule.skipFile(currentSubDir): + skipFileRequestingRule = rule + break + if skipFileRequestingRule: + self.addLog(' Rule ' + rule.__class__.__name__ + ' requested to skip this file.') + continue - dirName = os.path.dirname(patchedFilePath) - if not os.path.exists(dirName): - os.makedirs(dirName) + try: + ds = pydicom.read_file(filePath) + except (OSError, pydicom.filereader.InvalidDicomError): + self.addLog(' Not DICOM file. Skipped.') + continue - self.addLog(' Writing DICOM...') - pydicom.write_file(patchedFilePath, ds) - self.addLog(' Created DICOM file: %s' % patchedFilePath) + self.addLog(' Patching...') - self.addLog(f'DICOM patching completed. Patched files are written to:\n{outputDirPath}') + for rule in self.patchingRules: + rule.processDataSet(ds) - def importDicomDir(self, outputDirPath): - """ - Utility function to import DICOM files from a directory - """ - self.addLog('Initiate DICOM importing from folder '+outputDirPath) - slicer.util.selectModule('DICOM') - dicomBrowser = slicer.modules.dicom.widgetRepresentation().self().browserWidget.dicomBrowser - dicomBrowser.importDirectory(outputDirPath) + patchedFilePath = os.path.abspath(os.path.join(rootOutput, file)) + for rule in self.patchingRules: + patchedFilePath = rule.generateOutputFilePath(ds, patchedFilePath) + + ###################################################### + # Write + + dirName = os.path.dirname(patchedFilePath) + if not os.path.exists(dirName): + os.makedirs(dirName) + + self.addLog(' Writing DICOM...') + pydicom.write_file(patchedFilePath, ds) + self.addLog(' Created DICOM file: %s' % patchedFilePath) + + self.addLog(f'DICOM patching completed. Patched files are written to:\n{outputDirPath}') + + def importDicomDir(self, outputDirPath): + """ + Utility function to import DICOM files from a directory + """ + self.addLog('Initiate DICOM importing from folder ' + outputDirPath) + slicer.util.selectModule('DICOM') + dicomBrowser = slicer.modules.dicom.widgetRepresentation().self().browserWidget.dicomBrowser + dicomBrowser.importDirectory(outputDirPath) # @@ -632,92 +632,92 @@ def importDicomDir(self, outputDirPath): # class DICOMPatcherTest(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_DICOMPatcher1() - - def test_DICOMPatcher1(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. + 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 """ - import tempfile - testDir = tempfile.mkdtemp(prefix="DICOMPatcherTest-", dir=slicer.app.temporaryPath) - self.assertTrue(os.path.isdir(testDir)) - - inputTestDir = testDir+"/input" - os.makedirs(inputTestDir) - outputTestDir = testDir+"/output" - self.delayDisplay('Created test directory: '+testDir) - - self.delayDisplay("Generate test files") - - testFileNonDICOM = open(inputTestDir+"/NonDICOMFile.txt", "w") - testFileNonDICOM.write("This is not a DICOM file") - testFileNonDICOM.close() - - testFileDICOMFilename = inputTestDir+"/DICOMFile.dcm" - self.delayDisplay('Writing test file: '+testFileDICOMFilename) - import pydicom - file_meta = pydicom.dataset.Dataset() - file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2' # CT Image Storage - file_meta.MediaStorageSOPInstanceUID = "1.2.3" # !! Need valid UID here for real work - file_meta.ImplementationClassUID = "1.2.3.4" # !!! Need valid UIDs here - ds = pydicom.dataset.FileDataset(testFileDICOMFilename, {}, file_meta=file_meta, preamble=b"\0" * 128) - ds.PatientName = "Test^Firstname" - ds.PatientID = "123456" - # Set the transfer syntax - ds.is_little_endian = True - ds.is_implicit_VR = True - ds.save_as(testFileDICOMFilename) - - self.delayDisplay("Patch input files") - - logic = DICOMPatcherLogic() - logic.addRule("GenerateMissingIDs") - logic.addRule("RemoveDICOMDIR") - logic.addRule("FixPrivateMediaStorageSOPClassUID") - logic.addRule("AddMissingSliceSpacingToMultiframe") - logic.addRule("Anonymize") - logic.addRule("NormalizeFileNames") - logic.patchDicomDir(inputTestDir, outputTestDir) - - self.delayDisplay("Verify generated files") - - expectedWalk = [] - expectedWalk.append([['pa000'], [ ]]) - expectedWalk.append([['st000'], [ ]]) - expectedWalk.append([['se000'], [ ]]) - expectedWalk.append([[ ], ['000.dcm']]) - step = 0 - for root, subFolders, files in os.walk(outputTestDir): - self.assertEqual(subFolders, expectedWalk[step][0]) - self.assertEqual(files, expectedWalk[step][1]) - step += 1 - - # TODO: test rule ForceSamePatientNameIdInEachDirectory - - self.delayDisplay("Clean up") - - import shutil - shutil.rmtree(testDir) + 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_DICOMPatcher1() + + def test_DICOMPatcher1(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. + """ + + import tempfile + testDir = tempfile.mkdtemp(prefix="DICOMPatcherTest-", dir=slicer.app.temporaryPath) + self.assertTrue(os.path.isdir(testDir)) + + inputTestDir = testDir + "/input" + os.makedirs(inputTestDir) + outputTestDir = testDir + "/output" + self.delayDisplay('Created test directory: ' + testDir) + + self.delayDisplay("Generate test files") + + testFileNonDICOM = open(inputTestDir + "/NonDICOMFile.txt", "w") + testFileNonDICOM.write("This is not a DICOM file") + testFileNonDICOM.close() + + testFileDICOMFilename = inputTestDir + "/DICOMFile.dcm" + self.delayDisplay('Writing test file: ' + testFileDICOMFilename) + import pydicom + file_meta = pydicom.dataset.Dataset() + file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2' # CT Image Storage + file_meta.MediaStorageSOPInstanceUID = "1.2.3" # !! Need valid UID here for real work + file_meta.ImplementationClassUID = "1.2.3.4" # !!! Need valid UIDs here + ds = pydicom.dataset.FileDataset(testFileDICOMFilename, {}, file_meta=file_meta, preamble=b"\0" * 128) + ds.PatientName = "Test^Firstname" + ds.PatientID = "123456" + # Set the transfer syntax + ds.is_little_endian = True + ds.is_implicit_VR = True + ds.save_as(testFileDICOMFilename) + + self.delayDisplay("Patch input files") + + logic = DICOMPatcherLogic() + logic.addRule("GenerateMissingIDs") + logic.addRule("RemoveDICOMDIR") + logic.addRule("FixPrivateMediaStorageSOPClassUID") + logic.addRule("AddMissingSliceSpacingToMultiframe") + logic.addRule("Anonymize") + logic.addRule("NormalizeFileNames") + logic.patchDicomDir(inputTestDir, outputTestDir) + + self.delayDisplay("Verify generated files") + + expectedWalk = [] + expectedWalk.append([['pa000'], []]) + expectedWalk.append([['st000'], []]) + expectedWalk.append([['se000'], []]) + expectedWalk.append([[], ['000.dcm']]) + step = 0 + for root, subFolders, files in os.walk(outputTestDir): + self.assertEqual(subFolders, expectedWalk[step][0]) + self.assertEqual(files, expectedWalk[step][1]) + step += 1 + + # TODO: test rule ForceSamePatientNameIdInEachDirectory + + self.delayDisplay("Clean up") + + import shutil + shutil.rmtree(testDir) diff --git a/Modules/Scripted/DICOMPlugins/DICOMEnhancedUSVolumePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMEnhancedUSVolumePlugin.py index bfc7dbde9ac..2dd263cf0a2 100644 --- a/Modules/Scripted/DICOMPlugins/DICOMEnhancedUSVolumePlugin.py +++ b/Modules/Scripted/DICOMPlugins/DICOMEnhancedUSVolumePlugin.py @@ -13,171 +13,171 @@ # class DICOMEnhancedUSVolumePluginClass(DICOMPlugin): - """ 3D ultrasound loader plugin. - Limitation: ultrasound calibrated regions are not supported (each calibrated region - would need to be split out to its own volume sequence). - """ - - def __init__(self): - super().__init__() - self.loadType = "Enhanced US volume" - - self.tags['sopClassUID'] = "0008,0016" - self.tags['seriesNumber'] = "0020,0011" - self.tags['seriesDescription'] = "0008,103E" - self.tags['instanceNumber'] = "0020,0013" - self.tags['modality'] = "0008,0060" - self.tags['photometricInterpretation'] = "0028,0004" - - self.detailedLogging = False - - def examine(self,fileLists): - """ Returns a list of DICOMLoadable instances - corresponding to ways of interpreting the - fileLists parameter. + """ 3D ultrasound loader plugin. + Limitation: ultrasound calibrated regions are not supported (each calibrated region + would need to be split out to its own volume sequence). """ - loadables = [] - for files in fileLists: - loadables += self.examineFiles(files) - return loadables - - def examineFiles(self,files): - """ Returns a list of DICOMLoadable instances - corresponding to ways of interpreting the - files parameter. - """ - - self.detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool) - - supportedSOPClassUIDs = [ - '1.2.840.10008.5.1.4.1.1.6.2', # Enhanced US Volume Storage - ] - - # The only sample data set that we received from GE LOGIQE10 (software version R1.5.1). - # It added all volumes into a single series, even though they were acquired minutes apart. - # Therefore, instead of loading the volumes into a sequence, we load each as a separate volume. - - loadables = [] - - for filePath in files: - # Quick check of SOP class UID without parsing the file... - sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID']) - if not (sopClassUID in supportedSOPClassUIDs): - # Unsupported class - continue - - instanceNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['instanceNumber']) - modality = slicer.dicomDatabase.fileValue(filePath, self.tags['modality']) - seriesNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesNumber']) - seriesDescription = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesDescription']) - photometricInterpretation = slicer.dicomDatabase.fileValue(filePath, self.tags['photometricInterpretation']) - name = '' - if seriesNumber: - name = f'{seriesNumber}:' - if modality: - name = f'{name} {modality}' - if seriesDescription: - name = f'{name} {seriesDescription}' - else: - name = f'{name} volume' - if instanceNumber: - name = f'{name} [{instanceNumber}]' - - loadable = DICOMLoadable() - loadable.singleSequence = False # put each instance in a separate sequence - loadable.files = [filePath] - loadable.name = name.strip() # remove leading and trailing spaces, if any - loadable.warning = "Loading of this image type is experimental. Please verify image geometry and report any problem is found." - loadable.tooltip = f"Ultrasound volume" - loadable.selected = True - # Confidence is slightly larger than default scalar volume plugin's (0.5) - # and DICOMVolumeSequencePlugin (0.7) - # but still leaving room for more specialized plugins. - loadable.confidence = 0.8 - loadable.grayscale = ('MONOCHROME' in photometricInterpretation) - loadables.append(loadable) - - return loadables - - def load(self,loadable): - """Load the selection - """ - - if loadable.grayscale: - volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", loadable.name) - else: - volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode", loadable.name) - - import vtkITK - if loadable.grayscale: - reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader() - else: - reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile() - filePath = loadable.files[0] - reader.SetArchetype(filePath) - reader.AddFileName(filePath) - reader.SetSingleFile(True) - reader.SetOutputScalarTypeToNative() - reader.SetDesiredCoordinateOrientationToNative() - reader.SetUseNativeOriginOn() - # GDCM is not particularly better in this than DCMTK, we just select one explicitly - # so that we know which one is used - reader.SetDICOMImageIOApproachToGDCM() - reader.Update() - if reader.GetErrorCode() != vtk.vtkErrorCode.NoError: - errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode()) - raise ValueError( - f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}") - - rasToIjk = reader.GetRasToIjkMatrix() - ijkToRas = vtk.vtkMatrix4x4() - vtk.vtkMatrix4x4.Invert(rasToIjk, ijkToRas) - - imageData = reader.GetOutput() - imageData.SetSpacing(1.0, 1.0, 1.0) - imageData.SetOrigin(0.0, 0.0, 0.0) - volumeNode.SetIJKToRASMatrix(ijkToRas) - volumeNode.SetAndObserveImageData(imageData) - - # show volume - appLogic = slicer.app.applicationLogic() - selNode = appLogic.GetSelectionNode() - selNode.SetActiveVolumeID(volumeNode.GetID()) - appLogic.PropagateVolumeSelection() - - return volumeNode + def __init__(self): + super().__init__() + self.loadType = "Enhanced US volume" + + self.tags['sopClassUID'] = "0008,0016" + self.tags['seriesNumber'] = "0020,0011" + self.tags['seriesDescription'] = "0008,103E" + self.tags['instanceNumber'] = "0020,0013" + self.tags['modality'] = "0008,0060" + self.tags['photometricInterpretation'] = "0028,0004" + + self.detailedLogging = False + + def examine(self, fileLists): + """ Returns a list of DICOMLoadable instances + corresponding to ways of interpreting the + fileLists parameter. + """ + loadables = [] + for files in fileLists: + loadables += self.examineFiles(files) + + return loadables + + def examineFiles(self, files): + """ Returns a list of DICOMLoadable instances + corresponding to ways of interpreting the + files parameter. + """ + + self.detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool) + + supportedSOPClassUIDs = [ + '1.2.840.10008.5.1.4.1.1.6.2', # Enhanced US Volume Storage + ] + + # The only sample data set that we received from GE LOGIQE10 (software version R1.5.1). + # It added all volumes into a single series, even though they were acquired minutes apart. + # Therefore, instead of loading the volumes into a sequence, we load each as a separate volume. + + loadables = [] + + for filePath in files: + # Quick check of SOP class UID without parsing the file... + sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID']) + if not (sopClassUID in supportedSOPClassUIDs): + # Unsupported class + continue + + instanceNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['instanceNumber']) + modality = slicer.dicomDatabase.fileValue(filePath, self.tags['modality']) + seriesNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesNumber']) + seriesDescription = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesDescription']) + photometricInterpretation = slicer.dicomDatabase.fileValue(filePath, self.tags['photometricInterpretation']) + name = '' + if seriesNumber: + name = f'{seriesNumber}:' + if modality: + name = f'{name} {modality}' + if seriesDescription: + name = f'{name} {seriesDescription}' + else: + name = f'{name} volume' + if instanceNumber: + name = f'{name} [{instanceNumber}]' + + loadable = DICOMLoadable() + loadable.singleSequence = False # put each instance in a separate sequence + loadable.files = [filePath] + loadable.name = name.strip() # remove leading and trailing spaces, if any + loadable.warning = "Loading of this image type is experimental. Please verify image geometry and report any problem is found." + loadable.tooltip = f"Ultrasound volume" + loadable.selected = True + # Confidence is slightly larger than default scalar volume plugin's (0.5) + # and DICOMVolumeSequencePlugin (0.7) + # but still leaving room for more specialized plugins. + loadable.confidence = 0.8 + loadable.grayscale = ('MONOCHROME' in photometricInterpretation) + loadables.append(loadable) + + return loadables + + def load(self, loadable): + """Load the selection + """ + + if loadable.grayscale: + volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", loadable.name) + else: + volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode", loadable.name) + + import vtkITK + if loadable.grayscale: + reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader() + else: + reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile() + filePath = loadable.files[0] + reader.SetArchetype(filePath) + reader.AddFileName(filePath) + reader.SetSingleFile(True) + reader.SetOutputScalarTypeToNative() + reader.SetDesiredCoordinateOrientationToNative() + reader.SetUseNativeOriginOn() + # GDCM is not particularly better in this than DCMTK, we just select one explicitly + # so that we know which one is used + reader.SetDICOMImageIOApproachToGDCM() + reader.Update() + if reader.GetErrorCode() != vtk.vtkErrorCode.NoError: + errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode()) + raise ValueError( + f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}") + + rasToIjk = reader.GetRasToIjkMatrix() + ijkToRas = vtk.vtkMatrix4x4() + vtk.vtkMatrix4x4.Invert(rasToIjk, ijkToRas) + + imageData = reader.GetOutput() + imageData.SetSpacing(1.0, 1.0, 1.0) + imageData.SetOrigin(0.0, 0.0, 0.0) + volumeNode.SetIJKToRASMatrix(ijkToRas) + volumeNode.SetAndObserveImageData(imageData) + + # show volume + appLogic = slicer.app.applicationLogic() + selNode = appLogic.GetSelectionNode() + selNode.SetActiveVolumeID(volumeNode.GetID()) + appLogic.PropagateVolumeSelection() + + return volumeNode # # DICOMEnhancedUSVolumePlugin # class DICOMEnhancedUSVolumePlugin: - """ - 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 Enhanced US volume Plugin" - parent.categories = ["Developer Tools.DICOM Plugins"] - parent.contributors = ["Andras Lasso (PerkLab)"] - parent.helpText = """ + """ + 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 Enhanced US volume Plugin" + parent.categories = ["Developer Tools.DICOM Plugins"] + parent.contributors = ["Andras Lasso (PerkLab)"] + parent.helpText = """ Plugin to the DICOM Module to parse and load 3D enhanced US volumes. No module interface here, only in the DICOM module. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ The file was originally developed by Andras Lasso (PerkLab). """ - # don't show this module - it only appears in the DICOM module - parent.hidden = True - - # 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['DICOMEnhancedUSVolumePlugin'] = DICOMEnhancedUSVolumePluginClass + # don't show this module - it only appears in the DICOM module + parent.hidden = True + + # 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['DICOMEnhancedUSVolumePlugin'] = DICOMEnhancedUSVolumePluginClass diff --git a/Modules/Scripted/DICOMPlugins/DICOMGeAbusPlugin.py b/Modules/Scripted/DICOMPlugins/DICOMGeAbusPlugin.py index df36f9b8333..45fcd5ae411 100644 --- a/Modules/Scripted/DICOMPlugins/DICOMGeAbusPlugin.py +++ b/Modules/Scripted/DICOMPlugins/DICOMGeAbusPlugin.py @@ -19,286 +19,286 @@ # class DICOMGeAbusPluginClass(DICOMPlugin): - """ Image loader plugin for GE Invenia - ABUS (automated breast ultrasound) images. - """ - - def __init__(self): - super().__init__() - self.loadType = "GE ABUS" - - self.tags['sopClassUID'] = "0008,0016" - self.tags['seriesNumber'] = "0020,0011" - self.tags['seriesDescription'] = "0008,103E" - self.tags['instanceNumber'] = "0020,0013" - self.tags['manufacturerModelName'] = "0008,1090" - - # Accepted private creator identifications - self.privateCreators = ["U-Systems", "General Electric Company 01"] - - def examine(self,fileLists): - """ Returns a list of DICOMLoadable instances - corresponding to ways of interpreting the - fileLists parameter. + """ Image loader plugin for GE Invenia + ABUS (automated breast ultrasound) images. """ - loadables = [] - for files in fileLists: - loadables += self.examineFiles(files) - return loadables - - def examineFiles(self,files): - """ Returns a list of DICOMLoadable instances - corresponding to ways of interpreting the - files parameter. - """ - - detailedLogging = self.isDetailedLogging() - - supportedSOPClassUIDs = [ - '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage - ] - - loadables = [] - - for filePath in files: - # Quick check of SOP class UID without parsing the file... - try: - sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID']) - if not (sopClassUID in supportedSOPClassUIDs): - # Unsupported class - continue - - manufacturerModelName = slicer.dicomDatabase.fileValue(filePath,self.tags['manufacturerModelName']) - if manufacturerModelName != "Invenia": - if detailedLogging: - logging.debug("ManufacturerModelName is not Invenia, the series will not be considered as an ABUS image") - continue - - except Exception as e: - # Quick check could not be completed (probably Slicer DICOM database is not initialized). - # No problem, we'll try to parse the file and check the SOP class UID then. - pass - - try: - ds = dicom.read_file(filePath, stop_before_pixels=True) - except Exception as e: - logging.debug(f"Failed to parse DICOM file: {str(e)}") - continue - - # check if probeCurvatureRadius is available - probeCurvatureRadiusFound = False - for privateCreator in self.privateCreators: - if self.findPrivateTag(ds, 0x0021, 0x40, privateCreator): - probeCurvatureRadiusFound = True - break - - if not probeCurvatureRadiusFound: - if detailedLogging: - logging.debug("Probe curvature radius is not found, the series will not be considered as an ABUS image") - continue - - name = '' - if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber: - name = f'{ds.SeriesNumber}:' - if hasattr(ds, 'Modality') and ds.Modality: - name = f'{name} {ds.Modality}' - if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription: - name = f'{name} {ds.SeriesDescription}' - if hasattr(ds, 'InstanceNumber') and ds.InstanceNumber: - name = f'{name} [{ds.InstanceNumber}]' - - loadable = DICOMLoadable() - loadable.files = [filePath] - loadable.name = name.strip() # remove leading and trailing spaces, if any - loadable.tooltip = "GE Invenia ABUS" - loadable.warning = "Loading of this image type is experimental. Please verify image size and orientation and report any problem is found." - loadable.selected = True - loadable.confidence = 0.9 # this has to be higher than 0.7 (ultrasound sequence) - - # Add to loadables list - loadables.append(loadable) - - return loadables - - def getMetadata(self, filePath): - try: - ds = dicom.read_file(filePath, stop_before_pixels=True) - except Exception as e: - raise ValueError(f"Failed to parse DICOM file: {str(e)}") - - fieldsInfo = { - 'NipplePosition': { 'group': 0x0021, 'element': 0x20, 'private': True, 'required': False}, - 'FirstElementPosition': { 'group': 0x0021, 'element': 0x21, 'private': True, 'required': False}, - 'CurvatureRadiusProbe': { 'group': 0x0021, 'element': 0x40, 'private': True, 'required': True}, - 'CurvatureRadiusTrack': { 'group': 0x0021, 'element': 0x41, 'private': True, 'required': True}, - 'LineDensity': { 'group': 0x0021, 'element': 0x62, 'private': True, 'required': False}, - 'ScanDepthCm': { 'group': 0x0021, 'element': 0x63, 'private': True, 'required': True}, - 'SpacingBetweenSlices': { 'group': 0x0018, 'element': 0x0088, 'private': False, 'required': True}, - 'PixelSpacing': { 'group': 0x0028, 'element': 0x0030, 'private': False, 'required': True}, - } - - fieldValues = {} - for fieldName in fieldsInfo: - fieldInfo = fieldsInfo[fieldName] - if fieldInfo['private']: - for privateCreator in self.privateCreators: - tag = self.findPrivateTag(ds, fieldInfo['group'], fieldInfo['element'], privateCreator) - if tag: - break - else: - tag = dicom.tag.Tag(fieldInfo['group'], fieldInfo['element']) - if tag: - fieldValues[fieldName] = ds[tag].value - - # Make sure all mandatory fields are found - for fieldName in fieldsInfo: - fieldInfo = fieldsInfo[fieldName] - if not fieldInfo['required']: - continue - if not fieldName in fieldValues: - raise ValueError(f"Mandatory field {fieldName} was not found") - - return fieldValues - - def load(self,loadable): - """Load the selection - """ - - filePath = loadable.files[0] - metadata = self.getMetadata(filePath) - - import vtkITK - reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader() - reader.SetArchetype(filePath) - reader.AddFileName(filePath) - reader.SetSingleFile(True) - reader.SetOutputScalarTypeToNative() - reader.SetDesiredCoordinateOrientationToNative() - reader.SetUseNativeOriginOn() - # GDCM is not particularly better in this than DCMTK, we just select one explicitly - # so that we know which one is used - reader.SetDICOMImageIOApproachToGDCM() - reader.Update() - imageData = reader.GetOutput() - - if reader.GetErrorCode() != vtk.vtkErrorCode.NoError: - errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode()) - raise ValueError( - f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}") - - # Image origin and spacing is stored in IJK to RAS matrix - imageData.SetSpacing(1.0, 1.0, 1.0) - imageData.SetOrigin(0.0, 0.0, 0.0) - - volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", slicer.mrmlScene.GenerateUniqueName(loadable.name)) - - # I axis: scanline index (lateralSpacing) - # J axis: sound propagation (axialSpacing) - # K axis: slice (sliceSpacing) - lateralSpacing = metadata['PixelSpacing'][1] - axialSpacing = metadata['PixelSpacing'][0] - sliceSpacing = metadata['SpacingBetweenSlices'] - - ijkToRas = vtk.vtkMatrix4x4() - ijkToRas.SetElement(0,0,-1) - ijkToRas.SetElement(1,1,-1) # so that J axis points toward posterior - volumeNode.SetIJKToRASMatrix(ijkToRas) - volumeNode.SetSpacing(lateralSpacing, axialSpacing, sliceSpacing) - extent = imageData.GetExtent() - volumeNode.SetOrigin((extent[1]-extent[0]+1)*0.5*lateralSpacing, 0, -(extent[5]-extent[2]+1)*0.5*sliceSpacing) - volumeNode.SetAndObserveImageData(imageData) - - # Apply scan conversion transform - acquisitionTransform = self.createAcquisitionTransform(volumeNode, metadata) - volumeNode.SetAndObserveTransformNodeID(acquisitionTransform.GetID()) - - # Create Subject hierarchy nodes for the loaded series - self.addSeriesInSubjectHierarchy(loadable, volumeNode) - - # Place transform in the same subject hierarchy folder as the volume node - shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - volumeParentItemId = shNode.GetItemParent(shNode.GetItemByDataNode(volumeNode)) - shNode.SetItemParent(shNode.GetItemByDataNode(acquisitionTransform), volumeParentItemId) - - # Show in slice views - selectionNode = slicer.app.applicationLogic().GetSelectionNode() - selectionNode.SetReferenceActiveVolumeID(volumeNode.GetID()) - slicer.app.applicationLogic().PropagateVolumeSelection(1) - - return volumeNode - - def createAcquisitionTransform(self, volumeNode, metadata): - - # Creates transform that applies scan conversion transform - probeRadius = metadata['CurvatureRadiusProbe'] - trackRadius = metadata['CurvatureRadiusTrack'] - if trackRadius != 0.0: - raise ValueError(f"Curvature Radius (Track) is {trackRadius}. Currently, only volume with zero radius can be imported.") - - # Create a sampling grid for the transform - import numpy as np - spacing = np.array(volumeNode.GetSpacing()) - averageSpacing = (spacing[0]+spacing[1]+spacing[2])/3.0 - voxelsPerTransformControlPoint = 20 # the transform is changing smoothly, so we don't need to add too many control points - gridSpacingMm = averageSpacing * voxelsPerTransformControlPoint - gridSpacingVoxel = np.floor(gridSpacingMm/spacing).astype(int) - gridAxesIJK = [] - imageData = volumeNode.GetImageData() - extent = imageData.GetExtent() - for axis in range(3): - gridAxesIJK.append(list(range(extent[axis*2], extent[axis*2+1]+gridSpacingVoxel[axis], gridSpacingVoxel[axis]))) - samplingPoints_shape = [len(gridAxesIJK[0]),len(gridAxesIJK[1]),len(gridAxesIJK[2]),3] - - # create a grid transform with one vector at the corner of each slice - # the transform is in the same space and orientation as the volume node - import vtk - gridImage = vtk.vtkImageData() - gridImage.SetOrigin(*volumeNode.GetOrigin()) - gridImage.SetDimensions(samplingPoints_shape[:3]) - gridImage.SetSpacing(gridSpacingVoxel[0]*spacing[0], gridSpacingVoxel[1]*spacing[1], gridSpacingVoxel[2]*spacing[2]) - gridImage.AllocateScalars(vtk.VTK_DOUBLE, 3) - transform = slicer.vtkOrientedGridTransform() - directionMatrix = vtk.vtkMatrix4x4() - volumeNode.GetIJKToRASDirectionMatrix(directionMatrix) - transform.SetGridDirectionMatrix(directionMatrix) - transform.SetDisplacementGridData(gridImage) - - # create the grid transform node - gridTransform = slicer.vtkMRMLGridTransformNode() - gridTransform.SetName(slicer.mrmlScene.GenerateUniqueName(volumeNode.GetName()+' acquisition transform')) - slicer.mrmlScene.AddNode(gridTransform) - gridTransform.SetAndObserveTransformToParent(transform) - - # populate the grid so that each corner of each slice - # is mapped from the source corner to the target corner - - nshape = tuple(reversed(gridImage.GetDimensions())) - nshape = nshape + (3,) - displacements = vtk.util.numpy_support.vtk_to_numpy(gridImage.GetPointData().GetScalars()).reshape(nshape) - - # Get displacements - from math import sin, cos - ijkToRas = vtk.vtkMatrix4x4() - volumeNode.GetIJKToRASMatrix(ijkToRas) - spacing = volumeNode.GetSpacing() - center_IJK = [(extent[0]+extent[1])/2.0, extent[2], (extent[4]+extent[5])/2.0] - sourcePoints_RAS = numpy.zeros(shape=samplingPoints_shape) - targetPoints_RAS = numpy.zeros(shape=samplingPoints_shape) - for k in range(samplingPoints_shape[2]): - for j in range(samplingPoints_shape[1]): - for i in range(samplingPoints_shape[0]): - samplingPoint_IJK = [gridAxesIJK[0][i], gridAxesIJK[1][j], gridAxesIJK[2][k], 1] - sourcePoint_RAS = np.array(ijkToRas.MultiplyPoint(samplingPoint_IJK)[:3]) - radius = probeRadius - (samplingPoint_IJK[1]-center_IJK[1]) * spacing[1] - angleRad = (samplingPoint_IJK[0]-center_IJK[0]) * spacing[0] / probeRadius - targetPoint_RAS = np.array([ - -radius * sin(angleRad), - radius * cos(angleRad) - probeRadius, - spacing[2] * (samplingPoint_IJK[2]-center_IJK[2])]) - displacements[k][j][i] = targetPoint_RAS - sourcePoint_RAS - - return gridTransform + def __init__(self): + super().__init__() + self.loadType = "GE ABUS" + + self.tags['sopClassUID'] = "0008,0016" + self.tags['seriesNumber'] = "0020,0011" + self.tags['seriesDescription'] = "0008,103E" + self.tags['instanceNumber'] = "0020,0013" + self.tags['manufacturerModelName'] = "0008,1090" + + # Accepted private creator identifications + self.privateCreators = ["U-Systems", "General Electric Company 01"] + + def examine(self, fileLists): + """ Returns a list of DICOMLoadable instances + corresponding to ways of interpreting the + fileLists parameter. + """ + loadables = [] + for files in fileLists: + loadables += self.examineFiles(files) + + return loadables + + def examineFiles(self, files): + """ Returns a list of DICOMLoadable instances + corresponding to ways of interpreting the + files parameter. + """ + + detailedLogging = self.isDetailedLogging() + + supportedSOPClassUIDs = [ + '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage + ] + + loadables = [] + + for filePath in files: + # Quick check of SOP class UID without parsing the file... + try: + sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID']) + if not (sopClassUID in supportedSOPClassUIDs): + # Unsupported class + continue + + manufacturerModelName = slicer.dicomDatabase.fileValue(filePath, self.tags['manufacturerModelName']) + if manufacturerModelName != "Invenia": + if detailedLogging: + logging.debug("ManufacturerModelName is not Invenia, the series will not be considered as an ABUS image") + continue + + except Exception as e: + # Quick check could not be completed (probably Slicer DICOM database is not initialized). + # No problem, we'll try to parse the file and check the SOP class UID then. + pass + + try: + ds = dicom.read_file(filePath, stop_before_pixels=True) + except Exception as e: + logging.debug(f"Failed to parse DICOM file: {str(e)}") + continue + + # check if probeCurvatureRadius is available + probeCurvatureRadiusFound = False + for privateCreator in self.privateCreators: + if self.findPrivateTag(ds, 0x0021, 0x40, privateCreator): + probeCurvatureRadiusFound = True + break + + if not probeCurvatureRadiusFound: + if detailedLogging: + logging.debug("Probe curvature radius is not found, the series will not be considered as an ABUS image") + continue + + name = '' + if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber: + name = f'{ds.SeriesNumber}:' + if hasattr(ds, 'Modality') and ds.Modality: + name = f'{name} {ds.Modality}' + if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription: + name = f'{name} {ds.SeriesDescription}' + if hasattr(ds, 'InstanceNumber') and ds.InstanceNumber: + name = f'{name} [{ds.InstanceNumber}]' + + loadable = DICOMLoadable() + loadable.files = [filePath] + loadable.name = name.strip() # remove leading and trailing spaces, if any + loadable.tooltip = "GE Invenia ABUS" + loadable.warning = "Loading of this image type is experimental. Please verify image size and orientation and report any problem is found." + loadable.selected = True + loadable.confidence = 0.9 # this has to be higher than 0.7 (ultrasound sequence) + + # Add to loadables list + loadables.append(loadable) + + return loadables + + def getMetadata(self, filePath): + try: + ds = dicom.read_file(filePath, stop_before_pixels=True) + except Exception as e: + raise ValueError(f"Failed to parse DICOM file: {str(e)}") + + fieldsInfo = { + 'NipplePosition': {'group': 0x0021, 'element': 0x20, 'private': True, 'required': False}, + 'FirstElementPosition': {'group': 0x0021, 'element': 0x21, 'private': True, 'required': False}, + 'CurvatureRadiusProbe': {'group': 0x0021, 'element': 0x40, 'private': True, 'required': True}, + 'CurvatureRadiusTrack': {'group': 0x0021, 'element': 0x41, 'private': True, 'required': True}, + 'LineDensity': {'group': 0x0021, 'element': 0x62, 'private': True, 'required': False}, + 'ScanDepthCm': {'group': 0x0021, 'element': 0x63, 'private': True, 'required': True}, + 'SpacingBetweenSlices': {'group': 0x0018, 'element': 0x0088, 'private': False, 'required': True}, + 'PixelSpacing': {'group': 0x0028, 'element': 0x0030, 'private': False, 'required': True}, + } + + fieldValues = {} + for fieldName in fieldsInfo: + fieldInfo = fieldsInfo[fieldName] + if fieldInfo['private']: + for privateCreator in self.privateCreators: + tag = self.findPrivateTag(ds, fieldInfo['group'], fieldInfo['element'], privateCreator) + if tag: + break + else: + tag = dicom.tag.Tag(fieldInfo['group'], fieldInfo['element']) + if tag: + fieldValues[fieldName] = ds[tag].value + + # Make sure all mandatory fields are found + for fieldName in fieldsInfo: + fieldInfo = fieldsInfo[fieldName] + if not fieldInfo['required']: + continue + if fieldName not in fieldValues: + raise ValueError(f"Mandatory field {fieldName} was not found") + + return fieldValues + + def load(self, loadable): + """Load the selection + """ + + filePath = loadable.files[0] + metadata = self.getMetadata(filePath) + + import vtkITK + reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader() + reader.SetArchetype(filePath) + reader.AddFileName(filePath) + reader.SetSingleFile(True) + reader.SetOutputScalarTypeToNative() + reader.SetDesiredCoordinateOrientationToNative() + reader.SetUseNativeOriginOn() + # GDCM is not particularly better in this than DCMTK, we just select one explicitly + # so that we know which one is used + reader.SetDICOMImageIOApproachToGDCM() + reader.Update() + imageData = reader.GetOutput() + + if reader.GetErrorCode() != vtk.vtkErrorCode.NoError: + errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode()) + raise ValueError( + f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}") + + # Image origin and spacing is stored in IJK to RAS matrix + imageData.SetSpacing(1.0, 1.0, 1.0) + imageData.SetOrigin(0.0, 0.0, 0.0) + + volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", slicer.mrmlScene.GenerateUniqueName(loadable.name)) + + # I axis: scanline index (lateralSpacing) + # J axis: sound propagation (axialSpacing) + # K axis: slice (sliceSpacing) + lateralSpacing = metadata['PixelSpacing'][1] + axialSpacing = metadata['PixelSpacing'][0] + sliceSpacing = metadata['SpacingBetweenSlices'] + + ijkToRas = vtk.vtkMatrix4x4() + ijkToRas.SetElement(0, 0, -1) + ijkToRas.SetElement(1, 1, -1) # so that J axis points toward posterior + volumeNode.SetIJKToRASMatrix(ijkToRas) + volumeNode.SetSpacing(lateralSpacing, axialSpacing, sliceSpacing) + extent = imageData.GetExtent() + volumeNode.SetOrigin((extent[1] - extent[0] + 1) * 0.5 * lateralSpacing, 0, -(extent[5] - extent[2] + 1) * 0.5 * sliceSpacing) + volumeNode.SetAndObserveImageData(imageData) + + # Apply scan conversion transform + acquisitionTransform = self.createAcquisitionTransform(volumeNode, metadata) + volumeNode.SetAndObserveTransformNodeID(acquisitionTransform.GetID()) + + # Create Subject hierarchy nodes for the loaded series + self.addSeriesInSubjectHierarchy(loadable, volumeNode) + + # Place transform in the same subject hierarchy folder as the volume node + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + volumeParentItemId = shNode.GetItemParent(shNode.GetItemByDataNode(volumeNode)) + shNode.SetItemParent(shNode.GetItemByDataNode(acquisitionTransform), volumeParentItemId) + + # Show in slice views + selectionNode = slicer.app.applicationLogic().GetSelectionNode() + selectionNode.SetReferenceActiveVolumeID(volumeNode.GetID()) + slicer.app.applicationLogic().PropagateVolumeSelection(1) + + return volumeNode + + def createAcquisitionTransform(self, volumeNode, metadata): + + # Creates transform that applies scan conversion transform + probeRadius = metadata['CurvatureRadiusProbe'] + trackRadius = metadata['CurvatureRadiusTrack'] + if trackRadius != 0.0: + raise ValueError(f"Curvature Radius (Track) is {trackRadius}. Currently, only volume with zero radius can be imported.") + + # Create a sampling grid for the transform + import numpy as np + spacing = np.array(volumeNode.GetSpacing()) + averageSpacing = (spacing[0] + spacing[1] + spacing[2]) / 3.0 + voxelsPerTransformControlPoint = 20 # the transform is changing smoothly, so we don't need to add too many control points + gridSpacingMm = averageSpacing * voxelsPerTransformControlPoint + gridSpacingVoxel = np.floor(gridSpacingMm / spacing).astype(int) + gridAxesIJK = [] + imageData = volumeNode.GetImageData() + extent = imageData.GetExtent() + for axis in range(3): + gridAxesIJK.append(list(range(extent[axis * 2], extent[axis * 2 + 1] + gridSpacingVoxel[axis], gridSpacingVoxel[axis]))) + samplingPoints_shape = [len(gridAxesIJK[0]), len(gridAxesIJK[1]), len(gridAxesIJK[2]), 3] + + # create a grid transform with one vector at the corner of each slice + # the transform is in the same space and orientation as the volume node + import vtk + gridImage = vtk.vtkImageData() + gridImage.SetOrigin(*volumeNode.GetOrigin()) + gridImage.SetDimensions(samplingPoints_shape[:3]) + gridImage.SetSpacing(gridSpacingVoxel[0] * spacing[0], gridSpacingVoxel[1] * spacing[1], gridSpacingVoxel[2] * spacing[2]) + gridImage.AllocateScalars(vtk.VTK_DOUBLE, 3) + transform = slicer.vtkOrientedGridTransform() + directionMatrix = vtk.vtkMatrix4x4() + volumeNode.GetIJKToRASDirectionMatrix(directionMatrix) + transform.SetGridDirectionMatrix(directionMatrix) + transform.SetDisplacementGridData(gridImage) + + # create the grid transform node + gridTransform = slicer.vtkMRMLGridTransformNode() + gridTransform.SetName(slicer.mrmlScene.GenerateUniqueName(volumeNode.GetName() + ' acquisition transform')) + slicer.mrmlScene.AddNode(gridTransform) + gridTransform.SetAndObserveTransformToParent(transform) + + # populate the grid so that each corner of each slice + # is mapped from the source corner to the target corner + + nshape = tuple(reversed(gridImage.GetDimensions())) + nshape = nshape + (3,) + displacements = vtk.util.numpy_support.vtk_to_numpy(gridImage.GetPointData().GetScalars()).reshape(nshape) + + # Get displacements + from math import sin, cos + ijkToRas = vtk.vtkMatrix4x4() + volumeNode.GetIJKToRASMatrix(ijkToRas) + spacing = volumeNode.GetSpacing() + center_IJK = [(extent[0] + extent[1]) / 2.0, extent[2], (extent[4] + extent[5]) / 2.0] + sourcePoints_RAS = numpy.zeros(shape=samplingPoints_shape) + targetPoints_RAS = numpy.zeros(shape=samplingPoints_shape) + for k in range(samplingPoints_shape[2]): + for j in range(samplingPoints_shape[1]): + for i in range(samplingPoints_shape[0]): + samplingPoint_IJK = [gridAxesIJK[0][i], gridAxesIJK[1][j], gridAxesIJK[2][k], 1] + sourcePoint_RAS = np.array(ijkToRas.MultiplyPoint(samplingPoint_IJK)[:3]) + radius = probeRadius - (samplingPoint_IJK[1] - center_IJK[1]) * spacing[1] + angleRad = (samplingPoint_IJK[0] - center_IJK[0]) * spacing[0] / probeRadius + targetPoint_RAS = np.array([ + -radius * sin(angleRad), + radius * cos(angleRad) - probeRadius, + spacing[2] * (samplingPoint_IJK[2] - center_IJK[2])]) + displacements[k][j][i] = targetPoint_RAS - sourcePoint_RAS + + return gridTransform # @@ -306,31 +306,31 @@ def createAcquisitionTransform(self, volumeNode, metadata): # class DICOMGeAbusPlugin: - """ - 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 GE ABUS Import Plugin" - parent.categories = ["Developer Tools.DICOM Plugins"] - parent.contributors = ["Andras Lasso (PerkLab)"] - parent.helpText = """ + """ + 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 GE ABUS Import Plugin" + parent.categories = ["Developer Tools.DICOM Plugins"] + parent.contributors = ["Andras Lasso (PerkLab)"] + parent.helpText = """ Plugin to the DICOM Module to parse and load GE Invenia ABUS images. No module interface here, only in the DICOM module. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ The file was originally developed by Andras Lasso (PerkLab). """ - # don't show this module - it only appears in the DICOM module - parent.hidden = True - - # 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['DICOMGeAbusPlugin'] = DICOMGeAbusPluginClass + # don't show this module - it only appears in the DICOM module + parent.hidden = True + + # 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['DICOMGeAbusPlugin'] = DICOMGeAbusPluginClass diff --git a/Modules/Scripted/DICOMPlugins/DICOMImageSequencePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMImageSequencePlugin.py index 1bbd31eefb3..06e1efc633b 100644 --- a/Modules/Scripted/DICOMPlugins/DICOMImageSequencePlugin.py +++ b/Modules/Scripted/DICOMPlugins/DICOMImageSequencePlugin.py @@ -17,369 +17,369 @@ # class DICOMImageSequencePluginClass(DICOMPlugin): - """ 2D image sequence loader plugin. - It supports X-ray angiography and ultrasound images. - The main difference compared to plain scalar volume plugin is that it - loads frames as a single-slice-volume sequence (and not as a 3D volume), - it accepts color images, and handles multiple instances within a series - (e.g., multiple independent acquisitions and synchronized biplane acquisitions). - Limitation: ultrasound calibrated regions are not supported (each calibrated region - would need to be split out to its own volume sequence). - """ - - def __init__(self): - super().__init__() - self.loadType = "Image sequence" - - self.tags['sopClassUID'] = "0008,0016" - self.tags['seriesNumber'] = "0020,0011" - self.tags['seriesDescription'] = "0008,103E" - self.tags['instanceNumber'] = "0020,0013" - self.tags['triggerTime'] = "0018,1060" - self.tags['modality'] = "0008,0060" - self.tags['photometricInterpretation'] = "0028,0004" - self.tags['orientation'] = "0020,0037" - - self.detailedLogging = False - - def examine(self,fileLists): - """ Returns a list of DICOMLoadable instances - corresponding to ways of interpreting the - fileLists parameter. + """ 2D image sequence loader plugin. + It supports X-ray angiography and ultrasound images. + The main difference compared to plain scalar volume plugin is that it + loads frames as a single-slice-volume sequence (and not as a 3D volume), + it accepts color images, and handles multiple instances within a series + (e.g., multiple independent acquisitions and synchronized biplane acquisitions). + Limitation: ultrasound calibrated regions are not supported (each calibrated region + would need to be split out to its own volume sequence). """ - loadables = [] - for files in fileLists: - loadables += self.examineFiles(files) - return loadables + def __init__(self): + super().__init__() + self.loadType = "Image sequence" + + self.tags['sopClassUID'] = "0008,0016" + self.tags['seriesNumber'] = "0020,0011" + self.tags['seriesDescription'] = "0008,103E" + self.tags['instanceNumber'] = "0020,0013" + self.tags['triggerTime'] = "0018,1060" + self.tags['modality'] = "0008,0060" + self.tags['photometricInterpretation'] = "0028,0004" + self.tags['orientation'] = "0020,0037" + + self.detailedLogging = False + + def examine(self, fileLists): + """ Returns a list of DICOMLoadable instances + corresponding to ways of interpreting the + fileLists parameter. + """ + loadables = [] + for files in fileLists: + loadables += self.examineFiles(files) + + return loadables + + def examineFiles(self, files): + """ Returns a list of DICOMLoadable instances + corresponding to ways of interpreting the + files parameter. + """ + + self.detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool) + + supportedSOPClassUIDs = [ + '1.2.840.10008.5.1.4.1.1.12.1', # X-Ray Angiographic Image Storage + '1.2.840.10008.5.1.4.1.1.12.2', # X-Ray Fluoroscopy Image Storage + '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage + '1.2.840.10008.5.1.4.1.1.6.1', # Ultrasound Image Storage + '1.2.840.10008.5.1.4.1.1.7', # Secondary Capture Image Storage (only accepted for modalities that typically acquire 2D image sequences) + '1.2.840.10008.5.1.4.1.1.4', # MR Image Storage (will be only accepted if cine-MRI) + ] + + # Modalities that typically acquire 2D image sequences: + suppportedSecondaryCaptureModalities = ['US', 'XA', 'RF', 'ES'] + + # Each instance will be a loadable, that will result in one sequence browser node + # and usually one sequence (except simultaneous biplane acquisition, which will + # result in two sequences). + # Each pedal press on the XA/RF acquisition device creates a new instance number, + # but if the device has two imaging planes (biplane) then two sequences + # will be acquired, which have the same instance number. These two sequences + # are synchronized in time, therefore they have to be assigned to the same + # browser node. + instanceNumberToLoadableIndex = {} + + loadables = [] + + canBeCineMri = True + cineMriTriggerTimes = set() + cineMriImageOrientations = set() + cineMriInstanceNumberToFilenameIndex = {} + + for filePath in files: + # Quick check of SOP class UID without parsing the file... + try: + sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID']) + if not (sopClassUID in supportedSOPClassUIDs): + # Unsupported class + continue + + # Only accept MRI if it looks like cine-MRI + if sopClassUID != '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage (will be only accepted if cine-MRI) + canBeCineMri = False + if not canBeCineMri and sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage + continue + + except Exception as e: + # Quick check could not be completed (probably Slicer DICOM database is not initialized). + # No problem, we'll try to parse the file and check the SOP class UID then. + pass + + instanceNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['instanceNumber']) + if canBeCineMri and sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage + if not instanceNumber: + # no instance number, probably not cine-MRI + canBeCineMri = False + if self.detailedLogging: + logging.debug("No instance number attribute found, the series will not be considered as a cine MRI") + continue + cineMriInstanceNumberToFilenameIndex[int(instanceNumber)] = filePath + cineMriTriggerTimes.add(slicer.dicomDatabase.fileValue(filePath, self.tags['triggerTime'])) + cineMriImageOrientations.add(slicer.dicomDatabase.fileValue(filePath, self.tags['orientation'])) + + else: + modality = slicer.dicomDatabase.fileValue(filePath, self.tags['modality']) + if sopClassUID == '1.2.840.10008.5.1.4.1.1.7': # Secondary Capture Image Storage + if modality not in suppportedSecondaryCaptureModalities: + # practice of dumping secondary capture images into the same series + # is only prevalent in US and XA/RF modalities + continue + + if not (instanceNumber in instanceNumberToLoadableIndex.keys()): + # new instance number + seriesNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesNumber']) + seriesDescription = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesDescription']) + photometricInterpretation = slicer.dicomDatabase.fileValue(filePath, self.tags['photometricInterpretation']) + name = '' + if seriesNumber: + name = f'{seriesNumber}:' + if modality: + name = f'{name} {modality}' + if seriesDescription: + name = f'{name} {seriesDescription}' + if instanceNumber: + name = f'{name} [{instanceNumber}]' + + loadable = DICOMLoadable() + loadable.singleSequence = False # put each instance in a separate sequence + loadable.files = [filePath] + loadable.name = name.strip() # remove leading and trailing spaces, if any + loadable.warning = "Image spacing may need to be calibrated for accurate size measurements." + loadable.tooltip = f"{modality} image sequence" + loadable.selected = True + # Confidence is slightly larger than default scalar volume plugin's (0.5) + # but still leaving room for more specialized plugins. + loadable.confidence = 0.7 + loadable.grayscale = ('MONOCHROME' in photometricInterpretation) + + # Add to loadables list + loadables.append(loadable) + instanceNumberToLoadableIndex[instanceNumber] = len(loadables) - 1 + else: + # existing instance number, add this file + loadableIndex = instanceNumberToLoadableIndex[instanceNumber] + loadables[loadableIndex].files.append(filePath) + loadable.tooltip = f"{modality} image sequence ({len(loadables[loadableIndex].files)} planes)" + + if canBeCineMri and len(cineMriInstanceNumberToFilenameIndex) > 1: + # Get description from first + ds = dicom.read_file(cineMriInstanceNumberToFilenameIndex[next(iter(cineMriInstanceNumberToFilenameIndex))], stop_before_pixels=True) + name = '' + if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber: + name = f'{ds.SeriesNumber}:' + if hasattr(ds, 'Modality') and ds.Modality: + name = f'{name} {ds.Modality}' + if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription: + name = f'{name} {ds.SeriesDescription}' + + loadable = DICOMLoadable() + loadable.singleSequence = True # put all instances in a single sequence + loadable.instanceNumbers = sorted(cineMriInstanceNumberToFilenameIndex) + loadable.files = [cineMriInstanceNumberToFilenameIndex[instanceNumber] for instanceNumber in loadable.instanceNumbers] + loadable.name = name.strip() # remove leading and trailing spaces, if any + loadable.tooltip = f"{ds.Modality} image sequence" + loadable.selected = True + if len(cineMriTriggerTimes) > 3: + if self.detailedLogging: + logging.debug("Several different trigger times found (" + repr(cineMriTriggerTimes) + ") - assuming this series is a cine MRI") + # This is likely a cardiac cine acquisition. + if len(cineMriImageOrientations) > 1: + if self.detailedLogging: + logging.debug("Several different image orientations found (" + repr(cineMriImageOrientations) + ") - assuming this series is a rotational cine MRI") + # Multivolume importer sets confidence=0.9-1.0, so we need to set a bit higher confidence to be selected by default + loadable.confidence = 1.05 + else: + if self.detailedLogging: + logging.debug("All image orientations are the same (" + repr(cineMriImageOrientations) + ") - probably the MultiVolume plugin should load this") + # Multivolume importer sets confidence=0.9-1.0, so we need to set a bit lower confidence to allow multivolume selected by default + loadable.confidence = 0.85 + else: + # This may be a 3D acquisition,so set lower confidence than scalar volume's default (0.5) + if self.detailedLogging: + logging.debug("Only one or few different trigger times found (" + repr(cineMriTriggerTimes) + ") - assuming this series is not a cine MRI") + loadable.confidence = 0.4 + loadable.grayscale = ('MONOCHROME' in ds.PhotometricInterpretation) + + # Add to loadables list + loadables.append(loadable) + + return loadables + + def loadImageData(self, filePath, grayscale, volumeNode): + import vtkITK + if grayscale: + reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader() + else: + reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile() + reader.SetArchetype(filePath) + reader.AddFileName(filePath) + reader.SetSingleFile(True) + reader.SetOutputScalarTypeToNative() + reader.SetDesiredCoordinateOrientationToNative() + reader.SetUseNativeOriginOn() + # GDCM is not particularly better in this than DCMTK, we just select one explicitly + # so that we know which one is used + reader.SetDICOMImageIOApproachToGDCM() + reader.Update() + if reader.GetErrorCode() != vtk.vtkErrorCode.NoError: + errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode()) + raise ValueError( + f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}") + + rasToIjk = reader.GetRasToIjkMatrix() + ijkToRas = vtk.vtkMatrix4x4() + vtk.vtkMatrix4x4.Invert(rasToIjk, ijkToRas) + return reader.GetOutput(), ijkToRas + + def addSequenceBrowserNode(self, name, outputSequenceNodes, playbackRateFps, loadable): + # Add a browser node and show the volume in the slice viewer for user convenience + outputSequenceBrowserNode = slicer.vtkMRMLSequenceBrowserNode() + outputSequenceBrowserNode.SetName(slicer.mrmlScene.GenerateUniqueName(name + ' browser')) + outputSequenceBrowserNode.SetPlaybackRateFps(playbackRateFps) + slicer.mrmlScene.AddNode(outputSequenceBrowserNode) + + # Add all sequences to the sequence browser + first = True + for outputSequenceNode in outputSequenceNodes: + outputSequenceBrowserNode.AddSynchronizedSequenceNode(outputSequenceNode) + proxyVolumeNode = outputSequenceBrowserNode.GetProxyNode(outputSequenceNode) + # create Subject hierarchy nodes for the loaded series + self.addSeriesInSubjectHierarchy(loadable, proxyVolumeNode) + + if first: + first = False + # Automatically select the volume to display + appLogic = slicer.app.applicationLogic() + selNode = appLogic.GetSelectionNode() + selNode.SetReferenceActiveVolumeID(proxyVolumeNode.GetID()) + appLogic.PropagateVolumeSelection() + appLogic.FitSliceToAll() + slicer.modules.sequences.setToolBarActiveBrowserNode(outputSequenceBrowserNode) + + # Show sequence browser toolbar + slicer.modules.sequences.showSequenceBrowser(outputSequenceBrowserNode) + + def addSequenceFromImageData(self, imageData, tempFrameVolume, filePath, name, singleFileInLoadable): + + # Rotate 180deg, otherwise the image would appear upside down + ijkToRas = vtk.vtkMatrix4x4() + ijkToRas.SetElement(0, 0, -1.0) + ijkToRas.SetElement(1, 1, -1.0) + tempFrameVolume.SetIJKToRASMatrix(ijkToRas) + # z axis is time + [spacingX, spacingY, frameTimeMsec] = imageData.GetSpacing() + imageData.SetSpacing(1.0, 1.0, 1.0) + tempFrameVolume.SetSpacing(spacingX, spacingY, 1.0) - def examineFiles(self,files): - """ Returns a list of DICOMLoadable instances - corresponding to ways of interpreting the - files parameter. - """ + # Create new sequence + outputSequenceNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceNode") - self.detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool) - - supportedSOPClassUIDs = [ - '1.2.840.10008.5.1.4.1.1.12.1', # X-Ray Angiographic Image Storage - '1.2.840.10008.5.1.4.1.1.12.2', # X-Ray Fluoroscopy Image Storage - '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage - '1.2.840.10008.5.1.4.1.1.6.1', # Ultrasound Image Storage - '1.2.840.10008.5.1.4.1.1.7', # Secondary Capture Image Storage (only accepted for modalities that typically acquire 2D image sequences) - '1.2.840.10008.5.1.4.1.1.4', # MR Image Storage (will be only accepted if cine-MRI) - ] - - # Modalities that typically acquire 2D image sequences: - suppportedSecondaryCaptureModalities = ['US', 'XA', 'RF', 'ES'] - - # Each instance will be a loadable, that will result in one sequence browser node - # and usually one sequence (except simultaneous biplane acquisition, which will - # result in two sequences). - # Each pedal press on the XA/RF acquisition device creates a new instance number, - # but if the device has two imaging planes (biplane) then two sequences - # will be acquired, which have the same instance number. These two sequences - # are synchronized in time, therefore they have to be assigned to the same - # browser node. - instanceNumberToLoadableIndex = {} - - loadables = [] - - canBeCineMri = True - cineMriTriggerTimes = set() - cineMriImageOrientations = set() - cineMriInstanceNumberToFilenameIndex = {} - - for filePath in files: - # Quick check of SOP class UID without parsing the file... - try: - sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID']) - if not (sopClassUID in supportedSOPClassUIDs): - # Unsupported class - continue - - # Only accept MRI if it looks like cine-MRI - if sopClassUID != '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage (will be only accepted if cine-MRI) - canBeCineMri = False - if not canBeCineMri and sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage - continue - - except Exception as e: - # Quick check could not be completed (probably Slicer DICOM database is not initialized). - # No problem, we'll try to parse the file and check the SOP class UID then. - pass - - instanceNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['instanceNumber']) - if canBeCineMri and sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage - if not instanceNumber: - # no instance number, probably not cine-MRI - canBeCineMri = False - if self.detailedLogging: - logging.debug("No instance number attribute found, the series will not be considered as a cine MRI") - continue - cineMriInstanceNumberToFilenameIndex[int(instanceNumber)] = filePath - cineMriTriggerTimes.add(slicer.dicomDatabase.fileValue(filePath, self.tags['triggerTime'])) - cineMriImageOrientations.add(slicer.dicomDatabase.fileValue(filePath, self.tags['orientation'])) - - else: - modality = slicer.dicomDatabase.fileValue(filePath, self.tags['modality']) - if sopClassUID == '1.2.840.10008.5.1.4.1.1.7': # Secondary Capture Image Storage - if modality not in suppportedSecondaryCaptureModalities: - # practice of dumping secondary capture images into the same series - # is only prevalent in US and XA/RF modalities - continue - - if not (instanceNumber in instanceNumberToLoadableIndex.keys()): - # new instance number - seriesNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesNumber']) - seriesDescription = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesDescription']) - photometricInterpretation = slicer.dicomDatabase.fileValue(filePath, self.tags['photometricInterpretation']) - name = '' - if seriesNumber: - name = f'{seriesNumber}:' - if modality: - name = f'{name} {modality}' - if seriesDescription: - name = f'{name} {seriesDescription}' - if instanceNumber: - name = f'{name} [{instanceNumber}]' - - loadable = DICOMLoadable() - loadable.singleSequence = False # put each instance in a separate sequence - loadable.files = [filePath] - loadable.name = name.strip() # remove leading and trailing spaces, if any - loadable.warning = "Image spacing may need to be calibrated for accurate size measurements." - loadable.tooltip = f"{modality} image sequence" - loadable.selected = True - # Confidence is slightly larger than default scalar volume plugin's (0.5) - # but still leaving room for more specialized plugins. - loadable.confidence = 0.7 - loadable.grayscale = ('MONOCHROME' in photometricInterpretation) - - # Add to loadables list - loadables.append(loadable) - instanceNumberToLoadableIndex[instanceNumber] = len(loadables)-1 + # Get sequence name + if singleFileInLoadable: + outputSequenceNode.SetName(name) else: - # existing instance number, add this file - loadableIndex = instanceNumberToLoadableIndex[instanceNumber] - loadables[loadableIndex].files.append(filePath) - loadable.tooltip = f"{modality} image sequence ({len(loadables[loadableIndex].files)} planes)" - - if canBeCineMri and len(cineMriInstanceNumberToFilenameIndex) > 1: - # Get description from first - ds = dicom.read_file(cineMriInstanceNumberToFilenameIndex[next(iter(cineMriInstanceNumberToFilenameIndex))], stop_before_pixels=True) - name = '' - if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber: - name = f'{ds.SeriesNumber}:' - if hasattr(ds, 'Modality') and ds.Modality: - name = f'{name} {ds.Modality}' - if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription: - name = f'{name} {ds.SeriesDescription}' - - loadable = DICOMLoadable() - loadable.singleSequence = True # put all instances in a single sequence - loadable.instanceNumbers = sorted(cineMriInstanceNumberToFilenameIndex) - loadable.files = [cineMriInstanceNumberToFilenameIndex[instanceNumber] for instanceNumber in loadable.instanceNumbers] - loadable.name = name.strip() # remove leading and trailing spaces, if any - loadable.tooltip = f"{ds.Modality} image sequence" - loadable.selected = True - if len(cineMriTriggerTimes)>3: - if self.detailedLogging: - logging.debug("Several different trigger times found ("+repr(cineMriTriggerTimes)+") - assuming this series is a cine MRI") - # This is likely a cardiac cine acquisition. - if len(cineMriImageOrientations) > 1: - if self.detailedLogging: - logging.debug("Several different image orientations found ("+repr(cineMriImageOrientations)+") - assuming this series is a rotational cine MRI") - # Multivolume importer sets confidence=0.9-1.0, so we need to set a bit higher confidence to be selected by default - loadable.confidence = 1.05 + ds = dicom.read_file(filePath, stop_before_pixels=True) + if hasattr(ds, 'PositionerPrimaryAngle') and hasattr(ds, 'PositionerSecondaryAngle'): + outputSequenceNode.SetName(f'{name} ({ds.PositionerPrimaryAngle}/{ds.PositionerSecondaryAngle})') + else: + outputSequenceNode.SetName(name) + + if frameTimeMsec == 1.0: + # frame time is not found, set it to 1.0fps + frameTime = 1 + outputSequenceNode.SetIndexName("frame") + outputSequenceNode.SetIndexUnit("") + playbackRateFps = 10 else: - if self.detailedLogging: - logging.debug("All image orientations are the same ("+repr(cineMriImageOrientations)+") - probably the MultiVolume plugin should load this") - # Multivolume importer sets confidence=0.9-1.0, so we need to set a bit lower confidence to allow multivolume selected by default - loadable.confidence = 0.85 - else: - # This may be a 3D acquisition,so set lower confidence than scalar volume's default (0.5) - if self.detailedLogging: - logging.debug("Only one or few different trigger times found ("+repr(cineMriTriggerTimes)+") - assuming this series is not a cine MRI") - loadable.confidence = 0.4 - loadable.grayscale = ('MONOCHROME' in ds.PhotometricInterpretation) - - # Add to loadables list - loadables.append(loadable) - - return loadables - - def loadImageData(self, filePath, grayscale, volumeNode): - import vtkITK - if grayscale: - reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader() - else: - reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile() - reader.SetArchetype(filePath) - reader.AddFileName(filePath) - reader.SetSingleFile(True) - reader.SetOutputScalarTypeToNative() - reader.SetDesiredCoordinateOrientationToNative() - reader.SetUseNativeOriginOn() - # GDCM is not particularly better in this than DCMTK, we just select one explicitly - # so that we know which one is used - reader.SetDICOMImageIOApproachToGDCM() - reader.Update() - if reader.GetErrorCode() != vtk.vtkErrorCode.NoError: - errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode()) - raise ValueError( - f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}") - - rasToIjk = reader.GetRasToIjkMatrix() - ijkToRas = vtk.vtkMatrix4x4() - vtk.vtkMatrix4x4.Invert(rasToIjk, ijkToRas) - return reader.GetOutput(), ijkToRas - - def addSequenceBrowserNode(self, name, outputSequenceNodes, playbackRateFps, loadable): - # Add a browser node and show the volume in the slice viewer for user convenience - outputSequenceBrowserNode = slicer.vtkMRMLSequenceBrowserNode() - outputSequenceBrowserNode.SetName(slicer.mrmlScene.GenerateUniqueName(name + ' browser')) - outputSequenceBrowserNode.SetPlaybackRateFps(playbackRateFps) - slicer.mrmlScene.AddNode(outputSequenceBrowserNode) - - # Add all sequences to the sequence browser - first = True - for outputSequenceNode in outputSequenceNodes: - outputSequenceBrowserNode.AddSynchronizedSequenceNode(outputSequenceNode) - proxyVolumeNode = outputSequenceBrowserNode.GetProxyNode(outputSequenceNode) - # create Subject hierarchy nodes for the loaded series - self.addSeriesInSubjectHierarchy(loadable, proxyVolumeNode) - - if first: - first = False - # Automatically select the volume to display - appLogic = slicer.app.applicationLogic() - selNode = appLogic.GetSelectionNode() - selNode.SetReferenceActiveVolumeID(proxyVolumeNode.GetID()) - appLogic.PropagateVolumeSelection() - appLogic.FitSliceToAll() - slicer.modules.sequences.setToolBarActiveBrowserNode(outputSequenceBrowserNode) - - # Show sequence browser toolbar - slicer.modules.sequences.showSequenceBrowser(outputSequenceBrowserNode) - - def addSequenceFromImageData(self, imageData, tempFrameVolume, filePath, name, singleFileInLoadable): - - # Rotate 180deg, otherwise the image would appear upside down - ijkToRas = vtk.vtkMatrix4x4() - ijkToRas.SetElement(0, 0, -1.0) - ijkToRas.SetElement(1, 1, -1.0) - tempFrameVolume.SetIJKToRASMatrix(ijkToRas) - # z axis is time - [spacingX, spacingY, frameTimeMsec] = imageData.GetSpacing() - imageData.SetSpacing(1.0, 1.0, 1.0) - tempFrameVolume.SetSpacing(spacingX, spacingY, 1.0) - - # Create new sequence - outputSequenceNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceNode") - - # Get sequence name - if singleFileInLoadable: - outputSequenceNode.SetName(name) - else: - ds = dicom.read_file(filePath, stop_before_pixels=True) - if hasattr(ds, 'PositionerPrimaryAngle') and hasattr(ds, 'PositionerSecondaryAngle'): - outputSequenceNode.SetName(f'{name} ({ds.PositionerPrimaryAngle}/{ds.PositionerSecondaryAngle})') - else: - outputSequenceNode.SetName(name) - - if frameTimeMsec == 1.0: - # frame time is not found, set it to 1.0fps - frameTime = 1 - outputSequenceNode.SetIndexName("frame") - outputSequenceNode.SetIndexUnit("") - playbackRateFps = 10 - else: - # frame time is set, use it - frameTime = frameTimeMsec * 0.001 - outputSequenceNode.SetIndexName("time") - outputSequenceNode.SetIndexUnit("s") - playbackRateFps = 1.0 / frameTime - - # Add frames to the sequence - numberOfFrames = imageData.GetDimensions()[2] - extent = imageData.GetExtent() - numberOfFrames = extent[5] - extent[4] + 1 - for frame in range(numberOfFrames): - # get current frame from multiframe - crop = vtk.vtkImageClip() - crop.SetInputData(imageData) - crop.SetOutputWholeExtent(extent[0], extent[1], extent[2], extent[3], extent[4] + frame, extent[4] + frame) - crop.ClipDataOn() - crop.Update() - croppedOutput = crop.GetOutput() - croppedOutput.SetExtent(extent[0], extent[1], extent[2], extent[3], 0, 0) - croppedOutput.SetOrigin(0.0, 0.0, 0.0) - tempFrameVolume.SetAndObserveImageData(croppedOutput) - # get timestamp - if type(frameTime) == int: - timeStampSec = str(frame * frameTime) - else: - timeStampSec = f"{frame * frameTime:.3f}" - outputSequenceNode.SetDataNodeAtValue(tempFrameVolume, timeStampSec) - - # Create storage node that allows saving node as nrrd - outputSequenceStorageNode = slicer.vtkMRMLVolumeSequenceStorageNode() - slicer.mrmlScene.AddNode(outputSequenceStorageNode) - outputSequenceNode.SetAndObserveStorageNodeID(outputSequenceStorageNode.GetID()) - - return outputSequenceNode, playbackRateFps - - def load(self,loadable): - """Load the selection - """ - - outputSequenceNodes = [] - - if loadable.singleSequence: - outputSequenceNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceNode") - outputSequenceNode.SetName(loadable.name) - outputSequenceNode.SetIndexName("instance number") - outputSequenceNode.SetIndexUnit("") - playbackRateFps = 10 - outputSequenceNodes.append(outputSequenceNode) - - # Create a temporary volume node that will be used to insert volume nodes in the sequence - if loadable.grayscale: - tempFrameVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") - else: - tempFrameVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode") - - for fileIndex, filePath in enumerate(loadable.files): - imageData, ijkToRas = self.loadImageData(filePath, loadable.grayscale, tempFrameVolume) - if loadable.singleSequence: - # each file is a frame (cine-MRI) - imageData.SetSpacing(1.0, 1.0, 1.0) - imageData.SetOrigin(0.0, 0.0, 0.0) - tempFrameVolume.SetIJKToRASMatrix(ijkToRas) - tempFrameVolume.SetAndObserveImageData(imageData) - instanceNumber = loadable.instanceNumbers[fileIndex] - # Save DICOM SOP instance UID into the sequence so DICOM metadata can be retrieved later if needed - tempFrameVolume.SetAttribute('DICOM.instanceUIDs', slicer.dicomDatabase.instanceForFile(filePath)) - # Save trigger time, because it may be needed for 4D cine-MRI volume reconstruction - triggerTime = slicer.dicomDatabase.fileValue(filePath, self.tags['triggerTime']) - if triggerTime: - tempFrameVolume.SetAttribute('DICOM.triggerTime', triggerTime) - outputSequenceNode.SetDataNodeAtValue(tempFrameVolume, str(instanceNumber)) - else: - # each file is a new sequence - outputSequenceNode, playbackRateFps = self.addSequenceFromImageData( - imageData, tempFrameVolume, filePath, loadable.name, (len(loadable.files) == 1)) - outputSequenceNodes.append(outputSequenceNode) - - # Delete temporary volume node - slicer.mrmlScene.RemoveNode(tempFrameVolume) - - if not hasattr(loadable, 'createBrowserNode') or loadable.createBrowserNode: - self.addSequenceBrowserNode(loadable.name, outputSequenceNodes, playbackRateFps, loadable) - - # Return the last loaded sequence node (that is the one currently displayed in slice views) - return outputSequenceNodes[-1] + # frame time is set, use it + frameTime = frameTimeMsec * 0.001 + outputSequenceNode.SetIndexName("time") + outputSequenceNode.SetIndexUnit("s") + playbackRateFps = 1.0 / frameTime + + # Add frames to the sequence + numberOfFrames = imageData.GetDimensions()[2] + extent = imageData.GetExtent() + numberOfFrames = extent[5] - extent[4] + 1 + for frame in range(numberOfFrames): + # get current frame from multiframe + crop = vtk.vtkImageClip() + crop.SetInputData(imageData) + crop.SetOutputWholeExtent(extent[0], extent[1], extent[2], extent[3], extent[4] + frame, extent[4] + frame) + crop.ClipDataOn() + crop.Update() + croppedOutput = crop.GetOutput() + croppedOutput.SetExtent(extent[0], extent[1], extent[2], extent[3], 0, 0) + croppedOutput.SetOrigin(0.0, 0.0, 0.0) + tempFrameVolume.SetAndObserveImageData(croppedOutput) + # get timestamp + if type(frameTime) == int: + timeStampSec = str(frame * frameTime) + else: + timeStampSec = f"{frame * frameTime:.3f}" + outputSequenceNode.SetDataNodeAtValue(tempFrameVolume, timeStampSec) + + # Create storage node that allows saving node as nrrd + outputSequenceStorageNode = slicer.vtkMRMLVolumeSequenceStorageNode() + slicer.mrmlScene.AddNode(outputSequenceStorageNode) + outputSequenceNode.SetAndObserveStorageNodeID(outputSequenceStorageNode.GetID()) + + return outputSequenceNode, playbackRateFps + + def load(self, loadable): + """Load the selection + """ + + outputSequenceNodes = [] + + if loadable.singleSequence: + outputSequenceNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceNode") + outputSequenceNode.SetName(loadable.name) + outputSequenceNode.SetIndexName("instance number") + outputSequenceNode.SetIndexUnit("") + playbackRateFps = 10 + outputSequenceNodes.append(outputSequenceNode) + + # Create a temporary volume node that will be used to insert volume nodes in the sequence + if loadable.grayscale: + tempFrameVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") + else: + tempFrameVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode") + + for fileIndex, filePath in enumerate(loadable.files): + imageData, ijkToRas = self.loadImageData(filePath, loadable.grayscale, tempFrameVolume) + if loadable.singleSequence: + # each file is a frame (cine-MRI) + imageData.SetSpacing(1.0, 1.0, 1.0) + imageData.SetOrigin(0.0, 0.0, 0.0) + tempFrameVolume.SetIJKToRASMatrix(ijkToRas) + tempFrameVolume.SetAndObserveImageData(imageData) + instanceNumber = loadable.instanceNumbers[fileIndex] + # Save DICOM SOP instance UID into the sequence so DICOM metadata can be retrieved later if needed + tempFrameVolume.SetAttribute('DICOM.instanceUIDs', slicer.dicomDatabase.instanceForFile(filePath)) + # Save trigger time, because it may be needed for 4D cine-MRI volume reconstruction + triggerTime = slicer.dicomDatabase.fileValue(filePath, self.tags['triggerTime']) + if triggerTime: + tempFrameVolume.SetAttribute('DICOM.triggerTime', triggerTime) + outputSequenceNode.SetDataNodeAtValue(tempFrameVolume, str(instanceNumber)) + else: + # each file is a new sequence + outputSequenceNode, playbackRateFps = self.addSequenceFromImageData( + imageData, tempFrameVolume, filePath, loadable.name, (len(loadable.files) == 1)) + outputSequenceNodes.append(outputSequenceNode) + + # Delete temporary volume node + slicer.mrmlScene.RemoveNode(tempFrameVolume) + + if not hasattr(loadable, 'createBrowserNode') or loadable.createBrowserNode: + self.addSequenceBrowserNode(loadable.name, outputSequenceNodes, playbackRateFps, loadable) + + # Return the last loaded sequence node (that is the one currently displayed in slice views) + return outputSequenceNodes[-1] # @@ -387,31 +387,31 @@ def load(self,loadable): # class DICOMImageSequencePlugin: - """ - 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 Image Sequence Import Plugin" - parent.categories = ["Developer Tools.DICOM Plugins"] - parent.contributors = ["Andras Lasso (PerkLab)"] - parent.helpText = """ + """ + 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 Image Sequence Import Plugin" + parent.categories = ["Developer Tools.DICOM Plugins"] + parent.contributors = ["Andras Lasso (PerkLab)"] + parent.helpText = """ Plugin to the DICOM Module to parse and load 2D image sequences. No module interface here, only in the DICOM module. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ The file was originally developed by Andras Lasso (PerkLab). """ - # don't show this module - it only appears in the DICOM module - parent.hidden = True - - # 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['DICOMImageSequencePlugin'] = DICOMImageSequencePluginClass + # don't show this module - it only appears in the DICOM module + parent.hidden = True + + # 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['DICOMImageSequencePlugin'] = DICOMImageSequencePluginClass diff --git a/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py index ec14edb5808..48e351ffcd5 100644 --- a/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py +++ b/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py @@ -22,826 +22,826 @@ # class DICOMScalarVolumePluginClass(DICOMPlugin): - """ ScalarVolume specific interpretation code - """ - - def __init__(self,epsilon=0.01): - super().__init__() - self.loadType = "Scalar Volume" - self.epsilon = epsilon - self.acquisitionModeling = None - self.defaultStudyID = 'SLICER10001' #TODO: What should be the new study ID? - - self.tags['sopClassUID'] = "0008,0016" - self.tags['photometricInterpretation'] = "0028,0004" - self.tags['seriesDescription'] = "0008,103e" - self.tags['seriesUID'] = "0020,000E" - self.tags['seriesNumber'] = "0020,0011" - self.tags['position'] = "0020,0032" - self.tags['orientation'] = "0020,0037" - self.tags['pixelData'] = "7fe0,0010" - self.tags['seriesInstanceUID'] = "0020,000E" - self.tags['acquisitionNumber'] = "0020,0012" - self.tags['imageType'] = "0008,0008" - self.tags['contentTime'] = "0008,0033" - self.tags['triggerTime'] = "0018,1060" - self.tags['diffusionGradientOrientation'] = "0018,9089" - self.tags['imageOrientationPatient'] = "0020,0037" - self.tags['numberOfFrames'] = "0028,0008" - self.tags['instanceUID'] = "0008,0018" - self.tags['windowCenter'] = "0028,1050" - self.tags['windowWidth'] = "0028,1051" - self.tags['rows'] = "0028,0010" - self.tags['columns'] = "0028,0011" - - @staticmethod - def readerApproaches(): - """Available reader implementations. First entry is initial default. - Note: the settings file stores the index of the user's selected reader - approach, so if new approaches are added the should go at the - end of the list. + """ ScalarVolume specific interpretation code """ - return ["GDCM with DCMTK fallback", "DCMTK", "GDCM", "Archetype"] - @staticmethod - def settingsPanelEntry(panel, parent): - """Create a settings panel entry for this plugin class. - It is added to the DICOM panel of the application settings - by the DICOM module. - """ - formLayout = qt.QFormLayout(parent) - - readersComboBox = qt.QComboBox() - for approach in DICOMScalarVolumePluginClass.readerApproaches(): - readersComboBox.addItem(approach) - readersComboBox.toolTip = ("Preferred back end. Archetype was used by default in Slicer before June of 2017." - "Change this setting if data that previously loaded stops working (and report an issue).") - formLayout.addRow("DICOM reader approach:", readersComboBox) - panel.registerProperty( - "DICOM/ScalarVolume/ReaderApproach", readersComboBox, - "currentIndex", str(qt.SIGNAL("currentIndexChanged(int)"))) - - importFormatsComboBox = ctk.ctkComboBox() - importFormatsComboBox.toolTip = ("Enable adding non-linear transform to regularize images acquired irregular geometry:" - " non-rectilinear grid (such as tilted gantry CT acquisitions) and non-uniform slice spacing." - " If no regularization is applied then image may appear distorted if it was acquired with irregular geometry.") - importFormatsComboBox.addItem("default (none)", "default") - importFormatsComboBox.addItem("none", "none") - importFormatsComboBox.addItem("apply regularization transform", "transform") - # in the future additional option, such as "resample" may be added - importFormatsComboBox.currentIndex = 0 - formLayout.addRow("Acquisition geometry regularization:", importFormatsComboBox) - panel.registerProperty( - "DICOM/ScalarVolume/AcquisitionGeometryRegularization", importFormatsComboBox, - "currentUserDataAsString", str(qt.SIGNAL("currentIndexChanged(int)")), - "DICOM examination settings", ctk.ctkSettingsPanel.OptionRequireRestart) - # DICOM examination settings are cached so we need to restart to make sure changes take effect - - allowLoadingByTimeCheckBox = qt.QCheckBox() - allowLoadingByTimeCheckBox.toolTip = ("Offer loading of individual slices or group of slices" - " that were acquired at a specific time (content or trigger time)." - " If this option is enabled then a large number of loadable items may be displayed in the Advanced section of DICOM browser.") - formLayout.addRow("Allow loading subseries by time:", allowLoadingByTimeCheckBox) - allowLoadingByTimeMapper = ctk.ctkBooleanMapper(allowLoadingByTimeCheckBox, "checked", str(qt.SIGNAL("toggled(bool)"))) - panel.registerProperty( - "DICOM/ScalarVolume/AllowLoadingByTime", allowLoadingByTimeMapper, - "valueAsInt", str(qt.SIGNAL("valueAsIntChanged(int)")), - "DICOM examination settings", ctk.ctkSettingsPanel.OptionRequireRestart) - # DICOM examination settings are cached so we need to restart to make sure changes take effect - - @staticmethod - def compareVolumeNodes(volumeNode1,volumeNode2): - """ - Given two mrml volume nodes, return true of the numpy arrays have identical data - and other metadata matches. Returns empty string on match, otherwise - a string with a list of differences separated by newlines. - """ - volumesLogic = slicer.modules.volumes.logic() - comparison = "" - comparison += volumesLogic.CompareVolumeGeometry(volumeNode1, volumeNode2) - image1 = volumeNode1.GetImageData() - image2 = volumeNode2.GetImageData() - if image1.GetScalarType() != image2.GetScalarType(): - comparison += f"First volume is {image1.GetScalarTypeAsString()}, but second is {image2.GetScalarTypeAsString()}" - array1 = slicer.util.array(volumeNode1.GetID()) - array2 = slicer.util.array(volumeNode2.GetID()) - if not numpy.all(array1 == array2): - comparison += "Pixel data mismatch\n" - return comparison - - def acquisitionGeometryRegularizationEnabled(self): - settings = qt.QSettings() - return (settings.value("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "default") == "transform") - - def allowLoadingByTime(self): - settings = qt.QSettings() - return (int(settings.value("DICOM/ScalarVolume/AllowLoadingByTime", "0")) != 0) - - def examineForImport(self,fileLists): - """ Returns a sorted list of DICOMLoadable instances - corresponding to ways of interpreting the - fileLists parameter (list of file lists). - """ - loadables = [] - for files in fileLists: - cachedLoadables = self.getCachedLoadables(files) - if cachedLoadables: - loadables += cachedLoadables - else: - loadablesForFiles = self.examineFiles(files) - loadables += loadablesForFiles - self.cacheLoadables(files,loadablesForFiles) - - # sort the loadables by series number if possible - loadables.sort(key=cmp_to_key(lambda x,y: self.seriesSorter(x,y))) - - return loadables - - def cleanNodeName(self, value): - cleanValue = value - cleanValue = cleanValue.replace("|", "-") - cleanValue = cleanValue.replace("/", "-") - cleanValue = cleanValue.replace("\\", "-") - cleanValue = cleanValue.replace("*", "(star)") - cleanValue = cleanValue.replace("\\", "-") - return cleanValue - - def examineFiles(self,files): - """ Returns a list of DICOMLoadable instances - corresponding to ways of interpreting the - files parameter. - """ - - seriesUID = slicer.dicomDatabase.fileValue(files[0],self.tags['seriesUID']) - seriesName = self.defaultSeriesNodeName(seriesUID) - - # default loadable includes all files for series - allFilesLoadable = DICOMLoadable() - allFilesLoadable.files = files - allFilesLoadable.name = self.cleanNodeName(seriesName) - allFilesLoadable.tooltip = "%d files, first file: %s" % (len(allFilesLoadable.files), allFilesLoadable.files[0]) - allFilesLoadable.selected = True - # add it to the list of loadables later, if pixel data is available in at least one file - - # make subseries volumes based on tag differences - subseriesTags = [ - "seriesInstanceUID", - "acquisitionNumber", - # GE volume viewer and Siemens Axiom CBCT systems put an overview (localizer) slice and all the reconstructed slices - # in one series, using two different image types. Splitting based on image type allows loading of these volumes - # (loading the series without localizer). - "imageType", - "imageOrientationPatient", - "diffusionGradientOrientation", - ] - - if self.allowLoadingByTime(): - subseriesTags.append("contentTime") - subseriesTags.append("triggerTime") - - # Values for these tags will only be enumerated (value itself will not be part of the loadable name) - # because the vale itself is usually too long and complicated to be displayed to users - subseriesTagsToEnumerateValues = [ - "seriesInstanceUID", - "imageOrientationPatient", - "diffusionGradientOrientation", - ] - - # - # first, look for subseries within this series - # - build a list of files for each unique value - # of each tag - # - subseriesFiles = {} - subseriesValues = {} - for file in allFilesLoadable.files: - # check for subseries values - for tag in subseriesTags: - value = slicer.dicomDatabase.fileValue(file,self.tags[tag]) - value = value.replace(",","_") # remove commas so it can be used as an index - if tag not in subseriesValues: - subseriesValues[tag] = [] - if not subseriesValues[tag].__contains__(value): - subseriesValues[tag].append(value) - if (tag,value) not in subseriesFiles: - subseriesFiles[tag,value] = [] - subseriesFiles[tag,value].append(file) - - loadables = [] - - # Pixel data is available, so add the default loadable to the output - loadables.append(allFilesLoadable) - - # - # second, for any tags that have more than one value, create a new - # virtual series - # - subseriesCount = 0 - # List of loadables that look like subseries that contain the full series except a single frame - probableLocalizerFreeLoadables = [] - for tag in subseriesTags: - if len(subseriesValues[tag]) > 1: - subseriesCount += 1 - for valueIndex, value in enumerate(subseriesValues[tag]): - # default loadable includes all files for series - loadable = DICOMLoadable() - loadable.files = subseriesFiles[tag,value] - # value can be a long string (and it will be used for generating node name) - # therefore use just an index instead - if tag in subseriesTagsToEnumerateValues: - loadable.name = seriesName + " - %s %d" % (tag, valueIndex+1) - else: - loadable.name = seriesName + f" - {tag} {value}" - loadable.name = self.cleanNodeName(loadable.name) - loadable.tooltip = "%d files, grouped by %s = %s. First file: %s. %s = %s" % (len(loadable.files), tag, value, loadable.files[0], tag, value) - loadable.selected = False - loadables.append(loadable) - if len(subseriesValues[tag]) == 2: - otherValue = subseriesValues[tag][1-valueIndex] - if len(subseriesFiles[tag,value]) > 1 and len(subseriesFiles[tag, otherValue]) == 1: - # this looks like a subseries without a localizer image - probableLocalizerFreeLoadables.append(loadable) - - # remove any files from loadables that don't have pixel data (no point sending them to ITK for reading) - # also remove DICOM SEG, since it is not handled by ITK readers - newLoadables = [] - for loadable in loadables: - newFiles = [] - excludedLoadable = False - for file in loadable.files: - if slicer.dicomDatabase.fileValueExists(file,self.tags['pixelData']): - newFiles.append(file) - if slicer.dicomDatabase.fileValue(file,self.tags['sopClassUID'])=='1.2.840.10008.5.1.4.1.1.66.4': - excludedLoadable = True - if 'DICOMSegmentationPlugin' not in slicer.modules.dicomPlugins: - logging.warning('Please install Quantitative Reporting extension to enable loading of DICOM Segmentation objects') - elif slicer.dicomDatabase.fileValue(file,self.tags['sopClassUID'])=='1.2.840.10008.5.1.4.1.1.481.3': - excludedLoadable = True - if 'DicomRtImportExportPlugin' not in slicer.modules.dicomPlugins: - logging.warning('Please install SlicerRT extension to enable loading of DICOM RT Structure Set objects') - if len(newFiles) > 0 and not excludedLoadable: - loadable.files = newFiles - loadable.grayscale = ('MONOCHROME' in slicer.dicomDatabase.fileValue(newFiles[0],self.tags['photometricInterpretation'])) - newLoadables.append(loadable) - elif excludedLoadable: - continue - else: - # here all files in have no pixel data, so they might be - # secondary capture images which will read, so let's pass - # them through with a warning and low confidence - loadable.warning += "There is no pixel data attribute for the DICOM objects, but they might be readable as secondary capture images. " - loadable.confidence = 0.2 - loadable.grayscale = ('MONOCHROME' in slicer.dicomDatabase.fileValue(loadable.files[0],self.tags['photometricInterpretation'])) - newLoadables.append(loadable) - loadables = newLoadables + def __init__(self, epsilon=0.01): + super().__init__() + self.loadType = "Scalar Volume" + self.epsilon = epsilon + self.acquisitionModeling = None + self.defaultStudyID = 'SLICER10001' # TODO: What should be the new study ID? + + self.tags['sopClassUID'] = "0008,0016" + self.tags['photometricInterpretation'] = "0028,0004" + self.tags['seriesDescription'] = "0008,103e" + self.tags['seriesUID'] = "0020,000E" + self.tags['seriesNumber'] = "0020,0011" + self.tags['position'] = "0020,0032" + self.tags['orientation'] = "0020,0037" + self.tags['pixelData'] = "7fe0,0010" + self.tags['seriesInstanceUID'] = "0020,000E" + self.tags['acquisitionNumber'] = "0020,0012" + self.tags['imageType'] = "0008,0008" + self.tags['contentTime'] = "0008,0033" + self.tags['triggerTime'] = "0018,1060" + self.tags['diffusionGradientOrientation'] = "0018,9089" + self.tags['imageOrientationPatient'] = "0020,0037" + self.tags['numberOfFrames'] = "0028,0008" + self.tags['instanceUID'] = "0008,0018" + self.tags['windowCenter'] = "0028,1050" + self.tags['windowWidth'] = "0028,1051" + self.tags['rows'] = "0028,0010" + self.tags['columns'] = "0028,0011" + + @staticmethod + def readerApproaches(): + """Available reader implementations. First entry is initial default. + Note: the settings file stores the index of the user's selected reader + approach, so if new approaches are added the should go at the + end of the list. + """ + return ["GDCM with DCMTK fallback", "DCMTK", "GDCM", "Archetype"] + + @staticmethod + def settingsPanelEntry(panel, parent): + """Create a settings panel entry for this plugin class. + It is added to the DICOM panel of the application settings + by the DICOM module. + """ + formLayout = qt.QFormLayout(parent) + + readersComboBox = qt.QComboBox() + for approach in DICOMScalarVolumePluginClass.readerApproaches(): + readersComboBox.addItem(approach) + readersComboBox.toolTip = ("Preferred back end. Archetype was used by default in Slicer before June of 2017." + "Change this setting if data that previously loaded stops working (and report an issue).") + formLayout.addRow("DICOM reader approach:", readersComboBox) + panel.registerProperty( + "DICOM/ScalarVolume/ReaderApproach", readersComboBox, + "currentIndex", str(qt.SIGNAL("currentIndexChanged(int)"))) + + importFormatsComboBox = ctk.ctkComboBox() + importFormatsComboBox.toolTip = ("Enable adding non-linear transform to regularize images acquired irregular geometry:" + " non-rectilinear grid (such as tilted gantry CT acquisitions) and non-uniform slice spacing." + " If no regularization is applied then image may appear distorted if it was acquired with irregular geometry.") + importFormatsComboBox.addItem("default (none)", "default") + importFormatsComboBox.addItem("none", "none") + importFormatsComboBox.addItem("apply regularization transform", "transform") + # in the future additional option, such as "resample" may be added + importFormatsComboBox.currentIndex = 0 + formLayout.addRow("Acquisition geometry regularization:", importFormatsComboBox) + panel.registerProperty( + "DICOM/ScalarVolume/AcquisitionGeometryRegularization", importFormatsComboBox, + "currentUserDataAsString", str(qt.SIGNAL("currentIndexChanged(int)")), + "DICOM examination settings", ctk.ctkSettingsPanel.OptionRequireRestart) + # DICOM examination settings are cached so we need to restart to make sure changes take effect + + allowLoadingByTimeCheckBox = qt.QCheckBox() + allowLoadingByTimeCheckBox.toolTip = ("Offer loading of individual slices or group of slices" + " that were acquired at a specific time (content or trigger time)." + " If this option is enabled then a large number of loadable items may be displayed in the Advanced section of DICOM browser.") + formLayout.addRow("Allow loading subseries by time:", allowLoadingByTimeCheckBox) + allowLoadingByTimeMapper = ctk.ctkBooleanMapper(allowLoadingByTimeCheckBox, "checked", str(qt.SIGNAL("toggled(bool)"))) + panel.registerProperty( + "DICOM/ScalarVolume/AllowLoadingByTime", allowLoadingByTimeMapper, + "valueAsInt", str(qt.SIGNAL("valueAsIntChanged(int)")), + "DICOM examination settings", ctk.ctkSettingsPanel.OptionRequireRestart) + # DICOM examination settings are cached so we need to restart to make sure changes take effect + + @staticmethod + def compareVolumeNodes(volumeNode1, volumeNode2): + """ + Given two mrml volume nodes, return true of the numpy arrays have identical data + and other metadata matches. Returns empty string on match, otherwise + a string with a list of differences separated by newlines. + """ + volumesLogic = slicer.modules.volumes.logic() + comparison = "" + comparison += volumesLogic.CompareVolumeGeometry(volumeNode1, volumeNode2) + image1 = volumeNode1.GetImageData() + image2 = volumeNode2.GetImageData() + if image1.GetScalarType() != image2.GetScalarType(): + comparison += f"First volume is {image1.GetScalarTypeAsString()}, but second is {image2.GetScalarTypeAsString()}" + array1 = slicer.util.array(volumeNode1.GetID()) + array2 = slicer.util.array(volumeNode2.GetID()) + if not numpy.all(array1 == array2): + comparison += "Pixel data mismatch\n" + return comparison + + def acquisitionGeometryRegularizationEnabled(self): + settings = qt.QSettings() + return (settings.value("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "default") == "transform") + + def allowLoadingByTime(self): + settings = qt.QSettings() + return (int(settings.value("DICOM/ScalarVolume/AllowLoadingByTime", "0")) != 0) + + def examineForImport(self, fileLists): + """ Returns a sorted list of DICOMLoadable instances + corresponding to ways of interpreting the + fileLists parameter (list of file lists). + """ + loadables = [] + for files in fileLists: + cachedLoadables = self.getCachedLoadables(files) + if cachedLoadables: + loadables += cachedLoadables + else: + loadablesForFiles = self.examineFiles(files) + loadables += loadablesForFiles + self.cacheLoadables(files, loadablesForFiles) + + # sort the loadables by series number if possible + loadables.sort(key=cmp_to_key(lambda x, y: self.seriesSorter(x, y))) + + return loadables + + def cleanNodeName(self, value): + cleanValue = value + cleanValue = cleanValue.replace("|", "-") + cleanValue = cleanValue.replace("/", "-") + cleanValue = cleanValue.replace("\\", "-") + cleanValue = cleanValue.replace("*", "(star)") + cleanValue = cleanValue.replace("\\", "-") + return cleanValue + + def examineFiles(self, files): + """ Returns a list of DICOMLoadable instances + corresponding to ways of interpreting the + files parameter. + """ + + seriesUID = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesUID']) + seriesName = self.defaultSeriesNodeName(seriesUID) + + # default loadable includes all files for series + allFilesLoadable = DICOMLoadable() + allFilesLoadable.files = files + allFilesLoadable.name = self.cleanNodeName(seriesName) + allFilesLoadable.tooltip = "%d files, first file: %s" % (len(allFilesLoadable.files), allFilesLoadable.files[0]) + allFilesLoadable.selected = True + # add it to the list of loadables later, if pixel data is available in at least one file + + # make subseries volumes based on tag differences + subseriesTags = [ + "seriesInstanceUID", + "acquisitionNumber", + # GE volume viewer and Siemens Axiom CBCT systems put an overview (localizer) slice and all the reconstructed slices + # in one series, using two different image types. Splitting based on image type allows loading of these volumes + # (loading the series without localizer). + "imageType", + "imageOrientationPatient", + "diffusionGradientOrientation", + ] + + if self.allowLoadingByTime(): + subseriesTags.append("contentTime") + subseriesTags.append("triggerTime") + + # Values for these tags will only be enumerated (value itself will not be part of the loadable name) + # because the vale itself is usually too long and complicated to be displayed to users + subseriesTagsToEnumerateValues = [ + "seriesInstanceUID", + "imageOrientationPatient", + "diffusionGradientOrientation", + ] + + # + # first, look for subseries within this series + # - build a list of files for each unique value + # of each tag + # + subseriesFiles = {} + subseriesValues = {} + for file in allFilesLoadable.files: + # check for subseries values + for tag in subseriesTags: + value = slicer.dicomDatabase.fileValue(file, self.tags[tag]) + value = value.replace(",", "_") # remove commas so it can be used as an index + if tag not in subseriesValues: + subseriesValues[tag] = [] + if not subseriesValues[tag].__contains__(value): + subseriesValues[tag].append(value) + if (tag, value) not in subseriesFiles: + subseriesFiles[tag, value] = [] + subseriesFiles[tag, value].append(file) + + loadables = [] + + # Pixel data is available, so add the default loadable to the output + loadables.append(allFilesLoadable) + + # + # second, for any tags that have more than one value, create a new + # virtual series + # + subseriesCount = 0 + # List of loadables that look like subseries that contain the full series except a single frame + probableLocalizerFreeLoadables = [] + for tag in subseriesTags: + if len(subseriesValues[tag]) > 1: + subseriesCount += 1 + for valueIndex, value in enumerate(subseriesValues[tag]): + # default loadable includes all files for series + loadable = DICOMLoadable() + loadable.files = subseriesFiles[tag, value] + # value can be a long string (and it will be used for generating node name) + # therefore use just an index instead + if tag in subseriesTagsToEnumerateValues: + loadable.name = seriesName + " - %s %d" % (tag, valueIndex + 1) + else: + loadable.name = seriesName + f" - {tag} {value}" + loadable.name = self.cleanNodeName(loadable.name) + loadable.tooltip = "%d files, grouped by %s = %s. First file: %s. %s = %s" % (len(loadable.files), tag, value, loadable.files[0], tag, value) + loadable.selected = False + loadables.append(loadable) + if len(subseriesValues[tag]) == 2: + otherValue = subseriesValues[tag][1 - valueIndex] + if len(subseriesFiles[tag, value]) > 1 and len(subseriesFiles[tag, otherValue]) == 1: + # this looks like a subseries without a localizer image + probableLocalizerFreeLoadables.append(loadable) + + # remove any files from loadables that don't have pixel data (no point sending them to ITK for reading) + # also remove DICOM SEG, since it is not handled by ITK readers + newLoadables = [] + for loadable in loadables: + newFiles = [] + excludedLoadable = False + for file in loadable.files: + if slicer.dicomDatabase.fileValueExists(file, self.tags['pixelData']): + newFiles.append(file) + if slicer.dicomDatabase.fileValue(file, self.tags['sopClassUID']) == '1.2.840.10008.5.1.4.1.1.66.4': + excludedLoadable = True + if 'DICOMSegmentationPlugin' not in slicer.modules.dicomPlugins: + logging.warning('Please install Quantitative Reporting extension to enable loading of DICOM Segmentation objects') + elif slicer.dicomDatabase.fileValue(file, self.tags['sopClassUID']) == '1.2.840.10008.5.1.4.1.1.481.3': + excludedLoadable = True + if 'DicomRtImportExportPlugin' not in slicer.modules.dicomPlugins: + logging.warning('Please install SlicerRT extension to enable loading of DICOM RT Structure Set objects') + if len(newFiles) > 0 and not excludedLoadable: + loadable.files = newFiles + loadable.grayscale = ('MONOCHROME' in slicer.dicomDatabase.fileValue(newFiles[0], self.tags['photometricInterpretation'])) + newLoadables.append(loadable) + elif excludedLoadable: + continue + else: + # here all files in have no pixel data, so they might be + # secondary capture images which will read, so let's pass + # them through with a warning and low confidence + loadable.warning += "There is no pixel data attribute for the DICOM objects, but they might be readable as secondary capture images. " + loadable.confidence = 0.2 + loadable.grayscale = ('MONOCHROME' in slicer.dicomDatabase.fileValue(loadable.files[0], self.tags['photometricInterpretation'])) + newLoadables.append(loadable) + loadables = newLoadables + + # + # now for each series and subseries, sort the images + # by position and check for consistency + # then adjust confidence values based on warnings + # + for loadable in loadables: + loadable.files, distances, loadable.warning = DICOMUtils.getSortedImageFiles(loadable.files, self.epsilon) + + loadablesBetterThanAllFiles = [] + if allFilesLoadable.warning != "": + for probableLocalizerFreeLoadable in probableLocalizerFreeLoadables: + if probableLocalizerFreeLoadable.warning == "": + # localizer-free loadables are better then all files, if they don't have warning + loadablesBetterThanAllFiles.append(probableLocalizerFreeLoadable) + if not loadablesBetterThanAllFiles and subseriesCount == 1: + # there was a sorting warning and + # only one kind of subseries, so it's probably correct + # to have lower confidence in the default all-files version. + for loadable in loadables: + if loadable != allFilesLoadable and loadable.warning == "": + loadablesBetterThanAllFiles.append(loadable) + + # if there are loadables that are clearly better then all files, then use those (otherwise use all files loadable) + preferredLoadables = loadablesBetterThanAllFiles if loadablesBetterThanAllFiles else [allFilesLoadable] + # reduce confidence and deselect all non-preferred loadables + for loadable in loadables: + if loadable in preferredLoadables: + loadable.selected = True + else: + loadable.selected = False + if loadable.confidence > .45: + loadable.confidence = .45 + + return loadables + + def seriesSorter(self, x, y): + """ returns -1, 0, 1 for sorting of strings like: "400: series description" + Works for DICOMLoadable or other objects with name attribute + """ + if not (hasattr(x, 'name') and hasattr(y, 'name')): + return 0 + xName = x.name + yName = y.name + try: + xNumber = int(xName[:xName.index(':')]) + yNumber = int(yName[:yName.index(':')]) + except ValueError: + return 0 + cmp = xNumber - yNumber + return cmp # - # now for each series and subseries, sort the images - # by position and check for consistency - # then adjust confidence values based on warnings + # different ways to load a set of dicom files: + # - Logic: relies on the same loading mechanism used + # by the File->Add Data dialog in the Slicer GUI. + # This uses vtkITK under the hood with GDCM as + # the default loader. + # - DCMTK: explicitly uses the DCMTKImageIO + # - GDCM: explicitly uses the GDCMImageIO # - for loadable in loadables: - loadable.files, distances, loadable.warning = DICOMUtils.getSortedImageFiles(loadable.files, self.epsilon) - - loadablesBetterThanAllFiles = [] - if allFilesLoadable.warning != "": - for probableLocalizerFreeLoadable in probableLocalizerFreeLoadables: - if probableLocalizerFreeLoadable.warning == "": - # localizer-free loadables are better then all files, if they don't have warning - loadablesBetterThanAllFiles.append(probableLocalizerFreeLoadable) - if not loadablesBetterThanAllFiles and subseriesCount == 1: - # there was a sorting warning and - # only one kind of subseries, so it's probably correct - # to have lower confidence in the default all-files version. - for loadable in loadables: - if loadable != allFilesLoadable and loadable.warning == "": - loadablesBetterThanAllFiles.append(loadable) - - # if there are loadables that are clearly better then all files, then use those (otherwise use all files loadable) - preferredLoadables = loadablesBetterThanAllFiles if loadablesBetterThanAllFiles else [allFilesLoadable] - # reduce confidence and deselect all non-preferred loadables - for loadable in loadables: - if loadable in preferredLoadables: - loadable.selected = True - else: - loadable.selected = False - if loadable.confidence > .45: - loadable.confidence = .45 - - return loadables - - def seriesSorter(self,x,y): - """ returns -1, 0, 1 for sorting of strings like: "400: series description" - Works for DICOMLoadable or other objects with name attribute - """ - if not (hasattr(x,'name') and hasattr(y,'name')): - return 0 - xName = x.name - yName = y.name - try: - xNumber = int(xName[:xName.index(':')]) - yNumber = int(yName[:yName.index(':')]) - except ValueError: - return 0 - cmp = xNumber - yNumber - return cmp - - # - # different ways to load a set of dicom files: - # - Logic: relies on the same loading mechanism used - # by the File->Add Data dialog in the Slicer GUI. - # This uses vtkITK under the hood with GDCM as - # the default loader. - # - DCMTK: explicitly uses the DCMTKImageIO - # - GDCM: explicitly uses the GDCMImageIO - # - - def loadFilesWithArchetype(self,files,name): - """Load files in the traditional Slicer manner - using the volume logic helper class - and the vtkITK archetype helper code - """ - fileList = vtk.vtkStringArray() - for f in files: - fileList.InsertNextValue(f) - volumesLogic = slicer.modules.volumes.logic() - return(volumesLogic.AddArchetypeScalarVolume(files[0],name,0,fileList)) - - def loadFilesWithSeriesReader(self,imageIOName,files,name,grayscale=True): - """ Explicitly use the named imageIO to perform the loading - """ - if grayscale: - reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader() - else: - reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile() - reader.SetArchetype(files[0]) - for f in files: - reader.AddFileName(f) - reader.SetSingleFile(0) - reader.SetOutputScalarTypeToNative() - reader.SetDesiredCoordinateOrientationToNative() - reader.SetUseNativeOriginOn() - if imageIOName == "GDCM": - reader.SetDICOMImageIOApproachToGDCM() - elif imageIOName == "DCMTK": - reader.SetDICOMImageIOApproachToDCMTK() - else: - raise Exception("Invalid imageIOName of %s" % imageIOName) - logging.info("Loading with imageIOName: %s" % imageIOName) - reader.Update() - - slicer.modules.reader = reader - if reader.GetErrorCode() != vtk.vtkErrorCode.NoError: - errorStrings = (imageIOName, vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode())) - logging.error("Could not read scalar volume using %s approach. Error is: %s" % errorStrings) - return - - imageChangeInformation = vtk.vtkImageChangeInformation() - imageChangeInformation.SetInputConnection(reader.GetOutputPort()) - imageChangeInformation.SetOutputSpacing( 1, 1, 1 ) - imageChangeInformation.SetOutputOrigin( 0, 0, 0 ) - imageChangeInformation.Update() - - name = slicer.mrmlScene.GenerateUniqueName(name) - if grayscale: - volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", name) - else: - volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode", name) - volumeNode.SetAndObserveImageData(imageChangeInformation.GetOutputDataObject(0)) - slicer.vtkMRMLVolumeArchetypeStorageNode.SetMetaDataDictionaryFromReader(volumeNode, reader) - volumeNode.SetRASToIJKMatrix(reader.GetRasToIjkMatrix()) - volumeNode.CreateDefaultDisplayNodes() - - slicer.modules.DICOMInstance.reader = reader - slicer.modules.DICOMInstance.imageChangeInformation = imageChangeInformation - - return(volumeNode) - - def setVolumeNodeProperties(self,volumeNode,loadable): - """After the scalar volume has been loaded, populate the node - attributes and display node with values extracted from the dicom instances - """ - if volumeNode: - # - # create subject hierarchy items for the loaded series - # - self.addSeriesInSubjectHierarchy(loadable,volumeNode) - - # - # add list of DICOM instance UIDs to the volume node - # corresponding to the loaded files - # - instanceUIDs = "" - for file in loadable.files: - uid = slicer.dicomDatabase.fileValue(file,self.tags['instanceUID']) - if uid == "": - uid = "Unknown" - instanceUIDs += uid + " " - instanceUIDs = instanceUIDs[:-1] # strip last space - volumeNode.SetAttribute("DICOM.instanceUIDs", instanceUIDs) - - # Choose a file in the middle of the series as representative frame, - # because that is more likely to contain the object of interest than the first or last frame. - # This is important for example for getting a relevant window/center value for the series. - file = loadable.files[int(len(loadable.files)/2)] - - # - # automatically select the volume to display - # - appLogic = slicer.app.applicationLogic() - selNode = appLogic.GetSelectionNode() - selNode.SetActiveVolumeID(volumeNode.GetID()) - appLogic.PropagateVolumeSelection() - - # - # apply window/level from DICOM if available (the first pair that is found) - # Note: There can be multiple presets (multiplicity 1-n) in the standard [1]. We have - # a way to put these into the display node [2], so they can be selected in the Volumes - # module. - # [1] https://medical.nema.org/medical/dicom/current/output/html/part06.html - # [2] https://github.com/Slicer/Slicer/blob/3bfa2fc2b310d41c09b7a9e8f8f6c4f43d3bd1e2/Libs/MRML/Core/vtkMRMLScalarVolumeDisplayNode.h#L172 - # - try: - windowCenter = float( slicer.dicomDatabase.fileValue(file,self.tags['windowCenter']) ) - windowWidth = float( slicer.dicomDatabase.fileValue(file,self.tags['windowWidth']) ) - displayNode = volumeNode.GetDisplayNode() - if displayNode: - logging.info('Window/level found in DICOM tags (center=' + str(windowCenter) + ', width=' + str(windowWidth) + ') has been applied to volume ' + volumeNode.GetName()) - displayNode.AddWindowLevelPreset(windowWidth, windowCenter) - displayNode.SetWindowLevelFromPreset(0) + def loadFilesWithArchetype(self, files, name): + """Load files in the traditional Slicer manner + using the volume logic helper class + and the vtkITK archetype helper code + """ + fileList = vtk.vtkStringArray() + for f in files: + fileList.InsertNextValue(f) + volumesLogic = slicer.modules.volumes.logic() + return(volumesLogic.AddArchetypeScalarVolume(files[0], name, 0, fileList)) + + def loadFilesWithSeriesReader(self, imageIOName, files, name, grayscale=True): + """ Explicitly use the named imageIO to perform the loading + """ + + if grayscale: + reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader() else: - logging.info('No display node: cannot use window/level found in DICOM tags') - except ValueError: - pass # DICOM tags cannot be parsed to floating point numbers - - sopClassUID = slicer.dicomDatabase.fileValue(file,self.tags['sopClassUID']) - - # initialize color lookup table - modality = self.mapSOPClassUIDToModality(sopClassUID) - if modality == "PT": - displayNode = volumeNode.GetDisplayNode() - if displayNode: - displayNode.SetAndObserveColorNodeID(slicer.modules.colors.logic().GetPETColorNodeID(slicer.vtkMRMLPETProceduralColorNode.PETheat)) - - # initialize quantity and units codes - (quantity,units) = self.mapSOPClassUIDToDICOMQuantityAndUnits(sopClassUID) - if quantity is not None: - volumeNode.SetVoxelValueQuantity(quantity) - if units is not None: - volumeNode.SetVoxelValueUnits(units) - - def loadWithMultipleLoaders(self,loadable): - """Load using multiple paths (for testing) - """ - volumeNode = self.loadFilesWithArchetype(loadable.files, loadable.name+"-archetype") - self.setVolumeNodeProperties(volumeNode, loadable) - volumeNode = self.loadFilesWithSeriesReader("GDCM", loadable.files, loadable.name+"-gdcm", loadable.grayscale) - self.setVolumeNodeProperties(volumeNode, loadable) - volumeNode = self.loadFilesWithSeriesReader("DCMTK", loadable.files, loadable.name+"-dcmtk", loadable.grayscale) - self.setVolumeNodeProperties(volumeNode, loadable) - - return volumeNode - - def load(self,loadable,readerApproach=None): - """Load the select as a scalar volume using desired approach - """ - # first, determine which reader approach the user prefers - if not readerApproach: - readerIndex = slicer.util.settingsValue('DICOM/ScalarVolume/ReaderApproach', 0, converter=int) - readerApproach = DICOMScalarVolumePluginClass.readerApproaches()[readerIndex] - # second, try to load with the selected approach - if readerApproach == "Archetype": - volumeNode = self.loadFilesWithArchetype(loadable.files, loadable.name) - elif readerApproach == "GDCM with DCMTK fallback": - volumeNode = self.loadFilesWithSeriesReader("GDCM", loadable.files, loadable.name, loadable.grayscale) - if not volumeNode: - volumeNode = self.loadFilesWithSeriesReader("DCMTK", loadable.files, loadable.name, loadable.grayscale) - else: - volumeNode = self.loadFilesWithSeriesReader(readerApproach, loadable.files, loadable.name, loadable.grayscale) - # third, transfer data from the dicom instances into the appropriate Slicer data containers - self.setVolumeNodeProperties(volumeNode, loadable) - - # examine the loaded volume and if needed create a new transform - # that makes the loaded volume match the DICOM coordinates of - # the individual frames. Save the class instance so external - # code such as the DICOMReaders test can introspect to validate. - - if volumeNode: - self.acquisitionModeling = self.AcquisitionModeling() - self.acquisitionModeling.createAcquisitionTransform(volumeNode, - addAcquisitionTransformIfNeeded=self.acquisitionGeometryRegularizationEnabled()) - - return volumeNode - - def examineForExport(self,subjectHierarchyItemID): - """Return a list of DICOMExportable instances that describe the - available techniques that this plugin offers to convert MRML - data into DICOM data - """ - # cannot export if there is no data node or the data node is not a volume - shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - dataNode = shn.GetItemDataNode(subjectHierarchyItemID) - if dataNode is None or not dataNode.IsA('vtkMRMLScalarVolumeNode'): - return [] - - # Define basic properties of the exportable - exportable = slicer.qSlicerDICOMExportable() - exportable.name = self.loadType - exportable.tooltip = "Creates a series of DICOM files from scalar volumes" - exportable.subjectHierarchyItemID = subjectHierarchyItemID - exportable.pluginClass = self.__module__ - exportable.confidence = 0.5 # There could be more specialized volume types - - # Define required tags and default values - exportable.setTag('SeriesDescription', 'No series description') - exportable.setTag('Modality', 'CT') - exportable.setTag('Manufacturer', 'Unknown manufacturer') - exportable.setTag('Model', 'Unknown model') - exportable.setTag('StudyDate', '') - exportable.setTag('StudyTime', '') - exportable.setTag('StudyInstanceUID', '') - exportable.setTag('SeriesDate', '') - exportable.setTag('SeriesTime', '') - exportable.setTag('ContentDate', '') - exportable.setTag('ContentTime', '') - exportable.setTag('SeriesNumber', '1') - exportable.setTag('SeriesInstanceUID', '') - exportable.setTag('FrameOfReferenceUID', '') - - return [exportable] - - def export(self,exportables): - for exportable in exportables: - # Get volume node to export - shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - if shNode is None: - error = "Invalid subject hierarchy" - logging.error(error) - return error - volumeNode = shNode.GetItemDataNode(exportable.subjectHierarchyItemID) - if volumeNode is None or not volumeNode.IsA('vtkMRMLScalarVolumeNode'): - error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported" - logging.error(error) - return error - - # Get output directory and create a subdirectory. This is necessary - # to avoid overwriting the files in case of multiple exportables, as - # naming of the DICOM files is static - directoryName = 'ScalarVolume_' + str(exportable.subjectHierarchyItemID) - directoryDir = qt.QDir(exportable.directory) - directoryDir.mkpath(directoryName) - directoryDir.cd(directoryName) - directory = directoryDir.absolutePath() - logging.info("Export scalar volume '" + volumeNode.GetName() + "' to directory " + directory) - - # Get study and patient items - studyItemID = shNode.GetItemParent(exportable.subjectHierarchyItemID) - if not studyItemID: - error = "Unable to get study for series '" + volumeNode.GetName() + "'" - logging.error(error) - return error - patientItemID = shNode.GetItemParent(studyItemID) - if not patientItemID: - error = "Unable to get patient for series '" + volumeNode.GetName() + "'" - logging.error(error) - return error - - # Assemble tags dictionary for volume export - tags = {} - tags['Patient Name'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName()) - tags['Patient ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName()) - tags['Patient Birth Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName()) - tags['Patient Sex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName()) - tags['Patient Comments'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName()) - tags['Study ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDTagName()) - tags['Study Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName()) - tags['Study Time'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName()) - tags['Study Description'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName()) - tags['Modality'] = exportable.tag('Modality') - tags['Manufacturer'] = exportable.tag('Manufacturer') - tags['Model'] = exportable.tag('Model') - tags['Series Description'] = exportable.tag('SeriesDescription') - tags['Series Number'] = exportable.tag('SeriesNumber') - tags['Series Date'] = exportable.tag('SeriesDate') - tags['Series Time'] = exportable.tag('SeriesTime') - tags['Content Date'] = exportable.tag('ContentDate') - tags['Content Time'] = exportable.tag('ContentTime') - - tags['Study Instance UID'] = exportable.tag('StudyInstanceUID') - tags['Series Instance UID'] = exportable.tag('SeriesInstanceUID') - tags['Frame of Reference UID'] = exportable.tag('FrameOfReferenceUID') - - # Generate any missing but required UIDs - if not tags['Study Instance UID']: - import pydicom as dicom - tags['Study Instance UID'] = dicom.uid.generate_uid() - if not tags['Series Instance UID']: - import pydicom as dicom - tags['Series Instance UID'] = dicom.uid.generate_uid() - if not tags['Frame of Reference UID']: - import pydicom as dicom - tags['Frame of Reference UID'] = dicom.uid.generate_uid() - - # Use the default Study ID if none is specified - if not tags['Study ID']: - tags['Study ID'] = self.defaultStudyID - - # Validate tags - if tags['Modality'] == "": - error = "Empty modality for series '" + volumeNode.GetName() + "'" - logging.error(error) - return error - - seriesInstanceUID = tags['Series Instance UID'] - if seriesInstanceUID: - # Make sure we don't use a series instance UID that already exists (it would mix in more slices into an existing series, - # which is very unlikely that users would want). - db = slicer.dicomDatabase - studyInstanceUID = db.studyForSeries(seriesInstanceUID) - if studyInstanceUID: - # This seriesInstanceUID is already found in the database - if len(seriesInstanceUID)>25: - seriesInstanceUID = seriesInstanceUID[:20] + "..." - error = f"A series already exists in the database by SeriesInstanceUID {seriesInstanceUID}." - logging.error(error) - return error - - #TODO: more tag checks - - # Perform export - exporter = DICOMExportScalarVolume(tags['Study ID'], volumeNode, tags, directory) - if not exporter.export(): - return "Creating DICOM files from scalar volume failed. See the application log for details." - - # Success - return "" - - class AcquisitionModeling: - """Code for representing and analyzing acquisition properties in slicer - This is an internal class of the DICOMScalarVolumePluginClass so that - it can be used here and from within the DICOMReaders test. - - TODO: This code work on legacy single frame DICOM images that have position and orientation - flags in each instance (not on multiframe with per-frame positions). - """ - - def __init__(self,cornerEpsilon=1e-3,zeroEpsilon=1e-6): - """cornerEpsilon sets the threshold for the amount of difference between the - vtkITK generated volume geometry vs the DICOM geometry. Any spatial dimension with - a difference larger than cornerEpsilon will trigger the addition of a grid transform. - Any difference less than zeroEpsilon is assumed to be numerical error. - """ - self.cornerEpsilon = cornerEpsilon - self.zeroEpsilon = zeroEpsilon - - def gridTransformFromCorners(self,volumeNode,sourceCorners,targetCorners): - """Create a grid transform that maps between the current and the desired corners. - """ - # sanity check - columns, rows, slices = volumeNode.GetImageData().GetDimensions() - cornerShape = (slices, 2, 2, 3) - if not (sourceCorners.shape == cornerShape and targetCorners.shape == cornerShape): - raise Exception("Corner shapes do not match volume dimensions %s, %s, %s" % - (sourceCorners.shape, targetCorners.shape, cornerShape)) - - # create the grid transform node - gridTransform = slicer.vtkMRMLGridTransformNode() - gridTransform.SetName(slicer.mrmlScene.GenerateUniqueName(volumeNode.GetName()+' acquisition transform')) - slicer.mrmlScene.AddNode(gridTransform) - - # place grid transform in the same subject hierarchy folder as the volume node - shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - volumeParentItemId = shNode.GetItemParent(shNode.GetItemByDataNode(volumeNode)) - shNode.SetItemParent(shNode.GetItemByDataNode(gridTransform), volumeParentItemId) - - # create a grid transform with one vector at the corner of each slice - # the transform is in the same space and orientation as the volume node - gridImage = vtk.vtkImageData() - gridImage.SetOrigin(*volumeNode.GetOrigin()) - gridImage.SetDimensions(2, 2, slices) - sourceSpacing = volumeNode.GetSpacing() - gridImage.SetSpacing(sourceSpacing[0] * columns, sourceSpacing[1] * rows, sourceSpacing[2]) - gridImage.AllocateScalars(vtk.VTK_DOUBLE, 3) - transform = slicer.vtkOrientedGridTransform() - directionMatrix = vtk.vtkMatrix4x4() - volumeNode.GetIJKToRASDirectionMatrix(directionMatrix) - transform.SetGridDirectionMatrix(directionMatrix) - transform.SetDisplacementGridData(gridImage) - gridTransform.SetAndObserveTransformToParent(transform) - volumeNode.SetAndObserveTransformNodeID(gridTransform.GetID()) - - # populate the grid so that each corner of each slice - # is mapped from the source corner to the target corner - displacements = slicer.util.arrayFromGridTransform(gridTransform) - for sliceIndex in range(slices): - for row in range(2): - for column in range(2): - displacements[sliceIndex][row][column] = targetCorners[sliceIndex][row][column] - sourceCorners[sliceIndex][row][column] - - def sliceCornersFromDICOM(self,volumeNode): - """Calculate the RAS position of each of the four corners of each - slice of a volume node based on the dicom headers - - Note: PixelSpacing is row spacing followed by column spacing [1] (i.e. vertical then horizontal) - while ImageOrientationPatient is row cosines then column cosines [2] (i.e. horizontal then vertical). - [1] https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_10.7.1.1 - [2] https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.2 - """ - spacingTag = "0028,0030" - positionTag = "0020,0032" - orientationTag = "0020,0037" - - columns, rows, slices = volumeNode.GetImageData().GetDimensions() - corners = numpy.zeros(shape=[slices,2,2,3]) - instanceUIDsAttribute = volumeNode.GetAttribute('DICOM.instanceUIDs') - uids = instanceUIDsAttribute.split() if instanceUIDsAttribute else [] - if len(uids) != slices: - # There is no uid for each slice, so most likely all frames are in a single file - # or maybe there is a problem with the sequence - logging.warning("Cannot get DICOM slice positions for volume "+volumeNode.GetName()) - return None - for sliceIndex in range(slices): - uid = uids[sliceIndex] - # get slice geometry from instance - positionString = slicer.dicomDatabase.instanceValue(uid, positionTag) - orientationString = slicer.dicomDatabase.instanceValue(uid, orientationTag) - spacingString = slicer.dicomDatabase.instanceValue(uid, spacingTag) - if positionString == "" or orientationString == "" or spacingString == "": - logging.warning('No geometry information available for DICOM data, skipping corner calculations') - return None - - position = numpy.array(list(map(float, positionString.split('\\')))) - orientation = list(map(float, orientationString.split('\\'))) - rowOrientation = numpy.array(orientation[:3]) - columnOrientation = numpy.array(orientation[3:]) - spacing = numpy.array(list(map(float, spacingString.split('\\')))) - # map from LPS to RAS - lpsToRAS = numpy.array([-1,-1,1]) - position *= lpsToRAS - rowOrientation *= lpsToRAS - columnOrientation *= lpsToRAS - rowVector = columns * spacing[1] * rowOrientation # dicom PixelSpacing is between rows first, then columns - columnVector = rows * spacing[0] * columnOrientation - # apply the transform to the four corners - for column in range(2): - for row in range(2): - corners[sliceIndex][row][column] = position - corners[sliceIndex][row][column] += column * rowVector - corners[sliceIndex][row][column] += row * columnVector - return corners - - def sliceCornersFromIJKToRAS(self,volumeNode): - """Calculate the RAS position of each of the four corners of each - slice of a volume node based on the ijkToRAS matrix of the volume node - """ - ijkToRAS = vtk.vtkMatrix4x4() - volumeNode.GetIJKToRASMatrix(ijkToRAS) - columns, rows, slices = volumeNode.GetImageData().GetDimensions() - corners = numpy.zeros(shape=[slices,2,2,3]) - for sliceIndex in range(slices): - for column in range(2): - for row in range(2): - corners[sliceIndex][row][column] = numpy.array(ijkToRAS.MultiplyPoint([column * columns, row * rows, sliceIndex, 1])[:3]) - return corners - - def cornersToWorld(self,volumeNode,corners): - """Map corners through the volumeNodes transform to world - This can be used to confirm that an acquisition transform has correctly - mapped the slice corners to match the dicom acquisition. - """ - columns, rows, slices = volumeNode.GetImageData().GetDimensions() - worldCorners = numpy.zeros(shape=[slices,2,2,3]) - for slice in range(slices): - for row in range(2): - for column in range(2): - volumeNode.TransformPointToWorld(corners[slice,row,column], worldCorners[slice,row,column]) - return worldCorners - - def createAcquisitionTransform(self, volumeNode, addAcquisitionTransformIfNeeded = True): - """Creates the actual transform if needed. - Slice corners are cached for inpection by tests - """ - self.originalCorners = self.sliceCornersFromIJKToRAS(volumeNode) - self.targetCorners = self.sliceCornersFromDICOM(volumeNode) - if self.originalCorners is None or self.targetCorners is None: - # can't create transform without corner information - return - maxError = (abs(self.originalCorners - self.targetCorners)).max() - - if maxError > self.cornerEpsilon: - warningText = f"Irregular volume geometry detected (maximum error of {maxError:g} mm is above tolerance threshold of {self.cornerEpsilon:g} mm)." - if addAcquisitionTransformIfNeeded: - logging.warning(warningText + " Adding acquisition transform to regularize geometry.") - self.gridTransformFromCorners(volumeNode, self.originalCorners, self.targetCorners) - self.fixedCorners = self.cornersToWorld(volumeNode, self.originalCorners) - if not numpy.allclose(self.fixedCorners, self.targetCorners): - raise Exception("Acquisition transform didn't fix slice corners!") + reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile() + reader.SetArchetype(files[0]) + for f in files: + reader.AddFileName(f) + reader.SetSingleFile(0) + reader.SetOutputScalarTypeToNative() + reader.SetDesiredCoordinateOrientationToNative() + reader.SetUseNativeOriginOn() + if imageIOName == "GDCM": + reader.SetDICOMImageIOApproachToGDCM() + elif imageIOName == "DCMTK": + reader.SetDICOMImageIOApproachToDCMTK() + else: + raise Exception("Invalid imageIOName of %s" % imageIOName) + logging.info("Loading with imageIOName: %s" % imageIOName) + reader.Update() + + slicer.modules.reader = reader + if reader.GetErrorCode() != vtk.vtkErrorCode.NoError: + errorStrings = (imageIOName, vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode())) + logging.error("Could not read scalar volume using %s approach. Error is: %s" % errorStrings) + return + + imageChangeInformation = vtk.vtkImageChangeInformation() + imageChangeInformation.SetInputConnection(reader.GetOutputPort()) + imageChangeInformation.SetOutputSpacing(1, 1, 1) + imageChangeInformation.SetOutputOrigin(0, 0, 0) + imageChangeInformation.Update() + + name = slicer.mrmlScene.GenerateUniqueName(name) + if grayscale: + volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", name) else: - logging.warning(warningText + " Regularization transform is not added, as the option is disabled.") - elif maxError > 0 and maxError > self.zeroEpsilon: - logging.debug("Irregular volume geometry detected, but maximum error is within tolerance"+ - f" (maximum error of {maxError:g} mm, tolerance threshold is {self.cornerEpsilon:g} mm).") + volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode", name) + volumeNode.SetAndObserveImageData(imageChangeInformation.GetOutputDataObject(0)) + slicer.vtkMRMLVolumeArchetypeStorageNode.SetMetaDataDictionaryFromReader(volumeNode, reader) + volumeNode.SetRASToIJKMatrix(reader.GetRasToIjkMatrix()) + volumeNode.CreateDefaultDisplayNodes() + + slicer.modules.DICOMInstance.reader = reader + slicer.modules.DICOMInstance.imageChangeInformation = imageChangeInformation + + return(volumeNode) + + def setVolumeNodeProperties(self, volumeNode, loadable): + """After the scalar volume has been loaded, populate the node + attributes and display node with values extracted from the dicom instances + """ + if volumeNode: + # + # create subject hierarchy items for the loaded series + # + self.addSeriesInSubjectHierarchy(loadable, volumeNode) + + # + # add list of DICOM instance UIDs to the volume node + # corresponding to the loaded files + # + instanceUIDs = "" + for file in loadable.files: + uid = slicer.dicomDatabase.fileValue(file, self.tags['instanceUID']) + if uid == "": + uid = "Unknown" + instanceUIDs += uid + " " + instanceUIDs = instanceUIDs[:-1] # strip last space + volumeNode.SetAttribute("DICOM.instanceUIDs", instanceUIDs) + + # Choose a file in the middle of the series as representative frame, + # because that is more likely to contain the object of interest than the first or last frame. + # This is important for example for getting a relevant window/center value for the series. + file = loadable.files[int(len(loadable.files) / 2)] + + # + # automatically select the volume to display + # + appLogic = slicer.app.applicationLogic() + selNode = appLogic.GetSelectionNode() + selNode.SetActiveVolumeID(volumeNode.GetID()) + appLogic.PropagateVolumeSelection() + + # + # apply window/level from DICOM if available (the first pair that is found) + # Note: There can be multiple presets (multiplicity 1-n) in the standard [1]. We have + # a way to put these into the display node [2], so they can be selected in the Volumes + # module. + # [1] https://medical.nema.org/medical/dicom/current/output/html/part06.html + # [2] https://github.com/Slicer/Slicer/blob/3bfa2fc2b310d41c09b7a9e8f8f6c4f43d3bd1e2/Libs/MRML/Core/vtkMRMLScalarVolumeDisplayNode.h#L172 + # + try: + windowCenter = float(slicer.dicomDatabase.fileValue(file, self.tags['windowCenter'])) + windowWidth = float(slicer.dicomDatabase.fileValue(file, self.tags['windowWidth'])) + displayNode = volumeNode.GetDisplayNode() + if displayNode: + logging.info('Window/level found in DICOM tags (center=' + str(windowCenter) + ', width=' + str(windowWidth) + ') has been applied to volume ' + volumeNode.GetName()) + displayNode.AddWindowLevelPreset(windowWidth, windowCenter) + displayNode.SetWindowLevelFromPreset(0) + else: + logging.info('No display node: cannot use window/level found in DICOM tags') + except ValueError: + pass # DICOM tags cannot be parsed to floating point numbers + + sopClassUID = slicer.dicomDatabase.fileValue(file, self.tags['sopClassUID']) + + # initialize color lookup table + modality = self.mapSOPClassUIDToModality(sopClassUID) + if modality == "PT": + displayNode = volumeNode.GetDisplayNode() + if displayNode: + displayNode.SetAndObserveColorNodeID(slicer.modules.colors.logic().GetPETColorNodeID(slicer.vtkMRMLPETProceduralColorNode.PETheat)) + + # initialize quantity and units codes + (quantity, units) = self.mapSOPClassUIDToDICOMQuantityAndUnits(sopClassUID) + if quantity is not None: + volumeNode.SetVoxelValueQuantity(quantity) + if units is not None: + volumeNode.SetVoxelValueUnits(units) + + def loadWithMultipleLoaders(self, loadable): + """Load using multiple paths (for testing) + """ + volumeNode = self.loadFilesWithArchetype(loadable.files, loadable.name + "-archetype") + self.setVolumeNodeProperties(volumeNode, loadable) + volumeNode = self.loadFilesWithSeriesReader("GDCM", loadable.files, loadable.name + "-gdcm", loadable.grayscale) + self.setVolumeNodeProperties(volumeNode, loadable) + volumeNode = self.loadFilesWithSeriesReader("DCMTK", loadable.files, loadable.name + "-dcmtk", loadable.grayscale) + self.setVolumeNodeProperties(volumeNode, loadable) + + return volumeNode + + def load(self, loadable, readerApproach=None): + """Load the select as a scalar volume using desired approach + """ + # first, determine which reader approach the user prefers + if not readerApproach: + readerIndex = slicer.util.settingsValue('DICOM/ScalarVolume/ReaderApproach', 0, converter=int) + readerApproach = DICOMScalarVolumePluginClass.readerApproaches()[readerIndex] + # second, try to load with the selected approach + if readerApproach == "Archetype": + volumeNode = self.loadFilesWithArchetype(loadable.files, loadable.name) + elif readerApproach == "GDCM with DCMTK fallback": + volumeNode = self.loadFilesWithSeriesReader("GDCM", loadable.files, loadable.name, loadable.grayscale) + if not volumeNode: + volumeNode = self.loadFilesWithSeriesReader("DCMTK", loadable.files, loadable.name, loadable.grayscale) + else: + volumeNode = self.loadFilesWithSeriesReader(readerApproach, loadable.files, loadable.name, loadable.grayscale) + # third, transfer data from the dicom instances into the appropriate Slicer data containers + self.setVolumeNodeProperties(volumeNode, loadable) + + # examine the loaded volume and if needed create a new transform + # that makes the loaded volume match the DICOM coordinates of + # the individual frames. Save the class instance so external + # code such as the DICOMReaders test can introspect to validate. + + if volumeNode: + self.acquisitionModeling = self.AcquisitionModeling() + self.acquisitionModeling.createAcquisitionTransform(volumeNode, + addAcquisitionTransformIfNeeded=self.acquisitionGeometryRegularizationEnabled()) + + return volumeNode + + def examineForExport(self, subjectHierarchyItemID): + """Return a list of DICOMExportable instances that describe the + available techniques that this plugin offers to convert MRML + data into DICOM data + """ + # cannot export if there is no data node or the data node is not a volume + shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + dataNode = shn.GetItemDataNode(subjectHierarchyItemID) + if dataNode is None or not dataNode.IsA('vtkMRMLScalarVolumeNode'): + return [] + + # Define basic properties of the exportable + exportable = slicer.qSlicerDICOMExportable() + exportable.name = self.loadType + exportable.tooltip = "Creates a series of DICOM files from scalar volumes" + exportable.subjectHierarchyItemID = subjectHierarchyItemID + exportable.pluginClass = self.__module__ + exportable.confidence = 0.5 # There could be more specialized volume types + + # Define required tags and default values + exportable.setTag('SeriesDescription', 'No series description') + exportable.setTag('Modality', 'CT') + exportable.setTag('Manufacturer', 'Unknown manufacturer') + exportable.setTag('Model', 'Unknown model') + exportable.setTag('StudyDate', '') + exportable.setTag('StudyTime', '') + exportable.setTag('StudyInstanceUID', '') + exportable.setTag('SeriesDate', '') + exportable.setTag('SeriesTime', '') + exportable.setTag('ContentDate', '') + exportable.setTag('ContentTime', '') + exportable.setTag('SeriesNumber', '1') + exportable.setTag('SeriesInstanceUID', '') + exportable.setTag('FrameOfReferenceUID', '') + + return [exportable] + + def export(self, exportables): + for exportable in exportables: + # Get volume node to export + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + if shNode is None: + error = "Invalid subject hierarchy" + logging.error(error) + return error + volumeNode = shNode.GetItemDataNode(exportable.subjectHierarchyItemID) + if volumeNode is None or not volumeNode.IsA('vtkMRMLScalarVolumeNode'): + error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported" + logging.error(error) + return error + + # Get output directory and create a subdirectory. This is necessary + # to avoid overwriting the files in case of multiple exportables, as + # naming of the DICOM files is static + directoryName = 'ScalarVolume_' + str(exportable.subjectHierarchyItemID) + directoryDir = qt.QDir(exportable.directory) + directoryDir.mkpath(directoryName) + directoryDir.cd(directoryName) + directory = directoryDir.absolutePath() + logging.info("Export scalar volume '" + volumeNode.GetName() + "' to directory " + directory) + + # Get study and patient items + studyItemID = shNode.GetItemParent(exportable.subjectHierarchyItemID) + if not studyItemID: + error = "Unable to get study for series '" + volumeNode.GetName() + "'" + logging.error(error) + return error + patientItemID = shNode.GetItemParent(studyItemID) + if not patientItemID: + error = "Unable to get patient for series '" + volumeNode.GetName() + "'" + logging.error(error) + return error + + # Assemble tags dictionary for volume export + tags = {} + tags['Patient Name'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName()) + tags['Patient ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName()) + tags['Patient Birth Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName()) + tags['Patient Sex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName()) + tags['Patient Comments'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName()) + tags['Study ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDTagName()) + tags['Study Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName()) + tags['Study Time'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName()) + tags['Study Description'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName()) + tags['Modality'] = exportable.tag('Modality') + tags['Manufacturer'] = exportable.tag('Manufacturer') + tags['Model'] = exportable.tag('Model') + tags['Series Description'] = exportable.tag('SeriesDescription') + tags['Series Number'] = exportable.tag('SeriesNumber') + tags['Series Date'] = exportable.tag('SeriesDate') + tags['Series Time'] = exportable.tag('SeriesTime') + tags['Content Date'] = exportable.tag('ContentDate') + tags['Content Time'] = exportable.tag('ContentTime') + + tags['Study Instance UID'] = exportable.tag('StudyInstanceUID') + tags['Series Instance UID'] = exportable.tag('SeriesInstanceUID') + tags['Frame of Reference UID'] = exportable.tag('FrameOfReferenceUID') + + # Generate any missing but required UIDs + if not tags['Study Instance UID']: + import pydicom as dicom + tags['Study Instance UID'] = dicom.uid.generate_uid() + if not tags['Series Instance UID']: + import pydicom as dicom + tags['Series Instance UID'] = dicom.uid.generate_uid() + if not tags['Frame of Reference UID']: + import pydicom as dicom + tags['Frame of Reference UID'] = dicom.uid.generate_uid() + + # Use the default Study ID if none is specified + if not tags['Study ID']: + tags['Study ID'] = self.defaultStudyID + + # Validate tags + if tags['Modality'] == "": + error = "Empty modality for series '" + volumeNode.GetName() + "'" + logging.error(error) + return error + + seriesInstanceUID = tags['Series Instance UID'] + if seriesInstanceUID: + # Make sure we don't use a series instance UID that already exists (it would mix in more slices into an existing series, + # which is very unlikely that users would want). + db = slicer.dicomDatabase + studyInstanceUID = db.studyForSeries(seriesInstanceUID) + if studyInstanceUID: + # This seriesInstanceUID is already found in the database + if len(seriesInstanceUID) > 25: + seriesInstanceUID = seriesInstanceUID[:20] + "..." + error = f"A series already exists in the database by SeriesInstanceUID {seriesInstanceUID}." + logging.error(error) + return error + + # TODO: more tag checks + + # Perform export + exporter = DICOMExportScalarVolume(tags['Study ID'], volumeNode, tags, directory) + if not exporter.export(): + return "Creating DICOM files from scalar volume failed. See the application log for details." + + # Success + return "" + + class AcquisitionModeling: + """Code for representing and analyzing acquisition properties in slicer + This is an internal class of the DICOMScalarVolumePluginClass so that + it can be used here and from within the DICOMReaders test. + + TODO: This code work on legacy single frame DICOM images that have position and orientation + flags in each instance (not on multiframe with per-frame positions). + """ + + def __init__(self, cornerEpsilon=1e-3, zeroEpsilon=1e-6): + """cornerEpsilon sets the threshold for the amount of difference between the + vtkITK generated volume geometry vs the DICOM geometry. Any spatial dimension with + a difference larger than cornerEpsilon will trigger the addition of a grid transform. + Any difference less than zeroEpsilon is assumed to be numerical error. + """ + self.cornerEpsilon = cornerEpsilon + self.zeroEpsilon = zeroEpsilon + + def gridTransformFromCorners(self, volumeNode, sourceCorners, targetCorners): + """Create a grid transform that maps between the current and the desired corners. + """ + # sanity check + columns, rows, slices = volumeNode.GetImageData().GetDimensions() + cornerShape = (slices, 2, 2, 3) + if not (sourceCorners.shape == cornerShape and targetCorners.shape == cornerShape): + raise Exception("Corner shapes do not match volume dimensions %s, %s, %s" % + (sourceCorners.shape, targetCorners.shape, cornerShape)) + + # create the grid transform node + gridTransform = slicer.vtkMRMLGridTransformNode() + gridTransform.SetName(slicer.mrmlScene.GenerateUniqueName(volumeNode.GetName() + ' acquisition transform')) + slicer.mrmlScene.AddNode(gridTransform) + + # place grid transform in the same subject hierarchy folder as the volume node + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + volumeParentItemId = shNode.GetItemParent(shNode.GetItemByDataNode(volumeNode)) + shNode.SetItemParent(shNode.GetItemByDataNode(gridTransform), volumeParentItemId) + + # create a grid transform with one vector at the corner of each slice + # the transform is in the same space and orientation as the volume node + gridImage = vtk.vtkImageData() + gridImage.SetOrigin(*volumeNode.GetOrigin()) + gridImage.SetDimensions(2, 2, slices) + sourceSpacing = volumeNode.GetSpacing() + gridImage.SetSpacing(sourceSpacing[0] * columns, sourceSpacing[1] * rows, sourceSpacing[2]) + gridImage.AllocateScalars(vtk.VTK_DOUBLE, 3) + transform = slicer.vtkOrientedGridTransform() + directionMatrix = vtk.vtkMatrix4x4() + volumeNode.GetIJKToRASDirectionMatrix(directionMatrix) + transform.SetGridDirectionMatrix(directionMatrix) + transform.SetDisplacementGridData(gridImage) + gridTransform.SetAndObserveTransformToParent(transform) + volumeNode.SetAndObserveTransformNodeID(gridTransform.GetID()) + + # populate the grid so that each corner of each slice + # is mapped from the source corner to the target corner + displacements = slicer.util.arrayFromGridTransform(gridTransform) + for sliceIndex in range(slices): + for row in range(2): + for column in range(2): + displacements[sliceIndex][row][column] = targetCorners[sliceIndex][row][column] - sourceCorners[sliceIndex][row][column] + + def sliceCornersFromDICOM(self, volumeNode): + """Calculate the RAS position of each of the four corners of each + slice of a volume node based on the dicom headers + + Note: PixelSpacing is row spacing followed by column spacing [1] (i.e. vertical then horizontal) + while ImageOrientationPatient is row cosines then column cosines [2] (i.e. horizontal then vertical). + [1] https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_10.7.1.1 + [2] https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.2 + """ + spacingTag = "0028,0030" + positionTag = "0020,0032" + orientationTag = "0020,0037" + + columns, rows, slices = volumeNode.GetImageData().GetDimensions() + corners = numpy.zeros(shape=[slices, 2, 2, 3]) + instanceUIDsAttribute = volumeNode.GetAttribute('DICOM.instanceUIDs') + uids = instanceUIDsAttribute.split() if instanceUIDsAttribute else [] + if len(uids) != slices: + # There is no uid for each slice, so most likely all frames are in a single file + # or maybe there is a problem with the sequence + logging.warning("Cannot get DICOM slice positions for volume " + volumeNode.GetName()) + return None + for sliceIndex in range(slices): + uid = uids[sliceIndex] + # get slice geometry from instance + positionString = slicer.dicomDatabase.instanceValue(uid, positionTag) + orientationString = slicer.dicomDatabase.instanceValue(uid, orientationTag) + spacingString = slicer.dicomDatabase.instanceValue(uid, spacingTag) + if positionString == "" or orientationString == "" or spacingString == "": + logging.warning('No geometry information available for DICOM data, skipping corner calculations') + return None + + position = numpy.array(list(map(float, positionString.split('\\')))) + orientation = list(map(float, orientationString.split('\\'))) + rowOrientation = numpy.array(orientation[:3]) + columnOrientation = numpy.array(orientation[3:]) + spacing = numpy.array(list(map(float, spacingString.split('\\')))) + # map from LPS to RAS + lpsToRAS = numpy.array([-1, -1, 1]) + position *= lpsToRAS + rowOrientation *= lpsToRAS + columnOrientation *= lpsToRAS + rowVector = columns * spacing[1] * rowOrientation # dicom PixelSpacing is between rows first, then columns + columnVector = rows * spacing[0] * columnOrientation + # apply the transform to the four corners + for column in range(2): + for row in range(2): + corners[sliceIndex][row][column] = position + corners[sliceIndex][row][column] += column * rowVector + corners[sliceIndex][row][column] += row * columnVector + return corners + + def sliceCornersFromIJKToRAS(self, volumeNode): + """Calculate the RAS position of each of the four corners of each + slice of a volume node based on the ijkToRAS matrix of the volume node + """ + ijkToRAS = vtk.vtkMatrix4x4() + volumeNode.GetIJKToRASMatrix(ijkToRAS) + columns, rows, slices = volumeNode.GetImageData().GetDimensions() + corners = numpy.zeros(shape=[slices, 2, 2, 3]) + for sliceIndex in range(slices): + for column in range(2): + for row in range(2): + corners[sliceIndex][row][column] = numpy.array(ijkToRAS.MultiplyPoint([column * columns, row * rows, sliceIndex, 1])[:3]) + return corners + + def cornersToWorld(self, volumeNode, corners): + """Map corners through the volumeNodes transform to world + This can be used to confirm that an acquisition transform has correctly + mapped the slice corners to match the dicom acquisition. + """ + columns, rows, slices = volumeNode.GetImageData().GetDimensions() + worldCorners = numpy.zeros(shape=[slices, 2, 2, 3]) + for slice in range(slices): + for row in range(2): + for column in range(2): + volumeNode.TransformPointToWorld(corners[slice, row, column], worldCorners[slice, row, column]) + return worldCorners + + def createAcquisitionTransform(self, volumeNode, addAcquisitionTransformIfNeeded=True): + """Creates the actual transform if needed. + Slice corners are cached for inpection by tests + """ + self.originalCorners = self.sliceCornersFromIJKToRAS(volumeNode) + self.targetCorners = self.sliceCornersFromDICOM(volumeNode) + if self.originalCorners is None or self.targetCorners is None: + # can't create transform without corner information + return + maxError = (abs(self.originalCorners - self.targetCorners)).max() + + if maxError > self.cornerEpsilon: + warningText = f"Irregular volume geometry detected (maximum error of {maxError:g} mm is above tolerance threshold of {self.cornerEpsilon:g} mm)." + if addAcquisitionTransformIfNeeded: + logging.warning(warningText + " Adding acquisition transform to regularize geometry.") + self.gridTransformFromCorners(volumeNode, self.originalCorners, self.targetCorners) + self.fixedCorners = self.cornersToWorld(volumeNode, self.originalCorners) + if not numpy.allclose(self.fixedCorners, self.targetCorners): + raise Exception("Acquisition transform didn't fix slice corners!") + else: + logging.warning(warningText + " Regularization transform is not added, as the option is disabled.") + elif maxError > 0 and maxError > self.zeroEpsilon: + logging.debug("Irregular volume geometry detected, but maximum error is within tolerance" + + f" (maximum error of {maxError:g} mm, tolerance threshold is {self.cornerEpsilon:g} mm).") # @@ -849,34 +849,34 @@ def createAcquisitionTransform(self, volumeNode, addAcquisitionTransformIfNeeded # class DICOMScalarVolumePlugin: - """ - 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 Scalar Volume Plugin" - parent.categories = ["Developer Tools.DICOM Plugins"] - parent.contributors = ["Steve Pieper (Isomics Inc.), Csaba Pinter (Queen's)"] - parent.helpText = """ + """ + 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 Scalar Volume Plugin" + parent.categories = ["Developer Tools.DICOM Plugins"] + parent.contributors = ["Steve Pieper (Isomics Inc.), Csaba Pinter (Queen's)"] + parent.helpText = """ Plugin to the DICOM Module to parse and load scalar volumes from DICOM files. No module interface here, only in the DICOM module """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This DICOM Plugin was developed by Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218. """ - # don't show this module - it only appears in the DICOM module - parent.hidden = True - - # 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['DICOMScalarVolumePlugin'] = DICOMScalarVolumePluginClass + # don't show this module - it only appears in the DICOM module + parent.hidden = True + + # 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['DICOMScalarVolumePlugin'] = DICOMScalarVolumePluginClass diff --git a/Modules/Scripted/DICOMPlugins/DICOMSlicerDataBundlePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMSlicerDataBundlePlugin.py index c5769825dc3..eab648dc128 100644 --- a/Modules/Scripted/DICOMPlugins/DICOMSlicerDataBundlePlugin.py +++ b/Modules/Scripted/DICOMPlugins/DICOMSlicerDataBundlePlugin.py @@ -16,217 +16,217 @@ # class DICOMSlicerDataBundlePluginClass(DICOMPlugin): - """ DICOM import/export plugin for Slicer Scene Bundle - (MRML scene file embedded in private tag of a DICOM file) - """ - - def __init__(self): - super().__init__() - self.loadType = "Slicer Data Bundle" - self.tags['seriesDescription'] = "0008,103e" - self.tags['candygram'] = "cadb,0010" - self.tags['zipSize'] = "cadb,1008" - self.tags['zipData'] = "cadb,1010" - - def examineForImport(self, fileLists): - """ Returns a list of DICOMLoadable instances - corresponding to ways of interpreting the - fileLists parameter. - """ - loadables = [] - for files in fileLists: - cachedLoadables = self.getCachedLoadables(files) - if cachedLoadables: - loadables += cachedLoadables - else: - loadablesForFiles = self.examineFiles(files) - loadables += loadablesForFiles - self.cacheLoadables(files, loadablesForFiles) - return loadables - - def examineFiles(self, files): - """ Returns a list of DICOMLoadable instances - corresponding to ways of interpreting the - files parameter. - Look for the special private creator tags that indicate - a slicer data bundle - Note that each data bundle is in a unique series, so - if 'files' is a list of more than one element, then this - is not a data bundle. - """ - - loadables = [] - if len(files) == 1: - f = files[0] - # get the series description to use as base for volume name - name = slicer.dicomDatabase.fileValue(f, self.tags['seriesDescription']) - if name == "": - name = "Unknown" - candygramValue = slicer.dicomDatabase.fileValue(f, self.tags['candygram']) - - if candygramValue: - # default loadable includes all files for series - loadable = DICOMLoadable() - loadable.files = [f] - loadable.name = name + ' - as Slicer Scene' - loadable.selected = True - loadable.tooltip = 'Contains a Slicer scene' - loadable.confidence = 0.9 - loadables.append(loadable) - return loadables - - def load(self, loadable): - """Load the selection as a data bundle - by extracting the embedded zip file and passing it to the application logic - """ - - f = loadable.files[0] - - try: - # TODO: this method should work, but not correctly encoded in real tag - zipSizeString = slicer.dicomDatabase.fileValue(f, self.tags['zipSize']) - zipSize = int(zipSizeString) - # instead use this hack where the number is in the creator string - candygramValue = slicer.dicomDatabase.fileValue(f, self.tags['candygram']) - zipSize = int(candygramValue.split(' ')[2]) - except ValueError: - logging.error("Could not get zipSize for %s" % f) - return False - - logging.info('importing file: %s' % f) - logging.info('size: %d' % zipSize) - - # require that the databundle be the last element of the file - # so we can seek from the end by the size of the zip data - sceneDir = tempfile.mkdtemp('', 'sceneImport', slicer.app.temporaryPath) - fp = open(f, 'rb') - - # The previous code only works for files with odd number of bits. - if zipSize % 2 == 0: - fp.seek(-1 * (zipSize), os.SEEK_END) - else: - fp.seek(-1 * (1 + zipSize), os.SEEK_END) - zipData = fp.read(zipSize) - fp.close() - - # save to a temp zip file - zipPath = os.path.join(sceneDir, 'scene.zip') - fp = open(zipPath, 'wb') - fp.write(zipData) - fp.close() - - logging.info('saved zip file to: %s' % zipPath) - - nodesBeforeLoading = slicer.util.getNodes() - - # let the scene unpack it and load it - appLogic = slicer.app.applicationLogic() - sceneFile = appLogic.OpenSlicerDataBundle(zipPath, sceneDir) - logging.info("loaded %s" % sceneFile) - - # Create subject hierarchy items for the loaded series. - # In order for the series information are saved in the scene (and subject hierarchy - # creation does not fail), a "main" data node needs to be selected: the first volume, - # model, or markups node is used as series node. - # TODO: Maybe all the nodes containing data could be added under the study, but - # the DICOM plugins don't support it yet. - dataNode = None - nodesAfterLoading = slicer.util.getNodes() - loadedNodes = [node for node in list(nodesAfterLoading.values()) if - node not in list(nodesBeforeLoading.values())] - for node in loadedNodes: - if node.IsA('vtkMRMLScalarVolumeNode'): - dataNode = node - if dataNode is None: - for node in loadedNodes: - if node.IsA('vtkMRMLModelNode') and node.GetName() not in ['Red Volume Slice', 'Yellow Volume Slice', - 'Green Volume Slice']: - dataNode = node - break - if dataNode is None: - for node in loadedNodes: - if node.IsA('vtkMRMLMarkupsNode'): - dataNode = node - break - if dataNode is not None: - self.addSeriesInSubjectHierarchy(loadable, dataNode) - else: - logging.warning('Failed to find suitable series node in loaded scene') - - return sceneFile != "" - - def examineForExport(self, subjectHierarchyItemID): - """Return a list of DICOMExportable instances that describe the - available techniques that this plugin offers to convert MRML - data into DICOM data + """ DICOM import/export plugin for Slicer Scene Bundle + (MRML scene file embedded in private tag of a DICOM file) """ - # Define basic properties of the exportable - exportable = slicer.qSlicerDICOMExportable() - exportable.name = "Slicer data bundle" - exportable.tooltip = "Creates a series that embeds the entire Slicer scene in a private DICOM tag" - exportable.subjectHierarchyItemID = subjectHierarchyItemID - exportable.pluginClass = self.__module__ - exportable.confidence = 0.1 # There could be more specialized volume types - - # Do not define tags (exportable.setTag) because they would overwrite values in the reference series - - return [exportable] - - def export(self, exportables): - shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - if shNode is None: - error = "Invalid subject hierarchy" - logging.error(error) - return error - dicomFiles = [] - for exportable in exportables: - # Find reference series (series that will be modified into a scene data bundle) - # Get DICOM UID - can be study instance UID or series instance UID - dicomUid = shNode.GetItemUID(exportable.subjectHierarchyItemID, - slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName()) - if not dicomUid: - continue - # Get series instance UID - if shNode.GetItemLevel(exportable.subjectHierarchyItemID) == slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy(): - # Study is selected - seriesInstanceUids = slicer.dicomDatabase.seriesForStudy(dicomUid) - seriesInstanceUid = seriesInstanceUids[0] if seriesInstanceUids else None - else: - # Series is selected - seriesInstanceUid = dicomUid - # Get first file of the series - dicomFiles = slicer.dicomDatabase.filesForSeries(seriesInstanceUid, 1) - if not dicomFiles: - continue - break - if not dicomFiles: - error = "Slicer data bundle export failed. No file is found for any of the selected items." - logging.error(error) - return error - - # Assemble tags dictionary for volume export - tags = {} - tags['PatientName'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName()) - tags['PatientID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName()) - tags['PatientBirthDate'] = exportable.tag( - slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName()) - tags['PatientSex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName()) - tags['PatientComments'] = exportable.tag( - slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName()) - - tags['StudyDate'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName()) - tags['StudyTime'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName()) - tags['StudyDescription'] = exportable.tag( - slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName()) - - # Perform export - exporter = DICOMExportScene(dicomFiles[0], exportable.directory) - exporter.optionalTags = tags - exporter.export() - - # Success - return "" + def __init__(self): + super().__init__() + self.loadType = "Slicer Data Bundle" + self.tags['seriesDescription'] = "0008,103e" + self.tags['candygram'] = "cadb,0010" + self.tags['zipSize'] = "cadb,1008" + self.tags['zipData'] = "cadb,1010" + + def examineForImport(self, fileLists): + """ Returns a list of DICOMLoadable instances + corresponding to ways of interpreting the + fileLists parameter. + """ + loadables = [] + for files in fileLists: + cachedLoadables = self.getCachedLoadables(files) + if cachedLoadables: + loadables += cachedLoadables + else: + loadablesForFiles = self.examineFiles(files) + loadables += loadablesForFiles + self.cacheLoadables(files, loadablesForFiles) + return loadables + + def examineFiles(self, files): + """ Returns a list of DICOMLoadable instances + corresponding to ways of interpreting the + files parameter. + Look for the special private creator tags that indicate + a slicer data bundle + Note that each data bundle is in a unique series, so + if 'files' is a list of more than one element, then this + is not a data bundle. + """ + + loadables = [] + if len(files) == 1: + f = files[0] + # get the series description to use as base for volume name + name = slicer.dicomDatabase.fileValue(f, self.tags['seriesDescription']) + if name == "": + name = "Unknown" + candygramValue = slicer.dicomDatabase.fileValue(f, self.tags['candygram']) + + if candygramValue: + # default loadable includes all files for series + loadable = DICOMLoadable() + loadable.files = [f] + loadable.name = name + ' - as Slicer Scene' + loadable.selected = True + loadable.tooltip = 'Contains a Slicer scene' + loadable.confidence = 0.9 + loadables.append(loadable) + return loadables + + def load(self, loadable): + """Load the selection as a data bundle + by extracting the embedded zip file and passing it to the application logic + """ + + f = loadable.files[0] + + try: + # TODO: this method should work, but not correctly encoded in real tag + zipSizeString = slicer.dicomDatabase.fileValue(f, self.tags['zipSize']) + zipSize = int(zipSizeString) + # instead use this hack where the number is in the creator string + candygramValue = slicer.dicomDatabase.fileValue(f, self.tags['candygram']) + zipSize = int(candygramValue.split(' ')[2]) + except ValueError: + logging.error("Could not get zipSize for %s" % f) + return False + + logging.info('importing file: %s' % f) + logging.info('size: %d' % zipSize) + + # require that the databundle be the last element of the file + # so we can seek from the end by the size of the zip data + sceneDir = tempfile.mkdtemp('', 'sceneImport', slicer.app.temporaryPath) + fp = open(f, 'rb') + + # The previous code only works for files with odd number of bits. + if zipSize % 2 == 0: + fp.seek(-1 * (zipSize), os.SEEK_END) + else: + fp.seek(-1 * (1 + zipSize), os.SEEK_END) + zipData = fp.read(zipSize) + fp.close() + + # save to a temp zip file + zipPath = os.path.join(sceneDir, 'scene.zip') + fp = open(zipPath, 'wb') + fp.write(zipData) + fp.close() + + logging.info('saved zip file to: %s' % zipPath) + + nodesBeforeLoading = slicer.util.getNodes() + + # let the scene unpack it and load it + appLogic = slicer.app.applicationLogic() + sceneFile = appLogic.OpenSlicerDataBundle(zipPath, sceneDir) + logging.info("loaded %s" % sceneFile) + + # Create subject hierarchy items for the loaded series. + # In order for the series information are saved in the scene (and subject hierarchy + # creation does not fail), a "main" data node needs to be selected: the first volume, + # model, or markups node is used as series node. + # TODO: Maybe all the nodes containing data could be added under the study, but + # the DICOM plugins don't support it yet. + dataNode = None + nodesAfterLoading = slicer.util.getNodes() + loadedNodes = [node for node in list(nodesAfterLoading.values()) if + node not in list(nodesBeforeLoading.values())] + for node in loadedNodes: + if node.IsA('vtkMRMLScalarVolumeNode'): + dataNode = node + if dataNode is None: + for node in loadedNodes: + if node.IsA('vtkMRMLModelNode') and node.GetName() not in ['Red Volume Slice', 'Yellow Volume Slice', + 'Green Volume Slice']: + dataNode = node + break + if dataNode is None: + for node in loadedNodes: + if node.IsA('vtkMRMLMarkupsNode'): + dataNode = node + break + if dataNode is not None: + self.addSeriesInSubjectHierarchy(loadable, dataNode) + else: + logging.warning('Failed to find suitable series node in loaded scene') + + return sceneFile != "" + + def examineForExport(self, subjectHierarchyItemID): + """Return a list of DICOMExportable instances that describe the + available techniques that this plugin offers to convert MRML + data into DICOM data + """ + + # Define basic properties of the exportable + exportable = slicer.qSlicerDICOMExportable() + exportable.name = "Slicer data bundle" + exportable.tooltip = "Creates a series that embeds the entire Slicer scene in a private DICOM tag" + exportable.subjectHierarchyItemID = subjectHierarchyItemID + exportable.pluginClass = self.__module__ + exportable.confidence = 0.1 # There could be more specialized volume types + + # Do not define tags (exportable.setTag) because they would overwrite values in the reference series + + return [exportable] + + def export(self, exportables): + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + if shNode is None: + error = "Invalid subject hierarchy" + logging.error(error) + return error + dicomFiles = [] + for exportable in exportables: + # Find reference series (series that will be modified into a scene data bundle) + # Get DICOM UID - can be study instance UID or series instance UID + dicomUid = shNode.GetItemUID(exportable.subjectHierarchyItemID, + slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName()) + if not dicomUid: + continue + # Get series instance UID + if shNode.GetItemLevel(exportable.subjectHierarchyItemID) == slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy(): + # Study is selected + seriesInstanceUids = slicer.dicomDatabase.seriesForStudy(dicomUid) + seriesInstanceUid = seriesInstanceUids[0] if seriesInstanceUids else None + else: + # Series is selected + seriesInstanceUid = dicomUid + # Get first file of the series + dicomFiles = slicer.dicomDatabase.filesForSeries(seriesInstanceUid, 1) + if not dicomFiles: + continue + break + if not dicomFiles: + error = "Slicer data bundle export failed. No file is found for any of the selected items." + logging.error(error) + return error + + # Assemble tags dictionary for volume export + tags = {} + tags['PatientName'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName()) + tags['PatientID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName()) + tags['PatientBirthDate'] = exportable.tag( + slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName()) + tags['PatientSex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName()) + tags['PatientComments'] = exportable.tag( + slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName()) + + tags['StudyDate'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName()) + tags['StudyTime'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName()) + tags['StudyDescription'] = exportable.tag( + slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName()) + + # Perform export + exporter = DICOMExportScene(dicomFiles[0], exportable.directory) + exporter.optionalTags = tags + exporter.export() + + # Success + return "" # @@ -234,37 +234,37 @@ def export(self, exportables): # class DICOMSlicerDataBundlePlugin: - """ - 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 Diffusion Volume Plugin" - parent.categories = ["Developer Tools.DICOM Plugins"] - parent.contributors = ["Steve Pieper (Isomics Inc.), Csaba Pinter (Pixel Medical, Inc.)"] - parent.helpText = """ + """ + 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 Diffusion Volume Plugin" + parent.categories = ["Developer Tools.DICOM Plugins"] + parent.contributors = ["Steve Pieper (Isomics Inc.), Csaba Pinter (Pixel Medical, Inc.)"] + parent.helpText = """ Plugin to the DICOM Module to parse and load diffusion volumes from DICOM files. No module interface here, only in the DICOM module """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This DICOM Plugin was developed by Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218. """ - # don't show this module - it only appears in the DICOM module - parent.hidden = True + # don't show this module - it only appears in the DICOM module + parent.hidden = True - # 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['DICOMSlicerDataBundlePlugin'] = DICOMSlicerDataBundlePluginClass + # 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['DICOMSlicerDataBundlePlugin'] = DICOMSlicerDataBundlePluginClass # @@ -272,15 +272,15 @@ def __init__(self, parent): # class DICOMSlicerDataBundleWidget: - def __init__(self, parent=None): - self.parent = parent + def __init__(self, parent=None): + self.parent = parent - def setup(self): - # don't display anything for this widget - it will be hidden anyway - pass + def setup(self): + # don't display anything for this widget - it will be hidden anyway + pass - def enter(self): - pass + def enter(self): + pass - def exit(self): - pass + def exit(self): + pass diff --git a/Modules/Scripted/DICOMPlugins/DICOMVolumeSequencePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMVolumeSequencePlugin.py index 7c0cb0d68d1..0181e4006fb 100644 --- a/Modules/Scripted/DICOMPlugins/DICOMVolumeSequencePlugin.py +++ b/Modules/Scripted/DICOMPlugins/DICOMVolumeSequencePlugin.py @@ -15,249 +15,249 @@ # class DICOMVolumeSequencePluginClass(DICOMPlugin): - """ Volume sequence export plugin - """ - - def __init__(self): - super().__init__() - self.loadType = "Volume Sequence" - - self.tags['studyID'] = '0020,0010' - self.tags['seriesDescription'] = "0008,103e" - self.tags['seriesUID'] = "0020,000E" - self.tags['seriesNumber'] = "0020,0011" - self.tags['seriesDate'] = "0008,0021" - self.tags['seriesTime'] = "0020,0031" - self.tags['position'] = "0020,0032" - self.tags['orientation'] = "0020,0037" - self.tags['pixelData'] = "7fe0,0010" - self.tags['seriesInstanceUID'] = "0020,000E" - self.tags['contentTime'] = "0008,0033" - self.tags['triggerTime'] = "0018,1060" - self.tags['diffusionGradientOrientation'] = "0018,9089" - self.tags['imageOrientationPatient'] = "0020,0037" - self.tags['numberOfFrames'] = "0028,0008" - self.tags['instanceUID'] = "0008,0018" - self.tags['windowCenter'] = "0028,1050" - self.tags['windowWidth'] = "0028,1051" - self.tags['classUID'] = "0008,0016" - - def getSequenceBrowserNodeForMasterOutputNode(self, masterOutputNode): - browserNodes = slicer.mrmlScene.GetNodesByClass('vtkMRMLSequenceBrowserNode') - browserNodes.UnRegister(None) - for itemIndex in range(browserNodes.GetNumberOfItems()): - sequenceBrowserNode = browserNodes.GetItemAsObject(itemIndex) - if sequenceBrowserNode.GetProxyNode(sequenceBrowserNode.GetMasterSequenceNode()) == masterOutputNode: - return sequenceBrowserNode - return None - - def examineForExport(self, subjectHierarchyItemID): - """Return a list of DICOMExportable instances that describe the - available techniques that this plugin offers to convert MRML - data into DICOM data + """ Volume sequence export plugin """ - # Check if setting of DICOM UIDs is supported (if not, then we cannot export to sequence) - dicomUIDSettingSupported = False - createDicomSeriesParameterNode = slicer.modules.createdicomseries.cliModuleLogic().CreateNode() - # CreateNode() factory method incremented the reference count, we decrement now prevent memory leaks - # (a reference is still kept by createDicomSeriesParameterNode Python variable). - createDicomSeriesParameterNode.UnRegister(None) - for groupIndex in range(createDicomSeriesParameterNode.GetNumberOfParameterGroups()): - if createDicomSeriesParameterNode.GetParameterGroupLabel(groupIndex) == "Unique Identifiers (UIDs)": - dicomUIDSettingSupported = True - if not dicomUIDSettingSupported: - # This version of Slicer does not allow setting DICOM UIDs for export - return [] - - # cannot export if there is no data node or the data node is not a volume - shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - dataNode = shn.GetItemDataNode(subjectHierarchyItemID) - if dataNode is None or not dataNode.IsA('vtkMRMLScalarVolumeNode'): - # not a volume node - return [] - - sequenceBrowserNode = self.getSequenceBrowserNodeForMasterOutputNode(dataNode) - if not sequenceBrowserNode: - # this seems to be a simple volume node (not a proxy node of a volume - # sequence). This plugin only deals with volume sequences. - return [] - - sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes() - if sequenceItemCount <= 1: - # this plugin is only relevant if there are multiple items in the sequence - return [] - - # Define basic properties of the exportable - exportable = slicer.qSlicerDICOMExportable() - exportable.name = self.loadType - exportable.tooltip = "Creates a series of DICOM files from volume sequences" - exportable.subjectHierarchyItemID = subjectHierarchyItemID - exportable.pluginClass = self.__module__ - exportable.confidence = 0.6 # Simple volume has confidence of 0.5, use a slightly higher value here - - # Define required tags and default values - exportable.setTag('SeriesDescription', f'Volume sequence of {sequenceItemCount} frames') - exportable.setTag('Modality', 'CT') - exportable.setTag('Manufacturer', 'Unknown manufacturer') - exportable.setTag('Model', 'Unknown model') - exportable.setTag('StudyID', '1') - exportable.setTag('SeriesNumber', '1') - exportable.setTag('SeriesDate', '') - exportable.setTag('SeriesTime', '') - - return [exportable] - - def datetimeFromDicom(self, dt, tm): - year = 0 - month = 0 - day = 0 - if len(dt)==8: # YYYYMMDD - year = int(dt[0:4]) - month = int(dt[4:6]) - day = int(dt[6:8]) - else: - raise OSError("Invalid DICOM date string: "+tm+" (failed to parse YYYYMMDD)") - - hour = 0 - minute = 0 - second = 0 - microsecond = 0 - if len(tm)>=6: - try: - hhmmss = str.split(tm,'.')[0] - except: - hhmmss = tm - try: - microsecond = int(float('0.'+str.split(tm,'.')[1]) * 1e6) - except: + def __init__(self): + super().__init__() + self.loadType = "Volume Sequence" + + self.tags['studyID'] = '0020,0010' + self.tags['seriesDescription'] = "0008,103e" + self.tags['seriesUID'] = "0020,000E" + self.tags['seriesNumber'] = "0020,0011" + self.tags['seriesDate'] = "0008,0021" + self.tags['seriesTime'] = "0020,0031" + self.tags['position'] = "0020,0032" + self.tags['orientation'] = "0020,0037" + self.tags['pixelData'] = "7fe0,0010" + self.tags['seriesInstanceUID'] = "0020,000E" + self.tags['contentTime'] = "0008,0033" + self.tags['triggerTime'] = "0018,1060" + self.tags['diffusionGradientOrientation'] = "0018,9089" + self.tags['imageOrientationPatient'] = "0020,0037" + self.tags['numberOfFrames'] = "0028,0008" + self.tags['instanceUID'] = "0008,0018" + self.tags['windowCenter'] = "0028,1050" + self.tags['windowWidth'] = "0028,1051" + self.tags['classUID'] = "0008,0016" + + def getSequenceBrowserNodeForMasterOutputNode(self, masterOutputNode): + browserNodes = slicer.mrmlScene.GetNodesByClass('vtkMRMLSequenceBrowserNode') + browserNodes.UnRegister(None) + for itemIndex in range(browserNodes.GetNumberOfItems()): + sequenceBrowserNode = browserNodes.GetItemAsObject(itemIndex) + if sequenceBrowserNode.GetProxyNode(sequenceBrowserNode.GetMasterSequenceNode()) == masterOutputNode: + return sequenceBrowserNode + return None + + def examineForExport(self, subjectHierarchyItemID): + """Return a list of DICOMExportable instances that describe the + available techniques that this plugin offers to convert MRML + data into DICOM data + """ + + # Check if setting of DICOM UIDs is supported (if not, then we cannot export to sequence) + dicomUIDSettingSupported = False + createDicomSeriesParameterNode = slicer.modules.createdicomseries.cliModuleLogic().CreateNode() + # CreateNode() factory method incremented the reference count, we decrement now prevent memory leaks + # (a reference is still kept by createDicomSeriesParameterNode Python variable). + createDicomSeriesParameterNode.UnRegister(None) + for groupIndex in range(createDicomSeriesParameterNode.GetNumberOfParameterGroups()): + if createDicomSeriesParameterNode.GetParameterGroupLabel(groupIndex) == "Unique Identifiers (UIDs)": + dicomUIDSettingSupported = True + if not dicomUIDSettingSupported: + # This version of Slicer does not allow setting DICOM UIDs for export + return [] + + # cannot export if there is no data node or the data node is not a volume + shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + dataNode = shn.GetItemDataNode(subjectHierarchyItemID) + if dataNode is None or not dataNode.IsA('vtkMRMLScalarVolumeNode'): + # not a volume node + return [] + + sequenceBrowserNode = self.getSequenceBrowserNodeForMasterOutputNode(dataNode) + if not sequenceBrowserNode: + # this seems to be a simple volume node (not a proxy node of a volume + # sequence). This plugin only deals with volume sequences. + return [] + + sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes() + if sequenceItemCount <= 1: + # this plugin is only relevant if there are multiple items in the sequence + return [] + + # Define basic properties of the exportable + exportable = slicer.qSlicerDICOMExportable() + exportable.name = self.loadType + exportable.tooltip = "Creates a series of DICOM files from volume sequences" + exportable.subjectHierarchyItemID = subjectHierarchyItemID + exportable.pluginClass = self.__module__ + exportable.confidence = 0.6 # Simple volume has confidence of 0.5, use a slightly higher value here + + # Define required tags and default values + exportable.setTag('SeriesDescription', f'Volume sequence of {sequenceItemCount} frames') + exportable.setTag('Modality', 'CT') + exportable.setTag('Manufacturer', 'Unknown manufacturer') + exportable.setTag('Model', 'Unknown model') + exportable.setTag('StudyID', '1') + exportable.setTag('SeriesNumber', '1') + exportable.setTag('SeriesDate', '') + exportable.setTag('SeriesTime', '') + + return [exportable] + + def datetimeFromDicom(self, dt, tm): + year = 0 + month = 0 + day = 0 + if len(dt) == 8: # YYYYMMDD + year = int(dt[0:4]) + month = int(dt[4:6]) + day = int(dt[6:8]) + else: + raise OSError("Invalid DICOM date string: " + tm + " (failed to parse YYYYMMDD)") + + hour = 0 + minute = 0 + second = 0 microsecond = 0 - if len(hhmmss)==6: # HHMMSS - hour = int(hhmmss[0:2]) - minute = int(hhmmss[2:4]) - second = int(hhmmss[4:6]) - elif len(hhmmss)==4: # HHMM - hour = int(hhmmss[0:2]) - minute = int(hhmmss[2:4]) - elif len(hhmmss)==2: # HH - hour = int(hhmmss[0:2]) - else: - raise OSError("Invalid DICOM time string: "+tm+" (failed to parse HHMMSS)") - - import datetime - return datetime.datetime(year, month, day, hour, minute, second, microsecond) - - def export(self,exportables): - for exportable in exportables: - # Get volume node to export - shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) - if shNode is None: - error = "Invalid subject hierarchy" - logging.error(error) - return error - volumeNode = shNode.GetItemDataNode(exportable.subjectHierarchyItemID) - if volumeNode is None or not volumeNode.IsA('vtkMRMLScalarVolumeNode'): - error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence" - logging.error(error) - return error - - sequenceBrowserNode = self.getSequenceBrowserNodeForMasterOutputNode(volumeNode) - if not sequenceBrowserNode: - error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence" - logging.error(error) - return error - - volumeSequenceNode = sequenceBrowserNode.GetSequenceNode(volumeNode) - if not volumeSequenceNode: - error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence" - logging.error(error) - return error - - # Get study and patient items - studyItemID = shNode.GetItemParent(exportable.subjectHierarchyItemID) - if not studyItemID: - error = "Unable to get study for series '" + volumeNode.GetName() + "'" - logging.error(error) - return error - patientItemID = shNode.GetItemParent(studyItemID) - if not patientItemID: - error = "Unable to get patient for series '" + volumeNode.GetName() + "'" - logging.error(error) - return error - - # Assemble tags dictionary for volume export - - tags = {} - tags['Patient Name'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName()) - tags['Patient ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName()) - tags['Patient Comments'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName()) - tags['Study Instance UID'] = pydicom.uid.generate_uid() - tags['Patient Birth Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName()) - tags['Patient Sex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName()) - tags['Study ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDTagName()) - tags['Study Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName()) - tags['Study Time'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName()) - tags['Study Description'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName()) - tags['Modality'] = exportable.tag('Modality') - tags['Manufacturer'] = exportable.tag('Manufacturer') - tags['Model'] = exportable.tag('Model') - tags['Series Description'] = exportable.tag('SeriesDescription') - tags['Series Number'] = exportable.tag('SeriesNumber') - tags['Series Date'] = exportable.tag("SeriesDate") - tags['Series Time'] = exportable.tag("SeriesTime") - tags['Series Instance UID'] = pydicom.uid.generate_uid() - tags['Frame of Reference UID'] = pydicom.uid.generate_uid() - - # Validate tags - if tags['Modality'] == "": - error = "Empty modality for series '" + volumeNode.GetName() + "'" - logging.error(error) - return error - #TODO: more tag checks - - sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes() - originalSelectedSequenceItemNumber = sequenceBrowserNode.GetSelectedItemNumber() - masterVolumeNode = sequenceBrowserNode.GetMasterSequenceNode() - - # initialize content datetime from series datetime - contentStartDate = exportable.tag("SeriesDate") - contentStartTime = exportable.tag("SeriesTime") - import datetime - datetimeNow = datetime.datetime.now() - if not contentStartDate: - contentStartDate = datetimeNow.strftime("%Y%m%d") - if not contentStartTime: - contentStartTime = datetimeNow.strftime("%H%M%S.%f") - contentStartDatetime = self.datetimeFromDicom(contentStartDate, contentStartTime) - - # Get output directory and create a subdirectory. This is necessary - # to avoid overwriting the files in case of multiple exportables, as - # naming of the DICOM files is static - directoryName = 'VolumeSequence_' + str(exportable.subjectHierarchyItemID) - directoryDir = qt.QDir(exportable.directory) - directoryDir.mkdir(directoryName) - directoryDir.cd(directoryName) - directory = directoryDir.absolutePath() - logging.info("Export scalar volume '" + volumeNode.GetName() + "' to directory " + directory) - - for sequenceItemIndex in range(sequenceItemCount): - - # Switch to next item in the series - sequenceBrowserNode.SetSelectedItemNumber(sequenceItemIndex) - slicer.app.processEvents() - # Compute content date&time - # TODO: verify that unit in sequence node is "second" (and convert to seconds if not) - timeOffsetSec = float(masterVolumeNode.GetNthIndexValue(sequenceItemIndex))-float(masterVolumeNode.GetNthIndexValue(0)) - contentDatetime = contentStartDatetime + datetime.timedelta(seconds=timeOffsetSec) - tags['Content Date'] = contentDatetime.strftime("%Y%m%d") - tags['Content Time'] = contentDatetime.strftime("%H%M%S.%f") - # Perform export - filenamePrefix = f"IMG_{sequenceItemIndex:04d}_" - exporter = DICOMExportScalarVolume(tags['Study ID'], volumeNode, tags, directory, filenamePrefix) - exporter.export() - - # Success - return "" + if len(tm) >= 6: + try: + hhmmss = str.split(tm, '.')[0] + except: + hhmmss = tm + try: + microsecond = int(float('0.' + str.split(tm, '.')[1]) * 1e6) + except: + microsecond = 0 + if len(hhmmss) == 6: # HHMMSS + hour = int(hhmmss[0:2]) + minute = int(hhmmss[2:4]) + second = int(hhmmss[4:6]) + elif len(hhmmss) == 4: # HHMM + hour = int(hhmmss[0:2]) + minute = int(hhmmss[2:4]) + elif len(hhmmss) == 2: # HH + hour = int(hhmmss[0:2]) + else: + raise OSError("Invalid DICOM time string: " + tm + " (failed to parse HHMMSS)") + + import datetime + return datetime.datetime(year, month, day, hour, minute, second, microsecond) + + def export(self, exportables): + for exportable in exportables: + # Get volume node to export + shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) + if shNode is None: + error = "Invalid subject hierarchy" + logging.error(error) + return error + volumeNode = shNode.GetItemDataNode(exportable.subjectHierarchyItemID) + if volumeNode is None or not volumeNode.IsA('vtkMRMLScalarVolumeNode'): + error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence" + logging.error(error) + return error + + sequenceBrowserNode = self.getSequenceBrowserNodeForMasterOutputNode(volumeNode) + if not sequenceBrowserNode: + error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence" + logging.error(error) + return error + + volumeSequenceNode = sequenceBrowserNode.GetSequenceNode(volumeNode) + if not volumeSequenceNode: + error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence" + logging.error(error) + return error + + # Get study and patient items + studyItemID = shNode.GetItemParent(exportable.subjectHierarchyItemID) + if not studyItemID: + error = "Unable to get study for series '" + volumeNode.GetName() + "'" + logging.error(error) + return error + patientItemID = shNode.GetItemParent(studyItemID) + if not patientItemID: + error = "Unable to get patient for series '" + volumeNode.GetName() + "'" + logging.error(error) + return error + + # Assemble tags dictionary for volume export + + tags = {} + tags['Patient Name'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName()) + tags['Patient ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName()) + tags['Patient Comments'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName()) + tags['Study Instance UID'] = pydicom.uid.generate_uid() + tags['Patient Birth Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName()) + tags['Patient Sex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName()) + tags['Study ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDTagName()) + tags['Study Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName()) + tags['Study Time'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName()) + tags['Study Description'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName()) + tags['Modality'] = exportable.tag('Modality') + tags['Manufacturer'] = exportable.tag('Manufacturer') + tags['Model'] = exportable.tag('Model') + tags['Series Description'] = exportable.tag('SeriesDescription') + tags['Series Number'] = exportable.tag('SeriesNumber') + tags['Series Date'] = exportable.tag("SeriesDate") + tags['Series Time'] = exportable.tag("SeriesTime") + tags['Series Instance UID'] = pydicom.uid.generate_uid() + tags['Frame of Reference UID'] = pydicom.uid.generate_uid() + + # Validate tags + if tags['Modality'] == "": + error = "Empty modality for series '" + volumeNode.GetName() + "'" + logging.error(error) + return error + # TODO: more tag checks + + sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes() + originalSelectedSequenceItemNumber = sequenceBrowserNode.GetSelectedItemNumber() + masterVolumeNode = sequenceBrowserNode.GetMasterSequenceNode() + + # initialize content datetime from series datetime + contentStartDate = exportable.tag("SeriesDate") + contentStartTime = exportable.tag("SeriesTime") + import datetime + datetimeNow = datetime.datetime.now() + if not contentStartDate: + contentStartDate = datetimeNow.strftime("%Y%m%d") + if not contentStartTime: + contentStartTime = datetimeNow.strftime("%H%M%S.%f") + contentStartDatetime = self.datetimeFromDicom(contentStartDate, contentStartTime) + + # Get output directory and create a subdirectory. This is necessary + # to avoid overwriting the files in case of multiple exportables, as + # naming of the DICOM files is static + directoryName = 'VolumeSequence_' + str(exportable.subjectHierarchyItemID) + directoryDir = qt.QDir(exportable.directory) + directoryDir.mkdir(directoryName) + directoryDir.cd(directoryName) + directory = directoryDir.absolutePath() + logging.info("Export scalar volume '" + volumeNode.GetName() + "' to directory " + directory) + + for sequenceItemIndex in range(sequenceItemCount): + + # Switch to next item in the series + sequenceBrowserNode.SetSelectedItemNumber(sequenceItemIndex) + slicer.app.processEvents() + # Compute content date&time + # TODO: verify that unit in sequence node is "second" (and convert to seconds if not) + timeOffsetSec = float(masterVolumeNode.GetNthIndexValue(sequenceItemIndex)) - float(masterVolumeNode.GetNthIndexValue(0)) + contentDatetime = contentStartDatetime + datetime.timedelta(seconds=timeOffsetSec) + tags['Content Date'] = contentDatetime.strftime("%Y%m%d") + tags['Content Time'] = contentDatetime.strftime("%H%M%S.%f") + # Perform export + filenamePrefix = f"IMG_{sequenceItemIndex:04d}_" + exporter = DICOMExportScalarVolume(tags['Study ID'], volumeNode, tags, directory, filenamePrefix) + exporter.export() + + # Success + return "" # @@ -265,31 +265,31 @@ def export(self,exportables): # class DICOMVolumeSequencePlugin: - """ - 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 Volume Sequence Export Plugin" - parent.categories = ["Developer Tools.DICOM Plugins"] - parent.contributors = ["Andras Lasso (PerkLab)"] - parent.helpText = """ + """ + 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 Volume Sequence Export Plugin" + parent.categories = ["Developer Tools.DICOM Plugins"] + parent.contributors = ["Andras Lasso (PerkLab)"] + parent.helpText = """ Plugin to the DICOM Module to export volume sequence to DICOM file. No module interface here, only in the DICOM module. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ Originally developed by Andras Lasso (PekLab). """ - # don't show this module - it only appears in the DICOM module - parent.hidden = True - - # 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['DICOMVolumeSequencePlugin'] = DICOMVolumeSequencePluginClass + # don't show this module - it only appears in the DICOM module + parent.hidden = True + + # 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['DICOMVolumeSequencePlugin'] = DICOMVolumeSequencePluginClass diff --git a/Modules/Scripted/DMRIInstall/DMRIInstall.py b/Modules/Scripted/DMRIInstall/DMRIInstall.py index e7f11abe83b..985f249488d 100644 --- a/Modules/Scripted/DMRIInstall/DMRIInstall.py +++ b/Modules/Scripted/DMRIInstall/DMRIInstall.py @@ -12,11 +12,11 @@ # class DMRIInstall(ScriptedLoadableModule): - """ - """ + """ + """ - helpText = textwrap.dedent( - """ + helpText = textwrap.dedent( + """ The SlicerDMRI extension provides diffusion-related tools including:

          @@ -39,8 +39,8 @@ class DMRIInstall(ScriptedLoadableModule):    https://discourse.slicer.org

          """) - errorText = textwrap.dedent( - """ + errorText = textwrap.dedent( + """
          The SlicerDMRI extension is currently unavailable.

          Please try a manual installation via the Extensions Manager, and contact the Slicer forum at:

          @@ -52,26 +52,26 @@ class DMRIInstall(ScriptedLoadableModule): Slicer revision: {revision}
          Platform: {platform} """).format(builddate=slicer.app.applicationVersion, - revision = slicer.app.repositoryRevision, - platform = slicer.app.platform) - - def __init__(self, parent): - - # Hide this module if SlicerDMRI is already installed - model = slicer.app.extensionsManagerModel() - if model.isExtensionInstalled("SlicerDMRI"): - parent.hidden = True - - ScriptedLoadableModule.__init__(self, parent) - - self.parent.categories = ["Diffusion"] - self.parent.title = "Install Slicer Diffusion Tools (SlicerDMRI)" - self.parent.dependencies = [] - self.parent.contributors = ["Isaiah Norton (BWH), Lauren O'Donnell (BWH)"] - self.parent.helpText = DMRIInstall.helpText - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = textwrap.dedent( - """ + revision=slicer.app.repositoryRevision, + platform=slicer.app.platform) + + def __init__(self, parent): + + # Hide this module if SlicerDMRI is already installed + model = slicer.app.extensionsManagerModel() + if model.isExtensionInstalled("SlicerDMRI"): + parent.hidden = True + + ScriptedLoadableModule.__init__(self, parent) + + self.parent.categories = ["Diffusion"] + self.parent.title = "Install Slicer Diffusion Tools (SlicerDMRI)" + self.parent.dependencies = [] + self.parent.contributors = ["Isaiah Norton (BWH), Lauren O'Donnell (BWH)"] + self.parent.helpText = DMRIInstall.helpText + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = textwrap.dedent( + """ SlicerDMRI supported by NIH NCI ITCR U01CA199459 (Open Source Diffusion MRI Technology For Brain Cancer Research), and made possible by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. @@ -79,49 +79,49 @@ def __init__(self, parent): class DMRIInstallWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - self.textBox = ctk.ctkFittedTextBrowser() - self.textBox.setOpenExternalLinks(True) # Open links in default browser - self.textBox.setHtml(DMRIInstall.helpText) - self.parent.layout().addWidget(self.textBox) - - # - # Apply Button - # - self.applyButton = qt.QPushButton("Install SlicerDMRI") - self.applyButton.toolTip = 'Installs the "SlicerDMRI" extension from the Diffusion category.' - self.applyButton.icon = qt.QIcon(":/Icons/ExtensionDefaultIcon.png") - self.applyButton.enabled = True - self.applyButton.connect('clicked()', self.onApply) - self.parent.layout().addWidget(self.applyButton) - - self.parent.layout().addStretch(1) - - def onError(self): - self.applyButton.enabled = False - self.textBox.setHtml(DMRIInstall.errorText) - return - - def onApply(self): - emm = slicer.app.extensionsManagerModel() - - if emm.isExtensionInstalled("SlicerDMRI"): - self.textBox.setHtml("

          SlicerDMRI is already installed.

          ") - self.applyButton.enabled = False - return - - md = emm.retrieveExtensionMetadataByName("SlicerDMRI") - - if not md or 'extension_id' not in md: - return self.onError() - - if emm.downloadAndInstallExtension(md['extension_id']): - slicer.app.confirmRestart("Restart to complete SlicerDMRI installation?") - else: - self.onError() + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + self.textBox = ctk.ctkFittedTextBrowser() + self.textBox.setOpenExternalLinks(True) # Open links in default browser + self.textBox.setHtml(DMRIInstall.helpText) + self.parent.layout().addWidget(self.textBox) + + # + # Apply Button + # + self.applyButton = qt.QPushButton("Install SlicerDMRI") + self.applyButton.toolTip = 'Installs the "SlicerDMRI" extension from the Diffusion category.' + self.applyButton.icon = qt.QIcon(":/Icons/ExtensionDefaultIcon.png") + self.applyButton.enabled = True + self.applyButton.connect('clicked()', self.onApply) + self.parent.layout().addWidget(self.applyButton) + + self.parent.layout().addStretch(1) + + def onError(self): + self.applyButton.enabled = False + self.textBox.setHtml(DMRIInstall.errorText) + return + + def onApply(self): + emm = slicer.app.extensionsManagerModel() + + if emm.isExtensionInstalled("SlicerDMRI"): + self.textBox.setHtml("

          SlicerDMRI is already installed.

          ") + self.applyButton.enabled = False + return + + md = emm.retrieveExtensionMetadataByName("SlicerDMRI") + + if not md or 'extension_id' not in md: + return self.onError() + + if emm.downloadAndInstallExtension(md['extension_id']): + slicer.app.confirmRestart("Restart to complete SlicerDMRI installation?") + else: + self.onError() diff --git a/Modules/Scripted/DataProbe/DataProbe.py b/Modules/Scripted/DataProbe/DataProbe.py index a5730ee6120..178a32b1986 100644 --- a/Modules/Scripted/DataProbe/DataProbe.py +++ b/Modules/Scripted/DataProbe/DataProbe.py @@ -15,503 +15,503 @@ # class DataProbe(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) - parent.title = "DataProbe" - parent.categories = ["Quantification"] - parent.contributors = ["Steve Pieper (Isomics)"] - parent.helpText = """ + parent.title = "DataProbe" + parent.categories = ["Quantification"] + parent.contributors = ["Steve Pieper (Isomics)"] + parent.helpText = """ The DataProbe module is used to get information about the current RAS position being indicated by the mouse position. """ - self.parent.helpText += self.getDefaultModuleDocumentationLink() - parent.acknowledgementText = """This work is supported by NA-MIC, NAC, NCIGT, NIH U24 CA180918 (PIs Kikinis and Fedorov) and the Slicer Community.""" - # TODO: need a DataProbe icon - #parent.icon = qt.QIcon(':Icons/XLarge/SlicerDownloadMRHead.png') - self.infoWidget = None - - if slicer.mrmlScene.GetTagByClassName( "vtkMRMLScriptedModuleNode" ) != 'ScriptedModule': - slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode()) - - # Trigger the menu to be added when application has started up - if not slicer.app.commandOptions().noMainWindow : - slicer.app.connect("startupCompleted()", self.addView) - - def __del__(self): - if self.infoWidget: - self.infoWidget.removeObservers() - - def addView(self): - """ - Create the persistent widget shown in the bottom left of the user interface - Do this in a startupCompleted callback so the rest of the interface is already - built. - """ - try: - mw = slicer.util.mainWindow() - parent = slicer.util.findChild(mw, "DataProbeCollapsibleWidget") - except IndexError: - print("No Data Probe frame - cannot create DataProbe") - return - self.infoWidget = DataProbeInfoWidget(parent) - parent.layout().insertWidget(0,self.infoWidget.frame) - - def showZoomedSlice(self, value=False): - self.showZoomedSlice = value - if self.infoWidget: - self.infoWidget.onShowImage(value) + self.parent.helpText += self.getDefaultModuleDocumentationLink() + parent.acknowledgementText = """This work is supported by NA-MIC, NAC, NCIGT, NIH U24 CA180918 (PIs Kikinis and Fedorov) and the Slicer Community.""" + # TODO: need a DataProbe icon + # parent.icon = qt.QIcon(':Icons/XLarge/SlicerDownloadMRHead.png') + self.infoWidget = None + + if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != 'ScriptedModule': + slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode()) + + # Trigger the menu to be added when application has started up + if not slicer.app.commandOptions().noMainWindow: + slicer.app.connect("startupCompleted()", self.addView) + + def __del__(self): + if self.infoWidget: + self.infoWidget.removeObservers() + + def addView(self): + """ + Create the persistent widget shown in the bottom left of the user interface + Do this in a startupCompleted callback so the rest of the interface is already + built. + """ + try: + mw = slicer.util.mainWindow() + parent = slicer.util.findChild(mw, "DataProbeCollapsibleWidget") + except IndexError: + print("No Data Probe frame - cannot create DataProbe") + return + self.infoWidget = DataProbeInfoWidget(parent) + parent.layout().insertWidget(0, self.infoWidget.frame) + + def showZoomedSlice(self, value=False): + self.showZoomedSlice = value + if self.infoWidget: + self.infoWidget.onShowImage(value) class DataProbeInfoWidget: - def __init__(self, parent=None): - self.nameSize = 24 - - self.CrosshairNode = None - self.CrosshairNodeObserverTag = None - - self.frame = qt.QFrame(parent) - self.frame.setLayout(qt.QVBoxLayout()) - # Set horizontal policy to Ignored to prevent a long segment or volume name making the widget wider. - # If the module panel made larger then the image viewers would move and the mouse pointer position - # would change in the image, potentially pointing outside the node with the long name, resulting in the - # module panel collapsing to the original size, causing an infinite oscillation. - qSize = qt.QSizePolicy() - qSize.setHorizontalPolicy(qt.QSizePolicy.Ignored) - qSize.setVerticalPolicy(qt.QSizePolicy.Preferred) - self.frame.setSizePolicy(qSize) - - modulePath = slicer.modules.dataprobe.path.replace("DataProbe.py","") - self.iconsDIR = modulePath + '/Resources/Icons' - - self.showImage = False - - # Used in _createMagnifiedPixmap() - self.imageCrop = vtk.vtkExtractVOI() - self.canvas = vtk.vtkImageCanvasSource2D() - self.painter = qt.QPainter() - self.pen = qt.QPen() - - self._createSmall() - - #Helper class to calculate and display tensor scalars - self.calculateTensorScalars = CalculateTensorScalars() - - # Observe the crosshair node to get the current cursor position - self.CrosshairNode = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLCrosshairNode') - if self.CrosshairNode: - self.CrosshairNodeObserverTag = self.CrosshairNode.AddObserver(slicer.vtkMRMLCrosshairNode.CursorPositionModifiedEvent, self.processEvent) - - def __del__(self): - self.removeObservers() - - def fitName(self,name,nameSize=None): - if not nameSize: - nameSize = self.nameSize - if len(name) > nameSize: - preSize = int(nameSize / 2) - postSize = preSize - 3 - name = name[:preSize] + "..." + name[-postSize:] - return name - - def removeObservers(self): - # remove observers and reset - if self.CrosshairNode and self.CrosshairNodeObserverTag: - self.CrosshairNode.RemoveObserver(self.CrosshairNodeObserverTag) - self.CrosshairNodeObserverTag = None - - def getPixelString(self,volumeNode,ijk): - """Given a volume node, create a human readable - string describing the contents""" - # TODO: the volume nodes should have a way to generate - # these strings in a generic way - if not volumeNode: - return "No volume" - imageData = volumeNode.GetImageData() - if not imageData: - return "No Image" - dims = imageData.GetDimensions() - for ele in range(3): - if ijk[ele] < 0 or ijk[ele] >= dims[ele]: - return "Out of Frame" - pixel = "" - if volumeNode.IsA("vtkMRMLLabelMapVolumeNode"): - labelIndex = int(imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], 0)) - labelValue = "Unknown" - displayNode = volumeNode.GetDisplayNode() - if displayNode: - colorNode = displayNode.GetColorNode() - if colorNode: - labelValue = colorNode.GetColorName(labelIndex) - return "%s (%d)" % (labelValue, labelIndex) - - if volumeNode.IsA("vtkMRMLDiffusionTensorVolumeNode"): - point_idx = imageData.FindPoint(ijk[0], ijk[1], ijk[2]) - if point_idx == -1: - return "Out of bounds" - - if not imageData.GetPointData(): - return "No Point Data" - - tensors = imageData.GetPointData().GetTensors() - if not tensors: - return "No Tensor Data" - - tensor = imageData.GetPointData().GetTensors().GetTuple9(point_idx) - scalarVolumeDisplayNode = volumeNode.GetScalarVolumeDisplayNode() - - if scalarVolumeDisplayNode: - operation = scalarVolumeDisplayNode.GetScalarInvariant() + def __init__(self, parent=None): + self.nameSize = 24 + + self.CrosshairNode = None + self.CrosshairNodeObserverTag = None + + self.frame = qt.QFrame(parent) + self.frame.setLayout(qt.QVBoxLayout()) + # Set horizontal policy to Ignored to prevent a long segment or volume name making the widget wider. + # If the module panel made larger then the image viewers would move and the mouse pointer position + # would change in the image, potentially pointing outside the node with the long name, resulting in the + # module panel collapsing to the original size, causing an infinite oscillation. + qSize = qt.QSizePolicy() + qSize.setHorizontalPolicy(qt.QSizePolicy.Ignored) + qSize.setVerticalPolicy(qt.QSizePolicy.Preferred) + self.frame.setSizePolicy(qSize) + + modulePath = slicer.modules.dataprobe.path.replace("DataProbe.py", "") + self.iconsDIR = modulePath + '/Resources/Icons' + + self.showImage = False + + # Used in _createMagnifiedPixmap() + self.imageCrop = vtk.vtkExtractVOI() + self.canvas = vtk.vtkImageCanvasSource2D() + self.painter = qt.QPainter() + self.pen = qt.QPen() + + self._createSmall() + + # Helper class to calculate and display tensor scalars + self.calculateTensorScalars = CalculateTensorScalars() + + # Observe the crosshair node to get the current cursor position + self.CrosshairNode = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLCrosshairNode') + if self.CrosshairNode: + self.CrosshairNodeObserverTag = self.CrosshairNode.AddObserver(slicer.vtkMRMLCrosshairNode.CursorPositionModifiedEvent, self.processEvent) + + def __del__(self): + self.removeObservers() + + def fitName(self, name, nameSize=None): + if not nameSize: + nameSize = self.nameSize + if len(name) > nameSize: + preSize = int(nameSize / 2) + postSize = preSize - 3 + name = name[:preSize] + "..." + name[-postSize:] + return name + + def removeObservers(self): + # remove observers and reset + if self.CrosshairNode and self.CrosshairNodeObserverTag: + self.CrosshairNode.RemoveObserver(self.CrosshairNodeObserverTag) + self.CrosshairNodeObserverTag = None + + def getPixelString(self, volumeNode, ijk): + """Given a volume node, create a human readable + string describing the contents""" + # TODO: the volume nodes should have a way to generate + # these strings in a generic way + if not volumeNode: + return "No volume" + imageData = volumeNode.GetImageData() + if not imageData: + return "No Image" + dims = imageData.GetDimensions() + for ele in range(3): + if ijk[ele] < 0 or ijk[ele] >= dims[ele]: + return "Out of Frame" + pixel = "" + if volumeNode.IsA("vtkMRMLLabelMapVolumeNode"): + labelIndex = int(imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], 0)) + labelValue = "Unknown" + displayNode = volumeNode.GetDisplayNode() + if displayNode: + colorNode = displayNode.GetColorNode() + if colorNode: + labelValue = colorNode.GetColorName(labelIndex) + return "%s (%d)" % (labelValue, labelIndex) + + if volumeNode.IsA("vtkMRMLDiffusionTensorVolumeNode"): + point_idx = imageData.FindPoint(ijk[0], ijk[1], ijk[2]) + if point_idx == -1: + return "Out of bounds" + + if not imageData.GetPointData(): + return "No Point Data" + + tensors = imageData.GetPointData().GetTensors() + if not tensors: + return "No Tensor Data" + + tensor = imageData.GetPointData().GetTensors().GetTuple9(point_idx) + scalarVolumeDisplayNode = volumeNode.GetScalarVolumeDisplayNode() + + if scalarVolumeDisplayNode: + operation = scalarVolumeDisplayNode.GetScalarInvariant() + else: + operation = None + + value = self.calculateTensorScalars(tensor, operation=operation) + if value is not None: + valueString = ("%f" % value).rstrip('0').rstrip('.') + return "%s %s" % (scalarVolumeDisplayNode.GetScalarInvariantAsString(), valueString) + else: + return scalarVolumeDisplayNode.GetScalarInvariantAsString() + + # default - non label scalar volume + numberOfComponents = imageData.GetNumberOfScalarComponents() + if numberOfComponents > 3: + return "%d components" % numberOfComponents + for c in range(numberOfComponents): + component = imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], c) + if component.is_integer(): + component = int(component) + # format string according to suggestion here: + # https://stackoverflow.com/questions/2440692/formatting-floats-in-python-without-superfluous-zeros + # also set the default field width for each coordinate + componentString = ("%4f" % component).rstrip('0').rstrip('.') + pixel += ("%s, " % componentString) + return pixel[:-2] + + def processEvent(self, observee, event): + # TODO: use a timer to delay calculation and compress events + insideView = False + ras = [0.0, 0.0, 0.0] + xyz = [0.0, 0.0, 0.0] + sliceNode = None + if self.CrosshairNode: + insideView = self.CrosshairNode.GetCursorPositionRAS(ras) + sliceNode = self.CrosshairNode.GetCursorPositionXYZ(xyz) + + sliceLogic = None + if sliceNode: + appLogic = slicer.app.applicationLogic() + if appLogic: + sliceLogic = appLogic.GetSliceLogic(sliceNode) + + if not insideView or not sliceNode or not sliceLogic: + # reset all the readouts + self.viewerColor.text = "" + self.viewInfo.text = "" + layers = ('L', 'F', 'B') + for layer in layers: + self.layerNames[layer].setText("") + self.layerIJKs[layer].setText("") + self.layerValues[layer].setText("") + self.imageLabel.hide() + self.viewerColor.hide() + self.viewInfo.hide() + self.viewerFrame.hide() + self.showImageFrame.show() + return + + self.viewerColor.show() + self.viewInfo.show() + self.viewerFrame.show() + self.showImageFrame.hide() + + # populate the widgets + self.viewerColor.setText(" ") + rgbColor = sliceNode.GetLayoutColor() + color = qt.QColor.fromRgbF(rgbColor[0], rgbColor[1], rgbColor[2]) + if hasattr(color, 'name'): + self.viewerColor.setStyleSheet('QLabel {background-color : %s}' % color.name()) + + self.viewInfo.text = self.generateViewDescription(xyz, ras, sliceNode, sliceLogic) + + def _roundInt(value): + try: + return int(round(value)) + except ValueError: + return 0 + + hasVolume = False + layerLogicCalls = (('L', sliceLogic.GetLabelLayer), + ('F', sliceLogic.GetForegroundLayer), + ('B', sliceLogic.GetBackgroundLayer)) + for layer, logicCall in layerLogicCalls: + layerLogic = logicCall() + volumeNode = layerLogic.GetVolumeNode() + ijk = [0, 0, 0] + if volumeNode: + hasVolume = True + xyToIJK = layerLogic.GetXYToIJKTransform() + ijkFloat = xyToIJK.TransformDoublePoint(xyz) + ijk = [_roundInt(value) for value in ijkFloat] + self.layerNames[layer].setText(self.generateLayerName(layerLogic)) + self.layerIJKs[layer].setText(self.generateIJKPixelDescription(ijk, layerLogic)) + self.layerValues[layer].setText(self.generateIJKPixelValueDescription(ijk, layerLogic)) + + # collect information from displayable managers + displayableManagerCollection = vtk.vtkCollection() + if sliceNode: + sliceWidget = slicer.app.layoutManager().sliceWidget(sliceNode.GetName()) + if sliceWidget: + # sliceWidget is owned by the layout manager + sliceView = sliceWidget.sliceView() + sliceView.getDisplayableManagers(displayableManagerCollection) + aggregatedDisplayableManagerInfo = '' + for index in range(displayableManagerCollection.GetNumberOfItems()): + displayableManager = displayableManagerCollection.GetItemAsObject(index) + infoString = displayableManager.GetDataProbeInfoStringForPosition(xyz) + if infoString != "": + aggregatedDisplayableManagerInfo += infoString + "
          " + if aggregatedDisplayableManagerInfo != '': + self.displayableManagerInfo.text = '' + aggregatedDisplayableManagerInfo + '' + self.displayableManagerInfo.show() else: - operation = None - - value = self.calculateTensorScalars(tensor, operation=operation) - if value is not None: - valueString = ("%f" % value).rstrip('0').rstrip('.') - return "%s %s"%(scalarVolumeDisplayNode.GetScalarInvariantAsString(), valueString) + self.displayableManagerInfo.hide() + + # set image + if (not slicer.mrmlScene.IsBatchProcessing()) and sliceLogic and hasVolume and self.showImage: + pixmap = self._createMagnifiedPixmap( + xyz, sliceLogic.GetBlend().GetOutputPort(), self.imageLabel.size, color) + if pixmap: + self.imageLabel.setPixmap(pixmap) + self.onShowImage(self.showImage) + + if hasattr(self.frame.parent(), 'text'): + sceneName = slicer.mrmlScene.GetURL() + if sceneName != "": + self.frame.parent().text = "Data Probe: %s" % self.fitName(sceneName, nameSize=2 * self.nameSize) + else: + self.frame.parent().text = "Data Probe" + + def generateViewDescription(self, xyz, ras, sliceNode, sliceLogic): + + # Note that 'xyz' is unused in the Slicer implementation but could + # be used when customizing the behavior of this function in extension. + + # Described below are the details for the ras coordinate width set to 6: + # 1: sign + # 3: suggested number of digits before decimal point + # 1: decimal point: + # 1: number of digits after decimal point + + spacing = "%.1f" % sliceLogic.GetLowestVolumeSliceSpacing()[2] + if sliceNode.GetSliceSpacingMode() == slicer.vtkMRMLSliceNode.PrescribedSliceSpacingMode: + spacing = "(%s)" % spacing + + return \ + " {layoutName: <8s} ({rLabel} {ras_x:3.1f}, {aLabel} {ras_y:3.1f}, {sLabel} {ras_z:3.1f}) {orient: >8s} Sp: {spacing:s}" \ + .format(layoutName=sliceNode.GetLayoutName(), + rLabel=sliceNode.GetAxisLabel(1) if ras[0] >= 0 else sliceNode.GetAxisLabel(0), + aLabel=sliceNode.GetAxisLabel(3) if ras[1] >= 0 else sliceNode.GetAxisLabel(2), + sLabel=sliceNode.GetAxisLabel(5) if ras[2] >= 0 else sliceNode.GetAxisLabel(4), + ras_x=abs(ras[0]), + ras_y=abs(ras[1]), + ras_z=abs(ras[2]), + orient=sliceNode.GetOrientationString(), + spacing=spacing + ) + + def generateLayerName(self, slicerLayerLogic): + volumeNode = slicerLayerLogic.GetVolumeNode() + return "%s" % (self.fitName(volumeNode.GetName()) if volumeNode else "None") + + def generateIJKPixelDescription(self, ijk, slicerLayerLogic): + volumeNode = slicerLayerLogic.GetVolumeNode() + return f"({ijk[0]:3d}, {ijk[1]:3d}, {ijk[2]:3d})" if volumeNode else "" + + def generateIJKPixelValueDescription(self, ijk, slicerLayerLogic): + volumeNode = slicerLayerLogic.GetVolumeNode() + return "%s" % self.getPixelString(volumeNode, ijk) if volumeNode else "" + + def _createMagnifiedPixmap(self, xyz, inputImageDataConnection, outputSize, crosshairColor, imageZoom=10): + + # Use existing instance of objects to avoid instantiating one at each event. + imageCrop = self.imageCrop + painter = self.painter + pen = self.pen + + def _roundInt(value): + try: + return int(round(value)) + except ValueError: + return 0 + + imageCrop.SetInputConnection(inputImageDataConnection) + xyzInt = [0, 0, 0] + xyzInt = [_roundInt(value) for value in xyz] + producer = inputImageDataConnection.GetProducer() + dims = producer.GetOutput().GetDimensions() + minDim = min(dims[0], dims[1]) + imageSize = _roundInt(minDim / imageZoom / 2.0) + imin = xyzInt[0] - imageSize + imax = xyzInt[0] + imageSize + jmin = xyzInt[1] - imageSize + jmax = xyzInt[1] + imageSize + imin_trunc = max(0, imin) + imax_trunc = min(dims[0] - 1, imax) + jmin_trunc = max(0, jmin) + jmax_trunc = min(dims[1] - 1, jmax) + # The extra complexity of the canvas is used here to maintain a fixed size + # output due to the imageCrop returning a smaller image if the limits are + # outside the input image bounds. Specially useful when zooming at the borders. + canvas = self.canvas + canvas.SetScalarType(producer.GetOutput().GetScalarType()) + canvas.SetNumberOfScalarComponents(producer.GetOutput().GetNumberOfScalarComponents()) + canvas.SetExtent(imin, imax, jmin, jmax, 0, 0) + canvas.FillBox(imin, imax, jmin, jmax) + canvas.Update() + if (imin_trunc <= imax_trunc) and (jmin_trunc <= jmax_trunc): + imageCrop.SetVOI(imin_trunc, imax_trunc, jmin_trunc, jmax_trunc, 0, 0) + imageCrop.Update() + vtkImageCropped = imageCrop.GetOutput() + xyzBounds = [0] * 6 + vtkImageCropped.GetBounds(xyzBounds) + xyzBounds = [_roundInt(value) for value in xyzBounds] + canvas.DrawImage(xyzBounds[0], xyzBounds[2], vtkImageCropped) + canvas.Update() + vtkImageFromCanvas = canvas.GetOutput() + if vtkImageFromCanvas: + qImage = qt.QImage() + slicer.qMRMLUtils().vtkImageDataToQImage(vtkImageFromCanvas, qImage) + imagePixmap = qt.QPixmap.fromImage(qImage) + imagePixmap = imagePixmap.scaled(outputSize, qt.Qt.KeepAspectRatio, qt.Qt.FastTransformation) + + # draw crosshair + painter.begin(imagePixmap) + pen = qt.QPen() + pen.setColor(crosshairColor) + painter.setPen(pen) + painter.drawLine(0, int(imagePixmap.height() / 2), imagePixmap.width(), int(imagePixmap.height() / 2)) + painter.drawLine(int(imagePixmap.width() / 2), 0, int(imagePixmap.width() / 2), imagePixmap.height()) + painter.end() + return imagePixmap + return None + + def _createSmall(self): + """Make the internals of the widget to display in the + Data Probe frame (lower left of slicer main window by default)""" + + # this method makes SliceView Annotation + self.sliceAnnotations = DataProbeLib.SliceAnnotations() + + # goto module button + self.goToModule = qt.QPushButton('->', self.frame) + self.goToModule.setToolTip('Go to the DataProbe module for more information and options') + self.frame.layout().addWidget(self.goToModule) + self.goToModule.connect("clicked()", self.onGoToModule) + # hide this for now - there's not much to see in the module itself + self.goToModule.hide() + + # image view: To ensure the height of the checkbox matches the height of the + # viewerFrame, it is added to a frame setting the layout and hard-coding the + # content margins. + # TODO: Revisit the approach and avoid hard-coding content margins + self.showImageFrame = qt.QFrame(self.frame) + self.frame.layout().addWidget(self.showImageFrame) + self.showImageFrame.setLayout(qt.QHBoxLayout()) + self.showImageFrame.layout().setContentsMargins(0, 3, 0, 3) + self.showImageBox = qt.QCheckBox('Show Zoomed Slice', self.showImageFrame) + self.showImageFrame.layout().addWidget(self.showImageBox) + self.showImageBox.connect("toggled(bool)", self.onShowImage) + self.showImageBox.setChecked(False) + + self.imageLabel = qt.QLabel() + + # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + # fails on some systems, therefore set the policies using separate method calls + qSize = qt.QSizePolicy() + qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding) + qSize.setVerticalPolicy(qt.QSizePolicy.Expanding) + self.imageLabel.setSizePolicy(qSize) + # self.imageLabel.setScaledContents(True) + self.frame.layout().addWidget(self.imageLabel) + self.onShowImage(False) + + # top row - things about the viewer itself + self.viewerFrame = qt.QFrame(self.frame) + self.viewerFrame.setLayout(qt.QHBoxLayout()) + self.frame.layout().addWidget(self.viewerFrame) + self.viewerColor = qt.QLabel(self.viewerFrame) + self.viewerColor.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Preferred) + self.viewerFrame.layout().addWidget(self.viewerColor) + self.viewInfo = qt.QLabel() + self.viewerFrame.layout().addWidget(self.viewInfo) + + def _setFixedFontFamily(widget, family=None): + if family is None: + family = qt.QFontDatabase.systemFont(qt.QFontDatabase.FixedFont).family() + font = widget.font + font.setFamily(family) + widget.font = font + widget.wordWrap = True + _setFixedFontFamily(self.viewInfo) + + # the grid - things about the layers + # this method makes labels + self.layerGrid = qt.QFrame(self.frame) + layout = qt.QGridLayout() + self.layerGrid.setLayout(layout) + self.frame.layout().addWidget(self.layerGrid) + layers = ('L', 'F', 'B') + self.layerNames = {} + self.layerIJKs = {} + self.layerValues = {} + for (row, layer) in enumerate(layers): + col = 0 + layout.addWidget(qt.QLabel(layer), row, col) + col += 1 + self.layerNames[layer] = qt.QLabel() + layout.addWidget(self.layerNames[layer], row, col) + col += 1 + self.layerIJKs[layer] = qt.QLabel() + layout.addWidget(self.layerIJKs[layer], row, col) + col += 1 + self.layerValues[layer] = qt.QLabel() + layout.addWidget(self.layerValues[layer], row, col) + layout.setColumnStretch(col, 100) + + _setFixedFontFamily(self.layerNames[layer]) + _setFixedFontFamily(self.layerIJKs[layer]) + _setFixedFontFamily(self.layerValues[layer]) + + # information collected about the current crosshair position + # from displayable managers registered to the current view + self.displayableManagerInfo = qt.QLabel() + self.displayableManagerInfo.indent = 6 + self.displayableManagerInfo.wordWrap = True + self.frame.layout().addWidget(self.displayableManagerInfo) + # only show if not empty + self.displayableManagerInfo.hide() + + # goto module button + self.goToModule = qt.QPushButton('->', self.frame) + self.goToModule.setToolTip('Go to the DataProbe module for more information and options') + self.frame.layout().addWidget(self.goToModule) + self.goToModule.connect("clicked()", self.onGoToModule) + # hide this for now - there's not much to see in the module itself + self.goToModule.hide() + + def onGoToModule(self): + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('DataProbe') + + def onShowImage(self, value=False): + self.showImage = value + if value: + self.imageLabel.show() else: - return scalarVolumeDisplayNode.GetScalarInvariantAsString() - - # default - non label scalar volume - numberOfComponents = imageData.GetNumberOfScalarComponents() - if numberOfComponents > 3: - return "%d components" % numberOfComponents - for c in range(numberOfComponents): - component = imageData.GetScalarComponentAsDouble(ijk[0],ijk[1],ijk[2],c) - if component.is_integer(): - component = int(component) - # format string according to suggestion here: - # https://stackoverflow.com/questions/2440692/formatting-floats-in-python-without-superfluous-zeros - # also set the default field width for each coordinate - componentString = ("%4f" % component).rstrip('0').rstrip('.') - pixel += ("%s, " % componentString) - return pixel[:-2] - - def processEvent(self,observee,event): - # TODO: use a timer to delay calculation and compress events - insideView = False - ras = [0.0,0.0,0.0] - xyz = [0.0,0.0,0.0] - sliceNode = None - if self.CrosshairNode: - insideView = self.CrosshairNode.GetCursorPositionRAS(ras) - sliceNode = self.CrosshairNode.GetCursorPositionXYZ(xyz) - - sliceLogic = None - if sliceNode: - appLogic = slicer.app.applicationLogic() - if appLogic: - sliceLogic = appLogic.GetSliceLogic(sliceNode) - - if not insideView or not sliceNode or not sliceLogic: - # reset all the readouts - self.viewerColor.text = "" - self.viewInfo.text = "" - layers = ('L', 'F', 'B') - for layer in layers: - self.layerNames[layer].setText( "" ) - self.layerIJKs[layer].setText( "" ) - self.layerValues[layer].setText( "" ) - self.imageLabel.hide() - self.viewerColor.hide() - self.viewInfo.hide() - self.viewerFrame.hide() - self.showImageFrame.show() - return - - self.viewerColor.show() - self.viewInfo.show() - self.viewerFrame.show() - self.showImageFrame.hide() - - # populate the widgets - self.viewerColor.setText( " " ) - rgbColor = sliceNode.GetLayoutColor() - color = qt.QColor.fromRgbF(rgbColor[0], rgbColor[1], rgbColor[2]) - if hasattr(color, 'name'): - self.viewerColor.setStyleSheet('QLabel {background-color : %s}' % color.name()) - - self.viewInfo.text = self.generateViewDescription(xyz, ras, sliceNode, sliceLogic) - - def _roundInt(value): - try: - return int(round(value)) - except ValueError: - return 0 - - hasVolume = False - layerLogicCalls = (('L', sliceLogic.GetLabelLayer), - ('F', sliceLogic.GetForegroundLayer), - ('B', sliceLogic.GetBackgroundLayer)) - for layer,logicCall in layerLogicCalls: - layerLogic = logicCall() - volumeNode = layerLogic.GetVolumeNode() - ijk = [0, 0, 0] - if volumeNode: - hasVolume = True - xyToIJK = layerLogic.GetXYToIJKTransform() - ijkFloat = xyToIJK.TransformDoublePoint(xyz) - ijk = [_roundInt(value) for value in ijkFloat] - self.layerNames[layer].setText(self.generateLayerName(layerLogic)) - self.layerIJKs[layer].setText(self.generateIJKPixelDescription(ijk, layerLogic)) - self.layerValues[layer].setText(self.generateIJKPixelValueDescription(ijk, layerLogic)) - - # collect information from displayable managers - displayableManagerCollection = vtk.vtkCollection() - if sliceNode: - sliceWidget = slicer.app.layoutManager().sliceWidget(sliceNode.GetName()) - if sliceWidget: - # sliceWidget is owned by the layout manager - sliceView = sliceWidget.sliceView() - sliceView.getDisplayableManagers(displayableManagerCollection) - aggregatedDisplayableManagerInfo = '' - for index in range(displayableManagerCollection.GetNumberOfItems()): - displayableManager = displayableManagerCollection.GetItemAsObject(index) - infoString = displayableManager.GetDataProbeInfoStringForPosition(xyz) - if infoString != "": - aggregatedDisplayableManagerInfo += infoString + "
          " - if aggregatedDisplayableManagerInfo != '': - self.displayableManagerInfo.text = '' + aggregatedDisplayableManagerInfo + '' - self.displayableManagerInfo.show() - else: - self.displayableManagerInfo.hide() - - # set image - if (not slicer.mrmlScene.IsBatchProcessing()) and sliceLogic and hasVolume and self.showImage: - pixmap = self._createMagnifiedPixmap( - xyz, sliceLogic.GetBlend().GetOutputPort(), self.imageLabel.size, color) - if pixmap: - self.imageLabel.setPixmap(pixmap) - self.onShowImage(self.showImage) - - if hasattr(self.frame.parent(), 'text'): - sceneName = slicer.mrmlScene.GetURL() - if sceneName != "": - self.frame.parent().text = "Data Probe: %s" % self.fitName(sceneName,nameSize=2*self.nameSize) - else: - self.frame.parent().text = "Data Probe" - - def generateViewDescription(self, xyz, ras, sliceNode, sliceLogic): - - # Note that 'xyz' is unused in the Slicer implementation but could - # be used when customizing the behavior of this function in extension. - - # Described below are the details for the ras coordinate width set to 6: - # 1: sign - # 3: suggested number of digits before decimal point - # 1: decimal point: - # 1: number of digits after decimal point - - spacing = "%.1f" % sliceLogic.GetLowestVolumeSliceSpacing()[2] - if sliceNode.GetSliceSpacingMode() == slicer.vtkMRMLSliceNode.PrescribedSliceSpacingMode: - spacing = "(%s)" % spacing - - return \ - " {layoutName: <8s} ({rLabel} {ras_x:3.1f}, {aLabel} {ras_y:3.1f}, {sLabel} {ras_z:3.1f}) {orient: >8s} Sp: {spacing:s}" \ - .format(layoutName=sliceNode.GetLayoutName(), - rLabel=sliceNode.GetAxisLabel(1) if ras[0]>=0 else sliceNode.GetAxisLabel(0), - aLabel=sliceNode.GetAxisLabel(3) if ras[1]>=0 else sliceNode.GetAxisLabel(2), - sLabel=sliceNode.GetAxisLabel(5) if ras[2]>=0 else sliceNode.GetAxisLabel(4), - ras_x=abs(ras[0]), - ras_y=abs(ras[1]), - ras_z=abs(ras[2]), - orient=sliceNode.GetOrientationString(), - spacing=spacing - ) - - def generateLayerName(self, slicerLayerLogic): - volumeNode = slicerLayerLogic.GetVolumeNode() - return "%s" % (self.fitName(volumeNode.GetName()) if volumeNode else "None") - - def generateIJKPixelDescription(self, ijk, slicerLayerLogic): - volumeNode = slicerLayerLogic.GetVolumeNode() - return f"({ijk[0]:3d}, {ijk[1]:3d}, {ijk[2]:3d})" if volumeNode else "" - - def generateIJKPixelValueDescription(self, ijk, slicerLayerLogic): - volumeNode = slicerLayerLogic.GetVolumeNode() - return "%s" % self.getPixelString(volumeNode,ijk) if volumeNode else "" - - def _createMagnifiedPixmap(self, xyz, inputImageDataConnection, outputSize, crosshairColor, imageZoom=10): - - # Use existing instance of objects to avoid instantiating one at each event. - imageCrop = self.imageCrop - painter = self.painter - pen = self.pen - - def _roundInt(value): - try: - return int(round(value)) - except ValueError: - return 0 - - imageCrop.SetInputConnection(inputImageDataConnection) - xyzInt = [0, 0, 0] - xyzInt = [_roundInt(value) for value in xyz] - producer = inputImageDataConnection.GetProducer() - dims = producer.GetOutput().GetDimensions() - minDim = min(dims[0],dims[1]) - imageSize = _roundInt(minDim/imageZoom/2.0) - imin = xyzInt[0]-imageSize - imax = xyzInt[0]+imageSize - jmin = xyzInt[1]-imageSize - jmax = xyzInt[1]+imageSize - imin_trunc = max(0,imin) - imax_trunc = min(dims[0]-1, imax) - jmin_trunc = max(0, jmin) - jmax_trunc = min(dims[1]-1, jmax) - # The extra complexity of the canvas is used here to maintain a fixed size - # output due to the imageCrop returning a smaller image if the limits are - # outside the input image bounds. Specially useful when zooming at the borders. - canvas = self.canvas - canvas.SetScalarType(producer.GetOutput().GetScalarType()) - canvas.SetNumberOfScalarComponents(producer.GetOutput().GetNumberOfScalarComponents()) - canvas.SetExtent(imin, imax, jmin , jmax, 0 ,0) - canvas.FillBox(imin, imax, jmin , jmax) - canvas.Update() - if (imin_trunc <= imax_trunc) and (jmin_trunc <= jmax_trunc): - imageCrop.SetVOI(imin_trunc, imax_trunc, jmin_trunc, jmax_trunc, 0,0) - imageCrop.Update() - vtkImageCropped = imageCrop.GetOutput() - xyzBounds = [0]*6 - vtkImageCropped.GetBounds(xyzBounds) - xyzBounds = [_roundInt(value) for value in xyzBounds] - canvas.DrawImage(xyzBounds[0], xyzBounds[2], vtkImageCropped) - canvas.Update() - vtkImageFromCanvas = canvas.GetOutput() - if vtkImageFromCanvas: - qImage = qt.QImage() - slicer.qMRMLUtils().vtkImageDataToQImage(vtkImageFromCanvas, qImage) - imagePixmap = qt.QPixmap.fromImage(qImage) - imagePixmap = imagePixmap.scaled(outputSize, qt.Qt.KeepAspectRatio, qt.Qt.FastTransformation) - - # draw crosshair - painter.begin(imagePixmap) - pen = qt.QPen() - pen.setColor(crosshairColor) - painter.setPen(pen) - painter.drawLine(0, int(imagePixmap.height()/2), imagePixmap.width(), int(imagePixmap.height()/2)) - painter.drawLine(int(imagePixmap.width()/2), 0, int(imagePixmap.width()/2), imagePixmap.height()) - painter.end() - return imagePixmap - return None - - def _createSmall(self): - """Make the internals of the widget to display in the - Data Probe frame (lower left of slicer main window by default)""" - - # this method makes SliceView Annotation - self.sliceAnnotations = DataProbeLib.SliceAnnotations() - - # goto module button - self.goToModule = qt.QPushButton('->', self.frame) - self.goToModule.setToolTip('Go to the DataProbe module for more information and options') - self.frame.layout().addWidget(self.goToModule) - self.goToModule.connect("clicked()", self.onGoToModule) - # hide this for now - there's not much to see in the module itself - self.goToModule.hide() - - # image view: To ensure the height of the checkbox matches the height of the - # viewerFrame, it is added to a frame setting the layout and hard-coding the - # content margins. - # TODO: Revisit the approach and avoid hard-coding content margins - self.showImageFrame = qt.QFrame(self.frame) - self.frame.layout().addWidget(self.showImageFrame) - self.showImageFrame.setLayout(qt.QHBoxLayout()) - self.showImageFrame.layout().setContentsMargins(0, 3, 0, 3) - self.showImageBox = qt.QCheckBox('Show Zoomed Slice', self.showImageFrame) - self.showImageFrame.layout().addWidget(self.showImageBox) - self.showImageBox.connect("toggled(bool)", self.onShowImage) - self.showImageBox.setChecked(False) - - self.imageLabel = qt.QLabel() - - # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) - # fails on some systems, therefore set the policies using separate method calls - qSize = qt.QSizePolicy() - qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding) - qSize.setVerticalPolicy(qt.QSizePolicy.Expanding) - self.imageLabel.setSizePolicy(qSize) - #self.imageLabel.setScaledContents(True) - self.frame.layout().addWidget(self.imageLabel) - self.onShowImage(False) - - # top row - things about the viewer itself - self.viewerFrame = qt.QFrame(self.frame) - self.viewerFrame.setLayout(qt.QHBoxLayout()) - self.frame.layout().addWidget(self.viewerFrame) - self.viewerColor = qt.QLabel(self.viewerFrame) - self.viewerColor.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Preferred) - self.viewerFrame.layout().addWidget(self.viewerColor) - self.viewInfo = qt.QLabel() - self.viewerFrame.layout().addWidget(self.viewInfo) - - def _setFixedFontFamily(widget, family=None): - if family is None: - family = qt.QFontDatabase.systemFont(qt.QFontDatabase.FixedFont).family() - font = widget.font - font.setFamily(family) - widget.font = font - widget.wordWrap = True - _setFixedFontFamily(self.viewInfo) - - # the grid - things about the layers - # this method makes labels - self.layerGrid = qt.QFrame(self.frame) - layout = qt.QGridLayout() - self.layerGrid.setLayout(layout) - self.frame.layout().addWidget(self.layerGrid) - layers = ('L', 'F', 'B') - self.layerNames = {} - self.layerIJKs = {} - self.layerValues = {} - for (row, layer) in enumerate(layers): - col = 0 - layout.addWidget(qt.QLabel(layer), row, col) - col += 1 - self.layerNames[layer] = qt.QLabel() - layout.addWidget(self.layerNames[layer], row, col) - col += 1 - self.layerIJKs[layer] = qt.QLabel() - layout.addWidget(self.layerIJKs[layer], row, col) - col += 1 - self.layerValues[layer] = qt.QLabel() - layout.addWidget(self.layerValues[layer], row, col) - layout.setColumnStretch(col, 100) - - _setFixedFontFamily(self.layerNames[layer]) - _setFixedFontFamily(self.layerIJKs[layer]) - _setFixedFontFamily(self.layerValues[layer]) - - # information collected about the current crosshair position - # from displayable managers registered to the current view - self.displayableManagerInfo = qt.QLabel() - self.displayableManagerInfo.indent = 6 - self.displayableManagerInfo.wordWrap = True - self.frame.layout().addWidget(self.displayableManagerInfo) - # only show if not empty - self.displayableManagerInfo.hide() - - # goto module button - self.goToModule = qt.QPushButton('->', self.frame) - self.goToModule.setToolTip('Go to the DataProbe module for more information and options') - self.frame.layout().addWidget(self.goToModule) - self.goToModule.connect("clicked()", self.onGoToModule) - # hide this for now - there's not much to see in the module itself - self.goToModule.hide() - - def onGoToModule(self): - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('DataProbe') - - def onShowImage(self, value=False): - self.showImage = value - if value: - self.imageLabel.show() - else: - self.imageLabel.hide() - pixmap = qt.QPixmap() - self.imageLabel.setPixmap(pixmap) + self.imageLabel.hide() + pixmap = qt.QPixmap() + self.imageLabel.setPixmap(pixmap) # @@ -520,29 +520,29 @@ def onShowImage(self, value=False): class DataProbeWidget(ScriptedLoadableModuleWidget): - def enter(self): - pass + def enter(self): + pass - def exit(self): - pass + def exit(self): + pass - def updateGUIFromMRML(self, caller, event): - pass + def updateGUIFromMRML(self, caller, event): + pass - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - # Instantiate and connect widgets ... + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + # Instantiate and connect widgets ... - settingsCollapsibleButton = ctk.ctkCollapsibleButton() - settingsCollapsibleButton.text = "Slice View Annotations Settings" - self.layout.addWidget(settingsCollapsibleButton) - settingsVBoxLayout = qt.QVBoxLayout(settingsCollapsibleButton) - dataProbeInstance = slicer.modules.DataProbeInstance - if dataProbeInstance.infoWidget: - sliceAnnotationsFrame = dataProbeInstance.infoWidget.sliceAnnotations.window - settingsVBoxLayout.addWidget(sliceAnnotationsFrame) + settingsCollapsibleButton = ctk.ctkCollapsibleButton() + settingsCollapsibleButton.text = "Slice View Annotations Settings" + self.layout.addWidget(settingsCollapsibleButton) + settingsVBoxLayout = qt.QVBoxLayout(settingsCollapsibleButton) + dataProbeInstance = slicer.modules.DataProbeInstance + if dataProbeInstance.infoWidget: + sliceAnnotationsFrame = dataProbeInstance.infoWidget.sliceAnnotations.window + settingsVBoxLayout.addWidget(sliceAnnotationsFrame) - self.parent.layout().addStretch(1) + self.parent.layout().addStretch(1) class CalculateTensorScalars: @@ -583,49 +583,49 @@ def __call__(self, tensor, operation=None): class DataProbeTest(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. - """ - pass - - def runTest(self): - """Run as few or as many tests as needed here. """ - self.setUp() - self.test_DataProbe1() - - def test_DataProbe1(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. + 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 """ - self.delayDisplay("Starting the test") - - # - # first, get some data - # - import SampleData - SampleData.downloadFromURL( - nodeNames='FA', - fileNames='FA.nrrd', - uris=TESTING_DATA_URL + 'SHA256/12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560', - checksums='SHA256:12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560') - self.delayDisplay('Finished with download and loading') - - self.widget = DataProbeInfoWidget() - self.widget.frame.show() - - self.delayDisplay('Test passed!') + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + pass + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_DataProbe1() + + def test_DataProbe1(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 SampleData + SampleData.downloadFromURL( + nodeNames='FA', + fileNames='FA.nrrd', + uris=TESTING_DATA_URL + 'SHA256/12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560', + checksums='SHA256:12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560') + self.delayDisplay('Finished with download and loading') + + self.widget = DataProbeInfoWidget() + self.widget.frame.show() + + self.delayDisplay('Test passed!') diff --git a/Modules/Scripted/DataProbe/DataProbeLib/DataProbeUtil.py b/Modules/Scripted/DataProbe/DataProbeLib/DataProbeUtil.py index 43b07683754..5608bae1feb 100644 --- a/Modules/Scripted/DataProbe/DataProbeLib/DataProbeUtil.py +++ b/Modules/Scripted/DataProbe/DataProbeLib/DataProbeUtil.py @@ -18,33 +18,33 @@ class DataProbeUtil: - def getParameterNode(self): - """Get the DataProbe parameter node - a singleton in the scene""" - node = self._findParameterNodeInScene() - if not node: - node = self._createParameterNode() - return node - - def _findParameterNodeInScene(self): - node = None - size = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScriptedModuleNode") - for i in range(size): - n = slicer.mrmlScene.GetNthNodeByClass( i, "vtkMRMLScriptedModuleNode" ) - if n.GetModuleName() == "DataProbe": - node = n - return node - - def _createParameterNode(self): - """create the DataProbe parameter node - a singleton in the scene - This is used internally by getParameterNode - shouldn't really - be called for any other reason. - """ - node = slicer.vtkMRMLScriptedModuleNode() - node.SetSingletonTag( "DataProbe" ) - node.SetModuleName( "DataProbe" ) - #node.SetParameter( "label", "1" ) - slicer.mrmlScene.AddNode(node) - # Since we are a singleton, the scene won't add our node into the scene, - # but will instead insert a copy, so we find that and return it - node = self._findParameterNodeInScene() - return node + def getParameterNode(self): + """Get the DataProbe parameter node - a singleton in the scene""" + node = self._findParameterNodeInScene() + if not node: + node = self._createParameterNode() + return node + + def _findParameterNodeInScene(self): + node = None + size = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScriptedModuleNode") + for i in range(size): + n = slicer.mrmlScene.GetNthNodeByClass(i, "vtkMRMLScriptedModuleNode") + if n.GetModuleName() == "DataProbe": + node = n + return node + + def _createParameterNode(self): + """create the DataProbe parameter node - a singleton in the scene + This is used internally by getParameterNode - shouldn't really + be called for any other reason. + """ + node = slicer.vtkMRMLScriptedModuleNode() + node.SetSingletonTag("DataProbe") + node.SetModuleName("DataProbe") + # node.SetParameter( "label", "1" ) + slicer.mrmlScene.AddNode(node) + # Since we are a singleton, the scene won't add our node into the scene, + # but will instead insert a copy, so we find that and return it + node = self._findParameterNodeInScene() + return node diff --git a/Modules/Scripted/DataProbe/DataProbeLib/SliceViewAnnotations.py b/Modules/Scripted/DataProbe/DataProbeLib/SliceViewAnnotations.py index 7ceb27d32ad..5774a7cb5c6 100644 --- a/Modules/Scripted/DataProbe/DataProbeLib/SliceViewAnnotations.py +++ b/Modules/Scripted/DataProbe/DataProbeLib/SliceViewAnnotations.py @@ -11,647 +11,647 @@ class SliceAnnotations(VTKObservationMixin): - """Implement the Qt window showing settings for Slice View Annotations - """ - - def __init__(self, layoutManager=None): - VTKObservationMixin.__init__(self) - - self.layoutManager = layoutManager - if self.layoutManager is None: - self.layoutManager = slicer.app.layoutManager() - self.layoutManager.connect("destroyed()", self.onLayoutManagerDestroyed) - - self.dataProbeUtil = DataProbeUtil.DataProbeUtil() - - self.dicomVolumeNode = 0 - - # Cache recently used extracted DICOM values. - # Getting all necessary DICOM values from the database (tag cache) - # would slow down slice browsing significantly. - # We may have several different volumes shown in different slice views, - # so we keep in the cache a number of items, not just 2. - self.extractedDICOMValuesCacheSize = 12 - import collections - self.extractedDICOMValuesCache = collections.OrderedDict() - - self.sliceViewNames = [] - self.popupGeometry = qt.QRect() - self.cornerTexts =[] - # Bottom Left Corner Text - self.cornerTexts.append({ - '1-Label':{'text':'','category':'A'}, - '2-Foreground':{'text':'','category':'A'}, - '3-Background':{'text':'','category':'A'} - }) - # Bottom Right Corner Text - # Not used - orientation figure may be drawn there - self.cornerTexts.append({ - '1-TR':{'text':'','category':'A'}, - '2-TE':{'text':'','category':'A'} - }) - # Top Left Corner Text - self.cornerTexts.append({ - '1-PatientName':{'text':'','category':'B'}, - '2-PatientID':{'text':'','category':'A'}, - '3-PatientInfo':{'text':'','category':'B'}, - '4-Bg-SeriesDate':{'text':'','category':'B'}, - '5-Fg-SeriesDate':{'text':'','category':'B'}, - '6-Bg-SeriesTime':{'text':'','category':'C'}, - '7-Bg-SeriesTime':{'text':'','category':'C'}, - '8-Bg-SeriesDescription':{'text':'','category':'C'}, - '9-Fg-SeriesDescription':{'text':'','category':'C'} - }) - # Top Right Corner Text - self.cornerTexts.append({ - '1-Institution-Name':{'text':'','category':'B'}, - '2-Referring-Phisycian':{'text':'','category':'B'}, - '3-Manufacturer':{'text':'','category':'C'}, - '4-Model':{'text':'','category':'C'}, - '5-Patient-Position':{'text':'','category':'A'}, - '6-TR':{'text':'','category':'A'}, - '7-TE':{'text':'','category':'A'} - }) - - self.annotationsDisplayAmount = 0 - - # - self.scene = slicer.mrmlScene - self.sliceViews = {} - - # If there are no user settings load defaults - self.sliceViewAnnotationsEnabled = settingsValue('DataProbe/sliceViewAnnotations.enabled', 1, converter=int) - - self.bottomLeft = settingsValue('DataProbe/sliceViewAnnotations.bottomLeft', 1, converter=int) - self.topLeft = settingsValue('DataProbe/sliceViewAnnotations.topLeft', 0, converter=int) - self.topRight = settingsValue('DataProbe/sliceViewAnnotations.topRight', 0, converter=int) - self.fontFamily = settingsValue('DataProbe/sliceViewAnnotations.fontFamily', 'Times') - self.fontSize = settingsValue('DataProbe/sliceViewAnnotations.fontSize', 14, converter=int) - self.backgroundDICOMAnnotationsPersistence = settingsValue( - 'DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence', 0, converter=int) - self.sliceViewAnnotationsEnabledparameter = 'sliceViewAnnotationsEnabled' - self.parameterNode = self.dataProbeUtil.getParameterNode() - self.addObserver(self.parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromMRML) - - self.maximumTextLength = 35 - - self.create() - - if self.sliceViewAnnotationsEnabled: - self.updateSliceViewFromGUI() - - def create(self): - # Instantiate and connect widgets ... - loader = qt.QUiLoader() - path = os.path.join(os.path.dirname(__file__), 'Resources', 'UI','settings.ui') - qfile = qt.QFile(path) - qfile.open(qt.QFile.ReadOnly) - self.window = loader.load(qfile) - window = self.window - - find = slicer.util.findChildren - self.cornerTextParametersCollapsibleButton = find(window, 'cornerTextParametersCollapsibleButton')[0] - self.sliceViewAnnotationsCheckBox = find(window,'sliceViewAnnotationsCheckBox')[0] - self.sliceViewAnnotationsCheckBox.checked = self.sliceViewAnnotationsEnabled - - self.activateCornersGroupBox = find(window,'activateCornersGroupBox')[0] - self.topLeftCheckBox = find(window,'topLeftCheckBox')[0] - self.topLeftCheckBox.checked = self.topLeft - self.topRightCheckBox = find(window,'topRightCheckBox')[0] - self.topRightCheckBox.checked = self.topRight - - self.bottomLeftCheckBox = find(window, 'bottomLeftCheckBox')[0] - self.bottomLeftCheckBox.checked = self.bottomLeft - - self.level1RadioButton = find(window,'level1RadioButton')[0] - self.level2RadioButton = find(window,'level2RadioButton')[0] - self.level3RadioButton = find(window,'level3RadioButton')[0] - - self.fontPropertiesGroupBox = find(window,'fontPropertiesGroupBox')[0] - self.timesFontRadioButton = find(window,'timesFontRadioButton')[0] - self.arialFontRadioButton = find(window,'arialFontRadioButton')[0] - if self.fontFamily == 'Times': - self.timesFontRadioButton.checked = True - else: - self.arialFontRadioButton.checked = True - - self.fontSizeSpinBox = find(window,'fontSizeSpinBox')[0] - self.fontSizeSpinBox.value = self.fontSize - - self.backgroundPersistenceCheckBox = find(window,'backgroundPersistenceCheckBox')[0] - self.backgroundPersistenceCheckBox.checked = self.backgroundDICOMAnnotationsPersistence - - self.annotationsAmountGroupBox = find(window,'annotationsAmountGroupBox')[0] - - self.restoreDefaultsButton = find(window, 'restoreDefaultsButton')[0] - - self.updateEnabledButtons() - - # connections - self.sliceViewAnnotationsCheckBox.connect('clicked()', self.onSliceViewAnnotationsCheckBox) - - self.topLeftCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox) - self.topRightCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox) - self.bottomLeftCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox) - self.timesFontRadioButton.connect('clicked()', self.onFontFamilyRadioButton) - self.arialFontRadioButton.connect('clicked()', self.onFontFamilyRadioButton) - self.fontSizeSpinBox.connect('valueChanged(int)', self.onFontSizeSpinBox) - - self.level1RadioButton.connect('clicked()', self.updateSliceViewFromGUI) - self.level2RadioButton.connect('clicked()', self.updateSliceViewFromGUI) - self.level3RadioButton.connect('clicked()', self.updateSliceViewFromGUI) - - self.backgroundPersistenceCheckBox.connect('clicked()', self.onBackgroundLayerPersistenceCheckBox) - - self.restoreDefaultsButton.connect('clicked()', self.restoreDefaultValues) - - def onLayoutManagerDestroyed(self): - self.layoutManager = slicer.app.layoutManager() - if self.layoutManager: - self.layoutManager.connect("destroyed()", self.onLayoutManagerDestroyed) - - def onSliceViewAnnotationsCheckBox(self): - if self.sliceViewAnnotationsCheckBox.checked: - self.sliceViewAnnotationsEnabled = 1 - else: - self.sliceViewAnnotationsEnabled = 0 - - settings = qt.QSettings() - settings.setValue('DataProbe/sliceViewAnnotations.enabled', self.sliceViewAnnotationsEnabled) - - self.updateEnabledButtons() - self.updateSliceViewFromGUI() - - def onBackgroundLayerPersistenceCheckBox(self): - if self.backgroundPersistenceCheckBox.checked: - self.backgroundDICOMAnnotationsPersistence = 1 - else: - self.backgroundDICOMAnnotationsPersistence = 0 - settings = qt.QSettings() - settings.setValue('DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence', - self.backgroundDICOMAnnotationsPersistence) - self.updateSliceViewFromGUI() - - def onCornerTextsActivationCheckBox(self): - self.topLeft = int(self.topLeftCheckBox.checked) - self.topRight = int(self.topRightCheckBox.checked) - self.bottomLeft = int(self.bottomLeftCheckBox.checked) - - self.updateSliceViewFromGUI() - - settings = qt.QSettings() - settings.setValue('DataProbe/sliceViewAnnotations.topLeft', - self.topLeft) - settings.setValue('DataProbe/sliceViewAnnotations.topRight', - self.topRight) - settings.setValue('DataProbe/sliceViewAnnotations.bottomLeft', - self.bottomLeft) - - def onFontFamilyRadioButton(self): - # Updating font size and family - if self.timesFontRadioButton.checked: - self.fontFamily = 'Times' - else: - self.fontFamily = 'Arial' - settings = qt.QSettings() - settings.setValue('DataProbe/sliceViewAnnotations.fontFamily', - self.fontFamily) - self.updateSliceViewFromGUI() - - def onFontSizeSpinBox(self): - self.fontSize = self.fontSizeSpinBox.value - settings = qt.QSettings() - settings.setValue('DataProbe/sliceViewAnnotations.fontSize', - self.fontSize) - self.updateSliceViewFromGUI() - - def restoreDefaultValues(self): - self.topLeftCheckBox.checked = True - self.topLeft = 1 - self.topRightCheckBox.checked = True - self.topRight = 1 - self.bottomLeftCheckBox.checked = True - self.bottomLeft = 1 - self.fontSizeSpinBox.value = 14 - self.timesFontRadioButton.checked = True - self.fontFamily = 'Times' - self.backgroundDICOMAnnotationsPersistence = 0 - self.backgroundPersistenceCheckBox.checked = False - - settings = qt.QSettings() - settings.setValue('DataProbe/sliceViewAnnotations.enabled', self.sliceViewAnnotationsEnabled) - settings.setValue('DataProbe/sliceViewAnnotations.topLeft', self.topLeft) - settings.setValue('DataProbe/sliceViewAnnotations.topRight', self.topRight) - settings.setValue('DataProbe/sliceViewAnnotations.bottomLeft', self.bottomLeft) - settings.setValue('DataProbe/sliceViewAnnotations.fontFamily',self.fontFamily) - settings.setValue('DataProbe/sliceViewAnnotations.fontSize',self.fontSize) - settings.setValue('DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence', - self.backgroundDICOMAnnotationsPersistence) - - self.updateSliceViewFromGUI() - - def updateGUIFromMRML(self,caller,event): - if self.parameterNode.GetParameter(self.sliceViewAnnotationsEnabledparameter) == '': - # parameter does not exist - probably initializing - return - self.sliceViewAnnotationsEnabled = int(self.parameterNode.GetParameter(self.sliceViewAnnotationsEnabledparameter)) - self.updateSliceViewFromGUI() - - def updateEnabledButtons(self): - enabled = self.sliceViewAnnotationsEnabled - - self.cornerTextParametersCollapsibleButton.enabled = enabled - self.activateCornersGroupBox.enabled = enabled - self.fontPropertiesGroupBox.enabled = enabled - self.annotationsAmountGroupBox.enabled = enabled - self.restoreDefaultsButton.enabled = enabled - - def updateSliceViewFromGUI(self): - if not self.sliceViewAnnotationsEnabled: - self.removeObservers(method=self.updateViewAnnotations) - self.removeObservers(method=self.updateGUIFromMRML) - return - - # Create corner annotations if have not created already - if len(self.sliceViewNames) == 0: - self.createCornerAnnotations() - - # Updating Annotations Amount - if self.level1RadioButton.checked: - self.annotationsDisplayAmount = 0 - elif self.level2RadioButton.checked: - self.annotationsDisplayAmount = 1 - elif self.level3RadioButton.checked: - self.annotationsDisplayAmount = 2 - - for sliceViewName in self.sliceViewNames: - sliceWidget = self.layoutManager.sliceWidget(sliceViewName) - if sliceWidget: - sl = sliceWidget.sliceLogic() - self.updateCornerAnnotation(sl) - - def createGlobalVariables(self): - self.sliceViewNames = [] - self.sliceWidgets = {} - self.sliceViews = {} - self.renderers = {} - - def createCornerAnnotations(self): - self.createGlobalVariables() - self.sliceViewNames = list(self.layoutManager.sliceViewNames()) - for sliceViewName in self.sliceViewNames: - self.addSliceViewObserver(sliceViewName) - self.createActors(sliceViewName) - - def addSliceViewObserver(self, sliceViewName): - sliceWidget = self.layoutManager.sliceWidget(sliceViewName) - self.sliceWidgets[sliceViewName] = sliceWidget - sliceView = sliceWidget.sliceView() - - renderWindow = sliceView.renderWindow() - renderer = renderWindow.GetRenderers().GetItemAsObject(0) - self.renderers[sliceViewName] = renderer - - self.sliceViews[sliceViewName] = sliceView - sliceLogic = sliceWidget.sliceLogic() - self.addObserver(sliceLogic, vtk.vtkCommand.ModifiedEvent, self.updateViewAnnotations) - - def createActors(self, sliceViewName): - sliceWidget = self.layoutManager.sliceWidget(sliceViewName) - self.sliceWidgets[sliceViewName] = sliceWidget - - def updateViewAnnotations(self,caller,event): - if not self.sliceViewAnnotationsEnabled: - # when self.sliceViewAnnotationsEnabled is set to false - # then annotation and scalar bar gets hidden, therefore - # we have nothing to do here - return - - layoutManager = self.layoutManager - if layoutManager is None: - return - sliceViewNames = layoutManager.sliceViewNames() - for sliceViewName in sliceViewNames: - if sliceViewName not in self.sliceViewNames: - self.sliceViewNames.append(sliceViewName) - self.addSliceViewObserver(sliceViewName) - self.createActors(sliceViewName) + """Implement the Qt window showing settings for Slice View Annotations + """ + + def __init__(self, layoutManager=None): + VTKObservationMixin.__init__(self) + + self.layoutManager = layoutManager + if self.layoutManager is None: + self.layoutManager = slicer.app.layoutManager() + self.layoutManager.connect("destroyed()", self.onLayoutManagerDestroyed) + + self.dataProbeUtil = DataProbeUtil.DataProbeUtil() + + self.dicomVolumeNode = 0 + + # Cache recently used extracted DICOM values. + # Getting all necessary DICOM values from the database (tag cache) + # would slow down slice browsing significantly. + # We may have several different volumes shown in different slice views, + # so we keep in the cache a number of items, not just 2. + self.extractedDICOMValuesCacheSize = 12 + import collections + self.extractedDICOMValuesCache = collections.OrderedDict() + + self.sliceViewNames = [] + self.popupGeometry = qt.QRect() + self.cornerTexts = [] + # Bottom Left Corner Text + self.cornerTexts.append({ + '1-Label': {'text': '', 'category': 'A'}, + '2-Foreground': {'text': '', 'category': 'A'}, + '3-Background': {'text': '', 'category': 'A'} + }) + # Bottom Right Corner Text + # Not used - orientation figure may be drawn there + self.cornerTexts.append({ + '1-TR': {'text': '', 'category': 'A'}, + '2-TE': {'text': '', 'category': 'A'} + }) + # Top Left Corner Text + self.cornerTexts.append({ + '1-PatientName': {'text': '', 'category': 'B'}, + '2-PatientID': {'text': '', 'category': 'A'}, + '3-PatientInfo': {'text': '', 'category': 'B'}, + '4-Bg-SeriesDate': {'text': '', 'category': 'B'}, + '5-Fg-SeriesDate': {'text': '', 'category': 'B'}, + '6-Bg-SeriesTime': {'text': '', 'category': 'C'}, + '7-Bg-SeriesTime': {'text': '', 'category': 'C'}, + '8-Bg-SeriesDescription': {'text': '', 'category': 'C'}, + '9-Fg-SeriesDescription': {'text': '', 'category': 'C'} + }) + # Top Right Corner Text + self.cornerTexts.append({ + '1-Institution-Name': {'text': '', 'category': 'B'}, + '2-Referring-Phisycian': {'text': '', 'category': 'B'}, + '3-Manufacturer': {'text': '', 'category': 'C'}, + '4-Model': {'text': '', 'category': 'C'}, + '5-Patient-Position': {'text': '', 'category': 'A'}, + '6-TR': {'text': '', 'category': 'A'}, + '7-TE': {'text': '', 'category': 'A'} + }) + + self.annotationsDisplayAmount = 0 + + # + self.scene = slicer.mrmlScene + self.sliceViews = {} + + # If there are no user settings load defaults + self.sliceViewAnnotationsEnabled = settingsValue('DataProbe/sliceViewAnnotations.enabled', 1, converter=int) + + self.bottomLeft = settingsValue('DataProbe/sliceViewAnnotations.bottomLeft', 1, converter=int) + self.topLeft = settingsValue('DataProbe/sliceViewAnnotations.topLeft', 0, converter=int) + self.topRight = settingsValue('DataProbe/sliceViewAnnotations.topRight', 0, converter=int) + self.fontFamily = settingsValue('DataProbe/sliceViewAnnotations.fontFamily', 'Times') + self.fontSize = settingsValue('DataProbe/sliceViewAnnotations.fontSize', 14, converter=int) + self.backgroundDICOMAnnotationsPersistence = settingsValue( + 'DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence', 0, converter=int) + self.sliceViewAnnotationsEnabledparameter = 'sliceViewAnnotationsEnabled' + self.parameterNode = self.dataProbeUtil.getParameterNode() + self.addObserver(self.parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromMRML) + + self.maximumTextLength = 35 + + self.create() + + if self.sliceViewAnnotationsEnabled: + self.updateSliceViewFromGUI() + + def create(self): + # Instantiate and connect widgets ... + loader = qt.QUiLoader() + path = os.path.join(os.path.dirname(__file__), 'Resources', 'UI', 'settings.ui') + qfile = qt.QFile(path) + qfile.open(qt.QFile.ReadOnly) + self.window = loader.load(qfile) + window = self.window + + find = slicer.util.findChildren + self.cornerTextParametersCollapsibleButton = find(window, 'cornerTextParametersCollapsibleButton')[0] + self.sliceViewAnnotationsCheckBox = find(window, 'sliceViewAnnotationsCheckBox')[0] + self.sliceViewAnnotationsCheckBox.checked = self.sliceViewAnnotationsEnabled + + self.activateCornersGroupBox = find(window, 'activateCornersGroupBox')[0] + self.topLeftCheckBox = find(window, 'topLeftCheckBox')[0] + self.topLeftCheckBox.checked = self.topLeft + self.topRightCheckBox = find(window, 'topRightCheckBox')[0] + self.topRightCheckBox.checked = self.topRight + + self.bottomLeftCheckBox = find(window, 'bottomLeftCheckBox')[0] + self.bottomLeftCheckBox.checked = self.bottomLeft + + self.level1RadioButton = find(window, 'level1RadioButton')[0] + self.level2RadioButton = find(window, 'level2RadioButton')[0] + self.level3RadioButton = find(window, 'level3RadioButton')[0] + + self.fontPropertiesGroupBox = find(window, 'fontPropertiesGroupBox')[0] + self.timesFontRadioButton = find(window, 'timesFontRadioButton')[0] + self.arialFontRadioButton = find(window, 'arialFontRadioButton')[0] + if self.fontFamily == 'Times': + self.timesFontRadioButton.checked = True + else: + self.arialFontRadioButton.checked = True + + self.fontSizeSpinBox = find(window, 'fontSizeSpinBox')[0] + self.fontSizeSpinBox.value = self.fontSize + + self.backgroundPersistenceCheckBox = find(window, 'backgroundPersistenceCheckBox')[0] + self.backgroundPersistenceCheckBox.checked = self.backgroundDICOMAnnotationsPersistence + + self.annotationsAmountGroupBox = find(window, 'annotationsAmountGroupBox')[0] + + self.restoreDefaultsButton = find(window, 'restoreDefaultsButton')[0] + + self.updateEnabledButtons() + + # connections + self.sliceViewAnnotationsCheckBox.connect('clicked()', self.onSliceViewAnnotationsCheckBox) + + self.topLeftCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox) + self.topRightCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox) + self.bottomLeftCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox) + self.timesFontRadioButton.connect('clicked()', self.onFontFamilyRadioButton) + self.arialFontRadioButton.connect('clicked()', self.onFontFamilyRadioButton) + self.fontSizeSpinBox.connect('valueChanged(int)', self.onFontSizeSpinBox) + + self.level1RadioButton.connect('clicked()', self.updateSliceViewFromGUI) + self.level2RadioButton.connect('clicked()', self.updateSliceViewFromGUI) + self.level3RadioButton.connect('clicked()', self.updateSliceViewFromGUI) + + self.backgroundPersistenceCheckBox.connect('clicked()', self.onBackgroundLayerPersistenceCheckBox) + + self.restoreDefaultsButton.connect('clicked()', self.restoreDefaultValues) + + def onLayoutManagerDestroyed(self): + self.layoutManager = slicer.app.layoutManager() + if self.layoutManager: + self.layoutManager.connect("destroyed()", self.onLayoutManagerDestroyed) + + def onSliceViewAnnotationsCheckBox(self): + if self.sliceViewAnnotationsCheckBox.checked: + self.sliceViewAnnotationsEnabled = 1 + else: + self.sliceViewAnnotationsEnabled = 0 + + settings = qt.QSettings() + settings.setValue('DataProbe/sliceViewAnnotations.enabled', self.sliceViewAnnotationsEnabled) + + self.updateEnabledButtons() self.updateSliceViewFromGUI() - self.makeAnnotationText(caller) - - def updateCornerAnnotation(self, sliceLogic): - - sliceNode = sliceLogic.GetBackgroundLayer().GetSliceNode() - sliceViewName = sliceNode.GetLayoutName() - - enabled = self.sliceViewAnnotationsEnabled - - cornerAnnotation = self.sliceViews[sliceViewName].cornerAnnotation() - - if enabled: - # Font - cornerAnnotation.SetMaximumFontSize(self.fontSize) - cornerAnnotation.SetMinimumFontSize(self.fontSize) - cornerAnnotation.SetNonlinearFontScaleFactor(1) - textProperty = cornerAnnotation.GetTextProperty() - if self.fontFamily == 'Times': - textProperty.SetFontFamilyToTimes() - else: - textProperty.SetFontFamilyToArial() - # Text - self.makeAnnotationText(sliceLogic) - else: - # Clear Annotations - for position in range(4): - cornerAnnotation.SetText(position, "") - - self.sliceViews[sliceViewName].scheduleRender() - - def makeAnnotationText(self, sliceLogic): - self.resetTexts() - sliceCompositeNode = sliceLogic.GetSliceCompositeNode() - if not sliceCompositeNode: - return - - # Get the layers - backgroundLayer = sliceLogic.GetBackgroundLayer() - foregroundLayer = sliceLogic.GetForegroundLayer() - labelLayer = sliceLogic.GetLabelLayer() - - # Get the volumes - backgroundVolume = backgroundLayer.GetVolumeNode() - foregroundVolume = foregroundLayer.GetVolumeNode() - labelVolume = labelLayer.GetVolumeNode() - - # Get slice view name - sliceNode = backgroundLayer.GetSliceNode() - if not sliceNode: - return - sliceViewName = sliceNode.GetLayoutName() - - if self.sliceViews[sliceViewName]: - # - # Update slice corner annotations - # - # Case I: Both background and foregraound - if ( backgroundVolume is not None and foregroundVolume is not None): - if self.bottomLeft: - foregroundOpacity = sliceCompositeNode.GetForegroundOpacity() - backgroundVolumeName = backgroundVolume.GetName() - foregroundVolumeName = foregroundVolume.GetName() - self.cornerTexts[0]['3-Background']['text'] = 'B: ' + backgroundVolumeName - self.cornerTexts[0]['2-Foreground']['text'] = 'F: ' + foregroundVolumeName + ' (' + str( - "%d"%(foregroundOpacity*100)) + '%)' - - bgUids = backgroundVolume.GetAttribute('DICOM.instanceUIDs') - fgUids = foregroundVolume.GetAttribute('DICOM.instanceUIDs') - if (bgUids and fgUids): - bgUid = bgUids.partition(' ')[0] - fgUid = fgUids.partition(' ')[0] - self.dicomVolumeNode = 1 - self.makeDicomAnnotation(bgUid,fgUid,sliceViewName) - elif (bgUids and self.backgroundDICOMAnnotationsPersistence): - uid = bgUids.partition(' ')[0] - self.dicomVolumeNode = 1 - self.makeDicomAnnotation(uid,None,sliceViewName) + + def onBackgroundLayerPersistenceCheckBox(self): + if self.backgroundPersistenceCheckBox.checked: + self.backgroundDICOMAnnotationsPersistence = 1 + else: + self.backgroundDICOMAnnotationsPersistence = 0 + settings = qt.QSettings() + settings.setValue('DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence', + self.backgroundDICOMAnnotationsPersistence) + self.updateSliceViewFromGUI() + + def onCornerTextsActivationCheckBox(self): + self.topLeft = int(self.topLeftCheckBox.checked) + self.topRight = int(self.topRightCheckBox.checked) + self.bottomLeft = int(self.bottomLeftCheckBox.checked) + + self.updateSliceViewFromGUI() + + settings = qt.QSettings() + settings.setValue('DataProbe/sliceViewAnnotations.topLeft', + self.topLeft) + settings.setValue('DataProbe/sliceViewAnnotations.topRight', + self.topRight) + settings.setValue('DataProbe/sliceViewAnnotations.bottomLeft', + self.bottomLeft) + + def onFontFamilyRadioButton(self): + # Updating font size and family + if self.timesFontRadioButton.checked: + self.fontFamily = 'Times' else: - for key in self.cornerTexts[2]: - self.cornerTexts[2][key]['text'] = '' - self.dicomVolumeNode = 0 - - # Case II: Only background - elif (backgroundVolume is not None): - backgroundVolumeName = backgroundVolume.GetName() - if self.bottomLeft: - self.cornerTexts[0]['3-Background']['text'] = 'B: ' + backgroundVolumeName - - uids = backgroundVolume.GetAttribute('DICOM.instanceUIDs') - if uids: - uid = uids.partition(' ')[0] - self.makeDicomAnnotation(uid,None,sliceViewName) - self.dicomVolumeNode = 1 + self.fontFamily = 'Arial' + settings = qt.QSettings() + settings.setValue('DataProbe/sliceViewAnnotations.fontFamily', + self.fontFamily) + self.updateSliceViewFromGUI() + + def onFontSizeSpinBox(self): + self.fontSize = self.fontSizeSpinBox.value + settings = qt.QSettings() + settings.setValue('DataProbe/sliceViewAnnotations.fontSize', + self.fontSize) + self.updateSliceViewFromGUI() + + def restoreDefaultValues(self): + self.topLeftCheckBox.checked = True + self.topLeft = 1 + self.topRightCheckBox.checked = True + self.topRight = 1 + self.bottomLeftCheckBox.checked = True + self.bottomLeft = 1 + self.fontSizeSpinBox.value = 14 + self.timesFontRadioButton.checked = True + self.fontFamily = 'Times' + self.backgroundDICOMAnnotationsPersistence = 0 + self.backgroundPersistenceCheckBox.checked = False + + settings = qt.QSettings() + settings.setValue('DataProbe/sliceViewAnnotations.enabled', self.sliceViewAnnotationsEnabled) + settings.setValue('DataProbe/sliceViewAnnotations.topLeft', self.topLeft) + settings.setValue('DataProbe/sliceViewAnnotations.topRight', self.topRight) + settings.setValue('DataProbe/sliceViewAnnotations.bottomLeft', self.bottomLeft) + settings.setValue('DataProbe/sliceViewAnnotations.fontFamily', self.fontFamily) + settings.setValue('DataProbe/sliceViewAnnotations.fontSize', self.fontSize) + settings.setValue('DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence', + self.backgroundDICOMAnnotationsPersistence) + + self.updateSliceViewFromGUI() + + def updateGUIFromMRML(self, caller, event): + if self.parameterNode.GetParameter(self.sliceViewAnnotationsEnabledparameter) == '': + # parameter does not exist - probably initializing + return + self.sliceViewAnnotationsEnabled = int(self.parameterNode.GetParameter(self.sliceViewAnnotationsEnabledparameter)) + self.updateSliceViewFromGUI() + + def updateEnabledButtons(self): + enabled = self.sliceViewAnnotationsEnabled + + self.cornerTextParametersCollapsibleButton.enabled = enabled + self.activateCornersGroupBox.enabled = enabled + self.fontPropertiesGroupBox.enabled = enabled + self.annotationsAmountGroupBox.enabled = enabled + self.restoreDefaultsButton.enabled = enabled + + def updateSliceViewFromGUI(self): + if not self.sliceViewAnnotationsEnabled: + self.removeObservers(method=self.updateViewAnnotations) + self.removeObservers(method=self.updateGUIFromMRML) + return + + # Create corner annotations if have not created already + if len(self.sliceViewNames) == 0: + self.createCornerAnnotations() + + # Updating Annotations Amount + if self.level1RadioButton.checked: + self.annotationsDisplayAmount = 0 + elif self.level2RadioButton.checked: + self.annotationsDisplayAmount = 1 + elif self.level3RadioButton.checked: + self.annotationsDisplayAmount = 2 + + for sliceViewName in self.sliceViewNames: + sliceWidget = self.layoutManager.sliceWidget(sliceViewName) + if sliceWidget: + sl = sliceWidget.sliceLogic() + self.updateCornerAnnotation(sl) + + def createGlobalVariables(self): + self.sliceViewNames = [] + self.sliceWidgets = {} + self.sliceViews = {} + self.renderers = {} + + def createCornerAnnotations(self): + self.createGlobalVariables() + self.sliceViewNames = list(self.layoutManager.sliceViewNames()) + for sliceViewName in self.sliceViewNames: + self.addSliceViewObserver(sliceViewName) + self.createActors(sliceViewName) + + def addSliceViewObserver(self, sliceViewName): + sliceWidget = self.layoutManager.sliceWidget(sliceViewName) + self.sliceWidgets[sliceViewName] = sliceWidget + sliceView = sliceWidget.sliceView() + + renderWindow = sliceView.renderWindow() + renderer = renderWindow.GetRenderers().GetItemAsObject(0) + self.renderers[sliceViewName] = renderer + + self.sliceViews[sliceViewName] = sliceView + sliceLogic = sliceWidget.sliceLogic() + self.addObserver(sliceLogic, vtk.vtkCommand.ModifiedEvent, self.updateViewAnnotations) + + def createActors(self, sliceViewName): + sliceWidget = self.layoutManager.sliceWidget(sliceViewName) + self.sliceWidgets[sliceViewName] = sliceWidget + + def updateViewAnnotations(self, caller, event): + if not self.sliceViewAnnotationsEnabled: + # when self.sliceViewAnnotationsEnabled is set to false + # then annotation and scalar bar gets hidden, therefore + # we have nothing to do here + return + + layoutManager = self.layoutManager + if layoutManager is None: + return + sliceViewNames = layoutManager.sliceViewNames() + for sliceViewName in sliceViewNames: + if sliceViewName not in self.sliceViewNames: + self.sliceViewNames.append(sliceViewName) + self.addSliceViewObserver(sliceViewName) + self.createActors(sliceViewName) + self.updateSliceViewFromGUI() + self.makeAnnotationText(caller) + + def updateCornerAnnotation(self, sliceLogic): + + sliceNode = sliceLogic.GetBackgroundLayer().GetSliceNode() + sliceViewName = sliceNode.GetLayoutName() + + enabled = self.sliceViewAnnotationsEnabled + + cornerAnnotation = self.sliceViews[sliceViewName].cornerAnnotation() + + if enabled: + # Font + cornerAnnotation.SetMaximumFontSize(self.fontSize) + cornerAnnotation.SetMinimumFontSize(self.fontSize) + cornerAnnotation.SetNonlinearFontScaleFactor(1) + textProperty = cornerAnnotation.GetTextProperty() + if self.fontFamily == 'Times': + textProperty.SetFontFamilyToTimes() + else: + textProperty.SetFontFamilyToArial() + # Text + self.makeAnnotationText(sliceLogic) else: - self.dicomVolumeNode = 0 - - # Case III: Only foreground - elif (foregroundVolume is not None): - if self.bottomLeft: - foregroundVolumeName = foregroundVolume.GetName() - self.cornerTexts[0]['2-Foreground']['text'] = 'F: ' + foregroundVolumeName - - uids = foregroundVolume.GetAttribute('DICOM.instanceUIDs') - if uids: - uid = uids.partition(' ')[0] - # passed UID as bg - self.makeDicomAnnotation(uid,None,sliceViewName) - self.dicomVolumeNode = 1 + # Clear Annotations + for position in range(4): + cornerAnnotation.SetText(position, "") + + self.sliceViews[sliceViewName].scheduleRender() + + def makeAnnotationText(self, sliceLogic): + self.resetTexts() + sliceCompositeNode = sliceLogic.GetSliceCompositeNode() + if not sliceCompositeNode: + return + + # Get the layers + backgroundLayer = sliceLogic.GetBackgroundLayer() + foregroundLayer = sliceLogic.GetForegroundLayer() + labelLayer = sliceLogic.GetLabelLayer() + + # Get the volumes + backgroundVolume = backgroundLayer.GetVolumeNode() + foregroundVolume = foregroundLayer.GetVolumeNode() + labelVolume = labelLayer.GetVolumeNode() + + # Get slice view name + sliceNode = backgroundLayer.GetSliceNode() + if not sliceNode: + return + sliceViewName = sliceNode.GetLayoutName() + + if self.sliceViews[sliceViewName]: + # + # Update slice corner annotations + # + # Case I: Both background and foregraound + if (backgroundVolume is not None and foregroundVolume is not None): + if self.bottomLeft: + foregroundOpacity = sliceCompositeNode.GetForegroundOpacity() + backgroundVolumeName = backgroundVolume.GetName() + foregroundVolumeName = foregroundVolume.GetName() + self.cornerTexts[0]['3-Background']['text'] = 'B: ' + backgroundVolumeName + self.cornerTexts[0]['2-Foreground']['text'] = 'F: ' + foregroundVolumeName + ' (' + str( + "%d" % (foregroundOpacity * 100)) + '%)' + + bgUids = backgroundVolume.GetAttribute('DICOM.instanceUIDs') + fgUids = foregroundVolume.GetAttribute('DICOM.instanceUIDs') + if (bgUids and fgUids): + bgUid = bgUids.partition(' ')[0] + fgUid = fgUids.partition(' ')[0] + self.dicomVolumeNode = 1 + self.makeDicomAnnotation(bgUid, fgUid, sliceViewName) + elif (bgUids and self.backgroundDICOMAnnotationsPersistence): + uid = bgUids.partition(' ')[0] + self.dicomVolumeNode = 1 + self.makeDicomAnnotation(uid, None, sliceViewName) + else: + for key in self.cornerTexts[2]: + self.cornerTexts[2][key]['text'] = '' + self.dicomVolumeNode = 0 + + # Case II: Only background + elif (backgroundVolume is not None): + backgroundVolumeName = backgroundVolume.GetName() + if self.bottomLeft: + self.cornerTexts[0]['3-Background']['text'] = 'B: ' + backgroundVolumeName + + uids = backgroundVolume.GetAttribute('DICOM.instanceUIDs') + if uids: + uid = uids.partition(' ')[0] + self.makeDicomAnnotation(uid, None, sliceViewName) + self.dicomVolumeNode = 1 + else: + self.dicomVolumeNode = 0 + + # Case III: Only foreground + elif (foregroundVolume is not None): + if self.bottomLeft: + foregroundVolumeName = foregroundVolume.GetName() + self.cornerTexts[0]['2-Foreground']['text'] = 'F: ' + foregroundVolumeName + + uids = foregroundVolume.GetAttribute('DICOM.instanceUIDs') + if uids: + uid = uids.partition(' ')[0] + # passed UID as bg + self.makeDicomAnnotation(uid, None, sliceViewName) + self.dicomVolumeNode = 1 + else: + self.dicomVolumeNode = 0 + + if (labelVolume is not None) and self.bottomLeft: + labelOpacity = sliceCompositeNode.GetLabelOpacity() + labelVolumeName = labelVolume.GetName() + self.cornerTexts[0]['1-Label']['text'] = 'L: ' + labelVolumeName + ' (' + str( + "%d" % (labelOpacity * 100)) + '%)' + + self.drawCornerAnnotations(sliceViewName) + + def makeDicomAnnotation(self, bgUid, fgUid, sliceViewName): + # Do not attempt to retrieve dicom values if no local database exists + if not slicer.dicomDatabase.isOpen: + return + viewHeight = self.sliceViews[sliceViewName].height + if fgUid is not None and bgUid is not None: + backgroundDicomDic = self.extractDICOMValues(bgUid) + foregroundDicomDic = self.extractDICOMValues(fgUid) + # check if background and foreground are from different patients + # and remove the annotations + + if self.topLeft and viewHeight > 150: + if backgroundDicomDic['Patient Name'] != foregroundDicomDic['Patient Name' + ] or backgroundDicomDic['Patient ID'] != foregroundDicomDic['Patient ID' + ] or backgroundDicomDic['Patient Birth Date'] != foregroundDicomDic['Patient Birth Date']: + for key in self.cornerTexts[2]: + self.cornerTexts[2][key]['text'] = '' + else: + if '1-PatientName' in self.cornerTexts[2]: + self.cornerTexts[2]['1-PatientName']['text'] = backgroundDicomDic['Patient Name'].replace('^', ', ') + if '2-PatientID' in self.cornerTexts[2]: + self.cornerTexts[2]['2-PatientID']['text'] = 'ID: ' + backgroundDicomDic['Patient ID'] + backgroundDicomDic['Patient Birth Date'] = self.formatDICOMDate(backgroundDicomDic['Patient Birth Date']) + if '3-PatientInfo' in self.cornerTexts[2]: + self.cornerTexts[2]['3-PatientInfo']['text'] = self.makePatientInfo(backgroundDicomDic) + + if (backgroundDicomDic['Series Date'] != foregroundDicomDic['Series Date']): + if '4-Bg-SeriesDate' in self.cornerTexts[2]: + self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = 'B: ' + self.formatDICOMDate(backgroundDicomDic['Series Date']) + if '5-Fg-SeriesDate' in self.cornerTexts[2]: + self.cornerTexts[2]['5-Fg-SeriesDate']['text'] = 'F: ' + self.formatDICOMDate(foregroundDicomDic['Series Date']) + else: + if '4-Bg-SeriesDate' in self.cornerTexts[2]: + self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = self.formatDICOMDate(backgroundDicomDic['Series Date']) + + if (backgroundDicomDic['Series Time'] != foregroundDicomDic['Series Time']): + if '6-Bg-SeriesTime' in self.cornerTexts[2]: + self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = 'B: ' + self.formatDICOMTime(backgroundDicomDic['Series Time']) + if '7-Fg-SeriesTime' in self.cornerTexts[2]: + self.cornerTexts[2]['7-Fg-SeriesTime']['text'] = 'F: ' + self.formatDICOMTime(foregroundDicomDic['Series Time']) + else: + if '6-Bg-SeriesTime' in self.cornerTexts[2]: + self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = self.formatDICOMTime(backgroundDicomDic['Series Time']) + + if (backgroundDicomDic['Series Description'] != foregroundDicomDic['Series Description']): + if '8-Bg-SeriesDescription' in self.cornerTexts[2]: + self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = 'B: ' + backgroundDicomDic['Series Description'] + if '9-Fg-SeriesDescription' in self.cornerTexts[2]: + self.cornerTexts[2]['9-Fg-SeriesDescription']['text'] = 'F: ' + foregroundDicomDic['Series Description'] + else: + if '8-Bg-SeriesDescription' in self.cornerTexts[2]: + self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = backgroundDicomDic['Series Description'] + + # Only Background or Only Foreground else: - self.dicomVolumeNode = 0 - - if (labelVolume is not None) and self.bottomLeft: - labelOpacity = sliceCompositeNode.GetLabelOpacity() - labelVolumeName = labelVolume.GetName() - self.cornerTexts[0]['1-Label']['text'] = 'L: ' + labelVolumeName + ' (' + str( - "%d"%(labelOpacity*100)) + '%)' - - self.drawCornerAnnotations(sliceViewName) - - def makeDicomAnnotation(self,bgUid,fgUid,sliceViewName): - # Do not attempt to retrieve dicom values if no local database exists - if not slicer.dicomDatabase.isOpen: - return - viewHeight = self.sliceViews[sliceViewName].height - if fgUid is not None and bgUid is not None: - backgroundDicomDic = self.extractDICOMValues(bgUid) - foregroundDicomDic = self.extractDICOMValues(fgUid) - # check if background and foreground are from different patients - # and remove the annotations - - if self.topLeft and viewHeight > 150: - if backgroundDicomDic['Patient Name'] != foregroundDicomDic['Patient Name' - ] or backgroundDicomDic['Patient ID'] != foregroundDicomDic['Patient ID' - ] or backgroundDicomDic['Patient Birth Date'] != foregroundDicomDic['Patient Birth Date']: - for key in self.cornerTexts[2]: - self.cornerTexts[2][key]['text'] = '' + uid = bgUid + dicomDic = self.extractDICOMValues(uid) + + if self.topLeft and viewHeight > 150: + self.cornerTexts[2]['1-PatientName']['text'] = dicomDic['Patient Name'].replace('^', ', ') + self.cornerTexts[2]['2-PatientID']['text'] = 'ID: ' + dicomDic['Patient ID'] + dicomDic['Patient Birth Date'] = self.formatDICOMDate(dicomDic['Patient Birth Date']) + self.cornerTexts[2]['3-PatientInfo']['text'] = self.makePatientInfo(dicomDic) + self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = self.formatDICOMDate(dicomDic['Series Date']) + self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = self.formatDICOMTime(dicomDic['Series Time']) + self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = dicomDic['Series Description'] + + # top right corner annotation would be hidden if view height is less than 260 pixels + if (self.topRight): + self.cornerTexts[3]['1-Institution-Name']['text'] = dicomDic['Institution Name'] + self.cornerTexts[3]['2-Referring-Phisycian']['text'] = dicomDic['Referring Physician Name'].replace('^', ', ') + self.cornerTexts[3]['3-Manufacturer']['text'] = dicomDic['Manufacturer'] + self.cornerTexts[3]['4-Model']['text'] = dicomDic['Model'] + self.cornerTexts[3]['5-Patient-Position']['text'] = dicomDic['Patient Position'] + modality = dicomDic['Modality'] + if modality == 'MR': + self.cornerTexts[3]['6-TR']['text'] = 'TR ' + dicomDic['Repetition Time'] + self.cornerTexts[3]['7-TE']['text'] = 'TE ' + dicomDic['Echo Time'] + + @staticmethod + def makePatientInfo(dicomDic): + # This will give an string of patient's birth date, + # patient's age and sex + patientInfo = dicomDic['Patient Birth Date' + ] + ', ' + dicomDic['Patient Age' + ] + ', ' + dicomDic['Patient Sex'] + return patientInfo + + @staticmethod + def formatDICOMDate(date): + standardDate = '' + if date != '': + date = date.rstrip() + # convert to ISO 8601 Date format + standardDate = date[:4] + '-' + date[4:6] + '-' + date[6:] + return standardDate + + @staticmethod + def formatDICOMTime(time): + if time == '': + # time field is empty + return '' + studyH = time[:2] + if int(studyH) > 12: + studyH = str(int(studyH) - 12) + clockTime = ' PM' else: - if '1-PatientName' in self.cornerTexts[2]: - self.cornerTexts[2]['1-PatientName']['text'] = backgroundDicomDic['Patient Name'].replace('^',', ') - if '2-PatientID' in self.cornerTexts[2]: - self.cornerTexts[2]['2-PatientID']['text'] = 'ID: ' + backgroundDicomDic['Patient ID'] - backgroundDicomDic['Patient Birth Date'] = self.formatDICOMDate(backgroundDicomDic['Patient Birth Date']) - if '3-PatientInfo' in self.cornerTexts[2]: - self.cornerTexts[2]['3-PatientInfo']['text'] = self.makePatientInfo(backgroundDicomDic) - - if (backgroundDicomDic['Series Date'] != foregroundDicomDic['Series Date']): - if '4-Bg-SeriesDate' in self.cornerTexts[2]: - self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = 'B: ' + self.formatDICOMDate(backgroundDicomDic['Series Date']) - if '5-Fg-SeriesDate' in self.cornerTexts[2]: - self.cornerTexts[2]['5-Fg-SeriesDate']['text'] = 'F: ' + self.formatDICOMDate(foregroundDicomDic['Series Date']) - else: - if '4-Bg-SeriesDate' in self.cornerTexts[2]: - self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = self.formatDICOMDate(backgroundDicomDic['Series Date']) - - if (backgroundDicomDic['Series Time'] != foregroundDicomDic['Series Time']): - if '6-Bg-SeriesTime' in self.cornerTexts[2]: - self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = 'B: ' + self.formatDICOMTime(backgroundDicomDic['Series Time']) - if '7-Fg-SeriesTime' in self.cornerTexts[2]: - self.cornerTexts[2]['7-Fg-SeriesTime']['text'] = 'F: ' + self.formatDICOMTime(foregroundDicomDic['Series Time']) - else: - if '6-Bg-SeriesTime' in self.cornerTexts[2]: - self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = self.formatDICOMTime(backgroundDicomDic['Series Time']) - - if (backgroundDicomDic['Series Description'] != foregroundDicomDic['Series Description']): - if '8-Bg-SeriesDescription' in self.cornerTexts[2]: - self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = 'B: ' + backgroundDicomDic['Series Description'] - if '9-Fg-SeriesDescription' in self.cornerTexts[2]: - self.cornerTexts[2]['9-Fg-SeriesDescription']['text'] = 'F: ' + foregroundDicomDic['Series Description'] - else: - if '8-Bg-SeriesDescription' in self.cornerTexts[2]: - self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = backgroundDicomDic['Series Description'] - - # Only Background or Only Foreground - else: - uid = bgUid - dicomDic = self.extractDICOMValues(uid) - - if self.topLeft and viewHeight > 150: - self.cornerTexts[2]['1-PatientName']['text'] = dicomDic['Patient Name'].replace('^',', ') - self.cornerTexts[2]['2-PatientID']['text'] = 'ID: ' + dicomDic ['Patient ID'] - dicomDic['Patient Birth Date'] = self.formatDICOMDate(dicomDic['Patient Birth Date']) - self.cornerTexts[2]['3-PatientInfo']['text'] = self.makePatientInfo(dicomDic) - self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = self.formatDICOMDate(dicomDic['Series Date']) - self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = self.formatDICOMTime(dicomDic['Series Time']) - self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = dicomDic['Series Description'] - - # top right corner annotation would be hidden if view height is less than 260 pixels - if (self.topRight): - self.cornerTexts[3]['1-Institution-Name']['text'] = dicomDic['Institution Name'] - self.cornerTexts[3]['2-Referring-Phisycian']['text'] = dicomDic['Referring Physician Name'].replace('^',', ') - self.cornerTexts[3]['3-Manufacturer']['text'] = dicomDic['Manufacturer'] - self.cornerTexts[3]['4-Model']['text'] = dicomDic['Model'] - self.cornerTexts[3]['5-Patient-Position']['text'] = dicomDic['Patient Position'] - modality = dicomDic['Modality'] - if modality == 'MR': - self.cornerTexts[3]['6-TR']['text'] = 'TR ' + dicomDic['Repetition Time'] - self.cornerTexts[3]['7-TE']['text'] = 'TE ' + dicomDic['Echo Time'] - - @staticmethod - def makePatientInfo(dicomDic): - # This will give an string of patient's birth date, - # patient's age and sex - patientInfo = dicomDic['Patient Birth Date' - ] + ', ' + dicomDic['Patient Age' - ] + ', ' + dicomDic['Patient Sex'] - return patientInfo - - @staticmethod - def formatDICOMDate(date): - standardDate = '' - if date != '': - date = date.rstrip() - # convert to ISO 8601 Date format - standardDate = date[:4] + '-' + date[4:6]+ '-' + date[6:] - return standardDate - - @staticmethod - def formatDICOMTime(time): - if time == '': - # time field is empty - return '' - studyH = time[:2] - if int(studyH) > 12 : - studyH = str (int(studyH) - 12) - clockTime = ' PM' - else: - studyH = studyH - clockTime = ' AM' - studyM = time[2:4] - studyS = time[4:6] - return studyH + ':' + studyM + ':' + studyS +clockTime - - @staticmethod - def fitText(text,textSize): - if len(text) > textSize: - preSize = int(textSize/2) - postSize = preSize - 3 - text = text[:preSize] + "..." + text[-postSize:] - return text - - def drawCornerAnnotations(self, sliceViewName): - if not self.sliceViewAnnotationsEnabled: - return - # Auto-Adjust - # adjust maximum text length based on fontsize and view width - viewWidth = self.sliceViews[sliceViewName].width - self.maximumTextLength = int((viewWidth - 40) / self.fontSize) - - for i, cornerText in enumerate(self.cornerTexts): - keys = sorted(cornerText.keys()) - cornerAnnotation = '' - for key in keys: - text = cornerText[key]['text'] - if ( text != ''): - text = self.fitText(text, self.maximumTextLength) - # level 1: All categories will be displayed - if self.annotationsDisplayAmount == 0: - cornerAnnotation = cornerAnnotation+ text + '\n' - # level 2: Category A and B will be displayed - elif self.annotationsDisplayAmount == 1: - if (cornerText[key]['category'] != 'C'): - cornerAnnotation = cornerAnnotation+ text + '\n' - # level 3 only Category A will be displayed - elif self.annotationsDisplayAmount == 2: - if (cornerText[key]['category'] == 'A'): - cornerAnnotation = cornerAnnotation+ text + '\n' - sliceCornerAnnotation = self.sliceViews[sliceViewName].cornerAnnotation() - # encode to avoid 'unicode conversion error' for patient names containing international characters - cornerAnnotation = cornerAnnotation - sliceCornerAnnotation.SetText(i, cornerAnnotation) - textProperty = sliceCornerAnnotation.GetTextProperty() - textProperty.SetShadow(1) - - self.sliceViews[sliceViewName].scheduleRender() - - def resetTexts(self): - for i, cornerText in enumerate(self.cornerTexts): - for key in cornerText.keys(): - self.cornerTexts[i][key]['text'] = '' - - def extractDICOMValues(self, uid): - - # Used cached tags, if found. - # DICOM objects are not allowed to be changed, - # so if the UID matches then the content has to match as well - if uid in self.extractedDICOMValuesCache.keys(): - return self.extractedDICOMValuesCache[uid] - - p ={} - tags = { - "0008,0021": "Series Date", - "0008,0031": "Series Time", - "0008,0060": "Modality", - "0008,0070": "Manufacturer", - "0008,0080": "Institution Name", - "0008,0090": "Referring Physician Name", - "0008,103e": "Series Description", - "0008,1090": "Model", - "0010,0010": "Patient Name", - "0010,0020": "Patient ID", - "0010,0030": "Patient Birth Date", - "0010,0040": "Patient Sex", - "0010,1010": "Patient Age", - "0018,5100": "Patient Position", - "0018,0080": "Repetition Time", - "0018,0081": "Echo Time" - } - for tag in tags.keys(): - value = slicer.dicomDatabase.instanceValue(uid,tag) - p[tags[tag]] = value - - # Store DICOM tags in cache - self.extractedDICOMValuesCache[uid] = p - if len(self.extractedDICOMValuesCache) > self.extractedDICOMValuesCacheSize: - # cache is full, drop oldest item - self.extractedDICOMValuesCache.popitem(last=False) - - return p + studyH = studyH + clockTime = ' AM' + studyM = time[2:4] + studyS = time[4:6] + return studyH + ':' + studyM + ':' + studyS + clockTime + + @staticmethod + def fitText(text, textSize): + if len(text) > textSize: + preSize = int(textSize / 2) + postSize = preSize - 3 + text = text[:preSize] + "..." + text[-postSize:] + return text + + def drawCornerAnnotations(self, sliceViewName): + if not self.sliceViewAnnotationsEnabled: + return + # Auto-Adjust + # adjust maximum text length based on fontsize and view width + viewWidth = self.sliceViews[sliceViewName].width + self.maximumTextLength = int((viewWidth - 40) / self.fontSize) + + for i, cornerText in enumerate(self.cornerTexts): + keys = sorted(cornerText.keys()) + cornerAnnotation = '' + for key in keys: + text = cornerText[key]['text'] + if (text != ''): + text = self.fitText(text, self.maximumTextLength) + # level 1: All categories will be displayed + if self.annotationsDisplayAmount == 0: + cornerAnnotation = cornerAnnotation + text + '\n' + # level 2: Category A and B will be displayed + elif self.annotationsDisplayAmount == 1: + if (cornerText[key]['category'] != 'C'): + cornerAnnotation = cornerAnnotation + text + '\n' + # level 3 only Category A will be displayed + elif self.annotationsDisplayAmount == 2: + if (cornerText[key]['category'] == 'A'): + cornerAnnotation = cornerAnnotation + text + '\n' + sliceCornerAnnotation = self.sliceViews[sliceViewName].cornerAnnotation() + # encode to avoid 'unicode conversion error' for patient names containing international characters + cornerAnnotation = cornerAnnotation + sliceCornerAnnotation.SetText(i, cornerAnnotation) + textProperty = sliceCornerAnnotation.GetTextProperty() + textProperty.SetShadow(1) + + self.sliceViews[sliceViewName].scheduleRender() + + def resetTexts(self): + for i, cornerText in enumerate(self.cornerTexts): + for key in cornerText.keys(): + self.cornerTexts[i][key]['text'] = '' + + def extractDICOMValues(self, uid): + + # Used cached tags, if found. + # DICOM objects are not allowed to be changed, + # so if the UID matches then the content has to match as well + if uid in self.extractedDICOMValuesCache.keys(): + return self.extractedDICOMValuesCache[uid] + + p = {} + tags = { + "0008,0021": "Series Date", + "0008,0031": "Series Time", + "0008,0060": "Modality", + "0008,0070": "Manufacturer", + "0008,0080": "Institution Name", + "0008,0090": "Referring Physician Name", + "0008,103e": "Series Description", + "0008,1090": "Model", + "0010,0010": "Patient Name", + "0010,0020": "Patient ID", + "0010,0030": "Patient Birth Date", + "0010,0040": "Patient Sex", + "0010,1010": "Patient Age", + "0018,5100": "Patient Position", + "0018,0080": "Repetition Time", + "0018,0081": "Echo Time" + } + for tag in tags.keys(): + value = slicer.dicomDatabase.instanceValue(uid, tag) + p[tags[tag]] = value + + # Store DICOM tags in cache + self.extractedDICOMValuesCache[uid] = p + if len(self.extractedDICOMValuesCache) > self.extractedDICOMValuesCacheSize: + # cache is full, drop oldest item + self.extractedDICOMValuesCache.popitem(last=False) + + return p diff --git a/Modules/Scripted/Endoscopy/Endoscopy.py b/Modules/Scripted/Endoscopy/Endoscopy.py index 52d1771d585..c527ab897a4 100644 --- a/Modules/Scripted/Endoscopy/Endoscopy.py +++ b/Modules/Scripted/Endoscopy/Endoscopy.py @@ -12,17 +12,17 @@ # class Endoscopy(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 = "Endoscopy" - self.parent.categories = ["Endoscopy"] - self.parent.dependencies = [] - self.parent.contributors = ["Steve Pieper (Isomics)"] - self.parent.helpText = """ + """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 = "Endoscopy" + self.parent.categories = ["Endoscopy"] + self.parent.dependencies = [] + self.parent.contributors = ["Steve Pieper (Isomics)"] + self.parent.helpText = """ Create a path model as a spline interpolation of a set of fiducial points. Pick the Camera to be modified by the path and the Fiducial List defining the control points. Clicking "Create path" will make a path model and enable the flythrough panel. @@ -31,8 +31,8 @@ def __init__(self, parent): The Frame Delay slider slows down the animation by adding more time between frames. The View Angle provides is used to approximate the optics of an endoscopy system. """ - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = """ + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = """ This work is supported by PAR-07-249: R01CA131718 NA-MIC Virtual Colonoscopy (See https://www.na-mic.org/Wiki/index.php/NA-MIC_NCBC_Collaboration:NA-MIC_virtual_colonoscopy) NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. @@ -44,558 +44,558 @@ def __init__(self, parent): # class EndoscopyWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def __init__(self, parent=None): - ScriptedLoadableModuleWidget.__init__(self, parent) - self.cameraNode = None - self.cameraNodeObserverTag = None - self.cameraObserverTag= None - # Flythough variables - self.transform = None - self.path = None - self.camera = None - self.skip = 0 - self.timer = qt.QTimer() - self.timer.setInterval(20) - self.timer.connect('timeout()', self.flyToNext) - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - # Path collapsible button - pathCollapsibleButton = ctk.ctkCollapsibleButton() - pathCollapsibleButton.text = "Path" - self.layout.addWidget(pathCollapsibleButton) - - # Layout within the path collapsible button - pathFormLayout = qt.QFormLayout(pathCollapsibleButton) - - # Camera node selector - cameraNodeSelector = slicer.qMRMLNodeComboBox() - cameraNodeSelector.objectName = 'cameraNodeSelector' - cameraNodeSelector.toolTip = "Select a camera that will fly along this path." - cameraNodeSelector.nodeTypes = ['vtkMRMLCameraNode'] - cameraNodeSelector.noneEnabled = False - cameraNodeSelector.addEnabled = False - cameraNodeSelector.removeEnabled = False - cameraNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton) - cameraNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setCameraNode) - pathFormLayout.addRow("Camera:", cameraNodeSelector) - - # Input fiducials node selector - inputFiducialsNodeSelector = slicer.qMRMLNodeComboBox() - inputFiducialsNodeSelector.objectName = 'inputFiducialsNodeSelector' - inputFiducialsNodeSelector.toolTip = "Select a fiducial list to define control points for the path." - inputFiducialsNodeSelector.nodeTypes = ['vtkMRMLMarkupsFiducialNode', 'vtkMRMLMarkupsCurveNode', - 'vtkMRMLAnnotationHierarchyNode'] - inputFiducialsNodeSelector.noneEnabled = False - inputFiducialsNodeSelector.addEnabled = False - inputFiducialsNodeSelector.removeEnabled = False - inputFiducialsNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton) - pathFormLayout.addRow("Input Fiducials:", inputFiducialsNodeSelector) - - # Output path node selector - outputPathNodeSelector = slicer.qMRMLNodeComboBox() - outputPathNodeSelector.objectName = 'outputPathNodeSelector' - outputPathNodeSelector.toolTip = "Select a fiducial list to define control points for the path." - outputPathNodeSelector.nodeTypes = ['vtkMRMLModelNode'] - outputPathNodeSelector.noneEnabled = False - outputPathNodeSelector.addEnabled = True - outputPathNodeSelector.removeEnabled = True - outputPathNodeSelector.renameEnabled = True - outputPathNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton) - pathFormLayout.addRow("Output Path:", outputPathNodeSelector) - - # CreatePath button - createPathButton = qt.QPushButton("Create path") - createPathButton.toolTip = "Create the path." - createPathButton.enabled = False - pathFormLayout.addRow(createPathButton) - createPathButton.connect('clicked()', self.onCreatePathButtonClicked) - - # Flythrough collapsible button - flythroughCollapsibleButton = ctk.ctkCollapsibleButton() - flythroughCollapsibleButton.text = "Flythrough" - flythroughCollapsibleButton.enabled = False - self.layout.addWidget(flythroughCollapsibleButton) - - # Layout within the Flythrough collapsible button - flythroughFormLayout = qt.QFormLayout(flythroughCollapsibleButton) - - # Frame slider - frameSlider = ctk.ctkSliderWidget() - frameSlider.connect('valueChanged(double)', self.frameSliderValueChanged) - frameSlider.decimals = 0 - flythroughFormLayout.addRow("Frame:", frameSlider) - - # Frame skip slider - frameSkipSlider = ctk.ctkSliderWidget() - frameSkipSlider.connect('valueChanged(double)', self.frameSkipSliderValueChanged) - frameSkipSlider.decimals = 0 - frameSkipSlider.minimum = 0 - frameSkipSlider.maximum = 50 - flythroughFormLayout.addRow("Frame skip:", frameSkipSlider) - - # Frame delay slider - frameDelaySlider = ctk.ctkSliderWidget() - frameDelaySlider.connect('valueChanged(double)', self.frameDelaySliderValueChanged) - frameDelaySlider.decimals = 0 - frameDelaySlider.minimum = 5 - frameDelaySlider.maximum = 100 - frameDelaySlider.suffix = " ms" - frameDelaySlider.value = 20 - flythroughFormLayout.addRow("Frame delay:", frameDelaySlider) - - # View angle slider - viewAngleSlider = ctk.ctkSliderWidget() - viewAngleSlider.connect('valueChanged(double)', self.viewAngleSliderValueChanged) - viewAngleSlider.decimals = 0 - viewAngleSlider.minimum = 30 - viewAngleSlider.maximum = 180 - flythroughFormLayout.addRow("View Angle:", viewAngleSlider) - - # Play button - playButton = qt.QPushButton("Play") - playButton.toolTip = "Fly through path." - playButton.checkable = True - flythroughFormLayout.addRow(playButton) - playButton.connect('toggled(bool)', self.onPlayButtonToggled) - - # Add vertical spacer - self.layout.addStretch(1) - - # Set local var as instance attribute - self.cameraNodeSelector = cameraNodeSelector - self.inputFiducialsNodeSelector = inputFiducialsNodeSelector - self.outputPathNodeSelector = outputPathNodeSelector - self.createPathButton = createPathButton - self.flythroughCollapsibleButton = flythroughCollapsibleButton - self.frameSlider = frameSlider - self.viewAngleSlider = viewAngleSlider - self.playButton = playButton - - cameraNodeSelector.setMRMLScene(slicer.mrmlScene) - inputFiducialsNodeSelector.setMRMLScene(slicer.mrmlScene) - outputPathNodeSelector.setMRMLScene(slicer.mrmlScene) - - def setCameraNode(self, newCameraNode): - """Allow to set the current camera node. - Connected to signal 'currentNodeChanged()' emitted by camera node selector.""" - - # Remove previous observer - if self.cameraNode and self.cameraNodeObserverTag: - self.cameraNode.RemoveObserver(self.cameraNodeObserverTag) - if self.camera and self.cameraObserverTag: - self.camera.RemoveObserver(self.cameraObserverTag) - - newCamera = None - if newCameraNode: - newCamera = newCameraNode.GetCamera() - # Add CameraNode ModifiedEvent observer - self.cameraNodeObserverTag = newCameraNode.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onCameraNodeModified) - # Add Camera ModifiedEvent observer - self.cameraObserverTag = newCamera.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onCameraNodeModified) - - self.cameraNode = newCameraNode - self.camera = newCamera - - # Update UI - self.updateWidgetFromMRML() - - def updateWidgetFromMRML(self): - if self.camera: - self.viewAngleSlider.value = self.camera.GetViewAngle() - if self.cameraNode: - pass - - def onCameraModified(self, observer, eventid): - self.updateWidgetFromMRML() - - def onCameraNodeModified(self, observer, eventid): - self.updateWidgetFromMRML() - - def enableOrDisableCreateButton(self): - """Connected to both the fiducial and camera node selector. It allows to - enable or disable the 'create path' button.""" - self.createPathButton.enabled = (self.cameraNodeSelector.currentNode() is not None - and self.inputFiducialsNodeSelector.currentNode() is not None - and self.outputPathNodeSelector.currentNode() is not None) - - def onCreatePathButtonClicked(self): - """Connected to 'create path' button. It allows to: - - compute the path - - create the associated model""" - - fiducialsNode = self.inputFiducialsNodeSelector.currentNode() - outputPathNode = self.outputPathNodeSelector.currentNode() - print("Calculating Path...") - result = EndoscopyComputePath(fiducialsNode) - print("-> Computed path contains %d elements" % len(result.path)) - - print("Create Model...") - model = EndoscopyPathModel(result.path, fiducialsNode, outputPathNode) - print("-> Model created") - - # Update frame slider range - self.frameSlider.maximum = len(result.path) - 2 - - # Update flythrough variables - self.camera = self.camera - self.transform = model.transform - self.pathPlaneNormal = model.planeNormal - self.path = result.path - - # Enable / Disable flythrough button - self.flythroughCollapsibleButton.enabled = len(result.path) > 0 - - def frameSliderValueChanged(self, newValue): - #print "frameSliderValueChanged:", newValue - self.flyTo(newValue) - - def frameSkipSliderValueChanged(self, newValue): - #print "frameSkipSliderValueChanged:", newValue - self.skip = int(newValue) - - def frameDelaySliderValueChanged(self, newValue): - #print "frameDelaySliderValueChanged:", newValue - self.timer.interval = newValue - - def viewAngleSliderValueChanged(self, newValue): - if not self.cameraNode: - return - #print "viewAngleSliderValueChanged:", newValue - self.cameraNode.GetCamera().SetViewAngle(newValue) - - def onPlayButtonToggled(self, checked): - if checked: - self.timer.start() - self.playButton.text = "Stop" - else: - self.timer.stop() - self.playButton.text = "Play" - - def flyToNext(self): - currentStep = self.frameSlider.value - nextStep = currentStep + self.skip + 1 - if nextStep > len(self.path) - 2: - nextStep = 0 - self.frameSlider.value = nextStep - - def flyTo(self, pathPointIndex): - """ Apply the pathPointIndex-th step in the path to the global camera""" - - if self.path is None: - return - - pathPointIndex = int(pathPointIndex) - cameraPosition = self.path[pathPointIndex] - wasModified = self.cameraNode.StartModify() - - self.camera.SetPosition(cameraPosition) - focalPointPosition = self.path[pathPointIndex+1] - self.camera.SetFocalPoint(*focalPointPosition) - self.camera.OrthogonalizeViewUp() - - toParent = vtk.vtkMatrix4x4() - self.transform.GetMatrixTransformToParent(toParent) - toParent.SetElement(0 ,3, cameraPosition[0]) - toParent.SetElement(1, 3, cameraPosition[1]) - toParent.SetElement(2, 3, cameraPosition[2]) - - # Set up transform orientation component so that - # Z axis is aligned with view direction and - # Y vector is aligned with the curve's plane normal. - # This can be used for example to show a reformatted slice - # using with SlicerIGT extension's VolumeResliceDriver module. - import numpy as np - zVec = (focalPointPosition-cameraPosition)/np.linalg.norm(focalPointPosition-cameraPosition) - yVec = self.pathPlaneNormal - xVec = np.cross(yVec, zVec) - xVec /= np.linalg.norm(xVec) - yVec = np.cross(zVec, xVec) - toParent.SetElement(0, 0, xVec[0]) - toParent.SetElement(1, 0, xVec[1]) - toParent.SetElement(2, 0, xVec[2]) - toParent.SetElement(0, 1, yVec[0]) - toParent.SetElement(1, 1, yVec[1]) - toParent.SetElement(2, 1, yVec[2]) - toParent.SetElement(0, 2, zVec[0]) - toParent.SetElement(1, 2, zVec[1]) - toParent.SetElement(2, 2, zVec[2]) - - self.transform.SetMatrixTransformToParent(toParent) - - self.cameraNode.EndModify(wasModified) - self.cameraNode.ResetClippingRange() + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + def __init__(self, parent=None): + ScriptedLoadableModuleWidget.__init__(self, parent) + self.cameraNode = None + self.cameraNodeObserverTag = None + self.cameraObserverTag = None + # Flythough variables + self.transform = None + self.path = None + self.camera = None + self.skip = 0 + self.timer = qt.QTimer() + self.timer.setInterval(20) + self.timer.connect('timeout()', self.flyToNext) + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + # Path collapsible button + pathCollapsibleButton = ctk.ctkCollapsibleButton() + pathCollapsibleButton.text = "Path" + self.layout.addWidget(pathCollapsibleButton) + + # Layout within the path collapsible button + pathFormLayout = qt.QFormLayout(pathCollapsibleButton) + + # Camera node selector + cameraNodeSelector = slicer.qMRMLNodeComboBox() + cameraNodeSelector.objectName = 'cameraNodeSelector' + cameraNodeSelector.toolTip = "Select a camera that will fly along this path." + cameraNodeSelector.nodeTypes = ['vtkMRMLCameraNode'] + cameraNodeSelector.noneEnabled = False + cameraNodeSelector.addEnabled = False + cameraNodeSelector.removeEnabled = False + cameraNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton) + cameraNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setCameraNode) + pathFormLayout.addRow("Camera:", cameraNodeSelector) + + # Input fiducials node selector + inputFiducialsNodeSelector = slicer.qMRMLNodeComboBox() + inputFiducialsNodeSelector.objectName = 'inputFiducialsNodeSelector' + inputFiducialsNodeSelector.toolTip = "Select a fiducial list to define control points for the path." + inputFiducialsNodeSelector.nodeTypes = ['vtkMRMLMarkupsFiducialNode', 'vtkMRMLMarkupsCurveNode', + 'vtkMRMLAnnotationHierarchyNode'] + inputFiducialsNodeSelector.noneEnabled = False + inputFiducialsNodeSelector.addEnabled = False + inputFiducialsNodeSelector.removeEnabled = False + inputFiducialsNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton) + pathFormLayout.addRow("Input Fiducials:", inputFiducialsNodeSelector) + + # Output path node selector + outputPathNodeSelector = slicer.qMRMLNodeComboBox() + outputPathNodeSelector.objectName = 'outputPathNodeSelector' + outputPathNodeSelector.toolTip = "Select a fiducial list to define control points for the path." + outputPathNodeSelector.nodeTypes = ['vtkMRMLModelNode'] + outputPathNodeSelector.noneEnabled = False + outputPathNodeSelector.addEnabled = True + outputPathNodeSelector.removeEnabled = True + outputPathNodeSelector.renameEnabled = True + outputPathNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton) + pathFormLayout.addRow("Output Path:", outputPathNodeSelector) + + # CreatePath button + createPathButton = qt.QPushButton("Create path") + createPathButton.toolTip = "Create the path." + createPathButton.enabled = False + pathFormLayout.addRow(createPathButton) + createPathButton.connect('clicked()', self.onCreatePathButtonClicked) + + # Flythrough collapsible button + flythroughCollapsibleButton = ctk.ctkCollapsibleButton() + flythroughCollapsibleButton.text = "Flythrough" + flythroughCollapsibleButton.enabled = False + self.layout.addWidget(flythroughCollapsibleButton) + + # Layout within the Flythrough collapsible button + flythroughFormLayout = qt.QFormLayout(flythroughCollapsibleButton) + + # Frame slider + frameSlider = ctk.ctkSliderWidget() + frameSlider.connect('valueChanged(double)', self.frameSliderValueChanged) + frameSlider.decimals = 0 + flythroughFormLayout.addRow("Frame:", frameSlider) + + # Frame skip slider + frameSkipSlider = ctk.ctkSliderWidget() + frameSkipSlider.connect('valueChanged(double)', self.frameSkipSliderValueChanged) + frameSkipSlider.decimals = 0 + frameSkipSlider.minimum = 0 + frameSkipSlider.maximum = 50 + flythroughFormLayout.addRow("Frame skip:", frameSkipSlider) + + # Frame delay slider + frameDelaySlider = ctk.ctkSliderWidget() + frameDelaySlider.connect('valueChanged(double)', self.frameDelaySliderValueChanged) + frameDelaySlider.decimals = 0 + frameDelaySlider.minimum = 5 + frameDelaySlider.maximum = 100 + frameDelaySlider.suffix = " ms" + frameDelaySlider.value = 20 + flythroughFormLayout.addRow("Frame delay:", frameDelaySlider) + + # View angle slider + viewAngleSlider = ctk.ctkSliderWidget() + viewAngleSlider.connect('valueChanged(double)', self.viewAngleSliderValueChanged) + viewAngleSlider.decimals = 0 + viewAngleSlider.minimum = 30 + viewAngleSlider.maximum = 180 + flythroughFormLayout.addRow("View Angle:", viewAngleSlider) + + # Play button + playButton = qt.QPushButton("Play") + playButton.toolTip = "Fly through path." + playButton.checkable = True + flythroughFormLayout.addRow(playButton) + playButton.connect('toggled(bool)', self.onPlayButtonToggled) + + # Add vertical spacer + self.layout.addStretch(1) + + # Set local var as instance attribute + self.cameraNodeSelector = cameraNodeSelector + self.inputFiducialsNodeSelector = inputFiducialsNodeSelector + self.outputPathNodeSelector = outputPathNodeSelector + self.createPathButton = createPathButton + self.flythroughCollapsibleButton = flythroughCollapsibleButton + self.frameSlider = frameSlider + self.viewAngleSlider = viewAngleSlider + self.playButton = playButton + + cameraNodeSelector.setMRMLScene(slicer.mrmlScene) + inputFiducialsNodeSelector.setMRMLScene(slicer.mrmlScene) + outputPathNodeSelector.setMRMLScene(slicer.mrmlScene) + + def setCameraNode(self, newCameraNode): + """Allow to set the current camera node. + Connected to signal 'currentNodeChanged()' emitted by camera node selector.""" + + # Remove previous observer + if self.cameraNode and self.cameraNodeObserverTag: + self.cameraNode.RemoveObserver(self.cameraNodeObserverTag) + if self.camera and self.cameraObserverTag: + self.camera.RemoveObserver(self.cameraObserverTag) + + newCamera = None + if newCameraNode: + newCamera = newCameraNode.GetCamera() + # Add CameraNode ModifiedEvent observer + self.cameraNodeObserverTag = newCameraNode.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onCameraNodeModified) + # Add Camera ModifiedEvent observer + self.cameraObserverTag = newCamera.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onCameraNodeModified) + + self.cameraNode = newCameraNode + self.camera = newCamera + + # Update UI + self.updateWidgetFromMRML() + + def updateWidgetFromMRML(self): + if self.camera: + self.viewAngleSlider.value = self.camera.GetViewAngle() + if self.cameraNode: + pass + + def onCameraModified(self, observer, eventid): + self.updateWidgetFromMRML() + + def onCameraNodeModified(self, observer, eventid): + self.updateWidgetFromMRML() + + def enableOrDisableCreateButton(self): + """Connected to both the fiducial and camera node selector. It allows to + enable or disable the 'create path' button.""" + self.createPathButton.enabled = (self.cameraNodeSelector.currentNode() is not None + and self.inputFiducialsNodeSelector.currentNode() is not None + and self.outputPathNodeSelector.currentNode() is not None) + + def onCreatePathButtonClicked(self): + """Connected to 'create path' button. It allows to: + - compute the path + - create the associated model""" + + fiducialsNode = self.inputFiducialsNodeSelector.currentNode() + outputPathNode = self.outputPathNodeSelector.currentNode() + print("Calculating Path...") + result = EndoscopyComputePath(fiducialsNode) + print("-> Computed path contains %d elements" % len(result.path)) + + print("Create Model...") + model = EndoscopyPathModel(result.path, fiducialsNode, outputPathNode) + print("-> Model created") + + # Update frame slider range + self.frameSlider.maximum = len(result.path) - 2 + + # Update flythrough variables + self.camera = self.camera + self.transform = model.transform + self.pathPlaneNormal = model.planeNormal + self.path = result.path + + # Enable / Disable flythrough button + self.flythroughCollapsibleButton.enabled = len(result.path) > 0 + + def frameSliderValueChanged(self, newValue): + # print "frameSliderValueChanged:", newValue + self.flyTo(newValue) + + def frameSkipSliderValueChanged(self, newValue): + # print "frameSkipSliderValueChanged:", newValue + self.skip = int(newValue) + + def frameDelaySliderValueChanged(self, newValue): + # print "frameDelaySliderValueChanged:", newValue + self.timer.interval = newValue + + def viewAngleSliderValueChanged(self, newValue): + if not self.cameraNode: + return + # print "viewAngleSliderValueChanged:", newValue + self.cameraNode.GetCamera().SetViewAngle(newValue) + + def onPlayButtonToggled(self, checked): + if checked: + self.timer.start() + self.playButton.text = "Stop" + else: + self.timer.stop() + self.playButton.text = "Play" + + def flyToNext(self): + currentStep = self.frameSlider.value + nextStep = currentStep + self.skip + 1 + if nextStep > len(self.path) - 2: + nextStep = 0 + self.frameSlider.value = nextStep + + def flyTo(self, pathPointIndex): + """ Apply the pathPointIndex-th step in the path to the global camera""" + + if self.path is None: + return + + pathPointIndex = int(pathPointIndex) + cameraPosition = self.path[pathPointIndex] + wasModified = self.cameraNode.StartModify() + + self.camera.SetPosition(cameraPosition) + focalPointPosition = self.path[pathPointIndex + 1] + self.camera.SetFocalPoint(*focalPointPosition) + self.camera.OrthogonalizeViewUp() + + toParent = vtk.vtkMatrix4x4() + self.transform.GetMatrixTransformToParent(toParent) + toParent.SetElement(0, 3, cameraPosition[0]) + toParent.SetElement(1, 3, cameraPosition[1]) + toParent.SetElement(2, 3, cameraPosition[2]) + + # Set up transform orientation component so that + # Z axis is aligned with view direction and + # Y vector is aligned with the curve's plane normal. + # This can be used for example to show a reformatted slice + # using with SlicerIGT extension's VolumeResliceDriver module. + import numpy as np + zVec = (focalPointPosition - cameraPosition) / np.linalg.norm(focalPointPosition - cameraPosition) + yVec = self.pathPlaneNormal + xVec = np.cross(yVec, zVec) + xVec /= np.linalg.norm(xVec) + yVec = np.cross(zVec, xVec) + toParent.SetElement(0, 0, xVec[0]) + toParent.SetElement(1, 0, xVec[1]) + toParent.SetElement(2, 0, xVec[2]) + toParent.SetElement(0, 1, yVec[0]) + toParent.SetElement(1, 1, yVec[1]) + toParent.SetElement(2, 1, yVec[2]) + toParent.SetElement(0, 2, zVec[0]) + toParent.SetElement(1, 2, zVec[1]) + toParent.SetElement(2, 2, zVec[2]) + + self.transform.SetMatrixTransformToParent(toParent) + + self.cameraNode.EndModify(wasModified) + self.cameraNode.ResetClippingRange() -class EndoscopyComputePath: - """Compute path given a list of fiducials. - Path is stored in 'path' member variable as a numpy array. - If a point list is received then curve points are generated using Hermite spline interpolation. - See https://en.wikipedia.org/wiki/Cubic_Hermite_spline - - Example: - result = EndoscopyComputePath(fiducialListNode) - print "computer path has %d elements" % len(result.path) - - """ - - def __init__(self, fiducialListNode, dl = 0.5): - import numpy - self.dl = dl # desired world space step size (in mm) - self.dt = dl # current guess of parametric stepsize - self.fids = fiducialListNode - - # Already a curve, just get the points, sampled at equal distances. - if (self.fids.GetClassName() == "vtkMRMLMarkupsCurveNode" - or self.fids.GetClassName() == "vtkMRMLMarkupsClosedCurveNode"): - # Temporarily increase the number of points per segment, to get a very smooth curve - pointsPerSegment = int(self.fids.GetCurveLengthWorld() / self.dl / self.fids.GetNumberOfControlPoints()) + 1 - originalPointsPerSegment = self.fids.GetNumberOfPointsPerInterpolatingSegment() - if originalPointsPerSegment 1.0, then - this is the amount of world space not covered by step - """ - import numpy.linalg - p0 = self.path[self.path.__len__() - 1] # last element in path - remainder = 0 - ratio = 100 - count = 0 - while abs(1. - ratio) > 0.05: - t1 = t + self.dt - pguess = self.point(segment,t1) - dist = numpy.linalg.norm(pguess - p0) - ratio = self.dl / dist - self.dt *= ratio - if self.dt < 0.00000001: - return - count += 1 - if count > 500: - return (t1, pguess, 0) - if t1 > 1.: - t1 = 1. - p1 = self.point(segment, t1) - remainder = numpy.linalg.norm(p1 - pguess) - pguess = p1 - return (t1, pguess, remainder) +class EndoscopyComputePath: + """Compute path given a list of fiducials. + Path is stored in 'path' member variable as a numpy array. + If a point list is received then curve points are generated using Hermite spline interpolation. + See https://en.wikipedia.org/wiki/Cubic_Hermite_spline -class EndoscopyPathModel: - """Create a vtkPolyData for a polyline: - - Add one point per path point. - - Add a single polyline - """ + Example: + result = EndoscopyComputePath(fiducialListNode) + print "computer path has %d elements" % len(result.path) - def __init__(self, path, fiducialListNode, outputPathNode=None, cursorType=None): - """ - :param path: path points as numpy array. - :param fiducialListNode: input node, just used for naming the output node. - :param outputPathNode: output model node that stores the path points. - :param cursorType: can be 'markups' or 'model'. Markups has a number of advantages (radius it is easier to change the size, - can jump to views by clicking on it, has more visualization options, can be scaled to fixed display size), - but if some applications relied on having a model node as cursor then this argument can be used to achieve that. """ - fids = fiducialListNode - scene = slicer.mrmlScene - - self.cursorType = "markups" if cursorType is None else cursorType - - points = vtk.vtkPoints() - polyData = vtk.vtkPolyData() - polyData.SetPoints(points) - - lines = vtk.vtkCellArray() - polyData.SetLines(lines) - linesIDArray = lines.GetData() - linesIDArray.Reset() - linesIDArray.InsertNextTuple1(0) - - polygons = vtk.vtkCellArray() - polyData.SetPolys( polygons ) - idArray = polygons.GetData() - idArray.Reset() - idArray.InsertNextTuple1(0) - - for point in path: - pointIndex = points.InsertNextPoint(*point) - linesIDArray.InsertNextTuple1(pointIndex) - linesIDArray.SetTuple1( 0, linesIDArray.GetNumberOfTuples() - 1 ) - lines.SetNumberOfCells(1) - - pointsArray = vtk.util.numpy_support.vtk_to_numpy(points.GetData()) - self.planePosition, self.planeNormal = self.planeFit(pointsArray.T) - - # Create model node - model = outputPathNode - if not model: - model = scene.AddNewNodeByClass("vtkMRMLModelNode", scene.GenerateUniqueName("Path-%s" % fids.GetName())) - model.CreateDefaultDisplayNodes() - model.GetDisplayNode().SetColor(1,1,0) # yellow - - model.SetAndObservePolyData(polyData) - - # Camera cursor - cursor = model.GetNodeReference("CameraCursor") - if not cursor: - - if self.cursorType == "markups": - # Markups cursor - cursor = scene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", scene.GenerateUniqueName("Cursor-%s" % fids.GetName())) - cursor.CreateDefaultDisplayNodes() - cursor.GetDisplayNode().SetSelectedColor(1,0,0) # red - cursor.GetDisplayNode().SetSliceProjection(True) - cursor.AddControlPoint(vtk.vtkVector3d(0,0,0)," ") # do not show any visible label - cursor.SetNthControlPointLocked(0, True) - else: - # Model cursor - cursor = scene.AddNewNodeByClass("vtkMRMLMarkupsModelNode", scene.GenerateUniqueName("Cursor-%s" % fids.GetName())) - cursor.CreateDefaultDisplayNodes() - cursor.GetDisplayNode().SetColor(1,0,0) # red - cursor.GetDisplayNode().BackfaceCullingOn() # so that the camera can see through the cursor from inside - # Add a sphere as cursor - sphere = vtk.vtkSphereSource() - sphere.Update() - cursor.SetPolyDataConnection(sphere.GetOutputPort()) - - model.SetNodeReferenceID("CameraCursor", cursor.GetID()) - - # Transform node - transform = model.GetNodeReference("CameraTransform") - if not transform: - transform = scene.AddNewNodeByClass("vtkMRMLLinearTransformNode", scene.GenerateUniqueName("Transform-%s" % fids.GetName())) - model.SetNodeReferenceID("CameraTransform", transform.GetID()) - cursor.SetAndObserveTransformNodeID(transform.GetID()) - - self.transform = transform - - # source: https://stackoverflow.com/questions/12299540/plane-fitting-to-4-or-more-xyz-points - def planeFit(self, points): - """ - p, n = planeFit(points) + def __init__(self, fiducialListNode, dl=0.5): + import numpy + self.dl = dl # desired world space step size (in mm) + self.dt = dl # current guess of parametric stepsize + self.fids = fiducialListNode + + # Already a curve, just get the points, sampled at equal distances. + if (self.fids.GetClassName() == "vtkMRMLMarkupsCurveNode" + or self.fids.GetClassName() == "vtkMRMLMarkupsClosedCurveNode"): + # Temporarily increase the number of points per segment, to get a very smooth curve + pointsPerSegment = int(self.fids.GetCurveLengthWorld() / self.dl / self.fids.GetNumberOfControlPoints()) + 1 + originalPointsPerSegment = self.fids.GetNumberOfPointsPerInterpolatingSegment() + if originalPointsPerSegment < pointsPerSegment: + self.fids.SetNumberOfPointsPerInterpolatingSegment(pointsPerSegment) + # Get equidistant points + resampledPoints = vtk.vtkPoints() + slicer.vtkMRMLMarkupsCurveNode.ResamplePoints(self.fids.GetCurvePointsWorld(), resampledPoints, self.dl, self.fids.GetCurveClosed()) + # Restore original number of pointsPerSegment + if originalPointsPerSegment < pointsPerSegment: + self.fids.SetNumberOfPointsPerInterpolatingSegment(originalPointsPerSegment) + # Get it as a numpy array as an independent copy + self.path = vtk.util.numpy_support.vtk_to_numpy(resampledPoints.GetData()) + return + + # hermite interpolation functions + self.h00 = lambda t: 2 * t**3 - 3 * t**2 + 1 + self.h10 = lambda t: t**3 - 2 * t**2 + t + self.h01 = lambda t: -2 * t**3 + 3 * t**2 + self.h11 = lambda t: t**3 - t**2 + + # n is the number of control points in the piecewise curve + + if self.fids.GetClassName() == "vtkMRMLAnnotationHierarchyNode": + # slicer4 style hierarchy nodes + collection = vtk.vtkCollection() + self.fids.GetChildrenDisplayableNodes(collection) + self.n = collection.GetNumberOfItems() + if self.n == 0: + return + self.p = numpy.zeros((self.n, 3)) + for i in range(self.n): + f = collection.GetItemAsObject(i) + coords = [0, 0, 0] + f.GetFiducialCoordinates(coords) + self.p[i] = coords + elif self.fids.GetClassName() == "vtkMRMLMarkupsFiducialNode": + # slicer4 Markups node + self.n = self.fids.GetNumberOfControlPoints() + n = self.n + if n == 0: + return + # get fiducial positions + # sets self.p + self.p = numpy.zeros((n, 3)) + for i in range(n): + coord = [0.0, 0.0, 0.0] + self.fids.GetNthControlPointPositionWorld(i, coord) + self.p[i] = coord + else: + # slicer3 style fiducial lists + self.n = self.fids.GetNumberOfFiducials() + n = self.n + if n == 0: + return + # get control point data + # sets self.p + self.p = numpy.zeros((n, 3)) + for i in range(n): + self.p[i] = self.fids.GetNthFiducialXYZ(i) + + # calculate the tangent vectors + # - fm is forward difference + # - m is average of in and out vectors + # - first tangent is out vector, last is in vector + # - sets self.m + n = self.n + fm = numpy.zeros((n, 3)) + for i in range(0, n - 1): + fm[i] = self.p[i + 1] - self.p[i] + self.m = numpy.zeros((n, 3)) + for i in range(1, n - 1): + self.m[i] = (fm[i - 1] + fm[i]) / 2. + self.m[0] = fm[0] + self.m[n - 1] = fm[n - 2] + + self.path = [self.p[0]] + self.calculatePath() + + def calculatePath(self): + """ Generate a flight path for of steps of length dl """ + # + # calculate the actual path + # - take steps of self.dl in world space + # -- if dl steps into next segment, take a step of size "remainder" in the new segment + # - put resulting points into self.path + # + n = self.n + segment = 0 # which first point of current segment + t = 0 # parametric current parametric increment + remainder = 0 # how much of dl isn't included in current step + while segment < n - 1: + t, p, remainder = self.step(segment, t, self.dl) + if remainder != 0 or t == 1.: + segment += 1 + t = 0 + if segment < n - 1: + t, p, remainder = self.step(segment, t, remainder) + self.path.append(p) + + def point(self, segment, t): + return (self.h00(t) * self.p[segment] + + self.h10(t) * self.m[segment] + + self.h01(t) * self.p[segment + 1] + + self.h11(t) * self.m[segment + 1]) + + def step(self, segment, t, dl): + """ Take a step of dl and return the path point and new t + return: + t = new parametric coordinate after step + p = point after step + remainder = if step results in parametric coordinate > 1.0, then + this is the amount of world space not covered by step + """ + import numpy.linalg + p0 = self.path[self.path.__len__() - 1] # last element in path + remainder = 0 + ratio = 100 + count = 0 + while abs(1. - ratio) > 0.05: + t1 = t + self.dt + pguess = self.point(segment, t1) + dist = numpy.linalg.norm(pguess - p0) + ratio = self.dl / dist + self.dt *= ratio + if self.dt < 0.00000001: + return + count += 1 + if count > 500: + return (t1, pguess, 0) + if t1 > 1.: + t1 = 1. + p1 = self.point(segment, t1) + remainder = numpy.linalg.norm(p1 - pguess) + pguess = p1 + return (t1, pguess, remainder) - Given an array, points, of shape (d,...) - representing points in d-dimensional space, - fit an d-dimensional plane to the points. - Return a point, p, on the plane (the point-cloud centroid), - and the normal, n. + +class EndoscopyPathModel: + """Create a vtkPolyData for a polyline: + - Add one point per path point. + - Add a single polyline """ - import numpy as np - from numpy.linalg import svd - points = np.reshape(points, (np.shape(points)[0], -1)) # Collapse trialing dimensions - assert points.shape[0] <= points.shape[1], f"There are only {points.shape[1]} points in {points.shape[0]} dimensions." - ctr = points.mean(axis=1) - x = points - ctr[:,np.newaxis] - M = np.dot(x, x.T) # Could also use np.cov(x) here. - return ctr, svd(M)[0][:,-1] + + def __init__(self, path, fiducialListNode, outputPathNode=None, cursorType=None): + """ + :param path: path points as numpy array. + :param fiducialListNode: input node, just used for naming the output node. + :param outputPathNode: output model node that stores the path points. + :param cursorType: can be 'markups' or 'model'. Markups has a number of advantages (radius it is easier to change the size, + can jump to views by clicking on it, has more visualization options, can be scaled to fixed display size), + but if some applications relied on having a model node as cursor then this argument can be used to achieve that. + """ + + fids = fiducialListNode + scene = slicer.mrmlScene + + self.cursorType = "markups" if cursorType is None else cursorType + + points = vtk.vtkPoints() + polyData = vtk.vtkPolyData() + polyData.SetPoints(points) + + lines = vtk.vtkCellArray() + polyData.SetLines(lines) + linesIDArray = lines.GetData() + linesIDArray.Reset() + linesIDArray.InsertNextTuple1(0) + + polygons = vtk.vtkCellArray() + polyData.SetPolys(polygons) + idArray = polygons.GetData() + idArray.Reset() + idArray.InsertNextTuple1(0) + + for point in path: + pointIndex = points.InsertNextPoint(*point) + linesIDArray.InsertNextTuple1(pointIndex) + linesIDArray.SetTuple1(0, linesIDArray.GetNumberOfTuples() - 1) + lines.SetNumberOfCells(1) + + pointsArray = vtk.util.numpy_support.vtk_to_numpy(points.GetData()) + self.planePosition, self.planeNormal = self.planeFit(pointsArray.T) + + # Create model node + model = outputPathNode + if not model: + model = scene.AddNewNodeByClass("vtkMRMLModelNode", scene.GenerateUniqueName("Path-%s" % fids.GetName())) + model.CreateDefaultDisplayNodes() + model.GetDisplayNode().SetColor(1, 1, 0) # yellow + + model.SetAndObservePolyData(polyData) + + # Camera cursor + cursor = model.GetNodeReference("CameraCursor") + if not cursor: + + if self.cursorType == "markups": + # Markups cursor + cursor = scene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", scene.GenerateUniqueName("Cursor-%s" % fids.GetName())) + cursor.CreateDefaultDisplayNodes() + cursor.GetDisplayNode().SetSelectedColor(1, 0, 0) # red + cursor.GetDisplayNode().SetSliceProjection(True) + cursor.AddControlPoint(vtk.vtkVector3d(0, 0, 0), " ") # do not show any visible label + cursor.SetNthControlPointLocked(0, True) + else: + # Model cursor + cursor = scene.AddNewNodeByClass("vtkMRMLMarkupsModelNode", scene.GenerateUniqueName("Cursor-%s" % fids.GetName())) + cursor.CreateDefaultDisplayNodes() + cursor.GetDisplayNode().SetColor(1, 0, 0) # red + cursor.GetDisplayNode().BackfaceCullingOn() # so that the camera can see through the cursor from inside + # Add a sphere as cursor + sphere = vtk.vtkSphereSource() + sphere.Update() + cursor.SetPolyDataConnection(sphere.GetOutputPort()) + + model.SetNodeReferenceID("CameraCursor", cursor.GetID()) + + # Transform node + transform = model.GetNodeReference("CameraTransform") + if not transform: + transform = scene.AddNewNodeByClass("vtkMRMLLinearTransformNode", scene.GenerateUniqueName("Transform-%s" % fids.GetName())) + model.SetNodeReferenceID("CameraTransform", transform.GetID()) + cursor.SetAndObserveTransformNodeID(transform.GetID()) + + self.transform = transform + + # source: https://stackoverflow.com/questions/12299540/plane-fitting-to-4-or-more-xyz-points + def planeFit(self, points): + """ + p, n = planeFit(points) + + Given an array, points, of shape (d,...) + representing points in d-dimensional space, + fit an d-dimensional plane to the points. + Return a point, p, on the plane (the point-cloud centroid), + and the normal, n. + """ + import numpy as np + from numpy.linalg import svd + points = np.reshape(points, (np.shape(points)[0], -1)) # Collapse trialing dimensions + assert points.shape[0] <= points.shape[1], f"There are only {points.shape[1]} points in {points.shape[0]} dimensions." + ctr = points.mean(axis=1) + x = points - ctr[:, np.newaxis] + M = np.dot(x, x.T) # Could also use np.cov(x) here. + return ctr, svd(M)[0][:, -1] diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizard.py b/Modules/Scripted/ExtensionWizard/ExtensionWizard.py index e1fc4255025..6ae4111e1bf 100644 --- a/Modules/Scripted/ExtensionWizard/ExtensionWizard.py +++ b/Modules/Scripted/ExtensionWizard/ExtensionWizard.py @@ -14,460 +14,460 @@ from ExtensionWizardLib import * -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _settingsList(settings, key, convertToAbsolutePaths=False): - # Return a settings value as a list (even if empty or a single value) - - value = settings.value(key) - if value is None: - return [] - if isinstance(value, str): - value = [value] - - if convertToAbsolutePaths: - absolutePaths = [] - for path in value: - absolutePaths.append(slicer.app.toSlicerHomeAbsolutePath(path)) - return absolutePaths - else: - return value + # Return a settings value as a list (even if empty or a single value) + + value = settings.value(key) + if value is None: + return [] + if isinstance(value, str): + value = [value] + + if convertToAbsolutePaths: + absolutePaths = [] + for path in value: + absolutePaths.append(slicer.app.toSlicerHomeAbsolutePath(path)) + return absolutePaths + else: + return value -#============================================================================= +# ============================================================================= # # ExtensionWizard # -#============================================================================= +# ============================================================================= class ExtensionWizard: - #--------------------------------------------------------------------------- - def __init__(self, parent): - parent.title = "Extension Wizard" - parent.icon = qt.QIcon(":/Icons/Medium/ExtensionWizard.png") - parent.categories = ["Developer Tools"] - parent.dependencies = [] - parent.contributors = ["Matthew Woehlke (Kitware)"] - parent.helpText = """ + # --------------------------------------------------------------------------- + def __init__(self, parent): + parent.title = "Extension Wizard" + parent.icon = qt.QIcon(":/Icons/Medium/ExtensionWizard.png") + parent.categories = ["Developer Tools"] + parent.dependencies = [] + parent.contributors = ["Matthew Woehlke (Kitware)"] + parent.helpText = """ This module provides tools to create and manage extensions from within Slicer. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This work is supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. """ - self.parent = parent + self.parent = parent - self.settingsPanel = SettingsPanel() - slicer.app.settingsDialog().addPanel("Extension Wizard", self.settingsPanel) + self.settingsPanel = SettingsPanel() + slicer.app.settingsDialog().addPanel("Extension Wizard", self.settingsPanel) -#============================================================================= +# ============================================================================= # # ExtensionWizardWidget # -#============================================================================= +# ============================================================================= class ExtensionWizardWidget: - #--------------------------------------------------------------------------- - def __init__(self, parent = None): - if not parent: - self.parent = qt.QWidget() - self.parent.setLayout(qt.QVBoxLayout()) + # --------------------------------------------------------------------------- + def __init__(self, parent=None): + if not parent: + self.parent = qt.QWidget() + self.parent.setLayout(qt.QVBoxLayout()) - else: - self.parent = parent - - self.layout = self.parent.layout() - - if not parent: - self.setup() - self.parent.show() - - self.extensionProject = None - self.extensionDescription = None - self.extensionLocation = None - - self.templateManager = None - self.setupTemplates() - - #--------------------------------------------------------------------------- - def setup(self): - # Instantiate and connect widgets ... - - icon = self.parent.style().standardIcon(qt.QStyle.SP_ArrowForward) - iconSize = qt.QSize(22, 22) - - def createToolButton(text): - tb = qt.QToolButton() - - tb.text = text - tb.icon = icon - - font = tb.font - font.setBold(True) - font.setPixelSize(14) - tb.font = font - - tb.iconSize = iconSize - tb.toolButtonStyle = qt.Qt.ToolButtonTextBesideIcon - tb.autoRaise = True - - return tb - - def createReadOnlyLineEdit(): - le = qt.QLineEdit() - le.readOnly = True - le.frame = False - le.styleSheet = "QLineEdit { background:transparent; }" - le.cursor = qt.QCursor(qt.Qt.IBeamCursor) - return le - - # - # Tools Area - # - self.toolsCollapsibleButton = ctk.ctkCollapsibleButton() - self.toolsCollapsibleButton.text = "Extension Tools" - self.layout.addWidget(self.toolsCollapsibleButton) - - self.createExtensionButton = createToolButton("Create Extension") - self.createExtensionButton.connect('clicked(bool)', self.createExtension) - - self.selectExtensionButton = createToolButton("Select Extension") - self.selectExtensionButton.connect('clicked(bool)', self.selectExtension) - - toolsLayout = qt.QVBoxLayout(self.toolsCollapsibleButton) - toolsLayout.addWidget(self.createExtensionButton) - toolsLayout.addWidget(self.selectExtensionButton) - - # - # Editor Area - # - self.editorCollapsibleButton = ctk.ctkCollapsibleButton() - self.editorCollapsibleButton.text = "Extension Editor" - self.editorCollapsibleButton.enabled = False - self.editorCollapsibleButton.collapsed = True - self.layout.addWidget(self.editorCollapsibleButton) - - self.extensionNameField = createReadOnlyLineEdit() - self.extensionLocationField = createReadOnlyLineEdit() - self.extensionRepositoryField = createReadOnlyLineEdit() - - self.extensionContentsModel = qt.QFileSystemModel() - self.extensionContentsView = qt.QTreeView() - self.extensionContentsView.setModel(self.extensionContentsModel) - self.extensionContentsView.sortingEnabled = True - self.extensionContentsView.hideColumn(3) - - self.createExtensionModuleButton = createToolButton("Add Module to Extension") - self.createExtensionModuleButton.connect('clicked(bool)', - self.createExtensionModule) - - self.editExtensionMetadataButton = createToolButton("Edit Extension Metadata") - self.editExtensionMetadataButton.connect('clicked(bool)', - self.editExtensionMetadata) - - editorLayout = qt.QFormLayout(self.editorCollapsibleButton) - editorLayout.addRow("Name:", self.extensionNameField) - editorLayout.addRow("Location:", self.extensionLocationField) - editorLayout.addRow("Repository:", self.extensionRepositoryField) - editorLayout.addRow("Contents:", self.extensionContentsView) - editorLayout.addRow(self.createExtensionModuleButton) - editorLayout.addRow(self.editExtensionMetadataButton) - - # Add vertical spacer - self.layout.addStretch(1) - - #--------------------------------------------------------------------------- - def cleanup(self): - pass - - #--------------------------------------------------------------------------- - def setupTemplates(self): - self.templateManager = SlicerWizard.TemplateManager() - - builtinPath = builtinTemplatePath() - if builtinPath is not None: - try: - self.templateManager.addPath(builtinPath) - except: - qt.qWarning("failed to add built-in template path %r" % builtinPath) - qt.qWarning(traceback.format_exc()) - - # Read base template paths - s = qt.QSettings() - for path in _settingsList(s, userTemplatePathKey(), convertToAbsolutePaths=True): - try: - self.templateManager.addPath(path) - except: - qt.qWarning("failed to add template path %r" % path) - qt.qWarning(traceback.format_exc()) - - # Read per-category template paths - s.beginGroup(userTemplatePathKey()) - for c in s.allKeys(): - for path in _settingsList(s, c, convertToAbsolutePaths=True): + else: + self.parent = parent + + self.layout = self.parent.layout() + + if not parent: + self.setup() + self.parent.show() + + self.extensionProject = None + self.extensionDescription = None + self.extensionLocation = None + + self.templateManager = None + self.setupTemplates() + + # --------------------------------------------------------------------------- + def setup(self): + # Instantiate and connect widgets ... + + icon = self.parent.style().standardIcon(qt.QStyle.SP_ArrowForward) + iconSize = qt.QSize(22, 22) + + def createToolButton(text): + tb = qt.QToolButton() + + tb.text = text + tb.icon = icon + + font = tb.font + font.setBold(True) + font.setPixelSize(14) + tb.font = font + + tb.iconSize = iconSize + tb.toolButtonStyle = qt.Qt.ToolButtonTextBesideIcon + tb.autoRaise = True + + return tb + + def createReadOnlyLineEdit(): + le = qt.QLineEdit() + le.readOnly = True + le.frame = False + le.styleSheet = "QLineEdit { background:transparent; }" + le.cursor = qt.QCursor(qt.Qt.IBeamCursor) + return le + + # + # Tools Area + # + self.toolsCollapsibleButton = ctk.ctkCollapsibleButton() + self.toolsCollapsibleButton.text = "Extension Tools" + self.layout.addWidget(self.toolsCollapsibleButton) + + self.createExtensionButton = createToolButton("Create Extension") + self.createExtensionButton.connect('clicked(bool)', self.createExtension) + + self.selectExtensionButton = createToolButton("Select Extension") + self.selectExtensionButton.connect('clicked(bool)', self.selectExtension) + + toolsLayout = qt.QVBoxLayout(self.toolsCollapsibleButton) + toolsLayout.addWidget(self.createExtensionButton) + toolsLayout.addWidget(self.selectExtensionButton) + + # + # Editor Area + # + self.editorCollapsibleButton = ctk.ctkCollapsibleButton() + self.editorCollapsibleButton.text = "Extension Editor" + self.editorCollapsibleButton.enabled = False + self.editorCollapsibleButton.collapsed = True + self.layout.addWidget(self.editorCollapsibleButton) + + self.extensionNameField = createReadOnlyLineEdit() + self.extensionLocationField = createReadOnlyLineEdit() + self.extensionRepositoryField = createReadOnlyLineEdit() + + self.extensionContentsModel = qt.QFileSystemModel() + self.extensionContentsView = qt.QTreeView() + self.extensionContentsView.setModel(self.extensionContentsModel) + self.extensionContentsView.sortingEnabled = True + self.extensionContentsView.hideColumn(3) + + self.createExtensionModuleButton = createToolButton("Add Module to Extension") + self.createExtensionModuleButton.connect('clicked(bool)', + self.createExtensionModule) + + self.editExtensionMetadataButton = createToolButton("Edit Extension Metadata") + self.editExtensionMetadataButton.connect('clicked(bool)', + self.editExtensionMetadata) + + editorLayout = qt.QFormLayout(self.editorCollapsibleButton) + editorLayout.addRow("Name:", self.extensionNameField) + editorLayout.addRow("Location:", self.extensionLocationField) + editorLayout.addRow("Repository:", self.extensionRepositoryField) + editorLayout.addRow("Contents:", self.extensionContentsView) + editorLayout.addRow(self.createExtensionModuleButton) + editorLayout.addRow(self.editExtensionMetadataButton) + + # Add vertical spacer + self.layout.addStretch(1) + + # --------------------------------------------------------------------------- + def cleanup(self): + pass + + # --------------------------------------------------------------------------- + def setupTemplates(self): + self.templateManager = SlicerWizard.TemplateManager() + + builtinPath = builtinTemplatePath() + if builtinPath is not None: + try: + self.templateManager.addPath(builtinPath) + except: + qt.qWarning("failed to add built-in template path %r" % builtinPath) + qt.qWarning(traceback.format_exc()) + + # Read base template paths + s = qt.QSettings() + for path in _settingsList(s, userTemplatePathKey(), convertToAbsolutePaths=True): + try: + self.templateManager.addPath(path) + except: + qt.qWarning("failed to add template path %r" % path) + qt.qWarning(traceback.format_exc()) + + # Read per-category template paths + s.beginGroup(userTemplatePathKey()) + for c in s.allKeys(): + for path in _settingsList(s, c, convertToAbsolutePaths=True): + try: + self.templateManager.addCategoryPath(c, path) + except: + mp = (c, path) + qt.qWarning("failed to add template path %r for category %r" % mp) + qt.qWarning(traceback.format_exc()) + + # --------------------------------------------------------------------------- + def createExtension(self): + dlg = CreateComponentDialog("extension", self.parent.window()) + dlg.setTemplates(self.templateManager.templates("extensions")) + + while dlg.exec_() == qt.QDialog.Accepted: + + # If the selected destination is in a repository then use the root of that repository + # as destination + try: + repo = SlicerWizard.Utilities.getRepo(dlg.destination) + + createInSubdirectory = True + requireEmptyDirectory = True + + if repo is None: + destination = os.path.join(dlg.destination, dlg.componentName) + if os.path.exists(destination): + raise OSError("create extension: refusing to overwrite" + " existing directory '%s'" % destination) + createInSubdirectory = False + + else: + destination = SlicerWizard.Utilities.localRoot(repo) + cmakeFile = os.path.join(destination, "CMakeLists.txt") + createInSubdirectory = False # create the files in the destination directory + requireEmptyDirectory = False # we only check if no CMakeLists.txt file exists + if os.path.exists(cmakeFile): + raise OSError("create extension: refusing to overwrite" + " directory containing CMakeLists.txt file at '%s'" % dlg.destination) + + path = self.templateManager.copyTemplate( + destination, "extensions", + dlg.componentType, dlg.componentName, + createInSubdirectory, requireEmptyDirectory) + + except: + if not slicer.util.confirmRetryCloseDisplay("An error occurred while trying to create the extension.", + parent=self.parent.window(), detailedText=traceback.format_exc()): + return + + continue + + if self.selectExtension(path): + self.editExtensionMetadata() + + return + + # --------------------------------------------------------------------------- + def selectExtension(self, path=None): + if path is None or isinstance(path, bool): + path = qt.QFileDialog.getExistingDirectory( + self.parent.window(), "Select Extension...", + self.extensionLocation) + + if not len(path): + return False + + # Attempt to open extension try: - self.templateManager.addCategoryPath(c, path) - except: - mp = (c, path) - qt.qWarning("failed to add template path %r for category %r" % mp) - qt.qWarning(traceback.format_exc()) - - #--------------------------------------------------------------------------- - def createExtension(self): - dlg = CreateComponentDialog("extension", self.parent.window()) - dlg.setTemplates(self.templateManager.templates("extensions")) - - while dlg.exec_() == qt.QDialog.Accepted: + repo = SlicerWizard.Utilities.getRepo(path) - # If the selected destination is in a repository then use the root of that repository - # as destination - try: - repo = SlicerWizard.Utilities.getRepo(dlg.destination) + xd = None + if repo: + try: + xd = SlicerWizard.ExtensionDescription(repo=repo) + path = SlicerWizard.Utilities.localRoot(repo) + except: + # Failed to determine repository path automatically (git is not installed, etc.) + # Continue with assuming that the user selected the top-level directory of the extension. + pass - createInSubdirectory = True - requireEmptyDirectory = True + if not xd: + xd = SlicerWizard.ExtensionDescription(sourcedir=path) - if repo is None: - destination = os.path.join(dlg.destination, dlg.componentName) - if os.path.exists(destination): - raise OSError("create extension: refusing to overwrite" - " existing directory '%s'" % destination) - createInSubdirectory = False + xp = SlicerWizard.ExtensionProject(path) - else: - destination = SlicerWizard.Utilities.localRoot(repo) - cmakeFile = os.path.join(destination, "CMakeLists.txt") - createInSubdirectory = False # create the files in the destination directory - requireEmptyDirectory = False # we only check if no CMakeLists.txt file exists - if os.path.exists(cmakeFile): - raise OSError("create extension: refusing to overwrite" - " directory containing CMakeLists.txt file at '%s'" % dlg.destination) - - path = self.templateManager.copyTemplate( - destination, "extensions", - dlg.componentType, dlg.componentName, - createInSubdirectory, requireEmptyDirectory) - - except: - if not slicer.util.confirmRetryCloseDisplay("An error occurred while trying to create the extension.", - parent=self.parent.window(), detailedText=traceback.format_exc()): - return - - continue - - if self.selectExtension(path): - self.editExtensionMetadata() - - return - - #--------------------------------------------------------------------------- - def selectExtension(self, path=None): - if path is None or isinstance(path, bool): - path = qt.QFileDialog.getExistingDirectory( - self.parent.window(), "Select Extension...", - self.extensionLocation) - - if not len(path): - return False - - # Attempt to open extension - try: - repo = SlicerWizard.Utilities.getRepo(path) - - xd = None - if repo: - try: - xd = SlicerWizard.ExtensionDescription(repo=repo) - path = SlicerWizard.Utilities.localRoot(repo) except: - # Failed to determine repository path automatically (git is not installed, etc.) - # Continue with assuming that the user selected the top-level directory of the extension. - pass - - if not xd: - xd = SlicerWizard.ExtensionDescription(sourcedir=path) - - xp = SlicerWizard.ExtensionProject(path) + slicer.util.errorDisplay("Failed to open extension '%s'." % path, parent=self.parent.window(), + detailedText=traceback.format_exc(), standardButtons=qt.QMessageBox.Close) + return False - except: - slicer.util.errorDisplay("Failed to open extension '%s'." % path, parent=self.parent.window(), - detailedText=traceback.format_exc(), standardButtons=qt.QMessageBox.Close) - return False + # Enable and show edit section + self.editorCollapsibleButton.enabled = True + self.editorCollapsibleButton.collapsed = False - # Enable and show edit section - self.editorCollapsibleButton.enabled = True - self.editorCollapsibleButton.collapsed = False + # Populate edit information + self.extensionNameField.text = xp.project + self.extensionLocationField.text = path - # Populate edit information - self.extensionNameField.text = xp.project - self.extensionLocationField.text = path + if xd.scmurl == "NA": + if repo is None: + repoText = "(none)" + elif hasattr(repo, "remotes"): + repoText = "(local git repository)" + else: + repoText = "(unknown local repository)" - if xd.scmurl == "NA": - if repo is None: - repoText = "(none)" - elif hasattr(repo, "remotes"): - repoText = "(local git repository)" - else: - repoText = "(unknown local repository)" + self.extensionRepositoryField.clear() + self.extensionRepositoryField.placeholderText = repoText - self.extensionRepositoryField.clear() - self.extensionRepositoryField.placeholderText = repoText - - else: - self.extensionRepositoryField.text = xd.scmurl - - ri = self.extensionContentsModel.setRootPath(path) - self.extensionContentsView.setRootIndex(ri) - - w = self.extensionContentsView.width - self.extensionContentsView.setColumnWidth(0, int((w * 4) / 9)) - - # Prompt to load scripted modules from extension - self.loadModules(path) - - # Store extension location, project and description for later use - self.extensionProject = xp - self.extensionDescription = xd - self.extensionLocation = path - return True - - #--------------------------------------------------------------------------- - def loadModules(self, path, depth=1): - # Get list of modules in specified path - modules = ModuleInfo.findModules(path, depth) - - # Determine which modules in above are not already loaded - factory = slicer.app.moduleManager().factoryManager() - loadedModules = factory.instantiatedModuleNames() - - candidates = [m for m in modules if m.key not in loadedModules] - - # Prompt to load additional module(s) - if len(candidates): - dlg = LoadModulesDialog(self.parent.window()) - dlg.setModules(candidates) - - if dlg.exec_() == qt.QDialog.Accepted: - modulesToLoad = dlg.selectedModules - - # Add module(s) to permanent search paths, if requested - if dlg.addToSearchPaths: - settings = slicer.app.revisionUserSettings() - rawSearchPaths = list(_settingsList(settings, "Modules/AdditionalPaths", convertToAbsolutePaths=True)) - searchPaths = [qt.QDir(path) for path in rawSearchPaths] - modified = False - - for module in modulesToLoad: - rawPath = os.path.dirname(module.path) - path = qt.QDir(rawPath) - if not path in searchPaths: - searchPaths.append(path) - rawSearchPaths.append(rawPath) - modified = True - - if modified: - settings.setValue("Modules/AdditionalPaths", slicer.app.toSlicerHomeRelativePaths(rawSearchPaths)) - - # Enable developer mode (shows Reload&Test section, etc.), if requested - if dlg.enableDeveloperMode: - qt.QSettings().setValue('Developer/DeveloperMode', 'true') - - # Register requested module(s) - failed = [] - - for module in modulesToLoad: - factory.registerModule(qt.QFileInfo(module.path)) - if not factory.isRegistered(module.key): - failed.append(module) - - if len(failed): - - if len(failed) > 1: - text = "The following modules could not be registered:" - else: - text = "The '%s' module could not be registered:" % failed[0].key - - failedFormat = "
          • %(key)s
            (%(path)s)
          " - detailedInformation = "".join( - [failedFormat % m.__dict__ for m in failed]) - - slicer.util.errorDisplay(text, parent=self.parent.window(), windowTitle="Module loading failed", - standardButtons=qt.QMessageBox.Close, informativeText=detailedInformation) - - return - - # Instantiate and load requested module(s) - if not factory.loadModules([module.key for module in modulesToLoad]): - text = ("The module factory manager reported an error. " - "One or more of the requested module(s) and/or " - "dependencies thereof may not have been loaded.") - slicer.util.errorDisplay(text, parent=self.parent.window(), windowTitle="Error loading module(s)", - standardButtons=qt.QMessageBox.Close) - - #--------------------------------------------------------------------------- - def createExtensionModule(self): - if (self.extensionLocation is None): - # Action shouldn't be enabled if no extension is selected, but guard - # against that just in case... - return - - dlg = CreateComponentDialog("module", self.parent.window()) - dlg.setTemplates(self.templateManager.templates("modules"), - default="scripted") - dlg.showDestination = False - - while dlg.exec_() == qt.QDialog.Accepted: - name = dlg.componentName - - try: - self.templateManager.copyTemplate(self.extensionLocation, "modules", - dlg.componentType, name) - - except: - if not slicer.util.confirmRetryCloseDisplay("An error occurred while trying to create the module.", - parent=self.parent.window(), - detailedText=traceback.format_exc()): - return - - continue - - try: - self.extensionProject.addModule(name) - self.extensionProject.save() - - except: - text = "An error occurred while adding the module to the extension." - detailedInformation = "The module has been created, but the extension" \ - " CMakeLists.txt could not be updated. In order" \ - " to include the module in the extension build," \ - " you will need to update the extension" \ - " CMakeLists.txt by hand." - slicer.util.errorDisplay(text, parent=self.parent.window(), detailedText = traceback.format_exc(), - standardButtons=qt.QMessageBox.Close, informativeText=detailedInformation) - - self.loadModules(os.path.join(self.extensionLocation, name), depth=0) - return - - #--------------------------------------------------------------------------- - def editExtensionMetadata(self): - xd = self.extensionDescription - xp = self.extensionProject - - dlg = EditExtensionMetadataDialog(self.parent.window()) - dlg.project = xp.project - dlg.category = xd.category - dlg.description = xd.description - dlg.contributors = xd.contributors - - if dlg.exec_() == qt.QDialog.Accepted: - # Update cached metadata - xd.category = dlg.category - xd.description = dlg.description - xd.contributors = dlg.contributors - - # Write changes to extension project file (CMakeLists.txt) - xp.project = dlg.project - xp.setValue("EXTENSION_CATEGORY", xd.category) - xp.setValue("EXTENSION_DESCRIPTION", xd.description) - xp.setValue("EXTENSION_CONTRIBUTORS", xd.contributors) - xp.save() - - # Update the displayed extension name - self.extensionNameField.text = xp.project + else: + self.extensionRepositoryField.text = xd.scmurl + + ri = self.extensionContentsModel.setRootPath(path) + self.extensionContentsView.setRootIndex(ri) + + w = self.extensionContentsView.width + self.extensionContentsView.setColumnWidth(0, int((w * 4) / 9)) + + # Prompt to load scripted modules from extension + self.loadModules(path) + + # Store extension location, project and description for later use + self.extensionProject = xp + self.extensionDescription = xd + self.extensionLocation = path + return True + + # --------------------------------------------------------------------------- + def loadModules(self, path, depth=1): + # Get list of modules in specified path + modules = ModuleInfo.findModules(path, depth) + + # Determine which modules in above are not already loaded + factory = slicer.app.moduleManager().factoryManager() + loadedModules = factory.instantiatedModuleNames() + + candidates = [m for m in modules if m.key not in loadedModules] + + # Prompt to load additional module(s) + if len(candidates): + dlg = LoadModulesDialog(self.parent.window()) + dlg.setModules(candidates) + + if dlg.exec_() == qt.QDialog.Accepted: + modulesToLoad = dlg.selectedModules + + # Add module(s) to permanent search paths, if requested + if dlg.addToSearchPaths: + settings = slicer.app.revisionUserSettings() + rawSearchPaths = list(_settingsList(settings, "Modules/AdditionalPaths", convertToAbsolutePaths=True)) + searchPaths = [qt.QDir(path) for path in rawSearchPaths] + modified = False + + for module in modulesToLoad: + rawPath = os.path.dirname(module.path) + path = qt.QDir(rawPath) + if path not in searchPaths: + searchPaths.append(path) + rawSearchPaths.append(rawPath) + modified = True + + if modified: + settings.setValue("Modules/AdditionalPaths", slicer.app.toSlicerHomeRelativePaths(rawSearchPaths)) + + # Enable developer mode (shows Reload&Test section, etc.), if requested + if dlg.enableDeveloperMode: + qt.QSettings().setValue('Developer/DeveloperMode', 'true') + + # Register requested module(s) + failed = [] + + for module in modulesToLoad: + factory.registerModule(qt.QFileInfo(module.path)) + if not factory.isRegistered(module.key): + failed.append(module) + + if len(failed): + + if len(failed) > 1: + text = "The following modules could not be registered:" + else: + text = "The '%s' module could not be registered:" % failed[0].key + + failedFormat = "
          • %(key)s
            (%(path)s)
          " + detailedInformation = "".join( + [failedFormat % m.__dict__ for m in failed]) + + slicer.util.errorDisplay(text, parent=self.parent.window(), windowTitle="Module loading failed", + standardButtons=qt.QMessageBox.Close, informativeText=detailedInformation) + + return + + # Instantiate and load requested module(s) + if not factory.loadModules([module.key for module in modulesToLoad]): + text = ("The module factory manager reported an error. " + "One or more of the requested module(s) and/or " + "dependencies thereof may not have been loaded.") + slicer.util.errorDisplay(text, parent=self.parent.window(), windowTitle="Error loading module(s)", + standardButtons=qt.QMessageBox.Close) + + # --------------------------------------------------------------------------- + def createExtensionModule(self): + if (self.extensionLocation is None): + # Action shouldn't be enabled if no extension is selected, but guard + # against that just in case... + return + + dlg = CreateComponentDialog("module", self.parent.window()) + dlg.setTemplates(self.templateManager.templates("modules"), + default="scripted") + dlg.showDestination = False + + while dlg.exec_() == qt.QDialog.Accepted: + name = dlg.componentName + + try: + self.templateManager.copyTemplate(self.extensionLocation, "modules", + dlg.componentType, name) + + except: + if not slicer.util.confirmRetryCloseDisplay("An error occurred while trying to create the module.", + parent=self.parent.window(), + detailedText=traceback.format_exc()): + return + + continue + + try: + self.extensionProject.addModule(name) + self.extensionProject.save() + + except: + text = "An error occurred while adding the module to the extension." + detailedInformation = "The module has been created, but the extension" \ + " CMakeLists.txt could not be updated. In order" \ + " to include the module in the extension build," \ + " you will need to update the extension" \ + " CMakeLists.txt by hand." + slicer.util.errorDisplay(text, parent=self.parent.window(), detailedText=traceback.format_exc(), + standardButtons=qt.QMessageBox.Close, informativeText=detailedInformation) + + self.loadModules(os.path.join(self.extensionLocation, name), depth=0) + return + + # --------------------------------------------------------------------------- + def editExtensionMetadata(self): + xd = self.extensionDescription + xp = self.extensionProject + + dlg = EditExtensionMetadataDialog(self.parent.window()) + dlg.project = xp.project + dlg.category = xd.category + dlg.description = xd.description + dlg.contributors = xd.contributors + + if dlg.exec_() == qt.QDialog.Accepted: + # Update cached metadata + xd.category = dlg.category + xd.description = dlg.description + xd.contributors = dlg.contributors + + # Write changes to extension project file (CMakeLists.txt) + xp.project = dlg.project + xp.setValue("EXTENSION_CATEGORY", xd.category) + xp.setValue("EXTENSION_DESCRIPTION", xd.description) + xp.setValue("EXTENSION_CONTRIBUTORS", xd.contributors) + xp.save() + + # Update the displayed extension name + self.extensionNameField.text = xp.project diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/CreateComponentDialog.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/CreateComponentDialog.py index 5121b21bb48..a392ef3f8d6 100644 --- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/CreateComponentDialog.py +++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/CreateComponentDialog.py @@ -6,112 +6,112 @@ import slicer -#============================================================================= +# ============================================================================= # # _ui_CreateComponentDialog # -#============================================================================= +# ============================================================================= class _ui_CreateComponentDialog: - #--------------------------------------------------------------------------- - def __init__(self, parent): - self.vLayout = qt.QVBoxLayout(parent) - self.formLayout = qt.QFormLayout() + # --------------------------------------------------------------------------- + def __init__(self, parent): + self.vLayout = qt.QVBoxLayout(parent) + self.formLayout = qt.QFormLayout() - self.componentName = qt.QLineEdit() - self.formLayout.addRow("Name:", self.componentName) + self.componentName = qt.QLineEdit() + self.formLayout.addRow("Name:", self.componentName) - self.componentNameValidator = qt.QRegExpValidator( - qt.QRegExp(r"^[a-zA-Z_][a-zA-Z0-9_]*$")) - self.componentName.setValidator(self.componentNameValidator) + self.componentNameValidator = qt.QRegExpValidator( + qt.QRegExp(r"^[a-zA-Z_][a-zA-Z0-9_]*$")) + self.componentName.setValidator(self.componentNameValidator) - self.componentType = qt.QComboBox() - self.formLayout.addRow("Type:", self.componentType) + self.componentType = qt.QComboBox() + self.formLayout.addRow("Type:", self.componentType) - self.destination = ctk.ctkPathLineEdit() - self.destination.filters = ctk.ctkPathLineEdit.Dirs - self.formLayout.addRow("Destination:", self.destination) + self.destination = ctk.ctkPathLineEdit() + self.destination.filters = ctk.ctkPathLineEdit.Dirs + self.formLayout.addRow("Destination:", self.destination) - self.vLayout.addLayout(self.formLayout) - self.vLayout.addStretch(1) + self.vLayout.addLayout(self.formLayout) + self.vLayout.addStretch(1) - self.buttonBox = qt.QDialogButtonBox() - self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Ok | - qt.QDialogButtonBox.Cancel) - self.vLayout.addWidget(self.buttonBox) + self.buttonBox = qt.QDialogButtonBox() + self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Ok | + qt.QDialogButtonBox.Cancel) + self.vLayout.addWidget(self.buttonBox) -#============================================================================= +# ============================================================================= # # CreateComponentDialog # -#============================================================================= +# ============================================================================= class CreateComponentDialog: - #--------------------------------------------------------------------------- - def __init__(self, componenttype, parent): - self.dialog = qt.QDialog(parent) - self.ui = _ui_CreateComponentDialog(self.dialog) - - self.ui.buttonBox.connect("accepted()", self.accept) - self.ui.buttonBox.connect("rejected()", self.dialog, "reject()") - - self._typelc = componenttype.lower() - self._typetc = componenttype.title() - - #--------------------------------------------------------------------------- - def accept(self): - if not len(self.componentName): - slicer.util.errorDisplay("%s name may not be empty." % self._typetc, - windowTitle="Cannot create %s" % self._typelc, parent=self.dialog) - return - - if self.showDestination: - dest = self.destination - if not len(dest) or not os.path.exists(dest): - slicer.util.errorDisplay("Destination must be an existing directory.", - windowTitle="Cannot create %s" % self._typelc, parent=self.dialog) - return - - self.dialog.accept() - - #--------------------------------------------------------------------------- - def setTemplates(self, templates, default="default"): - self.ui.componentType.clear() - self.ui.componentType.addItems(templates) - - try: - self.ui.componentType.currentIndex = templates.index(default) - except ValueError: - pass - - #--------------------------------------------------------------------------- - def exec_(self): - return self.dialog.exec_() - - #--------------------------------------------------------------------------- - @property - def showDestination(self): - return self.ui.destination.visible - - #--------------------------------------------------------------------------- - @showDestination.setter - def showDestination(self, value): - field = self.ui.destination - label = self.ui.formLayout.labelForField(field) - - label.visible = value - field.visible = value - - #--------------------------------------------------------------------------- - @property - def componentName(self): - return self.ui.componentName.text - - #--------------------------------------------------------------------------- - @property - def componentType(self): - return self.ui.componentType.currentText - - #--------------------------------------------------------------------------- - @property - def destination(self): - return self.ui.destination.currentPath + # --------------------------------------------------------------------------- + def __init__(self, componenttype, parent): + self.dialog = qt.QDialog(parent) + self.ui = _ui_CreateComponentDialog(self.dialog) + + self.ui.buttonBox.connect("accepted()", self.accept) + self.ui.buttonBox.connect("rejected()", self.dialog, "reject()") + + self._typelc = componenttype.lower() + self._typetc = componenttype.title() + + # --------------------------------------------------------------------------- + def accept(self): + if not len(self.componentName): + slicer.util.errorDisplay("%s name may not be empty." % self._typetc, + windowTitle="Cannot create %s" % self._typelc, parent=self.dialog) + return + + if self.showDestination: + dest = self.destination + if not len(dest) or not os.path.exists(dest): + slicer.util.errorDisplay("Destination must be an existing directory.", + windowTitle="Cannot create %s" % self._typelc, parent=self.dialog) + return + + self.dialog.accept() + + # --------------------------------------------------------------------------- + def setTemplates(self, templates, default="default"): + self.ui.componentType.clear() + self.ui.componentType.addItems(templates) + + try: + self.ui.componentType.currentIndex = templates.index(default) + except ValueError: + pass + + # --------------------------------------------------------------------------- + def exec_(self): + return self.dialog.exec_() + + # --------------------------------------------------------------------------- + @property + def showDestination(self): + return self.ui.destination.visible + + # --------------------------------------------------------------------------- + @showDestination.setter + def showDestination(self, value): + field = self.ui.destination + label = self.ui.formLayout.labelForField(field) + + label.visible = value + field.visible = value + + # --------------------------------------------------------------------------- + @property + def componentName(self): + return self.ui.componentName.text + + # --------------------------------------------------------------------------- + @property + def componentType(self): + return self.ui.componentType.currentText + + # --------------------------------------------------------------------------- + @property + def destination(self): + return self.ui.destination.currentPath diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/DirectoryListWidget.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/DirectoryListWidget.py index b83d6577187..be5dbd27aeb 100644 --- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/DirectoryListWidget.py +++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/DirectoryListWidget.py @@ -3,47 +3,47 @@ import slicer -#============================================================================= +# ============================================================================= # # _ui_DirectoryListWidget # -#============================================================================= +# ============================================================================= class _ui_DirectoryListWidget: - #--------------------------------------------------------------------------- - def __init__(self, parent): - layout = qt.QGridLayout(parent) + # --------------------------------------------------------------------------- + def __init__(self, parent): + layout = qt.QGridLayout(parent) - self.pathList = slicer.qSlicerDirectoryListView() - layout.addWidget(self.pathList, 0, 0, 3, 1) + self.pathList = slicer.qSlicerDirectoryListView() + layout.addWidget(self.pathList, 0, 0, 3, 1) - self.addPathButton = qt.QToolButton() - self.addPathButton.icon = qt.QIcon.fromTheme("list-add") - self.addPathButton.text = "Add" - layout.addWidget(self.addPathButton, 0, 1) + self.addPathButton = qt.QToolButton() + self.addPathButton.icon = qt.QIcon.fromTheme("list-add") + self.addPathButton.text = "Add" + layout.addWidget(self.addPathButton, 0, 1) - self.removePathButton = qt.QToolButton() - self.removePathButton.icon = qt.QIcon.fromTheme("list-remove") - self.removePathButton.text = "Remove" - layout.addWidget(self.removePathButton, 1, 1) + self.removePathButton = qt.QToolButton() + self.removePathButton.icon = qt.QIcon.fromTheme("list-remove") + self.removePathButton.text = "Remove" + layout.addWidget(self.removePathButton, 1, 1) -#============================================================================= +# ============================================================================= # # DirectoryListWidget # -#============================================================================= +# ============================================================================= class DirectoryListWidget(qt.QWidget): - #--------------------------------------------------------------------------- - def __init__(self, *args, **kwargs): - qt.QWidget.__init__(self, *args, **kwargs) - self.ui = _ui_DirectoryListWidget(self) - - self.ui.addPathButton.connect('clicked()', self.addDirectory) - self.ui.removePathButton.connect('clicked()', self.ui.pathList, - 'removeSelectedDirectories()') - - #--------------------------------------------------------------------------- - def addDirectory(self): - path = qt.QFileDialog.getExistingDirectory(self.window(), "Select folder") - if len(path): - self.ui.pathList.addDirectory(path) + # --------------------------------------------------------------------------- + def __init__(self, *args, **kwargs): + qt.QWidget.__init__(self, *args, **kwargs) + self.ui = _ui_DirectoryListWidget(self) + + self.ui.addPathButton.connect('clicked()', self.addDirectory) + self.ui.removePathButton.connect('clicked()', self.ui.pathList, + 'removeSelectedDirectories()') + + # --------------------------------------------------------------------------- + def addDirectory(self): + path = qt.QFileDialog.getExistingDirectory(self.window(), "Select folder") + if len(path): + self.ui.pathList.addDirectory(path) diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditExtensionMetadataDialog.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditExtensionMetadataDialog.py index 96b7037671e..94d532819b7 100644 --- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditExtensionMetadataDialog.py +++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditExtensionMetadataDialog.py @@ -7,113 +7,113 @@ from .EditableTreeWidget import EditableTreeWidget -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _map_property(objfunc, name): - return property(lambda self: getattr(objfunc(self), name), - lambda self, value: setattr(objfunc(self), name, value)) + return property(lambda self: getattr(objfunc(self), name), + lambda self, value: setattr(objfunc(self), name, value)) -#============================================================================= +# ============================================================================= # # _ui_EditExtensionMetadataDialog # -#============================================================================= +# ============================================================================= class _ui_EditExtensionMetadataDialog: - #--------------------------------------------------------------------------- - def __init__(self, parent): - vLayout = qt.QVBoxLayout(parent) - formLayout = qt.QFormLayout() + # --------------------------------------------------------------------------- + def __init__(self, parent): + vLayout = qt.QVBoxLayout(parent) + formLayout = qt.QFormLayout() - self.nameEdit = qt.QLineEdit() - formLayout.addRow("Name:", self.nameEdit) + self.nameEdit = qt.QLineEdit() + formLayout.addRow("Name:", self.nameEdit) - self.categoryEdit = qt.QLineEdit() - formLayout.addRow("Category:", self.categoryEdit) + self.categoryEdit = qt.QLineEdit() + formLayout.addRow("Category:", self.categoryEdit) - self.descriptionEdit = qt.QTextEdit() - self.descriptionEdit.acceptRichText = False - formLayout.addRow("Description:", self.descriptionEdit) + self.descriptionEdit = qt.QTextEdit() + self.descriptionEdit.acceptRichText = False + formLayout.addRow("Description:", self.descriptionEdit) - self.contributorsList = EditableTreeWidget() - self.contributorsList.rootIsDecorated = False - self.contributorsList.selectionBehavior = qt.QAbstractItemView.SelectRows - self.contributorsList.selectionMode = qt.QAbstractItemView.ExtendedSelection - self.contributorsList.setHeaderLabels(["Name", "Organization"]) - formLayout.addRow("Contributors:", self.contributorsList) + self.contributorsList = EditableTreeWidget() + self.contributorsList.rootIsDecorated = False + self.contributorsList.selectionBehavior = qt.QAbstractItemView.SelectRows + self.contributorsList.selectionMode = qt.QAbstractItemView.ExtendedSelection + self.contributorsList.setHeaderLabels(["Name", "Organization"]) + formLayout.addRow("Contributors:", self.contributorsList) - vLayout.addLayout(formLayout) - vLayout.addStretch(1) + vLayout.addLayout(formLayout) + vLayout.addStretch(1) - self.buttonBox = qt.QDialogButtonBox() - self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Ok | - qt.QDialogButtonBox.Cancel) - vLayout.addWidget(self.buttonBox) + self.buttonBox = qt.QDialogButtonBox() + self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Ok | + qt.QDialogButtonBox.Cancel) + vLayout.addWidget(self.buttonBox) -#============================================================================= +# ============================================================================= # # EditExtensionMetadataDialog # -#============================================================================= +# ============================================================================= class EditExtensionMetadataDialog: - project = _map_property(lambda self: self.ui.nameEdit, "text") - category = _map_property(lambda self: self.ui.categoryEdit, "text") - description = _map_property(lambda self: self.ui.descriptionEdit, "plainText") - - #--------------------------------------------------------------------------- - def __init__(self, parent): - self.dialog = qt.QDialog(parent) - self.ui = _ui_EditExtensionMetadataDialog(self.dialog) - - self.ui.buttonBox.connect("accepted()", self.accept) - self.ui.buttonBox.connect("rejected()", self.dialog, "reject()") - - #--------------------------------------------------------------------------- - def accept(self): - if not len(self.project): - slicer.util.errorDisplay("Extension name may not be empty.", windowTitle="Invalid metadata", parent=self.dialog) - return - - if not len(self.description): - slicer.util.errorDisplay("Extension description may not be empty.", - windowTitle="Invalid metadata", parent=self.dialog) - return - - self.dialog.accept() - - #--------------------------------------------------------------------------- - def exec_(self): - return self.dialog.exec_() - - #--------------------------------------------------------------------------- - @property - def contributors(self): - result = [] - for row in range(self.ui.contributorsList.itemCount): - item = self.ui.contributorsList.topLevelItem(row) - name = item.text(0) - organization = item.text(1) - if len(organization): - result.append(f"{name} ({organization})") - else: - result.append(name) - return ", ".join(result) - - #--------------------------------------------------------------------------- - @contributors.setter - def contributors(self, value): - self.ui.contributorsList.clear() - for c in re.split(r"(?<=[)])\s*,", value): - c = c.strip() - item = qt.QTreeWidgetItem() - - try: - n = c.index("(") - item.setText(0, c[:n].strip()) - item.setText(1, c[n+1:-1].strip()) - - except ValueError: - qt.qWarning("%r: badly formatted contributor" % c) - item.setText(0, c) - - self.ui.contributorsList.addItem(item) + project = _map_property(lambda self: self.ui.nameEdit, "text") + category = _map_property(lambda self: self.ui.categoryEdit, "text") + description = _map_property(lambda self: self.ui.descriptionEdit, "plainText") + + # --------------------------------------------------------------------------- + def __init__(self, parent): + self.dialog = qt.QDialog(parent) + self.ui = _ui_EditExtensionMetadataDialog(self.dialog) + + self.ui.buttonBox.connect("accepted()", self.accept) + self.ui.buttonBox.connect("rejected()", self.dialog, "reject()") + + # --------------------------------------------------------------------------- + def accept(self): + if not len(self.project): + slicer.util.errorDisplay("Extension name may not be empty.", windowTitle="Invalid metadata", parent=self.dialog) + return + + if not len(self.description): + slicer.util.errorDisplay("Extension description may not be empty.", + windowTitle="Invalid metadata", parent=self.dialog) + return + + self.dialog.accept() + + # --------------------------------------------------------------------------- + def exec_(self): + return self.dialog.exec_() + + # --------------------------------------------------------------------------- + @property + def contributors(self): + result = [] + for row in range(self.ui.contributorsList.itemCount): + item = self.ui.contributorsList.topLevelItem(row) + name = item.text(0) + organization = item.text(1) + if len(organization): + result.append(f"{name} ({organization})") + else: + result.append(name) + return ", ".join(result) + + # --------------------------------------------------------------------------- + @contributors.setter + def contributors(self, value): + self.ui.contributorsList.clear() + for c in re.split(r"(?<=[)])\s*,", value): + c = c.strip() + item = qt.QTreeWidgetItem() + + try: + n = c.index("(") + item.setText(0, c[:n].strip()) + item.setText(1, c[n + 1:-1].strip()) + + except ValueError: + qt.qWarning("%r: badly formatted contributor" % c) + item.setText(0, c) + + self.ui.contributorsList.addItem(item) diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditableTreeWidget.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditableTreeWidget.py index 061f30ab100..b555a26fe67 100644 --- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditableTreeWidget.py +++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditableTreeWidget.py @@ -1,160 +1,160 @@ import qt -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _makeAction(parent, text, icon=None, shortcut=None, slot=None): - action = qt.QAction(text, parent) + action = qt.QAction(text, parent) - if icon is not None: - action.setIcon(qt.QIcon.fromTheme(icon)) + if icon is not None: + action.setIcon(qt.QIcon.fromTheme(icon)) - if shortcut is not None: - action.shortcut = qt.QKeySequence.fromString(shortcut) - action.shortcutContext = qt.Qt.WidgetWithChildrenShortcut + if shortcut is not None: + action.shortcut = qt.QKeySequence.fromString(shortcut) + action.shortcutContext = qt.Qt.WidgetWithChildrenShortcut - if slot is not None: - action.connect('triggered(bool)', slot) + if slot is not None: + action.connect('triggered(bool)', slot) - parent.addAction(action) + parent.addAction(action) - return action + return action -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _newItemPlaceholderItem(parent): - palette = parent.palette - color = qt.QColor(palette.text().color()) - color.setAlphaF(0.5) + palette = parent.palette + color = qt.QColor(palette.text().color()) + color.setAlphaF(0.5) - item = qt.QTreeWidgetItem() - item.setText(0, "(New item)") - item.setForeground(0, qt.QBrush(color)) + item = qt.QTreeWidgetItem() + item.setText(0, "(New item)") + item.setForeground(0, qt.QBrush(color)) - return item + return item -#============================================================================= +# ============================================================================= # # EditableTreeWidget # -#============================================================================= +# ============================================================================= class EditableTreeWidget(qt.QTreeWidget): - #--------------------------------------------------------------------------- - def __init__(self, *args, **kwargs): - qt.QTreeWidget.__init__(self, *args, **kwargs) - - # Create initial placeholder item - self._items = [] - self.addItem(_newItemPlaceholderItem(self), placeholder=True) - - # Set up context menu - self._shiftUpAction = _makeAction(self, text="Move &Up", - icon="arrow-up", - shortcut="ctrl+shift+up", - slot=self.shiftSelectionUp) - - self._shiftDownAction = _makeAction(self, text="Move &Down", - icon="arrow-down", - shortcut="ctrl+shift+down", - slot=self.shiftSelectionDown) - - self._deleteAction = _makeAction(self, text="&Delete", icon="edit-delete", - shortcut="del", slot=self.deleteSelection) - - self.contextMenuPolicy = qt.Qt.ActionsContextMenu - - # Connect internal slots - self.connect('itemChanged(QTreeWidgetItem*,int)', self.updateItemData) - self.connect('itemSelectionChanged()', self.updateActions) - - #--------------------------------------------------------------------------- - def addItem(self, item, placeholder=False): - item.setFlags(item.flags() | qt.Qt.ItemIsEditable) - - if placeholder: - self._items.append(item) - self.addTopLevelItem(item) - else: - pos = len(self._items) - 1 - self._items.insert(pos, item) - self.insertTopLevelItem(pos, item) - - #--------------------------------------------------------------------------- - def clear(self): - # Delete all but placeholder item - while len(self._items) > 1: - del self._items[0] - - #--------------------------------------------------------------------------- - @property - def itemCount(self): - return self.topLevelItemCount - 1 - - #--------------------------------------------------------------------------- - def selectedRows(self): - placeholder = self._items[-1] - items = self.selectedItems() - return [self.indexOfTopLevelItem(i) for i in items if i is not placeholder] - - #--------------------------------------------------------------------------- - def setSelectedRows(self, rows): - sm = self.selectionModel() - sm.clear() - - for item in (self.topLevelItem(row) for row in rows): - item.setSelected(True) - - #--------------------------------------------------------------------------- - def updateActions(self): - placeholder = self._items[-1] - - items = self.selectedItems() - rows = self.selectedRows() - - last = self.topLevelItemCount - 2 - - self._shiftUpAction.enabled = len(rows) and not 0 in rows - self._shiftDownAction.enabled = len(rows) and not last in rows - self._deleteAction.enabled = True if len(rows) else False - - #--------------------------------------------------------------------------- - def updateItemData(self, item, column): - # Create new placeholder item if edited item is current placeholder - if item is self._items[-1]: - self.addItem(_newItemPlaceholderItem(self), placeholder=True) - - # Remove placeholder effect from new item - item.setData(0, qt.Qt.ForegroundRole, None) - if column != 0: - item.setText(0, "Anonymous") - - # Update actions so new item can be moved/deleted - self.updateActions() - - #--------------------------------------------------------------------------- - def shiftSelection(self, delta): - current = self.currentItem() - - rows = sorted(self.selectedRows()) - for row in rows if delta < 0 else reversed(rows): - item = self.takeTopLevelItem(row) - self._items.pop(row) - self._items.insert(row + delta, item) - self.insertTopLevelItem(row + delta, item) - - self.setSelectedRows(row + delta for row in rows) - self.setCurrentItem(current) - - #--------------------------------------------------------------------------- - def shiftSelectionUp(self): - self.shiftSelection(-1) - - #--------------------------------------------------------------------------- - def shiftSelectionDown(self): - self.shiftSelection(+1) - - #--------------------------------------------------------------------------- - def deleteSelection(self): - rows = self.selectedRows() - for row in reversed(sorted(rows)): - del self._items[row] + # --------------------------------------------------------------------------- + def __init__(self, *args, **kwargs): + qt.QTreeWidget.__init__(self, *args, **kwargs) + + # Create initial placeholder item + self._items = [] + self.addItem(_newItemPlaceholderItem(self), placeholder=True) + + # Set up context menu + self._shiftUpAction = _makeAction(self, text="Move &Up", + icon="arrow-up", + shortcut="ctrl+shift+up", + slot=self.shiftSelectionUp) + + self._shiftDownAction = _makeAction(self, text="Move &Down", + icon="arrow-down", + shortcut="ctrl+shift+down", + slot=self.shiftSelectionDown) + + self._deleteAction = _makeAction(self, text="&Delete", icon="edit-delete", + shortcut="del", slot=self.deleteSelection) + + self.contextMenuPolicy = qt.Qt.ActionsContextMenu + + # Connect internal slots + self.connect('itemChanged(QTreeWidgetItem*,int)', self.updateItemData) + self.connect('itemSelectionChanged()', self.updateActions) + + # --------------------------------------------------------------------------- + def addItem(self, item, placeholder=False): + item.setFlags(item.flags() | qt.Qt.ItemIsEditable) + + if placeholder: + self._items.append(item) + self.addTopLevelItem(item) + else: + pos = len(self._items) - 1 + self._items.insert(pos, item) + self.insertTopLevelItem(pos, item) + + # --------------------------------------------------------------------------- + def clear(self): + # Delete all but placeholder item + while len(self._items) > 1: + del self._items[0] + + # --------------------------------------------------------------------------- + @property + def itemCount(self): + return self.topLevelItemCount - 1 + + # --------------------------------------------------------------------------- + def selectedRows(self): + placeholder = self._items[-1] + items = self.selectedItems() + return [self.indexOfTopLevelItem(i) for i in items if i is not placeholder] + + # --------------------------------------------------------------------------- + def setSelectedRows(self, rows): + sm = self.selectionModel() + sm.clear() + + for item in (self.topLevelItem(row) for row in rows): + item.setSelected(True) + + # --------------------------------------------------------------------------- + def updateActions(self): + placeholder = self._items[-1] + + items = self.selectedItems() + rows = self.selectedRows() + + last = self.topLevelItemCount - 2 + + self._shiftUpAction.enabled = len(rows) and 0 not in rows + self._shiftDownAction.enabled = len(rows) and last not in rows + self._deleteAction.enabled = True if len(rows) else False + + # --------------------------------------------------------------------------- + def updateItemData(self, item, column): + # Create new placeholder item if edited item is current placeholder + if item is self._items[-1]: + self.addItem(_newItemPlaceholderItem(self), placeholder=True) + + # Remove placeholder effect from new item + item.setData(0, qt.Qt.ForegroundRole, None) + if column != 0: + item.setText(0, "Anonymous") + + # Update actions so new item can be moved/deleted + self.updateActions() + + # --------------------------------------------------------------------------- + def shiftSelection(self, delta): + current = self.currentItem() + + rows = sorted(self.selectedRows()) + for row in rows if delta < 0 else reversed(rows): + item = self.takeTopLevelItem(row) + self._items.pop(row) + self._items.insert(row + delta, item) + self.insertTopLevelItem(row + delta, item) + + self.setSelectedRows(row + delta for row in rows) + self.setCurrentItem(current) + + # --------------------------------------------------------------------------- + def shiftSelectionUp(self): + self.shiftSelection(-1) + + # --------------------------------------------------------------------------- + def shiftSelectionDown(self): + self.shiftSelection(+1) + + # --------------------------------------------------------------------------- + def deleteSelection(self): + rows = self.selectedRows() + for row in reversed(sorted(rows)): + del self._items[row] diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/LoadModulesDialog.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/LoadModulesDialog.py index 1236da150ae..d9498691dcc 100644 --- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/LoadModulesDialog.py +++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/LoadModulesDialog.py @@ -3,142 +3,142 @@ import slicer -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _dialogIcon(icon): - s = slicer.app.style() - i = s.standardIcon(icon) - return i.pixmap(qt.QSize(64, 64)) + s = slicer.app.style() + i = s.standardIcon(icon) + return i.pixmap(qt.QSize(64, 64)) -#============================================================================= +# ============================================================================= # # _ui_LoadModulesDialog # -#============================================================================= +# ============================================================================= class _ui_LoadModulesDialog: - #--------------------------------------------------------------------------- - def __init__(self, parent): - vLayout = qt.QVBoxLayout(parent) - hLayout = qt.QHBoxLayout() + # --------------------------------------------------------------------------- + def __init__(self, parent): + vLayout = qt.QVBoxLayout(parent) + hLayout = qt.QHBoxLayout() - self.icon = qt.QLabel() - self.icon.setPixmap(_dialogIcon(qt.QStyle.SP_MessageBoxQuestion)) - hLayout.addWidget(self.icon, 0) + self.icon = qt.QLabel() + self.icon.setPixmap(_dialogIcon(qt.QStyle.SP_MessageBoxQuestion)) + hLayout.addWidget(self.icon, 0) - self.label = qt.QLabel() - self.label.wordWrap = True - hLayout.addWidget(self.label, 1) + self.label = qt.QLabel() + self.label.wordWrap = True + hLayout.addWidget(self.label, 1) - vLayout.addLayout(hLayout) + vLayout.addLayout(hLayout) - self.moduleList = qt.QListWidget() - self.moduleList.selectionMode = qt.QAbstractItemView.NoSelection - vLayout.addWidget(self.moduleList) + self.moduleList = qt.QListWidget() + self.moduleList.selectionMode = qt.QAbstractItemView.NoSelection + vLayout.addWidget(self.moduleList) - self.addToSearchPaths = qt.QCheckBox() - vLayout.addWidget(self.addToSearchPaths) - self.addToSearchPaths.checked = True + self.addToSearchPaths = qt.QCheckBox() + vLayout.addWidget(self.addToSearchPaths) + self.addToSearchPaths.checked = True - self.enableDeveloperMode = qt.QCheckBox() - self.enableDeveloperMode.text = "Enable developer mode" - self.enableDeveloperMode.toolTip = "Sets the 'Developer mode' application option to enabled. Enabling developer mode is recommended while developing scripted modules, as it makes the Reload and Testing section displayed in the module user interface." - self.enableDeveloperMode.checked = True - vLayout.addWidget(self.enableDeveloperMode) + self.enableDeveloperMode = qt.QCheckBox() + self.enableDeveloperMode.text = "Enable developer mode" + self.enableDeveloperMode.toolTip = "Sets the 'Developer mode' application option to enabled. Enabling developer mode is recommended while developing scripted modules, as it makes the Reload and Testing section displayed in the module user interface." + self.enableDeveloperMode.checked = True + vLayout.addWidget(self.enableDeveloperMode) - self.buttonBox = qt.QDialogButtonBox() - self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Yes | - qt.QDialogButtonBox.No) - vLayout.addWidget(self.buttonBox) + self.buttonBox = qt.QDialogButtonBox() + self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Yes | + qt.QDialogButtonBox.No) + vLayout.addWidget(self.buttonBox) -#============================================================================= +# ============================================================================= # # LoadModulesDialog # -#============================================================================= +# ============================================================================= class LoadModulesDialog: - #--------------------------------------------------------------------------- - def __init__(self, parent): - self.dialog = qt.QDialog(parent) - self.ui = _ui_LoadModulesDialog(self.dialog) - - self.ui.buttonBox.connect("accepted()", self.dialog, "accept()") - self.ui.buttonBox.connect("rejected()", self.dialog, "reject()") - self.ui.moduleList.connect("itemChanged(QListWidgetItem*)", self.validate) - - #--------------------------------------------------------------------------- - def validate(self): - moduleCount = len(self.selectedModules) - - if moduleCount == 0: - self.ui.buttonBox.button(qt.QDialogButtonBox.Yes).enabled = False - self.ui.addToSearchPaths.enabled = False - - moduleCount = len(self._moduleItems) - - else: - self.ui.buttonBox.button(qt.QDialogButtonBox.Yes).enabled = True - self.ui.addToSearchPaths.enabled = True - - if moduleCount == 1: - self.ui.addToSearchPaths.text = "Add selected module to search paths" - else: - self.ui.addToSearchPaths.text = "Add selected modules to search paths" - - # If developer mode is already enabled then don't even show the option - developerModeAlreadyEnabled = slicer.util.settingsValue('Developer/DeveloperMode', False, converter=slicer.util.toBool) - if developerModeAlreadyEnabled: - self.ui.enableDeveloperMode.visible = False - self.ui.enableDeveloperMode.checked = False - - #--------------------------------------------------------------------------- - def exec_(self): - return self.dialog.exec_() - - #--------------------------------------------------------------------------- - def setModules(self, modules): - self.ui.moduleList.clear() - self._moduleItems = {} - - for module in modules: - item = qt.QListWidgetItem(module.key) - item.setFlags(item.flags() | qt.Qt.ItemIsUserCheckable) - item.setCheckState(qt.Qt.Checked) - self.ui.moduleList.addItem(item) - self._moduleItems[item] = module - - if len(modules) > 1: - self.ui.label.text = ( - "The following modules can be loaded. " - "Would you like to load them now?") - - elif len(modules) == 1: - self.ui.label.text = ( - "The following module can be loaded. " - "Would you like to load it now?") - - else: - raise ValueError("At least one module must be provided") - - self.validate() - - #--------------------------------------------------------------------------- - @property - def addToSearchPaths(self): - return self.ui.addToSearchPaths.checked - - #--------------------------------------------------------------------------- - @property - def enableDeveloperMode(self): - return self.ui.enableDeveloperMode.checked - - #--------------------------------------------------------------------------- - @property - def selectedModules(self): - result = [] - - for item, module in self._moduleItems.items(): - if item.checkState(): - result.append(module) - - return result + # --------------------------------------------------------------------------- + def __init__(self, parent): + self.dialog = qt.QDialog(parent) + self.ui = _ui_LoadModulesDialog(self.dialog) + + self.ui.buttonBox.connect("accepted()", self.dialog, "accept()") + self.ui.buttonBox.connect("rejected()", self.dialog, "reject()") + self.ui.moduleList.connect("itemChanged(QListWidgetItem*)", self.validate) + + # --------------------------------------------------------------------------- + def validate(self): + moduleCount = len(self.selectedModules) + + if moduleCount == 0: + self.ui.buttonBox.button(qt.QDialogButtonBox.Yes).enabled = False + self.ui.addToSearchPaths.enabled = False + + moduleCount = len(self._moduleItems) + + else: + self.ui.buttonBox.button(qt.QDialogButtonBox.Yes).enabled = True + self.ui.addToSearchPaths.enabled = True + + if moduleCount == 1: + self.ui.addToSearchPaths.text = "Add selected module to search paths" + else: + self.ui.addToSearchPaths.text = "Add selected modules to search paths" + + # If developer mode is already enabled then don't even show the option + developerModeAlreadyEnabled = slicer.util.settingsValue('Developer/DeveloperMode', False, converter=slicer.util.toBool) + if developerModeAlreadyEnabled: + self.ui.enableDeveloperMode.visible = False + self.ui.enableDeveloperMode.checked = False + + # --------------------------------------------------------------------------- + def exec_(self): + return self.dialog.exec_() + + # --------------------------------------------------------------------------- + def setModules(self, modules): + self.ui.moduleList.clear() + self._moduleItems = {} + + for module in modules: + item = qt.QListWidgetItem(module.key) + item.setFlags(item.flags() | qt.Qt.ItemIsUserCheckable) + item.setCheckState(qt.Qt.Checked) + self.ui.moduleList.addItem(item) + self._moduleItems[item] = module + + if len(modules) > 1: + self.ui.label.text = ( + "The following modules can be loaded. " + "Would you like to load them now?") + + elif len(modules) == 1: + self.ui.label.text = ( + "The following module can be loaded. " + "Would you like to load it now?") + + else: + raise ValueError("At least one module must be provided") + + self.validate() + + # --------------------------------------------------------------------------- + @property + def addToSearchPaths(self): + return self.ui.addToSearchPaths.checked + + # --------------------------------------------------------------------------- + @property + def enableDeveloperMode(self): + return self.ui.enableDeveloperMode.checked + + # --------------------------------------------------------------------------- + @property + def selectedModules(self): + result = [] + + for item, module in self._moduleItems.items(): + if item.checkState(): + result.append(module) + + return result diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/ModuleInfo.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/ModuleInfo.py index faa811f5db3..6f67ef67509 100644 --- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/ModuleInfo.py +++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/ModuleInfo.py @@ -1,49 +1,49 @@ import os -#============================================================================= +# ============================================================================= # # _ui_CreateComponentDialog # -#============================================================================= -#============================================================================= +# ============================================================================= +# ============================================================================= # # ModuleInfo # -#============================================================================= +# ============================================================================= class ModuleInfo: - #--------------------------------------------------------------------------- - def __init__(self, path, key=None): - self.path = path - self.searchPath = os.path.dirname(path) - - if key is None: - self.key = os.path.splitext(os.path.basename(path))[0] - else: - self.key = key - - #--------------------------------------------------------------------------- - def __repr__(self): - return "ModuleInfo(key=%(key)r, path=%(path)r)" % self.__dict__ - - #--------------------------------------------------------------------------- - def __str__(self): - return self.path - - #--------------------------------------------------------------------------- - @staticmethod - def findModules(path, depth): - result = [] - entries = [os.path.join(path, entry) for entry in os.listdir(path)] - - if depth > 0: - for entry in filter(os.path.isdir, entries): - result += ModuleInfo.findModules(entry, depth - 1) - - for entry in filter(os.path.isfile, entries): - # __init__.py is not a module but an embedded Python library - # that a module will load. - if entry.endswith(".py") and not entry.endswith("__init__.py"): - result.append(ModuleInfo(entry)) - - return result + # --------------------------------------------------------------------------- + def __init__(self, path, key=None): + self.path = path + self.searchPath = os.path.dirname(path) + + if key is None: + self.key = os.path.splitext(os.path.basename(path))[0] + else: + self.key = key + + # --------------------------------------------------------------------------- + def __repr__(self): + return "ModuleInfo(key=%(key)r, path=%(path)r)" % self.__dict__ + + # --------------------------------------------------------------------------- + def __str__(self): + return self.path + + # --------------------------------------------------------------------------- + @staticmethod + def findModules(path, depth): + result = [] + entries = [os.path.join(path, entry) for entry in os.listdir(path)] + + if depth > 0: + for entry in filter(os.path.isdir, entries): + result += ModuleInfo.findModules(entry, depth - 1) + + for entry in filter(os.path.isfile, entries): + # __init__.py is not a module but an embedded Python library + # that a module will load. + if entry.endswith(".py") and not entry.endswith("__init__.py"): + result.append(ModuleInfo(entry)) + + return result diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/SettingsPanel.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/SettingsPanel.py index 166ad1bc5ec..be8768d249b 100644 --- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/SettingsPanel.py +++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/SettingsPanel.py @@ -7,62 +7,62 @@ from .TemplatePathUtilities import * -#============================================================================= +# ============================================================================= # # _ui_SettingsPanel # -#============================================================================= +# ============================================================================= class _ui_SettingsPanel: - #--------------------------------------------------------------------------- - def __init__(self, parent): - self.formLayout = qt.QFormLayout(parent) + # --------------------------------------------------------------------------- + def __init__(self, parent): + self.formLayout = qt.QFormLayout(parent) - self.builtinPath = qt.QLineEdit() - builtinPath = builtinTemplatePath() - if (builtinPath): - self.builtinPath.text = builtinPath - else: - self.builtinPath.text = "(Unavailable)" - self.builtinPath.enabled = False - self.builtinPath.readOnly = True - self.addRow("Built-in template path:", self.builtinPath) + self.builtinPath = qt.QLineEdit() + builtinPath = builtinTemplatePath() + if (builtinPath): + self.builtinPath.text = builtinPath + else: + self.builtinPath.text = "(Unavailable)" + self.builtinPath.enabled = False + self.builtinPath.readOnly = True + self.addRow("Built-in template path:", self.builtinPath) - self.genericPaths = DirectoryListWidget() - self.addRow("Additional template\npaths:", self.genericPaths) + self.genericPaths = DirectoryListWidget() + self.addRow("Additional template\npaths:", self.genericPaths) - self.paths = {} + self.paths = {} - for category in SlicerWizard.TemplateManager.categories(): - self.paths[category] = DirectoryListWidget() - self.addRow("Additional template\npaths for %s:" % category, - self.paths[category]) + for category in SlicerWizard.TemplateManager.categories(): + self.paths[category] = DirectoryListWidget() + self.addRow("Additional template\npaths for %s:" % category, + self.paths[category]) - #--------------------------------------------------------------------------- - def addRow(self, label, widget): - self.formLayout.addRow(label, widget) - label = self.formLayout.labelForField(widget) - label.alignment = self.formLayout.labelAlignment + # --------------------------------------------------------------------------- + def addRow(self, label, widget): + self.formLayout.addRow(label, widget) + label = self.formLayout.labelForField(widget) + label.alignment = self.formLayout.labelAlignment -#============================================================================= +# ============================================================================= # # SettingsPanel # -#============================================================================= +# ============================================================================= class SettingsPanel(ctk.ctkSettingsPanel): - #--------------------------------------------------------------------------- - def __init__(self, *args, **kwargs): - ctk.ctkSettingsPanel.__init__(self, *args, **kwargs) - self.ui = _ui_SettingsPanel(self) + # --------------------------------------------------------------------------- + def __init__(self, *args, **kwargs): + ctk.ctkSettingsPanel.__init__(self, *args, **kwargs) + self.ui = _ui_SettingsPanel(self) - self.registerProperty( - userTemplatePathKey(), self.ui.genericPaths.ui.pathList, - "directoryList", str(qt.SIGNAL("directoryListChanged()")), - "Additional template paths", ctk.ctkSettingsPanel.OptionRequireRestart) + self.registerProperty( + userTemplatePathKey(), self.ui.genericPaths.ui.pathList, + "directoryList", str(qt.SIGNAL("directoryListChanged()")), + "Additional template paths", ctk.ctkSettingsPanel.OptionRequireRestart) - for category in self.ui.paths.keys(): - self.registerProperty( - userTemplatePathKey(category), self.ui.paths[category].ui.pathList, - "directoryList", str(qt.SIGNAL("directoryListChanged()")), - "Additional template paths for %s" % category, - ctk.ctkSettingsPanel.OptionRequireRestart) + for category in self.ui.paths.keys(): + self.registerProperty( + userTemplatePathKey(category), self.ui.paths[category].ui.pathList, + "directoryList", str(qt.SIGNAL("directoryListChanged()")), + "Additional template paths for %s" % category, + ctk.ctkSettingsPanel.OptionRequireRestart) diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/TemplatePathUtilities.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/TemplatePathUtilities.py index 03d77b0c2f4..63e0aaae87d 100644 --- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/TemplatePathUtilities.py +++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/TemplatePathUtilities.py @@ -5,31 +5,31 @@ _userTemplatePathKey = "ExtensionWizard/TemplatePaths" -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def userTemplatePathKey(category=None): - if category is None: - return _userTemplatePathKey - else: - return f"{_userTemplatePathKey}/{category}" + if category is None: + return _userTemplatePathKey + else: + return f"{_userTemplatePathKey}/{category}" -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def builtinTemplatePath(): - # Look for templates in source directory first - path = slicer.util.sourceDir() + # Look for templates in source directory first + path = slicer.util.sourceDir() - if path is not None: - path = os.path.join(path, "Utilities", "Templates") + if path is not None: + path = os.path.join(path, "Utilities", "Templates") - if os.path.exists(path): - return path + if os.path.exists(path): + return path - # Look for installed templates - path = os.path.join(slicer.app.slicerHome, slicer.app.slicerSharePath, - "Wizard", "Templates") + # Look for installed templates + path = os.path.join(slicer.app.slicerHome, slicer.app.slicerSharePath, + "Wizard", "Templates") - if os.path.exists(path): - return path + if os.path.exists(path): + return path - # No templates found - return None + # No templates found + return None diff --git a/Modules/Scripted/PerformanceTests/PerformanceTests.py b/Modules/Scripted/PerformanceTests/PerformanceTests.py index 3b6d690fd66..82f14ee6d4b 100644 --- a/Modules/Scripted/PerformanceTests/PerformanceTests.py +++ b/Modules/Scripted/PerformanceTests/PerformanceTests.py @@ -9,19 +9,19 @@ # class PerformanceTests(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "Performance Tests" - parent.categories = ["Testing.TestCases"] - parent.contributors = ["Steve Pieper (Isomics)"] - parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "Performance Tests" + parent.categories = ["Testing.TestCases"] + parent.contributors = ["Steve Pieper (Isomics)"] + parent.helpText = """ Module to run interactive performance tests on the core of slicer. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This file was based on work originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. and others. This work was partially funded by NIH grant 3P41RR013218-12S1. """ - self.parent = parent + self.parent = parent # @@ -29,212 +29,212 @@ def __init__(self, parent): # class PerformanceTestsWidget(ScriptedLoadableModuleWidget): - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - tests = ( - ( 'Get Sample Data', self.downloadMRHead ), - ( 'Reslicing', self.reslicing ), - ( 'Crosshair Jump', self.crosshairJump ), - ( 'Web View Test', self.webViewTest ), - ( 'Fill Out Web Form Test', self.webViewFormTest ), - ( 'Memory Check', self.memoryCheck ), - ) - - for test in tests: - b = qt.QPushButton(test[0]) - self.layout.addWidget(b) - b.connect('clicked()', test[1]) - - self.log = qt.QTextEdit() - self.log.readOnly = True - self.layout.addWidget(self.log) - self.log.insertHtml('

          Status: Idle\n') - self.log.insertPlainText('\n') - self.log.ensureCursorVisible() - - # Add spacer to layout - self.layout.addStretch(1) - - def downloadMRHead(self): - import SampleData - self.log.insertHtml('Requesting downloading MRHead') - self.log.repaint() - mrHeadVolume = SampleData.downloadSample("MRHead") - if mrHeadVolume: - self.log.insertHtml('finished.\n') - self.log.insertPlainText('\n') - self.log.repaint() - else: - self.log.insertHtml('Download failed!\n') - self.log.insertPlainText('\n') - self.log.repaint() - self.log.ensureCursorVisible() - - def timeSteps(self, iters, f): - import time - elapsedTime = 0 - for i in range(iters): - startTime = time.time() - f() - slicer.app.processEvents() - endTime = time.time() - elapsedTime += (endTime - startTime) - fps = int(iters / elapsedTime) - result = f"fps = {fps:g} ({1000./fps:g} ms per frame)" - print (result) - self.log.insertHtml('%s' % result) - self.log.insertPlainText('\n') - self.log.ensureCursorVisible() - self.log.repaint() - - def reslicing(self, iters=100): - """ go into a loop that stresses the reslice performance - """ - import time - import math - import numpy as np - sliceNode = slicer.util.getNode('vtkMRMLSliceNodeRed') - dims = sliceNode.GetDimensions() - elapsedTime = 0 - sliceOffset = 5 - offsetSteps = 10 - numerOfSweeps = int(math.ceil(iters / offsetSteps)) - renderingTimesSec = np.zeros(numerOfSweeps*offsetSteps*2) - sampleIndex = 0 - startOffset = sliceNode.GetSliceOffset() - for i in range(numerOfSweeps): - for offset in ([sliceOffset]*offsetSteps + [-sliceOffset]*offsetSteps): - startTime = time.time() - sliceNode.SetSliceOffset(sliceNode.GetSliceOffset()+offset) - slicer.app.processEvents() - endTime = time.time() - renderingTimesSec[sampleIndex] = (endTime-startTime) - sampleIndex += 1 - sliceNode.SetSliceOffset(startOffset) - - resultTableName = slicer.mrmlScene.GetUniqueNameByString("Reslice performance") - resultTableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", resultTableName) - slicer.util.updateTableFromArray(resultTableNode, renderingTimesSec, "Rendering time [s]") - - renderingTimeMean = np.mean(renderingTimesSec) - renderingTimeStd = np.std(renderingTimesSec) - result = ("%d x %d, fps = %.1f (%.1f +/- %.2f ms per frame) - see details in table '%s'" - % (dims[0], dims[1], 1.0/renderingTimeMean, 1000. * renderingTimeMean, 1000. * renderingTimeStd, resultTableNode.GetName())) - print (result) - self.log.insertHtml('%s' % result) - self.log.insertPlainText('\n') - self.log.ensureCursorVisible() - self.log.repaint() - - def crosshairJump(self, iters=15): - """ go into a loop that stresses jumping to slices by moving crosshair - """ - import time - sliceNode = slicer.util.getNode('vtkMRMLSliceNodeRed') - dims = sliceNode.GetDimensions() - layoutManager = slicer.app.layoutManager() - sliceViewNames = layoutManager.sliceViewNames() - # Order of slice view names is random, prefer 'Red' slice to make results more predictable - firstSliceViewName = 'Red' if 'Red' in sliceViewNames else sliceViewNames[0] - firstSliceWidget = layoutManager.sliceWidget(firstSliceViewName) - elapsedTime = 0 - startPoint = (int(dims[0]*0.3), int(dims[1]*0.3)) - endPoint = (int(dims[0]*0.6), int(dims[1]*0.6)) - for i in range(iters): - startTime = time.time() - slicer.util.clickAndDrag(firstSliceWidget, button = None, modifiers = ['Shift'], start=startPoint, end=endPoint, steps=2) - slicer.app.processEvents() - endTime1 = time.time() - slicer.util.clickAndDrag(firstSliceWidget, button = None, modifiers = ['Shift'], start=endPoint, end=startPoint, steps=2) - slicer.app.processEvents() - endTime2 = time.time() - delta = ((endTime1-startTime) + (endTime2 - endTime1)) / 2. - elapsedTime += delta - fps = int(iters / elapsedTime) - result = "number of slice views = %d, fps = %g (%g ms per frame)" % (len(sliceViewNames), fps, 1000./fps) - print (result) - self.log.insertHtml('%s' % result) - self.log.insertPlainText('\n') - self.log.ensureCursorVisible() - self.log.repaint() - - def webViewCallback(self,qurl): - url = qurl.toString() - print(url) - if url == 'reslicing': - self.reslicing() - if url == 'chart': - self.chartTest() - pass - - def webViewTest(self): - self.webView = qt.QWebView() - html = """ + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + tests = ( + ('Get Sample Data', self.downloadMRHead), + ('Reslicing', self.reslicing), + ('Crosshair Jump', self.crosshairJump), + ('Web View Test', self.webViewTest), + ('Fill Out Web Form Test', self.webViewFormTest), + ('Memory Check', self.memoryCheck), + ) + + for test in tests: + b = qt.QPushButton(test[0]) + self.layout.addWidget(b) + b.connect('clicked()', test[1]) + + self.log = qt.QTextEdit() + self.log.readOnly = True + self.layout.addWidget(self.log) + self.log.insertHtml('

          Status: Idle\n') + self.log.insertPlainText('\n') + self.log.ensureCursorVisible() + + # Add spacer to layout + self.layout.addStretch(1) + + def downloadMRHead(self): + import SampleData + self.log.insertHtml('Requesting downloading MRHead') + self.log.repaint() + mrHeadVolume = SampleData.downloadSample("MRHead") + if mrHeadVolume: + self.log.insertHtml('finished.\n') + self.log.insertPlainText('\n') + self.log.repaint() + else: + self.log.insertHtml('Download failed!\n') + self.log.insertPlainText('\n') + self.log.repaint() + self.log.ensureCursorVisible() + + def timeSteps(self, iters, f): + import time + elapsedTime = 0 + for i in range(iters): + startTime = time.time() + f() + slicer.app.processEvents() + endTime = time.time() + elapsedTime += (endTime - startTime) + fps = int(iters / elapsedTime) + result = f"fps = {fps:g} ({1000./fps:g} ms per frame)" + print(result) + self.log.insertHtml('%s' % result) + self.log.insertPlainText('\n') + self.log.ensureCursorVisible() + self.log.repaint() + + def reslicing(self, iters=100): + """ go into a loop that stresses the reslice performance + """ + import time + import math + import numpy as np + sliceNode = slicer.util.getNode('vtkMRMLSliceNodeRed') + dims = sliceNode.GetDimensions() + elapsedTime = 0 + sliceOffset = 5 + offsetSteps = 10 + numerOfSweeps = int(math.ceil(iters / offsetSteps)) + renderingTimesSec = np.zeros(numerOfSweeps * offsetSteps * 2) + sampleIndex = 0 + startOffset = sliceNode.GetSliceOffset() + for i in range(numerOfSweeps): + for offset in ([sliceOffset] * offsetSteps + [-sliceOffset] * offsetSteps): + startTime = time.time() + sliceNode.SetSliceOffset(sliceNode.GetSliceOffset() + offset) + slicer.app.processEvents() + endTime = time.time() + renderingTimesSec[sampleIndex] = (endTime - startTime) + sampleIndex += 1 + sliceNode.SetSliceOffset(startOffset) + + resultTableName = slicer.mrmlScene.GetUniqueNameByString("Reslice performance") + resultTableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", resultTableName) + slicer.util.updateTableFromArray(resultTableNode, renderingTimesSec, "Rendering time [s]") + + renderingTimeMean = np.mean(renderingTimesSec) + renderingTimeStd = np.std(renderingTimesSec) + result = ("%d x %d, fps = %.1f (%.1f +/- %.2f ms per frame) - see details in table '%s'" + % (dims[0], dims[1], 1.0 / renderingTimeMean, 1000. * renderingTimeMean, 1000. * renderingTimeStd, resultTableNode.GetName())) + print(result) + self.log.insertHtml('%s' % result) + self.log.insertPlainText('\n') + self.log.ensureCursorVisible() + self.log.repaint() + + def crosshairJump(self, iters=15): + """ go into a loop that stresses jumping to slices by moving crosshair + """ + import time + sliceNode = slicer.util.getNode('vtkMRMLSliceNodeRed') + dims = sliceNode.GetDimensions() + layoutManager = slicer.app.layoutManager() + sliceViewNames = layoutManager.sliceViewNames() + # Order of slice view names is random, prefer 'Red' slice to make results more predictable + firstSliceViewName = 'Red' if 'Red' in sliceViewNames else sliceViewNames[0] + firstSliceWidget = layoutManager.sliceWidget(firstSliceViewName) + elapsedTime = 0 + startPoint = (int(dims[0] * 0.3), int(dims[1] * 0.3)) + endPoint = (int(dims[0] * 0.6), int(dims[1] * 0.6)) + for i in range(iters): + startTime = time.time() + slicer.util.clickAndDrag(firstSliceWidget, button=None, modifiers=['Shift'], start=startPoint, end=endPoint, steps=2) + slicer.app.processEvents() + endTime1 = time.time() + slicer.util.clickAndDrag(firstSliceWidget, button=None, modifiers=['Shift'], start=endPoint, end=startPoint, steps=2) + slicer.app.processEvents() + endTime2 = time.time() + delta = ((endTime1 - startTime) + (endTime2 - endTime1)) / 2. + elapsedTime += delta + fps = int(iters / elapsedTime) + result = "number of slice views = %d, fps = %g (%g ms per frame)" % (len(sliceViewNames), fps, 1000. / fps) + print(result) + self.log.insertHtml('%s' % result) + self.log.insertPlainText('\n') + self.log.ensureCursorVisible() + self.log.repaint() + + def webViewCallback(self, qurl): + url = qurl.toString() + print(url) + if url == 'reslicing': + self.reslicing() + if url == 'chart': + self.chartTest() + pass + + def webViewTest(self): + self.webView = qt.QWebView() + html = """ Run reslicing test

          Run chart test """ - self.webView.setHtml(html) - self.webView.settings().setAttribute(qt.QWebSettings.DeveloperExtrasEnabled, True) - self.webView.page().setLinkDelegationPolicy(qt.QWebPage.DelegateAllLinks) - self.webView.connect('linkClicked(QUrl)', self.webViewCallback) - self.webView.show() - - def webViewFormTest(self): - """Just as a demo, load a google search in a web view - and use the qt api to fill in a search term""" - self.webView = qt.QWebView() - self.webView.settings().setAttribute(qt.QWebSettings.DeveloperExtrasEnabled, True) - self.webView.connect('loadFinished(bool)', self.webViewFormLoadedCallback) - self.webView.show() - u = qt.QUrl('https://www.google.com') - self.webView.setUrl(u) - - def webViewFormLoadedCallback(self,ok): - if not ok: - print('page did not load') - return - page = self.webView.page() - frame = page.mainFrame() - document = frame.documentElement() - element = document.findFirst('.lst') - element.setAttribute("value", "where can I learn more about this 3D Slicer program?") - - def memoryCallback(self): - if self.sysInfoWindow.visible: - self.sysInfo.RunMemoryCheck() - self.sysInfoWindow.append('p: %d of %d, v: %d of %d' % - (self.sysInfo.GetAvailablePhysicalMemory(), - self.sysInfo.GetTotalPhysicalMemory(), - self.sysInfo.GetAvailableVirtualMemory(), - self.sysInfo.GetTotalVirtualMemory(), - )) - qt.QTimer.singleShot(1000,self.memoryCallback) - - def memoryCheck(self): - """Run a periodic memory check in a window""" - if not hasattr(self,'sysInfo'): - self.sysInfo = slicer.vtkSystemInformation() - self.sysInfoWindow = qt.QTextBrowser() - if self.sysInfoWindow.visible: - return - self.sysInfoWindow.show() - self.memoryCallback() + self.webView.setHtml(html) + self.webView.settings().setAttribute(qt.QWebSettings.DeveloperExtrasEnabled, True) + self.webView.page().setLinkDelegationPolicy(qt.QWebPage.DelegateAllLinks) + self.webView.connect('linkClicked(QUrl)', self.webViewCallback) + self.webView.show() + + def webViewFormTest(self): + """Just as a demo, load a google search in a web view + and use the qt api to fill in a search term""" + self.webView = qt.QWebView() + self.webView.settings().setAttribute(qt.QWebSettings.DeveloperExtrasEnabled, True) + self.webView.connect('loadFinished(bool)', self.webViewFormLoadedCallback) + self.webView.show() + u = qt.QUrl('https://www.google.com') + self.webView.setUrl(u) + + def webViewFormLoadedCallback(self, ok): + if not ok: + print('page did not load') + return + page = self.webView.page() + frame = page.mainFrame() + document = frame.documentElement() + element = document.findFirst('.lst') + element.setAttribute("value", "where can I learn more about this 3D Slicer program?") + + def memoryCallback(self): + if self.sysInfoWindow.visible: + self.sysInfo.RunMemoryCheck() + self.sysInfoWindow.append('p: %d of %d, v: %d of %d' % + (self.sysInfo.GetAvailablePhysicalMemory(), + self.sysInfo.GetTotalPhysicalMemory(), + self.sysInfo.GetAvailableVirtualMemory(), + self.sysInfo.GetTotalVirtualMemory(), + )) + qt.QTimer.singleShot(1000, self.memoryCallback) + + def memoryCheck(self): + """Run a periodic memory check in a window""" + if not hasattr(self, 'sysInfo'): + self.sysInfo = slicer.vtkSystemInformation() + self.sysInfoWindow = qt.QTextBrowser() + if self.sysInfoWindow.visible: + return + self.sysInfoWindow.show() + self.memoryCallback() class sliceLogicTest: - def __init__(self): - self.step = 0 - self.sliceLogic = slicer.vtkMRMLSliceLayerLogic() - self.sliceLogic.SetMRMLScene(slicer.mrmlScene) - self.sliceNode = slicer.vtkMRMLSliceNode() - self.sliceNode.SetLayoutName("Black") - slicer.mrmlScene.AddNode(self.sliceNode) - self.sliceLogic.SetSliceNode(self.sliceNode) - - def stepSliceLogic(self): - self.sliceNode.SetSliceOffset( -1*self.step*10) - self.step = 1^self.step - - def testSliceLogic(self, iters): - timeSteps(iters, self.stepSliceLogic) + def __init__(self): + self.step = 0 + self.sliceLogic = slicer.vtkMRMLSliceLayerLogic() + self.sliceLogic.SetMRMLScene(slicer.mrmlScene) + self.sliceNode = slicer.vtkMRMLSliceNode() + self.sliceNode.SetLayoutName("Black") + slicer.mrmlScene.AddNode(self.sliceNode) + self.sliceLogic.SetSliceNode(self.sliceNode) + + def stepSliceLogic(self): + self.sliceNode.SetSliceOffset(-1 * self.step * 10) + self.step = 1 ^ self.step + + def testSliceLogic(self, iters): + timeSteps(iters, self.stepSliceLogic) diff --git a/Modules/Scripted/SampleData/SampleData.py b/Modules/Scripted/SampleData/SampleData.py index 383996f431c..dd108364b7d 100644 --- a/Modules/Scripted/SampleData/SampleData.py +++ b/Modules/Scripted/SampleData/SampleData.py @@ -16,46 +16,46 @@ # def downloadFromURL(uris=None, fileNames=None, nodeNames=None, checksums=None, loadFiles=None, - customDownloader=None, loadFileTypes=None, loadFileProperties={}): - """Download and optionally load data into the application. + customDownloader=None, loadFileTypes=None, loadFileProperties={}): + """Download and optionally load data into the application. - :param uris: Download URL(s). - :param fileNames: File name(s) that will be downloaded (and loaded). - :param nodeNames: Node name(s) in the scene. - :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. - :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides. - :param customDownloader: Custom function for downloading. - :param loadFileTypes: file format name(s) ('VolumeFile' by default). - :param loadFileProperties: custom properties passed to the IO plugin. + :param uris: Download URL(s). + :param fileNames: File name(s) that will be downloaded (and loaded). + :param nodeNames: Node name(s) in the scene. + :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. + :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides. + :param customDownloader: Custom function for downloading. + :param loadFileTypes: file format name(s) ('VolumeFile' by default). + :param loadFileProperties: custom properties passed to the IO plugin. - If the given ``fileNames`` are not found in the application cache directory, they - are downloaded using the associated URIs. - See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()`` + If the given ``fileNames`` are not found in the application cache directory, they + are downloaded using the associated URIs. + See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()`` - If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are - guessed based on the corresponding filename extensions. + If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are + guessed based on the corresponding filename extensions. - If a given fileName has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded - by default. To ensure the file is loaded, ``loadFiles`` must be set. + If a given fileName has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded + by default. To ensure the file is loaded, ``loadFiles`` must be set. - The ``loadFileProperties`` are common for all files. If different properties - need to be associated with files of different types, downloadFromURL must - be called for each. - """ - return SampleDataLogic().downloadFromURL( - uris, fileNames, nodeNames, checksums, loadFiles, customDownloader, loadFileTypes, loadFileProperties) + The ``loadFileProperties`` are common for all files. If different properties + need to be associated with files of different types, downloadFromURL must + be called for each. + """ + return SampleDataLogic().downloadFromURL( + uris, fileNames, nodeNames, checksums, loadFiles, customDownloader, loadFileTypes, loadFileProperties) def downloadSample(sampleName): - """For a given sample name this will search the available sources - and load it if it is available. Returns the first loaded node.""" - return SampleDataLogic().downloadSamples(sampleName)[0] + """For a given sample name this will search the available sources + and load it if it is available. Returns the first loaded node.""" + return SampleDataLogic().downloadSamples(sampleName)[0] def downloadSamples(sampleName): - """For a given sample name this will search the available sources - and load it if it is available. Returns the loaded nodes.""" - return SampleDataLogic().downloadSamples(sampleName) + """For a given sample name this will search the available sources + and load it if it is available. Returns the loaded nodes.""" + return SampleDataLogic().downloadSamples(sampleName) # @@ -63,21 +63,21 @@ def downloadSamples(sampleName): # class SampleData(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 = "Sample Data" - self.parent.categories = ["Informatics"] - self.parent.dependencies = [] - self.parent.contributors = ["Steve Pieper (Isomics), Benjamin Long (Kitware), Jean-Christophe Fillion-Robin (Kitware)"] - self.parent.helpText = """ + """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 = "Sample Data" + self.parent.categories = ["Informatics"] + self.parent.dependencies = [] + self.parent.contributors = ["Steve Pieper (Isomics), Benjamin Long (Kitware), Jean-Christophe Fillion-Robin (Kitware)"] + self.parent.helpText = """ The SampleData module can be used to download data for working with in slicer. Use of this module requires an active network connection. """ - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = """ + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = """

          This work was was funded by Cancer Care Ontario and the Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO)

          @@ -91,154 +91,154 @@ def __init__(self, parent): use it for commercial purposes.

          """ - if slicer.mrmlScene.GetTagByClassName( "vtkMRMLScriptedModuleNode" ) != 'ScriptedModule': - slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode()) + if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != 'ScriptedModule': + slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode()) - # Trigger the menu to be added when application has started up - if not slicer.app.commandOptions().noMainWindow : - slicer.app.connect("startupCompleted()", self.addMenu) + # Trigger the menu to be added when application has started up + if not slicer.app.commandOptions().noMainWindow: + slicer.app.connect("startupCompleted()", self.addMenu) - # allow other modules to register sample data sources by appending - # instances or subclasses SampleDataSource objects on this list - try: - slicer.modules.sampleDataSources - except AttributeError: - slicer.modules.sampleDataSources = {} + # allow other modules to register sample data sources by appending + # instances or subclasses SampleDataSource objects on this list + try: + slicer.modules.sampleDataSources + except AttributeError: + slicer.modules.sampleDataSources = {} - def addMenu(self): - a = qt.QAction('Download Sample Data', slicer.util.mainWindow()) - a.setToolTip('Go to the SampleData module to download data from the network') - a.connect('triggered()', self.select) + def addMenu(self): + a = qt.QAction('Download Sample Data', slicer.util.mainWindow()) + a.setToolTip('Go to the SampleData module to download data from the network') + a.connect('triggered()', self.select) - fileMenu = slicer.util.lookupTopLevelWidget('FileMenu') - if fileMenu: - for action in fileMenu.actions(): - if action.objectName == "FileSaveSceneAction": - fileMenu.insertAction(action, a) - fileMenu.insertSeparator(action) + fileMenu = slicer.util.lookupTopLevelWidget('FileMenu') + if fileMenu: + for action in fileMenu.actions(): + if action.objectName == "FileSaveSceneAction": + fileMenu.insertAction(action, a) + fileMenu.insertSeparator(action) - def select(self): - m = slicer.util.mainWindow() - m.moduleSelector().selectModule('SampleData') + def select(self): + m = slicer.util.mainWindow() + m.moduleSelector().selectModule('SampleData') # # SampleDataSource # class SampleDataSource: - """Describe a set of sample data associated with one or multiple URIs and filenames. - - Example:: - - import SampleData - from slicer.util import TESTING_DATA_URL - dataSource = SampleData.SampleDataSource( - nodeNames='fixed', - fileNames='fixed.nrrd', - uris=TESTING_DATA_URL + 'SHA256/b757f9c61c1b939f104e5d7861130bb28d90f33267a012eb8bb763a435f29d37') - loadedNode = SampleData.SampleDataLogic().downloadFromSource(dataSource)[0] - """ - - def __init__(self, sampleName=None, sampleDescription=None, uris=None, fileNames=None, nodeNames=None, - checksums=None, loadFiles=None, - customDownloader=None, thumbnailFileName=None, - loadFileType=None, loadFileProperties=None): - """ - :param sampleName: Name identifying the data set. - :param sampleDescription: Displayed name of data set in SampleData module GUI. (default is ``sampleName``) - :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI, - :param uris: Download URL(s). - :param fileNames: File name(s) that will be downloaded (and loaded). - :param nodeNames: Node name(s) in the scene. - :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. - :param loadFiles: Boolean indicating if file(s) should be loaded. - :param customDownloader: Custom function for downloading. - :param loadFileType: file format name(s) ('VolumeFile' by default if node name is specified). - :param loadFileProperties: custom properties passed to the IO plugin. + """Describe a set of sample data associated with one or multiple URIs and filenames. + + Example:: + + import SampleData + from slicer.util import TESTING_DATA_URL + dataSource = SampleData.SampleDataSource( + nodeNames='fixed', + fileNames='fixed.nrrd', + uris=TESTING_DATA_URL + 'SHA256/b757f9c61c1b939f104e5d7861130bb28d90f33267a012eb8bb763a435f29d37') + loadedNode = SampleData.SampleDataLogic().downloadFromSource(dataSource)[0] """ - self.sampleName = sampleName - if sampleDescription is None: - sampleDescription = sampleName - self.sampleDescription = sampleDescription - if (isinstance(uris, list) or isinstance(uris, tuple)): - if isinstance(loadFileType, str) or loadFileType is None: - loadFileType = [loadFileType] * len(uris) - if nodeNames is None: - nodeNames = [None] * len(uris) - if loadFiles is None: - loadFiles = [None] * len(uris) - if checksums is None: - checksums = [None] * len(uris) - elif isinstance(uris, str): - uris = [uris,] - fileNames = [fileNames,] - nodeNames = [nodeNames,] - loadFiles = [loadFiles,] - loadFileType = [loadFileType,] - checksums = [checksums,] - - updatedFileType = [] - for fileName, nodeName, fileType in zip(fileNames, nodeNames, loadFileType): - # If not explicitly specified, attempt to guess fileType - if fileType is None: - if nodeName is not None: - # TODO: Use method from Slicer IO logic ? - fileType = "VolumeFile" - else: - ext = os.path.splitext(fileName.lower())[1] - if ext in [".mrml", ".mrb"]: - fileType = "SceneFile" - elif ext in [".zip"]: - fileType = "ZipFile" - updatedFileType.append(fileType) - - if loadFileProperties is None: - loadFileProperties = {} - - self.uris = uris - self.fileNames = fileNames - self.nodeNames = nodeNames - self.loadFiles = loadFiles - self.customDownloader = customDownloader - self.thumbnailFileName = thumbnailFileName - self.loadFileType = updatedFileType - self.loadFileProperties = loadFileProperties - self.checksums = checksums - if not len(uris) == len(fileNames) == len(nodeNames) == len(loadFiles) == len(updatedFileType) == len(checksums): - raise ValueError( - f"All fields of sample data source must have the same length\n" - f" uris : {uris}\n" - f" len(uris) : {len(uris)}\n" - f" len(fileNames) : {len(fileNames)}\n" - f" len(nodeNames) : {len(nodeNames)}\n" - f" len(loadFiles) : {len(loadFiles)}\n" - f" len(updatedFileType) : {len(updatedFileType)}\n" - f" len(checksums) : {len(checksums)}\n" - ) - def __eq__(self, other): - return str(self) == str(other) - - def __str__(self): - output = [ - "sampleName : %s" % self.sampleName, - "sampleDescription : %s" % self.sampleDescription, - "thumbnailFileName : %s" % self.thumbnailFileName, - "loadFileProperties: %s" % self.loadFileProperties, - "customDownloader : %s" % self.customDownloader, - "" - ] - for fileName, uri, nodeName, loadFile, fileType, checksum in zip(self.fileNames, self.uris, self.nodeNames, self.loadFiles, self.loadFileType, self.checksums): - output.extend([ - " fileName : %s" % fileName, - " uri : %s" % uri, - " checksum : %s" % checksum, - " nodeName : %s" % nodeName, - " loadFile : %s" % loadFile, - " loadFileType: %s" % fileType, - "" - ]) - return "\n".join(output) + def __init__(self, sampleName=None, sampleDescription=None, uris=None, fileNames=None, nodeNames=None, + checksums=None, loadFiles=None, + customDownloader=None, thumbnailFileName=None, + loadFileType=None, loadFileProperties=None): + """ + :param sampleName: Name identifying the data set. + :param sampleDescription: Displayed name of data set in SampleData module GUI. (default is ``sampleName``) + :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI, + :param uris: Download URL(s). + :param fileNames: File name(s) that will be downloaded (and loaded). + :param nodeNames: Node name(s) in the scene. + :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. + :param loadFiles: Boolean indicating if file(s) should be loaded. + :param customDownloader: Custom function for downloading. + :param loadFileType: file format name(s) ('VolumeFile' by default if node name is specified). + :param loadFileProperties: custom properties passed to the IO plugin. + """ + self.sampleName = sampleName + if sampleDescription is None: + sampleDescription = sampleName + self.sampleDescription = sampleDescription + if (isinstance(uris, list) or isinstance(uris, tuple)): + if isinstance(loadFileType, str) or loadFileType is None: + loadFileType = [loadFileType] * len(uris) + if nodeNames is None: + nodeNames = [None] * len(uris) + if loadFiles is None: + loadFiles = [None] * len(uris) + if checksums is None: + checksums = [None] * len(uris) + elif isinstance(uris, str): + uris = [uris, ] + fileNames = [fileNames, ] + nodeNames = [nodeNames, ] + loadFiles = [loadFiles, ] + loadFileType = [loadFileType, ] + checksums = [checksums, ] + + updatedFileType = [] + for fileName, nodeName, fileType in zip(fileNames, nodeNames, loadFileType): + # If not explicitly specified, attempt to guess fileType + if fileType is None: + if nodeName is not None: + # TODO: Use method from Slicer IO logic ? + fileType = "VolumeFile" + else: + ext = os.path.splitext(fileName.lower())[1] + if ext in [".mrml", ".mrb"]: + fileType = "SceneFile" + elif ext in [".zip"]: + fileType = "ZipFile" + updatedFileType.append(fileType) + + if loadFileProperties is None: + loadFileProperties = {} + + self.uris = uris + self.fileNames = fileNames + self.nodeNames = nodeNames + self.loadFiles = loadFiles + self.customDownloader = customDownloader + self.thumbnailFileName = thumbnailFileName + self.loadFileType = updatedFileType + self.loadFileProperties = loadFileProperties + self.checksums = checksums + if not len(uris) == len(fileNames) == len(nodeNames) == len(loadFiles) == len(updatedFileType) == len(checksums): + raise ValueError( + f"All fields of sample data source must have the same length\n" + f" uris : {uris}\n" + f" len(uris) : {len(uris)}\n" + f" len(fileNames) : {len(fileNames)}\n" + f" len(nodeNames) : {len(nodeNames)}\n" + f" len(loadFiles) : {len(loadFiles)}\n" + f" len(updatedFileType) : {len(updatedFileType)}\n" + f" len(checksums) : {len(checksums)}\n" + ) + + def __eq__(self, other): + return str(self) == str(other) + + def __str__(self): + output = [ + "sampleName : %s" % self.sampleName, + "sampleDescription : %s" % self.sampleDescription, + "thumbnailFileName : %s" % self.thumbnailFileName, + "loadFileProperties: %s" % self.loadFileProperties, + "customDownloader : %s" % self.customDownloader, + "" + ] + for fileName, uri, nodeName, loadFile, fileType, checksum in zip(self.fileNames, self.uris, self.nodeNames, self.loadFiles, self.loadFileType, self.checksums): + output.extend([ + " fileName : %s" % fileName, + " uri : %s" % uri, + " checksum : %s" % checksum, + " nodeName : %s" % nodeName, + " loadFile : %s" % loadFile, + " loadFileType: %s" % fileType, + "" + ]) + return "\n".join(output) # @@ -246,160 +246,160 @@ def __str__(self): # class SampleDataWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - # This module is often used in developer mode, therefore - # collapse reload & test section by default. - if hasattr(self, "reloadCollapsibleButton"): - self.reloadCollapsibleButton.collapsed = True - - self.logic = SampleDataLogic(self.logMessage) - - self.categoryLayout = qt.QVBoxLayout() - self.categoryLayout.setContentsMargins(0, 0, 0, 0) - self.layout.addLayout(self.categoryLayout) - - SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, slicer.modules.sampleDataSources, self.logic) - if self.developerMode is False: - self.setCategoryVisible(self.logic.developmentCategoryName, False) - - self.log = qt.QTextEdit() - self.log.readOnly = True - self.layout.addWidget(self.log) - self.logMessage('

          Status: Idle

          ') - - # Add spacer to layout - self.layout.addStretch(1) - - def cleanup(self): - SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, {}, self.logic) - - @staticmethod - def removeCategories(categoryLayout): - """Remove all categories from the given category layout. - """ - while categoryLayout.count() > 0: - frame = categoryLayout.itemAt(0).widget() - frame.visible = False - categoryLayout.removeWidget(frame) - frame.setParent(0) - del frame - - @staticmethod - def setCategoriesFromSampleDataSources(categoryLayout, dataSources, logic): - """Update categoryLayout adding buttons for downloading dataSources. - - Download buttons are organized in collapsible GroupBox with one GroupBox - per category. + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - iconPath = os.path.join(os.path.dirname(__file__).replace('\\','/'), 'Resources','Icons') - mainWindow = slicer.util.mainWindow() - if mainWindow: - iconSize = qt.QSize(int(mainWindow.width/8),int(mainWindow.height/6)) - else: - # There is no main window in the automated tests - desktop = qt.QDesktopWidget() - mainScreenSize = desktop.availableGeometry(desktop.primaryScreen) - iconSize = qt.QSize(int(mainScreenSize.width()/15),int(mainScreenSize.height()/10)) - - categories = sorted(dataSources.keys()) - - # Ensure "builtIn" catergory is always first - if logic.builtInCategoryName in categories: - categories.remove(logic.builtInCategoryName) - categories.insert(0, logic.builtInCategoryName) - - # Clear category layout - SampleDataWidget.removeCategories(categoryLayout) - - # Populate category layout - for category in categories: - frame = ctk.ctkCollapsibleGroupBox(categoryLayout.parentWidget()) - categoryLayout.addWidget(frame) - frame.title = category - frame.name = '%sCollapsibleGroupBox' % category - layout = ctk.ctkFlowLayout() - layout.preferredExpandingDirections = qt.Qt.Vertical - frame.setLayout(layout) - for source in dataSources[category]: - name = source.sampleDescription - if not name: - name = source.nodeNames[0] - - b = qt.QToolButton() - b.setText(name) - - # Set thumbnail - if source.thumbnailFileName: - # Thumbnail provided - thumbnailImage = source.thumbnailFileName - else: - # Look for thumbnail image with the name of any node name with .png extension - thumbnailImage = None - for nodeName in source.nodeNames: - if not nodeName: - continue - thumbnailImageAttempt = os.path.join(iconPath, nodeName+'.png') - if os.path.exists(thumbnailImageAttempt): - thumbnailImage = thumbnailImageAttempt - break - if thumbnailImage and os.path.exists(thumbnailImage): - b.setIcon(qt.QIcon(thumbnailImage)) - - b.setIconSize(iconSize) - b.setToolButtonStyle(qt.Qt.ToolButtonTextUnderIcon) - qSize = qt.QSizePolicy() - qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding) - b.setSizePolicy(qSize) - - b.name = '%sPushButton' % name - layout.addWidget(b) - if source.customDownloader: - b.connect('clicked()', lambda s=source: s.customDownloader(s)) - else: - b.connect('clicked()', lambda s=source: logic.downloadFromSource(s)) - - def logMessage(self, message, logLevel=logging.DEBUG): - # Set text color based on log level - if logLevel >= logging.ERROR: - message = '' + message + '' - elif logLevel >= logging.WARNING: - message = '' + message + '' - # Show message in status bar - doc = qt.QTextDocument() - doc.setHtml(message) - slicer.util.showStatusMessage(doc.toPlainText(),3000) - # Show message in log window at the bottom of the module widget - self.log.insertHtml(message) - self.log.insertPlainText('\n') - self.log.ensureCursorVisible() - self.log.repaint() - logging.log(logLevel, message) - slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents) - - def isCategoryVisible(self, category): - """Check the visibility of a SampleData category given its name. - - Returns False if the category is not visible or if it does not exist, - otherwise returns True. - """ - if not SampleDataLogic.sampleDataSourcesByCategory(category): - return False - return slicer.util.findChild(self.parent, '%sCollapsibleGroupBox' % category).isVisible() - - def setCategoryVisible(self, category, visible): - """Update visibility of a SampleData category given its name. - The function is a no-op if the category does not exist. - """ - if not SampleDataLogic.sampleDataSourcesByCategory(category): - return - slicer.util.findChild(self.parent, '%sCollapsibleGroupBox' % category).setVisible(visible) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + # This module is often used in developer mode, therefore + # collapse reload & test section by default. + if hasattr(self, "reloadCollapsibleButton"): + self.reloadCollapsibleButton.collapsed = True + + self.logic = SampleDataLogic(self.logMessage) + + self.categoryLayout = qt.QVBoxLayout() + self.categoryLayout.setContentsMargins(0, 0, 0, 0) + self.layout.addLayout(self.categoryLayout) + + SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, slicer.modules.sampleDataSources, self.logic) + if self.developerMode is False: + self.setCategoryVisible(self.logic.developmentCategoryName, False) + + self.log = qt.QTextEdit() + self.log.readOnly = True + self.layout.addWidget(self.log) + self.logMessage('

          Status: Idle

          ') + + # Add spacer to layout + self.layout.addStretch(1) + + def cleanup(self): + SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, {}, self.logic) + + @staticmethod + def removeCategories(categoryLayout): + """Remove all categories from the given category layout. + """ + while categoryLayout.count() > 0: + frame = categoryLayout.itemAt(0).widget() + frame.visible = False + categoryLayout.removeWidget(frame) + frame.setParent(0) + del frame + + @staticmethod + def setCategoriesFromSampleDataSources(categoryLayout, dataSources, logic): + """Update categoryLayout adding buttons for downloading dataSources. + + Download buttons are organized in collapsible GroupBox with one GroupBox + per category. + """ + iconPath = os.path.join(os.path.dirname(__file__).replace('\\', '/'), 'Resources', 'Icons') + mainWindow = slicer.util.mainWindow() + if mainWindow: + iconSize = qt.QSize(int(mainWindow.width / 8), int(mainWindow.height / 6)) + else: + # There is no main window in the automated tests + desktop = qt.QDesktopWidget() + mainScreenSize = desktop.availableGeometry(desktop.primaryScreen) + iconSize = qt.QSize(int(mainScreenSize.width() / 15), int(mainScreenSize.height() / 10)) + + categories = sorted(dataSources.keys()) + + # Ensure "builtIn" catergory is always first + if logic.builtInCategoryName in categories: + categories.remove(logic.builtInCategoryName) + categories.insert(0, logic.builtInCategoryName) + + # Clear category layout + SampleDataWidget.removeCategories(categoryLayout) + + # Populate category layout + for category in categories: + frame = ctk.ctkCollapsibleGroupBox(categoryLayout.parentWidget()) + categoryLayout.addWidget(frame) + frame.title = category + frame.name = '%sCollapsibleGroupBox' % category + layout = ctk.ctkFlowLayout() + layout.preferredExpandingDirections = qt.Qt.Vertical + frame.setLayout(layout) + for source in dataSources[category]: + name = source.sampleDescription + if not name: + name = source.nodeNames[0] + + b = qt.QToolButton() + b.setText(name) + + # Set thumbnail + if source.thumbnailFileName: + # Thumbnail provided + thumbnailImage = source.thumbnailFileName + else: + # Look for thumbnail image with the name of any node name with .png extension + thumbnailImage = None + for nodeName in source.nodeNames: + if not nodeName: + continue + thumbnailImageAttempt = os.path.join(iconPath, nodeName + '.png') + if os.path.exists(thumbnailImageAttempt): + thumbnailImage = thumbnailImageAttempt + break + if thumbnailImage and os.path.exists(thumbnailImage): + b.setIcon(qt.QIcon(thumbnailImage)) + + b.setIconSize(iconSize) + b.setToolButtonStyle(qt.Qt.ToolButtonTextUnderIcon) + qSize = qt.QSizePolicy() + qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding) + b.setSizePolicy(qSize) + + b.name = '%sPushButton' % name + layout.addWidget(b) + if source.customDownloader: + b.connect('clicked()', lambda s=source: s.customDownloader(s)) + else: + b.connect('clicked()', lambda s=source: logic.downloadFromSource(s)) + + def logMessage(self, message, logLevel=logging.DEBUG): + # Set text color based on log level + if logLevel >= logging.ERROR: + message = '' + message + '' + elif logLevel >= logging.WARNING: + message = '' + message + '' + # Show message in status bar + doc = qt.QTextDocument() + doc.setHtml(message) + slicer.util.showStatusMessage(doc.toPlainText(), 3000) + # Show message in log window at the bottom of the module widget + self.log.insertHtml(message) + self.log.insertPlainText('\n') + self.log.ensureCursorVisible() + self.log.repaint() + logging.log(logLevel, message) + slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents) + + def isCategoryVisible(self, category): + """Check the visibility of a SampleData category given its name. + + Returns False if the category is not visible or if it does not exist, + otherwise returns True. + """ + if not SampleDataLogic.sampleDataSourcesByCategory(category): + return False + return slicer.util.findChild(self.parent, '%sCollapsibleGroupBox' % category).isVisible() + + def setCategoryVisible(self, category, visible): + """Update visibility of a SampleData category given its name. + + The function is a no-op if the category does not exist. + """ + if not SampleDataLogic.sampleDataSourcesByCategory(category): + return + slicer.util.findChild(self.parent, '%sCollapsibleGroupBox' % category).setVisible(visible) # @@ -407,770 +407,770 @@ def setCategoryVisible(self, category, visible): # class SampleDataLogic: - """Manage the slicer.modules.sampleDataSources dictionary. - The dictionary keys are categories of sample data sources. - The BuiltIn category is managed here. Modules or extensions can - register their own sample data by creating instances of the - SampleDataSource class. These instances should be stored in a - list that is assigned to a category following the model - used in registerBuiltInSampleDataSources below. - - Checksums are expected to be formatted as a string of the form - ``:``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. - """ - - @staticmethod - def registerCustomSampleDataSource(category='Custom', - sampleName=None, uris=None, fileNames=None, nodeNames=None, - customDownloader=None, thumbnailFileName=None, - loadFileType='VolumeFile', loadFiles=None, loadFileProperties={}, - checksums=None): - """Adds custom data sets to SampleData. - :param category: Section title of data set in SampleData module GUI. - :param sampleName: Displayed name of data set in SampleData module GUI. - :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI, - :param uris: Download URL(s). - :param fileNames: File name(s) that will be loaded. - :param nodeNames: Node name(s) in the scene. - :param customDownloader: Custom function for downloading. - :param loadFileType: file format name(s) ('VolumeFile' by default). - :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides. - :param loadFileProperties: custom properties passed to the IO plugin. - :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. - """ - - try: - slicer.modules.sampleDataSources - except AttributeError: - slicer.modules.sampleDataSources = {} - - if category not in slicer.modules.sampleDataSources: - slicer.modules.sampleDataSources[category] = [] - - dataSource = SampleDataSource( - sampleName=sampleName, - uris=uris, - fileNames=fileNames, - nodeNames=nodeNames, - thumbnailFileName=thumbnailFileName, - loadFileType=loadFileType, - loadFiles=loadFiles, - loadFileProperties=loadFileProperties, - checksums=checksums, - customDownloader=customDownloader, - ) - - if SampleDataLogic.isSampleDataSourceRegistered(category, dataSource): - return - - slicer.modules.sampleDataSources[category].append(dataSource) - - @staticmethod - def sampleDataSourcesByCategory(category=None): - """Return the registered SampleDataSources for with the given category. - - If no category is specified, returns all registered SampleDataSources. - """ - try: - slicer.modules.sampleDataSources - except AttributeError: - slicer.modules.sampleDataSources = {} - - if category is None: - return slicer.modules.sampleDataSources - else: - return slicer.modules.sampleDataSources.get(category, []) - - @staticmethod - def isSampleDataSourceRegistered(category, sampleDataSource): - """Returns True if the sampleDataSource is registered with the category. + """Manage the slicer.modules.sampleDataSources dictionary. + The dictionary keys are categories of sample data sources. + The BuiltIn category is managed here. Modules or extensions can + register their own sample data by creating instances of the + SampleDataSource class. These instances should be stored in a + list that is assigned to a category following the model + used in registerBuiltInSampleDataSources below. + + Checksums are expected to be formatted as a string of the form + ``:``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. """ - try: - slicer.modules.sampleDataSources - except AttributeError: - slicer.modules.sampleDataSources = {} - - if not isinstance(sampleDataSource, SampleDataSource): - raise TypeError(f"unsupported sampleDataSource type '{type(sampleDataSource)}': '{str(SampleDataSource)}' is expected") - - return sampleDataSource in slicer.modules.sampleDataSources.get(category, []) - - def __init__(self, logMessage=None): - if logMessage: - self.logMessage = logMessage - self.builtInCategoryName = 'BuiltIn' - self.developmentCategoryName = 'Development' - self.registerBuiltInSampleDataSources() - self.registerDevelopmentSampleDataSources() - if slicer.app.testingEnabled(): - self.registerTestingDataSources() - self.downloadPercent = 0 - - def registerBuiltInSampleDataSources(self): - """Fills in the pre-define sample data sources""" - - # Arguments: - # sampleName=None, sampleDescription=None, - # uris=None, - # fileNames=None, nodeNames=None, - # checksums=None, - # loadFiles=None, customDownloader=None, thumbnailFileName=None, loadFileType=None, loadFileProperties=None - sourceArguments = ( - ('MRHead', None, TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', - 'MR-head.nrrd', 'MRHead', 'SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93'), - ('CTChest', None, TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e', - 'CT-chest.nrrd', 'CTChest', 'SHA256:4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'), - ('CTACardio', None, TESTING_DATA_URL + 'SHA256/3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2', - 'CTA-cardio.nrrd', 'CTACardio', 'SHA256:3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2'), - ('DTIBrain', None, TESTING_DATA_URL + 'SHA256/5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a', - 'DTI-Brain.nrrd', 'DTIBrain', 'SHA256:5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a'), - ('MRBrainTumor1', None, TESTING_DATA_URL + 'SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95', - 'RegLib_C01_1.nrrd', 'MRBrainTumor1', 'SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95'), - ('MRBrainTumor2', None, TESTING_DATA_URL + 'SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97', - 'RegLib_C01_2.nrrd', 'MRBrainTumor2', 'SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97'), - ('BaselineVolume', None, TESTING_DATA_URL + 'SHA256/dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2', - 'BaselineVolume.nrrd', 'BaselineVolume', 'SHA256:dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2'), - ('DTIVolume', None, - (TESTING_DATA_URL + 'SHA256/d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d', - TESTING_DATA_URL + 'SHA256/67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe', ), - ('DTIVolume.raw.gz', 'DTIVolume.nhdr'), (None, 'DTIVolume'), - ('SHA256:d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d', - 'SHA256:67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe')), - ('DWIVolume', None, - (TESTING_DATA_URL + 'SHA256/cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692', - TESTING_DATA_URL + 'SHA256/7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed'), - ('dwi.raw.gz', 'dwi.nhdr'), (None, 'dwi'), - ('SHA256:cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692', - 'SHA256:7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed')), - ('CTAAbdomenPanoramix', 'CTA abdomen\n(Panoramix)', TESTING_DATA_URL + 'SHA256/146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433', - 'Panoramix-cropped.nrrd', 'Panoramix-cropped', 'SHA256:146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433'), - ('CBCTDentalSurgery', None, - (TESTING_DATA_URL + 'SHA256/7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968', - TESTING_DATA_URL + 'SHA256/4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd',), - ('PreDentalSurgery.gipl.gz', 'PostDentalSurgery.gipl.gz'), ('PreDentalSurgery', 'PostDentalSurgery'), - ('SHA256:7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968', - 'SHA256:4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd')), - ('MRUSProstate', 'MR-US Prostate', - (TESTING_DATA_URL + 'SHA256/4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa', - TESTING_DATA_URL + 'SHA256/34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2',), - ('Case10-MR.nrrd', 'case10_US_resampled.nrrd'), ('MRProstate', 'USProstate'), - ('SHA256:4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa', - 'SHA256:34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2')), - ('CTMRBrain', 'CT-MR Brain', - (TESTING_DATA_URL + 'SHA256/6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1', - TESTING_DATA_URL + 'SHA256/2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593', - TESTING_DATA_URL + 'SHA256/fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7',), - ('CT-brain.nrrd', 'MR-brain-T1.nrrd', 'MR-brain-T2.nrrd'), - ('CTBrain', 'MRBrainT1', 'MRBrainT2'), - ('SHA256:6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1', - 'SHA256:2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593', - 'SHA256:fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7')), - ('CBCTMRHead', 'CBCT-MR Head', - (TESTING_DATA_URL + 'SHA256/4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db', - TESTING_DATA_URL + 'SHA256/b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842',), - ('DZ-CBCT.nrrd', 'DZ-MR.nrrd'), - ('DZ-CBCT', 'DZ-MR'), - ('SHA256:4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db', - 'SHA256:b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842')), - ('CTLiver', None, TESTING_DATA_URL + 'SHA256/e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e', - 'CTLiver.nrrd', 'CTLiver', 'SHA256:e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e'), - ('CTPCardioSeq', "CTP Cardio Sequence", - 'https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2', - 'CTP-cardio.seq.nrrd', 'CTPCardioSeq', - 'SHA256:7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2', - None, None, None, "SequenceFile"), - ('CTCardioSeq', "CT Cardio Sequence", - 'https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93', - 'CT-cardio.seq.nrrd', 'CTCardioSeq', - 'SHA256:d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93', - None, None, None, "SequenceFile"), - ) - - if self.builtInCategoryName not in slicer.modules.sampleDataSources: - slicer.modules.sampleDataSources[self.builtInCategoryName] = [] - for sourceArgument in sourceArguments: - dataSource = SampleDataSource(*sourceArgument) - if SampleDataLogic.isSampleDataSourceRegistered(self.builtInCategoryName, dataSource): - continue - slicer.modules.sampleDataSources[self.builtInCategoryName].append(dataSource) - - def registerDevelopmentSampleDataSources(self): - """Fills in the sample data sources displayed only if developer mode is enabled.""" - iconPath = os.path.join(os.path.dirname(__file__).replace('\\','/'), 'Resources','Icons') - self.registerCustomSampleDataSource( - category=self.developmentCategoryName, sampleName='TinyPatient', - uris=[TESTING_DATA_URL + 'SHA256/c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b', - TESTING_DATA_URL + 'SHA256/3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470'], - fileNames=['TinyPatient_CT.nrrd', 'TinyPatient_Structures.seg.nrrd'], - nodeNames=['TinyPatient_CT', 'TinyPatient_Segments'], - thumbnailFileName=os.path.join(iconPath, 'TinyPatient.png'), - loadFileType=['VolumeFile', 'SegmentationFile'], - checksums=['SHA256:c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b', 'SHA256:3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470'] - ) - - def registerTestingDataSources(self): - """Register sample data sources used by SampleData self-test to test module functionalities.""" - self.registerCustomSampleDataSource(**SampleDataTest.CustomDownloaderDataSource) - - def downloadFileIntoCache(self, uri, name, checksum=None): - """Given a uri and and a filename, download the data into - a file of the given name in the scene's cache""" - destFolderPath = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory() - - if not os.access(destFolderPath, os.W_OK): - try: - os.makedirs(destFolderPath, exist_ok=True) - except: - self.logMessage('Failed to create cache folder %s' % destFolderPath, logging.ERROR) - if not os.access(destFolderPath, os.W_OK): - self.logMessage('Cache folder %s is not writable' % destFolderPath, logging.ERROR) - return self.downloadFile(uri, destFolderPath, name, checksum) - - def downloadSourceIntoCache(self, source): - """Download all files for the given source and return a - list of file paths for the results""" - filePaths = [] - for uri,fileName,checksum in zip(source.uris,source.fileNames,source.checksums): - filePaths.append(self.downloadFileIntoCache(uri, fileName, checksum)) - return filePaths - - def downloadFromSource(self, source, maximumAttemptsCount=3): - """Given an instance of SampleDataSource, downloads the associated data and - load them into Slicer if it applies. - - The function always returns a list. - - Based on the fileType(s), nodeName(s) and loadFile(s) associated with - the source, different values may be appended to the returned list: - - - if nodeName is specified, appends loaded nodes but if ``loadFile`` is False appends downloaded filepath - - if fileType is ``SceneFile``, appends downloaded filepath - - if fileType is ``ZipFile``, appends directory of extracted archive but if ``loadFile`` is False appends downloaded filepath - - If no ``nodeNames`` and no ``fileTypes`` are specified or if ``loadFiles`` are all False, - returns the list of all downloaded filepaths. - """ - - # Input may contain urls without associated node names, which correspond to additional data files - # (e.g., .raw file for a .nhdr header file). Therefore we collect nodes and file paths separately - # and we only return file paths if no node names have been provided. - resultNodes = [] - resultFilePaths = [] - for uri,fileName,nodeName,checksum,loadFile,loadFileType in zip(source.uris,source.fileNames,source.nodeNames,source.checksums,source.loadFiles,source.loadFileType): + @staticmethod + def registerCustomSampleDataSource(category='Custom', + sampleName=None, uris=None, fileNames=None, nodeNames=None, + customDownloader=None, thumbnailFileName=None, + loadFileType='VolumeFile', loadFiles=None, loadFileProperties={}, + checksums=None): + """Adds custom data sets to SampleData. + :param category: Section title of data set in SampleData module GUI. + :param sampleName: Displayed name of data set in SampleData module GUI. + :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI, + :param uris: Download URL(s). + :param fileNames: File name(s) that will be loaded. + :param nodeNames: Node name(s) in the scene. + :param customDownloader: Custom function for downloading. + :param loadFileType: file format name(s) ('VolumeFile' by default). + :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides. + :param loadFileProperties: custom properties passed to the IO plugin. + :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. + """ - current_source = SampleDataSource(uris=uri, fileNames=fileName, nodeNames=nodeName, checksums=checksum, loadFiles=loadFile, loadFileType=loadFileType, loadFileProperties=source.loadFileProperties) - - for attemptsCount in range(maximumAttemptsCount): - - # Download try: - filePath = self.downloadFileIntoCache(uri, fileName, checksum) - except ValueError: - self.logMessage('Download failed (attempt %d of %d)...' % (attemptsCount+1, maximumAttemptsCount), logging.ERROR) - continue - resultFilePaths.append(filePath) - - if loadFileType == 'ZipFile': - if loadFile == False: - resultNodes.append(filePath) - break - outputDir = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory() + "/" + os.path.splitext(os.path.basename(filePath))[0] - qt.QDir().mkpath(outputDir) - if slicer.util.extractArchive(filePath, outputDir): - # Success - resultNodes.append(outputDir) - break - elif loadFileType == 'SceneFile': - if not loadFile: - resultNodes.append(filePath) - break - if self.loadScene(filePath, source.loadFileProperties.copy()): - # Success - resultNodes.append(filePath) - break - elif nodeName: - if loadFile == False: - resultNodes.append(filePath) - break - loadedNode = self.loadNode(filePath, nodeName, loadFileType, source.loadFileProperties.copy()) - if loadedNode: - # Success - resultNodes.append(loadedNode) - break - else: - # no need to load node - break - - # Failed. Clean up downloaded file (it might have been a partial download) - file = qt.QFile(filePath) - if file.exists() and not file.remove(): - self.logMessage('Load failed (attempt %d of %d). Unable to delete and try again loading %s' - % (attemptsCount+1, maximumAttemptsCount, filePath), logging.ERROR) - resultNodes.append(loadedNode) - break - self.logMessage('Load failed (attempt %d of %d)...' % (attemptsCount+1, maximumAttemptsCount), logging.ERROR) - - if resultNodes: - return resultNodes - else: - return resultFilePaths - - def sourceForSampleName(self,sampleName): - """For a given sample name this will search the available sources. - Returns SampleDataSource instance.""" - for category in slicer.modules.sampleDataSources.keys(): - for source in slicer.modules.sampleDataSources[category]: - if sampleName == source.sampleName: - return source - return None - - def categoryForSource(self, a_source): - """For a given SampleDataSource return the associated category name. - """ - for category in slicer.modules.sampleDataSources.keys(): - for source in slicer.modules.sampleDataSources[category]: - if a_source == source: - return category - return None - - def downloadFromURL(self, uris=None, fileNames=None, nodeNames=None, checksums=None, loadFiles=None, - customDownloader=None, loadFileTypes=None, loadFileProperties={}): - """Download and optionally load data into the application. - - :param uris: Download URL(s). - :param fileNames: File name(s) that will be downloaded (and loaded). - :param nodeNames: Node name(s) in the scene. - :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. - :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides. - :param customDownloader: Custom function for downloading. - :param loadFileTypes: file format name(s) ('VolumeFile' by default). - :param loadFileProperties: custom properties passed to the IO plugin. - - If the given ``fileNames`` are not found in the application cache directory, they - are downloaded using the associated URIs. - See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()`` - - If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are - guessed based on the corresponding filename extensions. - - If a given fileName has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded - by default. To ensure the file is loaded, ``loadFiles`` must be set. - - The ``loadFileProperties`` are common for all files. If different properties - need to be associated with files of different types, downloadFromURL must - be called for each. - """ - return self.downloadFromSource(SampleDataSource( - uris=uris, fileNames=fileNames, nodeNames=nodeNames, loadFiles=loadFiles, - loadFileType=loadFileTypes, loadFileProperties=loadFileProperties, checksums=checksums - )) - - def downloadSample(self,sampleName): - """For a given sample name this will search the available sources - and load it if it is available. Returns the first loaded node.""" - return self.downloadSamples(sampleName)[0] - - def downloadSamples(self,sampleName): - """For a given sample name this will search the available sources - and load it if it is available. Returns the loaded nodes.""" - source = self.sourceForSampleName(sampleName) - nodes = [] - if source: - nodes = self.downloadFromSource(source) - return nodes - - def logMessage(self, message, logLevel=logging.DEBUG): - logging.log(logLevel, message) - - """Utility methods for backwards compatibility""" - - def downloadMRHead(self): - return self.downloadSample('MRHead') - - def downloadCTChest(self): - return self.downloadSample('CTChest') - - def downloadCTACardio(self): - return self.downloadSample('CTACardio') - - def downloadDTIBrain(self): - return self.downloadSample('DTIBrain') - - def downloadMRBrainTumor1(self): - return self.downloadSample('MRBrainTumor1') - - def downloadMRBrainTumor2(self): - return self.downloadSample('MRBrainTumor2') - - def downloadWhiteMatterExplorationBaselineVolume(self): - return self.downloadSample('BaselineVolume') + slicer.modules.sampleDataSources + except AttributeError: + slicer.modules.sampleDataSources = {} + + if category not in slicer.modules.sampleDataSources: + slicer.modules.sampleDataSources[category] = [] + + dataSource = SampleDataSource( + sampleName=sampleName, + uris=uris, + fileNames=fileNames, + nodeNames=nodeNames, + thumbnailFileName=thumbnailFileName, + loadFileType=loadFileType, + loadFiles=loadFiles, + loadFileProperties=loadFileProperties, + checksums=checksums, + customDownloader=customDownloader, + ) - def downloadWhiteMatterExplorationDTIVolume(self): - return self.downloadSample('DTIVolume') + if SampleDataLogic.isSampleDataSourceRegistered(category, dataSource): + return - def downloadDiffusionMRIDWIVolume(self): - return self.downloadSample('DWIVolume') + slicer.modules.sampleDataSources[category].append(dataSource) - def downloadAbdominalCTVolume(self): - return self.downloadSample('CTAAbdomenPanoramix') + @staticmethod + def sampleDataSourcesByCategory(category=None): + """Return the registered SampleDataSources for with the given category. - def downloadDentalSurgery(self): - # returns list since that's what earlier method did - return self.downloadSamples('CBCTDentalSurgery') + If no category is specified, returns all registered SampleDataSources. + """ + try: + slicer.modules.sampleDataSources + except AttributeError: + slicer.modules.sampleDataSources = {} - def downloadMRUSPostate(self): - # returns list since that's what earlier method did - return self.downloadSamples('MRUSProstate') + if category is None: + return slicer.modules.sampleDataSources + else: + return slicer.modules.sampleDataSources.get(category, []) - def humanFormatSize(self,size): - """ from https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size""" - for x in ['bytes','KB','MB','GB']: - if size < 1024.0 and size > -1024.0: - return f"{size:3.1f} {x}" - size /= 1024.0 - return "{:3.1f} {}".format(size, 'TB') + @staticmethod + def isSampleDataSourceRegistered(category, sampleDataSource): + """Returns True if the sampleDataSource is registered with the category. + """ + try: + slicer.modules.sampleDataSources + except AttributeError: + slicer.modules.sampleDataSources = {} + + if not isinstance(sampleDataSource, SampleDataSource): + raise TypeError(f"unsupported sampleDataSource type '{type(sampleDataSource)}': '{str(SampleDataSource)}' is expected") + + return sampleDataSource in slicer.modules.sampleDataSources.get(category, []) + + def __init__(self, logMessage=None): + if logMessage: + self.logMessage = logMessage + self.builtInCategoryName = 'BuiltIn' + self.developmentCategoryName = 'Development' + self.registerBuiltInSampleDataSources() + self.registerDevelopmentSampleDataSources() + if slicer.app.testingEnabled(): + self.registerTestingDataSources() + self.downloadPercent = 0 + + def registerBuiltInSampleDataSources(self): + """Fills in the pre-define sample data sources""" + + # Arguments: + # sampleName=None, sampleDescription=None, + # uris=None, + # fileNames=None, nodeNames=None, + # checksums=None, + # loadFiles=None, customDownloader=None, thumbnailFileName=None, loadFileType=None, loadFileProperties=None + sourceArguments = ( + ('MRHead', None, TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', + 'MR-head.nrrd', 'MRHead', 'SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93'), + ('CTChest', None, TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e', + 'CT-chest.nrrd', 'CTChest', 'SHA256:4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'), + ('CTACardio', None, TESTING_DATA_URL + 'SHA256/3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2', + 'CTA-cardio.nrrd', 'CTACardio', 'SHA256:3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2'), + ('DTIBrain', None, TESTING_DATA_URL + 'SHA256/5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a', + 'DTI-Brain.nrrd', 'DTIBrain', 'SHA256:5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a'), + ('MRBrainTumor1', None, TESTING_DATA_URL + 'SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95', + 'RegLib_C01_1.nrrd', 'MRBrainTumor1', 'SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95'), + ('MRBrainTumor2', None, TESTING_DATA_URL + 'SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97', + 'RegLib_C01_2.nrrd', 'MRBrainTumor2', 'SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97'), + ('BaselineVolume', None, TESTING_DATA_URL + 'SHA256/dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2', + 'BaselineVolume.nrrd', 'BaselineVolume', 'SHA256:dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2'), + ('DTIVolume', None, + (TESTING_DATA_URL + 'SHA256/d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d', + TESTING_DATA_URL + 'SHA256/67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe', ), + ('DTIVolume.raw.gz', 'DTIVolume.nhdr'), (None, 'DTIVolume'), + ('SHA256:d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d', + 'SHA256:67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe')), + ('DWIVolume', None, + (TESTING_DATA_URL + 'SHA256/cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692', + TESTING_DATA_URL + 'SHA256/7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed'), + ('dwi.raw.gz', 'dwi.nhdr'), (None, 'dwi'), + ('SHA256:cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692', + 'SHA256:7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed')), + ('CTAAbdomenPanoramix', 'CTA abdomen\n(Panoramix)', TESTING_DATA_URL + 'SHA256/146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433', + 'Panoramix-cropped.nrrd', 'Panoramix-cropped', 'SHA256:146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433'), + ('CBCTDentalSurgery', None, + (TESTING_DATA_URL + 'SHA256/7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968', + TESTING_DATA_URL + 'SHA256/4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd',), + ('PreDentalSurgery.gipl.gz', 'PostDentalSurgery.gipl.gz'), ('PreDentalSurgery', 'PostDentalSurgery'), + ('SHA256:7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968', + 'SHA256:4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd')), + ('MRUSProstate', 'MR-US Prostate', + (TESTING_DATA_URL + 'SHA256/4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa', + TESTING_DATA_URL + 'SHA256/34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2',), + ('Case10-MR.nrrd', 'case10_US_resampled.nrrd'), ('MRProstate', 'USProstate'), + ('SHA256:4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa', + 'SHA256:34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2')), + ('CTMRBrain', 'CT-MR Brain', + (TESTING_DATA_URL + 'SHA256/6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1', + TESTING_DATA_URL + 'SHA256/2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593', + TESTING_DATA_URL + 'SHA256/fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7',), + ('CT-brain.nrrd', 'MR-brain-T1.nrrd', 'MR-brain-T2.nrrd'), + ('CTBrain', 'MRBrainT1', 'MRBrainT2'), + ('SHA256:6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1', + 'SHA256:2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593', + 'SHA256:fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7')), + ('CBCTMRHead', 'CBCT-MR Head', + (TESTING_DATA_URL + 'SHA256/4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db', + TESTING_DATA_URL + 'SHA256/b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842',), + ('DZ-CBCT.nrrd', 'DZ-MR.nrrd'), + ('DZ-CBCT', 'DZ-MR'), + ('SHA256:4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db', + 'SHA256:b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842')), + ('CTLiver', None, TESTING_DATA_URL + 'SHA256/e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e', + 'CTLiver.nrrd', 'CTLiver', 'SHA256:e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e'), + ('CTPCardioSeq', "CTP Cardio Sequence", + 'https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2', + 'CTP-cardio.seq.nrrd', 'CTPCardioSeq', + 'SHA256:7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2', + None, None, None, "SequenceFile"), + ('CTCardioSeq', "CT Cardio Sequence", + 'https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93', + 'CT-cardio.seq.nrrd', 'CTCardioSeq', + 'SHA256:d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93', + None, None, None, "SequenceFile"), + ) - def reportHook(self,blocksSoFar,blockSize,totalSize): - # we clamp to 100% because the blockSize might be larger than the file itself - percent = min(int((100. * blocksSoFar * blockSize) / totalSize), 100) - if percent == 100 or (percent - self.downloadPercent >= 10): - # we clamp to totalSize when blockSize is larger than totalSize - humanSizeSoFar = self.humanFormatSize(min(blocksSoFar * blockSize, totalSize)) - humanSizeTotal = self.humanFormatSize(totalSize) - self.logMessage('Downloaded %s (%d%% of %s)...' % (humanSizeSoFar, percent, humanSizeTotal)) - self.downloadPercent = percent + if self.builtInCategoryName not in slicer.modules.sampleDataSources: + slicer.modules.sampleDataSources[self.builtInCategoryName] = [] + for sourceArgument in sourceArguments: + dataSource = SampleDataSource(*sourceArgument) + if SampleDataLogic.isSampleDataSourceRegistered(self.builtInCategoryName, dataSource): + continue + slicer.modules.sampleDataSources[self.builtInCategoryName].append(dataSource) + + def registerDevelopmentSampleDataSources(self): + """Fills in the sample data sources displayed only if developer mode is enabled.""" + iconPath = os.path.join(os.path.dirname(__file__).replace('\\', '/'), 'Resources', 'Icons') + self.registerCustomSampleDataSource( + category=self.developmentCategoryName, sampleName='TinyPatient', + uris=[TESTING_DATA_URL + 'SHA256/c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b', + TESTING_DATA_URL + 'SHA256/3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470'], + fileNames=['TinyPatient_CT.nrrd', 'TinyPatient_Structures.seg.nrrd'], + nodeNames=['TinyPatient_CT', 'TinyPatient_Segments'], + thumbnailFileName=os.path.join(iconPath, 'TinyPatient.png'), + loadFileType=['VolumeFile', 'SegmentationFile'], + checksums=['SHA256:c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b', 'SHA256:3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470'] + ) - def downloadFile(self, uri, destFolderPath, name, checksum=None): - """ - :param uri: Download URL. - :param destFolderPath: Folder to download the file into. - :param name: File name that will be downloaded. - :param checksum: Checksum formatted as ``:`` to verify the downloaded file. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. - """ - self.downloadPercent = 0 - filePath = destFolderPath + '/' + name - (algo, digest) = extractAlgoAndDigest(checksum) - if not os.path.exists(filePath) or os.stat(filePath).st_size == 0: - import urllib.request, urllib.parse, urllib.error - self.logMessage(f'Requesting download {name} from {uri} ...') - try: - urllib.request.urlretrieve(uri, filePath, self.reportHook) - self.logMessage('Download finished') - except OSError as e: - self.logMessage('\tDownload failed: %s' % e, logging.ERROR) - raise ValueError(f"Failed to download {uri} to {filePath}") - - if algo is not None: - self.logMessage('Verifying checksum') - current_digest = computeChecksum(algo, filePath) - if current_digest != digest: - self.logMessage(f'Checksum verification failed. Computed checksum {current_digest} different from expected checksum {digest}') - qt.QFile(filePath).remove() + def registerTestingDataSources(self): + """Register sample data sources used by SampleData self-test to test module functionalities.""" + self.registerCustomSampleDataSource(**SampleDataTest.CustomDownloaderDataSource) + + def downloadFileIntoCache(self, uri, name, checksum=None): + """Given a uri and and a filename, download the data into + a file of the given name in the scene's cache""" + destFolderPath = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory() + + if not os.access(destFolderPath, os.W_OK): + try: + os.makedirs(destFolderPath, exist_ok=True) + except: + self.logMessage('Failed to create cache folder %s' % destFolderPath, logging.ERROR) + if not os.access(destFolderPath, os.W_OK): + self.logMessage('Cache folder %s is not writable' % destFolderPath, logging.ERROR) + return self.downloadFile(uri, destFolderPath, name, checksum) + + def downloadSourceIntoCache(self, source): + """Download all files for the given source and return a + list of file paths for the results""" + filePaths = [] + for uri, fileName, checksum in zip(source.uris, source.fileNames, source.checksums): + filePaths.append(self.downloadFileIntoCache(uri, fileName, checksum)) + return filePaths + + def downloadFromSource(self, source, maximumAttemptsCount=3): + """Given an instance of SampleDataSource, downloads the associated data and + load them into Slicer if it applies. + + The function always returns a list. + + Based on the fileType(s), nodeName(s) and loadFile(s) associated with + the source, different values may be appended to the returned list: + + - if nodeName is specified, appends loaded nodes but if ``loadFile`` is False appends downloaded filepath + - if fileType is ``SceneFile``, appends downloaded filepath + - if fileType is ``ZipFile``, appends directory of extracted archive but if ``loadFile`` is False appends downloaded filepath + + If no ``nodeNames`` and no ``fileTypes`` are specified or if ``loadFiles`` are all False, + returns the list of all downloaded filepaths. + """ + + # Input may contain urls without associated node names, which correspond to additional data files + # (e.g., .raw file for a .nhdr header file). Therefore we collect nodes and file paths separately + # and we only return file paths if no node names have been provided. + resultNodes = [] + resultFilePaths = [] + + for uri, fileName, nodeName, checksum, loadFile, loadFileType in zip(source.uris, source.fileNames, source.nodeNames, source.checksums, source.loadFiles, source.loadFileType): + + current_source = SampleDataSource(uris=uri, fileNames=fileName, nodeNames=nodeName, checksums=checksum, loadFiles=loadFile, loadFileType=loadFileType, loadFileProperties=source.loadFileProperties) + + for attemptsCount in range(maximumAttemptsCount): + + # Download + try: + filePath = self.downloadFileIntoCache(uri, fileName, checksum) + except ValueError: + self.logMessage('Download failed (attempt %d of %d)...' % (attemptsCount + 1, maximumAttemptsCount), logging.ERROR) + continue + resultFilePaths.append(filePath) + + if loadFileType == 'ZipFile': + if loadFile is False: + resultNodes.append(filePath) + break + outputDir = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory() + "/" + os.path.splitext(os.path.basename(filePath))[0] + qt.QDir().mkpath(outputDir) + if slicer.util.extractArchive(filePath, outputDir): + # Success + resultNodes.append(outputDir) + break + elif loadFileType == 'SceneFile': + if not loadFile: + resultNodes.append(filePath) + break + if self.loadScene(filePath, source.loadFileProperties.copy()): + # Success + resultNodes.append(filePath) + break + elif nodeName: + if loadFile is False: + resultNodes.append(filePath) + break + loadedNode = self.loadNode(filePath, nodeName, loadFileType, source.loadFileProperties.copy()) + if loadedNode: + # Success + resultNodes.append(loadedNode) + break + else: + # no need to load node + break + + # Failed. Clean up downloaded file (it might have been a partial download) + file = qt.QFile(filePath) + if file.exists() and not file.remove(): + self.logMessage('Load failed (attempt %d of %d). Unable to delete and try again loading %s' + % (attemptsCount + 1, maximumAttemptsCount, filePath), logging.ERROR) + resultNodes.append(loadedNode) + break + self.logMessage('Load failed (attempt %d of %d)...' % (attemptsCount + 1, maximumAttemptsCount), logging.ERROR) + + if resultNodes: + return resultNodes else: - self.downloadPercent = 100 - self.logMessage('Checksum OK') - else: - if algo is not None: - self.logMessage('Verifying checksum') - current_digest = computeChecksum(algo, filePath) - if current_digest != digest: - self.logMessage('File already exists in cache but checksum is different - re-downloading it.') - qt.QFile(filePath).remove() - return self.downloadFile(uri, destFolderPath, name, checksum) + return resultFilePaths + + def sourceForSampleName(self, sampleName): + """For a given sample name this will search the available sources. + Returns SampleDataSource instance.""" + for category in slicer.modules.sampleDataSources.keys(): + for source in slicer.modules.sampleDataSources[category]: + if sampleName == source.sampleName: + return source + return None + + def categoryForSource(self, a_source): + """For a given SampleDataSource return the associated category name. + """ + for category in slicer.modules.sampleDataSources.keys(): + for source in slicer.modules.sampleDataSources[category]: + if a_source == source: + return category + return None + + def downloadFromURL(self, uris=None, fileNames=None, nodeNames=None, checksums=None, loadFiles=None, + customDownloader=None, loadFileTypes=None, loadFileProperties={}): + """Download and optionally load data into the application. + + :param uris: Download URL(s). + :param fileNames: File name(s) that will be downloaded (and loaded). + :param nodeNames: Node name(s) in the scene. + :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. + :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides. + :param customDownloader: Custom function for downloading. + :param loadFileTypes: file format name(s) ('VolumeFile' by default). + :param loadFileProperties: custom properties passed to the IO plugin. + + If the given ``fileNames`` are not found in the application cache directory, they + are downloaded using the associated URIs. + See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()`` + + If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are + guessed based on the corresponding filename extensions. + + If a given fileName has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded + by default. To ensure the file is loaded, ``loadFiles`` must be set. + + The ``loadFileProperties`` are common for all files. If different properties + need to be associated with files of different types, downloadFromURL must + be called for each. + """ + return self.downloadFromSource(SampleDataSource( + uris=uris, fileNames=fileNames, nodeNames=nodeNames, loadFiles=loadFiles, + loadFileType=loadFileTypes, loadFileProperties=loadFileProperties, checksums=checksums + )) + + def downloadSample(self, sampleName): + """For a given sample name this will search the available sources + and load it if it is available. Returns the first loaded node.""" + return self.downloadSamples(sampleName)[0] + + def downloadSamples(self, sampleName): + """For a given sample name this will search the available sources + and load it if it is available. Returns the loaded nodes.""" + source = self.sourceForSampleName(sampleName) + nodes = [] + if source: + nodes = self.downloadFromSource(source) + return nodes + + def logMessage(self, message, logLevel=logging.DEBUG): + logging.log(logLevel, message) + + """Utility methods for backwards compatibility""" + + def downloadMRHead(self): + return self.downloadSample('MRHead') + + def downloadCTChest(self): + return self.downloadSample('CTChest') + + def downloadCTACardio(self): + return self.downloadSample('CTACardio') + + def downloadDTIBrain(self): + return self.downloadSample('DTIBrain') + + def downloadMRBrainTumor1(self): + return self.downloadSample('MRBrainTumor1') + + def downloadMRBrainTumor2(self): + return self.downloadSample('MRBrainTumor2') + + def downloadWhiteMatterExplorationBaselineVolume(self): + return self.downloadSample('BaselineVolume') + + def downloadWhiteMatterExplorationDTIVolume(self): + return self.downloadSample('DTIVolume') + + def downloadDiffusionMRIDWIVolume(self): + return self.downloadSample('DWIVolume') + + def downloadAbdominalCTVolume(self): + return self.downloadSample('CTAAbdomenPanoramix') + + def downloadDentalSurgery(self): + # returns list since that's what earlier method did + return self.downloadSamples('CBCTDentalSurgery') + + def downloadMRUSPostate(self): + # returns list since that's what earlier method did + return self.downloadSamples('MRUSProstate') + + def humanFormatSize(self, size): + """ from https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size""" + for x in ['bytes', 'KB', 'MB', 'GB']: + if size < 1024.0 and size > -1024.0: + return f"{size:3.1f} {x}" + size /= 1024.0 + return "{:3.1f} {}".format(size, 'TB') + + def reportHook(self, blocksSoFar, blockSize, totalSize): + # we clamp to 100% because the blockSize might be larger than the file itself + percent = min(int((100. * blocksSoFar * blockSize) / totalSize), 100) + if percent == 100 or (percent - self.downloadPercent >= 10): + # we clamp to totalSize when blockSize is larger than totalSize + humanSizeSoFar = self.humanFormatSize(min(blocksSoFar * blockSize, totalSize)) + humanSizeTotal = self.humanFormatSize(totalSize) + self.logMessage('Downloaded %s (%d%% of %s)...' % (humanSizeSoFar, percent, humanSizeTotal)) + self.downloadPercent = percent + + def downloadFile(self, uri, destFolderPath, name, checksum=None): + """ + :param uri: Download URL. + :param destFolderPath: Folder to download the file into. + :param name: File name that will be downloaded. + :param checksum: Checksum formatted as ``:`` to verify the downloaded file. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``. + """ + self.downloadPercent = 0 + filePath = destFolderPath + '/' + name + (algo, digest) = extractAlgoAndDigest(checksum) + if not os.path.exists(filePath) or os.stat(filePath).st_size == 0: + import urllib.request, urllib.parse, urllib.error + self.logMessage(f'Requesting download {name} from {uri} ...') + try: + urllib.request.urlretrieve(uri, filePath, self.reportHook) + self.logMessage('Download finished') + except OSError as e: + self.logMessage('\tDownload failed: %s' % e, logging.ERROR) + raise ValueError(f"Failed to download {uri} to {filePath}") + + if algo is not None: + self.logMessage('Verifying checksum') + current_digest = computeChecksum(algo, filePath) + if current_digest != digest: + self.logMessage(f'Checksum verification failed. Computed checksum {current_digest} different from expected checksum {digest}') + qt.QFile(filePath).remove() + else: + self.downloadPercent = 100 + self.logMessage('Checksum OK') else: - self.downloadPercent = 100 - self.logMessage('File already exists and checksum is OK - reusing it.') - else: - self.downloadPercent = 100 - self.logMessage('File already exists in cache - reusing it.') - return filePath - - def loadScene(self, uri, fileProperties = {}): - self.logMessage('Requesting load %s ...' % uri) - fileProperties['fileName'] = uri - success = slicer.app.coreIOManager().loadNodes('SceneFile', fileProperties) - if not success: - self.logMessage('\tLoad failed!', logging.ERROR) - return False - self.logMessage('Load finished') - return True - - def loadNode(self, uri, name, fileType = 'VolumeFile', fileProperties = {}): - self.logMessage(f'Requesting load {name} from {uri} ...') - - fileProperties['fileName'] = uri - fileProperties['name'] = name - firstLoadedNode = None - loadedNodes = vtk.vtkCollection() - success = slicer.app.coreIOManager().loadNodes(fileType, fileProperties, loadedNodes) - - if not success or loadedNodes.GetNumberOfItems()<1: - self.logMessage('\tLoad failed!', logging.ERROR) - return None - - self.logMessage('Load finished') - - # since nodes were read from a temp directory remove the storage nodes - for i in range(loadedNodes.GetNumberOfItems()): - loadedNode = loadedNodes.GetItemAsObject(i) - if not loadedNode.IsA("vtkMRMLStorableNode"): - continue - storageNode = loadedNode.GetStorageNode() - if not storageNode: - continue - slicer.mrmlScene.RemoveNode(storageNode) - loadedNode.SetAndObserveStorageNodeID(None) - - return loadedNodes.GetItemAsObject(0) + if algo is not None: + self.logMessage('Verifying checksum') + current_digest = computeChecksum(algo, filePath) + if current_digest != digest: + self.logMessage('File already exists in cache but checksum is different - re-downloading it.') + qt.QFile(filePath).remove() + return self.downloadFile(uri, destFolderPath, name, checksum) + else: + self.downloadPercent = 100 + self.logMessage('File already exists and checksum is OK - reusing it.') + else: + self.downloadPercent = 100 + self.logMessage('File already exists in cache - reusing it.') + return filePath + + def loadScene(self, uri, fileProperties={}): + self.logMessage('Requesting load %s ...' % uri) + fileProperties['fileName'] = uri + success = slicer.app.coreIOManager().loadNodes('SceneFile', fileProperties) + if not success: + self.logMessage('\tLoad failed!', logging.ERROR) + return False + self.logMessage('Load finished') + return True + + def loadNode(self, uri, name, fileType='VolumeFile', fileProperties={}): + self.logMessage(f'Requesting load {name} from {uri} ...') + + fileProperties['fileName'] = uri + fileProperties['name'] = name + firstLoadedNode = None + loadedNodes = vtk.vtkCollection() + success = slicer.app.coreIOManager().loadNodes(fileType, fileProperties, loadedNodes) + + if not success or loadedNodes.GetNumberOfItems() < 1: + self.logMessage('\tLoad failed!', logging.ERROR) + return None + + self.logMessage('Load finished') + + # since nodes were read from a temp directory remove the storage nodes + for i in range(loadedNodes.GetNumberOfItems()): + loadedNode = loadedNodes.GetItemAsObject(i) + if not loadedNode.IsA("vtkMRMLStorableNode"): + continue + storageNode = loadedNode.GetStorageNode() + if not storageNode: + continue + slicer.mrmlScene.RemoveNode(storageNode) + loadedNode.SetAndObserveStorageNodeID(None) + + return loadedNodes.GetItemAsObject(0) class SampleDataTest(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 - """ - - customDownloads = [] - - def setUp(self): - slicer.mrmlScene.Clear(0) - SampleDataTest.customDownloads = [] - - def runTest(self): - for test in [ - self.test_downloadFromSource_downloadFiles, - self.test_downloadFromSource_downloadZipFile, - self.test_downloadFromSource_loadMRBFile, - self.test_downloadFromSource_loadMRMLFile, - self.test_downloadFromSource_downloadMRBFile, - self.test_downloadFromSource_downloadMRMLFile, - self.test_downloadFromSource_loadNode, - self.test_downloadFromSource_loadNodeFromMultipleFiles, - self.test_downloadFromSource_loadNodes, - self.test_downloadFromSource_loadNodesWithLoadFileFalse, - self.test_sampleDataSourcesByCategory, - self.test_categoryVisibility, - self.test_setCategoriesFromSampleDataSources, - self.test_isSampleDataSourceRegistered, - self.test_customDownloader, - self.test_categoryForSource, - ]: - self.setUp() - test() - - @staticmethod - def path2uri(path): - """Gets a URI from a local file path. - Typically it prefixes the received path by file:// or file:///. """ - import urllib.parse, urllib.request, urllib.parse, urllib.error - return urllib.parse.urljoin('file:', urllib.request.pathname2url(path)) - - def test_downloadFromSource_downloadFiles(self): - """Specifying URIs and fileNames without nodeNames is expected to download the files - without loading into Slicer. + 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 """ - logic = SampleDataLogic() - - sceneMTime = slicer.mrmlScene.GetMTime() - filePaths = logic.downloadFromSource(SampleDataSource( - uris=TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', - fileNames='MR-head.nrrd')) - self.assertEqual(len(filePaths), 1) - self.assertTrue(os.path.exists(filePaths[0])) - self.assertTrue(os.path.isfile(filePaths[0])) - self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) - - sceneMTime = slicer.mrmlScene.GetMTime() - filePaths = logic.downloadFromSource(SampleDataSource( - uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', - TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'], - fileNames=['MR-head.nrrd', 'CT-chest.nrrd'])) - self.assertEqual(len(filePaths), 2) - self.assertTrue(os.path.exists(filePaths[0])) - self.assertTrue(os.path.isfile(filePaths[0])) - self.assertTrue(os.path.exists(filePaths[1])) - self.assertTrue(os.path.isfile(filePaths[1])) - self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) - - def test_downloadFromSource_downloadZipFile(self): - logic = SampleDataLogic() - sceneMTime = slicer.mrmlScene.GetMTime() - filePaths = logic.downloadFromSource(SampleDataSource( - uris=TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7', - fileNames='TinyPatient_Seg.zip')) - self.assertEqual(len(filePaths), 1) - self.assertTrue(os.path.exists(filePaths[0])) - self.assertTrue(os.path.isdir(filePaths[0])) - self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) - - def test_downloadFromSource_loadMRBFile(self): - logic = SampleDataLogic() - sceneMTime = slicer.mrmlScene.GetMTime() - filePaths = logic.downloadFromSource(SampleDataSource( - uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8', - loadFiles=True, fileNames='slicer4minute.mrb')) - self.assertEqual(len(filePaths), 1) - self.assertTrue(os.path.exists(filePaths[0])) - self.assertTrue(os.path.isfile(filePaths[0])) - self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime()) - - def test_downloadFromSource_loadMRMLFile(self): - logic = SampleDataLogic() - tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml") - tempFile.open() - tempFile.write(textwrap.dedent(""" + + customDownloads = [] + + def setUp(self): + slicer.mrmlScene.Clear(0) + SampleDataTest.customDownloads = [] + + def runTest(self): + for test in [ + self.test_downloadFromSource_downloadFiles, + self.test_downloadFromSource_downloadZipFile, + self.test_downloadFromSource_loadMRBFile, + self.test_downloadFromSource_loadMRMLFile, + self.test_downloadFromSource_downloadMRBFile, + self.test_downloadFromSource_downloadMRMLFile, + self.test_downloadFromSource_loadNode, + self.test_downloadFromSource_loadNodeFromMultipleFiles, + self.test_downloadFromSource_loadNodes, + self.test_downloadFromSource_loadNodesWithLoadFileFalse, + self.test_sampleDataSourcesByCategory, + self.test_categoryVisibility, + self.test_setCategoriesFromSampleDataSources, + self.test_isSampleDataSourceRegistered, + self.test_customDownloader, + self.test_categoryForSource, + ]: + self.setUp() + test() + + @staticmethod + def path2uri(path): + """Gets a URI from a local file path. + Typically it prefixes the received path by file:// or file:///. + """ + import urllib.parse, urllib.request, urllib.parse, urllib.error + return urllib.parse.urljoin('file:', urllib.request.pathname2url(path)) + + def test_downloadFromSource_downloadFiles(self): + """Specifying URIs and fileNames without nodeNames is expected to download the files + without loading into Slicer. + """ + logic = SampleDataLogic() + + sceneMTime = slicer.mrmlScene.GetMTime() + filePaths = logic.downloadFromSource(SampleDataSource( + uris=TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', + fileNames='MR-head.nrrd')) + self.assertEqual(len(filePaths), 1) + self.assertTrue(os.path.exists(filePaths[0])) + self.assertTrue(os.path.isfile(filePaths[0])) + self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) + + sceneMTime = slicer.mrmlScene.GetMTime() + filePaths = logic.downloadFromSource(SampleDataSource( + uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', + TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'], + fileNames=['MR-head.nrrd', 'CT-chest.nrrd'])) + self.assertEqual(len(filePaths), 2) + self.assertTrue(os.path.exists(filePaths[0])) + self.assertTrue(os.path.isfile(filePaths[0])) + self.assertTrue(os.path.exists(filePaths[1])) + self.assertTrue(os.path.isfile(filePaths[1])) + self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) + + def test_downloadFromSource_downloadZipFile(self): + logic = SampleDataLogic() + sceneMTime = slicer.mrmlScene.GetMTime() + filePaths = logic.downloadFromSource(SampleDataSource( + uris=TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7', + fileNames='TinyPatient_Seg.zip')) + self.assertEqual(len(filePaths), 1) + self.assertTrue(os.path.exists(filePaths[0])) + self.assertTrue(os.path.isdir(filePaths[0])) + self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) + + def test_downloadFromSource_loadMRBFile(self): + logic = SampleDataLogic() + sceneMTime = slicer.mrmlScene.GetMTime() + filePaths = logic.downloadFromSource(SampleDataSource( + uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8', + loadFiles=True, fileNames='slicer4minute.mrb')) + self.assertEqual(len(filePaths), 1) + self.assertTrue(os.path.exists(filePaths[0])) + self.assertTrue(os.path.isfile(filePaths[0])) + self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime()) + + def test_downloadFromSource_loadMRMLFile(self): + logic = SampleDataLogic() + tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml") + tempFile.open() + tempFile.write(textwrap.dedent(""" """).strip()) - tempFile.close() - sceneMTime = slicer.mrmlScene.GetMTime() - filePaths = logic.downloadFromSource(SampleDataSource( - uris=self.path2uri(tempFile.fileName()), loadFiles=True, fileNames='scene.mrml')) - self.assertEqual(len(filePaths), 1) - self.assertTrue(os.path.exists(filePaths[0])) - self.assertTrue(os.path.isfile(filePaths[0])) - self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime()) - - def test_downloadFromSource_downloadMRBFile(self): - logic = SampleDataLogic() - sceneMTime = slicer.mrmlScene.GetMTime() - filePaths = logic.downloadFromSource(SampleDataSource( - uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8', - fileNames='slicer4minute.mrb')) - self.assertEqual(len(filePaths), 1) - self.assertTrue(os.path.exists(filePaths[0])) - self.assertTrue(os.path.isfile(filePaths[0])) - self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) - - def test_downloadFromSource_downloadMRMLFile(self): - logic = SampleDataLogic() - tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml") - tempFile.open() - tempFile.write(textwrap.dedent(""" + tempFile.close() + sceneMTime = slicer.mrmlScene.GetMTime() + filePaths = logic.downloadFromSource(SampleDataSource( + uris=self.path2uri(tempFile.fileName()), loadFiles=True, fileNames='scene.mrml')) + self.assertEqual(len(filePaths), 1) + self.assertTrue(os.path.exists(filePaths[0])) + self.assertTrue(os.path.isfile(filePaths[0])) + self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime()) + + def test_downloadFromSource_downloadMRBFile(self): + logic = SampleDataLogic() + sceneMTime = slicer.mrmlScene.GetMTime() + filePaths = logic.downloadFromSource(SampleDataSource( + uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8', + fileNames='slicer4minute.mrb')) + self.assertEqual(len(filePaths), 1) + self.assertTrue(os.path.exists(filePaths[0])) + self.assertTrue(os.path.isfile(filePaths[0])) + self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) + + def test_downloadFromSource_downloadMRMLFile(self): + logic = SampleDataLogic() + tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml") + tempFile.open() + tempFile.write(textwrap.dedent(""" """).strip()) - tempFile.close() - sceneMTime = slicer.mrmlScene.GetMTime() - filePaths = logic.downloadFromSource(SampleDataSource( - uris=self.path2uri(tempFile.fileName()), fileNames='scene.mrml')) - self.assertEqual(len(filePaths), 1) - self.assertTrue(os.path.exists(filePaths[0])) - self.assertTrue(os.path.isfile(filePaths[0])) - self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) - - def test_downloadFromSource_loadNode(self): - logic = SampleDataLogic() - nodes = logic.downloadFromSource(SampleDataSource( - uris=TESTING_DATA_URL + 'MD5/39b01631b7b38232a220007230624c8e', - fileNames='MR-head.nrrd', nodeNames='MRHead')) - self.assertEqual(len(nodes), 1) - self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead")) - - def test_downloadFromSource_loadNodeFromMultipleFiles(self): - logic = SampleDataLogic() - nodes = logic.downloadFromSource(SampleDataSource( - uris=[TESTING_DATA_URL + 'SHA256/d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d', - TESTING_DATA_URL + 'SHA256/67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe'], - fileNames=['DTIVolume.raw.gz', 'DTIVolume.nhdr'], - nodeNames=[None, 'DTIVolume'])) - self.assertEqual(len(nodes), 1) - self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("DTIVolume")) - - def test_downloadFromSource_loadNodesWithLoadFileFalse(self): - logic = SampleDataLogic() - nodes = logic.downloadFromSource(SampleDataSource( - uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', - TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'], - fileNames=['MR-head.nrrd', 'CT-chest.nrrd'], - nodeNames=['MRHead', 'CTChest'], - loadFiles=[False, True])) - self.assertEqual(len(nodes), 2) - self.assertTrue(os.path.exists(nodes[0])) - self.assertTrue(os.path.isfile(nodes[0])) - self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest")) - - def test_downloadFromSource_loadNodes(self): - logic = SampleDataLogic() - nodes = logic.downloadFromSource(SampleDataSource( - uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', - TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'], - fileNames=['MR-head.nrrd', 'CT-chest.nrrd'], - nodeNames=['MRHead', 'CTChest'])) - self.assertEqual(len(nodes), 2) - self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead")) - self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest")) - - def test_sampleDataSourcesByCategory(self): - self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory()) > 0) - self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory('BuiltIn')) > 0) - self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory('Not_A_Registered_Category')) == 0) - - def test_categoryVisibility(self): - slicer.util.selectModule("SampleData") - widget = slicer.modules.SampleDataWidget - widget.setCategoryVisible('BuiltIn', False) - self.assertFalse(widget.isCategoryVisible('BuiltIn')) - widget.setCategoryVisible('BuiltIn', True) - self.assertTrue(widget.isCategoryVisible('BuiltIn')) - - def test_setCategoriesFromSampleDataSources(self): - slicer.util.selectModule("SampleData") - widget = slicer.modules.SampleDataWidget - self.assertGreater(widget.categoryLayout.count(), 0) - - SampleDataWidget.removeCategories(widget.categoryLayout) - self.assertEqual(widget.categoryLayout.count(), 0) - - SampleDataWidget.setCategoriesFromSampleDataSources(widget.categoryLayout, slicer.modules.sampleDataSources, widget.logic) - self.assertGreater(widget.categoryLayout.count(), 0) - - def test_isSampleDataSourceRegistered(self): - if not slicer.app.testingEnabled(): - return - sourceArguments = { - 'sampleName': 'isSampleDataSourceRegistered', - 'uris': 'https://slicer.org', - 'fileNames': 'volume.nrrd', - 'loadFileType': 'VolumeFile', + tempFile.close() + sceneMTime = slicer.mrmlScene.GetMTime() + filePaths = logic.downloadFromSource(SampleDataSource( + uris=self.path2uri(tempFile.fileName()), fileNames='scene.mrml')) + self.assertEqual(len(filePaths), 1) + self.assertTrue(os.path.exists(filePaths[0])) + self.assertTrue(os.path.isfile(filePaths[0])) + self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime()) + + def test_downloadFromSource_loadNode(self): + logic = SampleDataLogic() + nodes = logic.downloadFromSource(SampleDataSource( + uris=TESTING_DATA_URL + 'MD5/39b01631b7b38232a220007230624c8e', + fileNames='MR-head.nrrd', nodeNames='MRHead')) + self.assertEqual(len(nodes), 1) + self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead")) + + def test_downloadFromSource_loadNodeFromMultipleFiles(self): + logic = SampleDataLogic() + nodes = logic.downloadFromSource(SampleDataSource( + uris=[TESTING_DATA_URL + 'SHA256/d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d', + TESTING_DATA_URL + 'SHA256/67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe'], + fileNames=['DTIVolume.raw.gz', 'DTIVolume.nhdr'], + nodeNames=[None, 'DTIVolume'])) + self.assertEqual(len(nodes), 1) + self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("DTIVolume")) + + def test_downloadFromSource_loadNodesWithLoadFileFalse(self): + logic = SampleDataLogic() + nodes = logic.downloadFromSource(SampleDataSource( + uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', + TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'], + fileNames=['MR-head.nrrd', 'CT-chest.nrrd'], + nodeNames=['MRHead', 'CTChest'], + loadFiles=[False, True])) + self.assertEqual(len(nodes), 2) + self.assertTrue(os.path.exists(nodes[0])) + self.assertTrue(os.path.isfile(nodes[0])) + self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest")) + + def test_downloadFromSource_loadNodes(self): + logic = SampleDataLogic() + nodes = logic.downloadFromSource(SampleDataSource( + uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93', + TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'], + fileNames=['MR-head.nrrd', 'CT-chest.nrrd'], + nodeNames=['MRHead', 'CTChest'])) + self.assertEqual(len(nodes), 2) + self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead")) + self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest")) + + def test_sampleDataSourcesByCategory(self): + self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory()) > 0) + self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory('BuiltIn')) > 0) + self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory('Not_A_Registered_Category')) == 0) + + def test_categoryVisibility(self): + slicer.util.selectModule("SampleData") + widget = slicer.modules.SampleDataWidget + widget.setCategoryVisible('BuiltIn', False) + self.assertFalse(widget.isCategoryVisible('BuiltIn')) + widget.setCategoryVisible('BuiltIn', True) + self.assertTrue(widget.isCategoryVisible('BuiltIn')) + + def test_setCategoriesFromSampleDataSources(self): + slicer.util.selectModule("SampleData") + widget = slicer.modules.SampleDataWidget + self.assertGreater(widget.categoryLayout.count(), 0) + + SampleDataWidget.removeCategories(widget.categoryLayout) + self.assertEqual(widget.categoryLayout.count(), 0) + + SampleDataWidget.setCategoriesFromSampleDataSources(widget.categoryLayout, slicer.modules.sampleDataSources, widget.logic) + self.assertGreater(widget.categoryLayout.count(), 0) + + def test_isSampleDataSourceRegistered(self): + if not slicer.app.testingEnabled(): + return + sourceArguments = { + 'sampleName': 'isSampleDataSourceRegistered', + 'uris': 'https://slicer.org', + 'fileNames': 'volume.nrrd', + 'loadFileType': 'VolumeFile', + } + self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments))) + SampleDataLogic.registerCustomSampleDataSource(**sourceArguments, category="Testing") + self.assertTrue(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments))) + self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Other", SampleDataSource(**sourceArguments))) + + class CustomDownloader: + def __call__(self, source): + SampleDataTest.customDownloads.append(source) + + CustomDownloaderDataSource = { + 'category': "Testing", + 'sampleName': 'customDownloader', + 'uris': 'http://down.load/test', + 'fileNames': 'cust.om', + 'customDownloader': CustomDownloader() } - self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments))) - SampleDataLogic.registerCustomSampleDataSource(**sourceArguments, category="Testing") - self.assertTrue(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments))) - self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Other", SampleDataSource(**sourceArguments))) - - class CustomDownloader: - def __call__(self, source): - SampleDataTest.customDownloads.append(source) - - CustomDownloaderDataSource = { - 'category': "Testing", - 'sampleName': 'customDownloader', - 'uris': 'http://down.load/test', - 'fileNames': 'cust.om', - 'customDownloader': CustomDownloader() - } - - def test_customDownloader(self): - if not slicer.app.testingEnabled(): - return - slicer.util.selectModule("SampleData") - widget = slicer.modules.SampleDataWidget - button = slicer.util.findChild(widget.parent,'customDownloaderPushButton') - - self.assertEqual(self.customDownloads, []) - - button.click() - - self.assertEqual(len(self.customDownloads), 1) - self.assertEqual(self.customDownloads[0].sampleName, 'customDownloader') - - def test_categoryForSource(self): - logic = SampleDataLogic() - source = slicer.modules.sampleDataSources[logic.builtInCategoryName][0] - self.assertEqual(logic.categoryForSource(source), logic.builtInCategoryName) + + def test_customDownloader(self): + if not slicer.app.testingEnabled(): + return + slicer.util.selectModule("SampleData") + widget = slicer.modules.SampleDataWidget + button = slicer.util.findChild(widget.parent, 'customDownloaderPushButton') + + self.assertEqual(self.customDownloads, []) + + button.click() + + self.assertEqual(len(self.customDownloads), 1) + self.assertEqual(self.customDownloads[0].sampleName, 'customDownloader') + + def test_categoryForSource(self): + logic = SampleDataLogic() + source = slicer.modules.sampleDataSources[logic.builtInCategoryName][0] + self.assertEqual(logic.categoryForSource(source), logic.builtInCategoryName) diff --git a/Modules/Scripted/ScreenCapture/ScreenCapture.py b/Modules/Scripted/ScreenCapture/ScreenCapture.py index 95f426053f7..35b3afe674d 100644 --- a/Modules/Scripted/ScreenCapture/ScreenCapture.py +++ b/Modules/Scripted/ScreenCapture/ScreenCapture.py @@ -14,22 +14,22 @@ # class ScreenCapture(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 = "Screen Capture" - self.parent.categories = ["Utilities"] - self.parent.dependencies = [] - self.parent.contributors = ["Andras Lasso (PerkLab Queen's University)"] - self.parent.helpText = """ + """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 = "Screen Capture" + self.parent.categories = ["Utilities"] + self.parent.dependencies = [] + self.parent.contributors = ["Andras Lasso (PerkLab Queen's University)"] + self.parent.helpText = """ This module captures image sequences and videos from dynamic contents shown in 3D and slice viewers. """ - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = """ + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = """ This work was was funded by Cancer Care Ontario and the Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO) """ @@ -47,753 +47,753 @@ def __init__(self, parent): class ScreenCaptureWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - self.logic = ScreenCaptureLogic() - self.logic.logCallback = self.addLog - self.viewNodeType = None - self.animationMode = None - self.createdOutputFile = None - - self.snapshotIndex = 0 # this counter is used for determining file names for single-image snapshots - self.snapshotOutputDir = None - self.snapshotFileNamePattern = None - - # Instantiate and connect widgets ... - - # - # Input area - # - self.inputCollapsibleButton = ctk.ctkCollapsibleButton() - self.inputCollapsibleButton.text = "Input" - self.layout.addWidget(self.inputCollapsibleButton) - inputFormLayout = qt.QFormLayout(self.inputCollapsibleButton) - - # Input view selector - self.viewNodeSelector = slicer.qMRMLNodeComboBox() - self.viewNodeSelector.nodeTypes = ["vtkMRMLSliceNode", "vtkMRMLViewNode"] - self.viewNodeSelector.addEnabled = False - self.viewNodeSelector.removeEnabled = False - self.viewNodeSelector.noneEnabled = False - self.viewNodeSelector.showHidden = False - self.viewNodeSelector.showChildNodeTypes = False - self.viewNodeSelector.setMRMLScene( slicer.mrmlScene ) - self.viewNodeSelector.setToolTip("This slice or 3D view will be updated during capture." - "Only this view will be captured unless 'Capture of all views' option in output section is enabled." ) - inputFormLayout.addRow("Master view: ", self.viewNodeSelector) - - self.captureAllViewsCheckBox = qt.QCheckBox(" ") - self.captureAllViewsCheckBox.checked = False - self.captureAllViewsCheckBox.setToolTip("If checked, all views will be captured. If unchecked then only the selected view will be captured.") - inputFormLayout.addRow("Capture all views:", self.captureAllViewsCheckBox) - - # Mode - self.animationModeWidget = qt.QComboBox() - self.animationModeWidget.setToolTip("Select the property that will be adjusted") - inputFormLayout.addRow("Animation mode:", self.animationModeWidget) - - # Slice start offset position - self.sliceStartOffsetSliderLabel = qt.QLabel("Start sweep offset:") - self.sliceStartOffsetSliderWidget = ctk.ctkSliderWidget() - self.sliceStartOffsetSliderWidget.singleStep = 30 - self.sliceStartOffsetSliderWidget.minimum = -100 - self.sliceStartOffsetSliderWidget.maximum = 100 - self.sliceStartOffsetSliderWidget.value = 0 - self.sliceStartOffsetSliderWidget.setToolTip("Start slice sweep offset.") - inputFormLayout.addRow(self.sliceStartOffsetSliderLabel, self.sliceStartOffsetSliderWidget) - - # Slice end offset position - self.sliceEndOffsetSliderLabel = qt.QLabel("End sweep offset:") - self.sliceEndOffsetSliderWidget = ctk.ctkSliderWidget() - self.sliceEndOffsetSliderWidget.singleStep = 5 - self.sliceEndOffsetSliderWidget.minimum = -100 - self.sliceEndOffsetSliderWidget.maximum = 100 - self.sliceEndOffsetSliderWidget.value = 0 - self.sliceEndOffsetSliderWidget.setToolTip("End slice sweep offset.") - inputFormLayout.addRow(self.sliceEndOffsetSliderLabel, self.sliceEndOffsetSliderWidget) - - # 3D rotation range - self.rotationSliderLabel = qt.QLabel("Rotation range:") - self.rotationSliderWidget = ctk.ctkRangeWidget() - self.rotationSliderWidget.singleStep = 5 - self.rotationSliderWidget.minimum = -180 - self.rotationSliderWidget.maximum = 180 - self.rotationSliderWidget.minimumValue = -180 - self.rotationSliderWidget.maximumValue = 180 - self.rotationSliderWidget.setToolTip("View rotation range, relative to current view orientation.") - inputFormLayout.addRow(self.rotationSliderLabel, self.rotationSliderWidget) - - # 3D rotation axis - self.rotationAxisLabel = qt.QLabel("Rotation axis:") - self.rotationAxisWidget = ctk.ctkRangeWidget() - self.rotationAxisWidget = qt.QComboBox() - self.rotationAxisWidget.addItem("Yaw", AXIS_YAW) - self.rotationAxisWidget.addItem("Pitch", AXIS_PITCH) - inputFormLayout.addRow(self.rotationAxisLabel, self.rotationAxisWidget) - - # Sequence browser node selector - self.sequenceBrowserNodeSelectorLabel = qt.QLabel("Sequence:") - self.sequenceBrowserNodeSelectorWidget = slicer.qMRMLNodeComboBox() - self.sequenceBrowserNodeSelectorWidget.nodeTypes = ["vtkMRMLSequenceBrowserNode"] - self.sequenceBrowserNodeSelectorWidget.addEnabled = False - self.sequenceBrowserNodeSelectorWidget.removeEnabled = False - self.sequenceBrowserNodeSelectorWidget.noneEnabled = False - self.sequenceBrowserNodeSelectorWidget.showHidden = False - self.sequenceBrowserNodeSelectorWidget.setMRMLScene( slicer.mrmlScene ) - self.sequenceBrowserNodeSelectorWidget.setToolTip( "Items defined by this sequence browser will be replayed." ) - inputFormLayout.addRow(self.sequenceBrowserNodeSelectorLabel, self.sequenceBrowserNodeSelectorWidget) - - # Sequence start index - self.sequenceStartItemIndexLabel = qt.QLabel("Start index:") - self.sequenceStartItemIndexWidget = ctk.ctkSliderWidget() - self.sequenceStartItemIndexWidget.minimum = 0 - self.sequenceStartItemIndexWidget.decimals = 0 - self.sequenceStartItemIndexWidget.setToolTip("First item in the sequence to capture.") - inputFormLayout.addRow(self.sequenceStartItemIndexLabel, self.sequenceStartItemIndexWidget) - - # Sequence end index - self.sequenceEndItemIndexLabel = qt.QLabel("End index:") - self.sequenceEndItemIndexWidget = ctk.ctkSliderWidget() - self.sequenceEndItemIndexWidget.minimum = 0 - self.sequenceEndItemIndexWidget.decimals = 0 - self.sequenceEndItemIndexWidget.setToolTip("Last item in the sequence to capture.") - inputFormLayout.addRow(self.sequenceEndItemIndexLabel, self.sequenceEndItemIndexWidget) - - # - # Output area - # - self.outputCollapsibleButton = ctk.ctkCollapsibleButton() - self.outputCollapsibleButton.text = "Output" - self.layout.addWidget(self.outputCollapsibleButton) - outputFormLayout = qt.QFormLayout(self.outputCollapsibleButton) - - self.outputTypeWidget = qt.QComboBox() - self.outputTypeWidget.setToolTip("Select how captured images will be saved. Video mode requires setting of ffmpeg executable path in Advanced section.") - self.outputTypeWidget.addItem("image series") - self.outputTypeWidget.addItem("video") - self.outputTypeWidget.addItem("lightbox image") - outputFormLayout.addRow("Output type:", self.outputTypeWidget) - - # Number of steps value - self.numberOfStepsSliderWidget = ctk.ctkSliderWidget() - self.numberOfStepsSliderWidget.singleStep = 1 - self.numberOfStepsSliderWidget.pageStep = 10 - self.numberOfStepsSliderWidget.minimum = 1 - self.numberOfStepsSliderWidget.maximum = 600 - self.numberOfStepsSliderWidget.value = 31 - self.numberOfStepsSliderWidget.decimals = 0 - self.numberOfStepsSliderWidget.setToolTip("Number of images extracted between start and stop positions.") - - # Single step toggle button - self.singleStepButton = qt.QToolButton() - self.singleStepButton.setText("single") - self.singleStepButton.setCheckable(True) - self.singleStepButton.toolTip = "Capture a single image of current state only.\n" + \ - "New filename is generated for each captured image (no files are overwritten)." - - hbox = qt.QHBoxLayout() - hbox.addWidget(self.singleStepButton) - hbox.addWidget(self.numberOfStepsSliderWidget) - outputFormLayout.addRow("Number of images:", hbox) - - # Output directory selector - self.outputDirSelector = ctk.ctkPathLineEdit() - self.outputDirSelector.filters = ctk.ctkPathLineEdit.Dirs - self.outputDirSelector.settingKey = 'ScreenCaptureOutputDir' - outputFormLayout.addRow("Output directory:", self.outputDirSelector) - if not self.outputDirSelector.currentPath: - defaultOutputPath = os.path.abspath(os.path.join(slicer.app.defaultScenePath,'SlicerCapture')) - self.outputDirSelector.setCurrentPath(defaultOutputPath) - - self.videoFileNameWidget = qt.QLineEdit() - self.videoFileNameWidget.setToolTip("String that defines file name and type.") - self.videoFileNameWidget.text = "SlicerCapture.avi" - self.videoFileNameWidget.setEnabled(False) - - self.lightboxImageFileNameWidget = qt.QLineEdit() - self.lightboxImageFileNameWidget.setToolTip("String that defines output lightbox file name and type.") - self.lightboxImageFileNameWidget.text = "SlicerCaptureLightbox.png" - self.lightboxImageFileNameWidget.setEnabled(False) - - hbox = qt.QHBoxLayout() - hbox.addWidget(self.videoFileNameWidget) - hbox.addWidget(self.lightboxImageFileNameWidget) - outputFormLayout.addRow("Output file name:", hbox) - - self.videoFormatWidget = qt.QComboBox() - self.videoFormatWidget.enabled = False - self.videoFormatWidget.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred) - for videoFormatPreset in self.logic.videoFormatPresets: - self.videoFormatWidget.addItem(videoFormatPreset["name"]) - outputFormLayout.addRow("Video format:", self.videoFormatWidget) - - self.videoLengthSliderWidget = ctk.ctkSliderWidget() - self.videoLengthSliderWidget.singleStep = 0.1 - self.videoLengthSliderWidget.minimum = 0.1 - self.videoLengthSliderWidget.maximum = 30 - self.videoLengthSliderWidget.value = 5 - self.videoLengthSliderWidget.suffix = "s" - self.videoLengthSliderWidget.decimals = 1 - self.videoLengthSliderWidget.setToolTip("Length of the exported video in seconds (without backward steps and repeating).") - self.videoLengthSliderWidget.setEnabled(False) - outputFormLayout.addRow("Video length:", self.videoLengthSliderWidget) - - self.videoFrameRateSliderWidget = ctk.ctkSliderWidget() - self.videoFrameRateSliderWidget.singleStep = 0.1 - self.videoFrameRateSliderWidget.minimum = 0.1 - self.videoFrameRateSliderWidget.maximum = 60 - self.videoFrameRateSliderWidget.value = 5.0 - self.videoFrameRateSliderWidget.suffix = "fps" - self.videoFrameRateSliderWidget.decimals = 3 - self.videoFrameRateSliderWidget.setToolTip("Frame rate in frames per second.") - self.videoFrameRateSliderWidget.setEnabled(False) - outputFormLayout.addRow("Video frame rate:", self.videoFrameRateSliderWidget) - - # - # Advanced area - # - self.advancedCollapsibleButton = ctk.ctkCollapsibleButton() - self.advancedCollapsibleButton.text = "Advanced" - self.advancedCollapsibleButton.collapsed = True - outputFormLayout.addRow(self.advancedCollapsibleButton) - advancedFormLayout = qt.QFormLayout(self.advancedCollapsibleButton) - - self.forwardBackwardCheckBox = qt.QCheckBox(" ") - self.forwardBackwardCheckBox.checked = False - self.forwardBackwardCheckBox.setToolTip("If checked, image series will be generated playing forward and then backward.") - advancedFormLayout.addRow("Forward-backward:", self.forwardBackwardCheckBox) - - self.repeatSliderWidget = ctk.ctkSliderWidget() - self.repeatSliderWidget.decimals = 0 - self.repeatSliderWidget.singleStep = 1 - self.repeatSliderWidget.minimum = 1 - self.repeatSliderWidget.maximum = 50 - self.repeatSliderWidget.value = 1 - self.repeatSliderWidget.setToolTip("Number of times image series are repeated. Useful for making short videos longer for playback in software" - " that does not support looped playback.") - advancedFormLayout.addRow("Repeat:", self.repeatSliderWidget) - - ffmpegPath = self.logic.getFfmpegPath() - self.ffmpegPathSelector = ctk.ctkPathLineEdit() - self.ffmpegPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength - self.ffmpegPathSelector.setCurrentPath(ffmpegPath) - self.ffmpegPathSelector.nameFilters = [self.logic.getFfmpegExecutableFilename()] - self.ffmpegPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) - self.ffmpegPathSelector.setToolTip("Set the path to ffmpeg executable. Download from: https://www.ffmpeg.org/") - advancedFormLayout.addRow("ffmpeg executable:", self.ffmpegPathSelector) - - self.videoExportFfmpegWarning = qt.QLabel('Set valid ffmpeg executable path! '+ - 'Help...') - self.videoExportFfmpegWarning.connect('linkActivated(QString)', self.openURL) - self.videoExportFfmpegWarning.setVisible(False) - advancedFormLayout.addRow("", self.videoExportFfmpegWarning) - - self.extraVideoOptionsWidget = qt.QLineEdit() - self.extraVideoOptionsWidget.setToolTip('Additional video conversion options passed to ffmpeg. Parameters -i (input files), -y' - +'(overwrite without asking), -r (frame rate), -start_number are specified by the module and therefore' - +'should not be included in this list.') - advancedFormLayout.addRow("Video extra options:", self.extraVideoOptionsWidget) - - self.fileNamePatternWidget = qt.QLineEdit() - self.fileNamePatternWidget.setToolTip( - "String that defines file name, type, and numbering scheme. Default: image%05d.png.") - self.fileNamePatternWidget.text = "image_%05d.png" - advancedFormLayout.addRow("Image file name pattern:", self.fileNamePatternWidget) - - self.lightboxColumnCountSliderWidget = ctk.ctkSliderWidget() - self.lightboxColumnCountSliderWidget.decimals = 0 - self.lightboxColumnCountSliderWidget.singleStep = 1 - self.lightboxColumnCountSliderWidget.minimum = 1 - self.lightboxColumnCountSliderWidget.maximum = 20 - self.lightboxColumnCountSliderWidget.value = 6 - self.lightboxColumnCountSliderWidget.setToolTip("Number of columns in lightbox image") - advancedFormLayout.addRow("Lightbox image columns:", self.lightboxColumnCountSliderWidget) - - self.maxFramesWidget = qt.QSpinBox() - self.maxFramesWidget.setRange(1, 9999) - self.maxFramesWidget.setValue(600) - self.maxFramesWidget.setToolTip( - "Maximum number of images to be captured (without backward steps and repeating).") - advancedFormLayout.addRow("Maximum number of images:", self.maxFramesWidget) - - self.volumeNodeComboBox = slicer.qMRMLNodeComboBox() - self.volumeNodeComboBox.nodeTypes = ["vtkMRMLVectorVolumeNode"] - self.volumeNodeComboBox.baseName = "Screenshot" - self.volumeNodeComboBox.renameEnabled = True - self.volumeNodeComboBox.noneEnabled = True - self.volumeNodeComboBox.setToolTip("Select a volume node to store the captured image in the scene instead of just writing immediately to disk. Requires output 'Number of images' to be set to 1.") - self.volumeNodeComboBox.setMRMLScene(slicer.mrmlScene) - advancedFormLayout.addRow("Output volume node:", self.volumeNodeComboBox) - - self.showViewControllersCheckBox = qt.QCheckBox(" ") - self.showViewControllersCheckBox.checked = False - self.showViewControllersCheckBox.setToolTip("If checked, images will be captured with view controllers visible.") - advancedFormLayout.addRow("View controllers:", self.showViewControllersCheckBox) - - self.transparentBackgroundCheckBox = qt.QCheckBox(" ") - self.transparentBackgroundCheckBox.checked = False - self.transparentBackgroundCheckBox.setToolTip("If checked, images will be captured with transparent background.") - advancedFormLayout.addRow("Transparent background:", self.transparentBackgroundCheckBox) - - watermarkEnabled = slicer.util.settingsValue('ScreenCapture/WatermarkEnabled', False, converter=slicer.util.toBool) - - self.watermarkEnabledCheckBox = qt.QCheckBox(" ") - self.watermarkEnabledCheckBox.checked = watermarkEnabled - self.watermarkEnabledCheckBox.setToolTip("If checked, selected watermark image will be added to all exported images.") - - self.watermarkPositionWidget = qt.QComboBox() - self.watermarkPositionWidget.enabled = watermarkEnabled - self.watermarkPositionWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) - self.watermarkPositionWidget.setToolTip("Add a watermark image to all exported images.") - for watermarkPositionPreset in self.logic.watermarkPositionPresets: - self.watermarkPositionWidget.addItem(watermarkPositionPreset["name"]) - self.watermarkPositionWidget.setCurrentText( - slicer.util.settingsValue('ScreenCapture/WatermarkPosition', self.logic.watermarkPositionPresets[0]["name"])) - - self.watermarkSizeSliderWidget = qt.QSpinBox() - self.watermarkSizeSliderWidget.enabled = watermarkEnabled - self.watermarkSizeSliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) - self.watermarkSizeSliderWidget.singleStep = 10 - self.watermarkSizeSliderWidget.minimum = 10 - self.watermarkSizeSliderWidget.maximum = 1000 - self.watermarkSizeSliderWidget.value = 100 - self.watermarkSizeSliderWidget.suffix = "%" - self.watermarkSizeSliderWidget.setToolTip("Size scaling applied to the watermark image. 100% is original size") - try: - self.watermarkSizeSliderWidget.value = int(slicer.util.settingsValue('ScreenCapture/WatermarkSize', 100)) - except: - pass - - self.watermarkOpacitySliderWidget = qt.QSpinBox() - self.watermarkOpacitySliderWidget.enabled = watermarkEnabled - self.watermarkOpacitySliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) - self.watermarkOpacitySliderWidget.singleStep = 10 - self.watermarkOpacitySliderWidget.minimum = 0 - self.watermarkOpacitySliderWidget.maximum = 100 - self.watermarkOpacitySliderWidget.value = 30 - self.watermarkOpacitySliderWidget.suffix = "%" - self.watermarkOpacitySliderWidget.setToolTip("Opacity of the watermark image. 100% is fully opaque.") - try: - self.watermarkOpacitySliderWidget.value = int(slicer.util.settingsValue('ScreenCapture/WatermarkOpacity', 30)) - except: - pass - - self.watermarkPathSelector = ctk.ctkPathLineEdit() - self.watermarkPathSelector.enabled = watermarkEnabled - self.watermarkPathSelector.settingKey = 'ScreenCaptureWatermarkImagePath' - self.watermarkPathSelector.nameFilters = ["*.png"] - self.watermarkPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength - self.watermarkPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) - self.watermarkPathSelector.setToolTip("Watermark image file in png format") - - hbox = qt.QHBoxLayout() - hbox.addWidget(self.watermarkEnabledCheckBox) - hbox.addWidget(qt.QLabel("Position:")) - hbox.addWidget(self.watermarkPositionWidget) - hbox.addWidget(qt.QLabel("Size:")) - hbox.addWidget(self.watermarkSizeSliderWidget) - hbox.addWidget(qt.QLabel("Opacity:")) - hbox.addWidget(self.watermarkOpacitySliderWidget) - #hbox.addStretch() - advancedFormLayout.addRow("Watermark image:", hbox) - - hbox = qt.QHBoxLayout() - hbox.addWidget(self.watermarkPathSelector) - advancedFormLayout.addRow("", hbox) - - # Capture button - self.captureButtonLabelCapture = "Capture" - self.captureButtonLabelCancel = "Cancel" - self.captureButton = qt.QPushButton(self.captureButtonLabelCapture) - self.captureButton.toolTip = "Capture slice sweep to image sequence." - self.showCreatedOutputFileButton = qt.QPushButton() - self.showCreatedOutputFileButton.setIcon(qt.QIcon(':Icons/Go.png')) - self.showCreatedOutputFileButton.setMaximumWidth(60) - self.showCreatedOutputFileButton.enabled = False - self.showCreatedOutputFileButton.toolTip = "Show created output file." - hbox = qt.QHBoxLayout() - hbox.addWidget(self.captureButton) - hbox.addWidget(self.showCreatedOutputFileButton) - self.layout.addLayout(hbox) - - self.statusLabel = qt.QPlainTextEdit() - self.statusLabel.setTextInteractionFlags(qt.Qt.TextSelectableByMouse) - self.statusLabel.setCenterOnScroll(True) - self.layout.addWidget(self.statusLabel) - - # - # Add vertical spacer - # self.layout.addStretch(1) - - # connections - self.captureButton.connect('clicked(bool)', self.onCaptureButton) - self.showCreatedOutputFileButton.connect('clicked(bool)', self.onShowCreatedOutputFile) - self.viewNodeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions) - self.animationModeWidget.connect("currentIndexChanged(int)", self.updateViewOptions) - self.sliceStartOffsetSliderWidget.connect('valueChanged(double)', self.setSliceOffset) - self.sliceEndOffsetSliderWidget.connect('valueChanged(double)', self.setSliceOffset) - self.sequenceBrowserNodeSelectorWidget.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions) - self.sequenceStartItemIndexWidget.connect('valueChanged(double)', self.setSequenceItemIndex) - self.sequenceEndItemIndexWidget.connect('valueChanged(double)', self.setSequenceItemIndex) - self.outputTypeWidget.connect('currentIndexChanged(int)', self.updateOutputType) - self.videoFormatWidget.connect("currentIndexChanged(int)", self.updateVideoFormat) - self.maxFramesWidget.connect('valueChanged(int)', self.maxFramesChanged) - self.videoLengthSliderWidget.connect('valueChanged(double)', self.setVideoLength) - self.videoFrameRateSliderWidget.connect('valueChanged(double)', self.setVideoFrameRate) - self.singleStepButton.connect('toggled(bool)', self.setForceSingleStep) - self.numberOfStepsSliderWidget.connect('valueChanged(double)', self.setNumberOfSteps) - self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkPositionWidget, 'setEnabled(bool)') - self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkSizeSliderWidget, 'setEnabled(bool)') - self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkPathSelector, 'setEnabled(bool)') - - self.setVideoLength() # update frame rate based on video length - self.updateOutputType() - self.updateVideoFormat(0) - self.updateViewOptions() - - def maxFramesChanged(self): - self.numberOfStepsSliderWidget.maximum = self.maxFramesWidget.value - - def openURL(self, URL): - qt.QDesktopServices().openUrl(qt.QUrl(URL)) - QDesktopServices - - def onShowCreatedOutputFile(self): - if not self.createdOutputFile: - return - qt.QDesktopServices().openUrl(qt.QUrl("file:///"+self.createdOutputFile, qt.QUrl.TolerantMode)) - - def updateOutputType(self, selectionIndex=0): - isVideo = self.outputTypeWidget.currentText == "video" - isLightbox = self.outputTypeWidget.currentText == "lightbox image" - self.fileNamePatternWidget.enabled = not (isVideo or isLightbox) - self.videoFileNameWidget.enabled = isVideo - self.videoFormatWidget.enabled = isVideo - self.videoLengthSliderWidget.enabled = isVideo - self.videoFrameRateSliderWidget.enabled = isVideo - self.videoFileNameWidget.setVisible(not isLightbox) - self.videoFileNameWidget.enabled = isVideo - self.lightboxImageFileNameWidget.setVisible(isLightbox) - self.lightboxImageFileNameWidget.enabled = isLightbox - - def updateVideoFormat(self, selectionIndex): - videoFormatPreset = self.logic.videoFormatPresets[selectionIndex] - - import os - filenameExt = os.path.splitext(self.videoFileNameWidget.text) - self.videoFileNameWidget.text = filenameExt[0] + "." + videoFormatPreset["fileExtension"] - - self.extraVideoOptionsWidget.text = videoFormatPreset["extraVideoOptions"] - - def currentViewNodeType(self): - viewNode = self.viewNodeSelector.currentNode() - if not viewNode: - return None - elif viewNode.IsA("vtkMRMLSliceNode"): - return VIEW_SLICE - elif viewNode.IsA("vtkMRMLViewNode"): - return VIEW_3D - else: - return None - - def addLog(self, text): - """Append text to log window + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - self.statusLabel.appendPlainText(text) - self.statusLabel.ensureCursorVisible() - slicer.app.processEvents() # force update - - def cleanup(self): - pass - - def updateViewOptions(self): - - sequencesModuleAvailable = hasattr(slicer.modules, 'sequences') - - if self.viewNodeType != self.currentViewNodeType(): - self.viewNodeType = self.currentViewNodeType() - - self.animationModeWidget.clear() - if self.viewNodeType == VIEW_SLICE: - self.animationModeWidget.addItem("slice sweep") - self.animationModeWidget.addItem("slice fade") - if self.viewNodeType == VIEW_3D: - self.animationModeWidget.addItem("3D rotation") - if sequencesModuleAvailable: - self.animationModeWidget.addItem("sequence") - - if self.animationMode != self.animationModeWidget.currentText: - self.animationMode = self.animationModeWidget.currentText - - # slice sweep - self.sliceStartOffsetSliderLabel.visible = (self.animationMode == "slice sweep") - self.sliceStartOffsetSliderWidget.visible = (self.animationMode == "slice sweep") - self.sliceEndOffsetSliderLabel.visible = (self.animationMode == "slice sweep") - self.sliceEndOffsetSliderWidget.visible = (self.animationMode == "slice sweep") - if self.animationMode == "slice sweep": - offsetResolution = self.logic.getSliceOffsetResolution(self.viewNodeSelector.currentNode()) - sliceOffsetMin, sliceOffsetMax = self.logic.getSliceOffsetRange(self.viewNodeSelector.currentNode()) - - wasBlocked = self.sliceStartOffsetSliderWidget.blockSignals(True) - self.sliceStartOffsetSliderWidget.singleStep = offsetResolution - self.sliceStartOffsetSliderWidget.minimum = sliceOffsetMin - self.sliceStartOffsetSliderWidget.maximum = sliceOffsetMax - self.sliceStartOffsetSliderWidget.value = sliceOffsetMin - self.sliceStartOffsetSliderWidget.blockSignals(wasBlocked) - - wasBlocked = self.sliceEndOffsetSliderWidget.blockSignals(True) - self.sliceEndOffsetSliderWidget.singleStep = offsetResolution - self.sliceEndOffsetSliderWidget.minimum = sliceOffsetMin - self.sliceEndOffsetSliderWidget.maximum = sliceOffsetMax - self.sliceEndOffsetSliderWidget.value = sliceOffsetMax - self.sliceEndOffsetSliderWidget.blockSignals(wasBlocked) - - # 3D rotation - self.rotationSliderLabel.visible = (self.animationMode == "3D rotation") - self.rotationSliderWidget.visible = (self.animationMode == "3D rotation") - self.rotationAxisLabel.visible = (self.animationMode == "3D rotation") - self.rotationAxisWidget.visible = (self.animationMode == "3D rotation") - - # Sequence - self.sequenceBrowserNodeSelectorLabel.visible = (self.animationMode == "sequence") - self.sequenceBrowserNodeSelectorWidget.visible = (self.animationMode == "sequence") - self.sequenceStartItemIndexLabel.visible = (self.animationMode == "sequence") - self.sequenceStartItemIndexWidget.visible = (self.animationMode == "sequence") - self.sequenceEndItemIndexLabel.visible = (self.animationMode == "sequence") - self.sequenceEndItemIndexWidget.visible = (self.animationMode == "sequence") - if self.animationMode == "sequence": - sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode() - - sequenceItemCount = 0 - if sequenceBrowserNode and sequenceBrowserNode.GetMasterSequenceNode(): - sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes() - - if sequenceItemCount>0: - wasBlocked = self.sequenceStartItemIndexWidget.blockSignals(True) - self.sequenceStartItemIndexWidget.maximum = sequenceItemCount-1 - self.sequenceStartItemIndexWidget.value = 0 - self.sequenceStartItemIndexWidget.blockSignals(wasBlocked) - - wasBlocked = self.sequenceEndItemIndexWidget.blockSignals(True) - self.sequenceEndItemIndexWidget.maximum = sequenceItemCount-1 - self.sequenceEndItemIndexWidget.value = sequenceItemCount-1 - self.sequenceEndItemIndexWidget.blockSignals(wasBlocked) - - self.sequenceStartItemIndexWidget.enabled = sequenceItemCount>0 - self.sequenceEndItemIndexWidget.enabled = sequenceItemCount>0 - - numberOfSteps = int(self.numberOfStepsSliderWidget.value) - forceSingleStep = self.singleStepButton.checked - if forceSingleStep: - numberOfSteps = 1 - self.numberOfStepsSliderWidget.setDisabled(forceSingleStep) - self.forwardBackwardCheckBox.enabled = (numberOfSteps > 1) - self.repeatSliderWidget.enabled = (numberOfSteps > 1) - self.volumeNodeComboBox.setEnabled(numberOfSteps == 1) - - def setSliceOffset(self, offset): - sliceLogic = self.logic.getSliceLogicFromSliceNode(self.viewNodeSelector.currentNode()) - sliceLogic.SetSliceOffset(offset) - - def setVideoLength(self, lengthSec=None): - wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True) - self.videoFrameRateSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoLengthSliderWidget.value - self.videoFrameRateSliderWidget.blockSignals(wasBlocked) - - def setVideoFrameRate(self, frameRateFps): - wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True) - self.videoLengthSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoFrameRateSliderWidget.value - self.videoFrameRateSliderWidget.blockSignals(wasBlocked) - - def setNumberOfSteps(self, steps): - self.setVideoLength() - self.updateViewOptions() - - def setForceSingleStep(self, force): - self.updateViewOptions() - - def setSequenceItemIndex(self, index): - sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode() - sequenceBrowserNode.SetSelectedItemNumber(int(index)) - - def enableInputOutputWidgets(self, enable): - self.inputCollapsibleButton.setEnabled(enable) - self.outputCollapsibleButton.setEnabled(enable) - - def onCaptureButton(self): - - # Disable capture button to prevent multiple clicks - self.captureButton.setEnabled(False) - self.enableInputOutputWidgets(False) - slicer.app.processEvents() - - if self.captureButton.text == self.captureButtonLabelCancel: - self.logic.requestCancel() - return - - self.logic.setFfmpegPath(self.ffmpegPathSelector.currentPath) - - qt.QSettings().setValue('ScreenCapture/WatermarkEnabled', bool(self.watermarkEnabledCheckBox.checked)) - qt.QSettings().setValue('ScreenCapture/WatermarkPosition', self.watermarkPositionWidget.currentText) - qt.QSettings().setValue('ScreenCapture/WatermarkOpacity', self.watermarkOpacitySliderWidget.value) - qt.QSettings().setValue('ScreenCapture/WatermarkSize', self.watermarkSizeSliderWidget.value) - - if self.watermarkEnabledCheckBox.checked: - if self.watermarkPathSelector.currentPath: - self.logic.setWatermarkImagePath(self.watermarkPathSelector.currentPath) - self.watermarkPathSelector.addCurrentPathToHistory() - else: - self.logic.setWatermarkImagePath(self.resourcePath('SlicerWatermark.png')) - self.logic.setWatermarkPosition(self.watermarkPositionWidget.currentIndex) - self.logic.setWatermarkSizePercent(self.watermarkSizeSliderWidget.value) - self.logic.setWatermarkOpacityPercent(self.watermarkOpacitySliderWidget.value) - else: - self.logic.setWatermarkPosition(-1) - - self.statusLabel.plainText = '' - - videoOutputRequested = (self.outputTypeWidget.currentText == "video") - viewNode = self.viewNodeSelector.currentNode() - numberOfSteps = int(self.numberOfStepsSliderWidget.value) - if self.singleStepButton.checked: - numberOfSteps = 1 - if numberOfSteps < 2: - # If a single image is selected - videoOutputRequested = False - outputDir = self.outputDirSelector.currentPath - self.outputDirSelector.addCurrentPathToHistory() - - self.videoExportFfmpegWarning.setVisible(False) - if videoOutputRequested: - if not self.logic.isFfmpegPathValid(): - # ffmpeg not found, try to automatically find it at common locations - self.logic.findFfmpeg() - if not self.logic.isFfmpegPathValid() and os.name == 'nt': # TODO: implement download for Linux/MacOS? - # ffmpeg not found, offer downloading it - if slicer.util.confirmOkCancelDisplay( - 'Video encoder not detected on your system. ' - 'Download ffmpeg video encoder?', - windowTitle='Download confirmation'): - if not self.logic.ffmpegDownload(): - slicer.util.errorDisplay("ffmpeg download failed") - if not self.logic.isFfmpegPathValid(): - # still not found, user has to specify path manually - self.videoExportFfmpegWarning.setVisible(True) - self.advancedCollapsibleButton.collapsed = False - self.captureButton.setEnabled(True) - self.enableInputOutputWidgets(True) - return - self.ffmpegPathSelector.currentPath = self.logic.getFfmpegPath() - - # Need to create a new random file pattern if video output is requested to make sure that new image files are not mixed up with - # existing files in the output directory - imageFileNamePattern = self.fileNamePatternWidget.text if (self.outputTypeWidget.currentText == "image series") else self.logic.getRandomFilePattern() - - self.captureButton.setEnabled(True) - self.captureButton.text = self.captureButtonLabelCancel - slicer.app.setOverrideCursor(qt.Qt.WaitCursor) - captureAllViews = self.captureAllViewsCheckBox.checked - transparentBackground = self.transparentBackgroundCheckBox.checked - showViewControllers = self.showViewControllersCheckBox.checked - if captureAllViews: - self.logic.showViewControllers(showViewControllers) - elif showViewControllers: - logging.warning("View controllers are only available to be shown when capturing all views.") - try: - if numberOfSteps < 2: - if imageFileNamePattern != self.snapshotFileNamePattern or outputDir != self.snapshotOutputDir: - self.snapshotIndex = 0 - if outputDir: - [filename, self.snapshotIndex] = self.logic.getNextAvailableFileName(outputDir, imageFileNamePattern, self.snapshotIndex) + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + self.logic = ScreenCaptureLogic() + self.logic.logCallback = self.addLog + self.viewNodeType = None + self.animationMode = None + self.createdOutputFile = None + + self.snapshotIndex = 0 # this counter is used for determining file names for single-image snapshots + self.snapshotOutputDir = None + self.snapshotFileNamePattern = None + + # Instantiate and connect widgets ... + + # + # Input area + # + self.inputCollapsibleButton = ctk.ctkCollapsibleButton() + self.inputCollapsibleButton.text = "Input" + self.layout.addWidget(self.inputCollapsibleButton) + inputFormLayout = qt.QFormLayout(self.inputCollapsibleButton) + + # Input view selector + self.viewNodeSelector = slicer.qMRMLNodeComboBox() + self.viewNodeSelector.nodeTypes = ["vtkMRMLSliceNode", "vtkMRMLViewNode"] + self.viewNodeSelector.addEnabled = False + self.viewNodeSelector.removeEnabled = False + self.viewNodeSelector.noneEnabled = False + self.viewNodeSelector.showHidden = False + self.viewNodeSelector.showChildNodeTypes = False + self.viewNodeSelector.setMRMLScene(slicer.mrmlScene) + self.viewNodeSelector.setToolTip("This slice or 3D view will be updated during capture." + "Only this view will be captured unless 'Capture of all views' option in output section is enabled.") + inputFormLayout.addRow("Master view: ", self.viewNodeSelector) + + self.captureAllViewsCheckBox = qt.QCheckBox(" ") + self.captureAllViewsCheckBox.checked = False + self.captureAllViewsCheckBox.setToolTip("If checked, all views will be captured. If unchecked then only the selected view will be captured.") + inputFormLayout.addRow("Capture all views:", self.captureAllViewsCheckBox) + + # Mode + self.animationModeWidget = qt.QComboBox() + self.animationModeWidget.setToolTip("Select the property that will be adjusted") + inputFormLayout.addRow("Animation mode:", self.animationModeWidget) + + # Slice start offset position + self.sliceStartOffsetSliderLabel = qt.QLabel("Start sweep offset:") + self.sliceStartOffsetSliderWidget = ctk.ctkSliderWidget() + self.sliceStartOffsetSliderWidget.singleStep = 30 + self.sliceStartOffsetSliderWidget.minimum = -100 + self.sliceStartOffsetSliderWidget.maximum = 100 + self.sliceStartOffsetSliderWidget.value = 0 + self.sliceStartOffsetSliderWidget.setToolTip("Start slice sweep offset.") + inputFormLayout.addRow(self.sliceStartOffsetSliderLabel, self.sliceStartOffsetSliderWidget) + + # Slice end offset position + self.sliceEndOffsetSliderLabel = qt.QLabel("End sweep offset:") + self.sliceEndOffsetSliderWidget = ctk.ctkSliderWidget() + self.sliceEndOffsetSliderWidget.singleStep = 5 + self.sliceEndOffsetSliderWidget.minimum = -100 + self.sliceEndOffsetSliderWidget.maximum = 100 + self.sliceEndOffsetSliderWidget.value = 0 + self.sliceEndOffsetSliderWidget.setToolTip("End slice sweep offset.") + inputFormLayout.addRow(self.sliceEndOffsetSliderLabel, self.sliceEndOffsetSliderWidget) + + # 3D rotation range + self.rotationSliderLabel = qt.QLabel("Rotation range:") + self.rotationSliderWidget = ctk.ctkRangeWidget() + self.rotationSliderWidget.singleStep = 5 + self.rotationSliderWidget.minimum = -180 + self.rotationSliderWidget.maximum = 180 + self.rotationSliderWidget.minimumValue = -180 + self.rotationSliderWidget.maximumValue = 180 + self.rotationSliderWidget.setToolTip("View rotation range, relative to current view orientation.") + inputFormLayout.addRow(self.rotationSliderLabel, self.rotationSliderWidget) + + # 3D rotation axis + self.rotationAxisLabel = qt.QLabel("Rotation axis:") + self.rotationAxisWidget = ctk.ctkRangeWidget() + self.rotationAxisWidget = qt.QComboBox() + self.rotationAxisWidget.addItem("Yaw", AXIS_YAW) + self.rotationAxisWidget.addItem("Pitch", AXIS_PITCH) + inputFormLayout.addRow(self.rotationAxisLabel, self.rotationAxisWidget) + + # Sequence browser node selector + self.sequenceBrowserNodeSelectorLabel = qt.QLabel("Sequence:") + self.sequenceBrowserNodeSelectorWidget = slicer.qMRMLNodeComboBox() + self.sequenceBrowserNodeSelectorWidget.nodeTypes = ["vtkMRMLSequenceBrowserNode"] + self.sequenceBrowserNodeSelectorWidget.addEnabled = False + self.sequenceBrowserNodeSelectorWidget.removeEnabled = False + self.sequenceBrowserNodeSelectorWidget.noneEnabled = False + self.sequenceBrowserNodeSelectorWidget.showHidden = False + self.sequenceBrowserNodeSelectorWidget.setMRMLScene(slicer.mrmlScene) + self.sequenceBrowserNodeSelectorWidget.setToolTip("Items defined by this sequence browser will be replayed.") + inputFormLayout.addRow(self.sequenceBrowserNodeSelectorLabel, self.sequenceBrowserNodeSelectorWidget) + + # Sequence start index + self.sequenceStartItemIndexLabel = qt.QLabel("Start index:") + self.sequenceStartItemIndexWidget = ctk.ctkSliderWidget() + self.sequenceStartItemIndexWidget.minimum = 0 + self.sequenceStartItemIndexWidget.decimals = 0 + self.sequenceStartItemIndexWidget.setToolTip("First item in the sequence to capture.") + inputFormLayout.addRow(self.sequenceStartItemIndexLabel, self.sequenceStartItemIndexWidget) + + # Sequence end index + self.sequenceEndItemIndexLabel = qt.QLabel("End index:") + self.sequenceEndItemIndexWidget = ctk.ctkSliderWidget() + self.sequenceEndItemIndexWidget.minimum = 0 + self.sequenceEndItemIndexWidget.decimals = 0 + self.sequenceEndItemIndexWidget.setToolTip("Last item in the sequence to capture.") + inputFormLayout.addRow(self.sequenceEndItemIndexLabel, self.sequenceEndItemIndexWidget) + + # + # Output area + # + self.outputCollapsibleButton = ctk.ctkCollapsibleButton() + self.outputCollapsibleButton.text = "Output" + self.layout.addWidget(self.outputCollapsibleButton) + outputFormLayout = qt.QFormLayout(self.outputCollapsibleButton) + + self.outputTypeWidget = qt.QComboBox() + self.outputTypeWidget.setToolTip("Select how captured images will be saved. Video mode requires setting of ffmpeg executable path in Advanced section.") + self.outputTypeWidget.addItem("image series") + self.outputTypeWidget.addItem("video") + self.outputTypeWidget.addItem("lightbox image") + outputFormLayout.addRow("Output type:", self.outputTypeWidget) + + # Number of steps value + self.numberOfStepsSliderWidget = ctk.ctkSliderWidget() + self.numberOfStepsSliderWidget.singleStep = 1 + self.numberOfStepsSliderWidget.pageStep = 10 + self.numberOfStepsSliderWidget.minimum = 1 + self.numberOfStepsSliderWidget.maximum = 600 + self.numberOfStepsSliderWidget.value = 31 + self.numberOfStepsSliderWidget.decimals = 0 + self.numberOfStepsSliderWidget.setToolTip("Number of images extracted between start and stop positions.") + + # Single step toggle button + self.singleStepButton = qt.QToolButton() + self.singleStepButton.setText("single") + self.singleStepButton.setCheckable(True) + self.singleStepButton.toolTip = "Capture a single image of current state only.\n" + \ + "New filename is generated for each captured image (no files are overwritten)." + + hbox = qt.QHBoxLayout() + hbox.addWidget(self.singleStepButton) + hbox.addWidget(self.numberOfStepsSliderWidget) + outputFormLayout.addRow("Number of images:", hbox) + + # Output directory selector + self.outputDirSelector = ctk.ctkPathLineEdit() + self.outputDirSelector.filters = ctk.ctkPathLineEdit.Dirs + self.outputDirSelector.settingKey = 'ScreenCaptureOutputDir' + outputFormLayout.addRow("Output directory:", self.outputDirSelector) + if not self.outputDirSelector.currentPath: + defaultOutputPath = os.path.abspath(os.path.join(slicer.app.defaultScenePath, 'SlicerCapture')) + self.outputDirSelector.setCurrentPath(defaultOutputPath) + + self.videoFileNameWidget = qt.QLineEdit() + self.videoFileNameWidget.setToolTip("String that defines file name and type.") + self.videoFileNameWidget.text = "SlicerCapture.avi" + self.videoFileNameWidget.setEnabled(False) + + self.lightboxImageFileNameWidget = qt.QLineEdit() + self.lightboxImageFileNameWidget.setToolTip("String that defines output lightbox file name and type.") + self.lightboxImageFileNameWidget.text = "SlicerCaptureLightbox.png" + self.lightboxImageFileNameWidget.setEnabled(False) + + hbox = qt.QHBoxLayout() + hbox.addWidget(self.videoFileNameWidget) + hbox.addWidget(self.lightboxImageFileNameWidget) + outputFormLayout.addRow("Output file name:", hbox) + + self.videoFormatWidget = qt.QComboBox() + self.videoFormatWidget.enabled = False + self.videoFormatWidget.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred) + for videoFormatPreset in self.logic.videoFormatPresets: + self.videoFormatWidget.addItem(videoFormatPreset["name"]) + outputFormLayout.addRow("Video format:", self.videoFormatWidget) + + self.videoLengthSliderWidget = ctk.ctkSliderWidget() + self.videoLengthSliderWidget.singleStep = 0.1 + self.videoLengthSliderWidget.minimum = 0.1 + self.videoLengthSliderWidget.maximum = 30 + self.videoLengthSliderWidget.value = 5 + self.videoLengthSliderWidget.suffix = "s" + self.videoLengthSliderWidget.decimals = 1 + self.videoLengthSliderWidget.setToolTip("Length of the exported video in seconds (without backward steps and repeating).") + self.videoLengthSliderWidget.setEnabled(False) + outputFormLayout.addRow("Video length:", self.videoLengthSliderWidget) + + self.videoFrameRateSliderWidget = ctk.ctkSliderWidget() + self.videoFrameRateSliderWidget.singleStep = 0.1 + self.videoFrameRateSliderWidget.minimum = 0.1 + self.videoFrameRateSliderWidget.maximum = 60 + self.videoFrameRateSliderWidget.value = 5.0 + self.videoFrameRateSliderWidget.suffix = "fps" + self.videoFrameRateSliderWidget.decimals = 3 + self.videoFrameRateSliderWidget.setToolTip("Frame rate in frames per second.") + self.videoFrameRateSliderWidget.setEnabled(False) + outputFormLayout.addRow("Video frame rate:", self.videoFrameRateSliderWidget) + + # + # Advanced area + # + self.advancedCollapsibleButton = ctk.ctkCollapsibleButton() + self.advancedCollapsibleButton.text = "Advanced" + self.advancedCollapsibleButton.collapsed = True + outputFormLayout.addRow(self.advancedCollapsibleButton) + advancedFormLayout = qt.QFormLayout(self.advancedCollapsibleButton) + + self.forwardBackwardCheckBox = qt.QCheckBox(" ") + self.forwardBackwardCheckBox.checked = False + self.forwardBackwardCheckBox.setToolTip("If checked, image series will be generated playing forward and then backward.") + advancedFormLayout.addRow("Forward-backward:", self.forwardBackwardCheckBox) + + self.repeatSliderWidget = ctk.ctkSliderWidget() + self.repeatSliderWidget.decimals = 0 + self.repeatSliderWidget.singleStep = 1 + self.repeatSliderWidget.minimum = 1 + self.repeatSliderWidget.maximum = 50 + self.repeatSliderWidget.value = 1 + self.repeatSliderWidget.setToolTip("Number of times image series are repeated. Useful for making short videos longer for playback in software" + " that does not support looped playback.") + advancedFormLayout.addRow("Repeat:", self.repeatSliderWidget) + + ffmpegPath = self.logic.getFfmpegPath() + self.ffmpegPathSelector = ctk.ctkPathLineEdit() + self.ffmpegPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength + self.ffmpegPathSelector.setCurrentPath(ffmpegPath) + self.ffmpegPathSelector.nameFilters = [self.logic.getFfmpegExecutableFilename()] + self.ffmpegPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) + self.ffmpegPathSelector.setToolTip("Set the path to ffmpeg executable. Download from: https://www.ffmpeg.org/") + advancedFormLayout.addRow("ffmpeg executable:", self.ffmpegPathSelector) + + self.videoExportFfmpegWarning = qt.QLabel('Set valid ffmpeg executable path! ' + + 'Help...') + self.videoExportFfmpegWarning.connect('linkActivated(QString)', self.openURL) + self.videoExportFfmpegWarning.setVisible(False) + advancedFormLayout.addRow("", self.videoExportFfmpegWarning) + + self.extraVideoOptionsWidget = qt.QLineEdit() + self.extraVideoOptionsWidget.setToolTip('Additional video conversion options passed to ffmpeg. Parameters -i (input files), -y' + + '(overwrite without asking), -r (frame rate), -start_number are specified by the module and therefore' + + 'should not be included in this list.') + advancedFormLayout.addRow("Video extra options:", self.extraVideoOptionsWidget) + + self.fileNamePatternWidget = qt.QLineEdit() + self.fileNamePatternWidget.setToolTip( + "String that defines file name, type, and numbering scheme. Default: image%05d.png.") + self.fileNamePatternWidget.text = "image_%05d.png" + advancedFormLayout.addRow("Image file name pattern:", self.fileNamePatternWidget) + + self.lightboxColumnCountSliderWidget = ctk.ctkSliderWidget() + self.lightboxColumnCountSliderWidget.decimals = 0 + self.lightboxColumnCountSliderWidget.singleStep = 1 + self.lightboxColumnCountSliderWidget.minimum = 1 + self.lightboxColumnCountSliderWidget.maximum = 20 + self.lightboxColumnCountSliderWidget.value = 6 + self.lightboxColumnCountSliderWidget.setToolTip("Number of columns in lightbox image") + advancedFormLayout.addRow("Lightbox image columns:", self.lightboxColumnCountSliderWidget) + + self.maxFramesWidget = qt.QSpinBox() + self.maxFramesWidget.setRange(1, 9999) + self.maxFramesWidget.setValue(600) + self.maxFramesWidget.setToolTip( + "Maximum number of images to be captured (without backward steps and repeating).") + advancedFormLayout.addRow("Maximum number of images:", self.maxFramesWidget) + + self.volumeNodeComboBox = slicer.qMRMLNodeComboBox() + self.volumeNodeComboBox.nodeTypes = ["vtkMRMLVectorVolumeNode"] + self.volumeNodeComboBox.baseName = "Screenshot" + self.volumeNodeComboBox.renameEnabled = True + self.volumeNodeComboBox.noneEnabled = True + self.volumeNodeComboBox.setToolTip("Select a volume node to store the captured image in the scene instead of just writing immediately to disk. Requires output 'Number of images' to be set to 1.") + self.volumeNodeComboBox.setMRMLScene(slicer.mrmlScene) + advancedFormLayout.addRow("Output volume node:", self.volumeNodeComboBox) + + self.showViewControllersCheckBox = qt.QCheckBox(" ") + self.showViewControllersCheckBox.checked = False + self.showViewControllersCheckBox.setToolTip("If checked, images will be captured with view controllers visible.") + advancedFormLayout.addRow("View controllers:", self.showViewControllersCheckBox) + + self.transparentBackgroundCheckBox = qt.QCheckBox(" ") + self.transparentBackgroundCheckBox.checked = False + self.transparentBackgroundCheckBox.setToolTip("If checked, images will be captured with transparent background.") + advancedFormLayout.addRow("Transparent background:", self.transparentBackgroundCheckBox) + + watermarkEnabled = slicer.util.settingsValue('ScreenCapture/WatermarkEnabled', False, converter=slicer.util.toBool) + + self.watermarkEnabledCheckBox = qt.QCheckBox(" ") + self.watermarkEnabledCheckBox.checked = watermarkEnabled + self.watermarkEnabledCheckBox.setToolTip("If checked, selected watermark image will be added to all exported images.") + + self.watermarkPositionWidget = qt.QComboBox() + self.watermarkPositionWidget.enabled = watermarkEnabled + self.watermarkPositionWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) + self.watermarkPositionWidget.setToolTip("Add a watermark image to all exported images.") + for watermarkPositionPreset in self.logic.watermarkPositionPresets: + self.watermarkPositionWidget.addItem(watermarkPositionPreset["name"]) + self.watermarkPositionWidget.setCurrentText( + slicer.util.settingsValue('ScreenCapture/WatermarkPosition', self.logic.watermarkPositionPresets[0]["name"])) + + self.watermarkSizeSliderWidget = qt.QSpinBox() + self.watermarkSizeSliderWidget.enabled = watermarkEnabled + self.watermarkSizeSliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) + self.watermarkSizeSliderWidget.singleStep = 10 + self.watermarkSizeSliderWidget.minimum = 10 + self.watermarkSizeSliderWidget.maximum = 1000 + self.watermarkSizeSliderWidget.value = 100 + self.watermarkSizeSliderWidget.suffix = "%" + self.watermarkSizeSliderWidget.setToolTip("Size scaling applied to the watermark image. 100% is original size") + try: + self.watermarkSizeSliderWidget.value = int(slicer.util.settingsValue('ScreenCapture/WatermarkSize', 100)) + except: + pass + + self.watermarkOpacitySliderWidget = qt.QSpinBox() + self.watermarkOpacitySliderWidget.enabled = watermarkEnabled + self.watermarkOpacitySliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) + self.watermarkOpacitySliderWidget.singleStep = 10 + self.watermarkOpacitySliderWidget.minimum = 0 + self.watermarkOpacitySliderWidget.maximum = 100 + self.watermarkOpacitySliderWidget.value = 30 + self.watermarkOpacitySliderWidget.suffix = "%" + self.watermarkOpacitySliderWidget.setToolTip("Opacity of the watermark image. 100% is fully opaque.") + try: + self.watermarkOpacitySliderWidget.value = int(slicer.util.settingsValue('ScreenCapture/WatermarkOpacity', 30)) + except: + pass + + self.watermarkPathSelector = ctk.ctkPathLineEdit() + self.watermarkPathSelector.enabled = watermarkEnabled + self.watermarkPathSelector.settingKey = 'ScreenCaptureWatermarkImagePath' + self.watermarkPathSelector.nameFilters = ["*.png"] + self.watermarkPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength + self.watermarkPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred) + self.watermarkPathSelector.setToolTip("Watermark image file in png format") + + hbox = qt.QHBoxLayout() + hbox.addWidget(self.watermarkEnabledCheckBox) + hbox.addWidget(qt.QLabel("Position:")) + hbox.addWidget(self.watermarkPositionWidget) + hbox.addWidget(qt.QLabel("Size:")) + hbox.addWidget(self.watermarkSizeSliderWidget) + hbox.addWidget(qt.QLabel("Opacity:")) + hbox.addWidget(self.watermarkOpacitySliderWidget) + # hbox.addStretch() + advancedFormLayout.addRow("Watermark image:", hbox) + + hbox = qt.QHBoxLayout() + hbox.addWidget(self.watermarkPathSelector) + advancedFormLayout.addRow("", hbox) + + # Capture button + self.captureButtonLabelCapture = "Capture" + self.captureButtonLabelCancel = "Cancel" + self.captureButton = qt.QPushButton(self.captureButtonLabelCapture) + self.captureButton.toolTip = "Capture slice sweep to image sequence." + self.showCreatedOutputFileButton = qt.QPushButton() + self.showCreatedOutputFileButton.setIcon(qt.QIcon(':Icons/Go.png')) + self.showCreatedOutputFileButton.setMaximumWidth(60) + self.showCreatedOutputFileButton.enabled = False + self.showCreatedOutputFileButton.toolTip = "Show created output file." + hbox = qt.QHBoxLayout() + hbox.addWidget(self.captureButton) + hbox.addWidget(self.showCreatedOutputFileButton) + self.layout.addLayout(hbox) + + self.statusLabel = qt.QPlainTextEdit() + self.statusLabel.setTextInteractionFlags(qt.Qt.TextSelectableByMouse) + self.statusLabel.setCenterOnScroll(True) + self.layout.addWidget(self.statusLabel) + + # + # Add vertical spacer + # self.layout.addStretch(1) + + # connections + self.captureButton.connect('clicked(bool)', self.onCaptureButton) + self.showCreatedOutputFileButton.connect('clicked(bool)', self.onShowCreatedOutputFile) + self.viewNodeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions) + self.animationModeWidget.connect("currentIndexChanged(int)", self.updateViewOptions) + self.sliceStartOffsetSliderWidget.connect('valueChanged(double)', self.setSliceOffset) + self.sliceEndOffsetSliderWidget.connect('valueChanged(double)', self.setSliceOffset) + self.sequenceBrowserNodeSelectorWidget.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions) + self.sequenceStartItemIndexWidget.connect('valueChanged(double)', self.setSequenceItemIndex) + self.sequenceEndItemIndexWidget.connect('valueChanged(double)', self.setSequenceItemIndex) + self.outputTypeWidget.connect('currentIndexChanged(int)', self.updateOutputType) + self.videoFormatWidget.connect("currentIndexChanged(int)", self.updateVideoFormat) + self.maxFramesWidget.connect('valueChanged(int)', self.maxFramesChanged) + self.videoLengthSliderWidget.connect('valueChanged(double)', self.setVideoLength) + self.videoFrameRateSliderWidget.connect('valueChanged(double)', self.setVideoFrameRate) + self.singleStepButton.connect('toggled(bool)', self.setForceSingleStep) + self.numberOfStepsSliderWidget.connect('valueChanged(double)', self.setNumberOfSteps) + self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkPositionWidget, 'setEnabled(bool)') + self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkSizeSliderWidget, 'setEnabled(bool)') + self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkPathSelector, 'setEnabled(bool)') + + self.setVideoLength() # update frame rate based on video length + self.updateOutputType() + self.updateVideoFormat(0) + self.updateViewOptions() + + def maxFramesChanged(self): + self.numberOfStepsSliderWidget.maximum = self.maxFramesWidget.value + + def openURL(self, URL): + qt.QDesktopServices().openUrl(qt.QUrl(URL)) + QDesktopServices + + def onShowCreatedOutputFile(self): + if not self.createdOutputFile: + return + qt.QDesktopServices().openUrl(qt.QUrl("file:///" + self.createdOutputFile, qt.QUrl.TolerantMode)) + + def updateOutputType(self, selectionIndex=0): + isVideo = self.outputTypeWidget.currentText == "video" + isLightbox = self.outputTypeWidget.currentText == "lightbox image" + self.fileNamePatternWidget.enabled = not (isVideo or isLightbox) + self.videoFileNameWidget.enabled = isVideo + self.videoFormatWidget.enabled = isVideo + self.videoLengthSliderWidget.enabled = isVideo + self.videoFrameRateSliderWidget.enabled = isVideo + self.videoFileNameWidget.setVisible(not isLightbox) + self.videoFileNameWidget.enabled = isVideo + self.lightboxImageFileNameWidget.setVisible(isLightbox) + self.lightboxImageFileNameWidget.enabled = isLightbox + + def updateVideoFormat(self, selectionIndex): + videoFormatPreset = self.logic.videoFormatPresets[selectionIndex] + + import os + filenameExt = os.path.splitext(self.videoFileNameWidget.text) + self.videoFileNameWidget.text = filenameExt[0] + "." + videoFormatPreset["fileExtension"] + + self.extraVideoOptionsWidget.text = videoFormatPreset["extraVideoOptions"] + + def currentViewNodeType(self): + viewNode = self.viewNodeSelector.currentNode() + if not viewNode: + return None + elif viewNode.IsA("vtkMRMLSliceNode"): + return VIEW_SLICE + elif viewNode.IsA("vtkMRMLViewNode"): + return VIEW_3D else: - filename = None - view = None if captureAllViews else self.logic.viewFromNode(viewNode) - volumeNode = None if numberOfSteps>1 else self.volumeNodeComboBox.currentNode() - self.logic.captureImageFromView(view, filename, transparentBackground, volumeNode=volumeNode) - if filename: - self.logic.addLog("Write "+filename) - if volumeNode: - self.logic.addLog(f"Write to volume node '{volumeNode.GetName()}'") - elif self.animationModeWidget.currentText == "slice sweep": - self.logic.captureSliceSweep(viewNode, self.sliceStartOffsetSliderWidget.value, - self.sliceEndOffsetSliderWidget.value, numberOfSteps, outputDir, imageFileNamePattern, - captureAllViews = captureAllViews, transparentBackground = transparentBackground) - elif self.animationModeWidget.currentText == "slice fade": - self.logic.captureSliceFade(viewNode, numberOfSteps, outputDir, imageFileNamePattern, - captureAllViews = captureAllViews, transparentBackground = transparentBackground) - elif self.animationModeWidget.currentText == "3D rotation": - self.logic.capture3dViewRotation(viewNode, self.rotationSliderWidget.minimumValue, - self.rotationSliderWidget.maximumValue, numberOfSteps, - self.rotationAxisWidget.itemData(self.rotationAxisWidget.currentIndex), - outputDir, imageFileNamePattern, - captureAllViews = captureAllViews, transparentBackground = transparentBackground) - elif self.animationModeWidget.currentText == "sequence": - self.logic.captureSequence(viewNode, self.sequenceBrowserNodeSelectorWidget.currentNode(), - self.sequenceStartItemIndexWidget.value, self.sequenceEndItemIndexWidget.value, - numberOfSteps, outputDir, imageFileNamePattern, - captureAllViews = captureAllViews, transparentBackground = transparentBackground) - else: - raise ValueError('Unsupported view node type.') - - import shutil - - fps = self.videoFrameRateSliderWidget.value - - if numberOfSteps > 1: - forwardBackward = self.forwardBackwardCheckBox.checked - numberOfRepeats = int(self.repeatSliderWidget.value) - filePathPattern = os.path.join(outputDir, imageFileNamePattern) - fileIndex = numberOfSteps - for repeatIndex in range(numberOfRepeats): - if forwardBackward: - for step in reversed(range(1, numberOfSteps-1)): - sourceFilename = filePathPattern % step - destinationFilename = filePathPattern % fileIndex - self.logic.addLog("Copy to "+destinationFilename) - shutil.copyfile(sourceFilename, destinationFilename) - fileIndex += 1 - if repeatIndex < numberOfRepeats - 1: - for step in range(numberOfSteps): - sourceFilename = filePathPattern % step - destinationFilename = filePathPattern % fileIndex - self.logic.addLog("Copy to "+destinationFilename) - shutil.copyfile(sourceFilename, destinationFilename) - fileIndex += 1 - if forwardBackward and (numberOfSteps > 2): - numberOfSteps += numberOfSteps - 2 - numberOfSteps *= numberOfRepeats - - try: + return None + + def addLog(self, text): + """Append text to log window + """ + self.statusLabel.appendPlainText(text) + self.statusLabel.ensureCursorVisible() + slicer.app.processEvents() # force update + + def cleanup(self): + pass + + def updateViewOptions(self): + + sequencesModuleAvailable = hasattr(slicer.modules, 'sequences') + + if self.viewNodeType != self.currentViewNodeType(): + self.viewNodeType = self.currentViewNodeType() + + self.animationModeWidget.clear() + if self.viewNodeType == VIEW_SLICE: + self.animationModeWidget.addItem("slice sweep") + self.animationModeWidget.addItem("slice fade") + if self.viewNodeType == VIEW_3D: + self.animationModeWidget.addItem("3D rotation") + if sequencesModuleAvailable: + self.animationModeWidget.addItem("sequence") + + if self.animationMode != self.animationModeWidget.currentText: + self.animationMode = self.animationModeWidget.currentText + + # slice sweep + self.sliceStartOffsetSliderLabel.visible = (self.animationMode == "slice sweep") + self.sliceStartOffsetSliderWidget.visible = (self.animationMode == "slice sweep") + self.sliceEndOffsetSliderLabel.visible = (self.animationMode == "slice sweep") + self.sliceEndOffsetSliderWidget.visible = (self.animationMode == "slice sweep") + if self.animationMode == "slice sweep": + offsetResolution = self.logic.getSliceOffsetResolution(self.viewNodeSelector.currentNode()) + sliceOffsetMin, sliceOffsetMax = self.logic.getSliceOffsetRange(self.viewNodeSelector.currentNode()) + + wasBlocked = self.sliceStartOffsetSliderWidget.blockSignals(True) + self.sliceStartOffsetSliderWidget.singleStep = offsetResolution + self.sliceStartOffsetSliderWidget.minimum = sliceOffsetMin + self.sliceStartOffsetSliderWidget.maximum = sliceOffsetMax + self.sliceStartOffsetSliderWidget.value = sliceOffsetMin + self.sliceStartOffsetSliderWidget.blockSignals(wasBlocked) + + wasBlocked = self.sliceEndOffsetSliderWidget.blockSignals(True) + self.sliceEndOffsetSliderWidget.singleStep = offsetResolution + self.sliceEndOffsetSliderWidget.minimum = sliceOffsetMin + self.sliceEndOffsetSliderWidget.maximum = sliceOffsetMax + self.sliceEndOffsetSliderWidget.value = sliceOffsetMax + self.sliceEndOffsetSliderWidget.blockSignals(wasBlocked) + + # 3D rotation + self.rotationSliderLabel.visible = (self.animationMode == "3D rotation") + self.rotationSliderWidget.visible = (self.animationMode == "3D rotation") + self.rotationAxisLabel.visible = (self.animationMode == "3D rotation") + self.rotationAxisWidget.visible = (self.animationMode == "3D rotation") + + # Sequence + self.sequenceBrowserNodeSelectorLabel.visible = (self.animationMode == "sequence") + self.sequenceBrowserNodeSelectorWidget.visible = (self.animationMode == "sequence") + self.sequenceStartItemIndexLabel.visible = (self.animationMode == "sequence") + self.sequenceStartItemIndexWidget.visible = (self.animationMode == "sequence") + self.sequenceEndItemIndexLabel.visible = (self.animationMode == "sequence") + self.sequenceEndItemIndexWidget.visible = (self.animationMode == "sequence") + if self.animationMode == "sequence": + sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode() + + sequenceItemCount = 0 + if sequenceBrowserNode and sequenceBrowserNode.GetMasterSequenceNode(): + sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes() + + if sequenceItemCount > 0: + wasBlocked = self.sequenceStartItemIndexWidget.blockSignals(True) + self.sequenceStartItemIndexWidget.maximum = sequenceItemCount - 1 + self.sequenceStartItemIndexWidget.value = 0 + self.sequenceStartItemIndexWidget.blockSignals(wasBlocked) + + wasBlocked = self.sequenceEndItemIndexWidget.blockSignals(True) + self.sequenceEndItemIndexWidget.maximum = sequenceItemCount - 1 + self.sequenceEndItemIndexWidget.value = sequenceItemCount - 1 + self.sequenceEndItemIndexWidget.blockSignals(wasBlocked) + + self.sequenceStartItemIndexWidget.enabled = sequenceItemCount > 0 + self.sequenceEndItemIndexWidget.enabled = sequenceItemCount > 0 + + numberOfSteps = int(self.numberOfStepsSliderWidget.value) + forceSingleStep = self.singleStepButton.checked + if forceSingleStep: + numberOfSteps = 1 + self.numberOfStepsSliderWidget.setDisabled(forceSingleStep) + self.forwardBackwardCheckBox.enabled = (numberOfSteps > 1) + self.repeatSliderWidget.enabled = (numberOfSteps > 1) + self.volumeNodeComboBox.setEnabled(numberOfSteps == 1) + + def setSliceOffset(self, offset): + sliceLogic = self.logic.getSliceLogicFromSliceNode(self.viewNodeSelector.currentNode()) + sliceLogic.SetSliceOffset(offset) + + def setVideoLength(self, lengthSec=None): + wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True) + self.videoFrameRateSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoLengthSliderWidget.value + self.videoFrameRateSliderWidget.blockSignals(wasBlocked) + + def setVideoFrameRate(self, frameRateFps): + wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True) + self.videoLengthSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoFrameRateSliderWidget.value + self.videoFrameRateSliderWidget.blockSignals(wasBlocked) + + def setNumberOfSteps(self, steps): + self.setVideoLength() + self.updateViewOptions() + + def setForceSingleStep(self, force): + self.updateViewOptions() + + def setSequenceItemIndex(self, index): + sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode() + sequenceBrowserNode.SetSelectedItemNumber(int(index)) + + def enableInputOutputWidgets(self, enable): + self.inputCollapsibleButton.setEnabled(enable) + self.outputCollapsibleButton.setEnabled(enable) + + def onCaptureButton(self): + + # Disable capture button to prevent multiple clicks + self.captureButton.setEnabled(False) + self.enableInputOutputWidgets(False) + slicer.app.processEvents() + + if self.captureButton.text == self.captureButtonLabelCancel: + self.logic.requestCancel() + return + + self.logic.setFfmpegPath(self.ffmpegPathSelector.currentPath) + + qt.QSettings().setValue('ScreenCapture/WatermarkEnabled', bool(self.watermarkEnabledCheckBox.checked)) + qt.QSettings().setValue('ScreenCapture/WatermarkPosition', self.watermarkPositionWidget.currentText) + qt.QSettings().setValue('ScreenCapture/WatermarkOpacity', self.watermarkOpacitySliderWidget.value) + qt.QSettings().setValue('ScreenCapture/WatermarkSize', self.watermarkSizeSliderWidget.value) + + if self.watermarkEnabledCheckBox.checked: + if self.watermarkPathSelector.currentPath: + self.logic.setWatermarkImagePath(self.watermarkPathSelector.currentPath) + self.watermarkPathSelector.addCurrentPathToHistory() + else: + self.logic.setWatermarkImagePath(self.resourcePath('SlicerWatermark.png')) + self.logic.setWatermarkPosition(self.watermarkPositionWidget.currentIndex) + self.logic.setWatermarkSizePercent(self.watermarkSizeSliderWidget.value) + self.logic.setWatermarkOpacityPercent(self.watermarkOpacitySliderWidget.value) + else: + self.logic.setWatermarkPosition(-1) + + self.statusLabel.plainText = '' + + videoOutputRequested = (self.outputTypeWidget.currentText == "video") + viewNode = self.viewNodeSelector.currentNode() + numberOfSteps = int(self.numberOfStepsSliderWidget.value) + if self.singleStepButton.checked: + numberOfSteps = 1 + if numberOfSteps < 2: + # If a single image is selected + videoOutputRequested = False + outputDir = self.outputDirSelector.currentPath + self.outputDirSelector.addCurrentPathToHistory() + + self.videoExportFfmpegWarning.setVisible(False) if videoOutputRequested: - self.logic.createVideo(fps, self.extraVideoOptionsWidget.text, - outputDir, imageFileNamePattern, self.videoFileNameWidget.text) - elif (self.outputTypeWidget.currentText == "lightbox image"): - self.logic.createLightboxImage(int(self.lightboxColumnCountSliderWidget.value), - outputDir, imageFileNamePattern, numberOfSteps, self.lightboxImageFileNameWidget.text) - finally: - if not self.outputTypeWidget.currentText == "image series": - self.logic.deleteTemporaryFiles(outputDir, imageFileNamePattern, numberOfSteps) - - self.addLog("Done.") - self.createdOutputFile = os.path.join(outputDir, self.videoFileNameWidget.text) if videoOutputRequested else outputDir - self.showCreatedOutputFileButton.enabled = True - except Exception as e: - self.addLog(f"Error: {str(e)}") - import traceback - traceback.print_exc() - self.showCreatedOutputFileButton.enabled = False - self.createdOutputFile = None - if captureAllViews: - self.logic.showViewControllers(True) - slicer.app.restoreOverrideCursor() - self.captureButton.text = self.captureButtonLabelCapture - self.captureButton.setEnabled(True) - self.enableInputOutputWidgets(True) + if not self.logic.isFfmpegPathValid(): + # ffmpeg not found, try to automatically find it at common locations + self.logic.findFfmpeg() + if not self.logic.isFfmpegPathValid() and os.name == 'nt': # TODO: implement download for Linux/MacOS? + # ffmpeg not found, offer downloading it + if slicer.util.confirmOkCancelDisplay( + 'Video encoder not detected on your system. ' + 'Download ffmpeg video encoder?', + windowTitle='Download confirmation'): + if not self.logic.ffmpegDownload(): + slicer.util.errorDisplay("ffmpeg download failed") + if not self.logic.isFfmpegPathValid(): + # still not found, user has to specify path manually + self.videoExportFfmpegWarning.setVisible(True) + self.advancedCollapsibleButton.collapsed = False + self.captureButton.setEnabled(True) + self.enableInputOutputWidgets(True) + return + self.ffmpegPathSelector.currentPath = self.logic.getFfmpegPath() + + # Need to create a new random file pattern if video output is requested to make sure that new image files are not mixed up with + # existing files in the output directory + imageFileNamePattern = self.fileNamePatternWidget.text if (self.outputTypeWidget.currentText == "image series") else self.logic.getRandomFilePattern() + + self.captureButton.setEnabled(True) + self.captureButton.text = self.captureButtonLabelCancel + slicer.app.setOverrideCursor(qt.Qt.WaitCursor) + captureAllViews = self.captureAllViewsCheckBox.checked + transparentBackground = self.transparentBackgroundCheckBox.checked + showViewControllers = self.showViewControllersCheckBox.checked + if captureAllViews: + self.logic.showViewControllers(showViewControllers) + elif showViewControllers: + logging.warning("View controllers are only available to be shown when capturing all views.") + try: + if numberOfSteps < 2: + if imageFileNamePattern != self.snapshotFileNamePattern or outputDir != self.snapshotOutputDir: + self.snapshotIndex = 0 + if outputDir: + [filename, self.snapshotIndex] = self.logic.getNextAvailableFileName(outputDir, imageFileNamePattern, self.snapshotIndex) + else: + filename = None + view = None if captureAllViews else self.logic.viewFromNode(viewNode) + volumeNode = None if numberOfSteps > 1 else self.volumeNodeComboBox.currentNode() + self.logic.captureImageFromView(view, filename, transparentBackground, volumeNode=volumeNode) + if filename: + self.logic.addLog("Write " + filename) + if volumeNode: + self.logic.addLog(f"Write to volume node '{volumeNode.GetName()}'") + elif self.animationModeWidget.currentText == "slice sweep": + self.logic.captureSliceSweep(viewNode, self.sliceStartOffsetSliderWidget.value, + self.sliceEndOffsetSliderWidget.value, numberOfSteps, outputDir, imageFileNamePattern, + captureAllViews=captureAllViews, transparentBackground=transparentBackground) + elif self.animationModeWidget.currentText == "slice fade": + self.logic.captureSliceFade(viewNode, numberOfSteps, outputDir, imageFileNamePattern, + captureAllViews=captureAllViews, transparentBackground=transparentBackground) + elif self.animationModeWidget.currentText == "3D rotation": + self.logic.capture3dViewRotation(viewNode, self.rotationSliderWidget.minimumValue, + self.rotationSliderWidget.maximumValue, numberOfSteps, + self.rotationAxisWidget.itemData(self.rotationAxisWidget.currentIndex), + outputDir, imageFileNamePattern, + captureAllViews=captureAllViews, transparentBackground=transparentBackground) + elif self.animationModeWidget.currentText == "sequence": + self.logic.captureSequence(viewNode, self.sequenceBrowserNodeSelectorWidget.currentNode(), + self.sequenceStartItemIndexWidget.value, self.sequenceEndItemIndexWidget.value, + numberOfSteps, outputDir, imageFileNamePattern, + captureAllViews=captureAllViews, transparentBackground=transparentBackground) + else: + raise ValueError('Unsupported view node type.') + + import shutil + + fps = self.videoFrameRateSliderWidget.value + + if numberOfSteps > 1: + forwardBackward = self.forwardBackwardCheckBox.checked + numberOfRepeats = int(self.repeatSliderWidget.value) + filePathPattern = os.path.join(outputDir, imageFileNamePattern) + fileIndex = numberOfSteps + for repeatIndex in range(numberOfRepeats): + if forwardBackward: + for step in reversed(range(1, numberOfSteps - 1)): + sourceFilename = filePathPattern % step + destinationFilename = filePathPattern % fileIndex + self.logic.addLog("Copy to " + destinationFilename) + shutil.copyfile(sourceFilename, destinationFilename) + fileIndex += 1 + if repeatIndex < numberOfRepeats - 1: + for step in range(numberOfSteps): + sourceFilename = filePathPattern % step + destinationFilename = filePathPattern % fileIndex + self.logic.addLog("Copy to " + destinationFilename) + shutil.copyfile(sourceFilename, destinationFilename) + fileIndex += 1 + if forwardBackward and (numberOfSteps > 2): + numberOfSteps += numberOfSteps - 2 + numberOfSteps *= numberOfRepeats + + try: + if videoOutputRequested: + self.logic.createVideo(fps, self.extraVideoOptionsWidget.text, + outputDir, imageFileNamePattern, self.videoFileNameWidget.text) + elif (self.outputTypeWidget.currentText == "lightbox image"): + self.logic.createLightboxImage(int(self.lightboxColumnCountSliderWidget.value), + outputDir, imageFileNamePattern, numberOfSteps, self.lightboxImageFileNameWidget.text) + finally: + if not self.outputTypeWidget.currentText == "image series": + self.logic.deleteTemporaryFiles(outputDir, imageFileNamePattern, numberOfSteps) + + self.addLog("Done.") + self.createdOutputFile = os.path.join(outputDir, self.videoFileNameWidget.text) if videoOutputRequested else outputDir + self.showCreatedOutputFileButton.enabled = True + except Exception as e: + self.addLog(f"Error: {str(e)}") + import traceback + traceback.print_exc() + self.showCreatedOutputFileButton.enabled = False + self.createdOutputFile = None + if captureAllViews: + self.logic.showViewControllers(True) + slicer.app.restoreOverrideCursor() + self.captureButton.text = self.captureButtonLabelCapture + self.captureButton.setEnabled(True) + self.enableInputOutputWidgets(True) # @@ -801,758 +801,758 @@ def onCaptureButton(self): # class ScreenCaptureLogic(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): - self.logCallback = None - self.cancelRequested = False - - self.videoFormatPresets = [ - {"name": "H.264", "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -pix_fmt yuv420p"}, - {"name": "H.264 (high-quality)", "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -crf 18 -pix_fmt yuv420p"}, - {"name": "MPEG-4", "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 5"}, - {"name": "MPEG-4 (high-quality)", "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 3"}, - {"name": "Animated GIF", "fileExtension": "gif", "extraVideoOptions": "-filter_complex palettegen,[v]paletteuse"}, - {"name": "Animated GIF (grayscale)", "fileExtension": "gif", "extraVideoOptions": "-vf format=gray"} ] - - self.watermarkPositionPresets = [ - {"name": "bottom-left", "position": lambda capturedImageSize, watermarkSize, spacing: [-2, -2]}, - {"name": "bottom-right", "position": lambda capturedImageSize, watermarkSize, spacing: [ - -capturedImageSize[0]*spacing+watermarkSize[0]+2, -2]}, - {"name": "top-left", "position": lambda capturedImageSize, watermarkSize, spacing: [ - -2, -capturedImageSize[1]*spacing+watermarkSize[1]+2]}, - {"name": "top-right", "position": lambda capturedImageSize, watermarkSize, spacing: [ - -capturedImageSize[0]*spacing+watermarkSize[0]+2, -capturedImageSize[1]*spacing+watermarkSize[1]+2]} ] - - self.watermarkPosition = -1 - self.watermarkSizePercent = 100 - self.watermarkOpacityPercent = 100 - self.watermarkImagePath = None - - def requestCancel(self): - logging.info("User requested cancelling of capture") - self.cancelRequested = True - - def addLog(self, text): - logging.info(text) - if self.logCallback: - self.logCallback(text) - - def showViewControllers(self, show): - slicer.util.setViewControllersVisible(show) - - def getRandomFilePattern(self): - import string - import random - numberOfRandomChars=5 - randomString = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(numberOfRandomChars)) - filePathPattern = "tmp-"+randomString+"-%05d.png" - return filePathPattern - - def isFfmpegPathValid(self): - import os - ffmpegPath = self.getFfmpegPath() - return os.path.isfile(ffmpegPath) - - def getDownloadedFfmpegDirectory(self): - return os.path.dirname(slicer.app.slicerUserSettingsFilePath)+'/ffmpeg' - - def getFfmpegExecutableFilename(self): - if os.name == 'nt': - return 'ffmpeg.exe' - else: - return 'ffmpeg' - - def findFfmpeg(self): - # Try to find the executable at specific paths - commonFfmpegPaths = [ - '/usr/local/bin/ffmpeg', - '/usr/bin/ffmpeg' - ] - for ffmpegPath in commonFfmpegPaths: - if os.path.isfile(ffmpegPath): - # found one - self.setFfmpegPath(ffmpegPath) - return True - # Search for the executable in directories - commonFfmpegDirs = [ - self.getDownloadedFfmpegDirectory() - ] - for ffmpegDir in commonFfmpegDirs: - if self.findFfmpegInDirectory(ffmpegDir): - # found it - return True - # Not found - return False - - def findFfmpegInDirectory(self, ffmpegDir): - ffmpegExecutableFilename = self.getFfmpegExecutableFilename() - for dirpath, dirnames, files in os.walk(ffmpegDir): - for name in files: - if name==ffmpegExecutableFilename: - ffmpegExecutablePath = (dirpath + '/' + name).replace('\\','/') - self.setFfmpegPath(ffmpegExecutablePath) - return True - return False - - def unzipFfmpeg(self, filePath, ffmpegTargetDirectory): - if not os.path.exists(filePath) or os.stat(filePath).st_size == 0: - logging.info('ffmpeg package is not found at ' + filePath) - return False - - logging.info('Unzipping ffmpeg package ' + filePath) - qt.QDir().mkpath(ffmpegTargetDirectory) - slicer.app.applicationLogic().Unzip(filePath, ffmpegTargetDirectory) - success = self.findFfmpegInDirectory(ffmpegTargetDirectory) - return success - - def ffmpegDownload(self): - ffmpegTargetDirectory = self.getDownloadedFfmpegDirectory() - # The number in the filePath can be incremented each time a significantly different ffmpeg version - # is to be introduced (it prevents reusing a previously downloaded package). - filePath = slicer.app.temporaryPath + '/ffmpeg-package-slicer-01.zip' - success = self.unzipFfmpeg(filePath, ffmpegTargetDirectory) - if success: - # there was a valid downloaded package already - return True - - # List of mirror sites to attempt download ffmpeg pre-built binaries from - urls = [] - if os.name == 'nt': - urls.append('https://github.com/Slicer/SlicerBinaryDependencies/releases/download/ffmpeg/ffmpeg-2021-05-16-win64.zip') - else: - # TODO: implement downloading for Linux/MacOS? - pass - - success = False - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - - for url in urls: - - success = True - try: - logging.info('Requesting download ffmpeg from %s...' % url) - import urllib.request, urllib.error, urllib.parse - req = urllib.request.Request(url, headers={ 'User-Agent': 'Mozilla/5.0' }) - data = urllib.request.urlopen(req).read() - with open(filePath, "wb") as f: - f.write(data) + """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): + self.logCallback = None + self.cancelRequested = False + + self.videoFormatPresets = [ + {"name": "H.264", "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -pix_fmt yuv420p"}, + {"name": "H.264 (high-quality)", "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -crf 18 -pix_fmt yuv420p"}, + {"name": "MPEG-4", "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 5"}, + {"name": "MPEG-4 (high-quality)", "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 3"}, + {"name": "Animated GIF", "fileExtension": "gif", "extraVideoOptions": "-filter_complex palettegen,[v]paletteuse"}, + {"name": "Animated GIF (grayscale)", "fileExtension": "gif", "extraVideoOptions": "-vf format=gray"}] + + self.watermarkPositionPresets = [ + {"name": "bottom-left", "position": lambda capturedImageSize, watermarkSize, spacing: [-2, -2]}, + {"name": "bottom-right", "position": lambda capturedImageSize, watermarkSize, spacing: [ + -capturedImageSize[0] * spacing + watermarkSize[0] + 2, -2]}, + {"name": "top-left", "position": lambda capturedImageSize, watermarkSize, spacing: [ + -2, -capturedImageSize[1] * spacing + watermarkSize[1] + 2]}, + {"name": "top-right", "position": lambda capturedImageSize, watermarkSize, spacing: [ + -capturedImageSize[0] * spacing + watermarkSize[0] + 2, -capturedImageSize[1] * spacing + watermarkSize[1] + 2]}] + + self.watermarkPosition = -1 + self.watermarkSizePercent = 100 + self.watermarkOpacityPercent = 100 + self.watermarkImagePath = None + + def requestCancel(self): + logging.info("User requested cancelling of capture") + self.cancelRequested = True + + def addLog(self, text): + logging.info(text) + if self.logCallback: + self.logCallback(text) + + def showViewControllers(self, show): + slicer.util.setViewControllersVisible(show) + + def getRandomFilePattern(self): + import string + import random + numberOfRandomChars = 5 + randomString = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(numberOfRandomChars)) + filePathPattern = "tmp-" + randomString + "-%05d.png" + return filePathPattern + + def isFfmpegPathValid(self): + import os + ffmpegPath = self.getFfmpegPath() + return os.path.isfile(ffmpegPath) + + def getDownloadedFfmpegDirectory(self): + return os.path.dirname(slicer.app.slicerUserSettingsFilePath) + '/ffmpeg' + + def getFfmpegExecutableFilename(self): + if os.name == 'nt': + return 'ffmpeg.exe' + else: + return 'ffmpeg' + + def findFfmpeg(self): + # Try to find the executable at specific paths + commonFfmpegPaths = [ + '/usr/local/bin/ffmpeg', + '/usr/bin/ffmpeg' + ] + for ffmpegPath in commonFfmpegPaths: + if os.path.isfile(ffmpegPath): + # found one + self.setFfmpegPath(ffmpegPath) + return True + # Search for the executable in directories + commonFfmpegDirs = [ + self.getDownloadedFfmpegDirectory() + ] + for ffmpegDir in commonFfmpegDirs: + if self.findFfmpegInDirectory(ffmpegDir): + # found it + return True + # Not found + return False + + def findFfmpegInDirectory(self, ffmpegDir): + ffmpegExecutableFilename = self.getFfmpegExecutableFilename() + for dirpath, dirnames, files in os.walk(ffmpegDir): + for name in files: + if name == ffmpegExecutableFilename: + ffmpegExecutablePath = (dirpath + '/' + name).replace('\\', '/') + self.setFfmpegPath(ffmpegExecutablePath) + return True + return False + + def unzipFfmpeg(self, filePath, ffmpegTargetDirectory): + if not os.path.exists(filePath) or os.stat(filePath).st_size == 0: + logging.info('ffmpeg package is not found at ' + filePath) + return False + + logging.info('Unzipping ffmpeg package ' + filePath) + qt.QDir().mkpath(ffmpegTargetDirectory) + slicer.app.applicationLogic().Unzip(filePath, ffmpegTargetDirectory) + success = self.findFfmpegInDirectory(ffmpegTargetDirectory) + return success + + def ffmpegDownload(self): + ffmpegTargetDirectory = self.getDownloadedFfmpegDirectory() + # The number in the filePath can be incremented each time a significantly different ffmpeg version + # is to be introduced (it prevents reusing a previously downloaded package). + filePath = slicer.app.temporaryPath + '/ffmpeg-package-slicer-01.zip' success = self.unzipFfmpeg(filePath, ffmpegTargetDirectory) - except: - success = False - - if success: - break - - qt.QApplication.restoreOverrideCursor() - return success - - def getFfmpegPath(self): - settings = qt.QSettings() - if settings.contains('General/ffmpegPath'): - return slicer.app.toSlicerHomeAbsolutePath(settings.value('General/ffmpegPath')) - return '' - - def setFfmpegPath(self, ffmpegPath): - # don't save it if already saved - settings = qt.QSettings() - if settings.contains('General/ffmpegPath'): - if ffmpegPath == slicer.app.toSlicerHomeAbsolutePath(settings.value('General/ffmpegPath')): - return - settings.setValue('General/ffmpegPath', slicer.app.toSlicerHomeRelativePath(ffmpegPath)) - - def setWatermarkPosition(self, watermarkPosition): - self.watermarkPosition = watermarkPosition - - def setWatermarkSizePercent(self, watermarkSizePercent): - self.watermarkSizePercent = watermarkSizePercent - - def setWatermarkOpacityPercent(self, watermarkOpacityPercent): - self.watermarkOpacityPercent = watermarkOpacityPercent - - def setWatermarkImagePath(self, watermarkImagePath): - self.watermarkImagePath = watermarkImagePath + if success: + # there was a valid downloaded package already + return True + + # List of mirror sites to attempt download ffmpeg pre-built binaries from + urls = [] + if os.name == 'nt': + urls.append('https://github.com/Slicer/SlicerBinaryDependencies/releases/download/ffmpeg/ffmpeg-2021-05-16-win64.zip') + else: + # TODO: implement downloading for Linux/MacOS? + pass - def getSliceLogicFromSliceNode(self, sliceNode): - lm = slicer.app.layoutManager() - sliceLogic = lm.sliceWidget(sliceNode.GetLayoutName()).sliceLogic() - return sliceLogic + success = False + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + + for url in urls: + + success = True + try: + logging.info('Requesting download ffmpeg from %s...' % url) + import urllib.request, urllib.error, urllib.parse + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + data = urllib.request.urlopen(req).read() + with open(filePath, "wb") as f: + f.write(data) + + success = self.unzipFfmpeg(filePath, ffmpegTargetDirectory) + except: + success = False + + if success: + break + + qt.QApplication.restoreOverrideCursor() + return success + + def getFfmpegPath(self): + settings = qt.QSettings() + if settings.contains('General/ffmpegPath'): + return slicer.app.toSlicerHomeAbsolutePath(settings.value('General/ffmpegPath')) + return '' + + def setFfmpegPath(self, ffmpegPath): + # don't save it if already saved + settings = qt.QSettings() + if settings.contains('General/ffmpegPath'): + if ffmpegPath == slicer.app.toSlicerHomeAbsolutePath(settings.value('General/ffmpegPath')): + return + settings.setValue('General/ffmpegPath', slicer.app.toSlicerHomeRelativePath(ffmpegPath)) + + def setWatermarkPosition(self, watermarkPosition): + self.watermarkPosition = watermarkPosition + + def setWatermarkSizePercent(self, watermarkSizePercent): + self.watermarkSizePercent = watermarkSizePercent + + def setWatermarkOpacityPercent(self, watermarkOpacityPercent): + self.watermarkOpacityPercent = watermarkOpacityPercent + + def setWatermarkImagePath(self, watermarkImagePath): + self.watermarkImagePath = watermarkImagePath + + def getSliceLogicFromSliceNode(self, sliceNode): + lm = slicer.app.layoutManager() + sliceLogic = lm.sliceWidget(sliceNode.GetLayoutName()).sliceLogic() + return sliceLogic + + def getSliceOffsetRange(self, sliceNode): + sliceLogic = self.getSliceLogicFromSliceNode(sliceNode) + + sliceBounds = [0, -1, 0, -1, 0, -1] + sliceLogic.GetLowestVolumeSliceBounds(sliceBounds) + sliceOffsetMin = sliceBounds[4] + sliceOffsetMax = sliceBounds[5] + + # increase range if it is empty + # to allow capturing even when no volumes are shown in slice views + if sliceOffsetMin == sliceOffsetMax: + sliceOffsetMin = sliceLogic.GetSliceOffset() - 100 + sliceOffsetMax = sliceLogic.GetSliceOffset() + 100 + + return sliceOffsetMin, sliceOffsetMax + + def getSliceOffsetResolution(self, sliceNode): + sliceLogic = self.getSliceLogicFromSliceNode(sliceNode) + + sliceOffsetResolution = 1.0 + sliceSpacing = sliceLogic.GetLowestVolumeSliceSpacing() + if sliceSpacing is not None and sliceSpacing[2] > 0: + sliceOffsetResolution = sliceSpacing[2] + + return sliceOffsetResolution + + def captureImageFromView(self, view, filename=None, transparentBackground=False, volumeNode=None): + """ + Capture an image of the specified view and store in the specified object. + + :param view: View to capture. If none, all views are captured. + :param filename: Filename of the desired output file. If none, no file will be written. + :param transparentBackground: Set the background to be transparent for single-view captures. + :param volumeNode: Vector volume node to store the capture image. If none, no vector volume node will be updated. + """ + slicer.app.processEvents() + if view: + if type(view) == slicer.qMRMLSliceView or type(view) == slicer.qMRMLThreeDView: + view.forceRender() + else: + view.repaint() + else: + slicer.util.forceRenderAllViews() - def getSliceOffsetRange(self, sliceNode): - sliceLogic = self.getSliceLogicFromSliceNode(sliceNode) + if view is None: + if transparentBackground: + logging.warning("Transparent background is only available for single-view capture") - sliceBounds = [0, -1, 0, -1, 0, -1] - sliceLogic.GetLowestVolumeSliceBounds(sliceBounds) - sliceOffsetMin = sliceBounds[4] - sliceOffsetMax = sliceBounds[5] + # no view is specified, capture the entire view layout - # increase range if it is empty - # to allow capturing even when no volumes are shown in slice views - if sliceOffsetMin == sliceOffsetMax: - sliceOffsetMin = sliceLogic.GetSliceOffset()-100 - sliceOffsetMax = sliceLogic.GetSliceOffset()+100 + # Simply using grabwidget on the view layout frame would grab the screen without background: + # img = ctk.ctkWidgetsUtils.grabWidget(slicer.app.layoutManager().viewport()) - return sliceOffsetMin, sliceOffsetMax + # Grab the main window and use only the viewport's area + allViews = slicer.app.layoutManager().viewport() + topLeft = allViews.mapTo(slicer.util.mainWindow(), allViews.rect.topLeft()) + bottomRight = allViews.mapTo(slicer.util.mainWindow(), allViews.rect.bottomRight()) + imageSize = bottomRight - topLeft - def getSliceOffsetResolution(self, sliceNode): - sliceLogic = self.getSliceLogicFromSliceNode(sliceNode) + if imageSize.x() < 2 or imageSize.y() < 2: + # image is too small, most likely it is invalid + raise ValueError('Capture image from view failed') - sliceOffsetResolution = 1.0 - sliceSpacing = sliceLogic.GetLowestVolumeSliceSpacing() - if sliceSpacing is not None and sliceSpacing[2]>0: - sliceOffsetResolution = sliceSpacing[2] + img = ctk.ctkWidgetsUtils.grabWidget(slicer.util.mainWindow(), qt.QRect(topLeft.x(), topLeft.y(), imageSize.x(), imageSize.y())) - return sliceOffsetResolution + capturedImage = vtk.vtkImageData() + ctk.ctkVTKWidgetsUtils.qImageToVTKImageData(img, capturedImage) - def captureImageFromView(self, view, filename=None, transparentBackground=False, volumeNode=None): - """ - Capture an image of the specified view and store in the specified object. + else: + # Capture single view + + rw = view.renderWindow() + wti = vtk.vtkWindowToImageFilter() + + if transparentBackground: + originalAlphaBitPlanes = rw.GetAlphaBitPlanes() + rw.SetAlphaBitPlanes(1) + ren = rw.GetRenderers().GetFirstRenderer() + originalGradientBackground = ren.GetGradientBackground() + ren.SetGradientBackground(False) + wti.SetInputBufferTypeToRGBA() + rw.Render() # need to render after changing bit planes + + wti.SetInput(rw) + wti.Update() + + if transparentBackground: + rw.SetAlphaBitPlanes(originalAlphaBitPlanes) + ren.SetGradientBackground(originalGradientBackground) + + capturedImage = wti.GetOutput() + + imageSize = capturedImage.GetDimensions() + + if imageSize[0] < 2 or imageSize[1] < 2: + # image is too small, most likely it is invalid + raise ValueError('Capture image from view failed') + + # Make sure image width and height is even, otherwise encoding may fail + imageWidthOdd = (imageSize[0] & 1 == 1) + imageHeightOdd = (imageSize[1] & 1 == 1) + if imageWidthOdd or imageHeightOdd: + imageClipper = vtk.vtkImageClip() + imageClipper.SetClipData(True) + imageClipper.SetInputData(capturedImage) + extent = capturedImage.GetExtent() + imageClipper.SetOutputWholeExtent(extent[0], extent[1] - 1 if imageWidthOdd else extent[1], + extent[2], extent[3] - 1 if imageHeightOdd else extent[3], + extent[4], extent[5]) + imageClipper.Update() + capturedImage = imageClipper.GetOutput() + + capturedImage = self.addWatermark(capturedImage) + if volumeNode is not None: + if isinstance(volumeNode, slicer.vtkMRMLVectorVolumeNode): + ijkToRas = vtk.vtkMatrix4x4() + ijkToRas.SetElement(0, 0, -1) + ijkToRas.SetElement(1, 1, -1) + volumeNode.SetIJKToRASMatrix(ijkToRas) + vflip = vtk.vtkImageFlip() + vflip.SetInputData(capturedImage) + vflip.SetFilteredAxis(1) + vflip.Update() + volumeNode.SetAndObserveImageData(vflip.GetOutput()) + else: + raise ValueError("Invalid vector volume node.") + if filename: + writer = self.createImageWriter(filename) + writer.SetInputData(capturedImage) + writer.SetFileName(filename) + writer.Write() + + def createImageWriter(self, filename): + name, extension = os.path.splitext(filename) + if extension.lower() == '.png': + return vtk.vtkPNGWriter() + elif extension.lower() == '.jpg' or extension.lower() == '.jpeg': + return vtk.vtkJPEGWriter() + else: + raise ValueError('Unsupported image format based on file name ' + filename) + + def createImageReader(self, filename): + name, extension = os.path.splitext(filename) + if extension.lower() == '.png': + return vtk.vtkPNGReader() + elif extension.lower() == '.jpg' or extension.lower() == '.jpeg': + return vtk.vtkJPEGReader() + else: + raise ValueError('Unsupported image format based on file name ' + filename) + + def addWatermark(self, capturedImage): + + if self.watermarkPosition < 0: + # no watermark + return capturedImage + + watermarkReader = vtk.vtkPNGReader() + watermarkReader.SetFileName(self.watermarkImagePath) + watermarkReader.Update() + watermarkImage = watermarkReader.GetOutput() + + # Add alpha channel, if image is only RGB and not RGBA + if watermarkImage.GetNumberOfScalarComponents() == 3: + alphaImage = vtk.vtkImageData() + alphaImage.SetDimensions(watermarkImage.GetDimensions()) + alphaImage.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) + alphaImage.GetPointData().GetScalars().Fill(255) + appendRGBA = vtk.vtkImageAppendComponents() + appendRGBA.AddInputData(watermarkImage) + appendRGBA.AddInputData(alphaImage) + appendRGBA.Update() + watermarkImage = appendRGBA.GetOutput() + + watermarkSize = [0] * 3 + watermarkImage.GetDimensions(watermarkSize) + capturedImageSize = [0] * 3 + capturedImage.GetDimensions(capturedImageSize) + spacing = 100.0 / self.watermarkSizePercent + watermarkResize = vtk.vtkImageReslice() + watermarkResize.SetInterpolationModeToCubic() + watermarkResize.SetInputData(watermarkImage) + watermarkResize.SetOutputExtent(capturedImage.GetExtent()) + watermarkResize.SetOutputSpacing(spacing, spacing, 1) + position = self.watermarkPositionPresets[self.watermarkPosition]["position"](capturedImageSize, watermarkSize, spacing) + watermarkResize.SetOutputOrigin(position[0], position[1], 0.0) + watermarkResize.Update() + + blend = vtk.vtkImageBlend() + blend.SetOpacity(0, 1.0 - self.watermarkOpacityPercent * 0.01) + blend.SetOpacity(1, self.watermarkOpacityPercent * 0.01) + blend.AddInputData(capturedImage) + blend.AddInputData(watermarkResize.GetOutput()) + blend.Update() + + return blend.GetOutput() + + def viewFromNode(self, viewNode): + if not viewNode: + raise ValueError('Invalid view node.') + elif viewNode.IsA("vtkMRMLSliceNode"): + return slicer.app.layoutManager().sliceWidget(viewNode.GetLayoutName()).sliceView() + elif viewNode.IsA("vtkMRMLViewNode"): + renderView = None + lm = slicer.app.layoutManager() + for widgetIndex in range(lm.threeDViewCount): + view = lm.threeDWidget(widgetIndex).threeDView() + if viewNode == view.mrmlViewNode(): + renderView = view + break + if not renderView: + raise ValueError('Selected 3D view is not visible in the current layout.') + return renderView + elif viewNode.IsA("vtkMRMLPlotViewNode"): + renderView = None + lm = slicer.app.layoutManager() + for viewIndex in range(lm.plotViewCount): + if viewNode == lm.plotWidget(viewIndex).mrmlPlotViewNode(): + renderView = lm.plotWidget(viewIndex).plotView() + break + if not renderView: + raise ValueError('Selected 3D view is not visible in the current layout.') + return renderView + else: + raise ValueError('Invalid view node.') + + def captureSliceSweep(self, sliceNode, startSliceOffset, endSliceOffset, numberOfImages, + outputDir, outputFilenamePattern, captureAllViews=None, transparentBackground=False): + + self.cancelRequested = False + + if not captureAllViews and not sliceNode.IsMappedInLayout(): + raise ValueError('Selected slice view is not visible in the current layout.') + + if not os.path.exists(outputDir): + os.makedirs(outputDir) + filePathPattern = os.path.join(outputDir, outputFilenamePattern) + + sliceLogic = self.getSliceLogicFromSliceNode(sliceNode) + originalSliceOffset = sliceLogic.GetSliceOffset() + + sliceView = self.viewFromNode(sliceNode) + compositeNode = sliceLogic.GetSliceCompositeNode() + offsetStepSize = (endSliceOffset - startSliceOffset) / (numberOfImages - 1) + for offsetIndex in range(numberOfImages): + filename = filePathPattern % offsetIndex + self.addLog("Write " + filename) + sliceLogic.SetSliceOffset(startSliceOffset + offsetIndex * offsetStepSize) + self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground) + if self.cancelRequested: + break + + sliceLogic.SetSliceOffset(originalSliceOffset) + if self.cancelRequested: + raise ValueError('User requested cancel.') + + def captureSliceFade(self, sliceNode, numberOfImages, outputDir, + outputFilenamePattern, captureAllViews=None, transparentBackground=False): + + self.cancelRequested = False + + if not captureAllViews and not sliceNode.IsMappedInLayout(): + raise ValueError('Selected slice view is not visible in the current layout.') + + if not os.path.exists(outputDir): + os.makedirs(outputDir) + filePathPattern = os.path.join(outputDir, outputFilenamePattern) + + sliceLogic = self.getSliceLogicFromSliceNode(sliceNode) + sliceView = self.viewFromNode(sliceNode) + compositeNode = sliceLogic.GetSliceCompositeNode() + originalForegroundOpacity = compositeNode.GetForegroundOpacity() + startForegroundOpacity = 0.0 + endForegroundOpacity = 1.0 + opacityStepSize = (endForegroundOpacity - startForegroundOpacity) / (numberOfImages - 1) + for offsetIndex in range(numberOfImages): + filename = filePathPattern % offsetIndex + self.addLog("Write " + filename) + compositeNode.SetForegroundOpacity(startForegroundOpacity + offsetIndex * opacityStepSize) + self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground) + if self.cancelRequested: + break + + compositeNode.SetForegroundOpacity(originalForegroundOpacity) + + if self.cancelRequested: + raise ValueError('User requested cancel.') + + def capture3dViewRotation(self, viewNode, startRotation, endRotation, numberOfImages, rotationAxis, + outputDir, outputFilenamePattern, captureAllViews=None, transparentBackground=False): + """ + Acquire a set of screenshots of the 3D view while rotating it. + """ + + self.cancelRequested = False + + if not os.path.exists(outputDir): + os.makedirs(outputDir) + filePathPattern = os.path.join(outputDir, outputFilenamePattern) + + renderView = self.viewFromNode(viewNode) + + # Save original orientation and go to start orientation + originalPitchRollYawIncrement = renderView.pitchRollYawIncrement + originalDirection = renderView.pitchDirection + renderView.setPitchRollYawIncrement(-startRotation) + if rotationAxis == AXIS_YAW: + renderView.yawDirection = renderView.YawRight + renderView.yaw() + else: + renderView.pitchDirection = renderView.PitchDown + renderView.pitch() + + # Rotate step-by-step + rotationStepSize = (endRotation - startRotation) / (numberOfImages - 1) + renderView.setPitchRollYawIncrement(rotationStepSize) + if rotationAxis == AXIS_YAW: + renderView.yawDirection = renderView.YawLeft + else: + renderView.pitchDirection = renderView.PitchUp + for offsetIndex in range(numberOfImages): + if not self.cancelRequested: + filename = filePathPattern % offsetIndex + self.addLog("Write " + filename) + self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground) + if rotationAxis == AXIS_YAW: + renderView.yaw() + else: + renderView.pitch() + + # Restore original orientation and rotation step size & direction + if rotationAxis == AXIS_YAW: + renderView.yawDirection = renderView.YawRight + renderView.yaw() + renderView.setPitchRollYawIncrement(endRotation) + renderView.yaw() + renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement) + renderView.yawDirection = originalDirection + else: + renderView.pitchDirection = renderView.PitchDown + renderView.pitch() + renderView.setPitchRollYawIncrement(endRotation) + renderView.pitch() + renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement) + renderView.pitchDirection = originalDirection - :param view: View to capture. If none, all views are captured. - :param filename: Filename of the desired output file. If none, no file will be written. - :param transparentBackground: Set the background to be transparent for single-view captures. - :param volumeNode: Vector volume node to store the capture image. If none, no vector volume node will be updated. - """ - slicer.app.processEvents() - if view: - if type(view)==slicer.qMRMLSliceView or type(view)==slicer.qMRMLThreeDView: - view.forceRender() - else: - view.repaint() - else: - slicer.util.forceRenderAllViews() - - if view is None: - if transparentBackground: - logging.warning("Transparent background is only available for single-view capture") - - # no view is specified, capture the entire view layout - - # Simply using grabwidget on the view layout frame would grab the screen without background: - # img = ctk.ctkWidgetsUtils.grabWidget(slicer.app.layoutManager().viewport()) - - # Grab the main window and use only the viewport's area - allViews = slicer.app.layoutManager().viewport() - topLeft = allViews.mapTo(slicer.util.mainWindow(),allViews.rect.topLeft()) - bottomRight = allViews.mapTo(slicer.util.mainWindow(),allViews.rect.bottomRight()) - imageSize = bottomRight - topLeft - - if imageSize.x()<2 or imageSize.y()<2: - # image is too small, most likely it is invalid - raise ValueError('Capture image from view failed') - - img = ctk.ctkWidgetsUtils.grabWidget(slicer.util.mainWindow(), qt.QRect(topLeft.x(), topLeft.y(), imageSize.x(), imageSize.y())) - - capturedImage = vtk.vtkImageData() - ctk.ctkVTKWidgetsUtils.qImageToVTKImageData(img, capturedImage) - - else: - # Capture single view - - rw = view.renderWindow() - wti = vtk.vtkWindowToImageFilter() - - if transparentBackground: - originalAlphaBitPlanes = rw.GetAlphaBitPlanes() - rw.SetAlphaBitPlanes(1) - ren=rw.GetRenderers().GetFirstRenderer() - originalGradientBackground = ren.GetGradientBackground() - ren.SetGradientBackground(False) - wti.SetInputBufferTypeToRGBA() - rw.Render() # need to render after changing bit planes - - wti.SetInput(rw) - wti.Update() - - if transparentBackground: - rw.SetAlphaBitPlanes(originalAlphaBitPlanes) - ren.SetGradientBackground(originalGradientBackground) - - capturedImage = wti.GetOutput() - - imageSize = capturedImage.GetDimensions() - - if imageSize[0]<2 or imageSize[1]<2: - # image is too small, most likely it is invalid - raise ValueError('Capture image from view failed') - - # Make sure image width and height is even, otherwise encoding may fail - imageWidthOdd = (imageSize[0] & 1 == 1) - imageHeightOdd = (imageSize[1] & 1 == 1) - if imageWidthOdd or imageHeightOdd: - imageClipper = vtk.vtkImageClip() - imageClipper.SetClipData(True) - imageClipper.SetInputData(capturedImage) - extent = capturedImage.GetExtent() - imageClipper.SetOutputWholeExtent(extent[0], extent[1]-1 if imageWidthOdd else extent[1], - extent[2], extent[3]-1 if imageHeightOdd else extent[3], - extent[4], extent[5]) - imageClipper.Update() - capturedImage = imageClipper.GetOutput() - - capturedImage = self.addWatermark(capturedImage) - if volumeNode is not None: - if isinstance(volumeNode, slicer.vtkMRMLVectorVolumeNode): - ijkToRas = vtk.vtkMatrix4x4() - ijkToRas.SetElement(0, 0, -1) - ijkToRas.SetElement(1, 1, -1) - volumeNode.SetIJKToRASMatrix(ijkToRas) - vflip = vtk.vtkImageFlip() - vflip.SetInputData(capturedImage) - vflip.SetFilteredAxis(1) - vflip.Update() - volumeNode.SetAndObserveImageData(vflip.GetOutput()) - else: - raise ValueError("Invalid vector volume node.") - if filename: - writer = self.createImageWriter(filename) - writer.SetInputData(capturedImage) - writer.SetFileName(filename) - writer.Write() - - def createImageWriter(self, filename): - name, extension = os.path.splitext(filename) - if extension.lower() == '.png': - return vtk.vtkPNGWriter() - elif extension.lower() == '.jpg' or extension.lower() == '.jpeg': - return vtk.vtkJPEGWriter() - else: - raise ValueError('Unsupported image format based on file name ' + filename) - - def createImageReader(self, filename): - name, extension = os.path.splitext(filename) - if extension.lower() == '.png': - return vtk.vtkPNGReader() - elif extension.lower() == '.jpg' or extension.lower() == '.jpeg': - return vtk.vtkJPEGReader() - else: - raise ValueError('Unsupported image format based on file name ' + filename) - - def addWatermark(self, capturedImage): - - if self.watermarkPosition < 0: - # no watermark - return capturedImage - - watermarkReader = vtk.vtkPNGReader() - watermarkReader.SetFileName(self.watermarkImagePath) - watermarkReader.Update() - watermarkImage = watermarkReader.GetOutput() - - # Add alpha channel, if image is only RGB and not RGBA - if watermarkImage.GetNumberOfScalarComponents() == 3: - alphaImage = vtk.vtkImageData() - alphaImage.SetDimensions(watermarkImage.GetDimensions()) - alphaImage.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1) - alphaImage.GetPointData().GetScalars().Fill(255) - appendRGBA = vtk.vtkImageAppendComponents() - appendRGBA.AddInputData(watermarkImage) - appendRGBA.AddInputData(alphaImage) - appendRGBA.Update() - watermarkImage = appendRGBA.GetOutput() - - watermarkSize = [0] * 3 - watermarkImage.GetDimensions(watermarkSize) - capturedImageSize = [0] * 3 - capturedImage.GetDimensions(capturedImageSize) - spacing = 100.0 / self.watermarkSizePercent - watermarkResize = vtk.vtkImageReslice() - watermarkResize.SetInterpolationModeToCubic() - watermarkResize.SetInputData(watermarkImage) - watermarkResize.SetOutputExtent(capturedImage.GetExtent()) - watermarkResize.SetOutputSpacing(spacing, spacing, 1) - position = self.watermarkPositionPresets[self.watermarkPosition]["position"](capturedImageSize, watermarkSize, spacing) - watermarkResize.SetOutputOrigin(position[0], position[1], 0.0) - watermarkResize.Update() - - blend = vtk.vtkImageBlend() - blend.SetOpacity(0, 1.0-self.watermarkOpacityPercent*0.01) - blend.SetOpacity(1, self.watermarkOpacityPercent*0.01) - blend.AddInputData(capturedImage) - blend.AddInputData(watermarkResize.GetOutput()) - blend.Update() - - return blend.GetOutput() - - def viewFromNode(self, viewNode): - if not viewNode: - raise ValueError('Invalid view node.') - elif viewNode.IsA("vtkMRMLSliceNode"): - return slicer.app.layoutManager().sliceWidget(viewNode.GetLayoutName()).sliceView() - elif viewNode.IsA("vtkMRMLViewNode"): - renderView = None - lm = slicer.app.layoutManager() - for widgetIndex in range(lm.threeDViewCount): - view = lm.threeDWidget(widgetIndex).threeDView() - if viewNode == view.mrmlViewNode(): - renderView = view - break - if not renderView: - raise ValueError('Selected 3D view is not visible in the current layout.') - return renderView - elif viewNode.IsA("vtkMRMLPlotViewNode"): - renderView = None - lm = slicer.app.layoutManager() - for viewIndex in range(lm.plotViewCount): - if viewNode == lm.plotWidget(viewIndex).mrmlPlotViewNode(): - renderView = lm.plotWidget(viewIndex).plotView() - break - if not renderView: - raise ValueError('Selected 3D view is not visible in the current layout.') - return renderView - else: - raise ValueError('Invalid view node.') - - def captureSliceSweep(self, sliceNode, startSliceOffset, endSliceOffset, numberOfImages, - outputDir, outputFilenamePattern, captureAllViews = None, transparentBackground = False): - - self.cancelRequested = False - - if not captureAllViews and not sliceNode.IsMappedInLayout(): - raise ValueError('Selected slice view is not visible in the current layout.') - - if not os.path.exists(outputDir): - os.makedirs(outputDir) - filePathPattern = os.path.join(outputDir,outputFilenamePattern) - - sliceLogic = self.getSliceLogicFromSliceNode(sliceNode) - originalSliceOffset = sliceLogic.GetSliceOffset() - - sliceView = self.viewFromNode(sliceNode) - compositeNode = sliceLogic.GetSliceCompositeNode() - offsetStepSize = (endSliceOffset-startSliceOffset)/(numberOfImages-1) - for offsetIndex in range(numberOfImages): - filename = filePathPattern % offsetIndex - self.addLog("Write "+filename) - sliceLogic.SetSliceOffset(startSliceOffset+offsetIndex*offsetStepSize) - self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground) - if self.cancelRequested: - break - - sliceLogic.SetSliceOffset(originalSliceOffset) - if self.cancelRequested: - raise ValueError('User requested cancel.') - - def captureSliceFade(self, sliceNode, numberOfImages, outputDir, - outputFilenamePattern, captureAllViews = None, transparentBackground = False): - - self.cancelRequested = False - - if not captureAllViews and not sliceNode.IsMappedInLayout(): - raise ValueError('Selected slice view is not visible in the current layout.') - - if not os.path.exists(outputDir): - os.makedirs(outputDir) - filePathPattern = os.path.join(outputDir, outputFilenamePattern) - - sliceLogic = self.getSliceLogicFromSliceNode(sliceNode) - sliceView = self.viewFromNode(sliceNode) - compositeNode = sliceLogic.GetSliceCompositeNode() - originalForegroundOpacity = compositeNode.GetForegroundOpacity() - startForegroundOpacity = 0.0 - endForegroundOpacity = 1.0 - opacityStepSize = (endForegroundOpacity - startForegroundOpacity) / (numberOfImages - 1) - for offsetIndex in range(numberOfImages): - filename = filePathPattern % offsetIndex - self.addLog("Write "+filename) - compositeNode.SetForegroundOpacity(startForegroundOpacity + offsetIndex * opacityStepSize) - self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground) - if self.cancelRequested: - break - - compositeNode.SetForegroundOpacity(originalForegroundOpacity) - - if self.cancelRequested: - raise ValueError('User requested cancel.') - - def capture3dViewRotation(self, viewNode, startRotation, endRotation, numberOfImages, rotationAxis, - outputDir, outputFilenamePattern, captureAllViews = None, transparentBackground = False): - """ - Acquire a set of screenshots of the 3D view while rotating it. - """ + if self.cancelRequested: + raise ValueError('User requested cancel.') - self.cancelRequested = False - - if not os.path.exists(outputDir): - os.makedirs(outputDir) - filePathPattern = os.path.join(outputDir, outputFilenamePattern) - - renderView = self.viewFromNode(viewNode) - - # Save original orientation and go to start orientation - originalPitchRollYawIncrement = renderView.pitchRollYawIncrement - originalDirection = renderView.pitchDirection - renderView.setPitchRollYawIncrement(-startRotation) - if rotationAxis == AXIS_YAW: - renderView.yawDirection = renderView.YawRight - renderView.yaw() - else: - renderView.pitchDirection = renderView.PitchDown - renderView.pitch() - - # Rotate step-by-step - rotationStepSize = (endRotation - startRotation) / (numberOfImages - 1) - renderView.setPitchRollYawIncrement(rotationStepSize) - if rotationAxis == AXIS_YAW: - renderView.yawDirection = renderView.YawLeft - else: - renderView.pitchDirection = renderView.PitchUp - for offsetIndex in range(numberOfImages): - if not self.cancelRequested: - filename = filePathPattern % offsetIndex - self.addLog("Write " + filename) - self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground) - if rotationAxis == AXIS_YAW: - renderView.yaw() - else: - renderView.pitch() - - # Restore original orientation and rotation step size & direction - if rotationAxis == AXIS_YAW: - renderView.yawDirection = renderView.YawRight - renderView.yaw() - renderView.setPitchRollYawIncrement(endRotation) - renderView.yaw() - renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement) - renderView.yawDirection = originalDirection - else: - renderView.pitchDirection = renderView.PitchDown - renderView.pitch() - renderView.setPitchRollYawIncrement(endRotation) - renderView.pitch() - renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement) - renderView.pitchDirection = originalDirection - - if self.cancelRequested: - raise ValueError('User requested cancel.') - - def captureSequence(self, viewNode, sequenceBrowserNode, sequenceStartIndex, + def captureSequence(self, viewNode, sequenceBrowserNode, sequenceStartIndex, sequenceEndIndex, numberOfImages, outputDir, outputFilenamePattern, - captureAllViews = None, transparentBackground = False): - """ - Acquire a set of screenshots of a view while iterating through a sequence. - """ + captureAllViews=None, transparentBackground=False): + """ + Acquire a set of screenshots of a view while iterating through a sequence. + """ + + self.cancelRequested = False + + if not os.path.exists(outputDir): + os.makedirs(outputDir) + filePathPattern = os.path.join(outputDir, outputFilenamePattern) + + originalSelectedItemNumber = sequenceBrowserNode.GetSelectedItemNumber() + + renderView = self.viewFromNode(viewNode) + stepSize = (sequenceEndIndex - sequenceStartIndex) / (numberOfImages - 1) + for offsetIndex in range(numberOfImages): + sequenceBrowserNode.SetSelectedItemNumber(int(sequenceStartIndex + offsetIndex * stepSize)) + filename = filePathPattern % offsetIndex + self.addLog("Write " + filename) + self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground) + if self.cancelRequested: + break + + sequenceBrowserNode.SetSelectedItemNumber(originalSelectedItemNumber) + if self.cancelRequested: + raise ValueError('User requested cancel.') + + def createLightboxImage(self, numberOfColumns, outputDir, imageFileNamePattern, numberOfImages, lightboxImageFilename): + self.addLog("Export to lightbox image...") + filePathPattern = os.path.join(outputDir, imageFileNamePattern) + import math + numberOfRows = int(math.ceil(numberOfImages / numberOfColumns)) + imageMarginSizePixels = 5 + for row in range(numberOfRows): + for column in range(numberOfColumns): + imageIndex = row * numberOfColumns + column + if imageIndex >= numberOfImages: + break + sourceFilename = filePathPattern % imageIndex + reader = self.createImageReader(sourceFilename) + reader.SetFileName(sourceFilename) + reader.Update() + image = reader.GetOutput() + + if imageIndex == 0: + # First image, initialize output lightbox image + imageDimensions = image.GetDimensions() + lightboxImageExtent = [0, numberOfColumns * imageDimensions[0] + (numberOfColumns - 1) * imageMarginSizePixels - 1, + 0, numberOfRows * imageDimensions[1] + (numberOfRows - 1) * imageMarginSizePixels - 1, + 0, 0] + lightboxCanvas = vtk.vtkImageCanvasSource2D() + lightboxCanvas.SetNumberOfScalarComponents(3) + lightboxCanvas.SetScalarTypeToUnsignedChar() + lightboxCanvas.SetExtent(lightboxImageExtent) + # Fill background with black + lightboxCanvas.SetDrawColor(50, 50, 50) + lightboxCanvas.FillBox(*lightboxImageExtent[0:4]) + + drawingPosition = [column * (imageDimensions[0] + imageMarginSizePixels), + lightboxImageExtent[3] - (row + 1) * imageDimensions[1] - row * imageMarginSizePixels + 1] + lightboxCanvas.DrawImage(drawingPosition[0], drawingPosition[1], image) + + lightboxCanvas.Update() + outputLightboxImageFilePath = os.path.join(outputDir, lightboxImageFilename) + writer = self.createImageWriter(outputLightboxImageFilePath) + writer.SetFileName(outputLightboxImageFilePath) + writer.SetInputData(lightboxCanvas.GetOutput()) + writer.Write() + + self.addLog("Lighbox image saved to file: " + outputLightboxImageFilePath) + + def createVideo(self, frameRate, extraOptions, outputDir, imageFileNamePattern, videoFileName): + self.addLog("Export to video...") + + # Get ffmpeg + import os.path + ffmpegPath = os.path.abspath(self.getFfmpegPath()) + if not ffmpegPath: + raise ValueError("Video creation failed: ffmpeg executable path is not defined") + if not os.path.isfile(ffmpegPath): + raise ValueError("Video creation failed: ffmpeg executable path is invalid: " + ffmpegPath) - self.cancelRequested = False - - if not os.path.exists(outputDir): - os.makedirs(outputDir) - filePathPattern = os.path.join(outputDir, outputFilenamePattern) - - originalSelectedItemNumber = sequenceBrowserNode.GetSelectedItemNumber() - - renderView = self.viewFromNode(viewNode) - stepSize = (sequenceEndIndex - sequenceStartIndex) / (numberOfImages - 1) - for offsetIndex in range(numberOfImages): - sequenceBrowserNode.SetSelectedItemNumber(int(sequenceStartIndex+offsetIndex*stepSize)) - filename = filePathPattern % offsetIndex - self.addLog("Write " + filename) - self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground) - if self.cancelRequested: - break - - sequenceBrowserNode.SetSelectedItemNumber(originalSelectedItemNumber) - if self.cancelRequested: - raise ValueError('User requested cancel.') - - def createLightboxImage(self, numberOfColumns, outputDir, imageFileNamePattern, numberOfImages, lightboxImageFilename): - self.addLog("Export to lightbox image...") - filePathPattern = os.path.join(outputDir, imageFileNamePattern) - import math - numberOfRows = int(math.ceil(numberOfImages/numberOfColumns)) - imageMarginSizePixels = 5 - for row in range(numberOfRows): - for column in range(numberOfColumns): - imageIndex = row * numberOfColumns + column - if imageIndex >= numberOfImages: - break - sourceFilename = filePathPattern % imageIndex - reader = self.createImageReader(sourceFilename) - reader.SetFileName(sourceFilename) - reader.Update() - image = reader.GetOutput() - - if imageIndex == 0: - # First image, initialize output lightbox image - imageDimensions = image.GetDimensions() - lightboxImageExtent = [0, numberOfColumns * imageDimensions[0] + (numberOfColumns - 1) * imageMarginSizePixels - 1, - 0, numberOfRows * imageDimensions[1] + (numberOfRows - 1) * imageMarginSizePixels - 1, - 0, 0] - lightboxCanvas = vtk.vtkImageCanvasSource2D() - lightboxCanvas.SetNumberOfScalarComponents(3) - lightboxCanvas.SetScalarTypeToUnsignedChar() - lightboxCanvas.SetExtent(lightboxImageExtent) - # Fill background with black - lightboxCanvas.SetDrawColor(50, 50, 50) - lightboxCanvas.FillBox(*lightboxImageExtent[0:4]) - - drawingPosition = [column * (imageDimensions[0] + imageMarginSizePixels), - lightboxImageExtent[3] - (row + 1) * imageDimensions[1] - row * imageMarginSizePixels + 1] - lightboxCanvas.DrawImage(drawingPosition[0], drawingPosition[1], image) - - lightboxCanvas.Update() - outputLightboxImageFilePath = os.path.join(outputDir, lightboxImageFilename) - writer = self.createImageWriter(outputLightboxImageFilePath) - writer.SetFileName(outputLightboxImageFilePath) - writer.SetInputData(lightboxCanvas.GetOutput()) - writer.Write() - - self.addLog("Lighbox image saved to file: "+outputLightboxImageFilePath) - - def createVideo(self, frameRate, extraOptions, outputDir, imageFileNamePattern, videoFileName): - self.addLog("Export to video...") - - # Get ffmpeg - import os.path - ffmpegPath = os.path.abspath(self.getFfmpegPath()) - if not ffmpegPath: - raise ValueError("Video creation failed: ffmpeg executable path is not defined") - if not os.path.isfile(ffmpegPath): - raise ValueError("Video creation failed: ffmpeg executable path is invalid: "+ffmpegPath) - - filePathPattern = os.path.join(outputDir, imageFileNamePattern) - outputVideoFilePath = os.path.join(outputDir, videoFileName) - ffmpegParams = [ffmpegPath, - "-nostdin", # disable stdin (to prevent hang when running Slicer as background process) - "-y", # overwrite without asking - "-r", str(frameRate), - "-start_number", "0", - "-i", str(filePathPattern)] - ffmpegParams += [_f for _f in extraOptions.split(' ') if _f] - ffmpegParams.append(outputVideoFilePath) - - self.addLog("Start ffmpeg:\n"+' '.join(ffmpegParams)) - - import subprocess - p = subprocess.Popen(ffmpegParams, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=outputDir) - stdout, stderr = p.communicate() - if p.returncode != 0: - self.addLog("ffmpeg error output: " + stderr.decode()) - raise ValueError("ffmpeg returned with error") - else: - self.addLog("Video export succeeded to file: "+outputVideoFilePath) - logging.debug("ffmpeg standard output: " + stdout.decode()) - logging.debug("ffmpeg error output: " + stderr.decode()) - - def deleteTemporaryFiles(self, outputDir, imageFileNamePattern, numberOfImages): - """ - Delete files after a video has been created from them. - """ - import os - filePathPattern = os.path.join(outputDir, imageFileNamePattern) - for imageIndex in range(numberOfImages): - filename = filePathPattern % imageIndex - logging.debug("Delete temporary file " + filename) - os.remove(filename) - - def getNextAvailableFileName(self, outputDir, outputFilenamePattern, snapshotIndex): - """ - Find a file index that does not overwrite any existing file. - """ - if not os.path.exists(outputDir): - os.makedirs(outputDir) - filePathPattern = os.path.join(outputDir,outputFilenamePattern) - filename = None - while True: - filename = filePathPattern % snapshotIndex - if not os.path.exists(filename): - # found an available file name - break - snapshotIndex += 1 - return [filename, snapshotIndex] + filePathPattern = os.path.join(outputDir, imageFileNamePattern) + outputVideoFilePath = os.path.join(outputDir, videoFileName) + ffmpegParams = [ffmpegPath, + "-nostdin", # disable stdin (to prevent hang when running Slicer as background process) + "-y", # overwrite without asking + "-r", str(frameRate), + "-start_number", "0", + "-i", str(filePathPattern)] + ffmpegParams += [_f for _f in extraOptions.split(' ') if _f] + ffmpegParams.append(outputVideoFilePath) + + self.addLog("Start ffmpeg:\n" + ' '.join(ffmpegParams)) + + import subprocess + p = subprocess.Popen(ffmpegParams, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=outputDir) + stdout, stderr = p.communicate() + if p.returncode != 0: + self.addLog("ffmpeg error output: " + stderr.decode()) + raise ValueError("ffmpeg returned with error") + else: + self.addLog("Video export succeeded to file: " + outputVideoFilePath) + logging.debug("ffmpeg standard output: " + stdout.decode()) + logging.debug("ffmpeg error output: " + stderr.decode()) + + def deleteTemporaryFiles(self, outputDir, imageFileNamePattern, numberOfImages): + """ + Delete files after a video has been created from them. + """ + import os + filePathPattern = os.path.join(outputDir, imageFileNamePattern) + for imageIndex in range(numberOfImages): + filename = filePathPattern % imageIndex + logging.debug("Delete temporary file " + filename) + os.remove(filename) + + def getNextAvailableFileName(self, outputDir, outputFilenamePattern, snapshotIndex): + """ + Find a file index that does not overwrite any existing file. + """ + if not os.path.exists(outputDir): + os.makedirs(outputDir) + filePathPattern = os.path.join(outputDir, outputFilenamePattern) + filename = None + while True: + filename = filePathPattern % snapshotIndex + if not os.path.exists(filename): + # found an available file name + break + snapshotIndex += 1 + return [filename, snapshotIndex] class ScreenCaptureTest(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) - import SampleData - self.image1 = SampleData.downloadSample('MRBrainTumor1') - self.image2 = SampleData.downloadSample('MRBrainTumor2') - - # make the output volume appear in all the slice views - selectionNode = slicer.app.applicationLogic().GetSelectionNode() - selectionNode.SetActiveVolumeID(self.image1.GetID()) - selectionNode.SetSecondaryVolumeID(self.image2.GetID()) - slicer.app.applicationLogic().PropagateVolumeSelection(1) - - # Show slice and 3D views - layoutManager = slicer.app.layoutManager() - layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView) - for sliceViewNodeId in ['vtkMRMLSliceNodeRed', 'vtkMRMLSliceNodeYellow', 'vtkMRMLSliceNodeGreen']: - slicer.mrmlScene.GetNodeByID(sliceViewNodeId).SetSliceVisible(True) - - self.tempDir= slicer.app.temporaryPath + '/ScreenCaptureTest' - self.numberOfImages = 10 - self.imageFileNamePattern = "image_%05d.png" - - self.logic = ScreenCaptureLogic() - - def verifyAndDeleteWrittenFiles(self): - import os - filePathPattern = os.path.join(self.tempDir, self.imageFileNamePattern) - for imageIndex in range(self.numberOfImages): - filename = filePathPattern % imageIndex - self.assertTrue(os.path.exists(filename)) - self.logic.deleteTemporaryFiles(self.tempDir, self.imageFileNamePattern, self.numberOfImages) - for imageIndex in range(self.numberOfImages): - filename = filePathPattern % imageIndex - self.assertFalse(os.path.exists(filename)) - - def runTest(self): - """Run as few or as many tests as needed here. + 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 """ - self.setUp() - self.test_SliceSweep() - self.test_SliceFade() - self.test_3dViewRotation() - self.test_VolumeNodeUpdate() - - def test_SliceSweep(self): - self.delayDisplay("Testing SliceSweep") - viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceNodeRed') - self.assertIsNotNone(viewNode) - self.logic.captureSliceSweep(viewNode, -125, 75, self.numberOfImages, self.tempDir, self.imageFileNamePattern) - self.verifyAndDeleteWrittenFiles() - self.delayDisplay('Testing SliceSweep completed successfully') - - def test_SliceFade(self): - self.delayDisplay("Testing SliceFade") - viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceNodeRed') - self.assertIsNotNone(viewNode) - self.logic.captureSliceFade(viewNode, self.numberOfImages, self.tempDir, self.imageFileNamePattern) - self.verifyAndDeleteWrittenFiles() - self.delayDisplay('Testing SliceFade completed successfully') - - def test_3dViewRotation(self): - self.delayDisplay("Testing 3D view rotation") - viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLViewNode1') - self.assertIsNotNone(viewNode) - self.logic.capture3dViewRotation(viewNode, -180, 180, self.numberOfImages, AXIS_YAW, self.tempDir, self.imageFileNamePattern) - self.verifyAndDeleteWrittenFiles() - self.delayDisplay('Testing 3D view rotation completed successfully') - - def test_VolumeNodeUpdate(self): - self.delayDisplay("Testing VolumeNode update") - viewNode = None # Capture All Views - volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode") - self.assertIsNotNone(volumeNode) - self.assertIsNone(volumeNode.GetImageData()) - self.logic.captureImageFromView(viewNode, volumeNode=volumeNode) - self.assertIsNotNone(volumeNode.GetImageData()) - self.delayDisplay('Testing VolumeNode update completed successfully') + + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) + import SampleData + self.image1 = SampleData.downloadSample('MRBrainTumor1') + self.image2 = SampleData.downloadSample('MRBrainTumor2') + + # make the output volume appear in all the slice views + selectionNode = slicer.app.applicationLogic().GetSelectionNode() + selectionNode.SetActiveVolumeID(self.image1.GetID()) + selectionNode.SetSecondaryVolumeID(self.image2.GetID()) + slicer.app.applicationLogic().PropagateVolumeSelection(1) + + # Show slice and 3D views + layoutManager = slicer.app.layoutManager() + layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView) + for sliceViewNodeId in ['vtkMRMLSliceNodeRed', 'vtkMRMLSliceNodeYellow', 'vtkMRMLSliceNodeGreen']: + slicer.mrmlScene.GetNodeByID(sliceViewNodeId).SetSliceVisible(True) + + self.tempDir = slicer.app.temporaryPath + '/ScreenCaptureTest' + self.numberOfImages = 10 + self.imageFileNamePattern = "image_%05d.png" + + self.logic = ScreenCaptureLogic() + + def verifyAndDeleteWrittenFiles(self): + import os + filePathPattern = os.path.join(self.tempDir, self.imageFileNamePattern) + for imageIndex in range(self.numberOfImages): + filename = filePathPattern % imageIndex + self.assertTrue(os.path.exists(filename)) + self.logic.deleteTemporaryFiles(self.tempDir, self.imageFileNamePattern, self.numberOfImages) + for imageIndex in range(self.numberOfImages): + filename = filePathPattern % imageIndex + self.assertFalse(os.path.exists(filename)) + + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_SliceSweep() + self.test_SliceFade() + self.test_3dViewRotation() + self.test_VolumeNodeUpdate() + + def test_SliceSweep(self): + self.delayDisplay("Testing SliceSweep") + viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceNodeRed') + self.assertIsNotNone(viewNode) + self.logic.captureSliceSweep(viewNode, -125, 75, self.numberOfImages, self.tempDir, self.imageFileNamePattern) + self.verifyAndDeleteWrittenFiles() + self.delayDisplay('Testing SliceSweep completed successfully') + + def test_SliceFade(self): + self.delayDisplay("Testing SliceFade") + viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceNodeRed') + self.assertIsNotNone(viewNode) + self.logic.captureSliceFade(viewNode, self.numberOfImages, self.tempDir, self.imageFileNamePattern) + self.verifyAndDeleteWrittenFiles() + self.delayDisplay('Testing SliceFade completed successfully') + + def test_3dViewRotation(self): + self.delayDisplay("Testing 3D view rotation") + viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLViewNode1') + self.assertIsNotNone(viewNode) + self.logic.capture3dViewRotation(viewNode, -180, 180, self.numberOfImages, AXIS_YAW, self.tempDir, self.imageFileNamePattern) + self.verifyAndDeleteWrittenFiles() + self.delayDisplay('Testing 3D view rotation completed successfully') + + def test_VolumeNodeUpdate(self): + self.delayDisplay("Testing VolumeNode update") + viewNode = None # Capture All Views + volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode") + self.assertIsNotNone(volumeNode) + self.assertIsNone(volumeNode.GetImageData()) + self.logic.captureImageFromView(viewNode, volumeNode=volumeNode) + self.assertIsNotNone(volumeNode.GetImageData()) + self.delayDisplay('Testing VolumeNode update completed successfully') diff --git a/Modules/Scripted/SegmentEditor/SegmentEditor.py b/Modules/Scripted/SegmentEditor/SegmentEditor.py index 1d5d140cf1b..f034a214b99 100644 --- a/Modules/Scripted/SegmentEditor/SegmentEditor.py +++ b/Modules/Scripted/SegmentEditor/SegmentEditor.py @@ -7,173 +7,173 @@ # SegmentEditor # class SegmentEditor(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "Segment Editor" - self.parent.categories = ["", "Segmentation"] - self.parent.dependencies = ["Segmentations", "SubjectHierarchy"] - self.parent.contributors = ["Csaba Pinter (Queen's University), Andras Lasso (Queen's University)"] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "Segment Editor" + self.parent.categories = ["", "Segmentation"] + self.parent.dependencies = ["Segmentations", "SubjectHierarchy"] + self.parent.contributors = ["Csaba Pinter (Queen's University), Andras Lasso (Queen's University)"] + self.parent.helpText = """ This module allows editing segmentation objects by directly drawing and using segmentaiton tools on the contained segments. Representations other than the labelmap one (which is used for editing) are automatically updated real-time, so for example the closed surface can be visualized as edited in the 3D view. """ - self.parent.helpText += parent.defaultDocumentationLink - self.parent.acknowledgementText = """ + self.parent.helpText += parent.defaultDocumentationLink + self.parent.acknowledgementText = """ This work is part of SparKit project, funded by Cancer Care Ontario (CCO)'s ACRU program and Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO). """ - def setup(self): - # Register subject hierarchy plugin - import SubjectHierarchyPlugins - scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None) - scriptedPlugin.setPythonSource(SubjectHierarchyPlugins.SegmentEditorSubjectHierarchyPlugin.filePath) + def setup(self): + # Register subject hierarchy plugin + import SubjectHierarchyPlugins + scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None) + scriptedPlugin.setPythonSource(SubjectHierarchyPlugins.SegmentEditorSubjectHierarchyPlugin.filePath) # # SegmentEditorWidget # class SegmentEditorWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): - def __init__(self, parent): - ScriptedLoadableModuleWidget.__init__(self, parent) - VTKObservationMixin.__init__(self) - - # Members - self.parameterSetNode = None - self.editor = None - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - # Add margin to the sides - self.layout.setContentsMargins(4,0,4,0) - - # - # Segment editor widget - # - import qSlicerSegmentationsModuleWidgetsPythonQt - self.editor = qSlicerSegmentationsModuleWidgetsPythonQt.qMRMLSegmentEditorWidget() - self.editor.setMaximumNumberOfUndoStates(10) - # Set parameter node first so that the automatic selections made when the scene is set are saved - self.selectParameterNode() - self.editor.setMRMLScene(slicer.mrmlScene) - self.layout.addWidget(self.editor) - - # Observe editor effect registrations to make sure that any effects that are registered - # later will show up in the segment editor widget. For example, if Segment Editor is set - # as startup module, additional effects are registered after the segment editor widget is created. - self.effectFactorySingleton = slicer.qSlicerSegmentEditorEffectFactory.instance() - self.effectFactorySingleton.connect('effectRegistered(QString)', self.editorEffectRegistered) - - # Connect observers to scene events - self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) - self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) - self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndImportEvent, self.onSceneEndImport) - - def editorEffectRegistered(self): - self.editor.updateEffectList() - - def selectParameterNode(self): - # Select parameter set node if one is found in the scene, and create one otherwise - segmentEditorSingletonTag = "SegmentEditor" - segmentEditorNode = slicer.mrmlScene.GetSingletonNode(segmentEditorSingletonTag, "vtkMRMLSegmentEditorNode") - if segmentEditorNode is None: - segmentEditorNode = slicer.mrmlScene.CreateNodeByClass("vtkMRMLSegmentEditorNode") - segmentEditorNode.UnRegister(None) - segmentEditorNode.SetSingletonTag(segmentEditorSingletonTag) - segmentEditorNode = slicer.mrmlScene.AddNode(segmentEditorNode) - if self.parameterSetNode == segmentEditorNode: - # nothing changed - return - self.parameterSetNode = segmentEditorNode - self.editor.setMRMLSegmentEditorNode(self.parameterSetNode) - - def getDefaultMasterVolumeNodeID(self): - layoutManager = slicer.app.layoutManager() - firstForegroundVolumeID = None - # Use first background volume node in any of the displayed layouts. - # If no beackground volume node is in any slice view then use the first - # foreground volume node. - for sliceViewName in layoutManager.sliceViewNames(): - sliceWidget = layoutManager.sliceWidget(sliceViewName) - if not sliceWidget: - continue - compositeNode = sliceWidget.mrmlSliceCompositeNode() - if compositeNode.GetBackgroundVolumeID(): - return compositeNode.GetBackgroundVolumeID() - if compositeNode.GetForegroundVolumeID() and not firstForegroundVolumeID: - firstForegroundVolumeID = compositeNode.GetForegroundVolumeID() - # No background volume was found, so use the foreground volume (if any was found) - return firstForegroundVolumeID - - def enter(self): - """Runs whenever the module is reopened - """ - if self.editor.turnOffLightboxes(): - slicer.util.warningDisplay('Segment Editor is not compatible with slice viewers in light box mode.' - 'Views are being reset.', windowTitle='Segment Editor') - - # Allow switching between effects and selected segment using keyboard shortcuts - self.editor.installKeyboardShortcuts() - - # Set parameter set node if absent - self.selectParameterNode() - self.editor.updateWidgetFromMRML() - - # If no segmentation node exists then create one so that the user does not have to create one manually - if not self.editor.segmentationNodeID(): - segmentationNode = slicer.mrmlScene.GetFirstNode(None, "vtkMRMLSegmentationNode") - if not segmentationNode: - segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') - self.editor.setSegmentationNode(segmentationNode) - if not self.editor.masterVolumeNodeID(): - masterVolumeNodeID = self.getDefaultMasterVolumeNodeID() - self.editor.setMasterVolumeNodeID(masterVolumeNodeID) - - def exit(self): - self.editor.setActiveEffect(None) - self.editor.uninstallKeyboardShortcuts() - self.editor.removeViewObservations() - - def onSceneStartClose(self, caller, event): - self.parameterSetNode = None - self.editor.setSegmentationNode(None) - self.editor.removeViewObservations() - - def onSceneEndClose(self, caller, event): - if self.parent.isEntered: - self.selectParameterNode() - self.editor.updateWidgetFromMRML() - - def onSceneEndImport(self, caller, event): - if self.parent.isEntered: - self.selectParameterNode() - self.editor.updateWidgetFromMRML() - - def cleanup(self): - self.removeObservers() - self.effectFactorySingleton.disconnect('effectRegistered(QString)', self.editorEffectRegistered) + def __init__(self, parent): + ScriptedLoadableModuleWidget.__init__(self, parent) + VTKObservationMixin.__init__(self) + + # Members + self.parameterSetNode = None + self.editor = None + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + # Add margin to the sides + self.layout.setContentsMargins(4, 0, 4, 0) + + # + # Segment editor widget + # + import qSlicerSegmentationsModuleWidgetsPythonQt + self.editor = qSlicerSegmentationsModuleWidgetsPythonQt.qMRMLSegmentEditorWidget() + self.editor.setMaximumNumberOfUndoStates(10) + # Set parameter node first so that the automatic selections made when the scene is set are saved + self.selectParameterNode() + self.editor.setMRMLScene(slicer.mrmlScene) + self.layout.addWidget(self.editor) + + # Observe editor effect registrations to make sure that any effects that are registered + # later will show up in the segment editor widget. For example, if Segment Editor is set + # as startup module, additional effects are registered after the segment editor widget is created. + self.effectFactorySingleton = slicer.qSlicerSegmentEditorEffectFactory.instance() + self.effectFactorySingleton.connect('effectRegistered(QString)', self.editorEffectRegistered) + + # Connect observers to scene events + self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) + self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) + self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndImportEvent, self.onSceneEndImport) + + def editorEffectRegistered(self): + self.editor.updateEffectList() + + def selectParameterNode(self): + # Select parameter set node if one is found in the scene, and create one otherwise + segmentEditorSingletonTag = "SegmentEditor" + segmentEditorNode = slicer.mrmlScene.GetSingletonNode(segmentEditorSingletonTag, "vtkMRMLSegmentEditorNode") + if segmentEditorNode is None: + segmentEditorNode = slicer.mrmlScene.CreateNodeByClass("vtkMRMLSegmentEditorNode") + segmentEditorNode.UnRegister(None) + segmentEditorNode.SetSingletonTag(segmentEditorSingletonTag) + segmentEditorNode = slicer.mrmlScene.AddNode(segmentEditorNode) + if self.parameterSetNode == segmentEditorNode: + # nothing changed + return + self.parameterSetNode = segmentEditorNode + self.editor.setMRMLSegmentEditorNode(self.parameterSetNode) + + def getDefaultMasterVolumeNodeID(self): + layoutManager = slicer.app.layoutManager() + firstForegroundVolumeID = None + # Use first background volume node in any of the displayed layouts. + # If no beackground volume node is in any slice view then use the first + # foreground volume node. + for sliceViewName in layoutManager.sliceViewNames(): + sliceWidget = layoutManager.sliceWidget(sliceViewName) + if not sliceWidget: + continue + compositeNode = sliceWidget.mrmlSliceCompositeNode() + if compositeNode.GetBackgroundVolumeID(): + return compositeNode.GetBackgroundVolumeID() + if compositeNode.GetForegroundVolumeID() and not firstForegroundVolumeID: + firstForegroundVolumeID = compositeNode.GetForegroundVolumeID() + # No background volume was found, so use the foreground volume (if any was found) + return firstForegroundVolumeID + + def enter(self): + """Runs whenever the module is reopened + """ + if self.editor.turnOffLightboxes(): + slicer.util.warningDisplay('Segment Editor is not compatible with slice viewers in light box mode.' + 'Views are being reset.', windowTitle='Segment Editor') + + # Allow switching between effects and selected segment using keyboard shortcuts + self.editor.installKeyboardShortcuts() + + # Set parameter set node if absent + self.selectParameterNode() + self.editor.updateWidgetFromMRML() + + # If no segmentation node exists then create one so that the user does not have to create one manually + if not self.editor.segmentationNodeID(): + segmentationNode = slicer.mrmlScene.GetFirstNode(None, "vtkMRMLSegmentationNode") + if not segmentationNode: + segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') + self.editor.setSegmentationNode(segmentationNode) + if not self.editor.masterVolumeNodeID(): + masterVolumeNodeID = self.getDefaultMasterVolumeNodeID() + self.editor.setMasterVolumeNodeID(masterVolumeNodeID) + + def exit(self): + self.editor.setActiveEffect(None) + self.editor.uninstallKeyboardShortcuts() + self.editor.removeViewObservations() + + def onSceneStartClose(self, caller, event): + self.parameterSetNode = None + self.editor.setSegmentationNode(None) + self.editor.removeViewObservations() + + def onSceneEndClose(self, caller, event): + if self.parent.isEntered: + self.selectParameterNode() + self.editor.updateWidgetFromMRML() + + def onSceneEndImport(self, caller, event): + if self.parent.isEntered: + self.selectParameterNode() + self.editor.updateWidgetFromMRML() + + def cleanup(self): + self.removeObservers() + self.effectFactorySingleton.disconnect('effectRegistered(QString)', self.editorEffectRegistered) class SegmentEditorTest(ScriptedLoadableModuleTest): - """ - This is the test case for your scripted module. - """ - - 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): - """Currently no testing functionality. + This is the test case for your scripted module. """ - self.setUp() - self.test_SegmentEditor1() - def test_SegmentEditor1(self): - """Add test here later. - """ - self.delayDisplay("Starting the test") - self.delayDisplay('Test passed!') + 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): + """Currently no testing functionality. + """ + self.setUp() + self.test_SegmentEditor1() + + def test_SegmentEditor1(self): + """Add test here later. + """ + self.delayDisplay("Starting the test") + self.delayDisplay('Test passed!') diff --git a/Modules/Scripted/SegmentEditor/SubjectHierarchyPlugins/SegmentEditorSubjectHierarchyPlugin.py b/Modules/Scripted/SegmentEditor/SubjectHierarchyPlugins/SegmentEditorSubjectHierarchyPlugin.py index 74c2e86a0b0..1393d24a63c 100644 --- a/Modules/Scripted/SegmentEditor/SubjectHierarchyPlugins/SegmentEditorSubjectHierarchyPlugin.py +++ b/Modules/Scripted/SegmentEditor/SubjectHierarchyPlugins/SegmentEditorSubjectHierarchyPlugin.py @@ -8,136 +8,136 @@ class SegmentEditorSubjectHierarchyPlugin(AbstractScriptedSubjectHierarchyPlugin): - """ Scripted subject hierarchy plugin for the Segment Editor module. - - This is also an example for scripted plugins, so includes all possible methods. - The methods that are not needed (i.e. the default implementation in - qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be - omitted in plugins created based on this one. - """ - - # Necessary static member to be able to set python source to scripted subject hierarchy plugin - filePath = __file__ - - def __init__(self, scriptedPlugin): - scriptedPlugin.name = 'SegmentEditor' - AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin) - - self.segmentEditorAction = qt.QAction("Segment this...", scriptedPlugin) - self.segmentEditorAction.connect("triggered()", self.onSegment) - - def canAddNodeToSubjectHierarchy(self, node, parentItemID): - # This plugin cannot own any items (it's not a role but a function plugin), - # but the it can be decided the following way: - # if node is not None and node.IsA("vtkMRMLMyNode"): - # return 1.0 - return 0.0 - - def canOwnSubjectHierarchyItem(self, itemID): - # This plugin cannot own any items (it's not a role but a function plugin), - # but the it can be decided the following way: - # pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - # shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - # associatedNode = shNode.GetItemDataNode(itemID) - # if associatedNode is not None and associatedNode.IsA("vtkMRMLMyNode"): - # return 1.0 - return 0.0 - - def roleForPlugin(self): - # As this plugin cannot own any items, it doesn't have a role either - return "N/A" - - def helpText(self): - # return ("

          " - # "" - # "SegmentEditor module subject hierarchy help text" - # "" - # "

          " - # "

          " - # "" - # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin." - # "" - # "

          \n") - return "" - - def icon(self, itemID): - # As this plugin cannot own any items, it doesn't have an icon either - # import os - # iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MyIcon.png') - # if self.canOwnSubjectHierarchyItem(itemID) > 0.0 and os.path.exists(iconPath): - # return qt.QIcon(iconPath) - # Item unknown by plugin - return qt.QIcon() - - def visibilityIcon(self, visible): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible) - - def editProperties(self, itemID): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - pluginHandlerSingleton.pluginByName('Default').editProperties(itemID) - - def itemContextMenuActions(self): - return [self.segmentEditorAction] - - def onSegment(self): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - currentItemID = pluginHandlerSingleton.currentItem() - if not currentItemID: - logging.error("Invalid current item") - return - - shNode = slicer.mrmlScene.GetSubjectHierarchyNode() - volumeNode = shNode.GetItemDataNode(currentItemID) - - # Switch to Segment Editor module - pluginHandlerSingleton.pluginByName('Default').switchToModule('SegmentEditor') - editorWidget = slicer.modules.segmenteditor.widgetRepresentation().self() - - # Create new segmentation only if there is no segmentation node, or the current segmentation is not empty - # (switching to the module will create an empty segmentation if there is none in the scene, but not otherwise) - segmentationNode = editorWidget.parameterSetNode.GetSegmentationNode() - if segmentationNode is None or segmentationNode.GetSegmentation().GetNumberOfSegments() > 0: - segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') - editorWidget.parameterSetNode.SetAndObserveSegmentationNode(segmentationNode) - # Name segmentation node based on the volume - segmentationNode.SetName(volumeNode.GetName() + '_Segmentation') - - # Set master volume - editorWidget.parameterSetNode.SetAndObserveMasterVolumeNode(volumeNode) - - # Place segmentation under the master volume in subject hierarchy - segmentationShItemID = shNode.GetItemByDataNode(segmentationNode) - shNode.SetItemParent(segmentationShItemID, shNode.GetItemParent(currentItemID)) - - def sceneContextMenuActions(self): - return [] - - def showContextMenuActionsForItem(self, itemID): - # Scene - if not itemID: - # No scene context menu actions in this plugin - return - - # Volume but not LabelMap - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - if ( pluginHandlerSingleton.pluginByName('Volumes').canOwnSubjectHierarchyItem(itemID) - and not pluginHandlerSingleton.pluginByName('LabelMaps').canOwnSubjectHierarchyItem(itemID) ): - # Get current item - currentItemID = pluginHandlerSingleton.currentItem() - if not currentItemID: - logging.error("Invalid current item") - return - self.segmentEditorAction.visible = True - - def tooltip(self, itemID): - # As this plugin cannot own any items, it doesn't provide tooltip either - return "" - - def setDisplayVisibility(self, itemID, visible): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible) - - def getDisplayVisibility(self, itemID): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID) + """ Scripted subject hierarchy plugin for the Segment Editor module. + + This is also an example for scripted plugins, so includes all possible methods. + The methods that are not needed (i.e. the default implementation in + qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be + omitted in plugins created based on this one. + """ + + # Necessary static member to be able to set python source to scripted subject hierarchy plugin + filePath = __file__ + + def __init__(self, scriptedPlugin): + scriptedPlugin.name = 'SegmentEditor' + AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin) + + self.segmentEditorAction = qt.QAction("Segment this...", scriptedPlugin) + self.segmentEditorAction.connect("triggered()", self.onSegment) + + def canAddNodeToSubjectHierarchy(self, node, parentItemID): + # This plugin cannot own any items (it's not a role but a function plugin), + # but the it can be decided the following way: + # if node is not None and node.IsA("vtkMRMLMyNode"): + # return 1.0 + return 0.0 + + def canOwnSubjectHierarchyItem(self, itemID): + # This plugin cannot own any items (it's not a role but a function plugin), + # but the it can be decided the following way: + # pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + # shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + # associatedNode = shNode.GetItemDataNode(itemID) + # if associatedNode is not None and associatedNode.IsA("vtkMRMLMyNode"): + # return 1.0 + return 0.0 + + def roleForPlugin(self): + # As this plugin cannot own any items, it doesn't have a role either + return "N/A" + + def helpText(self): + # return ("

          " + # "" + # "SegmentEditor module subject hierarchy help text" + # "" + # "

          " + # "

          " + # "" + # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin." + # "" + # "

          \n") + return "" + + def icon(self, itemID): + # As this plugin cannot own any items, it doesn't have an icon either + # import os + # iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MyIcon.png') + # if self.canOwnSubjectHierarchyItem(itemID) > 0.0 and os.path.exists(iconPath): + # return qt.QIcon(iconPath) + # Item unknown by plugin + return qt.QIcon() + + def visibilityIcon(self, visible): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible) + + def editProperties(self, itemID): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + pluginHandlerSingleton.pluginByName('Default').editProperties(itemID) + + def itemContextMenuActions(self): + return [self.segmentEditorAction] + + def onSegment(self): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + currentItemID = pluginHandlerSingleton.currentItem() + if not currentItemID: + logging.error("Invalid current item") + return + + shNode = slicer.mrmlScene.GetSubjectHierarchyNode() + volumeNode = shNode.GetItemDataNode(currentItemID) + + # Switch to Segment Editor module + pluginHandlerSingleton.pluginByName('Default').switchToModule('SegmentEditor') + editorWidget = slicer.modules.segmenteditor.widgetRepresentation().self() + + # Create new segmentation only if there is no segmentation node, or the current segmentation is not empty + # (switching to the module will create an empty segmentation if there is none in the scene, but not otherwise) + segmentationNode = editorWidget.parameterSetNode.GetSegmentationNode() + if segmentationNode is None or segmentationNode.GetSegmentation().GetNumberOfSegments() > 0: + segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') + editorWidget.parameterSetNode.SetAndObserveSegmentationNode(segmentationNode) + # Name segmentation node based on the volume + segmentationNode.SetName(volumeNode.GetName() + '_Segmentation') + + # Set master volume + editorWidget.parameterSetNode.SetAndObserveMasterVolumeNode(volumeNode) + + # Place segmentation under the master volume in subject hierarchy + segmentationShItemID = shNode.GetItemByDataNode(segmentationNode) + shNode.SetItemParent(segmentationShItemID, shNode.GetItemParent(currentItemID)) + + def sceneContextMenuActions(self): + return [] + + def showContextMenuActionsForItem(self, itemID): + # Scene + if not itemID: + # No scene context menu actions in this plugin + return + + # Volume but not LabelMap + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + if (pluginHandlerSingleton.pluginByName('Volumes').canOwnSubjectHierarchyItem(itemID) + and not pluginHandlerSingleton.pluginByName('LabelMaps').canOwnSubjectHierarchyItem(itemID)): + # Get current item + currentItemID = pluginHandlerSingleton.currentItem() + if not currentItemID: + logging.error("Invalid current item") + return + self.segmentEditorAction.visible = True + + def tooltip(self, itemID): + # As this plugin cannot own any items, it doesn't provide tooltip either + return "" + + def setDisplayVisibility(self, itemID, visible): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible) + + def getDisplayVisibility(self, itemID): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID) diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatistics.py b/Modules/Scripted/SegmentStatistics/SegmentStatistics.py index b035eb22954..64772ad1d58 100644 --- a/Modules/Scripted/SegmentStatistics/SegmentStatistics.py +++ b/Modules/Scripted/SegmentStatistics/SegmentStatistics.py @@ -11,17 +11,17 @@ class SegmentStatistics(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 = "Segment Statistics" - self.parent.categories = ["Quantification"] - self.parent.dependencies = ["SubjectHierarchy"] - self.parent.contributors = ["Andras Lasso (PerkLab), Christian Bauer (University of Iowa), Steve Pieper (Isomics)"] - self.parent.helpText = """ + """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 = "Segment Statistics" + self.parent.categories = ["Quantification"] + self.parent.dependencies = ["SubjectHierarchy"] + self.parent.contributors = ["Andras Lasso (PerkLab), Christian Bauer (University of Iowa), Steve Pieper (Isomics)"] + self.parent.helpText = """ Use this module to calculate counts and volumes for segments plus statistics on the grayscale background volume. Computed fields: Segment labelmap statistics (LM): voxel count, volume mm3, volume cm3. @@ -32,247 +32,247 @@ def __init__(self, parent): Closed surface statistics (CS): surface mm2, volume mm3, volume cm3 (computed from closed surface). Requires segment closed surface representation. """ - self.parent.helpText += parent.defaultDocumentationLink - self.parent.acknowledgementText = """ + self.parent.helpText += parent.defaultDocumentationLink + self.parent.acknowledgementText = """ Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details. """ - def setup(self): - # Register subject hierarchy plugin - import SubjectHierarchyPlugins - scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None) - scriptedPlugin.setPythonSource(SubjectHierarchyPlugins.SegmentStatisticsSubjectHierarchyPlugin.filePath) + def setup(self): + # Register subject hierarchy plugin + import SubjectHierarchyPlugins + scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None) + scriptedPlugin.setPythonSource(SubjectHierarchyPlugins.SegmentStatisticsSubjectHierarchyPlugin.filePath) class SegmentStatisticsWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - self.logic = SegmentStatisticsLogic() - self.grayscaleNode = None - self.labelNode = None - self.parameterNode = None - self.parameterNodeObserver = None - - # Instantiate and connect widgets ... - # - - # Parameter set selector - self.parameterNodeSelector = slicer.qMRMLNodeComboBox() - self.parameterNodeSelector.nodeTypes = ["vtkMRMLScriptedModuleNode"] - self.parameterNodeSelector.addAttribute( "vtkMRMLScriptedModuleNode", "ModuleName", "SegmentStatistics" ) - self.parameterNodeSelector.selectNodeUponCreation = True - self.parameterNodeSelector.addEnabled = True - self.parameterNodeSelector.renameEnabled = True - self.parameterNodeSelector.removeEnabled = True - self.parameterNodeSelector.noneEnabled = False - self.parameterNodeSelector.showHidden = True - self.parameterNodeSelector.showChildNodeTypes = False - self.parameterNodeSelector.baseName = "SegmentStatistics" - self.parameterNodeSelector.setMRMLScene( slicer.mrmlScene ) - self.parameterNodeSelector.setToolTip( "Pick parameter set" ) - self.layout.addWidget(self.parameterNodeSelector) - - # Inputs - inputsCollapsibleButton = ctk.ctkCollapsibleButton() - inputsCollapsibleButton.text = "Inputs" - self.layout.addWidget(inputsCollapsibleButton) - inputsFormLayout = qt.QFormLayout(inputsCollapsibleButton) - - # Segmentation selector - self.segmentationSelector = slicer.qMRMLNodeComboBox() - self.segmentationSelector.nodeTypes = ["vtkMRMLSegmentationNode"] - self.segmentationSelector.addEnabled = False - self.segmentationSelector.removeEnabled = True - self.segmentationSelector.renameEnabled = True - self.segmentationSelector.setMRMLScene( slicer.mrmlScene ) - self.segmentationSelector.setToolTip( "Pick the segmentation to compute statistics for" ) - inputsFormLayout.addRow("Segmentation:", self.segmentationSelector) - - # Scalar volume selector - self.scalarSelector = slicer.qMRMLNodeComboBox() - self.scalarSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] - self.scalarSelector.addEnabled = False - self.scalarSelector.removeEnabled = True - self.scalarSelector.renameEnabled = True - self.scalarSelector.noneEnabled = True - self.scalarSelector.showChildNodeTypes = False - self.scalarSelector.setMRMLScene( slicer.mrmlScene ) - self.scalarSelector.setToolTip( "Select the scalar volume for intensity statistics calculations") - inputsFormLayout.addRow("Scalar volume:", self.scalarSelector) - - # Output table selector - outputCollapsibleButton = ctk.ctkCollapsibleButton() - outputCollapsibleButton.text = "Output" - self.layout.addWidget(outputCollapsibleButton) - outputFormLayout = qt.QFormLayout(outputCollapsibleButton) - - self.outputTableSelector = slicer.qMRMLNodeComboBox() - self.outputTableSelector.noneDisplay = "Create new table" - self.outputTableSelector.setMRMLScene(slicer.mrmlScene) - self.outputTableSelector.nodeTypes = ["vtkMRMLTableNode"] - self.outputTableSelector.addEnabled = True - self.outputTableSelector.selectNodeUponCreation = True - self.outputTableSelector.renameEnabled = True - self.outputTableSelector.removeEnabled = True - self.outputTableSelector.noneEnabled = True - self.outputTableSelector.setToolTip("Select the table where statistics will be saved into") - self.outputTableSelector.setCurrentNode(None) - - outputFormLayout.addRow("Output table:", self.outputTableSelector) - - # Parameter set - parametersCollapsibleButton = ctk.ctkCollapsibleButton() - parametersCollapsibleButton.text = "Advanced" - parametersCollapsibleButton.collapsed = True - self.layout.addWidget(parametersCollapsibleButton) - self.parametersLayout = qt.QFormLayout(parametersCollapsibleButton) - - # Edit parameter set button to open SegmentStatisticsParameterEditorDialog - # Note: we add the plugins' option widgets to the module widget instead of using the editor dialog - #self.editParametersButton = qt.QPushButton("Edit Parameter Set") - #self.editParametersButton.toolTip = "Editor Statistics Plugin Parameter Set." - #self.parametersLayout.addRow(self.editParametersButton) - #self.editParametersButton.connect('clicked()', self.onEditParameters) - # add caclulator's option widgets - self.addPluginOptionWidgets() - - # Apply Button - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Calculate Statistics." - self.applyButton.enabled = False - self.parent.layout().addWidget(self.applyButton) - - # Add vertical spacer - self.parent.layout().addStretch(1) - - # connections - self.applyButton.connect('clicked()', self.onApply) - self.scalarSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged) - self.segmentationSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged) - self.outputTableSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged) - self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged) - self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onParameterSetSelected) - - self.parameterNodeSelector.setCurrentNode(self.logic.getParameterNode()) - self.onNodeSelectionChanged() - self.onParameterSetSelected() - - def enter(self): - """Runs whenever the module is reopened - """ - if self.parameterNodeSelector.currentNode() is None: - parameterNode = self.logic.getParameterNode() - slicer.mrmlScene.AddNode(parameterNode) - self.parameterNodeSelector.setCurrentNode(parameterNode) - if self.segmentationSelector.currentNode() is None: - segmentationNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLSegmentationNode") - self.segmentationSelector.setCurrentNode(segmentationNode) - - def cleanup(self): - if self.parameterNode and self.parameterNodeObserver: - self.parameterNode.RemoveObserver(self.parameterNodeObserver) - - def onNodeSelectionChanged(self): - self.applyButton.enabled = (self.segmentationSelector.currentNode() is not None and - self.parameterNodeSelector.currentNode() is not None) - if self.segmentationSelector.currentNode(): - self.outputTableSelector.baseName = self.segmentationSelector.currentNode().GetName() + ' statistics' - - def onApply(self): - """Calculate the label statistics + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): - if not self.outputTableSelector.currentNode(): - newTable = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") - self.outputTableSelector.setCurrentNode(newTable) - # Lock GUI - self.applyButton.text = "Working..." - self.applyButton.setEnabled(False) - slicer.app.processEvents() - # set up parameters for computation - self.logic.getParameterNode().SetParameter("Segmentation", self.segmentationSelector.currentNode().GetID()) - if self.scalarSelector.currentNode(): - self.logic.getParameterNode().SetParameter("ScalarVolume", self.scalarSelector.currentNode().GetID()) - else: - self.logic.getParameterNode().UnsetParameter("ScalarVolume") - self.logic.getParameterNode().SetParameter("MeasurementsTable", self.outputTableSelector.currentNode().GetID()) - # Compute statistics - self.logic.computeStatistics() - self.logic.exportToTable(self.outputTableSelector.currentNode()) - self.logic.showTable(self.outputTableSelector.currentNode()) - - # Unlock GUI - self.applyButton.setEnabled(True) - self.applyButton.text = "Apply" - - def onEditParameters(self, pluginName=None): - """Open dialog box to edit plugin's parameters""" - if self.parameterNodeSelector.currentNode(): - SegmentStatisticsParameterEditorDialog.editParameters(self.parameterNodeSelector.currentNode(),pluginName) - - def addPluginOptionWidgets(self): - self.pluginEnabledCheckboxes = {} - self.parametersLayout.addRow(qt.QLabel("Enabled segment statistics plugins:")) - for plugin in self.logic.plugins: - checkbox = qt.QCheckBox(plugin.name+" Statistics") - checkbox.checked = True - checkbox.connect('stateChanged(int)', self.updateParameterNodeFromGui) - optionButton = qt.QPushButton("Options") - from functools import partial - optionButton.connect('clicked()',partial(self.onEditParameters, plugin.name)) - editWidget = qt.QWidget() - editWidget.setLayout(qt.QHBoxLayout()) - editWidget.layout().margin = 0 - editWidget.layout().addWidget(checkbox, 0) - editWidget.layout().addStretch(1) - editWidget.layout().addWidget(optionButton, 0) - self.pluginEnabledCheckboxes[plugin.name] = checkbox - self.parametersLayout.addRow(editWidget) - # embed widgets for editing plugin' parameters - #for plugin in self.logic.plugins: - # pluginOptionsCollapsibleButton = ctk.ctkCollapsibleGroupBox() - # pluginOptionsCollapsibleButton.setTitle( plugin.name ) - # pluginOptionsFormLayout = qt.QFormLayout(pluginOptionsCollapsibleButton) - # pluginOptionsFormLayout.addRow(plugin.optionsWidget) - # self.parametersLayout.addRow(pluginOptionsCollapsibleButton) - - def onParameterSetSelected(self): - if self.parameterNode and self.parameterNodeObserver: - self.parameterNode.RemoveObserver(self.parameterNodeObserver) - self.parameterNode = self.parameterNodeSelector.currentNode() - if self.parameterNode: - self.logic.setParameterNode(self.parameterNode) - self.parameterNodeObserver = self.parameterNode.AddObserver(vtk.vtkCommand.ModifiedEvent, - self.updateGuiFromParameterNode) - self.updateGuiFromParameterNode() - - def updateGuiFromParameterNode(self, caller=None, event=None): - if not self.parameterNode: - return - for plugin in self.logic.plugins: - pluginName = plugin.__class__.__name__ - parameter = pluginName+'.enabled' - checkbox = self.pluginEnabledCheckboxes[plugin.name] - value = self.parameterNode.GetParameter(parameter)=='True' - if checkbox.checked!=value: - previousState = checkbox.blockSignals(True) - checkbox.checked = value - checkbox.blockSignals(previousState) - - def updateParameterNodeFromGui(self): - if not self.parameterNode: - return - for plugin in self.logic.plugins: - pluginName = plugin.__class__.__name__ - parameter = pluginName+'.enabled' - checkbox = self.pluginEnabledCheckboxes[plugin.name] - self.parameterNode.SetParameter(parameter, str(checkbox.checked)) + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + self.logic = SegmentStatisticsLogic() + self.grayscaleNode = None + self.labelNode = None + self.parameterNode = None + self.parameterNodeObserver = None + + # Instantiate and connect widgets ... + # + + # Parameter set selector + self.parameterNodeSelector = slicer.qMRMLNodeComboBox() + self.parameterNodeSelector.nodeTypes = ["vtkMRMLScriptedModuleNode"] + self.parameterNodeSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", "SegmentStatistics") + self.parameterNodeSelector.selectNodeUponCreation = True + self.parameterNodeSelector.addEnabled = True + self.parameterNodeSelector.renameEnabled = True + self.parameterNodeSelector.removeEnabled = True + self.parameterNodeSelector.noneEnabled = False + self.parameterNodeSelector.showHidden = True + self.parameterNodeSelector.showChildNodeTypes = False + self.parameterNodeSelector.baseName = "SegmentStatistics" + self.parameterNodeSelector.setMRMLScene(slicer.mrmlScene) + self.parameterNodeSelector.setToolTip("Pick parameter set") + self.layout.addWidget(self.parameterNodeSelector) + + # Inputs + inputsCollapsibleButton = ctk.ctkCollapsibleButton() + inputsCollapsibleButton.text = "Inputs" + self.layout.addWidget(inputsCollapsibleButton) + inputsFormLayout = qt.QFormLayout(inputsCollapsibleButton) + + # Segmentation selector + self.segmentationSelector = slicer.qMRMLNodeComboBox() + self.segmentationSelector.nodeTypes = ["vtkMRMLSegmentationNode"] + self.segmentationSelector.addEnabled = False + self.segmentationSelector.removeEnabled = True + self.segmentationSelector.renameEnabled = True + self.segmentationSelector.setMRMLScene(slicer.mrmlScene) + self.segmentationSelector.setToolTip("Pick the segmentation to compute statistics for") + inputsFormLayout.addRow("Segmentation:", self.segmentationSelector) + + # Scalar volume selector + self.scalarSelector = slicer.qMRMLNodeComboBox() + self.scalarSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] + self.scalarSelector.addEnabled = False + self.scalarSelector.removeEnabled = True + self.scalarSelector.renameEnabled = True + self.scalarSelector.noneEnabled = True + self.scalarSelector.showChildNodeTypes = False + self.scalarSelector.setMRMLScene(slicer.mrmlScene) + self.scalarSelector.setToolTip("Select the scalar volume for intensity statistics calculations") + inputsFormLayout.addRow("Scalar volume:", self.scalarSelector) + + # Output table selector + outputCollapsibleButton = ctk.ctkCollapsibleButton() + outputCollapsibleButton.text = "Output" + self.layout.addWidget(outputCollapsibleButton) + outputFormLayout = qt.QFormLayout(outputCollapsibleButton) + + self.outputTableSelector = slicer.qMRMLNodeComboBox() + self.outputTableSelector.noneDisplay = "Create new table" + self.outputTableSelector.setMRMLScene(slicer.mrmlScene) + self.outputTableSelector.nodeTypes = ["vtkMRMLTableNode"] + self.outputTableSelector.addEnabled = True + self.outputTableSelector.selectNodeUponCreation = True + self.outputTableSelector.renameEnabled = True + self.outputTableSelector.removeEnabled = True + self.outputTableSelector.noneEnabled = True + self.outputTableSelector.setToolTip("Select the table where statistics will be saved into") + self.outputTableSelector.setCurrentNode(None) + + outputFormLayout.addRow("Output table:", self.outputTableSelector) + + # Parameter set + parametersCollapsibleButton = ctk.ctkCollapsibleButton() + parametersCollapsibleButton.text = "Advanced" + parametersCollapsibleButton.collapsed = True + self.layout.addWidget(parametersCollapsibleButton) + self.parametersLayout = qt.QFormLayout(parametersCollapsibleButton) + + # Edit parameter set button to open SegmentStatisticsParameterEditorDialog + # Note: we add the plugins' option widgets to the module widget instead of using the editor dialog + # self.editParametersButton = qt.QPushButton("Edit Parameter Set") + # self.editParametersButton.toolTip = "Editor Statistics Plugin Parameter Set." + # self.parametersLayout.addRow(self.editParametersButton) + # self.editParametersButton.connect('clicked()', self.onEditParameters) + # add caclulator's option widgets + self.addPluginOptionWidgets() + + # Apply Button + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Calculate Statistics." + self.applyButton.enabled = False + self.parent.layout().addWidget(self.applyButton) + + # Add vertical spacer + self.parent.layout().addStretch(1) + + # connections + self.applyButton.connect('clicked()', self.onApply) + self.scalarSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged) + self.segmentationSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged) + self.outputTableSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged) + self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged) + self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onParameterSetSelected) + + self.parameterNodeSelector.setCurrentNode(self.logic.getParameterNode()) + self.onNodeSelectionChanged() + self.onParameterSetSelected() + + def enter(self): + """Runs whenever the module is reopened + """ + if self.parameterNodeSelector.currentNode() is None: + parameterNode = self.logic.getParameterNode() + slicer.mrmlScene.AddNode(parameterNode) + self.parameterNodeSelector.setCurrentNode(parameterNode) + if self.segmentationSelector.currentNode() is None: + segmentationNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLSegmentationNode") + self.segmentationSelector.setCurrentNode(segmentationNode) + + def cleanup(self): + if self.parameterNode and self.parameterNodeObserver: + self.parameterNode.RemoveObserver(self.parameterNodeObserver) + + def onNodeSelectionChanged(self): + self.applyButton.enabled = (self.segmentationSelector.currentNode() is not None and + self.parameterNodeSelector.currentNode() is not None) + if self.segmentationSelector.currentNode(): + self.outputTableSelector.baseName = self.segmentationSelector.currentNode().GetName() + ' statistics' + + def onApply(self): + """Calculate the label statistics + """ + + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + if not self.outputTableSelector.currentNode(): + newTable = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode") + self.outputTableSelector.setCurrentNode(newTable) + # Lock GUI + self.applyButton.text = "Working..." + self.applyButton.setEnabled(False) + slicer.app.processEvents() + # set up parameters for computation + self.logic.getParameterNode().SetParameter("Segmentation", self.segmentationSelector.currentNode().GetID()) + if self.scalarSelector.currentNode(): + self.logic.getParameterNode().SetParameter("ScalarVolume", self.scalarSelector.currentNode().GetID()) + else: + self.logic.getParameterNode().UnsetParameter("ScalarVolume") + self.logic.getParameterNode().SetParameter("MeasurementsTable", self.outputTableSelector.currentNode().GetID()) + # Compute statistics + self.logic.computeStatistics() + self.logic.exportToTable(self.outputTableSelector.currentNode()) + self.logic.showTable(self.outputTableSelector.currentNode()) + + # Unlock GUI + self.applyButton.setEnabled(True) + self.applyButton.text = "Apply" + + def onEditParameters(self, pluginName=None): + """Open dialog box to edit plugin's parameters""" + if self.parameterNodeSelector.currentNode(): + SegmentStatisticsParameterEditorDialog.editParameters(self.parameterNodeSelector.currentNode(), pluginName) + + def addPluginOptionWidgets(self): + self.pluginEnabledCheckboxes = {} + self.parametersLayout.addRow(qt.QLabel("Enabled segment statistics plugins:")) + for plugin in self.logic.plugins: + checkbox = qt.QCheckBox(plugin.name + " Statistics") + checkbox.checked = True + checkbox.connect('stateChanged(int)', self.updateParameterNodeFromGui) + optionButton = qt.QPushButton("Options") + from functools import partial + optionButton.connect('clicked()', partial(self.onEditParameters, plugin.name)) + editWidget = qt.QWidget() + editWidget.setLayout(qt.QHBoxLayout()) + editWidget.layout().margin = 0 + editWidget.layout().addWidget(checkbox, 0) + editWidget.layout().addStretch(1) + editWidget.layout().addWidget(optionButton, 0) + self.pluginEnabledCheckboxes[plugin.name] = checkbox + self.parametersLayout.addRow(editWidget) + # embed widgets for editing plugin' parameters + # for plugin in self.logic.plugins: + # pluginOptionsCollapsibleButton = ctk.ctkCollapsibleGroupBox() + # pluginOptionsCollapsibleButton.setTitle( plugin.name ) + # pluginOptionsFormLayout = qt.QFormLayout(pluginOptionsCollapsibleButton) + # pluginOptionsFormLayout.addRow(plugin.optionsWidget) + # self.parametersLayout.addRow(pluginOptionsCollapsibleButton) + + def onParameterSetSelected(self): + if self.parameterNode and self.parameterNodeObserver: + self.parameterNode.RemoveObserver(self.parameterNodeObserver) + self.parameterNode = self.parameterNodeSelector.currentNode() + if self.parameterNode: + self.logic.setParameterNode(self.parameterNode) + self.parameterNodeObserver = self.parameterNode.AddObserver(vtk.vtkCommand.ModifiedEvent, + self.updateGuiFromParameterNode) + self.updateGuiFromParameterNode() + + def updateGuiFromParameterNode(self, caller=None, event=None): + if not self.parameterNode: + return + for plugin in self.logic.plugins: + pluginName = plugin.__class__.__name__ + parameter = pluginName + '.enabled' + checkbox = self.pluginEnabledCheckboxes[plugin.name] + value = self.parameterNode.GetParameter(parameter) == 'True' + if checkbox.checked != value: + previousState = checkbox.blockSignals(True) + checkbox.checked = value + checkbox.blockSignals(previousState) + + def updateParameterNodeFromGui(self): + if not self.parameterNode: + return + for plugin in self.logic.plugins: + pluginName = plugin.__class__.__name__ + parameter = pluginName + '.enabled' + checkbox = self.pluginEnabledCheckboxes[plugin.name] + self.parameterNode.SetParameter(parameter, str(checkbox.checked)) class SegmentStatisticsParameterEditorDialog(qt.QDialog): @@ -282,691 +282,695 @@ class SegmentStatisticsParameterEditorDialog(qt.QDialog): @staticmethod def editParameters(parameterNode, pluginName=None): - """Executes a modal dialog to edit a segment statistics parameter node if a pluginName is specified, only - options for this plugin are displayed" - """ - dialog = SegmentStatisticsParameterEditorDialog(parent=None, parameterNode=parameterNode, - pluginName=pluginName) - return dialog.exec_() - - def __init__(self,parent=None, parameterNode=None, pluginName=None): - super(qt.QDialog,self).__init__(parent) - self.title = "Edit Segment Statistics Parameters" - self.parameterNode = parameterNode - self.pluginName = pluginName - self.logic = SegmentStatisticsLogic() # for access to plugins and editor widgets - self.logic.setParameterNode(self.parameterNode) - self.setup() + """Executes a modal dialog to edit a segment statistics parameter node if a pluginName is specified, only + options for this plugin are displayed" + """ + dialog = SegmentStatisticsParameterEditorDialog(parent=None, parameterNode=parameterNode, + pluginName=pluginName) + return dialog.exec_() + + def __init__(self, parent=None, parameterNode=None, pluginName=None): + super(qt.QDialog, self).__init__(parent) + self.title = "Edit Segment Statistics Parameters" + self.parameterNode = parameterNode + self.pluginName = pluginName + self.logic = SegmentStatisticsLogic() # for access to plugins and editor widgets + self.logic.setParameterNode(self.parameterNode) + self.setup() def setParameterNode(self, parameterNode): - """Set the parameter node the dialog will operate on""" - if parameterNode==self.parameterNode: - return - self.parameterNode = parameterNode - self.logic.setParameterNode(self.parameterNode) + """Set the parameter node the dialog will operate on""" + if parameterNode == self.parameterNode: + return + self.parameterNode = parameterNode + self.logic.setParameterNode(self.parameterNode) def setup(self): - self.setLayout(qt.QVBoxLayout()) - - self.descriptionLabel = qt.QLabel("Edit segment statistics plugin parameters:",0) - - self.doneButton = qt.QPushButton("Done") - self.doneButton.toolTip = "Finish editing." - doneWidget = qt.QWidget(self) - doneWidget.setLayout(qt.QHBoxLayout()) - doneWidget.layout().addStretch(1) - doneWidget.layout().addWidget(self.doneButton, 0) - - parametersScrollArea = qt.QScrollArea(self) - self.parametersWidget = qt.QWidget(parametersScrollArea) - self.parametersLayout = qt.QFormLayout(self.parametersWidget) - self._addPluginOptionWidgets() - parametersScrollArea.setWidget(self.parametersWidget) - parametersScrollArea.widgetResizable = True - parametersScrollArea.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded ) - parametersScrollArea.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded) - - self.layout().addWidget(self.descriptionLabel,0) - self.layout().addWidget(parametersScrollArea, 1) - self.layout().addWidget(doneWidget, 0) - self.doneButton.connect('clicked()', lambda: self.done(1)) + self.setLayout(qt.QVBoxLayout()) + + self.descriptionLabel = qt.QLabel("Edit segment statistics plugin parameters:", 0) + + self.doneButton = qt.QPushButton("Done") + self.doneButton.toolTip = "Finish editing." + doneWidget = qt.QWidget(self) + doneWidget.setLayout(qt.QHBoxLayout()) + doneWidget.layout().addStretch(1) + doneWidget.layout().addWidget(self.doneButton, 0) + + parametersScrollArea = qt.QScrollArea(self) + self.parametersWidget = qt.QWidget(parametersScrollArea) + self.parametersLayout = qt.QFormLayout(self.parametersWidget) + self._addPluginOptionWidgets() + parametersScrollArea.setWidget(self.parametersWidget) + parametersScrollArea.widgetResizable = True + parametersScrollArea.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded) + parametersScrollArea.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded) + + self.layout().addWidget(self.descriptionLabel, 0) + self.layout().addWidget(parametersScrollArea, 1) + self.layout().addWidget(doneWidget, 0) + self.doneButton.connect('clicked()', lambda: self.done(1)) def _addPluginOptionWidgets(self): - description = "Edit segment statistics plugin parameters:" - if self.pluginName: - description = "Edit "+self.pluginName+" plugin parameters:" - self.descriptionLabel.text = description - if self.pluginName: - for plugin in self.logic.plugins: - if plugin.name==self.pluginName: - self.parametersLayout.addRow(plugin.optionsWidget) - else: - for plugin in self.logic.plugins: - pluginOptionsCollapsibleButton = ctk.ctkCollapsibleGroupBox(self.parametersWidget) - pluginOptionsCollapsibleButton.setTitle( plugin.name ) - pluginOptionsFormLayout = qt.QFormLayout(pluginOptionsCollapsibleButton) - pluginOptionsFormLayout.addRow(plugin.optionsWidget) - self.parametersLayout.addRow(pluginOptionsCollapsibleButton) + description = "Edit segment statistics plugin parameters:" + if self.pluginName: + description = "Edit " + self.pluginName + " plugin parameters:" + self.descriptionLabel.text = description + if self.pluginName: + for plugin in self.logic.plugins: + if plugin.name == self.pluginName: + self.parametersLayout.addRow(plugin.optionsWidget) + else: + for plugin in self.logic.plugins: + pluginOptionsCollapsibleButton = ctk.ctkCollapsibleGroupBox(self.parametersWidget) + pluginOptionsCollapsibleButton.setTitle(plugin.name) + pluginOptionsFormLayout = qt.QFormLayout(pluginOptionsCollapsibleButton) + pluginOptionsFormLayout.addRow(plugin.optionsWidget) + self.parametersLayout.addRow(pluginOptionsCollapsibleButton) class SegmentStatisticsLogic(ScriptedLoadableModuleLogic): - """Implement the logic to calculate label statistics. - Nodes are passed in as arguments. - Results are stored as 'statistics' instance variable. - Additional plugins for computation of other statistical measurements may be registered. - Uses ScriptedLoadableModuleLogic base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - registeredPlugins = [LabelmapSegmentStatisticsPlugin, ScalarVolumeSegmentStatisticsPlugin, - ClosedSurfaceSegmentStatisticsPlugin] - - @staticmethod - def registerPlugin(plugin): - """Register a subclass of SegmentStatisticsPluginBase for calculation of additional measurements""" - if not isinstance(plugin, SegmentStatisticsPluginBase): - return - for key in plugin.keys: - if key.count(".")>0: - logging.warning("Plugin keys should not contain extra '.' as it might mix pluginname.measurementkey in " - "the parameter node") - if not plugin.__class__ in SegmentStatisticsLogic.registeredPlugins: - SegmentStatisticsLogic.registeredPlugins.append(plugin.__class__) - else: - logging.warning("SegmentStatisticsLogic.registerPlugin will not register plugin because \ - another plugin with the same name has already been registered") - - def __init__(self, parent = None): - ScriptedLoadableModuleLogic.__init__(self, parent) - self.plugins = [ x() for x in SegmentStatisticsLogic.registeredPlugins] - - self.isSingletonParameterNode = False - self.parameterNode = None - - self.keys = ["Segment"] - self.notAvailableValueString = "" - self.reset() - - def getParameterNode(self): - """Returns the current parameter node and creates one if it doesn't exist yet""" - if not self.parameterNode: - self.setParameterNode( ScriptedLoadableModuleLogic.getParameterNode(self) ) - return self.parameterNode - - def setParameterNode(self, parameterNode): - """Set the current parameter node and initialize all unset parameters to their default values""" - if self.parameterNode==parameterNode: - return - self.setDefaultParameters(parameterNode) - self.parameterNode = parameterNode - for plugin in self.plugins: - plugin.setParameterNode(parameterNode) - - def setDefaultParameters(self, parameterNode): - """Set all plugins to enabled and all plugins' parameters to their default value""" - for plugin in self.plugins: - plugin.setDefaultParameters(parameterNode) - if not parameterNode.GetParameter('visibleSegmentsOnly'): - parameterNode.SetParameter('visibleSegmentsOnly', str(True)) - - def getStatistics(self): - """Get the calculated statistical measurements""" - params = self.getParameterNode() - if not hasattr(params,'statistics'): - params.statistics = {"SegmentIDs": [], "MeasurementInfo": {}} - return params.statistics - - def reset(self): - """Clear all computation results""" - self.keys = ["Segment"] - for plugin in self.plugins: - self.keys += [plugin.toLongKey(k) for k in plugin.keys] - params = self.getParameterNode() - params.statistics = {"SegmentIDs":[], "MeasurementInfo": {}} - - def computeStatistics(self): - """Compute statistical measures for all (visible) segments""" - self.reset() - - segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) - transformedSegmentationNode = None - try: - if not segmentationNode.GetParentTransformNode() is None: - # Create a temporary segmentation and harden the transform to ensure that the statistics are calculated - # in world coordinates - transformedSegmentationNode = slicer.vtkMRMLSegmentationNode() - transformedSegmentationNode.Copy(segmentationNode) - transformedSegmentationNode.HideFromEditorsOn() - slicer.mrmlScene.AddNode(transformedSegmentationNode) - transformedSegmentationNode.HardenTransform() - self.getParameterNode().SetParameter("Segmentation", transformedSegmentationNode.GetID()) - - # Get segment ID list - visibleSegmentIds = vtk.vtkStringArray() - if self.getParameterNode().GetParameter('visibleSegmentsOnly')=='True': - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) - else: - segmentationNode.GetSegmentation().GetSegmentIDs(visibleSegmentIds) - if visibleSegmentIds.GetNumberOfValues() == 0: - logging.debug("computeStatistics will not return any results: there are no visible segments") - - # update statistics for all segment IDs - for segmentIndex in range(visibleSegmentIds.GetNumberOfValues()): - segmentID = visibleSegmentIds.GetValue(segmentIndex) - self.updateStatisticsForSegment(segmentID) - finally: - if not transformedSegmentationNode is None: - # We made a copy and hardened the segmentation transform - self.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) - slicer.mrmlScene.RemoveNode(transformedSegmentationNode) - - def updateStatisticsForSegment(self, segmentID): - """ - Update statistical measures for specified segment. - Note: This will not change or reset measurement results of other segments + """Implement the logic to calculate label statistics. + Nodes are passed in as arguments. + Results are stored as 'statistics' instance variable. + Additional plugins for computation of other statistical measurements may be registered. + Uses ScriptedLoadableModuleLogic base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ + registeredPlugins = [LabelmapSegmentStatisticsPlugin, ScalarVolumeSegmentStatisticsPlugin, + ClosedSurfaceSegmentStatisticsPlugin] - segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) - - if not segmentationNode.GetSegmentation().GetSegment(segmentID): - logging.debug("updateStatisticsForSegment will not update any results because the segment doesn't exist") - return - - segment = segmentationNode.GetSegmentation().GetSegment(segmentID) - statistics = self.getStatistics() - if segmentID not in statistics["SegmentIDs"]: - statistics["SegmentIDs"].append(segmentID) - statistics[segmentID,"Segment"] = segment.GetName() - - # apply all enabled plugins - for plugin in self.plugins: - pluginName = plugin.__class__.__name__ - if self.getParameterNode().GetParameter(pluginName+'.enabled')=='True': - stats = plugin.computeStatistics(segmentID) - for key in stats: - statistics[segmentID,pluginName+'.'+key] = stats[key] - statistics["MeasurementInfo"][pluginName+'.'+key] = plugin.getMeasurementInfo(key) - - def getPluginByKey(self, key): - """Get plugin responsible for obtaining measurement value for given key""" - for plugin in self.plugins: - if plugin.toShortKey(key) in plugin.keys: - return plugin - return None - - def getMeasurementInfo(self, key): - """Get information (name, description, units, ...) about the measurement for the given key""" - plugin = self.getPluginByKey(key) - if plugin: - return plugin.getMeasurementInfo(plugin.toShortKey(key)) - return None - - def getStatisticsValueAsString(self, segmentID, key): - statistics = self.getStatistics() - if (segmentID, key) in statistics: - value = statistics[segmentID, key] - if isinstance(value, float): - return "%0.3f" % value # round to 3 decimals - else: - return str(value) - else: - return self.notAvailableValueString - - def getNonEmptyKeys(self): - # Fill columns - statistics = self.getStatistics() - nonEmptyKeys = [] - for key in self.keys: - for segmentID in statistics["SegmentIDs"]: - if (segmentID, key) in statistics: - nonEmptyKeys.append(key) - break - return nonEmptyKeys - - def getHeaderNames(self, nonEmptyKeysOnly = True): - # Derive column header names based on: (a) DICOM information if present, - # (b) measurement info name if present (c) measurement key as fallback. - # Duplicate names get a postfix [1][2]... to make them unique - # Initial and unique column header names are returned - keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys - statistics = self.getStatistics() - headerNames = [] - for key in keys: - name = key - info = statistics['MeasurementInfo'][key] if key in statistics['MeasurementInfo'] else {} - entry = slicer.vtkCodedEntry() - dicomBasedName = False - if info: - if 'DICOM.DerivationCode' in info and info['DICOM.DerivationCode']: - entry.SetFromString(info['DICOM.DerivationCode']) - name = entry.GetCodeMeaning() - dicomBasedName = True - elif 'DICOM.QuantityCode' in info and info['DICOM.QuantityCode']: - entry.SetFromString(info['DICOM.QuantityCode']) - name = entry.GetCodeMeaning() - dicomBasedName = True - elif 'name' in info and info['name']: - name = info['name'] - if dicomBasedName and 'DICOM.UnitsCode' in info and info['DICOM.UnitsCode']: - entry.SetFromString(info['DICOM.UnitsCode']) - units = entry.GetCodeValue() - if len(units)>0 and units[0]=='[' and units[-1]==']': units = units[1:-1] - if len(units)>0: name += ' ['+units+']' - elif 'units' in info and info['units'] and len(info['units'])>0: - units = info['units'] - name += ' ['+units+']' - headerNames.append(name) - uniqueHeaderNames = list(headerNames) - for duplicateName in {name for name in uniqueHeaderNames if uniqueHeaderNames.count(name)>1}: - j = 1 - for i in range(len(uniqueHeaderNames)): - if uniqueHeaderNames[i]==duplicateName: - uniqueHeaderNames[i] = duplicateName+' ('+str(j)+')' - j += 1 - headerNames = {keys[i]: headerNames[i] for i in range(len(keys))} - uniqueHeaderNames = {keys[i]: uniqueHeaderNames[i] for i in range(len(keys))} - return headerNames, uniqueHeaderNames - - def exportToTable(self, table, nonEmptyKeysOnly = True): - """ - Export statistics to table node - """ - tableWasModified = table.StartModify() - table.RemoveAllColumns() - - keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys - columnHeaderNames, uniqueColumnHeaderNames = self.getHeaderNames(nonEmptyKeysOnly) - - # Define table columns - statistics = self.getStatistics() - for key in keys: - # create table column appropriate for data type; currently supported: float, int, long, string - measurements = [statistics[segmentID, key] for segmentID in statistics["SegmentIDs"] if - (segmentID, key) in statistics] - if len(measurements)==0: # there were not measurements and therefore use the default "string" representation - col = table.AddColumn() - elif isinstance(measurements[0], int): - col = table.AddColumn(vtk.vtkLongArray()) - elif isinstance(measurements[0], float): - col = table.AddColumn(vtk.vtkDoubleArray()) - elif isinstance(measurements[0], list): - length = len(measurements[0]) - if length == 0: - col = table.AddColumn() - else: - value = measurements[0][0] - if isinstance(value, int): - array = vtk.vtkLongArray() - array.SetNumberOfComponents(length) - col = table.AddColumn(array) - elif isinstance(value, float): - array = vtk.vtkDoubleArray() - array.SetNumberOfComponents(length) - col = table.AddColumn(array) - else: - col = table.AddColumn() - else: # default - col = table.AddColumn() - plugin = self.getPluginByKey(key) - columnName = uniqueColumnHeaderNames[key] - longColumnName = columnHeaderNames[key] - col.SetName( columnName ) - if plugin: - table.SetColumnProperty(columnName, "Plugin", plugin.name) - longColumnName += '
          Computed by '+plugin.name+' Statistics plugin' - table.SetColumnLongName(columnName, longColumnName) - measurementInfo = statistics["MeasurementInfo"][key] if key in statistics["MeasurementInfo"] else {} - if measurementInfo: - for mik, miv in measurementInfo.items(): - if mik=='description': - table.SetColumnDescription(columnName, str(miv)) - elif mik=='units': - table.SetColumnUnitLabel(columnName, str(miv)) - elif mik == 'componentNames': - componentNames = miv - array = table.GetTable().GetColumnByName(columnName) - componentIndex = 0 - for componentName in miv: - array.SetComponentName(componentIndex, componentName) - componentIndex += 1 - else: - table.SetColumnProperty(columnName, str(mik), str(miv)) - - # Fill columns - for segmentID in statistics["SegmentIDs"]: - rowIndex = table.AddEmptyRow() - columnIndex = 0 - for key in keys: - value = statistics[segmentID, key] if (segmentID, key) in statistics else None - if value is None and key!='Segment': - value = float('nan') - if isinstance(value, list): - for i in range(len(value)): - table.GetTable().GetColumn(columnIndex).SetComponent(rowIndex, i, value[i]) + @staticmethod + def registerPlugin(plugin): + """Register a subclass of SegmentStatisticsPluginBase for calculation of additional measurements""" + if not isinstance(plugin, SegmentStatisticsPluginBase): + return + for key in plugin.keys: + if key.count(".") > 0: + logging.warning("Plugin keys should not contain extra '.' as it might mix pluginname.measurementkey in " + "the parameter node") + if plugin.__class__ not in SegmentStatisticsLogic.registeredPlugins: + SegmentStatisticsLogic.registeredPlugins.append(plugin.__class__) else: - table.GetTable().GetColumn(columnIndex).SetValue(rowIndex, value) - columnIndex += 1 + logging.warning("SegmentStatisticsLogic.registerPlugin will not register plugin because \ + another plugin with the same name has already been registered") - table.Modified() - table.EndModify(tableWasModified) + def __init__(self, parent=None): + ScriptedLoadableModuleLogic.__init__(self, parent) + self.plugins = [x() for x in SegmentStatisticsLogic.registeredPlugins] - def showTable(self, table): - """ - Switch to a layout where tables are visible and show the selected table - """ - currentLayout = slicer.app.layoutManager().layout - layoutWithTable = slicer.modules.tables.logic().GetLayoutWithTable(currentLayout) - slicer.app.layoutManager().setLayout(layoutWithTable) - slicer.app.applicationLogic().GetSelectionNode().SetActiveTableID(table.GetID()) - slicer.app.applicationLogic().PropagateTableSelection() + self.isSingletonParameterNode = False + self.parameterNode = None - def exportToString(self, nonEmptyKeysOnly = True): - """ - Returns string with comma separated values, with header keys in quotes. - """ - keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys - # Header - csv = '"' + '","'.join(keys) + '"' - # Rows - statistics = self.getStatistics() - for segmentID in statistics["SegmentIDs"]: - csv += "\n" + str(statistics[segmentID,keys[0]]) - for key in keys[1:]: + self.keys = ["Segment"] + self.notAvailableValueString = "" + self.reset() + + def getParameterNode(self): + """Returns the current parameter node and creates one if it doesn't exist yet""" + if not self.parameterNode: + self.setParameterNode(ScriptedLoadableModuleLogic.getParameterNode(self)) + return self.parameterNode + + def setParameterNode(self, parameterNode): + """Set the current parameter node and initialize all unset parameters to their default values""" + if self.parameterNode == parameterNode: + return + self.setDefaultParameters(parameterNode) + self.parameterNode = parameterNode + for plugin in self.plugins: + plugin.setParameterNode(parameterNode) + + def setDefaultParameters(self, parameterNode): + """Set all plugins to enabled and all plugins' parameters to their default value""" + for plugin in self.plugins: + plugin.setDefaultParameters(parameterNode) + if not parameterNode.GetParameter('visibleSegmentsOnly'): + parameterNode.SetParameter('visibleSegmentsOnly', str(True)) + + def getStatistics(self): + """Get the calculated statistical measurements""" + params = self.getParameterNode() + if not hasattr(params, 'statistics'): + params.statistics = {"SegmentIDs": [], "MeasurementInfo": {}} + return params.statistics + + def reset(self): + """Clear all computation results""" + self.keys = ["Segment"] + for plugin in self.plugins: + self.keys += [plugin.toLongKey(k) for k in plugin.keys] + params = self.getParameterNode() + params.statistics = {"SegmentIDs": [], "MeasurementInfo": {}} + + def computeStatistics(self): + """Compute statistical measures for all (visible) segments""" + self.reset() + + segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) + transformedSegmentationNode = None + try: + if not segmentationNode.GetParentTransformNode() is None: + # Create a temporary segmentation and harden the transform to ensure that the statistics are calculated + # in world coordinates + transformedSegmentationNode = slicer.vtkMRMLSegmentationNode() + transformedSegmentationNode.Copy(segmentationNode) + transformedSegmentationNode.HideFromEditorsOn() + slicer.mrmlScene.AddNode(transformedSegmentationNode) + transformedSegmentationNode.HardenTransform() + self.getParameterNode().SetParameter("Segmentation", transformedSegmentationNode.GetID()) + + # Get segment ID list + visibleSegmentIds = vtk.vtkStringArray() + if self.getParameterNode().GetParameter('visibleSegmentsOnly') == 'True': + segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) + else: + segmentationNode.GetSegmentation().GetSegmentIDs(visibleSegmentIds) + if visibleSegmentIds.GetNumberOfValues() == 0: + logging.debug("computeStatistics will not return any results: there are no visible segments") + + # update statistics for all segment IDs + for segmentIndex in range(visibleSegmentIds.GetNumberOfValues()): + segmentID = visibleSegmentIds.GetValue(segmentIndex) + self.updateStatisticsForSegment(segmentID) + finally: + if transformedSegmentationNode is not None: + # We made a copy and hardened the segmentation transform + self.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) + slicer.mrmlScene.RemoveNode(transformedSegmentationNode) + + def updateStatisticsForSegment(self, segmentID): + """ + Update statistical measures for specified segment. + Note: This will not change or reset measurement results of other segments + """ + + segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) + + if not segmentationNode.GetSegmentation().GetSegment(segmentID): + logging.debug("updateStatisticsForSegment will not update any results because the segment doesn't exist") + return + + segment = segmentationNode.GetSegmentation().GetSegment(segmentID) + statistics = self.getStatistics() + if segmentID not in statistics["SegmentIDs"]: + statistics["SegmentIDs"].append(segmentID) + statistics[segmentID, "Segment"] = segment.GetName() + + # apply all enabled plugins + for plugin in self.plugins: + pluginName = plugin.__class__.__name__ + if self.getParameterNode().GetParameter(pluginName + '.enabled') == 'True': + stats = plugin.computeStatistics(segmentID) + for key in stats: + statistics[segmentID, pluginName + '.' + key] = stats[key] + statistics["MeasurementInfo"][pluginName + '.' + key] = plugin.getMeasurementInfo(key) + + def getPluginByKey(self, key): + """Get plugin responsible for obtaining measurement value for given key""" + for plugin in self.plugins: + if plugin.toShortKey(key) in plugin.keys: + return plugin + return None + + def getMeasurementInfo(self, key): + """Get information (name, description, units, ...) about the measurement for the given key""" + plugin = self.getPluginByKey(key) + if plugin: + return plugin.getMeasurementInfo(plugin.toShortKey(key)) + return None + + def getStatisticsValueAsString(self, segmentID, key): + statistics = self.getStatistics() if (segmentID, key) in statistics: - csv += "," + str(statistics[segmentID,key]) + value = statistics[segmentID, key] + if isinstance(value, float): + return "%0.3f" % value # round to 3 decimals + else: + return str(value) else: - csv += "," - return csv - - def exportToCSVFile(self, fileName, nonEmptyKeysOnly = True): - fp = open(fileName, "w") - fp.write(self.exportToString(nonEmptyKeysOnly)) - fp.close() + return self.notAvailableValueString + + def getNonEmptyKeys(self): + # Fill columns + statistics = self.getStatistics() + nonEmptyKeys = [] + for key in self.keys: + for segmentID in statistics["SegmentIDs"]: + if (segmentID, key) in statistics: + nonEmptyKeys.append(key) + break + return nonEmptyKeys + + def getHeaderNames(self, nonEmptyKeysOnly=True): + # Derive column header names based on: (a) DICOM information if present, + # (b) measurement info name if present (c) measurement key as fallback. + # Duplicate names get a postfix [1][2]... to make them unique + # Initial and unique column header names are returned + keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys + statistics = self.getStatistics() + headerNames = [] + for key in keys: + name = key + info = statistics['MeasurementInfo'][key] if key in statistics['MeasurementInfo'] else {} + entry = slicer.vtkCodedEntry() + dicomBasedName = False + if info: + if 'DICOM.DerivationCode' in info and info['DICOM.DerivationCode']: + entry.SetFromString(info['DICOM.DerivationCode']) + name = entry.GetCodeMeaning() + dicomBasedName = True + elif 'DICOM.QuantityCode' in info and info['DICOM.QuantityCode']: + entry.SetFromString(info['DICOM.QuantityCode']) + name = entry.GetCodeMeaning() + dicomBasedName = True + elif 'name' in info and info['name']: + name = info['name'] + if dicomBasedName and 'DICOM.UnitsCode' in info and info['DICOM.UnitsCode']: + entry.SetFromString(info['DICOM.UnitsCode']) + units = entry.GetCodeValue() + if len(units) > 0 and units[0] == '[' and units[-1] == ']': + units = units[1:-1] + if len(units) > 0: + name += ' [' + units + ']' + elif 'units' in info and info['units'] and len(info['units']) > 0: + units = info['units'] + name += ' [' + units + ']' + headerNames.append(name) + uniqueHeaderNames = list(headerNames) + for duplicateName in {name for name in uniqueHeaderNames if uniqueHeaderNames.count(name) > 1}: + j = 1 + for i in range(len(uniqueHeaderNames)): + if uniqueHeaderNames[i] == duplicateName: + uniqueHeaderNames[i] = duplicateName + ' (' + str(j) + ')' + j += 1 + headerNames = {keys[i]: headerNames[i] for i in range(len(keys))} + uniqueHeaderNames = {keys[i]: uniqueHeaderNames[i] for i in range(len(keys))} + return headerNames, uniqueHeaderNames + + def exportToTable(self, table, nonEmptyKeysOnly=True): + """ + Export statistics to table node + """ + tableWasModified = table.StartModify() + table.RemoveAllColumns() + + keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys + columnHeaderNames, uniqueColumnHeaderNames = self.getHeaderNames(nonEmptyKeysOnly) + + # Define table columns + statistics = self.getStatistics() + for key in keys: + # create table column appropriate for data type; currently supported: float, int, long, string + measurements = [statistics[segmentID, key] for segmentID in statistics["SegmentIDs"] if + (segmentID, key) in statistics] + if len(measurements) == 0: # there were not measurements and therefore use the default "string" representation + col = table.AddColumn() + elif isinstance(measurements[0], int): + col = table.AddColumn(vtk.vtkLongArray()) + elif isinstance(measurements[0], float): + col = table.AddColumn(vtk.vtkDoubleArray()) + elif isinstance(measurements[0], list): + length = len(measurements[0]) + if length == 0: + col = table.AddColumn() + else: + value = measurements[0][0] + if isinstance(value, int): + array = vtk.vtkLongArray() + array.SetNumberOfComponents(length) + col = table.AddColumn(array) + elif isinstance(value, float): + array = vtk.vtkDoubleArray() + array.SetNumberOfComponents(length) + col = table.AddColumn(array) + else: + col = table.AddColumn() + else: # default + col = table.AddColumn() + plugin = self.getPluginByKey(key) + columnName = uniqueColumnHeaderNames[key] + longColumnName = columnHeaderNames[key] + col.SetName(columnName) + if plugin: + table.SetColumnProperty(columnName, "Plugin", plugin.name) + longColumnName += '
          Computed by ' + plugin.name + ' Statistics plugin' + table.SetColumnLongName(columnName, longColumnName) + measurementInfo = statistics["MeasurementInfo"][key] if key in statistics["MeasurementInfo"] else {} + if measurementInfo: + for mik, miv in measurementInfo.items(): + if mik == 'description': + table.SetColumnDescription(columnName, str(miv)) + elif mik == 'units': + table.SetColumnUnitLabel(columnName, str(miv)) + elif mik == 'componentNames': + componentNames = miv + array = table.GetTable().GetColumnByName(columnName) + componentIndex = 0 + for componentName in miv: + array.SetComponentName(componentIndex, componentName) + componentIndex += 1 + else: + table.SetColumnProperty(columnName, str(mik), str(miv)) + + # Fill columns + for segmentID in statistics["SegmentIDs"]: + rowIndex = table.AddEmptyRow() + columnIndex = 0 + for key in keys: + value = statistics[segmentID, key] if (segmentID, key) in statistics else None + if value is None and key != 'Segment': + value = float('nan') + if isinstance(value, list): + for i in range(len(value)): + table.GetTable().GetColumn(columnIndex).SetComponent(rowIndex, i, value[i]) + else: + table.GetTable().GetColumn(columnIndex).SetValue(rowIndex, value) + columnIndex += 1 + + table.Modified() + table.EndModify(tableWasModified) + + def showTable(self, table): + """ + Switch to a layout where tables are visible and show the selected table + """ + currentLayout = slicer.app.layoutManager().layout + layoutWithTable = slicer.modules.tables.logic().GetLayoutWithTable(currentLayout) + slicer.app.layoutManager().setLayout(layoutWithTable) + slicer.app.applicationLogic().GetSelectionNode().SetActiveTableID(table.GetID()) + slicer.app.applicationLogic().PropagateTableSelection() + + def exportToString(self, nonEmptyKeysOnly=True): + """ + Returns string with comma separated values, with header keys in quotes. + """ + keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys + # Header + csv = '"' + '","'.join(keys) + '"' + # Rows + statistics = self.getStatistics() + for segmentID in statistics["SegmentIDs"]: + csv += "\n" + str(statistics[segmentID, keys[0]]) + for key in keys[1:]: + if (segmentID, key) in statistics: + csv += "," + str(statistics[segmentID, key]) + else: + csv += "," + return csv + + def exportToCSVFile(self, fileName, nonEmptyKeysOnly=True): + fp = open(fileName, "w") + fp.write(self.exportToString(nonEmptyKeysOnly)) + fp.close() class SegmentStatisticsTest(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,scenario=None): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_SegmentStatisticsBasic() - - self.setUp() - self.test_SegmentStatisticsPlugins() - - def test_SegmentStatisticsBasic(self): """ - This tests some aspects of the label statistics + 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 """ - self.delayDisplay("Starting test_SegmentStatisticsBasic") - - import SampleData - from SegmentStatistics import SegmentStatisticsLogic - - self.delayDisplay("Load master volume") - - masterVolumeNode = SampleData.downloadSample('MRBrainTumor1') - - self.delayDisplay("Create segmentation containing a few spheres") - - segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') - segmentationNode.CreateDefaultDisplayNodes() - segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode) - - # Geometry for each segment is defined by: radius, posX, posY, posZ - segmentGeometries = [[10, -6,30,28], [20, 0,65,32], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0,30,64], - [12, 31, 33, 27], [17, -42, 30, 27]] - for segmentGeometry in segmentGeometries: - sphereSource = vtk.vtkSphereSource() - sphereSource.SetRadius(segmentGeometry[0]) - sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3]) - sphereSource.Update() - uniqueSegmentID = segmentationNode.GetSegmentation().GenerateUniqueSegmentID("Test") - segmentationNode.AddSegmentFromClosedSurfaceRepresentation(sphereSource.GetOutput(), uniqueSegmentID) + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear(0) - self.delayDisplay("Compute statistics") - - segStatLogic = SegmentStatisticsLogic() - segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) - segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) - segStatLogic.computeStatistics() - - self.delayDisplay("Check a few numerical results") - self.assertEqual( segStatLogic.getStatistics()["Test_2","LabelmapSegmentStatisticsPlugin.voxel_count"], 9807) - self.assertEqual( segStatLogic.getStatistics()["Test_4","ScalarVolumeSegmentStatisticsPlugin.voxel_count"], 380) - - self.delayDisplay("Export results to table") - resultsTableNode = slicer.vtkMRMLTableNode() - slicer.mrmlScene.AddNode(resultsTableNode) - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - - self.delayDisplay("Export results to string") - logging.info(segStatLogic.exportToString()) - - outputFilename = slicer.app.temporaryPath + '/SegmentStatisticsTestOutput.csv' - self.delayDisplay("Export results to CSV file: "+outputFilename) - segStatLogic.exportToCSVFile(outputFilename) - - self.delayDisplay('test_SegmentStatisticsBasic passed!') - - def test_SegmentStatisticsPlugins(self): - """ - This tests some aspects of the segment statistics plugins - """ - - self.delayDisplay("Starting test_SegmentStatisticsPlugins") - - import vtkSegmentationCorePython as vtkSegmentationCore - import SampleData - from SegmentStatistics import SegmentStatisticsLogic - - self.delayDisplay("Load master volume") - - masterVolumeNode = SampleData.downloadSample('MRBrainTumor1') - - self.delayDisplay("Create segmentation containing a few spheres") - - segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') - segmentationNode.CreateDefaultDisplayNodes() - segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode) - - # Geometry for each segment is defined by: radius, posX, posY, posZ - segmentGeometries = [[10, -6,30,28], [20, 0,65,32], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0,30,64], - [12, 31, 33, 27], [17, -42, 30, 27]] - for segmentGeometry in segmentGeometries: - sphereSource = vtk.vtkSphereSource() - sphereSource.SetRadius(segmentGeometry[0]) - sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3]) - sphereSource.Update() - segment = vtkSegmentationCore.vtkSegment() - uniqueSegmentID = segmentationNode.GetSegmentation().GenerateUniqueSegmentID("Test") - segmentationNode.AddSegmentFromClosedSurfaceRepresentation(sphereSource.GetOutput(), uniqueSegmentID) - - # test calculating only measurements for selected segments - self.delayDisplay("Test calculating only measurements for individual segments") - segStatLogic = SegmentStatisticsLogic() - segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) - segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) - segStatLogic.updateStatisticsForSegment('Test_2') - resultsTableNode = slicer.vtkMRMLTableNode() - slicer.mrmlScene.AddNode(resultsTableNode) - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - self.assertEqual( segStatLogic.getStatistics()["Test_2","LabelmapSegmentStatisticsPlugin.voxel_count"], 9807) - with self.assertRaises(KeyError): segStatLogic.getStatistics()["Test_4","ScalarVolumeSegmentStatisticsPlugin.voxel count"] - # assert there are no result for this segment - segStatLogic.updateStatisticsForSegment('Test_4') - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - self.assertEqual( segStatLogic.getStatistics()["Test_2","LabelmapSegmentStatisticsPlugin.voxel_count"], 9807) - self.assertEqual( segStatLogic.getStatistics()["Test_4","LabelmapSegmentStatisticsPlugin.voxel_count"], 380) - with self.assertRaises(KeyError): segStatLogic.getStatistics()["Test_5","ScalarVolumeSegmentStatisticsPlugin.voxel count"] - # assert there are no result for this segment - - # calculate measurements for all segments - segStatLogic.computeStatistics() - self.assertEqual( segStatLogic.getStatistics()["Test","LabelmapSegmentStatisticsPlugin.voxel_count"], 2948) - self.assertEqual( segStatLogic.getStatistics()["Test_1","LabelmapSegmentStatisticsPlugin.voxel_count"], 23281) - - # test updating measurements for segments one by one - self.delayDisplay("Update some segments in the segmentation") - segmentGeometriesNew = [[5, -6,30,28], [21, 0,65,32]] - # We add/remove representations, so we temporarily block segment modifications - # to make sure display managers don't try to access data while it is in an - # inconsistent state. - wasModified = segmentationNode.StartModify() - for i in range(len(segmentGeometriesNew)): - segmentGeometry = segmentGeometriesNew[i] - sphereSource = vtk.vtkSphereSource() - sphereSource.SetRadius(segmentGeometry[0]) - sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3]) - sphereSource.Update() - segment = segmentationNode.GetSegmentation().GetNthSegment(i) - segment.RemoveAllRepresentations() - closedSurfaceName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName() - segment.AddRepresentation(closedSurfaceName, - sphereSource.GetOutput()) - segmentationNode.EndModify(wasModified) - self.assertEqual( segStatLogic.getStatistics()["Test","LabelmapSegmentStatisticsPlugin.voxel_count"], 2948) - self.assertEqual( segStatLogic.getStatistics()["Test_1","LabelmapSegmentStatisticsPlugin.voxel_count"], 23281) - segStatLogic.updateStatisticsForSegment('Test_1') - self.assertEqual( segStatLogic.getStatistics()["Test","LabelmapSegmentStatisticsPlugin.voxel_count"], 2948) - self.assertTrue( segStatLogic.getStatistics()["Test_1","LabelmapSegmentStatisticsPlugin.voxel_count"]!=23281) - segStatLogic.updateStatisticsForSegment('Test') - self.assertTrue( segStatLogic.getStatistics()["Test","LabelmapSegmentStatisticsPlugin.voxel_count"]!=2948) - self.assertTrue( segStatLogic.getStatistics()["Test_1","LabelmapSegmentStatisticsPlugin.voxel_count"]!=23281) - - # test enabling/disabling of individual measurements - self.delayDisplay("Test disabling of individual measurements") - segStatLogic = SegmentStatisticsLogic() - segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) - segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) - segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.voxel_count.enabled",str(False)) - segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.volume_cm3.enabled",str(False)) - segStatLogic.computeStatistics() - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] - self.assertFalse('Number of voxels [voxels] (1)' in columnHeaders) - self.assertTrue('Volume [mm3] (1)' in columnHeaders) - self.assertFalse('Volume [cm3] (3)' in columnHeaders) - - self.delayDisplay("Test re-enabling of individual measurements") - segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.voxel_count.enabled",str(True)) - segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.volume_cm3.enabled",str(True)) - segStatLogic.computeStatistics() - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] - self.assertTrue('Number of voxels [voxels] (1)' in columnHeaders) - self.assertTrue('Volume [mm3] (1)' in columnHeaders) - self.assertTrue('Volume [cm3] (1)' in columnHeaders) - - # test enabling/disabling of individual plugins - self.delayDisplay("Test disabling of plugin") - segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.enabled",str(False)) - segStatLogic.computeStatistics() - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] - self.assertFalse('Number of voxels [voxels] (3)' in columnHeaders) - self.assertFalse('Volume [mm3] (3)' in columnHeaders) - self.assertTrue('Volume [mm3] (2)' in columnHeaders) - - self.delayDisplay("Test re-enabling of plugin") - segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.enabled",str(True)) - segStatLogic.computeStatistics() - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] - self.assertTrue('Number of voxels [voxels] (2)' in columnHeaders) - self.assertTrue('Volume [mm3] (3)' in columnHeaders) - - # test unregistering/registering of plugins - self.delayDisplay("Test of removing all registered plugins") - SegmentStatisticsLogic.registeredPlugins = [] # remove all registered plugins - segStatLogic = SegmentStatisticsLogic() - segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) - segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) - segStatLogic.computeStatistics() - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] - self.assertEqual(len(columnHeaders),1) # only header element should be "Segment" - self.assertEqual(columnHeaders[0],"Segment") # only header element should be "Segment" - - self.delayDisplay("Test registering plugins") - SegmentStatisticsLogic.registerPlugin(LabelmapSegmentStatisticsPlugin()) - SegmentStatisticsLogic.registerPlugin(ScalarVolumeSegmentStatisticsPlugin()) - SegmentStatisticsLogic.registerPlugin(ClosedSurfaceSegmentStatisticsPlugin()) - segStatLogic = SegmentStatisticsLogic() - segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) - segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) - segStatLogic.computeStatistics() - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] - self.assertTrue('Number of voxels [voxels] (1)' in columnHeaders) - self.assertTrue('Number of voxels [voxels] (2)' in columnHeaders) - self.assertTrue('Surface area [mm2]' in columnHeaders) - - self.delayDisplay('test_SegmentStatisticsPlugins passed!') + def runTest(self, scenario=None): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_SegmentStatisticsBasic() + + self.setUp() + self.test_SegmentStatisticsPlugins() + + def test_SegmentStatisticsBasic(self): + """ + This tests some aspects of the label statistics + """ + + self.delayDisplay("Starting test_SegmentStatisticsBasic") + + import SampleData + from SegmentStatistics import SegmentStatisticsLogic + + self.delayDisplay("Load master volume") + + masterVolumeNode = SampleData.downloadSample('MRBrainTumor1') + + self.delayDisplay("Create segmentation containing a few spheres") + + segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') + segmentationNode.CreateDefaultDisplayNodes() + segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode) + + # Geometry for each segment is defined by: radius, posX, posY, posZ + segmentGeometries = [[10, -6, 30, 28], [20, 0, 65, 32], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 64], + [12, 31, 33, 27], [17, -42, 30, 27]] + for segmentGeometry in segmentGeometries: + sphereSource = vtk.vtkSphereSource() + sphereSource.SetRadius(segmentGeometry[0]) + sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3]) + sphereSource.Update() + uniqueSegmentID = segmentationNode.GetSegmentation().GenerateUniqueSegmentID("Test") + segmentationNode.AddSegmentFromClosedSurfaceRepresentation(sphereSource.GetOutput(), uniqueSegmentID) + + self.delayDisplay("Compute statistics") + + segStatLogic = SegmentStatisticsLogic() + segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) + segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) + segStatLogic.computeStatistics() + + self.delayDisplay("Check a few numerical results") + self.assertEqual(segStatLogic.getStatistics()["Test_2", "LabelmapSegmentStatisticsPlugin.voxel_count"], 9807) + self.assertEqual(segStatLogic.getStatistics()["Test_4", "ScalarVolumeSegmentStatisticsPlugin.voxel_count"], 380) + + self.delayDisplay("Export results to table") + resultsTableNode = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(resultsTableNode) + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + + self.delayDisplay("Export results to string") + logging.info(segStatLogic.exportToString()) + + outputFilename = slicer.app.temporaryPath + '/SegmentStatisticsTestOutput.csv' + self.delayDisplay("Export results to CSV file: " + outputFilename) + segStatLogic.exportToCSVFile(outputFilename) + + self.delayDisplay('test_SegmentStatisticsBasic passed!') + + def test_SegmentStatisticsPlugins(self): + """ + This tests some aspects of the segment statistics plugins + """ + + self.delayDisplay("Starting test_SegmentStatisticsPlugins") + + import vtkSegmentationCorePython as vtkSegmentationCore + import SampleData + from SegmentStatistics import SegmentStatisticsLogic + + self.delayDisplay("Load master volume") + + masterVolumeNode = SampleData.downloadSample('MRBrainTumor1') + + self.delayDisplay("Create segmentation containing a few spheres") + + segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') + segmentationNode.CreateDefaultDisplayNodes() + segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode) + + # Geometry for each segment is defined by: radius, posX, posY, posZ + segmentGeometries = [[10, -6, 30, 28], [20, 0, 65, 32], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 64], + [12, 31, 33, 27], [17, -42, 30, 27]] + for segmentGeometry in segmentGeometries: + sphereSource = vtk.vtkSphereSource() + sphereSource.SetRadius(segmentGeometry[0]) + sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3]) + sphereSource.Update() + segment = vtkSegmentationCore.vtkSegment() + uniqueSegmentID = segmentationNode.GetSegmentation().GenerateUniqueSegmentID("Test") + segmentationNode.AddSegmentFromClosedSurfaceRepresentation(sphereSource.GetOutput(), uniqueSegmentID) + + # test calculating only measurements for selected segments + self.delayDisplay("Test calculating only measurements for individual segments") + segStatLogic = SegmentStatisticsLogic() + segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) + segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) + segStatLogic.updateStatisticsForSegment('Test_2') + resultsTableNode = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(resultsTableNode) + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + self.assertEqual(segStatLogic.getStatistics()["Test_2", "LabelmapSegmentStatisticsPlugin.voxel_count"], 9807) + with self.assertRaises(KeyError): + segStatLogic.getStatistics()["Test_4", "ScalarVolumeSegmentStatisticsPlugin.voxel count"] + # assert there are no result for this segment + segStatLogic.updateStatisticsForSegment('Test_4') + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + self.assertEqual(segStatLogic.getStatistics()["Test_2", "LabelmapSegmentStatisticsPlugin.voxel_count"], 9807) + self.assertEqual(segStatLogic.getStatistics()["Test_4", "LabelmapSegmentStatisticsPlugin.voxel_count"], 380) + with self.assertRaises(KeyError): + segStatLogic.getStatistics()["Test_5", "ScalarVolumeSegmentStatisticsPlugin.voxel count"] + # assert there are no result for this segment + + # calculate measurements for all segments + segStatLogic.computeStatistics() + self.assertEqual(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"], 2948) + self.assertEqual(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"], 23281) + + # test updating measurements for segments one by one + self.delayDisplay("Update some segments in the segmentation") + segmentGeometriesNew = [[5, -6, 30, 28], [21, 0, 65, 32]] + # We add/remove representations, so we temporarily block segment modifications + # to make sure display managers don't try to access data while it is in an + # inconsistent state. + wasModified = segmentationNode.StartModify() + for i in range(len(segmentGeometriesNew)): + segmentGeometry = segmentGeometriesNew[i] + sphereSource = vtk.vtkSphereSource() + sphereSource.SetRadius(segmentGeometry[0]) + sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3]) + sphereSource.Update() + segment = segmentationNode.GetSegmentation().GetNthSegment(i) + segment.RemoveAllRepresentations() + closedSurfaceName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName() + segment.AddRepresentation(closedSurfaceName, + sphereSource.GetOutput()) + segmentationNode.EndModify(wasModified) + self.assertEqual(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"], 2948) + self.assertEqual(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"], 23281) + segStatLogic.updateStatisticsForSegment('Test_1') + self.assertEqual(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"], 2948) + self.assertTrue(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"] != 23281) + segStatLogic.updateStatisticsForSegment('Test') + self.assertTrue(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"] != 2948) + self.assertTrue(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"] != 23281) + + # test enabling/disabling of individual measurements + self.delayDisplay("Test disabling of individual measurements") + segStatLogic = SegmentStatisticsLogic() + segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) + segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) + segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.voxel_count.enabled", str(False)) + segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.volume_cm3.enabled", str(False)) + segStatLogic.computeStatistics() + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] + self.assertFalse('Number of voxels [voxels] (1)' in columnHeaders) + self.assertTrue('Volume [mm3] (1)' in columnHeaders) + self.assertFalse('Volume [cm3] (3)' in columnHeaders) + + self.delayDisplay("Test re-enabling of individual measurements") + segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.voxel_count.enabled", str(True)) + segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.volume_cm3.enabled", str(True)) + segStatLogic.computeStatistics() + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] + self.assertTrue('Number of voxels [voxels] (1)' in columnHeaders) + self.assertTrue('Volume [mm3] (1)' in columnHeaders) + self.assertTrue('Volume [cm3] (1)' in columnHeaders) + + # test enabling/disabling of individual plugins + self.delayDisplay("Test disabling of plugin") + segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.enabled", str(False)) + segStatLogic.computeStatistics() + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] + self.assertFalse('Number of voxels [voxels] (3)' in columnHeaders) + self.assertFalse('Volume [mm3] (3)' in columnHeaders) + self.assertTrue('Volume [mm3] (2)' in columnHeaders) + + self.delayDisplay("Test re-enabling of plugin") + segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.enabled", str(True)) + segStatLogic.computeStatistics() + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] + self.assertTrue('Number of voxels [voxels] (2)' in columnHeaders) + self.assertTrue('Volume [mm3] (3)' in columnHeaders) + + # test unregistering/registering of plugins + self.delayDisplay("Test of removing all registered plugins") + SegmentStatisticsLogic.registeredPlugins = [] # remove all registered plugins + segStatLogic = SegmentStatisticsLogic() + segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) + segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) + segStatLogic.computeStatistics() + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] + self.assertEqual(len(columnHeaders), 1) # only header element should be "Segment" + self.assertEqual(columnHeaders[0], "Segment") # only header element should be "Segment" + + self.delayDisplay("Test registering plugins") + SegmentStatisticsLogic.registerPlugin(LabelmapSegmentStatisticsPlugin()) + SegmentStatisticsLogic.registerPlugin(ScalarVolumeSegmentStatisticsPlugin()) + SegmentStatisticsLogic.registerPlugin(ClosedSurfaceSegmentStatisticsPlugin()) + segStatLogic = SegmentStatisticsLogic() + segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID()) + segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID()) + segStatLogic.computeStatistics() + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())] + self.assertTrue('Number of voxels [voxels] (1)' in columnHeaders) + self.assertTrue('Number of voxels [voxels] (2)' in columnHeaders) + self.assertTrue('Surface area [mm2]' in columnHeaders) + + self.delayDisplay('test_SegmentStatisticsPlugins passed!') class Slicelet: - """A slicer slicelet is a module widget that comes up in stand alone mode - implemented as a python class. - This class provides common wrapper functionality used by all slicer modlets. - """ - # TODO: put this in a SliceletLib - # TODO: parse command line args - - def __init__(self, widgetClass=None): - self.parent = qt.QFrame() - self.parent.setLayout( qt.QVBoxLayout() ) - - # TODO: should have way to pop up python interactor - self.buttons = qt.QFrame() - self.buttons.setLayout( qt.QHBoxLayout() ) - self.parent.layout().addWidget(self.buttons) - self.addDataButton = qt.QPushButton("Add Data") - self.buttons.layout().addWidget(self.addDataButton) - self.addDataButton.connect("clicked()",slicer.app.ioManager().openAddDataDialog) - self.loadSceneButton = qt.QPushButton("Load Scene") - self.buttons.layout().addWidget(self.loadSceneButton) - self.loadSceneButton.connect("clicked()",slicer.app.ioManager().openLoadSceneDialog) - - if widgetClass: - self.widget = widgetClass(self.parent) - self.widget.setup() - self.parent.show() + """A slicer slicelet is a module widget that comes up in stand alone mode + implemented as a python class. + This class provides common wrapper functionality used by all slicer modlets. + """ + # TODO: put this in a SliceletLib + # TODO: parse command line args + + def __init__(self, widgetClass=None): + self.parent = qt.QFrame() + self.parent.setLayout(qt.QVBoxLayout()) + + # TODO: should have way to pop up python interactor + self.buttons = qt.QFrame() + self.buttons.setLayout(qt.QHBoxLayout()) + self.parent.layout().addWidget(self.buttons) + self.addDataButton = qt.QPushButton("Add Data") + self.buttons.layout().addWidget(self.addDataButton) + self.addDataButton.connect("clicked()", slicer.app.ioManager().openAddDataDialog) + self.loadSceneButton = qt.QPushButton("Load Scene") + self.buttons.layout().addWidget(self.loadSceneButton) + self.loadSceneButton.connect("clicked()", slicer.app.ioManager().openLoadSceneDialog) + + if widgetClass: + self.widget = widgetClass(self.parent) + self.widget.setup() + self.parent.show() class SegmentStatisticsSlicelet(Slicelet): - """ Creates the interface when module is run as a stand alone gui app. - """ + """ Creates the interface when module is run as a stand alone gui app. + """ - def __init__(self): - super().__init__(SegmentStatisticsWidget) + def __init__(self): + super().__init__(SegmentStatisticsWidget) if __name__ == "__main__": - # TODO: need a way to access and parse command line arguments - # TODO: ideally command line args should handle --xml + # TODO: need a way to access and parse command line arguments + # TODO: ideally command line args should handle --xml - import sys - print( sys.argv ) + import sys + print(sys.argv) - slicelet = SegmentStatisticsSlicelet() + slicelet = SegmentStatisticsSlicelet() diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ClosedSurfaceSegmentStatisticsPlugin.py b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ClosedSurfaceSegmentStatisticsPlugin.py index 4c015053789..5514b2ef2a6 100644 --- a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ClosedSurfaceSegmentStatisticsPlugin.py +++ b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ClosedSurfaceSegmentStatisticsPlugin.py @@ -4,68 +4,68 @@ class ClosedSurfaceSegmentStatisticsPlugin(SegmentStatisticsPluginBase): - """Statistical plugin for closed surfaces""" + """Statistical plugin for closed surfaces""" - def __init__(self): - super().__init__() - self.name = "Closed Surface" - self.keys = ["surface_mm2", "volume_mm3", "volume_cm3"] - self.defaultKeys = self.keys # calculate all measurements by default - #... developer may add extra options to configure other parameters + def __init__(self): + super().__init__() + self.name = "Closed Surface" + self.keys = ["surface_mm2", "volume_mm3", "volume_cm3"] + self.defaultKeys = self.keys # calculate all measurements by default + # ... developer may add extra options to configure other parameters - def computeStatistics(self, segmentID): - import vtkSegmentationCorePython as vtkSegmentationCore - requestedKeys = self.getRequestedKeys() + def computeStatistics(self, segmentID): + import vtkSegmentationCorePython as vtkSegmentationCore + requestedKeys = self.getRequestedKeys() - segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) + segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) - if len(requestedKeys)==0: - return {} + if len(requestedKeys) == 0: + return {} - containsClosedSurfaceRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation( - vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()) - if not containsClosedSurfaceRepresentation: - return {} + containsClosedSurfaceRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation( + vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()) + if not containsClosedSurfaceRepresentation: + return {} - segmentClosedSurface = vtk.vtkPolyData() - segmentationNode.GetClosedSurfaceRepresentation(segmentID, segmentClosedSurface) + segmentClosedSurface = vtk.vtkPolyData() + segmentationNode.GetClosedSurfaceRepresentation(segmentID, segmentClosedSurface) - # Compute statistics - massProperties = vtk.vtkMassProperties() - massProperties.SetInputData(segmentClosedSurface) + # Compute statistics + massProperties = vtk.vtkMassProperties() + massProperties.SetInputData(segmentClosedSurface) - # Add data to statistics list - ccPerCubicMM = 0.001 - stats = {} - if "surface_mm2" in requestedKeys: - stats["surface_mm2"] = massProperties.GetSurfaceArea() - if "volume_mm3" in requestedKeys: - stats["volume_mm3"] = massProperties.GetVolume() - if "volume_cm3" in requestedKeys: - stats["volume_cm3"] = massProperties.GetVolume() * ccPerCubicMM - return stats + # Add data to statistics list + ccPerCubicMM = 0.001 + stats = {} + if "surface_mm2" in requestedKeys: + stats["surface_mm2"] = massProperties.GetSurfaceArea() + if "volume_mm3" in requestedKeys: + stats["volume_mm3"] = massProperties.GetVolume() + if "volume_cm3" in requestedKeys: + stats["volume_cm3"] = massProperties.GetVolume() * ccPerCubicMM + return stats - def getMeasurementInfo(self, key): - """Get information (name, description, units, ...) about the measurement for the given key""" - info = dict() + def getMeasurementInfo(self, key): + """Get information (name, description, units, ...) about the measurement for the given key""" + info = dict() - # I searched BioPortal, and found seemingly most suitable code. - # Prefixed with "99" since CHEMINF is not a recognized DICOM coding scheme. - # See https://bioportal.bioontology.org/ontologies/CHEMINF?p=classes&conceptid=http%3A%2F%2Fsemanticscience.org%2Fresource%2FCHEMINF_000247 - # - info["surface_mm2"] = \ - self.createMeasurementInfo(name="Surface mm2", description="Surface area in mm2", units="mm2", - quantityDicomCode=self.createCodedEntry("000247", "99CHEMINF", "Surface area", True), - unitsDicomCode=self.createCodedEntry("mm2", "UCUM", "squared millimeters", True)) + # I searched BioPortal, and found seemingly most suitable code. + # Prefixed with "99" since CHEMINF is not a recognized DICOM coding scheme. + # See https://bioportal.bioontology.org/ontologies/CHEMINF?p=classes&conceptid=http%3A%2F%2Fsemanticscience.org%2Fresource%2FCHEMINF_000247 + # + info["surface_mm2"] = \ + self.createMeasurementInfo(name="Surface mm2", description="Surface area in mm2", units="mm2", + quantityDicomCode=self.createCodedEntry("000247", "99CHEMINF", "Surface area", True), + unitsDicomCode=self.createCodedEntry("mm2", "UCUM", "squared millimeters", True)) - info["volume_mm3"] = \ - self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3", - quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), - unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True)) + info["volume_mm3"] = \ + self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3", + quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), + unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True)) - info["volume_cm3"] = \ - self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3", - quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), - unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True)) + info["volume_cm3"] = \ + self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3", + quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), + unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True)) - return info[key] if key in info else None + return info[key] if key in info else None diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/LabelmapSegmentStatisticsPlugin.py b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/LabelmapSegmentStatisticsPlugin.py index 2d1c1eaf5f9..c38f80f49ef 100644 --- a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/LabelmapSegmentStatisticsPlugin.py +++ b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/LabelmapSegmentStatisticsPlugin.py @@ -7,468 +7,468 @@ class LabelmapSegmentStatisticsPlugin(SegmentStatisticsPluginBase): - """Statistical plugin for Labelmaps""" - - def __init__(self): - super().__init__() - self.name = "Labelmap" - self.obbKeys = ["obb_origin_ras", "obb_diameter_mm", "obb_direction_ras_x", "obb_direction_ras_y", "obb_direction_ras_z"] - self.principalAxisKeys = ["principal_axis_x", "principal_axis_y", "principal_axis_z"] - self.shapeKeys = [ - "centroid_ras", "feret_diameter_mm", "surface_area_mm2", "roundness", "flatness", "elongation", - "principal_moments", - ] + self.principalAxisKeys + self.obbKeys - - self.defaultKeys = ["voxel_count", "volume_mm3", "volume_cm3"] # Don't calculate label shape statistics by default since they take longer to compute - self.keys = self.defaultKeys + self.shapeKeys - self.keyToShapeStatisticNames = { - "centroid_ras" : vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Centroid), - "feret_diameter_mm": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.FeretDiameter), - "surface_area_mm2" : vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Perimeter), - "roundness" : vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Roundness), - "flatness" : vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Flatness), - "elongation" : vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Elongation), - "oriented_bounding_box" : vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.OrientedBoundingBox), - "obb_origin_ras" : "OrientedBoundingBoxOrigin", - "obb_diameter_mm" : "OrientedBoundingBoxSize", - "obb_direction_ras_x" : "OrientedBoundingBoxDirectionX", - "obb_direction_ras_y" : "OrientedBoundingBoxDirectionY", - "obb_direction_ras_z" : "OrientedBoundingBoxDirectionZ", - "principal_moments" : vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.PrincipalMoments), - "principal_axes" : vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.PrincipalAxes), - "principal_axis_x" : "PrincipalAxisX", - "principal_axis_y" : "PrincipalAxisY", - "principal_axis_z" : "PrincipalAxisZ", - } - #... developer may add extra options to configure other parameters - - def computeStatistics(self, segmentID): - import vtkSegmentationCorePython as vtkSegmentationCore - requestedKeys = self.getRequestedKeys() - - segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) - - if len(requestedKeys)==0: - return {} - - containsLabelmapRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation( - vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()) - if not containsLabelmapRepresentation: - return {} - - segmentLabelmap = slicer.vtkOrientedImageData() - segmentationNode.GetBinaryLabelmapRepresentation(segmentID, segmentLabelmap) - if (not segmentLabelmap - or not segmentLabelmap.GetPointData() - or not segmentLabelmap.GetPointData().GetScalars()): - # No input label data - return {} - - # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value - labelValue = 1 - backgroundValue = 0 - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(segmentLabelmap) - thresh.ThresholdByLower(0) - thresh.SetInValue(backgroundValue) - thresh.SetOutValue(labelValue) - thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) - thresh.Update() - - # Use binary labelmap as a stencil - stencil = vtk.vtkImageToImageStencil() - stencil.SetInputData(thresh.GetOutput()) - stencil.ThresholdByUpper(labelValue) - stencil.Update() - - stat = vtk.vtkImageAccumulate() - stat.SetInputData(thresh.GetOutput()) - stat.SetStencilData(stencil.GetOutput()) - stat.Update() - - # Add data to statistics list - cubicMMPerVoxel = reduce(lambda x,y: x*y, segmentLabelmap.GetSpacing()) - ccPerCubicMM = 0.001 - stats = {} - if "voxel_count" in requestedKeys: - stats["voxel_count"] = stat.GetVoxelCount() - if "volume_mm3" in requestedKeys: - stats["volume_mm3"] = stat.GetVoxelCount() * cubicMMPerVoxel - if "volume_cm3" in requestedKeys: - stats["volume_cm3"] = stat.GetVoxelCount() * cubicMMPerVoxel * ccPerCubicMM - - calculateShapeStats = False - for shapeKey in self.shapeKeys: - if shapeKey in requestedKeys: - calculateShapeStats = True - break - - if calculateShapeStats: - directions = vtk.vtkMatrix4x4() - segmentLabelmap.GetDirectionMatrix(directions) - - # Remove oriented bounding box from requested keys and replace with individual keys - requestedOptions = requestedKeys - statFilterOptions = self.shapeKeys - calculateOBB = ( - "obb_diameter_mm" in requestedKeys or - "obb_origin_ras" in requestedKeys or - "obb_direction_ras_x" in requestedKeys or - "obb_direction_ras_y" in requestedKeys or - "obb_direction_ras_z" in requestedKeys - ) - - if calculateOBB: - temp = statFilterOptions - statFilterOptions = [] - for option in temp: - if not option in self.obbKeys: - statFilterOptions.append(option) - statFilterOptions.append("oriented_bounding_box") - - temp = requestedOptions - requestedOptions = [] - for option in temp: - if not option in self.obbKeys: - requestedOptions.append(option) - requestedOptions.append("oriented_bounding_box") - - calculatePrincipalAxis = ( - "principal_axis_x" in requestedKeys or - "principal_axis_y" in requestedKeys or - "principal_axis_z" in requestedKeys - ) - if calculatePrincipalAxis: - temp = statFilterOptions - statFilterOptions = [] - for option in temp: - if not option in self.principalAxisKeys: - statFilterOptions.append(option) - statFilterOptions.append("principal_axes") - - temp = requestedOptions - requestedOptions = [] - for option in temp: - if not option in self.principalAxisKeys: - requestedOptions.append(option) - requestedOptions.append("principal_axes") - requestedOptions.append("centroid_ras") - - shapeStat = vtkITK.vtkITKLabelShapeStatistics() - shapeStat.SetInputData(thresh.GetOutput()) - shapeStat.SetDirections(directions) - for shapeKey in statFilterOptions: - shapeStat.SetComputeShapeStatistic(self.keyToShapeStatisticNames[shapeKey], shapeKey in requestedOptions) - shapeStat.Update() - - # If segmentation node is transformed, apply that transform to get RAS coordinates - transformSegmentToRas = vtk.vtkGeneralTransform() - slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(segmentationNode.GetParentTransformNode(), None, transformSegmentToRas) - - statTable = shapeStat.GetOutput() - if "centroid_ras" in requestedKeys: - centroidRAS = [0,0,0] - centroidTuple = None - centroidArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"]) - if centroidArray is None: - logging.error("Could not calculate centroid_ras!") - else: - centroidTuple = centroidArray.GetTuple(0) - if centroidTuple is not None: - transformSegmentToRas.TransformPoint(centroidTuple, centroidRAS) - stats["centroid_ras"] = centroidRAS - - if "roundness" in requestedKeys: - roundnessTuple = None - roundnessArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["roundness"]) - if roundnessArray is None: - logging.error("Could not calculate roundness!") - else: - roundnessTuple = roundnessArray.GetTuple(0) - if roundnessTuple is not None: - roundness = roundnessTuple[0] - stats["roundness"] = roundness - - if "flatness" in requestedKeys: - flatnessTuple = None - flatnessArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["flatness"]) - if flatnessArray is None: - logging.error("Could not calculate flatness!") - else: - flatnessTuple = flatnessArray.GetTuple(0) - if flatnessTuple is not None: - flatness = flatnessTuple[0] - stats["flatness"] = flatness - - if "elongation" in requestedKeys: - elongationTuple = None - elongationArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["elongation"]) - if elongationArray is None: - logging.error("Could not calculate elongation!") - else: - elongationTuple = elongationArray.GetTuple(0) - if elongationTuple is not None: - elongation = elongationTuple[0] - stats["elongation"] = elongation - - if "feret_diameter_mm" in requestedKeys: - feretDiameterTuple = None - feretDiameterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["feret_diameter_mm"]) - if feretDiameterArray is None: - logging.error("Could not calculate feret_diameter_mm!") - else: - feretDiameterTuple = feretDiameterArray.GetTuple(0) - if feretDiameterTuple is not None: - feretDiameter = feretDiameterTuple[0] - stats["feret_diameter_mm"] = feretDiameter - - if "surface_area_mm2" in requestedKeys: - perimeterTuple = None - perimeterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["surface_area_mm2"]) - if perimeterArray is None: - logging.error("Could not calculate surface_area_mm2!") - else: - perimeterTuple = perimeterArray.GetTuple(0) - if perimeterTuple is not None: - perimeter = perimeterTuple[0] - stats["surface_area_mm2"] = perimeter - - if "obb_origin_ras" in requestedKeys: - obbOriginTuple = None - obbOriginRAS = [0,0,0] - obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"]) - if obbOriginArray is None: - logging.error("Could not calculate obb_origin_ras!") - else: - obbOriginTuple = obbOriginArray.GetTuple(0) - if obbOriginTuple is not None: - transformSegmentToRas.TransformPoint(obbOriginTuple, obbOriginRAS) - stats["obb_origin_ras"] = obbOriginRAS - - if "obb_diameter_mm" in requestedKeys: - obbDiameterMMTuple = None - obbDiameterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_diameter_mm"]) - if obbDiameterArray is None: - logging.error("Could not calculate obb_diameter_mm!") - else: - obbDiameterMMTuple = obbDiameterArray.GetTuple(0) - if obbDiameterMMTuple is not None: - obbDiameterMM = list(obbDiameterMMTuple) - stats["obb_diameter_mm"] = obbDiameterMM - - if "obb_direction_ras_x" in requestedKeys: - obbOriginTuple = None - obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"]) - if obbOriginArray is None: - logging.error("Could not calculate obb_direction_ras_x!") - else: - obbOriginTuple = obbOriginArray.GetTuple(0) - - obbDirectionXTuple = None - obbDirectionXArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_x"]) - if obbDirectionXArray is None: - logging.error("Could not calculate obb_direction_ras_x!") - else: - obbDirectionXTuple = obbDirectionXArray.GetTuple(0) - - if obbOriginTuple is not None and obbDirectionXTuple is not None: - obbDirectionX = list(obbDirectionXTuple) - transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionX, obbDirectionX) - stats["obb_direction_ras_x"] = obbDirectionX - - if "obb_direction_ras_y" in requestedKeys: - obbOriginTuple = None - obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"]) - if obbOriginArray is None: - logging.error("Could not calculate obb_direction_ras_y!") - else: - obbOriginTuple = obbOriginArray.GetTuple(0) - - obbDirectionYTuple = None - obbDirectionYArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_y"]) - if obbDirectionYArray is None: - logging.error("Could not calculate obb_direction_ras_y!") - else: - obbDirectionYTuple = obbDirectionYArray.GetTuple(0) - - if obbOriginTuple is not None and obbDirectionYTuple is not None: - obbDirectionY = list(obbDirectionYTuple) - transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionY, obbDirectionY) - stats["obb_direction_ras_y"] = obbDirectionY - - if "obb_direction_ras_z" in requestedKeys: - obbOriginTuple = None - obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"]) - if obbOriginArray is None: - logging.error("Could not calculate obb_direction_ras_z!") - else: - obbOriginTuple = obbOriginArray.GetTuple(0) - - obbDirectionZTuple = None - obbDirectionZArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_z"]) - if obbDirectionZArray is None: - logging.error("Could not calculate obb_direction_ras_z!") - else: - obbDirectionZTuple = obbDirectionZArray.GetTuple(0) - - if obbOriginTuple is not None and obbDirectionZTuple is not None: - obbDirectionZ = list(obbDirectionZTuple) - transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionZ, obbDirectionZ) - stats["obb_direction_ras_z"] = obbDirectionZ - - if "principal_moments" in requestedKeys: - principalMomentsTuple = None - principalMomentsArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_moments"]) - if principalMomentsArray is None: - logging.error("Could not calculate principal_moments!") - else: - principalMomentsTuple = principalMomentsArray.GetTuple(0) - if principalMomentsTuple is not None: - principalMoments = list(principalMomentsTuple) - stats["principal_moments"] = principalMoments - - if "principal_axis_x" in requestedKeys: - centroidRASTuple = None - centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"]) - if centroidRASArray is None: - logging.error("Could not calculate principal_axis_x!") - else: - centroidRASTuple = centroidRASArray.GetTuple(0) - - principalAxisXTuple = None - principalAxisXArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_x"]) - if principalAxisXArray is None: - logging.error("Could not calculate principal_axis_x!") - else: - principalAxisXTuple = principalAxisXArray.GetTuple(0) - - if centroidRASTuple is not None and principalAxisXTuple is not None: - principalAxisX = list(principalAxisXTuple) - transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisX, principalAxisX) - stats["principal_axis_x"] = principalAxisX - - if "principal_axis_y" in requestedKeys: - centroidRASTuple = None - centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"]) - if centroidRASArray is None: - logging.error("Could not calculate principal_axis_y!") - else: - centroidRASTuple = centroidRASArray.GetTuple(0) - - principalAxisYTuple = None - principalAxisYArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_y"]) - if principalAxisYArray is None: - logging.error("Could not calculate principal_axis_y!") - else: - principalAxisYTuple = principalAxisYArray.GetTuple(0) - - if centroidRASTuple is not None and principalAxisYTuple is not None: - principalAxisY = list(principalAxisYTuple) - transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisY, principalAxisY) - stats["principal_axis_y"] = principalAxisY - - if "principal_axis_z" in requestedKeys: - centroidRASTuple = None - centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"]) - if centroidRASArray is None: - logging.error("Could not calculate principal_axis_z!") - else: - centroidRASTuple = centroidRASArray.GetTuple(0) - - principalAxisZTuple = None - principalAxisZArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_z"]) - if principalAxisZArray is None: - logging.error("Could not calculate principal_axis_z!") - else: - principalAxisZTuple = principalAxisZArray.GetTuple(0) - - if centroidRASTuple is not None and principalAxisZTuple is not None: - principalAxisZ = list(principalAxisZTuple) - transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisZ, principalAxisZ) - stats["principal_axis_z"] = principalAxisZ - - return stats - - def getMeasurementInfo(self, key): - """Get information (name, description, units, ...) about the measurement for the given key""" - info = {} - - # @fedorov could not find any suitable DICOM quantity code for "number of voxels". - # DCM has "Number of needles" etc., so probably "Number of voxels" - # should be added too. Need to discuss with @dclunie. For now, a - # QIICR private scheme placeholder. - info["voxel_count"] = \ - self.createMeasurementInfo(name="Voxel count", description="Number of voxels", units="voxels", - quantityDicomCode=self.createCodedEntry("nvoxels", "99QIICR", "Number of voxels", True), - unitsDicomCode=self.createCodedEntry("voxels", "UCUM", "voxels", True)) - - info["volume_mm3"] = \ - self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3", - quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), - unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True)) - - info["volume_cm3"] = \ - self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3", - quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), - unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True), - measurementMethodDicomCode=self.createCodedEntry("126030", "DCM", - "Sum of segmented voxel volumes", True)) - - info["centroid_ras"] = \ - self.createMeasurementInfo(name="Centroid", description="Location of the centroid in RAS", units="", componentNames=["r", "a", "s"]) - - info["feret_diameter_mm"] = \ - self.createMeasurementInfo(name="Feret diameter mm", description="Feret diameter in mm", units="mm") - - info["surface_area_mm2"] = \ - self.createMeasurementInfo(name="Surface mm2", description="Surface area in mm2", units="mm2", - quantityDicomCode=self.createCodedEntry("000247", "99CHEMINF", "Surface area", True), - unitsDicomCode=self.createCodedEntry("mm2", "UCUM", "squared millimeters", True)) - - info["roundness"] = \ - self.createMeasurementInfo(name="Roundness", - description="Segment roundness. Calculated from ratio of the area of the hypersphere by the actual area. Value of 1 represents a spherical structure", units="") - - info["flatness"] = \ - self.createMeasurementInfo(name="Flatness", - description="Segment flatness. Calculated from square root of the ratio of the second smallest principal moment by the smallest. Value of 0 represents a flat structure." + - " ( https://hdl.handle.net/1926/584 )", - units="") - - info["elongation"] = \ - self.createMeasurementInfo(name="Elongation", - description="Segment elongation. Calculated from square root of the ratio of the second largest principal moment by the second smallest. ( https://hdl.handle.net/1926/584 )", - units="") - - info["oriented_bounding_box"] = \ - self.createMeasurementInfo(name="Oriented bounding box", description="Oriented bounding box", units="") - - info["obb_origin_ras"] = \ - self.createMeasurementInfo(name="OBB origin", description="Oriented bounding box origin in RAS coordinates", units="", componentNames=["r", "a", "s"]) - - info["obb_diameter_mm"] = \ - self.createMeasurementInfo(name="OBB diameter", description="Oriented bounding box diameter in mm", units="mm", componentNames=["x", "y", "z"]) - - info["obb_direction_ras_x"] = \ - self.createMeasurementInfo(name="OBB X direction", description="Oriented bounding box X direction in RAS coordinates", units="", componentNames=["r", "a", "s"]) - - info["obb_direction_ras_y"] = \ - self.createMeasurementInfo(name="OBB Y direction", description="Oriented bounding box Y direction in RAS coordinates", units="", componentNames=["r", "a", "s"]) - - info["obb_direction_ras_z"] = \ - self.createMeasurementInfo(name="OBB Z direction", description="Oriented bounding box Z direction in RAS coordinates", units="", componentNames=["r", "a", "s"]) - - info["principal_moments"] = \ - self.createMeasurementInfo(name="Principal moments", description="Principal moments of inertia for x, y and z axes", - units="", componentNames=["x", "y", "z"]) - - info["principal_axis_x"] = \ - self.createMeasurementInfo(name="Principal X axis", description="Principal X axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"]) - - info["principal_axis_y"] = \ - self.createMeasurementInfo(name="Principal Y axis", description="Principal Y axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"]) - - info["principal_axis_z"] = \ - self.createMeasurementInfo(name="Principal Z axis", description="Principal Z axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"]) - - return info[key] if key in info else None + """Statistical plugin for Labelmaps""" + + def __init__(self): + super().__init__() + self.name = "Labelmap" + self.obbKeys = ["obb_origin_ras", "obb_diameter_mm", "obb_direction_ras_x", "obb_direction_ras_y", "obb_direction_ras_z"] + self.principalAxisKeys = ["principal_axis_x", "principal_axis_y", "principal_axis_z"] + self.shapeKeys = [ + "centroid_ras", "feret_diameter_mm", "surface_area_mm2", "roundness", "flatness", "elongation", + "principal_moments", + ] + self.principalAxisKeys + self.obbKeys + + self.defaultKeys = ["voxel_count", "volume_mm3", "volume_cm3"] # Don't calculate label shape statistics by default since they take longer to compute + self.keys = self.defaultKeys + self.shapeKeys + self.keyToShapeStatisticNames = { + "centroid_ras": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Centroid), + "feret_diameter_mm": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.FeretDiameter), + "surface_area_mm2": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Perimeter), + "roundness": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Roundness), + "flatness": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Flatness), + "elongation": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Elongation), + "oriented_bounding_box": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.OrientedBoundingBox), + "obb_origin_ras": "OrientedBoundingBoxOrigin", + "obb_diameter_mm": "OrientedBoundingBoxSize", + "obb_direction_ras_x": "OrientedBoundingBoxDirectionX", + "obb_direction_ras_y": "OrientedBoundingBoxDirectionY", + "obb_direction_ras_z": "OrientedBoundingBoxDirectionZ", + "principal_moments": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.PrincipalMoments), + "principal_axes": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.PrincipalAxes), + "principal_axis_x": "PrincipalAxisX", + "principal_axis_y": "PrincipalAxisY", + "principal_axis_z": "PrincipalAxisZ", + } + # ... developer may add extra options to configure other parameters + + def computeStatistics(self, segmentID): + import vtkSegmentationCorePython as vtkSegmentationCore + requestedKeys = self.getRequestedKeys() + + segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) + + if len(requestedKeys) == 0: + return {} + + containsLabelmapRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation( + vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()) + if not containsLabelmapRepresentation: + return {} + + segmentLabelmap = slicer.vtkOrientedImageData() + segmentationNode.GetBinaryLabelmapRepresentation(segmentID, segmentLabelmap) + if (not segmentLabelmap + or not segmentLabelmap.GetPointData() + or not segmentLabelmap.GetPointData().GetScalars()): + # No input label data + return {} + + # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value + labelValue = 1 + backgroundValue = 0 + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(segmentLabelmap) + thresh.ThresholdByLower(0) + thresh.SetInValue(backgroundValue) + thresh.SetOutValue(labelValue) + thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) + thresh.Update() + + # Use binary labelmap as a stencil + stencil = vtk.vtkImageToImageStencil() + stencil.SetInputData(thresh.GetOutput()) + stencil.ThresholdByUpper(labelValue) + stencil.Update() + + stat = vtk.vtkImageAccumulate() + stat.SetInputData(thresh.GetOutput()) + stat.SetStencilData(stencil.GetOutput()) + stat.Update() + + # Add data to statistics list + cubicMMPerVoxel = reduce(lambda x, y: x * y, segmentLabelmap.GetSpacing()) + ccPerCubicMM = 0.001 + stats = {} + if "voxel_count" in requestedKeys: + stats["voxel_count"] = stat.GetVoxelCount() + if "volume_mm3" in requestedKeys: + stats["volume_mm3"] = stat.GetVoxelCount() * cubicMMPerVoxel + if "volume_cm3" in requestedKeys: + stats["volume_cm3"] = stat.GetVoxelCount() * cubicMMPerVoxel * ccPerCubicMM + + calculateShapeStats = False + for shapeKey in self.shapeKeys: + if shapeKey in requestedKeys: + calculateShapeStats = True + break + + if calculateShapeStats: + directions = vtk.vtkMatrix4x4() + segmentLabelmap.GetDirectionMatrix(directions) + + # Remove oriented bounding box from requested keys and replace with individual keys + requestedOptions = requestedKeys + statFilterOptions = self.shapeKeys + calculateOBB = ( + "obb_diameter_mm" in requestedKeys or + "obb_origin_ras" in requestedKeys or + "obb_direction_ras_x" in requestedKeys or + "obb_direction_ras_y" in requestedKeys or + "obb_direction_ras_z" in requestedKeys + ) + + if calculateOBB: + temp = statFilterOptions + statFilterOptions = [] + for option in temp: + if option not in self.obbKeys: + statFilterOptions.append(option) + statFilterOptions.append("oriented_bounding_box") + + temp = requestedOptions + requestedOptions = [] + for option in temp: + if option not in self.obbKeys: + requestedOptions.append(option) + requestedOptions.append("oriented_bounding_box") + + calculatePrincipalAxis = ( + "principal_axis_x" in requestedKeys or + "principal_axis_y" in requestedKeys or + "principal_axis_z" in requestedKeys + ) + if calculatePrincipalAxis: + temp = statFilterOptions + statFilterOptions = [] + for option in temp: + if option not in self.principalAxisKeys: + statFilterOptions.append(option) + statFilterOptions.append("principal_axes") + + temp = requestedOptions + requestedOptions = [] + for option in temp: + if option not in self.principalAxisKeys: + requestedOptions.append(option) + requestedOptions.append("principal_axes") + requestedOptions.append("centroid_ras") + + shapeStat = vtkITK.vtkITKLabelShapeStatistics() + shapeStat.SetInputData(thresh.GetOutput()) + shapeStat.SetDirections(directions) + for shapeKey in statFilterOptions: + shapeStat.SetComputeShapeStatistic(self.keyToShapeStatisticNames[shapeKey], shapeKey in requestedOptions) + shapeStat.Update() + + # If segmentation node is transformed, apply that transform to get RAS coordinates + transformSegmentToRas = vtk.vtkGeneralTransform() + slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(segmentationNode.GetParentTransformNode(), None, transformSegmentToRas) + + statTable = shapeStat.GetOutput() + if "centroid_ras" in requestedKeys: + centroidRAS = [0, 0, 0] + centroidTuple = None + centroidArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"]) + if centroidArray is None: + logging.error("Could not calculate centroid_ras!") + else: + centroidTuple = centroidArray.GetTuple(0) + if centroidTuple is not None: + transformSegmentToRas.TransformPoint(centroidTuple, centroidRAS) + stats["centroid_ras"] = centroidRAS + + if "roundness" in requestedKeys: + roundnessTuple = None + roundnessArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["roundness"]) + if roundnessArray is None: + logging.error("Could not calculate roundness!") + else: + roundnessTuple = roundnessArray.GetTuple(0) + if roundnessTuple is not None: + roundness = roundnessTuple[0] + stats["roundness"] = roundness + + if "flatness" in requestedKeys: + flatnessTuple = None + flatnessArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["flatness"]) + if flatnessArray is None: + logging.error("Could not calculate flatness!") + else: + flatnessTuple = flatnessArray.GetTuple(0) + if flatnessTuple is not None: + flatness = flatnessTuple[0] + stats["flatness"] = flatness + + if "elongation" in requestedKeys: + elongationTuple = None + elongationArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["elongation"]) + if elongationArray is None: + logging.error("Could not calculate elongation!") + else: + elongationTuple = elongationArray.GetTuple(0) + if elongationTuple is not None: + elongation = elongationTuple[0] + stats["elongation"] = elongation + + if "feret_diameter_mm" in requestedKeys: + feretDiameterTuple = None + feretDiameterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["feret_diameter_mm"]) + if feretDiameterArray is None: + logging.error("Could not calculate feret_diameter_mm!") + else: + feretDiameterTuple = feretDiameterArray.GetTuple(0) + if feretDiameterTuple is not None: + feretDiameter = feretDiameterTuple[0] + stats["feret_diameter_mm"] = feretDiameter + + if "surface_area_mm2" in requestedKeys: + perimeterTuple = None + perimeterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["surface_area_mm2"]) + if perimeterArray is None: + logging.error("Could not calculate surface_area_mm2!") + else: + perimeterTuple = perimeterArray.GetTuple(0) + if perimeterTuple is not None: + perimeter = perimeterTuple[0] + stats["surface_area_mm2"] = perimeter + + if "obb_origin_ras" in requestedKeys: + obbOriginTuple = None + obbOriginRAS = [0, 0, 0] + obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"]) + if obbOriginArray is None: + logging.error("Could not calculate obb_origin_ras!") + else: + obbOriginTuple = obbOriginArray.GetTuple(0) + if obbOriginTuple is not None: + transformSegmentToRas.TransformPoint(obbOriginTuple, obbOriginRAS) + stats["obb_origin_ras"] = obbOriginRAS + + if "obb_diameter_mm" in requestedKeys: + obbDiameterMMTuple = None + obbDiameterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_diameter_mm"]) + if obbDiameterArray is None: + logging.error("Could not calculate obb_diameter_mm!") + else: + obbDiameterMMTuple = obbDiameterArray.GetTuple(0) + if obbDiameterMMTuple is not None: + obbDiameterMM = list(obbDiameterMMTuple) + stats["obb_diameter_mm"] = obbDiameterMM + + if "obb_direction_ras_x" in requestedKeys: + obbOriginTuple = None + obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"]) + if obbOriginArray is None: + logging.error("Could not calculate obb_direction_ras_x!") + else: + obbOriginTuple = obbOriginArray.GetTuple(0) + + obbDirectionXTuple = None + obbDirectionXArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_x"]) + if obbDirectionXArray is None: + logging.error("Could not calculate obb_direction_ras_x!") + else: + obbDirectionXTuple = obbDirectionXArray.GetTuple(0) + + if obbOriginTuple is not None and obbDirectionXTuple is not None: + obbDirectionX = list(obbDirectionXTuple) + transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionX, obbDirectionX) + stats["obb_direction_ras_x"] = obbDirectionX + + if "obb_direction_ras_y" in requestedKeys: + obbOriginTuple = None + obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"]) + if obbOriginArray is None: + logging.error("Could not calculate obb_direction_ras_y!") + else: + obbOriginTuple = obbOriginArray.GetTuple(0) + + obbDirectionYTuple = None + obbDirectionYArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_y"]) + if obbDirectionYArray is None: + logging.error("Could not calculate obb_direction_ras_y!") + else: + obbDirectionYTuple = obbDirectionYArray.GetTuple(0) + + if obbOriginTuple is not None and obbDirectionYTuple is not None: + obbDirectionY = list(obbDirectionYTuple) + transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionY, obbDirectionY) + stats["obb_direction_ras_y"] = obbDirectionY + + if "obb_direction_ras_z" in requestedKeys: + obbOriginTuple = None + obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"]) + if obbOriginArray is None: + logging.error("Could not calculate obb_direction_ras_z!") + else: + obbOriginTuple = obbOriginArray.GetTuple(0) + + obbDirectionZTuple = None + obbDirectionZArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_z"]) + if obbDirectionZArray is None: + logging.error("Could not calculate obb_direction_ras_z!") + else: + obbDirectionZTuple = obbDirectionZArray.GetTuple(0) + + if obbOriginTuple is not None and obbDirectionZTuple is not None: + obbDirectionZ = list(obbDirectionZTuple) + transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionZ, obbDirectionZ) + stats["obb_direction_ras_z"] = obbDirectionZ + + if "principal_moments" in requestedKeys: + principalMomentsTuple = None + principalMomentsArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_moments"]) + if principalMomentsArray is None: + logging.error("Could not calculate principal_moments!") + else: + principalMomentsTuple = principalMomentsArray.GetTuple(0) + if principalMomentsTuple is not None: + principalMoments = list(principalMomentsTuple) + stats["principal_moments"] = principalMoments + + if "principal_axis_x" in requestedKeys: + centroidRASTuple = None + centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"]) + if centroidRASArray is None: + logging.error("Could not calculate principal_axis_x!") + else: + centroidRASTuple = centroidRASArray.GetTuple(0) + + principalAxisXTuple = None + principalAxisXArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_x"]) + if principalAxisXArray is None: + logging.error("Could not calculate principal_axis_x!") + else: + principalAxisXTuple = principalAxisXArray.GetTuple(0) + + if centroidRASTuple is not None and principalAxisXTuple is not None: + principalAxisX = list(principalAxisXTuple) + transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisX, principalAxisX) + stats["principal_axis_x"] = principalAxisX + + if "principal_axis_y" in requestedKeys: + centroidRASTuple = None + centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"]) + if centroidRASArray is None: + logging.error("Could not calculate principal_axis_y!") + else: + centroidRASTuple = centroidRASArray.GetTuple(0) + + principalAxisYTuple = None + principalAxisYArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_y"]) + if principalAxisYArray is None: + logging.error("Could not calculate principal_axis_y!") + else: + principalAxisYTuple = principalAxisYArray.GetTuple(0) + + if centroidRASTuple is not None and principalAxisYTuple is not None: + principalAxisY = list(principalAxisYTuple) + transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisY, principalAxisY) + stats["principal_axis_y"] = principalAxisY + + if "principal_axis_z" in requestedKeys: + centroidRASTuple = None + centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"]) + if centroidRASArray is None: + logging.error("Could not calculate principal_axis_z!") + else: + centroidRASTuple = centroidRASArray.GetTuple(0) + + principalAxisZTuple = None + principalAxisZArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_z"]) + if principalAxisZArray is None: + logging.error("Could not calculate principal_axis_z!") + else: + principalAxisZTuple = principalAxisZArray.GetTuple(0) + + if centroidRASTuple is not None and principalAxisZTuple is not None: + principalAxisZ = list(principalAxisZTuple) + transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisZ, principalAxisZ) + stats["principal_axis_z"] = principalAxisZ + + return stats + + def getMeasurementInfo(self, key): + """Get information (name, description, units, ...) about the measurement for the given key""" + info = {} + + # @fedorov could not find any suitable DICOM quantity code for "number of voxels". + # DCM has "Number of needles" etc., so probably "Number of voxels" + # should be added too. Need to discuss with @dclunie. For now, a + # QIICR private scheme placeholder. + info["voxel_count"] = \ + self.createMeasurementInfo(name="Voxel count", description="Number of voxels", units="voxels", + quantityDicomCode=self.createCodedEntry("nvoxels", "99QIICR", "Number of voxels", True), + unitsDicomCode=self.createCodedEntry("voxels", "UCUM", "voxels", True)) + + info["volume_mm3"] = \ + self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3", + quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), + unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True)) + + info["volume_cm3"] = \ + self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3", + quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), + unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True), + measurementMethodDicomCode=self.createCodedEntry("126030", "DCM", + "Sum of segmented voxel volumes", True)) + + info["centroid_ras"] = \ + self.createMeasurementInfo(name="Centroid", description="Location of the centroid in RAS", units="", componentNames=["r", "a", "s"]) + + info["feret_diameter_mm"] = \ + self.createMeasurementInfo(name="Feret diameter mm", description="Feret diameter in mm", units="mm") + + info["surface_area_mm2"] = \ + self.createMeasurementInfo(name="Surface mm2", description="Surface area in mm2", units="mm2", + quantityDicomCode=self.createCodedEntry("000247", "99CHEMINF", "Surface area", True), + unitsDicomCode=self.createCodedEntry("mm2", "UCUM", "squared millimeters", True)) + + info["roundness"] = \ + self.createMeasurementInfo(name="Roundness", + description="Segment roundness. Calculated from ratio of the area of the hypersphere by the actual area. Value of 1 represents a spherical structure", units="") + + info["flatness"] = \ + self.createMeasurementInfo(name="Flatness", + description="Segment flatness. Calculated from square root of the ratio of the second smallest principal moment by the smallest. Value of 0 represents a flat structure." + + " ( https://hdl.handle.net/1926/584 )", + units="") + + info["elongation"] = \ + self.createMeasurementInfo(name="Elongation", + description="Segment elongation. Calculated from square root of the ratio of the second largest principal moment by the second smallest. ( https://hdl.handle.net/1926/584 )", + units="") + + info["oriented_bounding_box"] = \ + self.createMeasurementInfo(name="Oriented bounding box", description="Oriented bounding box", units="") + + info["obb_origin_ras"] = \ + self.createMeasurementInfo(name="OBB origin", description="Oriented bounding box origin in RAS coordinates", units="", componentNames=["r", "a", "s"]) + + info["obb_diameter_mm"] = \ + self.createMeasurementInfo(name="OBB diameter", description="Oriented bounding box diameter in mm", units="mm", componentNames=["x", "y", "z"]) + + info["obb_direction_ras_x"] = \ + self.createMeasurementInfo(name="OBB X direction", description="Oriented bounding box X direction in RAS coordinates", units="", componentNames=["r", "a", "s"]) + + info["obb_direction_ras_y"] = \ + self.createMeasurementInfo(name="OBB Y direction", description="Oriented bounding box Y direction in RAS coordinates", units="", componentNames=["r", "a", "s"]) + + info["obb_direction_ras_z"] = \ + self.createMeasurementInfo(name="OBB Z direction", description="Oriented bounding box Z direction in RAS coordinates", units="", componentNames=["r", "a", "s"]) + + info["principal_moments"] = \ + self.createMeasurementInfo(name="Principal moments", description="Principal moments of inertia for x, y and z axes", + units="", componentNames=["x", "y", "z"]) + + info["principal_axis_x"] = \ + self.createMeasurementInfo(name="Principal X axis", description="Principal X axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"]) + + info["principal_axis_y"] = \ + self.createMeasurementInfo(name="Principal Y axis", description="Principal Y axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"]) + + info["principal_axis_z"] = \ + self.createMeasurementInfo(name="Principal Z axis", description="Principal Z axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"]) + + return info[key] if key in info else None diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ScalarVolumeSegmentStatisticsPlugin.py b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ScalarVolumeSegmentStatisticsPlugin.py index 7f3beb142b7..e5b44d71751 100644 --- a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ScalarVolumeSegmentStatisticsPlugin.py +++ b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ScalarVolumeSegmentStatisticsPlugin.py @@ -4,194 +4,194 @@ class ScalarVolumeSegmentStatisticsPlugin(SegmentStatisticsPluginBase): - """Statistical plugin for segmentations with scalar volumes""" - - def __init__(self): - super().__init__() - self.name = "Scalar Volume" - self.keys = ["voxel_count", "volume_mm3", "volume_cm3", "min", "max", "mean", "median", "stdev"] - self.defaultKeys = self.keys # calculate all measurements by default - #... developer may add extra options to configure other parameters - - def computeStatistics(self, segmentID): - requestedKeys = self.getRequestedKeys() - - segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) - grayscaleNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("ScalarVolume")) - - if len(requestedKeys)==0: - return {} - - stencil = self.getStencilForVolume(segmentationNode, segmentID, grayscaleNode) - if not stencil: - return {} - - cubicMMPerVoxel = reduce(lambda x,y: x*y, grayscaleNode.GetSpacing()) - ccPerCubicMM = 0.001 - - stat = vtk.vtkImageAccumulate() - stat.SetInputData(grayscaleNode.GetImageData()) - stat.SetStencilData(stencil.GetOutput()) - stat.Update() - - medians = vtk.vtkImageHistogramStatistics() - medians.SetInputData(grayscaleNode.GetImageData()) - medians.SetStencilData(stencil.GetOutput()) - medians.Update() - - # create statistics list - stats = {} - if "voxel_count" in requestedKeys: - stats["voxel_count"] = stat.GetVoxelCount() - if "volume_mm3" in requestedKeys: - stats["volume_mm3"] = stat.GetVoxelCount() * cubicMMPerVoxel - if "volume_cm3" in requestedKeys: - stats["volume_cm3"] = stat.GetVoxelCount() * cubicMMPerVoxel * ccPerCubicMM - if stat.GetVoxelCount()>0: - if "min" in requestedKeys: - stats["min"] = stat.GetMin()[0] - if "max" in requestedKeys: - stats["max"] = stat.GetMax()[0] - if "mean" in requestedKeys: - stats["mean"] = stat.GetMean()[0] - if "stdev" in requestedKeys: - stats["stdev"] = stat.GetStandardDeviation()[0] - if "median" in requestedKeys: - stats["median"] = medians.GetMedian() - return stats - - def getStencilForVolume(self, segmentationNode, segmentID, grayscaleNode): - import vtkSegmentationCorePython as vtkSegmentationCore - - containsLabelmapRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation( - vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()) - if not containsLabelmapRepresentation: - return None - - if (not grayscaleNode - or not grayscaleNode.GetImageData() - or not grayscaleNode.GetImageData().GetPointData() - or not grayscaleNode.GetImageData().GetPointData().GetScalars()): - # Input grayscale node does not contain valid image data - return None - - # Get geometry of grayscale volume node as oriented image data - # reference geometry in reference node coordinate system - referenceGeometry_Reference = vtkSegmentationCore.vtkOrientedImageData() - referenceGeometry_Reference.SetExtent(grayscaleNode.GetImageData().GetExtent()) - ijkToRasMatrix = vtk.vtkMatrix4x4() - grayscaleNode.GetIJKToRASMatrix(ijkToRasMatrix) - referenceGeometry_Reference.SetGeometryFromImageToWorldMatrix(ijkToRasMatrix) - - # Get transform between grayscale volume and segmentation - segmentationToReferenceGeometryTransform = vtk.vtkGeneralTransform() - slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(segmentationNode.GetParentTransformNode(), - grayscaleNode.GetParentTransformNode(), segmentationToReferenceGeometryTransform) - - segmentLabelmap = vtkSegmentationCore.vtkOrientedImageData() - segmentationNode.GetBinaryLabelmapRepresentation(segmentID, segmentLabelmap) - if (not segmentLabelmap - or not segmentLabelmap.GetPointData() - or not segmentLabelmap.GetPointData().GetScalars()): - # No input label data - return None - - segmentLabelmap_Reference = vtkSegmentationCore.vtkOrientedImageData() - vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( - segmentLabelmap, referenceGeometry_Reference, segmentLabelmap_Reference, - False, # nearest neighbor interpolation - False, # no padding - segmentationToReferenceGeometryTransform) - - # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value - labelValue = 1 - backgroundValue = 0 - thresh = vtk.vtkImageThreshold() - thresh.SetInputData(segmentLabelmap_Reference) - thresh.ThresholdByLower(0) - thresh.SetInValue(backgroundValue) - thresh.SetOutValue(labelValue) - thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) - thresh.Update() - - # Use binary labelmap as a stencil - stencil = vtk.vtkImageToImageStencil() - stencil.SetInputData(thresh.GetOutput()) - stencil.ThresholdByUpper(labelValue) - stencil.Update() - - return stencil - - def getMeasurementInfo(self, key): - """Get information (name, description, units, ...) about the measurement for the given key""" - - scalarVolumeNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("ScalarVolume")) - - scalarVolumeQuantity = scalarVolumeNode.GetVoxelValueQuantity() if scalarVolumeNode else self.createCodedEntry("", "", "") - scalarVolumeUnits = scalarVolumeNode.GetVoxelValueUnits() if scalarVolumeNode else self.createCodedEntry("", "", "") - if not scalarVolumeQuantity: - scalarVolumeQuantity = self.createCodedEntry("", "", "") - if not scalarVolumeUnits: - scalarVolumeUnits = self.createCodedEntry("", "", "") - - info = dict() - - # @fedorov could not find any suitable DICOM quantity code for "number of voxels". - # DCM has "Number of needles" etc., so probably "Number of voxels" - # should be added too. Need to discuss with @dclunie. For now, a - # QIICR private scheme placeholder. - # @moselhy also could not find DICOM quantity code for "median" - - info["voxel_count"] = \ - self.createMeasurementInfo(name="Voxel count", description="Number of voxels", units="voxels", - quantityDicomCode=self.createCodedEntry("nvoxels", "99QIICR", "Number of voxels", True), - unitsDicomCode=self.createCodedEntry("voxels", "UCUM", "voxels", True)) - - info["volume_mm3"] = \ - self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3", - quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), - unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True)) - - info["volume_cm3"] = \ - self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3", - quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), - unitsDicomCode=self.createCodedEntry("cm3","UCUM","cubic centimeter", True), - measurementMethodDicomCode=self.createCodedEntry("126030", "DCM", - "Sum of segmented voxel volumes", True)) - - info["min"] = \ - self.createMeasurementInfo(name="Minimum", description="Minimum scalar value", - units=scalarVolumeUnits.GetCodeMeaning(), - quantityDicomCode=scalarVolumeQuantity.GetAsString(), - unitsDicomCode=scalarVolumeUnits.GetAsString(), - derivationDicomCode=self.createCodedEntry("255605001", "SCT", "Minimum", True)) - - info["max"] = \ - self.createMeasurementInfo(name="Maximum", description="Maximum scalar value", - units=scalarVolumeUnits.GetCodeMeaning(), - quantityDicomCode=scalarVolumeQuantity.GetAsString(), - unitsDicomCode=scalarVolumeUnits.GetAsString(), - derivationDicomCode=self.createCodedEntry("56851009","SCT","Maximum", True)) - - info["mean"] = \ - self.createMeasurementInfo(name="Mean", description="Mean scalar value", - units=scalarVolumeUnits.GetCodeMeaning(), - quantityDicomCode=scalarVolumeQuantity.GetAsString(), - unitsDicomCode=scalarVolumeUnits.GetAsString(), - derivationDicomCode=self.createCodedEntry("373098007","SCT","Mean", True)) - - info["median"] = \ - self.createMeasurementInfo(name="Median", description="Median scalar value", - units=scalarVolumeUnits.GetCodeMeaning(), - quantityDicomCode=scalarVolumeQuantity.GetAsString(), - unitsDicomCode=scalarVolumeUnits.GetAsString(), - derivationDicomCode=self.createCodedEntry("median","SCT","Median", True)) - - info["stdev"] = \ - self.createMeasurementInfo(name="Standard deviation", description="Standard deviation of scalar values", - units=scalarVolumeUnits.GetCodeMeaning(), - quantityDicomCode=scalarVolumeQuantity.GetAsString(), - unitsDicomCode=scalarVolumeUnits.GetAsString(), - derivationDicomCode=self.createCodedEntry('386136009','SCT','Standard Deviation', True)) - - return info[key] if key in info else None + """Statistical plugin for segmentations with scalar volumes""" + + def __init__(self): + super().__init__() + self.name = "Scalar Volume" + self.keys = ["voxel_count", "volume_mm3", "volume_cm3", "min", "max", "mean", "median", "stdev"] + self.defaultKeys = self.keys # calculate all measurements by default + # ... developer may add extra options to configure other parameters + + def computeStatistics(self, segmentID): + requestedKeys = self.getRequestedKeys() + + segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation")) + grayscaleNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("ScalarVolume")) + + if len(requestedKeys) == 0: + return {} + + stencil = self.getStencilForVolume(segmentationNode, segmentID, grayscaleNode) + if not stencil: + return {} + + cubicMMPerVoxel = reduce(lambda x, y: x * y, grayscaleNode.GetSpacing()) + ccPerCubicMM = 0.001 + + stat = vtk.vtkImageAccumulate() + stat.SetInputData(grayscaleNode.GetImageData()) + stat.SetStencilData(stencil.GetOutput()) + stat.Update() + + medians = vtk.vtkImageHistogramStatistics() + medians.SetInputData(grayscaleNode.GetImageData()) + medians.SetStencilData(stencil.GetOutput()) + medians.Update() + + # create statistics list + stats = {} + if "voxel_count" in requestedKeys: + stats["voxel_count"] = stat.GetVoxelCount() + if "volume_mm3" in requestedKeys: + stats["volume_mm3"] = stat.GetVoxelCount() * cubicMMPerVoxel + if "volume_cm3" in requestedKeys: + stats["volume_cm3"] = stat.GetVoxelCount() * cubicMMPerVoxel * ccPerCubicMM + if stat.GetVoxelCount() > 0: + if "min" in requestedKeys: + stats["min"] = stat.GetMin()[0] + if "max" in requestedKeys: + stats["max"] = stat.GetMax()[0] + if "mean" in requestedKeys: + stats["mean"] = stat.GetMean()[0] + if "stdev" in requestedKeys: + stats["stdev"] = stat.GetStandardDeviation()[0] + if "median" in requestedKeys: + stats["median"] = medians.GetMedian() + return stats + + def getStencilForVolume(self, segmentationNode, segmentID, grayscaleNode): + import vtkSegmentationCorePython as vtkSegmentationCore + + containsLabelmapRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation( + vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()) + if not containsLabelmapRepresentation: + return None + + if (not grayscaleNode + or not grayscaleNode.GetImageData() + or not grayscaleNode.GetImageData().GetPointData() + or not grayscaleNode.GetImageData().GetPointData().GetScalars()): + # Input grayscale node does not contain valid image data + return None + + # Get geometry of grayscale volume node as oriented image data + # reference geometry in reference node coordinate system + referenceGeometry_Reference = vtkSegmentationCore.vtkOrientedImageData() + referenceGeometry_Reference.SetExtent(grayscaleNode.GetImageData().GetExtent()) + ijkToRasMatrix = vtk.vtkMatrix4x4() + grayscaleNode.GetIJKToRASMatrix(ijkToRasMatrix) + referenceGeometry_Reference.SetGeometryFromImageToWorldMatrix(ijkToRasMatrix) + + # Get transform between grayscale volume and segmentation + segmentationToReferenceGeometryTransform = vtk.vtkGeneralTransform() + slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(segmentationNode.GetParentTransformNode(), + grayscaleNode.GetParentTransformNode(), segmentationToReferenceGeometryTransform) + + segmentLabelmap = vtkSegmentationCore.vtkOrientedImageData() + segmentationNode.GetBinaryLabelmapRepresentation(segmentID, segmentLabelmap) + if (not segmentLabelmap + or not segmentLabelmap.GetPointData() + or not segmentLabelmap.GetPointData().GetScalars()): + # No input label data + return None + + segmentLabelmap_Reference = vtkSegmentationCore.vtkOrientedImageData() + vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage( + segmentLabelmap, referenceGeometry_Reference, segmentLabelmap_Reference, + False, # nearest neighbor interpolation + False, # no padding + segmentationToReferenceGeometryTransform) + + # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value + labelValue = 1 + backgroundValue = 0 + thresh = vtk.vtkImageThreshold() + thresh.SetInputData(segmentLabelmap_Reference) + thresh.ThresholdByLower(0) + thresh.SetInValue(backgroundValue) + thresh.SetOutValue(labelValue) + thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR) + thresh.Update() + + # Use binary labelmap as a stencil + stencil = vtk.vtkImageToImageStencil() + stencil.SetInputData(thresh.GetOutput()) + stencil.ThresholdByUpper(labelValue) + stencil.Update() + + return stencil + + def getMeasurementInfo(self, key): + """Get information (name, description, units, ...) about the measurement for the given key""" + + scalarVolumeNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("ScalarVolume")) + + scalarVolumeQuantity = scalarVolumeNode.GetVoxelValueQuantity() if scalarVolumeNode else self.createCodedEntry("", "", "") + scalarVolumeUnits = scalarVolumeNode.GetVoxelValueUnits() if scalarVolumeNode else self.createCodedEntry("", "", "") + if not scalarVolumeQuantity: + scalarVolumeQuantity = self.createCodedEntry("", "", "") + if not scalarVolumeUnits: + scalarVolumeUnits = self.createCodedEntry("", "", "") + + info = dict() + + # @fedorov could not find any suitable DICOM quantity code for "number of voxels". + # DCM has "Number of needles" etc., so probably "Number of voxels" + # should be added too. Need to discuss with @dclunie. For now, a + # QIICR private scheme placeholder. + # @moselhy also could not find DICOM quantity code for "median" + + info["voxel_count"] = \ + self.createMeasurementInfo(name="Voxel count", description="Number of voxels", units="voxels", + quantityDicomCode=self.createCodedEntry("nvoxels", "99QIICR", "Number of voxels", True), + unitsDicomCode=self.createCodedEntry("voxels", "UCUM", "voxels", True)) + + info["volume_mm3"] = \ + self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3", + quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), + unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True)) + + info["volume_cm3"] = \ + self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3", + quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True), + unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True), + measurementMethodDicomCode=self.createCodedEntry("126030", "DCM", + "Sum of segmented voxel volumes", True)) + + info["min"] = \ + self.createMeasurementInfo(name="Minimum", description="Minimum scalar value", + units=scalarVolumeUnits.GetCodeMeaning(), + quantityDicomCode=scalarVolumeQuantity.GetAsString(), + unitsDicomCode=scalarVolumeUnits.GetAsString(), + derivationDicomCode=self.createCodedEntry("255605001", "SCT", "Minimum", True)) + + info["max"] = \ + self.createMeasurementInfo(name="Maximum", description="Maximum scalar value", + units=scalarVolumeUnits.GetCodeMeaning(), + quantityDicomCode=scalarVolumeQuantity.GetAsString(), + unitsDicomCode=scalarVolumeUnits.GetAsString(), + derivationDicomCode=self.createCodedEntry("56851009", "SCT", "Maximum", True)) + + info["mean"] = \ + self.createMeasurementInfo(name="Mean", description="Mean scalar value", + units=scalarVolumeUnits.GetCodeMeaning(), + quantityDicomCode=scalarVolumeQuantity.GetAsString(), + unitsDicomCode=scalarVolumeUnits.GetAsString(), + derivationDicomCode=self.createCodedEntry("373098007", "SCT", "Mean", True)) + + info["median"] = \ + self.createMeasurementInfo(name="Median", description="Median scalar value", + units=scalarVolumeUnits.GetCodeMeaning(), + quantityDicomCode=scalarVolumeQuantity.GetAsString(), + unitsDicomCode=scalarVolumeUnits.GetAsString(), + derivationDicomCode=self.createCodedEntry("median", "SCT", "Median", True)) + + info["stdev"] = \ + self.createMeasurementInfo(name="Standard deviation", description="Standard deviation of scalar values", + units=scalarVolumeUnits.GetCodeMeaning(), + quantityDicomCode=scalarVolumeQuantity.GetAsString(), + unitsDicomCode=scalarVolumeUnits.GetAsString(), + derivationDicomCode=self.createCodedEntry('386136009', 'SCT', 'Standard Deviation', True)) + + return info[key] if key in info else None diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/SegmentStatisticsPluginBase.py b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/SegmentStatisticsPluginBase.py index 7be81e1a58e..15b9f7c4e52 100644 --- a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/SegmentStatisticsPluginBase.py +++ b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/SegmentStatisticsPluginBase.py @@ -4,212 +4,215 @@ class SegmentStatisticsPluginBase: - """Base class for statistics plugins operating on segments. - Derived classes should specify: self.name, self.keys, self.defaultKeys - and implement: computeStatistics, getMeasurementInfo - """ - - @staticmethod - def createCodedEntry(codeValue, codingScheme, codeMeaning, returnAsString=False): - """Create a coded entry and return as string or vtkCodedEntry""" - entry = slicer.vtkCodedEntry() - entry.SetValueSchemeMeaning(codeValue, codingScheme, codeMeaning) - return entry if not returnAsString else entry.GetAsString() - - @staticmethod - def createMeasurementInfo(name, description, units, quantityDicomCode=None, unitsDicomCode=None, - measurementMethodDicomCode=None, derivationDicomCode=None, componentNames=None): - """Utility method to create measurement information""" - info = { - "name": name, - "description": description, - "units": units - } - if componentNames: - info["componentNames"] = componentNames - if quantityDicomCode: - info["DICOM.QuantityCode"] = quantityDicomCode - if unitsDicomCode: - info["DICOM.UnitsCode"] = unitsDicomCode - if measurementMethodDicomCode: - info["DICOM.MeasurementMethodCode"] = measurementMethodDicomCode - if derivationDicomCode: - info["DICOM.DerivationCode"] = derivationDicomCode - return info - - def __init__(self): - #: name of the statistics plugin - self.name = "" - #: keys for all supported measurements - self.keys = [] - #: measurements that will be calculated by default - self.defaultKeys = [] - self.requestedKeysCheckboxes = {} - self.parameterNode = None - self.parameterNodeObserver = None - - def __del__(self): - if self.parameterNode and self.parameterNodeObserver: - self.parameterNode.RemoveObserver(self.parameterNodeObserver) - - def computeStatistics(self, segmentID): - """Compute measurements for requested keys on the given segment and return - as dictionary mapping key's to measurement results + """Base class for statistics plugins operating on segments. + Derived classes should specify: self.name, self.keys, self.defaultKeys + and implement: computeStatistics, getMeasurementInfo """ - pass - def getMeasurementInfo(self, key): - """Get information (name, description, units, ...) about the measurement for the given key. - Utilize createMeasurementInfo() to create the dictionary containing the measurement information. - Measurement information should contain at least name, description, and units. - DICOM codes should be provided where possible. - """ - if key not in self.keys: - return None - return createMeasurementInfo(key, key, None) - - def setDefaultParameters(self, parameterNode, overwriteExisting=False): - # enable plugin - pluginName = self.__class__.__name__ - parameter = pluginName+'.enabled' - if not parameterNode.GetParameter(parameter): - parameterNode.SetParameter(parameter, str(True)) - # enable all default keys - for key in self.keys: - parameter = self.toLongKey(key)+'.enabled' - if not parameterNode.GetParameter(parameter) or overwriteExisting: - parameterNode.SetParameter(parameter, str(key in self.defaultKeys)) - - def getRequestedKeys(self): - if not self.parameterNode: - return () - requestedKeys = [key for key in self.keys if self.parameterNode.GetParameter(self.toLongKey(key)+'.enabled')=='True'] - return requestedKeys - - def toLongKey(self, key): - # add name of plugin as a prefix for use outside of plugin - pluginName = self.__class__.__name__ - return pluginName+'.'+key - - def toShortKey(self, key): - # remove prefix used outside of plugin - pluginName = self.__class__.__name__ - return key[len(pluginName)+1:] if key.startswith(pluginName+'.') else '' - - def setParameterNode(self, parameterNode): - if self.parameterNode==parameterNode: - return - if self.parameterNode and self.parameterNodeObserver: - self.parameterNode.RemoveObserver(self.parameterNodeObserver) - self.parameterNode = parameterNode - if self.parameterNode: - self.setDefaultParameters(self.parameterNode) - self.parameterNodeObserver = self.parameterNode.AddObserver(vtk.vtkCommand.ModifiedEvent, - self.updateGuiFromParameterNode) - self.createDefaultOptionsWidget() - self.updateGuiFromParameterNode() - - def getParameterNode(self): - return self.parameterNode - - def createDefaultOptionsWidget(self): - # create list of checkboxes that allow selection of requested keys - self.optionsWidget = qt.QWidget() - form = qt.QFormLayout(self.optionsWidget) - - # checkbox to enable/disable plugin - self.pluginCheckbox = qt.QCheckBox(self.name+" plugin enabled") - self.pluginCheckbox.checked = True - self.pluginCheckbox.connect('stateChanged(int)', self.updateParameterNodeFromGui) - form.addRow(self.pluginCheckbox) - - # select all/none/default buttons - selectAllNoneFrame = qt.QFrame(self.optionsWidget) - selectAllNoneFrame.setLayout(qt.QHBoxLayout()) - selectAllNoneFrame.layout().setSpacing(0) - selectAllNoneFrame.layout().setMargin(0) - selectAllNoneFrame.layout().addWidget(qt.QLabel("Select measurements: ",self.optionsWidget)) - selectAllButton = qt.QPushButton('all',self.optionsWidget) - selectAllNoneFrame.layout().addWidget(selectAllButton) - selectAllButton.connect('clicked()', self.requestAll) - selectNoneButton = qt.QPushButton('none',self.optionsWidget) - selectAllNoneFrame.layout().addWidget(selectNoneButton) - selectNoneButton.connect('clicked()', self.requestNone) - selectDefaultButton = qt.QPushButton('default',self.optionsWidget) - selectAllNoneFrame.layout().addWidget(selectDefaultButton) - selectDefaultButton.connect('clicked()', self.requestDefault) - form.addRow(selectAllNoneFrame) - - # checkboxes for individual keys - self.requestedKeysCheckboxes = {} - requestedKeys = self.getRequestedKeys() - for key in self.keys: - label = key - tooltip = "key: "+key - info = self.getMeasurementInfo(key) - if info and ("name" in info or "description" in info): - label = info["name"] if "name" in info else info["description"] - if "name" in info: tooltip += "\nname: " + str(info["name"]) - if "description" in info: tooltip += "\ndescription: " + str(info["description"]) - if "units" in info: tooltip += "\nunits: " + (str(info["units"]) if info["units"] else "n/a") - checkbox = qt.QCheckBox(label,self.optionsWidget) - checkbox.checked = key in requestedKeys - checkbox.setToolTip(tooltip) - form.addRow(checkbox) - self.requestedKeysCheckboxes[key] = checkbox - checkbox.connect('stateChanged(int)', self.updateParameterNodeFromGui) - - def updateGuiFromParameterNode(self, caller=None, event=None): - if not self.parameterNode: - return - pluginName = self.__class__.__name__ - isEnabled = self.parameterNode.GetParameter(pluginName+'.enabled')!='False' - self.pluginCheckbox.checked = isEnabled - for (key, checkbox) in self.requestedKeysCheckboxes.items(): - parameter = self.toLongKey(key)+'.enabled' - value = self.parameterNode.GetParameter(parameter)=='True' - if checkbox.checked!=value: - previousState = checkbox.blockSignals(True) - checkbox.checked = value - checkbox.blockSignals(previousState) - if checkbox.enabled!=isEnabled: - previousState = checkbox.blockSignals(True) - checkbox.enabled = isEnabled - checkbox.blockSignals(previousState) - - def updateParameterNodeFromGui(self): - if not self.parameterNode: - return - pluginName = self.__class__.__name__ - self.parameterNode.SetParameter(pluginName+'.enabled', str(self.pluginCheckbox.checked)) - for (key, checkbox) in self.requestedKeysCheckboxes.items(): - parameter = self.toLongKey(key)+'.enabled' - newValue = str(checkbox.checked) - currentValue = self.parameterNode.GetParameter(parameter) - if not currentValue or currentValue!=newValue: - self.parameterNode.SetParameter(parameter, newValue) - - def requestAll(self): - if not self.parameterNode: - return - for (key, checkbox) in self.requestedKeysCheckboxes.items(): - parameter = self.toLongKey(key)+'.enabled' - newValue = str(True) - currentValue = self.parameterNode.GetParameter(parameter) - if not currentValue or currentValue!=newValue: - self.parameterNode.SetParameter(parameter, newValue) - - def requestNone(self): - if not self.parameterNode: - return - for (key, checkbox) in self.requestedKeysCheckboxes.items(): - parameter = self.toLongKey(key)+'.enabled' - newValue = str(False) - currentValue = self.parameterNode.GetParameter(parameter) - if not currentValue or currentValue!=newValue: - self.parameterNode.SetParameter(parameter, newValue) - - def requestDefault(self): - if not self.parameterNode: - return - self.setDefaultParameters(self.parameterNode, overwriteExisting=True) + @staticmethod + def createCodedEntry(codeValue, codingScheme, codeMeaning, returnAsString=False): + """Create a coded entry and return as string or vtkCodedEntry""" + entry = slicer.vtkCodedEntry() + entry.SetValueSchemeMeaning(codeValue, codingScheme, codeMeaning) + return entry if not returnAsString else entry.GetAsString() + + @staticmethod + def createMeasurementInfo(name, description, units, quantityDicomCode=None, unitsDicomCode=None, + measurementMethodDicomCode=None, derivationDicomCode=None, componentNames=None): + """Utility method to create measurement information""" + info = { + "name": name, + "description": description, + "units": units + } + if componentNames: + info["componentNames"] = componentNames + if quantityDicomCode: + info["DICOM.QuantityCode"] = quantityDicomCode + if unitsDicomCode: + info["DICOM.UnitsCode"] = unitsDicomCode + if measurementMethodDicomCode: + info["DICOM.MeasurementMethodCode"] = measurementMethodDicomCode + if derivationDicomCode: + info["DICOM.DerivationCode"] = derivationDicomCode + return info + + def __init__(self): + #: name of the statistics plugin + self.name = "" + #: keys for all supported measurements + self.keys = [] + #: measurements that will be calculated by default + self.defaultKeys = [] + self.requestedKeysCheckboxes = {} + self.parameterNode = None + self.parameterNodeObserver = None + + def __del__(self): + if self.parameterNode and self.parameterNodeObserver: + self.parameterNode.RemoveObserver(self.parameterNodeObserver) + + def computeStatistics(self, segmentID): + """Compute measurements for requested keys on the given segment and return + as dictionary mapping key's to measurement results + """ + pass + + def getMeasurementInfo(self, key): + """Get information (name, description, units, ...) about the measurement for the given key. + Utilize createMeasurementInfo() to create the dictionary containing the measurement information. + Measurement information should contain at least name, description, and units. + DICOM codes should be provided where possible. + """ + if key not in self.keys: + return None + return createMeasurementInfo(key, key, None) + + def setDefaultParameters(self, parameterNode, overwriteExisting=False): + # enable plugin + pluginName = self.__class__.__name__ + parameter = pluginName + '.enabled' + if not parameterNode.GetParameter(parameter): + parameterNode.SetParameter(parameter, str(True)) + # enable all default keys + for key in self.keys: + parameter = self.toLongKey(key) + '.enabled' + if not parameterNode.GetParameter(parameter) or overwriteExisting: + parameterNode.SetParameter(parameter, str(key in self.defaultKeys)) + + def getRequestedKeys(self): + if not self.parameterNode: + return () + requestedKeys = [key for key in self.keys if self.parameterNode.GetParameter(self.toLongKey(key) + '.enabled') == 'True'] + return requestedKeys + + def toLongKey(self, key): + # add name of plugin as a prefix for use outside of plugin + pluginName = self.__class__.__name__ + return pluginName + '.' + key + + def toShortKey(self, key): + # remove prefix used outside of plugin + pluginName = self.__class__.__name__ + return key[len(pluginName) + 1:] if key.startswith(pluginName + '.') else '' + + def setParameterNode(self, parameterNode): + if self.parameterNode == parameterNode: + return + if self.parameterNode and self.parameterNodeObserver: + self.parameterNode.RemoveObserver(self.parameterNodeObserver) + self.parameterNode = parameterNode + if self.parameterNode: + self.setDefaultParameters(self.parameterNode) + self.parameterNodeObserver = self.parameterNode.AddObserver(vtk.vtkCommand.ModifiedEvent, + self.updateGuiFromParameterNode) + self.createDefaultOptionsWidget() + self.updateGuiFromParameterNode() + + def getParameterNode(self): + return self.parameterNode + + def createDefaultOptionsWidget(self): + # create list of checkboxes that allow selection of requested keys + self.optionsWidget = qt.QWidget() + form = qt.QFormLayout(self.optionsWidget) + + # checkbox to enable/disable plugin + self.pluginCheckbox = qt.QCheckBox(self.name + " plugin enabled") + self.pluginCheckbox.checked = True + self.pluginCheckbox.connect('stateChanged(int)', self.updateParameterNodeFromGui) + form.addRow(self.pluginCheckbox) + + # select all/none/default buttons + selectAllNoneFrame = qt.QFrame(self.optionsWidget) + selectAllNoneFrame.setLayout(qt.QHBoxLayout()) + selectAllNoneFrame.layout().setSpacing(0) + selectAllNoneFrame.layout().setMargin(0) + selectAllNoneFrame.layout().addWidget(qt.QLabel("Select measurements: ", self.optionsWidget)) + selectAllButton = qt.QPushButton('all', self.optionsWidget) + selectAllNoneFrame.layout().addWidget(selectAllButton) + selectAllButton.connect('clicked()', self.requestAll) + selectNoneButton = qt.QPushButton('none', self.optionsWidget) + selectAllNoneFrame.layout().addWidget(selectNoneButton) + selectNoneButton.connect('clicked()', self.requestNone) + selectDefaultButton = qt.QPushButton('default', self.optionsWidget) + selectAllNoneFrame.layout().addWidget(selectDefaultButton) + selectDefaultButton.connect('clicked()', self.requestDefault) + form.addRow(selectAllNoneFrame) + + # checkboxes for individual keys + self.requestedKeysCheckboxes = {} + requestedKeys = self.getRequestedKeys() + for key in self.keys: + label = key + tooltip = "key: " + key + info = self.getMeasurementInfo(key) + if info and ("name" in info or "description" in info): + label = info["name"] if "name" in info else info["description"] + if "name" in info: + tooltip += "\nname: " + str(info["name"]) + if "description" in info: + tooltip += "\ndescription: " + str(info["description"]) + if "units" in info: + tooltip += "\nunits: " + (str(info["units"]) if info["units"] else "n/a") + checkbox = qt.QCheckBox(label, self.optionsWidget) + checkbox.checked = key in requestedKeys + checkbox.setToolTip(tooltip) + form.addRow(checkbox) + self.requestedKeysCheckboxes[key] = checkbox + checkbox.connect('stateChanged(int)', self.updateParameterNodeFromGui) + + def updateGuiFromParameterNode(self, caller=None, event=None): + if not self.parameterNode: + return + pluginName = self.__class__.__name__ + isEnabled = self.parameterNode.GetParameter(pluginName + '.enabled') != 'False' + self.pluginCheckbox.checked = isEnabled + for (key, checkbox) in self.requestedKeysCheckboxes.items(): + parameter = self.toLongKey(key) + '.enabled' + value = self.parameterNode.GetParameter(parameter) == 'True' + if checkbox.checked != value: + previousState = checkbox.blockSignals(True) + checkbox.checked = value + checkbox.blockSignals(previousState) + if checkbox.enabled != isEnabled: + previousState = checkbox.blockSignals(True) + checkbox.enabled = isEnabled + checkbox.blockSignals(previousState) + + def updateParameterNodeFromGui(self): + if not self.parameterNode: + return + pluginName = self.__class__.__name__ + self.parameterNode.SetParameter(pluginName + '.enabled', str(self.pluginCheckbox.checked)) + for (key, checkbox) in self.requestedKeysCheckboxes.items(): + parameter = self.toLongKey(key) + '.enabled' + newValue = str(checkbox.checked) + currentValue = self.parameterNode.GetParameter(parameter) + if not currentValue or currentValue != newValue: + self.parameterNode.SetParameter(parameter, newValue) + + def requestAll(self): + if not self.parameterNode: + return + for (key, checkbox) in self.requestedKeysCheckboxes.items(): + parameter = self.toLongKey(key) + '.enabled' + newValue = str(True) + currentValue = self.parameterNode.GetParameter(parameter) + if not currentValue or currentValue != newValue: + self.parameterNode.SetParameter(parameter, newValue) + + def requestNone(self): + if not self.parameterNode: + return + for (key, checkbox) in self.requestedKeysCheckboxes.items(): + parameter = self.toLongKey(key) + '.enabled' + newValue = str(False) + currentValue = self.parameterNode.GetParameter(parameter) + if not currentValue or currentValue != newValue: + self.parameterNode.SetParameter(parameter, newValue) + + def requestDefault(self): + if not self.parameterNode: + return + self.setDefaultParameters(self.parameterNode, overwriteExisting=True) diff --git a/Modules/Scripted/SegmentStatistics/SubjectHierarchyPlugins/SegmentStatisticsSubjectHierarchyPlugin.py b/Modules/Scripted/SegmentStatistics/SubjectHierarchyPlugins/SegmentStatisticsSubjectHierarchyPlugin.py index 444b45b310f..4c4cbc7c0ea 100644 --- a/Modules/Scripted/SegmentStatistics/SubjectHierarchyPlugins/SegmentStatisticsSubjectHierarchyPlugin.py +++ b/Modules/Scripted/SegmentStatistics/SubjectHierarchyPlugins/SegmentStatisticsSubjectHierarchyPlugin.py @@ -8,124 +8,124 @@ class SegmentStatisticsSubjectHierarchyPlugin(AbstractScriptedSubjectHierarchyPlugin): - """ Scripted subject hierarchy plugin for the Segment Statistics module. - - This is also an example for scripted plugins, so includes all possible methods. - The methods that are not needed (i.e. the default implementation in - qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be - omitted in plugins created based on this one. - """ - - # Necessary static member to be able to set python source to scripted subject hierarchy plugin - filePath = __file__ - - def __init__(self, scriptedPlugin): - scriptedPlugin.name = 'SegmentStatistics' - AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin) - - self.segmentStatisticsAction = qt.QAction("Calculate statistics...", scriptedPlugin) - self.segmentStatisticsAction.connect("triggered()", self.onCalculateStatistics) - - def canAddNodeToSubjectHierarchy(self, node, parentItemID): - # This plugin cannot own any items (it's not a role but a function plugin), - # but the it can be decided the following way: - # if node is not None and node.IsA("vtkMRMLMyNode"): - # return 1.0 - return 0.0 - - def canOwnSubjectHierarchyItem(self, itemID): - # This plugin cannot own any items (it's not a role but a function plugin), - # but the it can be decided the following way: - # pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - # shNode = pluginHandlerSingleton.subjectHierarchyNode() - # associatedNode = shNode.GetItemDataNode(itemID) - # if associatedNode is not None and associatedNode.IsA("vtkMRMLMyNode") - # return 1.0 - return 0.0 - - def roleForPlugin(self): - # As this plugin cannot own any items, it doesn't have a role either - return "N/A" - - def helpText(self): - # return ("

          " - # "" - # "SegmentStatistics module subject hierarchy help text" - # "" - # "

          " - # "

          " - # "" - # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin." - # "" - # "

          \n") - return "" - - def icon(self, itemID): - # As this plugin cannot own any items, it doesn't have an icon eitherimport os - # import os - # iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MyIcon.png') - # if self.canOwnSubjectHierarchyItem(itemID) > 0.0 and os.path.exists(iconPath): - # return qt.QIcon(iconPath) - # Item unknown by plugin - return qt.QIcon() - - def visibilityIcon(self, visible): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible) - - def editProperties(self, itemID): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - pluginHandlerSingleton.pluginByName('Default').editProperties(itemID) - - def itemContextMenuActions(self): - return [self.segmentStatisticsAction] - - def onCalculateStatistics(self): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - currentItemID = pluginHandlerSingleton.currentItem() - if not currentItemID: - logging.error("Invalid current item") - - shNode = pluginHandlerSingleton.subjectHierarchyNode() - segmentationNode = shNode.GetItemDataNode(currentItemID) - - # Select segmentation node in segment statistics - pluginHandlerSingleton.pluginByName('Default').switchToModule('SegmentStatistics') - statisticsWidget = slicer.modules.segmentstatistics.widgetRepresentation().self() - statisticsWidget.segmentationSelector.setCurrentNode(segmentationNode) - - # Get master volume from segmentation - masterVolume = segmentationNode.GetNodeReference(slicer.vtkMRMLSegmentationNode.GetReferenceImageGeometryReferenceRole()) - if masterVolume is not None: - statisticsWidget.scalarSelector.setCurrentNode(masterVolume) - - def sceneContextMenuActions(self): - return [] - - def showContextMenuActionsForItem(self, itemID): - # Scene - if not itemID: - # No scene context menu actions in this plugin - return - - # Volume but not LabelMap - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - if pluginHandlerSingleton.pluginByName('Segmentations').canOwnSubjectHierarchyItem(itemID): - # Get current item - currentItemID = pluginHandlerSingleton.currentItem() - if not currentItemID: - logging.error("Invalid current item") - return - self.segmentStatisticsAction.visible = True - - def tooltip(self, itemID): - # As this plugin cannot own any items, it doesn't provide tooltip either - return "" - - def setDisplayVisibility(self, itemID, visible): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible) - - def getDisplayVisibility(self, itemID): - pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() - return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID) + """ Scripted subject hierarchy plugin for the Segment Statistics module. + + This is also an example for scripted plugins, so includes all possible methods. + The methods that are not needed (i.e. the default implementation in + qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be + omitted in plugins created based on this one. + """ + + # Necessary static member to be able to set python source to scripted subject hierarchy plugin + filePath = __file__ + + def __init__(self, scriptedPlugin): + scriptedPlugin.name = 'SegmentStatistics' + AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin) + + self.segmentStatisticsAction = qt.QAction("Calculate statistics...", scriptedPlugin) + self.segmentStatisticsAction.connect("triggered()", self.onCalculateStatistics) + + def canAddNodeToSubjectHierarchy(self, node, parentItemID): + # This plugin cannot own any items (it's not a role but a function plugin), + # but the it can be decided the following way: + # if node is not None and node.IsA("vtkMRMLMyNode"): + # return 1.0 + return 0.0 + + def canOwnSubjectHierarchyItem(self, itemID): + # This plugin cannot own any items (it's not a role but a function plugin), + # but the it can be decided the following way: + # pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + # shNode = pluginHandlerSingleton.subjectHierarchyNode() + # associatedNode = shNode.GetItemDataNode(itemID) + # if associatedNode is not None and associatedNode.IsA("vtkMRMLMyNode") + # return 1.0 + return 0.0 + + def roleForPlugin(self): + # As this plugin cannot own any items, it doesn't have a role either + return "N/A" + + def helpText(self): + # return ("

          " + # "" + # "SegmentStatistics module subject hierarchy help text" + # "" + # "

          " + # "

          " + # "" + # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin." + # "" + # "

          \n") + return "" + + def icon(self, itemID): + # As this plugin cannot own any items, it doesn't have an icon eitherimport os + # import os + # iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MyIcon.png') + # if self.canOwnSubjectHierarchyItem(itemID) > 0.0 and os.path.exists(iconPath): + # return qt.QIcon(iconPath) + # Item unknown by plugin + return qt.QIcon() + + def visibilityIcon(self, visible): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible) + + def editProperties(self, itemID): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + pluginHandlerSingleton.pluginByName('Default').editProperties(itemID) + + def itemContextMenuActions(self): + return [self.segmentStatisticsAction] + + def onCalculateStatistics(self): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + currentItemID = pluginHandlerSingleton.currentItem() + if not currentItemID: + logging.error("Invalid current item") + + shNode = pluginHandlerSingleton.subjectHierarchyNode() + segmentationNode = shNode.GetItemDataNode(currentItemID) + + # Select segmentation node in segment statistics + pluginHandlerSingleton.pluginByName('Default').switchToModule('SegmentStatistics') + statisticsWidget = slicer.modules.segmentstatistics.widgetRepresentation().self() + statisticsWidget.segmentationSelector.setCurrentNode(segmentationNode) + + # Get master volume from segmentation + masterVolume = segmentationNode.GetNodeReference(slicer.vtkMRMLSegmentationNode.GetReferenceImageGeometryReferenceRole()) + if masterVolume is not None: + statisticsWidget.scalarSelector.setCurrentNode(masterVolume) + + def sceneContextMenuActions(self): + return [] + + def showContextMenuActionsForItem(self, itemID): + # Scene + if not itemID: + # No scene context menu actions in this plugin + return + + # Volume but not LabelMap + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + if pluginHandlerSingleton.pluginByName('Segmentations').canOwnSubjectHierarchyItem(itemID): + # Get current item + currentItemID = pluginHandlerSingleton.currentItem() + if not currentItemID: + logging.error("Invalid current item") + return + self.segmentStatisticsAction.visible = True + + def tooltip(self, itemID): + # As this plugin cannot own any items, it doesn't provide tooltip either + return "" + + def setDisplayVisibility(self, itemID, visible): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible) + + def getDisplayVisibility(self, itemID): + pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance() + return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID) diff --git a/Modules/Scripted/SelfTests/SelfTests.py b/Modules/Scripted/SelfTests/SelfTests.py index 41a6497157f..90cc97ea0ed 100644 --- a/Modules/Scripted/SelfTests/SelfTests.py +++ b/Modules/Scripted/SelfTests/SelfTests.py @@ -17,44 +17,44 @@ class ExampleSelfTests: - @staticmethod - def closeScene(): - """Close the scene""" - slicer.mrmlScene.Clear(0) + @staticmethod + def closeScene(): + """Close the scene""" + slicer.mrmlScene.Clear(0) class SelfTests(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "SelfTests" - self.parent.categories = ["Testing"] - self.parent.contributors = ["Steve Pieper (Isomics)"] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "SelfTests" + self.parent.categories = ["Testing"] + self.parent.contributors = ["Steve Pieper (Isomics)"] + self.parent.helpText = """ The SelfTests module allows developers to provide built-in self-tests (BIST) for slicer so that users can tell if their installed version of slicer are running as designed. """ - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = """ + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = """ This work is part of SparKit project, funded by Cancer Care Ontario (CCO)'s ACRU program and Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO). """ - # - # slicer.selfTests is a dictionary of tests that are registered - # here or in other parts of the code. The key is the name of the test - # and the value is a python callable that runs the test and returns - # if the test passed or raises and exception if it fails. - # the __doc__ attribute of the test is used as a tooltip for the test - # button. - # - try: - slicer.selfTests - except AttributeError: - slicer.selfTests = {} - - # register the example tests - slicer.selfTests['MRMLSceneExists'] = lambda : slicer.app.mrmlScene - slicer.selfTests['CloseScene'] = ExampleSelfTests.closeScene + # + # slicer.selfTests is a dictionary of tests that are registered + # here or in other parts of the code. The key is the name of the test + # and the value is a python callable that runs the test and returns + # if the test passed or raises and exception if it fails. + # the __doc__ attribute of the test is used as a tooltip for the test + # button. + # + try: + slicer.selfTests + except AttributeError: + slicer.selfTests = {} + + # register the example tests + slicer.selfTests['MRMLSceneExists'] = lambda: slicer.app.mrmlScene + slicer.selfTests['CloseScene'] = ExampleSelfTests.closeScene # # SelfTests widget @@ -62,126 +62,126 @@ def __init__(self, parent): class SelfTestsWidget(ScriptedLoadableModuleWidget): - """Slicer module that creates the Qt GUI for interacting with SelfTests - Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ + """Slicer module that creates the Qt GUI for interacting with SelfTests + Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ - # sets up the widget - def setup(self): - ScriptedLoadableModuleWidget.setup(self) + # sets up the widget + def setup(self): + ScriptedLoadableModuleWidget.setup(self) - # This module is often used in developer mode, therefore - # collapse reload & test section by default. - if hasattr(self, "reloadCollapsibleButton"): - self.reloadCollapsibleButton.collapsed = True + # This module is often used in developer mode, therefore + # collapse reload & test section by default. + if hasattr(self, "reloadCollapsibleButton"): + self.reloadCollapsibleButton.collapsed = True - self.logic = SelfTestsLogic(slicer.selfTests) + self.logic = SelfTestsLogic(slicer.selfTests) - globals()['selfTests'] = self + globals()['selfTests'] = self - # - # test list - # + # + # test list + # - self.testList = ctk.ctkCollapsibleButton(self.parent) - self.testList.setLayout(qt.QVBoxLayout()) - self.testList.setText("Self Tests") - self.layout.addWidget(self.testList) - self.testList.collapsed = False + self.testList = ctk.ctkCollapsibleButton(self.parent) + self.testList.setLayout(qt.QVBoxLayout()) + self.testList.setText("Self Tests") + self.layout.addWidget(self.testList) + self.testList.collapsed = False - self.runAll = qt.QPushButton("Run All") - self.testList.layout().addWidget(self.runAll) - self.runAll.connect('clicked()', self.onRunAll) + self.runAll = qt.QPushButton("Run All") + self.testList.layout().addWidget(self.runAll) + self.runAll.connect('clicked()', self.onRunAll) - self.testButtons = {} - self.testMapper = qt.QSignalMapper() - self.testMapper.connect('mapped(const QString&)', self.onRun) - testKeys = sorted(slicer.selfTests.keys()) - for test in testKeys: - self.testButtons[test] = qt.QPushButton(test) - self.testButtons[test].setToolTip(slicer.selfTests[test].__doc__) - self.testList.layout().addWidget(self.testButtons[test]) - self.testMapper.setMapping(self.testButtons[test],test) - self.testButtons[test].connect('clicked()', self.testMapper, 'map()') + self.testButtons = {} + self.testMapper = qt.QSignalMapper() + self.testMapper.connect('mapped(const QString&)', self.onRun) + testKeys = sorted(slicer.selfTests.keys()) + for test in testKeys: + self.testButtons[test] = qt.QPushButton(test) + self.testButtons[test].setToolTip(slicer.selfTests[test].__doc__) + self.testList.layout().addWidget(self.testButtons[test]) + self.testMapper.setMapping(self.testButtons[test], test) + self.testButtons[test].connect('clicked()', self.testMapper, 'map()') - # Add spacer to layout - self.layout.addStretch(1) + # Add spacer to layout + self.layout.addStretch(1) - def onRunAll(self): - self.logic.run(continueCheck=self.continueCheck) - slicer.util.infoDisplay(self.logic, windowTitle='SelfTests') + def onRunAll(self): + self.logic.run(continueCheck=self.continueCheck) + slicer.util.infoDisplay(self.logic, windowTitle='SelfTests') - def onRun(self,test): - self.logic.run([test,], continueCheck=self.continueCheck) - slicer.util.infoDisplay(self.logic, windowTitle='SelfTests') + def onRun(self, test): + self.logic.run([test, ], continueCheck=self.continueCheck) + slicer.util.infoDisplay(self.logic, windowTitle='SelfTests') - def continueCheck(self,logic): - slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents) - return True + def continueCheck(self, logic): + slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents) + return True class SelfTestsLogic: - """Logic to handle invoking the tests and reporting the results""" - - def __init__(self,selfTests): - self.selfTests = selfTests - self.results = {} - self.passed = [] - self.failed = [] - - def __str__(self): - testsRun = len(list(self.results.keys())) - if testsRun == 0: - return "No tests run" - s = "%.0f%% passed (%d of %d)" % ( - (100. * len(self.passed) / testsRun), - len(self.passed), testsRun ) - s +="\n---\n" - for test in self.results: - s += f"{test}\t{self.results[test]}\n" - return s - - def run(self,tests=None,continueCheck=None): - if not tests: - tests = list(self.selfTests.keys()) - - for test in tests: - try: - result = self.selfTests[test]() - self.passed.append(test) - except Exception as e: - traceback.print_exc() - result = "Failed with: %s" % e - self.failed.append(test) - self.results[test] = result - if continueCheck: - if not continueCheck(self): - return + """Logic to handle invoking the tests and reporting the results""" + + def __init__(self, selfTests): + self.selfTests = selfTests + self.results = {} + self.passed = [] + self.failed = [] + + def __str__(self): + testsRun = len(list(self.results.keys())) + if testsRun == 0: + return "No tests run" + s = "%.0f%% passed (%d of %d)" % ( + (100. * len(self.passed) / testsRun), + len(self.passed), testsRun) + s += "\n---\n" + for test in self.results: + s += f"{test}\t{self.results[test]}\n" + return s + + def run(self, tests=None, continueCheck=None): + if not tests: + tests = list(self.selfTests.keys()) + + for test in tests: + try: + result = self.selfTests[test]() + self.passed.append(test) + except Exception as e: + traceback.print_exc() + result = "Failed with: %s" % e + self.failed.append(test) + self.results[test] = result + if continueCheck: + if not continueCheck(self): + return def SelfTestsTest(): - if hasattr(slicer,'selfTests'): - logic = SelfTestsLogic(list(slicer.selfTests.keys())) - logic.run() - print(logic.results) - print("SelfTestsTest Passed!") - return logic.failed == [] + if hasattr(slicer, 'selfTests'): + logic = SelfTestsLogic(list(slicer.selfTests.keys())) + logic.run() + print(logic.results) + print("SelfTestsTest Passed!") + return logic.failed == [] def SelfTestsDemo(): - pass + pass if __name__ == "__main__": - import sys - if '--test' in sys.argv: - if SelfTestsTest(): - exit(0) - exit(1) - if '--demo' in sys.argv: - SelfTestsDemo() - exit() - # TODO - 'exit()' returns so this code gets run - # even if the argument matches one of the cases above - #print ("usage: SelfTests.py [--test | --demo]") + import sys + if '--test' in sys.argv: + if SelfTestsTest(): + exit(0) + exit(1) + if '--demo' in sys.argv: + SelfTestsDemo() + exit() + # TODO - 'exit()' returns so this code gets run + # even if the argument matches one of the cases above + # print ("usage: SelfTests.py [--test | --demo]") diff --git a/Modules/Scripted/VectorToScalarVolume/VectorToScalarVolume.py b/Modules/Scripted/VectorToScalarVolume/VectorToScalarVolume.py index a829ff238db..a755f5c9768 100644 --- a/Modules/Scripted/VectorToScalarVolume/VectorToScalarVolume.py +++ b/Modules/Scripted/VectorToScalarVolume/VectorToScalarVolume.py @@ -12,45 +12,45 @@ @contextmanager def MyScopedQtPropertySetter(qobject, properties): - """ Context manager to set/reset properties""" - # TODO: Move it to slicer.utils and delete it here. - previousValues = {} - for propertyName, propertyValue in properties.items(): - previousValues[propertyName] = getattr(qobject, propertyName) - setattr(qobject, propertyName, propertyValue) - yield - for propertyName in properties.keys(): - setattr(qobject, propertyName, previousValues[propertyName]) + """ Context manager to set/reset properties""" + # TODO: Move it to slicer.utils and delete it here. + previousValues = {} + for propertyName, propertyValue in properties.items(): + previousValues[propertyName] = getattr(qobject, propertyName) + setattr(qobject, propertyName, propertyValue) + yield + for propertyName in properties.keys(): + setattr(qobject, propertyName, previousValues[propertyName]) @contextmanager def MyObjectsBlockSignals(*qobjects): - """ - Context manager to block/reset signals of any number of input qobjects. - Usage: - with MyObjectsBlockSignals(self.aComboBox, self.otherComboBox): - """ - # TODO: Move it to slicer.utils and delete it here. - previousValues = list() - for qobject in qobjects: - # blockedSignal returns the previous value of signalsBlocked() - previousValues.append(qobject.blockSignals(True)) - yield - for (qobject, previousValue) in zip(qobjects, previousValues): - qobject.blockSignals(previousValue) + """ + Context manager to block/reset signals of any number of input qobjects. + Usage: + with MyObjectsBlockSignals(self.aComboBox, self.otherComboBox): + """ + # TODO: Move it to slicer.utils and delete it here. + previousValues = list() + for qobject in qobjects: + # blockedSignal returns the previous value of signalsBlocked() + previousValues.append(qobject.blockSignals(True)) + yield + for (qobject, previousValue) in zip(qobjects, previousValues): + qobject.blockSignals(previousValue) def getNode(nodeID): - if nodeID is None: - return None - return slicer.mrmlScene.GetNodeByID(nodeID) + if nodeID is None: + return None + return slicer.mrmlScene.GetNodeByID(nodeID) def getNodeID(node): - if node is None: - return "" - else: - return node.GetID() + if node is None: + return "" + else: + return node.GetID() # @@ -58,15 +58,15 @@ def getNodeID(node): # class VectorToScalarVolume(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - self.parent.title = "Vector to Scalar Volume" - self.parent.categories = ["Converters"] - self.parent.dependencies = [] - self.parent.contributors = ["Steve Pieper (Isomics)", - "Pablo Hernandez-Cerdan (Kitware)", - "Jean-Christophe Fillion-Robin (Kitware)", ] - self.parent.helpText = """ + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "Vector to Scalar Volume" + self.parent.categories = ["Converters"] + self.parent.dependencies = [] + self.parent.contributors = ["Steve Pieper (Isomics)", + "Pablo Hernandez-Cerdan (Kitware)", + "Jean-Christophe Fillion-Robin (Kitware)", ] + self.parent.helpText = """

          Make a scalar (1 component) volume from a vector volume.

          It provides multiple conversion modes:

          @@ -77,7 +77,7 @@ def __init__(self, parent):
        • computes the mean of all the components.
        """ - self.parent.acknowledgementText = """ + self.parent.acknowledgementText = """ Developed by Steve Pieper, Isomics, Inc., partially funded by NIH grant 3P41RR013218-12S1 (NAC) and is part of the National Alliance for Medical Image Computing (NA-MIC), funded by the National Institutes of Health through the @@ -89,205 +89,205 @@ def __init__(self, parent): # class VectorToScalarVolumeWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): - """ - The user selected parameters are stored in a parameterNode. - """ - - def __init__(self, parent=None): - ScriptedLoadableModuleWidget.__init__(self, parent) - VTKObservationMixin.__init__(self) - self.logic = None - self._parameterNode = None - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - self.logic = VectorToScalarVolumeLogic() - # This will use createParameterNode with the provided default options - self.setParameterNode(self.logic.getParameterNode()) - - self.parameterSetSelectionCollapsibleButton = ctk.ctkCollapsibleButton() - self.parameterSetSelectionCollapsibleButton.text = "Parameter set" - self.layout.addWidget(self.parameterSetSelectionCollapsibleButton) - - # Layout within the "Selection" collapsible button - parameterSetSelectionFormLayout = qt.QFormLayout(self.parameterSetSelectionCollapsibleButton) - - # Parameter set selector (inspired by SegmentStatistics.py) - self.parameterNodeSelector = slicer.qMRMLNodeComboBox() - self.parameterNodeSelector.nodeTypes = (("vtkMRMLScriptedModuleNode"), "") - self.parameterNodeSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", "VectorToScalarVolume") - self.parameterNodeSelector.selectNodeUponCreation = True - self.parameterNodeSelector.addEnabled = True - self.parameterNodeSelector.renameEnabled = True - self.parameterNodeSelector.removeEnabled = True - self.parameterNodeSelector.noneEnabled = False - self.parameterNodeSelector.showHidden = True - self.parameterNodeSelector.showChildNodeTypes = False - self.parameterNodeSelector.baseName = "VectorToScalarVolume" - self.parameterNodeSelector.setMRMLScene(slicer.mrmlScene) - self.parameterNodeSelector.toolTip = "Pick parameter set" - parameterSetSelectionFormLayout.addRow("Parameter set: ", self.parameterNodeSelector) - - # Parameters - self.selectionCollapsibleButton = ctk.ctkCollapsibleButton() - self.selectionCollapsibleButton.text = "Conversion settings" - self.layout.addWidget(self.selectionCollapsibleButton) - - # Layout within the "Selection" collapsible button - parametersFormLayout = qt.QFormLayout(self.selectionCollapsibleButton) - - # - # the volume selectors - # - self.inputSelector = slicer.qMRMLNodeComboBox() - self.inputSelector.nodeTypes = ["vtkMRMLVectorVolumeNode"] - self.inputSelector.addEnabled = False - self.inputSelector.removeEnabled = False - self.inputSelector.setMRMLScene(slicer.mrmlScene) - parametersFormLayout.addRow("Input Vector Volume: ", self.inputSelector) - - self.outputSelector = slicer.qMRMLNodeComboBox() - self.outputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] - self.outputSelector.hideChildNodeTypes = ["vtkMRMLVectorVolumeNode"] - self.outputSelector.setMRMLScene(slicer.mrmlScene) - self.outputSelector.addEnabled = True - self.outputSelector.renameEnabled = True - self.outputSelector.baseName = "Scalar Volume" - parametersFormLayout.addRow("Output Scalar Volume: ", self.outputSelector) - - # - # Options to extract single components - # - self.conversionMethodWidget = VectorToScalarVolumeConversionMethodWidget() - parametersFormLayout.addRow("Conversion Method: ", self.conversionMethodWidget) - # Apply button - self.applyButton = qt.QPushButton("Apply") - self.applyButton.toolTip = "Run Convert the vector to scalar." - parametersFormLayout.addRow(self.applyButton) - - # Add vertical spacer - self.layout.addStretch(1) - - # Connections - self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setParameterNode) - self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.updateGuiFromMRML) - - # updateParameterNodeFromGui - self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGui) - self.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGui) - - self.applyButton.connect('clicked(bool)', self.onApply) - - # conversion widget - self.conversionMethodWidget.methodSelectorComboBox.connect('currentIndexChanged(int)', self.updateParameterNodeFromGui) - self.conversionMethodWidget.componentsComboBox.connect('currentIndexChanged(int)', self.updateParameterNodeFromGui) - - # The parameter node had defaults at creation, propagate them to the GUI. - self.updateGuiFromMRML() - - def cleanup(self): - self.removeObservers() - - def parameterNode(self): - return self._parameterNode - - def setParameterNode(self, inputParameterNode): - if inputParameterNode == self._parameterNode: - return - if self._parameterNode is not None: - self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGuiFromMRML) - if inputParameterNode is not None: - self.addObserver(inputParameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGuiFromMRML) - self._parameterNode = inputParameterNode - - def inputVolumeNode(self): - return self.inputSelector.currentNode() - - def setInputVolumeNode(self, node): - if isinstance(node, str): - node = getNode(node) - self.inputSelector.setCurrentNode(node) - - def outputVolumeNode(self): - return self.outputSelector.currentNode() - - def setOutputVolumeNode(self, node): - if isinstance(node, str): - node = getNode(node) - self.outputSelector.setCurrentNode(node) - - def updateButtonStates(self): - - isMethodSingleComponent = self._parameterNode.GetParameter("ConversionMethod") == VectorToScalarVolumeLogic.SINGLE_COMPONENT - - # Update apply button state and tooltip - applyErrorMessage = "" - if not self.inputVolumeNode(): - applyErrorMessage = "Please select Input Vector Volume" - elif not self.outputVolumeNode(): - applyErrorMessage = "Please select Output Scalar Volume" - elif not self.parameterNode(): - applyErrorMessage = "Please select Parameter set" - elif isMethodSingleComponent and (int(self._parameterNode.GetParameter("ComponentToExtract")) < 0): - applyErrorMessage = "Please select a component to extract" - - self.applyButton.enabled = (not applyErrorMessage) - self.applyButton.toolTip = applyErrorMessage - - self.conversionMethodWidget.componentsComboBox.visible = isMethodSingleComponent - - if (self.inputVolumeNode() is not None) and isMethodSingleComponent: - imageComponents = self.inputVolumeNode().GetImageData().GetNumberOfScalarComponents() - wasBlocked = self.conversionMethodWidget.componentsComboBox.blockSignals(True) - if self.conversionMethodWidget.componentsComboBox.count != imageComponents: - self.conversionMethodWidget.componentsComboBox.clear() - for comp in range(imageComponents): - self.conversionMethodWidget.componentsComboBox.insertItem(comp, str(comp)) - self.conversionMethodWidget.componentsComboBox.blockSignals(wasBlocked) - - def updateGuiFromMRML(self, caller=None, event=None): """ - Query all the parameters in the parameterNode, - and update the GUI state accordingly if something has changed. + The user selected parameters are stored in a parameterNode. """ - self.updateButtonStates() - - if not self.parameterNode(): - return + def __init__(self, parent=None): + ScriptedLoadableModuleWidget.__init__(self, parent) + VTKObservationMixin.__init__(self) + self.logic = None + self._parameterNode = None + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + self.logic = VectorToScalarVolumeLogic() + # This will use createParameterNode with the provided default options + self.setParameterNode(self.logic.getParameterNode()) + + self.parameterSetSelectionCollapsibleButton = ctk.ctkCollapsibleButton() + self.parameterSetSelectionCollapsibleButton.text = "Parameter set" + self.layout.addWidget(self.parameterSetSelectionCollapsibleButton) + + # Layout within the "Selection" collapsible button + parameterSetSelectionFormLayout = qt.QFormLayout(self.parameterSetSelectionCollapsibleButton) + + # Parameter set selector (inspired by SegmentStatistics.py) + self.parameterNodeSelector = slicer.qMRMLNodeComboBox() + self.parameterNodeSelector.nodeTypes = (("vtkMRMLScriptedModuleNode"), "") + self.parameterNodeSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", "VectorToScalarVolume") + self.parameterNodeSelector.selectNodeUponCreation = True + self.parameterNodeSelector.addEnabled = True + self.parameterNodeSelector.renameEnabled = True + self.parameterNodeSelector.removeEnabled = True + self.parameterNodeSelector.noneEnabled = False + self.parameterNodeSelector.showHidden = True + self.parameterNodeSelector.showChildNodeTypes = False + self.parameterNodeSelector.baseName = "VectorToScalarVolume" + self.parameterNodeSelector.setMRMLScene(slicer.mrmlScene) + self.parameterNodeSelector.toolTip = "Pick parameter set" + parameterSetSelectionFormLayout.addRow("Parameter set: ", self.parameterNodeSelector) + + # Parameters + self.selectionCollapsibleButton = ctk.ctkCollapsibleButton() + self.selectionCollapsibleButton.text = "Conversion settings" + self.layout.addWidget(self.selectionCollapsibleButton) + + # Layout within the "Selection" collapsible button + parametersFormLayout = qt.QFormLayout(self.selectionCollapsibleButton) + + # + # the volume selectors + # + self.inputSelector = slicer.qMRMLNodeComboBox() + self.inputSelector.nodeTypes = ["vtkMRMLVectorVolumeNode"] + self.inputSelector.addEnabled = False + self.inputSelector.removeEnabled = False + self.inputSelector.setMRMLScene(slicer.mrmlScene) + parametersFormLayout.addRow("Input Vector Volume: ", self.inputSelector) + + self.outputSelector = slicer.qMRMLNodeComboBox() + self.outputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"] + self.outputSelector.hideChildNodeTypes = ["vtkMRMLVectorVolumeNode"] + self.outputSelector.setMRMLScene(slicer.mrmlScene) + self.outputSelector.addEnabled = True + self.outputSelector.renameEnabled = True + self.outputSelector.baseName = "Scalar Volume" + parametersFormLayout.addRow("Output Scalar Volume: ", self.outputSelector) + + # + # Options to extract single components + # + self.conversionMethodWidget = VectorToScalarVolumeConversionMethodWidget() + parametersFormLayout.addRow("Conversion Method: ", self.conversionMethodWidget) + # Apply button + self.applyButton = qt.QPushButton("Apply") + self.applyButton.toolTip = "Run Convert the vector to scalar." + parametersFormLayout.addRow(self.applyButton) + + # Add vertical spacer + self.layout.addStretch(1) + + # Connections + self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setParameterNode) + self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.updateGuiFromMRML) + + # updateParameterNodeFromGui + self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGui) + self.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGui) + + self.applyButton.connect('clicked(bool)', self.onApply) + + # conversion widget + self.conversionMethodWidget.methodSelectorComboBox.connect('currentIndexChanged(int)', self.updateParameterNodeFromGui) + self.conversionMethodWidget.componentsComboBox.connect('currentIndexChanged(int)', self.updateParameterNodeFromGui) + + # The parameter node had defaults at creation, propagate them to the GUI. + self.updateGuiFromMRML() + + def cleanup(self): + self.removeObservers() + + def parameterNode(self): + return self._parameterNode + + def setParameterNode(self, inputParameterNode): + if inputParameterNode == self._parameterNode: + return + if self._parameterNode is not None: + self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGuiFromMRML) + if inputParameterNode is not None: + self.addObserver(inputParameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGuiFromMRML) + self._parameterNode = inputParameterNode + + def inputVolumeNode(self): + return self.inputSelector.currentNode() + + def setInputVolumeNode(self, node): + if isinstance(node, str): + node = getNode(node) + self.inputSelector.setCurrentNode(node) + + def outputVolumeNode(self): + return self.outputSelector.currentNode() + + def setOutputVolumeNode(self, node): + if isinstance(node, str): + node = getNode(node) + self.outputSelector.setCurrentNode(node) + + def updateButtonStates(self): + + isMethodSingleComponent = self._parameterNode.GetParameter("ConversionMethod") == VectorToScalarVolumeLogic.SINGLE_COMPONENT + + # Update apply button state and tooltip + applyErrorMessage = "" + if not self.inputVolumeNode(): + applyErrorMessage = "Please select Input Vector Volume" + elif not self.outputVolumeNode(): + applyErrorMessage = "Please select Output Scalar Volume" + elif not self.parameterNode(): + applyErrorMessage = "Please select Parameter set" + elif isMethodSingleComponent and (int(self._parameterNode.GetParameter("ComponentToExtract")) < 0): + applyErrorMessage = "Please select a component to extract" + + self.applyButton.enabled = (not applyErrorMessage) + self.applyButton.toolTip = applyErrorMessage + + self.conversionMethodWidget.componentsComboBox.visible = isMethodSingleComponent + + if (self.inputVolumeNode() is not None) and isMethodSingleComponent: + imageComponents = self.inputVolumeNode().GetImageData().GetNumberOfScalarComponents() + wasBlocked = self.conversionMethodWidget.componentsComboBox.blockSignals(True) + if self.conversionMethodWidget.componentsComboBox.count != imageComponents: + self.conversionMethodWidget.componentsComboBox.clear() + for comp in range(imageComponents): + self.conversionMethodWidget.componentsComboBox.insertItem(comp, str(comp)) + self.conversionMethodWidget.componentsComboBox.blockSignals(wasBlocked) + + def updateGuiFromMRML(self, caller=None, event=None): + """ + Query all the parameters in the parameterNode, + and update the GUI state accordingly if something has changed. + """ + + self.updateButtonStates() + + if not self.parameterNode(): + return + + self.setInputVolumeNode(self._parameterNode.GetParameter("InputVectorVolume")) + self.setOutputVolumeNode(self._parameterNode.GetParameter("OutputScalarVolume")) + self.conversionMethodWidget.methodSelectorComboBox.setCurrentIndex( + self.conversionMethodWidget.methodSelectorComboBox.findData( + self._parameterNode.GetParameter("ConversionMethod"))) + self.conversionMethodWidget.componentsComboBox.setCurrentIndex( + int(self._parameterNode.GetParameter("ComponentToExtract"))) - self.setInputVolumeNode(self._parameterNode.GetParameter("InputVectorVolume")) - self.setOutputVolumeNode(self._parameterNode.GetParameter("OutputScalarVolume")) - self.conversionMethodWidget.methodSelectorComboBox.setCurrentIndex( - self.conversionMethodWidget.methodSelectorComboBox.findData( - self._parameterNode.GetParameter("ConversionMethod"))) - self.conversionMethodWidget.componentsComboBox.setCurrentIndex( - int(self._parameterNode.GetParameter("ComponentToExtract"))) + def updateParameterNodeFromGui(self): - def updateParameterNodeFromGui(self): + self.updateButtonStates() - self.updateButtonStates() + if self._parameterNode is None: + return - if self._parameterNode is None: - return + with NodeModify(self._parameterNode): + self._parameterNode.SetParameter("InputVectorVolume", getNodeID(self.inputVolumeNode())) + self._parameterNode.SetParameter("OutputScalarVolume", getNodeID(self.outputVolumeNode())) + self._parameterNode.SetParameter("ConversionMethod", self.conversionMethodWidget.conversionMethod()) + self._parameterNode.SetParameter("ComponentToExtract", str(self.conversionMethodWidget.componentToExtract())) - with NodeModify(self._parameterNode): - self._parameterNode.SetParameter("InputVectorVolume", getNodeID(self.inputVolumeNode())) - self._parameterNode.SetParameter("OutputScalarVolume", getNodeID(self.outputVolumeNode())) - self._parameterNode.SetParameter("ConversionMethod", self.conversionMethodWidget.conversionMethod()) - self._parameterNode.SetParameter("ComponentToExtract", str(self.conversionMethodWidget.componentToExtract())) + def onApply(self): - def onApply(self): + with MyScopedQtPropertySetter(self.applyButton, {"enabled": False, "text": "Working..."}): + success = self.logic.run(self._parameterNode) - with MyScopedQtPropertySetter(self.applyButton, {"enabled": False, "text": "Working..."}): - success = self.logic.run(self._parameterNode) - - # make the output volume appear in all the slice views - if success: - selectionNode = slicer.app.applicationLogic().GetSelectionNode() - selectionNode.SetActiveVolumeID(self.outputVolumeNode().GetID()) - slicer.app.applicationLogic().PropagateVolumeSelection(0) + # make the output volume appear in all the slice views + if success: + selectionNode = slicer.app.applicationLogic().GetSelectionNode() + selectionNode.SetActiveVolumeID(self.outputVolumeNode().GetID()) + slicer.app.applicationLogic().PropagateVolumeSelection(0) # @@ -295,42 +295,42 @@ def onApply(self): # class VectorToScalarVolumeConversionMethodWidget(qt.QWidget): - """ - Widget to interact with conversion parameters only. - It is separated from VectorToScalarVolumeWidget to enable GUI reusability in other modules. - """ + """ + Widget to interact with conversion parameters only. + It is separated from VectorToScalarVolumeWidget to enable GUI reusability in other modules. + """ - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent) - self.setup() + def __init__(self, parent=None): + qt.QWidget.__init__(self, parent) + self.setup() - def setup(self): - self.methodLayout = qt.QHBoxLayout(self) - self.methodLayout.setContentsMargins(0,0,0,0) - self.methodSelectorComboBox = qt.QComboBox() + def setup(self): + self.methodLayout = qt.QHBoxLayout(self) + self.methodLayout.setContentsMargins(0, 0, 0, 0) + self.methodSelectorComboBox = qt.QComboBox() - self.methodSelectorComboBox.addItem("Luminance", VectorToScalarVolumeLogic.LUMINANCE) - self.methodSelectorComboBox.setItemData(0, '(RGB,RGBA) Luminance from first three components: 0.30*R + 0.59*G + 0.11*B + 0.0*A)', qt.Qt.ToolTipRole) - self.methodSelectorComboBox.addItem("Average", VectorToScalarVolumeLogic.AVERAGE) - self.methodSelectorComboBox.setItemData(1, 'Average all the components.', qt.Qt.ToolTipRole) - self.methodSelectorComboBox.addItem("Single Component Extraction", VectorToScalarVolumeLogic.SINGLE_COMPONENT) - self.methodSelectorComboBox.setItemData(2, 'Extract single component', qt.Qt.ToolTipRole) + self.methodSelectorComboBox.addItem("Luminance", VectorToScalarVolumeLogic.LUMINANCE) + self.methodSelectorComboBox.setItemData(0, '(RGB,RGBA) Luminance from first three components: 0.30*R + 0.59*G + 0.11*B + 0.0*A)', qt.Qt.ToolTipRole) + self.methodSelectorComboBox.addItem("Average", VectorToScalarVolumeLogic.AVERAGE) + self.methodSelectorComboBox.setItemData(1, 'Average all the components.', qt.Qt.ToolTipRole) + self.methodSelectorComboBox.addItem("Single Component Extraction", VectorToScalarVolumeLogic.SINGLE_COMPONENT) + self.methodSelectorComboBox.setItemData(2, 'Extract single component', qt.Qt.ToolTipRole) - self.methodLayout.addWidget(self.methodSelectorComboBox) + self.methodLayout.addWidget(self.methodSelectorComboBox) - # ComponentToExtract - singleComponentLayout = qt.QHBoxLayout() - self.componentsComboBox = qt.QComboBox() - singleComponentLayout.addWidget(self.componentsComboBox) - self.methodLayout.addLayout(singleComponentLayout) + # ComponentToExtract + singleComponentLayout = qt.QHBoxLayout() + self.componentsComboBox = qt.QComboBox() + singleComponentLayout.addWidget(self.componentsComboBox) + self.methodLayout.addLayout(singleComponentLayout) - def componentToExtract(self): - " returns current index. -1 is invalid or disabled combo box" - return self.componentsComboBox.currentIndex + def componentToExtract(self): + " returns current index. -1 is invalid or disabled combo box" + return self.componentsComboBox.currentIndex - def conversionMethod(self): - " returns data (str)" - return self.methodSelectorComboBox.currentData + def conversionMethod(self): + " returns data (str)" + return self.methodSelectorComboBox.currentData # @@ -338,184 +338,184 @@ def conversionMethod(self): # class VectorToScalarVolumeLogic(ScriptedLoadableModuleLogic): - """ - Implement the logic to compute the transform from vector to scalar. - It is stateless, with the run function getting inputs and setting outputs. - """ - - LUMINANCE = 'LUMINANCE' - AVERAGE = 'AVERAGE' - SINGLE_COMPONENT = 'SINGLE_COMPONENT' - EXTRACT_COMPONENT_NONE = -1 - - CONVERSION_METHODS = (LUMINANCE, AVERAGE, SINGLE_COMPONENT) - - def __init__(self, parent = None): - ScriptedLoadableModuleLogic.__init__(self, parent) - - def createParameterNode(self): - """ Override base class method to provide default parameters. """ - node = ScriptedLoadableModuleLogic.createParameterNode(self) - node.SetParameter("ConversionMethod", self.LUMINANCE) - node.SetParameter("ComponentToExtract", str(self.EXTRACT_COMPONENT_NONE)) - return node - - @staticmethod - def isValidInputOutputData(inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract): - """ - Validate parameters using the parameterNode. - Returns: (bool:isValid, string:errorMessage) """ - # - # Checking input/output consistency. - # - if not inputVolumeNode: - msg = 'no input volume node defined' - logging.debug("isValidInputOutputData failed: %s" % msg) - return False, msg - if not outputVolumeNode: - msg = 'no output volume node defined' - logging.debug("isValidInputOutputData failed: %s" % msg) - return False, msg - if inputVolumeNode.GetID() == outputVolumeNode.GetID(): - msg = 'input and output volume is the same. ' \ - 'Create a new volume for output to avoid this error.' - logging.debug("isValidInputOutputData failed: %s" % msg) - return False, msg - - # - # Checking based on method selected - # - if conversionMethod not in (VectorToScalarVolumeLogic.SINGLE_COMPONENT, - VectorToScalarVolumeLogic.LUMINANCE, - VectorToScalarVolumeLogic.AVERAGE): - msg = 'conversionMethod %s unrecognized.' % conversionMethod - logging.debug("isValidInputOutputData failed: %s" % msg) - return False, msg - - inputImage = inputVolumeNode.GetImageData() - numberOfComponents = inputImage.GetNumberOfScalarComponents() - - # SINGLE_COMPONENT: Check that input has enough components for the given componentToExtract - if conversionMethod == VectorToScalarVolumeLogic.SINGLE_COMPONENT: - # componentToExtract is an index with valid values in the range: [0, numberOfComponents-1] - if not(0 <= componentToExtract < numberOfComponents): - msg = 'componentToExtract %d is invalid. Image has only %d components.' % (componentToExtract, numberOfComponents) - logging.debug("isValidInputOutputData failed: %s" % msg) - return False, msg - - # LUMINANCE: Check that input vector has at least three components. - if conversionMethod == VectorToScalarVolumeLogic.LUMINANCE: - if numberOfComponents < 3: - msg = 'input has only %d components but requires ' \ - 'at least 3 components for luminance conversion.' % numberOfComponents - logging.debug("isValidInputOutputData failed: %s" % msg) - return False, msg - - return True, None - - def run(self, parameterNode): + Implement the logic to compute the transform from vector to scalar. + It is stateless, with the run function getting inputs and setting outputs. """ - Run the conversion with given parameterNode. - """ - if parameterNode is None: - slicer.util.errorDisplay('Invalid Parameter Node: None') - return False - - inputVolumeNode = getNode(parameterNode.GetParameter("InputVectorVolume")) - outputVolumeNode = getNode(parameterNode.GetParameter("OutputScalarVolume")) - conversionMethod = parameterNode.GetParameter("ConversionMethod") - componentToExtract = parameterNode.GetParameter("ComponentToExtract") - if componentToExtract == '': - componentToExtract = str(self.EXTRACT_COMPONENT_NONE) - componentToExtract = int(componentToExtract) - - valid, msg = self.isValidInputOutputData(inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract) - if not valid: - slicer.util.errorDisplay(msg) - return False - - logging.debug('Conversion mode is %s' % conversionMethod) - logging.debug('ComponentToExtract is %s' % componentToExtract) - - if conversionMethod == VectorToScalarVolumeLogic.SINGLE_COMPONENT: - self.runConversionMethodSingleComponent(inputVolumeNode, outputVolumeNode, componentToExtract) - - if conversionMethod == VectorToScalarVolumeLogic.LUMINANCE: - self.runConversionMethodLuminance(inputVolumeNode, outputVolumeNode) - - if conversionMethod == VectorToScalarVolumeLogic.AVERAGE: - self.runConversionMethodAverage(inputVolumeNode, outputVolumeNode) - - return True - - def runWithVariables(self, inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract): - """ Convenience method to run with variables, it creates a new parameterNode with these values. """ - - parameterNode = self.getParameterNode() - parameterNode.SetParameter("InputVectorVolume", getNodeID(inputVolumeNode)) - parameterNode.SetParameter("OutputScalarVolume", getNodeID(outputVolumeNode)) - parameterNode.SetParameter("ConversionMethod", conversionMethod) - parameterNode.SetParameter("ComponentToExtract", str(componentToExtract)) - return self.run(parameterNode) - - def runConversionMethodSingleComponent(self, inputVolumeNode, outputVolumeNode, componentToExtract): - ijkToRAS = vtk.vtkMatrix4x4() - inputVolumeNode.GetIJKToRASMatrix(ijkToRAS) - outputVolumeNode.SetIJKToRASMatrix(ijkToRAS) - - extract = vtk.vtkImageExtractComponents() - extract.SetInputConnection(inputVolumeNode.GetImageDataConnection()) - extract.SetComponents(componentToExtract) - extract.Update() - outputVolumeNode.SetImageDataConnection(extract.GetOutputPort()) - - def runConversionMethodLuminance(self, inputVolumeNode, outputVolumeNode): - ijkToRAS = vtk.vtkMatrix4x4() - inputVolumeNode.GetIJKToRASMatrix(ijkToRAS) - outputVolumeNode.SetIJKToRASMatrix(ijkToRAS) - - extract = vtk.vtkImageExtractComponents() - extract.SetInputConnection(inputVolumeNode.GetImageDataConnection()) - extract.SetComponents(0, 1, 2) - luminance = vtk.vtkImageLuminance() - luminance.SetInputConnection(extract.GetOutputPort()) - luminance.Update() - outputVolumeNode.SetImageDataConnection(luminance.GetOutputPort()) - - def runConversionMethodAverage(self, inputVolumeNode, outputVolumeNode): - ijkToRAS = vtk.vtkMatrix4x4() - inputVolumeNode.GetIJKToRASMatrix(ijkToRAS) - outputVolumeNode.SetIJKToRASMatrix(ijkToRAS) - - numberOfComponents = inputVolumeNode.GetImageData().GetNumberOfScalarComponents() - weightedSum = vtk.vtkImageWeightedSum() - weights = vtk.vtkDoubleArray() - weights.SetNumberOfValues(numberOfComponents) - # TODO: Average could be extended to let the user choose the weights of the components. - evenWeight = 1.0/numberOfComponents - logging.debug("ImageWeightedSum: weight value for all components: %s" % evenWeight) - for comp in range(numberOfComponents): - weights.SetValue(comp, evenWeight) - weightedSum.SetWeights(weights) - - for comp in range(numberOfComponents): - extract = vtk.vtkImageExtractComponents() - extract.SetInputConnection(inputVolumeNode.GetImageDataConnection()) - extract.SetComponents(comp) - extract.Update() - # Cast component to Double - compToDouble = vtk.vtkImageCast() - compToDouble.SetInputConnection(0, extract.GetOutputPort()) - compToDouble.SetOutputScalarTypeToDouble() - # Add to the weighted sum - weightedSum.AddInputConnection(compToDouble.GetOutputPort()) - - logging.debug("TotalInputConnections in weightedSum: %s" % weightedSum.GetTotalNumberOfInputConnections()) - weightedSum.SetNormalizeByWeight(False) # It is already normalized in the evenWeight case. - weightedSum.Update() - # Cast back to the type of the InputVolume, for consistency with other ConversionMethods - castBack = vtk.vtkImageCast() - castBack.SetInputConnection(0, weightedSum.GetOutputPort()) - castBack.SetOutputScalarType(inputVolumeNode.GetImageData().GetScalarType()) - outputVolumeNode.SetImageDataConnection(castBack.GetOutputPort()) + + LUMINANCE = 'LUMINANCE' + AVERAGE = 'AVERAGE' + SINGLE_COMPONENT = 'SINGLE_COMPONENT' + EXTRACT_COMPONENT_NONE = -1 + + CONVERSION_METHODS = (LUMINANCE, AVERAGE, SINGLE_COMPONENT) + + def __init__(self, parent=None): + ScriptedLoadableModuleLogic.__init__(self, parent) + + def createParameterNode(self): + """ Override base class method to provide default parameters. """ + node = ScriptedLoadableModuleLogic.createParameterNode(self) + node.SetParameter("ConversionMethod", self.LUMINANCE) + node.SetParameter("ComponentToExtract", str(self.EXTRACT_COMPONENT_NONE)) + return node + + @staticmethod + def isValidInputOutputData(inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract): + """ + Validate parameters using the parameterNode. + Returns: (bool:isValid, string:errorMessage) + """ + # + # Checking input/output consistency. + # + if not inputVolumeNode: + msg = 'no input volume node defined' + logging.debug("isValidInputOutputData failed: %s" % msg) + return False, msg + if not outputVolumeNode: + msg = 'no output volume node defined' + logging.debug("isValidInputOutputData failed: %s" % msg) + return False, msg + if inputVolumeNode.GetID() == outputVolumeNode.GetID(): + msg = 'input and output volume is the same. ' \ + 'Create a new volume for output to avoid this error.' + logging.debug("isValidInputOutputData failed: %s" % msg) + return False, msg + + # + # Checking based on method selected + # + if conversionMethod not in (VectorToScalarVolumeLogic.SINGLE_COMPONENT, + VectorToScalarVolumeLogic.LUMINANCE, + VectorToScalarVolumeLogic.AVERAGE): + msg = 'conversionMethod %s unrecognized.' % conversionMethod + logging.debug("isValidInputOutputData failed: %s" % msg) + return False, msg + + inputImage = inputVolumeNode.GetImageData() + numberOfComponents = inputImage.GetNumberOfScalarComponents() + + # SINGLE_COMPONENT: Check that input has enough components for the given componentToExtract + if conversionMethod == VectorToScalarVolumeLogic.SINGLE_COMPONENT: + # componentToExtract is an index with valid values in the range: [0, numberOfComponents-1] + if not(0 <= componentToExtract < numberOfComponents): + msg = 'componentToExtract %d is invalid. Image has only %d components.' % (componentToExtract, numberOfComponents) + logging.debug("isValidInputOutputData failed: %s" % msg) + return False, msg + + # LUMINANCE: Check that input vector has at least three components. + if conversionMethod == VectorToScalarVolumeLogic.LUMINANCE: + if numberOfComponents < 3: + msg = 'input has only %d components but requires ' \ + 'at least 3 components for luminance conversion.' % numberOfComponents + logging.debug("isValidInputOutputData failed: %s" % msg) + return False, msg + + return True, None + + def run(self, parameterNode): + """ + Run the conversion with given parameterNode. + """ + if parameterNode is None: + slicer.util.errorDisplay('Invalid Parameter Node: None') + return False + + inputVolumeNode = getNode(parameterNode.GetParameter("InputVectorVolume")) + outputVolumeNode = getNode(parameterNode.GetParameter("OutputScalarVolume")) + conversionMethod = parameterNode.GetParameter("ConversionMethod") + componentToExtract = parameterNode.GetParameter("ComponentToExtract") + if componentToExtract == '': + componentToExtract = str(self.EXTRACT_COMPONENT_NONE) + componentToExtract = int(componentToExtract) + + valid, msg = self.isValidInputOutputData(inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract) + if not valid: + slicer.util.errorDisplay(msg) + return False + + logging.debug('Conversion mode is %s' % conversionMethod) + logging.debug('ComponentToExtract is %s' % componentToExtract) + + if conversionMethod == VectorToScalarVolumeLogic.SINGLE_COMPONENT: + self.runConversionMethodSingleComponent(inputVolumeNode, outputVolumeNode, componentToExtract) + + if conversionMethod == VectorToScalarVolumeLogic.LUMINANCE: + self.runConversionMethodLuminance(inputVolumeNode, outputVolumeNode) + + if conversionMethod == VectorToScalarVolumeLogic.AVERAGE: + self.runConversionMethodAverage(inputVolumeNode, outputVolumeNode) + + return True + + def runWithVariables(self, inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract): + """ Convenience method to run with variables, it creates a new parameterNode with these values. """ + + parameterNode = self.getParameterNode() + parameterNode.SetParameter("InputVectorVolume", getNodeID(inputVolumeNode)) + parameterNode.SetParameter("OutputScalarVolume", getNodeID(outputVolumeNode)) + parameterNode.SetParameter("ConversionMethod", conversionMethod) + parameterNode.SetParameter("ComponentToExtract", str(componentToExtract)) + return self.run(parameterNode) + + def runConversionMethodSingleComponent(self, inputVolumeNode, outputVolumeNode, componentToExtract): + ijkToRAS = vtk.vtkMatrix4x4() + inputVolumeNode.GetIJKToRASMatrix(ijkToRAS) + outputVolumeNode.SetIJKToRASMatrix(ijkToRAS) + + extract = vtk.vtkImageExtractComponents() + extract.SetInputConnection(inputVolumeNode.GetImageDataConnection()) + extract.SetComponents(componentToExtract) + extract.Update() + outputVolumeNode.SetImageDataConnection(extract.GetOutputPort()) + + def runConversionMethodLuminance(self, inputVolumeNode, outputVolumeNode): + ijkToRAS = vtk.vtkMatrix4x4() + inputVolumeNode.GetIJKToRASMatrix(ijkToRAS) + outputVolumeNode.SetIJKToRASMatrix(ijkToRAS) + + extract = vtk.vtkImageExtractComponents() + extract.SetInputConnection(inputVolumeNode.GetImageDataConnection()) + extract.SetComponents(0, 1, 2) + luminance = vtk.vtkImageLuminance() + luminance.SetInputConnection(extract.GetOutputPort()) + luminance.Update() + outputVolumeNode.SetImageDataConnection(luminance.GetOutputPort()) + + def runConversionMethodAverage(self, inputVolumeNode, outputVolumeNode): + ijkToRAS = vtk.vtkMatrix4x4() + inputVolumeNode.GetIJKToRASMatrix(ijkToRAS) + outputVolumeNode.SetIJKToRASMatrix(ijkToRAS) + + numberOfComponents = inputVolumeNode.GetImageData().GetNumberOfScalarComponents() + weightedSum = vtk.vtkImageWeightedSum() + weights = vtk.vtkDoubleArray() + weights.SetNumberOfValues(numberOfComponents) + # TODO: Average could be extended to let the user choose the weights of the components. + evenWeight = 1.0 / numberOfComponents + logging.debug("ImageWeightedSum: weight value for all components: %s" % evenWeight) + for comp in range(numberOfComponents): + weights.SetValue(comp, evenWeight) + weightedSum.SetWeights(weights) + + for comp in range(numberOfComponents): + extract = vtk.vtkImageExtractComponents() + extract.SetInputConnection(inputVolumeNode.GetImageDataConnection()) + extract.SetComponents(comp) + extract.Update() + # Cast component to Double + compToDouble = vtk.vtkImageCast() + compToDouble.SetInputConnection(0, extract.GetOutputPort()) + compToDouble.SetOutputScalarTypeToDouble() + # Add to the weighted sum + weightedSum.AddInputConnection(compToDouble.GetOutputPort()) + + logging.debug("TotalInputConnections in weightedSum: %s" % weightedSum.GetTotalNumberOfInputConnections()) + weightedSum.SetNormalizeByWeight(False) # It is already normalized in the evenWeight case. + weightedSum.Update() + # Cast back to the type of the InputVolume, for consistency with other ConversionMethods + castBack = vtk.vtkImageCast() + castBack.SetInputConnection(0, weightedSum.GetOutputPort()) + castBack.SetOutputScalarType(inputVolumeNode.GetImageData().GetScalarType()) + outputVolumeNode.SetImageDataConnection(castBack.GetOutputPort()) diff --git a/Modules/Scripted/WebServer/WebServer.py b/Modules/Scripted/WebServer/WebServer.py index 04c1e2bd3c4..cbf64b89fd9 100644 --- a/Modules/Scripted/WebServer/WebServer.py +++ b/Modules/Scripted/WebServer/WebServer.py @@ -17,15 +17,15 @@ class WebServer(ScriptedLoadableModule): - def __init__(self, parent): - ScriptedLoadableModule.__init__(self, parent) - parent.title = "Web Server" - parent.categories = ["Servers"] - parent.dependencies = [] - parent.contributors = ["Steve Pieper (Isomics)", "Andras Lasso (PerkLab Queen's University)"] - parent.helpText = """Provides an embedded web server for slicer that provides a web services API for interacting with slicer. + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + parent.title = "Web Server" + parent.categories = ["Servers"] + parent.dependencies = [] + parent.contributors = ["Steve Pieper (Isomics)", "Andras Lasso (PerkLab Queen's University)"] + parent.helpText = """Provides an embedded web server for slicer that provides a web services API for interacting with slicer. """ - parent.acknowledgementText = """ + parent.acknowledgementText = """ This work was partially funded by NIH grant 3P41RR013218. """ @@ -35,212 +35,212 @@ def __init__(self, parent): class WebServerWidget(ScriptedLoadableModuleWidget): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def __init__(self, parent=None): - ScriptedLoadableModuleWidget.__init__(self, parent) - self.guiMessages = True - self.consoleMessages = False - # By default, no request handlers are created, we will add them in startServer - self.logic = WebServerLogic(logMessage=self.logMessage, requestHandlers=[]) - - def enter(self): - pass - - def exit(self): - pass - - def setup(self): - ScriptedLoadableModuleWidget.setup(self) - - # start button - self.startServerButton = qt.QPushButton("Start server") - self.startServerButton.name = "StartWebServer" - self.startServerButton.toolTip = "Start web server with the selected options." - self.layout.addWidget(self.startServerButton) - - # stop button - self.stopServerButton = qt.QPushButton("Stop server") - self.stopServerButton.name = "StopWebServer" - self.stopServerButton.toolTip = "Start web server with the selected options." - self.layout.addWidget(self.stopServerButton) - - # open browser page - self.localConnectionButton = qt.QPushButton("Open static pages in external browser") - self.localConnectionButton.toolTip = "Open a connection to the server on the local machine with your system browser." - self.layout.addWidget(self.localConnectionButton) - - # open slicer widget - self.localQtConnectionButton = qt.QPushButton("Open static pages in internal browser") - self.localQtConnectionButton.toolTip = "Open a connection with Qt to the server on the local machine." - self.layout.addWidget(self.localQtConnectionButton) - - # log window - self.log = qt.QTextEdit() - self.log.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) - self.log.readOnly = True - self.layout.addWidget(self.log) - self.logMessage('

        Status: Idle\n') - - # clear log button - self.clearLogButton = qt.QPushButton("Clear Log") - self.clearLogButton.toolTip = "Clear the log window." - self.layout.addWidget(self.clearLogButton) - - # TODO: warning dialog on first connect - # TODO: config option for port - # TODO: config option for optional plugins - # TODO: config option for certfile (https) - - self.advancedCollapsibleButton = ctk.ctkCollapsibleButton() - self.advancedCollapsibleButton.text = "Advanced" - self.layout.addWidget(self.advancedCollapsibleButton) - advancedFormLayout = qt.QFormLayout(self.advancedCollapsibleButton) - self.advancedCollapsibleButton.collapsed = True - - # handlers - - self.enableSlicerHandler = qt.QCheckBox() - self.enableSlicerHandler.toolTip = "Enable remote control of Slicer application (stop server to change option)" - advancedFormLayout.addRow('Slicer API: ', self.enableSlicerHandler) - - self.enableSlicerHandlerExec = qt.QCheckBox() - self.enableSlicerHandlerExec.toolTip = "Enable execution of arbitrary Python command using Slicer API. It only has effect if Slicer API is enabled, too (stop server to change option)." - advancedFormLayout.addRow('Slicer API exec: ', self.enableSlicerHandlerExec) - - self.enableDICOMHandler = qt.QCheckBox() - self.enableDICOMHandler.toolTip = "Enable serving Slicer DICOM database content via DICOMweb (stop server to change option)" - if hasattr(slicer.modules, "dicom"): - advancedFormLayout.addRow('DICOMweb API: ', self.enableDICOMHandler) - - self.enableStaticPagesHandler = qt.QCheckBox() - self.enableStaticPagesHandler.toolTip = "Enable serving static pages (stop server to change option)" - advancedFormLayout.addRow('Static pages: ', self.enableStaticPagesHandler) - - # log to console - self.logToConsole = qt.QCheckBox() - self.logToConsole.toolTip = "Copy log messages to the python console and parent terminal (disable to improve performance)" - advancedFormLayout.addRow('Log to Console: ', self.logToConsole) - - # log to GUI - self.logToGUI = qt.QCheckBox() - self.logToGUI.toolTip = "Copy log messages to the log widget (disable to improve performance)" - advancedFormLayout.addRow('Log to GUI: ', self.logToGUI) - - # Initialize GUI - self.updateGUIFromSettings() - self.updateGUIFromLogic() - - # Connections - self.startServerButton.connect('clicked(bool)', self.startServer) - self.stopServerButton.connect('clicked(bool)', self.stopServer) - self.enableSlicerHandler.connect('clicked()', self.updateHandlersFromGUI) - self.enableSlicerHandlerExec.connect('clicked()', self.updateHandlersFromGUI) - self.enableDICOMHandler.connect('clicked()', self.updateHandlersFromGUI) - self.enableStaticPagesHandler.connect('clicked()', self.updateHandlersFromGUI) - self.localConnectionButton.connect('clicked()', self.openLocalConnection) - self.localQtConnectionButton.connect('clicked()', self.openQtLocalConnection) - self.clearLogButton.connect('clicked()', self.log.clear) - self.logToConsole.connect('clicked()', self.updateLoggingFromGUI) - self.logToGUI.connect('clicked()', self.updateLoggingFromGUI) - - self.updateLoggingFromGUI() - - def startServer(self): - self.logic.requestHandlers = [] - self.logic.addDefaultRequestHandlers( - enableSlicer=self.enableSlicerHandler.checked, - enableExec=self.enableSlicerHandlerExec.checked, - enableDICOM=self.enableDICOMHandler.checked, - enableStaticPages=self.enableStaticPagesHandler.checked) - self.logic.start() - self.updateGUIFromLogic() - - def stopServer(self): - self.logic.stop() - self.updateGUIFromLogic() - - def updateGUIFromSettings(self): - self.logToConsole.checked = slicer.app.userSettings().value("WebServer/logToConsole", False) - self.logToGUI.checked = slicer.app.userSettings().value("WebServer/logToGUI", True) - self.enableSlicerHandler.checked = slicer.app.userSettings().value("WebServer/enableSlicerHandler", True) - self.enableSlicerHandlerExec.checked = slicer.app.userSettings().value("WebServer/enableSlicerHandlerExec", False) - if hasattr(slicer.modules, "dicom"): - self.enableDICOMHandler.checked = slicer.app.userSettings().value("WebServer/enableDICOMHandler", True) - else: - self.enableDICOMHandler.checked = False - self.enableStaticPagesHandler.checked = slicer.app.userSettings().value("WebServer/enableStaticPagesHandler", True) - - def updateGUIFromLogic(self): - self.startServerButton.setEnabled(not self.logic.serverStarted) - self.stopServerButton.setEnabled(self.logic.serverStarted) - - self.enableSlicerHandler.setEnabled(not self.logic.serverStarted) - self.enableSlicerHandlerExec.setEnabled(not self.logic.serverStarted) - self.enableDICOMHandler.setEnabled(not self.logic.serverStarted) - self.enableStaticPagesHandler.setEnabled(not self.logic.serverStarted) - - def updateLoggingFromGUI(self): - self.consoleMessages = self.logToConsole.checked - self.guiMessages = self.logToGUI.checked - slicer.app.userSettings().setValue("WebServer/logToConsole", self.logToConsole.checked) - slicer.app.userSettings().setValue("WebServer/logToGUI", self.logToGUI.checked) - - def updateHandlersFromGUI(self): - slicer.app.userSettings().setValue("WebServer/enableSlicerHandler", self.enableSlicerHandler.checked) - slicer.app.userSettings().setValue("WebServer/enableSlicerHandlerExec", self.enableSlicerHandlerExec.checked) - slicer.app.userSettings().setValue("WebServer/enableDICOMHandler", self.enableDICOMHandler.checked) - slicer.app.userSettings().setValue("WebServer/enableStaticPagesHandler", self.enableStaticPagesHandler.checked) - - def openLocalConnection(self): - qt.QDesktopServices.openUrl(qt.QUrl(f'http://localhost:{self.logic.port}')) - - def openQtLocalConnection(self): - self.webWidget = slicer.qSlicerWebWidget() - self.webWidget.url = f'http://localhost:{self.logic.port}' - self.webWidget.show() - - def onReload(self): - logging.debug("Reloading WebServer") - slicer._webServerStarted = self.logic.serverStarted - self.stopServer() - - packageName='WebServerLib' - submoduleNames=['SlicerRequestHandler', 'StaticPagesRequestHandler'] - if hasattr(slicer.modules, "dicom"): - submoduleNames.append('DICOMRequestHandler') - import imp - f, filename, description = imp.find_module(packageName) - package = imp.load_module(packageName, f, filename, description) - for submoduleName in submoduleNames: - f, filename, description = imp.find_module(submoduleName, package.__path__) - try: - imp.load_module(packageName+'.'+submoduleName, f, filename, description) - finally: - f.close() - - ScriptedLoadableModuleWidget.onReload(self) - - # Restart web server if it was running - if slicer._webServerStarted: - slicer.modules.WebServerWidget.startServer() - del slicer._webServerStarted - - def logMessage(self,*args): - if self.consoleMessages: - for arg in args: - print(arg) - if self.guiMessages: - if len(self.log.html) > 1024*256: - self.log.clear() - self.log.insertHtml("Log cleared\n") - for arg in args: - self.log.insertHtml(arg) - self.log.insertPlainText('\n') - self.log.ensureCursorVisible() - self.log.repaint() + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def __init__(self, parent=None): + ScriptedLoadableModuleWidget.__init__(self, parent) + self.guiMessages = True + self.consoleMessages = False + # By default, no request handlers are created, we will add them in startServer + self.logic = WebServerLogic(logMessage=self.logMessage, requestHandlers=[]) + + def enter(self): + pass + + def exit(self): + pass + + def setup(self): + ScriptedLoadableModuleWidget.setup(self) + + # start button + self.startServerButton = qt.QPushButton("Start server") + self.startServerButton.name = "StartWebServer" + self.startServerButton.toolTip = "Start web server with the selected options." + self.layout.addWidget(self.startServerButton) + + # stop button + self.stopServerButton = qt.QPushButton("Stop server") + self.stopServerButton.name = "StopWebServer" + self.stopServerButton.toolTip = "Start web server with the selected options." + self.layout.addWidget(self.stopServerButton) + + # open browser page + self.localConnectionButton = qt.QPushButton("Open static pages in external browser") + self.localConnectionButton.toolTip = "Open a connection to the server on the local machine with your system browser." + self.layout.addWidget(self.localConnectionButton) + + # open slicer widget + self.localQtConnectionButton = qt.QPushButton("Open static pages in internal browser") + self.localQtConnectionButton.toolTip = "Open a connection with Qt to the server on the local machine." + self.layout.addWidget(self.localQtConnectionButton) + + # log window + self.log = qt.QTextEdit() + self.log.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + self.log.readOnly = True + self.layout.addWidget(self.log) + self.logMessage('

        Status: Idle\n') + + # clear log button + self.clearLogButton = qt.QPushButton("Clear Log") + self.clearLogButton.toolTip = "Clear the log window." + self.layout.addWidget(self.clearLogButton) + + # TODO: warning dialog on first connect + # TODO: config option for port + # TODO: config option for optional plugins + # TODO: config option for certfile (https) + + self.advancedCollapsibleButton = ctk.ctkCollapsibleButton() + self.advancedCollapsibleButton.text = "Advanced" + self.layout.addWidget(self.advancedCollapsibleButton) + advancedFormLayout = qt.QFormLayout(self.advancedCollapsibleButton) + self.advancedCollapsibleButton.collapsed = True + + # handlers + + self.enableSlicerHandler = qt.QCheckBox() + self.enableSlicerHandler.toolTip = "Enable remote control of Slicer application (stop server to change option)" + advancedFormLayout.addRow('Slicer API: ', self.enableSlicerHandler) + + self.enableSlicerHandlerExec = qt.QCheckBox() + self.enableSlicerHandlerExec.toolTip = "Enable execution of arbitrary Python command using Slicer API. It only has effect if Slicer API is enabled, too (stop server to change option)." + advancedFormLayout.addRow('Slicer API exec: ', self.enableSlicerHandlerExec) + + self.enableDICOMHandler = qt.QCheckBox() + self.enableDICOMHandler.toolTip = "Enable serving Slicer DICOM database content via DICOMweb (stop server to change option)" + if hasattr(slicer.modules, "dicom"): + advancedFormLayout.addRow('DICOMweb API: ', self.enableDICOMHandler) + + self.enableStaticPagesHandler = qt.QCheckBox() + self.enableStaticPagesHandler.toolTip = "Enable serving static pages (stop server to change option)" + advancedFormLayout.addRow('Static pages: ', self.enableStaticPagesHandler) + + # log to console + self.logToConsole = qt.QCheckBox() + self.logToConsole.toolTip = "Copy log messages to the python console and parent terminal (disable to improve performance)" + advancedFormLayout.addRow('Log to Console: ', self.logToConsole) + + # log to GUI + self.logToGUI = qt.QCheckBox() + self.logToGUI.toolTip = "Copy log messages to the log widget (disable to improve performance)" + advancedFormLayout.addRow('Log to GUI: ', self.logToGUI) + + # Initialize GUI + self.updateGUIFromSettings() + self.updateGUIFromLogic() + + # Connections + self.startServerButton.connect('clicked(bool)', self.startServer) + self.stopServerButton.connect('clicked(bool)', self.stopServer) + self.enableSlicerHandler.connect('clicked()', self.updateHandlersFromGUI) + self.enableSlicerHandlerExec.connect('clicked()', self.updateHandlersFromGUI) + self.enableDICOMHandler.connect('clicked()', self.updateHandlersFromGUI) + self.enableStaticPagesHandler.connect('clicked()', self.updateHandlersFromGUI) + self.localConnectionButton.connect('clicked()', self.openLocalConnection) + self.localQtConnectionButton.connect('clicked()', self.openQtLocalConnection) + self.clearLogButton.connect('clicked()', self.log.clear) + self.logToConsole.connect('clicked()', self.updateLoggingFromGUI) + self.logToGUI.connect('clicked()', self.updateLoggingFromGUI) + + self.updateLoggingFromGUI() + + def startServer(self): + self.logic.requestHandlers = [] + self.logic.addDefaultRequestHandlers( + enableSlicer=self.enableSlicerHandler.checked, + enableExec=self.enableSlicerHandlerExec.checked, + enableDICOM=self.enableDICOMHandler.checked, + enableStaticPages=self.enableStaticPagesHandler.checked) + self.logic.start() + self.updateGUIFromLogic() + + def stopServer(self): + self.logic.stop() + self.updateGUIFromLogic() + + def updateGUIFromSettings(self): + self.logToConsole.checked = slicer.app.userSettings().value("WebServer/logToConsole", False) + self.logToGUI.checked = slicer.app.userSettings().value("WebServer/logToGUI", True) + self.enableSlicerHandler.checked = slicer.app.userSettings().value("WebServer/enableSlicerHandler", True) + self.enableSlicerHandlerExec.checked = slicer.app.userSettings().value("WebServer/enableSlicerHandlerExec", False) + if hasattr(slicer.modules, "dicom"): + self.enableDICOMHandler.checked = slicer.app.userSettings().value("WebServer/enableDICOMHandler", True) + else: + self.enableDICOMHandler.checked = False + self.enableStaticPagesHandler.checked = slicer.app.userSettings().value("WebServer/enableStaticPagesHandler", True) + + def updateGUIFromLogic(self): + self.startServerButton.setEnabled(not self.logic.serverStarted) + self.stopServerButton.setEnabled(self.logic.serverStarted) + + self.enableSlicerHandler.setEnabled(not self.logic.serverStarted) + self.enableSlicerHandlerExec.setEnabled(not self.logic.serverStarted) + self.enableDICOMHandler.setEnabled(not self.logic.serverStarted) + self.enableStaticPagesHandler.setEnabled(not self.logic.serverStarted) + + def updateLoggingFromGUI(self): + self.consoleMessages = self.logToConsole.checked + self.guiMessages = self.logToGUI.checked + slicer.app.userSettings().setValue("WebServer/logToConsole", self.logToConsole.checked) + slicer.app.userSettings().setValue("WebServer/logToGUI", self.logToGUI.checked) + + def updateHandlersFromGUI(self): + slicer.app.userSettings().setValue("WebServer/enableSlicerHandler", self.enableSlicerHandler.checked) + slicer.app.userSettings().setValue("WebServer/enableSlicerHandlerExec", self.enableSlicerHandlerExec.checked) + slicer.app.userSettings().setValue("WebServer/enableDICOMHandler", self.enableDICOMHandler.checked) + slicer.app.userSettings().setValue("WebServer/enableStaticPagesHandler", self.enableStaticPagesHandler.checked) + + def openLocalConnection(self): + qt.QDesktopServices.openUrl(qt.QUrl(f'http://localhost:{self.logic.port}')) + + def openQtLocalConnection(self): + self.webWidget = slicer.qSlicerWebWidget() + self.webWidget.url = f'http://localhost:{self.logic.port}' + self.webWidget.show() + + def onReload(self): + logging.debug("Reloading WebServer") + slicer._webServerStarted = self.logic.serverStarted + self.stopServer() + + packageName = 'WebServerLib' + submoduleNames = ['SlicerRequestHandler', 'StaticPagesRequestHandler'] + if hasattr(slicer.modules, "dicom"): + submoduleNames.append('DICOMRequestHandler') + import imp + f, filename, description = imp.find_module(packageName) + package = imp.load_module(packageName, f, filename, description) + for submoduleName in submoduleNames: + f, filename, description = imp.find_module(submoduleName, package.__path__) + try: + imp.load_module(packageName + '.' + submoduleName, f, filename, description) + finally: + f.close() + + ScriptedLoadableModuleWidget.onReload(self) + + # Restart web server if it was running + if slicer._webServerStarted: + slicer.modules.WebServerWidget.startServer() + del slicer._webServerStarted + + def logMessage(self, *args): + if self.consoleMessages: + for arg in args: + print(arg) + if self.guiMessages: + if len(self.log.html) > 1024 * 256: + self.log.clear() + self.log.insertHtml("Log cleared\n") + for arg in args: + self.log.insertHtml(arg) + self.log.insertPlainText('\n') + self.log.ensureCursorVisible() + self.log.repaint() # # SlicerHTTPServer @@ -248,284 +248,284 @@ def logMessage(self,*args): class SlicerHTTPServer(HTTPServer): - """ - This web server is configured to integrate with the Qt main loop - by listenting activity on the fileno of the servers socket. - """ - # TODO: set header so client knows that image refreshes are needed (avoid - # using the &time=xxx trick) - def __init__(self, server_address=("",2016), requestHandlers=None, docroot='.', logMessage=None, certfile=None): - """ - :param server_address: passed to parent class (default ("", 8070)) - :param requestHandlers: request handler objects; if not specified then Slicer, DICOM, and StaticPages handlers are registered - :param docroot: used to serve static pages content - :param logMessage: a callable for messages - :param certfile: path to a file with an ssl certificate (.pem file) - """ - HTTPServer.__init__(self,server_address, SlicerHTTPServer.DummyRequestHandler) - - self.requestHandlers = [] - - if requestHandlers is not None: - for requestHandler in requestHandlers: - self.requestHandlers.append(requestHandler) - - self.docroot = docroot - self.timeout = 1. - if certfile: - # https://stackoverflow.com/questions/19705785/python-3-simple-https-server - import ssl - self.socket = ssl.wrap_socket(self.socket, - server_side=True, - certfile=certfile, - ssl_version=ssl.PROTOCOL_TLS) - self.socket.settimeout(5.) - if logMessage: - self.logMessage = logMessage - self.notifiers = {} - self.connections = {} - self.requestCommunicators = {} - - class DummyRequestHandler(object): - pass - - class SlicerRequestCommunicator(object): """ - Encapsulate elements for handling event driven read of request. - An instance is created for each client connection to our web server. - This class handles event driven chunking of the communication. - .. note:: this is an internal class of the web server + This web server is configured to integrate with the Qt main loop + by listenting activity on the fileno of the servers socket. """ - def __init__(self, connectionSocket, requestHandlers, docroot, logMessage): - """ - :param connectionSocket: socket for this request - :param docroot: for handling static pages content - :param logMessage: callable - """ - self.connectionSocket = connectionSocket - self.docroot = docroot - self.logMessage = logMessage - self.bufferSize = 1024*1024 - self.requestHandlers = [] - for requestHandler in requestHandlers: - self.registerRequestHandler(requestHandler) - self.expectedRequestSize = -1 - self.requestSoFar = b"" - fileno = self.connectionSocket.fileno() - self.readNotifier = qt.QSocketNotifier(fileno, qt.QSocketNotifier.Read) - self.readNotifier.connect('activated(int)', self.onReadable) - self.logMessage('Waiting on %d...' % fileno) - - def registerRequestHandler(self, handler): - self.requestHandlers.append(handler) - handler.logMessage = self.logMessage - - def onReadableComplete(self): - self.logMessage("reading complete, freeing notifier") - self.readNotifier = None - - def onReadable(self, fileno): - self.logMessage('Reading...') - requestHeader = b"" - requestBody = b"" - requestComplete = False - requestPart = "" - try: - requestPart = self.connectionSocket.recv(self.bufferSize) - self.logMessage('Just received... %d bytes in this part' % len(requestPart)) - self.requestSoFar += requestPart - endOfHeader = self.requestSoFar.find(b'\r\n\r\n') - if self.expectedRequestSize > 0: - self.logMessage('received... %d of %d expected' % (len(self.requestSoFar), self.expectedRequestSize)) - if len(self.requestSoFar) >= self.expectedRequestSize: - requestHeader = self.requestSoFar[:endOfHeader+2] - requestBody = self.requestSoFar[4+endOfHeader:] - requestComplete = True - else: - if endOfHeader != -1: - self.logMessage('Looking for content in header...') - contentLengthTag = self.requestSoFar.find(b'Content-Length:') - if contentLengthTag != -1: - tag = self.requestSoFar[contentLengthTag:] - numberStartIndex = tag.find(b' ') - numberEndIndex = tag.find(b'\r\n') - contentLength = int(tag[numberStartIndex:numberEndIndex]) - self.expectedRequestSize = 4 + endOfHeader + contentLength - self.logMessage('Expecting a body of %d, total size %d' % (contentLength, self.expectedRequestSize)) - if len(requestPart) == self.expectedRequestSize: - requestHeader = requestPart[:endOfHeader+2] - requestBody = requestPart[4+endOfHeader:] + # TODO: set header so client knows that image refreshes are needed (avoid + # using the &time=xxx trick) + def __init__(self, server_address=("", 2016), requestHandlers=None, docroot='.', logMessage=None, certfile=None): + """ + :param server_address: passed to parent class (default ("", 8070)) + :param requestHandlers: request handler objects; if not specified then Slicer, DICOM, and StaticPages handlers are registered + :param docroot: used to serve static pages content + :param logMessage: a callable for messages + :param certfile: path to a file with an ssl certificate (.pem file) + """ + HTTPServer.__init__(self, server_address, SlicerHTTPServer.DummyRequestHandler) + + self.requestHandlers = [] + + if requestHandlers is not None: + for requestHandler in requestHandlers: + self.requestHandlers.append(requestHandler) + + self.docroot = docroot + self.timeout = 1. + if certfile: + # https://stackoverflow.com/questions/19705785/python-3-simple-https-server + import ssl + self.socket = ssl.wrap_socket(self.socket, + server_side=True, + certfile=certfile, + ssl_version=ssl.PROTOCOL_TLS) + self.socket.settimeout(5.) + if logMessage: + self.logMessage = logMessage + self.notifiers = {} + self.connections = {} + self.requestCommunicators = {} + + class DummyRequestHandler(object): + pass + + class SlicerRequestCommunicator(object): + """ + Encapsulate elements for handling event driven read of request. + An instance is created for each client connection to our web server. + This class handles event driven chunking of the communication. + .. note:: this is an internal class of the web server + """ + def __init__(self, connectionSocket, requestHandlers, docroot, logMessage): + """ + :param connectionSocket: socket for this request + :param docroot: for handling static pages content + :param logMessage: callable + """ + self.connectionSocket = connectionSocket + self.docroot = docroot + self.logMessage = logMessage + self.bufferSize = 1024 * 1024 + self.requestHandlers = [] + for requestHandler in requestHandlers: + self.registerRequestHandler(requestHandler) + self.expectedRequestSize = -1 + self.requestSoFar = b"" + fileno = self.connectionSocket.fileno() + self.readNotifier = qt.QSocketNotifier(fileno, qt.QSocketNotifier.Read) + self.readNotifier.connect('activated(int)', self.onReadable) + self.logMessage('Waiting on %d...' % fileno) + + def registerRequestHandler(self, handler): + self.requestHandlers.append(handler) + handler.logMessage = self.logMessage + + def onReadableComplete(self): + self.logMessage("reading complete, freeing notifier") + self.readNotifier = None + + def onReadable(self, fileno): + self.logMessage('Reading...') + requestHeader = b"" + requestBody = b"" + requestComplete = False + requestPart = "" + try: + requestPart = self.connectionSocket.recv(self.bufferSize) + self.logMessage('Just received... %d bytes in this part' % len(requestPart)) + self.requestSoFar += requestPart + endOfHeader = self.requestSoFar.find(b'\r\n\r\n') + if self.expectedRequestSize > 0: + self.logMessage('received... %d of %d expected' % (len(self.requestSoFar), self.expectedRequestSize)) + if len(self.requestSoFar) >= self.expectedRequestSize: + requestHeader = self.requestSoFar[:endOfHeader + 2] + requestBody = self.requestSoFar[4 + endOfHeader:] + requestComplete = True + else: + if endOfHeader != -1: + self.logMessage('Looking for content in header...') + contentLengthTag = self.requestSoFar.find(b'Content-Length:') + if contentLengthTag != -1: + tag = self.requestSoFar[contentLengthTag:] + numberStartIndex = tag.find(b' ') + numberEndIndex = tag.find(b'\r\n') + contentLength = int(tag[numberStartIndex:numberEndIndex]) + self.expectedRequestSize = 4 + endOfHeader + contentLength + self.logMessage('Expecting a body of %d, total size %d' % (contentLength, self.expectedRequestSize)) + if len(requestPart) == self.expectedRequestSize: + requestHeader = requestPart[:endOfHeader + 2] + requestBody = requestPart[4 + endOfHeader:] + requestComplete = True + else: + self.logMessage('Found end of header with no content, so body is empty') + requestHeader = self.requestSoFar[:-2] + requestComplete = True + except socket.error as e: + print('Socket error: ', e) + print('So far:\n', self.requestSoFar) requestComplete = True - else: - self.logMessage('Found end of header with no content, so body is empty') - requestHeader = self.requestSoFar[:-2] - requestComplete = True - except socket.error as e: - print('Socket error: ', e) - print('So far:\n', self.requestSoFar) - requestComplete = True - - if len(requestPart) == 0 or requestComplete: - self.logMessage('Got complete message of header size %d, body size %d' % (len(requestHeader), len(requestBody))) - self.readNotifier.disconnect('activated(int)', self.onReadable) - self.readNotifier.setEnabled(False) - qt.QTimer.singleShot(0, self.onReadableComplete) - - if len(self.requestSoFar) == 0: - self.logMessage("Ignoring empty request") - return - - method,uri,version = [b'GET', b'/', b'HTTP/1.1'] # defaults - requestLines = requestHeader.split(b'\r\n') - self.logMessage(requestLines[0]) + + if len(requestPart) == 0 or requestComplete: + self.logMessage('Got complete message of header size %d, body size %d' % (len(requestHeader), len(requestBody))) + self.readNotifier.disconnect('activated(int)', self.onReadable) + self.readNotifier.setEnabled(False) + qt.QTimer.singleShot(0, self.onReadableComplete) + + if len(self.requestSoFar) == 0: + self.logMessage("Ignoring empty request") + return + + method, uri, version = [b'GET', b'/', b'HTTP/1.1'] # defaults + requestLines = requestHeader.split(b'\r\n') + self.logMessage(requestLines[0]) + try: + method, uri, version = requestLines[0].split(b' ') + except ValueError as e: + self.logMessage("Could not interpret first request lines: ", requestLines) + + if requestLines == "": + self.logMessage("Assuming empty string is HTTP/1.1 GET of /.") + + if version != b"HTTP/1.1": + self.logMessage("Warning, we don't speak %s", version) + return + + # TODO: methods = ["GET", "POST", "PUT", "DELETE"] + methods = [b"GET", b"POST", b"PUT"] + if method not in methods: + self.logMessage("Warning, we only handle %s" % methods) + return + + parsedURL = urllib.parse.urlparse(uri) + request = parsedURL.path + if parsedURL.query != b"": + request += b'?' + parsedURL.query + self.logMessage('Parsing url request: ', parsedURL) + self.logMessage(' request is: %s' % request) + + highestConfidenceHandler = None + highestConfidence = 0.0 + for handler in self.requestHandlers: + confidence = handler.canHandleRequest(uri, requestBody) + if confidence > highestConfidence: + highestConfidenceHandler = handler + highestConfidence = confidence + + if highestConfidenceHandler is not None and highestConfidence > 0.0: + try: + contentType, responseBody = highestConfidenceHandler.handleRequest(uri, requestBody) + except: + etype, value, tb = sys.exc_info() + import traceback + for frame in traceback.format_tb(tb): + self.logMessage(frame) + self.logMessage(etype, value) + contentType = b'text/plain' + responseBody = b'Server error' # TODO: send correct error code in response + else: + contentType = b'text/plain' + responseBody = b'' + + if responseBody: + self.response = b"HTTP/1.1 200 OK\r\n" + self.response += b"Access-Control-Allow-Origin: *\r\n" + self.response += b"Content-Type: %s\r\n" % contentType + self.response += b"Content-Length: %d\r\n" % len(responseBody) + self.response += b"Cache-Control: no-cache\r\n" + self.response += b"\r\n" + self.response += responseBody + else: + self.response = b"HTTP/1.1 404 Not Found\r\n" + self.response += b"\r\n" + + self.toSend = len(self.response) + self.sentSoFar = 0 + fileno = self.connectionSocket.fileno() + self.writeNotifier = qt.QSocketNotifier(fileno, qt.QSocketNotifier.Write) + self.writeNotifier.connect('activated(int)', self.onWritable) + + def onWriteableComplete(self): + self.logMessage("writing complete, freeing notifier") + self.writeNotifier = None + self.connectionSocket = None + + def onWritable(self, fileno): + self.logMessage('Sending on %d...' % (fileno)) + sendError = False + try: + sent = self.connectionSocket.send(self.response[:500 * self.bufferSize]) + self.response = self.response[sent:] + self.sentSoFar += sent + self.logMessage('sent: %d (%d of %d, %f%%)' % (sent, self.sentSoFar, self.toSend, 100. * self.sentSoFar / self.toSend)) + except socket.error as e: + self.logMessage('Socket error while sending: %s' % e) + sendError = True + + if self.sentSoFar >= self.toSend or sendError: + self.writeNotifier.disconnect('activated(int)', self.onWritable) + self.writeNotifier.setEnabled(False) + qt.QTimer.singleShot(0, self.onWriteableComplete) + self.connectionSocket.close() + self.logMessage('closed fileno %d' % (fileno)) + + def onServerSocketNotify(self, fileno): + self.logMessage('got request on %d' % fileno) try: - method,uri,version = requestLines[0].split(b' ') - except ValueError as e: - self.logMessage("Could not interpret first request lines: ", requestLines) - - if requestLines == "": - self.logMessage("Assuming empty string is HTTP/1.1 GET of /.") - - if version != b"HTTP/1.1": - self.logMessage("Warning, we don't speak %s", version) - return - - # TODO: methods = ["GET", "POST", "PUT", "DELETE"] - methods = [b"GET", b"POST", b"PUT"] - if not method in methods: - self.logMessage("Warning, we only handle %s" % methods) - return - - parsedURL = urllib.parse.urlparse(uri) - request = parsedURL.path - if parsedURL.query != b"": - request += b'?' + parsedURL.query - self.logMessage('Parsing url request: ', parsedURL) - self.logMessage(' request is: %s' % request) - - highestConfidenceHandler = None - highestConfidence = 0.0 - for handler in self.requestHandlers: - confidence = handler.canHandleRequest(uri, requestBody) - if confidence > highestConfidence: - highestConfidenceHandler = handler - highestConfidence = confidence - - if highestConfidenceHandler is not None and highestConfidence > 0.0: - try: - contentType, responseBody = highestConfidenceHandler.handleRequest(uri, requestBody) - except: - etype, value, tb = sys.exc_info() - import traceback - for frame in traceback.format_tb(tb): - self.logMessage(frame) - self.logMessage(etype, value) - contentType = b'text/plain' - responseBody = b'Server error' # TODO: send correct error code in response - else: - contentType = b'text/plain' - responseBody = b'' - - if responseBody: - self.response = b"HTTP/1.1 200 OK\r\n" - self.response += b"Access-Control-Allow-Origin: *\r\n" - self.response += b"Content-Type: %s\r\n" % contentType - self.response += b"Content-Length: %d\r\n" % len(responseBody) - self.response += b"Cache-Control: no-cache\r\n" - self.response += b"\r\n" - self.response += responseBody - else: - self.response = b"HTTP/1.1 404 Not Found\r\n" - self.response += b"\r\n" - - self.toSend = len(self.response) - self.sentSoFar = 0 - fileno = self.connectionSocket.fileno() - self.writeNotifier = qt.QSocketNotifier(fileno, qt.QSocketNotifier.Write) - self.writeNotifier.connect('activated(int)', self.onWritable) - - def onWriteableComplete(self): - self.logMessage("writing complete, freeing notifier") - self.writeNotifier = None - self.connectionSocket = None - - def onWritable(self, fileno): - self.logMessage('Sending on %d...' % (fileno)) - sendError = False - try: - sent = self.connectionSocket.send(self.response[:500*self.bufferSize]) - self.response = self.response[sent:] - self.sentSoFar += sent - self.logMessage('sent: %d (%d of %d, %f%%)' % (sent, self.sentSoFar, self.toSend, 100.*self.sentSoFar / self.toSend)) - except socket.error as e: - self.logMessage('Socket error while sending: %s' % e) - sendError = True - - if self.sentSoFar >= self.toSend or sendError: - self.writeNotifier.disconnect('activated(int)', self.onWritable) - self.writeNotifier.setEnabled(False) - qt.QTimer.singleShot(0, self.onWriteableComplete) - self.connectionSocket.close() - self.logMessage('closed fileno %d' % (fileno)) - - def onServerSocketNotify(self,fileno): - self.logMessage('got request on %d' % fileno) - try: - (connectionSocket, clientAddress) = self.socket.accept() - fileno = connectionSocket.fileno() - self.requestCommunicators[fileno] = self.SlicerRequestCommunicator(connectionSocket, self.requestHandlers, self.docroot, self.logMessage) - self.logMessage('Connected on %s fileno %d' % (connectionSocket, connectionSocket.fileno())) - except socket.error as e: - self.logMessage('Socket Error', socket.error, e) - - def start(self): - """start the server - Uses one thread since we are event driven - """ - try: - self.logMessage('started httpserver...') - self.notifier = qt.QSocketNotifier(self.socket.fileno(),qt.QSocketNotifier.Read) - self.logMessage('listening on %d...' % self.socket.fileno()) - self.notifier.connect('activated(int)', self.onServerSocketNotify) - - except KeyboardInterrupt: - self.logMessage('KeyboardInterrupt - stopping') - self.stop() - - def stop(self): - self.socket.close() - if self.notifier: - self.notifier.disconnect('activated(int)', self.onServerSocketNotify) - self.notifier = None - - def handle_error(self, request, client_address): - """Handle an error gracefully. May be overridden. - - The default is to print a traceback and continue. - """ - print ('-'*40) - print ('Exception happened during processing of request', request) - print ('From', client_address) - import traceback - traceback.print_exc() # XXX But this goes to stderr! - print ('-'*40) - - @classmethod - def findFreePort(self,port=2016): - """returns a port that is not apparently in use""" - portFree = False - while not portFree: - try: - s = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) - s.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) - s.bind( ( "", port ) ) - except socket.error as e: + (connectionSocket, clientAddress) = self.socket.accept() + fileno = connectionSocket.fileno() + self.requestCommunicators[fileno] = self.SlicerRequestCommunicator(connectionSocket, self.requestHandlers, self.docroot, self.logMessage) + self.logMessage('Connected on %s fileno %d' % (connectionSocket, connectionSocket.fileno())) + except socket.error as e: + self.logMessage('Socket Error', socket.error, e) + + def start(self): + """start the server + Uses one thread since we are event driven + """ + try: + self.logMessage('started httpserver...') + self.notifier = qt.QSocketNotifier(self.socket.fileno(), qt.QSocketNotifier.Read) + self.logMessage('listening on %d...' % self.socket.fileno()) + self.notifier.connect('activated(int)', self.onServerSocketNotify) + + except KeyboardInterrupt: + self.logMessage('KeyboardInterrupt - stopping') + self.stop() + + def stop(self): + self.socket.close() + if self.notifier: + self.notifier.disconnect('activated(int)', self.onServerSocketNotify) + self.notifier = None + + def handle_error(self, request, client_address): + """Handle an error gracefully. May be overridden. + + The default is to print a traceback and continue. + """ + print('-' * 40) + print('Exception happened during processing of request', request) + print('From', client_address) + import traceback + traceback.print_exc() # XXX But this goes to stderr! + print('-' * 40) + + @classmethod + def findFreePort(self, port=2016): + """returns a port that is not apparently in use""" portFree = False - port += 1 - finally: - s.close() - portFree = True - return port + while not portFree: + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("", port)) + except socket.error as e: + portFree = False + port += 1 + finally: + s.close() + portFree = True + return port # @@ -533,69 +533,69 @@ def findFreePort(self,port=2016): # class WebServerLogic: - """Include a concrete subclass of SimpleHTTPServer - that speaks slicer. - If requestHandlers is not specified then default request handlers are added, - controlled by enableSlicer, enableDICOM, enableStaticPages arguments (all True by default). - Exec interface is enabled it enableExec and enableSlicer are both set to True - (enableExec is set to False by default for improved security). - """ - def __init__(self, port=None, enableSlicer=True, enableExec=False, enableDICOM=True, enableStaticPages=True, requestHandlers=None, logMessage=None): - if logMessage: - self.logMessage = logMessage - - if port: - self.port = port - else: - self.port = 2016 - - self.server = None - self.serverStarted = False - - moduleDirectory = os.path.dirname(slicer.modules.webserver.path.encode()) - self.docroot = moduleDirectory + b"/Resources/docroot" - - self.requestHandlers = [] - if requestHandlers is None: - # No custom request handlers are specified, use the defaults - self.addDefaultRequestHandlers(enableSlicer, enableExec, enableDICOM, enableStaticPages) - else: - # Use the specified custom request handlers - for requestHandler in requestHandlers: - self.requestHandlers.append(requestHandler) - - def addDefaultRequestHandlers(self, enableSlicer=True, enableExec=False, enableDICOM=True, enableStaticPages=True): - if enableSlicer: - from WebServerLib import SlicerRequestHandler - self.requestHandlers.append(SlicerRequestHandler(enableExec)) - if enableDICOM: - from WebServerLib import DICOMRequestHandler - self.requestHandlers.append(DICOMRequestHandler()) - if enableStaticPages: - from WebServerLib import StaticPagesRequestHandler - self.requestHandlers.append(StaticPagesRequestHandler(self.docroot)) - - def logMessage(self, *args): - logging.debug(args) - - def start(self): - """Set up the server""" - self.stop() - self.port = SlicerHTTPServer.findFreePort(self.port) - self.logMessage("Starting server on port %d" % self.port) - self.logMessage('docroot: %s' % self.docroot) - # example: certfile = '/Users/pieper/slicer/latest/SlicerWeb/localhost.pem' - certfile = None - self.server = SlicerHTTPServer(requestHandlers=self.requestHandlers, - docroot=self.docroot, - server_address=("",self.port), - logMessage=self.logMessage, - certfile=certfile) - self.server.start() - self.serverStarted = True - - def stop(self): - if self.server: - self.server.stop() - self.serverStarted = False - self.logMessage("Server stopped.") + """Include a concrete subclass of SimpleHTTPServer + that speaks slicer. + If requestHandlers is not specified then default request handlers are added, + controlled by enableSlicer, enableDICOM, enableStaticPages arguments (all True by default). + Exec interface is enabled it enableExec and enableSlicer are both set to True + (enableExec is set to False by default for improved security). + """ + def __init__(self, port=None, enableSlicer=True, enableExec=False, enableDICOM=True, enableStaticPages=True, requestHandlers=None, logMessage=None): + if logMessage: + self.logMessage = logMessage + + if port: + self.port = port + else: + self.port = 2016 + + self.server = None + self.serverStarted = False + + moduleDirectory = os.path.dirname(slicer.modules.webserver.path.encode()) + self.docroot = moduleDirectory + b"/Resources/docroot" + + self.requestHandlers = [] + if requestHandlers is None: + # No custom request handlers are specified, use the defaults + self.addDefaultRequestHandlers(enableSlicer, enableExec, enableDICOM, enableStaticPages) + else: + # Use the specified custom request handlers + for requestHandler in requestHandlers: + self.requestHandlers.append(requestHandler) + + def addDefaultRequestHandlers(self, enableSlicer=True, enableExec=False, enableDICOM=True, enableStaticPages=True): + if enableSlicer: + from WebServerLib import SlicerRequestHandler + self.requestHandlers.append(SlicerRequestHandler(enableExec)) + if enableDICOM: + from WebServerLib import DICOMRequestHandler + self.requestHandlers.append(DICOMRequestHandler()) + if enableStaticPages: + from WebServerLib import StaticPagesRequestHandler + self.requestHandlers.append(StaticPagesRequestHandler(self.docroot)) + + def logMessage(self, *args): + logging.debug(args) + + def start(self): + """Set up the server""" + self.stop() + self.port = SlicerHTTPServer.findFreePort(self.port) + self.logMessage("Starting server on port %d" % self.port) + self.logMessage('docroot: %s' % self.docroot) + # example: certfile = '/Users/pieper/slicer/latest/SlicerWeb/localhost.pem' + certfile = None + self.server = SlicerHTTPServer(requestHandlers=self.requestHandlers, + docroot=self.docroot, + server_address=("", self.port), + logMessage=self.logMessage, + certfile=certfile) + self.server.start() + self.serverStarted = True + + def stop(self): + if self.server: + self.server.stop() + self.serverStarted = False + self.logMessage("Server stopped.") diff --git a/Modules/Scripted/WebServer/WebServerLib/DICOMRequestHandler.py b/Modules/Scripted/WebServer/WebServerLib/DICOMRequestHandler.py index aed90199f62..82c3a3d9747 100644 --- a/Modules/Scripted/WebServer/WebServerLib/DICOMRequestHandler.py +++ b/Modules/Scripted/WebServer/WebServerLib/DICOMRequestHandler.py @@ -6,207 +6,207 @@ class DICOMRequestHandler(object): - """ - Implements the mapping between DICOMweb endpoints - and ctkDICOMDatabase api calls. - TODO: only a subset of api calls supported, but enough to server a viewer app (ohif) - """ - - def __init__(self): """ - :param logMessage: callable to log messages + Implements the mapping between DICOMweb endpoints + and ctkDICOMDatabase api calls. + TODO: only a subset of api calls supported, but enough to server a viewer app (ohif) """ - self.retrieveURLTag = pydicom.tag.Tag(0x00080190) - self.numberOfStudyRelatedSeriesTag = pydicom.tag.Tag(0x00200206) - self.numberOfStudyRelatedInstancesTag = pydicom.tag.Tag(0x00200208) - def logMessage(self, *args): - logging.debug(args) + def __init__(self): + """ + :param logMessage: callable to log messages + """ + self.retrieveURLTag = pydicom.tag.Tag(0x00080190) + self.numberOfStudyRelatedSeriesTag = pydicom.tag.Tag(0x00200206) + self.numberOfStudyRelatedInstancesTag = pydicom.tag.Tag(0x00200208) - def canHandleRequest(self, uri, requestBody): - parsedURL = urllib.parse.urlparse(uri) - return 0.5 if parsedURL.path.startswith(b'/dicom') else 0.0 + def logMessage(self, *args): + logging.debug(args) - def handleRequest(self, uri, requestBody): - """ - Dispatches various dicom requests - :param parsedURL: the REST path and arguments - :param requestBody: the binary that came with the request - """ - parsedURL = urllib.parse.urlparse(uri) - contentType = b'text/plain' - responseBody = None - splitPath = parsedURL.path.split(b'/') - if len(splitPath) > 4 and splitPath[4].startswith(b"series"): - self.logMessage("handling series") - contentType, responseBody = self.handleSeries(parsedURL, requestBody) - elif len(splitPath) > 2 and splitPath[2].startswith(b"studies"): - self.logMessage('handling studies') - contentType, responseBody = self.handleStudies(parsedURL, requestBody) - else: - self.logMessage('Looks like wadouri %s' % parsedURL.query) - contentType, responseBody = self.handleWADOURI(parsedURL, requestBody) - return contentType, responseBody + def canHandleRequest(self, uri, requestBody): + parsedURL = urllib.parse.urlparse(uri) + return 0.5 if parsedURL.path.startswith(b'/dicom') else 0.0 - def handleStudies(self, parsedURL, requestBody): - """ - Handle study requests by returning json - :param parsedURL: the REST path and arguments - :param requestBody: the binary that came with the request - """ - contentType = b'application/json' - splitPath = parsedURL.path.split(b'/') - offset = 0 - limit = 100 - params = parsedURL.query.split(b"&") - for param in params: - if param.split(b"=")[0] == b"offset": - offset = int(param.split(b"=")[1]) - if param.split(b"=")[0] == b"limit": - limit = int(param.split(b"=")[1]) - studyCount = 0 - responseBody = b"[{}]" - if len(splitPath) == 3: - # studies qido search - representativeSeries = None - studyResponseString = b"[" - for patient in slicer.dicomDatabase.patients(): - if studyCount > offset + limit: - break - for study in slicer.dicomDatabase.studiesForPatient(patient): - studyCount += 1 - if studyCount < offset: - continue - if studyCount > offset + limit: - break - series = slicer.dicomDatabase.seriesForStudy(study) - numberOfStudyRelatedSeries = len(series) - numberOfStudyRelatedInstances = 0 - modalitiesInStudy = set() - for serie in series: - seriesInstances = slicer.dicomDatabase.instancesForSeries(serie) - numberOfStudyRelatedInstances += len(seriesInstances) - if len(seriesInstances) > 0: - representativeSeries = serie - try: - dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(seriesInstances[0]), stop_before_pixels=True) - modalitiesInStudy.add(dataset.Modality) - except AttributeError as e: - print('Could not get instance information for %s' % seriesInstances[0]) - print(e) - if representativeSeries is None: - print('Could not find any instances for study %s' % study) - continue - instances = slicer.dicomDatabase.instancesForSeries(representativeSeries) - firstInstance = instances[0] - dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(firstInstance), stop_before_pixels=True) - studyDataset = pydicom.dataset.Dataset() - studyDataset.SpecificCharacterSet = [u'ISO_IR 100'] - studyDataset.StudyDate = dataset.StudyDate - studyDataset.StudyTime = dataset.StudyTime - studyDataset.StudyDescription = dataset.StudyDescription - studyDataset.StudyInstanceUID = dataset.StudyInstanceUID - studyDataset.AccessionNumber = dataset.AccessionNumber - studyDataset.InstanceAvailability = u'ONLINE' - studyDataset.ModalitiesInStudy = list(modalitiesInStudy) - studyDataset.ReferringPhysicianName = dataset.ReferringPhysicianName - studyDataset[self.retrieveURLTag] = pydicom.dataelem.DataElement( - 0x00080190, "UR", "http://example.com") #TODO: provide WADO-RS RetrieveURL - studyDataset.PatientName = dataset.PatientName - studyDataset.PatientID = dataset.PatientID - studyDataset.PatientBirthDate = dataset.PatientBirthDate - studyDataset.PatientSex = dataset.PatientSex - studyDataset.StudyID = dataset.StudyID - studyDataset[self.numberOfStudyRelatedSeriesTag] = pydicom.dataelem.DataElement( - self.numberOfStudyRelatedSeriesTag, "IS", str(numberOfStudyRelatedSeries)) - studyDataset[self.numberOfStudyRelatedInstancesTag] = pydicom.dataelem.DataElement( - self.numberOfStudyRelatedInstancesTag, "IS", str(numberOfStudyRelatedInstances)) - jsonDataset = studyDataset.to_json(studyDataset) - studyResponseString += jsonDataset.encode() + b"," - if studyResponseString.endswith(b','): - studyResponseString = studyResponseString[:-1] - studyResponseString += b']' - responseBody = studyResponseString - elif splitPath[4] == b'metadata': - self.logMessage('returning metadata') - contentType = b'application/json' - responseBody = b"[" - studyUID = splitPath[3].decode() - series = slicer.dicomDatabase.seriesForStudy(studyUID) - for serie in series: - seriesInstances = slicer.dicomDatabase.instancesForSeries(serie) - for instance in seriesInstances: - dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(instance), stop_before_pixels=True) - jsonDataset = dataset.to_json() - responseBody += jsonDataset.encode() + b"," - if responseBody.endswith(b','): - responseBody = responseBody[:-1] - responseBody += b']' - return contentType, responseBody + def handleRequest(self, uri, requestBody): + """ + Dispatches various dicom requests + :param parsedURL: the REST path and arguments + :param requestBody: the binary that came with the request + """ + parsedURL = urllib.parse.urlparse(uri) + contentType = b'text/plain' + responseBody = None + splitPath = parsedURL.path.split(b'/') + if len(splitPath) > 4 and splitPath[4].startswith(b"series"): + self.logMessage("handling series") + contentType, responseBody = self.handleSeries(parsedURL, requestBody) + elif len(splitPath) > 2 and splitPath[2].startswith(b"studies"): + self.logMessage('handling studies') + contentType, responseBody = self.handleStudies(parsedURL, requestBody) + else: + self.logMessage('Looks like wadouri %s' % parsedURL.query) + contentType, responseBody = self.handleWADOURI(parsedURL, requestBody) + return contentType, responseBody - def handleSeries(self, parsedURL, requestBody): - """ - Handle series requests by returning json - :param parsedURL: the REST path and arguments - :param requestBody: the binary that came with the request - """ - contentType = b'application/json' - splitPath = parsedURL.path.split(b'/') - responseBody = b"[{}]" - if len(splitPath) == 5: - # series qido search - studyUID = splitPath[-2].decode() - seriesResponseString = b"[" - series = slicer.dicomDatabase.seriesForStudy(studyUID) - for serie in series: - instances = slicer.dicomDatabase.instancesForSeries(serie, 1) - firstInstance = instances[0] - dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(firstInstance), stop_before_pixels=True) - seriesDataset = pydicom.dataset.Dataset() - seriesDataset.SpecificCharacterSet = [u'ISO_IR 100'] - seriesDataset.Modality = dataset.Modality - seriesDataset.SeriesInstanceUID = dataset.SeriesInstanceUID - seriesDataset.SeriesNumber = dataset.SeriesNumber - if hasattr(dataset, "PerformedProcedureStepStartDate"): - seriesDataset.PerformedProcedureStepStartDate = dataset.PerformedProcedureStepStartDate - if hasattr(dataset, "PerformedProcedureStepStartTime"): - seriesDataset.PerformedProcedureStepStartTime = dataset.PerformedProcedureStepStartTime - jsonDataset = seriesDataset.to_json(seriesDataset) - seriesResponseString += jsonDataset.encode() + b"," - if seriesResponseString.endswith(b','): - seriesResponseString = seriesResponseString[:-1] - seriesResponseString += b']' - responseBody = seriesResponseString - elif len(splitPath) == 7 and splitPath[6] == b'metadata': - self.logMessage('returning series metadata') - contentType = b'application/json' - responseBody = b"[" - seriesUID = splitPath[5].decode() - seriesInstances = slicer.dicomDatabase.instancesForSeries(seriesUID) - for instance in seriesInstances: - dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(instance), stop_before_pixels=True) - jsonDataset = dataset.to_json() - responseBody += jsonDataset.encode() + b"," - if responseBody.endswith(b','): - responseBody = responseBody[:-1] - responseBody += b']' - return contentType, responseBody + def handleStudies(self, parsedURL, requestBody): + """ + Handle study requests by returning json + :param parsedURL: the REST path and arguments + :param requestBody: the binary that came with the request + """ + contentType = b'application/json' + splitPath = parsedURL.path.split(b'/') + offset = 0 + limit = 100 + params = parsedURL.query.split(b"&") + for param in params: + if param.split(b"=")[0] == b"offset": + offset = int(param.split(b"=")[1]) + if param.split(b"=")[0] == b"limit": + limit = int(param.split(b"=")[1]) + studyCount = 0 + responseBody = b"[{}]" + if len(splitPath) == 3: + # studies qido search + representativeSeries = None + studyResponseString = b"[" + for patient in slicer.dicomDatabase.patients(): + if studyCount > offset + limit: + break + for study in slicer.dicomDatabase.studiesForPatient(patient): + studyCount += 1 + if studyCount < offset: + continue + if studyCount > offset + limit: + break + series = slicer.dicomDatabase.seriesForStudy(study) + numberOfStudyRelatedSeries = len(series) + numberOfStudyRelatedInstances = 0 + modalitiesInStudy = set() + for serie in series: + seriesInstances = slicer.dicomDatabase.instancesForSeries(serie) + numberOfStudyRelatedInstances += len(seriesInstances) + if len(seriesInstances) > 0: + representativeSeries = serie + try: + dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(seriesInstances[0]), stop_before_pixels=True) + modalitiesInStudy.add(dataset.Modality) + except AttributeError as e: + print('Could not get instance information for %s' % seriesInstances[0]) + print(e) + if representativeSeries is None: + print('Could not find any instances for study %s' % study) + continue + instances = slicer.dicomDatabase.instancesForSeries(representativeSeries) + firstInstance = instances[0] + dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(firstInstance), stop_before_pixels=True) + studyDataset = pydicom.dataset.Dataset() + studyDataset.SpecificCharacterSet = [u'ISO_IR 100'] + studyDataset.StudyDate = dataset.StudyDate + studyDataset.StudyTime = dataset.StudyTime + studyDataset.StudyDescription = dataset.StudyDescription + studyDataset.StudyInstanceUID = dataset.StudyInstanceUID + studyDataset.AccessionNumber = dataset.AccessionNumber + studyDataset.InstanceAvailability = u'ONLINE' + studyDataset.ModalitiesInStudy = list(modalitiesInStudy) + studyDataset.ReferringPhysicianName = dataset.ReferringPhysicianName + studyDataset[self.retrieveURLTag] = pydicom.dataelem.DataElement( + 0x00080190, "UR", "http://example.com") # TODO: provide WADO-RS RetrieveURL + studyDataset.PatientName = dataset.PatientName + studyDataset.PatientID = dataset.PatientID + studyDataset.PatientBirthDate = dataset.PatientBirthDate + studyDataset.PatientSex = dataset.PatientSex + studyDataset.StudyID = dataset.StudyID + studyDataset[self.numberOfStudyRelatedSeriesTag] = pydicom.dataelem.DataElement( + self.numberOfStudyRelatedSeriesTag, "IS", str(numberOfStudyRelatedSeries)) + studyDataset[self.numberOfStudyRelatedInstancesTag] = pydicom.dataelem.DataElement( + self.numberOfStudyRelatedInstancesTag, "IS", str(numberOfStudyRelatedInstances)) + jsonDataset = studyDataset.to_json(studyDataset) + studyResponseString += jsonDataset.encode() + b"," + if studyResponseString.endswith(b','): + studyResponseString = studyResponseString[:-1] + studyResponseString += b']' + responseBody = studyResponseString + elif splitPath[4] == b'metadata': + self.logMessage('returning metadata') + contentType = b'application/json' + responseBody = b"[" + studyUID = splitPath[3].decode() + series = slicer.dicomDatabase.seriesForStudy(studyUID) + for serie in series: + seriesInstances = slicer.dicomDatabase.instancesForSeries(serie) + for instance in seriesInstances: + dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(instance), stop_before_pixels=True) + jsonDataset = dataset.to_json() + responseBody += jsonDataset.encode() + b"," + if responseBody.endswith(b','): + responseBody = responseBody[:-1] + responseBody += b']' + return contentType, responseBody - def handleWADOURI(self, parsedURL, requestBody): - """ - Handle wado uri by returning the binary part10 contents of the dicom file - :param parsedURL: the REST path and arguments - :param requestBody: the binary that came with the request - """ - q = urllib.parse.parse_qs(parsedURL.query) - try: - instanceUID = q[b'objectUID'][0].decode().strip() - except KeyError: - return None,None - self.logMessage('found uid %s' % instanceUID) - contentType = b'application/dicom' - path = slicer.dicomDatabase.fileForInstance(instanceUID) - fp = open(path, 'rb') - responseBody = fp.read() - fp.close() - return contentType, responseBody + def handleSeries(self, parsedURL, requestBody): + """ + Handle series requests by returning json + :param parsedURL: the REST path and arguments + :param requestBody: the binary that came with the request + """ + contentType = b'application/json' + splitPath = parsedURL.path.split(b'/') + responseBody = b"[{}]" + if len(splitPath) == 5: + # series qido search + studyUID = splitPath[-2].decode() + seriesResponseString = b"[" + series = slicer.dicomDatabase.seriesForStudy(studyUID) + for serie in series: + instances = slicer.dicomDatabase.instancesForSeries(serie, 1) + firstInstance = instances[0] + dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(firstInstance), stop_before_pixels=True) + seriesDataset = pydicom.dataset.Dataset() + seriesDataset.SpecificCharacterSet = [u'ISO_IR 100'] + seriesDataset.Modality = dataset.Modality + seriesDataset.SeriesInstanceUID = dataset.SeriesInstanceUID + seriesDataset.SeriesNumber = dataset.SeriesNumber + if hasattr(dataset, "PerformedProcedureStepStartDate"): + seriesDataset.PerformedProcedureStepStartDate = dataset.PerformedProcedureStepStartDate + if hasattr(dataset, "PerformedProcedureStepStartTime"): + seriesDataset.PerformedProcedureStepStartTime = dataset.PerformedProcedureStepStartTime + jsonDataset = seriesDataset.to_json(seriesDataset) + seriesResponseString += jsonDataset.encode() + b"," + if seriesResponseString.endswith(b','): + seriesResponseString = seriesResponseString[:-1] + seriesResponseString += b']' + responseBody = seriesResponseString + elif len(splitPath) == 7 and splitPath[6] == b'metadata': + self.logMessage('returning series metadata') + contentType = b'application/json' + responseBody = b"[" + seriesUID = splitPath[5].decode() + seriesInstances = slicer.dicomDatabase.instancesForSeries(seriesUID) + for instance in seriesInstances: + dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(instance), stop_before_pixels=True) + jsonDataset = dataset.to_json() + responseBody += jsonDataset.encode() + b"," + if responseBody.endswith(b','): + responseBody = responseBody[:-1] + responseBody += b']' + return contentType, responseBody + + def handleWADOURI(self, parsedURL, requestBody): + """ + Handle wado uri by returning the binary part10 contents of the dicom file + :param parsedURL: the REST path and arguments + :param requestBody: the binary that came with the request + """ + q = urllib.parse.parse_qs(parsedURL.query) + try: + instanceUID = q[b'objectUID'][0].decode().strip() + except KeyError: + return None, None + self.logMessage('found uid %s' % instanceUID) + contentType = b'application/dicom' + path = slicer.dicomDatabase.fileForInstance(instanceUID) + fp = open(path, 'rb') + responseBody = fp.read() + fp.close() + return contentType, responseBody diff --git a/Modules/Scripted/WebServer/WebServerLib/SlicerRequestHandler.py b/Modules/Scripted/WebServer/WebServerLib/SlicerRequestHandler.py index a9407dbc6e2..9e5f9f447be 100644 --- a/Modules/Scripted/WebServer/WebServerLib/SlicerRequestHandler.py +++ b/Modules/Scripted/WebServer/WebServerLib/SlicerRequestHandler.py @@ -13,465 +13,465 @@ class SlicerRequestHandler(object): - """Implements the Slicer REST api""" - - def __init__(self, enableExec=False): - self.enableExec = enableExec - - def logMessage(self, *args): - logging.debug(args) - - def canHandleRequest(self, uri, requestBody): - parsedURL = urllib.parse.urlparse(uri) - pathParts = os.path.split(parsedURL.path) # path is like /slicer/timeimage - route = pathParts[0] - return 0.5 if route.startswith(b'/slicer') else 0.0 - - def handleRequest(self, uri, requestBody): - """Handle a slicer api request. - TODO: better routing (add routing plugins) - :param request: request portion of the URL - :param requestBody: binary data that came with request - :return: tuple of (mime) type and responseBody (binary) - """ - parsedURL = urllib.parse.urlparse(uri) - request = parsedURL.path - request = request[len(b'/slicer'):] - if parsedURL.query != b"": - request += b'?' + parsedURL.query - self.logMessage(' request is: %s' % request) - - responseBody = None - contentType = b'text/plain' - try: - if self.enableExec and request.find(b'/exec') == 0: - responseBody, contentType = self.exec(request, requestBody) - elif request.find(b'/timeimage') == 0: - responseBody, contentType = self.timeimage(request) - elif request.find(b'/gui') == 0: - responseBody, contentType = self.gui(request) - elif request.find(b'/screenshot') == 0: - responseBody, contentType = self.screenshot(request) - elif request.find(b'/slice') == 0: - responseBody, contentType = self.slice(request) - elif request.find(b'/threeD') == 0: - responseBody, contentType = self.threeD(request) - elif request.find(b'/mrml') == 0: - responseBody, contentType = self.mrml(request) - elif request.find(b'/tracking') == 0: - responseBody, contentType = self.tracking(request) - elif request.find(b'/sampledata') == 0: - responseBody, contentType = self.sampleData(request) - elif request.find(b'/volumeSelection') == 0: - responseBody, contentType = self.volumeSelection(request) - elif request.find(b'/volumes') == 0: - responseBody, contentType = self.volumes(request, requestBody) - elif request.find(b'/volume') == 0: - responseBody, contentType = self.volume(request, requestBody) - elif request.find(b'/gridTransforms') == 0: - responseBody, contentType = self.gridTransforms(request, requestBody) - elif request.find(b'/gridTransform') == 0: - responseBody, contentType = self.gridTransform(request, requestBody) - print("responseBody", len(responseBody)) - elif request.find(b'/fiducials') == 0: - responseBody, contentType = self.fiducials(request, requestBody) - elif request.find(b'/fiducial') == 0: - responseBody, contentType = self.fiducial(request, requestBody) - elif request.find(b'/accessDICOMwebStudy') == 0: - responseBody, contentType = self.accessDICOMwebStudy(request, requestBody) - else: - responseBody = b"unknown command \"" + request + b"\"" - except: - self.logMessage("Could not handle slicer command: %s" % request) - etype, value, tb = sys.exc_info() - import traceback - self.logMessage(etype, value) - self.logMessage(traceback.format_tb(tb)) - print(etype, value) - print(traceback.format_tb(tb)) - for frame in traceback.format_tb(tb): - print(frame) - return contentType, responseBody - - def exec(self,request, requestBody): - """ - Implements the Read Eval Print Loop for python code. - :param source: python code to run - :return: result of code running as json string (from the content of the - `dict` object set into the `__execResult` variable) - example: -curl -X POST localhost:2016/slicer/exec --data "slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)" - """ - self.logMessage('exec with body %s' % requestBody) - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - if requestBody: - source = requestBody - else: - try: - source = urllib.parse.unquote(q['source'][0]) - except KeyError: - self.logMessage('need to supply source code to run') - return "", b'text/plain' - self.logMessage('will run %s' % source) - exec("__execResult = {}", globals()) - exec(source, globals()) - result = json.dumps(eval("__execResult", globals())).encode() - self.logMessage('result: %s' % result) - return result, b'application/json' - - def setupMRMLTracking(self): - """ - For the tracking endpoint this creates a kind of 'cursor' in the scene. - Adds "trackingDevice" (model node) to self. - """ - if not hasattr(self, "trackingDevice"): - """ set up the mrml parts or use existing """ - nodes = slicer.mrmlScene.GetNodesByName('trackingDevice') - if nodes.GetNumberOfItems() > 0: - self.trackingDevice = nodes.GetItemAsObject(0) - nodes = slicer.mrmlScene.GetNodesByName('tracker') - self.tracker = nodes.GetItemAsObject(0) - else: - # trackingDevice cursor - self.cube = vtk.vtkCubeSource() - self.cube.SetXLength(30) - self.cube.SetYLength(70) - self.cube.SetZLength(5) - self.cube.Update() - # display node - self.modelDisplay = slicer.vtkMRMLModelDisplayNode() - self.modelDisplay.SetColor(1,1,0) # yellow - slicer.mrmlScene.AddNode(self.modelDisplay) - # self.modelDisplay.SetPolyData(self.cube.GetOutputPort()) - # Create model node - self.trackingDevice = slicer.vtkMRMLModelNode() - self.trackingDevice.SetScene(slicer.mrmlScene) - self.trackingDevice.SetName("trackingDevice") - self.trackingDevice.SetAndObservePolyData(self.cube.GetOutputDataObject(0)) - self.trackingDevice.SetAndObserveDisplayNodeID(self.modelDisplay.GetID()) - slicer.mrmlScene.AddNode(self.trackingDevice) - # tracker - self.tracker = slicer.vtkMRMLLinearTransformNode() - self.tracker.SetName('tracker') - slicer.mrmlScene.AddNode(self.tracker) - self.trackingDevice.SetAndObserveTransformNodeID(self.tracker.GetID()) - - def tracking(self,request): - """ - Send the matrix for a tracked object in the scene - :param m: 4x4 tracker matrix in column major order (position is last row) - :param q: quaternion in WXYZ order - :param p: position (last column of transform) - Matrix is overwritten if position or quaternion are provided - """ - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - self.logMessage (q) - try: - transformMatrix = list(map(float,q['m'][0].split(','))) - except KeyError: - transformMatrix = None - try: - quaternion = list(map(float,q['q'][0].split(','))) - except KeyError: - quaternion = None - try: - position = list(map(float,q['p'][0].split(','))) - except KeyError: - position = None - - self.setupMRMLTracking() - m = vtk.vtkMatrix4x4() - self.tracker.GetMatrixTransformToParent(m) - - if transformMatrix: - for row in range(3): - for column in range(3): - m.SetElement(row,column, transformMatrix[3*row+column]) - m.SetElement(row,column, transformMatrix[3*row+column]) - m.SetElement(row,column, transformMatrix[3*row+column]) - m.SetElement(row,column, transformMatrix[3*row+column]) - - if position: - for row in range(3): - m.SetElement(row,3, position[row]) - - if quaternion: - qu = vtk.vtkQuaternion['float64']() - qu.SetW(quaternion[0]) - qu.SetX(quaternion[1]) - qu.SetY(quaternion[2]) - qu.SetZ(quaternion[3]) - m3 = [[0,0,0],[0,0,0],[0,0,0]] - qu.ToMatrix3x3(m3) - for row in range(3): - for column in range(3): - m.SetElement(row,column, m3[row][column]) - - self.tracker.SetMatrixTransformToParent(m) - - return ( f"Set matrix".encode() ), b'text/plain' - - def sampleData(self, request): - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - self.logMessage(f"SampleData request: {repr(request)}") - try: - name = q['name'][0].strip() - except KeyError: - name = None - if not name: - return ( b"sampledata name was not specifiedXYZ" ), b'text/plain' - import SampleData - try: - SampleData.downloadSample(name) - except IndexError: - return ( f"sampledata {name} was not found".encode() ), b'text/plain' - return ( f"Sample data {name} loaded".encode() ), b'text/plain' - - def volumeSelection(self,request): - """ - Cycles through loaded volumes in the scene - :param cmd: either "next" or "previous" to indicate direction - """ - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - try: - cmd = q['cmd'][0].strip().lower() - except KeyError: - cmd = 'next' - options = ['next', 'previous'] - if not cmd in options: - cmd = 'next' - - applicationLogic = slicer.app.applicationLogic() - selectionNode = applicationLogic.GetSelectionNode() - currentNodeID = selectionNode.GetActiveVolumeID() - currentIndex = 0 - if currentNodeID: - nodes = slicer.util.getNodes('vtkMRML*VolumeNode*') - for nodeName in nodes: - if nodes[nodeName].GetID() == currentNodeID: - break - currentIndex += 1 - if currentIndex >= len(nodes): - currentIndex = 0 - if cmd == 'next': - newIndex = currentIndex + 1 - elif cmd == 'previous': - newIndex = currentIndex - 1 - if newIndex >= len(nodes): - newIndex = 0 - if newIndex < 0: - newIndex = len(nodes) - 1 - volumeNode = nodes[nodes.keys()[newIndex]] - selectionNode.SetReferenceActiveVolumeID( volumeNode.GetID() ) - applicationLogic.PropagateVolumeSelection(0) - return ( f"Volume selected".encode() ), b'text/plain' - - def volumes(self, request, requestBody): - """ - Returns a json list of mrml volume names and ids - """ - volumes = [] - mrmlVolumes = slicer.util.getNodes('vtkMRMLScalarVolumeNode*') - mrmlVolumes.update(slicer.util.getNodes('vtkMRMLLabelMapVolumeNode*')) - for id_ in mrmlVolumes.keys(): - volumeNode = mrmlVolumes[id_] - volumes.append({"name": volumeNode.GetName(), "id": volumeNode.GetID()}) - return ( json.dumps(volumes).encode() ), b'application/json' - - def volume(self, request, requestBody): - """ - If there is a request body, this tries to parse the binary as nrrd - and put it in the scene, either in an existing node or a new one. - If there is no request body then the binary of the nrrd is returned for the given id. - :param id: is the mrml id of the volume to get or put - """ - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - try: - volumeID = q['id'][0].strip() - except KeyError: - volumeID = 'vtkMRMLScalarVolumeNode*' - - if requestBody: - return self.postNRRD(volumeID, requestBody), b'application/octet-stream' - else: - return self.getNRRD(volumeID), b'application/octet-stream' - - def gridTransforms(self, request, requestBody): - """ - Returns a list of names and ids of grid transforms in the scene - """ - gridTransforms = [] - mrmlGridTransforms = slicer.util.getNodes('vtkMRMLGridTransformNode*') - for id_ in mrmlGridTransforms.keys(): - gridTransform = mrmlGridTransforms[id_] - gridTransforms.append({"name": gridTransform.GetName(), "id": gridTransform.GetID()}) - return ( json.dumps(gridTransforms).encode() ), b'application/json' - - def gridTransform(self, request, requestBody): - """ - If there is a request body, this tries to parse the binary as nrrd grid transform - and put it in the scene, either in an existing node or a new one. - If there is no request body then the binary of the nrrd is returned for the given id. - :param id: is the mrml id of the volume to get or put - """ - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - try: - transformID = q['id'][0].strip() - except KeyError: - transformID = 'vtkMRMLGridTransformNode*' - - if requestBody: - return self.postTransformNRRD(transformID, requestBody), b'application/octet-stream' - else: - return self.getTransformNRRD(transformID), b'application/octet-stream' - - def postNRRD(self, volumeID, requestBody): - """Convert a binary blob of nrrd data into a node in the scene. - Overwrite volumeID if it exists, otherwise create new - :param volumeID: mrml id of the volume to update (new is created if id is invalid) - :param requestBody: the binary of the nrrd. - .. note:: only a subset of valid nrrds are supported (just scalar volumes and grid transforms) - """ - - if requestBody[:4] != b"NRRD": - self.logMessage('Cannot load non-nrrd file (magic is %s)' % requestBody[:4]) - return - - fields = {} - endOfHeader = requestBody.find(b'\n\n') #TODO: could be \r\n - header = requestBody[:endOfHeader] - self.logMessage(header) - for line in header.split(b'\n'): - colonIndex = line.find(b':') - if line[0] != '#' and colonIndex != -1: - key = line[:colonIndex] - value = line[colonIndex+2:] - fields[key] = value - - if fields[b'type'] != b'short': - self.logMessage('Can only read short volumes') - return b"{'status': 'failed'}" - if fields[b'dimension'] != b'3': - self.logMessage('Can only read 3D, 1 component volumes') - return b"{'status': 'failed'}" - if fields[b'endian'] != b'little': - self.logMessage('Can only read little endian') - return b"{'status': 'failed'}" - if fields[b'encoding'] != b'raw': - self.logMessage('Can only read raw encoding') - return b"{'status': 'failed'}" - if fields[b'space'] != b'left-posterior-superior': - self.logMessage('Can only read space in LPS') - return b"{'status': 'failed'}" - - imageData = vtk.vtkImageData() - imageData.SetDimensions(list(map(int,fields[b'sizes'].split(b' ')))) - imageData.AllocateScalars(vtk.VTK_SHORT, 1) - - origin = list(map(float, fields[b'space origin'].replace(b'(',b'').replace(b')',b'').split(b','))) - origin[0] *= -1 - origin[1] *= -1 - - directions = [] - directionParts = fields[b'space directions'].split(b')')[:3] - for directionPart in directionParts: - part = directionPart.replace(b'(',b'').replace(b')',b'').split(b',') - directions.append(list(map(float, part))) - - ijkToRAS = vtk.vtkMatrix4x4() - ijkToRAS.Identity() - for row in range(3): - ijkToRAS.SetElement(row,3, origin[row]) - for column in range(3): - element = directions[column][row] - if row < 2: - element *= -1 - ijkToRAS.SetElement(row,column, element) - - try: - node = slicer.util.getNode(volumeID) - except slicer.util.MRMLNodeNotFoundException: - node = None - if not node: - node = slicer.vtkMRMLScalarVolumeNode() - node.SetName(volumeID) - slicer.mrmlScene.AddNode(node) - node.CreateDefaultDisplayNodes() - node.SetAndObserveImageData(imageData) - node.SetIJKToRASMatrix(ijkToRAS) - - pixels = numpy.frombuffer(requestBody[endOfHeader+2:],dtype=numpy.dtype('int16')) - array = slicer.util.array(node.GetID()) - array[:] = pixels.reshape(array.shape) - imageData.GetPointData().GetScalars().Modified() - - displayNode = node.GetDisplayNode() - displayNode.ProcessMRMLEvents(displayNode, vtk.vtkCommand.ModifiedEvent, "") - #TODO: this could be optional - slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveVolumeID(node.GetID()) - slicer.app.applicationLogic().PropagateVolumeSelection() - - return b"{'status': 'success'}" - - def getNRRD(self, volumeID): - """Return a nrrd binary blob with contents of the volume node - :param volumeID: must be a valid mrml id - """ - volumeNode = slicer.util.getNode(volumeID) - volumeArray = slicer.util.array(volumeID) - - if volumeNode is None or volumeArray is None: - self.logMessage('Could not find requested volume') - return None - supportedNodes = ["vtkMRMLScalarVolumeNode","vtkMRMLLabelMapVolumeNode"] - if not volumeNode.GetClassName() in supportedNodes: - self.logMessage('Can only get scalar volumes') - return None - - imageData = volumeNode.GetImageData() - - supportedScalarTypes = ["short", "double"] - scalarType = imageData.GetScalarTypeAsString() - if scalarType not in supportedScalarTypes: - self.logMessage('Can only get volumes of types %s, not %s' % (str(supportedScalarTypes), scalarType)) - self.logMessage('Converting to short, but may cause data loss.') - volumeArray = numpy.array(volumeArray, dtype='int16') - scalarType = 'short' - - sizes = imageData.GetDimensions() - sizes = " ".join(list(map(str,sizes))) - - originList = [0,]*3 - directionLists = [[0,]*3,[0,]*3,[0,]*3] - ijkToRAS = vtk.vtkMatrix4x4() - volumeNode.GetIJKToRASMatrix(ijkToRAS) - for row in range(3): - originList[row] = ijkToRAS.GetElement(row,3) - for column in range(3): - element = ijkToRAS.GetElement(row,column) - if row < 2: - element *= -1 - directionLists[column][row] = element - originList[0] *=-1 - originList[1] *=-1 - origin = '('+','.join(list(map(str,originList)))+')' - directions = "" - for directionList in directionLists: - direction = '('+','.join(list(map(str,directionList)))+')' - directions += direction + " " - directions = directions[:-1] - - # should look like: - #space directions: (0,1,0) (0,0,-1) (-1.2999954223632812,0,0) - #space origin: (86.644897460937486,-133.92860412597656,116.78569793701172) - - nrrdHeader = """NRRD0004 + """Implements the Slicer REST api""" + + def __init__(self, enableExec=False): + self.enableExec = enableExec + + def logMessage(self, *args): + logging.debug(args) + + def canHandleRequest(self, uri, requestBody): + parsedURL = urllib.parse.urlparse(uri) + pathParts = os.path.split(parsedURL.path) # path is like /slicer/timeimage + route = pathParts[0] + return 0.5 if route.startswith(b'/slicer') else 0.0 + + def handleRequest(self, uri, requestBody): + """Handle a slicer api request. + TODO: better routing (add routing plugins) + :param request: request portion of the URL + :param requestBody: binary data that came with request + :return: tuple of (mime) type and responseBody (binary) + """ + parsedURL = urllib.parse.urlparse(uri) + request = parsedURL.path + request = request[len(b'/slicer'):] + if parsedURL.query != b"": + request += b'?' + parsedURL.query + self.logMessage(' request is: %s' % request) + + responseBody = None + contentType = b'text/plain' + try: + if self.enableExec and request.find(b'/exec') == 0: + responseBody, contentType = self.exec(request, requestBody) + elif request.find(b'/timeimage') == 0: + responseBody, contentType = self.timeimage(request) + elif request.find(b'/gui') == 0: + responseBody, contentType = self.gui(request) + elif request.find(b'/screenshot') == 0: + responseBody, contentType = self.screenshot(request) + elif request.find(b'/slice') == 0: + responseBody, contentType = self.slice(request) + elif request.find(b'/threeD') == 0: + responseBody, contentType = self.threeD(request) + elif request.find(b'/mrml') == 0: + responseBody, contentType = self.mrml(request) + elif request.find(b'/tracking') == 0: + responseBody, contentType = self.tracking(request) + elif request.find(b'/sampledata') == 0: + responseBody, contentType = self.sampleData(request) + elif request.find(b'/volumeSelection') == 0: + responseBody, contentType = self.volumeSelection(request) + elif request.find(b'/volumes') == 0: + responseBody, contentType = self.volumes(request, requestBody) + elif request.find(b'/volume') == 0: + responseBody, contentType = self.volume(request, requestBody) + elif request.find(b'/gridTransforms') == 0: + responseBody, contentType = self.gridTransforms(request, requestBody) + elif request.find(b'/gridTransform') == 0: + responseBody, contentType = self.gridTransform(request, requestBody) + print("responseBody", len(responseBody)) + elif request.find(b'/fiducials') == 0: + responseBody, contentType = self.fiducials(request, requestBody) + elif request.find(b'/fiducial') == 0: + responseBody, contentType = self.fiducial(request, requestBody) + elif request.find(b'/accessDICOMwebStudy') == 0: + responseBody, contentType = self.accessDICOMwebStudy(request, requestBody) + else: + responseBody = b"unknown command \"" + request + b"\"" + except: + self.logMessage("Could not handle slicer command: %s" % request) + etype, value, tb = sys.exc_info() + import traceback + self.logMessage(etype, value) + self.logMessage(traceback.format_tb(tb)) + print(etype, value) + print(traceback.format_tb(tb)) + for frame in traceback.format_tb(tb): + print(frame) + return contentType, responseBody + + def exec(self, request, requestBody): + """ + Implements the Read Eval Print Loop for python code. + :param source: python code to run + :return: result of code running as json string (from the content of the + `dict` object set into the `__execResult` variable) + example: + curl -X POST localhost:2016/slicer/exec --data "slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)" + """ + self.logMessage('exec with body %s' % requestBody) + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + if requestBody: + source = requestBody + else: + try: + source = urllib.parse.unquote(q['source'][0]) + except KeyError: + self.logMessage('need to supply source code to run') + return "", b'text/plain' + self.logMessage('will run %s' % source) + exec("__execResult = {}", globals()) + exec(source, globals()) + result = json.dumps(eval("__execResult", globals())).encode() + self.logMessage('result: %s' % result) + return result, b'application/json' + + def setupMRMLTracking(self): + """ + For the tracking endpoint this creates a kind of 'cursor' in the scene. + Adds "trackingDevice" (model node) to self. + """ + if not hasattr(self, "trackingDevice"): + """ set up the mrml parts or use existing """ + nodes = slicer.mrmlScene.GetNodesByName('trackingDevice') + if nodes.GetNumberOfItems() > 0: + self.trackingDevice = nodes.GetItemAsObject(0) + nodes = slicer.mrmlScene.GetNodesByName('tracker') + self.tracker = nodes.GetItemAsObject(0) + else: + # trackingDevice cursor + self.cube = vtk.vtkCubeSource() + self.cube.SetXLength(30) + self.cube.SetYLength(70) + self.cube.SetZLength(5) + self.cube.Update() + # display node + self.modelDisplay = slicer.vtkMRMLModelDisplayNode() + self.modelDisplay.SetColor(1, 1, 0) # yellow + slicer.mrmlScene.AddNode(self.modelDisplay) + # self.modelDisplay.SetPolyData(self.cube.GetOutputPort()) + # Create model node + self.trackingDevice = slicer.vtkMRMLModelNode() + self.trackingDevice.SetScene(slicer.mrmlScene) + self.trackingDevice.SetName("trackingDevice") + self.trackingDevice.SetAndObservePolyData(self.cube.GetOutputDataObject(0)) + self.trackingDevice.SetAndObserveDisplayNodeID(self.modelDisplay.GetID()) + slicer.mrmlScene.AddNode(self.trackingDevice) + # tracker + self.tracker = slicer.vtkMRMLLinearTransformNode() + self.tracker.SetName('tracker') + slicer.mrmlScene.AddNode(self.tracker) + self.trackingDevice.SetAndObserveTransformNodeID(self.tracker.GetID()) + + def tracking(self, request): + """ + Send the matrix for a tracked object in the scene + :param m: 4x4 tracker matrix in column major order (position is last row) + :param q: quaternion in WXYZ order + :param p: position (last column of transform) + Matrix is overwritten if position or quaternion are provided + """ + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + self.logMessage(q) + try: + transformMatrix = list(map(float, q['m'][0].split(','))) + except KeyError: + transformMatrix = None + try: + quaternion = list(map(float, q['q'][0].split(','))) + except KeyError: + quaternion = None + try: + position = list(map(float, q['p'][0].split(','))) + except KeyError: + position = None + + self.setupMRMLTracking() + m = vtk.vtkMatrix4x4() + self.tracker.GetMatrixTransformToParent(m) + + if transformMatrix: + for row in range(3): + for column in range(3): + m.SetElement(row, column, transformMatrix[3 * row + column]) + m.SetElement(row, column, transformMatrix[3 * row + column]) + m.SetElement(row, column, transformMatrix[3 * row + column]) + m.SetElement(row, column, transformMatrix[3 * row + column]) + + if position: + for row in range(3): + m.SetElement(row, 3, position[row]) + + if quaternion: + qu = vtk.vtkQuaternion['float64']() + qu.SetW(quaternion[0]) + qu.SetX(quaternion[1]) + qu.SetY(quaternion[2]) + qu.SetZ(quaternion[3]) + m3 = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] + qu.ToMatrix3x3(m3) + for row in range(3): + for column in range(3): + m.SetElement(row, column, m3[row][column]) + + self.tracker.SetMatrixTransformToParent(m) + + return (f"Set matrix".encode()), b'text/plain' + + def sampleData(self, request): + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + self.logMessage(f"SampleData request: {repr(request)}") + try: + name = q['name'][0].strip() + except KeyError: + name = None + if not name: + return (b"sampledata name was not specifiedXYZ"), b'text/plain' + import SampleData + try: + SampleData.downloadSample(name) + except IndexError: + return (f"sampledata {name} was not found".encode()), b'text/plain' + return (f"Sample data {name} loaded".encode()), b'text/plain' + + def volumeSelection(self, request): + """ + Cycles through loaded volumes in the scene + :param cmd: either "next" or "previous" to indicate direction + """ + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + try: + cmd = q['cmd'][0].strip().lower() + except KeyError: + cmd = 'next' + options = ['next', 'previous'] + if cmd not in options: + cmd = 'next' + + applicationLogic = slicer.app.applicationLogic() + selectionNode = applicationLogic.GetSelectionNode() + currentNodeID = selectionNode.GetActiveVolumeID() + currentIndex = 0 + if currentNodeID: + nodes = slicer.util.getNodes('vtkMRML*VolumeNode*') + for nodeName in nodes: + if nodes[nodeName].GetID() == currentNodeID: + break + currentIndex += 1 + if currentIndex >= len(nodes): + currentIndex = 0 + if cmd == 'next': + newIndex = currentIndex + 1 + elif cmd == 'previous': + newIndex = currentIndex - 1 + if newIndex >= len(nodes): + newIndex = 0 + if newIndex < 0: + newIndex = len(nodes) - 1 + volumeNode = nodes[nodes.keys()[newIndex]] + selectionNode.SetReferenceActiveVolumeID(volumeNode.GetID()) + applicationLogic.PropagateVolumeSelection(0) + return (f"Volume selected".encode()), b'text/plain' + + def volumes(self, request, requestBody): + """ + Returns a json list of mrml volume names and ids + """ + volumes = [] + mrmlVolumes = slicer.util.getNodes('vtkMRMLScalarVolumeNode*') + mrmlVolumes.update(slicer.util.getNodes('vtkMRMLLabelMapVolumeNode*')) + for id_ in mrmlVolumes.keys(): + volumeNode = mrmlVolumes[id_] + volumes.append({"name": volumeNode.GetName(), "id": volumeNode.GetID()}) + return (json.dumps(volumes).encode()), b'application/json' + + def volume(self, request, requestBody): + """ + If there is a request body, this tries to parse the binary as nrrd + and put it in the scene, either in an existing node or a new one. + If there is no request body then the binary of the nrrd is returned for the given id. + :param id: is the mrml id of the volume to get or put + """ + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + try: + volumeID = q['id'][0].strip() + except KeyError: + volumeID = 'vtkMRMLScalarVolumeNode*' + + if requestBody: + return self.postNRRD(volumeID, requestBody), b'application/octet-stream' + else: + return self.getNRRD(volumeID), b'application/octet-stream' + + def gridTransforms(self, request, requestBody): + """ + Returns a list of names and ids of grid transforms in the scene + """ + gridTransforms = [] + mrmlGridTransforms = slicer.util.getNodes('vtkMRMLGridTransformNode*') + for id_ in mrmlGridTransforms.keys(): + gridTransform = mrmlGridTransforms[id_] + gridTransforms.append({"name": gridTransform.GetName(), "id": gridTransform.GetID()}) + return (json.dumps(gridTransforms).encode()), b'application/json' + + def gridTransform(self, request, requestBody): + """ + If there is a request body, this tries to parse the binary as nrrd grid transform + and put it in the scene, either in an existing node or a new one. + If there is no request body then the binary of the nrrd is returned for the given id. + :param id: is the mrml id of the volume to get or put + """ + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + try: + transformID = q['id'][0].strip() + except KeyError: + transformID = 'vtkMRMLGridTransformNode*' + + if requestBody: + return self.postTransformNRRD(transformID, requestBody), b'application/octet-stream' + else: + return self.getTransformNRRD(transformID), b'application/octet-stream' + + def postNRRD(self, volumeID, requestBody): + """Convert a binary blob of nrrd data into a node in the scene. + Overwrite volumeID if it exists, otherwise create new + :param volumeID: mrml id of the volume to update (new is created if id is invalid) + :param requestBody: the binary of the nrrd. + .. note:: only a subset of valid nrrds are supported (just scalar volumes and grid transforms) + """ + + if requestBody[:4] != b"NRRD": + self.logMessage('Cannot load non-nrrd file (magic is %s)' % requestBody[:4]) + return + + fields = {} + endOfHeader = requestBody.find(b'\n\n') # TODO: could be \r\n + header = requestBody[:endOfHeader] + self.logMessage(header) + for line in header.split(b'\n'): + colonIndex = line.find(b':') + if line[0] != '#' and colonIndex != -1: + key = line[:colonIndex] + value = line[colonIndex + 2:] + fields[key] = value + + if fields[b'type'] != b'short': + self.logMessage('Can only read short volumes') + return b"{'status': 'failed'}" + if fields[b'dimension'] != b'3': + self.logMessage('Can only read 3D, 1 component volumes') + return b"{'status': 'failed'}" + if fields[b'endian'] != b'little': + self.logMessage('Can only read little endian') + return b"{'status': 'failed'}" + if fields[b'encoding'] != b'raw': + self.logMessage('Can only read raw encoding') + return b"{'status': 'failed'}" + if fields[b'space'] != b'left-posterior-superior': + self.logMessage('Can only read space in LPS') + return b"{'status': 'failed'}" + + imageData = vtk.vtkImageData() + imageData.SetDimensions(list(map(int, fields[b'sizes'].split(b' ')))) + imageData.AllocateScalars(vtk.VTK_SHORT, 1) + + origin = list(map(float, fields[b'space origin'].replace(b'(', b'').replace(b')', b'').split(b','))) + origin[0] *= -1 + origin[1] *= -1 + + directions = [] + directionParts = fields[b'space directions'].split(b')')[:3] + for directionPart in directionParts: + part = directionPart.replace(b'(', b'').replace(b')', b'').split(b',') + directions.append(list(map(float, part))) + + ijkToRAS = vtk.vtkMatrix4x4() + ijkToRAS.Identity() + for row in range(3): + ijkToRAS.SetElement(row, 3, origin[row]) + for column in range(3): + element = directions[column][row] + if row < 2: + element *= -1 + ijkToRAS.SetElement(row, column, element) + + try: + node = slicer.util.getNode(volumeID) + except slicer.util.MRMLNodeNotFoundException: + node = None + if not node: + node = slicer.vtkMRMLScalarVolumeNode() + node.SetName(volumeID) + slicer.mrmlScene.AddNode(node) + node.CreateDefaultDisplayNodes() + node.SetAndObserveImageData(imageData) + node.SetIJKToRASMatrix(ijkToRAS) + + pixels = numpy.frombuffer(requestBody[endOfHeader + 2:], dtype=numpy.dtype('int16')) + array = slicer.util.array(node.GetID()) + array[:] = pixels.reshape(array.shape) + imageData.GetPointData().GetScalars().Modified() + + displayNode = node.GetDisplayNode() + displayNode.ProcessMRMLEvents(displayNode, vtk.vtkCommand.ModifiedEvent, "") + # TODO: this could be optional + slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveVolumeID(node.GetID()) + slicer.app.applicationLogic().PropagateVolumeSelection() + + return b"{'status': 'success'}" + + def getNRRD(self, volumeID): + """Return a nrrd binary blob with contents of the volume node + :param volumeID: must be a valid mrml id + """ + volumeNode = slicer.util.getNode(volumeID) + volumeArray = slicer.util.array(volumeID) + + if volumeNode is None or volumeArray is None: + self.logMessage('Could not find requested volume') + return None + supportedNodes = ["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"] + if not volumeNode.GetClassName() in supportedNodes: + self.logMessage('Can only get scalar volumes') + return None + + imageData = volumeNode.GetImageData() + + supportedScalarTypes = ["short", "double"] + scalarType = imageData.GetScalarTypeAsString() + if scalarType not in supportedScalarTypes: + self.logMessage('Can only get volumes of types %s, not %s' % (str(supportedScalarTypes), scalarType)) + self.logMessage('Converting to short, but may cause data loss.') + volumeArray = numpy.array(volumeArray, dtype='int16') + scalarType = 'short' + + sizes = imageData.GetDimensions() + sizes = " ".join(list(map(str, sizes))) + + originList = [0, ] * 3 + directionLists = [[0, ] * 3, [0, ] * 3, [0, ] * 3] + ijkToRAS = vtk.vtkMatrix4x4() + volumeNode.GetIJKToRASMatrix(ijkToRAS) + for row in range(3): + originList[row] = ijkToRAS.GetElement(row, 3) + for column in range(3): + element = ijkToRAS.GetElement(row, column) + if row < 2: + element *= -1 + directionLists[column][row] = element + originList[0] *= -1 + originList[1] *= -1 + origin = '(' + ','.join(list(map(str, originList))) + ')' + directions = "" + for directionList in directionLists: + direction = '(' + ','.join(list(map(str, directionList))) + ')' + directions += direction + " " + directions = directions[:-1] + + # should look like: + # space directions: (0,1,0) (0,0,-1) (-1.2999954223632812,0,0) + # space origin: (86.644897460937486,-133.92860412597656,116.78569793701172) + + nrrdHeader = """NRRD0004 # Complete NRRD file format specification at: # http://teem.sourceforge.net/nrrd/format.html type: %%scalarType%% @@ -486,53 +486,53 @@ def getNRRD(self, volumeID): """.replace("%%scalarType%%", scalarType).replace("%%sizes%%", sizes).replace("%%directions%%", directions).replace("%%origin%%", origin) - nrrdData = nrrdHeader.encode() + volumeArray.tobytes() - return nrrdData - - def getTransformNRRD(self, transformID): - """Return a nrrd binary blob with contents of the transform node - """ - transformNode = slicer.util.getNode(transformID) - transformArray = slicer.util.array(transformID) - - if transformNode is None or transformArray is None: - self.logMessage('Could not find requested transform') - return None - supportedNodes = ["vtkMRMLGridTransformNode",] - if not transformNode.GetClassName() in supportedNodes: - self.logMessage('Can only get grid transforms') - return None - - # map the vectors to be in the LPS measurement frame - # (need to make a copy so as not to change the slicer transform) - lpsArray = numpy.array(transformArray) - lpsArray *= numpy.array([-1,-1,1]) - - imageData = transformNode.GetTransformFromParent().GetDisplacementGrid() - - # for now, only handle non-oriented grid transform as - # generated from LandmarkRegistration - # TODO: generalize for any GridTransform node - # -- here we assume it is axial as generated by LandmarkTransform - - sizes = (3,) + imageData.GetDimensions() - sizes = " ".join(list(map(str,sizes))) - - spacing = list(imageData.GetSpacing()) - spacing[0] *= -1 # RAS to LPS - spacing[1] *= -1 # RAS to LPS - directions = '(%g,0,0) (0,%g,0) (0,0,%g)' % tuple(spacing) - - origin = list(imageData.GetOrigin()) - origin[0] *= -1 # RAS to LPS - origin[1] *= -1 # RAS to LPS - origin = '(%g,%g,%g)' % tuple(origin) - - # should look like: - #space directions: (0,1,0) (0,0,-1) (-1.2999954223632812,0,0) - #space origin: (86.644897460937486,-133.92860412597656,116.78569793701172) - - nrrdHeader = """NRRD0004 + nrrdData = nrrdHeader.encode() + volumeArray.tobytes() + return nrrdData + + def getTransformNRRD(self, transformID): + """Return a nrrd binary blob with contents of the transform node + """ + transformNode = slicer.util.getNode(transformID) + transformArray = slicer.util.array(transformID) + + if transformNode is None or transformArray is None: + self.logMessage('Could not find requested transform') + return None + supportedNodes = ["vtkMRMLGridTransformNode", ] + if not transformNode.GetClassName() in supportedNodes: + self.logMessage('Can only get grid transforms') + return None + + # map the vectors to be in the LPS measurement frame + # (need to make a copy so as not to change the slicer transform) + lpsArray = numpy.array(transformArray) + lpsArray *= numpy.array([-1, -1, 1]) + + imageData = transformNode.GetTransformFromParent().GetDisplacementGrid() + + # for now, only handle non-oriented grid transform as + # generated from LandmarkRegistration + # TODO: generalize for any GridTransform node + # -- here we assume it is axial as generated by LandmarkTransform + + sizes = (3,) + imageData.GetDimensions() + sizes = " ".join(list(map(str, sizes))) + + spacing = list(imageData.GetSpacing()) + spacing[0] *= -1 # RAS to LPS + spacing[1] *= -1 # RAS to LPS + directions = '(%g,0,0) (0,%g,0) (0,0,%g)' % tuple(spacing) + + origin = list(imageData.GetOrigin()) + origin[0] *= -1 # RAS to LPS + origin[1] *= -1 # RAS to LPS + origin = '(%g,%g,%g)' % tuple(origin) + + # should look like: + # space directions: (0,1,0) (0,0,-1) (-1.2999954223632812,0,0) + # space origin: (86.644897460937486,-133.92860412597656,116.78569793701172) + + nrrdHeader = """NRRD0004 # Complete NRRD file format specification at: # http://teem.sourceforge.net/nrrd/format.html type: float @@ -547,401 +547,401 @@ def getTransformNRRD(self, transformID): """.replace("%%sizes%%", sizes).replace("%%directions%%", directions).replace("%%origin%%", origin) - nrrdData = nrrdHeader.encode() + lpsArray.tobytes() - return nrrdData - - def fiducials(self, request, requestBody): - """return fiducials list in ad hoc json structure - TODO: should use the markups json version - """ - fiducials = {} - for markupsNode in slicer.util.getNodesByClass('vtkMRMLMarkupsFiducialNode'): - displayNode = markupsNode.GetDisplayNode() - node = {} - node['name'] = markupsNode.GetName() - node['color'] = displayNode.GetSelectedColor() - node['scale'] = displayNode.GetGlyphScale() - node['markups'] = [] - for markupIndex in range(markupsNode.GetNumberOfMarkups()): - position = [0,]*3 - markupsNode.GetNthFiducialPosition(markupIndex, position) - position - node['markups'].append( { - 'label': markupsNode.GetNthFiducialLabel(markupIndex), - 'position': position - }) - fiducials[markupsNode.GetID()] = node - return ( json.dumps( fiducials ).encode() ), b'application/json' - - def fiducial(self, request, requestBody): - """ - Set the location of a control point in a markups fiducial - :param id: mrml id of the fiducial list - :param r: Right coordinate - :param a: Anterior coordinate - :param s: Superior coordinate - """ - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - try: - fiducialID = q['id'][0].strip() - except KeyError: - fiducialID = 'vtkMRMLMarkupsFiducialNode*' - try: - index = q['index'][0].strip() - except KeyError: - index = 0 - try: - r = q['r'][0].strip() - except KeyError: - r = 0 - try: - a = q['a'][0].strip() - except KeyError: - a = 0 - try: - s = q['s'][0].strip() - except KeyError: - s = 0 - - fiducialNode = slicer.util.getNode(fiducialID) - fiducialNode.SetNthFiducialPosition(index, float(r), float(a), float(s)); - return "{'result': 'ok'}", b'application/json' - - def accessDICOMwebStudy(self, request, requestBody): - """ - Access DICOMweb server to download requested study, add it to - Slicer's dicom database, and load it into the scene. - :param requestBody: is a json string - :param requestBody['dicomWEBPrefix']: is the start of the url - :param requestBody['dicomWEBStore']: is the middle of the url - :param requestBody['studyUID']: is the end of the url - :param requestBody['accessToken']: is the authorization bearer token for the DICOMweb server - """ - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - - request = json.loads(requestBody), b'application/json' - - dicomWebEndpoint = request['dicomWEBPrefix'] + '/' + request['dicomWEBStore'] - print(f"Loading from {dicomWebEndpoint}") - - from DICOMLib import DICOMUtils - loadedUIDs = DICOMUtils.importFromDICOMWeb( - dicomWebEndpoint = request['dicomWEBPrefix'] + '/' + request['dicomWEBStore'], - studyInstanceUID = request['studyUID'], - accessToken = request['accessToken']) - - files = [] - for studyUID in loadedUIDs: - for seriesUID in slicer.dicomDatabase.seriesForStudy(studyUID): - for instance in slicer.dicomDatabase.instancesForSeries(seriesUID): - files.append(slicer.dicomDatabase.fileForInstance(instance)) - loadables = DICOMUtils.getLoadablesFromFileLists([files]) - loadedNodes = DICOMUtils.loadLoadables(loadLoadables) - - print(f"Loaded {loadedUIDs}, and {loadedNodes}") - - return b'{"result": "ok"}' - - def mrml(self,request): - """ - Returns a json list of all the mrml nodes - """ - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - return ( json.dumps( list(slicer.util.getNodes('*').keys()) ).encode() ), b'application/json' - - def screenshot(self,request): - """ - Returns screenshot of the application main window. - """ - slicer.app.processEvents() - slicer.util.forceRenderAllViews() - screenshot = slicer.util.mainWindow().grab() - bArray = qt.QByteArray() - buffer = qt.QBuffer(bArray) - buffer.open(qt.QIODevice.WriteOnly) - screenshot.save(buffer, "PNG") - pngData = bArray.data() - self.logMessage('returning an image of %d length' % len(pngData)) - return pngData, b'image/png' - - @staticmethod - def setViewersLayout(layoutName): - for att in dir(slicer.vtkMRMLLayoutNode): - if att.startswith("SlicerLayout") and att.endswith("View"): - foundLayoutName = att[12:-4] - if layoutName.lower() == foundLayoutName.lower(): - layoutId = eval(f"slicer.vtkMRMLLayoutNode.{att}") - slicer.app.layoutManager().setLayout(layoutId) - return - raise ValueError("Unknown layout name: " + layoutName) - - def gui(self,request): - """return a png of the application GUI. - :param contents: {full, viewers} - :param viewersLayout: {fourup, oneup3d, ...} slicer.vtkMRMLLayoutNode constants (SlicerLayout...View) - :return: png encoded screenshot after applying params - """ - - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - - try: - contents = q['contents'][0].strip().lower() - except KeyError: - contents = None - if contents == "viewers": - slicer.util.findChild(slicer.util.mainWindow(), "PanelDockWidget").hide() - slicer.util.setStatusBarVisible(False) - slicer.util.setMenuBarsVisible(False) - slicer.util.setToolbarsVisible(False) - elif contents == "full": - slicer.util.findChild(slicer.util.mainWindow(), "PanelDockWidget").show() - slicer.util.setStatusBarVisible(True) - slicer.util.setMenuBarsVisible(True) - slicer.util.setToolbarsVisible(True) - else: - if contents: - raise ValueError("contents must be 'viewers' or 'full'") - - try: - viewersLayout = q['viewersLayout'][0].strip().lower() - except KeyError: - viewersLayout = None - if viewersLayout is not None: - SlicerRequestHandler.setViewersLayout(viewersLayout) - - return ( f"Switched {contents} to {viewersLayout}".encode() ), b'text/plain' - - def slice(self,request): - """return a png for a slice view. - :param view: {red, yellow, green} - :param scrollTo: 0 to 1 for slice position within volume - :param offset: mm offset relative to slice origin (position of slice slider) - :param size: pixel size of output png - :param copySliceGeometryFrom: view name of other slice to copy from - :param orientation: {axial, sagittal, coronal} - :return: png encoded slice screenshot after applying params - """ - - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - try: - view = q['view'][0].strip().lower() - except KeyError: - view = 'red' - options = ['red', 'yellow', 'green'] - if not view in options: - view = 'red' - layoutManager = slicer.app.layoutManager() - sliceLogic = layoutManager.sliceWidget(view.capitalize()).sliceLogic() - try: - mode = str(q['mode'][0].strip()) - except (KeyError, ValueError): - mode = None - try: - offset = float(q['offset'][0].strip()) - except (KeyError, ValueError): - offset = None - try: - copySliceGeometryFrom = q['copySliceGeometryFrom'][0].strip() - except (KeyError, ValueError): - copySliceGeometryFrom = None - try: - scrollTo = float(q['scrollTo'][0].strip()) - except (KeyError, ValueError): - scrollTo = None - try: - size = int(q['size'][0].strip()) - except (KeyError, ValueError): - size = None - try: - orientation = q['orientation'][0].strip() - except (KeyError, ValueError): - orientation = None - - offsetKey = 'offset.'+view - #if mode == 'start' or not self.interactionState.has_key(offsetKey): - #self.interactionState[offsetKey] = sliceLogic.GetSliceOffset() - - if scrollTo: - volumeNode = sliceLogic.GetBackgroundLayer().GetVolumeNode() - bounds = [0,] * 6 - sliceLogic.GetVolumeSliceBounds(volumeNode,bounds) - sliceLogic.SetSliceOffset(bounds[4] + (scrollTo * (bounds[5] - bounds[4]))) - if offset: - #startOffset = self.interactionState[offsetKey] - sliceLogic.SetSliceOffset(startOffset + offset) - if copySliceGeometryFrom: - otherSliceLogic = layoutManager.sliceWidget(copySliceGeometryFrom.capitalize()).sliceLogic() - otherSliceNode = otherSliceLogic.GetSliceNode() - sliceNode = sliceLogic.GetSliceNode() - # technique from vtkMRMLSliceLinkLogic (TODO: should be exposed as method) - sliceNode.GetSliceToRAS().DeepCopy( otherSliceNode.GetSliceToRAS() ) - fov = sliceNode.GetFieldOfView() - otherFOV = otherSliceNode.GetFieldOfView() - sliceNode.SetFieldOfView( otherFOV[0], - otherFOV[0] * fov[1] / fov[0], - fov[2] ) - - if orientation: - sliceNode = sliceLogic.GetSliceNode() - previousOrientation = sliceNode.GetOrientationString().lower() - if orientation.lower() == 'axial': - sliceNode.SetOrientationToAxial() - if orientation.lower() == 'sagittal': - sliceNode.SetOrientationToSagittal() - if orientation.lower() == 'coronal': - sliceNode.SetOrientationToCoronal() - if orientation.lower() != previousOrientation: - sliceLogic.FitSliceToAll() - - imageData = sliceLogic.GetBlend().Update(0) - imageData = sliceLogic.GetBlend().GetOutputDataObject(0) - pngData = [] - if imageData: + nrrdData = nrrdHeader.encode() + lpsArray.tobytes() + return nrrdData + + def fiducials(self, request, requestBody): + """return fiducials list in ad hoc json structure + TODO: should use the markups json version + """ + fiducials = {} + for markupsNode in slicer.util.getNodesByClass('vtkMRMLMarkupsFiducialNode'): + displayNode = markupsNode.GetDisplayNode() + node = {} + node['name'] = markupsNode.GetName() + node['color'] = displayNode.GetSelectedColor() + node['scale'] = displayNode.GetGlyphScale() + node['markups'] = [] + for markupIndex in range(markupsNode.GetNumberOfMarkups()): + position = [0, ] * 3 + markupsNode.GetNthFiducialPosition(markupIndex, position) + position + node['markups'].append({ + 'label': markupsNode.GetNthFiducialLabel(markupIndex), + 'position': position + }) + fiducials[markupsNode.GetID()] = node + return (json.dumps(fiducials).encode()), b'application/json' + + def fiducial(self, request, requestBody): + """ + Set the location of a control point in a markups fiducial + :param id: mrml id of the fiducial list + :param r: Right coordinate + :param a: Anterior coordinate + :param s: Superior coordinate + """ + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + try: + fiducialID = q['id'][0].strip() + except KeyError: + fiducialID = 'vtkMRMLMarkupsFiducialNode*' + try: + index = q['index'][0].strip() + except KeyError: + index = 0 + try: + r = q['r'][0].strip() + except KeyError: + r = 0 + try: + a = q['a'][0].strip() + except KeyError: + a = 0 + try: + s = q['s'][0].strip() + except KeyError: + s = 0 + + fiducialNode = slicer.util.getNode(fiducialID) + fiducialNode.SetNthFiducialPosition(index, float(r), float(a), float(s)) + return "{'result': 'ok'}", b'application/json' + + def accessDICOMwebStudy(self, request, requestBody): + """ + Access DICOMweb server to download requested study, add it to + Slicer's dicom database, and load it into the scene. + :param requestBody: is a json string + :param requestBody['dicomWEBPrefix']: is the start of the url + :param requestBody['dicomWEBStore']: is the middle of the url + :param requestBody['studyUID']: is the end of the url + :param requestBody['accessToken']: is the authorization bearer token for the DICOMweb server + """ + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + + request = json.loads(requestBody), b'application/json' + + dicomWebEndpoint = request['dicomWEBPrefix'] + '/' + request['dicomWEBStore'] + print(f"Loading from {dicomWebEndpoint}") + + from DICOMLib import DICOMUtils + loadedUIDs = DICOMUtils.importFromDICOMWeb( + dicomWebEndpoint=request['dicomWEBPrefix'] + '/' + request['dicomWEBStore'], + studyInstanceUID=request['studyUID'], + accessToken=request['accessToken']) + + files = [] + for studyUID in loadedUIDs: + for seriesUID in slicer.dicomDatabase.seriesForStudy(studyUID): + for instance in slicer.dicomDatabase.instancesForSeries(seriesUID): + files.append(slicer.dicomDatabase.fileForInstance(instance)) + loadables = DICOMUtils.getLoadablesFromFileLists([files]) + loadedNodes = DICOMUtils.loadLoadables(loadLoadables) + + print(f"Loaded {loadedUIDs}, and {loadedNodes}") + + return b'{"result": "ok"}' + + def mrml(self, request): + """ + Returns a json list of all the mrml nodes + """ + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + return (json.dumps(list(slicer.util.getNodes('*').keys())).encode()), b'application/json' + + def screenshot(self, request): + """ + Returns screenshot of the application main window. + """ + slicer.app.processEvents() + slicer.util.forceRenderAllViews() + screenshot = slicer.util.mainWindow().grab() + bArray = qt.QByteArray() + buffer = qt.QBuffer(bArray) + buffer.open(qt.QIODevice.WriteOnly) + screenshot.save(buffer, "PNG") + pngData = bArray.data() + self.logMessage('returning an image of %d length' % len(pngData)) + return pngData, b'image/png' + + @staticmethod + def setViewersLayout(layoutName): + for att in dir(slicer.vtkMRMLLayoutNode): + if att.startswith("SlicerLayout") and att.endswith("View"): + foundLayoutName = att[12:-4] + if layoutName.lower() == foundLayoutName.lower(): + layoutId = eval(f"slicer.vtkMRMLLayoutNode.{att}") + slicer.app.layoutManager().setLayout(layoutId) + return + raise ValueError("Unknown layout name: " + layoutName) + + def gui(self, request): + """return a png of the application GUI. + :param contents: {full, viewers} + :param viewersLayout: {fourup, oneup3d, ...} slicer.vtkMRMLLayoutNode constants (SlicerLayout...View) + :return: png encoded screenshot after applying params + """ + + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + + try: + contents = q['contents'][0].strip().lower() + except KeyError: + contents = None + if contents == "viewers": + slicer.util.findChild(slicer.util.mainWindow(), "PanelDockWidget").hide() + slicer.util.setStatusBarVisible(False) + slicer.util.setMenuBarsVisible(False) + slicer.util.setToolbarsVisible(False) + elif contents == "full": + slicer.util.findChild(slicer.util.mainWindow(), "PanelDockWidget").show() + slicer.util.setStatusBarVisible(True) + slicer.util.setMenuBarsVisible(True) + slicer.util.setToolbarsVisible(True) + else: + if contents: + raise ValueError("contents must be 'viewers' or 'full'") + + try: + viewersLayout = q['viewersLayout'][0].strip().lower() + except KeyError: + viewersLayout = None + if viewersLayout is not None: + SlicerRequestHandler.setViewersLayout(viewersLayout) + + return (f"Switched {contents} to {viewersLayout}".encode()), b'text/plain' + + def slice(self, request): + """return a png for a slice view. + :param view: {red, yellow, green} + :param scrollTo: 0 to 1 for slice position within volume + :param offset: mm offset relative to slice origin (position of slice slider) + :param size: pixel size of output png + :param copySliceGeometryFrom: view name of other slice to copy from + :param orientation: {axial, sagittal, coronal} + :return: png encoded slice screenshot after applying params + """ + + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + try: + view = q['view'][0].strip().lower() + except KeyError: + view = 'red' + options = ['red', 'yellow', 'green'] + if view not in options: + view = 'red' + layoutManager = slicer.app.layoutManager() + sliceLogic = layoutManager.sliceWidget(view.capitalize()).sliceLogic() + try: + mode = str(q['mode'][0].strip()) + except (KeyError, ValueError): + mode = None + try: + offset = float(q['offset'][0].strip()) + except (KeyError, ValueError): + offset = None + try: + copySliceGeometryFrom = q['copySliceGeometryFrom'][0].strip() + except (KeyError, ValueError): + copySliceGeometryFrom = None + try: + scrollTo = float(q['scrollTo'][0].strip()) + except (KeyError, ValueError): + scrollTo = None + try: + size = int(q['size'][0].strip()) + except (KeyError, ValueError): + size = None + try: + orientation = q['orientation'][0].strip() + except (KeyError, ValueError): + orientation = None + + offsetKey = 'offset.' + view + # if mode == 'start' or not self.interactionState.has_key(offsetKey): + # self.interactionState[offsetKey] = sliceLogic.GetSliceOffset() + + if scrollTo: + volumeNode = sliceLogic.GetBackgroundLayer().GetVolumeNode() + bounds = [0, ] * 6 + sliceLogic.GetVolumeSliceBounds(volumeNode, bounds) + sliceLogic.SetSliceOffset(bounds[4] + (scrollTo * (bounds[5] - bounds[4]))) + if offset: + # startOffset = self.interactionState[offsetKey] + sliceLogic.SetSliceOffset(startOffset + offset) + if copySliceGeometryFrom: + otherSliceLogic = layoutManager.sliceWidget(copySliceGeometryFrom.capitalize()).sliceLogic() + otherSliceNode = otherSliceLogic.GetSliceNode() + sliceNode = sliceLogic.GetSliceNode() + # technique from vtkMRMLSliceLinkLogic (TODO: should be exposed as method) + sliceNode.GetSliceToRAS().DeepCopy(otherSliceNode.GetSliceToRAS()) + fov = sliceNode.GetFieldOfView() + otherFOV = otherSliceNode.GetFieldOfView() + sliceNode.SetFieldOfView(otherFOV[0], + otherFOV[0] * fov[1] / fov[0], + fov[2]) + + if orientation: + sliceNode = sliceLogic.GetSliceNode() + previousOrientation = sliceNode.GetOrientationString().lower() + if orientation.lower() == 'axial': + sliceNode.SetOrientationToAxial() + if orientation.lower() == 'sagittal': + sliceNode.SetOrientationToSagittal() + if orientation.lower() == 'coronal': + sliceNode.SetOrientationToCoronal() + if orientation.lower() != previousOrientation: + sliceLogic.FitSliceToAll() + + imageData = sliceLogic.GetBlend().Update(0) + imageData = sliceLogic.GetBlend().GetOutputDataObject(0) + pngData = [] + if imageData: + pngData = self.vtkImageDataToPNG(imageData) + self.logMessage('returning an image of %d length' % len(pngData)) + return pngData, b'image/png' + + def threeD(self, request): + """return a png for a threeD view + :param lookFromAxis: {L, R, A, P, I, S} + :return: png binary buffer + """ + + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + try: + view = q['view'][0].strip().lower() + except KeyError: + view = '1' + try: + lookFromAxis = q['lookFromAxis'][0].strip().lower() + except KeyError: + lookFromAxis = None + try: + size = int(q['size'][0].strip()) + except (KeyError, ValueError): + size = None + try: + mode = str(q['mode'][0].strip()) + except (KeyError, ValueError): + mode = None + try: + roll = float(q['roll'][0].strip()) + except (KeyError, ValueError): + roll = None + try: + panX = float(q['panX'][0].strip()) + except (KeyError, ValueError): + panX = None + try: + panY = float(q['panY'][0].strip()) + except (KeyError, ValueError): + panY = None + try: + orbitX = float(q['orbitX'][0].strip()) + except (KeyError, ValueError): + orbitX = None + try: + orbitY = float(q['orbitY'][0].strip()) + except (KeyError, ValueError): + orbitY = None + + layoutManager = slicer.app.layoutManager() + view = layoutManager.threeDWidget(0).threeDView() + view.renderEnabled = False + + if lookFromAxis: + axes = ['None', 'r', 'l', 's', 'i', 'a', 'p'] + try: + axis = axes.index(lookFromAxis[0].lower()) + view.lookFromViewAxis(axis) + except ValueError: + pass + + view.renderWindow().Render() + view.renderEnabled = True + view.forceRender() + w2i = vtk.vtkWindowToImageFilter() + w2i.SetInput(view.renderWindow()) + w2i.SetReadFrontBuffer(0) + w2i.Update() + imageData = w2i.GetOutput() + pngData = self.vtkImageDataToPNG(imageData) - self.logMessage('returning an image of %d length' % len(pngData)) - return pngData, b'image/png' - - def threeD(self,request): - """return a png for a threeD view - :param lookFromAxis: {L, R, A, P, I, S} - :return: png binary buffer - """ - - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - try: - view = q['view'][0].strip().lower() - except KeyError: - view = '1' - try: - lookFromAxis = q['lookFromAxis'][0].strip().lower() - except KeyError: - lookFromAxis = None - try: - size = int(q['size'][0].strip()) - except (KeyError, ValueError): - size = None - try: - mode = str(q['mode'][0].strip()) - except (KeyError, ValueError): - mode = None - try: - roll = float(q['roll'][0].strip()) - except (KeyError, ValueError): - roll = None - try: - panX = float(q['panX'][0].strip()) - except (KeyError, ValueError): - panX = None - try: - panY = float(q['panY'][0].strip()) - except (KeyError, ValueError): - panY = None - try: - orbitX = float(q['orbitX'][0].strip()) - except (KeyError, ValueError): - orbitX = None - try: - orbitY = float(q['orbitY'][0].strip()) - except (KeyError, ValueError): - orbitY = None - - layoutManager = slicer.app.layoutManager() - view = layoutManager.threeDWidget(0).threeDView() - view.renderEnabled = False - - if lookFromAxis: - axes = ['None', 'r','l','s','i','a','p'] - try: - axis = axes.index(lookFromAxis[0].lower()) - view.lookFromViewAxis(axis) - except ValueError: - pass - - view.renderWindow().Render() - view.renderEnabled = True - view.forceRender() - w2i = vtk.vtkWindowToImageFilter() - w2i.SetInput(view.renderWindow()) - w2i.SetReadFrontBuffer(0) - w2i.Update() - imageData = w2i.GetOutput() - - pngData = self.vtkImageDataToPNG(imageData) - self.logMessage('threeD returning an image of %d length' % len(pngData)) - return pngData, b'image/png' - - def timeimage(self,request=''): - """ - For timing and debugging - return an image with the current time - rendered as text down to the hundredth of a second - :param color: hex encoded RGB of dashed border (default 333 for dark gray) - :return: png image - """ - - # check arguments - p = urllib.parse.urlparse(request.decode()) - q = urllib.parse.parse_qs(p.query) - try: - color = "#" + q['color'][0].strip().lower() - except KeyError: - color = "#330" - - # - # make a generally transparent image, - # - imageWidth = 128 - imageHeight = 32 - timeImage = qt.QImage(imageWidth, imageHeight, qt.QImage().Format_ARGB32) - timeImage.fill(0) - - # a painter to use for various jobs - painter = qt.QPainter() - - # draw a border around the pixmap - painter.begin(timeImage) - pen = qt.QPen() - color = qt.QColor(color) - color.setAlphaF(0.8) - pen.setColor(color) - pen.setWidth(5) - pen.setStyle(3) # dotted line (Qt::DotLine) - painter.setPen(pen) - rect = qt.QRect(1, 1, imageWidth-2, imageHeight-2) - painter.drawRect(rect) - color = qt.QColor("#333") - pen.setColor(color) - painter.setPen(pen) - position = qt.QPoint(10,20) - text = str(time.time()) # text to draw - painter.drawText(position, text) - painter.end() - - # convert the image to vtk, then to png from there - vtkTimeImage = vtk.vtkImageData() - slicer.qMRMLUtils().qImageToVtkImageData(timeImage, vtkTimeImage) - pngData = self.vtkImageDataToPNG(vtkTimeImage) - return pngData, b'image/png' - - def vtkImageDataToPNG(self,imageData): - """Return a buffer of png data using the data - from the vtkImageData. - :param imageData: a vtkImageData instance - :return: bytes of a png image - """ - writer = vtk.vtkPNGWriter() - writer.SetWriteToMemory(True) - writer.SetInputData(imageData) - # use compression 0 since data transfer is faster than compressing - writer.SetCompressionLevel(0) - writer.Write() - result = writer.GetResult() - pngArray = vtk.util.numpy_support.vtk_to_numpy(result) - pngData = pngArray.tobytes() - - return pngData + self.logMessage('threeD returning an image of %d length' % len(pngData)) + return pngData, b'image/png' + + def timeimage(self, request=''): + """ + For timing and debugging - return an image with the current time + rendered as text down to the hundredth of a second + :param color: hex encoded RGB of dashed border (default 333 for dark gray) + :return: png image + """ + + # check arguments + p = urllib.parse.urlparse(request.decode()) + q = urllib.parse.parse_qs(p.query) + try: + color = "#" + q['color'][0].strip().lower() + except KeyError: + color = "#330" + + # + # make a generally transparent image, + # + imageWidth = 128 + imageHeight = 32 + timeImage = qt.QImage(imageWidth, imageHeight, qt.QImage().Format_ARGB32) + timeImage.fill(0) + + # a painter to use for various jobs + painter = qt.QPainter() + + # draw a border around the pixmap + painter.begin(timeImage) + pen = qt.QPen() + color = qt.QColor(color) + color.setAlphaF(0.8) + pen.setColor(color) + pen.setWidth(5) + pen.setStyle(3) # dotted line (Qt::DotLine) + painter.setPen(pen) + rect = qt.QRect(1, 1, imageWidth - 2, imageHeight - 2) + painter.drawRect(rect) + color = qt.QColor("#333") + pen.setColor(color) + painter.setPen(pen) + position = qt.QPoint(10, 20) + text = str(time.time()) # text to draw + painter.drawText(position, text) + painter.end() + + # convert the image to vtk, then to png from there + vtkTimeImage = vtk.vtkImageData() + slicer.qMRMLUtils().qImageToVtkImageData(timeImage, vtkTimeImage) + pngData = self.vtkImageDataToPNG(vtkTimeImage) + return pngData, b'image/png' + + def vtkImageDataToPNG(self, imageData): + """Return a buffer of png data using the data + from the vtkImageData. + :param imageData: a vtkImageData instance + :return: bytes of a png image + """ + writer = vtk.vtkPNGWriter() + writer.SetWriteToMemory(True) + writer.SetInputData(imageData) + # use compression 0 since data transfer is faster than compressing + writer.SetCompressionLevel(0) + writer.Write() + result = writer.GetResult() + pngArray = vtk.util.numpy_support.vtk_to_numpy(result) + pngData = pngArray.tobytes() + + return pngData diff --git a/Modules/Scripted/WebServer/WebServerLib/StaticPagesRequestHandler.py b/Modules/Scripted/WebServer/WebServerLib/StaticPagesRequestHandler.py index b4645819b7b..ae95d7bf9cf 100644 --- a/Modules/Scripted/WebServer/WebServerLib/StaticPagesRequestHandler.py +++ b/Modules/Scripted/WebServer/WebServerLib/StaticPagesRequestHandler.py @@ -4,58 +4,58 @@ class StaticPagesRequestHandler(object): - """Serves static pages content (files) from the configured docroot - """ - - def __init__(self, docroot): - """ - :param docroot: directory path of static pages content - :param logMessage: callable to log messages + """Serves static pages content (files) from the configured docroot """ - self.docroot = docroot - self.logMessage('docroot: %s' % self.docroot) + def __init__(self, docroot): + """ + :param docroot: directory path of static pages content + :param logMessage: callable to log messages + """ - def logMessage(self, *args): - logging.debug(args) + self.docroot = docroot + self.logMessage('docroot: %s' % self.docroot) - def canHandleRequest(self, uri, requestBody): - return 0.1 + def logMessage(self, *args): + logging.debug(args) - def handleRequest(self, uri, requestBody): - """Return directory listing or binary contents of files - TODO: other header fields like modified time + def canHandleRequest(self, uri, requestBody): + return 0.1 - :param uri: portion of the url specifying the file path - :param requestBody: binary data passed with the http request - :return: tuple of content type (based on file ext) and request body binary (contents of file) - """ - contentType = b'text/plain' - responseBody = None - if uri.startswith(b'/'): - uri = uri[1:] - path = os.path.join(self.docroot,uri) - self.logMessage('docroot: %s' % self.docroot) - if os.path.isdir(path): - for index in b"index.html", b"index.htm": - index = os.path.join(path, index) - if os.path.exists(index): - path = index - self.logMessage(b'Serving: %s' % path) - if os.path.isdir(path): - contentType = b"text/html" - responseBody = b"

          " - for entry in os.listdir(path): - responseBody += b"
        • %s
        • " % (os.path.join(uri,entry), entry) - responseBody += b"
        " - else: - ext = os.path.splitext(path)[-1].decode() - if ext in mimetypes.types_map: - contentType = mimetypes.types_map[ext].encode() - try: - fp = open(path, 'rb') - responseBody = fp.read() - fp.close() - except IOError: + def handleRequest(self, uri, requestBody): + """Return directory listing or binary contents of files + TODO: other header fields like modified time + + :param uri: portion of the url specifying the file path + :param requestBody: binary data passed with the http request + :return: tuple of content type (based on file ext) and request body binary (contents of file) + """ + contentType = b'text/plain' responseBody = None - return contentType, responseBody + if uri.startswith(b'/'): + uri = uri[1:] + path = os.path.join(self.docroot, uri) + self.logMessage('docroot: %s' % self.docroot) + if os.path.isdir(path): + for index in b"index.html", b"index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + self.logMessage(b'Serving: %s' % path) + if os.path.isdir(path): + contentType = b"text/html" + responseBody = b"
          " + for entry in os.listdir(path): + responseBody += b"
        • %s
        • " % (os.path.join(uri, entry), entry) + responseBody += b"
        " + else: + ext = os.path.splitext(path)[-1].decode() + if ext in mimetypes.types_map: + contentType = mimetypes.types_map[ext].encode() + try: + fp = open(path, 'rb') + responseBody = fp.read() + fp.close() + except IOError: + responseBody = None + return contentType, responseBody diff --git a/Testing/ModelRender.py b/Testing/ModelRender.py index bf1a50e8352..c266b3ee0f5 100644 --- a/Testing/ModelRender.py +++ b/Testing/ModelRender.py @@ -4,27 +4,27 @@ def newSphere(name=''): - if name == "": - name = "sphere-%g" % time.time() + if name == "": + name = "sphere-%g" % time.time() - sphere = Slicer.slicer.vtkSphereSource() - sphere.SetCenter( -100 + 200*random.random(), -100 + 200*random.random(), -100 + 200*random.random() ) - sphere.SetRadius( 10 + 20 *random.random() ) - sphere.GetOutput().Update() - modelDisplayNode = Slicer.slicer.vtkMRMLModelDisplayNode() - modelDisplayNode.SetColor(random.random(), random.random(), random.random()) - Slicer.slicer.MRMLScene.AddNode(modelDisplayNode) - modelNode = Slicer.slicer.vtkMRMLModelNode() + sphere = Slicer.slicer.vtkSphereSource() + sphere.SetCenter(-100 + 200 * random.random(), -100 + 200 * random.random(), -100 + 200 * random.random()) + sphere.SetRadius(10 + 20 * random.random()) + sphere.GetOutput().Update() + modelDisplayNode = Slicer.slicer.vtkMRMLModelDisplayNode() + modelDisplayNode.SetColor(random.random(), random.random(), random.random()) + Slicer.slicer.MRMLScene.AddNode(modelDisplayNode) + modelNode = Slicer.slicer.vtkMRMLModelNode() # VTK6 TODO - modelNode.SetAndObservePolyData( sphere.GetOutput() ) - modelNode.SetAndObserveDisplayNodeID( modelDisplayNode.GetID() ) - modelNode.SetName(name) - Slicer.slicer.MRMLScene.AddNode(modelNode) + modelNode.SetAndObservePolyData(sphere.GetOutput()) + modelNode.SetAndObserveDisplayNodeID(modelDisplayNode.GetID()) + modelNode.SetName(name) + Slicer.slicer.MRMLScene.AddNode(modelNode) def sphereMovie(dir="."): - for i in range(20): - newSphere() - Slicer.TkCall( "update" ) - Slicer.TkCall( "SlicerSaveLargeImage %s/spheres-%d.png 3" % (dir, i) ) + for i in range(20): + newSphere() + Slicer.TkCall("update") + Slicer.TkCall("SlicerSaveLargeImage %s/spheres-%d.png 3" % (dir, i)) diff --git a/Testing/TextureModel.py b/Testing/TextureModel.py index ef9a72ac8fc..2b45b0ccc59 100644 --- a/Testing/TextureModel.py +++ b/Testing/TextureModel.py @@ -4,74 +4,74 @@ def newPlane(): - # create a plane polydata - plane = Slicer.slicer.vtkPlaneSource() - plane.SetOrigin( 0., 0., 0. ) - plane.SetPoint1( 100., 0., 0. ) - plane.SetPoint2( 0., 0., 100. ) - plane.GetOutput().Update() - - # create a simple texture image - imageSource = Slicer.slicer.vtkImageEllipsoidSource() - imageSource.GetOutput().Update() - - # set up display node that includes the texture - modelDisplayNode = Slicer.slicer.vtkMRMLModelDisplayNode() - modelDisplayNode.SetBackfaceCulling(0) + # create a plane polydata + plane = Slicer.slicer.vtkPlaneSource() + plane.SetOrigin(0., 0., 0.) + plane.SetPoint1(100., 0., 0.) + plane.SetPoint2(0., 0., 100.) + plane.GetOutput().Update() + + # create a simple texture image + imageSource = Slicer.slicer.vtkImageEllipsoidSource() + imageSource.GetOutput().Update() + + # set up display node that includes the texture + modelDisplayNode = Slicer.slicer.vtkMRMLModelDisplayNode() + modelDisplayNode.SetBackfaceCulling(0) # VTK6 TODO - modelDisplayNode.SetAndObserveTextureImageData(imageSource.GetOutput()) - Slicer.slicer.MRMLScene.AddNode(modelDisplayNode) + modelDisplayNode.SetAndObserveTextureImageData(imageSource.GetOutput()) + Slicer.slicer.MRMLScene.AddNode(modelDisplayNode) - # transform node - transformNode = Slicer.slicer.vtkMRMLLinearTransformNode() - transformNode.SetName('PlaneToWorld') - Slicer.slicer.MRMLScene.AddNode(transformNode) + # transform node + transformNode = Slicer.slicer.vtkMRMLLinearTransformNode() + transformNode.SetName('PlaneToWorld') + Slicer.slicer.MRMLScene.AddNode(transformNode) - # set up model node - modelNode = Slicer.slicer.vtkMRMLModelNode() + # set up model node + modelNode = Slicer.slicer.vtkMRMLModelNode() # VTK6 TODO - modelNode.SetAndObservePolyData( plane.GetOutput() ) - modelNode.SetAndObserveDisplayNodeID( modelDisplayNode.GetID() ) - modelNode.SetAndObserveTransformNodeID( transformNode.GetID() ) - modelNode.SetName("Plane") - Slicer.slicer.MRMLScene.AddNode(modelNode) + modelNode.SetAndObservePolyData(plane.GetOutput()) + modelNode.SetAndObserveDisplayNodeID(modelDisplayNode.GetID()) + modelNode.SetAndObserveTransformNodeID(transformNode.GetID()) + modelNode.SetName("Plane") + Slicer.slicer.MRMLScene.AddNode(modelNode) - # need to invoke a NodeAddedEvent since some GUI elements - # don't respond to each event (for efficiency). In C++ - # you would use the vtkMRMLScene::NodeAddedEvent enum but - # it's not directly available from scripts - Slicer.slicer.MRMLScene.InvokeEvent(66000) + # need to invoke a NodeAddedEvent since some GUI elements + # don't respond to each event (for efficiency). In C++ + # you would use the vtkMRMLScene::NodeAddedEvent enum but + # it's not directly available from scripts + Slicer.slicer.MRMLScene.InvokeEvent(66000) - return (modelNode, transformNode, imageSource) + return (modelNode, transformNode, imageSource) def texturedPlane(): - # create the plane and modify the texture and transform - # every iteration. Call Modified on the PolyData so the - # viewer will know to update. Call Tk's "update" to flush - # the event queue so the Render will appear on screen - # (update is called here as part of the demo - most applications - # should not directly call update since it can lead to duplicate - # renders and choppy interaction) + # create the plane and modify the texture and transform + # every iteration. Call Modified on the PolyData so the + # viewer will know to update. Call Tk's "update" to flush + # the event queue so the Render will appear on screen + # (update is called here as part of the demo - most applications + # should not directly call update since it can lead to duplicate + # renders and choppy interaction) - steps = 200 - startTime = time.time() + steps = 200 + startTime = time.time() - modelNode,transformNode,imageSource = newPlane() + modelNode, transformNode, imageSource = newPlane() - toParent = vtk.vtkMatrix4x4() - transformNode.GetMatrixTransformToParent(toParent) - for i in range(steps): - imageSource.SetInValue( 200*(i%2) ) + toParent = vtk.vtkMatrix4x4() + transformNode.GetMatrixTransformToParent(toParent) + for i in range(steps): + imageSource.SetInValue(200 * (i % 2)) - toParent.SetElement(0, 3, i) - transformNode.SetMatrixTransformToParent(toParent) + toParent.SetElement(0, 3, i) + transformNode.SetMatrixTransformToParent(toParent) - modelNode.GetPolyData().Modified() - Slicer.TkCall( "update" ) + modelNode.GetPolyData().Modified() + Slicer.TkCall("update") - endTime = time.time() - elapsed = endTime - startTime - hertz = int(steps / elapsed) - print('ran %d iterations in %g seconds (%g hertz)' % (steps, elapsed, hertz)) + endTime = time.time() + elapsed = endTime - startTime + hertz = int(steps / elapsed) + print('ran %d iterations in %g seconds (%g hertz)' % (steps, elapsed, hertz)) diff --git a/Utilities/Scripts/ExtensionWizard.py b/Utilities/Scripts/ExtensionWizard.py index 6eff6639784..2101b49c5b8 100755 --- a/Utilities/Scripts/ExtensionWizard.py +++ b/Utilities/Scripts/ExtensionWizard.py @@ -7,5 +7,5 @@ if __name__ == "__main__": - w = ExtensionWizard() - w.execute() + w = ExtensionWizard() + w.execute() diff --git a/Utilities/Scripts/ModuleWizard.py b/Utilities/Scripts/ModuleWizard.py index 2e28ca0ab95..6c65ffe5271 100755 --- a/Utilities/Scripts/ModuleWizard.py +++ b/Utilities/Scripts/ModuleWizard.py @@ -6,118 +6,118 @@ def findSource(dir): - fileList = [] - for root, subFolders, files in os.walk(dir): - for file in files: - if fnmatch.fnmatch(file, "*.h") or \ - fnmatch.fnmatch(file, "*.cxx") or \ - fnmatch.fnmatch(file, "*.cpp") or \ - fnmatch.fnmatch(file, "CMakeLists.txt") or \ - fnmatch.fnmatch(file, "*.cmake") or \ - fnmatch.fnmatch(file, "*.ui") or \ - fnmatch.fnmatch(file, "*.qrc") or \ - fnmatch.fnmatch(file, "*.py") or \ - fnmatch.fnmatch(file, "*.xml") or \ - fnmatch.fnmatch(file, "*.xml.in") or \ - fnmatch.fnmatch(file, "*.md5") or \ - fnmatch.fnmatch(file, "*.png") or \ - fnmatch.fnmatch(file, "*.dox"): - file = os.path.join(root,file) - file = file[len(dir):] # strip common dir - fileList.append(file) - return fileList + fileList = [] + for root, subFolders, files in os.walk(dir): + for file in files: + if fnmatch.fnmatch(file, "*.h") or \ + fnmatch.fnmatch(file, "*.cxx") or \ + fnmatch.fnmatch(file, "*.cpp") or \ + fnmatch.fnmatch(file, "CMakeLists.txt") or \ + fnmatch.fnmatch(file, "*.cmake") or \ + fnmatch.fnmatch(file, "*.ui") or \ + fnmatch.fnmatch(file, "*.qrc") or \ + fnmatch.fnmatch(file, "*.py") or \ + fnmatch.fnmatch(file, "*.xml") or \ + fnmatch.fnmatch(file, "*.xml.in") or \ + fnmatch.fnmatch(file, "*.md5") or \ + fnmatch.fnmatch(file, "*.png") or \ + fnmatch.fnmatch(file, "*.dox"): + file = os.path.join(root, file) + file = file[len(dir):] # strip common dir + fileList.append(file) + return fileList def copyAndReplace(inFile, template, target, key, moduleName): - newFile = os.path.join( target, inFile.replace(key, moduleName) ) - print ("creating %s" % newFile) - path = os.path.dirname(newFile) - if not os.path.exists(path): - os.makedirs(path) - - fp = open(os.path.join(template,inFile)) - contents = fp.read() - fp.close() - contents = contents.replace(key, moduleName) - contents = contents.replace(key.upper(), moduleName.upper()) - fp = open(newFile, "w") - fp.write(contents) - fp.close() + newFile = os.path.join(target, inFile.replace(key, moduleName)) + print("creating %s" % newFile) + path = os.path.dirname(newFile) + if not os.path.exists(path): + os.makedirs(path) + + fp = open(os.path.join(template, inFile)) + contents = fp.read() + fp.close() + contents = contents.replace(key, moduleName) + contents = contents.replace(key.upper(), moduleName.upper()) + fp = open(newFile, "w") + fp.write(contents) + fp.close() def usage(): - print ("") - print ("Usage:") - print ("ModuleWizard [--template ] [--templateKey ] [--target ] ") - print (" --template default ./Extensions/Testing/LoadableExtensionTemplate") - print (" --templateKey default is dirname of template") - print (" --target default ./Modules/Loadable/") - print ("Examples (from Slicer source directory):") - print (" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/LoadableExtensionTemplate --target ../MyExtension MyExtension") - print (" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/ScriptedLoadableExtensionTemplate --target ../MyScript MyScript") - print (" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/EditorExtensionTemplate --target ../MyEditorEffect MyEditorEffect") - print (" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/CLIExtensionTemplate --target ../MyCLI MyCLI") - print (" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/SuperBuildExtensionTemplate --target ../MySuperBuild MySuperBuild") - print ("") + print("") + print("Usage:") + print("ModuleWizard [--template ] [--templateKey ] [--target ] ") + print(" --template default ./Extensions/Testing/LoadableExtensionTemplate") + print(" --templateKey default is dirname of template") + print(" --target default ./Modules/Loadable/") + print("Examples (from Slicer source directory):") + print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/LoadableExtensionTemplate --target ../MyExtension MyExtension") + print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/ScriptedLoadableExtensionTemplate --target ../MyScript MyScript") + print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/EditorExtensionTemplate --target ../MyEditorEffect MyEditorEffect") + print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/CLIExtensionTemplate --target ../MyCLI MyCLI") + print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/SuperBuildExtensionTemplate --target ../MySuperBuild MySuperBuild") + print("") def main(argv): - template = "" - templateKey = "" - target = "" - moduleName = "" - - while argv != []: - arg = argv.pop(0) - if arg == "--template": - template = argv.pop(0) - continue - if arg == "--templateKey": - templateKey = argv.pop(0) - continue - if arg == "--target": - target = argv.pop(0) - continue - if arg == "--help": - usage() - exit() - moduleName = arg - - if moduleName == "": - print ("Please specify module name") - usage() - exit() - - if template == "": - template = "Extensions/Testing/LoadableExtensionTemplate/" - if template[-1] != '/': - template += '/' - - if templateKey == "": - templateKey = os.path.split(template[:-1])[-1] - - if target == "": - target = "Modules/Loadable/" + moduleName - - if os.path.exists(target): - print((target, "exists - delete it first")) - exit() - - if not os.path.exists(template): - print((template, "does not exist - run from Slicer source dir or specify with --template")) - usage() - exit() - - print (f"\nWill copy \n\t{template} \nto \n\t{target} \nreplacing \"{templateKey}\" with \"{moduleName}\"\n") - sources = findSource( template ) - print (sources) - - for file in sources: - copyAndReplace(file, template, target, templateKey, moduleName) - - print ('\nModule %s created!' % moduleName) + template = "" + templateKey = "" + target = "" + moduleName = "" + + while argv != []: + arg = argv.pop(0) + if arg == "--template": + template = argv.pop(0) + continue + if arg == "--templateKey": + templateKey = argv.pop(0) + continue + if arg == "--target": + target = argv.pop(0) + continue + if arg == "--help": + usage() + exit() + moduleName = arg + + if moduleName == "": + print("Please specify module name") + usage() + exit() + + if template == "": + template = "Extensions/Testing/LoadableExtensionTemplate/" + if template[-1] != '/': + template += '/' + + if templateKey == "": + templateKey = os.path.split(template[:-1])[-1] + + if target == "": + target = "Modules/Loadable/" + moduleName + + if os.path.exists(target): + print((target, "exists - delete it first")) + exit() + + if not os.path.exists(template): + print((template, "does not exist - run from Slicer source dir or specify with --template")) + usage() + exit() + + print(f"\nWill copy \n\t{template} \nto \n\t{target} \nreplacing \"{templateKey}\" with \"{moduleName}\"\n") + sources = findSource(template) + print(sources) + + for file in sources: + copyAndReplace(file, template, target, templateKey, moduleName) + + print('\nModule %s created!' % moduleName) if __name__ == "__main__": - main(sys.argv[1:]) + main(sys.argv[1:]) diff --git a/Utilities/Scripts/SEMToMediaWiki.py b/Utilities/Scripts/SEMToMediaWiki.py index 4b253fe81f5..abd64e3125f 100644 --- a/Utilities/Scripts/SEMToMediaWiki.py +++ b/Utilities/Scripts/SEMToMediaWiki.py @@ -28,7 +28,7 @@ def getThisNodesInfoAsText(currentNode, label): Only get the text info for the matching label at this level of the tree """ labelNodeList = [node for node in - currentNode.childNodes if node.nodeName == label] + currentNode.childNodes if node.nodeName == label] if len(labelNodeList) > 0: labelNode = labelNodeList[0] # Only get the first one @@ -44,7 +44,7 @@ def getLongFlagDefinition(currentNode): if labelNodeList.length > 0: labelNode = labelNodeList[0] # Only get the first one return "{}{}{}".format("[--", - getTextValuesFromNode(labelNode.childNodes), "]") + getTextValuesFromNode(labelNode.childNodes), "]") return "" @@ -56,7 +56,7 @@ def getFlagDefinition(currentNode): if labelNodeList.length > 0: labelNode = labelNodeList[0] # Only get the first one return "{}{}{}".format("[-", - getTextValuesFromNode(labelNode.childNodes), "]") + getTextValuesFromNode(labelNode.childNodes), "]") return "" @@ -68,7 +68,7 @@ def getLabelDefinition(currentNode): if labelNodeList.length > 0: labelNode = labelNodeList[0] # Only get the first one return "{}{}{}".format("** '''", - getTextValuesFromNode(labelNode.childNodes), "'''") + getTextValuesFromNode(labelNode.childNodes), "'''") return "" @@ -80,7 +80,7 @@ def getDefaultValueDefinition(currentNode): if labelNodeList.length > 0: labelNode = labelNodeList[0] # Only get the first one return "{}{}{}".format("''Default value: ", - getTextValuesFromNode(labelNode.childNodes), "''") + getTextValuesFromNode(labelNode.childNodes), "''") return "" @@ -91,8 +91,8 @@ def GetSEMDoc(filename): """ doc = xml.dom.minidom.parse(filename) executableNode = [node for node in doc.childNodes if - node.nodeName == "executable"] - #Only use the first + node.nodeName == "executable"] + # Only use the first return executableNode[0] @@ -185,7 +185,7 @@ def DumpSEMMediaWikiFeatures(executableNode): outRegion = "" outRegion += "===Quick Tour of Features and Use===\n\n" outRegion += "{}{}".format("A list panels in the interface,", - " their features, what they mean, and how to use them.\n") + " their features, what they mean, and how to use them.\n") outRegion += "{|\n|\n" # Now print all the command line arguments and the labels # that showup in the GUI interface @@ -196,28 +196,28 @@ def DumpSEMMediaWikiFeatures(executableNode): currentNode = parameterNode.firstChild while currentNode is not None: if currentNode.nodeType == currentNode.ELEMENT_NODE: - #If this node doe not have a "label" element, then skip it. + # If this node doe not have a "label" element, then skip it. if getThisNodesInfoAsText(currentNode, "label") != "": # if this node has a default value -- document it! if getThisNodesInfoAsText(currentNode, "default") != "": outRegion += "{} {} {}: {} {}\n".format( - getLabelDefinition(currentNode), - getLongFlagDefinition(currentNode), - getFlagDefinition(currentNode), - getThisNodesInfoAsText(currentNode, - "description"), - getDefaultValueDefinition(currentNode)) + getLabelDefinition(currentNode), + getLongFlagDefinition(currentNode), + getFlagDefinition(currentNode), + getThisNodesInfoAsText(currentNode, + "description"), + getDefaultValueDefinition(currentNode)) else: outRegion += "{} {} {}: {}\n\n".format( - getLabelDefinition(currentNode), - getLongFlagDefinition(currentNode), - getFlagDefinition(currentNode), - getThisNodesInfoAsText(currentNode, - "description")) + getLabelDefinition(currentNode), + getLongFlagDefinition(currentNode), + getFlagDefinition(currentNode), + getThisNodesInfoAsText(currentNode, + "description")) currentNode = currentNode.nextSibling outRegion += "{}{}\n".format("|[[Image:screenshotBlankNotOptional.png|", - "thumb|280px|User Interface]]") + "thumb|280px|User Interface]]") outRegion += "|}\n\n" return outRegion @@ -287,15 +287,15 @@ def SEMToMediaWikiProg(): version = "%prog v0.1" parser = OptionParser() parser.add_option("-x", "--xmlfile", dest="xmlfilename", - action="store", type="string", - metavar="XMLFILE", help="The SEM formatted XMLFILE file") + action="store", type="string", + metavar="XMLFILE", help="The SEM formatted XMLFILE file") parser.add_option("-o", "--outfile", dest="outfilename", - action="store", type="string", default=None, - metavar="MEDIAWIKIFILE", - help="The MEDIAWIKIFILE ascii file with media-wiki formatted text.") + action="store", type="string", default=None, + metavar="MEDIAWIKIFILE", + help="The MEDIAWIKIFILE ascii file with media-wiki formatted text.") parser.add_option("-p", "--parts", dest="parts", - action="store", type="string", default="hbf", - help="The parts to print out, h=Header,b=body,f=footer") + action="store", type="string", default="hbf", + help="The parts to print out, h=Header,b=body,f=footer") parser.epilog = program_description # print program_description (options, args) = parser.parse_args() diff --git a/Utilities/Scripts/SlicerWizard/CMakeParser.py b/Utilities/Scripts/SlicerWizard/CMakeParser.py index 2f0ae39a6e5..e25f7faa79a 100644 --- a/Utilities/Scripts/SlicerWizard/CMakeParser.py +++ b/Utilities/Scripts/SlicerWizard/CMakeParser.py @@ -23,342 +23,342 @@ import string -#============================================================================= +# ============================================================================= class Token: - """Base class for CMake script tokens. + """Base class for CMake script tokens. - This is the base class for CMake script tokens. An occurrence of a token - whose type is exactly :class:`.Token` (i.e. not a subclass thereof) is a - syntactic error unless the token text is empty. + This is the base class for CMake script tokens. An occurrence of a token + whose type is exactly :class:`.Token` (i.e. not a subclass thereof) is a + syntactic error unless the token text is empty. - .. attribute:: text + .. attribute:: text - The textual content of the token. + The textual content of the token. - .. attribute:: indent + .. attribute:: indent - The whitespace (including newlines) which preceded the token. As the parser - is strictly preserving of whitespace, note that this must be non-empty in - many cases in order to produce a syntactically correct script. - """ + The whitespace (including newlines) which preceded the token. As the parser + is strictly preserving of whitespace, note that this must be non-empty in + many cases in order to produce a syntactically correct script. + """ - #--------------------------------------------------------------------------- - def __init__(self, text, indent=""): - self.text = text - self.indent = indent + # --------------------------------------------------------------------------- + def __init__(self, text, indent=""): + self.text = text + self.indent = indent - #--------------------------------------------------------------------------- - def __repr__(self): - return "Token(text=%(text)r, indent=%(indent)r)" % self.__dict__ + # --------------------------------------------------------------------------- + def __repr__(self): + return "Token(text=%(text)r, indent=%(indent)r)" % self.__dict__ - #--------------------------------------------------------------------------- - def __str__(self): - return self.indent + self.text + # --------------------------------------------------------------------------- + def __str__(self): + return self.indent + self.text -#============================================================================= +# ============================================================================= class String(Token): - """String token. + """String token. - .. attribute:: text + .. attribute:: text - The textual content of the string. Note that escapes are not evaluated and - will appear in their raw (escaped) form. + The textual content of the string. Note that escapes are not evaluated and + will appear in their raw (escaped) form. - .. attribute:: prefix + .. attribute:: prefix - The delimiter which starts this string. The delimiter may be empty, - ``'"'``, or a lua-style long bracket (e.g. ``'[['``, ``'[===['``, etc.). + The delimiter which starts this string. The delimiter may be empty, + ``'"'``, or a lua-style long bracket (e.g. ``'[['``, ``'[===['``, etc.). - .. attribute:: suffix + .. attribute:: suffix - The delimiter which ends this string, which shall match the :attr:`prefix`. + The delimiter which ends this string, which shall match the :attr:`prefix`. - String tokens appear as arguments to :class:`.Command`, as they are not valid - outside of a command context. - """ + String tokens appear as arguments to :class:`.Command`, as they are not valid + outside of a command context. + """ - #--------------------------------------------------------------------------- - def __init__(self, text, indent="", prefix="", suffix=""): - text = super().__init__(text, indent) - self.prefix = prefix - self.suffix = suffix + # --------------------------------------------------------------------------- + def __init__(self, text, indent="", prefix="", suffix=""): + text = super().__init__(text, indent) + self.prefix = prefix + self.suffix = suffix - #--------------------------------------------------------------------------- - def __repr__(self): - return "String(prefix=%(prefix)r, suffix=%(suffix)r," \ - " text=%(text)r, indent=%(indent)r)" % self.__dict__ + # --------------------------------------------------------------------------- + def __repr__(self): + return "String(prefix=%(prefix)r, suffix=%(suffix)r," \ + " text=%(text)r, indent=%(indent)r)" % self.__dict__ - #--------------------------------------------------------------------------- - def __str__(self): - return self.indent + self.prefix + self.text + self.suffix + # --------------------------------------------------------------------------- + def __str__(self): + return self.indent + self.prefix + self.text + self.suffix -#============================================================================= +# ============================================================================= class Comment(Token): - """Comment token. + """Comment token. - .. attribute:: text + .. attribute:: text - The textual content of the comment. + The textual content of the comment. - .. attribute:: prefix + .. attribute:: prefix - The delimiter which starts this comment: ``'#'``, optionally followed by a - lua-style long bracket (e.g. ``'[['``, ``'[===['``, etc.). + The delimiter which starts this comment: ``'#'``, optionally followed by a + lua-style long bracket (e.g. ``'[['``, ``'[===['``, etc.). - .. attribute:: suffix + .. attribute:: suffix - The delimiter which ends this comment: either empty, or a lua-style long - bracket which shall match the long bracket in :attr:`prefix`. - """ + The delimiter which ends this comment: either empty, or a lua-style long + bracket which shall match the long bracket in :attr:`prefix`. + """ - #--------------------------------------------------------------------------- - def __init__(self, prefix, text, indent="", suffix=""): - text = super().__init__(text, indent) - self.prefix = prefix - self.suffix = suffix + # --------------------------------------------------------------------------- + def __init__(self, prefix, text, indent="", suffix=""): + text = super().__init__(text, indent) + self.prefix = prefix + self.suffix = suffix - #--------------------------------------------------------------------------- - def __repr__(self): - return "Comment(prefix=%(prefix)r, suffix=%(suffix)r," \ - " text=%(text)r, indent=%(indent)r)" % self.__dict__ + # --------------------------------------------------------------------------- + def __repr__(self): + return "Comment(prefix=%(prefix)r, suffix=%(suffix)r," \ + " text=%(text)r, indent=%(indent)r)" % self.__dict__ - #--------------------------------------------------------------------------- - def __str__(self): - return self.indent + self.prefix + self.text + self.suffix + # --------------------------------------------------------------------------- + def __str__(self): + return self.indent + self.prefix + self.text + self.suffix -#============================================================================= +# ============================================================================= class Command(Token): - """Command token. + """Command token. - .. attribute:: text + .. attribute:: text - The name of the command. + The name of the command. - .. attribute:: prefix + .. attribute:: prefix - The delimiter which starts the command's argument list. This shall end with - ``'('`` and may begin with whitespace if there is whitespace separating the - command name from the '('. + The delimiter which starts the command's argument list. This shall end with + ``'('`` and may begin with whitespace if there is whitespace separating the + command name from the '('. - .. attribute:: suffix + .. attribute:: suffix - The delimiter which ends the command's argument list. This shall end with - ``')'`` and may begin with whitespace if there is whitespace separating the - last argument (or the opening '(' if there are no arguments) from the ')'. + The delimiter which ends the command's argument list. This shall end with + ``')'`` and may begin with whitespace if there is whitespace separating the + last argument (or the opening '(' if there are no arguments) from the ')'. - .. attribute:: arguments + .. attribute:: arguments - A :class:`list` of :class:`.String` tokens which comprise the arguments of - the command. - """ + A :class:`list` of :class:`.String` tokens which comprise the arguments of + the command. + """ - #--------------------------------------------------------------------------- - def __init__(self, text, arguments=[], indent="", prefix="(", suffix=")"): - text = super().__init__(text, indent) - self.prefix = prefix - self.suffix = suffix - self.arguments = arguments + # --------------------------------------------------------------------------- + def __init__(self, text, arguments=[], indent="", prefix="(", suffix=")"): + text = super().__init__(text, indent) + self.prefix = prefix + self.suffix = suffix + self.arguments = arguments - #--------------------------------------------------------------------------- - def __repr__(self): - return "Command(text=%(text)r, prefix=%(prefix)r," \ - " suffix=%(suffix)r, arguments=%(arguments)r," \ - " indent=%(indent)r)" % self.__dict__ + # --------------------------------------------------------------------------- + def __repr__(self): + return "Command(text=%(text)r, prefix=%(prefix)r," \ + " suffix=%(suffix)r, arguments=%(arguments)r," \ + " indent=%(indent)r)" % self.__dict__ - #--------------------------------------------------------------------------- - def __str__(self): - args = "".join([str(a) for a in self.arguments]) - return self.indent + self.text + self.prefix + args + self.suffix + # --------------------------------------------------------------------------- + def __str__(self): + args = "".join([str(a) for a in self.arguments]) + return self.indent + self.text + self.prefix + args + self.suffix -#============================================================================= +# ============================================================================= class CMakeScript: - """Tokenized representation of a CMake script. + """Tokenized representation of a CMake script. - .. attribute:: tokens + .. attribute:: tokens - The :class:`list` of tokens which comprise the script. Manipulations of - this list should be used to change the content of the script. - """ + The :class:`list` of tokens which comprise the script. Manipulations of + this list should be used to change the content of the script. + """ - _reWhitespace = re.compile(r"\s") - _reCommand = re.compile(r"([" + string.ascii_letters + r"]\w*)(\s*\()") - _reComment = re.compile(r"#(\[=*\[)?") - _reQuote = re.compile("\"") - _reBracketQuote = re.compile(r"\[=*\[") - _reEscape = re.compile(r"\\[\\\"nrt$ ]") + _reWhitespace = re.compile(r"\s") + _reCommand = re.compile(r"([" + string.ascii_letters + r"]\w*)(\s*\()") + _reComment = re.compile(r"#(\[=*\[)?") + _reQuote = re.compile("\"") + _reBracketQuote = re.compile(r"\[=*\[") + _reEscape = re.compile(r"\\[\\\"nrt$ ]") - #--------------------------------------------------------------------------- - def __init__(self, content): - """ - :param content: Textual content of a CMake script. - :type content: :class:`str` + # --------------------------------------------------------------------------- + def __init__(self, content): + """ + :param content: Textual content of a CMake script. + :type content: :class:`str` - :raises: - :exc:`~exceptions.SyntaxError` or :exc:`~exceptions.EOFError` if a - parsing error occurs (i.e. if the input text is not syntactically valid). + :raises: + :exc:`~exceptions.SyntaxError` or :exc:`~exceptions.EOFError` if a + parsing error occurs (i.e. if the input text is not syntactically valid). - .. code-block:: python + .. code-block:: python - with open('CMakeLists.txt') as input_file: - script = CMakeParser.CMakeScript(input_file.read()) + with open('CMakeLists.txt') as input_file: + script = CMakeParser.CMakeScript(input_file.read()) - with open('CMakeLists.txt.new', 'w') as output_file: - output_file.write(str(script)) - """ + with open('CMakeLists.txt.new', 'w') as output_file: + output_file.write(str(script)) + """ - self.tokens = [] + self.tokens = [] - self._content = content - self._match = None + self._content = content + self._match = None - while len(self._content): - indent = self._chompSpace() + while len(self._content): + indent = self._chompSpace() - # Consume comments - if self._is(self._reComment): - self.tokens.append(self._parseComment(self._match, indent)) + # Consume comments + if self._is(self._reComment): + self.tokens.append(self._parseComment(self._match, indent)) - # Consume commands - elif self._is(self._reCommand): - self.tokens.append(self._parseCommand(self._match, indent)) + # Consume commands + elif self._is(self._reCommand): + self.tokens.append(self._parseCommand(self._match, indent)) - # Consume other tokens (pedantically, if we get here, the script is - # malformed, except at EOF) - else: - m = self._reWhitespace.search(self._content) - n = m.start() if m is not None else len(self._content) - self.tokens.append(Token(text=self._content[:n], indent=indent)) - self._content = self._content[n:] + # Consume other tokens (pedantically, if we get here, the script is + # malformed, except at EOF) + else: + m = self._reWhitespace.search(self._content) + n = m.start() if m is not None else len(self._content) + self.tokens.append(Token(text=self._content[:n], indent=indent)) + self._content = self._content[n:] - #--------------------------------------------------------------------------- - def __repr__(self): - return repr(self.tokens) + # --------------------------------------------------------------------------- + def __repr__(self): + return repr(self.tokens) - #--------------------------------------------------------------------------- - def __str__(self): - return "".join([str(t) for t in self.tokens]) + # --------------------------------------------------------------------------- + def __str__(self): + return "".join([str(t) for t in self.tokens]) - #--------------------------------------------------------------------------- - def _chomp(self): - result = self._content[0] - self._content = self._content[1:] - return result + # --------------------------------------------------------------------------- + def _chomp(self): + result = self._content[0] + self._content = self._content[1:] + return result - #--------------------------------------------------------------------------- - def _chompSpace(self): - result = "" + # --------------------------------------------------------------------------- + def _chompSpace(self): + result = "" - while len(self._content) and self._content[0].isspace(): - result += self._content[0] - self._content = self._content[1:] + while len(self._content) and self._content[0].isspace(): + result += self._content[0] + self._content = self._content[1:] - return result + return result - #--------------------------------------------------------------------------- - def _chompString(self, end, escapes): - result = "" + # --------------------------------------------------------------------------- + def _chompString(self, end, escapes): + result = "" - while len(self._content): - if escapes and self._is(self._reEscape): - e = self._match.group(0) - result += e - self._content = self._content[len(e):] + while len(self._content): + if escapes and self._is(self._reEscape): + e = self._match.group(0) + result += e + self._content = self._content[len(e):] - elif self._content.startswith(end): - self._content = self._content[len(end):] - return result + elif self._content.startswith(end): + self._content = self._content[len(end):] + return result - else: - result += self._chomp() + else: + result += self._chomp() - raise EOFError("unexpected EOF while parsing string (expected %r)" % end) + raise EOFError("unexpected EOF while parsing string (expected %r)" % end) - #--------------------------------------------------------------------------- - def _parseArgument(self, indent): - text = "" + # --------------------------------------------------------------------------- + def _parseArgument(self, indent): + text = "" - while len(self._content): - if self._is(self._reQuote) or self._is(self._reBracketQuote): - prefix = self._match.group(0) - self._content = self._content[len(prefix):] + while len(self._content): + if self._is(self._reQuote) or self._is(self._reBracketQuote): + prefix = self._match.group(0) + self._content = self._content[len(prefix):] - if prefix == "\"": - suffix = prefix - s = self._chompString(suffix, escapes=True) + if prefix == "\"": + suffix = prefix + s = self._chompString(suffix, escapes=True) - else: - suffix = prefix.replace("[", "]") - s = self._chompString(suffix, escapes=False) + else: + suffix = prefix.replace("[", "]") + s = self._chompString(suffix, escapes=False) - if not len(text): - return String(prefix=prefix, suffix=suffix, text=s, indent=indent) + if not len(text): + return String(prefix=prefix, suffix=suffix, text=s, indent=indent) - text += prefix + s + suffix + text += prefix + s + suffix - elif self._content[0].isspace(): - break + elif self._content[0].isspace(): + break - elif self._is(self._reEscape): - e = self._match.group(0) - text += e - self._content = self._content[len(e):] + elif self._is(self._reEscape): + e = self._match.group(0) + text += e + self._content = self._content[len(e):] - elif self._content[0] == ")": - break + elif self._content[0] == ")": + break - else: - text += self._chomp() + else: + text += self._chomp() - return String(text=text, indent=indent) + return String(text=text, indent=indent) - #--------------------------------------------------------------------------- - def _parseComment(self, match, indent): - b = match.group(1) - e = "\n" if b is None else b.replace("[", "]") - n = self._content.find(e) - if n < 0: - raise EOFError("unexpected EOF while parsing comment (expected %r" % e) + # --------------------------------------------------------------------------- + def _parseComment(self, match, indent): + b = match.group(1) + e = "\n" if b is None else b.replace("[", "]") + n = self._content.find(e) + if n < 0: + raise EOFError("unexpected EOF while parsing comment (expected %r" % e) - i = match.end() - suffix = e.strip() - token = Comment(prefix=self._content[:i], suffix=suffix, - text=self._content[i:n], indent=indent) + i = match.end() + suffix = e.strip() + token = Comment(prefix=self._content[:i], suffix=suffix, + text=self._content[i:n], indent=indent) - self._content = self._content[n + len(suffix):] + self._content = self._content[n + len(suffix):] - return token + return token - #--------------------------------------------------------------------------- - def _parseCommand(self, match, indent): - command = match.group(1) - prefix = match.group(2) - arguments = [] + # --------------------------------------------------------------------------- + def _parseCommand(self, match, indent): + command = match.group(1) + prefix = match.group(2) + arguments = [] - self._content = self._content[match.end():] + self._content = self._content[match.end():] - while len(self._content): - argIndent = self._chompSpace() + while len(self._content): + argIndent = self._chompSpace() - if not len(self._content): - break + if not len(self._content): + break - if self._content[0] == ")": - self._content = self._content[1:] - return Command(text=command, arguments=arguments, indent=indent, - prefix=prefix, suffix=argIndent + ")") - elif self._is(self._reComment): - arguments.append(self._parseComment(self._match, argIndent)) + if self._content[0] == ")": + self._content = self._content[1:] + return Command(text=command, arguments=arguments, indent=indent, + prefix=prefix, suffix=argIndent + ")") + elif self._is(self._reComment): + arguments.append(self._parseComment(self._match, argIndent)) - else: - arguments.append(self._parseArgument(argIndent)) + else: + arguments.append(self._parseArgument(argIndent)) - raise EOFError("unexpected EOF while parsing command (expected ')')") + raise EOFError("unexpected EOF while parsing command (expected ')')") - #--------------------------------------------------------------------------- - def _is(self, regex): - self._match = regex.match(self._content) - return self._match is not None + # --------------------------------------------------------------------------- + def _is(self, regex): + self._match = regex.match(self._content) + return self._match is not None diff --git a/Utilities/Scripts/SlicerWizard/ExtensionDescription.py b/Utilities/Scripts/SlicerWizard/ExtensionDescription.py index 730c44c74f8..0f5a711b2d4 100644 --- a/Utilities/Scripts/SlicerWizard/ExtensionDescription.py +++ b/Utilities/Scripts/SlicerWizard/ExtensionDescription.py @@ -6,307 +6,308 @@ from .ExtensionProject import ExtensionProject -#============================================================================= +# ============================================================================= class ExtensionDescription: - """Representation of an extension description. - - This class provides a Python object representation of an extension - description. The extension information is made available as attributes on the - object. The "well known" attributes are described - :wikidoc:`Developers/Extensions/DescriptionFile here`. Custom attributes may - be added with :func:`setattr`. Attributes may be removed with :func:`delattr` - or the :meth:`.clear` method. - """ - - _reParam = re.compile(r"([a-zA-Z][a-zA-Z0-9_]*)\s+(.+)") - - DESCRIPTION_FILE_TEMPLATE = None - - #--------------------------------------------------------------------------- - def __init__(self, repo=None, filepath=None, sourcedir=None, cmakefile="CMakeLists.txt"): - """ - :param repo: - Extension repository from which to create the description. - :type repo: - :class:`git.Repo `, - :class:`.Subversion.Repository` or ``None``. - :param filepath: - Path to an existing ``.s4ext`` to read. - :type filepath: - :class:`str` or ``None``. - :param sourcedir: - Path to an extension source directory. - :type sourcedir: - :class:`str` or ``None``. - :param cmakefile: - Name of the CMake file where `EXTENSION_*` CMake variables - are set. Default is `CMakeLists.txt`. - :type cmakefile: - :class:`str` - - :raises: - * :exc:`~exceptions.KeyError` if the extension description is missing a - required attribute. - * :exc:`~exceptions.Exception` if there is some other problem - constructing the description. - - The description may be created from a repository instance (in which case - the description repository information will be populated), a path to the - extension source directory, or a path to an existing ``.s4ext`` file. - No more than one of ``repo``, ``filepath`` or ``sourcedir`` may be given. - If none are provided, the description will be incomplete. + """Representation of an extension description. + + This class provides a Python object representation of an extension + description. The extension information is made available as attributes on the + object. The "well known" attributes are described + :wikidoc:`Developers/Extensions/DescriptionFile here`. Custom attributes may + be added with :func:`setattr`. Attributes may be removed with :func:`delattr` + or the :meth:`.clear` method. """ - args = (repo, filepath, sourcedir) - if args.count(None) < len(args) - 1: - raise Exception("cannot construct %s: only one of" - " (repo, filepath, sourcedir) may be given" % - type(self).__name__) - - if filepath is not None: - with open(filepath) as fp: - self._read(fp) - - elif repo is not None: - # Handle git repositories - if hasattr(repo, "remotes"): - remote = None - svnRemote = None - - # Get SHA of HEAD (may not exist if no commit has been made yet!) - try: - sha = repo.head.commit.hexsha - - except ValueError: - sha = "NA" - - # Try to get git remote - try: - remote = repo.remotes.origin - except: - if len(repo.remotes) == 1: - remote = repo.remotes[0] - - if remote is None: - # Try to get svn remote - config = repo.config_reader() - for s in config.sections(): - if s.startswith("svn-remote"): - svnRemote = s[12:-1] - break - - if svnRemote is None: - # Do we have any remotes? - if len(repo.remotes) == 0: - setattr(self, "scm", "git") - setattr(self, "scmurl", "NA") - setattr(self, "scmrevision", sha) - - else: - raise Exception("unable to determine repository's primary remote") - - else: - si = self._gitSvnInfo(repo, svnRemote) - setattr(self, "scm", "svn") - setattr(self, "scmurl", si["URL"]) - setattr(self, "scmrevision", si["Revision"]) + _reParam = re.compile(r"([a-zA-Z][a-zA-Z0-9_]*)\s+(.+)") + + DESCRIPTION_FILE_TEMPLATE = None + + # --------------------------------------------------------------------------- + def __init__(self, repo=None, filepath=None, sourcedir=None, cmakefile="CMakeLists.txt"): + """ + :param repo: + Extension repository from which to create the description. + :type repo: + :class:`git.Repo `, + :class:`.Subversion.Repository` or ``None``. + :param filepath: + Path to an existing ``.s4ext`` to read. + :type filepath: + :class:`str` or ``None``. + :param sourcedir: + Path to an extension source directory. + :type sourcedir: + :class:`str` or ``None``. + :param cmakefile: + Name of the CMake file where `EXTENSION_*` CMake variables + are set. Default is `CMakeLists.txt`. + :type cmakefile: + :class:`str` + + :raises: + * :exc:`~exceptions.KeyError` if the extension description is missing a + required attribute. + * :exc:`~exceptions.Exception` if there is some other problem + constructing the description. + + The description may be created from a repository instance (in which case + the description repository information will be populated), a path to the + extension source directory, or a path to an existing ``.s4ext`` file. + No more than one of ``repo``, ``filepath`` or ``sourcedir`` may be given. + If none are provided, the description will be incomplete. + """ + + args = (repo, filepath, sourcedir) + if args.count(None) < len(args) - 1: + raise Exception("cannot construct %s: only one of" + " (repo, filepath, sourcedir) may be given" % + type(self).__name__) + + if filepath is not None: + with open(filepath) as fp: + self._read(fp) + + elif repo is not None: + # Handle git repositories + if hasattr(repo, "remotes"): + remote = None + svnRemote = None + + # Get SHA of HEAD (may not exist if no commit has been made yet!) + try: + sha = repo.head.commit.hexsha + + except ValueError: + sha = "NA" + + # Try to get git remote + try: + remote = repo.remotes.origin + except: + if len(repo.remotes) == 1: + remote = repo.remotes[0] + + if remote is None: + # Try to get svn remote + config = repo.config_reader() + for s in config.sections(): + if s.startswith("svn-remote"): + svnRemote = s[12:-1] + break + + if svnRemote is None: + # Do we have any remotes? + if len(repo.remotes) == 0: + setattr(self, "scm", "git") + setattr(self, "scmurl", "NA") + setattr(self, "scmrevision", sha) + + else: + raise Exception("unable to determine repository's primary remote") + + else: + si = self._gitSvnInfo(repo, svnRemote) + setattr(self, "scm", "svn") + setattr(self, "scmurl", si["URL"]) + setattr(self, "scmrevision", si["Revision"]) + + else: + setattr(self, "scm", "git") + setattr(self, "scmurl", self._remotePublicUrl(remote)) + setattr(self, "scmrevision", sha) + + sourcedir = repo.working_tree_dir + + # Handle svn repositories + elif hasattr(repo, "wc_root"): + setattr(self, "scm", "svn") + setattr(self, "scmurl", repo.url) + setattr(self, "scmrevision", repo.last_change_revision) + sourcedir = repo.wc_root + + # Handle local source directory + elif hasattr(repo, "relative_directory"): + setattr(self, "scm", "local") + setattr(self, "scmurl", repo.relative_directory) + setattr(self, "scmrevision", "NA") + sourcedir = os.path.join(repo.root, repo.relative_directory) else: - setattr(self, "scm", "git") - setattr(self, "scmurl", self._remotePublicUrl(remote)) - setattr(self, "scmrevision", sha) - - sourcedir = repo.working_tree_dir - - # Handle svn repositories - elif hasattr(repo, "wc_root"): - setattr(self, "scm", "svn") - setattr(self, "scmurl", repo.url) - setattr(self, "scmrevision", repo.last_change_revision) - sourcedir = repo.wc_root - - # Handle local source directory - elif hasattr(repo, "relative_directory"): - setattr(self, "scm", "local") - setattr(self, "scmurl", repo.relative_directory) - setattr(self, "scmrevision", "NA") - sourcedir = os.path.join(repo.root, repo.relative_directory) - - else: - setattr(self, "scm", "local") - setattr(self, "scmurl", "NA") - setattr(self, "scmrevision", "NA") - - if sourcedir is not None: - p = ExtensionProject(sourcedir, filename=cmakefile) - self._setProjectAttribute("homepage", p, required=True) - self._setProjectAttribute("category", p, required=True) - self._setProjectAttribute("description", p) - self._setProjectAttribute("contributors", p) - - self._setProjectAttribute("status", p) - self._setProjectAttribute("enabled", p, default="1") - self._setProjectAttribute("depends", p, default="NA") - self._setProjectAttribute("build_subdirectory", p, default=".") - - self._setProjectAttribute("iconurl", p) - self._setProjectAttribute("screenshoturls", p) - - if self.scm == "svn": - self._setProjectAttribute("svnusername", p, elideempty=True) - self._setProjectAttribute("svnpassword", p, elideempty=True) - - #--------------------------------------------------------------------------- - def __repr__(self): - return repr(self.__dict__) - - #--------------------------------------------------------------------------- - @staticmethod - def _remotePublicUrl(remote): - url = remote.url - if url.startswith("git@"): - return url.replace(":", "/").replace("git@", "https://") - - return url - - #--------------------------------------------------------------------------- - @staticmethod - def _gitSvnInfo(repo, remote): - result = {} - for l in repo.git.svn('info', R=remote).split("\n"): - if len(l): - key, value = l.split(":", 1) - result[key] = value.strip() - return result - - #--------------------------------------------------------------------------- - def _setProjectAttribute(self, name, project, default=None, required=False, - elideempty=False, substitute=True): - - if default is None and not required: - default="" - - v = project.getValue("EXTENSION_" + name.upper(), default, substitute) - - if len(v) or not elideempty: - setattr(self, name, v) - - #--------------------------------------------------------------------------- - def clear(self, attr=None): - """Remove attributes from the extension description. - - :param attr: Name of attribute to remove. - :type attr: :class:`str` or ``None`` - - If ``attr`` is not ``None``, this removes the specified attribute from the - description object, equivalent to calling ``delattr(instance, attr)``. If - ``attr`` is ``None``, all attributes are removed. - """ - - for key in self.__dict__.keys() if attr is None else (attr,): - delattr(self, key) - - #--------------------------------------------------------------------------- - def _read(self, fp): - for l in fp: - m = self._reParam.match(l) - if m is not None: - setattr(self, m.group(1), m.group(2).strip()) + setattr(self, "scm", "local") + setattr(self, "scmurl", "NA") + setattr(self, "scmrevision", "NA") + + if sourcedir is not None: + p = ExtensionProject(sourcedir, filename=cmakefile) + self._setProjectAttribute("homepage", p, required=True) + self._setProjectAttribute("category", p, required=True) + self._setProjectAttribute("description", p) + self._setProjectAttribute("contributors", p) + + self._setProjectAttribute("status", p) + self._setProjectAttribute("enabled", p, default="1") + self._setProjectAttribute("depends", p, default="NA") + self._setProjectAttribute("build_subdirectory", p, default=".") + + self._setProjectAttribute("iconurl", p) + self._setProjectAttribute("screenshoturls", p) + + if self.scm == "svn": + self._setProjectAttribute("svnusername", p, elideempty=True) + self._setProjectAttribute("svnpassword", p, elideempty=True) + + # --------------------------------------------------------------------------- + def __repr__(self): + return repr(self.__dict__) + + # --------------------------------------------------------------------------- + @staticmethod + def _remotePublicUrl(remote): + url = remote.url + if url.startswith("git@"): + return url.replace(":", "/").replace("git@", "https://") + + return url + + # --------------------------------------------------------------------------- + @staticmethod + def _gitSvnInfo(repo, remote): + result = {} + for line in repo.git.svn('info', R=remote).split("\n"): + if len(line): + key, value = line.split(":", 1) + result[key] = value.strip() + return result + + # --------------------------------------------------------------------------- + def _setProjectAttribute(self, name, project, default=None, required=False, + elideempty=False, substitute=True): + + if default is None and not required: + default = "" + + v = project.getValue("EXTENSION_" + name.upper(), default, substitute) + + if len(v) or not elideempty: + setattr(self, name, v) + + # --------------------------------------------------------------------------- + def clear(self, attr=None): + """Remove attributes from the extension description. + + :param attr: Name of attribute to remove. + :type attr: :class:`str` or ``None`` + + If ``attr`` is not ``None``, this removes the specified attribute from the + description object, equivalent to calling ``delattr(instance, attr)``. If + ``attr`` is ``None``, all attributes are removed. + """ + + for key in self.__dict__.keys() if attr is None else (attr,): + delattr(self, key) + + # --------------------------------------------------------------------------- + def _read(self, fp): + for line in fp: + m = self._reParam.match(line) + if m is not None: + setattr(self, m.group(1), m.group(2).strip()) + + # --------------------------------------------------------------------------- + def read(self, path): + """Read extension description from directory. + + :param path: Directory containing extension description. + :type path: :class:`str` + + :raises: + :exc:`~exceptions.IOError` if ``path`` does not contain exactly one + extension description file. + + This attempts to read an extension description from the specified ``path`` + which contains a single extension description (``.s4ext``) file (usually an + extension build directory). + """ + + self.clear() + + descriptionFiles = glob.glob(os.path.join(path, "*.[Ss]4[Ee][Xx][Tt]")) + if len(descriptionFiles) < 1: + raise OSError("extension description file not found") + + if len(descriptionFiles) > 1: + raise OSError("multiple extension description files found") + + with open(descriptionFiles[0]) as fp: + self._read(fp) + + # --------------------------------------------------------------------------- + @staticmethod + def _findOccurences(a_str, sub): + start = 0 + while True: + start = a_str.find(sub, start) + if start == -1: + return + yield start + start += len(sub) + + # --------------------------------------------------------------------------- + def _write(self, fp): + # Creation of the map + dictio = dict() + dictio["scm_type"] = getattr(self, "scm") + dictio["scm_url"] = getattr(self, "scmurl") + dictio["MY_EXTENSION_WC_REVISION"] = getattr(self, "scmrevision") + dictio["MY_EXTENSION_DEPENDS"] = getattr(self, "depends") + dictio["MY_EXTENSION_BUILD_SUBDIRECTORY"] = getattr(self, "build_subdirectory") + dictio["MY_EXTENSION_HOMEPAGE"] = getattr(self, "homepage") + dictio["MY_EXTENSION_CONTRIBUTORS"] = getattr(self, "contributors") + dictio["MY_EXTENSION_CATEGORY"] = getattr(self, "category") + dictio["MY_EXTENSION_ICONURL"] = getattr(self, "iconurl") + dictio["MY_EXTENSION_STATUS"] = getattr(self, "status") + dictio["MY_EXTENSION_DESCRIPTION"] = getattr(self, "description") + dictio["MY_EXTENSION_SCREENSHOTURLS"] = getattr(self, "screenshoturls") + dictio["MY_EXTENSION_ENABLED"] = getattr(self, "enabled") + + if self.DESCRIPTION_FILE_TEMPLATE is not None: + extDescriptFile = open(self.DESCRIPTION_FILE_TEMPLATE) + for line in extDescriptFile.readlines(): + if "${" in line: + variables = self._findOccurences(line, "$") + temp = line + for variable in variables: + if line[variable] == '$' and line[variable + 1] == '{': + var = "" + i = variable + 2 + while line[i] != '}': + var += line[i] + i += 1 + temp = temp.replace("${" + var + "}", dictio[var]) + fp.write(temp) + else: + fp.write(line) + else: + logging.warning("failed to generate description file using template") + logging.warning("generating description file using fallback method") + for key in sorted(self.__dict__): + fp.write((f"{key} {getattr(self, key)}").strip() + "\n") - #--------------------------------------------------------------------------- - def read(self, path): - """Read extension description from directory. + # --------------------------------------------------------------------------- + def write(self, out): + """Write extension description to a file or stream. - :param path: Directory containing extension description. - :type path: :class:`str` + :param out: Stream or path to which to write the description. + :type out: :class:`~io.IOBase` or :class:`str` - :raises: - :exc:`~exceptions.IOError` if ``path`` does not contain exactly one - extension description file. + This writes the extension description to the specified file path or stream + object. This is suitable for producing a ``.s4ext`` file from a description + object. + """ - This attempts to read an extension description from the specified ``path`` - which contains a single extension description (``.s4ext``) file (usually an - extension build directory). - """ + if hasattr(out, "write") and callable(out.write): + self._write(out) - self.clear() - - descriptionFiles = glob.glob(os.path.join(path, "*.[Ss]4[Ee][Xx][Tt]")) - if len(descriptionFiles) < 1: - raise OSError("extension description file not found") - - if len(descriptionFiles) > 1: - raise OSError("multiple extension description files found") - - with open(descriptionFiles[0]) as fp: - self._read(fp) - - #--------------------------------------------------------------------------- - @staticmethod - def _findOccurences(a_str, sub): - start = 0 - while True: - start = a_str.find(sub, start) - if start == -1: return - yield start - start += len(sub) - - #--------------------------------------------------------------------------- - def _write(self, fp): - # Creation of the map - dictio = dict() - dictio["scm_type"] = getattr(self, "scm") - dictio["scm_url"] = getattr(self, "scmurl") - dictio["MY_EXTENSION_WC_REVISION"] = getattr(self, "scmrevision") - dictio["MY_EXTENSION_DEPENDS"] = getattr(self, "depends") - dictio["MY_EXTENSION_BUILD_SUBDIRECTORY"] = getattr(self, "build_subdirectory") - dictio["MY_EXTENSION_HOMEPAGE"] = getattr(self, "homepage") - dictio["MY_EXTENSION_CONTRIBUTORS"] = getattr(self, "contributors") - dictio["MY_EXTENSION_CATEGORY"] = getattr(self, "category") - dictio["MY_EXTENSION_ICONURL"] = getattr(self, "iconurl") - dictio["MY_EXTENSION_STATUS"] = getattr(self, "status") - dictio["MY_EXTENSION_DESCRIPTION"] = getattr(self, "description") - dictio["MY_EXTENSION_SCREENSHOTURLS"] = getattr(self, "screenshoturls") - dictio["MY_EXTENSION_ENABLED"] = getattr(self, "enabled") - - if self.DESCRIPTION_FILE_TEMPLATE is not None: - extDescriptFile = open(self.DESCRIPTION_FILE_TEMPLATE) - for line in extDescriptFile.readlines() : - if "${" in line: - variables = self._findOccurences(line, "$") - temp = line - for variable in variables: - if line[variable] == '$' and line[variable + 1] == '{': - var = "" - i = variable + 2 - while line[i] != '}': - var+=line[i] - i+=1 - temp = temp.replace("${" + var + "}", dictio[var]) - fp.write(temp) else: - fp.write(line) - else: - logging.warning("failed to generate description file using template") - logging.warning("generating description file using fallback method") - for key in sorted(self.__dict__): - fp.write((f"{key} {getattr(self, key)}").strip() + "\n") - - #--------------------------------------------------------------------------- - def write(self, out): - """Write extension description to a file or stream. - - :param out: Stream or path to which to write the description. - :type out: :class:`~io.IOBase` or :class:`str` - - This writes the extension description to the specified file path or stream - object. This is suitable for producing a ``.s4ext`` file from a description - object. - """ - - if hasattr(out, "write") and callable(out.write): - self._write(out) - - else: - with open(out, "w") as fp: - self._write(fp) + with open(out, "w") as fp: + self._write(fp) diff --git a/Utilities/Scripts/SlicerWizard/ExtensionProject.py b/Utilities/Scripts/SlicerWizard/ExtensionProject.py index 228ffd8ab01..ee36cf94272 100644 --- a/Utilities/Scripts/SlicerWizard/ExtensionProject.py +++ b/Utilities/Scripts/SlicerWizard/ExtensionProject.py @@ -6,403 +6,403 @@ from .Utilities import detectEncoding -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _isCommand(token, name): - return isinstance(token, CMakeParser.Command) and token.text.lower() == name + return isinstance(token, CMakeParser.Command) and token.text.lower() == name -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _trimIndent(indent): - indent = "\n" + indent - n = indent.rindex("\n") - return indent[n:] + indent = "\n" + indent + n = indent.rindex("\n") + return indent[n:] -#============================================================================= +# ============================================================================= class ExtensionProject: - """Convenience class for manipulating an extension project. + """Convenience class for manipulating an extension project. - This class provides an additional layer of convenience for users that wish to - manipulate the CMakeLists.txt of an extension project. The term "build - script" is used throughout to refer to the CMakeLists.txt so encapsulated. + This class provides an additional layer of convenience for users that wish to + manipulate the CMakeLists.txt of an extension project. The term "build + script" is used throughout to refer to the CMakeLists.txt so encapsulated. - Modifications to the script are made to the in-memory, parsed representation. - Use :meth:`.save` to write changes back to disk. + Modifications to the script are made to the in-memory, parsed representation. + Use :meth:`.save` to write changes back to disk. - This class may be used as a context manager. When used in this manner, any - changes made are automatically written back to the project's CMakeLists.txt - when the context goes out of scope. - """ + This class may be used as a context manager. When used in this manner, any + changes made are automatically written back to the project's CMakeLists.txt + when the context goes out of scope. + """ - _moduleInsertPlaceholder = "# NEXT_MODULE" + _moduleInsertPlaceholder = "# NEXT_MODULE" - _referencedVariables = re.compile(r"\$\{([\w_\/\.\+\-]+)\}") + _referencedVariables = re.compile(r"\$\{([\w_\/\.\+\-]+)\}") + + # --------------------------------------------------------------------------- + def __init__(self, path, encoding=None, filename="CMakeLists.txt", ): + """ + :param path: Top level directory of the extension project. + :type path: :class:`str` + :param encoding: Encoding of extension CMakeLists.txt. + :type encoding: :class:`str` or ``None`` + :param filename: CMake file to parse. Default is `CMakeLists.txt`. + :type filename: :class:`str` + + If ``encoding`` is ``None``, the encoding will be guessed using + :meth:`~SlicerWizard.Utilities.detectEncoding`. + """ + cmakeFile = os.path.join(path, filename) + if not os.path.exists(cmakeFile): + raise OSError("%s not found" % filename) - #--------------------------------------------------------------------------- - def __init__(self, path, encoding=None, filename="CMakeLists.txt", ): - """ - :param path: Top level directory of the extension project. - :type path: :class:`str` - :param encoding: Encoding of extension CMakeLists.txt. - :type encoding: :class:`str` or ``None`` - :param filename: CMake file to parse. Default is `CMakeLists.txt`. - :type filename: :class:`str` - - If ``encoding`` is ``None``, the encoding will be guessed using - :meth:`~SlicerWizard.Utilities.detectEncoding`. - """ - cmakeFile = os.path.join(path, filename) - if not os.path.exists(cmakeFile): - raise OSError("%s not found" % filename) - - self._scriptContents, self._encoding = self._parse(cmakeFile, encoding=encoding) - try: - self._scriptPath = cmakeFile - self.getValue("EXTENSION_HOMEPAGE") - except KeyError: - for cmakeFile in self._collect_cmakefiles(path, filename): self._scriptContents, self._encoding = self._parse(cmakeFile, encoding=encoding) try: - self._scriptPath = cmakeFile - self.getValue("EXTENSION_HOMEPAGE") - break + self._scriptPath = cmakeFile + self.getValue("EXTENSION_HOMEPAGE") except KeyError: - continue - - @staticmethod - def _collect_cmakefiles(path, filename="CMakeLists.txt"): - """Return list of `filename` found in `path` at depth=1""" - cmakeFiles = [] - dirnames = [] - for _, dirnames, _ in os.walk(path): - break - for dirname in dirnames: - cmakeFile = os.path.join(path, dirname, filename) - if os.path.exists(cmakeFile): - cmakeFiles.append(cmakeFile) - return cmakeFiles - - #--------------------------------------------------------------------------- - @staticmethod - def _parse(cmakeFile, encoding=None): - with open(cmakeFile, "rb") as fp: - contents = fp.read() - - if encoding is None: - encoding, confidence = detectEncoding(contents) - - if encoding is not None: - if confidence < 0.5: - logging.warning("%s: encoding detection confidence is %f:" - " project contents might be corrupt" % - (path, confidence)) - - if encoding is None: - # If unable to determine encoding, skip unicode conversion... users - # must not feed any unicode into the script or things will likely break - # later (e.g. when trying to save the project) - contents = CMakeParser.CMakeScript(contents) - - else: - # Otherwise, decode the contents into unicode - contents = contents.decode(encoding) - contents = CMakeParser.CMakeScript(contents) - - return contents, encoding - - #--------------------------------------------------------------------------- - def __enter__(self): - return self - - #--------------------------------------------------------------------------- - def __exit__(self, exc_type, exc_value, traceback): - self.save() - - #--------------------------------------------------------------------------- - @property - def encoding(self): - """Character encoding of the extension project CMakeLists.txt. - - :type: :class:`str` or ``None`` - - This provides the character encoding of the CMakeLists.txt file from which - the project instance was created. If the encoding cannot be determined, the - property will have the value ``None``. - - .. 'note' directive needs '\' to span multiple lines! - .. note:: If ``encoding`` is ``None``, the project information is stored \ - as raw bytes using :class:`str`. In such case, passing a \ - non-ASCII :class:`unicode` to any method or property \ - assignment that modifies the project may make it impossible to \ - write the project back to disk. - """ - - return self._encoding - - #--------------------------------------------------------------------------- - @property - def project(self): - """Name of extension project. - - :type: :class:`str` + for cmakeFile in self._collect_cmakefiles(path, filename): + self._scriptContents, self._encoding = self._parse(cmakeFile, encoding=encoding) + try: + self._scriptPath = cmakeFile + self.getValue("EXTENSION_HOMEPAGE") + break + except KeyError: + continue + + @staticmethod + def _collect_cmakefiles(path, filename="CMakeLists.txt"): + """Return list of `filename` found in `path` at depth=1""" + cmakeFiles = [] + dirnames = [] + for _, dirnames, _ in os.walk(path): + break + for dirname in dirnames: + cmakeFile = os.path.join(path, dirname, filename) + if os.path.exists(cmakeFile): + cmakeFiles.append(cmakeFile) + return cmakeFiles + + # --------------------------------------------------------------------------- + @staticmethod + def _parse(cmakeFile, encoding=None): + with open(cmakeFile, "rb") as fp: + contents = fp.read() + + if encoding is None: + encoding, confidence = detectEncoding(contents) + + if encoding is not None: + if confidence < 0.5: + logging.warning("%s: encoding detection confidence is %f:" + " project contents might be corrupt" % + (path, confidence)) + + if encoding is None: + # If unable to determine encoding, skip unicode conversion... users + # must not feed any unicode into the script or things will likely break + # later (e.g. when trying to save the project) + contents = CMakeParser.CMakeScript(contents) + + else: + # Otherwise, decode the contents into unicode + contents = contents.decode(encoding) + contents = CMakeParser.CMakeScript(contents) + + return contents, encoding + + # --------------------------------------------------------------------------- + def __enter__(self): + return self + + # --------------------------------------------------------------------------- + def __exit__(self, exc_type, exc_value, traceback): + self.save() + + # --------------------------------------------------------------------------- + @property + def encoding(self): + """Character encoding of the extension project CMakeLists.txt. + + :type: :class:`str` or ``None`` + + This provides the character encoding of the CMakeLists.txt file from which + the project instance was created. If the encoding cannot be determined, the + property will have the value ``None``. + + .. 'note' directive needs '\' to span multiple lines! + .. note:: If ``encoding`` is ``None``, the project information is stored \ + as raw bytes using :class:`str`. In such case, passing a \ + non-ASCII :class:`unicode` to any method or property \ + assignment that modifies the project may make it impossible to \ + write the project back to disk. + """ + + return self._encoding + + # --------------------------------------------------------------------------- + @property + def project(self): + """Name of extension project. + + :type: :class:`str` + + :raises: + :exc:`~exceptions.EOFError` if no ``project()`` command is present in the + build script. + + This provides the name of the extension project, i.e. the identifier passed + to ``project()`` in the extension's build script. + + Assigning the property modifies the build script. + """ + + for t in self._scriptContents.tokens: + if _isCommand(t, "project") and len(t.arguments): + return t.arguments[0].text + + # Support older extension that do not call "project(Name)" + # in top-level CMakeLists.txt + try: + return self.getValue("EXTENSION_NAME") + except KeyError: + pass - :raises: - :exc:`~exceptions.EOFError` if no ``project()`` command is present in the - build script. + raise EOFError("could not find project") - This provides the name of the extension project, i.e. the identifier passed - to ``project()`` in the extension's build script. + # --------------------------------------------------------------------------- + @project.setter + def project(self, value): - Assigning the property modifies the build script. - """ + for t in self._scriptContents.tokens: + if _isCommand(t, "project"): + if len(t.arguments): + t.arguments[0].text = value + else: + t.arguments.append(CMakeParser.String(text=value)) - for t in self._scriptContents.tokens: - if _isCommand(t, "project") and len(t.arguments): - return t.arguments[0].text + return - # Support older extension that do not call "project(Name)" - # in top-level CMakeLists.txt - try: - return self.getValue("EXTENSION_NAME") - except KeyError: - pass + raise EOFError("could not find project") - raise EOFError("could not find project") + # --------------------------------------------------------------------------- + def substituteVariableReferences(self, text): + """Return a copy of ``text`` where all valid '``${var}``' occurrences + have been replaced. - #--------------------------------------------------------------------------- - @project.setter - def project(self, value): + Note that variable references can nest and are evaluated from the inside + out, e.g. '``${outer_${inner_variable}_variable}``'. - for t in self._scriptContents.tokens: - if _isCommand(t, "project"): - if len(t.arguments): - t.arguments[0].text = value - else: - t.arguments.append(CMakeParser.String(text=value)) + :param text: A text with zero or more variable references. + :type text: :class:`str` + """ - return + def _substitue(text): + variableNames = self._referencedVariables.findall(text) + if len(variableNames) == 0: + return text - raise EOFError("could not find project") + prefinedVariables = {} + try: + prefinedVariables["PROJECT_NAME"] = self.project + except EOFError: + pass - #--------------------------------------------------------------------------- - def substituteVariableReferences(self, text): - """Return a copy of ``text`` where all valid '``${var}``' occurrences - have been replaced. + for name in prefinedVariables.keys(): + try: + text = text.replace("${%s}" % name, prefinedVariables[name]) + except KeyError: + continue - Note that variable references can nest and are evaluated from the inside - out, e.g. '``${outer_${inner_variable}_variable}``'. + for name in variableNames: + try: + text = text.replace("${%s}" % name, self.getValue(name)) + except KeyError: + text = text.replace("${%s}" % name, "%s-NOTFOUND" % name) - :param text: A text with zero or more variable references. - :type text: :class:`str` - """ + return text - def _substitue(text): - variableNames = self._referencedVariables.findall(text) - if len(variableNames) == 0: + while len(self._referencedVariables.findall(text)) > 0: + text = _substitue(text) return text - prefinedVariables = {} - try: - prefinedVariables["PROJECT_NAME"] = self.project - except EOFError: - pass + # --------------------------------------------------------------------------- + def getValue(self, name, default=None, substitute=False): + """Get value of CMake variable set in project. - for name in prefinedVariables.keys(): - try: - text = text.replace("${%s}" % name, prefinedVariables[name]) - except KeyError: - continue + :param name: Name of the variable. + :type name: :class:`str` + :param default: Value to return if no such variable exists. + :param substitute: If ``True``, expand variable references in value. + :type substitute: :class:`bool` - for name in variableNames: - try: - text = text.replace("${%s}" % name, self.getValue(name)) - except KeyError: - text = text.replace("${%s}" % name, "%s-NOTFOUND" % name) + :returns: Value of the variable, or ``default`` if not set. + :rtype: :class:`str` or ``type(default)`` - return text + :raises: + :exc:`~exceptions.KeyError` if no such ``name`` is set and ``default`` is + ``None``. - while len(self._referencedVariables.findall(text)) > 0: - text = _substitue(text) - return text + This returns the raw value of the variable ``name`` which is set in the + build script. By default, no substitution is performed (the result is taken + from the raw argument). If more than one ``set()`` command sets the same + ``name``, the result is the raw argument to the last such command. If the + value consists of more than one argument, they are all concatenated together + while stripping newlines, tabs and extra whitespaces. + + If ``substitute`` is ``True``, each occurrence of '``${var}``' will be + replaced with the corresponding variable if it has been set. Variable + references can nest and are evaluated from the inside out, + e.g. '``${outer_${inner_variable}_variable}``'. If a variable + reference is not found, it will be replaced with '``-NOTFOUND``'. - #--------------------------------------------------------------------------- - def getValue(self, name, default=None, substitute=False): - """Get value of CMake variable set in project. + If no ``set()`` command sets ``name``, and ``default`` is not ``None``, + ``default`` is returned. Otherwise a :exc:`~exceptions.KeyError` is raised. + + .. note:: + + Variables set using a nested reference are not supported. + For example, if the underlying CMake code is ``set(foo \"world\")`` + and ``set(hello_${foo} \"earth\")``. Occurrences of + '``${hello_${foo}}``' will be replaced by '``hello_world-NOTFOUND``' + + .. seealso:: :func:`.substituteVariableReferences` + """ + + for t in reversed(self._scriptContents.tokens): + if _isCommand(t, "set") and len(t.arguments) and \ + t.arguments[0].text == name: + if len(t.arguments) < 2: + return None + value = " ".join([argument.text for argument in t.arguments[1:] if isinstance(argument, CMakeParser.String)]) + if substitute: + value = self.substituteVariableReferences(value) + return value - :param name: Name of the variable. - :type name: :class:`str` - :param default: Value to return if no such variable exists. - :param substitute: If ``True``, expand variable references in value. - :type substitute: :class:`bool` + if default is not None: + return default - :returns: Value of the variable, or ``default`` if not set. - :rtype: :class:`str` or ``type(default)`` + raise KeyError("script does not set %r" % name) - :raises: - :exc:`~exceptions.KeyError` if no such ``name`` is set and ``default`` is - ``None``. + # --------------------------------------------------------------------------- + def setValue(self, name, value): + """Change value of CMake variable set in project. - This returns the raw value of the variable ``name`` which is set in the - build script. By default, no substitution is performed (the result is taken - from the raw argument). If more than one ``set()`` command sets the same - ``name``, the result is the raw argument to the last such command. If the - value consists of more than one argument, they are all concatenated together - while stripping newlines, tabs and extra whitespaces. + :param name: Name of the variable. + :type name: :class:`str` + :param value: Value to assign to the variable. + :type value: :class:`str` - If ``substitute`` is ``True``, each occurrence of '``${var}``' will be - replaced with the corresponding variable if it has been set. Variable - references can nest and are evaluated from the inside out, - e.g. '``${outer_${inner_variable}_variable}``'. If a variable - reference is not found, it will be replaced with '``-NOTFOUND``'. + :raises: :exc:`~exceptions.KeyError` if no such ``name`` is set. - If no ``set()`` command sets ``name``, and ``default`` is not ``None``, - ``default`` is returned. Otherwise a :exc:`~exceptions.KeyError` is raised. + This modifies the build script to set the variable ``name`` to ``value``. + If more than one ``set()`` command sets the same ``name``, only the first + is modified. If the value of the modified ``set()`` command has more than + one argument, only the first is modified. - .. note:: + The build script must already contain a ``set()`` command which sets + ``name``. If it does not, a :exc:`~exceptions.KeyError` is raised. + """ - Variables set using a nested reference are not supported. - For example, if the underlying CMake code is ``set(foo \"world\")`` - and ``set(hello_${foo} \"earth\")``. Occurrences of - '``${hello_${foo}}``' will be replaced by '``hello_world-NOTFOUND``' + for t in self._scriptContents.tokens: + if _isCommand(t, "set") and len(t.arguments) and \ + t.arguments[0].text == name: + if len(t.arguments) < 2: + t.arguments.append(CMakeParser.String(text=value, indent=" ", + prefix="\"", suffix="\"")) - .. seealso:: :func:`.substituteVariableReferences` - """ - - for t in reversed(self._scriptContents.tokens): - if _isCommand(t, "set") and len(t.arguments) and \ - t.arguments[0].text == name: - if len(t.arguments) < 2: - return None - value = " ".join([argument.text for argument in t.arguments[1:] if isinstance(argument, CMakeParser.String)]) - if substitute: - value = self.substituteVariableReferences(value) - return value + else: + varg = t.arguments[1] + varg.text = value + varg.prefix = "\"" + varg.suffix = "\"" - if default is not None: - return default + return - raise KeyError("script does not set %r" % name) + raise KeyError("script does not set %r" % name) - #--------------------------------------------------------------------------- - def setValue(self, name, value): - """Change value of CMake variable set in project. + # --------------------------------------------------------------------------- + def addModule(self, name): + """Add a module to the build rules of the project. - :param name: Name of the variable. - :type name: :class:`str` - :param value: Value to assign to the variable. - :type value: :class:`str` + :param name: Name of the module to be added. + :type name: :class:`str` + + :raises: :exc:`~exceptions.EOFError` if no insertion point can be found. - :raises: :exc:`~exceptions.KeyError` if no such ``name`` is set. + This adds an ``add_subdirectory()`` call for ``name`` to the build script. + If possible, the new call is inserted immediately before a placeholder + comment which is designated for this purpose. Otherwise, the new call is + inserted after the last existing call to ``add_subdirectory()``. + """ - This modifies the build script to set the variable ``name`` to ``value``. - If more than one ``set()`` command sets the same ``name``, only the first - is modified. If the value of the modified ``set()`` command has more than - one argument, only the first is modified. + indent = "" + after = -1 - The build script must already contain a ``set()`` command which sets - ``name``. If it does not, a :exc:`~exceptions.KeyError` is raised. - """ + for n in range(len(self._scriptContents.tokens)): + t = self._scriptContents.tokens[n] - for t in self._scriptContents.tokens: - if _isCommand(t, "set") and len(t.arguments) and \ - t.arguments[0].text == name: - if len(t.arguments) < 2: - t.arguments.append(CMakeParser.String(text=value, indent=" ", - prefix="\"", suffix="\"")) - - else: - varg = t.arguments[1] - varg.text = value - varg.prefix = "\"" - varg.suffix = "\"" + if isinstance(t, CMakeParser.Comment) and \ + t.text.startswith(self._moduleInsertPlaceholder): + indent = t.indent + after = n + t.indent = _trimIndent(t.indent) + break - return + if _isCommand(t, "add_subdirectory"): + indent = _trimIndent(t.indent) + after = n + 1 - raise KeyError("script does not set %r" % name) + if after < 0: + raise EOFError("failed to find insertion point for module") - #--------------------------------------------------------------------------- - def addModule(self, name): - """Add a module to the build rules of the project. - - :param name: Name of the module to be added. - :type name: :class:`str` - - :raises: :exc:`~exceptions.EOFError` if no insertion point can be found. - - This adds an ``add_subdirectory()`` call for ``name`` to the build script. - If possible, the new call is inserted immediately before a placeholder - comment which is designated for this purpose. Otherwise, the new call is - inserted after the last existing call to ``add_subdirectory()``. - """ + arguments = [CMakeParser.String(text=name)] + t = CMakeParser.Command(text="add_subdirectory", arguments=arguments, + indent=indent) + self._scriptContents.tokens.insert(after, t) - indent = "" - after = -1 + # --------------------------------------------------------------------------- + def save(self, destination=None, encoding=None): + """Save the project. - for n in range(len(self._scriptContents.tokens)): - t = self._scriptContents.tokens[n] + :param destination: Location to which to write the build script. + :type destination: :class:`str` or ``None`` + :param encoding: Encoding with which to write the build script. + :type destination: :class:`str` or ``None`` - if isinstance(t, CMakeParser.Comment) and \ - t.text.startswith(self._moduleInsertPlaceholder): - indent = t.indent - after = n - t.indent = _trimIndent(t.indent) - break + This saves the extension project CMake script to the specified file: - if _isCommand(t, "add_subdirectory"): - indent = _trimIndent(t.indent) - after = n + 1 + .. code-block:: python - if after < 0: - raise EOFError("failed to find insertion point for module") + # Open a project + p = ExtensionProject('.') - arguments = [CMakeParser.String(text=name)] - t = CMakeParser.Command(text="add_subdirectory", arguments=arguments, - indent=indent) - self._scriptContents.tokens.insert(after, t) + # Set a value in the project + p.setValue('EXTENSION_DESCRIPTION', 'This is an awesome extension!') - #--------------------------------------------------------------------------- - def save(self, destination=None, encoding=None): - """Save the project. + # Save the changes + p.save() - :param destination: Location to which to write the build script. - :type destination: :class:`str` or ``None`` - :param encoding: Encoding with which to write the build script. - :type destination: :class:`str` or ``None`` + If ``destination`` is ``None``, the CMakeLists.txt file from which the + project instance was created is overwritten. Similarly, if ``encoding`` is + ``None``, the file is written with the original encoding of the + CMakeLists.txt file from which the project instance was created, if such + encoding is other than ASCII; otherwise the file is written in UTF-8. + """ - This saves the extension project CMake script to the specified file: + if destination is None: + destination = self._scriptPath - .. code-block:: python + if encoding is None and self.encoding is not None: + encoding = self.encoding if self.encoding.lower() != "ascii" else "utf-8" - # Open a project - p = ExtensionProject('.') + if encoding is None: + # If no encoding is specified and we don't know the original encoding, + # perform no conversion and hope for the best (will only work if there + # are no unicode instances in the script) + with open(destination, "w") as fp: + fp.write(str(self._scriptContents)) - # Set a value in the project - p.setValue('EXTENSION_DESCRIPTION', 'This is an awesome extension!') - - # Save the changes - p.save() - - If ``destination`` is ``None``, the CMakeLists.txt file from which the - project instance was created is overwritten. Similarly, if ``encoding`` is - ``None``, the file is written with the original encoding of the - CMakeLists.txt file from which the project instance was created, if such - encoding is other than ASCII; otherwise the file is written in UTF-8. - """ - - if destination is None: - destination = self._scriptPath - - if encoding is None and self.encoding is not None: - encoding = self.encoding if self.encoding.lower() != "ascii" else "utf-8" - - if encoding is None: - # If no encoding is specified and we don't know the original encoding, - # perform no conversion and hope for the best (will only work if there - # are no unicode instances in the script) - with open(destination, "w") as fp: - fp.write(str(self._scriptContents)) - - else: - # Otherwise, write the file using full encoding conversion - with open(destination, "wb") as fp: - fp.write(str(self._scriptContents).encode(encoding)) + else: + # Otherwise, write the file using full encoding conversion + with open(destination, "wb") as fp: + fp.write(str(self._scriptContents).encode(encoding)) diff --git a/Utilities/Scripts/SlicerWizard/ExtensionWizard.py b/Utilities/Scripts/SlicerWizard/ExtensionWizard.py index 3879f291df3..e5d0507024d 100644 --- a/Utilities/Scripts/SlicerWizard/ExtensionWizard.py +++ b/Utilities/Scripts/SlicerWizard/ExtensionWizard.py @@ -8,33 +8,33 @@ from urllib.parse import urlparse -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def haveGit(): - """Return True if git is available. - - A side effect of `import git` is that it shows a popup window on - macOS, asking the user to install XCode (if git is not installed already), - therefore this method should only be called if git is actually needed. - """ - - # If Python is not built with SSL support then do not even try to import - # GithubHelper (it would throw missing attribute error for HTTPSConnection) - import http.client - if hasattr(http.client, "HTTPSConnection"): - # SSL is available - try: - global git, GithubHelper, NotSet - import git # noqa: F401 - from . import GithubHelper - from .GithubHelper import NotSet - _haveGit = True - except ImportError: - _haveGit = False - else: - logging.debug("ExtensionWizard: git support is disabled because http.client.HTTPSConnection is not available") - _haveGit = False - - return _haveGit + """Return True if git is available. + + A side effect of `import git` is that it shows a popup window on + macOS, asking the user to install XCode (if git is not installed already), + therefore this method should only be called if git is actually needed. + """ + + # If Python is not built with SSL support then do not even try to import + # GithubHelper (it would throw missing attribute error for HTTPSConnection) + import http.client + if hasattr(http.client, "HTTPSConnection"): + # SSL is available + try: + global git, GithubHelper, NotSet + import git # noqa: F401 + from . import GithubHelper + from .GithubHelper import NotSet + _haveGit = True + except ImportError: + _haveGit = False + else: + logging.debug("ExtensionWizard: git support is disabled because http.client.HTTPSConnection is not available") + _haveGit = False + + return _haveGit from . import __version__, __version_info__ @@ -46,299 +46,299 @@ def haveGit(): from .WizardHelpFormatter import WizardHelpFormatter -#============================================================================= +# ============================================================================= class ExtensionWizard: - """Implementation class for the Extension Wizard. - - This class provides the entry point and implementation of the Extension - wizard. One normally uses it by writing a small bootstrap script to load the - module, which then calls code like: - - .. code-block:: python - - wizard = ExtensionWizard() - wizard.execute() - - Interaction with `GitHub `_ uses - :func:`.GithubHelper.logIn` to authenticate. + """Implementation class for the Extension Wizard. - .. 'note' directive needs '\' to span multiple lines! - .. note:: Most methods will signal the application to exit if \ - something goes wrong. This behavior is hidden by the \ - :meth:`~ExtensionWizard.execute` method when passing \ - ``exit=False``; callers that need to continue execution after \ - calling one of the other methods directly should catch \ - :exc:`~exceptions.SystemExit`. - """ + This class provides the entry point and implementation of the Extension + wizard. One normally uses it by writing a small bootstrap script to load the + module, which then calls code like: - _reModuleInsertPlaceholder = re.compile("(?<=\n)([ \t]*)## NEXT_MODULE") - _reAddSubdirectory = \ - re.compile("(?<=\n)([ \t]*)add_subdirectory[(][^)]+[)][^\n]*\n") + .. code-block:: python - #--------------------------------------------------------------------------- - def __init__(self): - self._templateManager = TemplateManager() + wizard = ExtensionWizard() + wizard.execute() - #--------------------------------------------------------------------------- - def create(self, args, name, kind="default"): - """Create a new extension from specified extension template. + Interaction with `GitHub `_ uses + :func:`.GithubHelper.logIn` to authenticate. - :param args.destination: Directory wherein the new extension is created. - :type args.destination: :class:`str` - :param name: Name for the new extension. - :type name: :class:`str` - :param kind: Identifier of the template from which to create the extension. - :type kind: :class:`str` - - Note that the extension is written to a *new subdirectory* which is created - in ``args.destination``. The ``name`` is used both as the name of this - directory, and as the replacement value when substituting the template key. - - If an error occurs, the application displays an error message and exits. - - .. seealso:: :meth:`.TemplateManager.copyTemplate` + .. 'note' directive needs '\' to span multiple lines! + .. note:: Most methods will signal the application to exit if \ + something goes wrong. This behavior is hidden by the \ + :meth:`~ExtensionWizard.execute` method when passing \ + ``exit=False``; callers that need to continue execution after \ + calling one of the other methods directly should catch \ + :exc:`~exceptions.SystemExit`. """ - try: - dest = args.destination - args.destination = self._templateManager.copyTemplate(dest, "extensions", - kind, name) - logging.info("created extension '%s'" % name) + _reModuleInsertPlaceholder = re.compile("(?<=\n)([ \t]*)## NEXT_MODULE") + _reAddSubdirectory = \ + re.compile("(?<=\n)([ \t]*)add_subdirectory[(][^)]+[)][^\n]*\n") - except: - die("failed to create extension: %s" % sys.exc_info()[1]) + # --------------------------------------------------------------------------- + def __init__(self): + self._templateManager = TemplateManager() - #--------------------------------------------------------------------------- - def addModule(self, args, kind, name): - """Add a module to an existing extension. + # --------------------------------------------------------------------------- + def create(self, args, name, kind="default"): + """Create a new extension from specified extension template. - :param args.destination: Location (directory) of the extension to modify. - :type args.destination: :class:`str` - :param kind: Identifier of the template from which to create the module. - :type kind: :class:`str` - :param name: Name for the new module. - :type name: :class:`str` + :param args.destination: Directory wherein the new extension is created. + :type args.destination: :class:`str` + :param name: Name for the new extension. + :type name: :class:`str` + :param kind: Identifier of the template from which to create the extension. + :type kind: :class:`str` - This creates a new module from the specified module template and adds it to - the CMakeLists.txt of the extension. The ``name`` is used both as the name - of the new module subdirectory (created in ``args.destination``) and as the - replacement value when substituting the template key. + Note that the extension is written to a *new subdirectory* which is created + in ``args.destination``. The ``name`` is used both as the name of this + directory, and as the replacement value when substituting the template key. - If an error occurs, the extension is not modified, and the application - displays an error message and then exits. + If an error occurs, the application displays an error message and exits. - .. seealso:: :meth:`.ExtensionProject.addModule`, - :meth:`.TemplateManager.copyTemplate` - """ + .. seealso:: :meth:`.TemplateManager.copyTemplate` + """ - try: - dest = args.destination - p = ExtensionProject(dest) - p.addModule(name) - self._templateManager.copyTemplate(dest, "modules", kind, name) - p.save() - logging.info("created module '%s'" % name) + try: + dest = args.destination + args.destination = self._templateManager.copyTemplate(dest, "extensions", + kind, name) + logging.info("created extension '%s'" % name) - except: - die("failed to add module: %s" % sys.exc_info()[1]) + except: + die("failed to create extension: %s" % sys.exc_info()[1]) - #--------------------------------------------------------------------------- - def describe(self, args): - """Generate extension description and write it to :attr:`sys.stdout`. + # --------------------------------------------------------------------------- + def addModule(self, args, kind, name): + """Add a module to an existing extension. - :param args.destination: Location (directory) of the extension to describe. - :type args.destination: :class:`str` + :param args.destination: Location (directory) of the extension to modify. + :type args.destination: :class:`str` + :param kind: Identifier of the template from which to create the module. + :type kind: :class:`str` + :param name: Name for the new module. + :type name: :class:`str` - If something goes wrong, the application displays a suitable error message. - """ + This creates a new module from the specified module template and adds it to + the CMakeLists.txt of the extension. The ``name`` is used both as the name + of the new module subdirectory (created in ``args.destination``) and as the + replacement value when substituting the template key. - try: - r = None + If an error occurs, the extension is not modified, and the application + displays an error message and then exits. - if args.localExtensionsDir: - r = SourceTreeDirectory(args.localExtensionsDir, os.path.relpath(args.destination, args.localExtensionsDir)) + .. seealso:: :meth:`.ExtensionProject.addModule`, + :meth:`.TemplateManager.copyTemplate` + """ - else: - r = getRepo(args.destination) + try: + dest = args.destination + p = ExtensionProject(dest) + p.addModule(name) + self._templateManager.copyTemplate(dest, "modules", kind, name) + p.save() + logging.info("created module '%s'" % name) - if r is None: - xd = ExtensionDescription(sourcedir=args.destination) + except: + die("failed to add module: %s" % sys.exc_info()[1]) - else: - xd = ExtensionDescription(repo=r) + # --------------------------------------------------------------------------- + def describe(self, args): + """Generate extension description and write it to :attr:`sys.stdout`. - xd.write(sys.stdout) + :param args.destination: Location (directory) of the extension to describe. + :type args.destination: :class:`str` - except: - die("failed to describe extension: %s" % sys.exc_info()[1]) + If something goes wrong, the application displays a suitable error message. + """ - #--------------------------------------------------------------------------- - def _setExtensionUrl(self, project, name, value): - name = "EXTENSION_%s" % name + try: + r = None - oldValue = project.getValue(name) + if args.localExtensionsDir: + r = SourceTreeDirectory(args.localExtensionsDir, os.path.relpath(args.destination, args.localExtensionsDir)) - try: - url = urlparse(oldValue) - confirm = not url.hostname.endswith("example.com") + else: + r = getRepo(args.destination) - except: - confirm = True + if r is None: + xd = ExtensionDescription(sourcedir=args.destination) - if confirm: - logging.info("Your extension currently uses '%s' for %s," - " which can be changed to '%s' to point to your new" - " public repository." % (oldValue, name, value)) - if not inquire("Change it"): - return + else: + xd = ExtensionDescription(repo=r) - project.setValue(name, value) + xd.write(sys.stdout) - #--------------------------------------------------------------------------- - def publish(self, args): - """Publish extension to github repository. + except: + die("failed to describe extension: %s" % sys.exc_info()[1]) - :param args.destination: Location (directory) of the extension to publish. - :type args.destination: :class:`str` - :param args.cmakefile: - Name of the CMake file where `EXTENSION_*` CMake variables - are set. Default is `CMakeLists.txt`. - :type args.cmakefile: - :class:`str` - :param args.name: - Name of extension. Default is value associated with `project()` - statement. - :type args.name: - :class:`str` + # --------------------------------------------------------------------------- + def _setExtensionUrl(self, project, name, value): + name = "EXTENSION_%s" % name - This creates a public github repository for an extension (whose name is the - extension name), adds it as a remote of the extension's local repository, - and pushes the extension to the new github repository. The extension - information (homepage, icon url) is also updated to refer to the new public - repository. + oldValue = project.getValue(name) - If the extension is not already tracked in a local git repository, a new - local git repository is also created and populated by the files currently - in the extension source directory. + try: + url = urlparse(oldValue) + confirm = not url.hostname.endswith("example.com") - If the local repository is dirty or already has a remote, or a github - repository with the name of the extension already exists, the application - displays a suitable error message and then exits. - """ + except: + confirm = True + + if confirm: + logging.info("Your extension currently uses '%s' for %s," + " which can be changed to '%s' to point to your new" + " public repository." % (oldValue, name, value)) + if not inquire("Change it"): + return + + project.setValue(name, value) + + # --------------------------------------------------------------------------- + def publish(self, args): + """Publish extension to github repository. + + :param args.destination: Location (directory) of the extension to publish. + :type args.destination: :class:`str` + :param args.cmakefile: + Name of the CMake file where `EXTENSION_*` CMake variables + are set. Default is `CMakeLists.txt`. + :type args.cmakefile: + :class:`str` + :param args.name: + Name of extension. Default is value associated with `project()` + statement. + :type args.name: + :class:`str` + + This creates a public github repository for an extension (whose name is the + extension name), adds it as a remote of the extension's local repository, + and pushes the extension to the new github repository. The extension + information (homepage, icon url) is also updated to refer to the new public + repository. + + If the extension is not already tracked in a local git repository, a new + local git repository is also created and populated by the files currently + in the extension source directory. + + If the local repository is dirty or already has a remote, or a github + repository with the name of the extension already exists, the application + displays a suitable error message and then exits. + """ + + createdRepo = False + r = getRepo(args.destination, tool="git") + + if r is None: + # Create new git repository + import git + r = git.Repo.init(args.destination) + createdRepo = True + + # Prepare the initial commit + branch = "master" + r.git.checkout(b=branch) + r.git.add(":/") + + logging.info("Creating initial commit containing the following files:") + for e in r.index.entries: + logging.info(" %s" % e[0]) + logging.info("") + if not inquire("Continue"): + prog = os.path.basename(sys.argv[0]) + die("canceling at user request:" + " update your index and run %s again" % prog) + + else: + # Check if repository is dirty + if r.is_dirty(): + die("declined: working tree is dirty;" + " commit or stash your changes first") + + # Check if a remote already exists + if len(r.remotes): + die("declined: publishing is only supported for repositories" + " with no pre-existing remotes") + + branch = r.active_branch + if branch.name != "master": + logging.warning("You are currently on the '%s' branch. " + "It is strongly recommended to publish" + " the 'master' branch." % branch) + if not inquire("Continue anyway"): + die("canceled at user request") + + logging.debug("preparing to publish %s branch", branch) + + try: + # Get extension name + p = ExtensionProject(args.destination, filename=args.cmakefile) + if args.name is None: + name = p.project + else: + name = args.name + logging.debug("extension name: '%s'", name) + + # Create github remote + logging.info("creating github repository") + gh = GithubHelper.logIn(r) + ghu = gh.get_user() + for ghr in ghu.get_repos(): + if ghr.name == name: + die("declined: a github repository named '%s' already exists" % name) + + description = p.getValue("EXTENSION_DESCRIPTION", default=NotSet) + ghr = ghu.create_repo(name, description=description) + logging.debug("created github repository: %s", ghr.url) + + # Set extension meta-information + logging.info("updating extension meta-information") + raw_url = "{}/{}".format(ghr.html_url.replace("//", "//raw."), branch) + self._setExtensionUrl(p, "HOMEPAGE", ghr.html_url) + self._setExtensionUrl(p, "ICONURL", f"{raw_url}/{name}.png") + p.save() + + # Commit the initial commit or updated meta-information + r.git.add(":/CMakeLists.txt") + if createdRepo: + logging.info("preparing initial commit") + r.index.commit("ENH: Initial commit for %s" % name) + else: + logging.info("committing changes") + r.index.commit("ENH: Update extension information\n\n" + "Set %s information to reference" + " new github repository." % name) + + # Set up the remote and push + logging.info("preparing to push extension repository") + remote = r.create_remote("origin", ghr.clone_url) + remote.push(branch) + logging.info("extension published to %s", ghr.url) + + except SystemExit: + raise + except: + die("failed to publish extension: %s" % sys.exc_info()[1]) - createdRepo = False - r = getRepo(args.destination, tool="git") - - if r is None: - # Create new git repository - import git - r = git.Repo.init(args.destination) - createdRepo = True - - # Prepare the initial commit - branch = "master" - r.git.checkout(b=branch) - r.git.add(":/") - - logging.info("Creating initial commit containing the following files:") - for e in r.index.entries: - logging.info(" %s" % e[0]) - logging.info("") - if not inquire("Continue"): - prog = os.path.basename(sys.argv[0]) - die("canceling at user request:" - " update your index and run %s again" % prog) + # --------------------------------------------------------------------------- + def _extensionIndexCommitMessage(self, name, description, update, wrap=True): + args = description.__dict__ + args["name"] = name - else: - # Check if repository is dirty - if r.is_dirty(): - die("declined: working tree is dirty;" - " commit or stash your changes first") - - # Check if a remote already exists - if len(r.remotes): - die("declined: publishing is only supported for repositories" - " with no pre-existing remotes") - - branch = r.active_branch - if branch.name != "master": - logging.warning("You are currently on the '%s' branch. " - "It is strongly recommended to publish" - " the 'master' branch." % branch) - if not inquire("Continue anyway"): - die("canceled at user request") - - logging.debug("preparing to publish %s branch", branch) - - try: - # Get extension name - p = ExtensionProject(args.destination, filename=args.cmakefile) - if args.name is None: - name = p.project - else: - name = args.name - logging.debug("extension name: '%s'", name) - - # Create github remote - logging.info("creating github repository") - gh = GithubHelper.logIn(r) - ghu = gh.get_user() - for ghr in ghu.get_repos(): - if ghr.name == name: - die("declined: a github repository named '%s' already exists" % name) - - description = p.getValue("EXTENSION_DESCRIPTION", default=NotSet) - ghr = ghu.create_repo(name, description=description) - logging.debug("created github repository: %s", ghr.url) - - # Set extension meta-information - logging.info("updating extension meta-information") - raw_url = "{}/{}".format(ghr.html_url.replace("//", "//raw."), branch) - self._setExtensionUrl(p, "HOMEPAGE", ghr.html_url) - self._setExtensionUrl(p, "ICONURL", f"{raw_url}/{name}.png") - p.save() - - # Commit the initial commit or updated meta-information - r.git.add(":/CMakeLists.txt") - if createdRepo: - logging.info("preparing initial commit") - r.index.commit("ENH: Initial commit for %s" % name) - else: - logging.info("committing changes") - r.index.commit("ENH: Update extension information\n\n" - "Set %s information to reference" - " new github repository." % name) - - # Set up the remote and push - logging.info("preparing to push extension repository") - remote = r.create_remote("origin", ghr.clone_url) - remote.push(branch) - logging.info("extension published to %s", ghr.url) - - except SystemExit: - raise - except: - die("failed to publish extension: %s" % sys.exc_info()[1]) - - #--------------------------------------------------------------------------- - def _extensionIndexCommitMessage(self, name, description, update, wrap=True): - args = description.__dict__ - args["name"] = name - - if update: - template = textwrap.dedent("""\ + if update: + template = textwrap.dedent("""\ ENH: Update %(name)s extension This updates the %(name)s extension to %(scmrevision)s. """) - if wrap: - paragraphs = (template % args).split("\n") - return "\n".join([textwrap.fill(p, width=76) for p in paragraphs]) - else: - return template % args - - else: - template = textwrap.dedent("""\ + if wrap: + paragraphs = (template % args).split("\n") + return "\n".join([textwrap.fill(p, width=76) for p in paragraphs]) + else: + return template % args + + else: + template = textwrap.dedent("""\ ENH: Add %(name)s extension Description: @@ -348,441 +348,441 @@ def _extensionIndexCommitMessage(self, name, description, update, wrap=True): %(contributors)s """) - if wrap: - for key in args: - args[key] = textwrap.fill(args[key], width=72) - - return template % args - - #--------------------------------------------------------------------------- - def contribute(self, args): - """Add or update an extension to/in the index repository. - - :param args.destination: - Location (directory) of the extension to contribute. - :type args.destination: - :class:`str` - :param args.cmakefile: - Name of the CMake file where `EXTENSION_*` CMake variables - are set. Default is `CMakeLists.txt`. - :type args.cmakefile: - :class:`str` - :param args.name: - Name of extension. Default is value associated with `project()` - statement. - :type args.name: - :class:`str` - :param args.target: - Name of branch which the extension targets (must match a branch name in - the extension index repository). - :type args.target: - :class:`str` - :param args.index: - Path to an existing clone of the extension index, or path to which the - index should be cloned. If ``None``, a subdirectory in the extension's - ``.git`` directory is used. - :type args.index: - :class:`str` or ``None`` - :param args.test: - If ``True``, include a note in the pull request that the request is a - test and should not be merged. - :type args.test: - :class:`bool` - - This writes the description of the specified extension --- which may be an - addition, or an update to a previously contributed extension --- to a user - fork of the `extension index repository`_, pushes the changes, and creates - a pull request to merge the contribution. In case of an update to an - extension with a github public repository, a "compare URL" (a github link - to view the changes between the previously contributed version of the - extension and the version being newly contributed) is included in the pull - request message. - - This attempts to find the user's already existing fork of the index - repository, and to create one if it does not already exist. The fork is - then either cloned (adding remotes for both upstream and the user's fork) - or updated, and the current upstream target branch pushed to the user's - fork, ensuring that the target branch in the user's fork is up to date. The - changes to the index repository are made in a separate branch. - - If a pull request for the extension already exists, its message is updated - and the corresponding branch is force-pushed (which automatically updates - the code portion of the request). - - If anything goes wrong, no pull request is created, and the application - displays a suitable error message and then exits. Additionally, a branch - push for the index changes only occurs if the failed operation is the - creation or update of the pull request; other errors cause the application - to exit before pushing the branch. (Updates of the user's fork to current - upstream may still occur.) - - .. _extension index repository: https://github.com/Slicer/ExtensionsIndex - """ + if wrap: + for key in args: + args[key] = textwrap.fill(args[key], width=72) + + return template % args + + # --------------------------------------------------------------------------- + def contribute(self, args): + """Add or update an extension to/in the index repository. + + :param args.destination: + Location (directory) of the extension to contribute. + :type args.destination: + :class:`str` + :param args.cmakefile: + Name of the CMake file where `EXTENSION_*` CMake variables + are set. Default is `CMakeLists.txt`. + :type args.cmakefile: + :class:`str` + :param args.name: + Name of extension. Default is value associated with `project()` + statement. + :type args.name: + :class:`str` + :param args.target: + Name of branch which the extension targets (must match a branch name in + the extension index repository). + :type args.target: + :class:`str` + :param args.index: + Path to an existing clone of the extension index, or path to which the + index should be cloned. If ``None``, a subdirectory in the extension's + ``.git`` directory is used. + :type args.index: + :class:`str` or ``None`` + :param args.test: + If ``True``, include a note in the pull request that the request is a + test and should not be merged. + :type args.test: + :class:`bool` + + This writes the description of the specified extension --- which may be an + addition, or an update to a previously contributed extension --- to a user + fork of the `extension index repository`_, pushes the changes, and creates + a pull request to merge the contribution. In case of an update to an + extension with a github public repository, a "compare URL" (a github link + to view the changes between the previously contributed version of the + extension and the version being newly contributed) is included in the pull + request message. + + This attempts to find the user's already existing fork of the index + repository, and to create one if it does not already exist. The fork is + then either cloned (adding remotes for both upstream and the user's fork) + or updated, and the current upstream target branch pushed to the user's + fork, ensuring that the target branch in the user's fork is up to date. The + changes to the index repository are made in a separate branch. + + If a pull request for the extension already exists, its message is updated + and the corresponding branch is force-pushed (which automatically updates + the code portion of the request). + + If anything goes wrong, no pull request is created, and the application + displays a suitable error message and then exits. Additionally, a branch + push for the index changes only occurs if the failed operation is the + creation or update of the pull request; other errors cause the application + to exit before pushing the branch. (Updates of the user's fork to current + upstream may still occur.) + + .. _extension index repository: https://github.com/Slicer/ExtensionsIndex + """ - try: - r = getRepo(args.destination) - if r is None: - die("extension repository not found") - - xd = ExtensionDescription(repo=r, cmakefile=args.cmakefile) - if args.name is None: - name = ExtensionProject(localRoot(r), filename=args.cmakefile).project - else: - name = args.name - logging.debug("extension name: '%s'", name) - - # Validate that extension has a SCM URL - if xd.scmurl == "NA": - raise Exception("extension 'scmurl' is not set") - - # Get (or create) the user's fork of the extension index - logging.info("obtaining github repository information") - gh = GithubHelper.logIn(r if xd.scm == "git" else None) - upstreamRepo = GithubHelper.getRepo(gh, name="Slicer/ExtensionsIndex") - if upstreamRepo is None: - die("error accessing extension index upstream repository") - - logging.debug("index upstream: %s", upstreamRepo.url) - - forkedRepo = GithubHelper.getFork(user=gh.get_user(), create=True, - upstream=upstreamRepo) - - logging.debug("index fork: %s", forkedRepo.url) - - # Get or create extension index repository - if args.index is not None: - xip = args.index - else: - xip = os.path.join(vcsPrivateDirectory(r), "extension-index") - - xiRepo = getRepo(xip) - - if xiRepo is None: - logging.info("cloning index repository") - xiRepo = getRepo(xip, create=createEmptyRepo) - xiRemote = getRemote(xiRepo, [forkedRepo.clone_url], create="origin") - - else: - # Check that the index repository is a clone of the github fork - xiRemote = [forkedRepo.clone_url, forkedRepo.git_url] - xiRemote = getRemote(xiRepo, xiRemote) - if xiRemote is None: - raise Exception("the extension index repository ('%s')" - " is not a clone of %s" % - (xiRepo.working_tree_dir, forkedRepo.clone_url)) - - logging.debug("index fork remote: %s", xiRemote.url) - - # Find or create the upstream remote for the index repository - xiUpstream = [upstreamRepo.clone_url, upstreamRepo.git_url] - xiUpstream = getRemote(xiRepo, xiUpstream, create="upstream") - logging.debug("index upstream remote: %s", xiUpstream.url) - - # Check that the index repository is clean - if xiRepo.is_dirty(): - raise Exception("the extension index repository ('%s') is dirty" % - xiRepo.working_tree_dir) - - # Update the index repository and get the base branch - logging.info("updating local index clone") - xiRepo.git.fetch(xiUpstream) - if not args.target in xiUpstream.refs: - die("target branch '%s' does not exist" % args.target) - - xiBase = xiUpstream.refs[args.target] - - # Ensure that user's fork is up to date - logging.info("updating target branch (%s) branch on fork", args.target) - xiRemote.push(f"{xiBase}:refs/heads/{args.target}") - - # Determine if this is an addition or update to the index - xdf = name + ".s4ext" - if xdf in xiBase.commit.tree: - branch = f'update-{name}-{args.target}' - update = True - else: - branch = f'add-{name}-{args.target}' - update = False - - logging.debug("create index branch %s", branch) - xiRepo.git.checkout(xiBase, B=branch) - - # Check to see if there is an existing pull request - pullRequest = GithubHelper.getPullRequest(upstreamRepo, fork=forkedRepo, - ref=branch) - logging.debug("existing pull request: %s", - pullRequest if pullRequest is None else pullRequest.url) - - if update: - # Get old SCM revision try: - odPath = os.path.join(xiRepo.working_tree_dir, xdf) - od = ExtensionDescription(filepath=odPath) - if od.scmrevision != "NA": - oldRef = od.scmrevision - + r = getRepo(args.destination) + if r is None: + die("extension repository not found") + + xd = ExtensionDescription(repo=r, cmakefile=args.cmakefile) + if args.name is None: + name = ExtensionProject(localRoot(r), filename=args.cmakefile).project + else: + name = args.name + logging.debug("extension name: '%s'", name) + + # Validate that extension has a SCM URL + if xd.scmurl == "NA": + raise Exception("extension 'scmurl' is not set") + + # Get (or create) the user's fork of the extension index + logging.info("obtaining github repository information") + gh = GithubHelper.logIn(r if xd.scm == "git" else None) + upstreamRepo = GithubHelper.getRepo(gh, name="Slicer/ExtensionsIndex") + if upstreamRepo is None: + die("error accessing extension index upstream repository") + + logging.debug("index upstream: %s", upstreamRepo.url) + + forkedRepo = GithubHelper.getFork(user=gh.get_user(), create=True, + upstream=upstreamRepo) + + logging.debug("index fork: %s", forkedRepo.url) + + # Get or create extension index repository + if args.index is not None: + xip = args.index + else: + xip = os.path.join(vcsPrivateDirectory(r), "extension-index") + + xiRepo = getRepo(xip) + + if xiRepo is None: + logging.info("cloning index repository") + xiRepo = getRepo(xip, create=createEmptyRepo) + xiRemote = getRemote(xiRepo, [forkedRepo.clone_url], create="origin") + + else: + # Check that the index repository is a clone of the github fork + xiRemote = [forkedRepo.clone_url, forkedRepo.git_url] + xiRemote = getRemote(xiRepo, xiRemote) + if xiRemote is None: + raise Exception("the extension index repository ('%s')" + " is not a clone of %s" % + (xiRepo.working_tree_dir, forkedRepo.clone_url)) + + logging.debug("index fork remote: %s", xiRemote.url) + + # Find or create the upstream remote for the index repository + xiUpstream = [upstreamRepo.clone_url, upstreamRepo.git_url] + xiUpstream = getRemote(xiRepo, xiUpstream, create="upstream") + logging.debug("index upstream remote: %s", xiUpstream.url) + + # Check that the index repository is clean + if xiRepo.is_dirty(): + raise Exception("the extension index repository ('%s') is dirty" % + xiRepo.working_tree_dir) + + # Update the index repository and get the base branch + logging.info("updating local index clone") + xiRepo.git.fetch(xiUpstream) + if args.target not in xiUpstream.refs: + die("target branch '%s' does not exist" % args.target) + + xiBase = xiUpstream.refs[args.target] + + # Ensure that user's fork is up to date + logging.info("updating target branch (%s) branch on fork", args.target) + xiRemote.push(f"{xiBase}:refs/heads/{args.target}") + + # Determine if this is an addition or update to the index + xdf = name + ".s4ext" + if xdf in xiBase.commit.tree: + branch = f'update-{name}-{args.target}' + update = True + else: + branch = f'add-{name}-{args.target}' + update = False + + logging.debug("create index branch %s", branch) + xiRepo.git.checkout(xiBase, B=branch) + + # Check to see if there is an existing pull request + pullRequest = GithubHelper.getPullRequest(upstreamRepo, fork=forkedRepo, + ref=branch) + logging.debug("existing pull request: %s", + pullRequest if pullRequest is None else pullRequest.url) + + if update: + # Get old SCM revision + try: + odPath = os.path.join(xiRepo.working_tree_dir, xdf) + od = ExtensionDescription(filepath=odPath) + if od.scmrevision != "NA": + oldRef = od.scmrevision + + except: + oldRef = None + + # Write the extension description and prepare to commit + xd.write(os.path.join(xiRepo.working_tree_dir, xdf)) + xiRepo.index.add([xdf]) + + # Commit and push the new/updated extension description + xiRepo.index.commit(self._extensionIndexCommitMessage( + name, xd, update=update)) + + try: + # We need the old branch, if it exists, to be fetched locally, so that + # push info resolution doesn't choke trying to resolve the old SHA + xiRemote.fetch(branch) + except: + pass + + xiRemote.push("+%s" % branch) + + # Get message formatted for pull request + msg = self._extensionIndexCommitMessage(name, xd, update=update, + wrap=False).split("\n") + if len(msg) > 2 and not len(msg[1].strip()): + del msg[1] + + # Update PR title to indicate the target name + msg[0] += " [%s]" % args.target + + # Try to add compare URL to pull request message, if applicable + if update and oldRef is not None: + extensionRepo = GithubHelper.getRepo(gh, url=xd.scmurl) + + if extensionRepo is not None: + logging.info("building compare URL for update") + logging.debug(" repository: %s", extensionRepo.url) + logging.debug(" old SHA: %s", oldRef) + logging.debug(" new SHA: %s", xd.scmrevision) + + try: + c = extensionRepo.compare(oldRef, xd.scmrevision) + + msg.append("") + msg.append("See %s to view changes to the extension." % c.html_url) + + except: + warn("failed to build compare URL: %s" % sys.exc_info()[1]) + + if args.test: + msg.append("") + msg.append("THIS PULL REQUEST WAS MACHINE GENERATED" + " FOR TESTING PURPOSES. DO NOT MERGE.") + + if args.dryRun: + if pullRequest is not None: + logging.info("updating pull request %s", pullRequest.html_url) + + logging.info("prepared pull request message:\n%s", "\n".join(msg)) + + return + + # Create or update the pull request + if pullRequest is None: + logging.info("creating pull request") + + pull = f"{forkedRepo.owner.login}:{branch}" + pullRequest = upstreamRepo.create_pull( + title=msg[0], body="\n".join(msg[1:]), + head=pull, base=args.target) + + logging.info("created pull request %s", pullRequest.html_url) + + else: + logging.info("updating pull request %s", pullRequest.html_url) + pullRequest.edit(title=msg[0], body="\n".join(msg[1:]), state="open") + logging.info("updated pull request %s", pullRequest.html_url) + + except SystemExit: + raise except: - oldRef = None - - # Write the extension description and prepare to commit - xd.write(os.path.join(xiRepo.working_tree_dir, xdf)) - xiRepo.index.add([xdf]) - - # Commit and push the new/updated extension description - xiRepo.index.commit(self._extensionIndexCommitMessage( - name, xd, update=update)) - - try: - # We need the old branch, if it exists, to be fetched locally, so that - # push info resolution doesn't choke trying to resolve the old SHA - xiRemote.fetch(branch) - except: - pass - - xiRemote.push("+%s" % branch) - - # Get message formatted for pull request - msg = self._extensionIndexCommitMessage(name, xd, update=update, - wrap=False).split("\n") - if len(msg) > 2 and not len(msg[1].strip()): - del msg[1] - - # Update PR title to indicate the target name - msg[0] += " [%s]" % args.target - - # Try to add compare URL to pull request message, if applicable - if update and oldRef is not None: - extensionRepo = GithubHelper.getRepo(gh, url=xd.scmurl) - - if extensionRepo is not None: - logging.info("building compare URL for update") - logging.debug(" repository: %s", extensionRepo.url) - logging.debug(" old SHA: %s", oldRef) - logging.debug(" new SHA: %s", xd.scmrevision) - - try: - c = extensionRepo.compare(oldRef, xd.scmrevision) - - msg.append("") - msg.append("See %s to view changes to the extension." % c.html_url) - - except: - warn("failed to build compare URL: %s" % sys.exc_info()[1]) - - if args.test: - msg.append("") - msg.append("THIS PULL REQUEST WAS MACHINE GENERATED" - " FOR TESTING PURPOSES. DO NOT MERGE.") - - if args.dryRun: - if pullRequest is not None: - logging.info("updating pull request %s", pullRequest.html_url) - - logging.info("prepared pull request message:\n%s", "\n".join(msg)) - - return - - # Create or update the pull request - if pullRequest is None: - logging.info("creating pull request") - - pull = f"{forkedRepo.owner.login}:{branch}" - pullRequest = upstreamRepo.create_pull( - title=msg[0], body="\n".join(msg[1:]), - head=pull, base=args.target) - - logging.info("created pull request %s", pullRequest.html_url) - - else: - logging.info("updating pull request %s", pullRequest.html_url) - pullRequest.edit(title=msg[0], body="\n".join(msg[1:]), state="open") - logging.info("updated pull request %s", pullRequest.html_url) - - except SystemExit: - raise - except: - die("failed to register extension: %s" % sys.exc_info()[1]) - - #--------------------------------------------------------------------------- - def _execute(self, args): - # Set up arguments - parser = argparse.ArgumentParser(description="Slicer Wizard", - formatter_class=WizardHelpFormatter) - - parser.add_argument('--version', action='version', - version=__version__) - - parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS) - parser.add_argument("--test", action="store_true", help=argparse.SUPPRESS) - parser.add_argument("--dryRun", action="store_true", help=argparse.SUPPRESS) - parser.add_argument("--localExtensionsDir", help=argparse.SUPPRESS) - - parser.add_argument("--create", metavar="NAME", - help="create TYPE extension NAME" - " under the destination directory;" - " any modules are added to the new extension" - " (default type: 'default')") - parser.add_argument("--addModule", metavar="TYPE:NAME", action="append", - help="add new TYPE module NAME to an existing project" - " in the destination directory;" - " may use more than once") - self._templateManager.addArguments(parser) - parser.add_argument("--listTemplates", action="store_true", - help="show list of available templates" - " and associated substitution keys") - parser.add_argument("--describe", action="store_true", - help="print the extension description (s4ext)" - " to standard output") - - parser.add_argument("--name", metavar="NAME", - help="name of the extension" - " (default: value associated with 'project()' statement)") - - parser.add_argument("--publish", action="store_true", - help="publish the extension in the destination" - " directory to github (account required)") - parser.add_argument("--contribute", action="store_true", - help="register or update a compiled extension with" - " the extension index (github account required)") - parser.add_argument("--target", metavar="VERSION", default="master", - help="version of Slicer for which the extension" - " is intended (default='master')") - parser.add_argument("--index", metavar="PATH", - help="location for the extension index clone" - " (default: private directory" - " in the extension clone)") - - parser.add_argument("destination", default=os.getcwd(), nargs="?", - help="location of output files / extension source" - " (default: '.')") - - parser.add_argument("cmakefile", default="CMakeLists.txt", nargs="?", - help="name of the CMake file where EXTENSION_* CMake variables are set" - " (default: 'CMakeLists.txt')") - - args = parser.parse_args(args) - initLogging(logging.getLogger(), args) - - # The following arguments are only available if haveGit() is True - if not haveGit() and (args.publish or args.contribute or args.name): - option = "--publish" - if args.contribute: - option = "--contribute" - elif args.name: - option = "--name" - die(textwrap.dedent( - """\ + die("failed to register extension: %s" % sys.exc_info()[1]) + + # --------------------------------------------------------------------------- + def _execute(self, args): + # Set up arguments + parser = argparse.ArgumentParser(description="Slicer Wizard", + formatter_class=WizardHelpFormatter) + + parser.add_argument('--version', action='version', + version=__version__) + + parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS) + parser.add_argument("--test", action="store_true", help=argparse.SUPPRESS) + parser.add_argument("--dryRun", action="store_true", help=argparse.SUPPRESS) + parser.add_argument("--localExtensionsDir", help=argparse.SUPPRESS) + + parser.add_argument("--create", metavar="NAME", + help="create TYPE extension NAME" + " under the destination directory;" + " any modules are added to the new extension" + " (default type: 'default')") + parser.add_argument("--addModule", metavar="TYPE:NAME", action="append", + help="add new TYPE module NAME to an existing project" + " in the destination directory;" + " may use more than once") + self._templateManager.addArguments(parser) + parser.add_argument("--listTemplates", action="store_true", + help="show list of available templates" + " and associated substitution keys") + parser.add_argument("--describe", action="store_true", + help="print the extension description (s4ext)" + " to standard output") + + parser.add_argument("--name", metavar="NAME", + help="name of the extension" + " (default: value associated with 'project()' statement)") + + parser.add_argument("--publish", action="store_true", + help="publish the extension in the destination" + " directory to github (account required)") + parser.add_argument("--contribute", action="store_true", + help="register or update a compiled extension with" + " the extension index (github account required)") + parser.add_argument("--target", metavar="VERSION", default="master", + help="version of Slicer for which the extension" + " is intended (default='master')") + parser.add_argument("--index", metavar="PATH", + help="location for the extension index clone" + " (default: private directory" + " in the extension clone)") + + parser.add_argument("destination", default=os.getcwd(), nargs="?", + help="location of output files / extension source" + " (default: '.')") + + parser.add_argument("cmakefile", default="CMakeLists.txt", nargs="?", + help="name of the CMake file where EXTENSION_* CMake variables are set" + " (default: 'CMakeLists.txt')") + + args = parser.parse_args(args) + initLogging(logging.getLogger(), args) + + # The following arguments are only available if haveGit() is True + if not haveGit() and (args.publish or args.contribute or args.name): + option = "--publish" + if args.contribute: + option = "--contribute" + elif args.name: + option = "--name" + die(textwrap.dedent( + """\ Option '%s' is not available. Consider re-building Slicer with SSL support or downloading Slicer from https://download.slicer.org """ % option)) - # Add built-in templates - scriptPath = os.path.dirname(os.path.realpath(__file__)) + # Add built-in templates + scriptPath = os.path.dirname(os.path.realpath(__file__)) - candidateBuiltInTemplatePaths = [ - os.path.join(scriptPath, "..", "..", "..", "Utilities", "Templates"), # Run from source directory - os.path.join(scriptPath, "..", "..", "..", "share", # Run from install - "Slicer-%s.%s" % tuple(__version_info__[:2]), - "Wizard", "Templates") + candidateBuiltInTemplatePaths = [ + os.path.join(scriptPath, "..", "..", "..", "Utilities", "Templates"), # Run from source directory + os.path.join(scriptPath, "..", "..", "..", "share", # Run from install + "Slicer-%s.%s" % tuple(__version_info__[:2]), + "Wizard", "Templates") ] - descriptionFileTemplate = None - for candidate in candidateBuiltInTemplatePaths: - if os.path.exists(candidate): - self._templateManager.addPath(candidate) - descriptionFileTemplate = os.path.join(candidate, "Extensions", "extension_description.s4ext.in") - if descriptionFileTemplate is None or not os.path.exists(descriptionFileTemplate): - logging.warning("failed to locate template 'Extensions/extension_description.s4ext.in' " - "in these directories: %s" % candidateBuiltInTemplatePaths) - else: - ExtensionDescription.DESCRIPTION_FILE_TEMPLATE = descriptionFileTemplate - - # Add user-specified template paths and keys - self._templateManager.parseArguments(args) - - acted = False - - # List available templates - if args.listTemplates: - self._templateManager.listTemplates() - acted = True - - # Create requested extensions - if args.create is not None: - extArgs = args.create.split(":") - extArgs.reverse() - self.create(args, *extArgs) - acted = True - - # Create requested modules - if args.addModule is not None: - for module in args.addModule: - self.addModule(args, *module.split(":")) - acted = True - - # Describe extension if requested - if args.describe: - self.describe(args) - acted = True - - # Publish extension if requested - if args.publish: - self.publish(args) - acted = True - - # Contribute extension if requested - if args.contribute: - self.contribute(args) - acted = True - - # Check that we did something - if not acted: - die(("no action was requested!", "", parser.format_usage().rstrip())) - - #--------------------------------------------------------------------------- - def execute(self, *args, **kwargs): - """execute(*args, exit=True, **kwargs) - Execute the wizard in |CLI| mode. - - :param exit: - * ``True``: The call does not return and the application exits. - * ``False``: The call returns an exit code, which is ``0`` if execution - was successful, or non-zero otherwise. - :type exit: - :class:`bool` - :param args: - |CLI| arguments to use for execution. - :type args: - :class:`~collections.Sequence` - :param kwargs: - Named |CLI| options to use for execution. - :type kwargs: - :class:`dict` - - This sets up |CLI| argument parsing and executes the wizard, using the - provided |CLI| arguments if any, or :attr:`sys.argv` otherwise. See - :func:`.buildProcessArgs` for an explanation of how ``args`` and ``kwargs`` - are processed. - - If multiple commands are given, an error in one may cause others to be - skipped. - - .. seealso:: :func:`.buildProcessArgs` - """ - - # Get values for non-CLI-argument named arguments - exit = kwargs.pop('exit', True) - - # Convert other named arguments to CLI arguments - args = buildProcessArgs(*args, **kwargs) + descriptionFileTemplate = None + for candidate in candidateBuiltInTemplatePaths: + if os.path.exists(candidate): + self._templateManager.addPath(candidate) + descriptionFileTemplate = os.path.join(candidate, "Extensions", "extension_description.s4ext.in") + if descriptionFileTemplate is None or not os.path.exists(descriptionFileTemplate): + logging.warning("failed to locate template 'Extensions/extension_description.s4ext.in' " + "in these directories: %s" % candidateBuiltInTemplatePaths) + else: + ExtensionDescription.DESCRIPTION_FILE_TEMPLATE = descriptionFileTemplate + + # Add user-specified template paths and keys + self._templateManager.parseArguments(args) + + acted = False + + # List available templates + if args.listTemplates: + self._templateManager.listTemplates() + acted = True + + # Create requested extensions + if args.create is not None: + extArgs = args.create.split(":") + extArgs.reverse() + self.create(args, *extArgs) + acted = True + + # Create requested modules + if args.addModule is not None: + for module in args.addModule: + self.addModule(args, *module.split(":")) + acted = True + + # Describe extension if requested + if args.describe: + self.describe(args) + acted = True + + # Publish extension if requested + if args.publish: + self.publish(args) + acted = True + + # Contribute extension if requested + if args.contribute: + self.contribute(args) + acted = True + + # Check that we did something + if not acted: + die(("no action was requested!", "", parser.format_usage().rstrip())) + + # --------------------------------------------------------------------------- + def execute(self, *args, **kwargs): + """execute(*args, exit=True, **kwargs) + Execute the wizard in |CLI| mode. + + :param exit: + * ``True``: The call does not return and the application exits. + * ``False``: The call returns an exit code, which is ``0`` if execution + was successful, or non-zero otherwise. + :type exit: + :class:`bool` + :param args: + |CLI| arguments to use for execution. + :type args: + :class:`~collections.Sequence` + :param kwargs: + Named |CLI| options to use for execution. + :type kwargs: + :class:`dict` + + This sets up |CLI| argument parsing and executes the wizard, using the + provided |CLI| arguments if any, or :attr:`sys.argv` otherwise. See + :func:`.buildProcessArgs` for an explanation of how ``args`` and ``kwargs`` + are processed. + + If multiple commands are given, an error in one may cause others to be + skipped. + + .. seealso:: :func:`.buildProcessArgs` + """ + + # Get values for non-CLI-argument named arguments + exit = kwargs.pop('exit', True) + + # Convert other named arguments to CLI arguments + args = buildProcessArgs(*args, **kwargs) - try: - self._execute(args if len(args) else None) - sys.exit(0) + try: + self._execute(args if len(args) else None) + sys.exit(0) - except SystemExit: - if not exit: - return sys.exc_info()[1].code + except SystemExit: + if not exit: + return sys.exc_info()[1].code - raise + raise diff --git a/Utilities/Scripts/SlicerWizard/GithubHelper.py b/Utilities/Scripts/SlicerWizard/GithubHelper.py index ed5ffba18f8..562eaa5dfac 100644 --- a/Utilities/Scripts/SlicerWizard/GithubHelper.py +++ b/Utilities/Scripts/SlicerWizard/GithubHelper.py @@ -10,305 +10,305 @@ from urllib.parse import urlparse __all__ = [ - 'logIn', - 'getRepo', - 'getFork', - 'getPullRequest', + 'logIn', + 'getRepo', + 'getFork', + 'getPullRequest', ] -#============================================================================= +# ============================================================================= class _CredentialToken: - #--------------------------------------------------------------------------- - def __init__(self, text=None, **kwargs): - # Set attributes from named arguments - self._keys = set(kwargs.keys()) - for k in kwargs: - setattr(self, k, kwargs[k]) - - # Set attributes from input text (i.e. 'git credential fill' output) - if text is not None: - for l in text.split("\n"): - if "=" in l: - t = l.split("=", 1) - self._keys.add(t[0]) - setattr(self, t[0], t[1]) - - #--------------------------------------------------------------------------- - def __str__(self): - # Return string representation suitable for being fed to 'git credential' - lines = [f"{k}={getattr(self, k)}" for k in self._keys] - return "%s\n\n" % "\n".join(lines) - - -#----------------------------------------------------------------------------- + # --------------------------------------------------------------------------- + def __init__(self, text=None, **kwargs): + # Set attributes from named arguments + self._keys = set(kwargs.keys()) + for k in kwargs: + setattr(self, k, kwargs[k]) + + # Set attributes from input text (i.e. 'git credential fill' output) + if text is not None: + for line in text.split("\n"): + if "=" in line: + t = line.split("=", 1) + self._keys.add(t[0]) + setattr(self, t[0], t[1]) + + # --------------------------------------------------------------------------- + def __str__(self): + # Return string representation suitable for being fed to 'git credential' + lines = [f"{k}={getattr(self, k)}" for k in self._keys] + return "%s\n\n" % "\n".join(lines) + + +# ----------------------------------------------------------------------------- def _credentials(client, request, action="fill"): - # Set up and execute 'git credential' process, passing stringized token to - # the process's stdin - p = client.credential(action, as_process=True, istream=subprocess.PIPE) - out, err = p.communicate(input=str(request).encode("utf-8")) + # Set up and execute 'git credential' process, passing stringized token to + # the process's stdin + p = client.credential(action, as_process=True, istream=subprocess.PIPE) + out, err = p.communicate(input=str(request).encode("utf-8")) - # Raise exception if process failed - if p.returncode != 0: - raise git.GitCommandError(["credential", action], p.returncode, - err.rstrip()) + # Raise exception if process failed + if p.returncode != 0: + raise git.GitCommandError(["credential", action], p.returncode, + err.rstrip()) - # Return token parsed from the command's output - return _CredentialToken(out.decode()) + # Return token parsed from the command's output + return _CredentialToken(out.decode()) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def logIn(repo=None): - """Create github session. + """Create github session. - :param repo: - If not ``None``, use the git client (i.e. configuration) from the specified - git repository; otherwise use a default client. - :type repo: - :class:`git.Repo ` or ``None``. + :param repo: + If not ``None``, use the git client (i.e. configuration) from the specified + git repository; otherwise use a default client. + :type repo: + :class:`git.Repo ` or ``None``. - :returns: A logged in github session. - :rtype: :class:`github.Github `. + :returns: A logged in github session. + :rtype: :class:`github.Github `. - :raises: - :class:`github:github.GithubException.BadCredentialsException` if - authentication fails. + :raises: + :class:`github:github.GithubException.BadCredentialsException` if + authentication fails. - This obtains and returns a logged in github session using the user's - credentials, as managed by `git-credentials`_; login information is obtained - as necessary via the same. On success, the credentials are also saved to any - store that the user has configured. + This obtains and returns a logged in github session using the user's + credentials, as managed by `git-credentials`_; login information is obtained + as necessary via the same. On success, the credentials are also saved to any + store that the user has configured. - If `GITHUB_TOKEN` environment variable is set, its value will be used - as password when invoking `git-credentials`_. + If `GITHUB_TOKEN` environment variable is set, its value will be used + as password when invoking `git-credentials`_. - .. _git-credentials: https://git-scm.com/docs/gitcredentials.html - """ + .. _git-credentials: https://git-scm.com/docs/gitcredentials.html + """ - # Get client; use generic client if no repository - client = repo.git if repo is not None else git.cmd.Git() + # Get client; use generic client if no repository + client = repo.git if repo is not None else git.cmd.Git() - # Request login credentials - github_token = {} - if "GITHUB_TOKEN" in os.environ: - github_token = {"password": os.environ["GITHUB_TOKEN"]} - credRequest = _CredentialToken(protocol="https", host="github.com", **github_token) - cred = _credentials(client, credRequest) + # Request login credentials + github_token = {} + if "GITHUB_TOKEN" in os.environ: + github_token = {"password": os.environ["GITHUB_TOKEN"]} + credRequest = _CredentialToken(protocol="https", host="github.com", **github_token) + cred = _credentials(client, credRequest) - # Log in - session = Github(cred.username, cred.password) + # Log in + session = Github(cred.username, cred.password) - # Try to get the logged in user name; will raise an exception if - # authentication failed - if session.get_user().login: - # Save the credentials - _credentials(client, cred, action="approve") + # Try to get the logged in user name; will raise an exception if + # authentication failed + if session.get_user().login: + # Save the credentials + _credentials(client, cred, action="approve") - # Return github session - return session + # Return github session + return session -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def getRepo(session, name=None, url=None): - """Get a github repository by name or URL. - - :param session: - A github session object, e.g. as returned from :func:`.logIn`. - :type session: - :class:`github.Github ` or - :class:`github:github.AuthenticatedUser.AuthenticatedUser` - :param name: - Name of the repository to look up. - :type name: - :class:`str` or ``None`` - :param url: - Clone URL of the repository. - :type url: - :class:`str` or ``None`` - - :returns: Matching repository, or ``None`` if no such repository was found. - :rtype: :class:`github:github.Repository.Repository` or ``None``. - - This function attempts to look up a github repository by either its qualified - github name (i.e. '**/**') or a clone URL: - - .. code-block:: python - - # Create session - session = GithubHelper.logIn() - - # Look up repository by name - repoA = GithubHelper.getRepo(session, 'octocat/Hello-World') - - # Look up repository by clone URL - cloneUrl = 'https://github.com/octocat/Hello-World.git' - repoB = GithubHelper.getRepo(session, cloneUrl) - - If both ``name`` and ``url`` are provided, only ``name`` is used. The ``url`` - must have "github.com" as the host. - """ - - try: - # Look up repository by name - if name is not None: - return session.get_repo(name) - - # Look up repository by clone URL - if url is not None: - # Parse URL - url = urlparse(url) - - # Check that this is a github URL - if not url.hostname.endswith("github.com"): - return None - - # Get repository name from clone URL - name = url.path - if name.startswith("/"): - name = name[1:] - if name.endswith(".git"): - name = name[:-4] + """Get a github repository by name or URL. + + :param session: + A github session object, e.g. as returned from :func:`.logIn`. + :type session: + :class:`github.Github ` or + :class:`github:github.AuthenticatedUser.AuthenticatedUser` + :param name: + Name of the repository to look up. + :type name: + :class:`str` or ``None`` + :param url: + Clone URL of the repository. + :type url: + :class:`str` or ``None`` + + :returns: Matching repository, or ``None`` if no such repository was found. + :rtype: :class:`github:github.Repository.Repository` or ``None``. + + This function attempts to look up a github repository by either its qualified + github name (i.e. '**/**') or a clone URL: + + .. code-block:: python + + # Create session + session = GithubHelper.logIn() # Look up repository by name - return getRepo(session, name=name) + repoA = GithubHelper.getRepo(session, 'octocat/Hello-World') + + # Look up repository by clone URL + cloneUrl = 'https://github.com/octocat/Hello-World.git' + repoB = GithubHelper.getRepo(session, cloneUrl) + + If both ``name`` and ``url`` are provided, only ``name`` is used. The ``url`` + must have "github.com" as the host. + """ + + try: + # Look up repository by name + if name is not None: + return session.get_repo(name) + + # Look up repository by clone URL + if url is not None: + # Parse URL + url = urlparse(url) + + # Check that this is a github URL + if not url.hostname.endswith("github.com"): + return None + + # Get repository name from clone URL + name = url.path + if name.startswith("/"): + name = name[1:] + if name.endswith(".git"): + name = name[:-4] + + # Look up repository by name + return getRepo(session, name=name) - except: - pass + except: + pass - return None + return None -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def getFork(user, upstream, create=False): - """Get user's fork of the specified repository. + """Get user's fork of the specified repository. - :param user: - Github user or organization which owns the requested fork. - :type user: - :class:`github:github.NamedUser.NamedUser`, - :class:`github:github.AuthenticatedUser.AuthenticatedUser` or - :class:`github:github.Organization.Organization` - :param upstream: - Upstream repository of the requested fork. - :type upstream: - :class:`github:github.Repository.Repository` - :param create: - If ``True``, create the forked repository if no such fork exists. - :type create: - :class:`bool` + :param user: + Github user or organization which owns the requested fork. + :type user: + :class:`github:github.NamedUser.NamedUser`, + :class:`github:github.AuthenticatedUser.AuthenticatedUser` or + :class:`github:github.Organization.Organization` + :param upstream: + Upstream repository of the requested fork. + :type upstream: + :class:`github:github.Repository.Repository` + :param create: + If ``True``, create the forked repository if no such fork exists. + :type create: + :class:`bool` - :return: - The specified fork repository, or ``None`` if no such fork exists and - ``create`` is ``False``. - :rtype: - :class:`github:github.Repository.Repository` or ``None``. + :return: + The specified fork repository, or ``None`` if no such fork exists and + ``create`` is ``False``. + :rtype: + :class:`github:github.Repository.Repository` or ``None``. - :raises: - :class:`github:github.GithubException.GithubException` if ``user`` does not - have permission to create a repository. + :raises: + :class:`github:github.GithubException.GithubException` if ``user`` does not + have permission to create a repository. - This function attempts to look up a repository owned by the specified user or - organization which is a fork of the specified upstream repository, optionally - creating one if it does not exist: + This function attempts to look up a repository owned by the specified user or + organization which is a fork of the specified upstream repository, optionally + creating one if it does not exist: - .. code-block:: python + .. code-block:: python - # Create session - session = GithubHelper.logIn() + # Create session + session = GithubHelper.logIn() - # Get user - user = session.get_user("jdoe") + # Get user + user = session.get_user("jdoe") - # Get upstream repository - upstream = GithubHelper.getRepo(session, 'octocat/Spoon-Knife') + # Get upstream repository + upstream = GithubHelper.getRepo(session, 'octocat/Spoon-Knife') - # Look up fork - fork = GithubHelper.getFork(user=user, upstream=upstream) - """ + # Look up fork + fork = GithubHelper.getFork(user=user, upstream=upstream) + """ - repo = user.get_repo(upstream.name) - if repo.fork and repo.parent.url == upstream.url: - return repo + repo = user.get_repo(upstream.name) + if repo.fork and repo.parent.url == upstream.url: + return repo - if create: - return user.create_fork(upstream) + if create: + return user.create_fork(upstream) - return None + return None -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def getPullRequest(upstream, ref, user=None, fork=None, target=None): - """Get pull request for the specified user's fork and ref. - - :param upstream: - Upstream (target) repository of the requested pull request. - :type upstream: - :class:`github:github.Repository.Repository` - :param user: - Github user or organization which owns the requested pull request. - :type user: - :class:`github:github.NamedUser.NamedUser`, - :class:`github:github.AuthenticatedUser.AuthenticatedUser`, - :class:`github:github.Organization.Organization` or ``None`` - :param ref: - Branch name or git ref of the requested pull request. - :type ref: - :class:`str` - :param fork: - Downstream (fork) repository of the requested pull request. - :type fork: - :class:`github:github.Repository.Repository` or ``None`` - :param target: - Branch name or git ref of the requested pull request target. - :type target: - :class:`str` or ``None`` - - :return: - The specified pull request, or ``None`` if no such pull request exists. - :rtype: - :class:`github:github.PullRequest.PullRequest` or ``None``. - - This function attempts to look up the pull request made by ``user`` for - ``upstream`` to integrate the user's ``ref`` into upstream's ``target``: - - .. code-block:: python - - # Create session - session = GithubHelper.logIn() - - # Get user and upstream repository - user = session.get_user("jdoe") - repo = GithubHelper.getRepo(session, 'octocat/Hello-World') - - # Look up request to merge 'my-branch' of any fork into 'master' - pr = GithubHelper.getPullRequest(upstream=repo, user=user, - ref='my-branch', target='master') - - If any of ``user``, ``fork`` or ``target`` are ``None``, those criteria are - not considered when searching for a matching pull request. If multiple - matching requests exist, the first matching request is returned. - """ - - if user is not None: - user = user.login - - for p in upstream.get_pulls(): - # Check candidate request against specified criteria - if p.head.ref != ref: - continue - - if user is not None and p.head.user.login != user: - continue - - if fork is not None and p.head.repo.url != fork.url: - continue - - if target is not None and p.base.ref != target: - continue - - # If we get here, we found a match - return p - - # No match - return None + """Get pull request for the specified user's fork and ref. + + :param upstream: + Upstream (target) repository of the requested pull request. + :type upstream: + :class:`github:github.Repository.Repository` + :param user: + Github user or organization which owns the requested pull request. + :type user: + :class:`github:github.NamedUser.NamedUser`, + :class:`github:github.AuthenticatedUser.AuthenticatedUser`, + :class:`github:github.Organization.Organization` or ``None`` + :param ref: + Branch name or git ref of the requested pull request. + :type ref: + :class:`str` + :param fork: + Downstream (fork) repository of the requested pull request. + :type fork: + :class:`github:github.Repository.Repository` or ``None`` + :param target: + Branch name or git ref of the requested pull request target. + :type target: + :class:`str` or ``None`` + + :return: + The specified pull request, or ``None`` if no such pull request exists. + :rtype: + :class:`github:github.PullRequest.PullRequest` or ``None``. + + This function attempts to look up the pull request made by ``user`` for + ``upstream`` to integrate the user's ``ref`` into upstream's ``target``: + + .. code-block:: python + + # Create session + session = GithubHelper.logIn() + + # Get user and upstream repository + user = session.get_user("jdoe") + repo = GithubHelper.getRepo(session, 'octocat/Hello-World') + + # Look up request to merge 'my-branch' of any fork into 'master' + pr = GithubHelper.getPullRequest(upstream=repo, user=user, + ref='my-branch', target='master') + + If any of ``user``, ``fork`` or ``target`` are ``None``, those criteria are + not considered when searching for a matching pull request. If multiple + matching requests exist, the first matching request is returned. + """ + + if user is not None: + user = user.login + + for p in upstream.get_pulls(): + # Check candidate request against specified criteria + if p.head.ref != ref: + continue + + if user is not None and p.head.user.login != user: + continue + + if fork is not None and p.head.repo.url != fork.url: + continue + + if target is not None and p.base.ref != target: + continue + + # If we get here, we found a match + return p + + # No match + return None diff --git a/Utilities/Scripts/SlicerWizard/Subversion.py b/Utilities/Scripts/SlicerWizard/Subversion.py index f70bffc5315..cdc3da1f1fe 100644 --- a/Utilities/Scripts/SlicerWizard/Subversion.py +++ b/Utilities/Scripts/SlicerWizard/Subversion.py @@ -6,203 +6,203 @@ from .Utilities import * __all__ = [ - 'Client', - 'Repository', + 'Client', + 'Repository', ] -#============================================================================= +# ============================================================================= class CommandError(Exception): - """ - .. attribute:: command + """ + .. attribute:: command - Complete command (including arguments) which experienced the error. + Complete command (including arguments) which experienced the error. - .. attribute:: code + .. attribute:: code - Command's status code. + Command's status code. - .. attribute:: stderr + .. attribute:: stderr - Raw text of the command's standard error stream. - """ + Raw text of the command's standard error stream. + """ - #--------------------------------------------------------------------------- - def __init__(self, command, code, stderr): - super(Exception, self).__init__("%r command exited with non-zero status" % - command[0]) - self.command = command - self.code = code - self.stderr = stderr + # --------------------------------------------------------------------------- + def __init__(self, command, code, stderr): + super(Exception, self).__init__("%r command exited with non-zero status" % + command[0]) + self.command = command + self.code = code + self.stderr = stderr -#============================================================================= +# ============================================================================= class Client: - """Wrapper for executing the ``svn`` process. + """Wrapper for executing the ``svn`` process. - This class provides a convenience wrapping for invoking the ``svn`` process. - In addition to the :meth:`~Client.execute` method, names of subversion - commands are implicitly available as methods: + This class provides a convenience wrapping for invoking the ``svn`` process. + In addition to the :meth:`~Client.execute` method, names of subversion + commands are implicitly available as methods: - .. code-block:: python + .. code-block:: python - c = Subversion.Client() - c.log('.', limit=5) - """ + c = Subversion.Client() + c.log('.', limit=5) + """ - #--------------------------------------------------------------------------- - def __init__(self, repo=None): - self._wc_root = repo.wc_root if repo is not None else None + # --------------------------------------------------------------------------- + def __init__(self, repo=None): + self._wc_root = repo.wc_root if repo is not None else None - #--------------------------------------------------------------------------- - def __getattr__(self, name): - """Return a lambda to invoke the svn command ``name``.""" + # --------------------------------------------------------------------------- + def __getattr__(self, name): + """Return a lambda to invoke the svn command ``name``.""" - if name[0] == "_": - raise AttributeError("%r object has no attribute %r" % - (self.__class__.__name__, name)) + if name[0] == "_": + raise AttributeError("%r object has no attribute %r" % + (self.__class__.__name__, name)) - return lambda *args, **kwargs: self.execute(name, *args, **kwargs) + return lambda *args, **kwargs: self.execute(name, *args, **kwargs) - #--------------------------------------------------------------------------- - def execute(self, command, *args, **kwargs): - """Execute ``command`` and return line-split output. + # --------------------------------------------------------------------------- + def execute(self, command, *args, **kwargs): + """Execute ``command`` and return line-split output. - :param args: Subversion command to execute. - :type args: :class:`str` - :param args: Arguments to pass to ``command``. - :type args: :class:`~collections.Sequence` - :param kwargs: Named options to pass to ``command``. - :type kwargs: :class:`dict` + :param args: Subversion command to execute. + :type args: :class:`str` + :param args: Arguments to pass to ``command``. + :type args: :class:`~collections.Sequence` + :param kwargs: Named options to pass to ``command``. + :type kwargs: :class:`dict` - :return: - Standard output from running the command, as a list (split by line). - :rtype: - :class:`list` of :class:`str` + :return: + Standard output from running the command, as a list (split by line). + :rtype: + :class:`list` of :class:`str` - :raises: :class:`.CommandError` if the command exits with non-zero status. + :raises: :class:`.CommandError` if the command exits with non-zero status. - This executes the specified ``svn`` command and returns the standard output - from the execution. See :func:`.buildProcessArgs` for an explanation of how - ``args`` and ``kwargs`` are processed. + This executes the specified ``svn`` command and returns the standard output + from the execution. See :func:`.buildProcessArgs` for an explanation of how + ``args`` and ``kwargs`` are processed. - .. seealso:: :func:`.buildProcessArgs` - """ + .. seealso:: :func:`.buildProcessArgs` + """ - command = ["svn", command] + buildProcessArgs(*args, **kwargs) - cwd = self._wc_root if self._wc_root is not None else os.getcwd() + command = ["svn", command] + buildProcessArgs(*args, **kwargs) + cwd = self._wc_root if self._wc_root is not None else os.getcwd() - proc = subprocess.Popen(command, cwd=cwd, stdin=subprocess.PIPE, - stderr=subprocess.PIPE, stdout=subprocess.PIPE) + proc = subprocess.Popen(command, cwd=cwd, stdin=subprocess.PIPE, + stderr=subprocess.PIPE, stdout=subprocess.PIPE) - out, err = proc.communicate() + out, err = proc.communicate() - # Raise exception if process failed - if proc.returncode != 0: - raise CommandError(command, proc.returncode, err) + # Raise exception if process failed + if proc.returncode != 0: + raise CommandError(command, proc.returncode, err) - # Strip trailing newline(s) - while out.endswith("\n"): - out = out[:-1] + # Strip trailing newline(s) + while out.endswith("\n"): + out = out[:-1] - return out.split("\n") + return out.split("\n") - #--------------------------------------------------------------------------- - def info(self, *args, **kwargs): - """Return information about the specified item. + # --------------------------------------------------------------------------- + def info(self, *args, **kwargs): + """Return information about the specified item. - :type args: :class:`str` - :param args: Arguments to pass to ``svn info``. - :type args: :class:`~collections.Sequence` - :param kwargs: Named options to pass to ``svn info``. - :type kwargs: :class:`dict` + :type args: :class:`str` + :param args: Arguments to pass to ``svn info``. + :type args: :class:`~collections.Sequence` + :param kwargs: Named options to pass to ``svn info``. + :type kwargs: :class:`dict` - :return: Mapping of information fields returned by ``svn info``. - :rtype: :class:`dict` of :class:`str` |rarr| :class:`str` + :return: Mapping of information fields returned by ``svn info``. + :rtype: :class:`dict` of :class:`str` |rarr| :class:`str` - :raises: :class:`.CommandError` if the command exits with non-zero status. + :raises: :class:`.CommandError` if the command exits with non-zero status. - This wraps the ``svn info`` command, returning the resulting information as - a :class:`dict`. The dictionary keys are the value names as printed by - ``svn info``. + This wraps the ``svn info`` command, returning the resulting information as + a :class:`dict`. The dictionary keys are the value names as printed by + ``svn info``. - .. |rarr| unicode:: U+02192 .. right arrow - """ + .. |rarr| unicode:: U+02192 .. right arrow + """ - out = self.execute("info", *args, **kwargs) + out = self.execute("info", *args, **kwargs) - result = {} - for line in out: - parts = line.split(": ", 1) - result[parts[0]] = parts[1] + result = {} + for line in out: + parts = line.split(": ", 1) + result[parts[0]] = parts[1] - return result + return result -#============================================================================= +# ============================================================================= class Repository: - """Abstract representation of a subversion repository. + """Abstract representation of a subversion repository. - .. attribute:: url + .. attribute:: url - The remote URL of the base of the working copy checkout. + The remote URL of the base of the working copy checkout. - .. attribute:: root_url + .. attribute:: root_url - The root URL of the remote repository. + The root URL of the remote repository. - .. attribute:: uuid + .. attribute:: uuid - The universally unique identifier of the repository. + The universally unique identifier of the repository. - .. attribute:: wc_root + .. attribute:: wc_root - The absolute path to the top level directory of the repository working copy. + The absolute path to the top level directory of the repository working copy. - .. attribute:: revision + .. attribute:: revision - The revision at which the working copy is checked out. + The revision at which the working copy is checked out. - .. attribute:: last_change_revision + .. attribute:: last_change_revision - The last revision which contains a change to content contained in the - working copy. + The last revision which contains a change to content contained in the + working copy. - .. attribute:: svn_dir + .. attribute:: svn_dir - The absolute path to the working copy ``.svn`` directory. + The absolute path to the working copy ``.svn`` directory. - .. attribute:: client + .. attribute:: client - A :class:`.Client` object which may be used to interact with the repository. - The client interprets non-absolute paths as relative to the working copy - root. - """ - - #--------------------------------------------------------------------------- - def __init__(self, path=os.getcwd()): - """ - :param path: Location of the repository checkout. - :type path: :class:`str` - - :raises: - * :exc:`.CommandError` if the request to get the repository information - fails (e.g. if ``path`` is not a repository). - * :exc:`~exceptions.KeyError` if the repository information is missing a - required value. + A :class:`.Client` object which may be used to interact with the repository. + The client interprets non-absolute paths as relative to the working copy + root. """ - c = Client() - info = c.info(path) - info = c.info(info["Working Copy Root Path"]) - - self.url = info["URL"] - self.root_url = info["Repository Root"] - self.uuid = info["Repository UUID"] - self.wc_root = info["Working Copy Root Path"] - self.revision = info["Revision"] - self.last_change_revision = info["Last Changed Rev"] - - self.svn_dir = os.path.join(self.wc_root, ".svn") - - self.client = Client(self) + # --------------------------------------------------------------------------- + def __init__(self, path=os.getcwd()): + """ + :param path: Location of the repository checkout. + :type path: :class:`str` + + :raises: + * :exc:`.CommandError` if the request to get the repository information + fails (e.g. if ``path`` is not a repository). + * :exc:`~exceptions.KeyError` if the repository information is missing a + required value. + """ + + c = Client() + info = c.info(path) + info = c.info(info["Working Copy Root Path"]) + + self.url = info["URL"] + self.root_url = info["Repository Root"] + self.uuid = info["Repository UUID"] + self.wc_root = info["Working Copy Root Path"] + self.revision = info["Revision"] + self.last_change_revision = info["Last Changed Rev"] + + self.svn_dir = os.path.join(self.wc_root, ".svn") + + self.client = Client(self) diff --git a/Utilities/Scripts/SlicerWizard/TemplateManager.py b/Utilities/Scripts/SlicerWizard/TemplateManager.py index 37e4958e189..6d5cd5a160e 100644 --- a/Utilities/Scripts/SlicerWizard/TemplateManager.py +++ b/Utilities/Scripts/SlicerWizard/TemplateManager.py @@ -5,398 +5,398 @@ from .Utilities import die, detectEncoding _sourcePatterns = [ - "*.h", - "*.cxx", - "*.cpp", - "CMakeLists.txt", - "*.cmake", - "*.ui", - "*.qrc", - "*.py", - "*.xml", - "*.xml.in", - "*.md5", - "*.png", - "*.dox", - "*.sha256", + "*.h", + "*.cxx", + "*.cpp", + "CMakeLists.txt", + "*.cmake", + "*.ui", + "*.qrc", + "*.py", + "*.xml", + "*.xml.in", + "*.md5", + "*.png", + "*.dox", + "*.sha256", ] _templateCategories = [ - "extensions", - "modules", + "extensions", + "modules", ] -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _isSourceFile(name): - for pat in _sourcePatterns: - if fnmatch.fnmatch(name, pat): - return True + for pat in _sourcePatterns: + if fnmatch.fnmatch(name, pat): + return True - return False + return False -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _isTemplateCategory(name, relPath): - if not os.path.isdir(os.path.join(relPath, name)): - return False + if not os.path.isdir(os.path.join(relPath, name)): + return False - name = name.lower() - return name in _templateCategories + name = name.lower() + return name in _templateCategories -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _listSources(directory): - for root, subFolders, files in os.walk(directory): - for f in files: - if _isSourceFile(f): - f = os.path.join(root, f) - yield f[len(directory) + 1:] # strip common dir + for root, subFolders, files in os.walk(directory): + for f in files: + if _isSourceFile(f): + f = os.path.join(root, f) + yield f[len(directory) + 1:] # strip common dir -#============================================================================= +# ============================================================================= class TemplateManager: - """Template collection manager. - - This class provides a template collection and operations for managing and - using that collection. - """ - - #--------------------------------------------------------------------------- - def __init__(self): - self._paths = {} - self._keys = {} - - for c in _templateCategories: - self._paths[c] = {} - - #--------------------------------------------------------------------------- - def _getKey(self, kind): - if kind in self._keys: - return self._keys[kind] - - return "TemplateKey" - - #--------------------------------------------------------------------------- - def _copyAndReplace(self, inFile, template, destination, key, name): - outFile = os.path.join(destination, inFile.replace(key, name)) - logging.info("creating '%s'" % outFile) - path = os.path.dirname(outFile) - if not os.path.exists(path): - os.makedirs(path) - - # Read file contents - p = os.path.join(template, inFile) - with open(p, "rb") as fp: - contents = fp.read() - - # Replace template key with copy name - if isinstance(name, bytes): - # If replacement is just bytes, we can just do the replacement... - contents = contents.replace(key, name) - contents = contents.replace(key.upper(), name.upper()) - - else: - # ...else we have to try to guess the template file encoding in order to - # convert it to unicode and back - encoding, confidence = detectEncoding(contents) - - if encoding is not None: - if confidence < 0.5: - logging.warning("%s: encoding detection confidence is %f:" - " copied file might be corrupt" % (p, confidence)) - - contents = contents.decode(encoding) - contents = contents.replace(key, name) - contents = contents.replace(key.upper(), name.upper()) - contents = contents.encode(encoding) - - else: - # Looks like a binary file; don't perform replacement - pass - - # Write adjusted contents - with open(outFile, "wb") as fp: - fp.write(contents) - - #--------------------------------------------------------------------------- - def copyTemplate(self, destination, category, kind, name, createInSubdirectory=True, requireEmptyDirectory=True): - """Copy (instantiate) a template. - - :param destination: Directory in which to create the template copy. - :type destination: :class:`str` - :param category: Category of template to instantiate. - :type category: :class:`str` - :param kind: Name of template to instantiate. - :type kind: :class:`str` - :param name: Name for the instantiated template. - :type name: :class:`str` - :param createInSubdirectory: If True then files are copied to ``destination/name/``, else ``destination/``. - :type name: :class:`bool` - :param requireEmptyDirectory: If True then files are only copied if the target directory is empty. - :type name: :class:`bool` - - :return: - Path to the new instance (``os.path.join(destination, name)``). - :rtype: - :class:`unicode` if either ``destination`` and/or ``name`` is also - :class:`unicode`, otherwise :class:`str`. - - :raises: - * :exc:`~exceptions.KeyError` if the specified template is not found. - * :exc:`~exceptions.IOError` if a subdirectory ``name`` already exists. - - This creates a copy of the specified template in ``destination``, with - occurrences of the template's key replaced with ``name``. The new copy is - in a subdirectory ``name``, which must not exist. - - .. note:: The replacement of the template key is case sensitive, however \ - the upper-case key is also replaced with the upper-case ``name``. - - .. seealso:: :meth:`.setKey` - """ - - templates = self._paths[category] - if not kind.lower() in templates: - raise KeyError("'%s' is not a known extension template" % kind) - - kind = kind.lower() - - if createInSubdirectory: - destination = os.path.join(destination, name) - - if requireEmptyDirectory and os.path.exists(destination): - raise OSError("create %s: refusing to overwrite" - " existing directory '%s'" % (category, destination)) - - template = templates[kind] - key = self._getKey(kind) - - logging.info("copy template '%s' to '%s', replacing '%s' -> '%s'" % - (template, destination, key, name)) - for f in _listSources(template): - self._copyAndReplace(f, template, destination, key, name) - - return destination - - #--------------------------------------------------------------------------- - def addCategoryPath(self, category, path): - """Add templates for a particular category to the collection. - - :param category: Category of templates to add. - :type category: :class:`str` - :param path: Path to a directory containing templates. - :type path: :class:`str` - - :raises: - :exc:`~exceptions.KeyError` if ``category`` is not a known template - category. - - This adds all templates found in ``path`` to ``category`` to the collection, - where each subdirectory of ``path`` is a template. If ``path`` contains any - templates whose names already exist in the ``category`` of the collection - (case insensitive), the existing entries are replaced. - """ - - for entry in os.listdir(path): - entryPath = os.path.join(path, entry) - if os.path.isdir(entryPath): - self._paths[category][entry.lower()] = entryPath - - #--------------------------------------------------------------------------- - def addPath(self, basePath): - """Add a template path to the collection. - - :param basePath: Path to a directory containing categorized templates. - :type basePath: :class:`str` + """Template collection manager. - This adds categorized templates to the collection. ``basePath`` should be - a directory which contains one or more directories whose names match a - known template category (case insensitive). Each such subdirectory is added - to the collection via :meth:`.addCategoryPath`. + This class provides a template collection and operations for managing and + using that collection. """ - if not os.path.exists(basePath): - return + # --------------------------------------------------------------------------- + def __init__(self): + self._paths = {} + self._keys = {} - basePath = os.path.realpath(basePath) + for c in _templateCategories: + self._paths[c] = {} - for entry in os.listdir(basePath): - if _isTemplateCategory(entry, basePath): - self.addCategoryPath(entry.lower(), os.path.join(basePath, entry)) + # --------------------------------------------------------------------------- + def _getKey(self, kind): + if kind in self._keys: + return self._keys[kind] - #--------------------------------------------------------------------------- - def setKey(self, name, value): - """Set template key for specified template. + return "TemplateKey" - :param name: Name of template for which to set key. - :type name: :class:`str` - :param key: Key for specified template. - :type name: :class:`str` + # --------------------------------------------------------------------------- + def _copyAndReplace(self, inFile, template, destination, key, name): + outFile = os.path.join(destination, inFile.replace(key, name)) + logging.info("creating '%s'" % outFile) + path = os.path.dirname(outFile) + if not os.path.exists(path): + os.makedirs(path) - This sets the template key for ``name`` to ``key``. + # Read file contents + p = os.path.join(template, inFile) + with open(p, "rb") as fp: + contents = fp.read() - .. 'note' directive needs '\' to span multiple lines! - .. note:: Template keys depend only on the template name, and not the \ - template category. As a result, two templates with the same name \ - in different categories will use the same key. + # Replace template key with copy name + if isinstance(name, bytes): + # If replacement is just bytes, we can just do the replacement... + contents = contents.replace(key, name) + contents = contents.replace(key.upper(), name.upper()) - .. seealso:: :meth:`.copyTemplate` - """ - - self._keys[name] = value + else: + # ...else we have to try to guess the template file encoding in order to + # convert it to unicode and back + encoding, confidence = detectEncoding(contents) + + if encoding is not None: + if confidence < 0.5: + logging.warning("%s: encoding detection confidence is %f:" + " copied file might be corrupt" % (p, confidence)) + + contents = contents.decode(encoding) + contents = contents.replace(key, name) + contents = contents.replace(key.upper(), name.upper()) + contents = contents.encode(encoding) + + else: + # Looks like a binary file; don't perform replacement + pass + + # Write adjusted contents + with open(outFile, "wb") as fp: + fp.write(contents) + + # --------------------------------------------------------------------------- + def copyTemplate(self, destination, category, kind, name, createInSubdirectory=True, requireEmptyDirectory=True): + """Copy (instantiate) a template. + + :param destination: Directory in which to create the template copy. + :type destination: :class:`str` + :param category: Category of template to instantiate. + :type category: :class:`str` + :param kind: Name of template to instantiate. + :type kind: :class:`str` + :param name: Name for the instantiated template. + :type name: :class:`str` + :param createInSubdirectory: If True then files are copied to ``destination/name/``, else ``destination/``. + :type name: :class:`bool` + :param requireEmptyDirectory: If True then files are only copied if the target directory is empty. + :type name: :class:`bool` + + :return: + Path to the new instance (``os.path.join(destination, name)``). + :rtype: + :class:`unicode` if either ``destination`` and/or ``name`` is also + :class:`unicode`, otherwise :class:`str`. + + :raises: + * :exc:`~exceptions.KeyError` if the specified template is not found. + * :exc:`~exceptions.IOError` if a subdirectory ``name`` already exists. + + This creates a copy of the specified template in ``destination``, with + occurrences of the template's key replaced with ``name``. The new copy is + in a subdirectory ``name``, which must not exist. + + .. note:: The replacement of the template key is case sensitive, however \ + the upper-case key is also replaced with the upper-case ``name``. + + .. seealso:: :meth:`.setKey` + """ - #--------------------------------------------------------------------------- - @classmethod - def categories(cls): - """Get list of known template categories. + templates = self._paths[category] + if not kind.lower() in templates: + raise KeyError("'%s' is not a known extension template" % kind) - :rtype: :class:`list` of :class:`str`. + kind = kind.lower() - .. seealso:: :meth:`templates`, :meth:`.listTemplates` - """ + if createInSubdirectory: + destination = os.path.join(destination, name) - return list(_templateCategories) + if requireEmptyDirectory and os.path.exists(destination): + raise OSError("create %s: refusing to overwrite" + " existing directory '%s'" % (category, destination)) + + template = templates[kind] + key = self._getKey(kind) - #--------------------------------------------------------------------------- - def templates(self, category=None): - """Get collection of available templates. + logging.info("copy template '%s' to '%s', replacing '%s' -> '%s'" % + (template, destination, key, name)) + for f in _listSources(template): + self._copyAndReplace(f, template, destination, key, name) + + return destination + + # --------------------------------------------------------------------------- + def addCategoryPath(self, category, path): + """Add templates for a particular category to the collection. + + :param category: Category of templates to add. + :type category: :class:`str` + :param path: Path to a directory containing templates. + :type path: :class:`str` + + :raises: + :exc:`~exceptions.KeyError` if ``category`` is not a known template + category. + + This adds all templates found in ``path`` to ``category`` to the collection, + where each subdirectory of ``path`` is a template. If ``path`` contains any + templates whose names already exist in the ``category`` of the collection + (case insensitive), the existing entries are replaced. + """ + + for entry in os.listdir(path): + entryPath = os.path.join(path, entry) + if os.path.isdir(entryPath): + self._paths[category][entry.lower()] = entryPath + + # --------------------------------------------------------------------------- + def addPath(self, basePath): + """Add a template path to the collection. + + :param basePath: Path to a directory containing categorized templates. + :type basePath: :class:`str` - :param category: Category of templates to query. - :type name: :class:`str` + This adds categorized templates to the collection. ``basePath`` should be + a directory which contains one or more directories whose names match a + known template category (case insensitive). Each such subdirectory is added + to the collection via :meth:`.addCategoryPath`. + """ - :return: - List of templates for the specified category, or a dictionary of such - (keyed by category name) if ``category`` is ``None``. - :rtype: - :class:`list` of :class:`str`, or :class:`dict` of - :class:`str` |rarr| (:class:`list` of :class:`str`). + if not os.path.exists(basePath): + return - :raises: - :exc:`~exceptions.KeyError` if ``category`` is not ``None`` or a known - template category. + basePath = os.path.realpath(basePath) - .. seealso:: :func:`~SlicerWizard.TemplateManager.categories`, - :meth:`.listTemplates` - """ + for entry in os.listdir(basePath): + if _isTemplateCategory(entry, basePath): + self.addCategoryPath(entry.lower(), os.path.join(basePath, entry)) - if category is None: - result = {} - for c in _templateCategories: - result[c] = list(self._paths[c].keys()) - return result + # --------------------------------------------------------------------------- + def setKey(self, name, value): + """Set template key for specified template. - else: - return tuple(self._paths[category].keys()) + :param name: Name of template for which to set key. + :type name: :class:`str` + :param key: Key for specified template. + :type name: :class:`str` - #--------------------------------------------------------------------------- - def listTemplates(self): - """List available templates. + This sets the template key for ``name`` to ``key``. - This displays a list of all available templates, using :func:`logging.info`, - organized by category. + .. 'note' directive needs '\' to span multiple lines! + .. note:: Template keys depend only on the template name, and not the \ + template category. As a result, two templates with the same name \ + in different categories will use the same key. - .. seealso:: :func:`~SlicerWizard.TemplateManager.categories`, - :meth:`.templates` - """ + .. seealso:: :meth:`.copyTemplate` + """ - for c in _templateCategories: - logging.info("Available templates for category '%s':" % c) + self._keys[name] = value - if len(self._paths[c]): - for t in sorted(self._paths[c].keys()): - logging.info(f" '{t}' ('{self._getKey(t)}')") + # --------------------------------------------------------------------------- + @classmethod + def categories(cls): + """Get list of known template categories. - else: - logging.info(" (none)") + :rtype: :class:`list` of :class:`str`. - logging.info("") + .. seealso:: :meth:`templates`, :meth:`.listTemplates` + """ - #--------------------------------------------------------------------------- - def addArguments(self, parser): - """Add template manager |CLI| arguments to parser. + return list(_templateCategories) - :param parser: Argument parser instance to which to add arguments. - :type parser: :class:`argparse.ArgumentParser` + # --------------------------------------------------------------------------- + def templates(self, category=None): + """Get collection of available templates. - This adds |CLI| arguments to the specified ``parser`` that may be used to - interact with the template collection. + :param category: Category of templates to query. + :type name: :class:`str` - .. 'note' directive needs '\' to span multiple lines! - .. note:: The arguments use ``'<'`` and ``'>'`` to annotate optional \ - values. It is recommended to use :class:`.WizardHelpFormatter` \ - with the parser so that these will be displayed using the \ - conventional ``'['`` and ``']'``. + :return: + List of templates for the specified category, or a dictionary of such + (keyed by category name) if ``category`` is ``None``. + :rtype: + :class:`list` of :class:`str`, or :class:`dict` of + :class:`str` |rarr| (:class:`list` of :class:`str`). - .. seealso:: :meth:`.parseArguments` - """ + :raises: + :exc:`~exceptions.KeyError` if ``category`` is not ``None`` or a known + template category. - parser.add_argument("--templatePath", metavar="PATH", - action="append", - help="add additional template path for specified" - " template category; if no category, expect that" - " PATH contains subdirectories for one or more" - " possible categories") - parser.add_argument("--templateKey", metavar="TYPE=KEY", action="append", - help="set template substitution key for specified" - " template (default key: 'TemplateKey')") - - #--------------------------------------------------------------------------- - def parseArguments(self, args): - """Automatically add paths and keys from |CLI| arguments. - - :param args.templatePath: List of additional template paths. - :type args.templatePath: :class:`list` of :class:`str` - :param args.templateKey: List of user-specified template key mappings. - :type args.templateKey: :class:`list` of :class:`str` - - This parses template-related command line arguments and updates the - collection accordingly: - - * Additional template paths are provided in the form - ``'[category=]path'``, and are added with either :meth:`.addPath` (if - ``category`` is omitted) or :meth:`.addCategoryPath` (otherwise). - * Template keys are provided in the form ``'name=value'``, and are - registered using :meth:`.setKey`. - - If a usage error is found, the application is terminated by calling - :func:`~.Utilities.die` with an appropriate error message. - - .. seealso:: :meth:`.parseArguments`, :meth:`.addPath`, - :meth:`.addCategoryPath`, :meth:`.setKey` - """ + .. seealso:: :func:`~SlicerWizard.TemplateManager.categories`, + :meth:`.listTemplates` + """ - # Add user-specified template paths - if args.templatePath is not None: - for tp in args.templatePath: - tpParts = tp.split("=", 1) + if category is None: + result = {} + for c in _templateCategories: + result[c] = list(self._paths[c].keys()) + return result - if len(tpParts) == 1: - if not os.path.exists(tp): - die("template path '%s' does not exist" % tp) - if not os.path.isdir(tp): - die("template path '%s' is not a directory" % tp) + else: + return tuple(self._paths[category].keys()) - self.addPath(tp) + # --------------------------------------------------------------------------- + def listTemplates(self): + """List available templates. - else: - if tpParts[0].lower() not in _templateCategories: - die(("'%s' is not a recognized template category" % tpParts[0], - "recognized categories: %s" % ", ".join(_templateCategories))) - - if not os.path.exists(tpParts[1]): - die("template path '%s' does not exist" % tpParts[1]) - if not os.path.isdir(tpParts[1]): - die("template path '%s' is not a directory" % tpParts[1]) - - self.addCategoryPath(tpParts[0].lower(), - os.path.realpath(tpParts[1])) - - # Set user-specified template keys - if args.templateKey is not None: - for tk in args.templateKey: - tkParts = tk.split("=") - if len(tkParts) != 2: - die("template key '%s' malformatted: expected 'NAME=KEY'" % tk) - - self.setKey(tkParts[0].lower(), tkParts[1]) + This displays a list of all available templates, using :func:`logging.info`, + organized by category. + + .. seealso:: :func:`~SlicerWizard.TemplateManager.categories`, + :meth:`.templates` + """ + + for c in _templateCategories: + logging.info("Available templates for category '%s':" % c) + + if len(self._paths[c]): + for t in sorted(self._paths[c].keys()): + logging.info(f" '{t}' ('{self._getKey(t)}')") + + else: + logging.info(" (none)") + + logging.info("") + + # --------------------------------------------------------------------------- + def addArguments(self, parser): + """Add template manager |CLI| arguments to parser. + + :param parser: Argument parser instance to which to add arguments. + :type parser: :class:`argparse.ArgumentParser` + + This adds |CLI| arguments to the specified ``parser`` that may be used to + interact with the template collection. + + .. 'note' directive needs '\' to span multiple lines! + .. note:: The arguments use ``'<'`` and ``'>'`` to annotate optional \ + values. It is recommended to use :class:`.WizardHelpFormatter` \ + with the parser so that these will be displayed using the \ + conventional ``'['`` and ``']'``. + + .. seealso:: :meth:`.parseArguments` + """ + + parser.add_argument("--templatePath", metavar="PATH", + action="append", + help="add additional template path for specified" + " template category; if no category, expect that" + " PATH contains subdirectories for one or more" + " possible categories") + parser.add_argument("--templateKey", metavar="TYPE=KEY", action="append", + help="set template substitution key for specified" + " template (default key: 'TemplateKey')") + + # --------------------------------------------------------------------------- + def parseArguments(self, args): + """Automatically add paths and keys from |CLI| arguments. + + :param args.templatePath: List of additional template paths. + :type args.templatePath: :class:`list` of :class:`str` + :param args.templateKey: List of user-specified template key mappings. + :type args.templateKey: :class:`list` of :class:`str` + + This parses template-related command line arguments and updates the + collection accordingly: + + * Additional template paths are provided in the form + ``'[category=]path'``, and are added with either :meth:`.addPath` (if + ``category`` is omitted) or :meth:`.addCategoryPath` (otherwise). + * Template keys are provided in the form ``'name=value'``, and are + registered using :meth:`.setKey`. + + If a usage error is found, the application is terminated by calling + :func:`~.Utilities.die` with an appropriate error message. + + .. seealso:: :meth:`.parseArguments`, :meth:`.addPath`, + :meth:`.addCategoryPath`, :meth:`.setKey` + """ + + # Add user-specified template paths + if args.templatePath is not None: + for tp in args.templatePath: + tpParts = tp.split("=", 1) + + if len(tpParts) == 1: + if not os.path.exists(tp): + die("template path '%s' does not exist" % tp) + if not os.path.isdir(tp): + die("template path '%s' is not a directory" % tp) + + self.addPath(tp) + + else: + if tpParts[0].lower() not in _templateCategories: + die(("'%s' is not a recognized template category" % tpParts[0], + "recognized categories: %s" % ", ".join(_templateCategories))) + + if not os.path.exists(tpParts[1]): + die("template path '%s' does not exist" % tpParts[1]) + if not os.path.isdir(tpParts[1]): + die("template path '%s' is not a directory" % tpParts[1]) + + self.addCategoryPath(tpParts[0].lower(), + os.path.realpath(tpParts[1])) + + # Set user-specified template keys + if args.templateKey is not None: + for tk in args.templateKey: + tkParts = tk.split("=") + if len(tkParts) != 2: + die("template key '%s' malformatted: expected 'NAME=KEY'" % tk) + + self.setKey(tkParts[0].lower(), tkParts[1]) diff --git a/Utilities/Scripts/SlicerWizard/Utilities.py b/Utilities/Scripts/SlicerWizard/Utilities.py index 2c237a9d1b4..c88a70ad428 100644 --- a/Utilities/Scripts/SlicerWizard/Utilities.py +++ b/Utilities/Scripts/SlicerWizard/Utilities.py @@ -7,547 +7,547 @@ import textwrap -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def haveGit(): - """Return True if git is available. + """Return True if git is available. - A side effect of `import git` is that it shows a popup window on - macOS, asking the user to install XCode (if git is not installed already), - therefore this method should only be called if git is actually needed. - """ + A side effect of `import git` is that it shows a popup window on + macOS, asking the user to install XCode (if git is not installed already), + therefore this method should only be called if git is actually needed. + """ - try: - import git # noqa: F401 - _haveGit = True + try: + import git # noqa: F401 + _haveGit = True - except ImportError: - _haveGit = False + except ImportError: + _haveGit = False - return _haveGit + return _haveGit try: - from charset_normalizer import detect - _haveCharDet = True + from charset_normalizer import detect + _haveCharDet = True except ImportError: - _haveCharDet = False + _haveCharDet = False __all__ = [ - 'warn', - 'die', - 'inquire', - 'initLogging', - 'detectEncoding', - 'buildProcessArgs', - 'createEmptyRepo', - 'SourceTreeDirectory', - 'getRepo', - 'getRemote', - 'localRoot', - 'vcsPrivateDirectory', + 'warn', + 'die', + 'inquire', + 'initLogging', + 'detectEncoding', + 'buildProcessArgs', + 'createEmptyRepo', + 'SourceTreeDirectory', + 'getRepo', + 'getRemote', + 'localRoot', + 'vcsPrivateDirectory', ] _yesno = { - "y": True, - "n": False, + "y": True, + "n": False, } _logLevel = None -#============================================================================= +# ============================================================================= class _LogWrapFormatter(logging.Formatter): - #--------------------------------------------------------------------------- - def __init__(self): - super().__init__() - try: - self._width = int(os.environ['COLUMNS']) - 1 - except: - self._width = 79 + # --------------------------------------------------------------------------- + def __init__(self): + super().__init__() + try: + self._width = int(os.environ['COLUMNS']) - 1 + except: + self._width = 79 - #--------------------------------------------------------------------------- - def format(self, record): - lines = super().format(record).split("\n") - return "\n".join([textwrap.fill(l, self._width) for l in lines]) + # --------------------------------------------------------------------------- + def format(self, record): + lines = super().format(record).split("\n") + return "\n".join([textwrap.fill(line, self._width) for line in lines]) -#============================================================================= +# ============================================================================= class _LogReverseLevelFilter(logging.Filter): - #--------------------------------------------------------------------------- - def __init__(self, levelLimit): - self._levelLimit = levelLimit + # --------------------------------------------------------------------------- + def __init__(self, levelLimit): + self._levelLimit = levelLimit - #--------------------------------------------------------------------------- - def filter(self, record): - return record.levelno < self._levelLimit + # --------------------------------------------------------------------------- + def filter(self, record): + return record.levelno < self._levelLimit -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def _log(func, msg): - if sys.exc_info()[0] is not None: - if _logLevel <= logging.DEBUG: - logging.exception("") + if sys.exc_info()[0] is not None: + if _logLevel <= logging.DEBUG: + logging.exception("") - if isinstance(msg, tuple): - for m in msg: - func(m) + if isinstance(msg, tuple): + for m in msg: + func(m) - else: - func(msg) + else: + func(msg) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def warn(msg): - """Output a warning message (or messages), with exception if present. + """Output a warning message (or messages), with exception if present. - :param msg: Message(s) to be output. - :type msg: :class:`str` or sequence of :class:`str` + :param msg: Message(s) to be output. + :type msg: :class:`str` or sequence of :class:`str` - This function outputs the specified message(s) using :func:`logging.warning`. - If ``msg`` is a sequence, each message in the sequence is output, with a call - to :func:`logging.warning` made for each message. + This function outputs the specified message(s) using :func:`logging.warning`. + If ``msg`` is a sequence, each message in the sequence is output, with a call + to :func:`logging.warning` made for each message. - If there is a current exception, and debugging is enabled, the exception is - reported prior to the other message(s) using :func:`logging.exception`. + If there is a current exception, and debugging is enabled, the exception is + reported prior to the other message(s) using :func:`logging.exception`. - .. seealso:: :func:`.initLogging`. - """ + .. seealso:: :func:`.initLogging`. + """ - _log(logging.warning, msg) + _log(logging.warning, msg) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def die(msg, exitCode=1): - """Output an error message (or messages), with exception if present. + """Output an error message (or messages), with exception if present. - :param msg: Message(s) to be output. - :type msg: :class:`str` or sequence of :class:`str` - :param exitCode: Value to use as the exit code of the program. - :type exitCode: :class:`int` + :param msg: Message(s) to be output. + :type msg: :class:`str` or sequence of :class:`str` + :param exitCode: Value to use as the exit code of the program. + :type exitCode: :class:`int` - The output behavior (including possible report of an exception) of this - function is the same as :func:`.warn`, except that :func:`logging.error` is - used instead of :func:`logging.warning`. After output, the program is - terminated by calling :func:`sys.exit` with the specified exit code. - """ + The output behavior (including possible report of an exception) of this + function is the same as :func:`.warn`, except that :func:`logging.error` is + used instead of :func:`logging.warning`. After output, the program is + terminated by calling :func:`sys.exit` with the specified exit code. + """ - _log(logging.error, msg) - sys.exit(exitCode) + _log(logging.error, msg) + sys.exit(exitCode) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def inquire(msg, choices=_yesno): - """Get multiple-choice input from the user. - - :param msg: - Text of the prompt which the user will be shown. - :type msg: - :class:`str` - :param choices: - Map of possible choices to their respective return values. - :type choices: - :class:`dict` - - :returns: - Value of the selected choice. - - This function presents a question (``msg``) to the user and asks them to - select an option from a list of choices, which are presented in the manner of - 'git add --patch' (i.e. the possible choices are shown between the prompt - text and the final '?'). The prompt is repeated indefinitely until a valid - selection is made. - - The ``choices`` are a :class:`dict`, with each key being a possible choice - (using a single letter is recommended). The value for the selected key is - returned to the caller. - - The default ``choices`` provides a yes/no prompt with a :class:`bool` return - value. - """ + """Get multiple-choice input from the user. + + :param msg: + Text of the prompt which the user will be shown. + :type msg: + :class:`str` + :param choices: + Map of possible choices to their respective return values. + :type choices: + :class:`dict` + + :returns: + Value of the selected choice. + + This function presents a question (``msg``) to the user and asks them to + select an option from a list of choices, which are presented in the manner of + 'git add --patch' (i.e. the possible choices are shown between the prompt + text and the final '?'). The prompt is repeated indefinitely until a valid + selection is made. + + The ``choices`` are a :class:`dict`, with each key being a possible choice + (using a single letter is recommended). The value for the selected key is + returned to the caller. + + The default ``choices`` provides a yes/no prompt with a :class:`bool` return + value. + """ - choiceKeys = list(choices.keys()) - msg = "{} {}? ".format(msg, ",".join(choiceKeys)) + choiceKeys = list(choices.keys()) + msg = "{} {}? ".format(msg, ",".join(choiceKeys)) - def throw(*args): - raise ValueError() + def throw(*args): + raise ValueError() - parser = argparse.ArgumentParser() - parser.add_argument("choice", choices=choiceKeys) - parser.error = throw + parser = argparse.ArgumentParser() + parser.add_argument("choice", choices=choiceKeys) + parser.error = throw - while True: - try: - args = parser.parse_args(input(msg)) - if args.choice in choices: - return choices[args.choice] + while True: + try: + args = parser.parse_args(input(msg)) + if args.choice in choices: + return choices[args.choice] - except: - pass + except: + pass -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def initLogging(logger, args): - """Initialize logging. + """Initialize logging. - :param args.debug: If ``True``, enable debug logging. - :type args.debug: :class:`bool` + :param args.debug: If ``True``, enable debug logging. + :type args.debug: :class:`bool` - This sets up the default logging object, with the following characteristics: + This sets up the default logging object, with the following characteristics: - * Messages of :data:`~logging.WARNING` severity or greater will be sent to - :data:`~sys.stderr`; other messages will be sent to :data:`~sys.stdout`. - * The log level is set to :data:`~logging.DEBUG` if ``args.debug`` is - ``True``, otherwise the log level is set to :data:`~logging.INFO`. - * The log handlers will wrap their output according to the current terminal - width (:envvar:`$COLUMNS`, if set, else 80). - """ + * Messages of :data:`~logging.WARNING` severity or greater will be sent to + :data:`~sys.stderr`; other messages will be sent to :data:`~sys.stdout`. + * The log level is set to :data:`~logging.DEBUG` if ``args.debug`` is + ``True``, otherwise the log level is set to :data:`~logging.INFO`. + * The log handlers will wrap their output according to the current terminal + width (:envvar:`$COLUMNS`, if set, else 80). + """ - global _logLevel - _logLevel = logging.DEBUG if args.debug else logging.INFO + global _logLevel + _logLevel = logging.DEBUG if args.debug else logging.INFO - # Create log output formatter - f = _LogWrapFormatter() + # Create log output formatter + f = _LogWrapFormatter() - # Create log output stream handlers - lho = logging.StreamHandler(sys.stdout) - lho.setLevel(_logLevel) - lho.addFilter(_LogReverseLevelFilter(logging.WARNING)) - lho.setFormatter(f) + # Create log output stream handlers + lho = logging.StreamHandler(sys.stdout) + lho.setLevel(_logLevel) + lho.addFilter(_LogReverseLevelFilter(logging.WARNING)) + lho.setFormatter(f) - lhe = logging.StreamHandler(sys.stderr) - lhe.setLevel(logging.WARNING) - lhe.setFormatter(f) + lhe = logging.StreamHandler(sys.stderr) + lhe.setLevel(logging.WARNING) + lhe.setFormatter(f) - # Set logging level and add handlers - logger.addHandler(lho) - logger.addHandler(lhe) - logger.setLevel(_logLevel) + # Set logging level and add handlers + logger.addHandler(lho) + logger.addHandler(lhe) + logger.setLevel(_logLevel) - # Turn of github debugging - ghLogger = logging.getLogger("github") - ghLogger.setLevel(logging.WARNING) + # Turn of github debugging + ghLogger = logging.getLogger("github") + ghLogger.setLevel(logging.WARNING) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def detectEncoding(data): - """Attempt to determine the encoding of a byte sequence. + """Attempt to determine the encoding of a byte sequence. - :param data: Input data on which to perform encoding detection. - :type data: :class:`str` + :param data: Input data on which to perform encoding detection. + :type data: :class:`str` - :return: Tuple of (encoding name, detection confidence). - :rtype: :class:`tuple` of (:class:`str` or ``None``, :class:`float`) + :return: Tuple of (encoding name, detection confidence). + :rtype: :class:`tuple` of (:class:`str` or ``None``, :class:`float`) - This function attempts to determine the character encoding of the input data. - It returns a tuple with the most likely encoding (or ``None`` if the input - data is not text) and the confidence of the detection. + This function attempts to determine the character encoding of the input data. + It returns a tuple with the most likely encoding (or ``None`` if the input + data is not text) and the confidence of the detection. - This function uses the :mod:`chardet` module, if it is available. Otherwise, - only ``'ascii'`` is detected, and ``None`` is returned for any non-ASCII - input. - """ + This function uses the :mod:`chardet` module, if it is available. Otherwise, + only ``'ascii'`` is detected, and ``None`` is returned for any non-ASCII + input. + """ - if _haveCharDet: - result = detect(data) - return result["encoding"], result["confidence"] + if _haveCharDet: + result = detect(data) + return result["encoding"], result["confidence"] - else: - chars = ''.join(map(chr, list(range(7,14)) + list(range(32, 128)))) - if len(data.translate(None, chars)): - return None, 0.0 + else: + chars = ''.join(map(chr, list(range(7, 14)) + list(range(32, 128)))) + if len(data.translate(None, chars)): + return None, 0.0 - return "ascii", 1.0 + return "ascii", 1.0 -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def buildProcessArgs(*args, **kwargs): - """Build |CLI| arguments from Python-like arguments. - - :param prefix: Prefix for named options. - :type prefix: :class:`str` - :param args: Positional arguments. - :type args: :class:`~collections.Sequence` - :param kwargs: Named options. - :type kwargs: :class:`dict` - - :return: Converted argument list. - :rtype: :class:`list` of :class:`str` - - This function converts Python-style arguments, including named arguments, to - a |CLI|-style argument list: - - .. code-block:: python - - >>> buildProcessArgs('p1', 'p2', None, 12, a=5, b=True, long_name='hello') - ['-a', '5', '--long-name', 'hello', '-b', 'p1', 'p2', '12'] - - Named arguments are converted to named options by adding ``'-'`` (if the name - is one letter) or ``'--'`` (otherwise), and converting any underscores - (``'_'``) to hyphens (``'-'``). If the value is ``True``, the option is - considered a flag that does not take a value. If the value is ``False`` or - ``None``, the option is skipped. Otherwise the stringified value is added - following the option argument. Positional arguments --- except for ``None``, - which is skipped --- are similarly stringified and added to the argument list - following named options. - """ + """Build |CLI| arguments from Python-like arguments. + + :param prefix: Prefix for named options. + :type prefix: :class:`str` + :param args: Positional arguments. + :type args: :class:`~collections.Sequence` + :param kwargs: Named options. + :type kwargs: :class:`dict` + + :return: Converted argument list. + :rtype: :class:`list` of :class:`str` + + This function converts Python-style arguments, including named arguments, to + a |CLI|-style argument list: + + .. code-block:: python + + >>> buildProcessArgs('p1', 'p2', None, 12, a=5, b=True, long_name='hello') + ['-a', '5', '--long-name', 'hello', '-b', 'p1', 'p2', '12'] + + Named arguments are converted to named options by adding ``'-'`` (if the name + is one letter) or ``'--'`` (otherwise), and converting any underscores + (``'_'``) to hyphens (``'-'``). If the value is ``True``, the option is + considered a flag that does not take a value. If the value is ``False`` or + ``None``, the option is skipped. Otherwise the stringified value is added + following the option argument. Positional arguments --- except for ``None``, + which is skipped --- are similarly stringified and added to the argument list + following named options. + """ - result = [] + result = [] - for k, v in kwargs.items(): - if v is None or v is False: - continue + for k, v in kwargs.items(): + if v is None or v is False: + continue - result += ["{}{}".format("-" if len(k) == 1 else "--", k.replace("_", "-"))] + result += ["{}{}".format("-" if len(k) == 1 else "--", k.replace("_", "-"))] - if v is not True: - result += ["%s" % v] + if v is not True: + result += ["%s" % v] - return result + ["%s" % a for a in args if a is not None] + return result + ["%s" % a for a in args if a is not None] -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def createEmptyRepo(path, tool=None): - """Create a repository in an empty or nonexistent location. - - :param path: - Location which should contain the newly created repository. - :type path: - :class:`str` - :param tool: - Name of the |VCS| tool to use to create the repository (e.g. ``'git'``). If - ``None``, a default tool (git) is used. - :type tool: - :class:`str` or ``None`` + """Create a repository in an empty or nonexistent location. + + :param path: + Location which should contain the newly created repository. + :type path: + :class:`str` + :param tool: + Name of the |VCS| tool to use to create the repository (e.g. ``'git'``). If + ``None``, a default tool (git) is used. + :type tool: + :class:`str` or ``None`` - :raises: - :exc:`~exceptions.Exception` if ``location`` exists and is not empty, or if - the specified |VCS| tool is not supported. + :raises: + :exc:`~exceptions.Exception` if ``location`` exists and is not empty, or if + the specified |VCS| tool is not supported. - This creates a new repository using the specified ``tool`` at ``location``, - first creating ``location`` (and any parents) as necessary. + This creates a new repository using the specified ``tool`` at ``location``, + first creating ``location`` (and any parents) as necessary. - This function is meant to be passed as the ``create`` argument to - :func:`.getRepo`. + This function is meant to be passed as the ``create`` argument to + :func:`.getRepo`. - .. note:: Only ``'git'`` repositories are supported at this time. - """ + .. note:: Only ``'git'`` repositories are supported at this time. + """ - # Check that the requested tool is supported - if not haveGit() or tool not in (None, "git"): - raise Exception("unable to create %r repository" % tool) + # Check that the requested tool is supported + if not haveGit() or tool not in (None, "git"): + raise Exception("unable to create %r repository" % tool) - # Create a repository at the specified location - if os.path.exists(path) and len(os.listdir(path)): - raise Exception("refusing to create repository in non-empty directory") + # Create a repository at the specified location + if os.path.exists(path) and len(os.listdir(path)): + raise Exception("refusing to create repository in non-empty directory") - os.makedirs(path) - import git - return git.Repo.init(path) + os.makedirs(path) + import git + return git.Repo.init(path) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- class SourceTreeDirectory: - """Abstract representation of a source tree directory. + """Abstract representation of a source tree directory. - .. attribute:: root + .. attribute:: root - Location of the source tree. + Location of the source tree. - .. attribute:: relative_directory + .. attribute:: relative_directory - The relative path to the source directory. - """ - #--------------------------------------------------------------------------- - - def __init__(self, root, relative_directory): + The relative path to the source directory. """ - :param root: Location of the source tree. - :type root: :class:`str` + # --------------------------------------------------------------------------- - :param relative_directory: Relative directory. - :type relative_directory: :class:`str` + def __init__(self, root, relative_directory): + """ + :param root: Location of the source tree. + :type root: :class:`str` - :raises: - * :exc:`~exceptions.IOError` if the ``root/relative_directory`` does not exist. - . - """ - if not os.path.exists(os.path.join(root, relative_directory)): - raise OSError("'root/relative_directory' does not exist") - self.root = root - self.relative_directory = relative_directory + :param relative_directory: Relative directory. + :type relative_directory: :class:`str` + :raises: + * :exc:`~exceptions.IOError` if the ``root/relative_directory`` does not exist. + . + """ + if not os.path.exists(os.path.join(root, relative_directory)): + raise OSError("'root/relative_directory' does not exist") + self.root = root + self.relative_directory = relative_directory -#----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- def getRepo(path, tool=None, create=False): - """Obtain a git repository for the specified path. - - :param path: Path to the repository. - :type path: :class:`str` - :param tool: Name of tool used to manage repository, e.g. ``'git'``. - :type tool: :class:`str` or ``None`` - :param create: See description. - :type create: :class:`callable` or :class:`bool` - - :returns: - The repository instance, or ``None`` if no such repository exists. - :rtype: - :class:`git.Repo `, :class:`.Subversion.Repository`, - or ``None``. - - This attempts to obtain a repository for the specified ``path``. If ``tool`` - is not ``None``, this will only look for a repository that is managed by the - specified ``tool``; otherwise, all supported repository types will be - considered. - - If ``create`` is callable, the specified function will be called to create - the repository if one does not exist. Otherwise if ``bool(create)`` is - ``True``, and ``tool`` is either ``None`` or ``'git'``, a repository is - created using :meth:`git.Repo.init `. (Creation - of other repository types is only supported at this time via a callable - ``create``.) - - .. seealso:: :func:`.createEmptyRepo` - """ - - from . import Subversion - - # Try to obtain git repository - if haveGit() and tool in (None, "git"): - try: - import git - repo = git.Repo(path) - return repo + """Obtain a git repository for the specified path. + + :param path: Path to the repository. + :type path: :class:`str` + :param tool: Name of tool used to manage repository, e.g. ``'git'``. + :type tool: :class:`str` or ``None`` + :param create: See description. + :type create: :class:`callable` or :class:`bool` + + :returns: + The repository instance, or ``None`` if no such repository exists. + :rtype: + :class:`git.Repo `, :class:`.Subversion.Repository`, + or ``None``. + + This attempts to obtain a repository for the specified ``path``. If ``tool`` + is not ``None``, this will only look for a repository that is managed by the + specified ``tool``; otherwise, all supported repository types will be + considered. + + If ``create`` is callable, the specified function will be called to create + the repository if one does not exist. Otherwise if ``bool(create)`` is + ``True``, and ``tool`` is either ``None`` or ``'git'``, a repository is + created using :meth:`git.Repo.init `. (Creation + of other repository types is only supported at this time via a callable + ``create``.) + + .. seealso:: :func:`.createEmptyRepo` + """ - except: - logging.debug("%r is not a git repository" % path) + from . import Subversion - # Try to obtain subversion repository - if tool in (None, "svn"): - try: - repo = Subversion.Repository(path) - return repo + # Try to obtain git repository + if haveGit() and tool in (None, "git"): + try: + import git + repo = git.Repo(path) + return repo - except: - logging.debug("%r is not a svn repository" % path) + except: + logging.debug("%r is not a git repository" % path) - # Specified path is not a supported / allowed repository; create a repository - # if requested, otherwise return None - if create: - if callable(create): - return create(path, tool) + # Try to obtain subversion repository + if tool in (None, "svn"): + try: + repo = Subversion.Repository(path) + return repo - elif haveGit() and tool in (None, "git"): - import git - return git.Repo.init(path) + except: + logging.debug("%r is not a svn repository" % path) - else: - raise Exception("unable to create %r repository" % tool) + # Specified path is not a supported / allowed repository; create a repository + # if requested, otherwise return None + if create: + if callable(create): + return create(path, tool) - return None + elif haveGit() and tool in (None, "git"): + import git + return git.Repo.init(path) + else: + raise Exception("unable to create %r repository" % tool) -#----------------------------------------------------------------------------- -def getRemote(repo, urls, create=None): - """Get the remote matching a URL. + return None - :param repo: - repository instance from which to obtain the remote. - :type repo: - :class:`git.Repo ` - :param urls: - A URL or list of URL's of the remote to obtain. - :type urls: - :class:`str` or sequence of :class:`str` - :param create: - What to name the remote when creating it, if it doesn't exist. - :type create: :class:`str` or ``None`` - :returns: - A matching or newly created :class:`git.Remote `, or - ``None`` if no such remote exists. +# ----------------------------------------------------------------------------- +def getRemote(repo, urls, create=None): + """Get the remote matching a URL. + + :param repo: + repository instance from which to obtain the remote. + :type repo: + :class:`git.Repo ` + :param urls: + A URL or list of URL's of the remote to obtain. + :type urls: + :class:`str` or sequence of :class:`str` + :param create: + What to name the remote when creating it, if it doesn't exist. + :type create: :class:`str` or ``None`` + + :returns: + A matching or newly created :class:`git.Remote `, or + ``None`` if no such remote exists. - :raises: - :exc:`~exceptions.Exception` if, when trying to create a remote, a remote - with the specified name already exists. + :raises: + :exc:`~exceptions.Exception` if, when trying to create a remote, a remote + with the specified name already exists. - This attempts to find a git remote of the specified repository whose upstream - URL matches (one of) ``urls``. If no such remote exists and ``create`` is not - ``None``, a new remote named ``create`` will be created using the first URL - of ``urls``. - """ + This attempts to find a git remote of the specified repository whose upstream + URL matches (one of) ``urls``. If no such remote exists and ``create`` is not + ``None``, a new remote named ``create`` will be created using the first URL + of ``urls``. + """ - urls = list(urls) + urls = list(urls) - for remote in repo.remotes: - if remote.url in urls: - return remote + for remote in repo.remotes: + if remote.url in urls: + return remote - if create is not None: - if not isinstance(create, str): - raise TypeError("name of remote to create must be a string") + if create is not None: + if not isinstance(create, str): + raise TypeError("name of remote to create must be a string") - if hasattr(repo.remotes, create): - raise Exception("cannot create remote '%s':" - " a remote with that name already exists" % create) + if hasattr(repo.remotes, create): + raise Exception("cannot create remote '%s':" + " a remote with that name already exists" % create) - return repo.create_remote(create, urls[0]) + return repo.create_remote(create, urls[0]) - return None + return None -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def localRoot(repo): - """Get top level local directory of a repository. + """Get top level local directory of a repository. - :param repo: - Repository instance. - :type repo: - :class:`git.Repo ` or - :class:`.Subversion.Repository`. + :param repo: + Repository instance. + :type repo: + :class:`git.Repo ` or + :class:`.Subversion.Repository`. - :return: Absolute path to the repository local root. - :rtype: :class:`str` + :return: Absolute path to the repository local root. + :rtype: :class:`str` - :raises: :exc:`~exceptions.Exception` if the local root cannot be determined. + :raises: :exc:`~exceptions.Exception` if the local root cannot be determined. - This returns the local file system path to the top level of a repository - working tree / working copy. - """ + This returns the local file system path to the top level of a repository + working tree / working copy. + """ - if hasattr(repo, "working_tree_dir"): - return repo.working_tree_dir + if hasattr(repo, "working_tree_dir"): + return repo.working_tree_dir - if hasattr(repo, "wc_root"): - return repo.wc_root + if hasattr(repo, "wc_root"): + return repo.wc_root - raise Exception("unable to determine repository local root") + raise Exception("unable to determine repository local root") -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def vcsPrivateDirectory(repo): - """Get |VCS| private directory of a repository. + """Get |VCS| private directory of a repository. - :param repo: - Repository instance. - :type repo: - :class:`git.Repo ` or - :class:`.Subversion.Repository`. + :param repo: + Repository instance. + :type repo: + :class:`git.Repo ` or + :class:`.Subversion.Repository`. - :return: Absolute path to the |VCS| private directory. - :rtype: :class:`str` + :return: Absolute path to the |VCS| private directory. + :rtype: :class:`str` - :raises: - :exc:`~exceptions.Exception` if the private directory cannot be determined. + :raises: + :exc:`~exceptions.Exception` if the private directory cannot be determined. - This returns the |VCS| private directory for a repository, e.g. the ``.git`` - or ``.svn`` directory. - """ + This returns the |VCS| private directory for a repository, e.g. the ``.git`` + or ``.svn`` directory. + """ - if hasattr(repo, "git_dir"): - return repo.git_dir + if hasattr(repo, "git_dir"): + return repo.git_dir - if hasattr(repo, "svn_dir"): - return repo.svn_dir + if hasattr(repo, "svn_dir"): + return repo.svn_dir - raise Exception("unable to determine repository local private directory") + raise Exception("unable to determine repository local private directory") diff --git a/Utilities/Scripts/SlicerWizard/WizardHelpFormatter.py b/Utilities/Scripts/SlicerWizard/WizardHelpFormatter.py index c4c9f39d988..6ecd205970a 100644 --- a/Utilities/Scripts/SlicerWizard/WizardHelpFormatter.py +++ b/Utilities/Scripts/SlicerWizard/WizardHelpFormatter.py @@ -1,23 +1,23 @@ import argparse -#============================================================================= +# ============================================================================= class WizardHelpFormatter(argparse.HelpFormatter): - """Custom formatter for |CLI| arguments. + """Custom formatter for |CLI| arguments. - This formatter overrides :class:`argparse.HelpFormatter` in order to replace - occurrences of the '<' and '>' characters with '[' and ']', respectively. - This is done to work around the formatter's wrapping, which tries to break - metavars if they contain these characters and then becomes confused (read: - raises an assertion). - """ + This formatter overrides :class:`argparse.HelpFormatter` in order to replace + occurrences of the '<' and '>' characters with '[' and ']', respectively. + This is done to work around the formatter's wrapping, which tries to break + metavars if they contain these characters and then becomes confused (read: + raises an assertion). + """ - #--------------------------------------------------------------------------- - def _format_action_invocation(self, *args): - text = super()._format_action_invocation(*args) - return text.replace("<", "[").replace(">", "]") + # --------------------------------------------------------------------------- + def _format_action_invocation(self, *args): + text = super()._format_action_invocation(*args) + return text.replace("<", "[").replace(">", "]") - #--------------------------------------------------------------------------- - def _format_usage(self, *args): - text = super()._format_usage(*args) - return text.replace("<", "[").replace(">", "]") + # --------------------------------------------------------------------------- + def _format_usage(self, *args): + text = super()._format_usage(*args) + return text.replace("<", "[").replace(">", "]") diff --git a/Utilities/Scripts/SlicerWizard/__version__.py b/Utilities/Scripts/SlicerWizard/__version__.py index 2f2842e19b9..c09116cd3b8 100644 --- a/Utilities/Scripts/SlicerWizard/__version__.py +++ b/Utilities/Scripts/SlicerWizard/__version__.py @@ -1,8 +1,8 @@ __version_info__ = ( - 5, - 1, - 0, - "dev0" + 5, + 1, + 0, + "dev0" ) __version__ = ".".join(map(str, __version_info__)) diff --git a/Utilities/Scripts/SlicerWizard/doc/conf.py b/Utilities/Scripts/SlicerWizard/doc/conf.py index 3f650806c50..d22c4f8a1f7 100644 --- a/Utilities/Scripts/SlicerWizard/doc/conf.py +++ b/Utilities/Scripts/SlicerWizard/doc/conf.py @@ -24,48 +24,48 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join('..', '..'))) -#%%% Site extensions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +# %%% Site extensions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -#=============================================================================== +# =============================================================================== class WikidocRole: - wiki_root = 'https://wiki.slicer.org/slicerWiki/index.php' + wiki_root = 'https://wiki.slicer.org/slicerWiki/index.php' - #----------------------------------------------------------------------------- - def __call__(self, role, rawtext, text, lineno, inliner, - options={}, content=[]): + # ----------------------------------------------------------------------------- + def __call__(self, role, rawtext, text, lineno, inliner, + options={}, content=[]): - roles.set_classes(options) + roles.set_classes(options) - parts = utils.unescape(text).split(' ', 1) - uri = '{}/Documentation/{}/{}'.format(self.wiki_root, self.wiki_doc_version, - parts[0]) - text = parts[1] + parts = utils.unescape(text).split(' ', 1) + uri = '{}/Documentation/{}/{}'.format(self.wiki_root, self.wiki_doc_version, + parts[0]) + text = parts[1] - node = nodes.reference(rawtext, text, refuri=uri, **options) - return [node], [] + node = nodes.reference(rawtext, text, refuri=uri, **options) + return [node], [] -#=============================================================================== +# =============================================================================== class ClassModuleClassDocumenter(autodoc.ClassDocumenter): - #----------------------------------------------------------------------------- - def resolve_name(self, *args): - module, attrs = super().resolve_name(*args) - module = module.split('.') - if module[-1] == attrs[0]: - del module[-1] - return '.'.join(module), attrs + # ----------------------------------------------------------------------------- + def resolve_name(self, *args): + module, attrs = super().resolve_name(*args) + module = module.split('.') + if module[-1] == attrs[0]: + del module[-1] + return '.'.join(module), attrs -#%%% Site customizations %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +# %%% Site customizations %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def setup(app): - app.add_autodocumenter(ClassModuleClassDocumenter) - app.add_role('wikidoc', WikidocRole()) + app.add_autodocumenter(ClassModuleClassDocumenter) + app.add_role('wikidoc', WikidocRole()) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- autoclass_content = 'both' autodoc_member_order = 'groupwise' @@ -75,35 +75,35 @@ def setup(app): args, pargs = parser.parse_known_args() for d in args.defs: - setattr(args, *d.split('=', 1)) + setattr(args, *d.split('=', 1)) setattr(WikidocRole, 'wiki_doc_version', args.wikidoc_version) -#%%% General configuration %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +# %%% General configuration %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] intersphinx_mapping = { - 'python': ('https://docs.python.org/%i.%i' % sys.version_info[:2], None), - 'github': ('http://jacquev6.github.io/PyGithub/v1', None), + 'python': ('https://docs.python.org/%i.%i' % sys.version_info[:2], None), + 'github': ('http://jacquev6.github.io/PyGithub/v1', None), } try: - import git - intersphinx_mapping['git'] = ('https://pythonhosted.org/GitPython/%s' % git.__version__.split(' ')[0], None) + import git + intersphinx_mapping['git'] = ('https://pythonhosted.org/GitPython/%s' % git.__version__.split(' ')[0], None) except: - pass + pass # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -120,44 +120,44 @@ def setup(app): # The short X.Y version. version = args.version # The full version, including alpha/beta/rc tags. -release = args.version # FIXME? +release = args.version # FIXME? # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] -#%%% Options for HTML output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +# %%% Options for HTML output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -166,119 +166,119 @@ def setup(app): # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +# html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'SlicerWizarddoc' -#%%% Options for LaTeX output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +# %%% Options for LaTeX output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'SlicerWizard.tex', 'SlicerWizard Documentation', - author, 'manual'), + ('index', 'SlicerWizard.tex', 'SlicerWizard Documentation', + author, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True -#%%% Options for manual page output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +# %%% Options for manual page output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). @@ -288,31 +288,31 @@ def setup(app): ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False -#%%% Options for Texinfo output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +# %%% Options for Texinfo output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'SlicerWizard', 'SlicerWizard Documentation', - author, 'SlicerWizard', 'One line description of project.', - 'Miscellaneous'), + ('index', 'SlicerWizard', 'SlicerWizard Documentation', + author, 'SlicerWizard', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' -#%%% Options for Epub output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +# %%% Options for Epub output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # Bibliographic Dublin Core info. epub_title = 'SlicerWizard' @@ -322,34 +322,34 @@ def setup(app): # The language of the text. It defaults to the language option # or en if the language is not set. -#epub_language = '' +# epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' +# epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. -#epub_identifier = '' +# epub_identifier = '' # A unique identification for the text. -#epub_uid = '' +# epub_uid = '' # A tuple containing the cover image and cover page html template filenames. -#epub_cover = () +# epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_pre_files = [] +# epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_post_files = [] +# epub_post_files = [] # A list of files that should not be packed into the epub file. -#epub_exclude_files = [] +# epub_exclude_files = [] # The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 +# epub_tocdepth = 3 # Allow duplicate toc entries. -#epub_tocdup = True +# epub_tocdup = True diff --git a/Utilities/Scripts/genqrc.py b/Utilities/Scripts/genqrc.py index 40a8d1c55e8..706678b8c54 100755 --- a/Utilities/Scripts/genqrc.py +++ b/Utilities/Scripts/genqrc.py @@ -5,95 +5,95 @@ import sys -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def writeFile(path, content): - # Test if file already contains desired content - if os.path.exists(path): - try: - with open(path) as f: - if f.read() == content: - return + # Test if file already contains desired content + if os.path.exists(path): + try: + with open(path) as f: + if f.read() == content: + return - except: - pass + except: + pass - # Write file - with open(path, "wt") as f: - f.write(content) + # Write file + with open(path, "wt") as f: + f.write(content) -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def addFile(path): - name = os.path.basename(path) - return [f" {path}"] + name = os.path.basename(path) + return [f" {path}"] -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def buildContent(root, path): - dirs = [] - out = [" " % path] + dirs = [] + out = [" " % path] - for entry in os.listdir(os.path.join(root, path)): - full_entry = os.path.join(root, path, entry) + for entry in os.listdir(os.path.join(root, path)): + full_entry = os.path.join(root, path, entry) - if os.path.isdir(full_entry): - dirs.append(os.path.join(path, entry)) + if os.path.isdir(full_entry): + dirs.append(os.path.join(path, entry)) - else: - ext = os.path.splitext(entry)[1].lower() + else: + ext = os.path.splitext(entry)[1].lower() - if ext == ".png" or ext == ".svg": - out += addFile(full_entry) + if ext == ".png" or ext == ".svg": + out += addFile(full_entry) - out += [" ", ""] + out += [" ", ""] - for d in dirs: - out += buildContent(root, d) + for d in dirs: + out += buildContent(root, d) - return out + return out -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- def main(argv): - parser = argparse.ArgumentParser(description="PythonQt Resource Compiler") + parser = argparse.ArgumentParser(description="PythonQt Resource Compiler") - parser.add_argument("-o", dest="out_path", metavar="PATH", default="-", - help="location to which to write the output .qrc file" - " (default=stdout)") - parser.add_argument("resource_directories", nargs="+", - help="list of directories containing resource files") + parser.add_argument("-o", dest="out_path", metavar="PATH", default="-", + help="location to which to write the output .qrc file" + " (default=stdout)") + parser.add_argument("resource_directories", nargs="+", + help="list of directories containing resource files") - args = parser.parse_args(argv) + args = parser.parse_args(argv) - qrc_content = [ - "", - "", - "", - "", - ""] + qrc_content = [ + "", + "", + "", + "", + ""] - for path in args.resource_directories: - path = os.path.dirname(os.path.join(path, '.')) # remove trailing '/' - qrc_content += buildContent(os.path.dirname(path), os.path.basename(path)) + for path in args.resource_directories: + path = os.path.dirname(os.path.join(path, '.')) # remove trailing '/' + qrc_content += buildContent(os.path.dirname(path), os.path.basename(path)) - qrc_content += [""] + qrc_content += [""] - qrc_content = "\n".join(qrc_content) + "\n" + qrc_content = "\n".join(qrc_content) + "\n" - if args.out_path == "-": - sys.stdout.write(qrc_content) + if args.out_path == "-": + sys.stdout.write(qrc_content) - else: - writeFile(args.out_path, qrc_content) + else: + writeFile(args.out_path, qrc_content) -#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% if __name__ == "__main__": - main(sys.argv[1:]) + main(sys.argv[1:]) diff --git a/Utilities/Scripts/qrcc.py b/Utilities/Scripts/qrcc.py index 3f23b2a4a19..a31317964fe 100755 --- a/Utilities/Scripts/qrcc.py +++ b/Utilities/Scripts/qrcc.py @@ -30,69 +30,69 @@ def qCleanupResources(): def compileResources(in_path, out_file, args): - # Determine command line for rcc - if sys.platform.startswith("win") or sys.platform.startswith("cygwin"): - # On Windows, rcc performs LF -> CRLF conversion when writing to stdout, - # resulting in corrupt data that can cause Qt to crash when loading the - # resources. To work around this, we must instead write to a temporary - # file. - if args.out_path == "-": - import tempfile - tmp_file, tmp_path = tempfile.mkstemp() - os.close(tmp_file) + # Determine command line for rcc + if sys.platform.startswith("win") or sys.platform.startswith("cygwin"): + # On Windows, rcc performs LF -> CRLF conversion when writing to stdout, + # resulting in corrupt data that can cause Qt to crash when loading the + # resources. To work around this, we must instead write to a temporary + # file. + if args.out_path == "-": + import tempfile + tmp_file, tmp_path = tempfile.mkstemp() + os.close(tmp_file) + + else: + tmp_path = args.out_path + ".rcctmp" + + command = [args.rcc, "-binary", "-o", tmp_path, in_path] else: - tmp_path = args.out_path + ".rcctmp" - - command = [args.rcc, "-binary", "-o", tmp_path, in_path] - - else: - tmp_path = None - command = [args.rcc, "-binary", in_path] + tmp_path = None + command = [args.rcc, "-binary", in_path] - # Run rcc - proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=sys.stderr) - data, err = proc.communicate() + # Run rcc + proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=sys.stderr) + data, err = proc.communicate() - # Check that rcc ran successfully - if proc.returncode != 0: - sys.exit(proc.returncode) + # Check that rcc ran successfully + if proc.returncode != 0: + sys.exit(proc.returncode) - # Read data, if using a temporary file (see above) - if tmp_path is not None: - with open(tmp_path,"rb") as tmp_file: - data = tmp_file.read() + # Read data, if using a temporary file (see above) + if tmp_path is not None: + with open(tmp_path, "rb") as tmp_file: + data = tmp_file.read() - os.remove(tmp_path) + os.remove(tmp_path) - _data = base64.encodebytes(data).rstrip().decode() + _data = base64.encodebytes(data).rstrip().decode() - # Write output script - out_file.write(_header) - out_file.write(_data) - out_file.write(_footer) + # Write output script + out_file.write(_header) + out_file.write(_data) + out_file.write(_footer) def main(argv): - parser = argparse.ArgumentParser(description="PythonQt Resource Compiler") + parser = argparse.ArgumentParser(description="PythonQt Resource Compiler") - parser.add_argument("--rcc", default="rcc", - help="location of the Qt resource compiler executable") - parser.add_argument("-o", "--output", dest="out_path", metavar="PATH", - default="-", - help="location to which to write the output Python" - " script (default=stdout)") - parser.add_argument("in_path", metavar="resource_script", - help="input resource script to compile") + parser.add_argument("--rcc", default="rcc", + help="location of the Qt resource compiler executable") + parser.add_argument("-o", "--output", dest="out_path", metavar="PATH", + default="-", + help="location to which to write the output Python" + " script (default=stdout)") + parser.add_argument("in_path", metavar="resource_script", + help="input resource script to compile") - args = parser.parse_args(argv) + args = parser.parse_args(argv) - if args.out_path == "-": - compileResources(args.in_path, sys.stdout, args) - else: - with open(args.out_path, "w") as f: - compileResources(args.in_path, f, args) + if args.out_path == "-": + compileResources(args.in_path, sys.stdout, args) + else: + with open(args.out_path, "w") as f: + compileResources(args.in_path, f, args) if __name__ == "__main__": - main(sys.argv[1:]) + main(sys.argv[1:]) diff --git a/Utilities/Templates/Modules/Scripted/TemplateKey.py b/Utilities/Templates/Modules/Scripted/TemplateKey.py index 19ac01bce59..78604a4ccd5 100644 --- a/Utilities/Templates/Modules/Scripted/TemplateKey.py +++ b/Utilities/Templates/Modules/Scripted/TemplateKey.py @@ -13,29 +13,29 @@ # class TemplateKey(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 = "TemplateKey" # TODO: make this more human readable by adding spaces - self.parent.categories = ["Examples"] # TODO: set categories (folders where the module shows up in the module selector) - self.parent.dependencies = [] # TODO: add here list of module names that this module requires - self.parent.contributors = ["John Doe (AnyWare Corp.)"] # TODO: replace with "Firstname Lastname (Organization)" - # TODO: update with short description of the module and a link to online module documentation - self.parent.helpText = """ + """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 = "TemplateKey" # TODO: make this more human readable by adding spaces + self.parent.categories = ["Examples"] # TODO: set categories (folders where the module shows up in the module selector) + self.parent.dependencies = [] # TODO: add here list of module names that this module requires + self.parent.contributors = ["John Doe (AnyWare Corp.)"] # TODO: replace with "Firstname Lastname (Organization)" + # TODO: update with short description of the module and a link to online module documentation + self.parent.helpText = """ This is an example of scripted loadable module bundled in an extension. See more information in module documentation. """ - # TODO: replace with organization, grant and thanks - self.parent.acknowledgementText = """ + # TODO: replace with organization, grant and thanks + self.parent.acknowledgementText = """ This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc., Andras Lasso, PerkLab, and Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. """ - # Additional initialization step after application startup is complete - slicer.app.connect("startupCompleted()", registerSampleData) + # Additional initialization step after application startup is complete + slicer.app.connect("startupCompleted()", registerSampleData) # @@ -43,49 +43,49 @@ def __init__(self, parent): # def registerSampleData(): - """ - Add data sets to Sample Data module. - """ - # It is always recommended to provide sample data for users to make it easy to try the module, - # but if no sample data is available then this method (and associated startupCompeted signal connection) can be removed. - - import SampleData - iconsPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons') - - # To ensure that the source code repository remains small (can be downloaded and installed quickly) - # it is recommended to store data sets that are larger than a few MB in a Github release. - - # TemplateKey1 - SampleData.SampleDataLogic.registerCustomSampleDataSource( - # Category and sample name displayed in Sample Data module - category='TemplateKey', - sampleName='TemplateKey1', - # Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder. - # It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single". - thumbnailFileName=os.path.join(iconsPath, 'TemplateKey1.png'), - # Download URL and target file name - uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95", - fileNames='TemplateKey1.nrrd', - # Checksum to ensure file integrity. Can be computed by this command: - # import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) - checksums = 'SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95', - # This node name will be used when the data set is loaded - nodeNames='TemplateKey1' - ) - - # TemplateKey2 - SampleData.SampleDataLogic.registerCustomSampleDataSource( - # Category and sample name displayed in Sample Data module - category='TemplateKey', - sampleName='TemplateKey2', - thumbnailFileName=os.path.join(iconsPath, 'TemplateKey2.png'), - # Download URL and target file name - uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97", - fileNames='TemplateKey2.nrrd', - checksums = 'SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97', - # This node name will be used when the data set is loaded - nodeNames='TemplateKey2' - ) + """ + Add data sets to Sample Data module. + """ + # It is always recommended to provide sample data for users to make it easy to try the module, + # but if no sample data is available then this method (and associated startupCompeted signal connection) can be removed. + + import SampleData + iconsPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons') + + # To ensure that the source code repository remains small (can be downloaded and installed quickly) + # it is recommended to store data sets that are larger than a few MB in a Github release. + + # TemplateKey1 + SampleData.SampleDataLogic.registerCustomSampleDataSource( + # Category and sample name displayed in Sample Data module + category='TemplateKey', + sampleName='TemplateKey1', + # Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder. + # It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single". + thumbnailFileName=os.path.join(iconsPath, 'TemplateKey1.png'), + # Download URL and target file name + uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95", + fileNames='TemplateKey1.nrrd', + # Checksum to ensure file integrity. Can be computed by this command: + # import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) + checksums='SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95', + # This node name will be used when the data set is loaded + nodeNames='TemplateKey1' + ) + + # TemplateKey2 + SampleData.SampleDataLogic.registerCustomSampleDataSource( + # Category and sample name displayed in Sample Data module + category='TemplateKey', + sampleName='TemplateKey2', + thumbnailFileName=os.path.join(iconsPath, 'TemplateKey2.png'), + # Download URL and target file name + uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97", + fileNames='TemplateKey2.nrrd', + checksums='SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97', + # This node name will be used when the data set is loaded + nodeNames='TemplateKey2' + ) # @@ -93,196 +93,196 @@ def registerSampleData(): # class TemplateKeyWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): - """Uses ScriptedLoadableModuleWidget base class, available at: - https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py - """ - - def __init__(self, parent=None): - """ - Called when the user opens the module the first time and the widget is initialized. - """ - ScriptedLoadableModuleWidget.__init__(self, parent) - VTKObservationMixin.__init__(self) # needed for parameter node observation - self.logic = None - self._parameterNode = None - self._updatingGUIFromParameterNode = False - - def setup(self): - """ - Called when the user opens the module the first time and the widget is initialized. - """ - ScriptedLoadableModuleWidget.setup(self) - - # Load widget from .ui file (created by Qt Designer). - # Additional widgets can be instantiated manually and added to self.layout. - uiWidget = slicer.util.loadUI(self.resourcePath('UI/TemplateKey.ui')) - self.layout.addWidget(uiWidget) - self.ui = slicer.util.childWidgetVariables(uiWidget) - - # Set scene in MRML widgets. Make sure that in Qt designer the top-level qMRMLWidget's - # "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to each MRML widget's. - # "setMRMLScene(vtkMRMLScene*)" slot. - uiWidget.setMRMLScene(slicer.mrmlScene) - - # Create logic class. Logic implements all computations that should be possible to run - # in batch mode, without a graphical user interface. - self.logic = TemplateKeyLogic() - - # Connections - - # These connections ensure that we update parameter node when scene is closed - self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) - self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) - - # These connections ensure that whenever user changes some settings on the GUI, that is saved in the MRML scene - # (in the selected parameter node). - self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) - self.ui.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) - self.ui.imageThresholdSliderWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI) - self.ui.invertOutputCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI) - self.ui.invertedOutputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) - - # Buttons - self.ui.applyButton.connect('clicked(bool)', self.onApplyButton) - - # Make sure parameter node is initialized (needed for module reload) - self.initializeParameterNode() - - def cleanup(self): - """ - Called when the application closes and the module widget is destroyed. - """ - self.removeObservers() - - def enter(self): - """ - Called each time the user opens this module. - """ - # Make sure parameter node exists and observed - self.initializeParameterNode() - - def exit(self): - """ - Called each time the user opens a different module. - """ - # Do not react to parameter node changes (GUI wlil be updated when the user enters into the module) - self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) - - def onSceneStartClose(self, caller, event): - """ - Called just before the scene is closed. - """ - # Parameter node will be reset, do not use it anymore - self.setParameterNode(None) - - def onSceneEndClose(self, caller, event): - """ - Called just after the scene is closed. - """ - # If this module is shown while the scene is closed then recreate a new parameter node immediately - if self.parent.isEntered: - self.initializeParameterNode() - - def initializeParameterNode(self): - """ - Ensure parameter node exists and observed. - """ - # Parameter node stores all user choices in parameter values, node selections, etc. - # so that when the scene is saved and reloaded, these settings are restored. - - self.setParameterNode(self.logic.getParameterNode()) - - # Select default input nodes if nothing is selected yet to save a few clicks for the user - if not self._parameterNode.GetNodeReference("InputVolume"): - firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode") - if firstVolumeNode: - self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID()) - - def setParameterNode(self, inputParameterNode): - """ - Set and observe parameter node. - Observation is needed because when the parameter node is changed then the GUI must be updated immediately. - """ - - if inputParameterNode: - self.logic.setDefaultParameters(inputParameterNode) - - # Unobserve previously selected parameter node and add an observer to the newly selected. - # Changes of parameter node are observed so that whenever parameters are changed by a script or any other module - # those are reflected immediately in the GUI. - if self._parameterNode is not None: - self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) - self._parameterNode = inputParameterNode - if self._parameterNode is not None: - self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) - - # Initial GUI update - self.updateGUIFromParameterNode() - - def updateGUIFromParameterNode(self, caller=None, event=None): - """ - This method is called whenever parameter node is changed. - The module GUI is updated to show the current state of the parameter node. - """ - - if self._parameterNode is None or self._updatingGUIFromParameterNode: - return - - # Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop) - self._updatingGUIFromParameterNode = True - - # Update node selectors and sliders - self.ui.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume")) - self.ui.outputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolume")) - self.ui.invertedOutputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolumeInverse")) - self.ui.imageThresholdSliderWidget.value = float(self._parameterNode.GetParameter("Threshold")) - self.ui.invertOutputCheckBox.checked = (self._parameterNode.GetParameter("Invert") == "true") - - # Update buttons states and tooltips - if self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetNodeReference("OutputVolume"): - self.ui.applyButton.toolTip = "Compute output volume" - self.ui.applyButton.enabled = True - else: - self.ui.applyButton.toolTip = "Select input and output volume nodes" - self.ui.applyButton.enabled = False - - # All the GUI updates are done - self._updatingGUIFromParameterNode = False - - def updateParameterNodeFromGUI(self, caller=None, event=None): - """ - This method is called when the user makes any change in the GUI. - The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded). - """ - - if self._parameterNode is None or self._updatingGUIFromParameterNode: - return - - wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch - - self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID) - self._parameterNode.SetNodeReferenceID("OutputVolume", self.ui.outputSelector.currentNodeID) - self._parameterNode.SetParameter("Threshold", str(self.ui.imageThresholdSliderWidget.value)) - self._parameterNode.SetParameter("Invert", "true" if self.ui.invertOutputCheckBox.checked else "false") - self._parameterNode.SetNodeReferenceID("OutputVolumeInverse", self.ui.invertedOutputSelector.currentNodeID) - - self._parameterNode.EndModify(wasModified) - - def onApplyButton(self): - """ - Run processing when user clicks "Apply" button. - """ - with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): - - # Compute output - self.logic.process(self.ui.inputSelector.currentNode(), self.ui.outputSelector.currentNode(), - self.ui.imageThresholdSliderWidget.value, self.ui.invertOutputCheckBox.checked) - - # Compute inverted output (if needed) - if self.ui.invertedOutputSelector.currentNode(): - # If additional output volume is selected then result with inverted threshold is written there - self.logic.process(self.ui.inputSelector.currentNode(), self.ui.invertedOutputSelector.currentNode(), - self.ui.imageThresholdSliderWidget.value, not self.ui.invertOutputCheckBox.checked, showResult=False) + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def __init__(self, parent=None): + """ + Called when the user opens the module the first time and the widget is initialized. + """ + ScriptedLoadableModuleWidget.__init__(self, parent) + VTKObservationMixin.__init__(self) # needed for parameter node observation + self.logic = None + self._parameterNode = None + self._updatingGUIFromParameterNode = False + + def setup(self): + """ + Called when the user opens the module the first time and the widget is initialized. + """ + ScriptedLoadableModuleWidget.setup(self) + + # Load widget from .ui file (created by Qt Designer). + # Additional widgets can be instantiated manually and added to self.layout. + uiWidget = slicer.util.loadUI(self.resourcePath('UI/TemplateKey.ui')) + self.layout.addWidget(uiWidget) + self.ui = slicer.util.childWidgetVariables(uiWidget) + + # Set scene in MRML widgets. Make sure that in Qt designer the top-level qMRMLWidget's + # "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to each MRML widget's. + # "setMRMLScene(vtkMRMLScene*)" slot. + uiWidget.setMRMLScene(slicer.mrmlScene) + + # Create logic class. Logic implements all computations that should be possible to run + # in batch mode, without a graphical user interface. + self.logic = TemplateKeyLogic() + + # Connections + + # These connections ensure that we update parameter node when scene is closed + self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) + self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) + + # These connections ensure that whenever user changes some settings on the GUI, that is saved in the MRML scene + # (in the selected parameter node). + self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) + self.ui.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) + self.ui.imageThresholdSliderWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI) + self.ui.invertOutputCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI) + self.ui.invertedOutputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI) + + # Buttons + self.ui.applyButton.connect('clicked(bool)', self.onApplyButton) + + # Make sure parameter node is initialized (needed for module reload) + self.initializeParameterNode() + + def cleanup(self): + """ + Called when the application closes and the module widget is destroyed. + """ + self.removeObservers() + + def enter(self): + """ + Called each time the user opens this module. + """ + # Make sure parameter node exists and observed + self.initializeParameterNode() + + def exit(self): + """ + Called each time the user opens a different module. + """ + # Do not react to parameter node changes (GUI wlil be updated when the user enters into the module) + self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) + + def onSceneStartClose(self, caller, event): + """ + Called just before the scene is closed. + """ + # Parameter node will be reset, do not use it anymore + self.setParameterNode(None) + + def onSceneEndClose(self, caller, event): + """ + Called just after the scene is closed. + """ + # If this module is shown while the scene is closed then recreate a new parameter node immediately + if self.parent.isEntered: + self.initializeParameterNode() + + def initializeParameterNode(self): + """ + Ensure parameter node exists and observed. + """ + # Parameter node stores all user choices in parameter values, node selections, etc. + # so that when the scene is saved and reloaded, these settings are restored. + + self.setParameterNode(self.logic.getParameterNode()) + + # Select default input nodes if nothing is selected yet to save a few clicks for the user + if not self._parameterNode.GetNodeReference("InputVolume"): + firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode") + if firstVolumeNode: + self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID()) + + def setParameterNode(self, inputParameterNode): + """ + Set and observe parameter node. + Observation is needed because when the parameter node is changed then the GUI must be updated immediately. + """ + + if inputParameterNode: + self.logic.setDefaultParameters(inputParameterNode) + + # Unobserve previously selected parameter node and add an observer to the newly selected. + # Changes of parameter node are observed so that whenever parameters are changed by a script or any other module + # those are reflected immediately in the GUI. + if self._parameterNode is not None: + self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) + self._parameterNode = inputParameterNode + if self._parameterNode is not None: + self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode) + + # Initial GUI update + self.updateGUIFromParameterNode() + + def updateGUIFromParameterNode(self, caller=None, event=None): + """ + This method is called whenever parameter node is changed. + The module GUI is updated to show the current state of the parameter node. + """ + + if self._parameterNode is None or self._updatingGUIFromParameterNode: + return + + # Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop) + self._updatingGUIFromParameterNode = True + + # Update node selectors and sliders + self.ui.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume")) + self.ui.outputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolume")) + self.ui.invertedOutputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolumeInverse")) + self.ui.imageThresholdSliderWidget.value = float(self._parameterNode.GetParameter("Threshold")) + self.ui.invertOutputCheckBox.checked = (self._parameterNode.GetParameter("Invert") == "true") + + # Update buttons states and tooltips + if self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetNodeReference("OutputVolume"): + self.ui.applyButton.toolTip = "Compute output volume" + self.ui.applyButton.enabled = True + else: + self.ui.applyButton.toolTip = "Select input and output volume nodes" + self.ui.applyButton.enabled = False + + # All the GUI updates are done + self._updatingGUIFromParameterNode = False + + def updateParameterNodeFromGUI(self, caller=None, event=None): + """ + This method is called when the user makes any change in the GUI. + The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded). + """ + + if self._parameterNode is None or self._updatingGUIFromParameterNode: + return + + wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch + + self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID) + self._parameterNode.SetNodeReferenceID("OutputVolume", self.ui.outputSelector.currentNodeID) + self._parameterNode.SetParameter("Threshold", str(self.ui.imageThresholdSliderWidget.value)) + self._parameterNode.SetParameter("Invert", "true" if self.ui.invertOutputCheckBox.checked else "false") + self._parameterNode.SetNodeReferenceID("OutputVolumeInverse", self.ui.invertedOutputSelector.currentNodeID) + + self._parameterNode.EndModify(wasModified) + + def onApplyButton(self): + """ + Run processing when user clicks "Apply" button. + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + + # Compute output + self.logic.process(self.ui.inputSelector.currentNode(), self.ui.outputSelector.currentNode(), + self.ui.imageThresholdSliderWidget.value, self.ui.invertOutputCheckBox.checked) + + # Compute inverted output (if needed) + if self.ui.invertedOutputSelector.currentNode(): + # If additional output volume is selected then result with inverted threshold is written there + self.logic.process(self.ui.inputSelector.currentNode(), self.ui.invertedOutputSelector.currentNode(), + self.ui.imageThresholdSliderWidget.value, not self.ui.invertOutputCheckBox.checked, showResult=False) # @@ -290,61 +290,61 @@ def onApplyButton(self): # class TemplateKeyLogic(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): - """ - Called when the logic class is instantiated. Can be used for initializing member variables. - """ - ScriptedLoadableModuleLogic.__init__(self) - - def setDefaultParameters(self, parameterNode): - """ - Initialize parameter node with default settings. - """ - if not parameterNode.GetParameter("Threshold"): - parameterNode.SetParameter("Threshold", "100.0") - if not parameterNode.GetParameter("Invert"): - parameterNode.SetParameter("Invert", "false") - - def process(self, inputVolume, outputVolume, imageThreshold, invert=False, showResult=True): - """ - Run the processing algorithm. - Can be used without GUI widget. - :param inputVolume: volume to be thresholded - :param outputVolume: thresholding result - :param imageThreshold: values above/below this threshold will be set to 0 - :param invert: if True then values above the threshold will be set to 0, otherwise values below are set to 0 - :param showResult: show output volume in slice viewers - """ - - if not inputVolume or not outputVolume: - raise ValueError("Input or output volume is invalid") - - import time - startTime = time.time() - logging.info('Processing started') - - # Compute the thresholded output volume using the "Threshold Scalar Volume" CLI module - cliParams = { - 'InputVolume': inputVolume.GetID(), - 'OutputVolume': outputVolume.GetID(), - 'ThresholdValue' : imageThreshold, - 'ThresholdType' : 'Above' if invert else 'Below' - } - cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True, update_display=showResult) - # We don't need the CLI module node anymore, remove it to not clutter the scene with it - slicer.mrmlScene.RemoveNode(cliNode) - - stopTime = time.time() - logging.info(f'Processing completed in {stopTime-startTime:.2f} seconds') + """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): + """ + Called when the logic class is instantiated. Can be used for initializing member variables. + """ + ScriptedLoadableModuleLogic.__init__(self) + + def setDefaultParameters(self, parameterNode): + """ + Initialize parameter node with default settings. + """ + if not parameterNode.GetParameter("Threshold"): + parameterNode.SetParameter("Threshold", "100.0") + if not parameterNode.GetParameter("Invert"): + parameterNode.SetParameter("Invert", "false") + + def process(self, inputVolume, outputVolume, imageThreshold, invert=False, showResult=True): + """ + Run the processing algorithm. + Can be used without GUI widget. + :param inputVolume: volume to be thresholded + :param outputVolume: thresholding result + :param imageThreshold: values above/below this threshold will be set to 0 + :param invert: if True then values above the threshold will be set to 0, otherwise values below are set to 0 + :param showResult: show output volume in slice viewers + """ + + if not inputVolume or not outputVolume: + raise ValueError("Input or output volume is invalid") + + import time + startTime = time.time() + logging.info('Processing started') + + # Compute the thresholded output volume using the "Threshold Scalar Volume" CLI module + cliParams = { + 'InputVolume': inputVolume.GetID(), + 'OutputVolume': outputVolume.GetID(), + 'ThresholdValue': imageThreshold, + 'ThresholdType': 'Above' if invert else 'Below' + } + cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True, update_display=showResult) + # We don't need the CLI module node anymore, remove it to not clutter the scene with it + slicer.mrmlScene.RemoveNode(cliNode) + + stopTime = time.time() + logging.info(f'Processing completed in {stopTime-startTime:.2f} seconds') # @@ -352,65 +352,65 @@ def process(self, inputVolume, outputVolume, imageThreshold, invert=False, showR # class TemplateKeyTest(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() - - def runTest(self): - """Run as few or as many tests as needed here. """ - self.setUp() - self.test_TemplateKey1() - - def test_TemplateKey1(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. + 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 """ - self.delayDisplay("Starting the test") + def setUp(self): + """ Do whatever is needed to reset the state - typically a scene clear will be enough. + """ + slicer.mrmlScene.Clear() - # Get/create input data + def runTest(self): + """Run as few or as many tests as needed here. + """ + self.setUp() + self.test_TemplateKey1() - import SampleData - registerSampleData() - inputVolume = SampleData.downloadSample('TemplateKey1') - self.delayDisplay('Loaded test data set') + def test_TemplateKey1(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") + + # Get/create input data + + import SampleData + registerSampleData() + inputVolume = SampleData.downloadSample('TemplateKey1') + self.delayDisplay('Loaded test data set') - inputScalarRange = inputVolume.GetImageData().GetScalarRange() - self.assertEqual(inputScalarRange[0], 0) - self.assertEqual(inputScalarRange[1], 695) + inputScalarRange = inputVolume.GetImageData().GetScalarRange() + self.assertEqual(inputScalarRange[0], 0) + self.assertEqual(inputScalarRange[1], 695) - outputVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") - threshold = 100 + outputVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") + threshold = 100 - # Test the module logic + # Test the module logic - logic = TemplateKeyLogic() + logic = TemplateKeyLogic() - # Test algorithm with non-inverted threshold - logic.process(inputVolume, outputVolume, threshold, True) - outputScalarRange = outputVolume.GetImageData().GetScalarRange() - self.assertEqual(outputScalarRange[0], inputScalarRange[0]) - self.assertEqual(outputScalarRange[1], threshold) + # Test algorithm with non-inverted threshold + logic.process(inputVolume, outputVolume, threshold, True) + outputScalarRange = outputVolume.GetImageData().GetScalarRange() + self.assertEqual(outputScalarRange[0], inputScalarRange[0]) + self.assertEqual(outputScalarRange[1], threshold) - # Test algorithm with inverted threshold - logic.process(inputVolume, outputVolume, threshold, False) - outputScalarRange = outputVolume.GetImageData().GetScalarRange() - self.assertEqual(outputScalarRange[0], inputScalarRange[0]) - self.assertEqual(outputScalarRange[1], inputScalarRange[1]) + # Test algorithm with inverted threshold + logic.process(inputVolume, outputVolume, threshold, False) + outputScalarRange = outputVolume.GetImageData().GetScalarRange() + self.assertEqual(outputScalarRange[0], inputScalarRange[0]) + self.assertEqual(outputScalarRange[1], inputScalarRange[1]) - self.delayDisplay('Test passed') + self.delayDisplay('Test passed') diff --git a/Utilities/Templates/Modules/ScriptedCLI/TemplateKey.py b/Utilities/Templates/Modules/ScriptedCLI/TemplateKey.py index 6176273552f..15e5d067030 100755 --- a/Utilities/Templates/Modules/ScriptedCLI/TemplateKey.py +++ b/Utilities/Templates/Modules/ScriptedCLI/TemplateKey.py @@ -22,12 +22,12 @@ def main(input, sigma, output): image = caster.Execute(image) writer = sitk.ImageFileWriter() - writer.SetFileName (output) - writer.Execute (image) + writer.SetFileName(output) + writer.Execute(image) if __name__ == "__main__": - if len (sys.argv) < 4: + if len(sys.argv) < 4: print("Usage: TemplateKey ") - sys.exit (1) + sys.exit(1) main(sys.argv[1], float(sys.argv[2]), sys.argv[3]) diff --git a/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKey.py b/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKey.py index 10360cb74d0..83f1b8d58c5 100644 --- a/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKey.py +++ b/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKey.py @@ -7,133 +7,133 @@ class SegmentEditorTemplateKey(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 = "SegmentEditorTemplateKey" - self.parent.categories = ["Segmentation"] - self.parent.dependencies = ["Segmentations"] - self.parent.contributors = ["Andras Lasso (PerkLab)"] - self.parent.hidden = True - self.parent.helpText = "This hidden module registers the segment editor effect" - self.parent.helpText += self.getDefaultModuleDocumentationLink() - self.parent.acknowledgementText = "Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details." - slicer.app.connect("startupCompleted()", self.registerEditorEffect) - - def registerEditorEffect(self): - import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects - instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None) - effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__+'Lib/SegmentEditorEffect.py') - instance.setPythonSource(effectFilename.replace('\\','/')) - instance.self().register() - - -class SegmentEditorTemplateKeyTest(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. + """Uses ScriptedLoadableModule base class, available at: + https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ - slicer.mrmlScene.Clear(0) - def runTest(self): - """Run as few or as many tests as needed here. - """ - self.setUp() - self.test_TemplateKey1() + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = "SegmentEditorTemplateKey" + self.parent.categories = ["Segmentation"] + self.parent.dependencies = ["Segmentations"] + self.parent.contributors = ["Andras Lasso (PerkLab)"] + self.parent.hidden = True + self.parent.helpText = "This hidden module registers the segment editor effect" + self.parent.helpText += self.getDefaultModuleDocumentationLink() + self.parent.acknowledgementText = "Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details." + slicer.app.connect("startupCompleted()", self.registerEditorEffect) + + def registerEditorEffect(self): + import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects + instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None) + effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__ + 'Lib/SegmentEditorEffect.py') + instance.setPythonSource(effectFilename.replace('\\', '/')) + instance.self().register() + - def test_TemplateKey1(self): +class SegmentEditorTemplateKeyTest(ScriptedLoadableModuleTest): """ - Basic automated test of the segmentation method: - - Create segmentation by placing sphere-shaped seeds - - Run segmentation - - Verify results using segment statistics - The test can be executed from SelfTests module (test name: SegmentEditorTemplateKey) + 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 """ - self.delayDisplay("Starting test_TemplateKey1") - - import vtkSegmentationCorePython as vtkSegmentationCore - import SampleData - from SegmentStatistics import SegmentStatisticsLogic - - ################################## - self.delayDisplay("Load master volume") - - masterVolumeNode = SampleData.downloadSample('MRBrainTumor1') - - ################################## - self.delayDisplay("Create segmentation containing a few spheres") - - segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') - segmentationNode.CreateDefaultDisplayNodes() - segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode) - - # Segments are defined by a list of: name and a list of sphere [radius, posX, posY, posZ] - segmentGeometries = [ - ['Tumor', [[10, -6,30,28]]], - ['Background', [[10, 0,65,22], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0,30,54], [12, 31, 33, 27], [17, -42, 30, 27], [6, -2,-17,71]]], - ['Air', [[10, 76,73,0], [15, -70,74,0]]] ] - for segmentGeometry in segmentGeometries: - segmentName = segmentGeometry[0] - appender = vtk.vtkAppendPolyData() - for sphere in segmentGeometry[1]: - sphereSource = vtk.vtkSphereSource() - sphereSource.SetRadius(sphere[0]) - sphereSource.SetCenter(sphere[1], sphere[2], sphere[3]) - appender.AddInputConnection(sphereSource.GetOutputPort()) - segment = vtkSegmentationCore.vtkSegment() - segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName)) - appender.Update() - segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), appender.GetOutput()) - segmentationNode.GetSegmentation().AddSegment(segment) - - ################################## - self.delayDisplay("Create segment editor") - - segmentEditorWidget = slicer.qMRMLSegmentEditorWidget() - segmentEditorWidget.show() - segmentEditorWidget.setMRMLScene(slicer.mrmlScene) - segmentEditorNode = slicer.vtkMRMLSegmentEditorNode() - slicer.mrmlScene.AddNode(segmentEditorNode) - segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode) - segmentEditorWidget.setSegmentationNode(segmentationNode) - segmentEditorWidget.setMasterVolumeNode(masterVolumeNode) - - ################################## - self.delayDisplay("Run segmentation") - segmentEditorWidget.setActiveEffectByName("TemplateKey") - effect = segmentEditorWidget.activeEffect() - effect.setParameter("ObjectScaleMm", 3.0) - effect.self().onApply() - - ################################## - self.delayDisplay("Make segmentation results nicely visible in 3D") - segmentationDisplayNode = segmentationNode.GetDisplayNode() - segmentationDisplayNode.SetSegmentVisibility("Air", False) - segmentationDisplayNode.SetSegmentOpacity3D("Background",0.5) - - ################################## - self.delayDisplay("Compute statistics") - - segStatLogic = SegmentStatisticsLogic() - segStatLogic.computeStatistics(segmentationNode, masterVolumeNode) - - # Export results to table (just to see all results) - resultsTableNode = slicer.vtkMRMLTableNode() - slicer.mrmlScene.AddNode(resultsTableNode) - segStatLogic.exportToTable(resultsTableNode) - segStatLogic.showTable(resultsTableNode) - - self.delayDisplay("Check a few numerical results") - self.assertEqual( round(segStatLogic.statistics["Tumor","LM volume cc"]), 16) - self.assertEqual( round(segStatLogic.statistics["Background","LM volume cc"]), 3010) - - self.delayDisplay('test_TemplateKey1 passed') + 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_TemplateKey1() + + def test_TemplateKey1(self): + """ + Basic automated test of the segmentation method: + - Create segmentation by placing sphere-shaped seeds + - Run segmentation + - Verify results using segment statistics + The test can be executed from SelfTests module (test name: SegmentEditorTemplateKey) + """ + + self.delayDisplay("Starting test_TemplateKey1") + + import vtkSegmentationCorePython as vtkSegmentationCore + import SampleData + from SegmentStatistics import SegmentStatisticsLogic + + ################################## + self.delayDisplay("Load master volume") + + masterVolumeNode = SampleData.downloadSample('MRBrainTumor1') + + ################################## + self.delayDisplay("Create segmentation containing a few spheres") + + segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode') + segmentationNode.CreateDefaultDisplayNodes() + segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode) + + # Segments are defined by a list of: name and a list of sphere [radius, posX, posY, posZ] + segmentGeometries = [ + ['Tumor', [[10, -6, 30, 28]]], + ['Background', [[10, 0, 65, 22], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 54], [12, 31, 33, 27], [17, -42, 30, 27], [6, -2, -17, 71]]], + ['Air', [[10, 76, 73, 0], [15, -70, 74, 0]]]] + for segmentGeometry in segmentGeometries: + segmentName = segmentGeometry[0] + appender = vtk.vtkAppendPolyData() + for sphere in segmentGeometry[1]: + sphereSource = vtk.vtkSphereSource() + sphereSource.SetRadius(sphere[0]) + sphereSource.SetCenter(sphere[1], sphere[2], sphere[3]) + appender.AddInputConnection(sphereSource.GetOutputPort()) + segment = vtkSegmentationCore.vtkSegment() + segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName)) + appender.Update() + segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), appender.GetOutput()) + segmentationNode.GetSegmentation().AddSegment(segment) + + ################################## + self.delayDisplay("Create segment editor") + + segmentEditorWidget = slicer.qMRMLSegmentEditorWidget() + segmentEditorWidget.show() + segmentEditorWidget.setMRMLScene(slicer.mrmlScene) + segmentEditorNode = slicer.vtkMRMLSegmentEditorNode() + slicer.mrmlScene.AddNode(segmentEditorNode) + segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode) + segmentEditorWidget.setSegmentationNode(segmentationNode) + segmentEditorWidget.setMasterVolumeNode(masterVolumeNode) + + ################################## + self.delayDisplay("Run segmentation") + segmentEditorWidget.setActiveEffectByName("TemplateKey") + effect = segmentEditorWidget.activeEffect() + effect.setParameter("ObjectScaleMm", 3.0) + effect.self().onApply() + + ################################## + self.delayDisplay("Make segmentation results nicely visible in 3D") + segmentationDisplayNode = segmentationNode.GetDisplayNode() + segmentationDisplayNode.SetSegmentVisibility("Air", False) + segmentationDisplayNode.SetSegmentOpacity3D("Background", 0.5) + + ################################## + self.delayDisplay("Compute statistics") + + segStatLogic = SegmentStatisticsLogic() + segStatLogic.computeStatistics(segmentationNode, masterVolumeNode) + + # Export results to table (just to see all results) + resultsTableNode = slicer.vtkMRMLTableNode() + slicer.mrmlScene.AddNode(resultsTableNode) + segStatLogic.exportToTable(resultsTableNode) + segStatLogic.showTable(resultsTableNode) + + self.delayDisplay("Check a few numerical results") + self.assertEqual(round(segStatLogic.statistics["Tumor", "LM volume cc"]), 16) + self.assertEqual(round(segStatLogic.statistics["Background", "LM volume cc"]), 3010) + + self.delayDisplay('test_TemplateKey1 passed') diff --git a/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKeyLib/SegmentEditorEffect.py b/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKeyLib/SegmentEditorEffect.py index ea4a54dd722..43c225703fd 100644 --- a/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKeyLib/SegmentEditorEffect.py +++ b/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKeyLib/SegmentEditorEffect.py @@ -10,123 +10,123 @@ class SegmentEditorEffect(AbstractScriptedSegmentEditorEffect): - """This effect uses Watershed algorithm to partition the input volume""" - - def __init__(self, scriptedEffect): - scriptedEffect.name = 'TemplateKey' - scriptedEffect.perSegment = False # this effect operates on all segments at once (not on a single selected segment) - scriptedEffect.requireSegments = True # this effect requires segment(s) existing in the segmentation - AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) - - def clone(self): - # It should not be necessary to modify this method - import qSlicerSegmentationsEditorEffectsPythonQt as effects - clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) - clonedEffect.setPythonSource(__file__.replace('\\','/')) - return clonedEffect - - def icon(self): - # It should not be necessary to modify this method - iconPath = os.path.join(os.path.dirname(__file__), 'SegmentEditorEffect.png') - if os.path.exists(iconPath): - return qt.QIcon(iconPath) - return qt.QIcon() - - def helpText(self): - return """Existing segments are grown to fill the image. + """This effect uses Watershed algorithm to partition the input volume""" + + def __init__(self, scriptedEffect): + scriptedEffect.name = 'TemplateKey' + scriptedEffect.perSegment = False # this effect operates on all segments at once (not on a single selected segment) + scriptedEffect.requireSegments = True # this effect requires segment(s) existing in the segmentation + AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect) + + def clone(self): + # It should not be necessary to modify this method + import qSlicerSegmentationsEditorEffectsPythonQt as effects + clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None) + clonedEffect.setPythonSource(__file__.replace('\\', '/')) + return clonedEffect + + def icon(self): + # It should not be necessary to modify this method + iconPath = os.path.join(os.path.dirname(__file__), 'SegmentEditorEffect.png') + if os.path.exists(iconPath): + return qt.QIcon(iconPath) + return qt.QIcon() + + def helpText(self): + return """Existing segments are grown to fill the image. The effect is different from the Grow from seeds effect in that smoothness of structures can be defined, which can prevent leakage. To segment a single object, create a segment and paint inside and create another segment and paint outside on each axis. """ - def setupOptionsFrame(self): - - # Object scale slider - self.objectScaleMmSlider = slicer.qMRMLSliderWidget() - self.objectScaleMmSlider.setMRMLScene(slicer.mrmlScene) - self.objectScaleMmSlider.quantity = "length" # get unit, precision, etc. from MRML unit node - self.objectScaleMmSlider.minimum = 0 - self.objectScaleMmSlider.maximum = 10 - self.objectScaleMmSlider.value = 2.0 - self.objectScaleMmSlider.setToolTip('Increasing this value smooths the segmentation and reduces leaks. This is the sigma used for edge detection.') - self.scriptedEffect.addLabeledOptionsWidget("Object scale:", self.objectScaleMmSlider) - self.objectScaleMmSlider.connect('valueChanged(double)', self.updateMRMLFromGUI) - - # Apply button - self.applyButton = qt.QPushButton("Apply") - self.applyButton.objectName = self.__class__.__name__ + 'Apply' - self.applyButton.setToolTip("Accept previewed result") - self.scriptedEffect.addOptionsWidget(self.applyButton) - self.applyButton.connect('clicked()', self.onApply) - - def createCursor(self, widget): - # Turn off effect-specific cursor for this effect - return slicer.util.mainWindow().cursor - - def setMRMLDefaults(self): - self.scriptedEffect.setParameterDefault("ObjectScaleMm", 2.0) - - def updateGUIFromMRML(self): - objectScaleMm = self.scriptedEffect.doubleParameter("ObjectScaleMm") - wasBlocked = self.objectScaleMmSlider.blockSignals(True) - self.objectScaleMmSlider.value = abs(objectScaleMm) - self.objectScaleMmSlider.blockSignals(wasBlocked) - - def updateMRMLFromGUI(self): - self.scriptedEffect.setParameter("ObjectScaleMm", self.objectScaleMmSlider.value) - - def onApply(self): - - # Get list of visible segment IDs, as the effect ignores hidden segments. - segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() - visibleSegmentIds = vtk.vtkStringArray() - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) - if visibleSegmentIds.GetNumberOfValues() == 0: - logging.info("Smoothing operation skipped: there are no visible segments") - return - - # This can be a long operation - indicate it to the user - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - - # Allow users revert to this state by clicking Undo - self.scriptedEffect.saveStateForUndo() - - # Export master image data to temporary new volume node. - # Note: Although the original master volume node is already in the scene, we do not use it here, - # because the master volume may have been resampled to match segmentation geometry. - masterVolumeNode = slicer.vtkMRMLScalarVolumeNode() - slicer.mrmlScene.AddNode(masterVolumeNode) - masterVolumeNode.SetAndObserveTransformNodeID(segmentationNode.GetTransformNodeID()) - slicer.vtkSlicerSegmentationsModuleLogic.CopyOrientedImageDataToVolumeNode(self.scriptedEffect.masterVolumeImageData(), masterVolumeNode) - # Generate merged labelmap of all visible segments, as the filter expects a single labelmap with all the labels. - mergedLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() - slicer.mrmlScene.AddNode(mergedLabelmapNode) - slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, visibleSegmentIds, mergedLabelmapNode, masterVolumeNode) - - # Run segmentation algorithm - import SimpleITK as sitk - import sitkUtils - # Read input data from Slicer into SimpleITK - labelImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName())) - backgroundImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(masterVolumeNode.GetName())) - # Run watershed filter - featureImage = sitk.GradientMagnitudeRecursiveGaussian(backgroundImage, float(self.scriptedEffect.doubleParameter("ObjectScaleMm"))) - del backgroundImage - f = sitk.MorphologicalWatershedFromMarkersImageFilter() - f.SetMarkWatershedLine(False) - f.SetFullyConnected(False) - labelImage = f.Execute(featureImage, labelImage) - del featureImage - # Pixel type of watershed output is the same as the input. Convert it to int16 now. - if labelImage.GetPixelID() != sitk.sitkInt16: - labelImage = sitk.Cast(labelImage, sitk.sitkInt16) - # Write result from SimpleITK to Slicer. This currently performs a deep copy of the bulk data. - sitk.WriteImage(labelImage, sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName())) - mergedLabelmapNode.GetImageData().Modified() - mergedLabelmapNode.Modified() - - # Update segmentation from labelmap node and remove temporary nodes - slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(mergedLabelmapNode, segmentationNode, visibleSegmentIds) - slicer.mrmlScene.RemoveNode(masterVolumeNode) - slicer.mrmlScene.RemoveNode(mergedLabelmapNode) - - qt.QApplication.restoreOverrideCursor() + def setupOptionsFrame(self): + + # Object scale slider + self.objectScaleMmSlider = slicer.qMRMLSliderWidget() + self.objectScaleMmSlider.setMRMLScene(slicer.mrmlScene) + self.objectScaleMmSlider.quantity = "length" # get unit, precision, etc. from MRML unit node + self.objectScaleMmSlider.minimum = 0 + self.objectScaleMmSlider.maximum = 10 + self.objectScaleMmSlider.value = 2.0 + self.objectScaleMmSlider.setToolTip('Increasing this value smooths the segmentation and reduces leaks. This is the sigma used for edge detection.') + self.scriptedEffect.addLabeledOptionsWidget("Object scale:", self.objectScaleMmSlider) + self.objectScaleMmSlider.connect('valueChanged(double)', self.updateMRMLFromGUI) + + # Apply button + self.applyButton = qt.QPushButton("Apply") + self.applyButton.objectName = self.__class__.__name__ + 'Apply' + self.applyButton.setToolTip("Accept previewed result") + self.scriptedEffect.addOptionsWidget(self.applyButton) + self.applyButton.connect('clicked()', self.onApply) + + def createCursor(self, widget): + # Turn off effect-specific cursor for this effect + return slicer.util.mainWindow().cursor + + def setMRMLDefaults(self): + self.scriptedEffect.setParameterDefault("ObjectScaleMm", 2.0) + + def updateGUIFromMRML(self): + objectScaleMm = self.scriptedEffect.doubleParameter("ObjectScaleMm") + wasBlocked = self.objectScaleMmSlider.blockSignals(True) + self.objectScaleMmSlider.value = abs(objectScaleMm) + self.objectScaleMmSlider.blockSignals(wasBlocked) + + def updateMRMLFromGUI(self): + self.scriptedEffect.setParameter("ObjectScaleMm", self.objectScaleMmSlider.value) + + def onApply(self): + + # Get list of visible segment IDs, as the effect ignores hidden segments. + segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode() + visibleSegmentIds = vtk.vtkStringArray() + segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds) + if visibleSegmentIds.GetNumberOfValues() == 0: + logging.info("Smoothing operation skipped: there are no visible segments") + return + + # This can be a long operation - indicate it to the user + qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) + + # Allow users revert to this state by clicking Undo + self.scriptedEffect.saveStateForUndo() + + # Export master image data to temporary new volume node. + # Note: Although the original master volume node is already in the scene, we do not use it here, + # because the master volume may have been resampled to match segmentation geometry. + masterVolumeNode = slicer.vtkMRMLScalarVolumeNode() + slicer.mrmlScene.AddNode(masterVolumeNode) + masterVolumeNode.SetAndObserveTransformNodeID(segmentationNode.GetTransformNodeID()) + slicer.vtkSlicerSegmentationsModuleLogic.CopyOrientedImageDataToVolumeNode(self.scriptedEffect.masterVolumeImageData(), masterVolumeNode) + # Generate merged labelmap of all visible segments, as the filter expects a single labelmap with all the labels. + mergedLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode() + slicer.mrmlScene.AddNode(mergedLabelmapNode) + slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, visibleSegmentIds, mergedLabelmapNode, masterVolumeNode) + + # Run segmentation algorithm + import SimpleITK as sitk + import sitkUtils + # Read input data from Slicer into SimpleITK + labelImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName())) + backgroundImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(masterVolumeNode.GetName())) + # Run watershed filter + featureImage = sitk.GradientMagnitudeRecursiveGaussian(backgroundImage, float(self.scriptedEffect.doubleParameter("ObjectScaleMm"))) + del backgroundImage + f = sitk.MorphologicalWatershedFromMarkersImageFilter() + f.SetMarkWatershedLine(False) + f.SetFullyConnected(False) + labelImage = f.Execute(featureImage, labelImage) + del featureImage + # Pixel type of watershed output is the same as the input. Convert it to int16 now. + if labelImage.GetPixelID() != sitk.sitkInt16: + labelImage = sitk.Cast(labelImage, sitk.sitkInt16) + # Write result from SimpleITK to Slicer. This currently performs a deep copy of the bulk data. + sitk.WriteImage(labelImage, sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName())) + mergedLabelmapNode.GetImageData().Modified() + mergedLabelmapNode.Modified() + + # Update segmentation from labelmap node and remove temporary nodes + slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(mergedLabelmapNode, segmentationNode, visibleSegmentIds) + slicer.mrmlScene.RemoveNode(masterVolumeNode) + slicer.mrmlScene.RemoveNode(mergedLabelmapNode) + + qt.QApplication.restoreOverrideCursor()