Slicer 5.4
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
Loading...
Searching...
No Matches
SegmentEditorIslandsEffect.py
Go to the documentation of this file.
1import logging
2import os
3
4import qt
5import vtk
6import vtkITK
7
8import slicer
9
10from SegmentEditorEffects import *
11
12
13class SegmentEditorIslandsEffect(AbstractScriptedSegmentEditorEffect):
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
36about each operation, hover the mouse over the option and wait for the tooltip to appear.</html>"""
37
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.updateMRMLFromGUIupdateMRMLFromGUI)
106
107 self.applyButton.connect('clicked()', self.onApplyonApply)
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 confirmedEditingAllowed = self.scriptedEffect.confirmCurrentSegmentVisible()
266 if confirmedEditingAllowed == self.scriptedEffect.NotConfirmed or confirmedEditingAllowed == self.scriptedEffect.ConfirmedWithDialog:
267 # ConfirmedWithDialog cancels the operation because without seeing the segment, the island may have looked different
268 # than what the user remembered/expected. The dialog is not displayed again for the same segment.
269
270 # The event has to be aborted, because otherwise there would be a LeftButtonPressEvent without a matching
271 # LeftButtonReleaseEvent (as the popup window received the release button event).
272 abortEvent = True
273
274 return abortEvent
275
276 abortEvent = True
277
278 # Generate merged labelmap of all visible segments
279 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
280 visibleSegmentIds = vtk.vtkStringArray()
281 segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
282 if visibleSegmentIds.GetNumberOfValues() == 0:
283 logging.info("Island operation skipped: there are no visible segments")
284 return abortEvent
285
286 self.scriptedEffect.saveStateForUndo()
287
288 # This can be a long operation - indicate it to the user
289 qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
290
291 operationName = self.scriptedEffect.parameter("Operation")
292
293 if operationName == ADD_SELECTED_ISLAND:
294 inputLabelImage = slicer.vtkOrientedImageData()
295 if not segmentationNode.GenerateMergedLabelmapForAllSegments(inputLabelImage,
296 vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
297 None, visibleSegmentIds):
298 logging.error('Failed to apply island operation: cannot get list of visible segments')
299 qt.QApplication.restoreOverrideCursor()
300 return abortEvent
301 else:
302 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
303 # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
304 labelValue = 1
305 backgroundValue = 0
306 thresh = vtk.vtkImageThreshold()
307 thresh.SetInputData(selectedSegmentLabelmap)
308 thresh.ThresholdByLower(0)
309 thresh.SetInValue(backgroundValue)
310 thresh.SetOutValue(labelValue)
311 thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
312 thresh.Update()
313 # Create oriented image data from output
314 import vtkSegmentationCorePython as vtkSegmentationCore
315 inputLabelImage = slicer.vtkOrientedImageData()
316 inputLabelImage.ShallowCopy(thresh.GetOutput())
317 selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
318 selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
319 inputLabelImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
320
321 xy = callerInteractor.GetEventPosition()
322 ijk = self.xyToIjk(xy, viewWidget, inputLabelImage, segmentationNode.GetParentTransformNode())
323 pixelValue = inputLabelImage.GetScalarComponentAsFloat(ijk[0], ijk[1], ijk[2], 0)
324
325 try:
326 floodFillingFilter = vtk.vtkImageThresholdConnectivity()
327 floodFillingFilter.SetInputData(inputLabelImage)
328 seedPoints = vtk.vtkPoints()
329 origin = inputLabelImage.GetOrigin()
330 spacing = inputLabelImage.GetSpacing()
331 seedPoints.InsertNextPoint(origin[0] + ijk[0] * spacing[0], origin[1] + ijk[1] * spacing[1], origin[2] + ijk[2] * spacing[2])
332 floodFillingFilter.SetSeedPoints(seedPoints)
333 floodFillingFilter.ThresholdBetween(pixelValue, pixelValue)
334
335 if operationName == ADD_SELECTED_ISLAND:
336 floodFillingFilter.SetInValue(1)
337 floodFillingFilter.SetOutValue(0)
338 floodFillingFilter.Update()
339 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
340 modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
341 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
342
343 elif pixelValue != 0: # if clicked on empty part then there is nothing to remove or keep
344
345 if operationName == KEEP_SELECTED_ISLAND:
346 floodFillingFilter.SetInValue(1)
347 floodFillingFilter.SetOutValue(0)
348 else: # operationName == REMOVE_SELECTED_ISLAND:
349 floodFillingFilter.SetInValue(1)
350 floodFillingFilter.SetOutValue(0)
351
352 floodFillingFilter.Update()
353 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
354 modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
355
356 if operationName == KEEP_SELECTED_ISLAND:
357 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
358 else: # operationName == REMOVE_SELECTED_ISLAND:
359 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove)
360
361 except IndexError:
362 logging.error('Island processing failed')
363 finally:
364 qt.QApplication.restoreOverrideCursor()
365
366 return abortEvent
367
368 def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
369 pass # For the sake of example
370
372 self.scriptedEffect.setParameterDefault("Operation", KEEP_LARGEST_ISLAND)
373 self.scriptedEffect.setParameterDefault("MinimumSize", 1000)
374
376 for operationRadioButton in self.operationRadioButtons:
377 operationRadioButton.blockSignals(True)
378 operationName = self.scriptedEffect.parameter("Operation")
379 currentOperationRadioButton = list(self.widgetToOperationNameMap.keys())[list(self.widgetToOperationNameMap.values()).index(operationName)]
380 currentOperationRadioButton.setChecked(True)
381 for operationRadioButton in self.operationRadioButtons:
382 operationRadioButton.blockSignals(False)
383
384 segmentSelectionRequired = self.currentOperationRequiresSegmentSelection()
385 self.applyButton.setEnabled(not segmentSelectionRequired)
386 if segmentSelectionRequired:
387 self.applyButton.setToolTip("Click in a slice view to select an island.")
388 else:
389 self.applyButton.setToolTip("")
390
391 # TODO: this call has no effect now
392 # qSlicerSegmentEditorAbstractEffect should be improved so that it triggers a cursor update
393 # self.scriptedEffect.showEffectCursorInSliceView = segmentSelectionRequired
394
395 showMinimumSizeOption = (operationName in [KEEP_LARGEST_ISLAND, REMOVE_SMALL_ISLANDS, SPLIT_ISLANDS_TO_SEGMENTS])
396 self.minimumSizeSpinBox.setEnabled(showMinimumSizeOption)
397 self.minimumSizeLabel.setEnabled(showMinimumSizeOption)
398
399 self.minimumSizeSpinBox.blockSignals(True)
400 self.minimumSizeSpinBox.value = self.scriptedEffect.integerParameter("MinimumSize")
401 self.minimumSizeSpinBox.blockSignals(False)
402
404 # Operation is managed separately
405 self.scriptedEffect.setParameter("MinimumSize", self.minimumSizeSpinBox.value)
406
407
408KEEP_LARGEST_ISLAND = 'KEEP_LARGEST_ISLAND'
409KEEP_SELECTED_ISLAND = 'KEEP_SELECTED_ISLAND'
410REMOVE_SMALL_ISLANDS = 'REMOVE_SMALL_ISLANDS'
411REMOVE_SELECTED_ISLAND = 'REMOVE_SELECTED_ISLAND'
412ADD_SELECTED_ISLAND = 'ADD_SELECTED_ISLAND'
413SPLIT_ISLANDS_TO_SEGMENTS = 'SPLIT_ISLANDS_TO_SEGMENTS'
processInteractionEvents(self, callerInteractor, eventId, viewWidget)
processViewNodeEvents(self, callerViewNode, eventId, viewWidget)
splitSegments(self, minimumSize=0, maxNumberOfSegments=0, split=True)