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 +93 -0
- morphoview/backends/__init__.py +9 -0
- morphoview/backends/movie.py +101 -0
- morphoview/backends/mpl.py +121 -0
- morphoview/backends/vispy.py +41 -0
- morphoview/backends/vtk.py +282 -0
- morphoview/cli.py +289 -0
- morphoview/colors.py +249 -0
- morphoview/graph.py +187 -0
- morphoview/py.typed +0 -0
- morphoview/swc.py +56 -0
- morphoview/transform.py +93 -0
- morphoview-0.1.0.dist-info/METADATA +209 -0
- morphoview-0.1.0.dist-info/RECORD +18 -0
- morphoview-0.1.0.dist-info/WHEEL +5 -0
- morphoview-0.1.0.dist-info/entry_points.txt +2 -0
- morphoview-0.1.0.dist-info/licenses/LICENSE +21 -0
- morphoview-0.1.0.dist-info/top_level.txt +1 -0
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()
|