Slicer  5.2
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 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 visible 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'