Slicer  5.0
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
SegmentEditorMarginEffect.py
Go to the documentation of this file.
1 import logging
2 import math
3 import os
4 
5 import qt
6 import vtk
7 
8 import slicer
9 
10 from SegmentEditorEffects import *
11 
12 
14  """ MaringEffect grows or shrinks the segment by a specified margin
15  """
16 
17  def __init__(self, scriptedEffect):
18  scriptedEffect.name = 'Margin'
19  AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
20 
21  def clone(self):
22  import qSlicerSegmentationsEditorEffectsPythonQt as effects
23  clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(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/Margin.png')
29  if os.path.exists(iconPath):
30  return qt.QIcon(iconPath)
31  return qt.QIcon()
32 
33  def helpText(self):
34  return "Grow or shrink selected segment by specified margin size."
35 
36  def setupOptionsFrame(self):
37 
38  operationLayout = qt.QVBoxLayout()
39 
40  self.shrinkOptionRadioButton = qt.QRadioButton("Shrink")
41  self.growOptionRadioButton = qt.QRadioButton("Grow")
42  operationLayout.addWidget(self.shrinkOptionRadioButton)
43  operationLayout.addWidget(self.growOptionRadioButton)
44  self.growOptionRadioButton.setChecked(True)
45 
46  self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout)
47 
48  self.marginSizeMMSpinBox = slicer.qMRMLSpinBox()
49  self.marginSizeMMSpinBox.setMRMLScene(slicer.mrmlScene)
50  self.marginSizeMMSpinBox.setToolTip("Segment boundaries will be shifted by this distance. Positive value means the segments will grow, negative value means segment will shrink.")
51  self.marginSizeMMSpinBox.quantity = "length"
52  self.marginSizeMMSpinBox.value = 3.0
53  self.marginSizeMMSpinBox.singleStep = 1.0
54 
55  self.marginSizeLabel = qt.QLabel()
56  self.marginSizeLabel.setToolTip("Size change in pixel. Computed from the segment's spacing and the specified margin size.")
57 
58  marginSizeFrame = qt.QHBoxLayout()
59  marginSizeFrame.addWidget(self.marginSizeMMSpinBox)
60  self.marginSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Margin size:", marginSizeFrame)
61  self.scriptedEffect.addLabeledOptionsWidget("", self.marginSizeLabel)
62 
63  self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox()
64  self.applyToAllVisibleSegmentsCheckBox.setToolTip("Grow or shrink all visible segments in this segmentation node. \
65  This operation may take a while.")
66  self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments'
67  self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox)
68 
69  self.applyButton = qt.QPushButton("Apply")
70  self.applyButton.objectName = self.__class__.__name__ + 'Apply'
71  self.applyButton.setToolTip("Grows or shrinks selected segment /default) or all segments (checkbox) by the specified margin.")
72  self.scriptedEffect.addOptionsWidget(self.applyButton)
73 
74  self.applyButton.connect('clicked()', self.onApply)
75  self.marginSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
76  self.growOptionRadioButton.connect("toggled(bool)", self.growOperationToggled)
77  self.shrinkOptionRadioButton.connect("toggled(bool)", self.shrinkOperationToggled)
78  self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
79 
80  def createCursor(self, widget):
81  # Turn off effect-specific cursor for this effect
82  return slicer.util.mainWindow().cursor
83 
84  def setMRMLDefaults(self):
85  self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
86  self.scriptedEffect.setParameterDefault("MarginSizeMm", 3)
87 
88  def getMarginSizePixel(self):
89  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
90  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
91  if selectedSegmentLabelmap:
92  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
93 
94  marginSizeMM = abs(self.scriptedEffect.doubleParameter("MarginSizeMm"))
95  marginSizePixel = [int(math.floor(marginSizeMM / spacing)) for spacing in selectedSegmentLabelmapSpacing]
96  return marginSizePixel
97 
98  def updateGUIFromMRML(self):
99  marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm")
100  wasBlocked = self.marginSizeMMSpinBox.blockSignals(True)
101  self.marginSizeMMSpinBox.value = abs(marginSizeMM)
102  self.marginSizeMMSpinBox.blockSignals(wasBlocked)
103 
104  wasBlocked = self.growOptionRadioButton.blockSignals(True)
105  self.growOptionRadioButton.setChecked(marginSizeMM > 0)
106  self.growOptionRadioButton.blockSignals(wasBlocked)
107 
108  wasBlocked = self.shrinkOptionRadioButton.blockSignals(True)
109  self.shrinkOptionRadioButton.setChecked(marginSizeMM < 0)
110  self.shrinkOptionRadioButton.blockSignals(wasBlocked)
111 
112  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
113  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
114  if selectedSegmentLabelmap:
115  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
116  marginSizePixel = self.getMarginSizePixel()
117  if marginSizePixel[0] < 1 or marginSizePixel[1] < 1 or marginSizePixel[2] < 1:
118  self.marginSizeLabel.text = "Not feasible at current resolution."
119  self.applyButton.setEnabled(False)
120  else:
121  marginSizeMM = self.getMarginSizeMM()
122  self.marginSizeLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*marginSizeMM, *marginSizePixel)
123  self.applyButton.setEnabled(True)
124  else:
125  self.marginSizeLabel.text = "Empty segment"
126 
127  applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
128  wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
129  self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
130  self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
131 
132  self.setWidgetMinMaxStepFromImageSpacing(self.marginSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
133 
134  def growOperationToggled(self, toggled):
135  if toggled:
136  self.scriptedEffect.setParameter("MarginSizeMm", self.marginSizeMMSpinBox.value)
137 
138  def shrinkOperationToggled(self, toggled):
139  if toggled:
140  self.scriptedEffect.setParameter("MarginSizeMm", -self.marginSizeMMSpinBox.value)
141 
142  def updateMRMLFromGUI(self):
143  marginSizeMM = (self.marginSizeMMSpinBox.value) if self.growOptionRadioButton.checked else (-self.marginSizeMMSpinBox.value)
144  self.scriptedEffect.setParameter("MarginSizeMm", marginSizeMM)
145  applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
146  self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
147 
148  def getMarginSizeMM(self):
149  selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
150  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
151  if selectedSegmentLabelmap:
152  selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
153 
154  marginSizePixel = self.getMarginSizePixel()
155  marginSizeMM = [abs((marginSizePixel[i]) * selectedSegmentLabelmapSpacing[i]) for i in range(3)]
156  for i in range(3):
157  if marginSizeMM[i] > 0:
158  marginSizeMM[i] = round(marginSizeMM[i], max(int(-math.floor(math.log10(marginSizeMM[i]))), 1))
159  return marginSizeMM
160 
161  def showStatusMessage(self, msg, timeoutMsec=500):
162  slicer.util.showStatusMessage(msg, timeoutMsec)
163  slicer.app.processEvents()
164 
165  def processMargin(self):
166  # Get modifier labelmap and parameters
167  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
168  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
169 
170  marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm")
171 
172  # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
173  labelValue = 1
174  backgroundValue = 0
175  thresh = vtk.vtkImageThreshold()
176  thresh.SetInputData(selectedSegmentLabelmap)
177  thresh.ThresholdByLower(0)
178  thresh.SetInValue(backgroundValue)
179  thresh.SetOutValue(labelValue)
180  thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
181  if (marginSizeMM < 0):
182  # The distance filter used in the margin filter starts at zero at the border voxels,
183  # so if we need to shrink the margin, it is more accurate to invert the labelmap and
184  # use positive distance when calculating the margin
185  thresh.SetInValue(labelValue)
186  thresh.SetOutValue(backgroundValue)
187 
188  import vtkITK
189  margin = vtkITK.vtkITKImageMargin()
190  margin.SetInputConnection(thresh.GetOutputPort())
191  margin.CalculateMarginInMMOn()
192  margin.SetOuterMarginMM(abs(marginSizeMM))
193  margin.Update()
194 
195  if marginSizeMM >= 0:
196  modifierLabelmap.ShallowCopy(margin.GetOutput())
197  else:
198  # If we are shrinking then the result needs to be inverted.
199  thresh = vtk.vtkImageThreshold()
200  thresh.SetInputData(margin.GetOutput())
201  thresh.ThresholdByLower(0)
202  thresh.SetInValue(labelValue)
203  thresh.SetOutValue(backgroundValue)
204  thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
205  thresh.Update()
206  modifierLabelmap.ShallowCopy(thresh.GetOutput())
207 
208  # Apply changes
209  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
210 
211  def onApply(self):
212  # Make sure the user wants to do the operation, even if the segment is not visible
213  if not self.scriptedEffect.confirmCurrentSegmentVisible():
214  return
215 
216  try:
217  # This can be a long operation - indicate it to the user
218  qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
219  self.scriptedEffect.saveStateForUndo()
220 
221  applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
222  if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
223 
224  if applyToAllVisibleSegments:
225  # Smooth all visible segments
226  inputSegmentIDs = vtk.vtkStringArray()
227  segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
228  segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
229  segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
230  segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
231  # store which segment was selected before operation
232  selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID()
233  if inputSegmentIDs.GetNumberOfValues() == 0:
234  logging.info("Margin operation skipped: there are no visible segments.")
235  return
236  # select input segments one by one, process
237  for index in range(inputSegmentIDs.GetNumberOfValues()):
238  segmentID = inputSegmentIDs.GetValue(index)
239  self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
240  segmentEditorNode.SetSelectedSegmentID(segmentID)
241  self.processMargin()
242  # restore segment selection
243  segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID)
244  else:
245  self.processMargin()
246 
247  finally:
248  qt.QApplication.restoreOverrideCursor()