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
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"