diff --git a/py_workspace/.beads/.gitignore b/py_workspace/.beads/.gitignore new file mode 100644 index 0000000..22f7963 --- /dev/null +++ b/py_workspace/.beads/.gitignore @@ -0,0 +1,45 @@ +# SQLite databases +*.db +*.db?* +*.db-journal +*.db-wal +*.db-shm + +# Daemon runtime files +daemon.lock +daemon.log +daemon.pid +bd.sock +sync-state.json +last-touched + +# Local version tracking (prevents upgrade notification spam after git ops) +.local_version + +# Legacy database files +db.sqlite +bd.db + +# Worktree redirect file (contains relative path to main repo's .beads/) +# Must not be committed as paths would be wrong in other clones +redirect + +# Merge artifacts (temporary files from 3-way merge) +beads.base.jsonl +beads.base.meta.json +beads.left.jsonl +beads.left.meta.json +beads.right.jsonl +beads.right.meta.json + +# Sync state (local-only, per-machine) +# These files are machine-specific and should not be shared across clones +.sync.lock +sync_base.jsonl +export-state/ + +# NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here. +# They would override fork protection in .git/info/exclude, allowing +# contributors to accidentally commit upstream issue databases. +# The JSONL files (issues.jsonl, interactions.jsonl) and config files +# are tracked by git by default since no pattern above ignores them. diff --git a/py_workspace/.beads/README.md b/py_workspace/.beads/README.md new file mode 100644 index 0000000..50f281f --- /dev/null +++ b/py_workspace/.beads/README.md @@ -0,0 +1,81 @@ +# Beads - AI-Native Issue Tracking + +Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code. + +## What is Beads? + +Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git. + +**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads) + +## Quick Start + +### Essential Commands + +```bash +# Create new issues +bd create "Add user authentication" + +# View all issues +bd list + +# View issue details +bd show + +# Update issue status +bd update --status in_progress +bd update --status done + +# Sync with git remote +bd sync +``` + +### Working with Issues + +Issues in Beads are: +- **Git-native**: Stored in `.beads/issues.jsonl` and synced like code +- **AI-friendly**: CLI-first design works perfectly with AI coding agents +- **Branch-aware**: Issues can follow your branch workflow +- **Always in sync**: Auto-syncs with your commits + +## Why Beads? + +โœจ **AI-Native Design** +- Built specifically for AI-assisted development workflows +- CLI-first interface works seamlessly with AI coding agents +- No context switching to web UIs + +๐Ÿš€ **Developer Focused** +- Issues live in your repo, right next to your code +- Works offline, syncs when you push +- Fast, lightweight, and stays out of your way + +๐Ÿ”ง **Git Integration** +- Automatic sync with git commits +- Branch-aware issue tracking +- Intelligent JSONL merge resolution + +## Get Started with Beads + +Try Beads in your own projects: + +```bash +# Install Beads +curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash + +# Initialize in your repo +bd init + +# Create your first issue +bd create "Try out Beads" +``` + +## Learn More + +- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) +- **Quick Start Guide**: Run `bd quickstart` +- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) + +--- + +*Beads: Issue tracking that moves at the speed of thought* โšก diff --git a/py_workspace/.beads/config.yaml b/py_workspace/.beads/config.yaml new file mode 100644 index 0000000..ff8bc92 --- /dev/null +++ b/py_workspace/.beads/config.yaml @@ -0,0 +1,67 @@ +# Beads Configuration File +# This file configures default behavior for all bd commands in this repository +# All settings can also be set via environment variables (BD_* prefix) +# or overridden with command-line flags + +# Issue prefix for this repository (used by bd init) +# If not set, bd init will auto-detect from directory name +# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. +# issue-prefix: "" + +# Use no-db mode: load from JSONL, no SQLite, write back after each command +# When true, bd will use .beads/issues.jsonl as the source of truth +# instead of SQLite database +# no-db: false + +# Disable daemon for RPC communication (forces direct database access) +# no-daemon: false + +# Disable auto-flush of database to JSONL after mutations +# no-auto-flush: false + +# Disable auto-import from JSONL when it's newer than database +# no-auto-import: false + +# Enable JSON output by default +# json: false + +# Default actor for audit trails (overridden by BD_ACTOR or --actor) +# actor: "" + +# Path to database (overridden by BEADS_DB or --db) +# db: "" + +# Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON) +# auto-start-daemon: true + +# Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE) +# flush-debounce: "5s" + +# Export events (audit trail) to .beads/events.jsonl on each flush/sync +# When enabled, new events are appended incrementally using a high-water mark. +# Use 'bd export --events' to trigger manually regardless of this setting. +# events-export: false + +# Git branch for beads commits (bd sync will commit to this branch) +# IMPORTANT: Set this for team projects so all clones use the same sync branch. +# This setting persists across clones (unlike database config which is gitignored). +# Can also use BEADS_SYNC_BRANCH env var for local override. +# If not set, bd sync will require you to run 'bd config set sync.branch '. +# sync-branch: "beads-sync" + +# Multi-repo configuration (experimental - bd-307) +# Allows hydrating from multiple repositories and routing writes to the correct JSONL +# repos: +# primary: "." # Primary repo (where this database lives) +# additional: # Additional repos to hydrate from (read-only) +# - ~/beads-planning # Personal planning repo +# - ~/work-planning # Work planning repo + +# Integration settings (access with 'bd config get/set') +# These are stored in the database, not in this file: +# - jira.url +# - jira.project +# - linear.url +# - linear.api-key +# - github.org +# - github.repo diff --git a/py_workspace/.beads/interactions.jsonl b/py_workspace/.beads/interactions.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/py_workspace/.beads/issues.jsonl b/py_workspace/.beads/issues.jsonl index e509bf5..0e03119 100644 --- a/py_workspace/.beads/issues.jsonl +++ b/py_workspace/.beads/issues.jsonl @@ -1 +1,2 @@ +{"id":"py_workspace-39i","title":"Fix success gate + add per-pair diagnostic logging","status":"closed","priority":1,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-10T15:22:17.79015381Z","created_by":"crosstyan","updated_at":"2026-02-10T15:22:22.41606631Z","closed_at":"2026-02-10T15:22:22.41606631Z","close_reason":"Fixed success gate, added logging and ensured pair results storage"} {"id":"py_workspace-k61","title":"Add point extraction functions (hybrid/full/SOR)","status":"closed","priority":2,"issue_type":"task","owner":"crosstyan@outlook.com","created_at":"2026-02-10T09:54:11.376739559Z","created_by":"crosstyan","updated_at":"2026-02-10T09:54:32.257034655Z","closed_at":"2026-02-10T09:54:32.257034655Z","close_reason":"Implemented point extraction functions and ICPConfig update"} diff --git a/py_workspace/.beads/metadata.json b/py_workspace/.beads/metadata.json new file mode 100644 index 0000000..c787975 --- /dev/null +++ b/py_workspace/.beads/metadata.json @@ -0,0 +1,4 @@ +{ + "database": "beads.db", + "jsonl_export": "issues.jsonl" +} \ No newline at end of file diff --git a/py_workspace/.sisyphus/notepads/full-icp-pipeline/decisions.md b/py_workspace/.sisyphus/notepads/full-icp-pipeline/decisions.md new file mode 100644 index 0000000..3a3b712 --- /dev/null +++ b/py_workspace/.sisyphus/notepads/full-icp-pipeline/decisions.md @@ -0,0 +1,2 @@ +## 2026-02-10T09:46:30Z Initialized +- No additional execution-time decisions recorded yet. diff --git a/py_workspace/.sisyphus/notepads/full-icp-pipeline/issues.md b/py_workspace/.sisyphus/notepads/full-icp-pipeline/issues.md new file mode 100644 index 0000000..259b616 --- /dev/null +++ b/py_workspace/.sisyphus/notepads/full-icp-pipeline/issues.md @@ -0,0 +1,2 @@ +## 2026-02-10T09:46:30Z Initialized +- No issues recorded yet. diff --git a/py_workspace/.sisyphus/notepads/full-icp-pipeline/problems.md b/py_workspace/.sisyphus/notepads/full-icp-pipeline/problems.md new file mode 100644 index 0000000..1cbe572 --- /dev/null +++ b/py_workspace/.sisyphus/notepads/full-icp-pipeline/problems.md @@ -0,0 +1,2 @@ +## 2026-02-10T09:46:30Z Initialized +- No unresolved blockers at session start. diff --git a/py_workspace/.sisyphus/notepads/icp-registration/decisions.md b/py_workspace/.sisyphus/notepads/icp-registration/decisions.md index 4a88f58..490d1ef 100644 --- a/py_workspace/.sisyphus/notepads/icp-registration/decisions.md +++ b/py_workspace/.sisyphus/notepads/icp-registration/decisions.md @@ -2,3 +2,8 @@ - Used `scipy.spatial.transform.Rotation` with `xyz` Euler convention for gravity regularization to ensure consistent blending of pitch/roll. - `extract_near_floor_band` uses dot product with floor normal to handle arbitrary floor orientations (not just Y-up). - `refine_with_icp` uses a BFS-based connectivity check to ensure only cameras reachable from the reference camera are optimized. + +## Balanced SN442 Profile +- Decided to document a "Balanced Profile" in the README to provide a standard recovery path for cameras that fail strict ground plane alignment. +- Chose GICP over point-to-plane for the balanced profile due to its superior robustness with noisy ZED depth data in multi-camera setups. +- Set `--seed 42` in the recommended command to ensure deterministic results for users debugging their calibration. diff --git a/py_workspace/.sisyphus/notepads/icp-registration/learnings.md b/py_workspace/.sisyphus/notepads/icp-registration/learnings.md index 178e7c3..6b91ecb 100644 --- a/py_workspace/.sisyphus/notepads/icp-registration/learnings.md +++ b/py_workspace/.sisyphus/notepads/icp-registration/learnings.md @@ -20,3 +20,9 @@ ## ICP Registration - GICP method in `pairwise_icp` requires normals, which are estimated internally if not provided. - Synthetic tests for ICP should use deterministic seeds for point cloud generation to ensure stability. + +## Balanced SN442 Profile +- A balanced profile was established to handle cameras (like SN44289123) that show significant floor disconnect (~5.5cm translation, ~1.5ยฐ rotation). +- Permissive RANSAC threshold (0.05m) and min inlier ratio (0.01) allow recovery when strict defaults fail. +- Safety limits were increased to `--max-rotation-deg 15` and `--max-translation-m 1.0` to accommodate observed disconnects. +- GICP with 0.04m voxel size provides robust inter-camera alignment following ground plane correction. diff --git a/py_workspace/.sisyphus/plans/full-icp-pipeline.md b/py_workspace/.sisyphus/plans/full-icp-pipeline.md new file mode 100644 index 0000000..ef74701 --- /dev/null +++ b/py_workspace/.sisyphus/plans/full-icp-pipeline.md @@ -0,0 +1,889 @@ +# 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