# wheel_kp_v2 — real-footage fine-tune (Phase B-real)

`wheel_kp_v1` (CARLA p2_029) fine-tuned on **2,236 pseudo-labeled real PassBox frames**
(1,800 train from 174 riders + 436 rider-disjoint holdout used for eval only).
Same architecture, IO, and sidecar contract as v1 — **drop-in** for the
onnxruntime / onnxruntime-web pipeline (`src/wheel_kp_infer.py`). v1 remains the
CARLA-domain model; v2 is the real-footage model.

## Files
- `wheel_kp.onnx` — single-file export (Git LFS). input `image[batch,3,512,512]`, output `heatmaps[batch,8,128,128]`.
- `wheel_kp.pt` — torch checkpoint, run2 epoch 7 (Git LFS).
- `wheel_kp.json` — sidecar (kp order, input/heatmap size, mean/std, provenance).
- `metrics.json` — full census + localization tables and QA summary.

## Why v2: firing census on real footage (43 holdout riders, 3,386 tracked frames)
fire@t = fraction of frames where the max contact-kp score >= t.

| condition | n | v1 @0.3 | v1 @0.5 | v2 @0.3 | v2 @0.5 | gain @0.5 |
|-----------|-----|---------|---------|---------|---------|-----------|
| all       | 3386 | 0.465 | 0.215 | 0.769 | **0.636** | 3.0x |
| clear     | 2756 | 0.466 | 0.224 | 0.779 | **0.650** | 2.9x |
| night     | 95   | 0.210 | 0.063 | 0.568 | **0.484** | **7.7x** |
| rain      | 86   | 0.546 | 0.174 | 0.826 | **0.744** | 4.3x |
| overcast  | 373  | 0.579 | 0.231 | 0.815 | **0.635** | 2.8x |
| fog       | 28   | 0.179 | 0.107 | 0.500 | **0.357** | 3.3x |
| dusk      | 48   | 0.083 | 0.042 | 0.271 | 0.104 | 2.5x (still weak) |

Localization vs v1-CONFIDENT pseudo-labels (holdout PNGs, n=1,095 kps): v2 median
**1.69 px** (p95 7.14) — no catastrophic forgetting of what v1 already knew.

Visual QA of "new fires" (v2 >= 0.5 where v1 < 0.15; n=238): 14 overlays viewed
across night/rain/overcast/clear — **13/14 correct** (kps on tyre hub/contact),
1 night frame marginal (heavy motion blur).

## Honest caveats
- **Pseudo-label circularity.** Train labels are v1 predictions (confident kps kept,
  weak ones propagated by track geometry). The census + spot-check QA measure firing on
  frames v1 missed, but there is no independent human ground truth yet. The hardware
  cm ground truth (PassBox) is the eventual arbiter via the IPM pipeline.
- **New-fire QA sample size.** 14 of 238 new-fire frames were eyeballed; night has only
  2 holdout clips among them.
- **Dusk stays weak** (10.4% @0.5, n=48). Do not rely on v2 at dusk/dawn.
- **Score inflation.** v2 scores run hot vs v1 (p50 0.28 -> 0.66 overall); a 0.5
  threshold on v2 is NOT the same operating point as 0.5 on v1. Recalibrate any
  downstream threshold against the census tables in `metrics.json`.
- v1's 0.04 px localization row is self-consistency (evaluated against its own
  confident labels), not accuracy.

## Training recipe (run2)
- init = `models/wheel_kp_v1/wheel_kp.pt`; input 512 / HM 128; 30 epochs, bs 16,
  lr 2e-4 cosine, seed 0; aug: dark 0.35, blur 0.25, lowres 0.25;
  weights: occ 2.0, near 2.0, front-far 2.0, rear-far 1.5.
- best.pt = epoch 7 (val_px_err 36.26 vs pseudo-labels incl. propagated kps —
  watch the trend, not the level); later epochs drift (45.6 by ep30).
- Trainer: commit `8665d79` (per-epoch atomic checkpointing) with fine-tune/sim2real
  levers from `e016fec`/`caabbd4`. Dataset builder + eval harness:
  `realworld/phaseBreal-code/` (pseudolabel/selection/finalize/eval_real/qa_v2),
  run ledger in `realworld/phaseBreal-code/ledger.md`.

## Provenance
Base: CARLA p2_029 (`models/wheel_kp_v1`). Fine-tune data: real PassBox close-pass
clips, rider-disjoint split (`split.json`), weather from the weatherhunt manifest.
Eval: Phase B-real census/localization report, 2026-07-29.
