Inner (median-side) pavement edge detection

2026-07-11 · AI3D-337 · asphaltedge repo · plan for Miro

Summary

Add two new polylines per segment — the pavement edges facing the Mittelstreifen (median) — on top of the accepted outer-edge pipeline. ~90 % of the machinery is reused unchanged.

The key structural fact: the run-7 axis sits near the corridor centre (accepted outer edges land at ≈ +12.4 m / −11.8 m), so the median straddles offset 0. The current search bands start at ±1 m specifically to exclude it. And the tracker is already direction-parameterised: the inner-left edge is the existing "right-side" step detector run in a narrow positive-offset band near the axis (and vice versa). Same rectified grid, same z-scored median-step evidence, same Viterbi, same polyline hygiene, two extra overlay lines.

Estimated effort: one Opus implementation pass + one QC/iterate loop — comparable to a single E-experiment, much smaller than the outer-edge effort because there is nothing to invent.

Key decisions

DecisionChoiceWhy
Detection machinery Reuse image_edge.extract_side with flipped polarity, no new algorithm The inner edge is the same physical transition (asphalt → vegetated strip) as the outer edge, mirrored. Median-step evidence already ignores the bright Randlinie painted 0.15–0.5 m from the edge — the exact failure mode a naive gradient would hit.
Search band placement Dynamic: locate the median gap per station from the rectified grid's no-data/valid mask + run4 surface presence near offset 0; config band inner_left_band ≈ [0.3, 5.0], inner_right_band ≈ [−5.0, −0.3] as hard limits only The axis wanders relative to the median (seg 74 outer-L at +15.6 vs 66 at +12.4 shows geometry drifts). A fixed narrow band would repeat the seg-73/74 S-weave failure. The median's signature (no-data holes + texture change) is strong enough to anchor the band.
Hard bound analogue Replace the outer surface-extent bound with an inner bound: the run4 gap around offset 0 (sustained-presence prior, mirrored inward) Outer edges use run4 extent as a "never go past this" cap. Inward, the same prior says "the edge is at or before the start of the median data gap". Prevents the path wandering onto the opposite carriageway where the median narrows.
No-inner-edge handling Confidence gating + existing flags (0 measured / 1 interp / 2 gap); never bridge Some stations may have a paved or barrier-only median (bridges, interchanges). Honest gaps are a standing project rule.
Output schema Extend EdgeResult + npz with inner_left_* / inner_right_* mirroring existing fields; overlay adds 2 lines (proposed: green = inner-L, orange = inner-R; yellow stays gutter) Backward-compatible; consumers of outer-edge fields unaffected.
Who implements One Opus subagent (geometry/vision per model policy), Fable QCs overlays Standing orchestration policy; task is well-scoped enough for a single agent, no experiment fan-out needed unless QC fails twice.

Phases

Phase 0 — Median recon small

Short diagnostic script over segments 66–74 (pattern: scripts/diagnose_profiles.py):

  • Per station: valid-pixel fraction and run4 presence in offset band [−5, +5] → median gap extent (left lip, right lip, width).
  • Distribution of median width; how often the gap vanishes (paved median / bridge).
  • Intensity/texture contrast across the inner lip vs the (accepted) outer lip — confirms the same evidence weights apply, or flags reweighting.

Output: one stats table + a couple of rectified-grid crops. Feeds the band defaults in Phase 1. ~30 min.

Phase 1 — Core implementation main effort

Opus agent, in-repo (no worktree needed — additive change):

  • image_edge.py: ImageEdgeConfig gains inner_left_band/inner_right_band + median-localisation params; extract_side gets an explicit direction (step polarity) decoupled from side (band choice) — inner-left = band left of the median lip, polarity of today's "right".
  • Median localiser: per-station gap from valid mask + run4 presence, smoothed with the existing sustained-presence prior code; produces per-station inner hard bounds.
  • detector.py: run 4 trackers (outer-L, outer-R, inner-L, inner-R) off the same rectified grid (built once — rectification dominates runtime, so total stays ≈14 s/segment); EdgeResult extended; same clean_offsets polyline hygiene from iolabs-geometry-geometry 0.7.0 applied per inner edge.
  • Sanity constraint: inner-L < outer-L and inner-R > outer-R at every station (min 2 m carriageway width) — violation ⇒ demote to gap.
  • overlay.py: two new lines + legend entries; dashed = interpolated, red dots = gap, as today.
  • asphalt_edge.default.json: new image.inner_* keys.
  • Tests: synthetic median gap (polarity correct on both sides), narrow/vanishing median → gap flags, Randlinie-adjacent edge not snapped to the paint line, cross-edge sanity constraint.
Phase 2 — Run + visual QC loop gated
  • scripts/run_segments.py --segments 66-74 --out output/run4 (runner needs no CLI change).
  • Fable visually checks all 9 intensity overlays against the material boundary — same bar as the outer-edge acceptance: lines on the asphalt/median transition, not on markings, not on the run4 extraction extent.
  • Iterate on band/weights if needed; if two QC rounds fail on the same failure mode, escalate to a worktree experiment fan-out (standing pattern).
Phase 3 — Wrap-up small
  • Commit under AI3D-337 (implementation and QC-accepted run separately).
  • Housekeeping ride-along: remove stale agent scratch scripts (scripts/exp_*.py, cache_segment.py, diagnose_profiles.py, smoke_image_edge.py) or move keepers under scripts/ with a purpose.
  • Deferred (from original handoff, not re-requested): XML export via iolabs-point-cloud-modelling-export, package release, git remote push.

Risks

Open questions