Slicer  4.10
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  operationName = self.scriptedEffect.parameter("Operation")
113  minimumSize = self.scriptedEffect.integerParameter("MinimumSize")
114  if operationName == KEEP_LARGEST_ISLAND:
115  self.splitSegments(minimumSize = minimumSize, maxNumberOfSegments = 1)
116  elif operationName == REMOVE_SMALL_ISLANDS:
117  self.splitSegments(minimumSize = minimumSize, split = False)
118  elif operationName == SPLIT_ISLANDS_TO_SEGMENTS:
119  self.splitSegments(minimumSize = minimumSize)
120 
121  def splitSegments(self, minimumSize = 0, maxNumberOfSegments = 0, split = True):
122  """
123  minimumSize: if 0 then it means that all islands are kept, regardless of size
124  maxNumberOfSegments: if 0 then it means that all islands are kept, regardless of how many
125  """
126  # This can be a long operation - indicate it to the user
127  qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
128 
129  self.scriptedEffect.saveStateForUndo()
130 
131  # Get modifier labelmap
132  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
133 
134  castIn = vtk.vtkImageCast()
135  castIn.SetInputData(selectedSegmentLabelmap)
136  castIn.SetOutputScalarTypeToUnsignedInt()
137 
138  # Identify the islands in the inverted volume and
139  # find the pixel that corresponds to the background
140  islandMath = vtkITK.vtkITKIslandMath()
141  islandMath.SetInputConnection(castIn.GetOutputPort())
142  islandMath.SetFullyConnected(False)
143  islandMath.SetMinimumSize(minimumSize)
144  islandMath.Update()
145 
146  # Create a separate image for the first (largest) island
147  labelValue = 1
148  backgroundValue = 0
149  thresh = vtk.vtkImageThreshold()
150  if split:
151  thresh.ThresholdBetween(1, 1)
152  else:
153  if maxNumberOfSegments != 0:
154  thresh.ThresholdBetween(1, maxNumberOfSegments)
155  else:
156  thresh.ThresholdByUpper(1)
157 
158  thresh.SetInputData(islandMath.GetOutput())
159  thresh.SetOutValue(backgroundValue)
160  thresh.SetInValue(labelValue)
161  thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
162  thresh.Update()
163  # Create oriented image data from output
164  import vtkSegmentationCorePython as vtkSegmentationCore
165  largestIslandImage = vtkSegmentationCore.vtkOrientedImageData()
166  largestIslandImage.ShallowCopy(thresh.GetOutput())
167  selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
168  selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
169  largestIslandImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
170 
171  if split and (maxNumberOfSegments != 1):
172 
173  thresh2 = vtk.vtkImageThreshold()
174  # 0 is background, 1 is largest island; we need label 2 and higher
175  if maxNumberOfSegments != 0:
176  thresh2.ThresholdBetween(2, maxNumberOfSegments)
177  else:
178  thresh2.ThresholdByUpper(2)
179  thresh2.SetInputData(islandMath.GetOutput())
180  thresh2.SetOutValue(backgroundValue)
181  thresh2.ReplaceInOff()
182  thresh2.Update()
183 
184  islandCount = islandMath.GetNumberOfIslands()
185  islandOrigCount = islandMath.GetOriginalNumberOfIslands()
186  ignoredIslands = islandOrigCount - islandCount
187  logging.info( "%d islands created (%d ignored)" % (islandCount, ignoredIslands) )
188 
189  # Create oriented image data from output
190  import vtkSegmentationCorePython as vtkSegmentationCore
191  multiLabelImage = vtkSegmentationCore.vtkOrientedImageData()
192  multiLabelImage.DeepCopy(thresh2.GetOutput())
193  selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
194  selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
195  multiLabelImage.SetGeometryFromImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
196 
197  # Import multi-label labelmap to segmentation
198  segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
199  selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
200  selectedSegmentIndex = segmentationNode.GetSegmentation().GetSegmentIndex(selectedSegmentID)
201  insertBeforeSegmentID = segmentationNode.GetSegmentation().GetNthSegmentID(selectedSegmentIndex + 1)
202  selectedSegmentName = segmentationNode.GetSegmentation().GetSegment(selectedSegmentID).GetName()
203  slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode( \
204  multiLabelImage, segmentationNode, selectedSegmentName+" -", insertBeforeSegmentID )
205 
206  self.scriptedEffect.modifySelectedSegmentByLabelmap(largestIslandImage, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
207 
208  qt.QApplication.restoreOverrideCursor()
209 
210  def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
211  import vtkSegmentationCorePython as vtkSegmentationCore
212 
213  abortEvent = False
214 
215  # Only allow in modes where segment selection is needed
217  return False
218 
219  # Only allow for slice views
220  if viewWidget.className() != "qMRMLSliceWidget":
221  return abortEvent
222 
223  if eventId != vtk.vtkCommand.LeftButtonPressEvent:
224  return abortEvent
225 
226  abortEvent = True
227 
228  # Generate merged labelmap of all visible segments
229  segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
230  visibleSegmentIds = vtk.vtkStringArray()
231  segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
232  if visibleSegmentIds.GetNumberOfValues() == 0:
233  logging.info("Smoothing operation skipped: there are no visible segments")
234  return abortEvent
235 
236  self.scriptedEffect.saveStateForUndo()
237 
238  # This can be a long operation - indicate it to the user
239  qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
240 
241  operationName = self.scriptedEffect.parameter("Operation")
242 
243  if operationName == ADD_SELECTED_ISLAND:
244  inputLabelImage = vtkSegmentationCore.vtkOrientedImageData()
245  if not segmentationNode.GenerateMergedLabelmapForAllSegments(inputLabelImage,
246  vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
247  None, visibleSegmentIds):
248  logging.error('Failed to apply smoothing: cannot get list of visible segments')
249  qt.QApplication.restoreOverrideCursor()
250  return abortEvent
251  else:
252  selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
253  # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
254  labelValue = 1
255  backgroundValue = 0
256  thresh = vtk.vtkImageThreshold()
257  thresh.SetInputData(selectedSegmentLabelmap)
258  thresh.ThresholdByLower(0)
259  thresh.SetInValue(backgroundValue)
260  thresh.SetOutValue(labelValue)
261  thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
262  thresh.Update()
263  # Create oriented image data from output
264  import vtkSegmentationCorePython as vtkSegmentationCore
265  inputLabelImage = vtkSegmentationCore.vtkOrientedImageData()
266  inputLabelImage.ShallowCopy(thresh.GetOutput())
267  selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
268  selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
269  inputLabelImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
270 
271  xy = callerInteractor.GetEventPosition()
272  ijk = self.xyToIjk(xy, viewWidget, inputLabelImage)
273  pixelValue = inputLabelImage.GetScalarComponentAsFloat(ijk[0], ijk[1], ijk[2], 0)
274 
275  try:
276 
277  floodFillingFilter = vtk.vtkImageThresholdConnectivity()
278  floodFillingFilter.SetInputData(inputLabelImage)
279  seedPoints = vtk.vtkPoints()
280  origin = inputLabelImage.GetOrigin()
281  spacing = inputLabelImage.GetSpacing()
282  seedPoints.InsertNextPoint(origin[0]+ijk[0]*spacing[0], origin[1]+ijk[1]*spacing[1], origin[2]+ijk[2]*spacing[2])
283  floodFillingFilter.SetSeedPoints(seedPoints)
284  floodFillingFilter.ThresholdBetween(pixelValue, pixelValue)
285 
286  if operationName == ADD_SELECTED_ISLAND:
287  floodFillingFilter.SetInValue(1)
288  floodFillingFilter.SetOutValue(0)
289  floodFillingFilter.Update()
290  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
291  modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
292  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
293 
294  elif pixelValue != 0: # if clicked on empty part then there is nothing to remove or keep
295 
296  if operationName == KEEP_SELECTED_ISLAND:
297  floodFillingFilter.SetInValue(1)
298  floodFillingFilter.SetOutValue(0)
299  else: # operationName == REMOVE_SELECTED_ISLAND:
300  floodFillingFilter.SetInValue(1)
301  floodFillingFilter.SetOutValue(0)
302 
303  floodFillingFilter.Update()
304  modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
305  modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
306 
307  if operationName == KEEP_SELECTED_ISLAND:
308  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
309  else: # operationName == REMOVE_SELECTED_ISLAND:
310  self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove)
311 
312  except IndexError:
313  logging.error('apply: Failed to threshold master volume!')
314  finally:
315  qt.QApplication.restoreOverrideCursor()
316 
317  return abortEvent
318 
319  def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
320  pass # For the sake of example
321 
322  def setMRMLDefaults(self):
323  self.scriptedEffect.setParameterDefault("Operation", KEEP_LARGEST_ISLAND)
324  self.scriptedEffect.setParameterDefault("MinimumSize", 1000)
325 
326  def updateGUIFromMRML(self):
327  for operationRadioButton in self.operationRadioButtons:
328  operationRadioButton.blockSignals(True)
329  operationName = self.scriptedEffect.parameter("Operation")
330  currentOperationRadioButton = self.widgetToOperationNameMap.keys()[self.widgetToOperationNameMap.values().index(operationName)]
331  currentOperationRadioButton.setChecked(True)
332  for operationRadioButton in self.operationRadioButtons:
333  operationRadioButton.blockSignals(False)
334 
335  segmentSelectionRequired = self.currentOperationRequiresSegmentSelection()
336  self.applyButton.setEnabled(not segmentSelectionRequired)
337  if segmentSelectionRequired:
338  self.applyButton.setToolTip("Click in a slice viewer to select segment")
339  else:
340  self.applyButton.setToolTip("")
341 
342  # TODO: this call has no effect now
343  # qSlicerSegmentEditorAbstractEffect should be improved so that it triggers a cursor update
344  # self.scriptedEffect.showEffectCursorInSliceView = segmentSelectionRequired
345 
346  showMinimumSizeOption = (operationName in [REMOVE_SMALL_ISLANDS, SPLIT_ISLANDS_TO_SEGMENTS])
347  self.minimumSizeSpinBox.setEnabled(showMinimumSizeOption)
348  self.minimumSizeLabel.setEnabled(showMinimumSizeOption)
349 
350  self.minimumSizeSpinBox.blockSignals(True)
351  self.minimumSizeSpinBox.value = self.scriptedEffect.integerParameter("MinimumSize")
352  self.minimumSizeSpinBox.blockSignals(False)
353 
354  def updateMRMLFromGUI(self):
355  # Operation is managed separately
356  self.scriptedEffect.setParameter("MinimumSize", self.minimumSizeSpinBox.value)
357 
358 KEEP_LARGEST_ISLAND = 'KEEP_LARGEST_ISLAND'
359 KEEP_SELECTED_ISLAND = 'KEEP_SELECTED_ISLAND'
360 REMOVE_SMALL_ISLANDS = 'REMOVE_SMALL_ISLANDS'
361 REMOVE_SELECTED_ISLAND = 'REMOVE_SELECTED_ISLAND'
362 ADD_SELECTED_ISLAND = 'ADD_SELECTED_ISLAND'
363 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)