Slicer 5.9
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
Loading...
Searching...
No Matches
SegmentEditorDrawEffect.py
Go to the documentation of this file.
1import logging
2import os
3
4import qt
5import vtk
6
7import slicer
8from slicer.i18n import tr as _
9
10from SegmentEditorEffects import *
11
12
14 """DrawEffect is a LabelEffect implementing the interactive draw
15 tool in the segment editor
16 """
17
18 def __init__(self, scriptedEffect):
19 scriptedEffect.name = "Draw" # no tr (don't translate it because modules find effects by name)
20 scriptedEffect.title = _("Draw")
21 self.drawPipelines = {}
22 AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect)
23
24 def clone(self):
25 import qSlicerSegmentationsEditorEffectsPythonQt as effects
26
27 clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None)
28 clonedEffect.setPythonSource(__file__.replace("\\", "/"))
29 return clonedEffect
30
31 def icon(self):
32 iconPath = os.path.join(os.path.dirname(__file__), "Resources/Icons/Draw.png")
33 if os.path.exists(iconPath):
34 return qt.QIcon(iconPath)
35 return qt.QIcon()
36
37 def helpText(self):
38 return "<html>" + _("""Draw segment outline in slice viewers<br>.
39<p><ul style="margin: 0">
40<li><b>Left-click:</b> add point.
41<li><b>Left-button drag-and-drop:</b> add multiple points.
42<li><b>x:</b> delete last point.
43<li><b>Double-left-click</b> or <b>right-click</b> or <b>a</b> or <b>enter</b>: apply outline.
44</ul><p>""")
45
46 def deactivate(self):
47 # Clear draw pipelines
48 for sliceWidget, pipeline in self.drawPipelines.items():
49 self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
50 self.drawPipelines = {}
51
52 def setupOptionsFrame(self):
53 pass
54
55 def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
56 abortEvent = False
57
58 # Only allow for slice views
59 if viewWidget.className() != "qMRMLSliceWidget":
60 return abortEvent
61 # Get draw pipeline for current slice
62 pipeline = self.pipelineForWidget(viewWidget)
63 if pipeline is None:
64 return abortEvent
65
66 anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey()
67
68 if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
69 # Make sure the user wants to do the operation, even if the segment is not visible
70 confirmedEditingAllowed = self.scriptedEffect.confirmCurrentSegmentVisible()
71 if confirmedEditingAllowed == self.scriptedEffect.NotConfirmed or confirmedEditingAllowed == self.scriptedEffect.ConfirmedWithDialog:
72 # ConfirmedWithDialog cancels the operation because the user had to move the mouse to click on the popup,
73 # which would interfere with the drawn shape. The dialog is not displayed again for the same segment.
74
75 # The event has to be aborted, because otherwise there would be a LeftButtonPressEvent without a matching
76 # LeftButtonReleaseEvent (as the popup window received the release button event).
77 abortEvent = True
78
79 return abortEvent
80
81 pipeline.actionState = "drawing"
82 self.scriptedEffect.cursorOff(viewWidget)
83 xy = callerInteractor.GetEventPosition()
84 ras = self.xyToRas(xy, viewWidget)
85 pipeline.addPoint(ras)
86 abortEvent = True
87 elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent:
88 if pipeline.actionState == "drawing":
89 pipeline.actionState = "moving"
90 self.scriptedEffect.cursorOn(viewWidget)
91 abortEvent = True
92 elif eventId == vtk.vtkCommand.RightButtonPressEvent and not anyModifierKeyPressed:
93 pipeline.actionState = "finishing"
94 sliceNode = viewWidget.sliceLogic().GetSliceNode()
95 pipeline.lastInsertSliceNodeMTime = sliceNode.GetMTime()
96 abortEvent = True
97 elif ((eventId == vtk.vtkCommand.RightButtonReleaseEvent and pipeline.actionState == "finishing")
98 or (eventId == vtk.vtkCommand.LeftButtonDoubleClickEvent and not anyModifierKeyPressed)):
99 abortEvent = (pipeline.rasPoints.GetNumberOfPoints() > 1)
100 sliceNode = viewWidget.sliceLogic().GetSliceNode()
101 if abs(pipeline.lastInsertSliceNodeMTime - sliceNode.GetMTime()) < 2:
102 pipeline.apply()
103 pipeline.actionState = ""
104 elif eventId == vtk.vtkCommand.MouseMoveEvent:
105 if pipeline.actionState == "drawing":
106 xy = callerInteractor.GetEventPosition()
107 ras = self.xyToRas(xy, viewWidget)
108 pipeline.addPoint(ras)
109 abortEvent = True
110 elif eventId == vtk.vtkCommand.KeyPressEvent:
111 key = callerInteractor.GetKeySym()
112 if key == "a" or key == "Return":
113 pipeline.apply()
114 abortEvent = True
115 if key == "x":
116 pipeline.deleteLastPoint()
117 abortEvent = True
118 else:
119 pass
120
121 pipeline.positionActors()
122 return abortEvent
123
124 def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
125 if callerViewNode and callerViewNode.IsA("vtkMRMLSliceNode"):
126 # Get draw pipeline for current slice
127 pipeline = self.pipelineForWidget(viewWidget)
128 if pipeline is None:
129 logging.error("processViewNodeEvents: Invalid pipeline")
130 return
131
132 # Make sure all points are on the current slice plane.
133 # If the SliceToRAS has been modified, then we're on a different plane
134 sliceLogic = viewWidget.sliceLogic()
135 lineMode = "solid"
136 currentSliceOffset = sliceLogic.GetSliceOffset()
137 if pipeline.activeSliceOffset:
138 offset = abs(currentSliceOffset - pipeline.activeSliceOffset)
139 if offset > 0.01:
140 if pipeline.rasPoints.GetNumberOfPoints() == 1:
141 # One placed point is not visible to the user so clear the state upon changing slice offset
142 pipeline.resetPolyData()
143 return
144 lineMode = "dashed"
145 pipeline.setLineMode(lineMode)
146 pipeline.positionActors()
147
148 def pipelineForWidget(self, sliceWidget):
149 if sliceWidget in self.drawPipelines:
150 return self.drawPipelines[sliceWidget]
151
152 # Create pipeline if does not yet exist
153 pipeline = DrawPipeline(self.scriptedEffect, sliceWidget)
154
155 # Add actor
156 renderer = self.scriptedEffect.renderer(sliceWidget)
157 if renderer is None:
158 logging.error("pipelineForWidget: Failed to get renderer!")
159 return None
160 self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)
161
162 self.drawPipelines[sliceWidget] = pipeline
163 return pipeline
164
165
166#
167# DrawPipeline
168#
170 """Visualization objects and pipeline for each slice view for drawing"""
171
172 def __init__(self, scriptedEffect, sliceWidget):
173 self.scriptedEffect = scriptedEffect
174 self.sliceWidget = sliceWidget
177 self.actionState = None
178
179 self.xyPoints = vtk.vtkPoints()
180 self.rasPoints = vtk.vtkPoints()
182
183 self.mapper = vtk.vtkPolyDataMapper2D()
184 self.actor = vtk.vtkTexturedActor2D()
185 self.mapper.SetInputData(self.polyData)
186 self.actor.SetMapper(self.mapper)
187 actorProperty = self.actor.GetProperty()
188 actorProperty.SetColor(1, 1, 0)
189 actorProperty.SetLineWidth(1)
190
191 self.createStippleTexture(0xAAAA, 8)
192
193 def createStippleTexture(self, lineStipplePattern, lineStippleRepeat):
194 self.tcoords = vtk.vtkDoubleArray()
195 self.texture = vtk.vtkTexture()
196
197 # Create texture
198 dimension = 16 * lineStippleRepeat
199
200 image = vtk.vtkImageData()
201 image.SetDimensions(dimension, 1, 1)
202 image.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 4)
203 image.SetExtent(0, dimension - 1, 0, 0, 0, 0)
204 on = 255
205 off = 0
206 i_dim = 0
207 while i_dim < dimension:
208 for i in range(0, 16):
209 mask = 1 << i
210 bit = (lineStipplePattern & mask) >> i
211 value = bit
212 if value == 0:
213 for j in range(0, lineStippleRepeat):
214 image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on)
215 image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on)
216 image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on)
217 image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, off)
218 i_dim += 1
219 else:
220 for j in range(0, lineStippleRepeat):
221 image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on)
222 image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on)
223 image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on)
224 image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, on)
225 i_dim += 1
226 self.texture.SetInputData(image)
227 self.texture.InterpolateOff()
228 self.texture.RepeatOn()
229
230 def createPolyData(self):
231 # Make an empty single-polyline polydata
232 polyData = vtk.vtkPolyData()
233 polyData.SetPoints(self.xyPoints)
234 lines = vtk.vtkCellArray()
235 polyData.SetLines(lines)
236 return polyData
237
238 def addPoint(self, ras):
239 # Add a world space point to the current outline
240
241 # Store active slice when first point is added
242 sliceLogic = self.sliceWidget.sliceLogic()
243 currentSliceOffset = sliceLogic.GetSliceOffset()
244 if not self.activeSliceOffset:
245 self.activeSliceOffset = currentSliceOffset
246 self.setLineMode("solid")
247
248 # Don't allow adding points on except on the active slice
249 # (where first point was laid down)
250 if self.activeSliceOffset != currentSliceOffset:
251 return
252
253 # Keep track of node state (in case of pan/zoom)
254 sliceNode = sliceLogic.GetSliceNode()
255 self.lastInsertSliceNodeMTime = sliceNode.GetMTime()
256
257 p = self.rasPoints.InsertNextPoint(ras)
258 if p > 0:
259 idList = vtk.vtkIdList()
260 idList.InsertNextId(p - 1)
261 idList.InsertNextId(p)
262 self.polyData.InsertNextCell(vtk.VTK_LINE, idList)
263
264 def setLineMode(self, mode="solid"):
265 actorProperty = self.actor.GetProperty()
266 if mode == "solid":
267 self.polyData.GetPointData().SetTCoords(None)
268 self.actor.SetTexture(None)
269 elif mode == "dashed":
270 # Create texture coordinates
271 self.tcoords.SetNumberOfComponents(1)
272 self.tcoords.SetNumberOfTuples(self.polyData.GetNumberOfPoints())
273 for i in range(0, self.polyData.GetNumberOfPoints()):
274 value = i * 0.5
275 self.tcoords.SetTypedTuple(i, [value])
276 self.polyData.GetPointData().SetTCoords(self.tcoords)
277 self.actor.SetTexture(self.texture)
278
279 def positionActors(self):
280 # Update draw feedback to follow slice node
281 sliceLogic = self.sliceWidget.sliceLogic()
282 sliceNode = sliceLogic.GetSliceNode()
283 rasToXY = vtk.vtkTransform()
284 rasToXY.SetMatrix(sliceNode.GetXYToRAS())
285 rasToXY.Inverse()
286 self.xyPoints.Reset()
287 rasToXY.TransformPoints(self.rasPoints, self.xyPoints)
288 self.polyData.Modified()
289 self.sliceWidget.sliceView().scheduleRender()
290
291 def apply(self):
292 lines = self.polyData.GetLines()
293 lineExists = lines.GetNumberOfCells() > 0
294 if lineExists:
295 # Close the polyline back to the first point
296 idList = vtk.vtkIdList()
297 idList.InsertNextId(self.polyData.GetNumberOfPoints() - 1)
298 idList.InsertNextId(0)
299 self.polyData.InsertNextCell(vtk.VTK_LINE, idList)
300
301 # Get modifier labelmap
302 modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
303
304 # Apply poly data on modifier labelmap
305 segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
306 self.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode)
307
308 self.resetPolyData()
309 if lineExists:
310 self.scriptedEffect.saveStateForUndo()
311 self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
312
313 def resetPolyData(self):
314 # Return the polyline to initial state with no points
315 lines = self.polyData.GetLines()
316 lines.Initialize()
317 self.xyPoints.Reset()
318 self.rasPoints.Reset()
319 self.activeSliceOffset = None
320
322 # Unwind through addPoint list back to empty polydata
323 pcount = self.rasPoints.GetNumberOfPoints()
324 if pcount <= 0:
325 return
326
327 pcount = pcount - 1
328 self.rasPoints.SetNumberOfPoints(pcount)
329
330 cellCount = self.polyData.GetNumberOfCells()
331 if cellCount > 0:
332 self.polyData.DeleteCell(cellCount - 1)
333 self.polyData.RemoveDeletedCells()
334
335 self.positionActors()
createStippleTexture(self, lineStipplePattern, lineStippleRepeat)