Inner (median-side) pavement edge detection
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
| Decision | Choice | Why |
|---|---|---|
| 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:ImageEdgeConfiggainsinner_left_band/inner_right_band+ median-localisation params;extract_sidegets an explicitdirection(step polarity) decoupled fromside(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);EdgeResultextended; sameclean_offsetspolyline 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: newimage.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 underscripts/with a purpose. - Deferred (from original handoff, not re-requested): XML export via iolabs-point-cloud-modelling-export, package release, git remote push.
Risks
- risk Median barrier / guardrail double transition. Steel barriers or concrete in the median produce a second intensity step inside the search band. Mitigation: inner hard bound from the run4 gap + Viterbi transition penalty; recon (Phase 0) tells us if 66–74 even has this.
- warn Sparse/no-data median. The 066 overlay shows black holes in the strip — evidence windows straddling no-data get noisy. The no-data (
invalid_penalty) machinery exists, but the inner band is much narrower than the outer one, somin_valid_fracmay need loosening for inner edges only. - warn Bright Randlinie ~0.3 m from the true edge. Robust median-step should ignore it (it ignores lane markings today), but the paint line is closer and wider near the median — a QC-watch item, and the reason mean-based evidence stays off.
- low Runtime. Two extra Viterbi passes on an existing grid: seconds. Memory unchanged (~3.8 GB peak) — fine for the 32 GB target.
Open questions
- Overlay colours: green (inner-L) / orange (inner-R) proposed — OK, or prefer paler variants of the outer cyan/magenta?
- Should inner edges also get the gutter annotation treatment? (Census found no Rinne in 4_5; proposal: skip for now.)
- Deliverable scope: npz + overlays now, XML export together with the outer edges at wrap-up?