morphoview 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
morphoview/__init__.py ADDED
@@ -0,0 +1,93 @@
1
+ """morphoview -- read, analyze and visualize neuronal morphologies.
2
+
3
+ Quick start::
4
+
5
+ import morphoview as nv
6
+
7
+ graph = nv.swc_to_graph("cell.swc")
8
+ print(nv.summary(graph))
9
+
10
+ # Matplotlib (portable, always available if matplotlib is installed)
11
+ from morphoview.backends import mpl
12
+ mpl.plot_3d(graph, color=nv.get_colormap("3cd2"))
13
+ mpl.show()
14
+
15
+ # VTK (tapered tubes, interactive)
16
+ from morphoview.backends import vtk
17
+ vtk.show(graph, colormap=nv.get_colormap("3cd2"), axes=True)
18
+
19
+ The rendering backends (matplotlib, VTK, vispy) are optional and imported
20
+ lazily, so the core I/O and analysis work with just numpy + networkx.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from . import colors, graph, swc, transform
26
+ from .colors import (
27
+ COLORMAPS,
28
+ DEFAULT_COLORMAP,
29
+ STRUCTURE_IDS,
30
+ STRUCTURE_NAMES,
31
+ color_for_structure,
32
+ get_colormap,
33
+ normalize,
34
+ structure_name,
35
+ )
36
+ from .graph import (
37
+ branch_points,
38
+ combine,
39
+ graph_to_swc,
40
+ leaf_nodes,
41
+ n_branches,
42
+ n_leaves,
43
+ soma_distance,
44
+ soma_pathlen,
45
+ structure_node_map,
46
+ summary,
47
+ swc_to_graph,
48
+ total_length,
49
+ )
50
+ from .swc import SWC_DTYPE, load_swc, save_swc
51
+ from .transform import apply_transform, mirror, rotate, translate
52
+
53
+ __version__ = "0.1.0"
54
+
55
+ __all__ = [
56
+ "__version__",
57
+ # submodules
58
+ "colors",
59
+ "graph",
60
+ "swc",
61
+ "transform",
62
+ # colors
63
+ "COLORMAPS",
64
+ "DEFAULT_COLORMAP",
65
+ "STRUCTURE_IDS",
66
+ "STRUCTURE_NAMES",
67
+ "color_for_structure",
68
+ "get_colormap",
69
+ "normalize",
70
+ "structure_name",
71
+ # swc I/O
72
+ "SWC_DTYPE",
73
+ "load_swc",
74
+ "save_swc",
75
+ # graph
76
+ "swc_to_graph",
77
+ "graph_to_swc",
78
+ "combine",
79
+ "branch_points",
80
+ "n_branches",
81
+ "leaf_nodes",
82
+ "n_leaves",
83
+ "total_length",
84
+ "soma_distance",
85
+ "soma_pathlen",
86
+ "structure_node_map",
87
+ "summary",
88
+ # transforms
89
+ "apply_transform",
90
+ "translate",
91
+ "rotate",
92
+ "mirror",
93
+ ]
@@ -0,0 +1,9 @@
1
+ """Rendering backends for neuronal morphologies.
2
+
3
+ Each backend is imported lazily so that the heavy, optional dependencies
4
+ (matplotlib, VTK, vispy) are only required if you actually use them.
5
+ Import the one you need directly, e.g.::
6
+
7
+ from morphoview.backends import mpl
8
+ from morphoview.backends import vtk
9
+ """
@@ -0,0 +1,101 @@
1
+ """Render a rotating movie of a morphology with VTK.
2
+
3
+ Rotates the neuron about one or more axes and writes each frame, producing
4
+ an animation. Requires the ``vtk`` package.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Dict, Optional, Tuple
10
+
11
+ import networkx as nx
12
+
13
+ from .. import colors, transform
14
+ from . import vtk as vtk_backend
15
+
16
+ vtk = vtk_backend.vtk
17
+
18
+
19
+ def _bounding_center(graph: nx.DiGraph) -> Tuple[float, float, float]:
20
+ xs = [graph.nodes[n]["x"] for n in graph]
21
+ ys = [graph.nodes[n]["y"] for n in graph]
22
+ zs = [graph.nodes[n]["z"] for n in graph]
23
+ return (
24
+ (min(xs) + max(xs)) * 0.5,
25
+ (min(ys) + max(ys)) * 0.5,
26
+ (min(zs) + max(zs)) * 0.5,
27
+ )
28
+
29
+
30
+ def dump_movie(
31
+ filename: str,
32
+ graph: nx.DiGraph,
33
+ colormap: Optional[Dict[int, colors.RGB]] = None,
34
+ background: Tuple[float, float, float] = (0, 0, 0),
35
+ lines: float = 0.0,
36
+ xrot: float = 0.0,
37
+ yrot: float = 0.0,
38
+ zrot: float = 0.0,
39
+ xangle: float = 0.0,
40
+ yangle: float = 0.0,
41
+ zangle: float = 0.0,
42
+ frames_per_degree: int = 10,
43
+ framerate: int = 25,
44
+ size: Tuple[int, int] = (800, 600),
45
+ ) -> None:
46
+ """Write a rotating animation of ``graph`` to ``filename`` (AVI).
47
+
48
+ ``xrot/yrot/zrot`` orient the neuron before recording; ``xangle`` etc.
49
+ are the total sweep in degrees about each axis during the movie.
50
+ """
51
+ if colormap is None:
52
+ colormap = colors.get_colormap(colors.DEFAULT_COLORMAP)
53
+
54
+ # Centre the morphology at the origin, then apply the initial orientation.
55
+ cx, cy, cz = _bounding_center(graph)
56
+ transform.apply_transform(
57
+ graph,
58
+ transform.rotation_translation_matrix(-cx, -cy, -cz, xrot, yrot, zrot),
59
+ )
60
+
61
+ renderer = vtk_backend.make_renderer(
62
+ graph, colormap=colormap, background=background, lines=lines
63
+ )
64
+ actor = renderer.GetActors().GetLastActor()
65
+ cx, cy, cz = _bounding_center(graph)
66
+ actor.SetOrigin(cx, cy, cz)
67
+
68
+ window = vtk.vtkRenderWindow()
69
+ window.SetSize(*size)
70
+ window.AddRenderer(renderer)
71
+ interactor = vtk.vtkRenderWindowInteractor()
72
+ interactor.SetRenderWindow(window)
73
+ window.Render()
74
+ interactor.Initialize()
75
+
76
+ def _step(angle):
77
+ return (0.0 if angle == 0 else 1.0 / frames_per_degree,
78
+ int(angle * frames_per_degree))
79
+
80
+ dx, xframes = _step(xangle)
81
+ dy, yframes = _step(yangle)
82
+ dz, zframes = _step(zangle)
83
+ frames = max(xframes, yframes, zframes)
84
+
85
+ w2if = vtk.vtkWindowToImageFilter()
86
+ w2if.SetInput(window)
87
+ w2if.ReadFrontBufferOff()
88
+ w2if.Update()
89
+ writer = vtk.vtkAVIWriter()
90
+ writer.SetRate(framerate)
91
+ writer.SetInputConnection(w2if.GetOutputPort())
92
+ writer.SetFileName(filename)
93
+ writer.Start()
94
+ for _ in range(frames):
95
+ actor.RotateX(dx)
96
+ actor.RotateY(dy)
97
+ actor.RotateZ(dz)
98
+ window.Render()
99
+ w2if.Modified() # crucial: force the filter to grab the new frame
100
+ writer.Write()
101
+ writer.End()
@@ -0,0 +1,121 @@
1
+ """Matplotlib rendering backend.
2
+
3
+ Lightweight 2-D projections and a 3-D line rendering of a morphology.
4
+ This backend has no interactive picking of individual compartments but is
5
+ the most portable and works well for figures and headless rendering.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Iterable
11
+
12
+ import networkx as nx
13
+
14
+ from .. import colors
15
+
16
+
17
+ def _resolve_color(color, sid, normalized):
18
+ """Pick a colour for a segment given a fixed colour or a palette."""
19
+ if isinstance(color, dict):
20
+ return colors.color_for_structure(normalized, sid)
21
+ return color
22
+
23
+
24
+ def plot_2d(
25
+ graph: nx.DiGraph,
26
+ ax=None,
27
+ proj: str = "xy",
28
+ color="k",
29
+ alpha: float = 0.8,
30
+ linewidth: float = 1.0,
31
+ ):
32
+ """Plot a 2-D projection of the morphology onto two axes.
33
+
34
+ ``proj`` selects the plane, e.g. ``"xy"``, ``"xz"`` or ``"yz"``.
35
+ ``color`` is either a matplotlib colour or a ``{sid: (r, g, b)}``
36
+ palette (0-255) used to colour each segment by its structure type.
37
+ """
38
+ import matplotlib.pyplot as plt
39
+
40
+ if len(proj) != 2 or any(c not in "xyz" for c in proj):
41
+ raise ValueError(f"proj must be two of x/y/z, got {proj!r}")
42
+
43
+ if ax is None:
44
+ _, ax = plt.subplots()
45
+ ax.set_aspect("equal")
46
+ ax.set_xlabel(f"{proj[0]} (um)")
47
+ ax.set_ylabel(f"{proj[1]} (um)")
48
+
49
+ normalized = colors.normalize(color) if isinstance(color, dict) else {}
50
+ a0, a1 = proj[0], proj[1]
51
+ for n0, n1 in graph.edges():
52
+ p0, p1 = graph.nodes[n0], graph.nodes[n1]
53
+ c = _resolve_color(color, p0["s"], normalized)
54
+ ax.plot(
55
+ [p0[a0], p1[a0]],
56
+ [p0[a1], p1[a1]],
57
+ color=c,
58
+ alpha=alpha,
59
+ linewidth=linewidth,
60
+ )
61
+ return ax
62
+
63
+
64
+ def plot_3d(
65
+ graph: nx.DiGraph,
66
+ ax=None,
67
+ color="k",
68
+ alpha: float = 0.8,
69
+ linewidth: float = 1.0,
70
+ ):
71
+ """Plot the morphology as 3-D lines.
72
+
73
+ ``color`` may be a matplotlib colour or a ``{sid: (r, g, b)}`` palette.
74
+ Returns the 3-D axes so callers can add markers or annotations.
75
+ """
76
+ import matplotlib.pyplot as plt
77
+
78
+ if ax is None:
79
+ fig = plt.figure()
80
+ ax = fig.add_subplot(projection="3d")
81
+ ax.set_xlabel("x (um)")
82
+ ax.set_ylabel("y (um)")
83
+ ax.set_zlabel("z (um)")
84
+
85
+ normalized = colors.normalize(color) if isinstance(color, dict) else {}
86
+ for n0, n1 in graph.edges():
87
+ p0, p1 = graph.nodes[n0], graph.nodes[n1]
88
+ c = _resolve_color(color, p0["s"], normalized)
89
+ ax.plot(
90
+ [p0["x"], p1["x"]],
91
+ [p0["y"], p1["y"]],
92
+ [p0["z"], p1["z"]],
93
+ color=c,
94
+ alpha=alpha,
95
+ linewidth=linewidth,
96
+ )
97
+ return ax
98
+
99
+
100
+ def mark_nodes(
101
+ graph: nx.DiGraph,
102
+ nodes: Iterable[int],
103
+ ax,
104
+ label: bool = True,
105
+ color: str = "r",
106
+ marker: str = "^",
107
+ ):
108
+ """Mark and optionally label a set of nodes on a 3-D axes."""
109
+ for n in nodes:
110
+ attr = graph.nodes[n]
111
+ ax.plot([attr["x"]], [attr["y"]], [attr["z"]], marker=marker, color=color)
112
+ if label:
113
+ ax.text(attr["x"], attr["y"], attr["z"], str(n))
114
+ return ax
115
+
116
+
117
+ def show():
118
+ """Display any open matplotlib figures."""
119
+ import matplotlib.pyplot as plt
120
+
121
+ plt.show()
@@ -0,0 +1,41 @@
1
+ """Vispy rendering backend.
2
+
3
+ A GPU-accelerated interactive 3-D view using tube visuals. Requires the
4
+ ``vispy`` package (``pip install morphoview[vispy]``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import networkx as nx
10
+ import numpy as np
11
+
12
+ try:
13
+ from vispy import scene
14
+ from vispy.scene import visuals
15
+ except ImportError as exc: # pragma: no cover - exercised only without vispy
16
+ raise ImportError(
17
+ "The vispy backend requires the 'vispy' package. "
18
+ "Install it with: pip install morphoview[vispy]"
19
+ ) from exc
20
+
21
+
22
+ def show(graph: nx.DiGraph, color="blue", name: str = "Neuron") -> None:
23
+ """Render a morphology as tubes in an interactive vispy canvas."""
24
+ canvas = scene.SceneCanvas(title=name, keys="interactive", show=True)
25
+ view = canvas.central_widget.add_view()
26
+ view.camera = scene.cameras.TurntableCamera(
27
+ fov=45, azimuth=-45, parent=view.scene
28
+ )
29
+ for n0, n1 in graph.edges():
30
+ a = graph.nodes[n0]
31
+ b = graph.nodes[n1]
32
+ pos0 = np.array((a["x"], a["y"], a["z"]))
33
+ pos1 = np.array((b["x"], b["y"], b["z"]))
34
+ mid = 0.5 * (pos0 + pos1)
35
+ tube = visuals.Tube(
36
+ points=np.vstack((pos0, mid, pos1)),
37
+ radius=max(a["r"], 1e-3),
38
+ color=color,
39
+ )
40
+ view.add(tube)
41
+ canvas.app.run()
@@ -0,0 +1,282 @@
1
+ """VTK rendering backend.
2
+
3
+ Renders a morphology as tapered tubes (using per-point radii) or as
4
+ fixed-width lines, coloured by structure type. Supports labelling nodes,
5
+ an optional scale-bar axis, fullscreen display and saving a PNG snapshot.
6
+
7
+ Requires the ``vtk`` package (``pip install morphoview[vtk]``).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Dict, List, Optional, Sequence, Tuple
13
+
14
+ import networkx as nx
15
+ import numpy as np
16
+
17
+ from .. import colors
18
+
19
+ try:
20
+ import vtk
21
+ from vtk.util import numpy_support as vtknp
22
+ except ImportError as exc: # pragma: no cover - exercised only without vtk
23
+ raise ImportError(
24
+ "The VTK backend requires the 'vtk' package. "
25
+ "Install it with: pip install morphoview[vtk]"
26
+ ) from exc
27
+
28
+
29
+ def graph_to_polydata(
30
+ graph: nx.DiGraph,
31
+ colormap: Optional[Dict[int, colors.RGB]] = None,
32
+ lines: bool = False,
33
+ ) -> "vtk.vtkPolyData":
34
+ """Convert a morphology graph into a ``vtkPolyData``.
35
+
36
+ When ``lines`` is False the point radii are attached so a tube filter
37
+ can taper the rendering; segment colours come from ``colormap`` keyed
38
+ by the parent node's structure id.
39
+ """
40
+ if colormap is None:
41
+ colormap = colors.get_colormap(colors.DEFAULT_COLORMAP)
42
+
43
+ node_map = {n: i for i, n in enumerate(graph.nodes())}
44
+ points = vtk.vtkPoints()
45
+ points.SetNumberOfPoints(graph.number_of_nodes())
46
+ for n, i in node_map.items():
47
+ attr = graph.nodes[n]
48
+ points.SetPoint(i, (attr["x"], attr["y"], attr["z"]))
49
+
50
+ edges = vtk.vtkCellArray()
51
+ color_arr = vtk.vtkUnsignedCharArray()
52
+ color_arr.SetName("Colors")
53
+ color_arr.SetNumberOfComponents(3)
54
+ color_arr.SetNumberOfTuples(graph.number_of_edges())
55
+ for i, (n0, n1) in enumerate(graph.edges()):
56
+ line = vtk.vtkLine()
57
+ line.GetPointIds().SetId(0, node_map[n0])
58
+ line.GetPointIds().SetId(1, node_map[n1])
59
+ edges.InsertNextCell(line)
60
+ color_arr.SetTuple3(
61
+ i, *colors.color_for_structure(colormap, graph.nodes[n0]["s"])
62
+ )
63
+
64
+ polydata = vtk.vtkPolyData()
65
+ polydata.SetPoints(points)
66
+ polydata.SetLines(edges)
67
+ polydata.GetCellData().SetScalars(color_arr)
68
+
69
+ if not lines:
70
+ radii = np.array([graph.nodes[n]["r"] for n in node_map], dtype=float)
71
+ radius_arr = vtknp.numpy_to_vtk(
72
+ radii, deep=True, array_type=vtk.VTK_FLOAT
73
+ )
74
+ radius_arr.SetName("Radius")
75
+ polydata.GetPointData().AddArray(radius_arr)
76
+ polydata.GetPointData().SetActiveScalars("Radius")
77
+ polydata.GetCellData().AddArray(color_arr)
78
+ return polydata
79
+
80
+
81
+ def make_actor(
82
+ graph: nx.DiGraph,
83
+ colormap: Optional[Dict[int, colors.RGB]] = None,
84
+ lines: float = 0.0,
85
+ ) -> "vtk.vtkActor":
86
+ """Build a ``vtkActor`` for a morphology.
87
+
88
+ ``lines`` <= 0 renders tapered tubes; a positive value renders
89
+ fixed-width lines of that width.
90
+ """
91
+ polydata = graph_to_polydata(graph, colormap=colormap, lines=lines > 0)
92
+ mapper = vtk.vtkPolyDataMapper()
93
+ if lines > 0:
94
+ mapper.SetInputData(polydata)
95
+ else:
96
+ tube = vtk.vtkTubeFilter()
97
+ tube.SetNumberOfSides(10)
98
+ tube.SetVaryRadiusToVaryRadiusByAbsoluteScalar()
99
+ tube.SetInputData(polydata)
100
+ mapper.SetInputConnection(tube.GetOutputPort())
101
+ mapper.ScalarVisibilityOn()
102
+ mapper.SetScalarModeToUseCellFieldData()
103
+ mapper.SelectColorArray("Colors")
104
+
105
+ actor = vtk.vtkActor()
106
+ actor.SetMapper(mapper)
107
+ if lines > 0:
108
+ actor.GetProperty().SetLineWidth(lines)
109
+ return actor
110
+
111
+
112
+ def make_label_actor(
113
+ graph: nx.DiGraph,
114
+ label_nodes: Sequence[int],
115
+ labels: Optional[Sequence[str]] = None,
116
+ priorities: Optional[Sequence[int]] = None,
117
+ color: Tuple[float, float, float] = (0, 0, 0),
118
+ ) -> "vtk.vtkActor2D":
119
+ """Build a 2-D label actor placing text at the given nodes."""
120
+ labels = labels or []
121
+ priorities = priorities or []
122
+
123
+ points = vtk.vtkPoints()
124
+ label_str = vtk.vtkStringArray()
125
+ label_str.SetName("Labels")
126
+ for i, n in enumerate(label_nodes):
127
+ attr = graph.nodes[n]
128
+ points.InsertNextPoint((attr["x"], attr["y"], attr["z"]))
129
+ text = str(labels[i]) if i < len(labels) else str(n)
130
+ label_str.InsertNextValue(text)
131
+
132
+ polydata = vtk.vtkPolyData()
133
+ polydata.SetPoints(points)
134
+ polydata.GetPointData().AddArray(label_str)
135
+
136
+ hierarchy = vtk.vtkPointSetToLabelHierarchy()
137
+ hierarchy.SetLabelArrayName(label_str.GetName())
138
+ hierarchy.GetTextProperty().SetColor(*color)
139
+ if priorities and len(priorities) == len(label_nodes):
140
+ parray = vtk.vtkIntArray()
141
+ parray.SetName("Priorities")
142
+ for p in priorities:
143
+ parray.InsertNextValue(p)
144
+ hierarchy.SetPriorityArrayName(parray.GetName())
145
+ hierarchy.SetInputData(polydata)
146
+
147
+ prop = vtk.vtkTextProperty()
148
+ prop.SetColor(*color)
149
+ prop.ItalicOn()
150
+ mapper = vtk.vtkLabelPlacementMapper()
151
+ mapper.SetInputConnection(hierarchy.GetOutputPort())
152
+ mapper.GetRenderStrategy().SetDefaultTextProperty(prop)
153
+
154
+ actor = vtk.vtkActor2D()
155
+ actor.SetMapper(mapper)
156
+ return actor
157
+
158
+
159
+ def _scalebar_axes(renderer, background):
160
+ color = tuple(1.0 - c for c in background)
161
+ axes = vtk.vtkCubeAxesActor2D()
162
+ axes.SetLabelFormat("%3.0f")
163
+ # A fixed 0-200 um reference cube. Show the two endpoint labels so the
164
+ # scale is readable; VTK requires >= 1 label per axis (0 warns).
165
+ axes.SetNumberOfLabels(2)
166
+ axes.SetBounds(0, 200, 0, 200, 0, 200)
167
+ axes.SetXLabel("X")
168
+ axes.SetYLabel("Y")
169
+ axes.SetZLabel("Z")
170
+ axes.SetXOrigin(0)
171
+ axes.SetYOrigin(0)
172
+ axes.SetZOrigin(0)
173
+ tprop = vtk.vtkTextProperty()
174
+ tprop.SetColor(*color)
175
+ axes.SetAxisLabelTextProperty(tprop)
176
+ axes.GetProperty().SetColor(*color)
177
+ axes.SetCamera(renderer.GetActiveCamera())
178
+ return axes
179
+
180
+
181
+ def make_renderer(
182
+ graphs,
183
+ colormap: Optional[Dict[int, colors.RGB]] = None,
184
+ background: Tuple[float, float, float] = (0, 0, 0),
185
+ lines: float = 0.0,
186
+ label_nodes: Optional[Sequence[int]] = None,
187
+ labels: Optional[Sequence[str]] = None,
188
+ axes: bool = False,
189
+ ) -> "vtk.vtkRenderer":
190
+ """Assemble a ``vtkRenderer`` for one or more morphology graphs.
191
+
192
+ ``graphs`` may be a single graph or an iterable of graphs.
193
+ """
194
+ if isinstance(graphs, nx.DiGraph):
195
+ graphs = [graphs]
196
+ if colormap is None:
197
+ colormap = colors.get_colormap(colors.DEFAULT_COLORMAP)
198
+
199
+ renderer = vtk.vtkRenderer()
200
+ renderer.SetBackground(*background)
201
+ for graph in graphs:
202
+ renderer.AddActor(make_actor(graph, colormap=colormap, lines=lines))
203
+ if label_nodes:
204
+ label_color = tuple(1.0 - c for c in background)
205
+ renderer.AddActor2D(
206
+ make_label_actor(
207
+ graph, label_nodes, labels=labels, color=label_color
208
+ )
209
+ )
210
+ if axes:
211
+ renderer.AddActor(_scalebar_axes(renderer, background))
212
+ renderer.ResetCamera()
213
+ return renderer
214
+
215
+
216
+ def show(
217
+ graphs,
218
+ colormap: Optional[Dict[int, colors.RGB]] = None,
219
+ background: Tuple[float, float, float] = (0, 0, 0),
220
+ lines: float = 0.0,
221
+ label_nodes: Optional[Sequence[int]] = None,
222
+ labels: Optional[Sequence[str]] = None,
223
+ axes: bool = False,
224
+ fullscreen: bool = False,
225
+ stereo: bool = False,
226
+ size: Tuple[int, int] = (1024, 768),
227
+ save: Optional[str] = None,
228
+ offscreen: bool = False,
229
+ ) -> None:
230
+ """Render morphologies in an interactive window (or offscreen).
231
+
232
+ When ``save`` is given a PNG snapshot is written. Set ``offscreen``
233
+ True to render without opening a window (useful for headless PNG
234
+ export); in that case the interactor loop is skipped.
235
+ """
236
+ renderer = make_renderer(
237
+ graphs,
238
+ colormap=colormap,
239
+ background=background,
240
+ lines=lines,
241
+ label_nodes=label_nodes,
242
+ labels=labels,
243
+ axes=axes,
244
+ )
245
+ window = vtk.vtkRenderWindow()
246
+ window.AddRenderer(renderer)
247
+ window.SetSize(*size)
248
+ if offscreen:
249
+ window.SetOffScreenRendering(1)
250
+ elif fullscreen:
251
+ window.FullScreenOn()
252
+ if stereo and not offscreen:
253
+ window.GetStereoCapableWindow()
254
+ window.StereoCapableWindowOn()
255
+ window.SetStereoRender(1)
256
+ window.SetStereoTypeToCrystalEyes()
257
+
258
+ interactor = None
259
+ if not offscreen:
260
+ interactor = vtk.vtkRenderWindowInteractor()
261
+ interactor.SetRenderWindow(window)
262
+
263
+ window.Render()
264
+
265
+ if save is not None:
266
+ _save_png(window, save)
267
+
268
+ if interactor is not None:
269
+ interactor.Initialize()
270
+ interactor.Start()
271
+
272
+
273
+ def _save_png(window, path: str) -> None:
274
+ w2if = vtk.vtkWindowToImageFilter()
275
+ w2if.SetInput(window)
276
+ w2if.SetInputBufferTypeToRGB()
277
+ w2if.ReadFrontBufferOff()
278
+ w2if.Update()
279
+ writer = vtk.vtkPNGWriter()
280
+ writer.SetFileName(path)
281
+ writer.SetInputConnection(w2if.GetOutputPort())
282
+ writer.Write()