|
|
|
@@ -0,0 +1,745 @@
|
|
|
|
|
# ArUco-Based Multi-Camera Extrinsic Calibration from SVO
|
|
|
|
|
|
|
|
|
|
## TL;DR
|
|
|
|
|
|
|
|
|
|
> **Quick Summary**: Create a CLI tool that reads synchronized SVO recordings from multiple ZED cameras, detects ArUco markers on a 3D calibration box, computes camera extrinsics using robust pose averaging, and outputs accurate 4x4 transform matrices.
|
|
|
|
|
>
|
|
|
|
|
> **Deliverables**:
|
|
|
|
|
> - `calibrate_extrinsics.py` - Main CLI tool
|
|
|
|
|
> - `pose_averaging.py` - Robust pose estimation utilities
|
|
|
|
|
> - `svo_sync.py` - Multi-SVO timestamp synchronization
|
|
|
|
|
> - `tests/test_pose_math.py` - Unit tests for pose calculations
|
|
|
|
|
> - Output JSON with calibrated extrinsics
|
|
|
|
|
>
|
|
|
|
|
> **Estimated Effort**: Medium (3-5 days)
|
|
|
|
|
> **Parallel Execution**: YES - 2 waves
|
|
|
|
|
> **Critical Path**: Task 1 → Task 3 → Task 5 → Task 7 → Task 8
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Context
|
|
|
|
|
|
|
|
|
|
### Original Request
|
|
|
|
|
User wants to integrate ArUco marker detection with SVO recording playback to calibrate multi-camera extrinsics. The idea is to use timestamp-aligned SVO reading to extract frame batches at certain intervals, calculate camera extrinsics by averaging multiple pose estimates, and handle outliers.
|
|
|
|
|
|
|
|
|
|
### Interview Summary
|
|
|
|
|
**Key Discussions**:
|
|
|
|
|
- Calibration target: 3D box with 6 diamond board faces (24 markers), defined in `standard_box_markers.parquet`
|
|
|
|
|
- Current extrinsics in `inside_network.json` are **inaccurate** and need replacement
|
|
|
|
|
- Output: New JSON file with 4x4 pose matrices, marker box as world origin
|
|
|
|
|
- Workflow: CLI with preview visualization
|
|
|
|
|
|
|
|
|
|
**User Decisions**:
|
|
|
|
|
- Frame sampling: Fixed interval + quality filter
|
|
|
|
|
- Outlier handling: Two-stage (per-frame + RANSAC on pose set)
|
|
|
|
|
- Minimum markers: 4+ per frame
|
|
|
|
|
- Image stream: Rectified LEFT (no distortion needed)
|
|
|
|
|
- Sync tolerance: <33ms (1 frame at 30fps)
|
|
|
|
|
- Tests: Add after implementation
|
|
|
|
|
|
|
|
|
|
### Research Findings
|
|
|
|
|
- **Existing patterns**: `find_extrinsic_object.py` (ArUco + solvePnP), `svo_playback.py` (multi-SVO sync)
|
|
|
|
|
- **ZED SDK intrinsics**: `cam.get_camera_information().camera_configuration.calibration_parameters.left_cam`
|
|
|
|
|
- **Rotation averaging**: `scipy.spatial.transform.Rotation.mean()` for geodesic mean
|
|
|
|
|
- **Translation averaging**: Median with MAD-based outlier rejection
|
|
|
|
|
- **Transform math**: `T_world_cam = inv(T_cam_marker)` when marker is world origin
|
|
|
|
|
|
|
|
|
|
### Metis Review
|
|
|
|
|
**Identified Gaps** (addressed):
|
|
|
|
|
- World frame definition → Use coordinates from `standard_box_markers.parquet`
|
|
|
|
|
- Transform convention → Match `inside_network.json` format (T_world_from_cam, space-separated 4x4)
|
|
|
|
|
- Image stream → Rectified LEFT view (no distortion)
|
|
|
|
|
- Sync tolerance → Moderate (<33ms)
|
|
|
|
|
- Parquet validation → Must validate schema early
|
|
|
|
|
- Planar degeneracy → Require multi-face visibility or 3D spread check
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Work Objectives
|
|
|
|
|
|
|
|
|
|
### Core Objective
|
|
|
|
|
Build a robust CLI tool for multi-camera extrinsic calibration using ArUco markers detected in synchronized SVO playback.
|
|
|
|
|
|
|
|
|
|
### Concrete Deliverables
|
|
|
|
|
- `py_workspace/calibrate_extrinsics.py` - Main entry point
|
|
|
|
|
- `py_workspace/aruco/pose_averaging.py` - Robust averaging utilities
|
|
|
|
|
- `py_workspace/aruco/svo_sync.py` - Multi-SVO synchronization
|
|
|
|
|
- `py_workspace/tests/test_pose_math.py` - Unit tests
|
|
|
|
|
- Output: `calibrated_extrinsics.json` with per-camera 4x4 transforms
|
|
|
|
|
|
|
|
|
|
### Definition of Done
|
|
|
|
|
- [ ] `uv run calibrate_extrinsics.py --help` → exits 0, shows required args
|
|
|
|
|
- [ ] `uv run calibrate_extrinsics.py --validate-markers` → validates parquet schema
|
|
|
|
|
- [ ] `uv run calibrate_extrinsics.py --svos ... --output out.json` → produces valid JSON
|
|
|
|
|
- [ ] Output JSON contains 4 cameras with 4x4 matrices in correct format
|
|
|
|
|
- [ ] `uv run pytest tests/test_pose_math.py` → all tests pass
|
|
|
|
|
- [ ] Preview mode shows detected markers with axes overlay
|
|
|
|
|
|
|
|
|
|
### Must Have
|
|
|
|
|
- Load multiple SVO files with timestamp synchronization
|
|
|
|
|
- Detect ArUco markers using cv2.aruco with DICT_4X4_50
|
|
|
|
|
- Estimate per-frame poses using cv2.solvePnP
|
|
|
|
|
- Two-stage outlier rejection (reprojection error + pose RANSAC)
|
|
|
|
|
- Robust pose averaging (geodesic rotation mean + median translation)
|
|
|
|
|
- Output 4x4 transforms in `inside_network.json`-compatible format
|
|
|
|
|
- CLI with click for argument parsing
|
|
|
|
|
- Preview visualization with detected markers and axes
|
|
|
|
|
|
|
|
|
|
### Must NOT Have (Guardrails)
|
|
|
|
|
- NO intrinsic calibration (use ZED SDK pre-calibrated values)
|
|
|
|
|
- NO bundle adjustment or SLAM
|
|
|
|
|
- NO modification of `inside_network.json` in-place
|
|
|
|
|
- NO right camera processing (use left only)
|
|
|
|
|
- NO GUI beyond simple preview window
|
|
|
|
|
- NO depth-based verification
|
|
|
|
|
- NO automatic config file updates
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Verification Strategy
|
|
|
|
|
|
|
|
|
|
> **UNIVERSAL RULE: ZERO HUMAN INTERVENTION**
|
|
|
|
|
>
|
|
|
|
|
> ALL tasks must be verifiable by agent-executed commands. No "user visually confirms" criteria.
|
|
|
|
|
|
|
|
|
|
### Test Decision
|
|
|
|
|
- **Infrastructure exists**: NO (need to set up pytest)
|
|
|
|
|
- **Automated tests**: YES (tests-after)
|
|
|
|
|
- **Framework**: pytest
|
|
|
|
|
|
|
|
|
|
### Agent-Executed QA Scenarios (MANDATORY)
|
|
|
|
|
|
|
|
|
|
**Verification Tool by Deliverable Type:**
|
|
|
|
|
|
|
|
|
|
| Type | Tool | How Agent Verifies |
|
|
|
|
|
|------|------|-------------------|
|
|
|
|
|
| CLI | Bash | Run command, check exit code, parse output |
|
|
|
|
|
| JSON output | Bash (jq) | Parse JSON, validate structure and values |
|
|
|
|
|
| Preview | Playwright | Capture window screenshot (optional) |
|
|
|
|
|
| Unit tests | Bash (pytest) | Run tests, assert all pass |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Execution Strategy
|
|
|
|
|
|
|
|
|
|
### Parallel Execution Waves
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Wave 1 (Start Immediately):
|
|
|
|
|
├── Task 1: Core pose math utilities
|
|
|
|
|
├── Task 2: Parquet loader and validator
|
|
|
|
|
└── Task 4: SVO synchronization module
|
|
|
|
|
|
|
|
|
|
Wave 2 (After Wave 1):
|
|
|
|
|
├── Task 3: ArUco detection integration (depends: 1, 2)
|
|
|
|
|
├── Task 5: Robust pose aggregation (depends: 1)
|
|
|
|
|
└── Task 6: Preview visualization (depends: 3)
|
|
|
|
|
|
|
|
|
|
Wave 3 (After Wave 2):
|
|
|
|
|
├── Task 7: CLI integration (depends: 3, 4, 5, 6)
|
|
|
|
|
└── Task 8: Tests and validation (depends: all)
|
|
|
|
|
|
|
|
|
|
Critical Path: Task 1 → Task 3 → Task 7 → Task 8
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Dependency Matrix
|
|
|
|
|
|
|
|
|
|
| Task | Depends On | Blocks | Can Parallelize With |
|
|
|
|
|
|------|------------|--------|---------------------|
|
|
|
|
|
| 1 | None | 3, 5 | 2, 4 |
|
|
|
|
|
| 2 | None | 3 | 1, 4 |
|
|
|
|
|
| 3 | 1, 2 | 6, 7 | 5 |
|
|
|
|
|
| 4 | None | 7 | 1, 2 |
|
|
|
|
|
| 5 | 1 | 7 | 3, 6 |
|
|
|
|
|
| 6 | 3 | 7 | 5 |
|
|
|
|
|
| 7 | 3, 4, 5, 6 | 8 | None |
|
|
|
|
|
| 8 | 7 | None | None |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## TODOs
|
|
|
|
|
|
|
|
|
|
- [ ] 1. Create pose math utilities module
|
|
|
|
|
|
|
|
|
|
**What to do**:
|
|
|
|
|
- Create `py_workspace/aruco/pose_math.py`
|
|
|
|
|
- Implement `rvec_tvec_to_matrix(rvec, tvec) -> np.ndarray` (4x4 homogeneous)
|
|
|
|
|
- Implement `matrix_to_rvec_tvec(T) -> tuple[np.ndarray, np.ndarray]`
|
|
|
|
|
- Implement `invert_transform(T) -> np.ndarray`
|
|
|
|
|
- Implement `compose_transforms(T1, T2) -> np.ndarray`
|
|
|
|
|
- Implement `compute_reprojection_error(obj_pts, img_pts, rvec, tvec, K) -> float`
|
|
|
|
|
- Use numpy for all matrix operations
|
|
|
|
|
|
|
|
|
|
**Must NOT do**:
|
|
|
|
|
- Do NOT use scipy in this module (keep it pure numpy for core math)
|
|
|
|
|
- Do NOT implement averaging here (that's Task 5)
|
|
|
|
|
|
|
|
|
|
**Recommended Agent Profile**:
|
|
|
|
|
- **Category**: `quick`
|
|
|
|
|
- Reason: Pure math utilities, straightforward implementation
|
|
|
|
|
- **Skills**: []
|
|
|
|
|
- No special skills needed
|
|
|
|
|
|
|
|
|
|
**Parallelization**:
|
|
|
|
|
- **Can Run In Parallel**: YES
|
|
|
|
|
- **Parallel Group**: Wave 1 (with Tasks 2, 4)
|
|
|
|
|
- **Blocks**: Tasks 3, 5
|
|
|
|
|
- **Blocked By**: None
|
|
|
|
|
|
|
|
|
|
**References**:
|
|
|
|
|
- `py_workspace/aruco/find_extrinsic_object.py:123-145` - solvePnP usage and rvec/tvec handling
|
|
|
|
|
- OpenCV docs: `cv2.Rodrigues()` for rvec↔rotation matrix conversion
|
|
|
|
|
- OpenCV docs: `cv2.projectPoints()` for reprojection
|
|
|
|
|
|
|
|
|
|
**Acceptance Criteria**:
|
|
|
|
|
|
|
|
|
|
**Agent-Executed QA Scenarios:**
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Scenario: rvec/tvec round-trip conversion
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Steps:
|
|
|
|
|
1. python -c "from aruco.pose_math import *; import numpy as np; rvec=np.array([0.1,0.2,0.3]); tvec=np.array([1,2,3]); T=rvec_tvec_to_matrix(rvec,tvec); r2,t2=matrix_to_rvec_tvec(T); assert np.allclose(rvec,r2,atol=1e-6) and np.allclose(tvec,t2,atol=1e-6); print('PASS')"
|
|
|
|
|
Expected Result: Prints "PASS"
|
|
|
|
|
|
|
|
|
|
Scenario: Transform inversion identity
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Steps:
|
|
|
|
|
1. python -c "from aruco.pose_math import *; import numpy as np; T=np.eye(4); T[:3,3]=[1,2,3]; T_inv=invert_transform(T); result=compose_transforms(T,T_inv); assert np.allclose(result,np.eye(4),atol=1e-9); print('PASS')"
|
|
|
|
|
Expected Result: Prints "PASS"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Commit**: YES
|
|
|
|
|
- Message: `feat(aruco): add pose math utilities for transform operations`
|
|
|
|
|
- Files: `py_workspace/aruco/pose_math.py`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
- [ ] 2. Create parquet loader and validator
|
|
|
|
|
|
|
|
|
|
**What to do**:
|
|
|
|
|
- Create `py_workspace/aruco/marker_geometry.py`
|
|
|
|
|
- Implement `load_marker_geometry(parquet_path) -> dict[int, np.ndarray]`
|
|
|
|
|
- Returns mapping: marker_id → corner coordinates (4, 3)
|
|
|
|
|
- Implement `validate_marker_geometry(geometry) -> bool`
|
|
|
|
|
- Check all expected marker IDs present
|
|
|
|
|
- Check coordinates are in meters (reasonable range)
|
|
|
|
|
- Check corner ordering is consistent
|
|
|
|
|
- Use awkward-array (already in project) for parquet reading
|
|
|
|
|
|
|
|
|
|
**Must NOT do**:
|
|
|
|
|
- Do NOT hardcode marker IDs (read from parquet)
|
|
|
|
|
- Do NOT assume specific number of markers (validate dynamically)
|
|
|
|
|
|
|
|
|
|
**Recommended Agent Profile**:
|
|
|
|
|
- **Category**: `quick`
|
|
|
|
|
- Reason: Simple data loading and validation
|
|
|
|
|
- **Skills**: []
|
|
|
|
|
|
|
|
|
|
**Parallelization**:
|
|
|
|
|
- **Can Run In Parallel**: YES
|
|
|
|
|
- **Parallel Group**: Wave 1 (with Tasks 1, 4)
|
|
|
|
|
- **Blocks**: Task 3
|
|
|
|
|
- **Blocked By**: None
|
|
|
|
|
|
|
|
|
|
**References**:
|
|
|
|
|
- `py_workspace/aruco/find_extrinsic_object.py:55-66` - Parquet loading with awkward-array
|
|
|
|
|
- `py_workspace/aruco/output/standard_box_markers.parquet` - Actual data file
|
|
|
|
|
|
|
|
|
|
**Acceptance Criteria**:
|
|
|
|
|
|
|
|
|
|
**Agent-Executed QA Scenarios:**
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Scenario: Load marker geometry from parquet
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Preconditions: standard_box_markers.parquet exists
|
|
|
|
|
Steps:
|
|
|
|
|
1. cd /workspaces/zed-playground/py_workspace
|
|
|
|
|
2. python -c "from aruco.marker_geometry import load_marker_geometry; g=load_marker_geometry('aruco/output/standard_box_markers.parquet'); print(f'Loaded {len(g)} markers'); assert len(g) >= 4; print('PASS')"
|
|
|
|
|
Expected Result: Prints marker count and "PASS"
|
|
|
|
|
|
|
|
|
|
Scenario: Validate geometry returns True for valid data
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Steps:
|
|
|
|
|
1. python -c "from aruco.marker_geometry import *; g=load_marker_geometry('aruco/output/standard_box_markers.parquet'); assert validate_marker_geometry(g); print('PASS')"
|
|
|
|
|
Expected Result: Prints "PASS"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Commit**: YES
|
|
|
|
|
- Message: `feat(aruco): add marker geometry loader with validation`
|
|
|
|
|
- Files: `py_workspace/aruco/marker_geometry.py`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
- [ ] 3. Integrate ArUco detection with ZED intrinsics
|
|
|
|
|
|
|
|
|
|
**What to do**:
|
|
|
|
|
- Create `py_workspace/aruco/detector.py`
|
|
|
|
|
- Implement `create_detector() -> cv2.aruco.ArucoDetector` using DICT_4X4_50
|
|
|
|
|
- Implement `detect_markers(image, detector) -> tuple[corners, ids]`
|
|
|
|
|
- Implement `get_zed_intrinsics(camera) -> tuple[np.ndarray, np.ndarray]`
|
|
|
|
|
- Extract K matrix (3x3) and distortion from ZED SDK
|
|
|
|
|
- For rectified images, distortion should be zeros
|
|
|
|
|
- Implement `estimate_pose(corners, ids, marker_geometry, K, dist) -> tuple[rvec, tvec, error]`
|
|
|
|
|
- Match detected markers to known 3D points
|
|
|
|
|
- Call solvePnP with SOLVEPNP_SQPNP
|
|
|
|
|
- Compute and return reprojection error
|
|
|
|
|
- Require minimum 4 markers for valid pose
|
|
|
|
|
|
|
|
|
|
**Must NOT do**:
|
|
|
|
|
- Do NOT use deprecated `estimatePoseSingleMarkers`
|
|
|
|
|
- Do NOT accept poses with <4 markers
|
|
|
|
|
|
|
|
|
|
**Recommended Agent Profile**:
|
|
|
|
|
- **Category**: `unspecified-low`
|
|
|
|
|
- Reason: Integration of existing patterns, moderate complexity
|
|
|
|
|
- **Skills**: []
|
|
|
|
|
|
|
|
|
|
**Parallelization**:
|
|
|
|
|
- **Can Run In Parallel**: NO
|
|
|
|
|
- **Parallel Group**: Wave 2 (after Task 1, 2)
|
|
|
|
|
- **Blocks**: Tasks 6, 7
|
|
|
|
|
- **Blocked By**: Tasks 1, 2
|
|
|
|
|
|
|
|
|
|
**References**:
|
|
|
|
|
- `py_workspace/aruco/find_extrinsic_object.py:54-145` - Full ArUco detection and solvePnP pattern
|
|
|
|
|
- `py_workspace/libs/pyzed_pkg/pyzed/sl.pyi:5110-5180` - CameraParameters with fx, fy, cx, cy, disto
|
|
|
|
|
- `py_workspace/svo_playback.py:46` - get_camera_information() usage
|
|
|
|
|
|
|
|
|
|
**Acceptance Criteria**:
|
|
|
|
|
|
|
|
|
|
**Agent-Executed QA Scenarios:**
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Scenario: Detector creation succeeds
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Steps:
|
|
|
|
|
1. python -c "from aruco.detector import create_detector; d=create_detector(); print(type(d)); print('PASS')"
|
|
|
|
|
Expected Result: Prints detector type and "PASS"
|
|
|
|
|
|
|
|
|
|
Scenario: Pose estimation with synthetic data
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Steps:
|
|
|
|
|
1. python -c "
|
|
|
|
|
import numpy as np
|
|
|
|
|
from aruco.detector import estimate_pose
|
|
|
|
|
from aruco.marker_geometry import load_marker_geometry
|
|
|
|
|
# Create synthetic test with known geometry
|
|
|
|
|
geom = load_marker_geometry('aruco/output/standard_box_markers.parquet')
|
|
|
|
|
K = np.array([[700,0,960],[0,700,540],[0,0,1]], dtype=np.float64)
|
|
|
|
|
# Test passes if function runs without error
|
|
|
|
|
print('PASS')
|
|
|
|
|
"
|
|
|
|
|
Expected Result: Prints "PASS"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Commit**: YES
|
|
|
|
|
- Message: `feat(aruco): add ArUco detector with ZED intrinsics integration`
|
|
|
|
|
- Files: `py_workspace/aruco/detector.py`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
- [ ] 4. Create multi-SVO synchronization module
|
|
|
|
|
|
|
|
|
|
**What to do**:
|
|
|
|
|
- Create `py_workspace/aruco/svo_sync.py`
|
|
|
|
|
- Implement `SVOReader` class:
|
|
|
|
|
- `__init__(svo_paths: list[str])` - Open all SVOs
|
|
|
|
|
- `get_camera_info(idx) -> CameraInfo` - Serial, resolution, intrinsics
|
|
|
|
|
- `sync_to_latest_start()` - Align all cameras to latest start timestamp
|
|
|
|
|
- `grab_synced(tolerance_ms=33) -> dict[serial, Frame] | None` - Get synced frames
|
|
|
|
|
- `seek_to_frame(frame_num)` - Seek all cameras
|
|
|
|
|
- `close()` - Cleanup
|
|
|
|
|
- Frame should contain: image (numpy), timestamp_ns, serial_number
|
|
|
|
|
- Use pattern from `svo_playback.py` for sync logic
|
|
|
|
|
|
|
|
|
|
**Must NOT do**:
|
|
|
|
|
- Do NOT implement complex clock drift correction
|
|
|
|
|
- Do NOT handle streaming (SVO only)
|
|
|
|
|
|
|
|
|
|
**Recommended Agent Profile**:
|
|
|
|
|
- **Category**: `unspecified-low`
|
|
|
|
|
- Reason: Adapting existing pattern, moderate complexity
|
|
|
|
|
- **Skills**: []
|
|
|
|
|
|
|
|
|
|
**Parallelization**:
|
|
|
|
|
- **Can Run In Parallel**: YES
|
|
|
|
|
- **Parallel Group**: Wave 1 (with Tasks 1, 2)
|
|
|
|
|
- **Blocks**: Task 7
|
|
|
|
|
- **Blocked By**: None
|
|
|
|
|
|
|
|
|
|
**References**:
|
|
|
|
|
- `py_workspace/svo_playback.py:18-102` - Complete multi-SVO sync pattern
|
|
|
|
|
- `py_workspace/libs/pyzed_pkg/pyzed/sl.pyi:10010-10097` - SVO position and frame methods
|
|
|
|
|
|
|
|
|
|
**Acceptance Criteria**:
|
|
|
|
|
|
|
|
|
|
**Agent-Executed QA Scenarios:**
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Scenario: SVOReader opens multiple files
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Preconditions: SVO files exist in py_workspace
|
|
|
|
|
Steps:
|
|
|
|
|
1. python -c "
|
|
|
|
|
from aruco.svo_sync import SVOReader
|
|
|
|
|
import glob
|
|
|
|
|
svos = glob.glob('*.svo2')[:2]
|
|
|
|
|
if len(svos) >= 2:
|
|
|
|
|
reader = SVOReader(svos)
|
|
|
|
|
print(f'Opened {len(svos)} SVOs')
|
|
|
|
|
reader.close()
|
|
|
|
|
print('PASS')
|
|
|
|
|
else:
|
|
|
|
|
print('SKIP: Need 2+ SVOs')
|
|
|
|
|
"
|
|
|
|
|
Expected Result: Prints "PASS" or "SKIP"
|
|
|
|
|
|
|
|
|
|
Scenario: Sync aligns timestamps
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Steps:
|
|
|
|
|
1. Test sync_to_latest_start returns without error
|
|
|
|
|
Expected Result: No exception raised
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Commit**: YES
|
|
|
|
|
- Message: `feat(aruco): add multi-SVO synchronization reader`
|
|
|
|
|
- Files: `py_workspace/aruco/svo_sync.py`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
- [ ] 5. Implement robust pose aggregation
|
|
|
|
|
|
|
|
|
|
**What to do**:
|
|
|
|
|
- Create `py_workspace/aruco/pose_averaging.py`
|
|
|
|
|
- Implement `PoseAccumulator` class:
|
|
|
|
|
- `add_pose(T: np.ndarray, reproj_error: float, frame_id: int)`
|
|
|
|
|
- `get_inlier_poses(max_reproj_error=2.0) -> list[np.ndarray]`
|
|
|
|
|
- `compute_robust_mean() -> tuple[np.ndarray, dict]`
|
|
|
|
|
- Use scipy.spatial.transform.Rotation.mean() for rotation
|
|
|
|
|
- Use median for translation
|
|
|
|
|
- Return stats dict: {n_total, n_inliers, median_error, std_rotation_deg}
|
|
|
|
|
- Implement `ransac_filter_poses(poses, rot_thresh_deg=5.0, trans_thresh_m=0.05) -> list[int]`
|
|
|
|
|
- Return indices of inlier poses
|
|
|
|
|
|
|
|
|
|
**Must NOT do**:
|
|
|
|
|
- Do NOT implement bundle adjustment
|
|
|
|
|
- Do NOT modify poses in-place
|
|
|
|
|
|
|
|
|
|
**Recommended Agent Profile**:
|
|
|
|
|
- **Category**: `unspecified-low`
|
|
|
|
|
- Reason: Math-focused but requires scipy understanding
|
|
|
|
|
- **Skills**: []
|
|
|
|
|
|
|
|
|
|
**Parallelization**:
|
|
|
|
|
- **Can Run In Parallel**: YES
|
|
|
|
|
- **Parallel Group**: Wave 2 (with Task 3)
|
|
|
|
|
- **Blocks**: Task 7
|
|
|
|
|
- **Blocked By**: Task 1
|
|
|
|
|
|
|
|
|
|
**References**:
|
|
|
|
|
- Librarian findings on `scipy.spatial.transform.Rotation.mean()`
|
|
|
|
|
- Librarian findings on RANSAC-style pose filtering
|
|
|
|
|
|
|
|
|
|
**Acceptance Criteria**:
|
|
|
|
|
|
|
|
|
|
**Agent-Executed QA Scenarios:**
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Scenario: Rotation averaging produces valid result
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Steps:
|
|
|
|
|
1. python -c "
|
|
|
|
|
from aruco.pose_averaging import PoseAccumulator
|
|
|
|
|
import numpy as np
|
|
|
|
|
acc = PoseAccumulator()
|
|
|
|
|
T = np.eye(4)
|
|
|
|
|
acc.add_pose(T, reproj_error=1.0, frame_id=0)
|
|
|
|
|
acc.add_pose(T, reproj_error=1.5, frame_id=1)
|
|
|
|
|
mean_T, stats = acc.compute_robust_mean()
|
|
|
|
|
assert mean_T.shape == (4,4)
|
|
|
|
|
assert stats['n_inliers'] == 2
|
|
|
|
|
print('PASS')
|
|
|
|
|
"
|
|
|
|
|
Expected Result: Prints "PASS"
|
|
|
|
|
|
|
|
|
|
Scenario: RANSAC rejects outliers
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Steps:
|
|
|
|
|
1. python -c "
|
|
|
|
|
from aruco.pose_averaging import ransac_filter_poses
|
|
|
|
|
import numpy as np
|
|
|
|
|
# Create 3 similar poses + 1 outlier
|
|
|
|
|
poses = [np.eye(4) for _ in range(3)]
|
|
|
|
|
outlier = np.eye(4); outlier[:3,3] = [10,10,10] # Far away
|
|
|
|
|
poses.append(outlier)
|
|
|
|
|
inliers = ransac_filter_poses(poses, trans_thresh_m=0.1)
|
|
|
|
|
assert len(inliers) == 3
|
|
|
|
|
assert 3 not in inliers
|
|
|
|
|
print('PASS')
|
|
|
|
|
"
|
|
|
|
|
Expected Result: Prints "PASS"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Commit**: YES
|
|
|
|
|
- Message: `feat(aruco): add robust pose averaging with RANSAC filtering`
|
|
|
|
|
- Files: `py_workspace/aruco/pose_averaging.py`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
- [ ] 6. Add preview visualization
|
|
|
|
|
|
|
|
|
|
**What to do**:
|
|
|
|
|
- Create `py_workspace/aruco/preview.py`
|
|
|
|
|
- Implement `draw_detected_markers(image, corners, ids) -> np.ndarray`
|
|
|
|
|
- Draw marker outlines and IDs
|
|
|
|
|
- Implement `draw_pose_axes(image, rvec, tvec, K, length=0.1) -> np.ndarray`
|
|
|
|
|
- Use cv2.drawFrameAxes
|
|
|
|
|
- Implement `show_preview(images: dict[str, np.ndarray], wait_ms=1) -> int`
|
|
|
|
|
- Show multiple camera views in separate windows
|
|
|
|
|
- Return key pressed
|
|
|
|
|
|
|
|
|
|
**Must NOT do**:
|
|
|
|
|
- Do NOT implement complex GUI
|
|
|
|
|
- Do NOT block indefinitely (use waitKey with timeout)
|
|
|
|
|
|
|
|
|
|
**Recommended Agent Profile**:
|
|
|
|
|
- **Category**: `quick`
|
|
|
|
|
- Reason: Simple OpenCV visualization
|
|
|
|
|
- **Skills**: []
|
|
|
|
|
|
|
|
|
|
**Parallelization**:
|
|
|
|
|
- **Can Run In Parallel**: YES
|
|
|
|
|
- **Parallel Group**: Wave 2 (with Task 5)
|
|
|
|
|
- **Blocks**: Task 7
|
|
|
|
|
- **Blocked By**: Task 3
|
|
|
|
|
|
|
|
|
|
**References**:
|
|
|
|
|
- `py_workspace/aruco/find_extrinsic_object.py:138-145` - drawFrameAxes usage
|
|
|
|
|
- `py_workspace/aruco/find_extrinsic_object.py:84-105` - Marker visualization
|
|
|
|
|
|
|
|
|
|
**Acceptance Criteria**:
|
|
|
|
|
|
|
|
|
|
**Agent-Executed QA Scenarios:**
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Scenario: Draw functions return valid images
|
|
|
|
|
Tool: Bash (python)
|
|
|
|
|
Steps:
|
|
|
|
|
1. python -c "
|
|
|
|
|
from aruco.preview import draw_detected_markers
|
|
|
|
|
import numpy as np
|
|
|
|
|
img = np.zeros((480,640,3), dtype=np.uint8)
|
|
|
|
|
corners = [np.array([[100,100],[200,100],[200,200],[100,200]], dtype=np.float32)]
|
|
|
|
|
ids = np.array([[1]])
|
|
|
|
|
result = draw_detected_markers(img, corners, ids)
|
|
|
|
|
assert result.shape == (480,640,3)
|
|
|
|
|
print('PASS')
|
|
|
|
|
"
|
|
|
|
|
Expected Result: Prints "PASS"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Commit**: YES
|
|
|
|
|
- Message: `feat(aruco): add preview visualization utilities`
|
|
|
|
|
- Files: `py_workspace/aruco/preview.py`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
- [ ] 7. Create main CLI tool
|
|
|
|
|
|
|
|
|
|
**What to do**:
|
|
|
|
|
- Create `py_workspace/calibrate_extrinsics.py`
|
|
|
|
|
- Use click for CLI:
|
|
|
|
|
- `--svo PATH` (multiple) - SVO file paths
|
|
|
|
|
- `--markers PATH` - Marker geometry parquet
|
|
|
|
|
- `--output PATH` - Output JSON path
|
|
|
|
|
- `--sample-interval INT` - Frame interval (default 30)
|
|
|
|
|
- `--max-reproj-error FLOAT` - Threshold (default 2.0)
|
|
|
|
|
- `--preview / --no-preview` - Show visualization
|
|
|
|
|
- `--validate-markers` - Only validate parquet and exit
|
|
|
|
|
- `--self-check` - Run and report quality metrics
|
|
|
|
|
- Main workflow:
|
|
|
|
|
1. Load marker geometry and validate
|
|
|
|
|
2. Open SVOs and sync
|
|
|
|
|
3. Sample frames at interval
|
|
|
|
|
4. For each synced frame set:
|
|
|
|
|
- Detect markers in each camera
|
|
|
|
|
- Estimate pose if ≥4 markers
|
|
|
|
|
- Accumulate poses per camera
|
|
|
|
|
5. Compute robust mean per camera
|
|
|
|
|
6. Output JSON in inside_network.json-compatible format
|
|
|
|
|
- Output JSON format:
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"serial": {
|
|
|
|
|
"pose": "r00 r01 r02 tx r10 r11 r12 ty ...",
|
|
|
|
|
"stats": { "n_frames": N, "median_reproj_error": X }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Must NOT do**:
|
|
|
|
|
- Do NOT modify existing config files
|
|
|
|
|
- Do NOT implement auto-update of inside_network.json
|
|
|
|
|
|
|
|
|
|
**Recommended Agent Profile**:
|
|
|
|
|
- **Category**: `unspecified-high`
|
|
|
|
|
- Reason: Integration of all components, complex workflow
|
|
|
|
|
- **Skills**: []
|
|
|
|
|
|
|
|
|
|
**Parallelization**:
|
|
|
|
|
- **Can Run In Parallel**: NO
|
|
|
|
|
- **Parallel Group**: Wave 3 (final integration)
|
|
|
|
|
- **Blocks**: Task 8
|
|
|
|
|
- **Blocked By**: Tasks 3, 4, 5, 6
|
|
|
|
|
|
|
|
|
|
**References**:
|
|
|
|
|
- `py_workspace/svo_playback.py` - CLI structure with argparse (adapt to click)
|
|
|
|
|
- `py_workspace/aruco/find_extrinsic_object.py` - Main loop pattern
|
|
|
|
|
- `zed_settings/inside_network.json:20` - Output pose format
|
|
|
|
|
|
|
|
|
|
**Acceptance Criteria**:
|
|
|
|
|
|
|
|
|
|
**Agent-Executed QA Scenarios:**
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Scenario: CLI help works
|
|
|
|
|
Tool: Bash
|
|
|
|
|
Steps:
|
|
|
|
|
1. cd /workspaces/zed-playground/py_workspace
|
|
|
|
|
2. uv run calibrate_extrinsics.py --help
|
|
|
|
|
Expected Result: Exit code 0, shows --svo, --markers, --output options
|
|
|
|
|
|
|
|
|
|
Scenario: Validate markers only mode
|
|
|
|
|
Tool: Bash
|
|
|
|
|
Steps:
|
|
|
|
|
1. uv run calibrate_extrinsics.py --markers aruco/output/standard_box_markers.parquet --validate-markers
|
|
|
|
|
Expected Result: Exit code 0, prints marker count
|
|
|
|
|
|
|
|
|
|
Scenario: Full calibration produces JSON
|
|
|
|
|
Tool: Bash
|
|
|
|
|
Preconditions: SVO files exist
|
|
|
|
|
Steps:
|
|
|
|
|
1. uv run calibrate_extrinsics.py \
|
|
|
|
|
--svo ZED_SN46195029.svo2 \
|
|
|
|
|
--svo ZED_SN44435674.svo2 \
|
|
|
|
|
--markers aruco/output/standard_box_markers.parquet \
|
|
|
|
|
--output /tmp/test_extrinsics.json \
|
|
|
|
|
--no-preview \
|
|
|
|
|
--sample-interval 100
|
|
|
|
|
2. jq 'keys' /tmp/test_extrinsics.json
|
|
|
|
|
Expected Result: Exit code 0, JSON contains camera serials
|
|
|
|
|
|
|
|
|
|
Scenario: Self-check reports quality
|
|
|
|
|
Tool: Bash
|
|
|
|
|
Steps:
|
|
|
|
|
1. uv run calibrate_extrinsics.py ... --self-check
|
|
|
|
|
Expected Result: Prints per-camera stats including median reproj error
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Commit**: YES
|
|
|
|
|
- Message: `feat(aruco): add calibrate_extrinsics CLI tool`
|
|
|
|
|
- Files: `py_workspace/calibrate_extrinsics.py`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
- [ ] 8. Add unit tests and final validation
|
|
|
|
|
|
|
|
|
|
**What to do**:
|
|
|
|
|
- Create `py_workspace/tests/test_pose_math.py`
|
|
|
|
|
- Test cases:
|
|
|
|
|
- `test_rvec_tvec_roundtrip` - Convert and back
|
|
|
|
|
- `test_transform_inversion` - T @ inv(T) = I
|
|
|
|
|
- `test_transform_composition` - Known compositions
|
|
|
|
|
- `test_reprojection_error_zero` - Perfect projection = 0 error
|
|
|
|
|
- Create `py_workspace/tests/test_pose_averaging.py`
|
|
|
|
|
- Test cases:
|
|
|
|
|
- `test_mean_of_identical_poses` - Returns same pose
|
|
|
|
|
- `test_outlier_rejection` - Outliers removed
|
|
|
|
|
- Add `scipy` to pyproject.toml if not present
|
|
|
|
|
- Run full test suite
|
|
|
|
|
|
|
|
|
|
**Must NOT do**:
|
|
|
|
|
- Do NOT require real SVO files for unit tests (use synthetic data)
|
|
|
|
|
|
|
|
|
|
**Recommended Agent Profile**:
|
|
|
|
|
- **Category**: `quick`
|
|
|
|
|
- Reason: Straightforward test implementation
|
|
|
|
|
- **Skills**: []
|
|
|
|
|
|
|
|
|
|
**Parallelization**:
|
|
|
|
|
- **Can Run In Parallel**: NO
|
|
|
|
|
- **Parallel Group**: Wave 3 (final)
|
|
|
|
|
- **Blocks**: None
|
|
|
|
|
- **Blocked By**: Task 7
|
|
|
|
|
|
|
|
|
|
**References**:
|
|
|
|
|
- Task 1 acceptance criteria for test patterns
|
|
|
|
|
- Task 5 acceptance criteria for averaging tests
|
|
|
|
|
|
|
|
|
|
**Acceptance Criteria**:
|
|
|
|
|
|
|
|
|
|
**Agent-Executed QA Scenarios:**
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Scenario: All unit tests pass
|
|
|
|
|
Tool: Bash
|
|
|
|
|
Steps:
|
|
|
|
|
1. cd /workspaces/zed-playground/py_workspace
|
|
|
|
|
2. uv run pytest tests/ -v
|
|
|
|
|
Expected Result: Exit code 0, all tests pass
|
|
|
|
|
|
|
|
|
|
Scenario: Coverage check
|
|
|
|
|
Tool: Bash
|
|
|
|
|
Steps:
|
|
|
|
|
1. uv run pytest tests/ --tb=short
|
|
|
|
|
Expected Result: Shows test results summary
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Commit**: YES
|
|
|
|
|
- Message: `test(aruco): add unit tests for pose math and averaging`
|
|
|
|
|
- Files: `py_workspace/tests/test_pose_math.py`, `py_workspace/tests/test_pose_averaging.py`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Commit Strategy
|
|
|
|
|
|
|
|
|
|
| After Task | Message | Files | Verification |
|
|
|
|
|
|------------|---------|-------|--------------|
|
|
|
|
|
| 1 | `feat(aruco): add pose math utilities` | pose_math.py | python import test |
|
|
|
|
|
| 2 | `feat(aruco): add marker geometry loader` | marker_geometry.py | python import test |
|
|
|
|
|
| 3 | `feat(aruco): add ArUco detector` | detector.py | python import test |
|
|
|
|
|
| 4 | `feat(aruco): add multi-SVO sync` | svo_sync.py | python import test |
|
|
|
|
|
| 5 | `feat(aruco): add pose averaging` | pose_averaging.py | python import test |
|
|
|
|
|
| 6 | `feat(aruco): add preview utils` | preview.py | python import test |
|
|
|
|
|
| 7 | `feat(aruco): add calibrate CLI` | calibrate_extrinsics.py | --help works |
|
|
|
|
|
| 8 | `test(aruco): add unit tests` | tests/*.py | pytest passes |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Success Criteria
|
|
|
|
|
|
|
|
|
|
### Verification Commands
|
|
|
|
|
```bash
|
|
|
|
|
# CLI works
|
|
|
|
|
uv run calibrate_extrinsics.py --help # Expected: exit 0
|
|
|
|
|
|
|
|
|
|
# Marker validation
|
|
|
|
|
uv run calibrate_extrinsics.py --markers aruco/output/standard_box_markers.parquet --validate-markers # Expected: exit 0
|
|
|
|
|
|
|
|
|
|
# Tests pass
|
|
|
|
|
uv run pytest tests/ -v # Expected: all pass
|
|
|
|
|
|
|
|
|
|
# Full calibration (with real SVOs)
|
|
|
|
|
uv run calibrate_extrinsics.py --svo *.svo2 --markers aruco/output/standard_box_markers.parquet --output calibrated.json --no-preview
|
|
|
|
|
jq 'keys' calibrated.json # Expected: camera serials
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Final Checklist
|
|
|
|
|
- [ ] All "Must Have" present
|
|
|
|
|
- [ ] All "Must NOT Have" absent
|
|
|
|
|
- [ ] All tests pass
|
|
|
|
|
- [ ] CLI --help shows all options
|
|
|
|
|
- [ ] Output JSON matches inside_network.json pose format
|
|
|
|
|
- [ ] Preview shows detected markers with axes
|