AI3D-382 · detection pass 9: unpaint the seg3d guardrail mask — the rails finally go, and no tree moves

7 Sep 2026 · segments 018 / 060 / 066 / 085 · roots: r8 (shipped low-band config — the reference) | r9 (r8 + guardrail_mask_unpaint_enabled = true) · masks /home/ai/seg3d_out/260905/guardrails_A1B0 · code wt-vs-382 @ bc5b3a32a06eb1 · previous rounds: passes 2–3 · pass 4 · pass 5 · pass 6 · pass 7 · pass 8 · background: what the DEM (Digital Elevation Model) bias was

  1. The diagnosis picked this move, not a hunch. Every DEM alternative was measured and every one loses more true tree than hard FP: the least-bad (ground.cell_m = 1.5) removes 3.0 k hard FP for 4.4 k GT-tree points (ratio 0.68), a noise-trim drops 135 k of 156 k GT-tree, p20/p35/p50 are worse still. Lowering the hard-cell point floor 300 → 100 captures 2.6 k (5.8 %) because the residual is an mrf failure, not an n failure (nlow p50 = 206, mrf p50 = 0.73); an elongation gate captures 10 points. The one move with a ratio above 1 was unpainting tree points covered by the seg3d guardrail mask: 42.8 % of the residual, zero GT-tree.
  2. It delivered exactly that. KEEP — ship ON where seg3d guardrail masks exist 085 human-GT guardrail points inside our tree paint 25 772 → 6 276 (−75.7 %); hard FP 46 519 → 26 965 (−42 %); precision 0.5028 → 0.5431, veg-tolerant precision 0.7705 → 0.8528.
  3. Zero true-tree loss. tp 156 209 in both roots, recall 0.7547 unchanged, lost_tree_bands {} empty. Everything unpainted sits at 0.3–1 m (23 052 pts) plus 2 points in 1–2 m; above 2 m: 0. Both truth crowns and both shrub banks visually identical.
  4. 0 detections added, 0 removed on all four segments — and every detection’s n_points and position is bit-identical to r8. The hook edits paint only, never cluster membership.
  5. The honest cost: 3 500 GT low/medium_veg points (0.6 % of the 085 veg paint) where a hedge grows through the rail and therefore falls inside the guardrail mask. The bush is still detected and still painted; only its rail-embedded sliver goes. If that ever matters the fix is guardrail_mask_unpaint_classes = ["w_beam"] plus mask erosion, not turning the knob off.
  6. What is left on 085 is not ours to fix with a paint rule: 19 892 road + 6 276 rail. The rail residual is what the seg3d guardrail model itself misses (its 085 vocabulary is w_beam + guardrail_support, no guardrail_top_rail); the road is not in the mask at all. Both sit inside the same two mega-instances that hold every true positive — that is instance splitting, which is seg3d’s domain.
  7. Downstream footnote worth knowing. In the fused product, guardrail masks already override tree paint, so these rails were partly self-correcting. But “tree” is not in the protected first-paint group in seg3d masks.py, so road and vehicle rows we paint tree still leak through the fuse. Adding tree to PAINT_FIRST_CLASSES is a one-line change that would catch the remaining road class.
  8. Review: no majors. Two robustness fixes went in before the config commit — the sidecar is now parsed with the shared reader (with a guard for desynced canopy extras, 15fd05a), and the run now warns when the mask’s record names match none of the segment’s run3 records instead of silently no-op’ing (ab85cfc).

0. How to read the renders

Each comparison render is one spot in the raw no-scan-angle-filter cloud, left to right: true RGB | number_of_returns (grey = 1, orange = 2, red = ≥ 3) | r8 (the BEFORE reference) | r9. The BEFORE column always comes first; top row oblique, bottom row the same points top-down; each column is labelled in the image itself. The last two figures are pass-8 diagnosis panels reproduced for context, captioned individually.

unmaskedtreemedium_vegetationsign-ish (sign / pole / delineator)guardrailother

Click any image for full resolution; ← / → (or swipe) step through the images of that card only; Esc closes.

1. What changed

featureknob / commitverdictreason (one line)
guardrail-mask unpainttree_detection.guardrail_mask_unpaint_enabled false → true (3e0d04f)keep — ship ON with masks085 GT class 66 −19 496 (−75.7 %), hard FP −19 554 (−42 %), 0 GT-tree lost, 0 detections changed. Defaults OFF, so runs without a seg3d guardrail model are unaffected.
which mask classes to unpaintguardrail_mask_unpaint_classes = [] (= all instance_type)keep as-is085 vocabulary is w_beam 1 496 703 + guardrail_support 64 681 pts. Restricting to w_beam is the escape hatch if the hedge cost ever bites.
mask sourcerail_halfpost.models_dir (already pointed at guardrails_A1B0)reuse, no new configmask_file_missing = 0 on all four segments; with no masks the hook is a no-op.
sidecar parsing15fd05areview fixParse the guardrail sidecar with the shared reader rather than a private path, and guard against desynced canopy extras.
silent-mismatch failure modeab85cfcreview fixIf the mask’s record_names match none of the segment’s run3 records the run now warns instead of quietly unpainting nothing.
ground.percentile / ground.cell_m / noise trim— (diagnosed, rejected)do not touchNo alternative DEM reaches hard-FP-removed / GT-tree-lost ≥ 1. Least-bad p8 / 1.5 m = 0.68 and barely touches rails; trim loses 135 k of 156 k GT-tree.
hard_blob_min_points_per_cell 300 → 100— (diagnosed, rejected)do not lowerResidual rails live in dense mixed cells (nlow p50 206, mrf p50 0.73) — they fail mrf, not n. Would capture 2 630 pts = 5.8 %.
elongated-join gate (L ≥ 5 m & W ≤ 2 m)— (diagnosed, rejected)do not addNew cells 8-join onto the wide mega-instance blobs, so the joined component is never elongated: 10 GT points captured on 085, 0 on 066.
recommended new-data config2a06eb1writtenLow band 0.3 m ON, mrf hysteresis 0.0, canopy-column guard as shipped, tcs_ground.per_segment = false, guardrail-mask unpaint ON.
mega-instance splitting— (open, seg3d)still openThe residual 19 892 road + 6 276 rail sits in the two instances that hold every TP. No paint rule reaches it.

2. The renders

The rails go — and the trees beside them do not move GT class 66 −75.7 %

Two 085 rail blobs, r8 against r9. In both the removed material is the w-beam itself: a continuous green line down the beam in r8, a broken residual in r9. The residual is where the seg3d model’s own mask stops, not where our rule stops.

BEFORE first · 085 (2289.72, 1229.92), r = 14 m, panels RGB | number_of_returns | r8 | r9 · r8 paints a solid bright-green line down the near w-beam and flecks along the far rail; r9 strips the beam to a broken residual. The conifer top-left and the shrub bank bottom-right are the identical green mass in both.
BEFORE first &middot; 085 (2289.72, 1229.92), r = 14 m, panels <b>RGB | number_of_returns | r8 | r9</b> &middot; r8 paints a solid bright-green line down the near w-beam and flecks along the far rail; <b>r9 strips the beam to a broken residual</b>. The conifer top-left and the shrub bank bottom-right are the identical green mass in both.
BEFORE first · 085 (2290.41, 1225.16), r = 12 m, a rail blob directly under a 24 m conifer · the continuous green rail line of r8 becomes short fragments in r9; the conifer is pixel-equivalent in both panels, as is the bush bank bottom-right. Correct removal, no crown damage.
BEFORE first &middot; 085 (2290.41, 1225.16), r = 12 m, a rail blob directly under a 24 m conifer &middot; the continuous green rail line of r8 becomes short fragments in r9; the <b>conifer is pixel-equivalent</b> in both panels, as is the bush bank bottom-right. Correct removal, no crown damage.

085 painted GT, guardrail (66): 25 772 → 6 276 · hard FP 46 519 → 26 965 · tp 156 209 → 156 209. Unpaint bands: 0.3–1 m 23 052, 1–2 m 2, > 2 m 0.

The honest cost: a hedge that grows through the rail 3 500 GT bush/grass points

Any bush growing through a guardrail has its rail-embedded points inside the seg3d guardrail mask, so they get unpainted with the beam. This is structural, not a bug — and it is bounded: the hedge is still detected and still painted, it just loses the sliver at the beam.

BEFORE first · 085 (2282.00, 1219.00), r = 8 m — the cost case. A real hedge grows through and behind the rail (RGB shows the green wall; the nr panel is solid orange multi-return right at the beam). 729 GT low/medium_veg points sit in the two 2 m cells at (2281.4, 1218.5) / (2282.7, 1219.5). r9 keeps the bush wall and its crown but shaves the foliage sliver lying inside the mask, so the top-down green line along the rail thins.
BEFORE first &middot; 085 (2282.00, 1219.00), r = 8 m &mdash; <b>the cost case</b>. A real hedge grows through and behind the rail (RGB shows the green wall; the nr panel is solid orange multi-return right at the beam). 729 GT low/medium_veg points sit in the two 2 m cells at (2281.4, 1218.5) / (2282.7, 1219.5). r9 keeps the bush wall and its crown but <b>shaves the foliage sliver lying inside the mask</b>, so the top-down green line along the rail thins.

085 GT low_veg −2 530 + medium_veg −970 = 3 500 points, 0.6 % of the segment’s veg paint. Also 55 road + 3 c70 points, and 44 points in the 066 1–2 m band (top of a w-beam post cluster, not crown).

Nothing happened to the crowns 0 GT-tree points, M1 / M1b / M2 / M3 identical

The truth crown is the visual check on the metric: r8 and r9 are the same image. The canopy metrics agree to 5 decimal places, and above-1 m tree paint on 018+060 moves by 272 points out of 10.15 M (0.0027 %).

BEFORE first · 018 (−3324.55, −2185.47), truth crown lowrej_018_c0_s0, canopy top 13.2 m · r8 and r9 are indistinguishable — full crown, full understorey; only the guardrail line at the bottom edge is lightly thinned. This is the whole GT-tree-loss story: 0 points.
BEFORE first &middot; 018 (&minus;3324.55, &minus;2185.47), truth crown <code>lowrej_018_c0_s0</code>, canopy top 13.2 m &middot; <b>r8 and r9 are indistinguishable</b> &mdash; full crown, full understorey; only the guardrail line at the bottom edge is lightly thinned. This is the whole GT-tree-loss story: <b>0 points</b>.

M1 hi 0.83264 · M1b mid 0.898 · M2 foliage 0.90608 · M3 9 cells / 28 556 pts (top-cell list cell-for-cell identical) · M4 6 signs / 56 delineators / 93 trees / 0 GHOST · M6 truth spots 12/12, recall 1.000, 0 FP — every one identical to r8.

Why the rails were there in the first place context, from the pass-8 diagnosis

Two panels from the pass-8 diagnosis that explain the shape of the problem this pass attacks: the paint above 2 m was already perfect, and the offending rail/road points live inside mega-instances that also hold real trees, so no per-cluster rejection could ever reach them. Only a point-level mask could.

Context (pass-8 diagnosis) · 085 instance 3 at (2288.69, 1263.25), 6.9 M points · 101 959 GT matches, 100 % non-tree, zero GT trees. GT paints the rail cyan and the deck brown; our paint greens the crown and the rail and the verge. The guardrail-mask unpaint is what finally reaches the cyan.
Context (pass-8 diagnosis) &middot; 085 instance 3 at (2288.69, 1263.25), 6.9 M points &middot; <b>101 959 GT matches, 100 % non-tree, zero GT trees</b>. GT paints the rail cyan and the deck brown; our paint greens the crown <b>and</b> the rail <b>and</b> the verge. The guardrail-mask unpaint is what finally reaches the cyan.
Context (pass-8 diagnosis) · 085 painted-GT counts per annotator class and per elevation band · above 2 m the tree paint is 100 % GT tree; everything wrong lives in the 0.3–1 m skirt. r9 removes 23 052 points from that skirt and 2 from 1–2 m — nothing above 2 m.
Context (pass-8 diagnosis) &middot; 085 painted-GT counts per annotator class and per elevation band &middot; <b>above 2 m the tree paint is 100 % GT tree</b>; everything wrong lives in the 0.3&ndash;1 m skirt. r9 removes 23 052 points from that skirt and <b>2</b> from 1&ndash;2 m &mdash; nothing above 2 m.

Residual after r9 on 085: 19 892 road (71) + 6 276 guardrail (66). The road is not in the mask; the rail residual is the part the seg3d guardrail model misses (no guardrail_top_rail class on 085).

3. Results

085 human GT (1 872 942 annotated points, 100.000 % matched @ 5 cm)

metricr6pr (pass 7)r8 (pass 8)r9r9 − r8
tp (GT tree, class 5)156 209156 209156 2090
fn50 76050 76050 7600
fp (all non-5)160 809154 467131 413−23 054
definition FP (74 low_veg + 75 medium_veg)108 498107 948104 448−3 500
hard FP (66 + 71 + 76 + 70)52 31146 51926 965−19 554 (−42 %)
  — guardrail (66)26 92225 7726 276−19 496 (−75.7 %)
  — road (71)20 01319 94719 892−55
  — c76 barrier5 1475715710
  — c70229229226−3
precision0.49270.50280.5431+0.0403
veg-tolerant precision0.74910.77050.8528+0.0823
recall0.75470.75470.75470

Unpainted r8 → r9 by class: {guardrail 19 496, low_veg 2 530, medium_veg 970, road 55, c70 3}; newly painted: {}. Bands: 0.3–1 m 23 052, 1–2 m 2, > 2 m 0, tree lost 0. nr≥2 share of painted GT 0.5685 → 0.6037.

Per segment — the new guardrail mask unpaint block, detections and runtime

segmentinstances touchedpoints unpaintedbiggest instancemask file missingdetections r8 / r9det_diffruntime r8 → r9
018211 701tree vegetation_rf, 11 387 of 525 437075 / 750 rem / 0 add255.66 → 242.44 s
060883 975tree vegetation_rf, 20 620 of 252 233080 / 800 / 0293.53 → 307.13 s
066897 797tree vegetation_rf, 75 977 of 3 522 476071 / 710 / 0388.33 → 414.44 s
0852399 092tree vegetation_rf, 220 598 of 6 935 43209 / 90 / 0376.49 → 369.72 s

Every touched instance is a tree; no detection dropped anywhere, and per-detection n_points / position lists compare equal r8 vs r9 on all four. hard_blob_unpaint blocks are identical to r8 (independent hook, runs first). Max runtime 414 s = 6.9 min against the 20 min/segment budget; the +26 s on 066 is mask load + point matching over 8 instances. returns stripper, canopy extension, hard-blob unpaint, Tree path and reject_rescue summary blocks are byte-identical; clusters.csv and rail_halfposts.csv md5 SAME on all four; 0 ERROR / Traceback / Exception in any log.

066 streak window x∈[685,706] y∈[317,338] — 3 387 256 raw points in both roots

DEM height bandr8r9delta
0.3–0.5 m10 4688 962−1 506
0.5–1 m17 4178 296−9 121
1–2 m3 8013 757−44
2–4 m12 79812 7980
4–10 m31 18031 1800
> 10 m64 53364 5330
h < 1 m (the vehicle/marking layer)27 88517 258−38 %, and −88.1 % vs the r6pr 145 164 baseline

4. Progress across all passes

085 human GT — base to r9

metricbasepass 2–3passes 4–7 (r6pr)pass 8 (r8)pass 9 (r9)
precision0.48930.49240.49270.50280.5431
veg-tolerant precision0.74130.74830.74910.77050.8528
hard FP (66+71+76+70)54 52052 53852 31146 51926 965
recall0.75470.75470.75470.75470.7547

Recall has never moved, and that is expected. The 085 annotation stops at the height a human could label reliably — canopy points above 10 m are effectively invisible to this GT, so the whole canopy-extension programme (which is where the recall work happened) is unmeasurable here. It is measured by the canopy metrics below.

018 / 060 canopy metrics — base to now

metricbasenow (r9)delta
M1 canopy recall > 10 m0.00920.8326+0.823
M1b canopy recall 4–10 m0.78030.8980+0.118
M2 foliage-cell recall (> 4 m)0.62990.9061+0.276
M3 hard contamination (points)195 67628 556−85 %
M6 truth spot recall / FP1.000 / 01.000 / 00

Read the two tables together: the canopy metrics are the recall story (a tree used to be detected but painted only to 10 m; now it is painted to its top), the 085 GT is the precision story (the paint used to include the rail and the deck; now it mostly does not).

5. Next