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