Slicer 5.4
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
Loading...
Searching...
No Matches
SegmentEditorMarginEffect.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 SegmentEditorMarginEffect(AbstractScriptedSegmentEditorEffect):
14 """ MaringEffect grows or shrinks the segment by a specified margin
15 """
16
17 def __init__(self, scriptedEffect):
18 scriptedEffect.name = 'Margin'
19 AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
20
21 def clone(self):
22 import qSlicerSegmentationsEditorEffectsPythonQt as effects
23 clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
24 clonedEffect.setPythonSource(__file__.replace('\\', '/'))
25 return clonedEffect
26
27 def icon(self):
28 iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Margin.png')
29 if os.path.exists(iconPath):
30 return qt.QIcon(iconPath)
31 return qt.QIcon()
32
33 def helpText(self):
34 return "Grow or shrink selected segment by specified margin size."
35
37
38 operationLayout = qt.QVBoxLayout()
39
40 self.shrinkOptionRadioButton = qt.QRadioButton("Shrink")
41 self.growOptionRadioButton = qt.QRadioButton("Grow")
42 operationLayout.addWidget(self.shrinkOptionRadioButton)
43 operationLayout.addWidget(self.growOptionRadioButton)
44 self.growOptionRadioButton.setChecked(True)
45
46 self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout)
47
48 self.marginSizeMMSpinBox = slicer.qMRMLSpinBox()
49 self.marginSizeMMSpinBox.setMRMLScene(slicer.mrmlScene)
50 self.marginSizeMMSpinBox.setToolTip("Segment boundaries will be shifted by this distance. Positive value means the segments will grow, negative value means segment will shrink.")
51 self.marginSizeMMSpinBox.quantity = "length"
52 self.marginSizeMMSpinBox.value = 3.0
53 self.marginSizeMMSpinBox.singleStep = 1.0
54
55 self.marginSizeLabel = qt.QLabel()
56 self.marginSizeLabel.setToolTip("Size change in pixel. Computed from the segment's spacing and the specified margin size.")
57
58 marginSizeFrame = qt.QHBoxLayout()
59 marginSizeFrame.addWidget(self.marginSizeMMSpinBox)
60 self.marginSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Margin size:", marginSizeFrame)
61 self.scriptedEffect.addLabeledOptionsWidget("", self.marginSizeLabel)
62
64 self.applyToAllVisibleSegmentsCheckBox.setToolTip("Grow or shrink all visible segments in this segmentation node. \
65 This operation may take a while.")
66 self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments'
67 self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to visible segments:", self.applyToAllVisibleSegmentsCheckBox)
68
69 self.applyButton = qt.QPushButton("Apply")
70 self.applyButton.objectName = self.__class__.__name__ + 'Apply'
71 self.applyButton.setToolTip("Grows or shrinks selected segment /default) or all segments (checkbox) by the specified margin.")
72 self.scriptedEffect.addOptionsWidget(self.applyButton)
73
74 self.applyButton.connect('clicked()', self.onApplyonApply)
75 self.marginSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUIupdateMRMLFromGUI)
78 self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUIupdateMRMLFromGUI)
79
80 def createCursor(self, widget):
81 # Turn off effect-specific cursor for this effect
82 return slicer.util.mainWindow().cursor
83
84 def setMRMLDefaults(self):
85 self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
86 self.scriptedEffect.setParameterDefault("MarginSizeMm", 3)
87
89 selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
90 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
91 if selectedSegmentLabelmap:
92 selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
93
94 marginSizeMM = abs(self.scriptedEffect.doubleParameter("MarginSizeMm"))
95 marginSizePixel = [int(math.floor(marginSizeMM / spacing)) for spacing in selectedSegmentLabelmapSpacing]
96 return marginSizePixel
97
99 marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm")
100 wasBlocked = self.marginSizeMMSpinBox.blockSignals(True)
101 self.marginSizeMMSpinBox.value = abs(marginSizeMM)
102 self.marginSizeMMSpinBox.blockSignals(wasBlocked)
103
104 wasBlocked = self.growOptionRadioButton.blockSignals(True)
105 self.growOptionRadioButton.setChecked(marginSizeMM > 0)
106 self.growOptionRadioButton.blockSignals(wasBlocked)
107
108 wasBlocked = self.shrinkOptionRadioButton.blockSignals(True)
109 self.shrinkOptionRadioButton.setChecked(marginSizeMM < 0)
110 self.shrinkOptionRadioButton.blockSignals(wasBlocked)
111
112 selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
113 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
114 if selectedSegmentLabelmap:
115 selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
116 marginSizePixel = self.getMarginSizePixel()
117 if marginSizePixel[0] < 1 or marginSizePixel[1] < 1 or marginSizePixel[2] < 1:
118 self.marginSizeLabel.text = "Not feasible at current resolution."
119 self.applyButton.setEnabled(False)
120 else:
121 marginSizeMM = self.getMarginSizeMM()
122 self.marginSizeLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*marginSizeMM, *marginSizePixel)
123 self.applyButton.setEnabled(True)
124 else:
125 self.marginSizeLabel.text = "Empty segment"
126
127 applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
128 wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
129 self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
130 self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
131
132 self.setWidgetMinMaxStepFromImageSpacing(self.marginSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
133
134 def growOperationToggled(self, toggled):
135 if toggled:
136 self.scriptedEffect.setParameter("MarginSizeMm", self.marginSizeMMSpinBox.value)
137
138 def shrinkOperationToggled(self, toggled):
139 if toggled:
140 self.scriptedEffect.setParameter("MarginSizeMm", -self.marginSizeMMSpinBox.value)
141
143 marginSizeMM = (self.marginSizeMMSpinBox.value) if self.growOptionRadioButton.checked else (-self.marginSizeMMSpinBox.value)
144 self.scriptedEffect.setParameter("MarginSizeMm", marginSizeMM)
145 applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
146 self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
147
149 selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
150 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
151 if selectedSegmentLabelmap:
152 selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
153
154 marginSizePixel = self.getMarginSizePixel()
155 marginSizeMM = [abs((marginSizePixel[i]) * selectedSegmentLabelmapSpacing[i]) for i in range(3)]
156 for i in range(3):
157 if marginSizeMM[i] > 0:
158 marginSizeMM[i] = round(marginSizeMM[i], max(int(-math.floor(math.log10(marginSizeMM[i]))), 1))
159 return marginSizeMM
160
161 def showStatusMessage(self, msg, timeoutMsec=500):
162 slicer.util.showStatusMessage(msg, timeoutMsec)
163 slicer.app.processEvents()
164
165 def processMargin(self):
166 # Get modifier labelmap and parameters
167 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
168 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
169
170 marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm")
171
172 # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
173 labelValue = 1
174 backgroundValue = 0
175 thresh = vtk.vtkImageThreshold()
176 thresh.SetInputData(selectedSegmentLabelmap)
177 thresh.ThresholdByLower(0)
178 thresh.SetInValue(backgroundValue)
179 thresh.SetOutValue(labelValue)
180 thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
181 if (marginSizeMM < 0):
182 # The distance filter used in the margin filter starts at zero at the border voxels,
183 # so if we need to shrink the margin, it is more accurate to invert the labelmap and
184 # use positive distance when calculating the margin
185 thresh.SetInValue(labelValue)
186 thresh.SetOutValue(backgroundValue)
187
188 import vtkITK
189 margin = vtkITK.vtkITKImageMargin()
190 margin.SetInputConnection(thresh.GetOutputPort())
191 margin.CalculateMarginInMMOn()
192 margin.SetOuterMarginMM(abs(marginSizeMM))
193 margin.Update()
194
195 if marginSizeMM >= 0:
196 modifierLabelmap.ShallowCopy(margin.GetOutput())
197 else:
198 # If we are shrinking then the result needs to be inverted.
199 thresh = vtk.vtkImageThreshold()
200 thresh.SetInputData(margin.GetOutput())
201 thresh.ThresholdByLower(0)
202 thresh.SetInValue(labelValue)
203 thresh.SetOutValue(backgroundValue)
204 thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
205 thresh.Update()
206 modifierLabelmap.ShallowCopy(thresh.GetOutput())
207
208 # Apply changes
209 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
210
211 def onApply(self):
212 # Make sure the user wants to do the operation, even if the segment is not visible
213 if not self.scriptedEffect.confirmCurrentSegmentVisible():
214 return
215
216 try:
217 # This can be a long operation - indicate it to the user
218 qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
219 self.scriptedEffect.saveStateForUndo()
220
221 applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
222 if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
223
224 if applyToAllVisibleSegments:
225 # Smooth all visible segments
226 inputSegmentIDs = vtk.vtkStringArray()
227 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
228 segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
229 # store which segment was selected before operation
230 selectedStartSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
231 if inputSegmentIDs.GetNumberOfValues() == 0:
232 logging.info("Margin operation skipped: there are no visible segments.")
233 return
234 # select input segments one by one, process
235 for index in range(inputSegmentIDs.GetNumberOfValues()):
236 segmentID = inputSegmentIDs.GetValue(index)
237 self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
238 self.scriptedEffect.parameterSetNode().SetSelectedSegmentID(segmentID)
239 self.processMargin()
240 # restore segment selection
241 self.scriptedEffect.parameterSetNode().SetSelectedSegmentID(selectedStartSegmentID)
242 else:
243 self.processMargin()
244
245 finally:
246 qt.QApplication.restoreOverrideCursor()