Slicer 5.13
Slicer is a multi-platform, free and open source software package for visualization and medical image computing
Loading...
Searching...
No Matches
CustomVR.py
Go to the documentation of this file.
1"""
2This example shows how to create a custom volume rendering for a volume node using the SlicerLayerDisplayableManager
3module.
4
5It goes over the following concepts:
6 - Creates a VR display pipeline on a selected volume node in the scene
7 - Register the pipeline creation mechanism
8
9Usage:
10 This example is implemented as a scripted module and can be added as such to Slicer.
11"""
12
13import logging
14
15import qt
16import slicer
17from slicer.ScriptedLoadableModule import ScriptedLoadableModule, ScriptedLoadableModuleWidget
18
19
20from LayerDMLib import ScriptedPipelineSceneConnector, vtkMRMLLayerDMScriptedPipeline
21from slicer import (
22 vtkMRMLAbstractViewNode,
23 vtkMRMLNode,
24 vtkMRMLScene,
25 vtkMRMLScriptedModuleNode,
26 vtkMRMLTransformNode,
27 vtkMRMLViewNode,
28 vtkMRMLVolumeNode,
29)
30from vtk import (
31 VTK_OBJECT,
32 calldata_type,
33 vtkColorTransferFunction,
34 vtkGPUVolumeRayCastMapper,
35 vtkMatrix4x4,
36 vtkPiecewiseFunction,
37 vtkRenderer,
38 vtkTransform,
39 vtkVolume,
40 vtkVolumeProperty,
41)
42
43
44class CustomVR(ScriptedLoadableModule):
45 def __init__(self, parent):
46 ScriptedLoadableModule.__init__(self, parent)
47 self.parent.title = "Custom VR Pipeline Example"
48 self.parent.categories = ["Examples"]
49 self.parent.dependencies = []
50 self.parent.contributors = []
51 self.parent.helpText = ""
52 self.parent.acknowledgementText = ""
53
54 # At startup completed, the pipelines are connected to the scene.
55 # This allows the pipeline registration to be done automatically at loading time,
56 # without requiring the module widget to be opened first.
57 slicer.app.connect("startupCompleted()", _sceneConnector.connect)
58
59
60class CustomVRWidget(ScriptedLoadableModuleWidget):
61 """In this example, we will display a custom volume rendering and use VTK compute shader for processing the volume."""
62
63 def setup(self) -> None:
64 # Re-connect the pipeline so that a module reload binds the reloaded pipeline class.
65 # connect first removes any previous connection, so this is safe to call
66 # even though the pipeline was already connected at application startup.
67 _sceneConnector.connect()
68 ScriptedLoadableModuleWidget.setup(self)
69
70 widget = qt.QWidget()
71 layout = qt.QVBoxLayout(widget)
72
73 # Configure volume selector node
74 self._volumeNodeSelector = slicer.qMRMLNodeComboBox(widget)
75 self._volumeNodeSelector.nodeTypes = ["vtkMRMLVolumeNode"]
76 self._volumeNodeSelector.selectNodeUponCreation = True
77 self._volumeNodeSelector.addEnabled = False
78 self._volumeNodeSelector.removeEnabled = False
79 self._volumeNodeSelector.showHidden = False
80 self._volumeNodeSelector.renameEnabled = True
81 self._volumeNodeSelector.setMRMLScene(slicer.mrmlScene)
82 layout.addWidget(self._volumeNodeSelector)
83
84 # Configure toggle button
85 toggleDisplayButton = qt.QPushButton("Toggle display")
86 toggleDisplayButton.clicked.connect(self._toggleVolumeDisplay)
87 layout.addWidget(toggleDisplayButton)
88 layout.addStretch()
89
90 self.layout.addWidget(widget)
91
92 def _toggleVolumeDisplay(self, *_):
93 volumeNode = self._volumeNodeSelector.currentNode()
94 if not volumeNode:
95 return
96
97 wasVisible = CustomVRPipeline.GetVRNodeVisibility(volumeNode, slicer.mrmlScene)
98 vrNode = CustomVRPipeline.CreateVRNode(volumeNode, slicer.mrmlScene)
99 CustomVRPipeline.SetVRNodeVisible(vrNode, not wasVisible)
100
101 def cleanup(self) -> None:
102 """
103 Called when the module is about to be unloaded and by slicer.util.reloadScriptedModule before the widget is
104 rebuilt (module Reload button).
105
106 Here, we disconnect the pipelines from the scene (unregister the pipeline creator from the factory
107 singleton and remove the scene observer). Without this, every reload would add a duplicate creator
108 and observer, and the previously registered creator would keep instantiating pipelines from the
109 pre-reload class.
110 """
111 _sceneConnector.disconnect()
112 ScriptedLoadableModuleWidget.cleanup(self)
113
114
115class CustomVRPipeline(vtkMRMLLayerDMScriptedPipeline):
116 """Custom VR pipeline"""
117
118 def __init__(self):
119 """
120 In the pipeline creation, we create the different VTK objects.
121 Here, the mapper properties are static, but they could be set in the data node and be reactive.
122 """
123 super().__init__()
124
125 self._mapper = vtkGPUVolumeRayCastMapper()
126 self._actor = vtkVolume()
127 self._actor.SetMapper(self._mapper)
128
129 # Arbitrary color / opacity functions
130 colorFunction = vtkColorTransferFunction()
131 colorFunction.AddRGBPoint(0, 0.0, 0.0, 0.0)
132 colorFunction.AddRGBPoint(3900, 1.0, 1.0, 1.0)
133
134 # Create the opacity
135 opacityFunction = vtkPiecewiseFunction()
136 opacityFunction.AddPoint(1000, 0.0)
137 opacityFunction.AddPoint(1900, 1.0)
138 opacityFunction.AddPoint(3900, 1.0)
139
140 # Create the volume property and set functions
141 self._volumeProperty = vtkVolumeProperty()
142 self._volumeProperty.SetScalarOpacity(opacityFunction)
143 self._volumeProperty.SetColor(colorFunction)
144 self._volumeProperty.SetInterpolationTypeToLinear()
145 self._volumeProperty.ShadeOn()
146
147 self._actor.SetProperty(self._volumeProperty)
148
149 # The attributes below are used to connect observers on the volumeNode and the volumeTransform ModifiedEvent
150 # The vtkMRMLLayerDMScriptedPipeline base class provides convenience methods to simplify observers
151 # See also: OnUpdate
152 # See also: UpdateObservation
153 self._volumeNode = None
155 self._imageData = None
156
157 @classmethod
158 def CreateVRNode(cls, volumeNode, scene) -> vtkMRMLScriptedModuleNode:
159 node = cls.GetVRNode(volumeNode, scene)
160 if node:
161 return node
162
164 node.SetAttribute("PipelineType", cls._GetClassName())
165 node.SetAttribute("VolumeNodeID", volumeNode.GetID())
166 node.SetAttribute("IsVisible", str(0))
167 return scene.AddNode(node)
168
169 @classmethod
170 def GetVRNodeVisibility(cls, volumeNode, scene) -> bool:
171 vrNode = cls.GetVRNode(volumeNode, scene)
172 if vrNode is None:
173 return False
174
175 return cls.GetVisibility(vrNode)
176
177 @classmethod
178 def GetVisibility(cls, node):
179 # GetAttribute returns None when the attribute has never been set.
180 value = node.GetAttribute("IsVisible") if node is not None else None
181 return bool(int(value)) if value else False
182
183 @classmethod
184 def SetVRNodeVisible(cls, node, isVisible: bool):
185 node.SetAttribute("IsVisible", str(int(isVisible)))
186
187 @classmethod
188 def IsPipelineNode(cls, node):
189 """
190 Returns True if the input vktMRMLNode is a scripted node and has the pipeline type attribute matching the
191 current pipeline class.
192 """
193 return isinstance(node, vtkMRMLScriptedModuleNode) and node.GetAttribute("PipelineType") == cls._GetClassName()
194
195 @classmethod
197 cls, viewNode: vtkMRMLAbstractViewNode, node: vtkMRMLNode,
198 ) -> vtkMRMLLayerDMScriptedPipeline | None:
199 """
200 Since we are creating pipelines for 3D views only, we check here if the view node is a ThreedView node and
201 if the node matches the current pipeline type (i.e. was created using the _CreatePipelineNode method).
202 """
203
204 if not cls.IsPipelineNode(node) or not isinstance(viewNode, vtkMRMLViewNode):
205 return None
206
207 return cls()
208
209 @classmethod
210 def _GetClassName(cls) -> str:
211 """
212 Convenience method to get the name of the current class.
213 This method will return the actual class name for inheriting classes.
214 """
215 return cls.__name__
216
217 def OnRendererAdded(self, renderer: vtkRenderer) -> None:
218 """
219 Triggered when the pipeline is displayed on a new renderer.
220 default behavior: does nothing.
221
222 Here, we add our actor to the input renderer.
223 If the pipeline renderer has changed, the pipeline's UpdateDisplay method will be triggered and in turn its
224 UpdateFromMRML method will be triggered.
225
226 Since we don't control the actual renderer used by the pipeline, this should be used systematically.
227 See also: self.GetRenderer()
228 """
229
230 if renderer is None or renderer.HasViewProp(self._actor):
231 return
232 renderer.AddViewProp(self._actor)
233
234 def OnRendererRemoved(self, renderer: vtkRenderer) -> None:
235 """
236 Triggered when the pipeline is removed from its previous renderer.
237 default behavior: does nothing.
238
239 Here, we add our actor to the input renderer.
240 If the pipeline renderer has changed, the pipeline's UpdateDisplay method will be triggered and in turn its
241 UpdateFromMRML method will be triggered.
242
243 Since we don't control the actual renderer used by the pipeline, this should be used systematically.
244 See also: self.GetRenderer()
245 """
246
247 if renderer is None or not renderer.HasViewProp(self._actor):
248 return
249 renderer.RemoveViewProp(self._actor)
250
251 def UpdateFromMRML(self):
252 """
253 Triggered by self.UpdateDisplay() calls:
254 - Called automatically at pipeline creation / add to the render window
255 - Called automatically when switching renderer
256 Override to update the representation of the pipeline in the different views.
257
258 See also: self.RequestRender()
259 default behavior: does nothing.
260 """
263 self.RequestRender()
264
265 def OnUpdate(self, obj, eventId, callData):
266 """
267 Observer update callback.
268 Triggered when any object & events observed using UpdateObservation is triggered.
269
270 :param obj: vtkObject instance which triggered the callback
271 :param eventId: Event id which triggered the callback
272 :param callData: Optional observer call data. Use self.CastCallData(callData, vtkType) to convert to Python
273 """
274
275 if obj == self._volumeNode:
278
279 self.UpdateDisplay()
280
281 def SetDisplayNode(self, node):
282 """
283 Set the display node for the pipeline has changed (initialization).
284 default behavior: Stored and display node is observed for vtkCommand::ModifiedEvent.
285 See also: self.UpdateObservation(prevObj, newObj, eventIds)
286 See also: self.OnUpdate(obj, eventId, callData)
287
288 :param node: The new instance of display node for the pipeline
289 """
290 super().SetDisplayNode(node)
291 self._ObserveVolumeNode()
292
294 volumeNode: vtkMRMLVolumeNode = self._GetVolumeNode()
295 transform = vtkTransform()
296 ijkToWorldMatrix = self._GetVolumeTransformMatrixToWorld()
297 if ijkToWorldMatrix:
298 transform.SetMatrix(ijkToWorldMatrix)
299
300 self._actor.SetUserTransform(transform)
301 self._mapper.SetInputConnection(volumeNode.GetImageDataConnection() if volumeNode else None)
302
304 """
305 Converts a volume's IJK coordinates to World coordinates.
306
307 :return: the IJK to World matrix, or None if there is no volume node.
308 """
309 volumeNode = self._GetVolumeNode()
310 if not volumeNode:
311 return None
312
313 # Check if we have a transform node
314 transformNode = volumeNode.GetParentTransformNode()
315
316 ijkToWorldMatrix = vtkMatrix4x4()
317 if not transformNode:
318 volumeNode.GetIJKToRASMatrix(ijkToWorldMatrix)
319 return ijkToWorldMatrix
320
321 # Check if the transform is linear
322 if not transformNode.IsTransformToWorldLinear():
323 return None
324
325 # IJK to RAS (Local)
326 ijkToRasMatrix = vtkMatrix4x4()
327 volumeNode.GetIJKToRASMatrix(ijkToRasMatrix)
328
329 # Parent transforms (RAS to World)
330 nodeToWorldMatrix = vtkMatrix4x4()
331 transformNode.GetMatrixTransformToWorld(nodeToWorldMatrix)
332
333 # Multiply: output = nodeToWorldMatrix * ijkToRasMatrix
334 vtkMatrix4x4.Multiply4x4(nodeToWorldMatrix, ijkToRasMatrix, ijkToWorldMatrix)
335 return ijkToWorldMatrix
336
338 self._actor.SetVisibility(self._IsVolumeVisible() and self.GetVisibility(self.GetDisplayNode()))
339
340 @classmethod
341 def RemoveVRNode(cls, volumeNode: vtkMRMLVolumeNode, scene: vtkMRMLScene):
342 node = cls.GetVRNode(volumeNode, scene)
343
344 if node is not None:
345 scene.RemoveNode(node)
346 else:
347 logging.warning(f"No VR node for: {volumeNode.GetID()}")
348
349 @classmethod
350 def GetVRNode(cls, volumeNode: vtkMRMLVolumeNode, scene: vtkMRMLScene):
351 for node in slicer.util.getNodesByClass("vtkMRMLScriptedModuleNode", scene):
352 if cls._GetVolumeNodeID(node) == volumeNode.GetID():
353 return node
354 return None
355
356 def _IsVolumeVisible(self) -> bool:
357 """Convenience method to check if the pipeline's volume node is visible."""
358 volumeNode = self._GetVolumeNode()
359 if volumeNode is None:
360 return False
361 return bool(volumeNode.GetDisplayVisibility())
362
363 def _GetVolumeNode(self) -> vtkMRMLVolumeNode | None:
364 """
365 Convenience method to get the volume node associated with the pipeline's data node.
366
367 Here, we use the following APIs:
368 - GetScene: This will return the scene on which the pipeline is attached
369 - GetDisplayNode: This will return the pipeline's data node instance
370 """
371 return self.GetScene().GetNodeByID(self._GetVolumeNodeID(self.GetDisplayNode()))
372
374 """
375 Convenience method to update the volume and the volume's transform node observers.
376
377 Here, we use the UpdateObservation method:
378 - UpdateObservation(vtkObject* prevObj, vtkObject* obj, const std::vector<unsigned long>& events) -> bool
379 - UpdateObservation(vtkObject* prevObj, vtkObject* obj, unsigned long event = vtkCommand::ModifiedEvent) -> bool
380
381 This method should be used to add an observer on VTK object.
382 By default, the object's modified event will be observed.
383 The method also supports lists of events.
384
385 On modify event, the class's self.OnUpdate method will be called.
386
387 Warning: prevObj is not mutated by this call. To update the pointer, a manual set is required after update.
388
389 See also: self.OnUpdate
390 """
391 if self._volumeNode == self._GetVolumeNode():
392 return
393
394 self.UpdateObservation(self._volumeNode, self._GetVolumeNode())
395 self._volumeNode = self._GetVolumeNode()
398
400 """
401 Convenience method to update the volume's transform node observer.
402
403 Note: Here we explicitly observe the transform modified event as modifying the transform doesn't trigger
404 its modified event.
405
406 See also: self._ObserveVolumeNode
407 """
408 transformNode = self._volumeNode.GetParentTransformNode() if self._volumeNode else None
409 if self._volumeTransform == transformNode:
410 return
411
412 self.UpdateObservation(
413 self._volumeTransform,
414 transformNode,
415 vtkMRMLTransformNode.TransformModifiedEvent,
416 )
417 self._volumeTransform = transformNode
418
420 imageData = self._volumeNode.GetImageData() if self._volumeNode else None
421 if self._imageData == imageData:
422 return
423
424 self.UpdateObservation(self._imageData, imageData)
425 self._imageData = imageData
426
427 @classmethod
428 def _GetVolumeNodeID(cls, node):
429 """Convenience method to get the volume node ID attached to the input MRML node."""
430 if cls.IsPipelineNode(node):
431 return node.GetAttribute("VolumeNodeID")
432 return ""
433
434
435class CustomVRSceneConnector(ScriptedPipelineSceneConnector):
436 """
437 Connects this module's pipelines to the scene: creates pipelines for our VR data nodes and garbage
438 collects the VR data node when the associated volume node is removed from the scene.
439
440 The ScriptedPipelineSceneConnector base class keeps track of what was connected so that the connection
441 can be undone (module unload) or replaced (module reload).
442 """
443
444 def getPipelines(self) -> list:
445 return [CustomVRPipeline]
446
447 def getSceneObservers(self) -> list:
448 return [(vtkMRMLScene.NodeRemovedEvent, self.OnNodeRemoved)]
449
450 @calldata_type(VTK_OBJECT)
451 def OnNodeRemoved(self, _caller, _event, node):
452 """
453 Garbage collect our VR data node when the associated volume node is removed from the scene.
454 Note: We use self.getScene() instead of the slicer.mrmlScene singleton so that the connector can be
455 used with an explicit scene (for instance in trame-slicer where the singleton is not available).
456 """
457 if isinstance(node, vtkMRMLVolumeNode):
458 CustomVRPipeline.RemoveVRNode(node, self.getScene())
459
460
461_sceneConnector = CustomVRSceneConnector()
OnUpdate(self, obj, eventId, callData)
Definition CustomVR.py:265
_GetVolumeNodeID(cls, node)
Definition CustomVR.py:428
None OnRendererAdded(self, vtkRenderer renderer)
Definition CustomVR.py:217
vtkMRMLScriptedModuleNode CreateVRNode(cls, volumeNode, scene)
Definition CustomVR.py:158
SetVRNodeVisible(cls, node, bool isVisible)
Definition CustomVR.py:184
vtkMRMLVolumeNode|None _volumeNode
Definition CustomVR.py:153
SetDisplayNode(self, node)
Definition CustomVR.py:281
vtkMRMLVolumeNode|None _GetVolumeNode(self)
Definition CustomVR.py:363
RemoveVRNode(cls, vtkMRMLVolumeNode volumeNode, vtkMRMLScene scene)
Definition CustomVR.py:341
vtkMRMLLayerDMScriptedPipeline|None TryCreatePipeline(cls, vtkMRMLAbstractViewNode viewNode, vtkMRMLNode node)
Definition CustomVR.py:198
None OnRendererRemoved(self, vtkRenderer renderer)
Definition CustomVR.py:234
bool _IsVolumeVisible(self)
Definition CustomVR.py:356
GetVRNode(cls, vtkMRMLVolumeNode volumeNode, vtkMRMLScene scene)
Definition CustomVR.py:350
bool GetVRNodeVisibility(cls, volumeNode, scene)
Definition CustomVR.py:170
_GetVolumeTransformMatrixToWorld(self)
Definition CustomVR.py:303
__init__(self, parent)
Definition CustomVR.py:45