10 from SegmentEditorEffects
import *
14 """ SmoothingEffect is an Effect that smoothes a selected segment 18 scriptedEffect.name =
'Smoothing' 19 AbstractScriptedSegmentEditorPaintEffect.__init__(self, scriptedEffect)
22 import qSlicerSegmentationsEditorEffectsPythonQt
as effects
23 clonedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(
None)
24 clonedEffect.setPythonSource(__file__.replace(
'\\',
'/'))
28 iconPath = os.path.join(os.path.dirname(__file__),
'Resources/Icons/Smoothing.png')
29 if os.path.exists(iconPath):
30 return qt.QIcon(iconPath)
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> 57 self.
kernelSizeMMSpinBox.setToolTip(
"Diameter of the neighborhood that will be considered around each voxel. Higher value makes smoothing stronger (more details are suppressed).")
64 self.
kernelSizePixel.setToolTip(
"Diameter of the neighborhood in pixel. Computed from the segment's spacing and the specified kernel size.")
66 kernelSizeFrame = qt.QHBoxLayout()
69 self.
kernelSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget(
"Kernel size:", kernelSizeFrame)
73 self.
gaussianStandardDeviationMMSpinBox.setToolTip(
"Standard deviation of the Gaussian smoothing filter coefficients. Higher value makes smoothing stronger (more details are suppressed).")
90 This operation may take a while.")
95 self.
applyButton.objectName = self.__class__.__name__ +
'Apply' 96 self.
applyButton.setToolTip(
"Apply smoothing to selected segment")
97 self.scriptedEffect.addOptionsWidget(self.
applyButton)
107 self.scriptedEffect.setColorSmudgeCheckboxVisible(
False)
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)
125 morphologicalMethod = (smoothingMethod == MEDIAN
or smoothingMethod == MORPHOLOGICAL_OPENING
or smoothingMethod == MORPHOLOGICAL_CLOSING)
137 selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
138 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
139 if selectedSegmentLabelmap:
140 selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
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
154 self.setWidgetMinMaxStepFromImageSpacing(self.
kernelSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
158 self.
kernelSizePixel.text = f
"{kernelSizePixel[0]}x{kernelSizePixel[1]}x{kernelSizePixel[2]} pixel" 169 applyToAllVisibleSegments = qt.Qt.Unchecked
if self.scriptedEffect.integerParameter(
"ApplyToAllVisibleSegments") == 0
else qt.Qt.Checked
179 self.scriptedEffect.setParameter(
"SmoothingMethod", smoothingMethod)
184 self.scriptedEffect.setParameter(
"ApplyToAllVisibleSegments", applyToAllVisibleSegments)
193 slicer.util.showStatusMessage(msg, timeoutMsec)
194 slicer.app.processEvents()
196 def onApply(self, maskImage=None, maskExtent=None):
197 """maskImage: contains nonzero where smoothing will be applied 199 smoothingMethod = self.scriptedEffect.parameter(
"SmoothingMethod")
200 applyToAllVisibleSegments = int(self.scriptedEffect.parameter(
"ApplyToAllVisibleSegments")) != 0 \
201 if self.scriptedEffect.parameter(
"ApplyToAllVisibleSegments")
else False 203 if smoothingMethod != JOINT_TAUBIN:
205 if not self.scriptedEffect.confirmCurrentSegmentVisible():
210 qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
211 self.scriptedEffect.saveStateForUndo()
213 if smoothingMethod == JOINT_TAUBIN:
215 elif applyToAllVisibleSegments:
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()
223 selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID()
224 if inputSegmentIDs.GetNumberOfValues() == 0:
225 logging.info(
"Smoothing operation skipped: there are no visible segments.")
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)
233 segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID)
237 qt.QApplication.restoreOverrideCursor()
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)
247 clippedImage = slicer.vtkOrientedImageData()
248 clippedImage.ShallowCopy(clipper.GetOutput())
249 clippedImage.CopyDirections(inputImage)
254 smoothedClippedSelectedSegmentLabelmap = slicer.vtkOrientedImageData()
255 smoothedClippedSelectedSegmentLabelmap.ShallowCopy(smoothedImage)
256 smoothedClippedSelectedSegmentLabelmap.CopyDirections(modifierLabelmap)
260 slicer.vtkOrientedImageDataResample.ApplyImageMask(smoothedClippedSelectedSegmentLabelmap, maskImage, fillValue,
False)
262 slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, selectedSegmentLabelmap,
263 slicer.vtkOrientedImageDataResample.OPERATION_MAXIMUM)
264 slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, smoothedClippedSelectedSegmentLabelmap,
265 slicer.vtkOrientedImageDataResample.OPERATION_MINIMUM)
267 updateExtent = [0, -1, 0, -1, 0, -1]
268 modifierExtent = modifierLabelmap.GetExtent()
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])
274 slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet,
277 modifierLabelmap.DeepCopy(smoothedImage)
283 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
284 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
286 smoothingMethod = self.scriptedEffect.parameter(
"SmoothingMethod")
288 if smoothingMethod == GAUSSIAN:
291 standardDeviationMM = self.scriptedEffect.doubleParameter(
"GaussianStandardDeviationMm")
292 spacing = modifierLabelmap.GetSpacing()
293 standardDeviationPixel = [1.0, 1.0, 1.0]
294 radiusPixel = [3, 3, 3]
296 standardDeviationPixel[idx] = standardDeviationMM / spacing[idx]
297 radiusPixel[idx] = int(standardDeviationPixel[idx] * radiusFactor) + 1
299 clippedSelectedSegmentLabelmap = self.
clipImage(selectedSegmentLabelmap, maskExtent, radiusPixel)
301 clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
303 thresh = vtk.vtkImageThreshold()
304 thresh.SetInputData(clippedSelectedSegmentLabelmap)
305 thresh.ThresholdByLower(0)
307 thresh.SetOutValue(maxValue)
308 thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
310 gaussianFilter = vtk.vtkImageGaussianSmooth()
311 gaussianFilter.SetInputConnection(thresh.GetOutputPort())
312 gaussianFilter.SetStandardDeviation(*standardDeviationPixel)
313 gaussianFilter.SetRadiusFactor(radiusFactor)
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())
330 clippedSelectedSegmentLabelmap = self.
clipImage(selectedSegmentLabelmap, maskExtent, kernelSizePixel)
332 clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
334 if smoothingMethod == MEDIAN:
336 smoothingFilter = vtk.vtkImageMedian3D()
337 smoothingFilter.SetInputData(clippedSelectedSegmentLabelmap)
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())
350 smoothingFilter = vtk.vtkImageOpenClose3D()
351 smoothingFilter.SetInputConnection(thresh.GetOutputPort())
352 if smoothingMethod == MORPHOLOGICAL_OPENING:
353 smoothingFilter.SetOpenValue(labelValue)
354 smoothingFilter.SetCloseValue(backgroundValue)
356 smoothingFilter.SetOpenValue(backgroundValue)
357 smoothingFilter.SetCloseValue(labelValue)
359 smoothingFilter.SetKernelSize(kernelSizePixel[0], kernelSizePixel[1], kernelSizePixel[2])
360 smoothingFilter.Update()
365 logging.error(
'apply: Failed to apply smoothing')
368 import vtkSegmentationCorePython
as vtkSegmentationCore
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")
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')
386 segmentLabelValues = []
387 for i
in range(visibleSegmentIds.GetNumberOfValues()):
388 segmentId = visibleSegmentIds.GetValue(i)
389 segmentLabelValues.append([segmentId, i + 1])
392 ici = vtk.vtkImageChangeInformation()
393 ici.SetInputData(mergedImage)
394 ici.SetOutputSpacing(1, 1, 1)
395 ici.SetOutputOrigin(0, 0, 0)
401 convertToPolyData = vtk.vtkDiscreteMarchingCubes()
402 convertToPolyData.SetInputConnection(ici.GetOutputPort())
403 convertToPolyData.SetNumberOfContours(len(segmentLabelValues))
406 for segmentId, labelValue
in segmentLabelValues:
407 convertToPolyData.SetValue(contourIndex, labelValue)
411 smoothingFactor = self.scriptedEffect.doubleParameter(
"JointTaubinSmoothingFactor")
412 smoothingIterations = 100
413 passBand = pow(10.0, -4.0 * smoothingFactor)
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()
425 threshold = vtk.vtkThreshold()
426 threshold.SetInputConnection(smoother.GetOutputPort())
429 geometryFilter = vtk.vtkGeometryFilter()
430 geometryFilter.SetInputConnection(threshold.GetOutputPort())
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())
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)
450 imageToWorldMatrix = vtk.vtkMatrix4x4()
451 mergedImage.GetImageToWorldMatrix(imageToWorldMatrix)
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)
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)
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.")
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]:
487 self.scriptedEffect.saveStateForUndo()
488 self.
onApply(maskImage, maskExtent)
492 GAUSSIAN =
'GAUSSIAN' 493 MORPHOLOGICAL_OPENING =
'MORPHOLOGICAL_OPENING' 494 MORPHOLOGICAL_CLOSING =
'MORPHOLOGICAL_CLOSING' 495 JOINT_TAUBIN =
'JOINT_TAUBIN' def updateGUIFromMRML(self)
def setupOptionsFrame(self)
applyToAllVisibleSegmentsLabel
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 showStatusMessage(self, msg, timeoutMsec=500)
applyToAllVisibleSegmentsCheckBox
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)