diff --git a/py_workspace/.beads/issues.jsonl b/py_workspace/.beads/issues.jsonl index fabe9f7..bfb3217 100644 --- a/py_workspace/.beads/issues.jsonl +++ b/py_workspace/.beads/issues.jsonl @@ -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-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"} @@ -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-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-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-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."} diff --git a/py_workspace/README.md b/py_workspace/README.md index f685b00..9bedb7a 100755 --- a/py_workspace/README.md +++ b/py_workspace/README.md @@ -89,7 +89,8 @@ uv run visualize_extrinsics.py -i output/extrinsics.json \ ``` **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.conf` filenames. For full options: diff --git a/py_workspace/visualize_extrinsics.py b/py_workspace/visualize_extrinsics.py index 1bc45fc..11102c8 100644 --- a/py_workspace/visualize_extrinsics.py +++ b/py_workspace/visualize_extrinsics.py @@ -151,6 +151,7 @@ def add_camera_trace( label: str, scale: float = 0.2, convention: str = "world_from_cam", + render_space: str = "opencv", frustum_scale: float = 0.5, fov_deg: float = 60.0, intrinsics: Optional[Dict[str, float]] = None, @@ -163,26 +164,46 @@ def add_camera_trace( t = pose[:3, 3] 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 center = -R.T @ t # Camera orientation in world coordinates: R_world_from_cam = R^T R_world = R.T else: - # world_from_cam + # world_from_cam (Standard convention for calibrate_extrinsics.py) + # calibrate_extrinsics.py inverts the solvePnP result before saving. center = t R_world = R # Local axes in world frame - x_axis = R_world[:, 0] - y_axis = R_world[:, 1] - z_axis = R_world[:, 2] + if render_space == "opengl": + # OpenGL convention: X right, Y up, Z backward (toOGL = diag(1,-1,-1)) + # 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) - pts_local = get_frustum_points(intrinsics, frustum_scale, fov_deg) + # To keep the frustum consistent (pointing in the same world direction), + # 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 - # pts_world = (R_world @ pts_local.T).T + center - pts_world = (R_world @ pts_local.T).T + center + # Frustum points in local coordinates (OpenCV: +Z fwd, +X right, +Y down) + pts_local = get_frustum_points(intrinsics, frustum_scale, fov_deg) + + # Transform to world + pts_world = (R_world @ pts_local.T).T + center # Create lines for frustum # 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(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 = [] rotations = [] @@ -389,7 +418,13 @@ def run_diagnostics(poses: Dict[str, np.ndarray], convention: str): "--pose-convention", type=click.Choice(["world_from_cam", "cam_from_world"]), 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( "--frustum-scale", type=float, default=0.5, help="Scale of the camera frustum." @@ -429,6 +464,7 @@ def main( scale: float, birdseye: bool, pose_convention: str, + render_space: str, frustum_scale: float, fov: float, zed_configs: List[str], @@ -484,6 +520,7 @@ def main( str(serial), scale=scale, convention=pose_convention, + render_space=render_space, frustum_scale=frustum_scale, fov_deg=fov, intrinsics=cam_intrinsics, @@ -507,7 +544,7 @@ def main( ) fig.update_layout( - title=f"Camera Extrinsics ({pose_convention})", + title=f"Camera Extrinsics ({pose_convention}, {render_space})", scene=scene_dict, margin=dict(l=0, r=0, b=0, t=40), legend=dict(x=0, y=1),