Handoff — seg3d LAS for all A1 + A4_5 segments
Summary
A 5-segment pilot is done and delivered. Your job is the full sweep: 285 segments —
A1 branch_000 (131), A1 branch_001 (11), A4_5 (143) — each fused into a
ReCap-ready segment_NNN_seg3d.las (LAS 1.4 PDRF 7, EPSG:25832, per-instance
point_source_id, las_split/).
Do not start by launching 285 runs. Two inputs do not exist at scale
(A1 surface meshes, A1 line-inference masks) and one dataset cannot get asphalt edges at all
(branch_001). Resolve Blockers first, get a written decision
from Miro on scope, then batch.
Current state
Done — pilot delivered 2026-08-07
Five segments fused, validated and copied to the N: share:
N:\02_Modelling\05_Rink\02_AI 3D modeling\03_Working folder\
260803_recap_annotation_Abschnitt_1\pre_annotation\260807_latest_upstream\
branch_000\segment_065 branch_000\segment_070 branch_001\segment_005
260803_recap_annotation_Abschnitt_4_5\pre_annotation\260807_latest_upstream\
segment_040 segment_100
Battlebox-side copies: D:\temp\recap_260807\{A1_branch_000,A1_branch_001,A45}
(WSL: /mnt/d/temp/recap_260807/). The _DISCARD_A1_long folder there is a
wrong-dataset run — ignore and delete when convenient. A README.md with provenance and
caveats sits in each delivered folder; copy that pattern for the full run.
Datasets — use these exact roots
| Dataset | Root | Segments | Tiles |
|---|---|---|---|
| A1 branch_000 | /mnt/d/a123-data/Abschnitt_1/branch_000/lane_points | 131 (segment_000…130) | 131 |
| A1 branch_001 | /mnt/d/a123-data/Abschnitt_1/branch_001/lane_points | 11 (segment_000…010) | 11 |
| A4_5 | /mnt/d/Data/02_AI 3D modeling/00_data/260416_Abschnitt_4_5/lane_points | 143 (segment_000…142) | 143 |
trap The A1 dataset is the branch-split one under
/mnt/d/a123-data/, geoshift 694541.4428 / 5386372.3686 / 467.4083.
00_data/251017_Color_Abschnitt_1_long and _short are a different
segmentation of the same survey (geoshift 695777.2088…, 2-digit segment_70
dirs). Miro: "Do not use short and long. Use branches 0 and 1." A whole pipeline pass was
wasted on _long before this surfaced.
Package versions (all pulled to latest on 2026-08-07)
| Stage | Checkout | Rev |
|---|---|---|
| asphalt edges | /home/ai/dev/ae-main-260807 (detached worktree of origin/main) | 8b8a3f6 |
| guardrails / walls | /home/ai/dev/3dai.iolabs.pointcloud.guardrails | main 58d66f1 |
| signs / gates / delineators / trees | /home/ai/dev/3dai.iolabs.pointcloud.verticalsigns | master f3e7151 |
| ground (tablecloth) | /home/ai/dev/3dai.iolabs.pointcloud.tablecloth | main 3a8cedb |
| lane markings | /home/ai/dev/3dai.iolabs.pointcloud.maskclustering | v0.2.4 / 2526ec6 |
| step-5 inference | /home/ai/dev/3dai.lanefinder, bundle models/ft_r34_260624 | — |
| fusion | /home/ai/dev/3dai.iolabs.pointcloud.3dsegmentation | 5dab3c8 |
note The user's own asphaltedge checkout sits on
feature/zrange-evidence-channel, which is behind origin/main. Use the
ae-main-260807 worktree; do not switch branches in their checkout. Re-pull everything before
you start — this handoff may be days old by the time you run.
Environment
# every WSL heredoc that touches uv:
export PATH="$HOME/.local/bin:$PATH"
set -a; . ~/.env; set +a # UV_INDEX_NEXUS_* — without it uv sync fails
# with "Missing credentials for nexus.iolabs.ch"
24 cores, 94 GB RAM, RTX 5070 Ti (needed only for step-5 inference).
Disk: /home/ai 807 GB free · /mnt/d 263 GB free (93 % full) —
write intermediates and outputs to /home/ai, not D:.
Blockers first — resolve before batching
1. A1 has no run4 road surface, and step-9 meshes exist for 6 segments only blocker
Fusion takes pavement from the asphalt-edge corridor plus a road surface. A4_5 has
*_run4_road_surface.npz everywhere; A1 has none, and the substitute
(--surface-mesh-dirs, seg3d commit 5dab3c8) only has data for the pilot segments:
/mnt/d/a123-data/Abschnitt_1/surface_mesh_s9/branch_000 -> segment_060, 065, 070 (3 of 131)
/mnt/d/a123-data/Abschnitt_1/surface_mesh_s9/branch_001 -> segment_004, 005, 006 (3 of 11)
Producer is the filteringsurface repo, currently a redesign worktree at
/home/ai/dev/wt-surface-mesh (branch worktree-surface-mesh-redesign, AI3D-347,
battlebox-local dep pins re-applied by /home/ai/dev/sync-surface-mesh.sh).
Work out its CLI, run one segment, and diff the output against
surface_mesh_s9/branch_000/segment_070/segment_070_surface_mesh_{left,right}.ply before
committing to 136 more. Measured effect of the mesh on the pilot: 41 % of points within 0.15 m on
segment_065, 78 % on segment_070 — without it, A1 asphalt coverage drops
materially.
2. A1 branch_001 has no road axis — asphalt edges cannot run there blocker
find /mnt/d/a123-data -name "cross_sections_run7_lanes_*.npz" returns exactly one file,
branch_000's cross_sections_run7_lanes_20260708-000411.npz (13 450 m, 26 902 vertices).
Projecting branch_001 segment_005 onto it gives median lateral offset 1 166 m — it is a
genuinely different centreline, not a frame problem. Existing older-pipeline edges cover 3 of 11
segments (edges_s8/branch_001). Either the axis gets regenerated upstream (run7 /
lanefinder), or 8 of branch_001's 11 segments ship without an asphalt corridor. Ask Miro.
3. A1 line-marking masks must be inferred for ~136 segments work
clusters_mask/ exists in the client tree for only 3 segments per A1 branch (package
0.2.2, written by an Azure ML run whose step-5 masks are not on this box). A4_5 has all 143
topdown_predictions/segment_NNN_intensity_mask.png already. Step-5 inference is cheap
(~3 s/tile on cuda:0) and reproduces the Azure masks to sub-percent, so just run it for every A1
segment into a staging tree.
4. Never write into client data rule
/mnt/d/Data and /mnt/d/a123-data are read-only for us. Mask clustering
writes clusters_mask/ inside the segment dir, so every dataset must be staged as
real dirs holding symlinks to the client npz files. Pilot staging tree to copy:
/home/ai/seg3d_stage_260807/{A1B0,A1B1,A45}/lane_points/ — builder script at
/tmp/build_shim2.py on battlebox (may be gone; the pattern is: real
segment_NNN/ dir, symlink each *_run3_points.npz, symlink root-level
run3_geoshift.json / run3_planes.npz / cross-sections / run7 XML, real
topdown_tiles/ and topdown_predictions/).
Per-stage recipe (verified on the pilot)
Asphalt edges — validate the axis or you get silent garbage
cd /home/ai/dev/ae-main-260807
uv run python scripts/run_segments.py \
--data-dir <STAGE>/lane_points --segments 0-130 \
--out /home/ai/seg3d_out/edges_<DS> -v
gotcha load_axis() picks
sorted(glob("cross_sections_run7_lanes_*.npz"))[-1] — newest filename, never
validated against the segment. Wrong axis ⇒ all stations clamp to 0 ⇒ ~8 KB npz with zero finite
clean offsets ⇒ exit 0, no warning. Stage exactly one cross-sections file per run
and pre-check by projecting points onto the axis (offsets in metres, station range spans the
segment). This burned an 18-minute run in the pilot.
Other notes: --max-points defaults to 7 M and is silently hit on these clouds
(raw 24 M); run4 is optional; no --config flag exists. Timing varies hugely with I/O:
A1 ≈ 30 s/segment, A4_5 ≈ 320 s/segment (load dominates). Known defect: z == 0.0
sentinel values in *_polyline_points (up to 31 % of vertices on one pilot segment) —
treat z == 0.0 as no-data downstream.
Guardrails / walls
cd /home/ai/dev/3dai.iolabs.pointcloud.guardrails
uv run python -m guardrails.detect \
--data-dir <STAGE>/lane_points \
--lane-xml <STAGE>/lane_points/<run7 lanes xml> \
--segments 000,001,... --out /home/ai/seg3d_out/guardrails_<DS> \
--dump-point-masks --log-level INFO
Lane XMLs: A1 b000 run7_lanes_branch_000_20260722-092426.xml, b001
…_branch_001_20260722-092434.xml, A4_5 run7_lanes_20260723-163130.xml.
--lane-xml is effectively mandatory on current main. If a segment yields zero masked
points and the log says "no Central Axis in segment", re-run it with
--set lane_xml_zones_enabled=false --set precision_gate_enabled=false and keep that.
known bug With zone gating on, points whose occupancy cell was
created by zone enrichment are dropped from point_masks.npz entirely — pilot measured
5 %, 17 % and 46 % of the JSON-reported point counts surviving. The per-point guardrail class is
still sound; per-instance splitting under-segments. Worth fixing upstream before a 285-segment run
if instance-level review matters.
run_summary.json is rewritten per invocation — copy it aside per batch.
Signs / gates / delineators / trees
cd /home/ai/dev/3dai.iolabs.pointcloud.verticalsigns # MUST cd: models/ is CWD-relative
uv run verticalsigns-detect --data-dir <STAGE>/lane_points \
--segments 000,001,... --out /home/ai/seg3d_out/signs_<DS> \
--config tree_run.config.json --dump-point-masks --log-level INFO
Confirm both models load in the log (verifier-2026-07-11.joblib and
vegetation-2026-07-11.joblib) — if a model fails to resolve the stage silently no-ops.
~45 s/segment. Yield is genuinely low on this corpus (~1 device/segment); the pilot's three A1
segments produced trees only.
Ground (tablecloth)
cd /home/ai/dev/3dai.iolabs.pointcloud.tablecloth
uv run python -m iolabs_point_cloud_tablecloth.filter \
--data-dir <STAGE>/lane_points --segments 000,001,... \
--out /home/ai/seg3d_out/ground_<DS>
For A1 the fresh masks came out bit-identical to the Azure set at
/mnt/d/a123-data/masks_flat/Abschnitt_1_branch_00N/ (the Azure run already used the
slope-aware defaults). At 285 segments, consider reusing masks_flat for A1 instead of
recomputing — but verify a sample first, and note A4_5 has no equivalent.
disk The filter also writes full cleaned NPZ copies that seg3d
never reads, with no flag to suppress them: ~275 MB/segment. 285 segments ≈ 78 GB. Budget it, or
delete *_run3_points.npz from the output dirs after each batch.
Lane markings — step-5 inference then step-6c clustering
cd /home/ai/dev/3dai.lanefinder # A1 only; A4_5 masks already exist
uv run python scripts/pipeline/run_5_line_bitmap_inference.py \
--data-dir <STAGE> --bundle /home/ai/dev/3dai.lanefinder/models/ft_r34_260624 \
--tile-size 512 --overlap 128 --device cuda:0
cd /home/ai/dev/3dai.iolabs.pointcloud.maskclustering
uv run iolabs-mask-cluster segment \
--segment-dir <STAGE>/lane_points/segment_NNN \
--mask <STAGE>/lane_points/topdown_predictions/segment_NNN_intensity_mask.png \
--raster-metadata <STAGE>/lane_points/topdown_tiles/segment_NNN.json \
--output-dir <STAGE>/lane_points/segment_NNN/clusters_mask --overwrite
The inference script's default subdirs (lane_points/topdown_tiles →
lane_points/topdown_predictions) match the staging layout, so --data-dir
pointed at the stage root is all it needs — no job config. Use the batch subcommand
with an explicit jobs.json for volume; never use
--from-segment/--to-segment, which compare segment names lexicographically.
~40–80 s/segment. Tile JSONs need geoshift.z; A1 branch tiles have it, the
_long tiles did not.
Fusion → LAS
cd /home/ai/dev/3dai.iolabs.pointcloud.3dsegmentation
uv run seg3d-fuse \
--dataset-root <STAGE> --segments 000-130 \
--edges-dirs /home/ai/seg3d_out/edges_<DS> \
--surface-mesh-dirs /mnt/d/a123-data/Abschnitt_1/surface_mesh_s9/branch_000 \ # A1 only
--guardrail-masks /home/ai/seg3d_out/guardrails_<DS> \
--signs-masks /home/ai/seg3d_out/signs_<DS> \
--ground-masks /home/ai/seg3d_out/ground_<DS> \
--lines-source clusters --las-split instance --render \
--out /home/ai/seg3d_out/las_<DS>
~1–5 min/segment. Missing inputs skip their stage with a WARNING and still produce output —
so grep every log for WARN and check run_summary.json
skips per segment rather than trusting exit codes. Validate a sample with laspy:
expect LAS 1.4, PDRF 7, VLR 2112 present, and classification codes from
{1, 2, 5, 64, 65, 66, 67, 68, 69, 70, 71}.
Next steps, in order
- Re-pull every repo above; re-run
uv sync. Confirm revs still match or note the drift. - Get Miro's decision on the two blockers: (a) generate step-9 surface meshes for all A1 segments, or ship A1 with edge-corridor-only pavement? (b) branch_001 without asphalt edges (8 of 11 segments), or wait for a run7 axis?
- Build full staging trees for the three datasets (all segments). Verify counts: 131 / 11 / 143.
- Run step-5 inference for all A1 tiles (fast, GPU) → step-6c clustering for all 285.
- Run ground, signs, guardrails, edges. These are independent — parallelise across datasets, but cap concurrency: edges peaks ~6 GB RSS, fusion similar. 24 cores / 94 GB says 4–6 concurrent processes, not 24.
- Checkpoint per batch of ~10 segments. Write a per-batch manifest of what
completed, so a crash does not restart the sweep. Nothing here has a native resume flag except
mask clustering's
--skip-existing. - Fuse, validate a random sample of ~10 LAS with laspy, then assemble the delivery with a README following the pilot's format.
- Ask before copying to N: the full sweep is roughly 20–35 GB of LAS +
las_split, and N: is a Google Drive mount — that is a large sync. Stage on/home/aiand confirm the destination with Miro.
Risks & open questions
- Runtime is the dominant risk. Rough pilot-derived estimate for 285 segments, single-stream: edges ~14 h (A4_5 dominates at ~5 min/segment), guardrails ~10 h, ground ~2 h, signs ~3.5 h, clustering ~5 h, fusion ~8 h. Call it 1.5–2 days serial, well under a day with sensible parallelism. Confirm with Miro that this is wanted before spending it.
- Battlebox jobs die if no ssh session is attached —
systemd-run/detached jobs get killed. Keep a session attached for the duration, or background it on the caller's side. - D: is 93 % full (263 GB). Any large intermediate on
/mnt/drisks filling the drive that also hosts the WSL VHDX. - Zero walls detected anywhere in the pilot's five segments, and zero sign/gate/delineator in the three A1 ones. Consistent with the detectors' own baselines, but if the full sweep also returns ~0 walls across 285 segments, that is a detector question worth raising.
- Is per-instance
las_splitwanted at this scale? It multiplies file count by ~20–40× per segment (285 segments ⇒ ~7 000 files). Miro asked for it on the pilot; confirm for the sweep. - N: is a per-session
substofG:\.shortcut-targets-by-id\1MDlZT8saBabIkbFmPNrpAHSNuIh0cO98\ioProperty_projects, created byGDriveOnBoot.batat logon. If nobody is logged into the battlebox desktop it does not exist in an ssh session (Get-PSDriveshows only C and D). Checkquery userfirst. - Delivery folders already contain a 2026-08-03 delivery, including
branch_000\segment_070. The pilot went into a dated260807_latest_upstream\subfolder rather than overwriting. Keep that discipline.
Details
Suggested skills & working style
battlebox-ssh— connection contract. Always the heredoc form (ssh battlebox "bash -s" <<'EOF' … EOF); inlinebash -c "…"gets mangled by Windows cmd.scplands inC:\Users\ai\, not WSL.- Fan the five detector stages out as parallel subagents, one per stage, as the pilot did —
each stage is independent given a staged tree. The pilot's workflow scripts are on the
orchestrating machine under
~/.claude/projects/-home-miro-dev-3dai-iolabs-pointcloud-3dsegmentation/<session>/workflows/scripts/(seg3d-detect-260807-*.js,seg3d-a1branch-260807-*.js) and are worth reading as templates. - Miro wants extremely concise reporting. Lead with what shipped and what broke.
Pilot results, for comparison
| Cloud | Points | Classes |
|---|---|---|
| A1 b000 segment_065 | 2.12 M | ground, asphalt, solid, dashed, guardrail, tree |
| A1 b000 segment_070 | 1.54 M | same |
| A1 b001 segment_005 | 2.38 M | same |
| A4_5 segment_040 | 1.52 M | same + delineator |
| A4_5 segment_100 | 0.57 M | ground, asphalt, solid, dashed, guardrail |
LAS 55–86 MB each; las_split/ 15–42 files each. Use these as the sanity yardstick
for the sweep: a segment producing a 5 MB LAS or 2 classes deserves inspection.
Reference docs
/home/ai/dev/3dai.iolabs.pointcloud.3dsegmentation/README.md— fusion CLI, class table, config keys.…/docs/recap_annotation.md— LAS format decision, ReCap workflow, N: folder convention, round-trip import.…/docs/point_masks_spec.md— thepoint_masks.npzcontract every detector writes.- Pilot delivery README:
/mnt/d/temp/recap_260807/README.md.