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