diff --git a/py_workspace/README.md b/py_workspace/README.md
index 0da6520..467fad0 100755
--- a/py_workspace/README.md
+++ b/py_workspace/README.md
@@ -75,11 +75,12 @@ Use `--birdseye` for a top-down X-Z view (looking down Y axis).
uv run visualize_extrinsics.py -i output/extrinsics.json --birdseye --show
```
-**Ground Plane Overlay:**
-Render a semi-transparent X-Z ground plane to anchor camera poses.
+**Ground Plane & Origin Overlay:**
+Render a semi-transparent X-Z ground plane and/or a world origin triad.
- `--show-ground/--no-show-ground`: Toggle ground plane (default: show).
- `--ground-y FLOAT`: Set the Y height of the plane (default: 0.0).
- `--ground-size FLOAT`: Set the side length of the plane in meters (default: 8.0).
+- `--show-origin-axes/--no-show-origin-axes`: Toggle world origin triad (X:red, Y:green, Z:blue) (default: show).
*Example: Ground plane at Y=-1.5 with 10m size*
```bash
diff --git a/py_workspace/visualize_extrinsics.py b/py_workspace/visualize_extrinsics.py
index 77eb5e4..78a7a30 100644
--- a/py_workspace/visualize_extrinsics.py
+++ b/py_workspace/visualize_extrinsics.py
@@ -474,6 +474,11 @@ def run_diagnostics(poses: Dict[str, np.ndarray], convention: str):
default=8.0,
help="Size of the ground plane (side length in meters).",
)
+@click.option(
+ "--show-origin-axes/--no-show-origin-axes",
+ default=True,
+ help="Show a world-origin axis triad (X:red, Y:green, Z:blue).",
+)
def main(
input: str,
output: Optional[str],
@@ -491,6 +496,7 @@ def main(
show_ground: bool,
ground_y: float,
ground_size: float,
+ show_origin_axes: bool,
):
"""Visualize camera extrinsics from JSON using Plotly."""
try:
@@ -546,6 +552,52 @@ def main(
intrinsics=cam_intrinsics,
)
+ if show_origin_axes:
+ origin = np.zeros(3)
+ axis_len = scale
+ fig.add_trace(
+ go.Scatter3d(
+ x=[origin[0], origin[0] + axis_len],
+ y=[origin[1], origin[1]],
+ z=[origin[2], origin[2]],
+ mode="lines",
+ line=dict(color="red", width=4),
+ name="World X",
+ legendgroup="Origin",
+ showlegend=True,
+ hoverinfo="text",
+ text="World X",
+ )
+ )
+ fig.add_trace(
+ go.Scatter3d(
+ x=[origin[0], origin[0]],
+ y=[origin[1], origin[1] + axis_len],
+ z=[origin[2], origin[2]],
+ mode="lines",
+ line=dict(color="green", width=4),
+ name="World Y",
+ legendgroup="Origin",
+ showlegend=True,
+ hoverinfo="text",
+ text="World Y",
+ )
+ )
+ fig.add_trace(
+ go.Scatter3d(
+ x=[origin[0], origin[0]],
+ y=[origin[1], origin[1]],
+ z=[origin[2], origin[2] + axis_len],
+ mode="lines",
+ line=dict(color="blue", width=4),
+ name="World Z",
+ legendgroup="Origin",
+ showlegend=True,
+ hoverinfo="text",
+ text="World Z",
+ )
+ )
+
if show_ground:
half_size = ground_size / 2.0
x_grid = np.linspace(-half_size, half_size, 2)
@@ -583,10 +635,16 @@ def main(
eye=dict(x=0, y=2.5, z=0),
)
+ render_desc = (
+ "OpenCV: local +X,+Y,+Z (Y-down)"
+ if render_space == "opencv"
+ else "OpenGL: local +X,-Y,-Z (Y-up, Z-back) rel. to OpenCV"
+ )
+
fig.update_layout(
- title=f"Camera Extrinsics ({pose_convention}, {render_space})",
+ title=f"Camera Extrinsics ({pose_convention})
{render_desc}",
scene=scene_dict,
- margin=dict(l=0, r=0, b=0, t=40),
+ margin=dict(l=0, r=0, b=0, t=60),
legend=dict(x=0, y=1),
)