Slicer  4.10
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
SegmentEditorHollowEffect.py
Go to the documentation of this file.
1 import os
2 import vtk, qt, ctk, slicer
3 import logging
4 from SegmentEditorEffects import *
5 
7  """This effect makes a segment hollow by replacing it with a shell at the segment boundary"""
8 
9  def __init__(self, scriptedEffect):
10  scriptedEffect.name = 'Hollow'
11  AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
12 
13  def clone(self):
14  import qSlicerSegmentationsEditorEffectsPythonQt as effects
15  clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
16  clonedEffect.setPythonSource(__file__.replace('\\','/'))
17  return clonedEffect
18 
19  def icon(self):
20  iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Hollow.png')
21  if os.path.exists(iconPath):
22  return qt.QIcon(iconPath)
23  return qt.QIcon()
24 
25  def helpText(self):
26  return """Make the selected segment hollow by replacing the segment with a uniform-thickness shell defined by the segment boundary."""
27 
28  def setupOptionsFrame(self):
29 
30  operationLayout = qt.QVBoxLayout()
31 
32  self.insideSurfaceOptionRadioButton = qt.QRadioButton("inside surface")
33  self.medialSurfaceOptionRadioButton = qt.QRadioButton("medial surface")
34  self.outsideSurfaceOptionRadioButton = qt.QRadioButton("outside surface")
35  operationLayout.addWidget(self.insideSurfaceOptionRadioButton)
36  operationLayout.addWidget(self.medialSurfaceOptionRadioButton)
37  operationLayout.addWidget(self.outsideSurfaceOptionRadioButton)
38  self.insideSurfaceOptionRadioButton.setChecked(True)
39 
40  self.scriptedEffect.addLabeledOptionsWidget("Use current segment as:", operationLayout)
41 
42  self.shellThicknessMmSpinBox = slicer.qMRMLSpinBox()
43  self.shellThicknessMmSpinBox.setMRMLScene(slicer.mrmlScene)
44  self.shellThicknessMmSpinBox.setToolTip("Thickness of the hollow shell.")
45  self.shellThicknessMmSpinBox.quantity = "length"
46  self.shellThicknessMmSpinBox.minimum = 0.0
47  self.shellThicknessMmSpinBox.value = 3.0
48  self.shellThicknessMmSpinBox.singleStep = 1.0
49 
50  self.kernelSizePixel = qt.QLabel()
51  self.kernelSizePixel.setToolTip("Thickness in pixels. Computed from the segment's spacing and the specified margin size.")
52 
53  shellThicknessFrame = qt.QHBoxLayout()
54  shellThicknessFrame.addWidget(self.shellThicknessMmSpinBox)
55  shellThicknessFrame.addWidget(self.kernelSizePixel)
56  self.shellThicknessMmLabel = self.scriptedEffect.addLabeledOptionsWidget("Shell thickness:", shellThicknessFrame)
57 
58  self.applyButton = qt.QPushButton("Apply")
59  self.applyButton.objectName = self.__class__.__name__ + 'Apply'
60  self.applyButton.setToolTip("Makes the segment hollow by replacing it with a thick shell at the segment boundary.")
61  self.scriptedEffect.addOptionsWidget(self.applyButton)
62 
63  self.applyButton.connect('clicked()', self.onApply)
64  self.shellThicknessMmSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
65  self.insideSurfaceOptionRadioButton.connect("toggled(bool)", self.insideSurfaceModeToggled)
66  self.medialSurfaceOptionRadioButton.connect("toggled(bool)", self.medialSurfaceModeToggled)
67  self.outsideSurfaceOptionRadioButton.connect("toggled(bool)", self.outsideSurfaceModeToggled)
68 
69  def createCursor(self, widget):
70  # Turn off effect-specific cursor for this effect
71  return slicer.util.mainWindow().cursor
72 
73  def setMRMLDefaults(self):
74  self.scriptedEffect.setParameterDefault("ShellMode", INSIDE_SURFACE)
75  self.scriptedEffect.setParameterDefault("ShellThicknessMm", 3.0)
76 
77  def getKernelSizePixel(self):
78  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
79  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
80  if selectedSegmentLabelmap:
81  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
82 
83  if self.scriptedEffect.parameter("ShellMode") == MEDIAL_SURFACE:
84  # Size rounded to nearest 2x of odd number, as kernel will be applied on both sides and kernel size must be odd number.
85  shellThicknessMm = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
86  kernelSizePixel = [int(round((shellThicknessMm / selectedSegmentLabelmapSpacing[componentIndex]+2)/4)*4) for componentIndex in range(3)]
87  else:
88  # Size rounded to nearest odd number. If kernel size is even then image gets shifted.
89  shellThicknessMm = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
90  kernelSizePixel = [int(round((shellThicknessMm / selectedSegmentLabelmapSpacing[componentIndex]+1)/2)*2-1) for componentIndex in range(3)]
91  return kernelSizePixel
92 
93  def updateGUIFromMRML(self):
94  shellThicknessMm = self.scriptedEffect.doubleParameter("ShellThicknessMm")
95  wasBlocked = self.shellThicknessMmSpinBox.blockSignals(True)
96  self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMmSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
97  self.shellThicknessMmSpinBox.value = abs(shellThicknessMm)
98  self.shellThicknessMmSpinBox.blockSignals(wasBlocked)
99 
100  wasBlocked = self.insideSurfaceOptionRadioButton.blockSignals(True)
101  self.insideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == INSIDE_SURFACE)
102  self.insideSurfaceOptionRadioButton.blockSignals(wasBlocked)
103 
104  wasBlocked = self.medialSurfaceOptionRadioButton.blockSignals(True)
105  self.medialSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == MEDIAL_SURFACE)
106  self.medialSurfaceOptionRadioButton.blockSignals(wasBlocked)
107 
108  wasBlocked = self.outsideSurfaceOptionRadioButton.blockSignals(True)
109  self.outsideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == OUTSIDE_SURFACE)
110  self.outsideSurfaceOptionRadioButton.blockSignals(wasBlocked)
111 
112  kernelSizePixel = self.getKernelSizePixel()
113 
114  if self.scriptedEffect.parameter("ShellMode") == MEDIAL_SURFACE:
115  minimumKernelSize = 2
116  else:
117  minimumKernelSize = 1
118  if kernelSizePixel[0]<=1 and kernelSizePixel[1]<=1 and kernelSizePixel[2]<=1:
119  self.kernelSizePixel.text = "too thin"
120  self.applyButton.setEnabled(False)
121  else:
122  self.kernelSizePixel.text = "{0}x{1}x{2} pixels".format(abs(kernelSizePixel[0]), abs(kernelSizePixel[1]), abs(kernelSizePixel[2]))
123  self.applyButton.setEnabled(True)
124 
125  self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMmSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
126 
127  def updateMRMLFromGUI(self):
128  # Operation is managed separately
129  self.scriptedEffect.setParameter("ShellThicknessMm", self.shellThicknessMmSpinBox.value)
130 
131  def insideSurfaceModeToggled(self, toggled):
132  if toggled:
133  self.scriptedEffect.setParameter("ShellMode", INSIDE_SURFACE)
134 
135  def medialSurfaceModeToggled(self, toggled):
136  if toggled:
137  self.scriptedEffect.setParameter("ShellMode", MEDIAL_SURFACE)
138 
139  def outsideSurfaceModeToggled(self, toggled):
140  if toggled:
141  self.scriptedEffect.setParameter("ShellMode", OUTSIDE_SURFACE)
142 
143  def onApply(self):
144 
145  self.scriptedEffect.saveStateForUndo()
146 
147  # Get modifier labelmap and parameters
148  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
149  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
150 
151  shellMode = self.scriptedEffect.parameter("ShellMode")
152 
153  marginSizeMm = self.scriptedEffect.doubleParameter("MarginSizeMm")
154  kernelSizePixel = self.getKernelSizePixel()
155  if shellMode == MEDIAL_SURFACE:
156  # both erosion and dilation will be applied, so kernel size must be half on each side
157  kernelSizePixel = [kernelSizePixel[0]/2, kernelSizePixel[1]/2, kernelSizePixel[2]/2]
158 
159  # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
160  labelValue = 1
161  backgroundValue = 0
162  thresh = vtk.vtkImageThreshold()
163  thresh.SetInputData(selectedSegmentLabelmap)
164  thresh.ThresholdByLower(0)
165  thresh.SetInValue(backgroundValue)
166  thresh.SetOutValue(labelValue)
167  thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
168  thresh.Update()
169 
170  subtract = vtk.vtkImageMathematics()
171  subtract.SetOperationToSubtract()
172 
173  if shellMode == INSIDE_SURFACE or shellMode == MEDIAL_SURFACE:
174  dilate = vtk.vtkImageDilateErode3D()
175  dilate.SetInputConnection(thresh.GetOutputPort())
176  dilate.SetDilateValue(labelValue)
177  dilate.SetErodeValue(backgroundValue)
178  dilate.SetKernelSize(kernelSizePixel[0],kernelSizePixel[1],kernelSizePixel[2])
179  dilate.Update()
180  subtract.SetInput1Data(dilate.GetOutput())
181  else:
182  subtract.SetInput1Data(thresh.GetOutput())
183 
184  if shellMode == OUTSIDE_SURFACE or shellMode == MEDIAL_SURFACE:
185  erode = vtk.vtkImageDilateErode3D()
186  erode.SetInputConnection(thresh.GetOutputPort())
187  erode.SetDilateValue(backgroundValue)
188  erode.SetErodeValue(labelValue)
189  erode.SetKernelSize(kernelSizePixel[0],kernelSizePixel[1],kernelSizePixel[2])
190  erode.Update()
191  subtract.SetInput2Data(erode.GetOutput())
192  else:
193  subtract.SetInput2Data(thresh.GetOutput())
194 
195  # This can be a long operation - indicate it to the user
196  qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
197 
198  subtract.Update()
199  modifierLabelmap.DeepCopy(subtract.GetOutput())
200 
201  # Apply changes
202  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
203 
204  qt.QApplication.restoreOverrideCursor()
205 
206 INSIDE_SURFACE = 'INSIDE_SURFACE'
207 MEDIAL_SURFACE = 'MEDIAL_SURFACE'
208 OUTSIDE_SURFACE = 'OUTSIDE_SURFACE'