Object instance IDs in LAS output (ReCap-safe)
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)
- Writer: laspy 2.7.0, LAS 1.4, point format 7, single write path in
src/iolabs_point_cloud_segmentation_3d/writer.py(header at line 180, fields at 197–226). No Extra Bytes anywhere yet; only VLR is the WKT CRS. - Instance IDs: built in
fuse.py:66-189(build_instances()) from upstream detector masks and the step-6 cluster manifest — no clustering in this repo. IDs are 1..N per segment, 0 = no instance, capped at 65535 becausepoint_source_idis uint16 (fuse.py:38, overflow → warning + ID 0). - Registry:
stats["instances"]already records per-instance metadata (class, type, ref, point counts) — a ready-made sidecar manifest. - ReCap: per
docs/recap_annotation.md:97-100ReCap surfaces neitherpoint_source_id,user_data,gps_timenor Extra Bytes. Current workaround is--las-split instance(one LAS per object, warning above 200 files). - Research (Autodesk docs + forums): ReCap 2025.1+ reads LAS 1.4 PDRF 6–10 incl. extended classification; a forum-tested LAS with four Extra Bytes dimensions imported "with no problem" (anecdotal, pre-2024 build); ReCap's own exports (E57/PTS) carry only XYZ/RGB/intensity, so any ID field is lost on a ReCap round-trip regardless of where we store it.
Key decisions
| Decision | Choice | Why |
|---|---|---|
| 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 fromwriter.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 theobject_instance_iduint32 dimension whenlas_instance_dimis enabled; fill from the decimated instance array (same source as line 207); keep thepoint_source_idmirror, clamping mirror writes at 65535.- Apply identically in
write_lasandwrite_las_splitso split files carry the ID too. - Raise
MAX_INSTANCE_IDhandling infuse.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: addlas_instance_dimwith 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 asfuse.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 matchpoint_source_idmirror below 65535, mirror clamps above, dimension absent whenlas_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
- ReCap Extra Bytes tolerance in ReCap 2024–2026 is based on one forum test from 2021, not Autodesk docs. Mitigated by Phase 0 gate + config flag fallback.
- round-trip Any export from ReCap (E57/PTS/RCS) loses the ID (and per Autodesk docs likely classification too in per-point form). The original seg3d LAS must remain the master file; already the pipeline's assumption, but now worth stating in client-facing docs.
- consumers Some strict/legacy LAS readers mis-handle Extra Bytes VLRs. Low likelihood (spec-compliant readers must skip unknown VLRs), and
las_instance_dim=falserestores byte-identical current behavior. - semantics
point_source_idofficially means flight-line/source provenance, not object identity. We already repurpose it; keeping the mirror preserves status quo, and the properly-named Extra Bytes dim fixes the semantics for new consumers. - size +4 bytes/point ≈ +11% file size on PDRF 7 (36 → 40 bytes). Negligible for deliveries; flag exists if it ever matters.
Open questions
- Default for the new
las_instance_dimflag (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.
- 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. - 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; thestats["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. - 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.