2 import vtk, qt, ctk, slicer
4 from SegmentEditorEffects
import *
7 """ SmoothingEffect is an Effect that smoothes a selected segment 11 scriptedEffect.name =
'Smoothing' 12 AbstractScriptedSegmentEditorPaintEffect.__init__(self, scriptedEffect)
15 import qSlicerSegmentationsEditorEffectsPythonQt
as effects
16 clonedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(
None)
17 clonedEffect.setPythonSource(__file__.replace(
'\\',
'/'))
21 iconPath = os.path.join(os.path.dirname(__file__),
'Resources/Icons/Smoothing.png')
22 if os.path.exists(iconPath):
23 return qt.QIcon(iconPath)
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> 50 self.
kernelSizeMMSpinBox.setToolTip(
"Diameter of the neighborhood that will be considered around each voxel. Higher value makes smoothing stronger (more details are suppressed).")
57 self.
kernelSizePixel.setToolTip(
"Diameter of the neighborhood in pixel. Computed from the segment's spacing and the specified kernel size.")
59 kernelSizeFrame = qt.QHBoxLayout()
62 self.
kernelSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget(
"Kernel size:", kernelSizeFrame)
66 self.
gaussianStandardDeviationMMSpinBox.setToolTip(
"Standard deviation of the Gaussian smoothing filter coefficients. Higher value makes smoothing stronger (more details are suppressed).")
82 self.
applyButton.objectName = self.__class__.__name__ +
'Apply' 83 self.
applyButton.setToolTip(
"Apply smoothing to selected segment")
84 self.scriptedEffect.addOptionsWidget(self.
applyButton)
93 self.scriptedEffect.setColorSmudgeCheckboxVisible(
False)
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)
110 morphologicalMethod = (smoothingMethod==MEDIAN
or smoothingMethod==MORPHOLOGICAL_OPENING
or smoothingMethod==MORPHOLOGICAL_CLOSING)
120 selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
121 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
122 if selectedSegmentLabelmap:
123 selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
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
137 self.setWidgetMinMaxStepFromImageSpacing(self.
kernelSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
141 self.
kernelSizePixel.text =
"{0}x{1}x{2} pixel".format(kernelSizePixel[0], kernelSizePixel[1], kernelSizePixel[2])
157 self.scriptedEffect.setParameter(
"SmoothingMethod", smoothingMethod)
168 def onApply(self, maskImage=None, maskExtent=None):
169 """maskImage: contains nonzero where smoothing will be applied 171 smoothingMethod = self.scriptedEffect.parameter(
"SmoothingMethod")
172 if smoothingMethod != JOINT_TAUBIN:
174 if not self.scriptedEffect.confirmCurrentSegmentVisible():
179 qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
180 self.scriptedEffect.saveStateForUndo()
181 if smoothingMethod == JOINT_TAUBIN:
186 qt.QApplication.restoreOverrideCursor()
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)
196 clippedImage = slicer.vtkOrientedImageData()
197 clippedImage.ShallowCopy(clipper.GetOutput())
198 clippedImage.CopyDirections(inputImage)
203 smoothedClippedSelectedSegmentLabelmap = slicer.vtkOrientedImageData()
204 smoothedClippedSelectedSegmentLabelmap.ShallowCopy(smoothedImage)
205 smoothedClippedSelectedSegmentLabelmap.CopyDirections(modifierLabelmap)
209 slicer.vtkOrientedImageDataResample.ApplyImageMask(smoothedClippedSelectedSegmentLabelmap, maskImage, fillValue,
False)
211 slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, selectedSegmentLabelmap,
212 slicer.vtkOrientedImageDataResample.OPERATION_MAXIMUM)
213 slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, smoothedClippedSelectedSegmentLabelmap,
214 slicer.vtkOrientedImageDataResample.OPERATION_MINIMUM)
216 updateExtent = [0, -1, 0, -1, 0, -1]
217 modifierExtent = modifierLabelmap.GetExtent()
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])
223 slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet,
226 modifierLabelmap.DeepCopy(smoothedImage)
233 import vtkSegmentationCorePython
236 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
237 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
239 smoothingMethod = self.scriptedEffect.parameter(
"SmoothingMethod")
241 if smoothingMethod == GAUSSIAN:
244 standardDeviationMM = self.scriptedEffect.doubleParameter(
"GaussianStandardDeviationMm")
245 spacing = modifierLabelmap.GetSpacing()
246 standardDeviationPixel = [1.0, 1.0, 1.0]
247 radiusPixel = [3, 3, 3]
249 standardDeviationPixel[idx] = standardDeviationMM / spacing[idx]
250 radiusPixel[idx] = int(standardDeviationPixel[idx] * radiusFactor) + 1
252 clippedSelectedSegmentLabelmap = self.
clipImage(selectedSegmentLabelmap, maskExtent, radiusPixel)
254 clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
256 thresh = vtk.vtkImageThreshold()
257 thresh.SetInputData(clippedSelectedSegmentLabelmap)
258 thresh.ThresholdByLower(0)
260 thresh.SetOutValue(maxValue)
261 thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
263 gaussianFilter = vtk.vtkImageGaussianSmooth()
264 gaussianFilter.SetInputConnection(thresh.GetOutputPort())
265 gaussianFilter.SetStandardDeviation(*standardDeviationPixel)
266 gaussianFilter.SetRadiusFactor(radiusFactor)
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())
283 clippedSelectedSegmentLabelmap = self.
clipImage(selectedSegmentLabelmap, maskExtent, kernelSizePixel)
285 clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
287 if smoothingMethod == MEDIAN:
289 smoothingFilter = vtk.vtkImageMedian3D()
290 smoothingFilter.SetInputData(clippedSelectedSegmentLabelmap)
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())
303 smoothingFilter = vtk.vtkImageOpenClose3D()
304 smoothingFilter.SetInputConnection(thresh.GetOutputPort())
305 if smoothingMethod == MORPHOLOGICAL_OPENING:
306 smoothingFilter.SetOpenValue(labelValue)
307 smoothingFilter.SetCloseValue(backgroundValue)
309 smoothingFilter.SetOpenValue(backgroundValue)
310 smoothingFilter.SetCloseValue(labelValue)
312 smoothingFilter.SetKernelSize(kernelSizePixel[0],kernelSizePixel[1],kernelSizePixel[2])
313 smoothingFilter.Update()
318 logging.error(
'apply: Failed to apply smoothing')
321 import vtkSegmentationCorePython
as vtkSegmentationCore
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")
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')
338 segmentLabelValues = []
339 for i
in range(visibleSegmentIds.GetNumberOfValues()):
340 segmentId = visibleSegmentIds.GetValue(i)
341 segmentLabelValues.append([segmentId, i+1])
344 ici = vtk.vtkImageChangeInformation()
345 ici.SetInputData(mergedImage)
346 ici.SetOutputSpacing(1, 1, 1)
347 ici.SetOutputOrigin(0, 0, 0)
353 convertToPolyData = vtk.vtkDiscreteMarchingCubes()
354 convertToPolyData.SetInputConnection(ici.GetOutputPort())
355 convertToPolyData.SetNumberOfContours(len(segmentLabelValues))
358 for segmentId, labelValue
in segmentLabelValues:
359 convertToPolyData.SetValue(contourIndex, labelValue)
363 smoothingFactor = self.scriptedEffect.doubleParameter(
"JointTaubinSmoothingFactor")
364 smoothingIterations = 100
365 passBand = pow(10.0, -4.0*smoothingFactor)
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()
377 threshold = vtk.vtkThreshold()
378 threshold.SetInputConnection(smoother.GetOutputPort())
381 geometryFilter = vtk.vtkGeometryFilter()
382 geometryFilter.SetInputConnection(threshold.GetOutputPort())
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())
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)
402 imageToWorldMatrix = vtk.vtkMatrix4x4()
403 mergedImage.GetImageToWorldMatrix(imageToWorldMatrix)
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)
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)
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.")
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]:
439 self.scriptedEffect.saveStateForUndo()
440 self.
onApply(maskImage, maskExtent)
443 GAUSSIAN =
'GAUSSIAN' 444 MORPHOLOGICAL_OPENING =
'MORPHOLOGICAL_OPENING' 445 MORPHOLOGICAL_CLOSING =
'MORPHOLOGICAL_CLOSING' 446 JOINT_TAUBIN =
'JOINT_TAUBIN' def updateGUIFromMRML(self)
def setupOptionsFrame(self)
gaussianStandardDeviationMMSpinBox
jointTaubinSmoothingFactorLabel
def modifySelectedSegmentByLabelmap(self, smoothedImage, selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)
def updateParameterWidgetsVisibility(self)
gaussianStandardDeviationMMLabel
def updateMRMLFromGUI(self)
def paintApply(self, viewWidget)
def setMRMLDefaults(self)
def smoothSelectedSegment(self, maskImage=None, maskExtent=None)
def onApply(self, maskImage=None, maskExtent=None)
def smoothMultipleSegments(self, maskImage=None, maskExtent=None)
def __init__(self, scriptedEffect)
jointTaubinSmoothingFactorSlider
def clipImage(self, inputImage, maskExtent, margin)
def getKernelSizePixel(self)