Slicer 5.9
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.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)