Slicer  5.0
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
SegmentEditorLogicalEffect.py
Go to the documentation of this file.
1 import logging
2 import os
3 
4 import qt
5 import vtk
6 
7 import slicer
8 
9 from SegmentEditorEffects import *
10 
11 
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 
47  def setupOptionsFrame(self):
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.onApply)
86  self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI)
87  self.modifierSegmentSelector.connect("selectionChanged(QItemSelection, QItemSelection)", self.updateMRMLFromGUI)
88  self.bypassMaskingCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
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 
99  def modifierSegmentID(self):
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 
111  def updateGUIFromMRML(self):
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 
152  def updateMRMLFromGUI(self):
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 
274 LOGICAL_COPY = 'COPY'
275 LOGICAL_UNION = 'UNION'
276 LOGICAL_INTERSECT = 'INTERSECT'
277 LOGICAL_SUBTRACT = 'SUBTRACT'
278 LOGICAL_INVERT = 'INVERT'
279 LOGICAL_CLEAR = 'CLEAR'
280 LOGICAL_FILL = 'FILL'