Slicer 5.13
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 numberOfIslands = labelValues.GetNumberOfTuples()
183
184 # The selected segment is not erased before the islands are written back; and its
185 # content is replaced (using "Set" modification mode) only at the end of the operation.
186 # This matters when the editable area is set to be the selected segment
187 # (or any segment set that includes it): erasing the segment first would make the
188 # editable area empty, so nothing could be written back and the whole segment would be
189 # cleared. For the same reason, when islands are split into new segments, those new
190 # segments are filled first, while the selected segment (which defines the editable
191 # area) is still intact.
192
193 if split:
194 for i in range(1, numberOfIslands):
195 if maxNumberOfSegments > 0 and i >= maxNumberOfSegments:
196 # We only care about the segments up to maxNumberOfSegments.
197 break
198
199 labelValue = int(labelValues.GetTuple1(i))
200 segment = slicer.vtkSegment()
201 name = baseSegmentName + "_" + str(i + 1)
202 segment.SetName(name)
203 segment.AddRepresentation(
204 slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(),
205 selectedSegment.GetRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()))
206 segmentation.AddSegment(segment)
207 segmentID = segmentation.GetSegmentIdBySegment(segment)
208 segment.SetLabelValue(segmentation.GetUniqueLabelValueForSharedLabelmap(selectedSegmentID))
209
210 threshold = vtk.vtkImageThreshold()
211 threshold.SetInputData(islandMath.GetOutput())
212 threshold.ThresholdBetween(labelValue, labelValue)
213 threshold.SetInValue(1)
214 threshold.SetOutValue(0)
215 threshold.Update()
216
217 # Create oriented image data from output
218 modifierImage = slicer.vtkOrientedImageData()
219 modifierImage.DeepCopy(threshold.GetOutput())
220 modifierImage.SetGeometryFromImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
221 # We could use a single slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode
222 # method call to import all the resulting segments at once but that would put all the imported segments
223 # in a new layer. By using modifySegmentByLabelmap, the number of layers will not increase.
224 self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, modifierImage,
225 slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
226
227 # Replace the content of the selected segment (done last, see comment above).
228 threshold = vtk.vtkImageThreshold()
229 threshold.SetInputData(islandMath.GetOutput())
230 if numberOfIslands > 0 and not split and maxNumberOfSegments <= 0:
231 # No need to split segments and no limit on the number of segments,
232 # so lump all islands into the selected segment.
233 threshold.ThresholdByLower(0)
234 threshold.SetInValue(0)
235 threshold.SetOutValue(1)
236 elif numberOfIslands > 0:
237 # Keep only the first (largest) island in the selected segment.
238 labelValue = int(labelValues.GetTuple1(0))
239 threshold.ThresholdBetween(labelValue, labelValue)
240 threshold.SetInValue(1)
241 threshold.SetOutValue(0)
242 else:
243 # No islands remain (for example, all islands are smaller than the minimum size):
244 # clear the selected segment.
245 threshold.ThresholdByLower(0)
246 threshold.SetInValue(0)
247 threshold.SetOutValue(0)
248 threshold.Update()
249
250 # Create oriented image data from output
251 modifierImage = slicer.vtkOrientedImageData()
252 modifierImage.DeepCopy(threshold.GetOutput())
253 modifierImage.SetGeometryFromImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
254 self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, selectedSegmentID, modifierImage,
255 slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
256
257 qt.QApplication.restoreOverrideCursor()
258
259 def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
260 import vtkSegmentationCorePython as vtkSegmentationCore
261
262 abortEvent = False
263
264 # Only allow in modes where segment selection is needed
266 return False
267
268 # Only allow for slice views
269 if viewWidget.className() != "qMRMLSliceWidget":
270 return abortEvent
271
272 if (
273 eventId != vtk.vtkCommand.LeftButtonPressEvent
274 or callerInteractor.GetShiftKey()
275 or callerInteractor.GetControlKey()
276 or callerInteractor.GetAltKey()
277 ):
278 return abortEvent
279
280 # Make sure the user wants to do the operation, even if the segment is not visible
281 confirmedEditingAllowed = self.scriptedEffect.confirmCurrentSegmentVisible()
282 if (
283 confirmedEditingAllowed == self.scriptedEffect.NotConfirmed
284 or confirmedEditingAllowed == self.scriptedEffect.ConfirmedWithDialog
285 ):
286 # ConfirmedWithDialog cancels the operation because without seeing the segment, the island may have looked different
287 # than what the user remembered/expected. The dialog is not displayed again for the same segment.
288
289 # The event has to be aborted, because otherwise there would be a LeftButtonPressEvent without a matching
290 # LeftButtonReleaseEvent (as the popup window received the release button event).
291 abortEvent = True
292
293 return abortEvent
294
295 abortEvent = True
296
297 # Generate merged labelmap of all visible segments
298 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
299 visibleSegmentIds = vtk.vtkStringArray()
300 segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
301 if visibleSegmentIds.GetNumberOfValues() == 0:
302 logging.info("Island operation skipped: there are no visible segments")
303 return abortEvent
304
305 self.scriptedEffect.saveStateForUndo()
306
307 # This can be a long operation - indicate it to the user
308 qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
309
310 operationName = self.scriptedEffect.parameter("Operation")
311
312 if operationName == ADD_SELECTED_ISLAND:
313 inputLabelImage = slicer.vtkOrientedImageData()
314 if not segmentationNode.GenerateMergedLabelmapForAllSegments(inputLabelImage,
315 vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
316 None, visibleSegmentIds):
317 logging.error("Failed to apply island operation: cannot get list of visible segments")
318 qt.QApplication.restoreOverrideCursor()
319 return abortEvent
320 else:
321 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
322 # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
323 labelValue = 1
324 backgroundValue = 0
325 thresh = vtk.vtkImageThreshold()
326 thresh.SetInputData(selectedSegmentLabelmap)
327 thresh.ThresholdByLower(0)
328 thresh.SetInValue(backgroundValue)
329 thresh.SetOutValue(labelValue)
330 thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
331 thresh.Update()
332 # Create oriented image data from output
333 import vtkSegmentationCorePython as vtkSegmentationCore
334
335 inputLabelImage = slicer.vtkOrientedImageData()
336 inputLabelImage.ShallowCopy(thresh.GetOutput())
337 selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
338 selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
339 inputLabelImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
340
341 xy = callerInteractor.GetEventPosition()
342 ijk = self.xyToIjk(xy, viewWidget, inputLabelImage, segmentationNode.GetParentTransformNode())
343 pixelValue = inputLabelImage.GetScalarComponentAsFloat(ijk[0], ijk[1], ijk[2], 0)
344
345 try:
346 floodFillingFilter = vtk.vtkImageThresholdConnectivity()
347 floodFillingFilter.SetInputData(inputLabelImage)
348 seedPoints = vtk.vtkPoints()
349 origin = inputLabelImage.GetOrigin()
350 spacing = inputLabelImage.GetSpacing()
351 seedPoints.InsertNextPoint(origin[0] + ijk[0] * spacing[0], origin[1] + ijk[1] * spacing[1], origin[2] + ijk[2] * spacing[2])
352 floodFillingFilter.SetSeedPoints(seedPoints)
353 floodFillingFilter.ThresholdBetween(pixelValue, pixelValue)
354
355 if operationName == ADD_SELECTED_ISLAND:
356 floodFillingFilter.SetInValue(1)
357 floodFillingFilter.SetOutValue(0)
358 floodFillingFilter.Update()
359 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
360 modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
361 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
362
363 elif pixelValue != 0: # if clicked on empty part then there is nothing to remove or keep
364 if operationName == KEEP_SELECTED_ISLAND:
365 floodFillingFilter.SetInValue(1)
366 floodFillingFilter.SetOutValue(0)
367 else: # operationName == REMOVE_SELECTED_ISLAND:
368 floodFillingFilter.SetInValue(1)
369 floodFillingFilter.SetOutValue(0)
370
371 floodFillingFilter.Update()
372 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
373 modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
374
375 if operationName == KEEP_SELECTED_ISLAND:
376 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
377 else: # operationName == REMOVE_SELECTED_ISLAND:
378 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove)
379
380 except IndexError:
381 logging.error("Island processing failed")
382 finally:
383 qt.QApplication.restoreOverrideCursor()
384
385 return abortEvent
386
387 def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
388 pass # For the sake of example
389
390 def setMRMLDefaults(self):
391 self.scriptedEffect.setParameterDefault("Operation", KEEP_LARGEST_ISLAND)
392 self.scriptedEffect.setParameterDefault("MinimumSize", 1000)
393
394 def updateGUIFromMRML(self):
395 for operationRadioButton in self.operationRadioButtons:
396 operationRadioButton.blockSignals(True)
397 operationName = self.scriptedEffect.parameter("Operation")
398 currentOperationRadioButton = list(self.widgetToOperationNameMap.keys())[list(self.widgetToOperationNameMap.values()).index(operationName)]
399 currentOperationRadioButton.setChecked(True)
400 for operationRadioButton in self.operationRadioButtons:
401 operationRadioButton.blockSignals(False)
402
403 segmentSelectionRequired = self.currentOperationRequiresSegmentSelection()
404 self.applyButton.setEnabled(not segmentSelectionRequired)
405 if segmentSelectionRequired:
406 self.applyButton.setToolTip(_("Click in a slice view to select an island."))
407 else:
408 self.applyButton.setToolTip("")
409
410 # TODO: this call has no effect now
411 # qSlicerSegmentEditorAbstractEffect should be improved so that it triggers a cursor update
412 # self.scriptedEffect.showEffectCursorInSliceView = segmentSelectionRequired
413
414 showMinimumSizeOption = operationName in [KEEP_LARGEST_ISLAND, REMOVE_SMALL_ISLANDS, SPLIT_ISLANDS_TO_SEGMENTS]
415 self.minimumSizeSpinBox.setEnabled(showMinimumSizeOption)
416 self.minimumSizeLabel.setEnabled(showMinimumSizeOption)
417
418 self.minimumSizeSpinBox.blockSignals(True)
419 self.minimumSizeSpinBox.value = self.scriptedEffect.integerParameter("MinimumSize")
420 self.minimumSizeSpinBox.blockSignals(False)
421
422 def updateMRMLFromGUI(self):
423 # Operation is managed separately
424 self.scriptedEffect.setParameter("MinimumSize", self.minimumSizeSpinBox.value)
425
426
427KEEP_LARGEST_ISLAND = "KEEP_LARGEST_ISLAND"
428KEEP_SELECTED_ISLAND = "KEEP_SELECTED_ISLAND"
429REMOVE_SMALL_ISLANDS = "REMOVE_SMALL_ISLANDS"
430REMOVE_SELECTED_ISLAND = "REMOVE_SELECTED_ISLAND"
431ADD_SELECTED_ISLAND = "ADD_SELECTED_ISLAND"
432SPLIT_ISLANDS_TO_SEGMENTS = "SPLIT_ISLANDS_TO_SEGMENTS"