Slicer 5.4
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
9
10from SegmentEditorEffects import *
11
12
13class SegmentEditorHollowEffect(AbstractScriptedSegmentEditorEffect):
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
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
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.onApplyonApply)
77 self.shellThicknessMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUIupdateMRMLFromGUI)
81 self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUIupdateMRMLFromGUI)
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
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
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
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 # store which segment was selected before operation
238 selectedStartSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
239 if inputSegmentIDs.GetNumberOfValues() == 0:
240 logging.info("Hollow operation skipped: there are no visible segments.")
241 return
242 # select input segments one by one, process
243 for index in range(inputSegmentIDs.GetNumberOfValues()):
244 segmentID = inputSegmentIDs.GetValue(index)
245 self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
246 self.scriptedEffect.parameterSetNode().SetSelectedSegmentID(segmentID)
247 self.processHollowing()
248 # restore segment selection
249 self.scriptedEffect.parameterSetNode().SetSelectedSegmentID(selectedStartSegmentID)
250 else:
251 self.processHollowing()
252
253 finally:
254 qt.QApplication.restoreOverrideCursor()
255
256
257INSIDE_SURFACE = 'INSIDE_SURFACE'
258MEDIAL_SURFACE = 'MEDIAL_SURFACE'
259OUTSIDE_SURFACE = 'OUTSIDE_SURFACE'