Slicer  5.2
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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 visible 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()