seg3d — tree precedence & vehicle reclaim: results of the 2026-09-07 pass
Summary
Change 1 (tree in PAINT_FIRST_CLASSES) is correct but almost a no-op on this data: it moves ≤46 k points per segment (tree → asphalt / lines), zero on 060, and grows no guardrail/wall anywhere. The mega-instance rows over the road were still unclassified at mask time, exactly as the handoff predicted, so only the vehicle-sweep reclaim (change 2) reaches them.
Change 2 as specified (reclaim anywhere inside the corridor polygon, variant c1c2) fixes every car-noise point the 085 ground truth still saw as tree (571 → 0) but risk confirmed strips verge canopy: the corridor polygon runs past the guardrail, and 1,654 human-tree + 2,793 human-low-veg points on 085 became vehicle. Net on GT it is negative.
Refinement shipped as the default (vehicle_reclaim_rule=asphalt_column, variant c3): reclaim only greenery whose XY cell holds painted asphalt, on the vegetation stage's occupancy grid. On 085 GT it keeps the car-noise fix (571 → 0 tree) and cuts the real-tree loss from 1,654 to 1,278 points; the remainder is canopy overhanging the hard shoulder, which a planar rule cannot tell from a truck — a height-above-DTM gate is the next lever. The corridor rule stays selectable. Per Miro's instruction the reclaim covers tree, low_vegetation and medium_vegetation; the r9 masks carry no detector low/medium instances, so that part is idle on these segments. The per-point vegetation stage's own rows are exempt (AI3D-373 median hedges).
What changed in the code
masks.py:PAINT_FIRST_CLASSES = {low_vegetation, medium_vegetation, tree}— rank −1 + soft gate (unclassified/ground only). Comment carries the mega-instance argument and the one-sidecar order-dependency note.fuse.py::_paint_vehicles: sweep candidates = unclassified ∪ detector greenery rows (minusstate.veg_stage_rows), gated byvehicle_reclaim_vegetation(default true) andvehicle_reclaim_rule(asphalt_columndefault |corridor). New metricvehicle_reclaimed_vegetation._config_model.py,seg3d.default.json,docs/fusion_spec.md(steps 6/7),README.mdupdated; tests: +7 (paint order with tree, tree-vs-hard-class gate, guardrail/wall beat tree on contested rows, fuse-level reclaim under both rules, flag off).uv run pytest: 581 passed.- Not changed:
PAINT_LAST_CLASSES, JSON-only sign/tree paint (already unclassified-only), no instance splitting.
Results per segment (full resolution and decimated)
Variants: base = main a584a8a; c1 = change 1 only (--set vehicle_reclaim_vegetation=false); c1c2 = change 1 + reclaim, corridor rule; c3 = change 1 + reclaim, asphalt_column rule (shipped default). Inputs: 260905/260904 stages, verticalsigns r9 masks (018/060/066/085) and val_r9 (093), 1 cm voxel. Direction check: in every variant only tree goes down; asphalt, lines, guardrail and vehicle go up (tiny ground/low-veg wobbles in the decimated counts are voxel-representative re-picks).
Segment 018
Reclaimed greenery → vehicle: c1c2 (corridor) 2,870 · c3 (asphalt_column) 1,001 points.
Tree instances: 41 → 41 (none vanish; they shrink).
Full-resolution deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 5,097,956 | -1,242 | -4,112 | -2,243 |
| vehicle | 342,304 | +0 | +2,870 | +1,001 |
| asphalt | 6,129,721 | +1,242 | +1,242 | +1,242 |
| solid_line | 1,137,764 | +0 | +0 | +0 |
| guardrail | 1,364,970 | +0 | +0 | +0 |
| wall | 134 | +0 | +0 | +0 |
| ground | 16,625,712 | +0 | +0 | +0 |
| low_vegetation | 3,143,268 | +0 | +0 | +0 |
| medium_vegetation | 1,155,591 | +0 | +0 | +0 |
Decimated (1 cm) deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 4,937,177 | -1,214 | -4,015 | -2,189 |
| vehicle | 254,737 | +0 | +2,800 | +974 |
| asphalt | 3,667,309 | +1,214 | +1,215 | +1,215 |
| solid_line | 637,686 | +0 | +0 | +0 |
| guardrail | 1,043,712 | +0 | +0 | +0 |
| wall | 134 | +0 | +0 | +0 |
| ground | 10,075,245 | +0 | +0 | +0 |
| low_vegetation | 2,911,933 | +0 | +0 | +0 |
| medium_vegetation | 1,048,851 | +0 | +0 | +0 |
Segment 060
Reclaimed greenery → vehicle: c1c2 (corridor) 0 · c3 (asphalt_column) 0 points.
Tree instances: 52 → 52 (none vanish; they shrink).
Full-resolution deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 7,163,411 | +0 | +0 | +0 |
| vehicle | 410,837 | +0 | +0 | +0 |
| asphalt | 16,481,924 | +0 | +0 | +0 |
| solid_line | 877,212 | +0 | +0 | +0 |
| guardrail | 3,040,409 | +0 | +0 | +0 |
| ground | 1,366,595 | +0 | +0 | +0 |
| low_vegetation | 411,959 | +0 | +0 | +0 |
| medium_vegetation | 668,206 | +0 | +0 | +0 |
Decimated (1 cm) deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 7,094,478 | +0 | +0 | +0 |
| vehicle | 321,808 | +0 | +0 | +0 |
| asphalt | 9,115,452 | +0 | +0 | +0 |
| solid_line | 457,728 | +0 | +0 | +0 |
| guardrail | 2,518,981 | +0 | +0 | +0 |
| ground | 1,151,167 | +0 | +0 | +0 |
| low_vegetation | 384,024 | +0 | +0 | +0 |
| medium_vegetation | 661,828 | +0 | +0 | +0 |
Segment 066
Reclaimed greenery → vehicle: c1c2 (corridor) 60,614 · c3 (asphalt_column) 43,334 points.
Tree instances: 61 → 61 (none vanish; they shrink).
Full-resolution deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 12,794,916 | -16,516 | -77,130 | -59,850 |
| vehicle | 695,114 | +0 | +60,614 | +43,334 |
| asphalt | 15,415,798 | +16,510 | +16,510 | +16,510 |
| solid_line | 914,455 | +6 | +6 | +6 |
| guardrail | 3,156,030 | +0 | +0 | +0 |
| ground | 3,449,574 | +0 | +0 | +0 |
| low_vegetation | 540,936 | +0 | +0 | +0 |
| medium_vegetation | 933,989 | +0 | +0 | +0 |
Decimated (1 cm) deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 12,708,084 | -16,277 | -75,486 | -58,568 |
| vehicle | 518,205 | +0 | +59,201 | +42,284 |
| asphalt | 8,672,826 | +16,271 | +16,276 | +16,276 |
| solid_line | 518,792 | +6 | +6 | +6 |
| guardrail | 2,506,335 | +0 | +0 | +0 |
| ground | 2,480,665 | +0 | +2 | +2 |
| low_vegetation | 480,252 | +0 | +1 | +0 |
| medium_vegetation | 914,754 | +0 | +0 | +0 |
Segment 085
Reclaimed greenery → vehicle: c1c2 (corridor) 158,285 · c3 (asphalt_column) 97,174 points.
Tree instances: 4 → 4 (none vanish; they shrink).
Full-resolution deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 18,635,552 | -46,209 | -204,494 | -143,383 |
| vehicle | 610,599 | +0 | +158,285 | +97,174 |
| asphalt | 18,489,172 | +45,953 | +45,953 | +45,953 |
| solid_line | 1,021,259 | +256 | +256 | +256 |
| guardrail | 2,139,245 | +0 | +0 | +0 |
| wall | 60,817 | +0 | +0 | +0 |
| ground | 3,711,772 | +0 | +0 | +0 |
| low_vegetation | 747,416 | +0 | +0 | +0 |
| medium_vegetation | 687,386 | +0 | +0 | +0 |
Decimated (1 cm) deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 18,467,888 | -44,500 | -194,744 | -138,055 |
| vehicle | 455,354 | +0 | +150,175 | +93,509 |
| asphalt | 9,896,196 | +44,275 | +44,319 | +44,319 |
| solid_line | 545,294 | +225 | +240 | +225 |
| guardrail | 1,583,885 | +0 | +3 | +0 |
| wall | 60,361 | +0 | +0 | +0 |
| ground | 2,929,504 | +0 | +3 | +2 |
| low_vegetation | 688,186 | +0 | +4 | +0 |
| medium_vegetation | 684,897 | +0 | +0 | +0 |
Segment 093
Reclaimed greenery → vehicle: c1c2 (corridor) 9,411 · c3 (asphalt_column) 8,899 points.
Tree instances: 33 → 33 (none vanish; they shrink).
Full-resolution deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 8,759,943 | -4,049 | -13,460 | -12,948 |
| vehicle | 887,459 | +0 | +9,411 | +8,899 |
| asphalt | 17,144,173 | +4,038 | +4,038 | +4,038 |
| solid_line | 992,778 | +11 | +11 | +11 |
| guardrail | 2,192,200 | +0 | +0 | +0 |
| ground | 4,113,604 | +0 | +0 | +0 |
| low_vegetation | 2,155,829 | +0 | +0 | +0 |
| medium_vegetation | 1,411,714 | +0 | +0 | +0 |
Decimated (1 cm) deltas vs base
| class | base | Δ c1 | Δ c1c2 | Δ c3 |
|---|---|---|---|---|
| tree | 8,604,954 | -3,838 | -12,724 | -12,264 |
| vehicle | 650,008 | +0 | +8,880 | +8,420 |
| asphalt | 9,336,257 | +3,827 | +3,832 | +3,832 |
| solid_line | 521,395 | +11 | +11 | +11 |
| guardrail | 1,665,813 | +0 | +0 | +0 |
| ground | 3,327,591 | +0 | +1 | +1 |
| low_vegetation | 2,071,688 | +0 | +0 | +0 |
| medium_vegetation | 1,394,901 | +0 | +0 | +0 |
085 against the human ground truth (1.87 M annotated points, 100 % matched within 5 cm)
caveat The GT was annotated on the OLD 085 cloud (scan-angle filter at 80°, coarser decimation, 1.87 M points). This pass fused the NEW all-returns / no-angle-filter data. The GT points exist verbatim in the new cloud (100 % matched at 0 mm), so every GT point is scored, but the GT sees only 44.7 % of the new cloud: 56.5 % of the new points lie beyond 80°, and of what we call tree 96 % is beyond 80° (GT covers 4.5 % of our tree points, 86 % of the ≤80° ones). Within ≤80° coverage is 98.6 %, and the reclaim acts there (74 % of our vehicle points are GT-covered). Read the table as a verdict on the road/shoulder zone, not on the added canopy; the tree precision/recall figures describe the old-data slice only.
| variant | tree precision | tree recall | tp | fp | fn |
|---|---|---|---|---|---|
| base (main a584a8a) | 0.543 | 0.755 | 156,216 | 131,436 | 50,753 |
| c1: tree rank −1 only | 0.545 | 0.751 | 155,514 | 130,038 | 51,455 |
| c1c2: + reclaim, corridor rule | 0.551 | 0.743 | 153,858 | 125,558 | 53,111 |
| c3: + reclaim, asphalt_column rule (default) | 0.547 | 0.746 | 154,296 | 127,772 | 52,673 |
Where the points moved (human class → our class; counts for base / c1 / c1c2 / c3):
| human | ours | base / c1 / c1c2 / c3 |
|---|---|---|
| human tree (5) | ours tree (5) | 156,216 / 155,514 / 153,858 / 154,296 |
| human tree (5) | ours vehicle (73) | 62 / 62 / 1,716 / 1,278 |
| human tree (5) | ours asphalt (11) | 80 / 782 / 784 / 784 |
| human car noise (76) | ours tree (5) | 571 / 570 / 0 / 0 |
| human car noise (76) | ours vehicle (73) | 15,597 / 15,597 / 16,167 / 16,167 |
| human guardrail (66) | ours tree (5) | 6,276 / 6,195 / 5,521 / 6,070 |
| human guardrail (66) | ours vehicle (73) | 13,589 / 13,588 / 14,262 / 13,713 |
| human low veg (74) | ours tree (5) | 18,428 / 17,291 / 14,491 / 15,873 |
| human low veg (74) | ours vehicle (73) | 9,380 / 9,380 / 12,173 / 10,793 |
| human ground/neutral (71) | ours tree (5) | 19,897 / 19,724 / 19,345 / 19,615 |
| human ground/neutral (71) | ours vehicle (73) | 5,943 / 5,943 / 6,320 / 6,051 |
Reading: c1c2 (corridor) buys 571 correct car-noise points and loses 1,654 real tree points; c3 (asphalt_column) keeps the 571 and loses 1,278 — the residual sits on the shoulder where asphalt cells exist under real canopy. Tree precision 0.543 → 0.547, recall 0.755 → 0.746 (base → c3). Human "low veg" and "ground" points inside the corridor that the detector had painted tree are wrong either way (tree or vehicle); they are not a loss but not a gain.
Images
Click a thumbnail for the lightbox; ←/→ step within one segment. Top-down rasters are priority-coloured (a tree point wins the pixel over vehicle and asphalt), so on-road changes are best read from the magenta reclaim overlays and the change-map crops.
Segment 085 — ground-truth segment
Segment 093 — road panel under the gantry
Segment 018 — tree-heavy regression segment
Not improved / caveats
- 060: zero change in every variant — its trees never touch the corridor or a hard class. Good as a regression result, useless as a demo.
- 003 (lamp mast / gantry column inside a tree instance) was not run: the handoff expects no fix from seg3d and it needs the detector to emit those rows as sign/gate. Report to the verticalsigns side.
- Instance geometry stays wrong: the 066 top tree instance shrinks from 4,178,556 to 4,174,991 points and stays one instance id; per-instance bbox/footprint remain those of the merged DBSCAN cluster. Splitting was deliberately not implemented (handoff recommendation); the real fix is upstream in the detector's DBSCAN.
- Decimated LAS still shows tree over a reclaimed vehicle voxel when both share a 1 cm voxel (tree tier 4 vs vehicle tier 0) — the decimated deltas above are a little smaller than the full-res ones for that reason.
- Guardrail shoulder overpaint: no growth of guardrail (66) or wall (67) at full resolution in any variant; the +hundreds in the decimated tables are voxel re-picks after tree rows left the voxel. Nothing to damp.
- Vehicle sweep itself is corridor-wide: independent of this pass, unclassified verge points beyond the guardrail were already becoming vehicle in base (magenta shows only the newly reclaimed ones). Worth a look at the corridor polygon width on A1.
Next
- PR from
t3code/6c51b0baunder AI3D-379. No push done. - Decide the default:
asphalt_column(shipped) vscorridor— the 085 GT table above is the number to decide on. - Optional third lever if the 1.3 k canopy points matter: gate the reclaim on height above the vegetation stage's DTM (vehicles < ~4.5 m, crowns above), since the pipeline carries no other height-above-surface value.
- Detector side (wt-vs-382): the mega-instances themselves (
tree_detection.hedge_split_enabled,tree_instance.enabled) and the 003 mast/gantry rows. - Delete the throwaway baseline worktree
/home/ai/wt-seg3d-base-260907and, if the run root is not needed,/home/ai/seg3d_out/260907/fuse_*(≈15 GB).