CRAS real pair: independent registration checks and RGB point transfer (#4381)¶
This directory closes the real-pair gate of #4381 with measured numbers. It records what was done, exactly how, and what the held-out residuals do and do not support. Nothing here is an accuracy approval: the tolerance below was derived from held-out residuals after the solve, by a rule fixed beforehand, and every coverage number is reported as the planner measured it.
Source data and licence¶
- Scan and model: Abreu et al., CRAS laboratory record Zenodo 7948116, CC BY 4.0. Retain the attribution and the record DOI in any derivative. The paper's 3 mm figure concerns scan-to-scan registration and is not used here.
- Archive
craslabannotated.zip, 4,267,281,425 bytes, downloaded complete on 2026-09-12 (curl -L -C - --retry 5, 2 min 37 s at ~26 MB/s); MD5e5ecedab8f2a1d1f91861a3aec028a72matches the record; SHA-256933732e81910116cec5e74c9205074b82d1f571f3d9816386b68a0a909112671. Its single memberCRASLAB_annotated.asc(26,752,658,348 bytes) holds a header row, a count row (584701977) and tab-separatedX Y Z R G B Intensity Classification. - Model
craslabbim.ifc, 67,553,572 bytes, MD5e20658f0d2d9e13c62363169b7fa3193(matches), SHA-256a83e5730…, IFC2X3 (seecras-ifc4-migration.json). - No point data is committed. Archive, subsets and the full IFC live under
~/ci/cras/on the host that ran this; every subset manifest pins SHA-256s.
Acquisition (whole archive, streamed, never unpacked)¶
python tools/texture-authoring/cras-archive-histogram.py craslabannotated.zip hist— one pass over all 584,701,977 rows (192 s): bounds x 0.01–18.32, y −4.18–20.27, z −2.07–3.25 m; 29,239 occupied 0.25 m cells; label counts.cras-archive-histogram.json. The floor sits at z ≈ −1.15 and the room ceilings at z ≈ 1.05 (corridor) / 1.55 (room B).python tools/texture-authoring/cras-archive-subset.py craslabannotated.zip BOXES.json OUT— five further passes wrote spatially bounded, systematically decimated subsets as binary PLY (double XYZ in unchanged source metres, RGB8, label, original row index): subsets/ manifests give box, stride, points inside/kept and SHA-256 per file. Rooms were chosen from the occupancy map, not from file order: the corridor (x 7.5–10.75, y −4.5–0.5) and room B (x 10.75–18.5, y 2.75–11.25), plus dense strips along its north, east, south and west walls and two undecimated window-jamb boxes.

The IFC counterparts, from the published geometry (IfcOpenShell 0.8.3, world
coordinates): the corridor between walls 0FDABnXCHCUR8gI54b$LZ2 (face
x = −2.0197) and 1gsZ4XbYD0qRhfvwOUEK3Z (face x = 0.3907) with end wall
1gsZ4XbYD0qRhfvwOUEJ7O and its recessed door; room B between walls
1gsZ4XbYD0qRhfvwOUEI1z (south, y = 3.0085), 3qeiF73TD1uOeHIcNGcTy8 (north,
y = 10.8615) and 0FDABnXCHCUR8gI54b$Kx2 (east brick wall, x = 7.9862) with
five clerestory windows per drywall (sill 2.29 m, head 2.77 m) and five 0.5 m
windows in the brick wall (sill 2.29, head 2.76). The IFC4 derivative
(ifcopenshell.util.schema.Migrator, all 2,414 GlobalIds preserved) has
identical wall geometry for every reference wall.
Landmarks: three-plane intersections, frozen fit/check lists¶
python tools/texture-authoring/cras-landmarks.py craslabbim.ifc subset landmarks.json
(landmarks.json). Each landmark is the intersection of
three named planar faces:
- IFC side: face coordinates read from the published IFC2X3 geometry (wall faces, opening jambs and heads, slab top z = 0.10). No IFC value is typed by hand; each row names the GlobalIds involved.
- Scan side: for each plane, points inside a search box are fitted by
seeded RANSAC (600 hypotheses, 6 mm inliers, normal within 30° of the
expected axis, ≥ 20 support points) followed by five least-squares refits;
the box for the next plane is placed from the planes already fitted. A
coarse offset (9.90, 0.22, −1.245) m read from wall-face histogram peaks
places the first box only; it never enters a coordinate. Re-running every
fit with the measured median offset instead moves no landmark by more than
1.7 mm (
refit_with_measured_offset_drift_m), so the boxes did not choose the answer. Plane RMS is 0.6–3.5 mm; the SHA-256 of each plane's supporting archive rows is recorded. - Partition: features are listed in a fixed order; those that could be measured alternate fit, check, fit, … starting with fit. The rule was written before any solve and the list was not revised against residuals.
Features and their fate:
| Landmark | Planes | Result |
|---|---|---|
| Room B north clerestory band, west end, head | wall face ∩ jamb reveal ∩ head reveal | not measurable: the jamb there is a curved frame profile, no planar reveal |
| Room B north band, east end, head | north face ∩ east face ∩ head | fit |
| Room B south band, west end / east end, head | as above | check / fit |
| East brick wall, windows 1328061 · 1327917 · 1327933 · 1327949 · 1327965, S and N jambs at the head | east face ∩ jamb reveal ∩ head reveal | 7 measured (fit/check alternating); 3 jambs are seen only at grazing angles and keep 2–33 candidate points without a coplanar set of 20, one of them even undecimated |
| Room B west door 1328449, south jamb at floor / at head | west face ∩ jamb ∩ floor / head | fit / check |
| Corridor floor corners SW / SE | two wall faces ∩ floor | check / fit |
| Corridor door 1328314 jambs at the floor, W / E | end-wall face ∩ jamb ∩ floor | check / fit |
| Room B floor SE | two wall faces ∩ floor | not measurable: benches hide the floor corner |
Sills were deliberately not used: from inside the room every sill reveal is hidden behind the sloped bottom frame member (see the window section below).

Result: 16 landmarks, 8 fit + 8 held-out, spanning both rooms, three walls, two heights (floor 0.10 m and heads 2.76–2.77 m) and 12 × 15 m in plan.
Registration (canonical solver) and the tolerance rule¶
node tools/texture-authoring/cras-register-transfer.mjs landmarks.json craslabbim-ifc4-tessellated.ifc SUBSET.ply WALL_GUID OUT ARCHIVE_SHA256
runs IfcAPI.registerScanCorrespondences on the frozen lists
(registration-request.json,
registration-report.json). Source frame
cras-archive-native-metres-v1 bound to the archive SHA-256; target frame
ifc-world-z-up-metres bound to the target IFC SHA-256.
| RMS | median | 90th pct. | max | |
|---|---|---|---|---|
| Fit (8) | 4.09 cm | 2.92 cm | — | 7.20 cm (B-south-band-E-head) |
| Held-out (8) | 4.53 cm | 3.35 cm | 6.54 cm | 6.54 cm (B-west-door-S-head) |
Rotation 0.72°; non-collinearity spread ratios 0.163 (source) / 0.165 (target).
Tolerance rule, fixed before the run: the largest held-out residual rounded up to the next centimetre → 7 cm. Fit residuals never enter it. This is the accuracy the transfer may claim for this pair: a scan surface within 7 cm of its IFC face is consistent with the independently checked registration; nothing tighter is supported. The residuals are not scanner noise (plane fits are millimetre-level) but as-built deviations from the model: the corridor is ~9 cm narrower than modelled (after registration its east face lies 6.0 cm into the corridor from the IFC face, its west face 3.1 cm), the north clerestory band starts ~10 cm further east than the modelled opening, and room B is 3–5 cm deeper between its south and north faces than modelled. A rigid fit spreads these over its 16 points; a transfer bound tighter than a wall's own deviation refuses that wall entirely (see the half-tolerance control).
Where the registered scan lies against each transfer wall¶
node tools/texture-authoring/cras-face-depth.mjs SUBSET.ply registration-report.json craslabbim-ifc4-tessellated.ifc WALL_GUID x|y OUT.json
applies the registration to a subset and histograms, at 1 cm, every point in a
wall's footprint along its thickness axis, measured from the wall's minimum
face (face-depth-wall-1117.json,
face-depth-wall-1273.json). This is the
measured basis for the side statements below; it never enters a transfer.
| Wall (10 cm) | scan surface | position after registration |
|---|---|---|
Corridor drywall 1gsZ4XbYD0qRhfvwOUEK3Z, faces x 0.3907 / 0.4907 |
corridor face (the only one in the subset), 89.8 % of 52,588 points | 6 cm in front of the modelled corridor face (x − 0.06), outside the solid |
North wall 3qeiF73TD1uOeHIcNGcTy8, faces y 10.8615 / 10.9615 |
room B face, 52 % of 421,312 points | 5–6 cm in front of the modelled south face (y − 0.05 … − 0.06), outside the solid |
| far (north) side face, 22 % | 4–5 cm inside the modelled solid (y + 0.04 … + 0.05): 5.5 cm behind the modelled north face, past its 5 cm midplane, and 4.5 cm behind the south face |
So the real north wall is shifted ~5.5 cm south of its model: the room-B capture sits in front of the south face while the far side's capture sits inside the modelled wall, nearer to the south face than to the north face it belongs to. Without orientation this is the hardest side question the planner faces, and the transfer below reports it as such.
Transfer target: a tessellated derivative¶
The published model is swept solids with 1,328,372 entities (each manufacturer
window/door family carries ~47,000 vertices). The F4 target policy takes direct
IfcTriangulatedFaceSet bodies (evaluated occurrences are F6, #4404) and the
planner bounds its target at 200,000 entities, so
python tools/texture-authoring/cras-tessellated-target.py craslabbim.ifc craslabbim-ifc4-tessellated.ifc cras-ifc4-tessellated.json
writes what an IFC4 Reference View export would: all 61 walls and 5 slabs as
world-coordinate tessellated bodies (openings applied by the independent
IfcOpenShell tessellation), GlobalIds and names unchanged, one neutral surface
style each, 484 entities, 73,495 bytes
(craslabbim-ifc4-tessellated.ifc,
cras-ifc4-tessellated.json). The tessellated
wall corners equal the extrusion corners the landmarks were read from.
RGB point transfer: measured coverage¶
Both runs use orientation: target-referenced (the archive carries no normals
or stations), support radius 4 cm, 6–48 supporting points, 6 mm surface band,
minNormalDot 0.8, ambiguity 2 mm, and maxDistanceMetres = maxBehindMetres =
the tolerance. Times are single Node runs on this host, not a browser claim.
The half-tolerance control runs at exactly half the tolerance (3.5 cm), as
cras-register-transfer.mjs computes it; the committed summaries carry
maxDistanceMetres = maxBehindMetres = 0.035 for those rows.
Corridor drywall 1gsZ4XbYD0qRhfvwOUEK3Z (10 cm, no openings; source:
corridor subset, stride 24, 465,029 points) —
transfer-summary-wall-1117.json:
| bound | texels/m | samples | observed | too far | normal | ambiguous | behind | sparse | observed area | work used |
|---|---|---|---|---|---|---|---|---|---|---|
| 7 cm | 64 | 105,126 | 33,843 | 63,591 | 793 | 700 | 119 | 6,080 | 8.20 m² of 25.28 m² | 43.5 M of 128 M (376 ms) |
| 3.5 cm | 64 | 105,126 | 0 | 105,092 | 0 | 15 | 0 | 19 | 0 | 15.8 M (113 ms) |
The wall's 25.3 m² are two 4.1 × 2.9 m faces plus edges. Only the corridor
face was scanned in this subset, and only below the corridor's suspended
ceiling (scan z 1.04 ↔ IFC 2.29 m), so ≈ 9.1 m² could ever be observed; 8.20 m²
were. Per face from the independent reopen (observed_by_face_normal in
oracle-wall-1117.json, 55 barycentric samples per
triangle): corridor face (−x) 75.5 % of samples observed, far face (+x) 0 %,
bottom edge 15.5 %, every other face 0 %. The far face, the 0.7 m above the
ceiling and the edges are unknown by distance. At half the tolerance nothing
is observed, because the as-built face lies 6 cm in front of the modelled one
— the tolerance is doing exactly the work the held-out residuals licence.
Atlas 512 × 700 (textures/b12ae7a6….png); the bright band near the ceiling
is the fluorescent lighting in the capture.
North wall 3qeiF73TD1uOeHIcNGcTy8 (10 cm drywall with five clerestory
windows, both faces scanned; source: strip stride 36, 623,244 points) —
transfer-summary-wall-1273.json:
| bound | texels/m | outcome |
|---|---|---|
| 7 cm | 64 | refused: Transfer work budget exhausted; reduce source extent or atlas density (763 ms) |
| 7 cm | 32 | 40,672 samples, 16,033 observed, 7,975 too far, 3,024 normal, 1,256 ambiguous, 7,478 behind, 4,906 sparse; 15.14 m² of 37.22 m²; 59.5 M work (331 ms); atlas 512 × 374 |
| 3.5 cm | 32 | 1,521 observed, 33,898 too far, 945 behind; 1.34 m² |
Per face from the independent reopen
(oracle-wall-1273.json): room-B face (−y) 49.0 %
of samples observed, far face (+y) 7.4 %, window heads / bottom (−z)
13.8 %, jambs (±x) 21–24 %, tops (+z) 0.5 %. This is the side question from
the depth table resolved by target geometry alone: the room-B face takes the
room capture 5–6 cm in front of it (a capture in front of a face is preferred
over one inside the solid), and the far side's capture, 5.5 cm inside the
modelled wall and past its midplane, is refused as behind on the north face
instead of being painted there — the 7,478 behind samples. The 7.4 % the north
face does observe is what lies within 7 cm in front of its modelled position
(a surface at y + 0.15 … + 0.16 in the depth histogram). Before the
nearest-face rule this wall reported 21,892 observed / 292 behind with both
faces ~39 % observed, i.e. the north face was painted from a capture 5.5 cm
behind it and the room-B face partly from the far side's capture 4.5 cm behind
it; those numbers were withdrawn with this revision.
The refusal at 64 texels/m is the bounded job behaving as specified on a real
wall: two scanned faces 10 cm apart double the points every query visits, and
the fixed 128,000,000-unit budget stops the plan rather than degrading it. The
planner now reports budget.workUsed beside coverage so this is auditable.
Independent apply and reopen¶
python tools/texture-authoring/cras-transfer-oracle.py craslabbim-ifc4-tessellated.ifc RUN tolerance WALL_GUID OUT
applies only the plan's created/edited/removed entities with IfcOpenShell,
writes the transferred IFC and PNG, reopens the file and checks it
(oracle-wall-1117.json,
oracle-wall-1273.json): wall geometry unchanged
(12 / 60 triangles), exactly one IfcIndexedTriangleTextureMap indexing every
triangle, the image URI equal to the atlas asset (named by its own SHA-256),
IfcSurfaceStyleWithTextures assigned, IfcOpenShell schema validation 0 errors
(EXPRESS rules not run). Sampling every triangle's UV centroid in the atlas: 2
of 12 (the corridor face) and 12 of 60 triangles carry captured colour, the
rest the prior style; observed_by_face_normal samples 55 barycentric points
per triangle and groups them by the triangle's world normal (the per-face
percentages quoted above).
apps/viewer/src/lib/appearance/scan/cras-transferred-output.test.ts is the
ifc-lite side over the committed
craslabbim-ifc4-tessellated-transferred-wall-1117.ifc:
the wall tessellates with identical ordered corners, one UV per vertex and the
atlas reference; packaged as IFCZIP (fflate + unwrapIfcZipWithResources)
the model bytes and the texture round-trip byte-exact; seeded into a room by
the real owner seed and reconstructed by a fresh guest, the wall is selectable
by its GlobalId path with byte-exact texture pixels and UVs.
What this does and does not establish¶
- Established: a licensed real scan/IFC pair, sixteen independently measured three-plane correspondences with frozen fit/check lists, held-out residuals published before any bake tolerance was chosen, a tolerance derived from them by a pre-stated rule, and RGB point transfer onto two walls of that model with observed/refused/unknown coverage, budget refusal, independent reopen, IFCZIP packaging and room join all measured on the output.
- Not established: absolute survey accuracy (the landmarks are manually defined building features measured in both datasets, not surveyed control points); any tolerance below 7 cm for this pair; behaviour on voided extrusions (the target is a tessellated derivative); browser timing (Node runs only); the colour fidelity of the captured atlas beyond what the scanner recorded; the side of an unoriented capture that lies deeper inside a modelled wall than its midplane (by geometry it belongs to the opposite face, and nothing in this archive can overrule that).
- Orientation: with no normals or stations in the archive, every point plan is
target-referenced, and the side of each sample rests on the target's own geometry: self-occlusion for captures outside the solid, nearest-face attribution for captures inside it (behind bound capped at half the thickness), and preference for a capture in front of the face over one inside the solid. The controlled and WASM tests pin these with captures 1 mm outside the faces (all three orientation sources), 1 mm and 3 mm inside a 4 mm partition, and the measured 10 cm-wall configuration above (5.5 cm in front / 4.5 cm inside).