Slicer  4.8
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("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)