890 lines
36 KiB
Markdown
890 lines
36 KiB
Markdown
# Full-Scene ICP Pipeline Upgrade
|
||
|
||
## TL;DR
|
||
|
||
> **Quick Summary**: Upgrade the current floor-band-only ICP registration to support hybrid (floor + vertical structure) and full-scene modes, with optional FPFH+RANSAC global pre-alignment, fixed success gates, robust kernels, and per-pair diagnostic logging.
|
||
>
|
||
> **Deliverables**:
|
||
> - `--icp-region floor|hybrid|full` CLI flag (default: hybrid)
|
||
> - `--icp-global-init` optional FPFH+RANSAC pre-alignment
|
||
> - Fixed success gate (`>0` instead of `>1`)
|
||
> - Per-pair diagnostic logging (all pairs, not just converged)
|
||
> - Statistical outlier removal preprocessing
|
||
> - TukeyLoss robust kernel support
|
||
> - 3D AABB overlap check for hybrid/full modes
|
||
> - Exposed CLI flags: `--icp-min-overlap`, `--icp-band-height`
|
||
> - Relaxed defaults: gravity penalty, correspondence distance factor, min_overlap_area
|
||
> - Comprehensive tests covering all new modes + edge cases
|
||
>
|
||
> **Estimated Effort**: Medium (3–5 days)
|
||
> **Parallel Execution**: YES - 3 waves
|
||
> **Critical Path**: Task 1 → Task 2 → Task 3 → Task 5 → Task 7 → Task 9
|
||
|
||
---
|
||
|
||
## Context
|
||
|
||
### Original Request
|
||
User observed that camera SN44289123 appears floor-disconnected by ~3–5 cm after ground-plane refinement. Diagnostic sweeps showed ICP consistently failing: only 1 of 6 pairs converges, graph stays disconnected, and the success gate (`>1`) rejects valid single-camera corrections. Oracle analysis identified floor-band degeneracy (planar sliding), tight gating, and over-aggressive gravity penalty as root causes. User requested upgrading from floor-only to full-scene/hybrid ICP.
|
||
|
||
### Interview Summary
|
||
**Key Discussions**:
|
||
- User chose **hybrid** as default ICP region mode (floor + vertical structure)
|
||
- User chose **FPFH+RANSAC** as optional global pre-alignment (`--icp-global-init`)
|
||
- User chose to **include success gate fix** in this plan (not a separate patch)
|
||
- Oracle recommended: lower gravity penalty (10→2), widen correspondence factor (1.4→2.5), lower min_overlap_area (1.0→0.5), add per-pair logging, add robust kernels
|
||
|
||
**Research Findings**:
|
||
- Open3D multiway registration: pairwise ICP → pose graph → global optimization (LM)
|
||
- FPFH features + RANSAC for global pre-alignment when init is poor
|
||
- Statistical outlier removal (nb_neighbors=20, std_ratio=2.0) standard for ZED
|
||
- TukeyLoss(k=0.05–0.1) effective for depth sensor noise at 3–5 m range
|
||
- Hybrid floor+structure recommended over pure floor (adds XZ constraints)
|
||
- Voxel size: 0.05 m for global/coarse, 0.02 m for fine refinement
|
||
|
||
### Metis Review
|
||
**Identified Gaps** (addressed):
|
||
- Backward compatibility: `--icp-region floor` preserves legacy behavior exactly
|
||
- Hybrid degeneracy: if no vertical structure found, gracefully falls back to floor-only with warning
|
||
- FPFH failure fallback: when RANSAC fails, continues with extrinsic-based init (no crash)
|
||
- Determinism: RANSAC seeds controlled for reproducibility; tests use synthetic geometry
|
||
- Ceiling/overhang dominance in full mode: gravity constraint prevents upside-down alignment
|
||
- Symmetric environments: transform sanity bounds catch FPFH mis-registration
|
||
|
||
---
|
||
|
||
## Work Objectives
|
||
|
||
### Core Objective
|
||
Replace the floor-band-only ICP pipeline with a configurable region selection system (floor/hybrid/full) that provides stronger geometric constraints, better pair convergence, and more reliable multi-camera correction.
|
||
|
||
### Concrete Deliverables
|
||
- Modified `aruco/icp_registration.py`: new extraction functions, 3D overlap, robust kernel, relaxed defaults, diagnostic logging
|
||
- Modified `refine_ground_plane.py`: new CLI flags for region, global-init, overlap, band-height
|
||
- New/extended `tests/test_icp_registration.py`: ~15 new test cases
|
||
- Updated `README.md`: new flags documented
|
||
|
||
### Definition of Done
|
||
- [ ] `uv run refine_ground_plane.py --help` shows `--icp-region`, `--icp-global-init`, `--icp-min-overlap`, `--icp-band-height`
|
||
- [ ] `uv run pytest -x -vv` → all tests pass (existing + new)
|
||
- [ ] `uv run basedpyright aruco/icp_registration.py refine_ground_plane.py` → 0 errors
|
||
- [ ] `--icp-region floor` produces identical output to current behavior (regression)
|
||
- [ ] `--icp-region hybrid` produces ≥ as many converged pairs as floor on test data
|
||
|
||
### Must Have
|
||
- Region selection: `floor`, `hybrid`, `full` modes
|
||
- Backward compatibility: `floor` mode matches current behavior
|
||
- Success gate: `num_cameras_optimized > 0` (not `> 1`)
|
||
- Per-pair diagnostic logging at INFO level
|
||
- Robust kernel (TukeyLoss) support
|
||
- Statistical outlier removal preprocessing
|
||
- 3D AABB overlap check for hybrid/full
|
||
- Optional FPFH+RANSAC global init with clean fallback
|
||
|
||
### Must NOT Have (Guardrails)
|
||
- No changes to HDF5 schema or `aruco/depth_save.py`
|
||
- No additional global registration methods beyond FPFH+RANSAC
|
||
- No auto-parameter search or adaptive voxel size loops
|
||
- No multiple outlier strategies (SOR only, no radius outlier removal)
|
||
- No multiple robust kernels (TukeyLoss only, no Huber/Cauchy menus)
|
||
- No refactoring of pose graph optimization or information matrix computation
|
||
- No new persistent output files beyond normal console logging
|
||
- No changes to `aruco/ground_plane.py` core functions (only consume them)
|
||
|
||
---
|
||
|
||
## Verification Strategy
|
||
|
||
> **UNIVERSAL RULE: ZERO HUMAN INTERVENTION**
|
||
>
|
||
> ALL tasks in this plan MUST be verifiable WITHOUT any human action.
|
||
|
||
### Test Decision
|
||
- **Infrastructure exists**: YES
|
||
- **Automated tests**: YES (Tests-after for new functionality)
|
||
- **Framework**: pytest
|
||
|
||
### Agent-Executed QA Scenarios (MANDATORY — ALL tasks)
|
||
|
||
**Verification Tool by Deliverable Type:**
|
||
|
||
| Type | Tool | How Agent Verifies |
|
||
|------|------|-------------------|
|
||
| **CLI flags** | Bash | `uv run refine_ground_plane.py --help` and grep for flags |
|
||
| **Library/Module** | Bash (pytest) | `uv run pytest tests/test_icp_registration.py -v` |
|
||
| **Type safety** | Bash (basedpyright) | `uv run basedpyright aruco/icp_registration.py` |
|
||
| **Regression** | Bash (pipeline run) | Compare outputs with floor-only baseline |
|
||
|
||
---
|
||
|
||
## Execution Strategy
|
||
|
||
### Parallel Execution Waves
|
||
|
||
```
|
||
Wave 1 (Start Immediately):
|
||
├── Task 1: Fix success gate + per-pair diagnostic logging
|
||
├── Task 2: Add point extraction functions (hybrid/full/SOR)
|
||
└── Task 3: Add 3D AABB overlap check
|
||
|
||
Wave 2 (After Wave 1):
|
||
├── Task 4: Add TukeyLoss robust kernel support
|
||
├── Task 5: Integrate region selection into refine_with_icp
|
||
└── Task 6: Add FPFH+RANSAC global pre-alignment
|
||
|
||
Wave 3 (After Wave 2):
|
||
├── Task 7: Wire CLI flags in refine_ground_plane.py
|
||
├── Task 8: Relax ICPConfig defaults
|
||
└── Task 9: Tests + README + regression verification
|
||
|
||
Critical Path: Task 1 → Task 2 → Task 5 → Task 7 → Task 9
|
||
Parallel Speedup: ~40% faster than sequential
|
||
```
|
||
|
||
### Dependency Matrix
|
||
|
||
| Task | Depends On | Blocks | Can Parallelize With |
|
||
|------|------------|--------|---------------------|
|
||
| 1 | None | 5, 9 | 2, 3 |
|
||
| 2 | None | 5 | 1, 3 |
|
||
| 3 | None | 5 | 1, 2 |
|
||
| 4 | None | 5 | 1, 2, 3 |
|
||
| 5 | 1, 2, 3, 4 | 7 | 6 |
|
||
| 6 | None | 7 | 4, 5 |
|
||
| 7 | 5, 6 | 9 | 8 |
|
||
| 8 | None | 9 | 7 |
|
||
| 9 | 7, 8 | None | None (final) |
|
||
|
||
### Agent Dispatch Summary
|
||
|
||
| Wave | Tasks | Recommended Agents |
|
||
|------|-------|-------------------|
|
||
| 1 | 1, 2, 3 | task(category="quick") for Task 1; task(category="unspecified-high") for Tasks 2, 3 |
|
||
| 2 | 4, 5, 6 | task(category="unspecified-high") for all three |
|
||
| 3 | 7, 8, 9 | task(category="unspecified-high") for Task 7, task(category="quick") for Task 8, task(category="unspecified-high") for Task 9 |
|
||
|
||
---
|
||
|
||
## TODOs
|
||
|
||
- [x] 1. Fix success gate + add per-pair diagnostic logging
|
||
|
||
**What to do**:
|
||
- Change `metrics.success = metrics.num_cameras_optimized > 1` to `metrics.success = metrics.num_cameras_optimized > 0` (line 475)
|
||
- Log ALL pair outcomes (not just converged) at INFO level: fitness, RMSE, converged status
|
||
- Always record to `metrics.per_pair_results` (currently only converged pairs are stored, line 417)
|
||
- Add `logger.info(f"Pair ({s1},{s2}): fitness={result.fitness:.3f}, rmse={result.inlier_rmse:.4f}, converged={result.converged}")` after each pairwise ICP
|
||
- Add `logger.debug(f"Pair ({s1},{s2}) overlap {area:.2f} m² < {config.min_overlap_area}. Skipping.")` before overlap skip
|
||
|
||
**Must NOT do**:
|
||
- Do not change pairwise_icp algorithm or pose graph logic
|
||
- Do not modify ICPConfig defaults (separate task)
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- Reason: Small, focused change — two lines of logic + logging additions
|
||
- **Skills**: []
|
||
- **Skills Evaluated but Omitted**:
|
||
- `git-master`: No git operations needed during implementation
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 1 (with Tasks 2, 3)
|
||
- **Blocks**: Tasks 5, 9
|
||
- **Blocked By**: None
|
||
|
||
**References**:
|
||
|
||
**Pattern References**:
|
||
- `aruco/icp_registration.py:414-421` — Current convergence check and pair_results storage (change to always store)
|
||
- `aruco/icp_registration.py:475` — Success gate line to modify
|
||
- `aruco/icp_registration.py:382-386` — Overlap skip without logging (add debug log)
|
||
|
||
**API/Type References**:
|
||
- `aruco/icp_registration.py:48-59` — ICPMetrics dataclass (per_pair_results field)
|
||
- `aruco/icp_registration.py:37-45` — ICPResult dataclass (fitness, inlier_rmse, converged fields)
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Line 475: the exact success gate that Oracle identified as too strict
|
||
- Lines 414-421: where pair results are conditionally stored; need to always store for diagnostics
|
||
- Lines 382-386: overlap rejection currently silent; need logging for operator debugging
|
||
|
||
**Acceptance Criteria**:
|
||
|
||
- [ ] `metrics.success` is `True` when `num_cameras_optimized == 1`
|
||
- [ ] All pair results appear in `metrics.per_pair_results` regardless of convergence
|
||
- [ ] Log output at INFO shows fitness/RMSE for every attempted pair
|
||
- [ ] `uv run pytest tests/test_icp_registration.py -v` → all existing tests pass
|
||
|
||
**Agent-Executed QA Scenarios:**
|
||
|
||
```
|
||
Scenario: Success gate accepts single-camera optimization
|
||
Tool: Bash (pytest)
|
||
Preconditions: test_icp_registration.py has test_success_gate_single_camera
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "success_gate" -v
|
||
2. Assert: exit code 0
|
||
3. Assert: output contains "PASSED"
|
||
Expected Result: Test passes confirming success=True when 1 camera optimized
|
||
Evidence: pytest output captured
|
||
|
||
Scenario: Per-pair logging visible in debug output
|
||
Tool: Bash (pipeline run)
|
||
Preconditions: output/e2e_refine_depth.json and .h5 exist
|
||
Steps:
|
||
1. uv run refine_ground_plane.py --input-extrinsics output/e2e_refine_depth.json --input-depth output/e2e_refine_depth.h5 --output-extrinsics /tmp/test_logging.json --icp --icp-method gicp --icp-voxel-size 0.04 --seed 42 --debug
|
||
2. Assert: stdout contains "fitness=" for multiple pairs
|
||
3. Assert: stdout contains "overlap" for skipped pairs
|
||
Expected Result: All 6 pairs produce diagnostic output
|
||
Evidence: Terminal output captured
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `fix(icp): relax success gate to >0 and add per-pair diagnostic logging`
|
||
- Files: `aruco/icp_registration.py`
|
||
- Pre-commit: `uv run pytest tests/test_icp_registration.py -q`
|
||
|
||
---
|
||
|
||
- [ ] 2. Add point extraction functions (hybrid/full/SOR)
|
||
|
||
**What to do**:
|
||
- Add `extract_scene_points(points_world, floor_y, floor_normal, mode, band_height)` function to `aruco/icp_registration.py`
|
||
- `mode="floor"`: delegate to existing `extract_near_floor_band` (backward compat)
|
||
- `mode="hybrid"`: floor band UNION with points whose surface normals are near-horizontal (walls/vertical structure). Use Open3D normal estimation on the full cloud, then filter by `abs(normal · floor_normal) < 0.3` for "vertical" points. Combine with floor band.
|
||
- `mode="full"`: return all points after SOR filtering
|
||
- Add `preprocess_point_cloud(pcd, voxel_size)` function:
|
||
- Statistical outlier removal: `pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)`
|
||
- Return cleaned point cloud
|
||
- Add `region: str = "floor"` field to `ICPConfig` dataclass (values: "floor", "hybrid", "full")
|
||
- If hybrid mode finds no vertical structure points, log warning and fall back to floor-only points
|
||
|
||
**Must NOT do**:
|
||
- Do not modify `extract_near_floor_band` (keep it as-is for floor mode)
|
||
- Do not add multiple outlier strategies
|
||
- Do not change ground_plane.py
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- Reason: New extraction logic with mode dispatch, normal-based filtering, and SOR integration
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 1 (with Tasks 1, 3)
|
||
- **Blocks**: Task 5
|
||
- **Blocked By**: None
|
||
|
||
**References**:
|
||
|
||
**Pattern References**:
|
||
- `aruco/icp_registration.py:62-82` — `extract_near_floor_band` (floor mode delegate, keep unchanged)
|
||
- `aruco/icp_registration.py:20-34` — `ICPConfig` dataclass (add `region` field here)
|
||
|
||
**API/Type References**:
|
||
- `aruco/ground_plane.py:114-145` — `detect_floor_plane` returns `FloorPlane(normal, d)` — used for floor_y and floor_normal inputs
|
||
- Open3D `remove_statistical_outlier(nb_neighbors, std_ratio)` — returns (cleaned_pcd, indices)
|
||
- Open3D `estimate_normals(KDTreeSearchParamHybrid(radius, max_nn))` — for hybrid vertical detection
|
||
|
||
**External References**:
|
||
- Open3D SOR: https://www.open3d.org/docs/release/tutorial/geometry/pointcloud.html#Statistical-outlier-removal
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Lines 62-82: the existing extraction function that floor mode must delegate to unchanged
|
||
- ICPConfig: where `region` field lives so `refine_with_icp` can dispatch
|
||
- Open3D SOR API: exact function signature for outlier removal
|
||
|
||
**Acceptance Criteria**:
|
||
|
||
- [ ] `extract_scene_points` exists and dispatches correctly for all 3 modes
|
||
- [ ] `preprocess_point_cloud` applies SOR and returns cleaned cloud
|
||
- [ ] `ICPConfig.region` field exists with default `"floor"`
|
||
- [ ] Floor mode produces identical output to `extract_near_floor_band`
|
||
- [ ] Hybrid mode includes vertical-normal points when present
|
||
- [ ] Hybrid mode falls back to floor-only with warning when no vertical points
|
||
- [ ] `uv run basedpyright aruco/icp_registration.py` → 0 errors
|
||
|
||
**Agent-Executed QA Scenarios:**
|
||
|
||
```
|
||
Scenario: Floor mode identical to legacy
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "floor_mode_legacy" -v
|
||
2. Assert: exit code 0
|
||
Expected Result: Floor extraction unchanged
|
||
Evidence: pytest output
|
||
|
||
Scenario: Hybrid includes vertical structure
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "hybrid_vertical" -v
|
||
2. Assert: exit code 0
|
||
Expected Result: Hybrid returns more points than floor-only when walls present
|
||
Evidence: pytest output
|
||
|
||
Scenario: Hybrid fallback when no vertical structure
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "hybrid_fallback" -v
|
||
2. Assert: exit code 0
|
||
Expected Result: Falls back to floor-only points, logs warning
|
||
Evidence: pytest output
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(icp): add hybrid/full point extraction with SOR preprocessing`
|
||
- Files: `aruco/icp_registration.py`
|
||
- Pre-commit: `uv run pytest tests/test_icp_registration.py -q`
|
||
|
||
---
|
||
|
||
- [ ] 3. Add 3D AABB overlap check
|
||
|
||
**What to do**:
|
||
- Add `compute_overlap_3d(points_a, points_b, margin)` function to `aruco/icp_registration.py`
|
||
- Compute 3D axis-aligned bounding box intersection volume
|
||
- Return volume in m³
|
||
- Add `overlap_mode: str = "xz"` field to `ICPConfig` (values: "xz", "3d")
|
||
- `"xz"`: use existing `compute_overlap_xz` (backward compat for floor mode)
|
||
- `"3d"`: use new `compute_overlap_3d` (for hybrid/full modes)
|
||
- Keep `compute_overlap_xz` unchanged
|
||
|
||
**Must NOT do**:
|
||
- Do not remove or modify `compute_overlap_xz`
|
||
- Do not add OBB or complex overlap metrics
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- Reason: Simple geometric function + config field addition
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 1 (with Tasks 1, 2)
|
||
- **Blocks**: Task 5
|
||
- **Blocked By**: None
|
||
|
||
**References**:
|
||
|
||
**Pattern References**:
|
||
- `aruco/icp_registration.py:85-105` — `compute_overlap_xz` (follow same pattern for 3D version)
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Lines 85-105: exact pattern to follow (min/max bounding box, intersection, area/volume)
|
||
|
||
**Acceptance Criteria**:
|
||
|
||
- [ ] `compute_overlap_3d` exists and returns volume in m³
|
||
- [ ] `ICPConfig.overlap_mode` field exists with default `"xz"`
|
||
- [ ] Disjoint clouds return 0.0
|
||
- [ ] Fully overlapping clouds return correct volume
|
||
- [ ] `uv run basedpyright aruco/icp_registration.py` → 0 errors
|
||
|
||
**Agent-Executed QA Scenarios:**
|
||
|
||
```
|
||
Scenario: 3D overlap correctness
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "overlap_3d" -v
|
||
2. Assert: exit code 0
|
||
Expected Result: Correct volume for known geometries
|
||
Evidence: pytest output
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(icp): add 3D AABB overlap check for hybrid/full modes`
|
||
- Files: `aruco/icp_registration.py`
|
||
- Pre-commit: `uv run pytest tests/test_icp_registration.py -q`
|
||
|
||
---
|
||
|
||
- [ ] 4. Add TukeyLoss robust kernel support
|
||
|
||
**What to do**:
|
||
- Add `robust_kernel: str = "none"` field to `ICPConfig` (values: "none", "tukey")
|
||
- Add `robust_kernel_k: float = 0.1` field to `ICPConfig`
|
||
- In `pairwise_icp`, when `config.robust_kernel == "tukey"`:
|
||
- Create `loss = o3d.pipelines.registration.TukeyLoss(k=config.robust_kernel_k)`
|
||
- Pass loss to `TransformationEstimationPointToPlane(loss)` or `TransformationEstimationForGeneralizedICP(loss)`
|
||
- When `config.robust_kernel == "none"`: use current behavior (no loss function)
|
||
|
||
**Must NOT do**:
|
||
- Do not add Huber, Cauchy, or other kernels
|
||
- Do not change default behavior (default is "none")
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- Reason: Config field + conditional in pairwise_icp
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES
|
||
- **Parallel Group**: Wave 2 (with Tasks 5, 6)
|
||
- **Blocks**: Task 5
|
||
- **Blocked By**: None
|
||
|
||
**References**:
|
||
|
||
**Pattern References**:
|
||
- `aruco/icp_registration.py:177-213` — `pairwise_icp` estimation method dispatch (add kernel here)
|
||
|
||
**External References**:
|
||
- Open3D robust kernels: https://www.open3d.org/docs/release/tutorial/t_pipelines/t_robust_kernel.html
|
||
- `TukeyLoss(k)` where k ≈ expected noise std dev in meters
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Lines 177-213: exact location where estimation objects are created; kernel wraps them
|
||
|
||
**Acceptance Criteria**:
|
||
|
||
- [ ] `ICPConfig.robust_kernel` and `robust_kernel_k` fields exist
|
||
- [ ] `pairwise_icp` uses TukeyLoss when configured
|
||
- [ ] Default behavior unchanged (no kernel applied)
|
||
- [ ] `uv run basedpyright aruco/icp_registration.py` → 0 errors
|
||
|
||
**Agent-Executed QA Scenarios:**
|
||
|
||
```
|
||
Scenario: Tukey kernel applied correctly
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "robust_kernel" -v
|
||
2. Assert: exit code 0
|
||
Expected Result: ICP runs with Tukey kernel without errors
|
||
Evidence: pytest output
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(icp): add TukeyLoss robust kernel support`
|
||
- Files: `aruco/icp_registration.py`
|
||
- Pre-commit: `uv run pytest tests/test_icp_registration.py -q`
|
||
|
||
---
|
||
|
||
- [ ] 5. Integrate region selection into refine_with_icp
|
||
|
||
**What to do**:
|
||
- Modify `refine_with_icp()` to use `extract_scene_points` instead of `extract_near_floor_band`
|
||
- Dispatch overlap check based on `config.overlap_mode`:
|
||
- `"xz"` → `compute_overlap_xz` (floor mode)
|
||
- `"3d"` → `compute_overlap_3d` (hybrid/full)
|
||
- Auto-set `overlap_mode` based on `region`:
|
||
- `floor` → `overlap_mode="xz"`
|
||
- `hybrid` or `full` → `overlap_mode="3d"`
|
||
- Apply `preprocess_point_cloud` (SOR) to all point clouds before ICP
|
||
- When `config.robust_kernel != "none"`, pass kernel config through to `pairwise_icp`
|
||
- Use world-frame points for ICP when in hybrid/full mode (current floor mode transforms back to camera frame — keep that for floor; for hybrid/full, ICP in world frame is more stable with mixed geometry)
|
||
|
||
**Must NOT do**:
|
||
- Do not change pose graph construction or optimization logic
|
||
- Do not change the validation/safety bounds logic
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- Reason: Core integration task connecting all new components
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES (with Task 6)
|
||
- **Parallel Group**: Wave 2
|
||
- **Blocks**: Task 7
|
||
- **Blocked By**: Tasks 1, 2, 3, 4
|
||
|
||
**References**:
|
||
|
||
**Pattern References**:
|
||
- `aruco/icp_registration.py:327-478` — `refine_with_icp` full function (the integration target)
|
||
- `aruco/icp_registration.py:346-372` — Current point extraction loop (replace with extract_scene_points)
|
||
- `aruco/icp_registration.py:378-386` — Current overlap check (dispatch based on mode)
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Lines 346-372: the exact loop to modify for region-aware extraction
|
||
- Lines 378-386: where overlap gating happens, needs mode dispatch
|
||
|
||
**Acceptance Criteria**:
|
||
|
||
- [ ] `refine_with_icp` uses `extract_scene_points` for point extraction
|
||
- [ ] Overlap check dispatches based on region mode
|
||
- [ ] SOR preprocessing applied to all point clouds
|
||
- [ ] Floor mode produces identical behavior to current implementation
|
||
- [ ] Hybrid/full modes extract more points and use 3D overlap
|
||
- [ ] `uv run pytest tests/test_icp_registration.py -v` → all pass
|
||
|
||
**Agent-Executed QA Scenarios:**
|
||
|
||
```
|
||
Scenario: Floor mode regression
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "floor_mode" -v
|
||
2. Assert: exit code 0
|
||
Expected Result: Floor mode unchanged
|
||
Evidence: pytest output
|
||
|
||
Scenario: Hybrid mode integration
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "hybrid_integration" -v
|
||
2. Assert: exit code 0
|
||
Expected Result: Hybrid uses mixed-geometry extraction + 3D overlap
|
||
Evidence: pytest output
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(icp): integrate region selection, SOR, and robust kernel into refine_with_icp`
|
||
- Files: `aruco/icp_registration.py`
|
||
- Pre-commit: `uv run pytest tests/test_icp_registration.py -q`
|
||
|
||
---
|
||
|
||
- [ ] 6. Add FPFH+RANSAC global pre-alignment
|
||
|
||
**What to do**:
|
||
- Add `global_init: bool = False` field to `ICPConfig`
|
||
- Add `compute_fpfh_features(pcd_down, voxel_size)` function:
|
||
- Compute FPFH features using `o3d.pipelines.registration.compute_fpfh_feature`
|
||
- Use `KDTreeSearchParamHybrid(radius=voxel_size*5, max_nn=100)`
|
||
- Add `global_registration(source_down, target_down, source_fpfh, target_fpfh, voxel_size)` function:
|
||
- Use `registration_ransac_based_on_feature_matching`
|
||
- Correspondence checkers: EdgeLength(0.9), Distance(voxel_size*1.5)
|
||
- Convergence: 4M iterations, 500 validations
|
||
- Return transformation matrix
|
||
- In `refine_with_icp` pairwise loop: when `config.global_init` is True:
|
||
- Attempt global registration first
|
||
- Validate result (fitness > 0.1, transform magnitude within bounds)
|
||
- If valid: use as init_T for pairwise_icp instead of extrinsic-derived init
|
||
- If invalid: log warning, fall back to extrinsic-derived init
|
||
- When `config.global_init` is False: use current extrinsic-based init (unchanged)
|
||
|
||
**Must NOT do**:
|
||
- Do not add other global registration methods
|
||
- Do not make global_init the default
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- Reason: New FPFH pipeline with validation and fallback logic
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES (with Tasks 4, 5)
|
||
- **Parallel Group**: Wave 2
|
||
- **Blocks**: Task 7
|
||
- **Blocked By**: None (uses only Open3D APIs + ICPConfig)
|
||
|
||
**References**:
|
||
|
||
**Pattern References**:
|
||
- `aruco/icp_registration.py:388-407` — Current pairwise ICP loop where init_T is computed and used
|
||
|
||
**External References**:
|
||
- Open3D FPFH: https://www.open3d.org/docs/release/tutorial/pipelines/global_registration.html
|
||
- Open3D `registration_ransac_based_on_feature_matching` API
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Lines 388-407: where init_T is computed; global_init replaces this when enabled
|
||
|
||
**Acceptance Criteria**:
|
||
|
||
- [ ] `ICPConfig.global_init` field exists (default False)
|
||
- [ ] `compute_fpfh_features` and `global_registration` functions exist
|
||
- [ ] When enabled: uses FPFH transform as init if valid, falls back otherwise
|
||
- [ ] When disabled: behavior unchanged
|
||
- [ ] Fallback logged at WARNING level
|
||
- [ ] `uv run basedpyright aruco/icp_registration.py` → 0 errors
|
||
|
||
**Agent-Executed QA Scenarios:**
|
||
|
||
```
|
||
Scenario: Global init fallback on bad data
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "global_init_fallback" -v
|
||
2. Assert: exit code 0
|
||
Expected Result: Falls back to extrinsic init when FPFH fails
|
||
Evidence: pytest output
|
||
|
||
Scenario: Global init disabled by default
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest tests/test_icp_registration.py -k "global_init_disabled" -v
|
||
2. Assert: exit code 0
|
||
Expected Result: No FPFH computation when global_init=False
|
||
Evidence: pytest output
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(icp): add optional FPFH+RANSAC global pre-alignment`
|
||
- Files: `aruco/icp_registration.py`
|
||
- Pre-commit: `uv run pytest tests/test_icp_registration.py -q`
|
||
|
||
---
|
||
|
||
- [ ] 7. Wire CLI flags in refine_ground_plane.py
|
||
|
||
**What to do**:
|
||
- Add CLI options to `refine_ground_plane.py`:
|
||
- `--icp-region`: type=click.Choice(["floor", "hybrid", "full"]), default="hybrid"
|
||
- `--icp-global-init / --no-icp-global-init`: default False
|
||
- `--icp-min-overlap`: type=float, default=0.5
|
||
- `--icp-band-height`: type=float, default=0.3
|
||
- `--icp-robust-kernel`: type=click.Choice(["none", "tukey"]), default="none"
|
||
- `--icp-robust-k`: type=float, default=0.1
|
||
- Pass these through to `ICPConfig` construction
|
||
- Include new config values in `_meta.icp_refined.config` output JSON
|
||
|
||
**Must NOT do**:
|
||
- Do not change existing CLI flag names or defaults for non-ICP flags
|
||
- Do not change ground-plane refinement logic
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- Reason: Multiple CLI flags with proper Click integration and JSON metadata
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES (with Task 8)
|
||
- **Parallel Group**: Wave 3
|
||
- **Blocks**: Task 9
|
||
- **Blocked By**: Tasks 5, 6
|
||
|
||
**References**:
|
||
|
||
**Pattern References**:
|
||
- `refine_ground_plane.py` — Existing `--icp`, `--icp-method`, `--icp-voxel-size` flags (follow same pattern)
|
||
- `refine_ground_plane.py` — `_meta.icp_refined.config` section in output JSON
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Existing ICP flags: exact pattern for Click option declaration + ICPConfig construction
|
||
|
||
**Acceptance Criteria**:
|
||
|
||
- [ ] `uv run refine_ground_plane.py --help` shows all new flags
|
||
- [ ] `--icp-region` accepts floor, hybrid, full
|
||
- [ ] `--icp-global-init` is a boolean flag
|
||
- [ ] Output JSON `_meta.icp_refined.config` includes region, global_init, min_overlap, band_height
|
||
- [ ] `uv run basedpyright refine_ground_plane.py` → 0 errors
|
||
|
||
**Agent-Executed QA Scenarios:**
|
||
|
||
```
|
||
Scenario: New CLI flags appear in help
|
||
Tool: Bash
|
||
Steps:
|
||
1. uv run refine_ground_plane.py --help
|
||
2. Assert: output contains "--icp-region"
|
||
3. Assert: output contains "floor|hybrid|full"
|
||
4. Assert: output contains "--icp-global-init"
|
||
5. Assert: output contains "--icp-min-overlap"
|
||
6. Assert: output contains "--icp-band-height"
|
||
Expected Result: All flags documented
|
||
Evidence: Help output captured
|
||
|
||
Scenario: Flags pass through to ICPConfig
|
||
Tool: Bash (pipeline run)
|
||
Steps:
|
||
1. uv run refine_ground_plane.py --input-extrinsics output/e2e_refine_depth.json --input-depth output/e2e_refine_depth.h5 --output-extrinsics /tmp/test_cli_flags.json --icp --icp-region hybrid --icp-voxel-size 0.04 --seed 42
|
||
2. Parse /tmp/test_cli_flags.json
|
||
3. Assert: _meta.icp_refined.config.region == "hybrid"
|
||
Expected Result: Config values in output JSON
|
||
Evidence: JSON file contents
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `feat(cli): wire ICP region, global-init, overlap, kernel CLI flags`
|
||
- Files: `refine_ground_plane.py`
|
||
- Pre-commit: `uv run pytest -q`
|
||
|
||
---
|
||
|
||
- [ ] 8. Relax ICPConfig defaults
|
||
|
||
**What to do**:
|
||
- Change `ICPConfig` defaults:
|
||
- `min_fitness: float = 0.3` → `0.15` (more permissive pair acceptance)
|
||
- `min_overlap_area: float = 1.0` → `0.5` (allow sparser overlap)
|
||
- `gravity_penalty_weight: float = 10.0` → `2.0` (allow useful tilt corrections)
|
||
- `max_correspondence_distance_factor: float = 1.4` → `2.5` (wider capture at coarse scale)
|
||
- `max_translation_m: float = 0.1` → `0.3` (allow reasonable corrections)
|
||
- `max_rotation_deg: float = 5.0` → `10.0` (allow moderate rotation corrections)
|
||
- Keep `region: str = "floor"` as default in ICPConfig (CLI default is "hybrid", but library default stays conservative)
|
||
|
||
**Must NOT do**:
|
||
- Do not change method, voxel_size, or band_height defaults
|
||
- Do not change any algorithmic logic
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick`
|
||
- Reason: Pure config value changes, no logic
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: YES (with Task 7)
|
||
- **Parallel Group**: Wave 3
|
||
- **Blocks**: Task 9
|
||
- **Blocked By**: None
|
||
|
||
**References**:
|
||
|
||
**Pattern References**:
|
||
- `aruco/icp_registration.py:20-34` — ICPConfig dataclass (all defaults here)
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Lines 20-34: the exact defaults to modify, with Oracle's recommendations as rationale
|
||
|
||
**Acceptance Criteria**:
|
||
|
||
- [ ] All default values updated as specified
|
||
- [ ] Existing tests still pass with new defaults
|
||
- [ ] `uv run pytest tests/test_icp_registration.py -v` → all pass
|
||
|
||
**Agent-Executed QA Scenarios:**
|
||
|
||
```
|
||
Scenario: Defaults changed correctly
|
||
Tool: Bash (python)
|
||
Steps:
|
||
1. uv run python -c "from aruco.icp_registration import ICPConfig; c=ICPConfig(); print(c.min_fitness, c.min_overlap_area, c.gravity_penalty_weight, c.max_correspondence_distance_factor, c.max_translation_m, c.max_rotation_deg)"
|
||
2. Assert: output is "0.15 0.5 2.0 2.5 0.3 10.0"
|
||
Expected Result: All defaults match spec
|
||
Evidence: Terminal output
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `chore(icp): relax ICPConfig defaults per Oracle recommendations`
|
||
- Files: `aruco/icp_registration.py`
|
||
- Pre-commit: `uv run pytest tests/test_icp_registration.py -q`
|
||
|
||
---
|
||
|
||
- [ ] 9. Tests + README + regression verification
|
||
|
||
**What to do**:
|
||
- Add new tests to `tests/test_icp_registration.py`:
|
||
- `test_success_gate_single_camera`: verify success=True when 1 camera optimized
|
||
- `test_floor_mode_legacy_equivalence`: floor extraction matches extract_near_floor_band
|
||
- `test_hybrid_includes_vertical_structure`: hybrid returns more points with walls
|
||
- `test_hybrid_fallback_no_vertical`: falls back to floor-only with warning
|
||
- `test_full_mode_all_points`: full mode returns all valid points after SOR
|
||
- `test_overlap_3d_disjoint`: 3D overlap returns 0 for disjoint clouds
|
||
- `test_overlap_3d_partial`: 3D overlap returns correct volume
|
||
- `test_robust_kernel_tukey`: ICP runs with TukeyLoss without errors
|
||
- `test_global_init_fallback`: falls back when FPFH fails
|
||
- `test_global_init_disabled_default`: no FPFH when global_init=False
|
||
- `test_per_pair_logging_all_pairs`: all pairs stored in metrics regardless of convergence
|
||
- `test_preprocess_sor`: SOR removes outliers correctly
|
||
- Update `README.md`:
|
||
- Document `--icp-region`, `--icp-global-init`, `--icp-min-overlap`, `--icp-band-height`, `--icp-robust-kernel`, `--icp-robust-k` flags
|
||
- Add example command with hybrid mode
|
||
- Run full regression:
|
||
- `uv run pytest -x -vv` → all pass
|
||
- `uv run basedpyright aruco/icp_registration.py refine_ground_plane.py` → 0 errors
|
||
|
||
**Must NOT do**:
|
||
- Do not add tests for features not implemented in Tasks 1-8
|
||
- Do not modify code logic (test-only + docs-only task)
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- Reason: Large test suite + documentation + regression verification
|
||
- **Skills**: []
|
||
|
||
**Parallelization**:
|
||
- **Can Run In Parallel**: NO (final task)
|
||
- **Parallel Group**: Wave 3 (after Tasks 7, 8)
|
||
- **Blocks**: None (final)
|
||
- **Blocked By**: Tasks 7, 8
|
||
|
||
**References**:
|
||
|
||
**Pattern References**:
|
||
- `tests/test_icp_registration.py` — Existing test patterns (synthetic point clouds, monkeypatching, numpy assertions)
|
||
|
||
**Test References**:
|
||
- `tests/test_icp_registration.py:test_pairwise_icp_known_transform` — Pattern for synthetic ICP tests
|
||
- `tests/test_icp_registration.py:test_refine_with_icp_synthetic_offset` — Pattern for integration tests
|
||
|
||
**WHY Each Reference Matters**:
|
||
- Existing tests: follow same synthetic data patterns, assertion styles, and monkeypatching approaches
|
||
|
||
**Acceptance Criteria**:
|
||
|
||
- [ ] ≥12 new test cases added
|
||
- [ ] `uv run pytest tests/test_icp_registration.py -v` → all pass (existing + new)
|
||
- [ ] `uv run pytest -x -vv` → all pass (full suite)
|
||
- [ ] `uv run basedpyright aruco/icp_registration.py refine_ground_plane.py` → 0 errors
|
||
- [ ] README.md documents all new CLI flags with examples
|
||
|
||
**Agent-Executed QA Scenarios:**
|
||
|
||
```
|
||
Scenario: Full test suite passes
|
||
Tool: Bash (pytest)
|
||
Steps:
|
||
1. uv run pytest -x -vv
|
||
2. Assert: exit code 0
|
||
3. Assert: output contains "passed" and no "FAILED"
|
||
Expected Result: All tests pass
|
||
Evidence: pytest output captured
|
||
|
||
Scenario: Type check clean
|
||
Tool: Bash (basedpyright)
|
||
Steps:
|
||
1. uv run basedpyright aruco/icp_registration.py refine_ground_plane.py
|
||
2. Assert: output contains "0 errors"
|
||
Expected Result: No type errors
|
||
Evidence: basedpyright output
|
||
|
||
Scenario: README documents new flags
|
||
Tool: Bash (grep)
|
||
Steps:
|
||
1. grep -c "icp-region" README.md
|
||
2. Assert: count > 0
|
||
3. grep -c "icp-global-init" README.md
|
||
4. Assert: count > 0
|
||
Expected Result: Flags documented
|
||
Evidence: grep output
|
||
```
|
||
|
||
**Commit**: YES
|
||
- Message: `test(icp): add comprehensive tests for full-scene ICP pipeline + update docs`
|
||
- Files: `tests/test_icp_registration.py`, `README.md`
|
||
- Pre-commit: `uv run pytest -x -vv`
|
||
|
||
---
|
||
|
||
## Commit Strategy
|
||
|
||
| After Task | Message | Files | Verification |
|
||
|------------|---------|-------|--------------|
|
||
| 1 | `fix(icp): relax success gate to >0 and add per-pair diagnostic logging` | `aruco/icp_registration.py` | `uv run pytest tests/test_icp_registration.py -q` |
|
||
| 2 | `feat(icp): add hybrid/full point extraction with SOR preprocessing` | `aruco/icp_registration.py` | `uv run pytest tests/test_icp_registration.py -q` |
|
||
| 3 | `feat(icp): add 3D AABB overlap check for hybrid/full modes` | `aruco/icp_registration.py` | `uv run pytest tests/test_icp_registration.py -q` |
|
||
| 4 | `feat(icp): add TukeyLoss robust kernel support` | `aruco/icp_registration.py` | `uv run pytest tests/test_icp_registration.py -q` |
|
||
| 5 | `feat(icp): integrate region selection, SOR, and robust kernel into refine_with_icp` | `aruco/icp_registration.py` | `uv run pytest tests/test_icp_registration.py -q` |
|
||
| 6 | `feat(icp): add optional FPFH+RANSAC global pre-alignment` | `aruco/icp_registration.py` | `uv run pytest tests/test_icp_registration.py -q` |
|
||
| 7 | `feat(cli): wire ICP region, global-init, overlap, kernel CLI flags` | `refine_ground_plane.py` | `uv run pytest -q` |
|
||
| 8 | `chore(icp): relax ICPConfig defaults per Oracle recommendations` | `aruco/icp_registration.py` | `uv run pytest tests/test_icp_registration.py -q` |
|
||
| 9 | `test(icp): add comprehensive tests for full-scene ICP pipeline + update docs` | `tests/test_icp_registration.py`, `README.md` | `uv run pytest -x -vv` |
|
||
|
||
---
|
||
|
||
## Success Criteria
|
||
|
||
### Verification Commands
|
||
```bash
|
||
uv run refine_ground_plane.py --help # Expected: shows --icp-region, --icp-global-init, etc.
|
||
uv run pytest -x -vv # Expected: all tests pass
|
||
uv run basedpyright aruco/icp_registration.py refine_ground_plane.py # Expected: 0 errors
|
||
```
|
||
|
||
### Final Checklist
|
||
- [ ] All "Must Have" present
|
||
- [ ] All "Must NOT Have" absent
|
||
- [ ] All tests pass
|
||
- [ ] `--icp-region floor` backward compatible
|
||
- [ ] `--icp-region hybrid` default in CLI
|
||
- [ ] README documents all new flags
|