Guardrails greenery filters — pass 2 (green-voted height floor) and final val gate

2026-09-07 · AI3D-387 · branch t3code/fc369e5b · commits 1bba057 (greenery floor), 0f95767 (detector-hag re-sweep), ef7ace7 (pass-1 returns filter) · pass-1 report: ai3d-387-returns-pass1-20260907 · handoff: handoff-guardrails-returns-vs-greenery-20260907-0922

KEEP both filters, default stays OFF. Recommended production config: --set returns_filter_enabled=true --set greenery_floor_enabled=true. Pass 2 adds a green-voted height floor for w_beam mask rows near the ground. On val segment 085 vs GT-66 (both filters on): P 0.7334→0.7937, R roughly flat (0.6824→0.6818), F1 0.7070→0.7335. The final gate on the never-tuned val split (003, 022, 023, 087, 093, 094) passed vision judge 12/12, with caveats: treat 0.15 m as a ceiling on the floor, 022's overgrown median hedge is only partially helped, and image checks can't rule out sub-pixel bottom-flange loss.

1. What changed (pass 2)

Second default-OFF knob family:

KnobValue
greenery_floor_enabledfalse (default)
greenery_floor_hag_m0.15
greenery_floor_green_votetrue
greenery_floor_exg_min0.06
greenery_floor_green_rate_tau0.30
greenery_floor_radius_m0.5
greenery_floor_min_neighbors5
greenery_floor_instance_types["w_beam"]

Rule: drop a w_beam row when height-above-ground < 0.15 m AND the share of mask neighbours (0.5 m ball in x, y, hag) with excess-green index exg = (2G−R−B)/(R+G+B) > 0.06 exceeds 0.30. RGB read once per record; both filters share one KD-tree; sequential stats ("greenery_floor" block after "returns_filter" in run_summary.json).

No-op guarantees: knob off → byte-identical; RGB absent with green vote on → zero drops plus a warning; a per-segment warning fires when the floor sits at or below the mask replay's own height floor (0.20 m on the non-component path, 0.10 m with component masks).

Code renamed: returns_filter.pymask_filters.py, ReturnsRowsMaskRowAttributes.

Adversarial review Opus + Codex: 10 findings, all fixed (default exg 0.03→0.06, lazy tree on no-op paths, chunk/decimation alignment test, in-place exg reduction 32→14 B/pt peak, boundary-strictness tests). 473 unit tests pass.

2. Why exg 0.06 and not the sweep's 0.03

dev/analysis/returns_085/resweep_detector_hag.md: the detector's own ground model is the 8th percentile of all points per 0.75 m cell, so where no ground return exists (under the beam, grass banks) it sits above the true surface; mask-row hag is biased low (median −0.010 m, RMSE 0.049 m; w_beam p1 −0.14 m). The DEM-tuned rule (hag<0.15 & green(exg>0.03)>0.30) costs 2,479 GT-66 rows on detector hag instead of 360; tightening the vote to exg>0.06 costs 447 (P 0.782, F1 0.702 on the A1B0 mask). An ungated 0.15 m floor costs 3,744 — the colour vote is load-bearing.

RuleDroppedGT-66 lostGT-74GT-71PF1
returns only8,7403652,2811800.73940.6845
+ floor 0.15, exg>0.03 vote52,3352,47917,00625,1990.79110.7052
+ floor 0.15, exg>0.06 vote (shipped)42,73444715,23919,6840.78170.7020
+ floor 0.15 ungated54,2493,74417,30425,4980.79210.7050
+ floor 0.20, exg>0.06 vote65,3071,75127,57927,2330.81000.7130

Table basis: detector hag, A1B0 mask, w_beam-scoped, returns rule included in every row.

3. Proof of no-op

On segment 085 versus the pre-change HEAD baseline (41bd9eb):

ConfigurationResult
All knobs offpoint_masks.npz and guardrails.json identical to baseline
Returns filter onlyidentical to the pass-1 output

4. Results on 085 (both filters on)

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

baselinepass 1pass 2
P0.73340.75300.7937
R0.68240.68190.6818
F10.70700.71570.7335
Rows dropped16,20349,935
GT-66 lost459518
Terrain / low-veg / bush removed229 / 3,374 / 10,24720,227 / 15,661 / 10,394

Floor increment alone (pass 2 vs pass 1): 33,732 rows dropped for 59 additional GT-66; 19,998 terrain, 12,287 low veg. guardrail_support drops 0. Runtime and peak RSS unchanged (4.28 GB).

5. Train segments

Rows dropped (returns / floor) of total mask rows; guardrail_support drops 0 everywhere.

SegmentMask rowsReturns droppedFloor dropped
018776,8277,85626,413
0601,682,7185,61148,652
0661,989,2049,02154,463

Vision judge (Opus, 10 renders of the floor increment, 085×4 + 018/060/066×2): 10/10 correct. Removals form a band on the ground side of the beam, no holes, rail-core pixel counts within ±2% (usually up, because overlapping grass no longer hides the core), post bases intact.

Caveat: 066 spot_01 — a rail end terminal ramping toward the ground loses a few pixels at its foot; worth one targeted check of terminals.

Remaining: above-floor tufts/ferns along the rails; the grass bank in 060 spot_01 that pass 1 mislabelled is only trimmed.

Floor increment renders — 085 (val, GT) ×3, 018/060/066 (train) ×1

085 . spot_01 . 8,134 removed
085 . spot_01 . 8,134 removed
085 . spot_02 . 8,777 removed
085 . spot_02 . 8,777 removed
085 . spot_03 . 2,998 removed
085 . spot_03 . 2,998 removed
018 . spot_01 . 3,067 removed
018 . spot_01 . 3,067 removed
060 . spot_01 . 7,918 removed
060 . spot_01 . 7,918 removed
066 . spot_01 . 10,499 removed (terminal caveat)
066 . spot_01 . 10,499 removed (terminal caveat)

6. Final gate on the val split

003, 022, 023, 087, 093, 094 — never tuned on. Base config vs recommended = both knobs on. No GT available for this split; vision judge only. Instance counts unchanged, runtime/RSS unchanged.

SegmentMask rowsReturns droppedFloor droppedTotal %
0031,054,0918,6162,6361.1%
0221,263,8548,41844,9684.2%
0231,366,8164,41849,3773.9%
0872,335,4321,16468,4013.0%
0931,390,5151,34377,1885.6%
0941,357,4741,77157,0914.3%

Vision judge (Opus, 12 renders, 2 per segment: largest removal cell + random): 12/12 correct.

PASS WITH CAVEATS

Segment 003 (val)

003 . spot_01 . 4,860 removed (largest cell)
003 . spot_01 . 4,860 removed (largest cell)
003 . spot_02 . 89 removed (random)
003 . spot_02 . 89 removed (random)

Segment 022 (val)

022 . spot_01 . 11,510 removed (largest cell)
022 . spot_01 . 11,510 removed (largest cell)
022 . spot_02 . 5,621 removed (random)
022 . spot_02 . 5,621 removed (random)

Segment 023 (val)

023 . spot_01 . 1,917 removed (largest cell)
023 . spot_01 . 1,917 removed (largest cell)
023 . spot_02 . 4,553 removed (random)
023 . spot_02 . 4,553 removed (random)

Segment 087 (val)

087 . spot_01 . 12,271 removed (largest cell)
087 . spot_01 . 12,271 removed (largest cell)
087 . spot_02 . 671 removed (random)
087 . spot_02 . 671 removed (random)

Segment 093 (val)

093 . spot_01 . 12,693 removed (largest cell)
093 . spot_01 . 12,693 removed (largest cell)
093 . spot_02 . 417 removed (random)
093 . spot_02 . 417 removed (random)

Segment 094 (val)

094 . spot_01 . 9,184 removed (largest cell)
094 . spot_01 . 9,184 removed (largest cell)
094 . spot_02 . 2,220 removed (random)
094 . spot_02 . 2,220 removed (random)

7. Verdict

KEEP both filters. Defaults remain OFF in the shipped config; recommended production config = --set returns_filter_enabled=true --set greenery_floor_enabled=true. Flipping the defaults ON is Miro's call (release decision).

8. Not improved / next