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
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)