Point masks in Autodesk ReCap — export & annotation plan
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.
| Question | Answer | Consequence |
|---|---|---|
| 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)
| Topic | Finding |
|---|---|
| Import formats | LAS, LAZ, E57, PTS/PTX, XYZ/TXT, RCS/RCP. PLY is not supported — the existing _classcolor.ply is unusable as a deliverable. |
| Classification | Read 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 fields | point_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 modes | RGB/realistic, elevation, intensity, normals, scan location, classification color. |
| Per-object review | Best 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. |
| Export | E57, 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/RCS | Proprietary. No public reader for per-point classification; the Autodesk SDK is access-gated. Not a viable round-trip path. |
| Import behaviour | Decimation, 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. |
| CRS | LAS 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. |
Key decisions
- One LAS 1.4 PDRF7 per segment (
segment_NNN_seg3d.las), classification as the mask channel, written withlaspynext to the existing npz/ply. Portable to CloudCompare/PDAL/QGIS as well as ReCap. - Georeference on export. This was the significant discovery: run3 writes points as
source − geoshift, with a per-dataset anchor inlane_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. - 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=0disables it. - 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_idand in the npz, with a registry instats.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. - 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. - 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.
- Sensor RGB by default, ×257 upscale for 8-bit sources;
las_rgb_mode=classbakes 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_modeconfig knob withConfigErrorvalidation; wired throughwrite_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;pyprojused only if already installed),looks_georeferenced()guard, applied shift + EPSG recorded instats.json. - Splitting:
writer.write_las_split()for per-class and per-instance files,--las-splitflag. - Importer: new
seg3d-recap-importentry point reading E57 (via optionalpye57), 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_idpopulated with 12 / 24 / 64 instances, surface match rate 1.0. - Handed over on the shared drive following the documented folder convention: LAS +
stats.jsonper 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
- Install ReCap Pro 2027.0.1; import one real segment's
segment_NNN_seg3d.laswith decimation set to 0. - Verify codes 64–71 appear as distinct, colorable, hideable, renameable classes.
- 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.
- Verify whether ReCap reads the WKT VLR or still asks for the CRS at import.
- 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. - 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=classso masks survive visually in RGB (already implemented). - If still unusable:
--las-split classgives 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
| Risk | Severity | Mitigation |
|---|---|---|
| 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
- Which dataset gets annotated first, and does it have a
run3_geoshift.jsonon disk? Only two such files exist on the N: share today — for other datasets the anchor has to come from the pipeline run that produced the segments, otherwise the export stays in the local frame (and correctly gets no CRS). - Is the vertical datum ever needed? The source scans declare metre units but no vertical datum, so the export is horizontal-CRS-only. If Civil 3D downstream needs a compound CRS, someone has to state which height reference the data is on.
- Per-instance review: is one file per object actually pleasant for the reviewer, or is class-level correction in ReCap plus instance-level checking in CloudCompare the better split of work? Worth a 10-minute trial in phase 2 before generating hundreds of files.
- Should the corrected labels feed back into detector training automatically, or stay a manual QC artefact for now?