# Ego-Motion Estimator + Telemetry Sync Gate

**Task**: AZ-662_movement_detector_ego_motion
**Name**: OpenCV optical-flow / global-motion estimator + telemetry-skew gate
**Description**: Compute per-frame ego-motion using OpenCV (Lucas–Kanade optical flow or feature-based homography), refined by the synchronised gimbal + UAV telemetry. Drop frames whose telemetry skew exceeds the per-zoom-band tolerance; never silent.
**Complexity**: 5 points
**Dependencies**: AZ-640_initial_structure, AZ-659_frame_ingest_publisher, AZ-656_gimbal_centre_on_target, AZ-649_mission_executor_telemetry_forwarding
**Component**: movement_detector
**Tracker**: AZ-662
**Epic**: AZ-629

## Problem

Naive frame differencing is rejected — the UAV and gimbal are moving, so most pixel motion is ego-motion. The estimator must (a) recover camera motion from the frame stream and (b) cross-check against telemetry (gimbal + UAV) within a per-zoom-band skew tolerance. Frames whose telemetry skew exceeds the tolerance MUST be dropped (with a counter), never silently consumed — otherwise the compensation is wrong and false positives flood the operator.

## Outcome

- `EgoMotionEstimator::estimate(frame, gimbal_state, uav_telemetry) -> Result<EgoMotion, SkewExceeded>` returns the per-frame ego-motion vector (or homography) refined by telemetry, OR rejects the frame as skewed.
- Per-zoom-band tolerance from config (defaults per `description.md §5`): zoom-out 50 ms frame↔gimbal / 100 ms frame↔UAV; zoom-in 25 ms / 50 ms.
- Health surface: `telemetry_skew_drops_total`, `optical_flow_degenerate_total`, `current_zoom_band`.

## Scope

### Included
- OpenCV bindings (Rust crate `opencv`).
- Optical-flow primary path (dense Lucas–Kanade or feature-based homography — `opencv::video::CalcOpticalFlow*` or `opencv::calib3d::findHomography`).
- Telemetry-skew gate per zoom band.
- Compensation output (the residual-pixel-motion field; downstream task 24 clusters it).

### Excluded
- Cluster persistence + candidate emission (task 24).
- Q14 fallback (task 25).

## Acceptance Criteria

**AC-1: Synthetic pure-pan: residual ≈ 0**
Given a synthetic frame pair where the camera panned by `dx` and the entire scene is static
When `estimate(frame, gimbal_state, uav_telemetry)` runs
Then the returned ego-motion captures `dx` and the residual motion field is ≈ 0 within epsilon.

**AC-2: Telemetry skew above zoom-out tolerance is dropped**
Given a frame whose gimbal-telemetry timestamp differs by 200 ms while `zoom_band = zoomed_out` (tolerance 50 ms)
When `estimate(...)` is called
Then it returns `Err(SkewExceeded)` and `telemetry_skew_drops_total{band="zoomed_out"}` increments by 1.

**AC-3: Optical-flow degenerate is observable**
Given a fully-saturated white frame
When `estimate(...)` runs
Then it returns `Err(OpticalFlowDegenerate)` and `optical_flow_degenerate_total` increments by 1.

## Non-Functional Requirements

**Performance**
- Per-frame ego-motion estimation: ≤30 ms p99 on Jetson Orin Nano (must coexist with Tier 1 + Tier 2 — per `description.md §9`).

**Reliability**
- Drops never silent.

## Runtime Completeness

- **Named capability**: ego-motion estimation using real OpenCV; telemetry-skew gating.
- **Production code that must exist**: real OpenCV optical-flow / homography path; real synchronisation logic.
- **Allowed external stubs**: synthetic frame pairs in tests; pinned `opencv` Rust crate in CI.
- **Unacceptable substitutes**: a fake/stub estimator that always returns "no motion" is unacceptable in production (would mask real movement candidates).
