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