Corner Geometry Spike
Where should the app get the real shape of a corner — edges, width, radius, grade, camber — when OSM is missing or coarse? Three sources were measured against each other on the Palmer library: the cached 0.22 m/px MassGIS ortho, the GPS traces, and USGS 3DEP 1 m lidar.
Verdict. Imagery is the source for plan geometry (edges, width, centreline, radius) and it works at NAIP resolution, so it is not Massachusetts-only. GPS traces already give the grade to 0.35% with no external data — lidar confirmed it rather than replaced it. The one thing neither GPS nor imagery can give is camber, and the lidar DEM gives it cleanly: T9 sits on a 15.7% cross-slope, T1/T7 on 7–8%. OSM's job shrinks to "a hint for the bbox"; at Palmer it is 0.8 m from the true centreline but sampled every 8.6 m, which is why its radii are off by 15–50% on the fast corners.
The track, as the app now sees it
MassGIS 2025 survey at its native 15 cm (z20, 0.11 m/px) along the corridor, over the z19 backdrop elsewhere. Green: the shipped track limits — the painted line where one exists, solid or dashed (the pit-lane blend at T1 and the run-off apron at T7 are fenced by dashed paint the material boundary walks straight past), the asphalt boundary otherwise; dashed white: inferred (1.8% of stations); red: kerbs. Traces are every clean lap from the 1 Aug (blue, 21 laps) and 2 Aug 2026 (orange, 22 laps) sessions. Shown reduced; switch to 1:1 and scroll.
What each source gives
Ortho imagery edges · width · radius
- Tarmac mask = colour distance from the pixels under the ridden line, sat-gated, opened/closed, small holes filled (a closed loop's infield is a hole too — filling it all cost the inside edge on the first run).
- Edges cast along the reference-line normals: median width 11.6 m, ridden band sits inside them with median 2.5 / 3.2 m of margin.
- Centreline = mid-edge. Its curvature through
osm._curvature_windowsreproduces all 13 OSM corners; boundary error ±10 m. - Downsampled to 0.6 and 1.0 m/px (NAIP-grade, the
usgsprovider): apex edges move 0.1–1.4 m, width 12.0–12.5 m. Only the paddock bleed at T1 differs.
GPS traces grade · ridden band · corner set
- Median altitude across laps vs lidar: r 0.9998, sd 0.49 m after a constant +4.46 m (geoid vs receiver datum). Grade rms 0.35%; 4 laps already give 0.49%.
- Ridden band (p2–p98 lateral) is only 4.7 m of a 12 m track — riders share one line — so GPS alone cannot find edges or width.
- The median ridden line's curvature finds 13/13 corners; the single fastest lap finds 12 (misses T3, a sweeper riders straighten). Full-rate lean is not required.
- Cannot give camber: one line, no cross-track information.
3DEP lidar DEM camber · grade with zero laps
- One
exportImagecall: 1 m float32 over the track bbox, 10 MB, seconds. EPQS point queries agree to 5 cm sd. - Cross-slope from DEM at 1.5 m inside each ortho edge: p5..p95 −6.5%..+5.4% around the lap; apex cross-sections are linear across the full width (real terrain, not edge error).
- US-only. Outside 3DEP the GPS median profile covers grade; camber is unavailable.
OSM centreline bbox hint · coarse corners
- Positionally good at Palmer: nodes are 0.8 m (median) from the ortho centreline.
- Sampled every 8.6 m (max gap 181 m), no width, no elevation. Chord error inflates fast-corner radii: T2 95 vs 81, T3 129 vs 110, T10 89 vs 51 m.
- Keep as the first bbox source and a corner-set cross-check; stop treating it as the geometry.
The ortho, read as geometry
Resolution: is the cached ortho the most detailed version?
No — and it doesn't need to be. The cached stitch is z19 (0.22 m/px on the ground) because imagery.py stops at mppMin 0.20. The MassGIS 2025 tile cache goes one level deeper, z20 = 0.11 m/px, which is the native 15 cm survey; z21+ 404s. A ±40 m corridor along the lap is 434 tiles and fetched in 9 s, versus ~1600 for a full-bbox stitch at that zoom.
Re-running the edge extraction on the z20 corridor moves the edges by a pixel or two of z19: bias −0.12 / +0.10 m, sd 0.40 / 0.23 m, p90 |Δ| 0.39 m on both sides; width 11.2 vs 11.4 m; edge jitter 0.25 → 0.20 m per metre. Resolution is not what limits the geometry — the tarmac classifier is (kerbs, paved shoulders, the paddock). Use z20 for the analysis stitch because it is cheap and resolves the kerb, keep z19 for the display backdrop, and spend the effort on the classifier.
Classifier, corrected
Correction to the section above: the first CIR sample was stitched with its origin one tile off (_tile_at on an exact tile boundary rounds to the neighbour), so NIR was being read ~30 m from where the RGB was. Re-aligned (luminance corr 0.91 at zero shift), NIR is informative and is used below.
What the cross-track profiles say
Median feature value across the corner window, along the normal from the ridden line (left +, right −), T3 and T5 at 0.25 m. The gravel shoulder has almost the same luminance as asphalt — 155–160 vs 165 — which is why a joint colour distance let it in. What separates them is chroma: R−B ≈ 4–8 on asphalt, 20–32 on the shoulder, and NIR 126–128 vs 147–190. Any single-number distance averages the decisive feature away.
| feature | asphalt (|off| ≤ 1.5 m) | robust σ | gravel shoulder | σ units |
|---|---|---|---|---|
| luminance | 167.7 | 3.6 | 155–160 | 2–3 |
| saturation (max−min) | 6.4 | 0.9 | 20–32 | 15–28 |
| NIR | 127 | ~3 | 147–190 | 7–20 |
| texture (5×5 σ) | 0.9 | 0.5 | 2–5 | 2–8 — but tyre marks and paint on asphalt reach 8 too; not used for the asphalt decision |
The rule that survived
Per-station 1-D profile with per-feature gates. Calibrate median/MAD of saturation, luminance and NIR from the pixels within ±1.5 m of the median ridden line (per ortho, no fixed asphalt colour); walk outward along the normal; asphalt ends at the first ≥0.55 m run where any of the three is more than 6 σ out. A kerb is a 0.3–2 m band touching that edge with luminance > 200 or saturation > 40 (paint), reported as a second edge. Then along-lap continuity (a station more than 1.5 m off its 21 m rolling median is an artefact and is interpolated) and a width prior.
| variant | width med | p10 | p90 | raw width outside 9–15 m | edge jitter m/m | vs 2-D mask L / R (bias, sd) |
|---|---|---|---|---|---|---|
| joint distance, RGB, p99 threshold (first attempt) | 23.2 | 16.4 | 31.1 | 93% | 0.61 | +6.1 (4.3) / −5.4 (3.8) |
| joint distance, RGB, 4 σ | 12.7 | 10.9 | 16.0 | 22% | 0.54 | +0.7 (1.4) / −1.0 (1.6) |
| gates: SAT + LUM, 6 σ | 11.8 | 10.5 | 18.0 | 15% | 0.48 | +0.2 (1.6) / −0.4 (1.0) |
| gates: SAT + LUM + NIR, 6 σ | 11.8 | 10.4 | 17.4 | 14% | 0.40 | +0.2 (1.5) / −0.4 (1.0) |
| gates: SAT + LUM + NIR, 8 σ | 12.3 | 10.8 | 22.0 | 18% | 0.39 | +0.8 (2.6) / −0.7 (1.2) |
| gate: SAT only, 6 σ | 12.2 | 10.5 | 20.6 | 24% | 0.41 | +0.8 (2.8) / −0.9 (1.7) |
The remaining p90 width (17 m) is real tarmac — paved run-off, the pit entry and the paddock — and is what the width prior flags as inferred. Kerbs were found on 10% / 5% of stations (L / R); Palmer has few, and they land where expected (T10 exit 97% of stations, T12 47%, T5 33%).
The painted track-limit line, and smoothing
Two refinements after the gate classifier shipped, and one correction later (see Thompson): the white edge line is the better edge wherever it exists: a thin bright peak in the station's luminance profile (+23–31 over asphalt, 0.22 m wide at half height) found within 2 m inside / 1 m outside the asphalt edge — the gate classifier stops at its inner side by itself, because paint fails the luminance gate. Present on 64% / 47% of Palmer's stations (L / R), absent on some corners (T7 left 8%, T10 right 0%), so it is a snap-when-found rule with the asphalt edge as fallback. Where it exists the edge jitters 0.10 m/m against 0.30 for the asphalt/shoulder boundary, and the lap-wide width median moves from 11.7 to 12.1 m against the nominal 12.2. Then a 3 m Gaussian on the final edges: no real edge varies at the 0.3–0.4 m-per-station scale the shoulder boundary was showing (tightest edge radius ~30 m), so jitter drops to 0.09 m/m on both sides while widths and radii are unchanged to the decimal.
Why the ridden band still crosses the inside edge at apexes
After the fix, four apexes still show the p2 rider line 1.2–2.0 m beyond the asphalt edge (T3, T7, T11, T12) — always on the inside of the corner and only at the apex. That is the antenna, not the classifier: the GPS puck sits ~0.8 m above the contact patch, so at 45° of lean it is displaced ~0.55 m towards the inside of the turn, plus ~0.5–1 m of fix scatter on the p2 tail. Any "track used" metric has to correct the ridden line by h·sin(lean) before comparing it to an edge.
Second track: Thompson
Same classifier, different everything else: TSMP Road Course, five sessions of 8 Sep 2025 (29 clean laps), imagery from the CT ECO 2023 tile cache at its deepest level, z21 = 0.055 m/px (its native 3-inch survey; ~1400 corridor tiles, hence the corridor cap was raised to 2000), no colour-infrared service, 3DEP lidar present. Thompson is the harder case on purpose: the road course threads through the oval's apron and banking, past paddocks and access roads, so on much of the lap the tarmac simply continues and there is no material edge to find.
| Palmer CCW | TSMP Road Course | |
|---|---|---|
| imagery | MassGIS 2025, z20, 0.11 m/px, RGB + NIR | CT ECO 2023, z21, 0.055 m/px, RGB only |
| asphalt calibration σ (SAT / LUM), lap-wide | 0.85 / 3.6 | 3.2 / 9.0 |
| raw material edge not found (L / R), global gate | 0.5% / 0% | 45% / 53% |
| … with local calibration (200 m, floored) | 0.3% / 0% | 18% / 16% |
| … plus the outside model (asphalt-vs-outside decision) | 0.1% / 0% | 3% / 8% |
| painted line found (L / R) | 42% / 32% | 43% / 44% |
| width median / p10 / p90 | 11.9 / 10.7 / 12.6 m | 12.3 / 11.4 / 14.8 m |
| inferred stations (of which neither edge measured) | 1.8% (0.1%) | 29% (1.4%) |
| corners from the ortho centreline (OSM) | 13 (13) | 9 (10) |
| edge jitter after smoothing | 0.09 m/m | 0.12 m/m |
What Thompson changed in the classifier
- Local calibration. One global asphalt level assumes one pavement; Thompson has several (old and new asphalt, the oval's), its σ read 2.5× Palmer's and the 6σ gate never left "asphalt" on half the lap. A 200 m rolling median/MAD fixes that — floored at half the global σ, because an unfloored local σ on a uniform stretch turns the gate hair-trigger on tyre marks (p10 width fell to 2 m). Smoothing the profile does not substitute: the σ is pavement variation, not pixel noise (flat from 0 to 1 m of box).
- Neither innermost nor outermost: width consistency. "Outermost within 25 m" locked onto an access road's edge line 20 m from the track at T3. "Innermost beyond the ridden band" — the fix that shipped next — took the pit-entry blend line running inside the tarmac along Palmer's T12–T13 straight, because the riders happen not to cross it, and read T2 as 7 m wide. The rule that holds on every case seen: per station, pair every persistent line and the material edge on each side, and take the pair whose width is nearest the track's typical width (the median of the material widths, so it isn't set by the paint it judges; paint preferred within 1 m). Palmer T1: the dashed line's 12.5 m beats the lane's 24; T2 and T12: the material edge's 12 m beats the blend line's 7; Thompson T3: the track edge's 12 beats the access road's 30. Palmer's width median landed at 12.0 m against the nominal 12.2.
- Paint-confirmed widths are never capped. The oval section is legitimately 15–20 m between two painted lines; the run-off cap would have "fixed" real geometry.
- Outside-in as well as inside-out. The gate only asks "is this still asphalt?" from the asphalt's side, and where the asphalt is variable a 6σ band swallows dry grass and dirt. Each side now also carries a model of what is out there — the far 18–25 m of the profile, rolling-median'd along the lap — and a pixel nearer that than the asphalt is non-asphalt too, but only where the two models are ≥2σ separable (a paddock or the oval says nothing). Raw edge misses 21% / 25% → 3% / 8%; Palmer unchanged to the decimal. The other reading of outside-in — tolerate interior gaps by demanding a longer non-asphalt run — was measured and rejected: 0.55 → 3 m moved Palmer 0.1 m and made Thompson worse (inferred 39 → 48%), because its thin verges vanish before its interior patches do.
- The riders are on the track. Every edge must contain the p2/p98 ridden band (2.5 m of slack for the antenna-lean effect) and sit within 14 m of it. The first Thompson build broke this at the T3 hairpin — parking-bay paint 24 m into the paddock passed the persistence test, the run-off cap rebuilt the other edge from it, and the whole "track" sat 10 m beside the riders, drawn as confetti. Now an edge that fails is dropped; a station with one valid edge takes the other from the median width, with none it keeps a guess for the centreline but draws nothing (2.5% of Thompson's stations).
- In-laps and out-laps must not define the ridden band. The pit lane at Thompson runs parallel to the S/F straight and 4 of 29 "clean" laps ran it; at Palmer the pit entry parallels the last section and the in-lap of every session is flagged clean (13 of 49). One such lap in 29 sits inside p98: it dragged the band 10 m sideways at S/F and the innermost-line rule then took the pit road's own edge line as the limit, bending off into the paddock. A lap more than 6 m off the median line for 50 stations went somewhere else and is dropped from the band. Palmer inferred 8.7 → 3.6%, paint found 45 → 53% on the left. (The lap flags themselves are a separate bug: in-laps count as clean laps.)
- The corner-window centreline is the material centreline. Paint moves the limit but not the road: Palmer's pit-exit blend line makes T2 7 m wide, and a mid-limit centreline bowed 2.5 m off the road's centre with R≈110 m — two corners for one. Limits are what the rider is measured against; the road's shape is what corners are detected on.
Verdict: the classifier transfers — a different state's survey, half the pixel size, no NIR — and its failure mode is the right one: it says inferred rather than inventing an edge. What Thompson adds to the requirements list is the same thing Palmer's T1 did, louder: on tarmac-bounded stretches only paint defines the track, so the paint detector is the classifier, not an add-on.
Third track: NYST, with no OSM at all
New York Safety Track, Uptown configuration — eight sessions of 4–5 Aug 2024, 33 clean laps. This is the case the spike was asked about in the first place: OpenStreetMap has no raceway mapped here, so before this work the app fell back to the lean-profile detector. Imagery is the NYS GIS Latest ortho — a dynamic service with no tile cache, so corridor() grew an export mode that renders 2048 px blocks on the z20 pixel grid (0.11 m/px; the service carries real detail to 0.075 m/px at NYST, but a z21 canvas is 16384² = 268 Mpx and ~800 MB, past what the mini should hold) — plus its Latest_cir twin for NIR. 3DEP lidar present.
| Palmer CCW | TSMP Road Course | NYST Uptown | |
|---|---|---|---|
| imagery | MassGIS 2025, z20, RGB + NIR | CT ECO 2023, z21, RGB | NYS Latest (export), z20, RGB + NIR |
| OSM raceway | yes (13 windows) | yes (10) | none |
| corners from the ortho centreline | 13 | 9 | 15 |
| width median / p10 / p90 | 11.9 / 10.7 / 12.6 m | 12.3 / 11.4 / 14.8 m | 11.0 / 10.5 / 12.0 m |
| painted line found (L / R) | 42% / 32% | 43% / 44% | 42% / 32% |
| inferred (neither edge measured) | 1.8% (0.1%) | 29% (1.4%) | 4.0% (0%) |
| edge jitter | 0.10 m/m | 0.11 m/m | 0.11 m/m |
| camber p5..p95 | −6.0..+5.1% | −3.2..+6.1% | −3.1..+3.0% |
| first build | 20 s | 25 s | 25 s |
Nothing in the classifier was changed for NYST; it ran as tuned on the other two. Grass-bounded on both sides nearly everywhere, painted edge lines on half the lap, kerbs on the inside of the tight turns — the Palmer case, on a different state's survey and a different fetch path.
Elevation: GPS traces vs lidar
Same 20 m smoothing the app's grade_profile uses, on the reference-line stations. The constant +4.46 m between the two is removed on the altitude chart — it is a datum difference, not an error, and it does not enter grade.
Sub-sampling the laps: 4 laps → 0.49% rms, 8 → 0.41%, 32 → 0.35%. GRADE_MIN_LAPS = 8 is already past the knee. The DEM's value for grade is a first-day-at-a-new-track fallback, not a better answer.
Camber, from the DEM at the ortho edges
Cross-slope = (z at 1.5 m inside the left edge − z inside the right edge) / their separation, 10 m smoothed. Positive = left edge higher. Shaded bands are the corner windows.
This is the channel the app has no other route to. It matters for the friction-circle story the same way grade did: on a 15% cross-slope the tyre's available lateral g at a given lean is different, and T9's banked hairpin and T3's flat sweeper currently look like the same physics.
Per corner
| T | apex m | dir | R ridden | R OSM | R ortho | width | band | to inside edge | grade | banking |
|---|---|---|---|---|---|---|---|---|---|---|
| T1 | 320 | L | 56 | 54 | 54 | 11.5 | 4.0 | +2.0 | +7.7% | +7.4% |
| T2 | 480 | L | 112 | 95 | 81 | 11.2 | 2.3 | +7.7 | -7.9% | +0.7% |
| T3 | 920 | L | 196 | 129 | 110 | 9.8 | 3.7 | -1.4 | +5.7% | +0.4% |
| T4 | 1090 | R | 34 | 37 | 39 | 11.8 | 3.8 | +2.2 | +7.2% | +0.3% |
| T5 | 1250 | L | 37 | 39 | 38 | 12.2 | 4.2 | -0.2 | +10.5% | +0.3% |
| T6 | 1560 | R | 76 | 71 | 76 | 10.5 | 4.8 | +2.6 | -2.4% | +0.4% |
| T7 | 1790 | L | 41 | 42 | 40 | 11.7 | 3.4 | -0.6 | -10.2% | +7.8% |
| T8 | 2060 | L | 108 | 84 | 86 | 12.2 | 4.5 | +1.8 | +3.8% | -0.5% |
| T9 | 2310 | R | 36 | 38 | 38 | 11.9 | 4.3 | +1.2 | -5.5% | +15.7% |
| T10 | 2650 | R | 112 | 89 | 51 | 11.2 | 8.0 | +0.8 | +0.3% | +2.4% |
| T11 | 2900 | L | 36 | 41 | 42 | 12.3 | 4.0 | +0.1 | +0.6% | +5.6% |
| T12 | 3010 | R | 76 | 37 | 39 | 12.2 | 9.4 | -2.5 | -2.4% | +5.5% |
| T13 | 3070 | L | 104 | 94 | 89 | 12.5 | 5.3 | +2.1 | -9.8% | +1.1% |
Radii are 1/κ minima over the corner window at 8 m smoothing, in metres. band is the p2–p98 lateral spread of the ridden line at the apex; to inside edge is the margin from the band's inside extreme to the ortho edge (negative = on the kerb, or a kerb the classifier doesn't count as tarmac). banking is the cross-slope signed into the turn.
Corner set by source
| source | found | spurious | boundary error vs OSM | needs |
|---|---|---|---|---|
| OSM centreline (today) | 13 | 0 | — | a mapped raceway |
| Ortho centreline | 13 | 0 | ±10 m typical, 24 m max (T10 exit) | imagery + any one lap for the arc mapping |
| Median ridden line, 49 laps | 13 | 0 | ±17 m, T3 +23/−35 | a few laps, any GPS rate |
| Fastest lap alone | 12 | 0 | misses T3 | — |
v2: after the review
An external review of the algorithm above made twelve points; ten were right, two were measured down to size, and one of my own dead-ends turned out to be mislabelled. Its diagnosis — that the many carefully measured constants were compensating for three missing latent variables: registration, the road-relative frame, and varying width — is what v2 addresses. Same interface, rewritten pipeline, all three tracks rebuilt with no per-track settings.
| review point | what was measured | what v2 does |
|---|---|---|
| the frame is circular (normals from the racing line are oblique) | Palmer: 2° p50, 8° p95, 12° max; width error 0.11 m p95 — small, because the median of 49 laps is close to the road tangent | pass 1 casts from the median ridden line, pass 2 re-casts from the material centreline pass 1 found; paint now sits at a constant lateral offset through a corner |
| no GPS↔image registration | sliding the lap over the ortho: Palmer MassGIS (+1.0, +0.1) m, CT ECO (+0.25, −0.5), NYS (+0.75, −0.75) — every sub-metre threshold had been assuming zero | step 0: coarse-to-fine translation search minimising saturation/luminance spread under the riders' interior; applied to every image lookup (rotation unsolved) |
| "typical width" is a target, not a prior; the 1.35× cap erases geometry | the cap had already needed a "unless paint confirms both sides" exception for the oval — the tell | width variation between stations is penalised; only a weak pull to the typical material width; no cap |
| decode the two limits jointly, not alternately | alternating decodes had settled on wrong mutually consistent pairs | one dynamic programme over (left, right) candidate pairs with evidence, per-side continuity, width variation and the occupancy prior |
| persistence pre-filter throws away dashed lines that move relative to the racing line | — | no pre-filter: every transition and every ridge is a candidate, carried 14 m along the lap as a virtual one; continuity is the decoder's job |
| the spectral model is too low-dimensional and assumes independence | the "joint distance" v1 rejected was an isotropic mean-of-z², not a Mahalanobis: R, G, B are correlated on asphalt and the shoulder's chroma difference was diluted | local robust Mahalanobis (rolling mean/covariance of RGB[+NIR], 20% trimmed) with the threshold floored at the interior's own p99.5 — a fixed 4.5 cut inside the tarmac and read 9.7 m |
| the training strip is contaminated exactly at apexes | — | asphalt is learned from the eroded occupancy interior (p15–p85 of all laps) across the whole ribbon |
| occupancy should be a soft prior, and the antenna is not the tyre | h·sin(lean) at 0.8 m and 35° = 0.46 m; measured below | offsets corrected by h·sin(lean) per sample; the band enters the decode as a cost, not a veto |
| separate material / legal / kerb / rideable | — | all four shipped: matLeft/Right, left/right, kerb flags, rideLeft/Right |
| fixed filters distort curvature | a residual-budget spline followed 0.5 m edge noise and read 2 m radii; a stiff spline as the frame cut the T5 hairpin and read 20 m width | centreline = mid of the decoded limits, smoothing spline with a second-derivative penalty, curvature analytic; the frame uses a looser penalty than the curvature |
| "native pixel size" must be real GSD | verified: edge energy per metre rises through z19→z20 at MassGIS and to 0.075 m at NYS | unchanged; thresholds are not yet GSD-aware (caveat) |
| camber from two differenced raster heights is too optimistic | — | robust quadratic across the lidar cross-section between the limits, Huber-weighted, residual sd shipped as camberSd (0.4–0.7 cm median) |
v1 → v2, three tracks
| Palmer v1 | Palmer v2 | Thompson v1 | Thompson v2 | NYST v1 | NYST v2 | |
|---|---|---|---|---|---|---|
| corners (OSM) | 13 (13) | 13 (13) | 9 (10) | 9 (10) | 15 (—) | 14 (—) |
| width median / p10 / p90 | 11.9 / 10.7 / 12.6 | 12.15 / 11.0 / 12.5 | 12.3 / 11.4 / 14.8 | 11.5 / 11.2 / 15.1 | 11.0 / 10.5 / 12.0 | 11.3 / 10.8 / 12.2 |
| inferred | 1.8% | 0.1% | 29% | 9% | 4.0% | 4.4% |
| painted line as limit (L / R) | 42% / 32% | 67% / 66% | 43% / 44% | 70% / 65% | 42% / 32% | 56% / 46% |
| registration shift (pooled laps → image) | — | +0.9, +0.1 m | — | +0.25, −0.5 m | — | +0.75, −0.75 m |
| build time (cached imagery) | 2.5 s | 9.8 s | 2.5 s | 8.9 s | 2.5 s | 9.4 s |
Palmer's width median is 12.15 m against the nominal 12.2 with 0.1% of stations inferred. Thompson is the big change: the joint decode with a width-variation prior follows the dashed paint round the paddock hairpin and through the S/F pit-lane junction that five successive v1 rules had each half-solved; its one lost corner against OSM is a 30 m window on the oval, marginal under the detector's 25 m minimum.
The antenna is not the tyre
The GPS puck sits ~0.8 m above the contact patch; at lean θ it reads h·sin θ to the inside of the turn. Correcting each sample by lean angle and direction moves the ridden band's inside edge outward at every apex — by 0.3–0.5 m at Palmer (mean 0.36 m; lean 21–38°), 0.33–0.39 m at Thompson — and turns three "over the line" margins positive without touching the limits: T3 −0.13 → +0.19 m, T5 +0.29 → +0.70, T10 +0.31 → +0.68. T12's margin stays negative (−1.21 → −0.84): the riders are on the kerb there, and the kerb is rideable. The median line itself barely moves (mean −0.07 m, max 0.52) — the correction is a symmetric squeeze of the band, which is exactly what a height offset should be.
| Palmer corner | dir | max lean | inside edge raw → corrected | margin to limit raw → corrected |
|---|---|---|---|---|
| T1 | L | 34° | +0.51 → +0.11 m | +1.44 → +1.84 |
| T3 | L | 31° | +1.69 → +1.36 | −0.13 → +0.19 |
| T5 | L | 37° | +0.91 → +0.51 | +0.29 → +0.70 |
| T7 | L | 38° | +2.99 → +2.55 | +0.96 → +1.41 |
| T9 | R | 38° | −1.41 → −0.94 | +1.75 → +2.22 |
| T10 | R | 34° | −1.08 → −0.72 | +0.31 → +0.68 |
| T12 | R | 30° | −2.74 → −2.36 | −1.21 → −0.84 (on the kerb) |
Sessions carry their own GPS bias — align them first
Spotted on the straight after S/F: the 2 Aug laps ran on the verge where the 1 Aug laps ran on the tarmac — a per-day GPS bias of about a metre, which one global registration can't remove. Registering each session against the image was tried first and is too noisy with 2–6 laps (per-session results scattered ±3 m). The robust route is GPS-only: a constant bias shows up as a lateral deviation from the pooled median that varies as dx·nx + dy·ny round the loop — the signature of a translation — which a rider's line choice does not have. That is a two-parameter Huber fit per session, and it works with two laps. Shifts found at Palmer run to ±1.9 m (2 Aug 16:39: −1.45, +1.87 m); Thompson ±1.0; NYST up to 2.0 on a four-lap session, which is the caveat — few laps and an unusual line can still buy a spurious metre. The shift ships as sessionShift and the map moves that session's trace by it.
It also settled the "riders over the limit" question from the antenna table: with sessions aligned and the lean correction, every Palmer apex margin is positive except T11 (−0.33 m — the kerb). T12's −1.06 m was day bias.
| Palmer corner | dir | max lean | inside edge raw → corrected | margin to limit raw → corrected |
|---|---|---|---|---|
| T1 | L | 34° | +0.39 → +0.02 m | +1.46 → +1.83 |
| T3 | L | 31° | +0.52 → +0.20 | +0.11 → +0.43 |
| T5 | L | 37° | +0.47 → +0.06 | +1.97 → +2.38 |
| T7 | L | 38° | +2.50 → +2.08 | +0.39 → +0.81 |
| T9 | R | 38° | −1.34 → −0.88 | +2.75 → +3.21 |
| T10 | R | 34° | −0.68 → −0.36 | −0.20 → +0.12 |
| T11 | L | 35° | +2.44 → +2.01 | −0.76 → −0.33 (kerb) |
| T12 | R | 30° | −0.78 → −0.51 | +0.52 → +0.79 |
What v2 actually implements of the review
Done: registration (translation); ribbon from a road centreline with the laps re-projected onto it for pass 2; eroded-interior training with spectral trimming; local Mahalanobis with an outside model; generous candidates, no persistence pre-filter, no band gate; joint pair decode with width variation and a soft occupancy cost; four boundary types shipped (material drawn faintly on the map); spline centreline with analytic curvature; cross-section camber with residual; lean correction; pit-lap exclusion; per-session GPS alignment; survey dates recorded.
Then in v2.1 (below): converged frame, rotation, real NIR, the richer features (in the learned classifier), fitted limits, GSD-aware paint, the kerb model, lidar registration, point-cloud camber, the rideable band on the map. Still not done: a local warp in registration; entropy as a feature; any pretrained model.
Two more things measured on the way
- The Mahalanobis threshold is max(12, interior p99 × 1.15), capped at 20. Measured flat from 12 to 16 (Palmer 12.05–12.15 m, 13 corners); 6 cut ~4% of the interior and shrank Palmer to 11.7 m with 15 corners; the first adaptive rule (untrimmed p99.5 × 1.15 ≈ 27) took the brown verge for asphalt. Hysteresis (continue a run past 0.7 × threshold) was tried and rejected: it grows runs over the interior's tail. And the interior's p99 is ~10 only because pass 2 re-projects the laps — with the approximated band it read 20, the training region being half off the tarmac.
- Per-side continuity is free only within 0.3 m per station. At 1.0 the paddock-side limit on Palmer's straight, where no material transition exists, zig-zagged between candidates 1–2 m apart and the smoothing drew it as chewed.
v2.1: the rest of the list
Everything that was "partial" or "not done" above is now in, with the two that needed outside data resolved: the real fourth band comes from USGS NAIPPlus (band 4 as z19 export blocks — the provider's CIR rendering is only a fallback now), and camber comes from the 3DEP source point cloud (Entwine EPT: the project covering the site from the published boundaries, octree nodes over the corridor, LAZ decoded with laspy — Palmer: MA QL2 2015, 1.16 M points at 4 pts/m², 21 s; Thompson: CT 2016, 625 k; NYST: NY 2018, 2.0 M), fitted as a Huber quadratic on the ground returns in a ±1.5 m slab per station with the point count and residual shipped, and registered to the GPS frame by the shift under which the cross-sections are flattest.
| item | v2.1 | what it took to get right |
|---|---|---|
| frame iterated to convergence | up to 4 passes, laps re-projected each pass, stop when the limits' mid moves < 0.1 m rms | converging on the material mid never settled — it is undefined where tarmac continues and jumped metres between passes |
| rotation in registration | ±0.3° about the corridor centroid after the translation | 0.0° at Palmer and Thompson, 0.1° at NYST |
| real NIR | NAIP band 4, 0.22 m/px, for any US track | the ImageServer endpoint is /exportImage, not /export ("output format not supported" is what a wrong path looks like) |
| richer features | RGB, NIR, R−B, 2G−R−B, texture, gradient | not all in the Mahalanobis: R−B and 2G−R−B are exact linear combinations of RGB and make the covariance singular (material width 10.5 m); texture and gradient are heavy-tailed on asphalt (10.4 m). The spectral model stays RGB[+NIR]; the derived channels feed the learned classifier |
| a learned evidence producer | a self-trained logistic classifier on all eight features, labels from the previous pass's own decode, 8 Newton steps with a ridge | merged into the surface mask it cut ~1 m off the material width on rubbered asphalt; its transitions are now their own candidate kind (+0.5) so they win only where the spectral model left nothing better |
| fitted limits | periodic smoothing spline per side, second-derivative penalty | — |
| GSD-aware paint | above 0.2 m/px: ridge against a 1 m local mean, rise scaled by 0.22/GSD, no half-height width | not exercised yet — all three providers are finer than that |
| kerb model | a band touching the limit, persistent on ≥60% of ±5 stations, painted (saturation alternates 20–80% along the lap) or concrete (bright and unsaturated on ≥80%) | kerb rates fell (Palmer 5–7% → 2–4%): the bright pit wall and apron edges it used to count are gone |
| lidar registration | cross-section flatness, ±2 m, applied only if it beats zero by 5% | Palmer (+0.5, −1.5) m; Thompson and NYST 0 |
| rideable band drawn | faint line on the map | — |
| Palmer v2.1 | Thompson v2.1 | NYST v2.1 | |
|---|---|---|---|
| corners (OSM) | 13 (13) | 9 (10) | 14 (—) |
| width median / material | 11.85 / 10.94 m | 11.40 / 10.82 m | 10.56 / 9.80 m |
| inferred | 0.0% | 2.9% | 0.8% |
| NIR | NAIP | NAIP | NAIP |
| camber source | points, 1.16 M | points, 0.62 M | points, 2.0 M |
| registration (shift, rotation; NIR objective) | +1.25/−0.25 m, 0.0° | +2.6/−0.75 m, −0.075° | +0.5/+0.4 m, 0.0° |
| first build (imagery cached) | 36 s | 53 s | 90 s |
Still true: the image registration objective is flat to about a metre, so the pooled shift is weakly determined and moved when the band changed — each raster (RGB, NIR, lidar) is registered separately and their disagreements are real, not errors to average away. The frame stops at four passes with the limits' mid still moving 0.14–0.26 m; that is re-decoding noise, not drift. Thompson's tenth corner is a 30 m window on the oval under the detector's 25 m minimum.
Corner detection and numbering, redone
The T-numbers were still the OSM-era detector's: resample the centreline at 2 m, Gaussian it, threshold |κ| at 1/200, split on sign, drop runs under 25 m, number what survives from S/F. It undercounted every track — Palmer 13 of the official 14, Thompson 9 of 11, NYST 14 of 18 — by merging close apexes and dropping short windows, and every number after a merge shifted between builds. It is replaced by detection on the fitted centreline's analytic curvature: an apex is a |κ| peak at R ≤ 200 m; two same-sign apexes are one corner unless |κ| relaxes below 55% of the smaller peak between them; a window runs while R ≤ 450 m, cut at the dip between neighbours.
Two rules encode the convention official maps follow, each set by a corner you called out. A hairpin that turns more than 100° splits at any internal dip below 85% of its neighbouring apexes — Thompson's T1–T2 is one 130° sweep with two numbers. (It had to be a post-split: at a hairpin's second apex the heading has turned under 100°, so a grouping-time rule never fired.) A kink flatter than R 120 m needs 30° of heading change to count, others 15° — NYST's "T4" was a R 167 m bend turning 23°, which nobody numbers. Measured: Palmer 14 and Thompson 11 now match the official counts; NYST 14 of 18, its map numbering kinks we drop on purpose.
Numbers persist. The first build numbers sequentially from S/F and stores each apex's position; a rebuilt apex within 40 m keeps its number, new apexes take the next free numbers, and only a corner that has vanished triggers a sequential renumber — so a faster reference lap no longer renumbers the track. Tested: dropping a corner renumbers; adding one appends. And because numbering is ultimately a convention, official numbering can be pinned: set "pinned": true in var/geometry/<site>_corners.json with the official apexes' positions, and the corner set is that list — each pinned apex takes the nearest detected window (or a ±25 m window for a kink no detector would number), the rest are dropped.
Palmer's numbers moved with the split of its long T2 (official 14): the banked hairpin quoted as T9 throughout this page is now T10, T7's descent is T8, and so on from T3.
"NYST too generous, Thompson too strict" — the frame, not the classifier
Two crops: at a NYST kink the limits sat a metre outside the tarmac on both sides; at Thompson's T1 hairpin a metre inside. Ablating NIR, the learned classifier and the width pull moved neither by a centimetre — the candidates were where they were. The cross-track profiles then showed the riders' band sitting on the edge of the NIR-dark asphalt at the NYST kink and at T1, which is impossible: the frame was off. The RGB registration objective (median saturation + luminance spread under the interior) is flat to about a metre and multimodal — at NYST it returned answers 4 m apart for two nearly identical interiors. Registering on the median NIR under the interior instead (asphalt is the darkest thing in NIR by a wide margin — NYST 80 against 150–170 for verge and gravel) gives a sharp surface, lands Palmer where the first registration did (+1.1, +0.1 m — the consistency check), and moves NYST by 1.5 m and Thompson by 2.4 m. With the frame right, the NYST kink reads 10.4 m wide (was 12.5) and Thompson's hairpin limits sit on the outer tarmac edge and the inner kerb.
Two smaller things came out of the same profiles. NAIP's 0.6 m NIR is blurred over a ~1 m ramp, so a thresholded transition on a dark-asphalt/bright-verge boundary fires a metre inside the edge: material limits are now refined to the ramp midpoint (the 50% crossing between the asphalt level just inside and the outside level just beyond). And material candidates earn evidence for the lateral image gradient under them — the boundary likelihood the review listed — at 0.6 per unit of normalised edge strength; at 1.2 it over-rewarded interior patches and cut Palmer's material width to 11.2 m.
And then the rendering caught me too: everything geometry measures lives in the GPS frame, but the illustration was drawing those limits on the ortho without the registration transform — so with Thompson's 2.6 m shift the limits sat beside the tarmac and T1 read two metres off its kerb. The illustration now maps GPS → image through the stored shift and rotation, and the app moves its backdrop by minus the shift under the traces. Every current-state figure on this page is re-rendered that way.
How to check any registration from now on: the riders' band must sit inside the NIR-dark span at a handful of stations. It is a one-line test and it would have caught both of these before you did.
The algorithm, step by step
What geometry.py (v2) does when a track is first opened, in the order it runs. Constants are the shipped values; every one of them was set by a measurement on this page.
- Inputs. The reference lap (fastest clean lap, 1 m stations), every clean lap's GPS and lean, the imagery corridor at the provider's deepest zoom that fits a 180 Mpx canvas (RGB, plus NIR where the provider renders colour-infrared), the 3DEP lidar raster.
- Where the riders were — on the ground, on the same day. Project every lap onto the reference line; correct each sample by h·sin(lean) (h = 0.8 m) so the band describes the tyre, not the antenna; drop laps that spend 50 stations more than 6 m off the median (pit in/out — the lap flags let them through); then fit each session's translation from its deviation signature dx·nx + dy·ny and remove it (per-day GPS bias, ~1 m). Keep the median line, the p2–p98 band, and the p15–p85 interior.
- Register. Slide (±3 m at 0.5 m, then ±0.75 m at 0.125 m) and rotate (±0.3°) the lap over the imagery to minimise the median NIR under the eroded interior — asphalt is the darkest thing in NIR — and apply the transform to every lookup. (The RGB objective is flat and multimodal; it put NYST 1.5 m and Thompson 2.4 m off.)
- Unwrap the ribbon. Sample I(s, d) for d ∈ ±25 m along the normals of a road centreline at native pixel size — pass 1 from the median ridden line, pass 2 from the material centreline pass 1 found (splined, loosely).
- Learn asphalt here. A rolling (200 m) robust mean and covariance of RGB[+NIR] from the interior pixels, 20% trimmed; a Mahalanobis distance per pixel, 3-px box along the profile; threshold max(12, interior p99 × 1.15), capped at 20. Per side, an outside model (the far ribbon's rolling median) under the same covariance: a pixel nearer it than the asphalt is outside too, where the two models are ≥3σ apart.
- Collect candidates generously. Per station and side, walking outward: every start of a ≥0.55 m non-asphalt run (a material transition, up to 3) and every thin bright ridge (≥12 over the local asphalt level, ≤0.45 m at half height — paint). No persistence test, no gate against the band. Each real candidate is also carried ±14 stations along the lap as a virtual candidate, so dashes and short occlusions stay available.
- Decode the pair. Dynamic programming over (left, right) candidate pairs per station, with a "none" state per side. Costs: evidence (paint −1, material 0, virtual +1, none +4); per-side continuity (free within 0.3 m per station, 10/m beyond, capped at 25; entering or leaving "none" costs 12.5); width variation between stations (3/m); a weak pull to the typical material width (0.4/m); the occupancy prior (a limit inside the lean-corrected band costs 15/m beyond 1 m of slack; more than 16 m beyond it, 15). One mechanism, no cap.
- Name the boundaries. The decoded pair is the legal limit; the first material transition is the material boundary; a 0.3–2 m band of paint values touching the limit is the kerb; the lean-corrected p2–p98 band is the rideable band. A side decoded as "none" is filled from the other side minus the typical width and flagged
inferred; both "none" isunknownand nothing is drawn. Limits get a 3 m Gaussian for drawing only. - Fit the geometry. Centreline = mid of the limits, fitted as a periodic smoothing spline with a second-derivative penalty; curvature is its analytic derivative. Corners are detected on it directly (prominence-split apexes, hairpin post-split, kink rule) and numbered persistently. Camber = the slope of a Huber-weighted quadratic fitted across the lidar cross-section between the limits, with its residual. Cached per site, keyed by the reference line.
Steps 2, 3 and 7 are the three latent variables the review named — plus the per-session alignment inside step 2, which the review didn't ask for and the data did; they replaced eleven corner-specific rules between them.
What shipped, and the approach it settled on
The plan at the top of this page was written before anything was built; this is what survived three tracks. It is in webapp/geometry.py, wired into track_context, with the load-bearing parts in CLAUDE.md.
- A fourth track artifact,
geometry, beside corner map / reference line / grade profile. Per 1 m station of the reference line: left/right limit, kerb outer edge,inferred/unknown/lineL/lineRflags, the material centreline and its curvature, cross-slope. Built once per site from an imagery corridor (the provider's deepest zoom along ±40 m of the line — tiles where there is a cache, 2048 px export blocks on the same grid where there isn't — plus the provider's CIR rendering for NIR) and a 3DEP raster; cached by the reference line it was built on; 20–25 s on first request, then free. - Corner windows come from centrelines, best first: ortho → OSM → median ridden line → lean profile, all through the unchanged
centerline_corner_windows. The ortho centreline is the material one, with the riders' median filling inferred stretches — never the mid-limit (paint moves the limit, not the road). Palmer 13/13 against OSM, NYST 14 with no OSM at all, Thompson 9 against OSM's 10. - The classifier is a 1-D profile per station, inside-out and outside-in, calibrated locally, with the painted line as the edge wherever it exists. Inside-out: per-feature gates (SAT / LUM / NIR, 6 robust σ from a 200 m rolling calibration under the ridden line, floored at half the lap-wide σ). Outside-in: a model of the far profile per side, and a pixel nearer it than the asphalt is out — only where the two are ≥2σ separable. Paint: thin bright peaks with along-lap persistence (a dashed line recurs, a tyre mark doesn't). Which line is the limit is decoded per stretch, not per station: a Viterbi over 0.5 m offset bins per side, emission = width error against the other side (paint preferred, a bin with no evidence costs a little so dashes bridge), drift ≤1 m per station free, a jump to another line costs 25 m·stations; the two sides alternate so the width coupling holds. Kerbs as a second edge. Then the invariant: every edge contains the ridden band and sits within 14 m of it, or it is dropped.
- Honesty over completeness in the output. A width past 1.35× the median is run-off unless paint confirms both edges; a station with one valid edge takes the other from the median width and is
inferred; with none it isunknownand the map draws nothing. Thompson is 25% inferred and that is the right answer for a road course threaded through an oval's apron and paddocks. - The ridden band is lean-corrected and excludes in/out-laps (h·sin(lean) per sample; a lap >6 m off the median line for 50 stations is dropped). The lap flags don't — every Palmer session's in-lap is "clean" — and one such lap in 29 was enough to drag the band 10 m sideways at S/F.
- Elevation stays with
grade_profile(median GPS altitude matches lidar at r 0.9998 / 0.35% grade rms); the DEM's job is camber, sampled 1.5 m inside each limit. Never per-lap altitude. - What the user sees: limits under the trace on the track map (dashed = inferred, warm = kerb, nothing where unknown);
R · width · camberon every corner row (Palmer T9: R 40 m, 12.2 m, +15.7% banked); the Corners subtitle names the source.
Caveats, and dead ends not to re-attempt
Dead ends — each measured, each rejected
- 2-D segmentation of the ortho (colour mask + morphology). Fine for a first look, wrong in production:
binary_fill_holesfills a closed loop's infield, the paddock joins the tarmac component, and there is no per-station bookkeeping to say which stations are trustworthy. The 1-D profile replaced it. - An isotropic colour distance (mean of per-feature z² over RGB or RGB+NIR — mislabelled "Mahalanobis" in v1). The gravel shoulder has asphalt's luminance and differs only in chroma/NIR; R, G, B are correlated on asphalt, so the chroma direction has tiny variance and a true covariance-weighted distance separates it — which is what v2 uses. Per-feature gates were the v1 workaround.
- Thresholds from the calibration set's percentiles (p99). Set by the cars, paint and shadows the ridden line crosses; read 23 m widths. Thresholds are fixed in robust σ.
- Texture as a gate. Separates shoulder from asphalt (0.9 vs 2–5), but tyre marks and paint on asphalt reach the same values.
- One lap-wide calibration. Assumes one pavement; Thompson has several, its σ read 2.5× Palmer's and a 6σ gate never left "asphalt" on half the lap. And an unfloored local σ over-corrects the other way (p10 width 2 m on tyre marks). Local, floored.
- Smoothing the profile to fix the σ. Measured flat from 0 to 1 m of box: the σ is pavement variation, not pixel noise. Likewise more resolution: z19 → z20 moved Palmer's edges ±0.4 m; the classifier, not the pixels, is the limit.
- Per-station paint detection (a peak snapped where found). Loses every dashed line — dash stations are the minority and the continuity filter discards them — and the pit-lane blend at Palmer T1, the T7 apron and most of Thompson are fenced by dashed paint. Persistence along the lap, then bridge.
- Choosing the limit by position at all. Superseded in v2 by the joint pair decode with a width-variation prior; the history: the outermost persistent line — then the innermost — then a per-station width-consistency argmin — then two sides decoded alternately. Outermost locked onto an access road's edge line 20 m from Thompson's T3; innermost took the pit-entry blend line inside the tarmac along Palmer's T12–T13 straight and read T2 as 7 m wide; the per-station width argmin fixed both but had no memory, so at T5's exit apron the limit flipped between two candidates station by station and the smoothing drew an S-bend, and on the T12–T13 straight the blend line and the edge traded wins. Width consistency decoded per stretch (Viterbi with a switching cost) is the rule that holds.
- Starting the paint search outside the ridden band. Riders sit on the line at apexes (antenna lean ~0.55 m at 45°, plus fix scatter), so the correct line fell inside the window's start; Palmer's paint hit rate dropped a third. Start half a metre inside the band's edge.
- "Outside-in" as gap tolerance (a longer required non-asphalt run). RUN 0.55 → 3 m moved Palmer 0.1 m and made Thompson worse (39 → 48% inferred): its thin verges vanish before its interior patches do. The outside model is the version that works.
- Rebuilding a bad edge from the other edge without a containment check. Parking-bay paint 24 m into a paddock passed persistence, the cap rebuilt the far edge from it, and the whole "track" sat 10 m beside the riders (the confetti at Thompson T3). Every edge must contain the ridden band — and at 1 m of slack that rule dropped Palmer's correct apex edges (inferred 2.5 → 24%); 2.5 m, because of the antenna.
- A mid-limit centreline for corner detection. Palmer's pit-exit blend line makes T2 7 m wide; the centreline between the limits bowed 2.5 m off the road's centre and back (R≈110 m) and the detector found two corners. A 20 m Gaussian to hide it then merged Thompson's T6/T7. Material centreline, 8 m smoothing.
- Bridging inferred centreline stretches with a straight line in offset space. Flattened two of Thompson's corners out of the detector's reach (10 → 8). The riders' median line has the shape.
- Hysteresis on the surface distance. Continuing a non-asphalt run past 0.7× the threshold grows runs over the interior's tail: Palmer read 15 corners / 11.9 m.
- Registering each session to the image. Too noisy with 2–6 laps (±3 m scatter). Align sessions to each other by their lateral-deviation signature instead.
- Deeper than the canvas can hold. NYST at z21 is a 16384² canvas (268 Mpx, ~800 MB): past PIL's bomb limit and more than the mini should hold, for a ±0.4 m gain. The corridor is capped by canvas pixels, not tile count.
Caveats that stand
- Tarmac-bounded stretches with no paint are unknowable from imagery — the oval's banking at Thompson, a paddock hairpin. The app says
inferred/unknownthere; nothing short of a track map will do better. On an oval the limit is a rules question (the apron line) anyway. - The antenna is not the tyre. The GPS puck is ~0.8 m above the contact patch, so the ridden band crosses the true edge by up to 2 m at apexes. Any "track used" metric must correct the line by
h·sin(lean)before comparing it to a limit; nothing does yet. - In-laps are flagged clean by the lap segmenter (13 of 49 at Palmer, one per session). Geometry filters them out of the band; the lap stats still count them. Separate bug, not touched here.
- Kerbs are a bright/saturated band detector, not a kerb model: a white pit wall or a concrete apron edge next to the track can read as kerb (seen at Thompson's T5 bridge). Reported as a flag, never as the limit.
- Corner counts on tight, closely spaced turns are marginal (Thompson's 30–40 m windows sit near
MIN_CORNER_M) and move with the centreline choices above; T-numbers there are less stable than at Palmer. - One date of imagery. Repaves, new kerbs and moved paint need a refetch; the corridor cache is keyed by the reference line, not by survey date.
- Camber assumes lidar on the road surface (true here: cross-sections are linear through both edges) and 3DEP is US-only; outside it, grade still comes from GPS and camber is unavailable.
- Untested: pure NAIP (0.6 m, JPEG) with no state ortho — the 0.6/1.0 m/px runs were downsampled MassGIS, which is cleaner than real NAIP; a track with no paint and grass on one side only; a first build on the mini's memory (NYST peaked at 2.2 GB resident during the fetch).
- The lean-profile detector is now the last fallback, kept for a track with no imagery provider at all; the median-ridden-line detector before it needs only a few laps at any GPS rate.
Scripts: tools/spike_geometry/ (spike measurements, run_track.py to build any track from the local library, illustration.py for the figures). DEM: 3DEPElevation/ImageServer/exportImage, bbox −72.2503,42.2305,−72.2378,42.2418, 1400×1700 F32.