Handoff: guardrail rail-vs-support split (options 1+2)
Summary
Goal: in the LaneFinder guardrails repo, implement the two low-cost tiers of the rail-vs-support decomposition decided on 2026-08-14: (1) a per-run post-cadence attribute (spacing, count, confidence, gap list) and (2) a per-point sub-mask guardrail_rail / guardrail_support / ambiguous in point_masks.npz. Do NOT add a second exported feature class — exports (XML/OKSTRA-style) stay single-class guardrail; the split is internal enrichment only.
Repo: /mnt/data/dev/3dai.iolabs.pointcloud.guardrails (work happened in worktree /home/miro/.t3/worktrees/3dai.iolabs.pointcloud.guardrails/t3code-66d41c12), branch t3code/separate-v-profile-support-classes, tip 0c471fb (v0.3.1). Jira context: AI3D-351 line of work.
Current state (nothing implemented yet)
- Research is complete and the verdict is fixed: internal sub-labeling + derived attributes, no new exported class. Full report lives in the Zettelkasten project note
Project notes/Point cloud analysis on linear structures/2026-08-14 Guardrail rail vs support-structure split research.md(vault/home/miro/zettelkasten). - Two untracked research artifacts sit in the worktree — keep them (commit with the feature or leave untracked, Miro decides):
docs/guardrail_rail_vs_support_density_research.md(density/dimension source table) andresearch/guardrail_benchmark_taxonomies.md. - No code changes on the branch for this feature yet.
Key facts the design relies on
- Swiss/German hardware: A-profile rail face 306 mm (B-profile 310±3 mm), top edge ≈0.75 m; Sigma-100 post 100×55 mm; post spacing 1.33 / 2.00 m (ASTRA 11005), 4.00 m (DDSP systems).
- MLS at highway speed: scan-line spacing 3.5–11 cm ⇒ a 100 mm post gets ~1–3 scan lines per pass (~30–120 pts). Individual posts can be missed by scan phase alone; the periodic train over a run is robust. Never treat a single missing post as a real-world defect without an
unobservedstate. - Only published precedent (Chen et al., Sensors 26(1):40, 2026, Dutch highway) got 59–63% IoU on brackets vs 86–99.5% on rail — support class is tiny (~0.1% of points). Report support metrics separately.
Design decisions (already made — do not relitigate)
- Blocker Current candidate gates discard post evidence:
max_cell_height_spread_m = 0.50(guardrails/config.py:99) rejects 0.10 m cells spanning a post (0.05→0.75 m ⇒ spread ≈0.7), andrail_band_min_m=0.35+min_rail_fraction=0.40(config.py:84-91, gate logicguardrails/geometry.py:172-221select_candidate_cells) suppress the sub-0.35 m column. Build a second, relaxed evidence pass scoped to accepted runs' corridors; do NOT widen the existing rail gates — they were tuned to reject vegetation/poles/trees. - Option 1 — post cadence: within each accepted run's corridor, accumulate an along-station histogram (0.10 m bins) of returns with height-above-ground
z ∈ [0.10, 0.35](below rail band). 1-D autocorrelation (or FFT) → dominant period; snap to catalog {1.33, 2.00, 4.00} m (soft prior, keep raw value too). Emit per-instance attributes:post_spacing_m,post_spacing_raw_m,post_count,post_confidence,post_gap_spans(station intervals where the periodic train has holes — cross-check against existinggap_spansocclusion bridging,config.py:130-132, before flagging). - Option 2 — per-point sub-mask: label points of each guardrail instance:
rail=z ∈ [0.50, 0.80]and along-run continuity;support=z < 0.50, vertical extent ≥ 0.25 m, XY footprint ≤ 0.25 m, recurring at the detected cadence (needs option 1's estimate); everything elseambiguous. Concrete plinth ⇒ambiguousfor now (explicitly decided; revisit only with a real consumer). Optional discriminator: PCA eigenfeatures already exist unused —local_eigen_features/eigen_features_from_points(guardrails/features.py:85-119, gated off byuse_eigen_cell_filter,config.py:112). - Plumbing: instance enrichment happens in
_enrich_instances(guardrails/instances.py:29-40). Point-mask sidecar:detect.py:576buildsinstance_typesfrominst.get("type")and feeds the sharedwrite_point_maskswriter (outputs.py) — add a sub-mask channel there rather than inventing a new sidecar. - Config parity: every new threshold added to
config.pymust be mirrored verbatim inguardrails/guardrails.default.json— parity is unit-tested and will fail otherwise. - No new exported class: do not touch
classify.GuardrailType,TYPE_COLORS/LEGEND_ORDER(colors.py:20-36), orexport.pytype blocks, except to append the new instance attributes to whatever attribute dict export already emits.
Next steps (ordered)
- Read
README.md,guardrails/config.py,guardrails/geometry.py(select_candidate_cells),guardrails/instances.py,guardrails/detect.py(~:517 and :576),guardrails/outputs.pyto confirm the line references above still hold on the current tip. - New module (e.g.
guardrails/posts.py): corridor-scoped low-band evidence pass + along-station histogram + autocorrelation cadence estimator. Stream via the existing chunked reader (iolabs.common.segment_points_io.iter_points_chunks) — respect the memory budget machinery (memory_budget_gb,record_chunk_points,config.py:181-196). - New config block (+ mirror in
guardrails.default.json): low band [0.10, 0.35], bin 0.10 m, catalog spacings [1.33, 2.00, 4.00], snap tolerance, min confidence, feature flagenable_post_cadence(default on) andenable_component_masks. - Wire cadence attributes into
_enrich_instances; pass through to JSON/XML instance attributes. - Implement the per-point sub-labeler using the cadence estimate; write the extra channel into
point_masks.npz(keep backward-compatible: additive keys only, existing consumers must not break — check the lane-zone replay pathlane_xml.pyanddev/probe.pywhich touch masks/records). - Unit tests: synthetic guardrail generator (rail band at 0.5–0.75 m + posts at exact cadence + noise) → assert recovered spacing, count, gap detection, and sub-mask precision on the synthetic truth. Test cadence robustness with 30% of posts randomly deleted (scan-phase simulation) — spacing must still snap correctly, missing posts must land in
post_gap_spans. - Run the module on a curated real segment (see
azure-cliskill for cached ACC LAS data) and eyeball a rendered mask overlay before calling it done. - Version bump + wrap-up per repo convention (
/wrap-upskill: Nexus publish, AI3D-351 Jira-tagged commit) — only when Miro asks.
Verification contract
- Existing test suite green (esp. config/schema parity tests).
- New synthetic-cadence tests green, incl. the 30%-deleted-posts case.
point_masks.npzschema change is additive — old readers load unchanged.- Detection outputs (instances, types, exports) byte-identical when both feature flags are off.
- Nothing run yet — no checks have been executed for this feature; all of the above is the target, not the status.
Risks and open questions
- Open Exact height-above-ground bands (0.35 / 0.50 cut lines) are literature-derived, not tuned on our clouds — expect one tuning loop on a real segment; keep them in config.
- Open Double-rail median systems (both faces detected, see
dedupe.pyparallel-face merge) — decide whether cadence runs per face or per merged barrier; naive per-face histograms may alias. - Risk Missing-post QC false alarms from scan phase/occlusion —
post_gap_spansmust stay "unobserved-or-missing", never "defect", in any user-facing output. - Risk Low band [0.10, 0.35] catches grass/curbs/debris; corridor scoping to accepted runs is the main defense — never run the low-band pass unscoped.
- Suggested skills for the implementing session:
tdd(synthetic-first fits it well),azure-cli(real-segment data),wrap-up(release),code-reviewbefore merge.