stereo-charuco-pipeline 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.
- recorder/__init__.py +90 -0
- recorder/auto_calibrate.py +493 -0
- recorder/calibration_ui.py +1106 -0
- recorder/calibration_ui_advanced.py +1013 -0
- recorder/camera.py +51 -0
- recorder/cli.py +122 -0
- recorder/config.py +75 -0
- recorder/configs/default.yaml +38 -0
- recorder/ffmpeg.py +137 -0
- recorder/paths.py +87 -0
- recorder/pipeline_ui.py +1838 -0
- recorder/project_manager.py +329 -0
- recorder/smart_recorder.py +478 -0
- recorder/ui.py +136 -0
- recorder/viz_3d.py +220 -0
- stereo_charuco_pipeline-0.1.0.dist-info/METADATA +10 -0
- stereo_charuco_pipeline-0.1.0.dist-info/RECORD +19 -0
- stereo_charuco_pipeline-0.1.0.dist-info/WHEEL +4 -0
- stereo_charuco_pipeline-0.1.0.dist-info/entry_points.txt +4 -0
recorder/viz_3d.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
3D Visualization module for reconstructed point data.
|
|
3
|
+
|
|
4
|
+
Loads xyz CSV output from caliscope reconstruction, optionally loads
|
|
5
|
+
wireframe skeleton definitions, and renders animated 3D frames using
|
|
6
|
+
matplotlib (embedded in Tkinter via FigureCanvasTkAgg).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ============================================================================
|
|
22
|
+
# Data classes
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class WireSegment:
|
|
27
|
+
"""A single wireframe connection between two point IDs."""
|
|
28
|
+
name: str
|
|
29
|
+
point_a_id: int
|
|
30
|
+
point_b_id: int
|
|
31
|
+
color_rgb: tuple[float, float, float] = (1.0, 1.0, 1.0)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Viz3DData:
|
|
36
|
+
"""Container for all data needed to render 3D playback."""
|
|
37
|
+
|
|
38
|
+
# {sync_index: {point_id: (x, y, z)}}
|
|
39
|
+
frames: dict[int, dict[int, tuple[float, float, float]]]
|
|
40
|
+
# Sorted list of sync indices for sequential playback
|
|
41
|
+
frame_indices: list[int]
|
|
42
|
+
# Wireframe segments (empty list = points only)
|
|
43
|
+
segments: list[WireSegment] = field(default_factory=list)
|
|
44
|
+
# Axis limits: (x_min, x_max, y_min, y_max, z_min, z_max)
|
|
45
|
+
axis_limits: tuple[float, float, float, float, float, float] = (
|
|
46
|
+
-1.0, 1.0, -1.0, 1.0, -1.0, 1.0,
|
|
47
|
+
)
|
|
48
|
+
# Total frame count
|
|
49
|
+
num_frames: int = 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ============================================================================
|
|
53
|
+
# Loading functions
|
|
54
|
+
# ============================================================================
|
|
55
|
+
|
|
56
|
+
def load_xyz_csv(csv_path: Path) -> Viz3DData:
|
|
57
|
+
"""
|
|
58
|
+
Load a caliscope xyz CSV file and parse into frame-indexed data.
|
|
59
|
+
|
|
60
|
+
CSV columns: sync_index, point_id, x_coord, y_coord, z_coord, frame_time
|
|
61
|
+
"""
|
|
62
|
+
logger.info(f"Loading xyz CSV: {csv_path}")
|
|
63
|
+
df = pd.read_csv(csv_path)
|
|
64
|
+
|
|
65
|
+
required_cols = {"sync_index", "point_id", "x_coord", "y_coord", "z_coord"}
|
|
66
|
+
missing = required_cols - set(df.columns)
|
|
67
|
+
if missing:
|
|
68
|
+
raise ValueError(f"CSV missing columns: {missing}")
|
|
69
|
+
|
|
70
|
+
# Drop rows with NaN coordinates
|
|
71
|
+
df = df.dropna(subset=["x_coord", "y_coord", "z_coord"])
|
|
72
|
+
|
|
73
|
+
# Build frame dictionary
|
|
74
|
+
frames: dict[int, dict[int, tuple[float, float, float]]] = {}
|
|
75
|
+
for sync_idx, group in df.groupby("sync_index"):
|
|
76
|
+
points: dict[int, tuple[float, float, float]] = {}
|
|
77
|
+
for _, row in group.iterrows():
|
|
78
|
+
pid = int(row["point_id"])
|
|
79
|
+
points[pid] = (float(row["x_coord"]), float(row["y_coord"]), float(row["z_coord"]))
|
|
80
|
+
frames[int(sync_idx)] = points
|
|
81
|
+
|
|
82
|
+
frame_indices = sorted(frames.keys())
|
|
83
|
+
|
|
84
|
+
# Compute axis limits from all points (with 10% padding)
|
|
85
|
+
all_x = df["x_coord"].values
|
|
86
|
+
all_y = df["y_coord"].values
|
|
87
|
+
all_z = df["z_coord"].values
|
|
88
|
+
|
|
89
|
+
x_min, x_max = float(np.min(all_x)), float(np.max(all_x))
|
|
90
|
+
y_min, y_max = float(np.min(all_y)), float(np.max(all_y))
|
|
91
|
+
z_min, z_max = float(np.min(all_z)), float(np.max(all_z))
|
|
92
|
+
|
|
93
|
+
# Add padding
|
|
94
|
+
pad_x = max((x_max - x_min) * 0.1, 0.01)
|
|
95
|
+
pad_y = max((y_max - y_min) * 0.1, 0.01)
|
|
96
|
+
pad_z = max((z_max - z_min) * 0.1, 0.01)
|
|
97
|
+
|
|
98
|
+
axis_limits = (
|
|
99
|
+
x_min - pad_x, x_max + pad_x,
|
|
100
|
+
y_min - pad_y, y_max + pad_y,
|
|
101
|
+
z_min - pad_z, z_max + pad_z,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
logger.info(f"Loaded {len(frame_indices)} frames, "
|
|
105
|
+
f"{len(df)} total points")
|
|
106
|
+
|
|
107
|
+
return Viz3DData(
|
|
108
|
+
frames=frames,
|
|
109
|
+
frame_indices=frame_indices,
|
|
110
|
+
axis_limits=axis_limits,
|
|
111
|
+
num_frames=len(frame_indices),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_wireframe_for_tracker(tracker_name: str) -> list[WireSegment]:
|
|
116
|
+
"""
|
|
117
|
+
Load wireframe segments for a given tracker type.
|
|
118
|
+
|
|
119
|
+
Uses caliscope's wireframe TOML files. Falls back to empty list
|
|
120
|
+
(points-only) if no wireframe is defined for the tracker.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
from caliscope.gui.geometry.wireframe import load_wireframe_config
|
|
124
|
+
import caliscope
|
|
125
|
+
caliscope_root = Path(caliscope.__file__).parent
|
|
126
|
+
toml_path = (
|
|
127
|
+
caliscope_root / "gui" / "geometry" / "wireframes"
|
|
128
|
+
/ f"{tracker_name.lower()}_wireframe.toml"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not toml_path.exists():
|
|
132
|
+
logger.info(f"No wireframe TOML for tracker '{tracker_name}', using points only")
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
config = load_wireframe_config(toml_path)
|
|
136
|
+
segments = [
|
|
137
|
+
WireSegment(
|
|
138
|
+
name=seg.name,
|
|
139
|
+
point_a_id=seg.point_a_id,
|
|
140
|
+
point_b_id=seg.point_b_id,
|
|
141
|
+
color_rgb=seg.color_rgb,
|
|
142
|
+
)
|
|
143
|
+
for seg in config.segments
|
|
144
|
+
]
|
|
145
|
+
logger.info(f"Loaded {len(segments)} wireframe segments for '{tracker_name}'")
|
|
146
|
+
return segments
|
|
147
|
+
|
|
148
|
+
except ImportError:
|
|
149
|
+
logger.warning("caliscope not available, cannot load wireframe")
|
|
150
|
+
return []
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.warning(f"Failed to load wireframe for '{tracker_name}': {e}")
|
|
153
|
+
return []
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ============================================================================
|
|
157
|
+
# Rendering
|
|
158
|
+
# ============================================================================
|
|
159
|
+
|
|
160
|
+
def render_frame(ax, viz_data: Viz3DData, frame_idx: int):
|
|
161
|
+
"""
|
|
162
|
+
Render a single frame onto a matplotlib 3D Axes.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
ax: matplotlib Axes3D instance
|
|
166
|
+
viz_data: loaded visualization data
|
|
167
|
+
frame_idx: index into viz_data.frame_indices (0-based position)
|
|
168
|
+
"""
|
|
169
|
+
ax.cla()
|
|
170
|
+
|
|
171
|
+
if frame_idx < 0 or frame_idx >= viz_data.num_frames:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
sync_idx = viz_data.frame_indices[frame_idx]
|
|
175
|
+
points = viz_data.frames.get(sync_idx, {})
|
|
176
|
+
|
|
177
|
+
if not points:
|
|
178
|
+
_apply_axis_limits(ax, viz_data)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Extract coordinates
|
|
182
|
+
pids = list(points.keys())
|
|
183
|
+
coords = np.array([points[pid] for pid in pids])
|
|
184
|
+
xs, ys, zs = coords[:, 0], coords[:, 1], coords[:, 2]
|
|
185
|
+
|
|
186
|
+
# Draw points
|
|
187
|
+
ax.scatter(xs, ys, zs, c="#00BFFF", s=8, alpha=0.8, depthshade=True)
|
|
188
|
+
|
|
189
|
+
# Draw wireframe segments
|
|
190
|
+
if viz_data.segments:
|
|
191
|
+
point_lookup = points
|
|
192
|
+
for seg in viz_data.segments:
|
|
193
|
+
if seg.point_a_id in point_lookup and seg.point_b_id in point_lookup:
|
|
194
|
+
pa = point_lookup[seg.point_a_id]
|
|
195
|
+
pb = point_lookup[seg.point_b_id]
|
|
196
|
+
ax.plot(
|
|
197
|
+
[pa[0], pb[0]], [pa[1], pb[1]], [pa[2], pb[2]],
|
|
198
|
+
color=seg.color_rgb, linewidth=1.2, alpha=0.9,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
_apply_axis_limits(ax, viz_data)
|
|
202
|
+
|
|
203
|
+
# Dark theme styling
|
|
204
|
+
ax.set_facecolor("#1a1a1a")
|
|
205
|
+
ax.set_xlabel("X", fontsize=7, color="#888888")
|
|
206
|
+
ax.set_ylabel("Y", fontsize=7, color="#888888")
|
|
207
|
+
ax.set_zlabel("Z", fontsize=7, color="#888888")
|
|
208
|
+
ax.tick_params(labelsize=6, colors="#666666")
|
|
209
|
+
for pane in [ax.xaxis.pane, ax.yaxis.pane, ax.zaxis.pane]:
|
|
210
|
+
pane.set_facecolor("#222222")
|
|
211
|
+
pane.set_edgecolor("#444444")
|
|
212
|
+
ax.grid(True, alpha=0.2)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _apply_axis_limits(ax, viz_data: Viz3DData):
|
|
216
|
+
"""Set consistent axis limits."""
|
|
217
|
+
lim = viz_data.axis_limits
|
|
218
|
+
ax.set_xlim(lim[0], lim[1])
|
|
219
|
+
ax.set_ylim(lim[2], lim[3])
|
|
220
|
+
ax.set_zlim(lim[4], lim[5])
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stereo-charuco-pipeline
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Stereo ChArUco 3D motion capture pipeline with auto-calibration
|
|
5
|
+
Requires-Python: <3.13,>=3.10
|
|
6
|
+
Requires-Dist: caliscope>=0.6.9
|
|
7
|
+
Requires-Dist: matplotlib>=3.7.0
|
|
8
|
+
Requires-Dist: opencv-contrib-python>=4.8.0.74
|
|
9
|
+
Requires-Dist: pillow>=10.0.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
recorder/__init__.py,sha256=uVaF6VHhg-JlMfpey8mbT_EMMEwyzFdGeoF69lIE3nY,2659
|
|
2
|
+
recorder/auto_calibrate.py,sha256=aRA9LRkQRLzWQy-oL9qJZWN1BBM54SVezBgQd-oztp0,18318
|
|
3
|
+
recorder/calibration_ui.py,sha256=xh-qQkxir2TprzvbRObd3CQKbAJVhyeNtQXtuMKyv78,40224
|
|
4
|
+
recorder/calibration_ui_advanced.py,sha256=C5hey8QZYPTzXb2549-8th5voXOFvjdU7S63Y2wPD5E,40480
|
|
5
|
+
recorder/camera.py,sha256=Q9C12QYNwD-6typOWVK3-bIiNC3r6NVaxCv9s381wyY,1392
|
|
6
|
+
recorder/cli.py,sha256=wGj9zdStCpmdVgBR1RQ6q0BDz0l4QVBhdlxHSzleiB0,3839
|
|
7
|
+
recorder/config.py,sha256=dN2khzDYT7Bbawo3X9aNRbgTrCykVfX4R1z8yMcetxM,2158
|
|
8
|
+
recorder/ffmpeg.py,sha256=wmUXlPqVBo6K8S5mrGad2_aqVTqrgv0cTNtpc3D1918,3397
|
|
9
|
+
recorder/paths.py,sha256=7AoEKZb_NlvCRkv_A1UERC6bF_AalALaVDVPuLQpito,2671
|
|
10
|
+
recorder/pipeline_ui.py,sha256=4Os5fhCh90y1z-lIBWMi20YFCqLIAdG533VdVTdNuVU,65931
|
|
11
|
+
recorder/project_manager.py,sha256=_TXzBKDRpnb9hhYTmjsY8PjvRrWJGxLgH2gwD78YLV4,10438
|
|
12
|
+
recorder/smart_recorder.py,sha256=JBQspi44uk8QgAPUmO3r3j9_mEiO01GMp4j6jx1Qeyo,16590
|
|
13
|
+
recorder/ui.py,sha256=Kw5ci9l9JimJ5Uf1HuKk_Upik3rO3AcAsPn-XrKcNSM,4329
|
|
14
|
+
recorder/viz_3d.py,sha256=hfrWKRm0sHGdcij2kuc57hgYpqXpBBkmHUCAmzfKXJ4,6956
|
|
15
|
+
recorder/configs/default.yaml,sha256=itVzn19PZQ90h0FtTQ5sfKA-nPgiVxlss8tZZbbQYnA,544
|
|
16
|
+
stereo_charuco_pipeline-0.1.0.dist-info/METADATA,sha256=KcdCmctKZydW6WJfao7Qt3il4Pa4rlsP2Srgn2ER3fI,339
|
|
17
|
+
stereo_charuco_pipeline-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
+
stereo_charuco_pipeline-0.1.0.dist-info/entry_points.txt,sha256=f7I33mf6XwGP_dwaqCjRIFKGi8ZyePBb52rdylR7d54,144
|
|
19
|
+
stereo_charuco_pipeline-0.1.0.dist-info/RECORD,,
|