brainrender 2.1.20__tar.gz → 2.2.1__tar.gz
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.
- {brainrender-2.1.20 → brainrender-2.2.1}/PKG-INFO +16 -10
- {brainrender-2.1.20 → brainrender-2.2.1}/README.md +13 -9
- brainrender-2.2.1/brainrender/_colors.py +131 -0
- brainrender-2.2.1/brainrender/_io.py +185 -0
- brainrender-2.2.1/brainrender/_utils.py +82 -0
- brainrender-2.2.1/brainrender/_video.py +60 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/actor.py +154 -79
- brainrender-2.2.1/brainrender/actors/cylinder.py +50 -0
- brainrender-2.2.1/brainrender/actors/line.py +35 -0
- brainrender-2.2.1/brainrender/actors/neurons.py +181 -0
- brainrender-2.2.1/brainrender/actors/points.py +230 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/actors/ruler.py +54 -15
- brainrender-2.2.1/brainrender/actors/streamlines.py +163 -0
- brainrender-2.2.1/brainrender/actors/volume.py +173 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/atlas.py +95 -37
- brainrender-2.2.1/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +232 -0
- brainrender-2.2.1/brainrender/camera.py +169 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/cameras.py +6 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/render.py +120 -55
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/scene.py +180 -100
- brainrender-2.2.1/brainrender/video.py +628 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender.egg-info/PKG-INFO +16 -10
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender.egg-info/SOURCES.txt +2 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender.egg-info/requires.txt +2 -0
- brainrender-2.2.1/brainrender.egg-info/scm_file_list.json +112 -0
- brainrender-2.2.1/brainrender.egg-info/scm_version.json +8 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/pyproject.toml +2 -0
- brainrender-2.1.20/brainrender/_colors.py +0 -83
- brainrender-2.1.20/brainrender/_io.py +0 -87
- brainrender-2.1.20/brainrender/_utils.py +0 -44
- brainrender-2.1.20/brainrender/_video.py +0 -36
- brainrender-2.1.20/brainrender/actors/cylinder.py +0 -34
- brainrender-2.1.20/brainrender/actors/line.py +0 -22
- brainrender-2.1.20/brainrender/actors/neurons.py +0 -106
- brainrender-2.1.20/brainrender/actors/points.py +0 -155
- brainrender-2.1.20/brainrender/actors/streamlines.py +0 -111
- brainrender-2.1.20/brainrender/actors/volume.py +0 -111
- brainrender-2.1.20/brainrender/atlas_specific/allen_brain_atlas/streamlines.py +0 -111
- brainrender-2.1.20/brainrender/camera.py +0 -110
- brainrender-2.1.20/brainrender/video.py +0 -437
- {brainrender-2.1.20 → brainrender-2.2.1}/.github/workflows/test_and_deploy.yml +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/.gitignore +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/CITATION.cff +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/LICENSE +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/MANIFEST.in +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/__init__.py +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/_jupyter.py +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/actors/__init__.py +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/atlas_specific/__init__.py +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/atlas_specific/allen_brain_atlas/gene_expression/__init__.py +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/atlas_specific/allen_brain_atlas/gene_expression/api.py +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/atlas_specific/allen_brain_atlas/gene_expression/ge_utils.py +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender/settings.py +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender.egg-info/dependency_links.txt +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/brainrender.egg-info/top_level.txt +0 -0
- {brainrender-2.1.20 → brainrender-2.2.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brainrender
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Visualisation and exploration of brain atlases and other anatomical data
|
|
5
5
|
Author-email: "Federico Claudi, Adam Tyson, Luigi Petrucco" <hello@brainglobe.info>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -23,11 +23,13 @@ Requires-Python: >=3.11
|
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Requires-Dist: brainglobe-atlasapi>=2.0.1
|
|
26
|
+
Requires-Dist: cloud-volume>=12
|
|
26
27
|
Requires-Dist: brainglobe-space>=1.0.0
|
|
27
28
|
Requires-Dist: brainglobe-utils>=0.5.0
|
|
28
29
|
Requires-Dist: h5py
|
|
29
30
|
Requires-Dist: k3d
|
|
30
31
|
Requires-Dist: loguru
|
|
32
|
+
Requires-Dist: meshio
|
|
31
33
|
Requires-Dist: morphapi>=0.3.3
|
|
32
34
|
Requires-Dist: msgpack
|
|
33
35
|
Requires-Dist: myterial
|
|
@@ -55,18 +57,22 @@ Requires-Dist: jupyter; extra == "nb"
|
|
|
55
57
|
Requires-Dist: k3d; extra == "nb"
|
|
56
58
|
Dynamic: license-file
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
[](https://pypi.org/project/brainrender)
|
|
63
|
-
[](https://pypi.org/project/brainrender)
|
|
60
|
+
[](https://brainglobe.info/documentation/brainrender/index.html)
|
|
61
|
+
[](https://brainglobe.info/contact.html)
|
|
62
|
+
[](https://doi.org/10.7554/eLife.65751)
|
|
63
|
+
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
64
64
|
[](https://github.com/brainglobe/brainrender/actions)
|
|
65
65
|
[](https://codecov.io/gh/brainglobe/brainrender)
|
|
66
|
+
[](https://pypi.org/project/brainrender)
|
|
67
|
+
[](https://pypi.org/project/brainrender)
|
|
66
68
|
[](https://pepy.tech/project/brainrender)
|
|
67
|
-
[](https://github.com/astral-sh/ruff)
|
|
70
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
71
|
+
[](https://brainglobe.info/community/developers/index.html)
|
|
72
|
+
|
|
73
|
+
# brainrender
|
|
74
|
+
|
|
75
|
+
*A user-friendly python library to create high-quality, 3D neuro-anatomical renderings combining data from publicly available brain atlases with user-generated experimental data.*
|
|
70
76
|
|
|
71
77
|
|
|
72
78
|
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://pypi.org/project/brainrender)
|
|
6
|
-
[](https://pypi.org/project/brainrender)
|
|
1
|
+
[](https://brainglobe.info/documentation/brainrender/index.html)
|
|
2
|
+
[](https://brainglobe.info/contact.html)
|
|
3
|
+
[](https://doi.org/10.7554/eLife.65751)
|
|
4
|
+
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
7
5
|
[](https://github.com/brainglobe/brainrender/actions)
|
|
8
6
|
[](https://codecov.io/gh/brainglobe/brainrender)
|
|
7
|
+
[](https://pypi.org/project/brainrender)
|
|
8
|
+
[](https://pypi.org/project/brainrender)
|
|
9
9
|
[](https://pepy.tech/project/brainrender)
|
|
10
|
-
[](https://github.com/astral-sh/ruff)
|
|
11
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
12
|
+
[](https://brainglobe.info/community/developers/index.html)
|
|
13
|
+
|
|
14
|
+
# brainrender
|
|
15
|
+
|
|
16
|
+
*A user-friendly python library to create high-quality, 3D neuro-anatomical renderings combining data from publicly available brain atlases with user-generated experimental data.*
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Color mapping and palette utilities for brainrender."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
import matplotlib as mpl
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
8
|
+
from vedo.colors import colors as vcolors
|
|
9
|
+
from vedo.colors import get_color as getColor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def map_color(
|
|
13
|
+
value: float,
|
|
14
|
+
name: str = "jet",
|
|
15
|
+
vmin: float | None = None,
|
|
16
|
+
vmax: float | None = None,
|
|
17
|
+
) -> tuple[float, float, float]:
|
|
18
|
+
"""
|
|
19
|
+
Map a scalar value in ``[vmin, vmax]`` to an RGB colour.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
value
|
|
24
|
+
Scalar value to transform into a colour.
|
|
25
|
+
name
|
|
26
|
+
Colormap name or matplotlib colormap. Default ``"jet"``.
|
|
27
|
+
vmin
|
|
28
|
+
Lower bound of the value range.
|
|
29
|
+
vmax
|
|
30
|
+
Upper bound of the value range.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
tuple of float
|
|
35
|
+
``(r, g, b)`` colour.
|
|
36
|
+
|
|
37
|
+
Raises
|
|
38
|
+
------
|
|
39
|
+
ValueError
|
|
40
|
+
If ``vmax`` is smaller than ``vmin``.
|
|
41
|
+
"""
|
|
42
|
+
if vmax < vmin:
|
|
43
|
+
raise ValueError("vmax should be larger than vmin")
|
|
44
|
+
|
|
45
|
+
mp = mpl.colormaps.get_cmap(name)
|
|
46
|
+
|
|
47
|
+
value -= vmin
|
|
48
|
+
value /= vmax - vmin
|
|
49
|
+
if value > 0.999:
|
|
50
|
+
value = 0.999
|
|
51
|
+
elif value < 0:
|
|
52
|
+
value = 0
|
|
53
|
+
return mp(value)[0:3]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def make_palette(N: int, *colors: str) -> list[npt.NDArray]:
|
|
57
|
+
"""
|
|
58
|
+
Generate N colours interpolated across the given input colours.
|
|
59
|
+
|
|
60
|
+
Adapted from vedo's ``make_palette`` function. Interpolation is
|
|
61
|
+
performed in RGB space.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
N
|
|
66
|
+
Number of output colours.
|
|
67
|
+
*colors
|
|
68
|
+
Input colours. Any number between 1 and N is accepted.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
list of numpy.ndarray
|
|
73
|
+
List of ``(r, g, b)`` colour arrays.
|
|
74
|
+
|
|
75
|
+
Raises
|
|
76
|
+
------
|
|
77
|
+
ValueError
|
|
78
|
+
If no colours are passed or more colours than N are passed.
|
|
79
|
+
"""
|
|
80
|
+
N = int(N)
|
|
81
|
+
|
|
82
|
+
N_input_colors = len(colors)
|
|
83
|
+
if not N_input_colors:
|
|
84
|
+
raise ValueError("No colors where passed to make_palette")
|
|
85
|
+
if N_input_colors > N:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"More input colors than out colors (N) where passed to make_palette"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if N_input_colors == N:
|
|
91
|
+
return colors
|
|
92
|
+
else:
|
|
93
|
+
# Get how many colors for each pair of colors we are interpolating over
|
|
94
|
+
fractions = [
|
|
95
|
+
N // N_input_colors + (1 if x < N % N_input_colors else 0)
|
|
96
|
+
for x in range(N_input_colors)
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
# Get pairs of colors
|
|
100
|
+
cs = [np.array(getColor(col)) for col in colors]
|
|
101
|
+
cs += [cs[-1]]
|
|
102
|
+
|
|
103
|
+
output = []
|
|
104
|
+
for n, (c1, c2) in enumerate(zip(cs, cs[1:])):
|
|
105
|
+
cols = []
|
|
106
|
+
for f in np.linspace(0, 1, fractions[n], endpoint=True):
|
|
107
|
+
c = c1 * (1 - f) + c2 * f
|
|
108
|
+
cols.append(c)
|
|
109
|
+
output.extend(cols)
|
|
110
|
+
return output
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_random_colors(n_colors: int = 1) -> str | list[str]:
|
|
114
|
+
"""
|
|
115
|
+
Return one or more random colour names from vedo's colour palette.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
n_colors
|
|
120
|
+
Number of colours to return. Default 1.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
str or list of str
|
|
125
|
+
A single colour name if ``n_colors == 1``, otherwise a list.
|
|
126
|
+
"""
|
|
127
|
+
col_names = list(vcolors.keys())
|
|
128
|
+
if n_colors == 1:
|
|
129
|
+
return random.choice(col_names)
|
|
130
|
+
else:
|
|
131
|
+
return list(random.choices(col_names, k=n_colors))
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""File I/O and network utilities for brainrender."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, ParamSpec, TypeVar
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from vedo import Mesh, Volume, load
|
|
9
|
+
|
|
10
|
+
P = ParamSpec("P")
|
|
11
|
+
R = TypeVar("R")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def connected_to_internet(
|
|
15
|
+
url: str = "http://www.google.com/",
|
|
16
|
+
timeout: int = 5,
|
|
17
|
+
) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Check that there is an internet connection.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
url
|
|
24
|
+
URL to use for testing. Default ``"http://www.google.com/"``.
|
|
25
|
+
timeout
|
|
26
|
+
Timeout in seconds. Default 5.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
bool
|
|
31
|
+
``True`` if an internet connection is available, otherwise ``False``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
_ = requests.get(url, timeout=timeout)
|
|
36
|
+
return True
|
|
37
|
+
except requests.ConnectionError: # pragma: no cover
|
|
38
|
+
print("No internet connection available.") # pragma: no cover
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def fail_on_no_connection(func: Callable[P, R]) -> Callable[P, R]:
|
|
43
|
+
"""
|
|
44
|
+
Decorator that raises an error if no internet connection is available.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
func
|
|
49
|
+
Function to wrap.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
collections.abc.Callable
|
|
54
|
+
|
|
55
|
+
Raises
|
|
56
|
+
------
|
|
57
|
+
ConnectionError
|
|
58
|
+
If no internet connection is found.
|
|
59
|
+
"""
|
|
60
|
+
if not connected_to_internet(): # pragma: no cover
|
|
61
|
+
raise ConnectionError(
|
|
62
|
+
"No internet connection found."
|
|
63
|
+
) # pragma: no cover
|
|
64
|
+
|
|
65
|
+
def inner(*args: Any, **kwargs: Any) -> Any:
|
|
66
|
+
return func(*args, **kwargs)
|
|
67
|
+
|
|
68
|
+
return inner
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def request(url: str) -> requests.Response:
|
|
72
|
+
"""
|
|
73
|
+
Send a GET request to a URL.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
url
|
|
78
|
+
URL to request.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
requests.Response
|
|
83
|
+
|
|
84
|
+
Raises
|
|
85
|
+
------
|
|
86
|
+
ConnectionError
|
|
87
|
+
If no internet connection is found.
|
|
88
|
+
ValueError
|
|
89
|
+
If the request fails.
|
|
90
|
+
"""
|
|
91
|
+
if not connected_to_internet(): # pragma: no cover
|
|
92
|
+
raise ConnectionError(
|
|
93
|
+
"No internet connection found."
|
|
94
|
+
) # pragma: no cover
|
|
95
|
+
|
|
96
|
+
response = requests.get(url)
|
|
97
|
+
if response.ok:
|
|
98
|
+
return response
|
|
99
|
+
else: # pragma: no cover
|
|
100
|
+
exception_string = "URL request failed: {}".format(
|
|
101
|
+
response.reason
|
|
102
|
+
) # pragma: no cover
|
|
103
|
+
raise ValueError(exception_string)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def check_file_exists(
|
|
107
|
+
func: Callable[P, R],
|
|
108
|
+
) -> Callable[P, R]: # pragma: no cover
|
|
109
|
+
"""
|
|
110
|
+
Decorator that raises an error if a function's first argument
|
|
111
|
+
is not a path to an existing file.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
func
|
|
116
|
+
Function to wrap.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
collections.abc.Callable
|
|
121
|
+
|
|
122
|
+
Raises
|
|
123
|
+
------
|
|
124
|
+
FileNotFoundError
|
|
125
|
+
If the file does not exist.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def inner(*args: Any, **kwargs: Any) -> Any:
|
|
129
|
+
if not Path(args[0]).exists():
|
|
130
|
+
raise FileNotFoundError(
|
|
131
|
+
f"File {args[0]} not found"
|
|
132
|
+
) # pragma: no cover
|
|
133
|
+
return func(*args, **kwargs)
|
|
134
|
+
|
|
135
|
+
return inner
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@check_file_exists
|
|
139
|
+
def load_mesh_from_file(
|
|
140
|
+
filepath: str | Path,
|
|
141
|
+
color: str | None = None,
|
|
142
|
+
alpha: float | None = None,
|
|
143
|
+
) -> Mesh | Volume:
|
|
144
|
+
"""
|
|
145
|
+
Load a mesh or volume from a file (e.g. .obj, .stl).
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
filepath
|
|
150
|
+
Path to the mesh file.
|
|
151
|
+
color
|
|
152
|
+
Colour to apply to the mesh.
|
|
153
|
+
alpha
|
|
154
|
+
Transparency to apply to the mesh.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
vedo.Mesh or vedo.Volume
|
|
159
|
+
"""
|
|
160
|
+
actor = load(str(filepath))
|
|
161
|
+
actor.c(color).alpha(alpha)
|
|
162
|
+
return actor
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def convert_meshio_to_vedo(meshio_mesh, color=None, alpha=None):
|
|
166
|
+
"""
|
|
167
|
+
Convert a meshio mesh to a vedo mesh.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
meshio_mesh : meshio.Mesh
|
|
172
|
+
The meshio mesh to convert.
|
|
173
|
+
color : str, optional
|
|
174
|
+
The color to apply to the mesh. Default is None.
|
|
175
|
+
alpha : float, optional
|
|
176
|
+
The transparency to apply to the mesh. Default is None.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
vedo.Mesh
|
|
181
|
+
The converted vedo mesh.
|
|
182
|
+
"""
|
|
183
|
+
actor = Mesh([meshio_mesh.points, meshio_mesh.cells_dict["triangle"]])
|
|
184
|
+
actor.c(color).alpha(alpha)
|
|
185
|
+
return actor
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""General utility helpers for file system traversal and list manipulation."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def listdir(fld: str | Path) -> list[str]:
|
|
10
|
+
"""
|
|
11
|
+
List the files into a folder with the complete file path instead of the relative file path like os.listdir.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
fld
|
|
16
|
+
Path to the folder.
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
list of str
|
|
21
|
+
"""
|
|
22
|
+
return [str(f) for f in Path(fld).glob("**/*") if f.is_file()]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_subdirs(folderpath: str | Path) -> list[str]:
|
|
26
|
+
"""
|
|
27
|
+
Return all subdirectories in a given folder.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
folderpath
|
|
32
|
+
Path to the folder.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
list of str
|
|
37
|
+
"""
|
|
38
|
+
return [str(f) for f in Path(folderpath).glob("**/*") if f.is_dir()]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def listify(obj: T | list[T] | tuple[T, ...]) -> list[T]:
|
|
42
|
+
"""
|
|
43
|
+
Ensure the object is a list.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
obj
|
|
48
|
+
Object to listify.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
list
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(obj, list):
|
|
55
|
+
return obj
|
|
56
|
+
elif isinstance(obj, tuple):
|
|
57
|
+
return list(obj)
|
|
58
|
+
else:
|
|
59
|
+
return [obj]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def return_list_smart(lst: list[T]) -> list[T] | T | None:
|
|
63
|
+
"""
|
|
64
|
+
Return a list, single element, or None depending on list length.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
lst
|
|
69
|
+
Input list.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
list, Any, or None
|
|
74
|
+
The list if length > 1, the single item if length == 1,
|
|
75
|
+
or None if empty.
|
|
76
|
+
"""
|
|
77
|
+
if len(lst) > 1:
|
|
78
|
+
return lst
|
|
79
|
+
elif len(lst) == 1:
|
|
80
|
+
return lst[0]
|
|
81
|
+
else:
|
|
82
|
+
return None
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Thin wrapper around vedo.Video for rendering brainrender scenes to video."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from myterial import amber_light
|
|
8
|
+
from rich import print
|
|
9
|
+
from vedo import Video as VtkVideo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Video(VtkVideo):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
*args: Any,
|
|
16
|
+
fmt: str = "mp4",
|
|
17
|
+
size: str = "1620x1050",
|
|
18
|
+
**kwargs: Any,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Store screenshots as frames and merge them into a video on close.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
*args
|
|
26
|
+
Positional arguments forwarded to ``vedo.Video``.
|
|
27
|
+
fmt
|
|
28
|
+
Video format. Default ``"mp4"``.
|
|
29
|
+
size
|
|
30
|
+
Frame size in pixels. Default ``"1620x1050"``.
|
|
31
|
+
**kwargs
|
|
32
|
+
Keyword arguments forwarded to ``vedo.Video``.
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(*args, **kwargs)
|
|
35
|
+
self.format = fmt
|
|
36
|
+
self.size = size
|
|
37
|
+
|
|
38
|
+
def close(self) -> tuple[int, str]:
|
|
39
|
+
"""
|
|
40
|
+
Render the video and write to file.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
tuple
|
|
45
|
+
The FFmpeg exit code and the executed command string.
|
|
46
|
+
"""
|
|
47
|
+
print(f"[{amber_light}]Saving video")
|
|
48
|
+
logger.debug(f"[{amber_light}]Saving video")
|
|
49
|
+
|
|
50
|
+
fld = os.path.join(self.tmp_dir.name, "%d.png")
|
|
51
|
+
fps = int(self.fps)
|
|
52
|
+
name = f"{self.name}.{self.format}"
|
|
53
|
+
fmt = "-vcodec libx264 -crf 28 -pix_fmt yuv420p"
|
|
54
|
+
if self.size:
|
|
55
|
+
fmt += f" -s {self.size}"
|
|
56
|
+
|
|
57
|
+
command = f'ffmpeg -hide_banner -loglevel panic -y -r {fps} -start_number 0 -i "{fld}" {fmt} "{name}"'
|
|
58
|
+
out = os.system(command)
|
|
59
|
+
self.tmp_dir.cleanup()
|
|
60
|
+
return out, command
|