Slicer  5.2
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.
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'