# C5 Orthorectifier → C6 mid-flight tile gen sub-path

**Task**: AZ-389_c5_orthorectifier_c6
**Name**: C5 internal orthorectifier — produces mid-flight tile candidates for C6
**Description**: Implement the orthorectifier sub-path inside C5: when a frame has converged in the iSAM2 graph (≥1 satellite anchor + visual consistency), apply the camera intrinsics + extrinsics + the C5-known pose to orthorectify the nav-camera frame into a tile-aligned image patch; emit a `MidFlightTileCandidate(tile_id, pixels, quality_metadata, source_pose)` to C6 (via the storage interface AZ-303 `tile_store.put_mid_flight_candidate(...)`). Quality metadata: `inlier_count`, `cov_norm`, `pose_age_ms`. The orthorectifier is C5-internal (per epic spec § Scope: "orthorectifier (lives within C5 as an internal subcomponent)"); it consumes the converged pose + nav frame from a per-frame buffer; it emits at most ONE candidate per frame (gated by quality thresholds: `cov_norm < threshold` AND `inlier_count > floor`). Triggered after a successful `current_estimate()` call when quality conditions hold.
**Complexity**: 3 points
**Dependencies**: AZ-384 (`current_estimate` body + cov norm), AZ-385 (only emit candidates when source_label == SATELLITE_ANCHORED), AZ-303 (`TileStore.put_mid_flight_candidate`), AZ-263, AZ-269, AZ-266, AZ-272 (FDR)
**Component**: c5_state (epic AZ-260 / E-C5)
**Tracker**: AZ-389
**Epic**: AZ-260 (E-C5)

### Document Dependencies

- `_docs/02_document/contracts/c5_state/state_estimator_protocol.md`.
- `_docs/02_document/components/07_c5_state/description.md` — orthorectifier mention; § 1 downstream "C6 (mid-flight tile gen via orthorectifier)".
- `_docs/02_document/contracts/c6_tile_cache/tile_store.md` — `put_mid_flight_candidate` API.

## Problem

Without this task, the system never emits mid-flight tile candidates → C6's cache never grows in flight → AC-NEW-3 (mid-flight tile gen) is unachievable.

## Outcome

- `src/gps_denied_onboard/components/c5_state/_orthorectifier.py` defining:
  - `Orthorectifier` class (component-internal; not in `__all__`).
  - Method: `try_emit_candidate(frame, pose_estimate, cov_6x6, inlier_count, source_label) -> MidFlightTileCandidate | None`.
  - Quality gates: `cov_norm < cov_threshold` AND `inlier_count > inlier_floor` AND `source_label == SATELLITE_ANCHORED`.
  - Orthorectification math: project nav-camera frame to tile plane via camera intrinsics + extrinsics + pose; nearest-neighbour or bilinear sampling.
- Hook in `GtsamIsam2StateEstimator.current_estimate()` post-emission (or post-`add_pose_anchor` — implementer choice; gated to fire AT MOST once per frame).
- ESKF estimator: also has the hook (mid-flight tile gen is independent of state-estimator strategy).
- Configurable thresholds in `config.state.orthorectifier.{cov_norm_threshold, inlier_floor}`.
- Defensive: skip emission silently if quality gates fail (NOT a degraded-mode error; tile gen is opportunistic per AC-NEW-3).
- DEBUG log on every emission attempt; INFO log on first emission per flight.
- Unit tests: known pose + frame → expected orthorectified output; quality-gate skip behaviour; emission rate-limit (once per frame).

## Scope

### Included
- `Orthorectifier` impl.
- Hook in `current_estimate` for both estimators.
- Quality-gate logic.
- Configurable thresholds.
- Unit tests.

### Excluded
- The C6 `tile_store.put_mid_flight_candidate` body — owned by AZ-303 / E-C6.
- C6's downstream tile-cache eviction integration — owned by AZ-308.
- The orthorectification kernel optimisation — production-acceptable kernel uses NumPy or OpenCV `cv2.warpPerspective`; CUDA optimisation is a feature-cycle improvement.

## Acceptance Criteria

**AC-1: Orthorectification correctness** — synthetic camera pose + planar tile → output pixels match expected projection within 1-pixel tolerance.

**AC-2: Quality gate skip** — `cov_norm > threshold` → no candidate emitted; DEBUG log only.

**AC-3: Source label gate** — `source_label != SATELLITE_ANCHORED` → no emission.

**AC-4: Once-per-frame rate limit** — even if `current_estimate` is called multiple times for the same frame, at most ONE candidate is emitted.

**AC-5: Both estimators participate** — iSAM2 + ESKF both attempt candidate emission.

**AC-6: Composition wiring** — the orthorectifier is constructed inside the estimator at `__init__` time; `tile_store` is constructor-injected.

**AC-7: First-emission INFO log** — `kind="c5.state.first_mid_flight_candidate"` with `{frame_id, tile_id, cov_norm}`.

**AC-8: Defensive skip on missing inputs** — if `frame` or `pose_estimate` is None, skip silently with DEBUG log (NOT an error).

## Non-Functional Requirements

- `try_emit_candidate` p95 ≤ 30 ms (orthorectification kernel cost).
- Memory ≤ 50 MB resident (frame buffer + working memory).

## Constraints

- Component-internal (not in C5 `__all__`).
- Once-per-frame rate limit.
- Quality gates are mandatory; AC-NEW-3 gain is contingent on emitted candidates being high-quality.

## Risks & Mitigation

- **Risk: Orthorectification produces low-quality tiles under degenerate pose** — quality gates filter; if still problematic, AZ-308 cache-eviction policy filters at storage time.
- **Risk: AZ-303 `put_mid_flight_candidate` API not yet stable** — this task ships against the documented API surface.

## Runtime Completeness

- **Named capability**: orthorectifier → mid-flight tile candidate emission.
- **Production code**: real orthorectification kernel (NumPy or OpenCV), real quality gates, real tile_store.put_mid_flight_candidate call.
- **Unacceptable substitutes**: emitting raw nav-frame pixels (not orthorectified); skipping the quality gates (AC-NEW-3 corruption).
