# RansacFilter Helper Module

**Task**: AZ-282_ransac_filter
**Name**: RansacFilter Helper
**Description**: Implement the shared `RansacFilter` helper that wraps OpenCV's RANSAC inlier filtering and reprojection-residual computation. Used by C2.5 (single-pair LightGlue inlier counting), C3 (2D-2D RANSAC over cross-domain correspondences), C3.5 (residual recompute after AdHoP refinement), and C4 (per-frame final reprojection residual for FDR provenance). Stateless static-only design; deterministic by setting OpenCV's RNG seed.
**Complexity**: 2 points
**Dependencies**: AZ-263_initial_structure, AZ-277_se3_utils
**Component**: shared.helpers.ransac_filter (cross-cutting; epic AZ-264 / E-CC-HELPERS)
**Tracker**: AZ-282
**Epic**: AZ-264 (E-CC-HELPERS)

### Document Dependencies

- `_docs/02_document/contracts/shared_helpers/ransac_filter.md` — frozen public interface this task produces.
- `_docs/02_document/contracts/shared_helpers/se3_utils.md` — the `SE3` (re-exported GTSAM `Pose3`) type used in `compute_reprojection_residual`.
- `_docs/02_document/common-helpers/07_helper_ransac_filter.md` — design rationale.

## Problem

Four components run RANSAC over 2D-2D correspondences (C2.5, C3, C3.5) or compute reprojection residuals after PnP (C4). Without a shared helper:
- Each component grows its own `cv2.findHomography(..., cv2.RANSAC)` wrapper; some set the RNG seed, some don't, producing non-determinism in tests.
- The "what counts as the residual" definition drifts — some compute mean, some median, some mean-of-squares — so the FDR `mre_px` field means different things in different components.
- C4's "final per-frame residual" used by FDR provenance ends up with a slightly different formula than C3.5's "did refinement help?" residual; cross-component comparisons in post-flight analysis become apples-to-oranges.

## Outcome

- A single `helpers.ransac_filter` module is the only path through which any onboard process runs 2D-2D RANSAC or computes a reprojection residual.
- RANSAC is deterministic given a fixed seed: `cv2.setRNGSeed(0)` (or the explicit `seed` kwarg where supported) is set inside `filter_correspondences` so the same input correspondences always produce the same `RansacResult`.
- Residual statistic is fixed to the MEDIAN (in pixels), not mean — outliers in the 2D residual distribution do not bias the consumer's quality signal. Documented in the contract.
- C4's `solvePnPRansac` continues to use OpenCV's internal RANSAC (out of contract); this helper covers the standalone 2D-2D case + the standalone reprojection-residual computation that lives outside the PnP call.

## Scope

### Included

- `RansacFilter` static methods: `filter_correspondences`, `compute_reprojection_residual`.
- `RansacResult` frozen dataclass with `inlier_correspondences`, `inlier_count`, `outlier_count`, `median_residual_px`.
- `RansacFilterError` exception type.
- Public interface contract published at `_docs/02_document/contracts/shared_helpers/ransac_filter.md`.

### Excluded

- 2D-3D RANSAC inside `solvePnPRansac` — OpenCV does it internally; this helper does not wrap it.
- Per-component RANSAC threshold defaults — they are documented in C2.5, C3, C3.5, C4 specs. This helper takes the threshold as a parameter.
- Adaptive RANSAC (PROSAC, USAC) — out of scope for v1.0.0.
- GPU-accelerated RANSAC — out of scope for v1.0.0.
- Confidence / iteration-count tuning of `cv2.findHomography` — exposed only via `ransac_threshold_px` for v1.0.0.

## Acceptance Criteria

**AC-1: Clean correspondences yield 100 % inliers + zero residual**
Given 100 correspondences generated from a known homography (no outliers, no noise)
When `filter_correspondences` runs with `ransac_threshold_px=1.5`
Then `RansacResult` has `inlier_count == 100`, `outlier_count == 0`, `median_residual_px ≈ 0.0` within `atol=1e-6`

**AC-2: Mixed correspondences produce expected inlier band**
Given 80 inliers (perfect homography) + 20 outliers (random noise)
When `filter_correspondences` runs with `ransac_threshold_px=1.5`
Then `inlier_count` ∈ `[78, 82]` (RANSAC noise tolerance) AND `outlier_count == 100 - inlier_count`

**AC-3: Determinism — fixed seed**
Given the same input correspondences
When `filter_correspondences` runs twice
Then both `RansacResult` outputs are byte-equal (numpy arrays equal-by-value, all fields match)

**AC-4: Reprojection residual ≈ 0 on clean inliers + known pose**
Given 4+ correspondences generated from a known camera intrinsics + pose pair (no noise)
When `compute_reprojection_residual` runs
Then the result is ≈ 0.0 within `atol=1e-6`

**AC-5: Empty inlier array → NaN, no exception**
Given an empty inlier array (shape `(0, 4)`)
When `compute_reprojection_residual` runs
Then it returns `NaN` (no exception)

**AC-6: Shape contract on correspondences**
Given correspondences with shape `(N, 3)` (missing one coordinate column)
When `filter_correspondences` runs
Then `RansacFilterError` is raised mentioning the expected shape `(N, 4)`

**AC-7: Threshold guard**
Given `ransac_threshold_px = -1.0`
When `filter_correspondences` runs
Then `RansacFilterError` is raised mentioning the positive-threshold requirement

**AC-8: Minimum point count**
Given correspondences with shape `(3, 4)` (fewer than 4 points)
When `filter_correspondences` runs
Then `RansacFilterError` is raised mentioning the 4-point minimum for homography RANSAC

**AC-9: K shape contract in residual call**
Given `K` of shape `(4, 4)`
When `compute_reprojection_residual` runs
Then `RansacFilterError` is raised mentioning the expected `(3, 3)` shape

**AC-10: No upward imports (Layer 1 invariant)**
Given the helper module
When a static-import check runs
Then it imports ONLY from `_types`, `helpers.se3_utils`, `cv2`, `numpy`, and stdlib — no `gps_denied_onboard.components.*` imports anywhere

## Non-Functional Requirements

**Performance**
- `filter_correspondences` p99 ≤ 5 ms on Tier-2 for `N=200` correspondences (matches the C3 cross-domain matcher's per-candidate budget).
- `compute_reprojection_residual` p99 ≤ 1 ms on Tier-2 for `I=100` inliers.
- Helper-level overhead vs. inline OpenCV ≤ 5 % (per E-CC-HELPERS hot-path NFR).

**Reliability**
- `RansacFilterError` is the ONLY exception type the public surface raises on shape / dtype / threshold violations. OpenCV's lower-level exceptions MUST be wrapped.
- Determinism: same input → byte-equal `RansacResult` outputs (RNG seed is set inside the helper).

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1 | 100 clean correspondences, threshold 1.5 px | `inlier_count == 100`; `median_residual_px ≈ 0.0` |
| AC-2 | 80 inliers + 20 outliers | `inlier_count ∈ [78, 82]` |
| AC-3 | same input run twice | byte-equal `RansacResult` outputs |
| AC-4 | clean inliers + known pose | residual ≈ 0.0 |
| AC-5 | empty inlier array | returns `NaN`; no exception |
| AC-6 | shape `(N, 3)` | `RansacFilterError`; mentions `(N, 4)` |
| AC-7 | `ransac_threshold_px = -1.0` | `RansacFilterError`; mentions positive threshold |
| AC-8 | shape `(3, 4)` | `RansacFilterError`; mentions 4-point minimum |
| AC-9 | `K.shape = (4, 4)` | `RansacFilterError`; mentions `(3, 3)` |
| AC-10 | importlinter / grep gate | no `components.*` imports |
| NFR-perf | microbench `filter_correspondences` (10k iters on Tier-2 fixture, N=200) | p99 ≤ 5 ms |
| NFR-perf-residual | microbench `compute_reprojection_residual` (10k iters on Tier-2 fixture, I=100) | p99 ≤ 1 ms |

## Constraints

- Public surface frozen by `_docs/02_document/contracts/shared_helpers/ransac_filter.md` v1.0.0.
- Layer 1 Foundation only.
- `cv2` is the single RANSAC backend; pinned in `pyproject.toml` at AZ-263 / E-BOOT (OpenCV ≥ 4.12.0 per D-CROSS-CVE-1).
- Static-only design satisfies `coderule.mdc`.
- Helper depends on `helpers.se3_utils` for the `SE3` type alias (allowed — same Layer 1).
- No new dependency beyond what AZ-263 / E-BOOT pinned.

## Risks & Mitigation

**Risk 1: Determinism regression on OpenCV upgrade**
- *Risk*: A future OpenCV version changes the RNG seeding API; tests start failing intermittently because the fixed-seed contract no longer holds.
- *Mitigation*: AC-3 is the canary. The helper's own test suite catches the regression on dependency upgrade before consumers do. The contract test should also pin a known-good `RansacResult` fixture for a reference input so any drift surfaces immediately.

**Risk 2: Median vs mean residual confusion**
- *Risk*: A future contributor "improves" the helper to return mean residual; consumers' quality thresholds (which were tuned against median) become looser; FDR data is inconsistent across cycles.
- *Mitigation*: The contract `Invariants` section pins median. AC-1's `≈ 0.0` clean residual is the same for both, but AC-2's mixed-quality residual would shift on a mean→median change; the test fixture pins enough digits that the regression is caught.

**Risk 3: 4-point minimum too low for noisy inputs**
- *Risk*: With exactly 4 points, RANSAC has zero redundancy; one bad point makes the homography unstable.
- *Mitigation*: The 4-point minimum is the OpenCV minimum — consumers that need higher minimums set them via `min_inliers` and check `RansacResult.inlier_count` themselves. The contract documents that `min_inliers` is informational; the consumer is the gatekeeper.

## Runtime Completeness

- **Named capability**: 2D-2D RANSAC inlier filtering + median reprojection residual via OpenCV (architecture / E-CC-HELPERS / `07_helper_ransac_filter.md`).
- **Production code that must exist**: real `cv2.findHomography(..., cv2.RANSAC)`-backed filtering; real `cv2.projectPoints`-backed residual computation; real fixed-seed determinism wiring.
- **Allowed external stubs**: none — OpenCV is the production runtime.
- **Unacceptable substitutes**: hand-rolled RANSAC (numerical instability, no OpenCV-tested edge-case handling); skipping RNG seed (silent intermittent test failures); switching residual statistic from median to mean (changes consumer-visible quality signal).

## Contract

This task produces the contract at `_docs/02_document/contracts/shared_helpers/ransac_filter.md`.
Consumers MUST read that file — not this task spec — to discover the interface.
