Slicer 5.6
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
Loading...
Searching...
No Matches
SegmentEditorSmoothingEffect.py
Go to the documentation of this file.
1import logging
2import os
3
4import ctk
5import qt
6import vtk
7
8import slicer
9from slicer.i18n import tr as _
10
11from SegmentEditorEffects import *
12
13
15 """SmoothingEffect is an Effect that smoothes a selected segment"""
16
17 def __init__(self, scriptedEffect):
18 scriptedEffect.name = "Smoothing" # no tr (don't translate it because modules find effects by name)
19 scriptedEffect.title = _("Smoothing")
20 AbstractScriptedSegmentEditorPaintEffect.__init__(self, scriptedEffect)
21
22 def clone(self):
23 import qSlicerSegmentationsEditorEffectsPythonQt as effects
24
25 clonedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(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/Smoothing.png")
31 if os.path.exists(iconPath):
32 return qt.QIcon(iconPath)
33 return qt.QIcon()
34
35 def helpText(self):
36 return "<html>" + _("""Make segment boundaries smoother<br> by removing extrusions and filling small holes. The effect can be either applied locally
37(by painting in viewers) or to the whole segment (by clicking Apply button). Available methods:<p>
38<ul style="margin: 0">
39<li><b>Median:</b> removes small details while keeps smooth contours mostly unchanged. Applied to selected segment only.
40<li><b>Opening:</b> removes extrusions smaller than the specified kernel size. Applied to selected segment only.
41<li><b>Closing:</b> fills sharp corners and holes smaller than the specified kernel size. Applied to selected segment only.
42<li><b>Gaussian:</b> smoothes all contours, tends to shrink the segment. Applied to selected segment only.
43<li><b>Joint smoothing:</b> smoothes multiple segments at once, preserving watertight interface between them. Masking settings are bypassed.
44If segments overlap, segment higher in the segments table will have priority. <b>Applied to all visible segments.</b>
45</ul><p>""")
46
47 def setupOptionsFrame(self):
48 self.methodSelectorComboBox = qt.QComboBox()
49 self.methodSelectorComboBox.addItem(_("Median"), MEDIAN)
50 self.methodSelectorComboBox.addItem(_("Opening (remove extrusions)"), MORPHOLOGICAL_OPENING)
51 self.methodSelectorComboBox.addItem(_("Closing (fill holes)"), MORPHOLOGICAL_CLOSING)
52 self.methodSelectorComboBox.addItem(_("Gaussian"), GAUSSIAN)
53 self.methodSelectorComboBox.addItem(_("Joint smoothing"), JOINT_TAUBIN)
54 self.scriptedEffect.addLabeledOptionsWidget(_("Smoothing method:"), self.methodSelectorComboBox)
55
56 self.kernelSizeMMSpinBox = slicer.qMRMLSpinBox()
57 self.kernelSizeMMSpinBox.setMRMLScene(slicer.mrmlScene)
58 self.kernelSizeMMSpinBox.setToolTip(_("Diameter of the neighborhood that will be considered around each voxel. "
59 "Higher value makes smoothing stronger (more details are suppressed)."))
60 self.kernelSizeMMSpinBox.quantity = "length"
61 self.kernelSizeMMSpinBox.minimum = 0.0
62 self.kernelSizeMMSpinBox.value = 3.0
63 self.kernelSizeMMSpinBox.singleStep = 1.0
64
65 self.kernelSizePixel = qt.QLabel()
66 self.kernelSizePixel.setToolTip(_("Diameter of the neighborhood in pixel. Computed from the segment's spacing and the specified kernel size."))
67
68 kernelSizeFrame = qt.QHBoxLayout()
69 kernelSizeFrame.addWidget(self.kernelSizeMMSpinBox)
70 kernelSizeFrame.addWidget(self.kernelSizePixel)
71 self.kernelSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget(_("Kernel size:"), kernelSizeFrame)
72
73 self.gaussianStandardDeviationMMSpinBox = slicer.qMRMLSpinBox()
74 self.gaussianStandardDeviationMMSpinBox.setMRMLScene(slicer.mrmlScene)
75 self.gaussianStandardDeviationMMSpinBox.setToolTip(_("Standard deviation of the Gaussian smoothing filter coefficients. "
76 "Higher value makes smoothing stronger (more details are suppressed)."))
77 self.gaussianStandardDeviationMMSpinBox.quantity = "length"
79 self.gaussianStandardDeviationMMSpinBox.singleStep = 1.0
80 self.gaussianStandardDeviationMMLabel = self.scriptedEffect.addLabeledOptionsWidget(_("Standard deviation:"), self.gaussianStandardDeviationMMSpinBox)
81
82 self.jointTaubinSmoothingFactorSlider = ctk.ctkSliderWidget()
83 self.jointTaubinSmoothingFactorSlider.setToolTip(_("Higher value means stronger smoothing."))
84 self.jointTaubinSmoothingFactorSlider.minimum = 0.01
85 self.jointTaubinSmoothingFactorSlider.maximum = 1.0
87 self.jointTaubinSmoothingFactorSlider.singleStep = 0.01
88 self.jointTaubinSmoothingFactorSlider.pageStep = 0.1
89 self.jointTaubinSmoothingFactorLabel = self.scriptedEffect.addLabeledOptionsWidget(_("Smoothing factor:"), self.jointTaubinSmoothingFactorSlider)
90
93 _("Apply smoothing effect to all visible segments in this segmentation node. This operation may take a while."))
94 self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + "ApplyToAllVisibleSegments"
95 self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget(_("Apply to visible segments:"),
97
98 self.applyButton = qt.QPushButton(_("Apply"))
99 self.applyButton.objectName = self.__class__.__name__ + "Apply"
100 self.applyButton.setToolTip(_("Apply smoothing to selected segment"))
101 self.scriptedEffect.addOptionsWidget(self.applyButton)
102
103 self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUIupdateMRMLFromGUI)
104 self.kernelSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUIupdateMRMLFromGUI)
105 self.gaussianStandardDeviationMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUIupdateMRMLFromGUI)
106 self.jointTaubinSmoothingFactorSlider.connect("valueChanged(double)", self.updateMRMLFromGUIupdateMRMLFromGUI)
107 self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUIupdateMRMLFromGUI)
108 self.applyButton.connect("clicked()", self.onApplyonApply)
109
110 # Customize smoothing brush
111 self.scriptedEffect.setColorSmudgeCheckboxVisible(False)
112 self.paintOptionsGroupBox = ctk.ctkCollapsibleGroupBox()
113 self.paintOptionsGroupBox.setTitle(_("Smoothing brush options"))
114 self.paintOptionsGroupBox.setLayout(qt.QVBoxLayout())
115 self.paintOptionsGroupBox.layout().addWidget(self.scriptedEffect.paintOptionsFrame())
116 self.paintOptionsGroupBox.collapsed = True
117 self.scriptedEffect.addOptionsWidget(self.paintOptionsGroupBox)
118
119 def setMRMLDefaults(self):
120 self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
121 self.scriptedEffect.setParameterDefault("GaussianStandardDeviationMm", 3)
122 self.scriptedEffect.setParameterDefault("JointTaubinSmoothingFactor", 0.5)
123 self.scriptedEffect.setParameterDefault("KernelSizeMm", 3)
124 self.scriptedEffect.setParameterDefault("SmoothingMethod", MEDIAN)
125
126 def updateParameterWidgetsVisibility(self):
127 methodIndex = self.methodSelectorComboBox.currentIndex
128 smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex)
129 morphologicalMethod = (smoothingMethod == MEDIAN or smoothingMethod == MORPHOLOGICAL_OPENING or smoothingMethod == MORPHOLOGICAL_CLOSING)
130 self.kernelSizeMMLabel.setVisible(morphologicalMethod)
131 self.kernelSizeMMSpinBox.setVisible(morphologicalMethod)
132 self.kernelSizePixel.setVisible(morphologicalMethod)
133 self.gaussianStandardDeviationMMLabel.setVisible(smoothingMethod == GAUSSIAN)
134 self.gaussianStandardDeviationMMSpinBox.setVisible(smoothingMethod == GAUSSIAN)
135 self.jointTaubinSmoothingFactorLabel.setVisible(smoothingMethod == JOINT_TAUBIN)
136 self.jointTaubinSmoothingFactorSlider.setVisible(smoothingMethod == JOINT_TAUBIN)
137 self.applyToAllVisibleSegmentsLabel.setVisible(smoothingMethod != JOINT_TAUBIN)
138 self.applyToAllVisibleSegmentsCheckBox.setVisible(smoothingMethod != JOINT_TAUBIN)
139
140 def getKernelSizePixel(self):
141 selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
142 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
143 if selectedSegmentLabelmap:
144 selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
145
146 # size rounded to nearest odd number. If kernel size is even then image gets shifted.
147 kernelSizeMM = self.scriptedEffect.doubleParameter("KernelSizeMm")
148 kernelSizePixel = [int(round((kernelSizeMM / selectedSegmentLabelmapSpacing[componentIndex] + 1) / 2) * 2 - 1) for componentIndex in range(3)]
149 return kernelSizePixel
150
151 def updateGUIFromMRML(self):
152 methodIndex = self.methodSelectorComboBox.findData(self.scriptedEffect.parameter("SmoothingMethod"))
153 wasBlocked = self.methodSelectorComboBox.blockSignals(True)
154 self.methodSelectorComboBox.setCurrentIndex(methodIndex)
155 self.methodSelectorComboBox.blockSignals(wasBlocked)
156
157 wasBlocked = self.kernelSizeMMSpinBox.blockSignals(True)
158 self.setWidgetMinMaxStepFromImageSpacing(self.kernelSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
159 self.kernelSizeMMSpinBox.value = self.scriptedEffect.doubleParameter("KernelSizeMm")
160 self.kernelSizeMMSpinBox.blockSignals(wasBlocked)
161 kernelSizePixel = self.getKernelSizePixel()
162 self.kernelSizePixel.text = f"{kernelSizePixel[0]}x{kernelSizePixel[1]}x{kernelSizePixel[2]} pixel"
163
164 wasBlocked = self.gaussianStandardDeviationMMSpinBox.blockSignals(True)
166 self.gaussianStandardDeviationMMSpinBox.value = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm")
167 self.gaussianStandardDeviationMMSpinBox.blockSignals(wasBlocked)
168
169 wasBlocked = self.jointTaubinSmoothingFactorSlider.blockSignals(True)
170 self.jointTaubinSmoothingFactorSlider.value = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor")
171 self.jointTaubinSmoothingFactorSlider.blockSignals(wasBlocked)
172
173 applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
174 wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
175 self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
176 self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
177
179
180 def updateMRMLFromGUI(self):
181 methodIndex = self.methodSelectorComboBox.currentIndex
182 smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex)
183 self.scriptedEffect.setParameter("SmoothingMethod", smoothingMethod)
184 self.scriptedEffect.setParameter("KernelSizeMm", self.kernelSizeMMSpinBox.value)
185 self.scriptedEffect.setParameter("GaussianStandardDeviationMm", self.gaussianStandardDeviationMMSpinBox.value)
186 self.scriptedEffect.setParameter("JointTaubinSmoothingFactor", self.jointTaubinSmoothingFactorSlider.value)
187 applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
188 self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
189
191
192 #
193 # Effect specific methods (the above ones are the API methods to override)
194 #
195
196 def showStatusMessage(self, msg, timeoutMsec=500):
197 slicer.util.showStatusMessage(msg, timeoutMsec)
198 slicer.app.processEvents()
199
200 def onApply(self, maskImage=None, maskExtent=None):
201 """maskImage: contains nonzero where smoothing will be applied"""
202 smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
203 applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
204 if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
205
206 if smoothingMethod != JOINT_TAUBIN:
207 # Make sure the user wants to do the operation, even if the segment is not visible
208 if not self.scriptedEffect.confirmCurrentSegmentVisible():
209 return
210
211 try:
212 # This can be a long operation - indicate it to the user
213 qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
214 self.scriptedEffect.saveStateForUndo()
215
216 if smoothingMethod == JOINT_TAUBIN:
217 self.smoothMultipleSegments(maskImage, maskExtent)
218 elif applyToAllVisibleSegments:
219 # Smooth all visible segments
220 inputSegmentIDs = vtk.vtkStringArray()
221 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
222 segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
223 # store which segment was selected before operation
224 selectedStartSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
225 if inputSegmentIDs.GetNumberOfValues() == 0:
226 logging.info("Smoothing operation skipped: there are no visible segments.")
227 return
228 for index in range(inputSegmentIDs.GetNumberOfValues()):
229 segmentID = inputSegmentIDs.GetValue(index)
230 self.showStatusMessage(_("Smoothing {segmentName}...").format(segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()))
231 self.scriptedEffect.parameterSetNode().SetSelectedSegmentID(segmentID)
232 self.smoothSelectedSegment(maskImage, maskExtent)
233 # restore segment selection
234 self.scriptedEffect.parameterSetNode().SetSelectedSegmentID(selectedStartSegmentID)
235 else:
236 self.smoothSelectedSegment(maskImage, maskExtent)
237 finally:
238 qt.QApplication.restoreOverrideCursor()
239
240 def clipImage(self, inputImage, maskExtent, margin):
241 clipper = vtk.vtkImageClip()
242 clipper.SetOutputWholeExtent(maskExtent[0] - margin[0], maskExtent[1] + margin[0],
243 maskExtent[2] - margin[1], maskExtent[3] + margin[1],
244 maskExtent[4] - margin[2], maskExtent[5] + margin[2])
245 clipper.SetInputData(inputImage)
246 clipper.SetClipData(True)
247 clipper.Update()
248 clippedImage = slicer.vtkOrientedImageData()
249 clippedImage.ShallowCopy(clipper.GetOutput())
250 clippedImage.CopyDirections(inputImage)
251 return clippedImage
252
253 def modifySelectedSegmentByLabelmap(self, smoothedImage, selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent):
254 if maskImage:
255 smoothedClippedSelectedSegmentLabelmap = slicer.vtkOrientedImageData()
256 smoothedClippedSelectedSegmentLabelmap.ShallowCopy(smoothedImage)
257 smoothedClippedSelectedSegmentLabelmap.CopyDirections(modifierLabelmap)
258
259 # fill smoothed selected segment outside the painted region to 1 so that in the end the image is not modified by OPERATION_MINIMUM
260 fillValue = 1.0
261 slicer.vtkOrientedImageDataResample.ApplyImageMask(smoothedClippedSelectedSegmentLabelmap, maskImage, fillValue, False)
262 # set original segment labelmap outside painted region, solid 1 inside painted region
263 slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, selectedSegmentLabelmap,
264 slicer.vtkOrientedImageDataResample.OPERATION_MAXIMUM)
265 slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, smoothedClippedSelectedSegmentLabelmap,
266 slicer.vtkOrientedImageDataResample.OPERATION_MINIMUM)
267
268 updateExtent = [0, -1, 0, -1, 0, -1]
269 modifierExtent = modifierLabelmap.GetExtent()
270 for i in range(3):
271 updateExtent[2 * i] = min(maskExtent[2 * i], modifierExtent[2 * i])
272 updateExtent[2 * i + 1] = max(maskExtent[2 * i + 1], modifierExtent[2 * i + 1])
273
274 self.scriptedEffect.modifySelectedSegmentByLabelmap(maskImage,
275 slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet,
276 updateExtent)
277 else:
278 modifierLabelmap.DeepCopy(smoothedImage)
279 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
280
281 def smoothSelectedSegment(self, maskImage=None, maskExtent=None):
282 try:
283 # Get modifier labelmap
284 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
285 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
286
287 smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
288
289 if smoothingMethod == GAUSSIAN:
290 maxValue = 255
291 radiusFactor = 4.0
292 standardDeviationMM = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm")
293 spacing = modifierLabelmap.GetSpacing()
294 standardDeviationPixel = [1.0, 1.0, 1.0]
295 radiusPixel = [3, 3, 3]
296 for idx in range(3):
297 standardDeviationPixel[idx] = standardDeviationMM / spacing[idx]
298 radiusPixel[idx] = int(standardDeviationPixel[idx] * radiusFactor) + 1
299 if maskExtent:
300 clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, radiusPixel)
301 else:
302 clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
303
304 thresh = vtk.vtkImageThreshold()
305 thresh.SetInputData(clippedSelectedSegmentLabelmap)
306 thresh.ThresholdByLower(0)
307 thresh.SetInValue(0)
308 thresh.SetOutValue(maxValue)
309 thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
310
311 gaussianFilter = vtk.vtkImageGaussianSmooth()
312 gaussianFilter.SetInputConnection(thresh.GetOutputPort())
313 gaussianFilter.SetStandardDeviation(*standardDeviationPixel)
314 gaussianFilter.SetRadiusFactor(radiusFactor)
315
316 thresh2 = vtk.vtkImageThreshold()
317 thresh2.SetInputConnection(gaussianFilter.GetOutputPort())
318 thresh2.ThresholdByUpper(int(maxValue / 2))
319 thresh2.SetInValue(1)
320 thresh2.SetOutValue(0)
321 thresh2.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
322 thresh2.Update()
323
324 self.modifySelectedSegmentByLabelmap(thresh2.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)
325
326 else:
327 # size rounded to nearest odd number. If kernel size is even then image gets shifted.
328 kernelSizePixel = self.getKernelSizePixel()
329
330 if maskExtent:
331 clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, kernelSizePixel)
332 else:
333 clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
334
335 if smoothingMethod == MEDIAN:
336 # Median filter does not require a particular label value
337 smoothingFilter = vtk.vtkImageMedian3D()
338 smoothingFilter.SetInputData(clippedSelectedSegmentLabelmap)
339
340 else:
341 # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
342 labelValue = 1
343 backgroundValue = 0
344 thresh = vtk.vtkImageThreshold()
345 thresh.SetInputData(clippedSelectedSegmentLabelmap)
346 thresh.ThresholdByLower(0)
347 thresh.SetInValue(backgroundValue)
348 thresh.SetOutValue(labelValue)
349 thresh.SetOutputScalarType(clippedSelectedSegmentLabelmap.GetScalarType())
350
351 smoothingFilter = vtk.vtkImageOpenClose3D()
352 smoothingFilter.SetInputConnection(thresh.GetOutputPort())
353 if smoothingMethod == MORPHOLOGICAL_OPENING:
354 smoothingFilter.SetOpenValue(labelValue)
355 smoothingFilter.SetCloseValue(backgroundValue)
356 else: # must be smoothingMethod == MORPHOLOGICAL_CLOSING:
357 smoothingFilter.SetOpenValue(backgroundValue)
358 smoothingFilter.SetCloseValue(labelValue)
359
360 smoothingFilter.SetKernelSize(kernelSizePixel[0], kernelSizePixel[1], kernelSizePixel[2])
361 smoothingFilter.Update()
362
363 self.modifySelectedSegmentByLabelmap(smoothingFilter.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)
364
365 except IndexError:
366 logging.error("apply: Failed to apply smoothing")
367
368 def smoothMultipleSegments(self, maskImage=None, maskExtent=None):
369 import vtkSegmentationCorePython as vtkSegmentationCore
370
371 self.showStatusMessage(_("Joint smoothing ..."))
372 # Generate merged labelmap of all visible segments
373 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
374 visibleSegmentIds = vtk.vtkStringArray()
375 segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
376 if visibleSegmentIds.GetNumberOfValues() == 0:
377 logging.info("Smoothing operation skipped: there are no visible segments")
378 return
379
380 mergedImage = slicer.vtkOrientedImageData()
381 if not segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage,
382 vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
383 None, visibleSegmentIds):
384 logging.error("Failed to apply smoothing: cannot get list of visible segments")
385 return
386
387 segmentLabelValues = [] # list of [segmentId, labelValue]
388 for i in range(visibleSegmentIds.GetNumberOfValues()):
389 segmentId = visibleSegmentIds.GetValue(i)
390 segmentLabelValues.append([segmentId, i + 1])
391
392 # Perform smoothing in voxel space
393 ici = vtk.vtkImageChangeInformation()
394 ici.SetInputData(mergedImage)
395 ici.SetOutputSpacing(1, 1, 1)
396 ici.SetOutputOrigin(0, 0, 0)
397
398 # Convert labelmap to combined polydata
399 # vtkDiscreteFlyingEdges3D cannot be used here, as in the output of that filter,
400 # each labeled region is completely disconnected from neighboring regions, and
401 # for joint smoothing it is essential for the points to move together.
402 convertToPolyData = vtk.vtkDiscreteMarchingCubes()
403 convertToPolyData.SetInputConnection(ici.GetOutputPort())
404 convertToPolyData.SetNumberOfContours(len(segmentLabelValues))
405
406 contourIndex = 0
407 for segmentId, labelValue in segmentLabelValues:
408 convertToPolyData.SetValue(contourIndex, labelValue)
409 contourIndex += 1
410
411 # Low-pass filtering using Taubin's method
412 smoothingFactor = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor")
413 smoothingIterations = 100 # according to VTK documentation 10-20 iterations could be enough but we use a higher value to reduce chance of shrinking
414 passBand = pow(10.0, -4.0 * smoothingFactor) # gives a nice range of 1-0.0001 from a user input of 0-1
415 smoother = vtk.vtkWindowedSincPolyDataFilter()
416 smoother.SetInputConnection(convertToPolyData.GetOutputPort())
417 smoother.SetNumberOfIterations(smoothingIterations)
418 smoother.BoundarySmoothingOff()
419 smoother.FeatureEdgeSmoothingOff()
420 smoother.SetFeatureAngle(90.0)
421 smoother.SetPassBand(passBand)
422 smoother.NonManifoldSmoothingOn()
423 smoother.NormalizeCoordinatesOn()
424
425 # Extract a label
426 threshold = vtk.vtkThreshold()
427 threshold.SetInputConnection(smoother.GetOutputPort())
428
429 # Convert to polydata
430 geometryFilter = vtk.vtkGeometryFilter()
431 geometryFilter.SetInputConnection(threshold.GetOutputPort())
432
433 # Convert polydata to stencil
434 polyDataToImageStencil = vtk.vtkPolyDataToImageStencil()
435 polyDataToImageStencil.SetInputConnection(geometryFilter.GetOutputPort())
436 polyDataToImageStencil.SetOutputSpacing(1, 1, 1)
437 polyDataToImageStencil.SetOutputOrigin(0, 0, 0)
438 polyDataToImageStencil.SetOutputWholeExtent(mergedImage.GetExtent())
439
440 # Convert stencil to image
441 stencil = vtk.vtkImageStencil()
442 emptyBinaryLabelMap = vtk.vtkImageData()
443 emptyBinaryLabelMap.SetExtent(mergedImage.GetExtent())
444 emptyBinaryLabelMap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
445 vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(emptyBinaryLabelMap, 0)
446 stencil.SetInputData(emptyBinaryLabelMap)
447 stencil.SetStencilConnection(polyDataToImageStencil.GetOutputPort())
448 stencil.ReverseStencilOn()
449 stencil.SetBackgroundValue(1) # General foreground value is 1 (background value because of reverse stencil)
450
451 imageToWorldMatrix = vtk.vtkMatrix4x4()
452 mergedImage.GetImageToWorldMatrix(imageToWorldMatrix)
453
454 # TODO: Temporarily setting the overwrite mode to OverwriteVisibleSegments is an approach that should be change once additional
455 # layer control options have been implemented. Users may wish to keep segments on separate layers, and not allow them to be
456 # separated/merged automatically. This effect could leverage those options once they have been implemented.
457 oldOverwriteMode = self.scriptedEffect.parameterSetNode().GetOverwriteMode()
458 self.scriptedEffect.parameterSetNode().SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteVisibleSegments)
459 for segmentId, labelValue in segmentLabelValues:
460 threshold.SetLowerThreshold(labelValue)
461 threshold.SetUpperThreshold(labelValue)
462 threshold.SetThresholdFunction(vtk.vtkThreshold.THRESHOLD_BETWEEN)
463 stencil.Update()
464 smoothedBinaryLabelMap = slicer.vtkOrientedImageData()
465 smoothedBinaryLabelMap.ShallowCopy(stencil.GetOutput())
466 smoothedBinaryLabelMap.SetImageToWorldMatrix(imageToWorldMatrix)
467 self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentId, smoothedBinaryLabelMap,
468 slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, False)
469 self.scriptedEffect.parameterSetNode().SetOverwriteMode(oldOverwriteMode)
470
471 def paintApply(self, viewWidget):
472 # Current limitation: smoothing brush is not implemented for joint smoothing
473 smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
474 if smoothingMethod == JOINT_TAUBIN:
475 self.scriptedEffect.clearBrushes()
476 self.scriptedEffect.forceRender(viewWidget)
477 slicer.util.messageBox(_("Smoothing brush is not available for 'joint smoothing' method."))
478 return
479
480 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
481 maskImage = slicer.vtkOrientedImageData()
482 maskImage.DeepCopy(modifierLabelmap)
483 maskExtent = self.scriptedEffect.paintBrushesIntoLabelmap(maskImage, viewWidget)
484 self.scriptedEffect.clearBrushes()
485 self.scriptedEffect.forceRender(viewWidget)
486 if maskExtent[0] > maskExtent[1] or maskExtent[2] > maskExtent[3] or maskExtent[4] > maskExtent[5]:
487 return
488
489 self.scriptedEffect.saveStateForUndo()
490 self.onApplyonApply(maskImage, maskExtent)
491
492
493MEDIAN = "MEDIAN"
494GAUSSIAN = "GAUSSIAN"
495MORPHOLOGICAL_OPENING = "MORPHOLOGICAL_OPENING"
496MORPHOLOGICAL_CLOSING = "MORPHOLOGICAL_CLOSING"
497JOINT_TAUBIN = "JOINT_TAUBIN"
modifySelectedSegmentByLabelmap(self, smoothedImage, selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)