Slicer  4.11
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 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  AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
13 
14  def clone(self):
15  import qSlicerSegmentationsEditorEffectsPythonQt as effects
16  clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(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. Available methods:<p>
28 <ul style="margin: 0">
29 <li><b>Median:</b> removes small details while keeps smooth contours mostly unchanged. Applied to selected segment only.</li>
30 <li><b>Opening:</b> removes extrusions smaller than the specified kernel size. Applied to selected segment only.</li>
31 <li><b>Closing:</b> fills sharp corners and holes smaller than the specified kernel size. Applied to selected segment only.</li>
32 <li><b>Gaussian:</b> smoothes all contours, tends to shrink the segment. Applied to selected segment only.</li>
33 <li><b>Joint smoothing:</b> smoothes multiple segments at once, preserving watertight interface between them. Masking settings are bypassed.
34 If segments overlap, segment higher in the segments table will have priority. <b>Applied to all visible segments.</b></li>
35 </ul><p></html>"""
36 
37  def setupOptionsFrame(self):
38 
39  self.methodSelectorComboBox = qt.QComboBox()
40  self.methodSelectorComboBox.addItem("Median", MEDIAN)
41  self.methodSelectorComboBox.addItem("Opening (remove extrusions)", MORPHOLOGICAL_OPENING)
42  self.methodSelectorComboBox.addItem("Closing (fill holes)", MORPHOLOGICAL_CLOSING)
43  self.methodSelectorComboBox.addItem("Gaussian", GAUSSIAN)
44  self.methodSelectorComboBox.addItem("Joint smoothing", JOINT_TAUBIN)
45  self.scriptedEffect.addLabeledOptionsWidget("Smoothing method:", self.methodSelectorComboBox)
46 
47  self.kernelSizeMmSpinBox = slicer.qMRMLSpinBox()
48  self.kernelSizeMmSpinBox.setMRMLScene(slicer.mrmlScene)
49  self.kernelSizeMmSpinBox.setToolTip("Diameter of the neighborhood that will be considered around each voxel. Higher value makes smoothing stronger (more details are suppressed).")
50  self.kernelSizeMmSpinBox.quantity = "length"
51  self.kernelSizeMmSpinBox.minimum = 0.0
52  self.kernelSizeMmSpinBox.value = 3.0
53  self.kernelSizeMmSpinBox.singleStep = 1.0
54 
55  self.kernelSizePixel = qt.QLabel()
56  self.kernelSizePixel.setToolTip("Diameter of the neighborhood in pixels. Computed from the segment's spacing and the specified kernel size.")
57 
58  kernelSizeFrame = qt.QHBoxLayout()
59  kernelSizeFrame.addWidget(self.kernelSizeMmSpinBox)
60  kernelSizeFrame.addWidget(self.kernelSizePixel)
61  self.kernelSizeMmLabel = self.scriptedEffect.addLabeledOptionsWidget("Kernel size:", kernelSizeFrame)
62 
63  self.gaussianStandardDeviationMmSpinBox = slicer.qMRMLSpinBox()
64  self.gaussianStandardDeviationMmSpinBox.setMRMLScene(slicer.mrmlScene)
65  self.gaussianStandardDeviationMmSpinBox.setToolTip("Standard deviation of the Gaussian smoothing filter coefficients. Higher value makes smoothing stronger (more details are suppressed).")
66  self.gaussianStandardDeviationMmSpinBox.quantity = "length"
67  self.gaussianStandardDeviationMmSpinBox.value = 3.0
68  self.gaussianStandardDeviationMmSpinBox.singleStep = 1.0
69  self.gaussianStandardDeviationMmLabel = self.scriptedEffect.addLabeledOptionsWidget("Standard deviation:", self.gaussianStandardDeviationMmSpinBox)
70 
71  self.jointTaubinSmoothingFactorSlider = ctk.ctkSliderWidget()
72  self.jointTaubinSmoothingFactorSlider.setToolTip("Higher value means stronger smoothing.")
73  self.jointTaubinSmoothingFactorSlider.minimum = 0.01
74  self.jointTaubinSmoothingFactorSlider.maximum = 1.0
75  self.jointTaubinSmoothingFactorSlider.value = 0.5
76  self.jointTaubinSmoothingFactorSlider.singleStep = 0.01
77  self.jointTaubinSmoothingFactorSlider.pageStep = 0.1
78  self.jointTaubinSmoothingFactorLabel = self.scriptedEffect.addLabeledOptionsWidget("Smoothing factor:", self.jointTaubinSmoothingFactorSlider)
79 
80  self.applyButton = qt.QPushButton("Apply")
81  self.applyButton.objectName = self.__class__.__name__ + 'Apply'
82  self.applyButton.setToolTip("Apply smoothing to selected segment")
83  self.scriptedEffect.addOptionsWidget(self.applyButton)
84 
85  self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI)
86  self.kernelSizeMmSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
87  self.gaussianStandardDeviationMmSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
88  self.jointTaubinSmoothingFactorSlider.connect("valueChanged(double)", self.updateMRMLFromGUI)
89  self.applyButton.connect('clicked()', self.onApply)
90 
91  def createCursor(self, widget):
92  # Turn off effect-specific cursor for this effect
93  return slicer.util.mainWindow().cursor
94 
95  def setMRMLDefaults(self):
96  self.scriptedEffect.setParameterDefault("SmoothingMethod", MEDIAN)
97  self.scriptedEffect.setParameterDefault("KernelSizeMm", 3)
98  self.scriptedEffect.setParameterDefault("GaussianStandardDeviationMm", 3)
99  self.scriptedEffect.setParameterDefault("JointTaubinSmoothingFactor", 0.5)
100 
102  methodIndex = self.methodSelectorComboBox.currentIndex
103  smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex)
104  morphologicalMethod = (smoothingMethod==MEDIAN or smoothingMethod==MORPHOLOGICAL_OPENING or smoothingMethod==MORPHOLOGICAL_CLOSING)
105  self.kernelSizeMmLabel.setVisible(morphologicalMethod)
106  self.kernelSizeMmSpinBox.setVisible(morphologicalMethod)
107  self.kernelSizePixel.setVisible(morphologicalMethod)
108  self.gaussianStandardDeviationMmLabel.setVisible(smoothingMethod==GAUSSIAN)
109  self.gaussianStandardDeviationMmSpinBox.setVisible(smoothingMethod==GAUSSIAN)
110  self.jointTaubinSmoothingFactorLabel.setVisible(smoothingMethod==JOINT_TAUBIN)
111  self.jointTaubinSmoothingFactorSlider.setVisible(smoothingMethod==JOINT_TAUBIN)
112 
114  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
115  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
116  if selectedSegmentLabelmap:
117  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
118 
119  # size rounded to nearest odd number. If kernel size is even then image gets shifted.
120  kernelSizeMm = self.scriptedEffect.doubleParameter("KernelSizeMm")
121  kernelSizePixel = [int(round((kernelSizeMm / selectedSegmentLabelmapSpacing[componentIndex]+1)/2)*2-1) for componentIndex in range(3)]
122  return kernelSizePixel
123 
124  def updateGUIFromMRML(self):
125  methodIndex = self.methodSelectorComboBox.findData(self.scriptedEffect.parameter("SmoothingMethod"))
126  wasBlocked = self.methodSelectorComboBox.blockSignals(True)
127  self.methodSelectorComboBox.setCurrentIndex(methodIndex)
128  self.methodSelectorComboBox.blockSignals(wasBlocked)
129 
130  wasBlocked = self.kernelSizeMmSpinBox.blockSignals(True)
131  self.setWidgetMinMaxStepFromImageSpacing(self.kernelSizeMmSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
132  self.kernelSizeMmSpinBox.value = self.scriptedEffect.doubleParameter("KernelSizeMm")
133  self.kernelSizeMmSpinBox.blockSignals(wasBlocked)
134  kernelSizePixel = self.getKernelSizePixel()
135  self.kernelSizePixel.text = "{0}x{1}x{2} pixels".format(kernelSizePixel[0], kernelSizePixel[1], kernelSizePixel[2])
136 
137  wasBlocked = self.gaussianStandardDeviationMmSpinBox.blockSignals(True)
138  self.setWidgetMinMaxStepFromImageSpacing(self.gaussianStandardDeviationMmSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
139  self.gaussianStandardDeviationMmSpinBox.value = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm")
140  self.gaussianStandardDeviationMmSpinBox.blockSignals(wasBlocked)
141 
142  wasBlocked = self.jointTaubinSmoothingFactorSlider.blockSignals(True)
143  self.jointTaubinSmoothingFactorSlider.value = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor")
144  self.jointTaubinSmoothingFactorSlider.blockSignals(wasBlocked)
145 
147 
148  def updateMRMLFromGUI(self):
149  methodIndex = self.methodSelectorComboBox.currentIndex
150  smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex)
151  self.scriptedEffect.setParameter("SmoothingMethod", smoothingMethod)
152  self.scriptedEffect.setParameter("KernelSizeMm", self.kernelSizeMmSpinBox.value)
153  self.scriptedEffect.setParameter("GaussianStandardDeviationMm", self.gaussianStandardDeviationMmSpinBox.value)
154  self.scriptedEffect.setParameter("JointTaubinSmoothingFactor", self.jointTaubinSmoothingFactorSlider.value)
155 
157 
158  #
159  # Effect specific methods (the above ones are the API methods to override)
160  #
161 
162  def onApply(self):
163  try:
164  # This can be a long operation - indicate it to the user
165  qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
166 
167  self.scriptedEffect.saveStateForUndo()
168 
169  smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
170  if smoothingMethod == JOINT_TAUBIN:
172  else:
173  self.smoothSelectedSegment()
174  finally:
175  qt.QApplication.restoreOverrideCursor()
176 
178  try:
179 
180  # Get master volume image data
181  import vtkSegmentationCorePython
182 
183  # Get modifier labelmap
184  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
185  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
186 
187  smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
188 
189  if smoothingMethod == GAUSSIAN:
190  maxValue = 255
191 
192  thresh = vtk.vtkImageThreshold()
193  thresh.SetInputData(selectedSegmentLabelmap)
194  thresh.ThresholdByLower(0)
195  thresh.SetInValue(0)
196  thresh.SetOutValue(maxValue)
197  thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
198 
199  standardDeviationMm = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm")
200  gaussianFilter = vtk.vtkImageGaussianSmooth()
201  gaussianFilter.SetInputConnection(thresh.GetOutputPort())
202  gaussianFilter.SetStandardDeviation(standardDeviationMm)
203  gaussianFilter.SetRadiusFactor(4)
204 
205  thresh2 = vtk.vtkImageThreshold()
206  thresh2.SetInputConnection(gaussianFilter.GetOutputPort())
207  thresh2.ThresholdByUpper(int(maxValue / 2))
208  thresh2.SetInValue(1)
209  thresh2.SetOutValue(0)
210  thresh2.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
211  thresh2.Update()
212  modifierLabelmap.DeepCopy(thresh2.GetOutput())
213 
214  else:
215  # size rounded to nearest odd number. If kernel size is even then image gets shifted.
216  kernelSizePixel = self.getKernelSizePixel()
217 
218  if smoothingMethod == MEDIAN:
219  # Median filter does not require a particular label value
220  smoothingFilter = vtk.vtkImageMedian3D()
221  smoothingFilter.SetInputData(selectedSegmentLabelmap)
222 
223  else:
224  # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
225  labelValue = 1
226  backgroundValue = 0
227  thresh = vtk.vtkImageThreshold()
228  thresh.SetInputData(selectedSegmentLabelmap)
229  thresh.ThresholdByLower(0)
230  thresh.SetInValue(backgroundValue)
231  thresh.SetOutValue(labelValue)
232  thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
233 
234  smoothingFilter = vtk.vtkImageOpenClose3D()
235  smoothingFilter.SetInputConnection(thresh.GetOutputPort())
236  if smoothingMethod == MORPHOLOGICAL_OPENING:
237  smoothingFilter.SetOpenValue(labelValue)
238  smoothingFilter.SetCloseValue(backgroundValue)
239  else: # must be smoothingMethod == MORPHOLOGICAL_CLOSING:
240  smoothingFilter.SetOpenValue(backgroundValue)
241  smoothingFilter.SetCloseValue(labelValue)
242 
243  smoothingFilter.SetKernelSize(kernelSizePixel[0],kernelSizePixel[1],kernelSizePixel[2])
244  smoothingFilter.Update()
245  modifierLabelmap.DeepCopy(smoothingFilter.GetOutput())
246 
247  except IndexError:
248  logging.error('apply: Failed to apply smoothing')
249 
250  # Apply changes
251  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
252 
254  import vtkSegmentationCorePython as vtkSegmentationCore
255 
256  # Generate merged labelmap of all visible segments
257  segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
258  visibleSegmentIds = vtk.vtkStringArray()
259  segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
260  if visibleSegmentIds.GetNumberOfValues() == 0:
261  logging.info("Smoothing operation skipped: there are no visible segments")
262  return
263 
264  mergedImage = slicer.vtkOrientedImageData()
265  if not segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage,
266  vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
267  None, visibleSegmentIds):
268  logging.error('Failed to apply smoothing: cannot get list of visible segments')
269  return
270 
271  segmentLabelValues = [] # list of [segmentId, labelValue]
272  for i in range(visibleSegmentIds.GetNumberOfValues()):
273  segmentId = visibleSegmentIds.GetValue(i)
274  segmentLabelValues.append([segmentId, i+1])
275 
276  # Perform smoothing in voxel space
277  ici = vtk.vtkImageChangeInformation()
278  ici.SetInputData(mergedImage)
279  ici.SetOutputSpacing(1, 1, 1)
280  ici.SetOutputOrigin(0, 0, 0)
281 
282  # Convert labelmap to combined polydata
283  # vtkDiscreteFlyingEdges3D cannot be used here, as in the output of that filter,
284  # each labeled region is completely disconnected from neighboring regions, and
285  # for joint smoothing it is essential for the points to move together.
286  convertToPolyData = vtk.vtkDiscreteMarchingCubes()
287  convertToPolyData.SetInputConnection(ici.GetOutputPort())
288  convertToPolyData.SetNumberOfContours(len(segmentLabelValues))
289 
290  contourIndex = 0
291  for segmentId, labelValue in segmentLabelValues:
292  convertToPolyData.SetValue(contourIndex, labelValue)
293  contourIndex += 1
294 
295  # Low-pass filtering using Taubin's method
296  smoothingFactor = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor")
297  smoothingIterations = 100 # according to VTK documentation 10-20 iterations could be enough but we use a higher value to reduce chance of shrinking
298  passBand = pow(10.0, -4.0*smoothingFactor) # gives a nice range of 1-0.0001 from a user input of 0-1
299  smoother = vtk.vtkWindowedSincPolyDataFilter()
300  smoother.SetInputConnection(convertToPolyData.GetOutputPort())
301  smoother.SetNumberOfIterations(smoothingIterations)
302  smoother.BoundarySmoothingOff()
303  smoother.FeatureEdgeSmoothingOff()
304  smoother.SetFeatureAngle(90.0)
305  smoother.SetPassBand(passBand)
306  smoother.NonManifoldSmoothingOn()
307  smoother.NormalizeCoordinatesOn()
308 
309  # Extract a label
310  threshold = vtk.vtkThreshold()
311  threshold.SetInputConnection(smoother.GetOutputPort())
312 
313  # Convert to polydata
314  geometryFilter = vtk.vtkGeometryFilter()
315  geometryFilter.SetInputConnection(threshold.GetOutputPort())
316 
317  # Convert polydata to stencil
318  polyDataToImageStencil = vtk.vtkPolyDataToImageStencil()
319  polyDataToImageStencil.SetInputConnection(geometryFilter.GetOutputPort())
320  polyDataToImageStencil.SetOutputSpacing(1,1,1)
321  polyDataToImageStencil.SetOutputOrigin(0,0,0)
322  polyDataToImageStencil.SetOutputWholeExtent(mergedImage.GetExtent())
323 
324  # Convert stencil to image
325  stencil = vtk.vtkImageStencil()
326  emptyBinaryLabelMap = vtk.vtkImageData()
327  emptyBinaryLabelMap.SetExtent(mergedImage.GetExtent())
328  emptyBinaryLabelMap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
329  vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(emptyBinaryLabelMap, 0)
330  stencil.SetInputData(emptyBinaryLabelMap)
331  stencil.SetStencilConnection(polyDataToImageStencil.GetOutputPort())
332  stencil.ReverseStencilOn()
333  stencil.SetBackgroundValue(1) # General foreground value is 1 (background value because of reverse stencil)
334 
335  imageToWorldMatrix = vtk.vtkMatrix4x4()
336  mergedImage.GetImageToWorldMatrix(imageToWorldMatrix)
337 
338  # TODO: Temporarily setting the overwite mode to OverwriteVisibleSegments is an approach that should be change once additional
339  # layer control options have been implemented. Users may wish to keep segments on separate layers, and not allow them to be separated/merged automatically.
340  # This effect could leverage those options once they have been implemented.
341  oldOverwriteMode = self.scriptedEffect.parameterSetNode().GetOverwriteMode()
342  self.scriptedEffect.parameterSetNode().SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteVisibleSegments)
343  for segmentId, labelValue in segmentLabelValues:
344  threshold.ThresholdBetween(labelValue, labelValue)
345  stencil.Update()
346  smoothedBinaryLabelMap = slicer.vtkOrientedImageData()
347  smoothedBinaryLabelMap.ShallowCopy(stencil.GetOutput())
348  smoothedBinaryLabelMap.SetImageToWorldMatrix(imageToWorldMatrix)
349  self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentId, smoothedBinaryLabelMap,
350  slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, False)
351  self.scriptedEffect.parameterSetNode().SetOverwriteMode(oldOverwriteMode)
352 
353 MEDIAN = 'MEDIAN'
354 GAUSSIAN = 'GAUSSIAN'
355 MORPHOLOGICAL_OPENING = 'MORPHOLOGICAL_OPENING'
356 MORPHOLOGICAL_CLOSING = 'MORPHOLOGICAL_CLOSING'
357 JOINT_TAUBIN = 'JOINT_TAUBIN'