Slicer 5.9
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
9from slicer.i18n import tr as _
10
11from SegmentEditorEffects import *
12
13
15 """Operate on connected components (islands) within a segment"""
16
17 def __init__(self, scriptedEffect):
18 scriptedEffect.name = "Islands" # no tr (don't translate it because modules find effects by name)
19 scriptedEffect.title = _("Islands")
20 AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
22
23 def clone(self):
24 import qSlicerSegmentationsEditorEffectsPythonQt as effects
25
26 clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
27 clonedEffect.setPythonSource(__file__.replace("\\", "/"))
28 return clonedEffect
29
30 def icon(self):
31 iconPath = os.path.join(os.path.dirname(__file__), "Resources/Icons/Islands.png")
32 if os.path.exists(iconPath):
33 return qt.QIcon(iconPath)
34 return qt.QIcon()
35
36 def helpText(self):
37 return "<html>" + _("""Edit islands (connected components) in a segment<br>. To get more information
38about each operation, hover the mouse over the option and wait for the tooltip to appear.""")
39
40 def setupOptionsFrame(self):
42
43 self.keepLargestOptionRadioButton = qt.QRadioButton(_("Keep largest island"))
44 self.keepLargestOptionRadioButton.setToolTip(
45 _("Keep only the largest island in selected segment, remove all other islands in the segment."))
47 self.widgetToOperationNameMap[self.keepLargestOptionRadioButton] = KEEP_LARGEST_ISLAND
48
49 self.keepSelectedOptionRadioButton = qt.QRadioButton(_("Keep selected island"))
50 self.keepSelectedOptionRadioButton.setToolTip(
51 _("Click on an island in a slice view to keep that island and remove all other islands in selected segment."))
53 self.widgetToOperationNameMap[self.keepSelectedOptionRadioButton] = KEEP_SELECTED_ISLAND
54
55 self.removeSmallOptionRadioButton = qt.QRadioButton(_("Remove small islands"))
56 self.removeSmallOptionRadioButton.setToolTip(
57 _("Remove all islands from the selected segment that are smaller than the specified minimum size."))
59 self.widgetToOperationNameMap[self.removeSmallOptionRadioButton] = REMOVE_SMALL_ISLANDS
60
61 self.removeSelectedOptionRadioButton = qt.QRadioButton(_("Remove selected island"))
62 self.removeSelectedOptionRadioButton.setToolTip(
63 _("Click on an island in a slice view to remove it from selected segment."))
65 self.widgetToOperationNameMap[self.removeSelectedOptionRadioButton] = REMOVE_SELECTED_ISLAND
66
67 self.addSelectedOptionRadioButton = qt.QRadioButton(_("Add selected island"))
68 self.addSelectedOptionRadioButton.setToolTip(
69 _("Click on a region in a slice view to add it to selected segment."))
71 self.widgetToOperationNameMap[self.addSelectedOptionRadioButton] = ADD_SELECTED_ISLAND
72
73 self.splitAllOptionRadioButton = qt.QRadioButton(_("Split islands to segments"))
74 self.splitAllOptionRadioButton.setToolTip(
75 _("Create a new segment for each island of selected segment. Islands smaller than minimum size will be removed. "
76 "Segments will be ordered by island size."))
78 self.widgetToOperationNameMap[self.splitAllOptionRadioButton] = SPLIT_ISLANDS_TO_SEGMENTS
79
80 operationLayout = qt.QGridLayout()
81 operationLayout.addWidget(self.keepLargestOptionRadioButton, 0, 0)
82 operationLayout.addWidget(self.removeSmallOptionRadioButton, 1, 0)
83 operationLayout.addWidget(self.splitAllOptionRadioButton, 2, 0)
84 operationLayout.addWidget(self.keepSelectedOptionRadioButton, 0, 1)
85 operationLayout.addWidget(self.removeSelectedOptionRadioButton, 1, 1)
86 operationLayout.addWidget(self.addSelectedOptionRadioButton, 2, 1)
87
88 self.operationRadioButtons[0].setChecked(True)
89 self.scriptedEffect.addOptionsWidget(operationLayout)
90
91 self.minimumSizeSpinBox = qt.QSpinBox()
92 self.minimumSizeSpinBox.setToolTip(_("Islands consisting of less voxels than this minimum size, will be deleted."))
93 self.minimumSizeSpinBox.setMinimum(0)
94 self.minimumSizeSpinBox.setMaximum(vtk.VTK_INT_MAX)
95 self.minimumSizeSpinBox.setValue(1000)
96 self.minimumSizeSpinBox.suffix = _(" voxels")
97 self.minimumSizeLabel = self.scriptedEffect.addLabeledOptionsWidget(_("Minimum size:"), self.minimumSizeSpinBox)
98
99 self.applyButton = qt.QPushButton(_("Apply"))
100 self.applyButton.objectName = self.__class__.__name__ + "Apply"
101 self.scriptedEffect.addOptionsWidget(self.applyButton)
102
103 for operationRadioButton in self.operationRadioButtons:
104 operationRadioButton.connect(
105 "toggled(bool)",
106 lambda toggle, widget=self.widgetToOperationNameMap[operationRadioButton]: self.onOperationSelectionChanged(widget, toggle))
107
108 self.minimumSizeSpinBox.connect("valueChanged(int)", self.updateMRMLFromGUI)
109
110 self.applyButton.connect("clicked()", self.onApply)
111
112 def onOperationSelectionChanged(self, operationName, toggle):
113 if not toggle:
114 return
115 self.scriptedEffect.setParameter("Operation", operationName)
116
117 def currentOperationRequiresSegmentSelection(self):
118 operationName = self.scriptedEffect.parameter("Operation")
119 return operationName in [KEEP_SELECTED_ISLAND, REMOVE_SELECTED_ISLAND, ADD_SELECTED_ISLAND]
120
121 def onApply(self):
122 # Make sure the user wants to do the operation, even if the segment is not visible
123 if not self.scriptedEffect.confirmCurrentSegmentVisible():
124 return
125 operationName = self.scriptedEffect.parameter("Operation")
126 minimumSize = self.scriptedEffect.integerParameter("MinimumSize")
127 if operationName == KEEP_LARGEST_ISLAND:
128 self.splitSegments(minimumSize=minimumSize, maxNumberOfSegments=1)
129 elif operationName == REMOVE_SMALL_ISLANDS:
130 self.splitSegments(minimumSize=minimumSize, split=False)
131 elif operationName == SPLIT_ISLANDS_TO_SEGMENTS:
132 self.splitSegments(minimumSize=minimumSize)
133
134 def splitSegments(self, minimumSize=0, maxNumberOfSegments=0, split=True):
135 """
136 minimumSize: if 0 then it means that all islands are kept, regardless of size
137 maxNumberOfSegments: if 0 then it means that all islands are kept, regardless of how many
138 """
139 # This can be a long operation - indicate it to the user
140 qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
141
142 self.scriptedEffect.saveStateForUndo()
143
144 # Get modifier labelmap
145 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
146
147 castIn = vtk.vtkImageCast()
148 castIn.SetInputData(selectedSegmentLabelmap)
149 castIn.SetOutputScalarTypeToUnsignedInt()
150
151 # Identify the islands in the inverted volume and
152 # find the pixel that corresponds to the background
153 islandMath = vtkITK.vtkITKIslandMath()
154 islandMath.SetInputConnection(castIn.GetOutputPort())
155 islandMath.SetFullyConnected(False)
156 islandMath.SetMinimumSize(minimumSize)
157 islandMath.Update()
158
159 islandImage = slicer.vtkOrientedImageData()
160 islandImage.ShallowCopy(islandMath.GetOutput())
161 selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
162 selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
163 islandImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
164
165 islandCount = islandMath.GetNumberOfIslands()
166 islandOrigCount = islandMath.GetOriginalNumberOfIslands()
167 ignoredIslands = islandOrigCount - islandCount
168 logging.debug("%d islands created (%d ignored)" % (islandCount, ignoredIslands))
169
170 baseSegmentName = "Label"
171 selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
172 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
173 with slicer.util.NodeModify(segmentationNode):
174 segmentation = segmentationNode.GetSegmentation()
175 selectedSegment = segmentation.GetSegment(selectedSegmentID)
176 selectedSegmentName = selectedSegment.GetName()
177 if selectedSegmentName is not None and selectedSegmentName != "":
178 baseSegmentName = selectedSegmentName
179
180 labelValues = vtk.vtkIntArray()
181 slicer.vtkSlicerSegmentationsModuleLogic.GetAllLabelValues(labelValues, islandImage)
182
183 # Erase segment from in original labelmap.
184 # Individual islands will be added back later.
185 threshold = vtk.vtkImageThreshold()
186 threshold.SetInputData(selectedSegmentLabelmap)
187 threshold.ThresholdBetween(0, 0)
188 threshold.SetInValue(0)
189 threshold.SetOutValue(0)
190 threshold.Update()
191 emptyLabelmap = slicer.vtkOrientedImageData()
192 emptyLabelmap.ShallowCopy(threshold.GetOutput())
193 emptyLabelmap.CopyDirections(selectedSegmentLabelmap)
194 self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, selectedSegmentID, emptyLabelmap,
195 slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
196
197 for i in range(labelValues.GetNumberOfTuples()):
198 if maxNumberOfSegments > 0 and i >= maxNumberOfSegments:
199 # We only care about the segments up to maxNumberOfSegments.
200 # If we do not want to split segments, we only care about the first.
201 break
202
203 labelValue = int(labelValues.GetTuple1(i))
204 segment = selectedSegment
205 segmentID = selectedSegmentID
206 if i != 0 and split:
207 segment = slicer.vtkSegment()
208 name = baseSegmentName + "_" + str(i + 1)
209 segment.SetName(name)
210 segment.AddRepresentation(
211 slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(),
212 selectedSegment.GetRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()))
213 segmentation.AddSegment(segment)
214 segmentID = segmentation.GetSegmentIdBySegment(segment)
215 segment.SetLabelValue(segmentation.GetUniqueLabelValueForSharedLabelmap(selectedSegmentID))
216
217 threshold = vtk.vtkImageThreshold()
218 threshold.SetInputData(islandMath.GetOutput())
219 if not split and maxNumberOfSegments <= 0:
220 # no need to split segments and no limit on number of segments, so we can lump all islands into one segment
221 threshold.ThresholdByLower(0)
222 threshold.SetInValue(0)
223 threshold.SetOutValue(1)
224 else:
225 # copy only selected islands; or copy islands into different segments
226 threshold.ThresholdBetween(labelValue, labelValue)
227 threshold.SetInValue(1)
228 threshold.SetOutValue(0)
229 threshold.Update()
230
231 modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd
232 if i == 0:
233 modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet
234
235 # Create oriented image data from output
236 modifierImage = slicer.vtkOrientedImageData()
237 modifierImage.DeepCopy(threshold.GetOutput())
238 selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
239 selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
240 modifierImage.SetGeometryFromImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
241 # We could use a single slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode
242 # method call to import all the resulting segments at once but that would put all the imported segments
243 # in a new layer. By using modifySegmentByLabelmap, the number of layers will not increase.
244 self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, modifierImage, modificationMode)
245
246 if not split and maxNumberOfSegments <= 0:
247 # all islands lumped into one segment, so we are done
248 break
249
250 qt.QApplication.restoreOverrideCursor()
251
252 def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
253 import vtkSegmentationCorePython as vtkSegmentationCore
254
255 abortEvent = False
256
257 # Only allow in modes where segment selection is needed
259 return False
260
261 # Only allow for slice views
262 if viewWidget.className() != "qMRMLSliceWidget":
263 return abortEvent
264
265 if (
266 eventId != vtk.vtkCommand.LeftButtonPressEvent
267 or callerInteractor.GetShiftKey()
268 or callerInteractor.GetControlKey()
269 or callerInteractor.GetAltKey()
270 ):
271 return abortEvent
272
273 # Make sure the user wants to do the operation, even if the segment is not visible
274 confirmedEditingAllowed = self.scriptedEffect.confirmCurrentSegmentVisible()
275 if (
276 confirmedEditingAllowed == self.scriptedEffect.NotConfirmed
277 or confirmedEditingAllowed == self.scriptedEffect.ConfirmedWithDialog
278 ):
279 # ConfirmedWithDialog cancels the operation because without seeing the segment, the island may have looked different
280 # than what the user remembered/expected. The dialog is not displayed again for the same segment.
281
282 # The event has to be aborted, because otherwise there would be a LeftButtonPressEvent without a matching
283 # LeftButtonReleaseEvent (as the popup window received the release button event).
284 abortEvent = True
285
286 return abortEvent
287
288 abortEvent = True
289
290 # Generate merged labelmap of all visible segments
291 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
292 visibleSegmentIds = vtk.vtkStringArray()
293 segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
294 if visibleSegmentIds.GetNumberOfValues() == 0:
295 logging.info("Island operation skipped: there are no visible segments")
296 return abortEvent
297
298 self.scriptedEffect.saveStateForUndo()
299
300 # This can be a long operation - indicate it to the user
301 qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
302
303 operationName = self.scriptedEffect.parameter("Operation")
304
305 if operationName == ADD_SELECTED_ISLAND:
306 inputLabelImage = slicer.vtkOrientedImageData()
307 if not segmentationNode.GenerateMergedLabelmapForAllSegments(inputLabelImage,
308 vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
309 None, visibleSegmentIds):
310 logging.error("Failed to apply island operation: cannot get list of visible segments")
311 qt.QApplication.restoreOverrideCursor()
312 return abortEvent
313 else:
314 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
315 # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
316 labelValue = 1
317 backgroundValue = 0
318 thresh = vtk.vtkImageThreshold()
319 thresh.SetInputData(selectedSegmentLabelmap)
320 thresh.ThresholdByLower(0)
321 thresh.SetInValue(backgroundValue)
322 thresh.SetOutValue(labelValue)
323 thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
324 thresh.Update()
325 # Create oriented image data from output
326 import vtkSegmentationCorePython as vtkSegmentationCore
327
328 inputLabelImage = slicer.vtkOrientedImageData()
329 inputLabelImage.ShallowCopy(thresh.GetOutput())
330 selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
331 selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
332 inputLabelImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
333
334 xy = callerInteractor.GetEventPosition()
335 ijk = self.xyToIjk(xy, viewWidget, inputLabelImage, segmentationNode.GetParentTransformNode())
336 pixelValue = inputLabelImage.GetScalarComponentAsFloat(ijk[0], ijk[1], ijk[2], 0)
337
338 try:
339 floodFillingFilter = vtk.vtkImageThresholdConnectivity()
340 floodFillingFilter.SetInputData(inputLabelImage)
341 seedPoints = vtk.vtkPoints()
342 origin = inputLabelImage.GetOrigin()
343 spacing = inputLabelImage.GetSpacing()
344 seedPoints.InsertNextPoint(origin[0] + ijk[0] * spacing[0], origin[1] + ijk[1] * spacing[1], origin[2] + ijk[2] * spacing[2])
345 floodFillingFilter.SetSeedPoints(seedPoints)
346 floodFillingFilter.ThresholdBetween(pixelValue, pixelValue)
347
348 if operationName == ADD_SELECTED_ISLAND:
349 floodFillingFilter.SetInValue(1)
350 floodFillingFilter.SetOutValue(0)
351 floodFillingFilter.Update()
352 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
353 modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
354 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
355
356 elif pixelValue != 0: # if clicked on empty part then there is nothing to remove or keep
357 if operationName == KEEP_SELECTED_ISLAND:
358 floodFillingFilter.SetInValue(1)
359 floodFillingFilter.SetOutValue(0)
360 else: # operationName == REMOVE_SELECTED_ISLAND:
361 floodFillingFilter.SetInValue(1)
362 floodFillingFilter.SetOutValue(0)
363
364 floodFillingFilter.Update()
365 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
366 modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
367
368 if operationName == KEEP_SELECTED_ISLAND:
369 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
370 else: # operationName == REMOVE_SELECTED_ISLAND:
371 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove)
372
373 except IndexError:
374 logging.error("Island processing failed")
375 finally:
376 qt.QApplication.restoreOverrideCursor()
377
378 return abortEvent
379
380 def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
381 pass # For the sake of example
382
383 def setMRMLDefaults(self):
384 self.scriptedEffect.setParameterDefault("Operation", KEEP_LARGEST_ISLAND)
385 self.scriptedEffect.setParameterDefault("MinimumSize", 1000)
386
387 def updateGUIFromMRML(self):
388 for operationRadioButton in self.operationRadioButtons:
389 operationRadioButton.blockSignals(True)
390 operationName = self.scriptedEffect.parameter("Operation")
391 currentOperationRadioButton = list(self.widgetToOperationNameMap.keys())[list(self.widgetToOperationNameMap.values()).index(operationName)]
392 currentOperationRadioButton.setChecked(True)
393 for operationRadioButton in self.operationRadioButtons:
394 operationRadioButton.blockSignals(False)
395
396 segmentSelectionRequired = self.currentOperationRequiresSegmentSelection()
397 self.applyButton.setEnabled(not segmentSelectionRequired)
398 if segmentSelectionRequired:
399 self.applyButton.setToolTip(_("Click in a slice view to select an island."))
400 else:
401 self.applyButton.setToolTip("")
402
403 # TODO: this call has no effect now
404 # qSlicerSegmentEditorAbstractEffect should be improved so that it triggers a cursor update
405 # self.scriptedEffect.showEffectCursorInSliceView = segmentSelectionRequired
406
407 showMinimumSizeOption = operationName in [KEEP_LARGEST_ISLAND, REMOVE_SMALL_ISLANDS, SPLIT_ISLANDS_TO_SEGMENTS]
408 self.minimumSizeSpinBox.setEnabled(showMinimumSizeOption)
409 self.minimumSizeLabel.setEnabled(showMinimumSizeOption)
410
411 self.minimumSizeSpinBox.blockSignals(True)
412 self.minimumSizeSpinBox.value = self.scriptedEffect.integerParameter("MinimumSize")
413 self.minimumSizeSpinBox.blockSignals(False)
414
415 def updateMRMLFromGUI(self):
416 # Operation is managed separately
417 self.scriptedEffect.setParameter("MinimumSize", self.minimumSizeSpinBox.value)
418
419
420KEEP_LARGEST_ISLAND = "KEEP_LARGEST_ISLAND"
421KEEP_SELECTED_ISLAND = "KEEP_SELECTED_ISLAND"
422REMOVE_SMALL_ISLANDS = "REMOVE_SMALL_ISLANDS"
423REMOVE_SELECTED_ISLAND = "REMOVE_SELECTED_ISLAND"
424ADD_SELECTED_ISLAND = "ADD_SELECTED_ISLAND"
425SPLIT_ISLANDS_TO_SEGMENTS = "SPLIT_ISLANDS_TO_SEGMENTS"