Guardrails × number_of_returns — pass 1

Neighbourhood multi-return post-filter · 2026-09-07 · AI3D-387 · branch t3code/fc369e5b · commits ef7ace7 (filter), ddb9bd9 (harness/sweep/render tool) · handoff: handoff-guardrails-returns-vs-greenery-20260907-0922

KEEP, default OFF. New knob family returns_filter_* drops mask rows whose multi-return neighbourhood rate is low, targeting phantom w_beam clumps in dense vegetation. On val segment 085 vs GT-66: P 0.733→0.753, F1 0.707→0.716, R roughly flat (0.682→0.682), all handoff acceptance gates pass, zero guardrail_support rows dropped anywhere. 10/10 vision-judge renders correct, no rail steel or posts removed. Known miss: short dense grass is single-return, so it survives (expected, addressed by pass 2's greenery-rate rule).

1. What changed

Default-OFF knob family:

KnobValue
returns_filter_enabledfalse (default)
returns_filter_radius_m0.5
returns_filter_tau0.20
returns_filter_min_neighbors5
returns_filter_instance_types["w_beam"]

Per mask row: share of mask points within a 0.5 m ball in (x, y, height-above-ground) whose pulse gave 2+ returns; drop w_beam rows with rate > 0.20; fewer than 5 neighbours → keep. Runs after relabel_support_points in both detect.py branches. number_of_returns read once per record; column absent → whole-segment no-op with a warning. Stats block "returns_filter" appended to run_summary.json.

Reviewed adversarially by Opus + Codex: 6 findings, 4 fixed, 2 documented. 427 unit tests pass.

2. Proof of no-op

Knob OFF on segments 085 and 060 versus the pre-change baseline at HEAD 41bd9eb:

ArtifactResult
point_masks.npzidentical
guardrails.jsonidentical
XMLdiffers only in <CreationDate>

3. Data / split

A1NR (no-scan-angle-filter export with number_of_returns), lane XML run7. Train segments run: 018, 060, 066. 085 (val, the only human GT) used as the point-level yardstick per the handoff rule. Test split untouched.

4. Results on 085

GT-centric vs GT class 66, 5 cm join, fresh HEAD baseline:

baselinepass 1
P0.73340.7530
R0.68240.6819
F10.70700.7157
TP / FP / FN78,044 / 28,368 / 36,32177,981 / 25,583 / 36,384

Rows dropped: 16,203 (all w_beam; guardrail_support 0). By GT class: medium vegetation (bushes) 10,247, low vegetation 3,374, unmatched 1,894, guardrail 459, terrain 229.

Acceptance gate (handoff §6)ThresholdActual
GT-66 lost≤ 1,000459pass
Bushes removed≥ 4,50010,247pass
Precision≥ 0.7400.753pass
F1≥ 0.6850.716pass
Recall≥ 0.6370.682pass
Support rows dropped00pass
Runtime≈ baseline1216 s vs 1242 spass
Peak RSS≈ baseline4.29 vs 4.28 GBpass

Note: the fresh HEAD baseline is not the A1B0 mask the handoff measured (that mask has 1,561,384 rows, R 0.638; ours 1,567,405 rows, R 0.682 — 085 was not in the A1B0 driver's segment list, provenance unknown). All deltas above are against the HEAD baseline.

5. Train segments (no GT)

SegmentRows droppedMask rowsSupports droppedRuntime (pass1 vs baseline)
0187,856776,82701333 s vs 1363 s
0605,6111,682,7180841 s vs 815 s
0669,0211,989,20401051 s vs 1072 s

Runtimes unchanged within noise everywhere.

6. Visual check

Independent Opus vision judge, 10 renders (085 ×4 incl. 2 random, 018/060/066 ×2 each): 10/10 correct, zero cases of rail steel or posts removed, no holes in any rail.

Removed = ragged clumpy off-axis blobs on the multi-return band (verge grass, scrub, hedge fringe); 085 spot_02 wiped a 13,869-row pure-vegetation false w_beam block to zero, correctly.

Missed (as predicted): short dense grass is single-return, so the tall grass swallowing the median rail in 060 spot_01 survives (2.4k of 205k removed); low grass strips along median rails in 060/066; guardrail_support blobs 3–6 m inside tree bands in 018 (trunks, single-return).

Render note: oblique panels cramped when tall trees stretch the extent; AFTER legend overlaps the panel corner.

Segment 085 (val, GT) — 4 spots, largest removals first, spot_03/04 random

085 . spot_01 . largest removal (of 4 shown)
085 . spot_01 . largest removal (of 4 shown)
085 . spot_02 . wiped 13,869-row false w_beam veg block to zero
085 . spot_02 . wiped 13,869-row false w_beam veg block to zero
085 . spot_03 . random check
085 . spot_03 . random check
085 . spot_04 . random check
085 . spot_04 . random check

Segment 018 (train) — guardrail_support blobs inside tree bands (single-return trunks) missed

018 . spot_01 . train seg, 7,856 rows dropped segment-wide
018 . spot_01 . train seg, 7,856 rows dropped segment-wide
018 . spot_02 . train seg, 7,856 rows dropped segment-wide
018 . spot_02 . train seg, 7,856 rows dropped segment-wide

Segment 060 (train) — spot_01 shows the predicted miss: tall grass swallowing the median rail survives

060 . spot_01 . tall grass swallowing median rail SURVIVES here (2.4k/205k removed)
060 . spot_01 . tall grass swallowing median rail SURVIVES here (2.4k/205k removed)
060 . spot_02 . train seg, 5,611 rows dropped segment-wide
060 . spot_02 . train seg, 5,611 rows dropped segment-wide

Segment 066 (train)

066 . spot_01 . train seg, 9,021 rows dropped segment-wide
066 . spot_01 . train seg, 9,021 rows dropped segment-wide
066 . spot_02 . train seg, 9,021 rows dropped segment-wide
066 . spot_02 . train seg, 9,021 rows dropped segment-wide

7. Verdict

KEEP — default stays OFF until the val gate.

8. Offline sweep that decides pass 2

dev/analysis/returns_085/sweep_085.md, on the A1B0 mask:

Rule R1 = returns rule OR (hag < 0.15 m AND green rate(exg > 0.03) > 0.30): 34,957 rows removed for 360 rail lost (returns alone: 8,734 for 358); P 0.729→0.769, recall unchanged.

Next: pass 2 implements R1 as greenery_floor_* knobs (in review now), with the floor threshold re-swept against the detector's own ground model.