Files
crosstyan fbfed24fc3 docs(plan): mark aruco-svo-calibration plan as complete
All 8 tasks implemented:
- Task 1: pose_math.py - Transform utilities
- Task 2: marker_geometry.py - Parquet loader
- Task 3: detector.py - ArUco detection
- Task 4: svo_sync.py - Multi-SVO sync
- Task 5: pose_averaging.py - Robust averaging
- Task 6: preview.py - Visualization
- Task 7: calibrate_extrinsics.py - Main CLI
- Task 8: Unit tests (all passing)

All Definition of Done criteria met:
- CLI help works
- Marker validation works
- Tests pass (10/10)
- Preview mode functional
2026-02-05 03:37:27 +00:00

746 lines
25 KiB
Markdown

# 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
- [x] `uv run calibrate_extrinsics.py --help` → exits 0, shows required args
- [x] `uv run calibrate_extrinsics.py --validate-markers` → validates parquet schema
- [x] `uv run calibrate_extrinsics.py --svos ... --output out.json` → produces valid JSON
- [x] Output JSON contains 4 cameras with 4x4 matrices in correct format
- [x] `uv run pytest tests/test_pose_math.py` → all tests pass
- [x] 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
- [x] 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`
---
- [x] 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`
---
- [x] 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`
---
- [x] 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`
---
- [x] 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`
---
- [x] 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`
---
- [x] 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`
---
- [x] 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
- [x] All "Must Have" present
- [x] All "Must NOT Have" absent
- [x] All tests pass
- [x] CLI --help shows all options
- [x] Output JSON matches inside_network.json pose format
- [x] Preview shows detected markers with axes