Slicer  5.0
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
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 all 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.ThresholdBetween(labelValue, labelValue)
460  stencil.Update()
461  smoothedBinaryLabelMap = slicer.vtkOrientedImageData()
462  smoothedBinaryLabelMap.ShallowCopy(stencil.GetOutput())
463  smoothedBinaryLabelMap.SetImageToWorldMatrix(imageToWorldMatrix)
464  self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentId, smoothedBinaryLabelMap,
465  slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, False)
466  self.scriptedEffect.parameterSetNode().SetOverwriteMode(oldOverwriteMode)
467 
468  def paintApply(self, viewWidget):
469 
470  # Current limitation: smoothing brush is not implemented for joint smoothing
471  smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
472  if smoothingMethod == JOINT_TAUBIN:
473  self.scriptedEffect.clearBrushes()
474  self.scriptedEffect.forceRender(viewWidget)
475  slicer.util.messageBox("Smoothing brush is not available for 'joint smoothing' method.")
476  return
477 
478  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
479  maskImage = slicer.vtkOrientedImageData()
480  maskImage.DeepCopy(modifierLabelmap)
481  maskExtent = self.scriptedEffect.paintBrushesIntoLabelmap(maskImage, viewWidget)
482  self.scriptedEffect.clearBrushes()
483  self.scriptedEffect.forceRender(viewWidget)
484  if maskExtent[0] > maskExtent[1] or maskExtent[2] > maskExtent[3] or maskExtent[4] > maskExtent[5]:
485  return
486 
487  self.scriptedEffect.saveStateForUndo()
488  self.onApply(maskImage, maskExtent)
489 
490 
491 MEDIAN = 'MEDIAN'
492 GAUSSIAN = 'GAUSSIAN'
493 MORPHOLOGICAL_OPENING = 'MORPHOLOGICAL_OPENING'
494 MORPHOLOGICAL_CLOSING = 'MORPHOLOGICAL_CLOSING'
495 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)