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
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.updateMRMLFromGUI)
104 self.kernelSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
105 self.gaussianStandardDeviationMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
106 self.jointTaubinSmoothingFactorSlider.connect("valueChanged(double)", self.updateMRMLFromGUI)
107 self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
108 self.applyButton.connect("clicked()", self.onApply)
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(
231 segmentName=segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()))
232 self.scriptedEffect.parameterSetNode().SetSelectedSegmentID(segmentID)
233 self.smoothSelectedSegment(maskImage, maskExtent)
234 # restore segment selection
235 self.scriptedEffect.parameterSetNode().SetSelectedSegmentID(selectedStartSegmentID)
236 else:
237 self.smoothSelectedSegment(maskImage, maskExtent)
238 finally:
239 qt.QApplication.restoreOverrideCursor()
240
241 def clipImage(self, inputImage, maskExtent, margin):
242 clipper = vtk.vtkImageClip()
243 clipper.SetOutputWholeExtent(maskExtent[0] - margin[0], maskExtent[1] + margin[0],
244 maskExtent[2] - margin[1], maskExtent[3] + margin[1],
245 maskExtent[4] - margin[2], maskExtent[5] + margin[2])
246 clipper.SetInputData(inputImage)
247 clipper.SetClipData(True)
248 clipper.Update()
249 clippedImage = slicer.vtkOrientedImageData()
250 clippedImage.ShallowCopy(clipper.GetOutput())
251 clippedImage.CopyDirections(inputImage)
252 return clippedImage
253
254 def modifySelectedSegmentByLabelmap(self, smoothedImage, selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent):
255 if maskImage:
256 smoothedClippedSelectedSegmentLabelmap = slicer.vtkOrientedImageData()
257 smoothedClippedSelectedSegmentLabelmap.ShallowCopy(smoothedImage)
258 smoothedClippedSelectedSegmentLabelmap.CopyDirections(modifierLabelmap)
259
260 # fill smoothed selected segment outside the painted region to 1 so that in the end the image is not modified by OPERATION_MINIMUM
261 fillValue = 1.0
262 slicer.vtkOrientedImageDataResample.ApplyImageMask(smoothedClippedSelectedSegmentLabelmap, maskImage, fillValue, False)
263 # set original segment labelmap outside painted region, solid 1 inside painted region
264 slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, selectedSegmentLabelmap,
265 slicer.vtkOrientedImageDataResample.OPERATION_MAXIMUM)
266 slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, smoothedClippedSelectedSegmentLabelmap,
267 slicer.vtkOrientedImageDataResample.OPERATION_MINIMUM)
268
269 updateExtent = [0, -1, 0, -1, 0, -1]
270 modifierExtent = modifierLabelmap.GetExtent()
271 for i in range(3):
272 updateExtent[2 * i] = min(maskExtent[2 * i], modifierExtent[2 * i])
273 updateExtent[2 * i + 1] = max(maskExtent[2 * i + 1], modifierExtent[2 * i + 1])
274
275 self.scriptedEffect.modifySelectedSegmentByLabelmap(maskImage,
276 slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet,
277 updateExtent)
278 else:
279 modifierLabelmap.DeepCopy(smoothedImage)
280 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
281
282 def smoothSelectedSegment(self, maskImage=None, maskExtent=None):
283 try:
284 # Get modifier labelmap
285 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
286 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
287
288 smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
289
290 if smoothingMethod == GAUSSIAN:
291 maxValue = 255
292 radiusFactor = 4.0
293 standardDeviationMM = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm")
294 spacing = modifierLabelmap.GetSpacing()
295 standardDeviationPixel = [1.0, 1.0, 1.0]
296 radiusPixel = [3, 3, 3]
297 for idx in range(3):
298 standardDeviationPixel[idx] = standardDeviationMM / spacing[idx]
299 radiusPixel[idx] = int(standardDeviationPixel[idx] * radiusFactor) + 1
300 if maskExtent:
301 clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, radiusPixel)
302 else:
303 clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
304
305 thresh = vtk.vtkImageThreshold()
306 thresh.SetInputData(clippedSelectedSegmentLabelmap)
307 thresh.ThresholdByLower(0)
308 thresh.SetInValue(0)
309 thresh.SetOutValue(maxValue)
310 thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
311
312 gaussianFilter = vtk.vtkImageGaussianSmooth()
313 gaussianFilter.SetInputConnection(thresh.GetOutputPort())
314 gaussianFilter.SetStandardDeviation(*standardDeviationPixel)
315 gaussianFilter.SetRadiusFactor(radiusFactor)
316
317 thresh2 = vtk.vtkImageThreshold()
318 thresh2.SetInputConnection(gaussianFilter.GetOutputPort())
319 thresh2.ThresholdByUpper(int(maxValue / 2))
320 thresh2.SetInValue(1)
321 thresh2.SetOutValue(0)
322 thresh2.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
323 thresh2.Update()
324
325 self.modifySelectedSegmentByLabelmap(thresh2.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)
326
327 else:
328 # size rounded to nearest odd number. If kernel size is even then image gets shifted.
329 kernelSizePixel = self.getKernelSizePixel()
330
331 if maskExtent:
332 clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, kernelSizePixel)
333 else:
334 clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
335
336 if smoothingMethod == MEDIAN:
337 # Median filter does not require a particular label value
338 smoothingFilter = vtk.vtkImageMedian3D()
339 smoothingFilter.SetInputData(clippedSelectedSegmentLabelmap)
340
341 else:
342 # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
343 labelValue = 1
344 backgroundValue = 0
345 thresh = vtk.vtkImageThreshold()
346 thresh.SetInputData(clippedSelectedSegmentLabelmap)
347 thresh.ThresholdByLower(0)
348 thresh.SetInValue(backgroundValue)
349 thresh.SetOutValue(labelValue)
350 thresh.SetOutputScalarType(clippedSelectedSegmentLabelmap.GetScalarType())
351
352 smoothingFilter = vtk.vtkImageOpenClose3D()
353 smoothingFilter.SetInputConnection(thresh.GetOutputPort())
354 if smoothingMethod == MORPHOLOGICAL_OPENING:
355 smoothingFilter.SetOpenValue(labelValue)
356 smoothingFilter.SetCloseValue(backgroundValue)
357 else: # must be smoothingMethod == MORPHOLOGICAL_CLOSING:
358 smoothingFilter.SetOpenValue(backgroundValue)
359 smoothingFilter.SetCloseValue(labelValue)
360
361 smoothingFilter.SetKernelSize(kernelSizePixel[0], kernelSizePixel[1], kernelSizePixel[2])
362 smoothingFilter.Update()
363
364 self.modifySelectedSegmentByLabelmap(smoothingFilter.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)
365
366 except IndexError:
367 logging.error("apply: Failed to apply smoothing")
368
369 def smoothMultipleSegments(self, maskImage=None, maskExtent=None):
370 import vtkSegmentationCorePython as vtkSegmentationCore
371
372 self.showStatusMessage(_("Joint smoothing ..."))
373 # Generate merged labelmap of all visible segments
374 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
375 visibleSegmentIds = vtk.vtkStringArray()
376 segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
377 if visibleSegmentIds.GetNumberOfValues() == 0:
378 logging.info("Smoothing operation skipped: there are no visible segments")
379 return
380
381 mergedImage = slicer.vtkOrientedImageData()
382 if not segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage,
383 vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
384 None, visibleSegmentIds):
385 logging.error("Failed to apply smoothing: cannot get list of visible segments")
386 return
387
388 segmentLabelValues = [] # list of [segmentId, labelValue]
389 for i in range(visibleSegmentIds.GetNumberOfValues()):
390 segmentId = visibleSegmentIds.GetValue(i)
391 segmentLabelValues.append([segmentId, i + 1])
392
393 # Perform smoothing in voxel space
394 ici = vtk.vtkImageChangeInformation()
395 ici.SetInputData(mergedImage)
396 ici.SetOutputSpacing(1, 1, 1)
397 ici.SetOutputOrigin(0, 0, 0)
398
399 # Convert labelmap to combined polydata
400 # vtkDiscreteFlyingEdges3D cannot be used here, as in the output of that filter,
401 # each labeled region is completely disconnected from neighboring regions, and
402 # for joint smoothing it is essential for the points to move together.
403 convertToPolyData = vtk.vtkDiscreteMarchingCubes()
404 convertToPolyData.SetInputConnection(ici.GetOutputPort())
405 convertToPolyData.SetNumberOfContours(len(segmentLabelValues))
406
407 contourIndex = 0
408 for segmentId, labelValue in segmentLabelValues:
409 convertToPolyData.SetValue(contourIndex, labelValue)
410 contourIndex += 1
411
412 # Low-pass filtering using Taubin's method
413 smoothingFactor = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor")
414 smoothingIterations = 100 # according to VTK documentation 10-20 iterations could be enough but we use a higher value to reduce chance of shrinking
415 passBand = pow(10.0, -4.0 * smoothingFactor) # gives a nice range of 1-0.0001 from a user input of 0-1
416 smoother = vtk.vtkWindowedSincPolyDataFilter()
417 smoother.SetInputConnection(convertToPolyData.GetOutputPort())
418 smoother.SetNumberOfIterations(smoothingIterations)
419 smoother.BoundarySmoothingOff()
420 smoother.FeatureEdgeSmoothingOff()
421 smoother.SetFeatureAngle(90.0)
422 smoother.SetPassBand(passBand)
423 smoother.NonManifoldSmoothingOn()
424 smoother.NormalizeCoordinatesOn()
425
426 # Extract a label
427 threshold = vtk.vtkThreshold()
428 threshold.SetInputConnection(smoother.GetOutputPort())
429
430 # Convert to polydata
431 geometryFilter = vtk.vtkGeometryFilter()
432 geometryFilter.SetInputConnection(threshold.GetOutputPort())
433
434 # Convert polydata to stencil
435 polyDataToImageStencil = vtk.vtkPolyDataToImageStencil()
436 polyDataToImageStencil.SetInputConnection(geometryFilter.GetOutputPort())
437 polyDataToImageStencil.SetOutputSpacing(1, 1, 1)
438 polyDataToImageStencil.SetOutputOrigin(0, 0, 0)
439 polyDataToImageStencil.SetOutputWholeExtent(mergedImage.GetExtent())
440
441 # Convert stencil to image
442 stencil = vtk.vtkImageStencil()
443 emptyBinaryLabelMap = vtk.vtkImageData()
444 emptyBinaryLabelMap.SetExtent(mergedImage.GetExtent())
445 emptyBinaryLabelMap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
446 vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(emptyBinaryLabelMap, 0)
447 stencil.SetInputData(emptyBinaryLabelMap)
448 stencil.SetStencilConnection(polyDataToImageStencil.GetOutputPort())
449 stencil.ReverseStencilOn()
450 stencil.SetBackgroundValue(1) # General foreground value is 1 (background value because of reverse stencil)
451
452 imageToWorldMatrix = vtk.vtkMatrix4x4()
453 mergedImage.GetImageToWorldMatrix(imageToWorldMatrix)
454
455 # TODO: Temporarily setting the overwrite mode to OverwriteVisibleSegments is an approach that should be change once additional
456 # layer control options have been implemented. Users may wish to keep segments on separate layers, and not allow them to be
457 # separated/merged automatically. This effect could leverage those options once they have been implemented.
458 oldOverwriteMode = self.scriptedEffect.parameterSetNode().GetOverwriteMode()
459 self.scriptedEffect.parameterSetNode().SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteVisibleSegments)
460 for segmentId, labelValue in segmentLabelValues:
461 threshold.SetLowerThreshold(labelValue)
462 threshold.SetUpperThreshold(labelValue)
463 threshold.SetThresholdFunction(vtk.vtkThreshold.THRESHOLD_BETWEEN)
464 stencil.Update()
465 smoothedBinaryLabelMap = slicer.vtkOrientedImageData()
466 smoothedBinaryLabelMap.ShallowCopy(stencil.GetOutput())
467 smoothedBinaryLabelMap.SetImageToWorldMatrix(imageToWorldMatrix)
468 self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentId, smoothedBinaryLabelMap,
469 slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, False)
470 self.scriptedEffect.parameterSetNode().SetOverwriteMode(oldOverwriteMode)
471
472 def paintApply(self, viewWidget):
473 # Current limitation: smoothing brush is not implemented for joint smoothing
474 smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
475 if smoothingMethod == JOINT_TAUBIN:
476 self.scriptedEffect.clearBrushes()
477 self.scriptedEffect.forceRender(viewWidget)
478 slicer.util.messageBox(_("Smoothing brush is not available for 'joint smoothing' method."))
479 return
480
481 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
482 maskImage = slicer.vtkOrientedImageData()
483 maskImage.DeepCopy(modifierLabelmap)
484 maskExtent = self.scriptedEffect.paintBrushesIntoLabelmap(maskImage, viewWidget)
485 self.scriptedEffect.clearBrushes()
486 self.scriptedEffect.forceRender(viewWidget)
487 if maskExtent[0] > maskExtent[1] or maskExtent[2] > maskExtent[3] or maskExtent[4] > maskExtent[5]:
488 return
489
490 self.scriptedEffect.saveStateForUndo()
491 self.onApply(maskImage, maskExtent)
492
493
494MEDIAN = "MEDIAN"
495GAUSSIAN = "GAUSSIAN"
496MORPHOLOGICAL_OPENING = "MORPHOLOGICAL_OPENING"
497MORPHOLOGICAL_CLOSING = "MORPHOLOGICAL_CLOSING"
498JOINT_TAUBIN = "JOINT_TAUBIN"
modifySelectedSegmentByLabelmap(self, smoothedImage, selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)