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