Spike · Palmer CCW · 49 clean laps, 13 sessions · 2026-08-29

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.

11.6 mtrack width from the ortho (median)Palmer's nominal 40 ft = 12.2 m; p10 10.3, p90 17.5 (paved run-offs / paddock)
0.9998GPS median altitude vs lidar, rgrade rms diff 0.35% at 20 m smoothing; +4.5 m constant offset
13 / 13corners found from the ortho centrelinewindows within ±10 m of the OSM ones (max 24 m)
+0.4 mortho centre vs GPS reference lineortho and GPS are georeferenced to sub-metre agreement

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.

track limitinferredkerb1 Aug laps2 Aug laps
Palmer CCW at 0.11 m/px with track limits and the 1-2 Aug 2026 laps
Palmer Motorsports Park, CCW. 7424 × 9472 px at full resolution (rendered here at 3712 × 4736). Imagery: MassGIS.

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_windows reproduces all 13 OSM corners; boundary error ±10 m.
  • Downsampled to 0.6 and 1.0 m/px (NAIP-grade, the usgs provider): 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 exportImage call: 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

tarmac edges (ortho)ridden band p2–p98 (49 laps)reference lap
Palmer CCW ortho with extracted tarmac edges and the ridden band
Whole circuit at 0.9 m/px. Edges hold everywhere the tarmac has a non-asphalt neighbour; they wander only where the track meets paved run-off or the paddock (T1, the start straight). The ridden band is a narrow ribbon inside the edges — the geometry has to come from the photo, not the riders.
T5 crop
T5 · R 38 m · +10.5% climb. The band touches the inside edge at the apex (margin −0.2 m, i.e. on the kerb). Width 12.2 m.
T9 crop
T9 · R 38 m · −5.5% grade, 15.7% cross-slope into the turn. The DEM cross-section rises 3.4 m over 28 m straight through the corner: the hairpin lies across the hillside.
T12 crop
T12 · the case OSM gets wrong: OSM 37 m, ortho centreline 39 m, ridden line 76 m — riders straighten the chicane, and the 9.4 m ridden band is the widest on the lap.

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.

T9 at z19 vs z20
T9, same 120 m of ground. Left z19 (cached), right z20. z20 is real detail — edge energy per metre 35 → 49, individual tyres in the walls, the kerb strip resolved — not an upsample.

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.

T9 RGB vs colour-infrared
Same corner in MassGIS's 2025 CIR service (NIR, R, G → R, G, B), z19 max. The fourth band the survey captured. In leaf-off April it separates vegetation (red) and the pinkish gravel shoulder from the neutral asphalt more cleanly than RGB; it does nothing for painted kerbs. Worth adding as a fourth classifier channel where a state publishes it; NAIP is 4-band too. (This sample was later found to be stitched one tile off — see “Classifier, corrected” below.)

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.

featureasphalt (|off| ≤ 1.5 m)robust σgravel shoulderσ units
luminance167.73.6155–1602–3
saturation (max−min)6.40.920–3215–28
NIR127~3147–1907–20
texture (5×5 σ)0.90.52–52–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.

variantwidth medp10p90raw width outside 9–15 medge jitter m/mvs 2-D mask L / R (bias, sd)
joint distance, RGB, p99 threshold (first attempt)23.216.431.193%0.61+6.1 (4.3) / −5.4 (3.8)
joint distance, RGB, 4 σ12.710.916.022%0.54+0.7 (1.4) / −1.0 (1.6)
gates: SAT + LUM, 6 σ11.810.518.015%0.48+0.2 (1.6) / −0.4 (1.0)
gates: SAT + LUM + NIR, 6 σ11.810.417.414%0.40+0.2 (1.5) / −0.4 (1.0)
gates: SAT + LUM + NIR, 8 σ12.310.822.018%0.39+0.8 (2.6) / −0.7 (1.2)
gate: SAT only, 6 σ12.210.520.624%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%).

T3 gate classifier
T3, gate classifier at z20: asphalt edges (orange) now stop at the shoulder on both sides; the ridden band (blue) hugs the inside edge.
T12 gate classifier
T12: the white ticks are kerb detections. The raw per-station edge is still jagged (0.4 m/m) — the along-lap continuity pass is what makes it a curve.

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.

T12 final edges
T12, shipped edges. Green = snapped to the painted line, orange = asphalt/shoulder fallback, blue = ridden band.
T9 final edges
T5 exit. A paved apron on the outside: with a per-station choice the limit flipped between two candidates and smoothed into an S-bend; decoded per stretch it stays on the edge.

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 CCWTSMP Road Course
imageryMassGIS 2025, z20, 0.11 m/px, RGB + NIRCT ECO 2023, z21, 0.055 m/px, RGB only
asphalt calibration σ (SAT / LUM), lap-wide0.85 / 3.63.2 / 9.0
raw material edge not found (L / R), global gate0.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 / p9011.9 / 10.7 / 12.6 m12.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 smoothing0.09 m/m0.12 m/m

What Thompson changed in the classifier

track limitinferredkerb8 Sep 2025 laps (29)
Thompson road course at 0.055 m/px with track limits and the 8 Sep 2025 laps
TSMP Road Course, 15616 × 10752 px at full resolution (rendered here at 4400 px wide). Imagery: CT ECO / UConn CLEAR / CT DEEP.
Thompson T3
T3. Limits down both straights and the hairpin's inside kerb; the outside of the hairpin sits against the paddock with no paint — nothing is drawn there rather than a guess.
Thompson T9
T9. Grass on both sides, paint on both edges: the Palmer case, and it reads the same — 11.4 m wide, kerb at the apex.
Thompson T5 on the oval
T5, on the oval. The inside limit follows the apron's painted edge; the outside picks up the banking's line in places and is inferred elsewhere. The oval is the one place where "the limit" is a rules question rather than a geometry one, and this is as far as imagery can take it.

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 CCWTSMP Road CourseNYST Uptown
imageryMassGIS 2025, z20, RGB + NIRCT ECO 2023, z21, RGBNYS Latest (export), z20, RGB + NIR
OSM racewayyes (13 windows)yes (10)none
corners from the ortho centreline13915
width median / p10 / p9011.9 / 10.7 / 12.6 m12.3 / 11.4 / 14.8 m11.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 jitter0.10 m/m0.11 m/m0.11 m/m
camber p5..p95−6.0..+5.1%−3.2..+6.1%−3.1..+3.0%
first build20 s25 s25 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.

track limitinferredkerb4 Aug 2024 laps (17)5 Aug 2024 laps (16)
NYST Uptown at 0.11 m/px with track limits and the 4-5 Aug 2024 laps
NYST Uptown, 10240 × 10240 px at full resolution (rendered here at 3600 px wide). The corridor is the NYS leaf-off ortho; the blocky summer imagery around it is the USGS/NAIP backdrop stitch, a different survey — cosmetic, outside the analysed corridor. Imagery: NYS ITS GPO, USGS.
NYST T1
T1. R 33 m, 11.9 m wide, −1.5% off-camber; inside kerb found.
NYST T10
T10. R 26 m, +3.4% banked; the painted line on the outside, the kerb on the inside.
NYST T8
T8. R 43 m, 11.0 m; the long left with the ridden band spread across most of the width.

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.

GPS, median of 49 laps3DEP lidar, 1 m

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

Tapex mdirR riddenR OSMR orthowidthbandto inside edgegradebanking
T1320L56545411.54.0+2.0+7.7%+7.4%
T2480L112958111.22.3+7.7-7.9%+0.7%
T3920L1961291109.83.7-1.4+5.7%+0.4%
T41090R34373911.83.8+2.2+7.2%+0.3%
T51250L37393812.24.2-0.2+10.5%+0.3%
T61560R76717610.54.8+2.6-2.4%+0.4%
T71790L41424011.73.4-0.6-10.2%+7.8%
T82060L108848612.24.5+1.8+3.8%-0.5%
T92310R36383811.94.3+1.2-5.5%+15.7%
T102650R112895111.28.0+0.8+0.3%+2.4%
T112900L36414212.34.0+0.1+0.6%+5.6%
T123010R76373912.29.4-2.5-2.4%+5.5%
T133070L104948912.55.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

sourcefoundspuriousboundary error vs OSMneeds
OSM centreline (today)130a mapped raceway
Ortho centreline130±10 m typical, 24 m max (T10 exit)imagery + any one lap for the arc mapping
Median ridden line, 49 laps130±17 m, T3 +23/−35a few laps, any GPS rate
Fastest lap alone120misses 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 pointwhat was measuredwhat 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 tangentpass 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 registrationsliding 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 zerostep 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 geometrythe cap had already needed a "unless paint confirms both sides" exception for the oval — the tellwidth variation between stations is penalised; only a weak pull to the typical material width; no cap
decode the two limits jointly, not alternatelyalternating decodes had settled on wrong mutually consistent pairsone 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 lineno 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 independencethe "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 dilutedlocal 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 apexesasphalt 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 tyreh·sin(lean) at 0.8 m and 35° = 0.46 m; measured belowoffsets corrected by h·sin(lean) per sample; the band enters the decode as a cost, not a veto
separate material / legal / kerb / rideableall four shipped: matLeft/Right, left/right, kerb flags, rideLeft/Right
fixed filters distort curvaturea 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 widthcentreline = 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 GSDverified: edge energy per metre rises through z19→z20 at MassGIS and to 0.075 m at NYSunchanged; thresholds are not yet GSD-aware (caveat)
camber from two differenced raster heights is too optimisticrobust 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 v1Palmer v2Thompson v1Thompson v2NYST v1NYST v2
corners (OSM)13 (13)13 (13)9 (10)9 (10)15 (—)14 (—)
width median / p10 / p9011.9 / 10.7 / 12.612.15 / 11.0 / 12.512.3 / 11.4 / 14.811.5 / 11.2 / 15.111.0 / 10.5 / 12.011.3 / 10.8 / 12.2
inferred1.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 s9.8 s2.5 s8.9 s2.5 s9.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 cornerdirmax leaninside edge raw → correctedmargin to limit raw → corrected
T1L34°+0.51 → +0.11 m+1.44 → +1.84
T3L31°+1.69 → +1.36−0.13 → +0.19
T5L37°+0.91 → +0.51+0.29 → +0.70
T7L38°+2.99 → +2.55+0.96 → +1.41
T9R38°−1.41 → −0.94+1.75 → +2.22
T10R34°−1.08 → −0.72+0.31 → +0.68
T12R30°−2.74 → −2.36−1.21 → −0.84 (on the kerb)
Palmer T2 v2
Palmer T2, v2. Legal limits (green) at 12 m on both sides; the faint white line is the material boundary, which runs out along the pit-exit lane — the two are different things and both are shipped.
Thompson S/F v2
Thompson S/F, v2. Limits on both edges of the straight with the pit lane outside them; the material boundary wanders across the paddock tarmac, as it should.

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.

Palmer straight before alignment
Before. Orange (2 Aug) on the verge, blue (1 Aug) on the tarmac; the paddock-side limit chews between candidates.
Palmer straight after alignment
After. Sessions aligned, continuity tightened to 0.3 m/station: the two days overlap and both limits sit on their edges (the paddock-side one on the paint).

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 cornerdirmax leaninside edge raw → correctedmargin to limit raw → corrected
T1L34°+0.39 → +0.02 m+1.46 → +1.83
T3L31°+0.52 → +0.20+0.11 → +0.43
T5L37°+0.47 → +0.06+1.97 → +2.38
T7L38°+2.50 → +2.08+0.39 → +0.81
T9R38°−1.34 → −0.88+2.75 → +3.21
T10R34°−0.68 → −0.36−0.20 → +0.12
T11L35°+2.44 → +2.01−0.76 → −0.33 (kerb)
T12R30°−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

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.

itemv2.1what it took to get right
frame iterated to convergenceup to 4 passes, laps re-projected each pass, stop when the limits' mid moves < 0.1 m rmsconverging 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 translation0.0° at Palmer and Thompson, 0.1° at NYST
real NIRNAIP band 4, 0.22 m/px, for any US trackthe ImageServer endpoint is /exportImage, not /export ("output format not supported" is what a wrong path looks like)
richer featuresRGB, NIR, R−B, 2G−R−B, texture, gradientnot 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 producera self-trained logistic classifier on all eight features, labels from the previous pass's own decode, 8 Newton steps with a ridgemerged 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 limitsperiodic smoothing spline per side, second-derivative penalty
GSD-aware paintabove 0.2 m/px: ridge against a 1 m local mean, rise scaled by 0.22/GSD, no half-height widthnot exercised yet — all three providers are finer than that
kerb modela 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 registrationcross-section flatness, ±2 m, applied only if it beats zero by 5%Palmer (+0.5, −1.5) m; Thompson and NYST 0
rideable band drawnfaint line on the map
Palmer v2.1Thompson v2.1NYST v2.1
corners (OSM)13 (13)9 (10)14 (—)
width median / material11.85 / 10.94 m11.40 / 10.82 m10.56 / 9.80 m
inferred0.0%2.9%0.8%
NIRNAIPNAIPNAIP
camber sourcepoints, 1.16 Mpoints, 0.62 Mpoints, 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 s53 s90 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.

Thompson T1 T2 hairpin
Thompson T1–T2. The 130° hairpin with two numbers, split at the dip between its apexes.
NYST former T4 kink
NYST, the former "T4". R 167 m, 23° — not a corner; the numbers after it move up.

"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.

Thompson hairpin registered
Thompson T1–T2, NIR-registered. Outer limit on the tarmac edge, inner on the kerb; the frame had been 2.4 m off.
NYST kink registered
NYST, the kink, NIR-registered. Limits on the asphalt edges; 10.4 m wide, was 12.5 with a metre of grass on each side.

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.

  1. 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.
  2. 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.
  3. 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.)
  4. 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).
  5. 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.
  6. 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.
  7. 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.
  8. 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" is unknown and nothing is drawn. Limits get a 3 m Gaussian for drawing only.
  9. 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.

  1. 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 / lineR flags, 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.
  2. 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.
  3. 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.
  4. 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 is unknown and 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.
  5. 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.
  6. 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.
  7. What the user sees: limits under the trace on the track map (dashed = inferred, warm = kerb, nothing where unknown); R · width · camber on 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

Caveats that stand

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.