Slicer 5.4
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
Loading...
Searching...
No Matches
SegmentEditorLogicalEffect.py
Go to the documentation of this file.
1import logging
2import os
3
4import qt
5import vtk
6
7import slicer
8
9from SegmentEditorEffects import *
10
11
12class SegmentEditorLogicalEffect(AbstractScriptedSegmentEditorEffect):
13 """ LogicalEffect is an MorphologyEffect to erode a layer of pixels from a segment
14 """
15
16 def __init__(self, scriptedEffect):
17 scriptedEffect.name = 'Logical operators'
18 self.operationsRequireModifierSegment = [LOGICAL_COPY, LOGICAL_UNION, LOGICAL_SUBTRACT, LOGICAL_INTERSECT]
19 AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
20
21 def clone(self):
22 import qSlicerSegmentationsEditorEffectsPythonQt as effects
23 clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
24 clonedEffect.setPythonSource(__file__.replace('\\', '/'))
25 return clonedEffect
26
27 def icon(self):
28 iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Logical.png')
29 if os.path.exists(iconPath):
30 return qt.QIcon(iconPath)
31 return qt.QIcon()
32
33 def helpText(self):
34 return """<html>Apply logical operators or combine segments<br>. Available operations:<p>
35<ul style="margin: 0">
36<li><b>Copy:</b> replace the selected segment by the modifier segment.</li>
37<li><b>Add:</b> add modifier segment to current segment.</li>
38<li><b>Subtract:</b> subtract region of modifier segment from the selected segment.</li>
39<li><b>Intersect:</b> only keeps those regions in the select segment that are common with the modifier segment.</li>
40<li><b>Invert:</b> inverts selected segment.</li>
41<li><b>Clear:</b> clears selected segment.</li>
42<li><b>Fill:</b> completely fills selected segment.</li>
43</ul><p>
44<b>Selected segment:</b> segment selected in the segment list - above. <b>Modifier segment:</b> segment chosen in segment list in effect options - below.
45<p></html>"""
46
48
49 self.methodSelectorComboBox = qt.QComboBox()
50 self.methodSelectorComboBox.addItem("Copy", LOGICAL_COPY)
51 self.methodSelectorComboBox.addItem("Add", LOGICAL_UNION)
52 self.methodSelectorComboBox.addItem("Subtract", LOGICAL_SUBTRACT)
53 self.methodSelectorComboBox.addItem("Intersect", LOGICAL_INTERSECT)
54 self.methodSelectorComboBox.addItem("Invert", LOGICAL_INVERT)
55 self.methodSelectorComboBox.addItem("Clear", LOGICAL_CLEAR)
56 self.methodSelectorComboBox.addItem("Fill", LOGICAL_FILL)
57 self.methodSelectorComboBox.setToolTip('Click <dfn>Show details</dfn> link above for description of operations.')
58
59 self.bypassMaskingCheckBox = qt.QCheckBox("Bypass masking")
60 self.bypassMaskingCheckBox.setToolTip("Ignore all masking options and only modify the selected segment.")
61 self.bypassMaskingCheckBox.objectName = self.__class__.__name__ + 'BypassMasking'
62
63 self.applyButton = qt.QPushButton("Apply")
64 self.applyButton.objectName = self.__class__.__name__ + 'Apply'
65
66 operationFrame = qt.QHBoxLayout()
67 operationFrame.addWidget(self.methodSelectorComboBox)
68 operationFrame.addWidget(self.applyButton)
69 operationFrame.addWidget(self.bypassMaskingCheckBox)
70 self.marginSizeMmLabel = self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationFrame)
71
72 self.modifierSegmentSelectorLabel = qt.QLabel("Modifier segment:")
73 self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelectorLabel)
74
75 self.modifierSegmentSelector = slicer.qMRMLSegmentsTableView()
76 self.modifierSegmentSelector.selectionMode = qt.QAbstractItemView.SingleSelection
77 self.modifierSegmentSelector.headerVisible = False
78 self.modifierSegmentSelector.visibilityColumnVisible = False
79 self.modifierSegmentSelector.opacityColumnVisible = False
80
81 self.modifierSegmentSelector.setMRMLScene(slicer.mrmlScene)
82 self.modifierSegmentSelector.setToolTip('Contents of this segment will be used for modifying the selected segment. This segment itself will not be changed.')
83 self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelector)
84
85 self.applyButton.connect('clicked()', self.onApplyonApply)
86 self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUIupdateMRMLFromGUI)
87 self.modifierSegmentSelector.connect("selectionChanged(QItemSelection, QItemSelection)", self.updateMRMLFromGUIupdateMRMLFromGUI)
88 self.bypassMaskingCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUIupdateMRMLFromGUI)
89
90 def createCursor(self, widget):
91 # Turn off effect-specific cursor for this effect
92 return slicer.util.mainWindow().cursor
93
94 def setMRMLDefaults(self):
95 self.scriptedEffect.setParameterDefault("Operation", LOGICAL_COPY)
96 self.scriptedEffect.setParameterDefault("ModifierSegmentID", "")
97 self.scriptedEffect.setParameterDefault("BypassMasking", 1)
98
100 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
101 if not segmentationNode:
102 return ""
103 if not self.scriptedEffect.parameterDefined("ModifierSegmentID"):
104 # Avoid logging warning
105 return ""
106 modifierSegmentIDs = self.scriptedEffect.parameter("ModifierSegmentID").split(';')
107 if not modifierSegmentIDs:
108 return ""
109 return modifierSegmentIDs[0]
110
112 operation = self.scriptedEffect.parameter("Operation")
113 operationIndex = self.methodSelectorComboBox.findData(operation)
114 wasBlocked = self.methodSelectorComboBox.blockSignals(True)
115 self.methodSelectorComboBox.setCurrentIndex(operationIndex)
116 self.methodSelectorComboBox.blockSignals(wasBlocked)
117
118 modifierSegmentID = self.modifierSegmentID()
119 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
120 wasBlocked = self.modifierSegmentSelector.blockSignals(True)
121 self.modifierSegmentSelector.setSegmentationNode(segmentationNode)
122 self.modifierSegmentSelector.setSelectedSegmentIDs([modifierSegmentID])
123 self.modifierSegmentSelector.blockSignals(wasBlocked)
124
125 modifierSegmentRequired = (operation in self.operationsRequireModifierSegment)
126 self.modifierSegmentSelectorLabel.setVisible(modifierSegmentRequired)
127 self.modifierSegmentSelector.setVisible(modifierSegmentRequired)
128
129 if operation == LOGICAL_COPY:
130 self.modifierSegmentSelectorLabel.text = "Copy from segment:"
131 elif operation == LOGICAL_UNION:
132 self.modifierSegmentSelectorLabel.text = "Add segment:"
133 elif operation == LOGICAL_SUBTRACT:
134 self.modifierSegmentSelectorLabel.text = "Subtract segment:"
135 elif operation == LOGICAL_INTERSECT:
136 self.modifierSegmentSelectorLabel.text = "Intersect with segment:"
137 else:
138 self.modifierSegmentSelectorLabel.text = "Modifier segment:"
139
140 if modifierSegmentRequired and not modifierSegmentID:
141 self.applyButton.setToolTip("Please select a modifier segment in the list below.")
142 self.applyButton.enabled = False
143 else:
144 self.applyButton.setToolTip("")
145 self.applyButton.enabled = True
146
147 bypassMasking = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("BypassMasking") == 0 else qt.Qt.Checked
148 wasBlocked = self.bypassMaskingCheckBox.blockSignals(True)
149 self.bypassMaskingCheckBox.setCheckState(bypassMasking)
150 self.bypassMaskingCheckBox.blockSignals(wasBlocked)
151
153 operationIndex = self.methodSelectorComboBox.currentIndex
154 operation = self.methodSelectorComboBox.itemData(operationIndex)
155 self.scriptedEffect.setParameter("Operation", operation)
156
157 bypassMasking = 1 if self.bypassMaskingCheckBox.isChecked() else 0
158 self.scriptedEffect.setParameter("BypassMasking", bypassMasking)
159
160 modifierSegmentIDs = ';'.join(self.modifierSegmentSelector.selectedSegmentIDs()) # semicolon-separated list of segment IDs
161 self.scriptedEffect.setParameter("ModifierSegmentID", modifierSegmentIDs)
162
163 def getInvertedBinaryLabelmap(self, modifierLabelmap):
164 fillValue = 1
165 eraseValue = 0
166 inverter = vtk.vtkImageThreshold()
167 inverter.SetInputData(modifierLabelmap)
168 inverter.SetInValue(fillValue)
169 inverter.SetOutValue(eraseValue)
170 inverter.ReplaceInOn()
171 inverter.ThresholdByLower(0)
172 inverter.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
173 inverter.Update()
174
175 invertedModifierLabelmap = slicer.vtkOrientedImageData()
176 invertedModifierLabelmap.ShallowCopy(inverter.GetOutput())
177 imageToWorldMatrix = vtk.vtkMatrix4x4()
178 modifierLabelmap.GetImageToWorldMatrix(imageToWorldMatrix)
179 invertedModifierLabelmap.SetGeometryFromImageToWorldMatrix(imageToWorldMatrix)
180 return invertedModifierLabelmap
181
182 def onApply(self):
183 # Make sure the user wants to do the operation, even if the segment is not visible
184 if not self.scriptedEffect.confirmCurrentSegmentVisible():
185 return
186
187 import vtkSegmentationCorePython as vtkSegmentationCore
188
189 self.scriptedEffect.saveStateForUndo()
190
191 # Get modifier labelmap and parameters
192
193 operation = self.scriptedEffect.parameter("Operation")
194 bypassMasking = (self.scriptedEffect.integerParameter("BypassMasking") != 0)
195
196 selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
197
198 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
199 segmentation = segmentationNode.GetSegmentation()
200
201 if operation in self.operationsRequireModifierSegment:
202
203 # Get modifier segment
204 modifierSegmentID = self.modifierSegmentID()
205 if not modifierSegmentID:
206 logging.error(f"Operation {operation} requires a selected modifier segment")
207 return
208 modifierSegment = segmentation.GetSegment(modifierSegmentID)
209 modifierSegmentLabelmap = slicer.vtkOrientedImageData()
210 segmentationNode.GetBinaryLabelmapRepresentation(modifierSegmentID, modifierSegmentLabelmap)
211
212 # Get common geometry
213 commonGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry(
214 vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS, None)
215 if not commonGeometryString:
216 logging.info("Logical operation skipped: all segments are empty")
217 return
218 commonGeometryImage = slicer.vtkOrientedImageData()
219 vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(commonGeometryString, commonGeometryImage, False)
220
221 # Make sure modifier segment has correct geometry
222 # (if modifier segment has been just copied over from another segment then its geometry may be different)
223 if not vtkSegmentationCore.vtkOrientedImageDataResample.DoGeometriesMatch(commonGeometryImage, modifierSegmentLabelmap):
224 modifierSegmentLabelmap_CommonGeometry = slicer.vtkOrientedImageData()
225 vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
226 modifierSegmentLabelmap, commonGeometryImage, modifierSegmentLabelmap_CommonGeometry,
227 False, # nearest neighbor interpolation,
228 True # make sure resampled modifier segment is not cropped
229 )
230 modifierSegmentLabelmap = modifierSegmentLabelmap_CommonGeometry
231
232 if operation == LOGICAL_COPY:
233 self.scriptedEffect.modifySelectedSegmentByLabelmap(
234 modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking)
235 elif operation == LOGICAL_UNION:
236 self.scriptedEffect.modifySelectedSegmentByLabelmap(
237 modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd, bypassMasking)
238 elif operation == LOGICAL_SUBTRACT:
239 self.scriptedEffect.modifySelectedSegmentByLabelmap(
240 modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove, bypassMasking)
241 elif operation == LOGICAL_INTERSECT:
242 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
243 intersectionLabelmap = slicer.vtkOrientedImageData()
244 vtkSegmentationCore.vtkOrientedImageDataResample.MergeImage(
245 selectedSegmentLabelmap, modifierSegmentLabelmap, intersectionLabelmap,
246 vtkSegmentationCore.vtkOrientedImageDataResample.OPERATION_MINIMUM, selectedSegmentLabelmap.GetExtent())
247 selectedSegmentLabelmapExtent = selectedSegmentLabelmap.GetExtent()
248 modifierSegmentLabelmapExtent = modifierSegmentLabelmap.GetExtent()
249 commonExtent = [max(selectedSegmentLabelmapExtent[0], modifierSegmentLabelmapExtent[0]),
250 min(selectedSegmentLabelmapExtent[1], modifierSegmentLabelmapExtent[1]),
251 max(selectedSegmentLabelmapExtent[2], modifierSegmentLabelmapExtent[2]),
252 min(selectedSegmentLabelmapExtent[3], modifierSegmentLabelmapExtent[3]),
253 max(selectedSegmentLabelmapExtent[4], modifierSegmentLabelmapExtent[4]),
254 min(selectedSegmentLabelmapExtent[5], modifierSegmentLabelmapExtent[5])]
255 self.scriptedEffect.modifySelectedSegmentByLabelmap(
256 intersectionLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, commonExtent, bypassMasking)
257
258 elif operation == LOGICAL_INVERT:
259 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
260 invertedSelectedSegmentLabelmap = self.getInvertedBinaryLabelmap(selectedSegmentLabelmap)
261 self.scriptedEffect.modifySelectedSegmentByLabelmap(
262 invertedSelectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking)
263
264 elif operation == LOGICAL_CLEAR or operation == LOGICAL_FILL:
265 selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
266 vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(selectedSegmentLabelmap, 1 if operation == LOGICAL_FILL else 0, selectedSegmentLabelmap.GetExtent())
267 self.scriptedEffect.modifySelectedSegmentByLabelmap(
268 selectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking)
269
270 else:
271 logging.error(f"Unknown operation: {operation}")
272
273
274LOGICAL_COPY = 'COPY'
275LOGICAL_UNION = 'UNION'
276LOGICAL_INTERSECT = 'INTERSECT'
277LOGICAL_SUBTRACT = 'SUBTRACT'
278LOGICAL_INVERT = 'INVERT'
279LOGICAL_CLEAR = 'CLEAR'
280LOGICAL_FILL = 'FILL'