cloudglancer 0.1.7__tar.gz → 0.1.9__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.
- {cloudglancer-0.1.7/cloudglancer.egg-info → cloudglancer-0.1.9}/PKG-INFO +5 -2
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/README.md +3 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/cloudglancer/animate.py +77 -42
- {cloudglancer-0.1.7 → cloudglancer-0.1.9/cloudglancer.egg-info}/PKG-INFO +5 -2
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/cloudglancer.egg-info/requires.txt +1 -1
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/pyproject.toml +9 -2
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/tests/test_animate.py +13 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/LICENSE +0 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/MANIFEST.in +0 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/cloudglancer/__init__.py +0 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/cloudglancer/scatter.py +0 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/cloudglancer.egg-info/SOURCES.txt +0 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/cloudglancer.egg-info/dependency_links.txt +0 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/cloudglancer.egg-info/top_level.txt +0 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/setup.cfg +0 -0
- {cloudglancer-0.1.7 → cloudglancer-0.1.9}/tests/test_scatter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudglancer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Simple interactive visualization of 3D point clouds
|
|
5
5
|
Author-email: Your Name <your.email@example.com>
|
|
6
6
|
License: MIT
|
|
@@ -24,7 +24,7 @@ Requires-Dist: plotly>=5.0.0
|
|
|
24
24
|
Requires-Dist: pandas>=2.0.0
|
|
25
25
|
Requires-Dist: numpy>=1.24.0
|
|
26
26
|
Requires-Dist: nbformat>=4.2.0
|
|
27
|
-
Requires-Dist: kaleido
|
|
27
|
+
Requires-Dist: kaleido>=0.2.1
|
|
28
28
|
Requires-Dist: Pillow>=9.0.0
|
|
29
29
|
Requires-Dist: tqdm>=4.0.0
|
|
30
30
|
Provides-Extra: dev
|
|
@@ -75,6 +75,9 @@ fig = cg.plot(points, size=2.0)
|
|
|
75
75
|
cg.animate(fig, "rotation.gif", axis="z", n_frames=60)
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
Frames are rendered in parallel; pass `n_workers` to control how many render
|
|
79
|
+
processes are used (defaults to `min(16, cpu_count)`).
|
|
80
|
+
|
|
78
81
|
Render a batch of point clouds as a grid of subplots (one cloud per cell):
|
|
79
82
|
|
|
80
83
|
```python
|
|
@@ -40,6 +40,9 @@ fig = cg.plot(points, size=2.0)
|
|
|
40
40
|
cg.animate(fig, "rotation.gif", axis="z", n_frames=60)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Frames are rendered in parallel; pass `n_workers` to control how many render
|
|
44
|
+
processes are used (defaults to `min(16, cpu_count)`).
|
|
45
|
+
|
|
43
46
|
Render a batch of point clouds as a grid of subplots (one cloud per cell):
|
|
44
47
|
|
|
45
48
|
```python
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import multiprocessing
|
|
4
4
|
import os
|
|
5
|
+
import sys
|
|
5
6
|
from concurrent.futures import ProcessPoolExecutor
|
|
6
7
|
from io import BytesIO
|
|
7
|
-
from typing import Tuple
|
|
8
|
+
from typing import Optional, Tuple
|
|
8
9
|
|
|
9
10
|
import numpy as np
|
|
10
11
|
from plotly.graph_objects import Figure
|
|
@@ -40,25 +41,36 @@ def _scene_keys(fig: Figure) -> list:
|
|
|
40
41
|
return keys
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def _eye_to_vec(eye) -> np.ndarray:
|
|
44
|
-
if eye
|
|
44
|
+
def _eye_to_vec(eye: dict) -> np.ndarray:
|
|
45
|
+
if not eye or eye.get("x") is None:
|
|
45
46
|
return np.array([1.25, 1.25, 1.25])
|
|
46
|
-
return np.array([eye
|
|
47
|
+
return np.array([eye["x"], eye["y"], eye["z"]], dtype=float)
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
_WORKER_FIG_DICT = None
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
def _init_worker(fig_dict: dict) -> None:
|
|
53
|
-
global
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
global _WORKER_FIG_DICT
|
|
55
|
+
import warnings
|
|
56
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning,
|
|
57
|
+
message=r".*[Kk]aleido.*")
|
|
58
|
+
_WORKER_FIG_DICT = fig_dict
|
|
59
|
+
import kaleido
|
|
60
|
+
if hasattr(kaleido, "start_sync_server"):
|
|
61
|
+
# kaleido >= 1 launches a new Chrome per to_image() call unless a
|
|
62
|
+
# server is running; keep one Chrome per worker for all its frames.
|
|
63
|
+
kaleido.start_sync_server(silence_warnings=True)
|
|
56
64
|
|
|
57
65
|
|
|
58
66
|
def _render_frame(args: Tuple[dict, int, int]) -> bytes:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
cameras, width, height = args
|
|
68
|
+
import plotly.io as pio
|
|
69
|
+
layout = _WORKER_FIG_DICT.setdefault("layout", {})
|
|
70
|
+
for key, cam in cameras.items():
|
|
71
|
+
layout.setdefault(key, {})["camera"] = cam
|
|
72
|
+
return pio.to_image(_WORKER_FIG_DICT, format="png",
|
|
73
|
+
width=width, height=height, validate=False)
|
|
62
74
|
|
|
63
75
|
|
|
64
76
|
def animate(
|
|
@@ -71,6 +83,7 @@ def animate(
|
|
|
71
83
|
height: int = 600,
|
|
72
84
|
loop: int = 0,
|
|
73
85
|
progress: bool = True,
|
|
86
|
+
n_workers: Optional[int] = None,
|
|
74
87
|
) -> str:
|
|
75
88
|
"""
|
|
76
89
|
Render a rotating GIF of a 3D Plotly figure.
|
|
@@ -90,12 +103,18 @@ def animate(
|
|
|
90
103
|
height: Frame height in pixels. Defaults to 600.
|
|
91
104
|
loop: Number of times the GIF should loop (0 = infinite). Defaults to 0.
|
|
92
105
|
progress: Show a tqdm progress bar while rendering frames. Defaults to True.
|
|
106
|
+
n_workers: Number of parallel render processes. Defaults to
|
|
107
|
+
``min(16, cpu_count, n_frames)``. Raise it on machines with many
|
|
108
|
+
cores for large frame counts. On non-Linux platforms worker
|
|
109
|
+
processes are spawned, so scripts calling ``animate()`` there must
|
|
110
|
+
be guarded by ``if __name__ == "__main__":``.
|
|
93
111
|
|
|
94
112
|
Returns:
|
|
95
113
|
The ``output_path`` it wrote to.
|
|
96
114
|
|
|
97
115
|
Raises:
|
|
98
|
-
ValueError: If `axis` is not one of ``'x'``, ``'y'``, ``'z'
|
|
116
|
+
ValueError: If `axis` is not one of ``'x'``, ``'y'``, ``'z'``, or
|
|
117
|
+
`n_workers` is less than 1.
|
|
99
118
|
ImportError: If ``kaleido`` or ``Pillow`` is not installed.
|
|
100
119
|
|
|
101
120
|
Examples:
|
|
@@ -116,6 +135,14 @@ def animate(
|
|
|
116
135
|
"animate() requires Pillow. Install it with `pip install Pillow`."
|
|
117
136
|
) from e
|
|
118
137
|
|
|
138
|
+
try:
|
|
139
|
+
import kaleido # noqa: F401
|
|
140
|
+
except ImportError as e:
|
|
141
|
+
raise ImportError(
|
|
142
|
+
"animate() requires kaleido for PNG export. "
|
|
143
|
+
"Install it with `pip install kaleido`."
|
|
144
|
+
) from e
|
|
145
|
+
|
|
119
146
|
axis_vec = _AXIS_VECTORS[axis]
|
|
120
147
|
up_vec = {"x": dict(x=1, y=0, z=0),
|
|
121
148
|
"y": dict(x=0, y=1, z=0),
|
|
@@ -125,45 +152,53 @@ def animate(
|
|
|
125
152
|
if not scene_keys:
|
|
126
153
|
raise ValueError("figure has no 3D scene to animate")
|
|
127
154
|
|
|
128
|
-
|
|
155
|
+
fig_dict = fig.to_dict()
|
|
156
|
+
layout_dict = fig_dict.setdefault("layout", {})
|
|
157
|
+
|
|
158
|
+
orig_cams = {}
|
|
159
|
+
initial_eyes = {}
|
|
160
|
+
for k in scene_keys:
|
|
161
|
+
cam = dict((layout_dict.get(k) or {}).get("camera") or {})
|
|
162
|
+
orig_cams[k] = cam
|
|
163
|
+
initial_eyes[k] = _eye_to_vec(cam.get("eye"))
|
|
129
164
|
|
|
130
165
|
tasks = []
|
|
131
166
|
for i in range(n_frames):
|
|
132
167
|
theta = 2.0 * np.pi * i / n_frames
|
|
133
168
|
R = _rotation_matrix(axis_vec, theta)
|
|
134
|
-
|
|
169
|
+
cameras = {}
|
|
135
170
|
for k in scene_keys:
|
|
136
171
|
eye = R @ initial_eyes[k]
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
n_workers
|
|
172
|
+
cameras[k] = {
|
|
173
|
+
**orig_cams[k],
|
|
174
|
+
"eye": dict(x=float(eye[0]), y=float(eye[1]), z=float(eye[2])),
|
|
175
|
+
"up": up_vec,
|
|
176
|
+
}
|
|
177
|
+
tasks.append((cameras, width, height))
|
|
178
|
+
|
|
179
|
+
if n_workers is None:
|
|
180
|
+
n_workers = min(16, os.cpu_count() or 2)
|
|
181
|
+
elif n_workers < 1:
|
|
182
|
+
raise ValueError(f"n_workers must be >= 1 (got {n_workers})")
|
|
183
|
+
n_workers = min(n_workers, n_frames)
|
|
184
|
+
|
|
185
|
+
# fork keeps unguarded user scripts working and skips per-worker re-imports;
|
|
186
|
+
# spawn elsewhere (fork is unsafe on macOS, unavailable on Windows).
|
|
187
|
+
start_method = "fork" if sys.platform.startswith("linux") else "spawn"
|
|
147
188
|
|
|
148
189
|
pngs: list = [None] * n_frames
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
pngs[i] = png
|
|
162
|
-
except ValueError as e:
|
|
163
|
-
raise ImportError(
|
|
164
|
-
"animate() requires kaleido for PNG export. "
|
|
165
|
-
"Install it with `pip install 'kaleido<1.0'`."
|
|
166
|
-
) from e
|
|
190
|
+
with ProcessPoolExecutor(
|
|
191
|
+
max_workers=n_workers,
|
|
192
|
+
mp_context=multiprocessing.get_context(start_method),
|
|
193
|
+
initializer=_init_worker,
|
|
194
|
+
initargs=(fig_dict,),
|
|
195
|
+
) as executor:
|
|
196
|
+
iterator = executor.map(_render_frame, tasks, chunksize=1)
|
|
197
|
+
for i, png in enumerate(
|
|
198
|
+
tqdm(iterator, total=n_frames, desc="Rendering frames",
|
|
199
|
+
unit="frame", disable=not progress)
|
|
200
|
+
):
|
|
201
|
+
pngs[i] = png
|
|
167
202
|
|
|
168
203
|
ref_palette = Image.open(BytesIO(pngs[0])).convert("P", palette=Image.ADAPTIVE)
|
|
169
204
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudglancer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Simple interactive visualization of 3D point clouds
|
|
5
5
|
Author-email: Your Name <your.email@example.com>
|
|
6
6
|
License: MIT
|
|
@@ -24,7 +24,7 @@ Requires-Dist: plotly>=5.0.0
|
|
|
24
24
|
Requires-Dist: pandas>=2.0.0
|
|
25
25
|
Requires-Dist: numpy>=1.24.0
|
|
26
26
|
Requires-Dist: nbformat>=4.2.0
|
|
27
|
-
Requires-Dist: kaleido
|
|
27
|
+
Requires-Dist: kaleido>=0.2.1
|
|
28
28
|
Requires-Dist: Pillow>=9.0.0
|
|
29
29
|
Requires-Dist: tqdm>=4.0.0
|
|
30
30
|
Provides-Extra: dev
|
|
@@ -75,6 +75,9 @@ fig = cg.plot(points, size=2.0)
|
|
|
75
75
|
cg.animate(fig, "rotation.gif", axis="z", n_frames=60)
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
Frames are rendered in parallel; pass `n_workers` to control how many render
|
|
79
|
+
processes are used (defaults to `min(16, cpu_count)`).
|
|
80
|
+
|
|
78
81
|
Render a batch of point clouds as a grid of subplots (one cloud per cell):
|
|
79
82
|
|
|
80
83
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cloudglancer"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "Simple interactive visualization of 3D point clouds"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -29,7 +29,7 @@ dependencies = [
|
|
|
29
29
|
"pandas>=2.0.0",
|
|
30
30
|
"numpy>=1.24.0",
|
|
31
31
|
"nbformat>=4.2.0",
|
|
32
|
-
"kaleido>=0.2.1
|
|
32
|
+
"kaleido>=0.2.1",
|
|
33
33
|
"Pillow>=9.0.0",
|
|
34
34
|
"tqdm>=4.0.0",
|
|
35
35
|
]
|
|
@@ -52,6 +52,13 @@ include = ["cloudglancer*"]
|
|
|
52
52
|
[tool.setuptools.package-data]
|
|
53
53
|
cloudglancer = ["py.typed"]
|
|
54
54
|
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
filterwarnings = [
|
|
57
|
+
# animate() forks render workers after the pool's manager thread starts; the
|
|
58
|
+
# workers never touch the inherited locks, so this CPython warning is benign here.
|
|
59
|
+
"ignore:This process .* is multi-threaded, use of fork:DeprecationWarning",
|
|
60
|
+
]
|
|
61
|
+
|
|
55
62
|
[tool.black]
|
|
56
63
|
line-length = 100
|
|
57
64
|
target-version = ["py39"]
|
|
@@ -54,6 +54,19 @@ def test_animate_combined_figure(tmp_path):
|
|
|
54
54
|
assert out.exists() and _is_gif(out)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def test_animate_n_workers(tmp_path):
|
|
58
|
+
fig = plot(np.random.randn(20, 3))
|
|
59
|
+
out = tmp_path / "w.gif"
|
|
60
|
+
animate(fig, str(out), n_frames=3, width=120, height=120, n_workers=2, progress=False)
|
|
61
|
+
assert _is_gif(out)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_animate_invalid_n_workers(tmp_path):
|
|
65
|
+
fig = plot(np.random.randn(10, 3))
|
|
66
|
+
with pytest.raises(ValueError, match="n_workers must be >= 1"):
|
|
67
|
+
animate(fig, str(tmp_path / "x.gif"), n_frames=2, n_workers=0)
|
|
68
|
+
|
|
69
|
+
|
|
57
70
|
@pytest.mark.parametrize("axis", ["x", "y", "z"])
|
|
58
71
|
def test_animate_each_axis(tmp_path, axis):
|
|
59
72
|
fig = plot(np.random.randn(20, 3))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|