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