feat: add --render-space option to visualize_extrinsics.py
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
{"id":"py_workspace-0mu","title":"Implement --render-space in visualize_extrinsics.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T16:08:12.543309499Z","created_by":"crosstyan","updated_at":"2026-02-07T16:08:17.303232927Z","closed_at":"2026-02-07T16:08:17.303232927Z","close_reason":"Implemented --render-space with opencv/opengl choices and updated README"}
|
||||||
{"id":"py_workspace-0q7","title":"Fix basedpyright errors in aruco/pose_averaging.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T08:53:33.377735199Z","created_by":"crosstyan","updated_at":"2026-02-07T08:58:49.252312392Z","closed_at":"2026-02-07T08:58:49.252312392Z","close_reason":"Fixed basedpyright errors"}
|
{"id":"py_workspace-0q7","title":"Fix basedpyright errors in aruco/pose_averaging.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T08:53:33.377735199Z","created_by":"crosstyan","updated_at":"2026-02-07T08:58:49.252312392Z","closed_at":"2026-02-07T08:58:49.252312392Z","close_reason":"Fixed basedpyright errors"}
|
||||||
{"id":"py_workspace-214","title":"Migrate visualize_extrinsics to Plotly with diagnose mode","status":"closed","priority":2,"issue_type":"feature","owner":"crosstyan@outlook.com","created_at":"2026-02-07T15:14:40.547616056Z","created_by":"crosstyan","updated_at":"2026-02-07T15:25:00.354290874Z","closed_at":"2026-02-07T15:25:00.354290874Z","close_reason":"Fixed QA issues: Y-up enforcement, README sync, dependencies"}
|
{"id":"py_workspace-214","title":"Migrate visualize_extrinsics to Plotly with diagnose mode","status":"closed","priority":2,"issue_type":"feature","owner":"crosstyan@outlook.com","created_at":"2026-02-07T15:14:40.547616056Z","created_by":"crosstyan","updated_at":"2026-02-07T15:25:00.354290874Z","closed_at":"2026-02-07T15:25:00.354290874Z","close_reason":"Fixed QA issues: Y-up enforcement, README sync, dependencies"}
|
||||||
{"id":"py_workspace-62y","title":"Fix depth pooling fallback threshold","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T08:12:12.046607198Z","created_by":"crosstyan","updated_at":"2026-02-07T08:13:12.98625698Z","closed_at":"2026-02-07T08:13:12.98625698Z","close_reason":"Updated fallback threshold to strict comparison"}
|
{"id":"py_workspace-62y","title":"Fix depth pooling fallback threshold","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T08:12:12.046607198Z","created_by":"crosstyan","updated_at":"2026-02-07T08:13:12.98625698Z","closed_at":"2026-02-07T08:13:12.98625698Z","close_reason":"Updated fallback threshold to strict comparison"}
|
||||||
@@ -5,6 +6,7 @@
|
|||||||
{"id":"py_workspace-6sg","title":"Document marker parquet structure","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T02:48:08.95742431Z","created_by":"crosstyan","updated_at":"2026-02-07T02:49:35.897152691Z","closed_at":"2026-02-07T02:49:35.897152691Z","close_reason":"Documented parquet structure in aruco/markers/PARQUET_FORMAT.md"}
|
{"id":"py_workspace-6sg","title":"Document marker parquet structure","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T02:48:08.95742431Z","created_by":"crosstyan","updated_at":"2026-02-07T02:49:35.897152691Z","closed_at":"2026-02-07T02:49:35.897152691Z","close_reason":"Documented parquet structure in aruco/markers/PARQUET_FORMAT.md"}
|
||||||
{"id":"py_workspace-98p","title":"Integrate multi-frame depth pooling into calibrate_extrinsics.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T07:59:35.333468652Z","created_by":"crosstyan","updated_at":"2026-02-07T08:06:37.662956356Z","closed_at":"2026-02-07T08:06:37.662956356Z","close_reason":"Implemented multi-frame depth pooling and verified with tests"}
|
{"id":"py_workspace-98p","title":"Integrate multi-frame depth pooling into calibrate_extrinsics.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T07:59:35.333468652Z","created_by":"crosstyan","updated_at":"2026-02-07T08:06:37.662956356Z","closed_at":"2026-02-07T08:06:37.662956356Z","close_reason":"Implemented multi-frame depth pooling and verified with tests"}
|
||||||
{"id":"py_workspace-a85","title":"Add CLI option for ArUco dictionary in calibrate_extrinsics.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-06T10:13:41.896728814Z","created_by":"crosstyan","updated_at":"2026-02-07T07:29:52.290976525Z","closed_at":"2026-02-07T07:29:52.290976525Z","close_reason":"Implemented multi-frame depth pooling in calibrate_extrinsics.py"}
|
{"id":"py_workspace-a85","title":"Add CLI option for ArUco dictionary in calibrate_extrinsics.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-06T10:13:41.896728814Z","created_by":"crosstyan","updated_at":"2026-02-07T07:29:52.290976525Z","closed_at":"2026-02-07T07:29:52.290976525Z","close_reason":"Implemented multi-frame depth pooling in calibrate_extrinsics.py"}
|
||||||
|
{"id":"py_workspace-afh","title":"Inspect tmp_visualizer.html camera layout","notes":"Inspected tmp_visualizer.html. cam_0 is at (0,0,0). cam_1 is at (1,0,0). cam_2 is at (0, 0.5, 1.0). Axes are RGB=XYZ. Layout matches expected synthetic geometry.","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T15:40:04.162565539Z","created_by":"crosstyan","updated_at":"2026-02-07T15:42:10.721124074Z","closed_at":"2026-02-07T15:42:10.721124074Z","close_reason":"Inspection complete. Layout matches synthetic input."}
|
||||||
{"id":"py_workspace-cg9","title":"Implement core alignment utilities (Task 1)","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-06T10:40:36.296030875Z","created_by":"crosstyan","updated_at":"2026-02-06T10:40:46.196825039Z","closed_at":"2026-02-06T10:40:46.196825039Z","close_reason":"Implemented compute_face_normal, rotation_align_vectors, and apply_alignment_to_pose in aruco/alignment.py"}
|
{"id":"py_workspace-cg9","title":"Implement core alignment utilities (Task 1)","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-06T10:40:36.296030875Z","created_by":"crosstyan","updated_at":"2026-02-06T10:40:46.196825039Z","closed_at":"2026-02-06T10:40:46.196825039Z","close_reason":"Implemented compute_face_normal, rotation_align_vectors, and apply_alignment_to_pose in aruco/alignment.py"}
|
||||||
{"id":"py_workspace-ee1","title":"Implement depth-mode argument resolution in calibrate_extrinsics.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T06:31:03.430147225Z","created_by":"crosstyan","updated_at":"2026-02-07T06:33:43.204825053Z","closed_at":"2026-02-07T06:33:43.204825053Z","close_reason":"Implemented depth-mode argument resolution logic and verified with multiple test cases."}
|
{"id":"py_workspace-ee1","title":"Implement depth-mode argument resolution in calibrate_extrinsics.py","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T06:31:03.430147225Z","created_by":"crosstyan","updated_at":"2026-02-07T06:33:43.204825053Z","closed_at":"2026-02-07T06:33:43.204825053Z","close_reason":"Implemented depth-mode argument resolution logic and verified with multiple test cases."}
|
||||||
{"id":"py_workspace-j8b","title":"Research scipy.optimize.least_squares robust optimization for depth residuals","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T04:54:04.720996955Z","created_by":"crosstyan","updated_at":"2026-02-07T04:55:22.995644Z","closed_at":"2026-02-07T04:55:22.995644Z","close_reason":"Research completed and recommendations provided."}
|
{"id":"py_workspace-j8b","title":"Research scipy.optimize.least_squares robust optimization for depth residuals","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-07T04:54:04.720996955Z","created_by":"crosstyan","updated_at":"2026-02-07T04:55:22.995644Z","closed_at":"2026-02-07T04:55:22.995644Z","close_reason":"Research completed and recommendations provided."}
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ uv run visualize_extrinsics.py -i output/extrinsics.json \
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Troubleshooting:**
|
**Troubleshooting:**
|
||||||
- **Cameras bunched up?** Check `--pose-convention`. `world_from_cam` is typical for optimization outputs.
|
- **Cameras bunched up?** Check `--pose-convention`. `world_from_cam` is the standard convention for `calibrate_extrinsics.py` outputs. `cam_from_world` is deprecated.
|
||||||
|
- **Axes flipped?** Use `--render-space opengl` to match C++ viewer conventions (X right, Y up, Z backward). Default is `opencv` (X right, Y down, Z forward).
|
||||||
- **Config not matching?** Ensure JSON keys match the serial numbers in `SN<serial>.conf` filenames.
|
- **Config not matching?** Ensure JSON keys match the serial numbers in `SN<serial>.conf` filenames.
|
||||||
|
|
||||||
For full options:
|
For full options:
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ def add_camera_trace(
|
|||||||
label: str,
|
label: str,
|
||||||
scale: float = 0.2,
|
scale: float = 0.2,
|
||||||
convention: str = "world_from_cam",
|
convention: str = "world_from_cam",
|
||||||
|
render_space: str = "opencv",
|
||||||
frustum_scale: float = 0.5,
|
frustum_scale: float = 0.5,
|
||||||
fov_deg: float = 60.0,
|
fov_deg: float = 60.0,
|
||||||
intrinsics: Optional[Dict[str, float]] = None,
|
intrinsics: Optional[Dict[str, float]] = None,
|
||||||
@@ -163,26 +164,46 @@ def add_camera_trace(
|
|||||||
t = pose[:3, 3]
|
t = pose[:3, 3]
|
||||||
|
|
||||||
if convention == "cam_from_world":
|
if convention == "cam_from_world":
|
||||||
|
# DEPRECATED: calibrate_extrinsics.py outputs world_from_cam.
|
||||||
|
# This path is kept for legacy compatibility but should be avoided for new calibrations.
|
||||||
# Camera center in world coordinates: C = -R^T * t
|
# Camera center in world coordinates: C = -R^T * t
|
||||||
center = -R.T @ t
|
center = -R.T @ t
|
||||||
# Camera orientation in world coordinates: R_world_from_cam = R^T
|
# Camera orientation in world coordinates: R_world_from_cam = R^T
|
||||||
R_world = R.T
|
R_world = R.T
|
||||||
else:
|
else:
|
||||||
# world_from_cam
|
# world_from_cam (Standard convention for calibrate_extrinsics.py)
|
||||||
|
# calibrate_extrinsics.py inverts the solvePnP result before saving.
|
||||||
center = t
|
center = t
|
||||||
R_world = R
|
R_world = R
|
||||||
|
|
||||||
# Local axes in world frame
|
# Local axes in world frame
|
||||||
x_axis = R_world[:, 0]
|
if render_space == "opengl":
|
||||||
y_axis = R_world[:, 1]
|
# OpenGL convention: X right, Y up, Z backward (toOGL = diag(1,-1,-1))
|
||||||
z_axis = R_world[:, 2]
|
# R_render = R_world @ diag(1, -1, -1)
|
||||||
|
R_render = R_world @ np.diag([1, -1, -1])
|
||||||
|
x_axis = R_render[:, 0]
|
||||||
|
y_axis = R_render[:, 1]
|
||||||
|
z_axis = R_render[:, 2]
|
||||||
|
|
||||||
# Frustum points in local coordinates (OpenCV: +Z fwd, +X right, +Y down)
|
# To keep the frustum consistent (pointing in the same world direction),
|
||||||
pts_local = get_frustum_points(intrinsics, frustum_scale, fov_deg)
|
# we must also flip its local coordinates because OpenGL looks down -Z.
|
||||||
|
# pts_local_cv = get_frustum_points(...)
|
||||||
|
# pts_local_ogl = diag(1,-1,-1) @ pts_local_cv
|
||||||
|
# pts_world = R_render @ pts_local_ogl + center
|
||||||
|
pts_local_cv = get_frustum_points(intrinsics, frustum_scale, fov_deg)
|
||||||
|
pts_local_ogl = pts_local_cv * np.array([1, -1, -1])
|
||||||
|
pts_world = (R_render @ pts_local_ogl.T).T + center
|
||||||
|
else:
|
||||||
|
# OpenCV convention: X right, Y down, Z forward
|
||||||
|
x_axis = R_world[:, 0]
|
||||||
|
y_axis = R_world[:, 1]
|
||||||
|
z_axis = R_world[:, 2]
|
||||||
|
|
||||||
# Transform to world
|
# Frustum points in local coordinates (OpenCV: +Z fwd, +X right, +Y down)
|
||||||
# pts_world = (R_world @ pts_local.T).T + center
|
pts_local = get_frustum_points(intrinsics, frustum_scale, fov_deg)
|
||||||
pts_world = (R_world @ pts_local.T).T + center
|
|
||||||
|
# Transform to world
|
||||||
|
pts_world = (R_world @ pts_local.T).T + center
|
||||||
|
|
||||||
# Create lines for frustum
|
# Create lines for frustum
|
||||||
# Edges: 0-1, 0-2, 0-3, 0-4 (pyramid sides)
|
# Edges: 0-1, 0-2, 0-3, 0-4 (pyramid sides)
|
||||||
@@ -280,6 +301,14 @@ def run_diagnostics(poses: Dict[str, np.ndarray], convention: str):
|
|||||||
"""
|
"""
|
||||||
print("\n--- Diagnostics ---")
|
print("\n--- Diagnostics ---")
|
||||||
print(f"Pose Convention: {convention}")
|
print(f"Pose Convention: {convention}")
|
||||||
|
if convention == "cam_from_world":
|
||||||
|
print(
|
||||||
|
" WARNING: 'cam_from_world' is deprecated. calibrate_extrinsics.py outputs 'world_from_cam'."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
" Note: Using 'world_from_cam' (matches calibrate_extrinsics.py output)."
|
||||||
|
)
|
||||||
|
|
||||||
centers = []
|
centers = []
|
||||||
rotations = []
|
rotations = []
|
||||||
@@ -389,7 +418,13 @@ def run_diagnostics(poses: Dict[str, np.ndarray], convention: str):
|
|||||||
"--pose-convention",
|
"--pose-convention",
|
||||||
type=click.Choice(["world_from_cam", "cam_from_world"]),
|
type=click.Choice(["world_from_cam", "cam_from_world"]),
|
||||||
default="world_from_cam",
|
default="world_from_cam",
|
||||||
help="Interpretation of the pose matrix in JSON. Defaults to 'world_from_cam'.",
|
help="Interpretation of the pose matrix in JSON. Defaults to 'world_from_cam' (matches calibrate_extrinsics.py). 'cam_from_world' is deprecated.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--render-space",
|
||||||
|
type=click.Choice(["opencv", "opengl"]),
|
||||||
|
default="opencv",
|
||||||
|
help="Render space convention. 'opencv' (default) is camera-local +Z forward. 'opengl' applies diag(1,-1,-1) conversion for visualization.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--frustum-scale", type=float, default=0.5, help="Scale of the camera frustum."
|
"--frustum-scale", type=float, default=0.5, help="Scale of the camera frustum."
|
||||||
@@ -429,6 +464,7 @@ def main(
|
|||||||
scale: float,
|
scale: float,
|
||||||
birdseye: bool,
|
birdseye: bool,
|
||||||
pose_convention: str,
|
pose_convention: str,
|
||||||
|
render_space: str,
|
||||||
frustum_scale: float,
|
frustum_scale: float,
|
||||||
fov: float,
|
fov: float,
|
||||||
zed_configs: List[str],
|
zed_configs: List[str],
|
||||||
@@ -484,6 +520,7 @@ def main(
|
|||||||
str(serial),
|
str(serial),
|
||||||
scale=scale,
|
scale=scale,
|
||||||
convention=pose_convention,
|
convention=pose_convention,
|
||||||
|
render_space=render_space,
|
||||||
frustum_scale=frustum_scale,
|
frustum_scale=frustum_scale,
|
||||||
fov_deg=fov,
|
fov_deg=fov,
|
||||||
intrinsics=cam_intrinsics,
|
intrinsics=cam_intrinsics,
|
||||||
@@ -507,7 +544,7 @@ def main(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fig.update_layout(
|
fig.update_layout(
|
||||||
title=f"Camera Extrinsics ({pose_convention})",
|
title=f"Camera Extrinsics ({pose_convention}, {render_space})",
|
||||||
scene=scene_dict,
|
scene=scene_dict,
|
||||||
margin=dict(l=0, r=0, b=0, t=40),
|
margin=dict(l=0, r=0, b=0, t=40),
|
||||||
legend=dict(x=0, y=1),
|
legend=dict(x=0, y=1),
|
||||||
|
|||||||
Reference in New Issue
Block a user