Slicer  4.11
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 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'