Guardrail Detection — ML Harness Plan
Summary
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
| Decision | Choice | Why |
|---|---|---|
| Modality | 2D 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 home | New repo 3dai.iolabs.mlharness, package iolabs-ml-harness | iolabs-common is torch-free utility code; the harness needs torch/lightning deps. Follows the existing Nexus-published package pattern. |
| What stays per-repo | Dataset code, DataConfig/HarnessConfig, task losses/metrics (clDice), configs, scripts | Task-specific; forcing them shared would couple unrelated experiments. clDice self-registers into the shared loss registry. |
| Notebook handling | cross_sections.ipynb + src/cross_sections/ moved (removed from lanefinder on a branch); dxf_and_xml_manipulation.ipynb copied | The DXF/XML notebook also serves line-XML export (PKG-8) in lanefinder; deleting it there would break that workflow. |
| Refactor safety | linebitmapsegmentation changes on branch feat/shared-ml-harness in a worktree; its 20-test synthetic suite is the gate | Working 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
| Item | What / why | Effort |
|---|---|---|
| Bitbucket remotes | Create 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 data | Locate 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 taxonomy | Defined 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 module | Target 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 Nexus | Wrap-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 drop | Create 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 book | Configs 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
| Risk | Severity | Mitigation |
|---|---|---|
| Refactor silently changes linebitmapsegmentation training behavior | high | Baseline 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) | med | Synthetic path keeps the harness testable; "What else to prepare" flags rasterization + labeling as the true critical path. |
| Segmentation-first turns out wrong for type classification | med | Registry 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 checkouts | low | Publish to Nexus soon after review; pyproject comments already point there. |
Questions — answered by Miro, 2026-07-07
| Question | Answer |
|---|---|
| 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.