AI3D-360 · Clean rail (V-member) vs guardrail-support separation
Summary
Guardrail V-members (the rail beam) and supports (posts) mix badly in the classified clouds: posts are painted as rail, and unmasked posts fall through to the vehicle sweep (the pink posts on A4_5 seg 105). Root cause, measured on real data: support instances are only created when a run-level autocorrelation comb passes post_min_confidence=0.35 — low-band clutter (grass, plinth) along the whole run drowns the comb contrast, so large rails get zero posts. Secondary causes: post XY is interpolated on the rail centerline (never measured), a hard 0.50 m height cap on support claiming, no seg3d fallback paint for supports, and voxel decimation letting rail outvote support.
Fix: measured per-post detection (peak-over-local-background in the station histogram, per-post confidence, comb demoted to a scoring prior), measured post XY/top geometry, claiming up to the actual beam bottom plus behind-beam shafts — plus seg3d-side support JSON paint, support-wins mask ordering, and a voxel-priority bump. Validated on battlebox against A4_5 segments 066/105/132 with a numeric metric gate.
Key decisions
- Per-post acceptance replaces the all-or-nothing run gate. Peaks with prominence ≥ 3 pts over a rolling median background (~5 m window), min separation 0.8 m, station extent ≤ 0.45 m, per-post confidence ≥ 0.25.
estimate_post_cadencestays as a prior: comb-slot peaks get boosted, missed slots are filled only if a weak peak exists there. - Post geometry is measured, not assumed. Post center = median (station, offset) of its low-band cluster mapped to world XY; post top = cluster z-max clamped to [0.35, beam_bottom+0.10]. JSON gains
post_offsets_m,post_top_z_m,post_confidences; support polyline/top-z become measured values (was: rail centerline + constant 0.50 m). - Height cap becomes per-station beam bottom (
polyline_bottom_z_m, fallbackground_z + 0.35) instead of the flat 0.50 m; behind-beam post shafts may be claimed up to rail top (flagpost_claim_behind_beam, default on). Dead knobcomponent_rail_band_mgets wired as the beam-band fallback ([0.35, 0.85]). - Low-band clutter not near any post stays rail (status quo, upstream's deliberate ambiguity decision) — this task cleans the post/rail split, not the rail/ground boundary.
- seg3d stops discriminating against supports: JSON paint fallback covers
guardrail_support(before the rail band paint, 0.35 m radius around post vertices), sidecar duplicate rows resolve support-over-rail, andguardrail_supportoutranksguardrailin voxel decimation so posts survive. - Flags-off output stays byte-identical (
enable_post_cadence/enable_support_class/enable_component_masks= false), enforced by the existing tests. - Execution: worktrees in both repos, frequent commits under AI3D-360, Opus subagents implement/validate/review via a deterministic workflow; production + debugging run on battlebox via the
~/devgit clones (plaingit pull).
Phases
1 · Upstream: measured per-post detection & claiming core
Repo 3dai.iolabs.pointcloud.guardrails, mostly guardrails/posts.py, outputs.py, config.py (+ guardrails.default.json mirror).
- Peak detection over the existing
RunLowBandEvidencestation histogram (0.10 m bins, band 0.10–0.35 m): smooth ~0.3 m, rolling-median background, prominence/extent/confidence gates; new knobspost_peak_min_prominence,post_peak_min_confidence,post_max_station_extent_m. - Measured post XY/top; support instances built for every rail with ≥1 accepted post (was: comb pass required).
adopt_post_points/relabel_support_points: cap at per-station beam bottom −0.02 m; footprint radius stays 0.25 m but centers on measured XY; behind-beam claiming up to rail top; decimation no-drop path preserved.- Synthetic test extensions in
tests/test_posts.py/test_support_class.py: clutter background, offset posts, per-station cap, behind-beam, JSON field round-trip.
Evidence anchoring the thresholds (A1 seg 050, rec045, near-line points): post band h∈[0.1,0.3], gap 0.3–0.4, beam band h∈[0.4,0.8]; 14.5k of 101k rail rows sit in the low band.
2 · seg3d: support paint, mask ordering, voxel priority core
guardrail_json.py: dropguardrail_supportfromSKIP_TYPES; paint unclassified points within 0.35 m XY of a post vertex, z ∈ [ground, top+0.10] → support; runs before the rail band paint so the rail can't absorb posts.fuse.pymask stage: support rows apply after rail rows on duplicate global rows (detector writesdedupe=False; decomposition intent = support wins).voxel.py/classes.py: bumpguardrail_supportpriority above tier 4 so post voxels survive decimation (LAS code 72 + violet color unchanged).- Unit tests for all three behaviors.
3 · Detector validation on battlebox gated
A4_5 segments 066, 105, 132 (data via lanefinder repo: ~/dev/3dai.lanefinder/data/00_external/260416_Abschnitt_4_5/lane_points), one detector invocation, --dump-point-masks, lane XML run7_lanes_20260723-163130.xml; baseline = out_260821. ~45–60 min (seg 132 alone ~25).
Metric gate (all segments):
- posts per 100 m of w_beam rail ≥ 20 (expect ~25–75 for 1.33–4.0 m hardware)
- support mask rows per post ≥ 30
- support rows in the beam band (road side) < 5%
- rail rows below beam bottom within 0.25 m of a post center < 20% of baseline
- rail instance count / total length within ±5% of baseline (no regression of rail acceptance)
Up to 2 fix-and-revalidate rounds on gate failure.
4 · Fusion validation + QC renders gated
seg3d-fuseon 066/105/132 with the new masks (inputs per~/rerun_seg.shrecipe), out toout/a45_vsep.- Checks: substantial support counts at every post; ~zero vehicle points within 0.5 m of post vertices (the old pink-post bug); support survival ratio through voxel decimation; height-split sanity.
- Top-down QC renders copied to local
/tmp/vsep_qc/for visual sign-off.
5 · Adversarial review + PRs
- Two Opus reviewers (one per repo diff) hunting wrong-output bugs: index/frame confusion, flags-off byte-identity, config mirror drift, decimation row loss, ordering side-effects. Criticals get fixed and re-tested.
- Then Bitbucket PRs from both branches; the seg3d branch also carries the previously uncommitted guardrail-JSON paint fallback (now committed).
Risks
- data Per-post peak detection may over-fire on kerb returns / reflector posts near the rail line; the station-extent ≤ 0.45 m and prominence gates are the guard, and the <5% beam-band contamination metric catches over-claiming. Thresholds are tunable knobs if A4_5 disagrees with the A1-derived numbers.
- regression Claiming up to beam bottom moves rows out of rail masks — downstream consumers that counted on rail row totals will see fewer rail rows near posts. Rail instance acceptance itself is untouched (gate: ±5% count/length).
- runtime Guardrail detect on seg 132 took ~25 min before; the extra per-post work must not blow this up (evidence pass is reused, so expected cost is small).
- voxel priority Bumping support above tier 4 can shadow other tier-4 classes (sign, tree) in shared voxels; reviewer + tests explicitly check other class orderings.
- infra Long battlebox runs die if the ssh/wsl session drops — runs are kept attached from the local side; run_summary.json is rewritten per invocation, so all 3 segments go in one call.
Open questions
- Behind-beam shaft claiming (
post_claim_behind_beam) — ship enabled?default Yes, default on — it's what makes the post read as one violet column instead of a stump; the beam-band contamination metric guards the road side.
alt Default off for the first delivery, enable after visual QC on more segments.
- Low-band clutter (grass/plinth) under the rail that is not near a post — keep it rail?
default Keep as rail (upstream status quo); scope stays post-vs-rail.
alt Drop it from the mask entirely — but then seg3d's vehicle sweep would claim it (pink), so this needs a companion seg3d rule first.
- Where should
guardrail_supportsit in the voxel priority order?default Just above
guardrail(support wins rail ties, everything else unchanged).alt Above all tier-4 detector classes — simpler, but posts could then eat sign/tree voxels at overlaps.
- After validation passes, open both Bitbucket PRs immediately?
default Yes — PRs from both t3code branches with the metric table + QC renders in the description; you merge when ready.
alt Hold PRs until you've eyeballed the /tmp/vsep_qc renders yourself.