Slicer  4.11
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 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'