AI3D-360 · Clean rail (V-member) vs guardrail-support separation

2026-08-24 · Miro / AI3D · branches t3code/AI3D-360-v-support-separation (guardrails) + t3code/AI3D-360-v-support-cleanup (seg3d)

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

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 RunLowBandEvidence station histogram (0.10 m bins, band 0.10–0.35 m): smooth ~0.3 m, rolling-median background, prominence/extent/confidence gates; new knobs post_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: drop guardrail_support from SKIP_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.py mask stage: support rows apply after rail rows on duplicate global rows (detector writes dedupe=False; decomposition intent = support wins).
  • voxel.py/classes.py: bump guardrail_support priority 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-fuse on 066/105/132 with the new masks (inputs per ~/rerun_seg.sh recipe), out to out/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

Open questions

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

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

  3. Where should guardrail_support sit 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.

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