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