Slicer 5.9
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
Loading...
Searching...
No Matches
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"