Sprint auto-split experiments — morning report (2026-10-09 overnight)

Result label everywhere: App-Measured. Tape owns the metre. No auto-DQ. Every auto event below is auto_detected_stub, humanReviewed=false. Nothing was sent, posted or hosted. No app code was changed (branch sprint-direction untouched; 323 tests not affected).

1. Footage found (details + URLs in the footage page)

No free clip matches the ideal (fixed tripod, cones at known distances, behind/front). Best available: | clip | licence | use | |---|---|---| | yt_wKP0iWaAlZI_30m_sprinting.mp4 (YouTube, 1280x720 30 fps, 6 reps) | CC BY | main test set: static ground-level camera, athlete runs straight at lens (true down-the-line). No cones; real distance unknown | | yt_3hREO5HBdw0_block_start.mp4 (25 fps) | CC BY | start-detection test (side view, block start) | | yt_A4hq-rH9z_I_30m_dash.mp4 | CC BY | handheld/panning failure case | | enugu_100m_boys.webm, athletissima_100m_f.ogv (+3 spares) | CC BY-SA 4.0 / CC BY 3.0 (Wikimedia Commons) | multi-runner, moving camera failure cases | Local assets (Gautam beep-test clips, shuttle videos) are not sprints and were not used. Pexels blocks scripted download (not circumvented); non-CC YouTube clips not downloaded.

2. What was run

3. Results

Start (first movement) vs manual frame inspection (my reading of 12-tile close-ups, itself ±1–2 frames)

run auto frame manual error
0 356 354 +2 f (+67 ms)
1 1384 1383 +1 (+33 ms)
2 2844 2845 −1 (−33 ms)
3 4225 4230 −5 (−167 ms; slow crouch release, triggered early)
4 4988 4986 +2 (+67 ms)
5 5760 5758 +2 (+67 ms)
3hRE (25 fps, side) 193 192 +1 (+40 ms)
**Start: 7/7 found; median signed error +40 ms, median error 67 ms; 6/7 within ±67 ms; worst −167 ms.**

Split-gate crossings (shoulder-scale gates)

Pose detection coverage vs runner size (6 reps; reference = crop pass)

runner bbox height frames full-frame yolov8s-pose@960 x3 crop pass
60–100 px (far) 585 0 % 100 %
100–150 px 197 0 % 100 %
150–250 px 802 14 % 100 %
Full-frame detection only starts ~190 px tall (~7–8 m out at assumed scale); upscaled crop needed for far start. Reference is the crop pass itself, so crop's 100 % is optimistic.

Frame-rate sensitivity (30 fps gate times subsampled; error vs 30 fps result)

rate with sub-frame interpolation: median / mean abs / p90 / max first-sample-over-threshold (no interp): median / mean abs
15 fps −4 / 24 / 49 / 118 ms +28 / 39 ms
10 fps −4 / 43 / 107 / 248 ms +64 / 83 ms
Take-away: ≥30 fps with interpolation; 60 fps recommended (quantisation alone is ±17 ms at 60, ±33 ms at 30).

Moving-camera clips (failure modes, measured)

Median global camera motion px/frame / % frames moving >2 px: wKP0 static 0.00 / 4 %; A4hq (handheld) 29 % moving, peaks 334 px/frame; Enugu 42 %; 3hRE 56 % (camera drifts 1–3 px/f even before the start); Athletissima broadcast 78 %. Fixed image gates are meaningless there; I did not run gate detection on them. Other failure modes seen: multi-person (8 lanes/crowd — tracker ID swaps, bystanders), tiny far athlete, bush false positives, pose loss when athlete fills frame (<~2–3 m), arms occluding shoulders, motion blur at the lens.

4. Simulated app output

out/sim_tables.md, out/sim_runs.json (full beep-sprint-run/1 JSON for each of 6 reps, auto start vs manual start). Example run 1 (auto start): 3 m 0.873 s, 5 m 1.287 s, 7 m 1.600 s; intervals 0.873/0.414/0.313 s (3.44/4.83/6.39 m/s); profile "Still speeding up at the finish (fastest 5–7 m, 6.39 m/s)…"; flags "not yet confirmed by a person" ×3. Insight: start error shifts all cumulative splits equally but leaves later intervals unchanged. Distances here are proxy metres — illustrative only, not a result. Note StartSource has no auto value; an auto start currently has to be tagged video_scrub/go_cue — suggest adding AUTO_DETECTED.

5. Visuals (out/)

6. Verdict

7. Recommended capture setup

Tripod/fixed phone (no pan, no handheld, no auto-zoom/stabilisation), 60 fps (30 minimum), locked exposure/short shutter. Place the phone 8–10 m behind (or in front of) the start line on the line of the lane, 1.0–1.5 m high (not on the ground), framing the whole course incl. start; or a fixed side/oblique view 15–20 m away with the full course visible (best for per-metre accuracy; down-the-line mainly compresses distance). Cones/flat markers at 0, 5, 10, 20 m placed with tape on both sides of the lane (wide enough to be visible from behind), high-contrast colour. One athlete in frame; others out of shot. Athlete in plain contrasting clothing.

8. Next build steps (proposed, no code changed)

  1. Calibration on frame 1: user taps the cone/line points (or 4 lane corners) → homography to ground metres; extend SprintCrossingDetector.detect to take calibration (image points + tape metres) and return confidence; add StartSource.AUTO_DETECTED.
  2. Detector: pose (ML Kit/MediaPipe on device) on a crop around the athlete with tracking; start = activity burst; crossing = pelvis ground-projection through gate line, sub-frame interpolation, monotone envelope.
  3. Review UI: proposals appear as unconfirmed ticks on the scrub bar (existing flags text); never auto-judge.
  4. Collect a small marked-course dataset (Gautam/Luke, 10 m + 5 m cones, 60 fps, ≥20 runs) and measure real gate error vs manual frames before shipping any detector.

9. Risks / limits

Manual references are mine and ±1–2 frames; only 6 reps of one athlete/one camera; scale assumptions (f, shoulder width) unverified; CPU model, not a phone model; far-pose crop positions were hand-chosen per clip; clips from YouTube CC BY (attribution needed if shown). Detection numbers are on a clean static clip — real-world will be worse.

Files: scripts pose_track.py pose_crop.py sprintdet.py wk.py analyse_wk.py exp_wk.py exp_final.py render.py sprintmath.py camshake.py; tracks work/tracks/*.json; metrics out/results.json.


10. Luke's own clip — footage/luke_run_20261011.mp4 (2026-10-11)

All numbers: App-Measured, proxy / illustrative, unreviewed (auto_detected_stub). No marker distance exists, so metres are assumed-scale. Nothing sent/deployed. Code: luke_analysis.py luke_sim.py luke_render.py; tracks work/tracks/luke_crop2.json; output out/luke_sim.json, out/luke_summary.md, out/luke_run{1,2}_annotated.mp4 (+_keyframes.jpg).

Video facts. H.264, 368x496 portrait (low-res, ~3:4 downscale), 30 fps, 1032 frames, 34.40 s. 0–5.9 s handheld/fingers over lens/camera being set down (settles ~frame 172); static ground-level camera, tilted up at trees, sun behind runner (backlit). Runner (white tee, grey shorts, sandals) ≈ 25–40 px tall at the far end.

Timeline verified by frames (30 fps; ±~6 f pass-by, ±~8 f turn). The watcher's times are roughly right but the start is not a standing start: the runner starts beside/behind the camera and is first seen as a foot at frame 176 (5.87 s), passing the lens ~f188 (6.3 s). Run 1: pass f188 → turn ~f408 (13.6 s; side-on f396–420) → returns, passes camera ~f580–600. Run 2 is a flying/rolling start: the returning runner passes the lens ~f603 (20.1 s) and carries straight on away from camera; turn ~f795 (26.5 s; side-on f783–805); back at lens ~f940 (31.3 s). So "first movement" is not observable in either run (run 1 starts out of shot, run 2 has no stop). Manual reference START = camera pass-by (not first movement).

Pose detection / tracking. Full-frame never tested at far range (earlier work: 0 % below ~150 px); used the x3 upscaled crop (pose_crop.py, crop x40–340,y0–340). Runner found in 323 frames of run 1 window (f176–560; 84 %) and 299/346 of run 2 (f600–945; 86 %); misses are at the lens (body mostly out of frame) and the dim turn. Pose at 25–40 px tall is coarse (box heights still stable). Runner reaches the assumed-scale ~20 m (plausible range 16–25 m) at the turn.

Start detector vs manual. Activity/first-confident-pose trigger: run 1 first pose f178 (−10 f, −333 ms vs pass-by ref), activity burst f214 (+26 f, +867 ms — fires only once the body is in view and swinging); run 2 f608 / f606 (+5 / +3 f, +167 / +100 ms). I.e. it detects "runner enters / starts running in view", within ~±100–330 ms of my reference when the runner is in shot, but it cannot detect a true start that happens off-frame. Not comparable to the 6/7 ±67 ms result on the CC BY footage.

Distance proxy. Pinhole: Z = f·1.75 m / bbox height (px), f=413 px (assumed ≈48° horizontal FOV; real f unknown), h smoothed. Valid only when the runner is >~3.8 m (h<190 px); the first ~4 m is extrapolated. Uncertainty: FOV/f ±25 % dominant, height ±3 %, upward camera pitch/posture/box noise → metres ±25–30 %. Monte-Carlo (scale 0.8–1.25×, start ±6 f) gives 10–90 % ranges below.

Relative (scale-free) timing, time from pass-by to reach a fraction of the turnaround distance: run 1: 25 % 2.30 s, 50 % 4.20 s, 75 % 5.30 s, 90 % 6.47 s (turnaround reached ~7.5–8 s). Run 2: 25 % 1.80 s, 50 % 3.30 s, 75 % 4.87 s, 90 % 5.63 s. Run 2 is ~25 % faster early (rolling start) — this part is robust to the metre-scale problem.

Simulated app output (same sprintmath.py; start source tagged video_scrub, see §4 note): | config | run | split 5 m | split 10 m | split 20 m | intervals (s, m/s) | |---|---|---|---|---|---| | 10 m (marks 5,10) | 1 | 2.30 s (1.9–2.7) | 4.18 s (3.4–4.7) | – | 0–5 2.30 s 2.17; 5–10 1.87 s 2.67 (2.5–3.2) | | 10 m | 2 | 1.80 s (1.5–2.2) | 3.31 s (2.7–4.0) | – | 0–5 1.80 s 2.78; 5–10 1.51 s 3.31 (2.9–4.0) | | 20 m (turn≈20 m; 20 m = turnaround plateau onset) | 1 | 2.31 | 4.21 | 7.59 | 10–20 3.38 s 2.96 m/s | | 20 m | 2 | 1.80 | 3.31 | 5.73 | 10–20 2.42 s 4.13 m/s | Profile line for 10 m (both): "Still speeding up at the finish (fastest 5–10 m …). Top speed not shown on this distance." Both runs are jogs/rolling runs (~2–4 m/s), not sprints (nowhere near 7–9 m/s). The 20 m column depends on the assumption turnaround = 20 m and the runner decelerating into the turn contaminates 10–20 m; treat as illustration of the UI only. Review flags: every mark "not yet confirmed by a person".

Verdict / comparison with CC BY footage. Earlier clip (wKP0): static camera 1.0 m-ish, runner comes toward the lens from a visible crouched start, 1280x720, 6 reps, start ±67 ms typical. Here: 368x496, runner already moving at first sight, fully backlit, no start posture, runner far (25–40 px), scale unknown, run 2 has no start at all. Start detection that worked there is not applicable; gate timing is dominated by ±25 % scale uncertainty (≈±0.3–0.5 s on the 10 m split) plus ±6 f start ambiguity. Useful as a demonstration that the pipeline runs end-to-end on real phone footage; not as a measurement.

What would make it trustworthy: (1) phone on a tripod/stable object 1–1.5 m high, 8–10 m behind the start line (or side-on 15–20 m away) so the start line, the athlete in the set position and the whole 20 m are in frame; (2) cones/tape at 0, 5, 10, 20 m (bright, both sides of lane) visible in frame — then the app taps them for calibration instead of assuming height/FOV; (3) a real standing start (static set position, a visible first movement), runner starting in frame; (4) 60 fps (30 minimum), 1080p, locked exposure/no auto-zoom, sun not directly behind the athlete; (5) a hard sprint, not a jog, ideally ≥3 reps with the same setup; (6) turnaround well beyond the last mark (e.g. 30 m) so the 20 m mark isn't confused with deceleration.


11. Iterations — app v0.2.2 → v0.2.6 (2026-10-11, branch sprint-direction)

Full per-iteration detail with tables: out/iterations.md (+ out/iter2_results.txt, iter3_results.json, iter4_results.txt, iter4_quality.json, baseline_v0.2.1.json). Code: motion_signal.py (frame-difference sampler), startsuggest.py (reference of Kotlin StartSuggester), calib.py (reference of Kotlin CourseCalibration), iter*_eval.py, iter_metrics.py, build_iter.sh. All numbers App-Measured / proxy / unreviewed; every auto event is an unconfirmed suggestion; nothing sent or deployed. No emulator or phone was used: "unit-tested" = JVM tests; "compile-checked" = builds into the APK, never run.

it version (code) tests change evidence (before → after) verified unmeasured
1 0.2.2 (48) 353 Golf flag poles: gold pole+flag on the AR floor plane at every split + finish, labelled, glow/pulse when reached; flag strip on capture (taps) and review (split times vs scrub) UX request, sim cannot measure flag state logic, geometry, glow all rendering (GL, labels, z-order, clutter at 50/60 m)
2 0.2.3 (49) 375 Start as an UNCONFIRMED SUGGESTION: frame-difference motion → StartSuggester; AUTO_DETECTED / first-appearance / pass-by start sources; suggested tick on scrub bar; accept by tap before: no help (manual scrub). After (6 static reps): median abs 83 ms, worst 200 ms vs my manual frames (pose research method 67 / 167 ms); rolling/handheld cases → low confidence + alternatives Kotlin == Python on 6 real signals phone decode speed; Luke's runs and the drifting-camera clip are NOT solved (1.0–1.2 s late, or wrong)
3 0.2.4 (50) 383 Cone/tape calibration (1-D projective map), crossing proposals (isotonic, sub-frame), quality warnings, calibration saved in run JSON before: metre unavailable from video (±20 % scale ≈ ±150–200 ms). After, synthetic: side-on 20–50 ms; behind-the-start 1.2 m: −239/−264/−329 ms median at 5/10/20 m (p90 357/420/714) — warning fires in all behind cases maths, warnings, parity with Python any real marked-course footage; foot proxy on real runners
4 0.2.5 (51) 390 Footage check (handheld/low-res/low-fps/short/coverage), indicative ± per split, relative-time block, JSON footage + uncertainty handheld flag right on 6/6 clips at clip level, frame recall 0.28–0.88; ±1 frame covered 3–8 % (behind) of simulated errors → new range 92–98 %; sub-frame interpolation removes the quantisation error (idealised sim) rules, uncertainty maths, JSON ± for person-set marks is an assumption; slow drift under-detected; wording usefulness
5 0.2.6 (52) 397 Open a video from the phone (review/test footage); regression of 0.2.1 fixes; end-to-end pure pipeline test; docs no algorithm change; all evaluations re-run, identical 0.2.1 fixes, whole pure pipeline picker/copy/review flow on a phone

Honest bottom line. What is solid: the start suggestion on a static, ≥720p camera (≈ ±0.1 s vs my manual frames), the calibration maths, the uncertainty wording, the 0.2.1 fixes. What is not: Luke's low-res handheld rolling runs (suggestions are late/wrong and the app now says "poor footage"), anything on a moving camera, and every piece of UI (never run). Calibration from behind the start line is geometrically poor at 10–20 m (3–4 px tap error = 1–3 m) — the app warns; a side-on view is ~10x better in the sim.

Film next (a marked-course test): phone on a tripod/stable object, 1.0–1.5 m high, 60 fps, 1080p, locked exposure, no hand on the phone; cones/tape at 0, 5, 10, 20 m placed ON the lane line; start with the camera SIDE-ON ~15 m away looking at the 10 m cone (best for calibration) and again from 8 m behind the start; athlete in the set position for ≥2 s before going, then runs; ≥10 runs each; keep one clip with a rolling start. Note the tape-measured cone distances and the phone model. Then open them in the app via "Open a video from this phone".

← Experiments overview