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.
t3code/implement-fence-detection, ticket AI3D-388). Master switch fence_detection_enabled, default false.guardrails.json, XML modulo CreationDate, all PNGs; verified on segments 002, 060, 086.number_of_returns + RGB present (/mnt/d/a123-data/Abschnitt_1_no_angle_filter_returns). Split reused from the AI3D-387 greenery work (train 64 / val 64 / test 14, dev/analysis/fence/split.json). Everything here is train only; val/test untouched.guardrail_overlap/rail_like_top. The guardrail list itself is untouched by design: the channel runs after the rail gates and never through them.| File | Role |
|---|---|
guardrails/_model_fence.py | Config slice: every fence_* key, JSON parity with the rest of the model. |
guardrails/fence_evidence.py | O(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.py | Cell gates, fit through fence_view_config → shared detect_instances, support cells, candidate assembly. |
guardrails/fence_metrics.py | 23+ 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.py | The 8 ordered post-gates (table below). |
guardrails.json + segment_<seg>_fence_candidates.json | Output: a "fences" list plus a sidecar with every candidate and its accepted/reject_reason — fully auditable, replayable offline. |
colors.py / export.py | Fences 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.py | 536 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. |
Applied in order inside detect_fence_candidates; the first firing rule is the recorded reject_reason. Defaults are shown inline in the rule column.
| # | Gate | Rule (default) | Why that number |
|---|---|---|---|
| 1 | inside_carriageway | interior_frac > 0.50 | All 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. |
| 2 | guardrail_overlap | nearest rail < 1.00 m | The 16 guardrail-labelled candidates are the segment’s own accepted rail refitted by the fence grid, 0.0-0.8 m from it. |
| 3 | wall_overlap | nearest wall < 3.00 m | The 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. |
| 4 | rail_like_top | top_p90 < 1.20 m | Labelled rails and bare post rows measure top_p90 0.95-1.15 m; the fence prior starts at 1.0-2.5 m. |
| 5 | tall_mass | tall_frac > 0.10 | Trees 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. |
| 6 | vehicle_like | length < 20 m and edge dist < 7 m and multi < 0.05 | A 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. |
| 7 | vegetation | (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. |
| 8 | sparse | points_per_m < 40 | Loose guard against dust and stray-return bridges only; the sparsest labelled real object is 67 pts/m. Never a class discriminator. |
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.
| Label | n | Rejected by |
|---|---|---|
| vehicle | 24 | 23 inside_carriageway + 1 vegetation |
| guardrail | 16 | 15 guardrail_overlap + 1 rail_like_top |
| tree | 13 | 13 tall_mass |
| hedge | 10 | 2 guardrail_overlap, 2 rail_like_top, 6 vegetation |
| noise_wall | 2 | 1 inside_carriageway + 1 tall_mass |
| other | 2 | 2 inside_carriageway |
| building | 1 | 1 inside_carriageway |
| fence | 0 | — none existed in the 11 pass-1 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/id | verdict | Fable label | Opus label | len m | top_p90 m | multi_up | green_up | spread m | edge m |
|---|---|---|---|---|---|---|---|---|---|
| 092/3 | fence | fence (0.90) | fence (0.85) | 34.4 | 1.75 | 0.67 | 0.15 | 0.10 | 21.4 |
| 048/4 | fence | fence (0.85) | fence (0.90) | 15.8 | 2.30 | 0.08 | 0.25 | 1.25 | 6.7 |
| 048/6 | fence | fence (0.85) | fence (0.90) | 9.2 | 1.33 | 0.13 | 0.37 | 0.19 | 6.8 |
| 008/6 | fence | fence (0.70) | — | 19.7 | 1.99 | 0.91 | 0.39 | 0.30 | 17.1 |
| 092/8 | fence | fence (0.75) | flagged in scan as probable fence | 9.5 | 1.75 | 0.93 | 0.40 | 0.02 | 22.7 |
| 127/1 | taxonomy | noise_wall (0.70) | noise_wall (0.55) | 37.7 | 2.37 | 0.02 | 0.15 | 0.29 | 18.3 |
| 077/2 | disputed | unclear (0.50) — wire fence | building_or_structure (0.90) — greenhouse eave | 12.5 | 1.30 | 0.50 | 0.01 | 0.19 | 30.9 |
| 039/5 | false pos | building_or_structure (0.85) | building_or_structure (0.88) | 8.7 | 2.94 | 0.05 | 0.03 | 0.08 | 18.0 |
| 077/3 | false pos | building_or_structure (0.90) | building_or_structure (0.92) | 9.5 | 2.46 | 0.20 | 0.34 | 1.30 | 24.8 |
| 077/4 | false pos | building_or_structure (0.90) | building_or_structure (0.92) | 9.5 | 2.48 | 0.18 | 0.37 | 0.87 | 24.0 |
| 008/11 | false pos | building_or_structure (0.80) | building_or_structure (0.85) | 11.4 | 2.82 | 0.08 | 0.06 | 0.24 | 18.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.
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.
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.
Both survive every gate; what they should be called is the open part.
What the current gate set still lets through: opaque man-made sheets that are not fences.
One example per dominant reject reason, from the 68 labelled pass-1 candidates and the scan.
Context for 092 (the clearest true positive) and the far-range recall limit.
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.
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 reason | train | val |
|---|---|---|
inside_carriageway | 145 | 202 |
guardrail_overlap | 80 | 85 |
wall_overlap | 4 | 1 |
rail_like_top | 8 | 12 |
tall_mass | 44 | 71 |
vehicle_like | 1 | 4 |
vegetation | 37 | 34 |
| rejected | 319 | 409 |
| accepted | 12 | 9 |
| candidates | 331 | 418 |
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.
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/id | verdict | Fable label | Opus label | len m | top_p90 m | multi_up | green_up | spread m | edge m |
|---|---|---|---|---|---|---|---|---|---|
| 015/0 | disputed | fence (0.90) — post-and-rail | guardrail (0.60) — double-rail barrier | 28.6 | 1.25 | 0.05 | 0.13 | 0.20 | 16.0 |
| 049/4 | disputed | building_or_structure (0.85) — roof | fence (0.78) — mesh panel | 8.8 | 1.95 | 0.44 | 0.29 | 0.31 | 23.4 |
| 015/5 | vehicle | vehicle (0.90) — parked HGV | vehicle (0.75) — HGV trailer flank | 12.7 | 2.95 | 0.01 | 0.62 | 0.70 | 7.9 |
| 015/6 | vehicle | vehicle (0.95) — car | vehicle (0.93) — car | 11.6 | 1.65 | 0.00 | 0.05 | 0.60 | 8.3 |
| 093/4 | structure | building_or_structure (0.90) — solar rows | building_or_structure (0.78) — panels on columns | 18.6 | 2.60 | 0.08 | 0.40 | 0.72 | 25.0 |
| 094/5 | structure | building_or_structure (0.90) — solar rows | building_or_structure (0.80) — roofed deck | 9.0 | 2.39 | 0.11 | 0.33 | 1.28 | 28.5 |
| 120/1 | structure | building_or_structure (0.80) — underpass abutment | vehicle (0.85) | 29.0 | 2.85 | 0.01 | 0.53 | 0.95 | 6.8 |
| 120/2 | structure | building_or_structure (0.85) — shelter/pergola | vehicle (0.75) | 9.6 | 2.90 | 0.01 | 0.64 | 0.57 | 20.1 |
| 120/3 | structure | building_or_structure (0.85) — same shelter | vehicle (0.75) | 9.2 | 2.84 | 0.01 | 0.63 | 0.49 | 20.5 |
vehicle_like bound, so the rule cannot reach them.guardrail_overlap and 12 by
rail_like_top; the guardrail list is untouched by construction, since the channel runs
after the rail gates and never through them.Reported by the Opus judge from the overviews and RGB overlays; not independently verified by Fable. Recorded as leads, not as counted misses.
uv run python dev/analysis/fence/replay_gates.py --runs-root /home/ai/gr_fence_out/p1 --labels dev/analysis/fence/labels_p1.json [--set fence_min_top_p90_m=1.4].fence_offset_max_m > 35 plus a sparse-fit path; today they are never proposed, so no gate can recover them.