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