Point masks in Autodesk ReCap — export & annotation plan

2026-08-03 · updated after Q&A + implementation · 3dsegmentation (AI3D-344)

Summary

The fused seg3d clouds are annotated by hand in Autodesk ReCap Pro. ReCap cannot read PLY, so the deliverable is one LAS 1.4 point-format-7 file per segment: the point masks ride in the full-byte classification field (codes 1, 2, 5, 64–71), alongside sensor RGB, intensity and a per-object point_source_id.

ReCap Pro 2025.1 added extended LAS 1.4 classification support for point formats 6–10, which is the release note that makes our user-defined codes safe. Annotation uses ReCap's native reclassify UI — no ReCap-side custom tooling. Corrections come back through per-class E57/PTS exports and a hash-join importer, because ReCap cannot export LAS at all.

LAS was also chosen for portability outside ReCap: CloudCompare, PDAL, QGIS and laspy all read it and expose classification, intensity and point-source-id directly.

Status: phases 1, 4 and 5 are implemented and pushed to PR #2, and the exporter has now been run on real data — three segments of the Abschnitt 4_5 dataset were fused, verified and handed over to the reviewer on the shared drive. Phase 2 (acceptance test in ReCap itself) is blocked on the licence; phase 3 is conditional on phase 2 failing.

Answers that shaped this revision

Four open questions from the first version were answered, and two of them changed the design materially.

QuestionAnswerConsequence
Which ReCap version? Latest stable — 2027.0.1 (build 27.0.1.242, released 2026-06-03) Well past 2024.1, so manual reclassification is available; and past 2025.1, so full-byte LAS 1.4 classification is officially supported. Phase 2 risk drops a lot.
Which CRS / EPSG? "You decide" Read from the actual data rather than guessed: the source scans carry GeoTIFF keys for ETRS89 / UTM zone 32N (EPSG:25832). But the pipeline frame turned out to be geoshifted — see the decisions below.
Per-instance identity? Fine to use an extra channel — plus "how does that look in ReCap?" Instance ids are now written to point_source_id + npz. Honest answer on the ReCap side: ReCap does not surface that field, so per-object review is done with one file per instance (each imports as an isolatable scan), or in CloudCompare.
Who annotates, and where do files live? Miro + a colleague (mostly the colleague), via the shared working folder on the N: drive A concrete folder convention is now documented, matching the existing YYMMDD_topic naming already used in that share.

Research findings (Aug 2026)

TopicFinding
Import formatsLAS, LAZ, E57, PTS/PTX, XYZ/TXT, RCS/RCP. PLY is not supported — the existing _classcolor.ply is unusable as a deliverable.
ClassificationRead and fully exposed: per-class color, show/hide, lock, rename, and manual reclassify (2024.1+). 2025.1 added LAS 1.4 PDRF 6–10 classification support, covering codes 64–255.
Other per-point fieldspoint_source_id, user_data, gps_time and Extra Bytes VLRs are not documented as surfaced anywhere in the UI. Import does not reject them; they are simply invisible.
Display modesRGB/realistic, elevation, intensity, normals, scan location, classification color.
Per-object reviewBest available mechanism is one file (scan) per object — each import is separately isolatable in the Project Navigator, with no documented scan-count limit. Regions exist but are not a per-point attribute and do not survive export as one.
ExportE57, PTS and RCP/RCS only — no LAS/LAZ export. Neither E57 nor PTS carries a classification field, so ReCap's "export classifications individually" (one file per class) is the supported way to get labels out.
RCP/RCSProprietary. No public reader for per-point classification; the Autodesk SDK is access-gated. Not a viable round-trip path.
Import behaviourDecimation, noise filtering and clipping are on by default — set decimation to 0. Documented precision floor is 1 mm, matching our LAS scale. No guarantee of point order or 1:1 identity.
CRSLAS 1.4 requires OGC WKT1 in the LASF_Projection VLR (record id 2112) with global-encoding bit 4 set. ReCap asks for the CRS interactively at import; it is not documented to read the VLR automatically.

Sources: Autodesk help & release notes, ReCap supported-formats page, ASPRS LAS 1.4 R15, Autodesk forums. Items that Autodesk does not document are flagged as unverified in docs/recap_annotation.md.

Key decisions

  1. One LAS 1.4 PDRF7 per segment (segment_NNN_seg3d.las), classification as the mask channel, written with laspy next to the existing npz/ply. Portable to CloudCompare/PDAL/QGIS as well as ReCap.
  2. Georeference on export. This was the significant discovery: run3 writes points as source − geoshift, with a per-dataset anchor in lane_points/run3_geoshift.json. Fused clouds are therefore in a local frame a few hundred metres from zero. The LAS writer adds the geoshift back so the deliverable sits in real survey coordinates; the npz and ply stay in the pipeline frame because the npz is the hash-join key for the round-trip.
  3. EPSG:25832 WKT1 VLR, but only stamped when the exported coordinates genuinely are world coordinates (geoshift applied, or never shifted). A local-frame cloud gets no CRS rather than a false one. las_crs_epsg=0 disables it.
  4. Instance channel. Every detected object — one lane-marking cluster component, or one guardrail/sign/gate/delineator/tree detector instance — gets a uint16 id in point_source_id and in the npz, with a registry in stats.json. A point only keeps an instance id where its final class still matches that instance's class, so the two channels can never disagree.
  5. Per-object review via file splitting (--las-split instance|class), since ReCap will not show the instance channel itself. Each file imports as its own isolatable scan.
  6. Round-trip by coordinate join, not by point identity. Class comes from the export filename; the join is the repo's 1 mm integer XYZ hash plus a 1 cm nearest-neighbour fallback — mandatory, because ReCap's exports are lossy and an exact-only hash loses several percent of points to bucket-boundary rounding.
  7. Sensor RGB by default, ×257 upscale for 8-bit sources; las_rgb_mode=class bakes the palette in as a fallback.

Phases

Phase 1 — LAS exporter done · PR #2
  • writer.write_las(): LAS 1.4 PDRF7, full-byte classification, uint16 intensity, sensor/class RGB modes, scan angle in 0.006° units, return 1-of-1.
  • las_rgb_mode config knob with ConfigError validation; wired through write_all() and the CLI.
  • Tests: mm-precision round-trip on national-grid coordinates, user-code 66 survival, RGB upscale, class-palette mode, bad-mode rejection.
  • Docs: docs/recap_annotation.md, README, fusion spec.
Phase 4 — Instance channel, georeferencing, round-trip importer done · PR #2

Everything the four answers unblocked, implemented in one pass:

  • Instances: clusters.component_index() joins points to individual cluster components in a single sorted-key pass (cost independent of component count); fuse.build_instances() assigns the ids and builds the registry. paint_lines_from_clusters() was reimplemented on top of the same path so masks and instance ids cannot drift apart.
  • Georeferencing: io_npz.load_geoshift(), crs.py (EPSG→WKT1 table, no new hard dependency; pyproj used only if already installed), looks_georeferenced() guard, applied shift + EPSG recorded in stats.json.
  • Splitting: writer.write_las_split() for per-class and per-instance files, --las-split flag.
  • Importer: new seg3d-recap-import entry point reading E57 (via optional pye57), PTS/PTX/TXT/XYZ and LAS/LAZ; class from filename (name or numeric code); geoshift undone; 1 mm hash + cKDTree tolerance join; corrected npz + JSON change report; loud failure above an unmatched-rate threshold.
  • Tests: 89 passing, including a regression test proving the lossy-ASCII-export case that exact-only matching fails.
Phase 6 — First real-data export and hand-over done · 2026-08-03

Everything up to here was verified on synthetic fixtures. This phase ran the whole thing on the actual dataset, on the machine that holds the data.

  • Fused three segments of the Abschnitt 4_5 dataset — 573k, 1'260k and 2'580k decimated points — with the real asphalt-edge, guardrail, sign and ground-mask inputs, lines taken from the mask clusters.
  • Verified on the written files: LAS 1.4 / PDRF 7, EPSG:25832 WKT VLR present with global-encoding bit 4, coordinates in the world frame (the per-dataset geoshift really was added back), classification codes 1/2/5/64–71 present, point_source_id populated with 12 / 24 / 64 instances, surface match rate 1.0.
  • Handed over on the shared drive following the documented folder convention: LAS + stats.json per segment, the run summary, and a plain-text README for the reviewer covering the decimation-0 import rule, the code legend and the return path.
  • One upstream gap surfaced and was recorded rather than papered over: two of the three segments have no asphalt class because the asphalt-edge run does not cover them, so their carriageway currently falls under ground.

Tests now at 95 passing.

Phase 2 — Acceptance test on the licensed build blocked · needs licence
  1. Install ReCap Pro 2027.0.1; import one real segment's segment_NNN_seg3d.las with decimation set to 0.
  2. Verify codes 64–71 appear as distinct, colorable, hideable, renameable classes.
  3. Verify RGB is not near-black, intensity looks sane, and coordinates land correctly at UTM 32N magnitudes (~6.9e5 / 5.4e6) — check a known control point.
  4. Verify whether ReCap reads the WKT VLR or still asks for the CRS at import.
  5. Reclassify a few points, run "export classifications individually", and feed the result through seg3d-recap-import — confirm the edit is recovered and the unmatched rate is low. This also settles whether E57 or PTS is the better export here.
  6. Record the build number and results in docs/recap_annotation.md.
Phase 3 — Fallbacks, only if Phase 2 fails conditional
  • If codes 64–71 are not shown as classes: re-run with --set las_rgb_mode=class so masks survive visually in RGB (already implemented).
  • If still unusable: --las-split class gives one file per class, each a toggleable scan (already implemented — this was built as part of phase 4 rather than deferred).
  • Last resort: remap user codes into the standard 0–31 range via a config-driven code map, if only standard codes render.
Phase 5 — Nice-to-haves CRS done rest optional
  • WKT CRS VLR — done in phase 4, EPSG:25832.
  • LAZ output via laspy[lazrs] if transfer size over the N: share becomes annoying.
  • Multi-segment ReCap project instructions (many segment LAS files in one project, each a scan).
  • A small QC step that renders the exported LAS back to PNG to catch a bad export before it reaches a reviewer.

Risks

RiskSeverityMitigation
A build does not expose user-defined codes 64–255 as distinct classes. med Downgraded from high: ReCap 2025.1's release note explicitly adds PDRF 6–10 classification support, and the licensed build will be 2027.x. Fallbacks (class-RGB, per-class split) are already shipped.
ReCap cannot export LAS, so labels return only through lossy per-class E57/PTS. high The importer handles it: class from filename, tolerance join, unmatched-rate guard. Validate early in phase 2 with a tiny cloud before a reviewer spends real hours.
Export precision loss breaks the coordinate join. med Found in testing, not in production: exact 1 mm hashing lost ~7 % of points on a 0.1 mm ASCII export. Fixed with a 1 cm nearest-neighbour fallback (well under the 6.67 cm voxel spacing) and reported as an exact/nearest split so precision problems are visible.
Wrong georeferencing — a cloud stamped with a CRS it is not in. med The CRS is only written when the geoshift was applied or the coordinates already look global; otherwise the file gets no CRS. The applied shift is recorded in stats.json and undone on import.
The 8-bit vs 16-bit RGB heuristic (max ≤ 255 → ×257) misfires on genuinely dark 16-bit data. low Acceptable for the ACC sources in use; revisit if colors look washed out.
ReCap import decimation or unification drops/moves points. low Instructions say decimation 0; the join is coordinate-based and order-free, and the unmatched rate fails loudly.

Open questions