Slicer 5.9
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.
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"