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
SegmentEditorLevelTracingEffect.py
Go to the documentation of this file.
1import logging
2import os
3
4import qt
5import vtk
6import vtkITK
7
8import slicer
9from slicer.i18n import tr as _
10
11from SegmentEditorEffects import *
12
13
15 """LevelTracingEffect is a LabelEffect implementing level tracing fill
16 using intensity-based isolines
17 """
18
19 def __init__(self, scriptedEffect):
20 scriptedEffect.name = "Level tracing" # no tr (don't translate it because modules find effects by name)
21 scriptedEffect.title = _("Level tracing")
22 AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect)
23
24 # Effect-specific members
26 self.lastXY = None
27
28 def clone(self):
29 import qSlicerSegmentationsEditorEffectsPythonQt as effects
30
31 clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None)
32 clonedEffect.setPythonSource(__file__.replace("\\", "/"))
33 return clonedEffect
34
35 def icon(self):
36 iconPath = os.path.join(os.path.dirname(__file__), "Resources/Icons/LevelTracing.png")
37 if os.path.exists(iconPath):
38 return qt.QIcon(iconPath)
39 return qt.QIcon()
40
41 def helpText(self):
42 return "<html>" + _("""Add uniform intensity region to selected segment<br>.
43 <p><ul style="margin: 0">
44 <li><b>Mouse move:</b> current background voxel is used to find a closed path that
45 follows the same intensity value back to the starting point within the current slice.
46 <li><b>Left-click:</b> add the previewed region to the current segment.
47 </ul><p>""")
48
49 def setupOptionsFrame(self):
50 self.sliceRotatedErrorLabel = qt.QLabel()
51 # This widget displays an error message if the slice view is not aligned
52 # with the segmentation's axes.
53 self.scriptedEffect.addOptionsWidget(self.sliceRotatedErrorLabel)
54
55 def activate(self):
56 self.sliceRotatedErrorLabel.text = ""
57
58 def deactivate(self):
59 # Clear draw pipelines
60 for sliceWidget, pipeline in self.levelTracingPipelines.items():
61 self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
62 self.levelTracingPipelines = {}
63 self.lastXY = None
64
65 def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
66 abortEvent = False
67
68 # Only allow for slice views
69 if viewWidget.className() != "qMRMLSliceWidget":
70 return abortEvent
71 # Get draw pipeline for current slice
72 pipeline = self.pipelineForWidget(viewWidget)
73 if pipeline is None:
74 return abortEvent
75
76 anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey()
77
78 if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
79 abortEvent = True
80
81 # Make sure the user wants to do the operation, even if the segment is not visible
82 if not self.scriptedEffect.confirmCurrentSegmentVisible():
83 self.sliceRotatedErrorLabel.text = ""
84 pipeline.actor.VisibilityOff()
85 self.lastXY = None
86 pipeline.sliceWidget.sliceView().scheduleRender()
87 return abortEvent
88
89 self.scriptedEffect.saveStateForUndo()
90
91 # Get modifier labelmap
92 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
93
94 # Apply poly data on modifier labelmap
95 pipeline.appendPolyMask(modifierLabelmap)
96 # TODO: it would be nice to reduce extent
97 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
98
99 elif eventId == vtk.vtkCommand.MouseMoveEvent:
100 if pipeline.actionState == "":
101 xy = callerInteractor.GetEventPosition()
102 if pipeline.preview(xy):
103 self.sliceRotatedErrorLabel.text = ""
104 else:
105 self.sliceRotatedErrorLabel.text = (
106 '<b><font color="red">'
107 + _("Slice view is not aligned with segmentation axis.<br>To use this effect, click the 'Slice views orientation' warning button.")
108 + "</font></b>")
109 abortEvent = True
110 self.lastXY = xy
111 elif eventId == vtk.vtkCommand.EnterEvent:
112 self.sliceRotatedErrorLabel.text = ""
113 pipeline.actor.VisibilityOn()
114 elif eventId == vtk.vtkCommand.LeaveEvent:
115 self.sliceRotatedErrorLabel.text = ""
116 pipeline.actor.VisibilityOff()
117 self.lastXY = None
118
119 return abortEvent
120
121 def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
122 if callerViewNode and callerViewNode.IsA("vtkMRMLSliceNode"):
123 # Get draw pipeline for current slice
124 pipeline = self.pipelineForWidget(viewWidget)
125 if pipeline is None:
126 logging.error("processViewNodeEvents: Invalid pipeline")
127 return
128
129 # Update the preview to the new slice
130 if pipeline.actionState == "" and self.lastXY:
131 pipeline.preview(self.lastXY)
132
133 def pipelineForWidget(self, sliceWidget):
134 if sliceWidget in self.levelTracingPipelines:
135 return self.levelTracingPipelines[sliceWidget]
136
137 # Create pipeline if does not yet exist
138 pipeline = LevelTracingPipeline(self, sliceWidget)
139
140 # Add actor
141 renderer = self.scriptedEffect.renderer(sliceWidget)
142 if renderer is None:
143 logging.error("setupPreviewDisplay: Failed to get renderer!")
144 return None
145 self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)
146
147 self.levelTracingPipelines[sliceWidget] = pipeline
148 return pipeline
149
150
151#
152# LevelTracingPipeline
153#
155 """Visualization objects and pipeline for each slice view for level tracing"""
156
157 def __init__(self, effect, sliceWidget):
158 self.effect = effect
159 self.sliceWidget = sliceWidget
160 self.actionState = ""
161
162 self.xyPoints = vtk.vtkPoints()
163 self.rasPoints = vtk.vtkPoints()
164 self.polyData = vtk.vtkPolyData()
165
166 self.tracingFilter = vtkITK.vtkITKLevelTracingImageFilter()
167 self.ijkToXY = vtk.vtkGeneralTransform()
168
169 self.mapper = vtk.vtkPolyDataMapper2D()
170 self.actor = vtk.vtkActor2D()
171 actorProperty = self.actor.GetProperty()
172 actorProperty.SetColor(107 / 255.0, 190 / 255.0, 99 / 255.0)
173 actorProperty.SetLineWidth(1)
174 self.mapper.SetInputData(self.polyData)
175 self.actor.SetMapper(self.mapper)
176 actorProperty = self.actor.GetProperty()
177 actorProperty.SetColor(1, 1, 0)
178 actorProperty.SetLineWidth(1)
179
180 def preview(self, xy):
181 """Calculate the current level trace view if the mouse is inside the volume extent
182 Returns False if slice views are rotated.
183 """
184
185 # Get source volume image data
186 sourceImageData = self.effect.scriptedEffect.sourceVolumeImageData()
187
188 segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode()
189 parentTransformNode = None
190 if segmentationNode:
191 parentTransformNode = segmentationNode.GetParentTransformNode()
192
193 self.xyPoints.Reset()
194 ijk = self.effect.xyToIjk(xy, self.sliceWidget, sourceImageData, parentTransformNode)
195 dimensions = sourceImageData.GetDimensions()
196
197 self.tracingFilter.SetInputData(sourceImageData)
198 self.tracingFilter.SetSeed(ijk)
199
200 # Select the plane corresponding to current slice orientation
201 # for the input volume
202 sliceNode = self.effect.scriptedEffect.viewNode(self.sliceWidget)
203 offset = max(sliceNode.GetDimensions())
204
205 i0, j0, k0 = self.effect.xyToIjk((0, 0), self.sliceWidget, sourceImageData, parentTransformNode)
206 i1, j1, k1 = self.effect.xyToIjk((offset, offset), self.sliceWidget, sourceImageData, parentTransformNode)
207 if i0 == i1:
208 self.tracingFilter.SetPlaneToJK()
209 elif j0 == j1:
210 self.tracingFilter.SetPlaneToIK()
211 elif k0 == k1:
212 self.tracingFilter.SetPlaneToIJ()
213 else:
214 self.polyData.Reset()
215 self.sliceWidget.sliceView().scheduleRender()
216 return False
217
218 self.tracingFilter.Update()
219 polyData = self.tracingFilter.GetOutput()
220
221 # Get source volume IJK to slice XY transform
222 xyToRas = sliceNode.GetXYToRAS()
223 rasToIjk = vtk.vtkMatrix4x4()
224 sourceImageData.GetImageToWorldMatrix(rasToIjk)
225 rasToIjk.Invert()
226 xyToIjk = vtk.vtkGeneralTransform()
227 xyToIjk.PostMultiply()
228 xyToIjk.Concatenate(xyToRas)
229 if parentTransformNode:
230 worldToSegmentation = vtk.vtkMatrix4x4()
231 parentTransformNode.GetMatrixTransformFromWorld(worldToSegmentation)
232 xyToIjk.Concatenate(worldToSegmentation)
233 xyToIjk.Concatenate(rasToIjk)
234 ijkToXy = xyToIjk.GetInverse()
235 if polyData.GetPoints():
236 ijkToXy.TransformPoints(polyData.GetPoints(), self.xyPoints)
237 self.polyData.DeepCopy(polyData)
238 self.polyData.GetPoints().DeepCopy(self.xyPoints)
239 else:
240 self.polyData.Reset()
241 self.sliceWidget.sliceView().scheduleRender()
242 return True
243
244 def appendPolyMask(self, modifierLabelmap):
245 lines = self.polyData.GetLines()
246 if lines.GetNumberOfCells() == 0:
247 return
248
249 # Apply poly data on modifier labelmap
250 segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode()
251 self.effect.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode)