Slicer  5.0
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 logging
2 import math
3 import os
4 
5 import qt
6 import vtk
7 
8 import slicer
9 
10 from SegmentEditorEffects import *
11 
12 
14  """This effect makes a segment hollow by replacing it with a shell at the segment boundary"""
15 
16  def __init__(self, scriptedEffect):
17  scriptedEffect.name = 'Hollow'
18  AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
19 
20  def clone(self):
21  import qSlicerSegmentationsEditorEffectsPythonQt as effects
22  clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
23  clonedEffect.setPythonSource(__file__.replace('\\', '/'))
24  return clonedEffect
25 
26  def icon(self):
27  iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Hollow.png')
28  if os.path.exists(iconPath):
29  return qt.QIcon(iconPath)
30  return qt.QIcon()
31 
32  def helpText(self):
33  return """Make the selected segment hollow by replacing the segment with a uniform-thickness shell defined by the segment boundary."""
34 
35  def setupOptionsFrame(self):
36 
37  operationLayout = qt.QVBoxLayout()
38 
39  self.insideSurfaceOptionRadioButton = qt.QRadioButton("inside surface")
40  self.medialSurfaceOptionRadioButton = qt.QRadioButton("medial surface")
41  self.outsideSurfaceOptionRadioButton = qt.QRadioButton("outside surface")
42  operationLayout.addWidget(self.insideSurfaceOptionRadioButton)
43  operationLayout.addWidget(self.medialSurfaceOptionRadioButton)
44  operationLayout.addWidget(self.outsideSurfaceOptionRadioButton)
45  self.insideSurfaceOptionRadioButton.setChecked(True)
46 
47  self.scriptedEffect.addLabeledOptionsWidget("Use current segment as:", operationLayout)
48 
49  self.shellThicknessMMSpinBox = slicer.qMRMLSpinBox()
50  self.shellThicknessMMSpinBox.setMRMLScene(slicer.mrmlScene)
51  self.shellThicknessMMSpinBox.setToolTip("Thickness of the hollow shell.")
52  self.shellThicknessMMSpinBox.quantity = "length"
53  self.shellThicknessMMSpinBox.minimum = 0.0
54  self.shellThicknessMMSpinBox.value = 3.0
55  self.shellThicknessMMSpinBox.singleStep = 1.0
56 
57  self.shellThicknessLabel = qt.QLabel()
58  self.shellThicknessLabel.setToolTip("Closest achievable thickness. Constrained by the segmentation's binary labelmap representation spacing.")
59 
60  shellThicknessFrame = qt.QHBoxLayout()
61  shellThicknessFrame.addWidget(self.shellThicknessMMSpinBox)
62  self.shellThicknessMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Shell thickness:", shellThicknessFrame)
63  self.scriptedEffect.addLabeledOptionsWidget("", self.shellThicknessLabel)
64 
65  self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox()
66  self.applyToAllVisibleSegmentsCheckBox.setToolTip("Apply hollow effect to all visible segments in this segmentation node. \
67  This operation may take a while.")
68  self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments'
69  self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox)
70 
71  self.applyButton = qt.QPushButton("Apply")
72  self.applyButton.objectName = self.__class__.__name__ + 'Apply'
73  self.applyButton.setToolTip("Makes the segment hollow by replacing it with a thick shell at the segment boundary.")
74  self.scriptedEffect.addOptionsWidget(self.applyButton)
75 
76  self.applyButton.connect('clicked()', self.onApply)
77  self.shellThicknessMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
78  self.insideSurfaceOptionRadioButton.connect("toggled(bool)", self.insideSurfaceModeToggled)
79  self.medialSurfaceOptionRadioButton.connect("toggled(bool)", self.medialSurfaceModeToggled)
80  self.outsideSurfaceOptionRadioButton.connect("toggled(bool)", self.outsideSurfaceModeToggled)
81  self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
82 
83  def createCursor(self, widget):
84  # Turn off effect-specific cursor for this effect
85  return slicer.util.mainWindow().cursor
86 
87  def setMRMLDefaults(self):
88  self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
89  self.scriptedEffect.setParameterDefault("ShellMode", INSIDE_SURFACE)
90  self.scriptedEffect.setParameterDefault("ShellThicknessMm", 3.0)
91 
93  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
94  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
95  if selectedSegmentLabelmap:
96  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
97 
98  shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
99  shellThicknessPixel = [int(math.floor(shellThicknessMM / selectedSegmentLabelmapSpacing[componentIndex])) for componentIndex in range(3)]
100  return shellThicknessPixel
101 
102  def updateGUIFromMRML(self):
103  shellThicknessMM = self.scriptedEffect.doubleParameter("ShellThicknessMm")
104  wasBlocked = self.shellThicknessMMSpinBox.blockSignals(True)
105  self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
106  self.shellThicknessMMSpinBox.value = abs(shellThicknessMM)
107  self.shellThicknessMMSpinBox.blockSignals(wasBlocked)
108 
109  wasBlocked = self.insideSurfaceOptionRadioButton.blockSignals(True)
110  self.insideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == INSIDE_SURFACE)
111  self.insideSurfaceOptionRadioButton.blockSignals(wasBlocked)
112 
113  wasBlocked = self.medialSurfaceOptionRadioButton.blockSignals(True)
114  self.medialSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == MEDIAL_SURFACE)
115  self.medialSurfaceOptionRadioButton.blockSignals(wasBlocked)
116 
117  wasBlocked = self.outsideSurfaceOptionRadioButton.blockSignals(True)
118  self.outsideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == OUTSIDE_SURFACE)
119  self.outsideSurfaceOptionRadioButton.blockSignals(wasBlocked)
120 
121  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
122  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
123  if selectedSegmentLabelmap:
124  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
125  shellThicknessPixel = self.getShellThicknessPixel()
126  if shellThicknessPixel[0] < 1 or shellThicknessPixel[1] < 1 or shellThicknessPixel[2] < 1:
127  self.shellThicknessLabel.text = "Not feasible at current resolution."
128  self.applyButton.setEnabled(False)
129  else:
130  thicknessMM = self.getShellThicknessMM()
131  self.shellThicknessLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*thicknessMM, *shellThicknessPixel)
132  self.applyButton.setEnabled(True)
133  else:
134  self.shellThicknessLabel.text = "Empty segment"
135 
136  self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
137 
138  applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
139  wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
140  self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
141  self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
142 
143  def updateMRMLFromGUI(self):
144  # Operation is managed separately
145  self.scriptedEffect.setParameter("ShellThicknessMm", self.shellThicknessMMSpinBox.value)
146  applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
147  self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
148 
149  def insideSurfaceModeToggled(self, toggled):
150  if toggled:
151  self.scriptedEffect.setParameter("ShellMode", INSIDE_SURFACE)
152 
153  def medialSurfaceModeToggled(self, toggled):
154  if toggled:
155  self.scriptedEffect.setParameter("ShellMode", MEDIAL_SURFACE)
156 
157  def outsideSurfaceModeToggled(self, toggled):
158  if toggled:
159  self.scriptedEffect.setParameter("ShellMode", OUTSIDE_SURFACE)
160 
162  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
163  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
164  if selectedSegmentLabelmap:
165  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
166 
167  shellThicknessPixel = self.getShellThicknessPixel()
168  shellThicknessMM = [abs((shellThicknessPixel[i]) * selectedSegmentLabelmapSpacing[i]) for i in range(3)]
169  for i in range(3):
170  if shellThicknessMM[i] > 0:
171  shellThicknessMM[i] = round(shellThicknessMM[i], max(int(-math.floor(math.log10(shellThicknessMM[i]))), 1))
172  return shellThicknessMM
173 
174  def showStatusMessage(self, msg, timeoutMsec=500):
175  slicer.util.showStatusMessage(msg, timeoutMsec)
176  slicer.app.processEvents()
177 
178  def processHollowing(self):
179  # Get modifier labelmap and parameters
180  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
181  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
182  # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
183  labelValue = 1
184  backgroundValue = 0
185  thresh = vtk.vtkImageThreshold()
186  thresh.SetInputData(selectedSegmentLabelmap)
187  thresh.ThresholdByLower(0)
188  thresh.SetInValue(backgroundValue)
189  thresh.SetOutValue(labelValue)
190  thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
191 
192  shellMode = self.scriptedEffect.parameter("ShellMode")
193  shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
194  import vtkITK
195  margin = vtkITK.vtkITKImageMargin()
196  margin.SetInputConnection(thresh.GetOutputPort())
197  margin.CalculateMarginInMMOn()
198 
199  spacing = selectedSegmentLabelmap.GetSpacing()
200  voxelDiameter = min(selectedSegmentLabelmap.GetSpacing())
201  if shellMode == MEDIAL_SURFACE:
202  margin.SetOuterMarginMM(0.5 * shellThicknessMM)
203  margin.SetInnerMarginMM(-0.5 * shellThicknessMM + 0.5 * voxelDiameter)
204  elif shellMode == INSIDE_SURFACE:
205  margin.SetOuterMarginMM(shellThicknessMM + 0.1 * voxelDiameter)
206  margin.SetInnerMarginMM(0.0 + 0.1 * voxelDiameter) # Don't include the original border (0.0)
207  elif shellMode == OUTSIDE_SURFACE:
208  margin.SetOuterMarginMM(0.0)
209  margin.SetInnerMarginMM(-shellThicknessMM + voxelDiameter)
210 
211  modifierLabelmap.DeepCopy(margin.GetOutput())
212 
213  margin.Update()
214  modifierLabelmap.ShallowCopy(margin.GetOutput())
215 
216  # Apply changes
217  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
218 
219  def onApply(self):
220  # Make sure the user wants to do the operation, even if the segment is not visible
221  if not self.scriptedEffect.confirmCurrentSegmentVisible():
222  return
223 
224  try:
225  # This can be a long operation - indicate it to the user
226  qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
227  self.scriptedEffect.saveStateForUndo()
228 
229  applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
230  if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
231 
232  if applyToAllVisibleSegments:
233  # Process all visible segments
234  inputSegmentIDs = vtk.vtkStringArray()
235  segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
236  segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
237  segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
238  segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
239  # store which segment was selected before operation
240  selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID()
241  if inputSegmentIDs.GetNumberOfValues() == 0:
242  logging.info("Hollow operation skipped: there are no visible segments.")
243  return
244  # select input segments one by one, process
245  for index in range(inputSegmentIDs.GetNumberOfValues()):
246  segmentID = inputSegmentIDs.GetValue(index)
247  self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
248  segmentEditorNode.SetSelectedSegmentID(segmentID)
249  self.processHollowing()
250  # restore segment selection
251  segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID)
252  else:
253  self.processHollowing()
254 
255  finally:
256  qt.QApplication.restoreOverrideCursor()
257 
258 
259 INSIDE_SURFACE = 'INSIDE_SURFACE'
260 MEDIAL_SURFACE = 'MEDIAL_SURFACE'
261 OUTSIDE_SURFACE = 'OUTSIDE_SURFACE'