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