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