Object instance IDs in LAS output (ReCap-safe)

2026-08-19 · seg3d / 3dai.iolabs.pointcloud.3dsegmentation · plan

Summary

Yes, it's possible — and half of it already exists. The seg3d fusion step already computes a per-point object instance ID (each lane-marking stripe, guardrail, sign, tree, … numbered from 1) and writes it into the standard LAS point_source_id field (writer.py:207). The catch: ReCap does not display point_source_id at all — it only exposes classification, RGB and intensity.

The plan: add a proper named Extra Bytes dimension object_instance_id (uint32) to the LAS 1.4 / PDRF 7 output. This is the ASPRS-standard extension mechanism — CloudCompare, PDAL and LAStools all read it, and evidence says ReCap imports such files fine (it silently ignores the extra dimension). point_source_id stays as a mirror for tools that read only standard fields. For actually seeing objects inside ReCap, the existing --las-split instance (one LAS per object) remains the workable path, optionally joined by a new "instance colors" RGB mode. A quick ReCap import smoke test on battlebox gates the rollout.

Current state (what exploration found)

Key decisions

DecisionChoiceWhy
Where the ID lives Named Extra Bytes dimension object_instance_id, uint32 standard ASPRS-standard extensibility; readable by CloudCompare / PDAL / LAStools / TerraScan workflows; lifts the 65535 cap; self-describing (name travels in the VLR).
point_source_id Keep writing the same ID there (uint16 mirror) no regression Zero-cost backward compatibility with existing consumers and tests; Extra Bytes becomes the canonical channel, mirror saturates at 65535.
ReCap visibility RGB is never touched (user decision 2026-08-19). Optional las_intensity_mode: "instance" encodes the object ID into the intensity channel → ReCap's built-in Intensity color mode separates objects. --las-split instance additionally gives per-object coloring via ReCap's Scan Location mode. ReCap renders RGB / Intensity / Classification / Scan Location; it never shows a custom scalar. Intensity is the only expendable channel that keeps photographic RGB intact.
Compatibility gate Smoke-test one Extra-Bytes LAS in ReCap (battlebox) before enabling by default Extra Bytes tolerance in ReCap 2024–2026 is anecdotal, not documented by Autodesk. If it fails, ship behind a config flag defaulting off.
Config New key las_instance_dim (default true pending gate) Consistent with existing las_* knobs in config.py:68-86; escape hatch if a downstream tool chokes.

Phases

Phase 0 — ReCap compatibility smoke test gate
  • Hand-write a small LAS 1.4 / PDRF 7 file with laspy: add_extra_dim(ExtraBytesParams("object_instance_id", "uint32")), a few thousand points, realistic classes + WKT VLR (mirror the production header from writer.py:180-195).
  • Import into ReCap Pro on battlebox; verify: import succeeds, classification/RGB/intensity intact, no warnings.
  • Also open in CloudCompare to confirm the dimension appears as a scalar field.
  • Outcome decides the default of las_instance_dim (on if clean, off + documented if not).
Phase 1 — Writer: Extra Bytes dimension core, small
  • writer.py: after header construction (line 180), add the object_instance_id uint32 dimension when las_instance_dim is enabled; fill from the decimated instance array (same source as line 207); keep the point_source_id mirror, clamping mirror writes at 65535.
  • Apply identically in write_las and write_las_split so split files carry the ID too.
  • Raise MAX_INSTANCE_ID handling in fuse.py:38,184-188: canonical uint32 channel no longer overflows in practice; only the mirror saturates (warning stays, ID no longer zeroed).
  • config.py + seg3d.default.json: add las_instance_dim with validation.
Phase 2 — Optional: instance-intensity mode small, revised
  • Revised per user decision: RGB stays photographic, always. New config key las_intensity_mode: "sensor" (default, pass-through as today) | "instance" — intensity replaced by a scrambled instance value (ID 0 → 0; ID n → 1 + (n·40503 mod 65534) so neighboring IDs get well-separated values).
  • In ReCap, select the Intensity color mode → each object renders in a distinct ramp color while RGB mode still shows true camera colors.
  • Alternative already available: --las-split instance + ReCap's Scan Location color mode (one scan per object → one color per object), no channel sacrificed.
Phase 3 — Round-trip + docs small
  • recap_import.py / recap_cli.py: when a reviewer-corrected class comes back from ReCap, re-run the class↔instance consistency rule (same rule as fuse.py:122-126) so a reclassified point drops its stale instance ID instead of silently disagreeing.
  • Docs: update docs/recap_annotation.md (field table + "ReCap ignores the dimension" note), docs/fusion_spec.md, README.md — including that ReCap exports (E57/PTS) drop the ID, so the original LAS stays master.
  • Mention the existing stats["instances"] registry as the ID→object manifest for consumers.
Phase 4 — Tests + release small
  • Extend tests/test_fusion.py:1012-1039: dimension present, values match point_source_id mirror below 65535, mirror clamps above, dimension absent when las_instance_dim=false, split files carry it.
  • tests/test_recap_roundtrip.py: consistency-check case (corrected class → ID dropped).
  • Version bump + wrap-up per repo convention (v0.3.0 — new output field is a minor).

Risks

Open questions

  1. Default for the new las_instance_dim flag (assuming Phase 0 passes)?

    default On — every delivery carries IDs; flag stays as escape hatch.

    alt Off until a client actually asks — zero risk to current deliveries.

  2. Instance visualization channel (Phase 2)? answered 2026-08-19

    decided RGB stays photographic; optional las_intensity_mode: "instance" repurposes intensity instead (ReCap Intensity color mode shows objects). Split-by-instance + Scan Location mode remains the zero-sacrifice alternative.

  3. ID numbering scheme: keep global 1..N per segment, or namespace per class (e.g. tree #1, tree #2 …)?

    default Keep global per-segment IDs (current behavior) — class is already in classification; the stats["instances"] registry gives the per-class view for free.

    alt Encode class in the ID (e.g. class*100000+n) — self-describing without the manifest, but breaks the mirror's 16-bit range immediately.

  4. Who runs the Phase 0 ReCap test?

    default I generate the test LAS and drive ReCap on battlebox via Codex computer use; you just get the verdict.

    alt I generate the file, you click through ReCap yourself.