AI3D-388 · Fence detection channel — passes 1-3 on the A1 train split (2026-09-07)

2026-09-07 · technical peer · branch t3code/implement-fence-detection · train split only

1. What was built

FileRole
guardrails/_model_fence.pyConfig slice: every fence_* key, JSON parity with the rest of the model.
guardrails/fence_evidence.pyO(grid) accumulator: 0.25 m cells, 0.3-3.0 m band histogram in 0.1 m bins, low band 0.10-0.30 m, tall band 3-8 m, multi-return and green (ExG > 0.06) counts whole-band and upper-band (hag ≥ 1.0 m), raw 3D moments, returns/RGB availability flags.
guardrails/fence.pyCell gates, fit through fence_view_config → shared detect_instances, support cells, candidate assembly.
guardrails/fence_metrics.py23+ metrics: top_p90/top_spread, tall_frac, low_band_frac, multi_return_rate(_upper), green_rate(_upper), interior_frac, edge_distance_med_m, nearest_guardrail/wall_lateral_m, planarity/linearity.
guardrails/fence_gates.pyThe 8 ordered post-gates (table below).
guardrails.json + segment_<seg>_fence_candidates.jsonOutput: a "fences" list plus a sidecar with every candidate and its accepted/reject_reason — fully auditable, replayable offline.
colors.py / export.pyFences drawn red, display ids Fnn, legend row painted only when a fence is present; XML feature type "fence".
tests/test_fence_channel.py, test_fence_gates.py, test_fence_upper_band.py536 tests passing.
dev/analysis/fence/render_fence_candidates.py (the 6-panel close-ups), aggregate_candidates.py, replay_gates.py (offline what-if on the candidate JSONs), labels_*.json, split.json.

2. Post-gates — order, rule, rationale

Applied in order inside detect_fence_candidates; the first firing rule is the recorded reject_reason. Defaults are shown inline in the rule column.

#GateRule (default)Why that number
1inside_carriagewayinterior_frac > 0.50All 24 labelled vehicles read interior_frac 1.0; hedges/trees read 0.0 and rails never exceed 0.5. A fence is never on the carriageway.
2guardrail_overlapnearest rail < 1.00 mThe 16 guardrail-labelled candidates are the segment’s own accepted rail refitted by the fence grid, 0.0-0.8 m from it.
3wall_overlapnearest wall < 3.00 mThe wall channel already owns bridge parapets, overpass edges and noise walls; pass-2 survivors sat 0.15 m and 2.78 m from an accepted wall.
4rail_like_toptop_p90 < 1.20 mLabelled rails and bare post rows measure top_p90 0.95-1.15 m; the fence prior starts at 1.0-2.5 m.
5tall_masstall_frac > 0.10Trees carry 0.11-0.71 of their mass above 3 m and the two noise-wall sheets 0.11/0.14; every other candidate is ≤ 0.08.
6vehicle_likelength < 20 m and edge dist < 7 m and multi < 0.05A short opaque body on a road surface the lane XML does not model. Costs a fence class and says so: short fences inside the clear zone are sacrificed.
7vegetation(green > 0.40 and multi > 0.12) or (multi > 0.55 and top_spread > 0.50 m)Upper-band rates preferred, whole-band fallback, null keeps. A wire mesh is itself 2-return, so transmissivity alone would delete real fences (092/3 reads multi_up 0.67) — the crest separates the classes: fence-like objects measure top_spread 0.02-0.30 m, the brown shrub row 059/4 measures 1.32 m.
8sparsepoints_per_m < 40Loose guard against dust and stray-return bridges only; the sparsest labelled real object is 67 pts/m. Never a class discriminator.

Pass-1 replay: all 68 labelled negatives rejected

replay_gates.py over the pass-1 candidate JSONs with the current defaults. No fence existed in those 11 segments, so a perfect score here is a necessary, not sufficient, condition.

LabelnRejected by
vehicle2423 inside_carriageway + 1 vegetation
guardrail1615 guardrail_overlap + 1 rail_like_top
tree1313 tall_mass
hedge102 guardrail_overlap, 2 rail_like_top, 6 vegetation
noise_wall21 inside_carriageway + 1 tall_mass
other22 inside_carriageway
building11 inside_carriageway
fence0— none existed in the 11 pass-1 segments

3. Pass-3 accepted candidates (11 of 90 on 12 segments)

Metrics from segment_<seg>_fence_candidates.json (accepted=true). multi_up/green_up are the upper-band (hag ≥ 1.0 m) rates. 048/4 and 048/6 are two fragments of one physical fence, so 6 fence-like rows = 5 distinct objects.

seg/idverdictFable labelOpus label len mtop_p90 mmulti_upgreen_up spread medge m
092/3fencefence (0.90)fence (0.85)34.41.750.670.150.1021.4
048/4fencefence (0.85)fence (0.90)15.82.300.080.251.256.7
048/6fencefence (0.85)fence (0.90)9.21.330.130.370.196.8
008/6fencefence (0.70)19.71.990.910.390.3017.1
092/8fencefence (0.75)flagged in scan as probable fence9.51.750.930.400.0222.7
127/1taxonomynoise_wall (0.70)noise_wall (0.55)37.72.370.020.150.2918.3
077/2disputedunclear (0.50) — wire fencebuilding_or_structure (0.90) — greenhouse eave12.51.300.500.010.1930.9
039/5false posbuilding_or_structure (0.85)building_or_structure (0.88)8.72.940.050.030.0818.0
077/3false posbuilding_or_structure (0.90)building_or_structure (0.92)9.52.460.200.341.3024.8
077/4false posbuilding_or_structure (0.90)building_or_structure (0.92)9.52.480.180.370.8724.0
008/11false posbuilding_or_structure (0.80)building_or_structure (0.85)11.42.820.080.060.2418.6

077/2 is a genuine dispute, recorded as such. Fable read a 1.3 m wire fence with posts every ~2.5 m; the Opus judge read the eave of a greenhouse/solar-panel roof slab (an inclined plane attached at the line in the cross-section). It is counted as a false positive in the tally above, i.e. the pessimistic reading. Its green_up of 0.01 with multi_up 0.50 is exactly the signature the crest cue was built to keep, which is why it survives either way.

4. Figures

Click any image for the full-resolution 1800×900 render; ←/→ (or A/D, swipe) cycle within one card. Each 6-panel render is: oblique RGB · oblique by number_of_returns · cross-section · top-down RGB · top-down returns · along-candidate side view.

True positives: fences found on train

Six panels each: oblique RGB, oblique coloured by number_of_returns (grey 1, orange 2, red 3+), cross-section, top-down RGB, top-down returns, along-candidate side view.

092/3 &middot; 1.8 m mesh fence, 34 m &middot; dead-straight top wire, posts every ~3 m, grass at the base &middot; multi_up 0.67, green_up 0.15, top_spread 0.10 m, top_p90 1.75 m
092/3 &middot; 1.8 m mesh fence, 34 m &middot; dead-straight top wire, posts every ~3 m, grass at the base &middot; multi_up 0.67, green_up 0.15, top_spread 0.10 m, top_p90 1.75 m
048/4 &middot; wooden post-and-rail fence &middot; three rails at 0.5/0.85/1.15 m on posts every ~2 m, single-return; the adjacent sign board lifts top_p90 to 2.30 m and top_spread to 1.25 m
048/4 &middot; wooden post-and-rail fence &middot; three rails at 0.5/0.85/1.15 m on posts every ~2 m, single-return; the adjacent sign board lifts top_p90 to 2.30 m and top_spread to 1.25 m
008/6 &middot; 2 m mesh fence in the gore between carriageway and ramp &middot; thin flat-topped sheet, ~0.3 m wide in the cross-section &middot; multi_up 0.91, green_up 0.39, top_spread 0.30 m
008/6 &middot; 2 m mesh fence in the gore between carriageway and ramp &middot; thin flat-topped sheet, ~0.3 m wide in the cross-section &middot; multi_up 0.91, green_up 0.39, top_spread 0.30 m
092/8 &middot; mesh panel 1.75 m under the tree canopy &middot; perfectly rectangular side view with two posts &middot; multi_up 0.93, green_up 0.40, top_spread 0.02 m, tall_frac 0.0002 (no canopy mass)
092/8 &middot; mesh panel 1.75 m under the tree canopy &middot; perfectly rectangular side view with two posts &middot; multi_up 0.93, green_up 0.40, top_spread 0.02 m, tall_frac 0.0002 (no canopy mass)

Taxonomy question + disputed

Both survive every gate; what they should be called is the open part.

127/1 &middot; solid 2.4 m painted panel wall with posters, 38 m &middot; uniformly single-return (multi_up 0.02), filled rectangle in the side view, no post gaps &middot; below the wall channel’s 2.5 m floor: fence or low noise wall?
127/1 &middot; solid 2.4 m painted panel wall with posters, 38 m &middot; uniformly single-return (multi_up 0.02), filled rectangle in the side view, no post gaps &middot; below the wall channel’s 2.5 m floor: fence or low noise wall?
077/2 &middot; disputed &middot; Fable read a 1.3 m wire fence with posts every ~2.5 m; Opus read the eave of a greenhouse/solar roof slab &middot; multi_up 0.50, green_up 0.01, top_spread 0.19 m, edge dist 30.9 m &middot; counted as a false positive in the tally
077/2 &middot; disputed &middot; Fable read a 1.3 m wire fence with posts every ~2.5 m; Opus read the eave of a greenhouse/solar roof slab &middot; multi_up 0.50, green_up 0.01, top_spread 0.19 m, edge dist 30.9 m &middot; counted as a false positive in the tally

Residual false positives (structures)

What the current gate set still lets through: opaque man-made sheets that are not fences.

039/5 &middot; overbridge parapet &middot; opaque grey planar slab, tall_frac 0.097 just under the 0.10 ceiling, accepted noise wall 4.5 m away (just over the 3.0 m wall_overlap clearance)
039/5 &middot; overbridge parapet &middot; opaque grey planar slab, tall_frac 0.097 just under the 0.10 ceiling, accepted noise wall 4.5 m away (just over the 3.0 m wall_overlap clearance)
077/3 &middot; gabled roof profiles of the same farm structure &middot; the side view is a sawtooth of inclined planes rising to 2.5 m, not posts with a flat top &middot; top_spread 1.30 m, multi_up 0.20
077/3 &middot; gabled roof profiles of the same farm structure &middot; the side view is a sawtooth of inclined planes rising to 2.5 m, not posts with a flat top &middot; top_spread 1.30 m, multi_up 0.20
008/11 &middot; facade/eave of a small building with a tiled roof &middot; top-down is a solid 12x8 m rectangle, cross-section a multi-layer mass &middot; top_p90 2.82 m, multi_up 0.08, green_up 0.06
008/11 &middot; facade/eave of a small building with a tiled roof &middot; top-down is a solid 12x8 m rectangle, cross-section a multi-layer mass &middot; top_p90 2.82 m, multi_up 0.08, green_up 0.06

What the gates remove (pass-1 negatives)

One example per dominant reject reason, from the 68 labelled pass-1 candidates and the scan.

002/3 &middot; car &middot; rejected by inside_carriageway: interior_frac 1.00 &middot; solid slab in the cross-section, zero multi-return
002/3 &middot; car &middot; rejected by inside_carriageway: interior_frac 1.00 &middot; solid slab in the cross-section, zero multi-return
002/0 &middot; hedge &middot; rejected by vegetation (AND branch): green 0.63 with multi-return 0.39 &middot; blobby cross-section, ragged crest
002/0 &middot; hedge &middot; rejected by vegetation (AND branch): green 0.63 with multi-return 0.39 &middot; blobby cross-section, ragged crest
018/0 &middot; stolen guardrail &mdash; the segment’s own accepted rail refitted by the fence grid over 83 m &middot; rejected by guardrail_overlap: 0.0 m from the accepted rail, top_p90 0.95 m (also below the 1.20 m rail_like_top floor)
018/0 &middot; stolen guardrail &mdash; the segment’s own accepted rail refitted by the fence grid over 83 m &middot; rejected by guardrail_overlap: 0.0 m from the accepted rail, top_p90 0.95 m (also below the 1.20 m rail_like_top floor)
059/4 &middot; brown non-green shrub row 30 m outboard &middot; transmissive but not green, so the colour branch cannot see it &middot; rejected by the crest cue: multi_up 0.80 with top_spread 1.32 m (fences measure 0.02-0.30 m)
059/4 &middot; brown non-green shrub row 30 m outboard &middot; transmissive but not green, so the colour branch cannot see it &middot; rejected by the crest cue: multi_up 0.80 with top_spread 1.32 m (fences measure 0.02-0.30 m)

Whole-segment views

Context for 092 (the clearest true positive) and the far-range recall limit.

092 overview &middot; all 0.3-3 m evidence cells coloured by number_of_returns; accepted candidate polylines red, rejected grey &middot; the accepted F03 runs along the property boundary at 34.6 m offside
092 overview &middot; all 0.3-3 m evidence cells coloured by number_of_returns; accepted candidate polylines red, rejected grey &middot; the accepted F03 runs along the property boundary at 34.6 m offside
092 top-down product overlay &middot; fences red (F03, F08), w_beam guardrails green, supports purple &middot; the fence list is additive: the guardrail products are byte-identical to the channel-off run
092 top-down product overlay &middot; fences red (F03, F08), w_beam guardrails green, supports purple &middot; the fence list is additive: the guardrail products are byte-identical to the channel-off run
008 probe &middot; recall limit &middot; a low fence at 45-55 m outboard, ~15 pts/m, outside the 35 m lateral corridor &middot; never proposed as a candidate, so no gate can be blamed for it
008 probe &middot; recall limit &middot; a low fence at 45-55 m outboard, ~15 pts/m, outside the 35 m lateral corridor &middot; never proposed as a candidate, so no gate can be blamed for it

Val gate survivors (final sweep)

The 9 candidates the final gate set accepts on the 64 val segments, plus the one new train survivor. Val was never used for tuning.

val 015/0 &middot; <b>disputed</b> &middot; Fable: 1.2 m post-and-rail fence, ladder of regular posts with a top and a lower rail; Opus: double-rail steel rest-area barrier (the upper rail ramps into an end terminal) &middot; len 28.6 m, top_p90 1.25 m, multi_up 0.05, top_spread 0.20 m
val 015/0 &middot; <b>disputed</b> &middot; Fable: 1.2 m post-and-rail fence, ladder of regular posts with a top and a lower rail; Opus: double-rail steel rest-area barrier (the upper rail ramps into an end terminal) &middot; len 28.6 m, top_p90 1.25 m, multi_up 0.05, top_spread 0.20 m
val 049/4 &middot; <b>disputed</b> &middot; Fable: gabled building roof; Opus: mesh panel fence on an embankment (flat top stepping down in panel-length increments) &middot; len 8.8 m, top_p90 1.95 m, multi_up 0.44, green_up 0.29
val 049/4 &middot; <b>disputed</b> &middot; Fable: gabled building roof; Opus: mesh panel fence on an embankment (flat top stepping down in panel-length increments) &middot; len 8.8 m, top_p90 1.95 m, multi_up 0.44, green_up 0.29
val 093/4 &middot; structure FP &middot; rows of tilted solar panels on posts &mdash; parallel inclined slabs at 4-5 m carried on columns &middot; top_p90 2.60 m, top_spread 0.72 m: mass at height beside the fitted line, which no current metric measures
val 093/4 &middot; structure FP &middot; rows of tilted solar panels on posts &mdash; parallel inclined slabs at 4-5 m carried on columns &middot; top_p90 2.60 m, top_spread 0.72 m: mass at height beside the fitted line, which no current metric measures
val 120/2 &middot; structure FP &middot; Fable: shelter/pergola at a parking bay; Opus: a vehicle &mdash; not a fence either way &middot; razor-flat top at 2.45 m, single-return, green_up 0.64
val 120/2 &middot; structure FP &middot; Fable: shelter/pergola at a parking bay; Opus: a vehicle &mdash; not a fence either way &middot; razor-flat top at 2.45 m, single-return, green_up 0.64
val 015/6 &middot; vehicle FP &middot; parked car at the rest area, unmistakable silhouette with wheel voids &middot; edge distance 8.3 m, just above the 7 m vehicle_like bound, so the rule cannot see it
val 015/6 &middot; vehicle FP &middot; parked car at the rest area, unmistakable silhouette with wheel voids &middot; edge distance 8.3 m, just above the 7 m vehicle_like bound, so the rule cannot see it
train 129/1 &middot; <b>new true positive</b> &middot; 1.3 m mesh fence, 27 m, posts every ~4-5 m, mesh 2-return between them &middot; multi_up 0.63, green_up 0.24, top_spread 0.28 m &middot; both judges agree
train 129/1 &middot; <b>new true positive</b> &middot; 1.3 m mesh fence, 27 m, posts every ~4-5 m, mesh 2-return between them &middot; multi_up 0.63, green_up 0.24, top_spread 0.28 m &middot; both judges agree

5. Final-code sweep over all train + val segments (val gate)

Code 78d825c (all gates), run from a frozen worktree over every train (64) and every val (64) segment, 0 failures. The val segments were never used for tuning — no threshold in §2 has ever seen them.

Reject reasontrainval
inside_carriageway145202
guardrail_overlap8085
wall_overlap41
rail_like_top812
tall_mass4471
vehicle_like14
vegetation3734
rejected319409
accepted129
candidates331418

Train: 12 survivors

The 11 of §3 plus segment 129 candidate 1 — a 1.3 m mesh fence 27 m long with posts every ~4-5 m, called a fence by both judges (Fable 0.85, Opus 0.94). See card q6.

Val: 9 survivors

Metrics from final/val/branch_000/<seg>/segment_<seg>/segment_<seg>_fence_candidates.json. Labels from labels_final_fable.json and labels_final_opus.json; where the two judges disagree, both readings are shown.

seg/idverdictFable labelOpus label len mtop_p90 mmulti_upgreen_up spread medge m
015/0disputedfence (0.90) — post-and-railguardrail (0.60) — double-rail barrier28.61.250.050.130.2016.0
049/4disputedbuilding_or_structure (0.85) — rooffence (0.78) — mesh panel8.81.950.440.290.3123.4
015/5vehiclevehicle (0.90) — parked HGVvehicle (0.75) — HGV trailer flank12.72.950.010.620.707.9
015/6vehiclevehicle (0.95) — carvehicle (0.93) — car11.61.650.000.050.608.3
093/4structurebuilding_or_structure (0.90) — solar rowsbuilding_or_structure (0.78) — panels on columns18.62.600.080.400.7225.0
094/5structurebuilding_or_structure (0.90) — solar rowsbuilding_or_structure (0.80) — roofed deck9.02.390.110.331.2828.5
120/1structurebuilding_or_structure (0.80) — underpass abutmentvehicle (0.85)29.02.850.010.530.956.8
120/2structurebuilding_or_structure (0.85) — shelter/pergolavehicle (0.75)9.62.900.010.640.5720.1
120/3structurebuilding_or_structure (0.85) — same sheltervehicle (0.75)9.22.840.010.630.4920.5

Val verdict, plainly

Val recall notes Opus judge only

Reported by the Opus judge from the overviews and RGB overlays; not independently verified by Fable. Recorded as leads, not as counted misses.

6. Method notes

7. Next steps

  1. Pass-4 structure cue. An O(grid) lateral-occupancy-at-height metric: mass 1-3 m to the side of the fitted line, above 1 m hag, single-return. Roofs, canopies and panel rows all have it; a fence sheet has nothing beside it. This is the single change that addresses the whole residual false-positive family.
  2. Vehicle rule. Raise the edge-distance bound, or drive it from a lane-XML parking / rest-area zone — the two val vehicle FPs sit at 7.9 m and 8.3 m, just outside the current 7 m bound.
  3. Post-cadence metric from the side-view evidence: periodic ground-to-top posts, which roofs and parapets do not have.
  4. Coverage. A fence-view merge/bridging step so one continuous physical fence becomes one instance (048, 049 and 129 are all truncated or split today).
  5. HBW taxonomy. Post-and-rail fence vs Geländer; solid panel wall vs Fence §7.6; double-rail barriers (the 015/0 dispute is exactly this question).
  6. Corridor. Fences at 30-45 m outboard need fence_offset_max_m > 35 plus a sparse-fit path; today they are never proposed, so no gate can recover them.