Slicer  4.11
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
SegmentEditorIslandsEffect.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 import vtkITK
6 
8  """ Operate on connected components (islands) within a segment
9  """
10 
11  def __init__(self, scriptedEffect):
12  scriptedEffect.name = 'Islands'
13  AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
15 
16  def clone(self):
17  import qSlicerSegmentationsEditorEffectsPythonQt as effects
18  clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
19  clonedEffect.setPythonSource(__file__.replace('\\','/'))
20  return clonedEffect
21 
22  def icon(self):
23  iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Islands.png')
24  if os.path.exists(iconPath):
25  return qt.QIcon(iconPath)
26  return qt.QIcon()
27 
28  def helpText(self):
29  return "Edit islands (connected components) in a segment."
30 
31  def setupOptionsFrame(self):
33 
34  self.keepLargestOptionRadioButton = qt.QRadioButton("Keep largest island")
35  self.keepLargestOptionRadioButton.setToolTip(
36  "Keep only the largest island in selected segment, remove all other islands in the segment.")
38  self.widgetToOperationNameMap[self.keepLargestOptionRadioButton] = KEEP_LARGEST_ISLAND
39 
40  self.keepSelectedOptionRadioButton = qt.QRadioButton("Keep selected island")
41  self.keepSelectedOptionRadioButton.setToolTip(
42  "Click on an island in a slice viewer to keep that island and remove all other islands in selected segment.")
44  self.widgetToOperationNameMap[self.keepSelectedOptionRadioButton] = KEEP_SELECTED_ISLAND
45 
46  self.removeSmallOptionRadioButton = qt.QRadioButton("Remove small islands")
47  self.removeSmallOptionRadioButton.setToolTip(
48  "Remove all islands from the selected segment that are smaller than the specified minimum size.")
50  self.widgetToOperationNameMap[self.removeSmallOptionRadioButton] = REMOVE_SMALL_ISLANDS
51 
52  self.removeSelectedOptionRadioButton = qt.QRadioButton("Remove selected island")
53  self.removeSelectedOptionRadioButton.setToolTip(
54  "Click on an island to remove it from selected segment.")
56  self.widgetToOperationNameMap[self.removeSelectedOptionRadioButton] = REMOVE_SELECTED_ISLAND
57 
58  self.addSelectedOptionRadioButton = qt.QRadioButton("Add selected island")
59  self.addSelectedOptionRadioButton.setToolTip(
60  "Click on a region to add it to selected segment.")
62  self.widgetToOperationNameMap[self.addSelectedOptionRadioButton] = ADD_SELECTED_ISLAND
63 
64  self.splitAllOptionRadioButton = qt.QRadioButton("Split islands to segments")
65  self.splitAllOptionRadioButton.setToolTip(
66  "Create a new segment for each island of selected segment. Islands smaller than minimum size will be removed. "+
67  "Segments will be ordered by island size.")
69  self.widgetToOperationNameMap[self.splitAllOptionRadioButton] = SPLIT_ISLANDS_TO_SEGMENTS
70 
71  operationLayout = qt.QGridLayout()
72  operationLayout.addWidget(self.keepLargestOptionRadioButton,0,0)
73  operationLayout.addWidget(self.removeSmallOptionRadioButton,1,0)
74  operationLayout.addWidget(self.splitAllOptionRadioButton,2,0)
75  operationLayout.addWidget(self.keepSelectedOptionRadioButton,0,1)
76  operationLayout.addWidget(self.removeSelectedOptionRadioButton,1,1)
77  operationLayout.addWidget(self.addSelectedOptionRadioButton,2,1)
78 
79  self.operationRadioButtons[0].setChecked(True)
80  self.scriptedEffect.addOptionsWidget(operationLayout)
81 
82  self.minimumSizeSpinBox = qt.QSpinBox()
83  self.minimumSizeSpinBox.setToolTip("Islands consisting of less voxels than this minimum size, will be deleted.")
84  self.minimumSizeSpinBox.setMinimum(0)
85  self.minimumSizeSpinBox.setMaximum(vtk.VTK_INT_MAX)
86  self.minimumSizeSpinBox.setValue(1000)
87  self.minimumSizeSpinBox.suffix = " voxels"
88  self.minimumSizeLabel = self.scriptedEffect.addLabeledOptionsWidget("Minimum size:", self.minimumSizeSpinBox)
89 
90  self.applyButton = qt.QPushButton("Apply")
91  self.applyButton.objectName = self.__class__.__name__ + 'Apply'
92  self.scriptedEffect.addOptionsWidget(self.applyButton)
93 
94  for operationRadioButton in self.operationRadioButtons:
95  operationRadioButton.connect('toggled(bool)',
96  lambda toggle, widget=self.widgetToOperationNameMap[operationRadioButton]: self.onOperationSelectionChanged(widget, toggle))
97 
98  self.minimumSizeSpinBox.connect('valueChanged(int)', self.updateMRMLFromGUI)
99 
100  self.applyButton.connect('clicked()', self.onApply)
101 
102  def onOperationSelectionChanged(self, operationName, toggle):
103  if not toggle:
104  return
105  self.scriptedEffect.setParameter("Operation", operationName)
106 
108  operationName = self.scriptedEffect.parameter("Operation")
109  return operationName in [KEEP_SELECTED_ISLAND, REMOVE_SELECTED_ISLAND, ADD_SELECTED_ISLAND]
110 
111  def onApply(self):
112  # Make sure the user wants to do the operation, even if the segment is not visible
113  if not self.scriptedEffect.confirmCurrentSegmentVisible():
114  return
115  operationName = self.scriptedEffect.parameter("Operation")
116  minimumSize = self.scriptedEffect.integerParameter("MinimumSize")
117  if operationName == KEEP_LARGEST_ISLAND:
118  self.splitSegments(minimumSize = minimumSize, maxNumberOfSegments = 1)
119  elif operationName == REMOVE_SMALL_ISLANDS:
120  self.splitSegments(minimumSize = minimumSize, split = False)
121  elif operationName == SPLIT_ISLANDS_TO_SEGMENTS:
122  self.splitSegments(minimumSize = minimumSize)
123 
124  def splitSegments(self, minimumSize = 0, maxNumberOfSegments = 0, split = True):
125  """
126  minimumSize: if 0 then it means that all islands are kept, regardless of size
127  maxNumberOfSegments: if 0 then it means that all islands are kept, regardless of how many
128  """
129  # This can be a long operation - indicate it to the user
130  qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
131 
132  self.scriptedEffect.saveStateForUndo()
133 
134  # Get modifier labelmap
135  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
136 
137  castIn = vtk.vtkImageCast()
138  castIn.SetInputData(selectedSegmentLabelmap)
139  castIn.SetOutputScalarTypeToUnsignedInt()
140 
141  # Identify the islands in the inverted volume and
142  # find the pixel that corresponds to the background
143  islandMath = vtkITK.vtkITKIslandMath()
144  islandMath.SetInputConnection(castIn.GetOutputPort())
145  islandMath.SetFullyConnected(False)
146  islandMath.SetMinimumSize(minimumSize)
147  islandMath.Update()
148 
149  islandImage = slicer.vtkOrientedImageData()
150  islandImage.ShallowCopy(islandMath.GetOutput())
151  selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
152  selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
153  islandImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
154 
155  islandCount = islandMath.GetNumberOfIslands()
156  islandOrigCount = islandMath.GetOriginalNumberOfIslands()
157  ignoredIslands = islandOrigCount - islandCount
158  logging.info( "%d islands created (%d ignored)" % (islandCount, ignoredIslands) )
159 
160  baseSegmentName = "Label"
161  selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
162  segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
163  with slicer.util.NodeModify(segmentationNode):
164  segmentation = segmentationNode.GetSegmentation()
165  selectedSegment = segmentation.GetSegment(selectedSegmentID)
166  selectedSegmentName = selectedSegment.GetName()
167  if selectedSegmentName is not None and selectedSegmentName != "":
168  baseSegmentName = selectedSegmentName
169 
170  labelValues = vtk.vtkIntArray()
171  slicer.vtkSlicerSegmentationsModuleLogic.GetAllLabelValues(labelValues, islandImage)
172 
173  # Erase segment from in original labelmap.
174  # Individuall islands will be added back later.
175  threshold = vtk.vtkImageThreshold()
176  threshold.SetInputData(selectedSegmentLabelmap)
177  threshold.ThresholdBetween(0, 0)
178  threshold.SetInValue(0)
179  threshold.SetOutValue(0)
180  threshold.Update()
181  emptyLabelmap = slicer.vtkOrientedImageData()
182  emptyLabelmap.ShallowCopy(threshold.GetOutput())
183  emptyLabelmap.CopyDirections(selectedSegmentLabelmap)
184  self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, selectedSegmentID, emptyLabelmap,
185  slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
186 
187  for i in range(labelValues.GetNumberOfTuples()):
188  if (maxNumberOfSegments > 0 and i >= maxNumberOfSegments):
189  # We only care about the segments up to maxNumberOfSegments.
190  # If we do not want to split segments, we only care about the first.
191  break
192 
193  labelValue = int(labelValues.GetTuple1(i))
194  segment = selectedSegment
195  segmentID = selectedSegmentID
196  if i != 0 and split:
197  segment = slicer.vtkSegment()
198  name = baseSegmentName + "_" + str(i+1)
199  segment.SetName(name)
200  segment.AddRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(),
201  selectedSegment.GetRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()))
202  segmentation.AddSegment(segment)
203  segmentID = segmentation.GetSegmentIdBySegment(segment)
204  segment.SetLabelValue(segmentation.GetUniqueLabelValueForSharedLabelmap(selectedSegmentID))
205 
206  threshold = vtk.vtkImageThreshold()
207  threshold.SetInputData(islandMath.GetOutput())
208  if not split and maxNumberOfSegments <= 0:
209  # no need to split segments and no limit on number of segments, so we can lump all islands into one segment
210  threshold.ThresholdByLower(0)
211  threshold.SetInValue(0)
212  threshold.SetOutValue(1)
213  else:
214  # copy only selected islands; or copy islands into different segments
215  threshold.ThresholdBetween(labelValue, labelValue)
216  threshold.SetInValue(1)
217  threshold.SetOutValue(0)
218  threshold.Update()
219 
220  modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd
221  if i == 0:
222  modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet
223 
224  # Create oriented image data from output
225  modifierImage = slicer.vtkOrientedImageData()
226  modifierImage.DeepCopy(threshold.GetOutput())
227  selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
228  selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
229  modifierImage.SetGeometryFromImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
230  # We could use a single slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode
231  # method call to import all the resulting segments at once but that would put all the imported segments
232  # in a new layer. By using modifySegmentByLabelmap, the number of layers will not increase.
233  self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, modifierImage, modificationMode)
234 
235  if not split and maxNumberOfSegments <= 0:
236  # all islands lumped into one segment, so we are done
237  break
238 
239  qt.QApplication.restoreOverrideCursor()
240 
241  def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
242  import vtkSegmentationCorePython as vtkSegmentationCore
243 
244  abortEvent = False
245 
246  # Only allow in modes where segment selection is needed
248  return False
249 
250  # Only allow for slice views
251  if viewWidget.className() != "qMRMLSliceWidget":
252  return abortEvent
253 
254  if eventId != vtk.vtkCommand.LeftButtonPressEvent:
255  return abortEvent
256 
257  # Make sure the user wants to do the operation, even if the segment is not visible
258  if not self.scriptedEffect.confirmCurrentSegmentVisible():
259  return abortEvent
260 
261  abortEvent = True
262 
263  # Generate merged labelmap of all visible segments
264  segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
265  visibleSegmentIds = vtk.vtkStringArray()
266  segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
267  if visibleSegmentIds.GetNumberOfValues() == 0:
268  logging.info("Smoothing operation skipped: there are no visible segments")
269  return abortEvent
270 
271  self.scriptedEffect.saveStateForUndo()
272 
273  # This can be a long operation - indicate it to the user
274  qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
275 
276  operationName = self.scriptedEffect.parameter("Operation")
277 
278  if operationName == ADD_SELECTED_ISLAND:
279  inputLabelImage = slicer.vtkOrientedImageData()
280  if not segmentationNode.GenerateMergedLabelmapForAllSegments(inputLabelImage,
281  vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
282  None, visibleSegmentIds):
283  logging.error('Failed to apply smoothing: cannot get list of visible segments')
284  qt.QApplication.restoreOverrideCursor()
285  return abortEvent
286  else:
287  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
288  # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
289  labelValue = 1
290  backgroundValue = 0
291  thresh = vtk.vtkImageThreshold()
292  thresh.SetInputData(selectedSegmentLabelmap)
293  thresh.ThresholdByLower(0)
294  thresh.SetInValue(backgroundValue)
295  thresh.SetOutValue(labelValue)
296  thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
297  thresh.Update()
298  # Create oriented image data from output
299  import vtkSegmentationCorePython as vtkSegmentationCore
300  inputLabelImage = slicer.vtkOrientedImageData()
301  inputLabelImage.ShallowCopy(thresh.GetOutput())
302  selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
303  selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
304  inputLabelImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
305 
306  xy = callerInteractor.GetEventPosition()
307  ijk = self.xyToIjk(xy, viewWidget, inputLabelImage, segmentationNode.GetParentTransformNode())
308  pixelValue = inputLabelImage.GetScalarComponentAsFloat(ijk[0], ijk[1], ijk[2], 0)
309 
310  try:
311  floodFillingFilter = vtk.vtkImageThresholdConnectivity()
312  floodFillingFilter.SetInputData(inputLabelImage)
313  seedPoints = vtk.vtkPoints()
314  origin = inputLabelImage.GetOrigin()
315  spacing = inputLabelImage.GetSpacing()
316  seedPoints.InsertNextPoint(origin[0]+ijk[0]*spacing[0], origin[1]+ijk[1]*spacing[1], origin[2]+ijk[2]*spacing[2])
317  floodFillingFilter.SetSeedPoints(seedPoints)
318  floodFillingFilter.ThresholdBetween(pixelValue, pixelValue)
319 
320  if operationName == ADD_SELECTED_ISLAND:
321  floodFillingFilter.SetInValue(1)
322  floodFillingFilter.SetOutValue(0)
323  floodFillingFilter.Update()
324  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
325  modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
326  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
327 
328  elif pixelValue != 0: # if clicked on empty part then there is nothing to remove or keep
329 
330  if operationName == KEEP_SELECTED_ISLAND:
331  floodFillingFilter.SetInValue(1)
332  floodFillingFilter.SetOutValue(0)
333  else: # operationName == REMOVE_SELECTED_ISLAND:
334  floodFillingFilter.SetInValue(1)
335  floodFillingFilter.SetOutValue(0)
336 
337  floodFillingFilter.Update()
338  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
339  modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
340 
341  if operationName == KEEP_SELECTED_ISLAND:
342  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
343  else: # operationName == REMOVE_SELECTED_ISLAND:
344  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove)
345 
346  except IndexError:
347  logging.error('apply: Failed to threshold master volume!')
348  finally:
349  qt.QApplication.restoreOverrideCursor()
350 
351  return abortEvent
352 
353  def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
354  pass # For the sake of example
355 
356  def setMRMLDefaults(self):
357  self.scriptedEffect.setParameterDefault("Operation", KEEP_LARGEST_ISLAND)
358  self.scriptedEffect.setParameterDefault("MinimumSize", 1000)
359 
360  def updateGUIFromMRML(self):
361  for operationRadioButton in self.operationRadioButtons:
362  operationRadioButton.blockSignals(True)
363  operationName = self.scriptedEffect.parameter("Operation")
364  currentOperationRadioButton = list(self.widgetToOperationNameMap.keys())[list(self.widgetToOperationNameMap.values()).index(operationName)]
365  currentOperationRadioButton.setChecked(True)
366  for operationRadioButton in self.operationRadioButtons:
367  operationRadioButton.blockSignals(False)
368 
369  segmentSelectionRequired = self.currentOperationRequiresSegmentSelection()
370  self.applyButton.setEnabled(not segmentSelectionRequired)
371  if segmentSelectionRequired:
372  self.applyButton.setToolTip("Click in a slice viewer to select segment")
373  else:
374  self.applyButton.setToolTip("")
375 
376  # TODO: this call has no effect now
377  # qSlicerSegmentEditorAbstractEffect should be improved so that it triggers a cursor update
378  # self.scriptedEffect.showEffectCursorInSliceView = segmentSelectionRequired
379 
380  showMinimumSizeOption = (operationName in [REMOVE_SMALL_ISLANDS, SPLIT_ISLANDS_TO_SEGMENTS])
381  self.minimumSizeSpinBox.setEnabled(showMinimumSizeOption)
382  self.minimumSizeLabel.setEnabled(showMinimumSizeOption)
383 
384  self.minimumSizeSpinBox.blockSignals(True)
385  self.minimumSizeSpinBox.value = self.scriptedEffect.integerParameter("MinimumSize")
386  self.minimumSizeSpinBox.blockSignals(False)
387 
388  def updateMRMLFromGUI(self):
389  # Operation is managed separately
390  self.scriptedEffect.setParameter("MinimumSize", self.minimumSizeSpinBox.value)
391 
392 KEEP_LARGEST_ISLAND = 'KEEP_LARGEST_ISLAND'
393 KEEP_SELECTED_ISLAND = 'KEEP_SELECTED_ISLAND'
394 REMOVE_SMALL_ISLANDS = 'REMOVE_SMALL_ISLANDS'
395 REMOVE_SELECTED_ISLAND = 'REMOVE_SELECTED_ISLAND'
396 ADD_SELECTED_ISLAND = 'ADD_SELECTED_ISLAND'
397 SPLIT_ISLANDS_TO_SEGMENTS = 'SPLIT_ISLANDS_TO_SEGMENTS'
def processInteractionEvents(self, callerInteractor, eventId, viewWidget)
def splitSegments(self, minimumSize=0, maxNumberOfSegments=0, split=True)
def processViewNodeEvents(self, callerViewNode, eventId, viewWidget)