Skip to content

Phase B5: sensor-trust scoring + adaptive Kalman by bbox area - #13

Merged
mandarwagh9 merged 9 commits into
mainfrom
feat/phaseB-5-sensor-trust
Jun 19, 2026
Merged

mandarwagh9 merged 9 commits into
mainfrom
feat/phaseB-5-sensor-trust

Conversation

@mandarwagh9

Copy link
Copy Markdown
Owner

What & why

Phase B5 — the final feature of the 10× roadmap. The Kalman update accepted a sensor_trust param but it was always 1.0, and measurement noise scaled only by confidence.

⚠️ Top of the stack (#12 → … → #4). Auto-retargets as the stack merges.

Changes

  • KalmanFilter.update now scales measurement noise R by confidence × sensor_trust × bbox-area factor, and returns the innovation magnitude.
  • World model tracks per-camera trust in [0.1, 1.0]: consistent measurements (low innovation) raise it, outliers decay it. _bbox_area_factor maps bbox area → a [0.1, 1.0] reliability factor (larger bbox = closer = lower noise). Both feed kf.update. Exposed via get_sensor_trust.
  • Configsensor_trust_innovation_threshold + bbox_reference_area as real Settings fields (env-wired) + .env.example docs.
  • README — flipped sensor-trust + adaptive-Kalman-by-area to ✅ implemented; refreshed the status note (the only remaining 🔭 items are the deferred DeepSORT/centroid chain and the out-of-scope compass ribbon / threat ring).

Tests (TDD)

8 new in test_sensor_trust.py: innovation return value, higher area-factor trusts the measurement more, trust rises on consistent / decays on outlier / clamped to [0.1,1], default trust = 1.0, area-factor bounds, config wiring.

Verification

  • ruff clean · mypy clean (18 files) · 111 tests pass (was 103).
  • Coverage 59% → 60% (floor 50%).

🎉 Roadmap complete

With this PR, every feature the README advertises is genuinely implemented and tested. The only items still marked 🔭 are explicitly deferred (DeepSORT fallback chain) or out of scope (compass ribbon, threat ring).

🤖 Generated with Claude Code

mandarwagh9 and others added 9 commits June 19, 2026 18:54
Phase A1 of the 10x roadmap. The README advertised several capabilities as
shipped that aren't wired into the live pipeline; this marks them as planned and
fixes concrete drift.

- Add an "Implementation status" table up front (source of truth: ✅ vs 🔭)
- Mark 🔭 planned: cross-camera homography (H-PROJ), pixel extrapolation (EXTRAP),
  appearance re-ID, sensor-trust scoring, adaptive-Kalman-by-area, GPS/IMU fusion,
  DeepSORT/centroid fallback chain, compass ribbon, threat ring
- Reframe the homography section as forward-looking design
- Note only the orange WORLD ghost path is emitted today
- Fix mobile handshake (no target_fps is sent; sensor_data is received, not fused)
- Update Testing section for the new frontend tests + comprehensive CI gate
- pyproject: allow Python 3.13 (requires-python <3.14; verified locally)
- .env.example: remove the duplicated JWT/AUTH block

No code behaviour change; backend gate green (ruff/mypy/57 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…AMERA_POSITIONS

Phase A2 of the 10x roadmap. Previously, with no CAMERA_POSITIONS configured,
pixel_to_world() returned None for every camera, so the world model produced
ZERO world objects and ZERO predictions — viewers saw only raw detections and
per-camera tracks, and the "fusion" layer silently no-opped.

- Add WorldModelRepositoryImpl._ensure_calibration(): lazily synthesize a default
  calibration (cameras spread along x-axis, height 2.5 m, focal 800) when a camera
  has none, with a one-time warning. Explicit CAMERA_POSITIONS still take precedence.
- Wire it into _process_track and generate_predictions so both track fusion and
  view-only prediction cameras work out of the box.
- Tests: 4 new (objects created without config; lazy creation for unseen camera;
  explicit positions win; predictions don't raise for a second camera).
- Docs: backend/ARCHITECTURE.md no longer claims CAMERA_POSITIONS is required.

Backend gate green: ruff + mypy clean, 61 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase A3 of the 10x roadmap. CameraTracker.update() previously mapped Hungarian
row indices back to tracks via `list(self.tracks.keys())[idx]` inside its loops —
O(n^2) and implicitly coupled to dict iteration order matching the cost-matrix
build order. Correct today, but fragile.

- _associate_detections now returns (matched[(track_id, det_idx)],
  unmatched_track_ids, unmatched_det_indices); update() consumes track ids
  directly. Behaviour-preserving; removes the ordering coupling and the
  per-match key-list rebuilds.
- Add 6 characterization tests for the CameraTracker lifecycle (create → confirm
  → coast → remove), including multi-track id-mapping correctness.

Coverage: tracking_adapter 46%→85%, total 42%→49%. ruff/mypy clean, 67 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase A4 of the 10x roadmap — completes Phase A (the honest & solid base).

- Add msgpack round-trip tests for WebSocketCommunicationRepository._serialize_*
  (snapshot envelope, JPEG frame encoding, detection/track/world-object/prediction
  shapes, empty snapshot). This pins the exact wire format the frontend decodes.
- CI: install fastapi + msgpack (needed to import the ws adapter) and raise the
  coverage floor 40% -> 50%.

websocket_adapter 0%->46%, total 49%->52%. ruff/mypy clean, 74 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… matching

Phase B1 — the first feature build. Activates appearance re-ID, which was
scaffolded but dormant (compute_appearance was never called; Detection.appearance
was always None, so tracking was pure-IoU and cross-camera matching distance-only).

- detection: extract module-level compute_hsv_appearance() and populate
  Detection.appearance per detection (64-dim L2-normalized HSV histogram;
  toggle via APPEARANCE_REID_ENABLED). This also activates the appearance term
  already present in the tracking cost matrix.
- world model: gate cross-camera association on appearance cosine similarity
  (CROSS_CAMERA_APPEARANCE_THRESHOLD, default 0.5) so differently-dressed people at
  the same ground position stay separate; EMA-smooth (alpha=0.3) the fused descriptor.
- config: add appearance_reid_enabled + cross_camera_appearance_threshold as real
  Settings fields (env-wired) + .env.example docs.
- README: flip "Appearance re-ID" from planned to implemented.
- Tests: 7 new (descriptor shape/similarity/degenerate; appearance-gated matching;
  distance fallback; config wiring).

Coverage 52%->55%. ruff/mypy clean, 81 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase B2. Implements the headline cross-camera homography feature: when one camera
sees a person and another doesn't, project the foot point between views to render a
green H-PROJ ghost.

- New app/infrastructure/homography.py: HomographyEstimator accumulates foot-point
  correspondences per camera pair, estimates H via cv2.findHomography + RANSAC once
  >= min_pairs, re-estimates periodically, projects points, caps the pair buffer.
- World model integration:
  - store per-camera foot points on WorldObject (camera_foot_points)
  - collect correspondences from objects co-visible on a tick (_collect_correspondences)
  - generate_predictions tries Path A (homography, HOMOGRAPHY method) first, falling
    back to Path C (world projection) when no homography exists
  - wired from the existing HOMOGRAPHY_* Settings
- README: flip homography / H-PROJ from planned to implemented.
- Tests: 8 new (estimator: known-H recovery, identity, directionality, min-pairs,
  buffer cap, same-cam ignore; integration: predictions emit HOMOGRAPHY, correspondence
  collection for co-visible objects).

Coverage 55%->58%. ruff/mypy clean, 89 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase B3 — the third and final ghost-prediction path. When a camera loses a
person it was previously tracking (but the world object is still alive via another
camera), dead-reckon a red EXTRAP ghost from this camera's last-known pixel
position along its pixel velocity, capped by an adaptive budget.

- world model: refine the prediction skip from "ever seen by this camera" to
  "seen within a live window" (~2 frames), so a camera that LOST a target becomes
  eligible for a ghost. generate_predictions now tries Path A (homography) ->
  Path B (extrapolation) -> Path C (world projection).
- _try_extrapolation_prediction: slide last pixel by velocity * time * fps, capped
  at min(250, 80 + 40*t) px; zero-velocity stays put; no pixel history -> None.
- README: flip EXTRAP / red ghost from planned to implemented; all 3 paths now active.
- Tests: 6 new (no-history None, moves in velocity direction, budget cap,
  zero-velocity stays, integration emits EXTRAP for a lost camera, live camera skipped).

Coverage 58%. ruff/mypy clean, 95 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase B4. The mobile client streams GPS (watchPosition) + IMU
(DeviceOrientationEvent), but the backend was receiving and discarding the
sensor_data message. Now it's fused.

- New app/infrastructure/geo.py: gps_to_local() equirectangular projection.
- World model: update_camera_sensor() converts GPS -> local position (relative to
  GPS_REFERENCE_* or the first fix as origin) and DeviceOrientation alpha/beta/gamma
  -> camera rotation, overriding the auto-default calibration. Added to the
  WorldModelRepository port (concrete no-op default).
- main.py: /ws/camera handler forwards sensor_data to the world model instead of pass.
- README: flip GPS/IMU fusion from planned to implemented.
- Tests: 8 new (geo: origin/north/east projection; fusion: first-fix origin, north
  offset, configured reference, orientation->rotation, no-data no-op).

Coverage 58%->59%. ruff/mypy clean, 103 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase B5 — the final roadmap feature. The Kalman update accepted a sensor_trust
param but it was always 1.0, and measurement noise scaled only by confidence.

- KalmanFilter.update now scales R by confidence * sensor_trust * bbox-area factor,
  and returns the innovation magnitude.
- World model tracks per-camera trust in [0.1, 1.0]: consistent measurements (low
  innovation) raise it, outliers decay it; _bbox_area_factor maps bbox area to a
  [0.1, 1.0] reliability factor (larger = closer = lower noise). Both feed kf.update.
- config: sensor_trust_innovation_threshold + bbox_reference_area Settings (env-wired)
  + .env.example docs.
- README: flip sensor-trust + adaptive-Kalman-by-area to implemented; refresh the
  status note (remaining planned items are the deferred DeepSORT/compass/threat ring).
- Tests: 8 new (innovation return, area-factor effect, trust up/down/clamped, default,
  area-factor bounds, config wiring).

Coverage 59%->60%. ruff/mypy clean, 111 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant