Guardrail Detection — ML Harness Plan

2026-07-07 · guardraildetection · generated by Claude

Summary

Revive the stale guardrail job (PKG-9: detect guardrail position + type from segment point-cloud numpy files + detected road axis; output XML) in a new repo 3dai.iolabs.imageanalyzer.guardraildetection, modeled on linebitmapsegmentation. The two guardrail notebooks move in (cross_sections.ipynb, dxf_and_xml_manipulation.ipynb). The task-agnostic harness core (strict-YAML config, model/loss registries, metrics, Lightning module, overlay writer, trainer assembly) is extracted into a shared package iolabs-ml-harness imported by both repos — single source of truth. Status at writing: extraction running (Codex), static scaffold running (Sonnet), notebooks moved, lanefinder removal branch committed, baseline tests green (20 passed).

Key decisions

DecisionChoiceWhy
Modality2D models on cross-section profiles (Miro, 2026-07-07): profiles are already prepared and partly annotated. Position along the road comes from the cross-section's place on the axis; type from the profile classification.Existing data beats theory — cross-sections were already produced by cross_section_pcd.py, and annotations exist. The height-channel/top-down option (lanefinder ADR-0001) stays available later via the same harness.
Shared code homeNew repo 3dai.iolabs.mlharness, package iolabs-ml-harnessiolabs-common is torch-free utility code; the harness needs torch/lightning deps. Follows the existing Nexus-published package pattern.
What stays per-repoDataset code, DataConfig/HarnessConfig, task losses/metrics (clDice), configs, scriptsTask-specific; forcing them shared would couple unrelated experiments. clDice self-registers into the shared loss registry.
Notebook handlingcross_sections.ipynb + src/cross_sections/ moved (removed from lanefinder on a branch); dxf_and_xml_manipulation.ipynb copiedThe DXF/XML notebook also serves line-XML export (PKG-8) in lanefinder; deleting it there would break that workflow.
Refactor safetylinebitmapsegmentation changes on branch feat/shared-ml-harness in a worktree; its 20-test synthetic suite is the gateWorking repo with published deliverables — behavior must stay identical (baseline recorded before refactor).

Phases

Phase 1 — Extract shared package M

iolabs-ml-harness: config.build_section (strict unknown-key dataclass builder) + generic ModelConfig/LossConfig/TrainerConfig; models registry with smp fallback; losses registry + WeightedSum + dice_focal/dice/cross_entropy/focal_tversky; metrics.SegmentationStats; SegmentationModule with pluggable extra_metric_factories; parameterized overlay visualizer; build_trainer() (logger/callbacks/checkpoints/precision assembly currently inlined in scripts/train.py). Ported tests, CPU-only.

uv sync --extra dev && uv run pytest -q
Phase 2 — Refactor linebitmapsegmentation to consume it M

Replace extracted modules with imports; keep tiles/rasterize/review, clDice loss+metric (self-registering), DataConfig/PairSpec/HarnessConfig, datamodule local. pyproject: add iolabs-ml-harness to the ml extra as a path source (switch to the private index once published). Gate: all 20 tests pass + --fast-dev-run smoke.

Phase 3 — Guardrail repo harness M

Repo-specific DataConfig/HarnessConfig composed via build_section; src/dataset/ with raster/label pairing + crop sampling for height rasters plus a synthetic-data path so scripts/train.py --fast-dev-run proves the plumbing before any real labels exist; starter config (unet/resnet18, dice_focal, classes background/guardrail); smoke test; README/CLAUDE.md; DVC init with the shared Drive remote (subfolder 00_DVC_Store_guardraildetection).

uv sync --extra ml
uv run python scripts/train.py --config configs/unet_baseline.yaml --fast-dev-run
Phase 4 — Review, commit, ship S

Fable reviews every delegated diff. Commit all repos; push the two branch repos (linebitmapsegmentation, lanefinder); new repos stay local until their Bitbucket remotes exist (push attempted; reported if the remote is missing).

What else to prepare for this repo

ItemWhat / whyEffort
Bitbucket remotesCreate 3dai.iolabs.imageanalyzer.guardraildetection and 3dai.iolabs.mlharness under the org; push mains. Needed before anyone else can pull.S
Ingest the prepared cross-section dataLocate the prepared cross-section profiles + existing annotations, pull them into data/00_external/ under DVC, and read the annotation format off the files — it defines src/dataset/'s real-data path (replacing the current placeholder stub).M
Guardrail type taxonomyDefined in the 250415_Guard rail profiles MS export folder on N: (Google Drive) — extract the class list from there; it sets CLASS_NAMES/num_classes, metrics, and configs. (Dataset-sorter will not be extended for guardrail labeling — decided.)S
XML export moduleTarget schema already exercised in dxf_and_xml_manipulation.ipynb ("Guard Rails" FeatureClass). Turn it into src/export/ + tests when detections exist.M
Publish iolabs-ml-harness to NexusWrap-up flow (version bump, build, publish, Jira-tagged commit); then flip both consumers' uv.sources from path to index. Until then path deps only work on machines with both checkouts side by side.S
DVC store + first data dropCreate the Drive subfolder, dvc add the first raster/label drop, push. Copy the service-account JSON from a sibling repo (gitignored).S
Inference package (later)When a model is worth shipping, mirror the linebitmap pattern: a separate …guardrailinference package as the deployment SSOT (FlipTTA/predict/vectorize equivalents), consumed back by the training repo.M
GPU run bookConfigs are 16 GB-GPU-sized like the sibling repo's; document tensorboard/runs conventions and add the first real experiment config once data lands.S

Risks

RiskSeverityMitigation
Refactor silently changes linebitmapsegmentation training behaviorhighBaseline 20-test run recorded; extraction is verbatim-first (genericization only where specced); fast-dev-run smoke; Fable reviews the diff line by line.
Harness ships but the data side stalls (no rasters/labels)medSynthetic path keeps the harness testable; "What else to prepare" flags rasterization + labeling as the true critical path.
Segmentation-first turns out wrong for type classificationmedRegistry accepts custom heads; per-polyline type classifier can sit downstream of segmentation, as solid/dashed classification does today.
Path deps break on machines without sibling checkoutslowPublish to Nexus soon after review; pyproject comments already point there.

Questions — answered by Miro, 2026-07-07

QuestionAnswer
Which raster representation carries guardrails best — top-down height-above-road tiles, cross-section profiles, or both? Cross-section profiles. They have already been prepared, and some data is already annotated — v1 works with cross-section profiles. (Top-down height tiles remain a possible later addition per the lanefinder ADR.)
Guardrail type list for v1 — binary presence first, types later? Types are defined in N:\02_Modelling\05_Rink\02_AI 3D modeling\03_Working folder\250415_Guard rail profiles MS export (Google Drive). Read off the DXF there (one layer per system, each a 14-point reference cross-section profile polyline): EDSP · ESP 2.0 · ESP 4.0 · Super-Rail Eco · Eco-Safe 2.0 — 5 types + background ⇒ the working CLASS_NAMES. (EDSP matches the Guard Rails_EDSP DXF layer already seen in the XML-export notebook.)
Extend the dataset-sorter tool for guardrail labeling? No.
Repo/package names OK? Approved: 3dai.iolabs.imageanalyzer.guardraildetection, iolabs-ml-harness.

Consequences folded into the plan: the real-data path in src/dataset/ targets annotated cross-section profiles (format to be read off the existing export/annotations); the synthetic corpus stays only as the plumbing smoke; "label acquisition" drops out of the critical path since annotations already exist for part of the data.