Slicer  4.11
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
SegmentEditorHollowEffect.py
Go to the documentation of this file.
1 import os
2 import vtk, qt, ctk, slicer
3 import logging
4 import math
5 from SegmentEditorEffects import *
6 
8  """This effect makes a segment hollow by replacing it with a shell at the segment boundary"""
9 
10  def __init__(self, scriptedEffect):
11  scriptedEffect.name = 'Hollow'
12  AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
13 
14  def clone(self):
15  import qSlicerSegmentationsEditorEffectsPythonQt as effects
16  clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
17  clonedEffect.setPythonSource(__file__.replace('\\','/'))
18  return clonedEffect
19 
20  def icon(self):
21  iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Hollow.png')
22  if os.path.exists(iconPath):
23  return qt.QIcon(iconPath)
24  return qt.QIcon()
25 
26  def helpText(self):
27  return """Make the selected segment hollow by replacing the segment with a uniform-thickness shell defined by the segment boundary."""
28 
29  def setupOptionsFrame(self):
30 
31  operationLayout = qt.QVBoxLayout()
32 
33  self.insideSurfaceOptionRadioButton = qt.QRadioButton("inside surface")
34  self.medialSurfaceOptionRadioButton = qt.QRadioButton("medial surface")
35  self.outsideSurfaceOptionRadioButton = qt.QRadioButton("outside surface")
36  operationLayout.addWidget(self.insideSurfaceOptionRadioButton)
37  operationLayout.addWidget(self.medialSurfaceOptionRadioButton)
38  operationLayout.addWidget(self.outsideSurfaceOptionRadioButton)
39  self.insideSurfaceOptionRadioButton.setChecked(True)
40 
41  self.scriptedEffect.addLabeledOptionsWidget("Use current segment as:", operationLayout)
42 
43  self.shellThicknessMMSpinBox = slicer.qMRMLSpinBox()
44  self.shellThicknessMMSpinBox.setMRMLScene(slicer.mrmlScene)
45  self.shellThicknessMMSpinBox.setToolTip("Thickness of the hollow shell.")
46  self.shellThicknessMMSpinBox.quantity = "length"
47  self.shellThicknessMMSpinBox.minimum = 0.0
48  self.shellThicknessMMSpinBox.value = 3.0
49  self.shellThicknessMMSpinBox.singleStep = 1.0
50 
51  self.shellThicknessLabel = qt.QLabel()
52  self.shellThicknessLabel.setToolTip("Closest achievable thickness. Constrained by the segmentation's binary labelmap representation spacing.")
53 
54  shellThicknessFrame = qt.QHBoxLayout()
55  shellThicknessFrame.addWidget(self.shellThicknessMMSpinBox)
56  self.shellThicknessMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Shell thickness:", shellThicknessFrame)
57  self.scriptedEffect.addLabeledOptionsWidget("", self.shellThicknessLabel)
58 
59  self.applyButton = qt.QPushButton("Apply")
60  self.applyButton.objectName = self.__class__.__name__ + 'Apply'
61  self.applyButton.setToolTip("Makes the segment hollow by replacing it with a thick shell at the segment boundary.")
62  self.scriptedEffect.addOptionsWidget(self.applyButton)
63 
64  self.applyButton.connect('clicked()', self.onApply)
65  self.shellThicknessMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
66  self.insideSurfaceOptionRadioButton.connect("toggled(bool)", self.insideSurfaceModeToggled)
67  self.medialSurfaceOptionRadioButton.connect("toggled(bool)", self.medialSurfaceModeToggled)
68  self.outsideSurfaceOptionRadioButton.connect("toggled(bool)", self.outsideSurfaceModeToggled)
69 
70  def createCursor(self, widget):
71  # Turn off effect-specific cursor for this effect
72  return slicer.util.mainWindow().cursor
73 
74  def setMRMLDefaults(self):
75  self.scriptedEffect.setParameterDefault("ShellMode", INSIDE_SURFACE)
76  self.scriptedEffect.setParameterDefault("ShellThicknessMm", 3.0)
77 
79  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
80  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
81  if selectedSegmentLabelmap:
82  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
83 
84  shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
85  shellThicknessPixel = [int(math.floor(shellThicknessMM / selectedSegmentLabelmapSpacing[componentIndex])) for componentIndex in range(3)]
86  return shellThicknessPixel
87 
88  def updateGUIFromMRML(self):
89  shellThicknessMM = self.scriptedEffect.doubleParameter("ShellThicknessMm")
90  wasBlocked = self.shellThicknessMMSpinBox.blockSignals(True)
91  self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
92  self.shellThicknessMMSpinBox.value = abs(shellThicknessMM)
93  self.shellThicknessMMSpinBox.blockSignals(wasBlocked)
94 
95  wasBlocked = self.insideSurfaceOptionRadioButton.blockSignals(True)
96  self.insideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == INSIDE_SURFACE)
97  self.insideSurfaceOptionRadioButton.blockSignals(wasBlocked)
98 
99  wasBlocked = self.medialSurfaceOptionRadioButton.blockSignals(True)
100  self.medialSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == MEDIAL_SURFACE)
101  self.medialSurfaceOptionRadioButton.blockSignals(wasBlocked)
102 
103  wasBlocked = self.outsideSurfaceOptionRadioButton.blockSignals(True)
104  self.outsideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == OUTSIDE_SURFACE)
105  self.outsideSurfaceOptionRadioButton.blockSignals(wasBlocked)
106 
107  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
108  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
109  if selectedSegmentLabelmap:
110  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
111  shellThicknessPixel = self.getShellThicknessPixel()
112  if shellThicknessPixel[0] < 1 or shellThicknessPixel[1] < 1 or shellThicknessPixel[2] < 1:
113  self.shellThicknessLabel.text = "Not feasible at current resolution."
114  self.applyButton.setEnabled(False)
115  else:
116  thicknessMM = self.getShellThicknessMM()
117  self.shellThicknessLabel.text = "Actual: {0} x {1} x {2} mm ({3}x{4}x{5} pixel)".format(*thicknessMM, *shellThicknessPixel)
118  self.applyButton.setEnabled(True)
119  else:
120  self.shellThicknessLabel.text = "Empty segment"
121 
122  self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
123 
124  def updateMRMLFromGUI(self):
125  # Operation is managed separately
126  self.scriptedEffect.setParameter("ShellThicknessMm", self.shellThicknessMMSpinBox.value)
127 
128  def insideSurfaceModeToggled(self, toggled):
129  if toggled:
130  self.scriptedEffect.setParameter("ShellMode", INSIDE_SURFACE)
131 
132  def medialSurfaceModeToggled(self, toggled):
133  if toggled:
134  self.scriptedEffect.setParameter("ShellMode", MEDIAL_SURFACE)
135 
136  def outsideSurfaceModeToggled(self, toggled):
137  if toggled:
138  self.scriptedEffect.setParameter("ShellMode", OUTSIDE_SURFACE)
139 
141  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
142  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
143  if selectedSegmentLabelmap:
144  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
145 
146  shellThicknessPixel = self.getShellThicknessPixel()
147  shellThicknessMM = [abs((shellThicknessPixel[i])*selectedSegmentLabelmapSpacing[i]) for i in range(3)]
148  for i in range(3):
149  if shellThicknessMM[i] > 0:
150  shellThicknessMM[i] = round(shellThicknessMM[i], max(int(-math.floor(math.log10(shellThicknessMM[i]))),1))
151  return shellThicknessMM
152 
153  def onApply(self):
154  # Make sure the user wants to do the operation, even if the segment is not visible
155  if not self.scriptedEffect.confirmCurrentSegmentVisible():
156  return
157 
158  self.scriptedEffect.saveStateForUndo()
159 
160  # Get modifier labelmap and parameters
161  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
162  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
163 
164  # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
165  labelValue = 1
166  backgroundValue = 0
167  thresh = vtk.vtkImageThreshold()
168  thresh.SetInputData(selectedSegmentLabelmap)
169  thresh.ThresholdByLower(0)
170  thresh.SetInValue(backgroundValue)
171  thresh.SetOutValue(labelValue)
172  thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
173 
174  shellMode = self.scriptedEffect.parameter("ShellMode")
175  shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
176  import vtkITK
177  margin = vtkITK.vtkITKImageMargin()
178  margin.SetInputConnection(thresh.GetOutputPort())
179  margin.CalculateMarginInMMOn()
180 
181  spacing = selectedSegmentLabelmap.GetSpacing()
182  voxelDiameter = min(selectedSegmentLabelmap.GetSpacing())
183  if shellMode == MEDIAL_SURFACE:
184  margin.SetOuterMarginMM( 0.5 * shellThicknessMM)
185  margin.SetInnerMarginMM(-0.5 * shellThicknessMM + 0.5*voxelDiameter)
186  elif shellMode == INSIDE_SURFACE:
187  margin.SetOuterMarginMM(shellThicknessMM + 0.1*voxelDiameter)
188  margin.SetInnerMarginMM(0.0 + 0.1*voxelDiameter) # Don't include the original border (0.0)
189  elif shellMode == OUTSIDE_SURFACE:
190  margin.SetOuterMarginMM(0.0)
191  margin.SetInnerMarginMM(-shellThicknessMM + voxelDiameter)
192 
193  modifierLabelmap.DeepCopy(margin.GetOutput())
194 
195  # This can be a long operation - indicate it to the user
196  qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
197 
198  margin.Update()
199  modifierLabelmap.ShallowCopy(margin.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'