Fence detection — passes 7, 8 and 9

AI3D-388 · branch t3code/implement-fence-detection · 2026-09-08 · 628 tests green · channel still default-off · off-state byte-identical to main (verified this pass)

Where we are. 312 candidates over the 25 segments that produce any → 29 accepted. Against your own verdicts: 7 of 7 fences kept, both crop edges rejected, the hedge rejected. Against the earlier agent labels: 14 of 14 fences kept. Recall on the training split was already settled two passes ago; this pass was about precision and about the class boundary you drew.

What is left is 19 candidates nobody has judged. They are all on one page — the judging page — with their renders and metrics. A line per id turns into a label file, and the thresholds follow from it.

What worked

1 · Your verdicts, applied: 13 for 13 on the objects you judged

110/5 ACCEPTED - the fence in grass. Crest 0.56 over all cells, 0.80 about its own line
110/5 ACCEPTED - the fence in grass. Crest 0.56 over all cells, 0.80 about its own line
093/3 ACCEPTED - crest 0.31, kept by the narrow-sheet exemption (width 0.44 m, sheet 0.94)
093/3 ACCEPTED - crest 0.31, kept by the narrow-sheet exemption (width 0.44 m, sheet 0.94)
028/8 REJECTED [vegetation] - the crop edge, sheet 0.17
028/8 REJECTED [vegetation] - the crop edge, sheet 0.17

Every object you called a fence is accepted; both objects you called not-a-fence are rejected; the hedge is rejected. The two exceptions are the ones you said either way was fine (127/3, 049/3) and the guardrail 015/2, which is below.

2 · Two bodies the first cut of this pass would have accepted

039/6 REJECTED [vegetation] - 0.91 m thick, 88% green. Pass 8 accepted it
039/6 REJECTED [vegetation] - 0.91 m thick, 88% green. Pass 8 accepted it
077/2 REJECTED [vegetation] - 56 m panel array, 0.79 m thick, sheet 0.26. Pass 8 accepted it
077/2 REJECTED [vegetation] - 56 m panel array, 0.79 m thick, sheet 0.26. Pass 8 accepted it

Both were found by the code review, not by me. Two separate mistakes let them in.

The sloped crest reading had inverted physics. The top profile is a height above ground, so a fence that merely follows the terrain is already flat by the plain reading — what the sloped reading actually admits is a top whose height above ground ramps. And capping the grade at 8 % let the acceptance window grow with length: over 30 m a line may wander 2.4 m, nine times the flat reading's own tolerance, which is enough for a six-bin fit to find a hedge top. The cap is now on the end-to-end drift (0.50 m at any length).

The thin-sheet exemption was an OR, and each half fails on a real object. The fitted width alone exempted your crop edge 028/8 — the ridge fit trims it to 0.36 m. The sheet share alone exempted these two: a body seen through its own occlusion puts a quarter of its upper mass in one column. It is now an AND, and every fence you named clears both readings.

What did not work

3 · The third echo does not separate the classes

299 candidates. Fences span the whole third-echo range; the sheet share is what splits them
299 candidates. Fences span the whole third-echo range; the sheet share is what splits them
028/7 ACCEPTED - the crop segment again, sheet 0.37, third echo 0.23. Needs your verdict
028/7 ACCEPTED - the crop segment again, sheet 0.37, third echo 0.23. Needs your verdict

The sign-detection repo separates foliage from hard objects on the share of pulses that split three ways or more — trees ~0.10 against 0.01–0.02 for sign plates — so I added that plane to the fence grid. On fences it does not hold: they run from 0.00 (the wooden post-and-rail 048/8) to 0.57 (103/6), straight through the crop edges at 0.43. That repo warns of exactly this in its own research notes — a mesh fence legitimately multi-echoes, because the pulse goes through the mesh. The metric ships measured and disarmed; sheet_frac_upper is doing the separation instead, and the dashed line is where it now sits.

Post cadence is still degenerate (~2.0 m for everything) and still reads no gate.

4 · The one false positive I cannot fix from this channel

015/2 ACCEPTED - you called it a guardrail. Opaque, 1.25 m, 43.8 m
015/2 ACCEPTED - you called it a guardrail. Opaque, 1.25 m, 43.8 m

On every measurement this channel takes, 015/2 and your wooden post-and-rail fence 048/8 are the same object: both opaque (6 % and 12 % multi-return), both flat-topped, both about the same height (1.25 m against 1.45 m), both thin. Any rule that deletes one deletes the other. The clean fix is arbitration — the guardrail channel claiming it — and you also said that segment's guardrail detections are themselves false positives, so there is nothing to arbitrate against yet. Logged, not tuned around.

Numbers

 pass 6pass 9
candidates312312
accepted2829
your fences kept4 of 77 of 7
your non-fences rejected0 of 22 of 2
agent-labelled fences kept14 of 1414 of 14
guardrails stolen00

Rejection reasons, pass 9: tall_mass 129, inside_carriageway 47, guardrail_overlap 40, outside_corridor 32, ragged_crest 13, vegetation 10, rail_like_top 6, wall_overlap 4, wall_like 2.

Tooling that came out of this pass

A candidate id is an ordinal of one run, so a label file keyed by id compares the wrong objects the moment a run adds or drops a candidate — which is what made the last report attribute losses to the wrong fences. Labels now transfer between runs by place (segment, side, offset, station overlap), labelled objects match first, and stale keys are reported apart from real recall losses. With the honest join, the pass-6 numbers above are themselves a correction of what the last report claimed.

Next