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