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/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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ stereo-calibrate = recorder.cli:calibrate_only
3
+ stereo-pipeline = recorder.cli:main
4
+ stereo-record = recorder.cli:pipeline_only