#!/usr/bin/env python3 """Derive the *(unit scale, rigid transform)* pair that compiles the **design export**. ``compile_scene.py`` splits two jobs that must not be confused (design document §8.1): * ``--meters-per-source-unit`` changes *lengths*: one stated source unit, in metres; * ``--transform`` is **rigid** (orthonormal rotation + translation, no scale), and the importer validates that before it compiles anything. The finger design export (``orisys-sensor-design/build/export/finger/r01/compute``) is the same surface as the SDK asset (``orisys-sdk/sensors/finger/assets/30fps-0703``) — identical ``faces`` and ``loop_to_vertex``, so vertex *i* is vertex *i* — but it lives in the design repo's camera/world frame and in *design* units. ``align_housing.py`` fits that relationship as a similarity ``p_sensor_mm = s·R·p_design + t``; the matrix it writes (``housing/finger_r01_design_to_sensor.npy``) therefore **already carries the scale**: it is not a legal ``--transform``, and pairing it with ``--meters-per-source-unit 1.8388e-5`` would apply the scale twice. This tool does the split, once and reproducibly: design frame, design units --(R, t_m)--> sensor frame, metres p_sensor_m = (0.001·s)·(R·p_design) + 0.001·t so the compiler can be given ``--meters-per-source-unit 0.001·s`` and a rigid 4x4 whose rotation block is exactly ``R`` and whose translation is ``0.001·t`` (metres). The metre translation matters: ``t`` from the fit is in the sensor frame's *millimetres*, because that is the frame the SDK asset is stated in. It also checks, numerically, the wrong pairings — all three of which are live mistakes here: * ``--transform`` = the scale-bearing matrix is **refused** by the rigid-transform validator (it is not a legal rigid transform at all, whatever the unit says), so the "scale applied twice" trap cannot reach the compiler through that door; * what the double scale *would* produce is computed directly from the importer's own ``_apply_rigid``, so the number is on the record rather than asserted; * ``--meters-per-source-unit 0.001`` (calling a design unit a millimetre) with the correct rigid transform does compile, and is 54.38x too large — a package that looks entirely plausible. python tools/scene_assets/fit_design_frame.py \\ --design D:/orisys_ws/orisys-sensor-design/build/export/finger/r01/compute \\ --sensor D:/orisys_ws/orisys-sdk/sensors/finger/assets/30fps-0703 \\ --rigid-output tools/scene_assets/housing/finger_r01_design_to_sensor_rigid_m.npy \\ --report tools/scene_assets/housing/finger_r01_design_frame.json """ from __future__ import annotations import argparse import json import sys from pathlib import Path import numpy as np def _bootstrap_paths() -> None: here = Path(__file__).resolve() repo = here.parents[2] for path in (repo / "apps" / "studio" / "src", repo / "shared" / "src"): if path.is_dir() and str(path) not in sys.path: sys.path.insert(0, str(path)) if str(here.parent) not in sys.path: # The fit itself stays in one place: ``align_housing.fit_similarity``, which # checks the 1032 correspondences before it writes anything. sys.path.insert(0, str(here.parent)) #: The SDK asset's frame is millimetres; the compiled package is metres. MM_PER_SENSOR_UNIT = 0.001 def derive(design_dir: Path, sensor_dir: Path, *, tolerance: float) -> dict: from align_housing import apply_similarity, fit_similarity, load_faces, load_vertices design_mesh = design_dir / "mesh.npz" sensor_mesh = sensor_dir / "mesh.npz" source = load_vertices(design_mesh) target = load_vertices(sensor_mesh) if source.shape != target.shape: raise SystemExit( f"the meshes disagree: {source.shape} vs {target.shape} — the correspondence the " "fit relies on does not exist" ) if not np.array_equal(load_faces(design_mesh), load_faces(sensor_mesh)): raise SystemExit( "the faces arrays differ, so vertex i is not vertex i; refusing to fit a transform" ) rotation, translation_mm, scale_mm = fit_similarity(source, target) residual_mm = apply_similarity(source, rotation, translation_mm, scale_mm) - target rms_mm = float(np.sqrt((residual_mm**2).sum(axis=1).mean())) diagonal_mm = float(np.linalg.norm(target.max(axis=0) - target.min(axis=0))) relative = rms_mm / max(diagonal_mm, 1e-12) if relative > float(tolerance): raise SystemExit( f"the fit is off by {relative:.3g} of the mesh diagonal (budget {tolerance:g}): " "these two meshes are not the same shape" ) # The split. ``scale_mm`` maps design units -> sensor millimetres, so the unit # statement is that many millimetres, and the translation is carried into metres # with the same factor the rotation gets. # # **Matrix convention.** ``align_housing.fit_similarity`` fits the *row-vector* form # ``p_sensor = s·(p_design @ R) + t``, while ``compile_scene.py`` hands ``--transform`` # to ``legacy._apply_rigid``, which is the *column* form ``p_sensor = T @ [p_design; 1]``. # So the block the compiler needs is ``R.T``, not ``R``: pasting ``R`` in would rotate the # surface the wrong way by twice the fitted angle while leaving every extent plausible # (a 90-degree rotation's transpose is its inverse). ``verify()`` is what proves this. meters_per_source_unit = MM_PER_SENSOR_UNIT * scale_mm rigid = np.eye(4, dtype=np.float64) rigid[:3, :3] = rotation.T rigid[:3, 3] = MM_PER_SENSOR_UNIT * translation_mm scale_bearing = np.eye(4, dtype=np.float64) scale_bearing[:3, :3] = scale_mm * rotation scale_bearing[:3, 3] = translation_mm extent_design = np.ptp(source, axis=0) extent_sensor_mm = np.ptp(target, axis=0) return { "design_dir": str(design_dir), "sensor_dir": str(sensor_dir), "vertices": int(len(source)), "fit": { "scale_design_unit_to_sensor_mm": scale_mm, "rotation_row_convention": rotation.tolist(), "rotation_transposed_column_convention": rotation.T.tolist(), "translation_sensor_mm": translation_mm.tolist(), "rms_sensor_mm": rms_mm, "rms_sensor_m": rms_mm * MM_PER_SENSOR_UNIT, "diagonal_sensor_mm": diagonal_mm, "relative_of_diagonal": relative, "tolerance": float(tolerance), }, "unit_statement": { "meters_per_source_unit": meters_per_source_unit, "one_design_unit_is_mm": scale_mm, "one_sensor_mm_is_design_units": 1.0 / scale_mm, }, "rigid_transform_sensor_from_design_m": rigid.tolist(), "scale_bearing_matrix_sensor_mm_from_design": scale_bearing.tolist(), "extent_design_units": extent_design.tolist(), "extent_sensor_mm": extent_sensor_mm.tolist(), "extent_compiled_m": (extent_sensor_mm * MM_PER_SENSOR_UNIT).tolist(), "rotation_orthonormality_error": float( np.abs(rotation @ rotation.T - np.eye(3)).max() ), "rotation_determinant": float(np.linalg.det(rotation)), } def verify(design_dir: Path, sensor_dir: Path, rigid: np.ndarray, units: float) -> dict: """Compile in memory through the real importer and compare with the SDK-frame route. The reference is the *shipped* route: the SDK asset with ``0.001`` and no transform. If the design route is right, both land on the same sensor-metre positions — order included, because identical ``faces``/``loop_to_vertex`` and no UV seam means the render vertices come out in the same order. """ from orisys_app_shared.scene3d.contracts import ThreeDError from orisys_app_shared.scene3d.legacy import LegacyFingerImporter, validate_rigid_transform design_map = design_dir / "unroll_map_uv_s0.42.npz" sensor_map = sensor_dir / "unroll_map_uv_s0.4.npz" importer = LegacyFingerImporter() reference = importer.convert( sensor_dir, sensor_map, transform_sensor_from_source=np.eye(4), meters_per_source_unit=MM_PER_SENSOR_UNIT, ).sensing_part().positions_sensor_m result: dict = {} compiled = importer.convert( design_dir, design_map, transform_sensor_from_source=rigid, meters_per_source_unit=units, ).sensing_part().positions_sensor_m difference = np.asarray(compiled, dtype=np.float64) - np.asarray(reference, dtype=np.float64) result["design_route"] = { "extent_m": np.ptp(compiled, axis=0).tolist(), "low_m": compiled.min(axis=0).tolist(), "high_m": compiled.max(axis=0).tolist(), "max_abs_difference_from_sdk_route_m": float(np.abs(difference).max()), "rms_difference_from_sdk_route_m": float(np.sqrt((difference**2).sum(axis=1).mean())), "bitwise_identical_to_sdk_route": bool( np.array_equal(np.asarray(compiled, dtype=np.float32), np.asarray(reference, dtype=np.float32)) ), } result["sdk_route_extent_m"] = np.ptp(reference, axis=0).tolist() from orisys_app_shared.scene3d.legacy import _apply_rigid, split_at_uv_seams stored = np.load( Path(__file__).resolve().parent / "housing" / "finger_r01_design_to_sensor.npy" ) # Trap 1: the scale-bearing matrix as ``--transform``. Refused, whatever the unit is. try: validate_rigid_transform(stored) result["trap_stored_matrix_as_transform"] = "ACCEPTED (the validator missed it)" except ThreeDError as exc: result["trap_stored_matrix_as_transform"] = ( f"refused: {exc.diagnostic.code}: {exc.diagnostic.message}" ) # Trap 2: what the double scale *would* do, computed through the importer's own # rigid application — the stored matrix already holds 1/54.38, and the derived unit # holds it too, so the rotation block scales twice and ``t`` (sensor-mm) is added to # a metre-scale result. with np.load(design_dir / "mesh.npz", allow_pickle=False) as handle: positions, _, _, _ = split_at_uv_seams( handle["vertices"], handle["faces"], handle["uv_data"], handle["loop_to_vertex"] ) doubled = _apply_rigid(positions * units, stored) result["trap_double_scale_if_forced"] = { "extent_m": np.ptp(doubled, axis=0).tolist(), "low_m": doubled.min(axis=0).tolist(), "high_m": doubled.max(axis=0).tolist(), "note": ( "the stated unit already carries the 1/54.38 fit scale and the matrix carries it " "again; the translation is sensor-mm added to a metre-scale result, so the extent " "collapses to the fit residual and the surface sits ~4.5 m from the origin" ), } # Trap 3: a *legal* rigid transform with the wrong unit statement (a design unit is not # a millimetre). This one compiles; nothing downstream can tell it is 54.38x too big. milli = importer.convert( design_dir, design_map, transform_sensor_from_source=rigid, meters_per_source_unit=MM_PER_SENSOR_UNIT, ).sensing_part().positions_sensor_m result["trap_design_unit_called_a_millimetre"] = { "extent_m": np.ptp(milli, axis=0).tolist(), "low_m": milli.min(axis=0).tolist(), "high_m": milli.max(axis=0).tolist(), "factor_too_large": float( np.ptp(milli, axis=0).max() / max(np.ptp(compiled, axis=0).max(), 1e-30) ), "note": "compiles cleanly and is wrong by the fit scale; the validator cannot see it", } return result def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--design", type=Path, required=True, help="design export dir") parser.add_argument("--sensor", type=Path, required=True, help="sensor-frame export dir") parser.add_argument("--rigid-output", type=Path, required=True, help="rigid 4x4 to write") parser.add_argument("--report", type=Path, default=None, help="JSON record of the derivation") parser.add_argument("--tolerance", type=float, default=1e-4) parser.add_argument( "--no-verify", action="store_true", help="skip the in-memory compile checks (they need the app's shared package)", ) args = parser.parse_args(argv) _bootstrap_paths() record = derive(args.design, args.sensor, tolerance=args.tolerance) rigid = np.asarray(record["rigid_transform_sensor_from_design_m"], dtype=np.float64) units = float(record["unit_statement"]["meters_per_source_unit"]) fit = record["fit"] print(f"fit over {record['vertices']} design->sensor correspondences " f"(identical faces, so vertex i is vertex i)") print(f" scale {fit['scale_design_unit_to_sensor_mm']:.12g} " f"sensor-mm per design unit (= 1/{(1.0 / fit['scale_design_unit_to_sensor_mm']):.8f})") print(f" residual rms {fit['rms_sensor_mm']:.3g} mm " f"({fit['rms_sensor_m']:.3g} m) = {fit['relative_of_diagonal']:.3g} of the " f"{fit['diagonal_sensor_mm']:.3f}-mm diagonal (budget {fit['tolerance']:g})") print(f" rotation orthonormality error {record['rotation_orthonormality_error']:.3g}, " f"det {record['rotation_determinant']:.12f}") print(f" translation {[round(v, 12) for v in fit['translation_sensor_mm']]} sensor-mm") print() print("the split the compiler needs:") print(f" --meters-per-source-unit {units:.12g} (one design unit is " f"{(1.0 / fit['scale_design_unit_to_sensor_mm']):.8f} sensor-mm)") print(f" --transform rotation = R, translation = 0.001*t = " f"{[round(float(v), 12) for v in rigid[:3, 3]]} m") print(f" design extent {np.round(record['extent_design_units'], 4)} -> sensor " f"{np.round(record['extent_sensor_mm'], 4)} mm = " f"{np.round(record['extent_compiled_m'], 6)} m") if not args.no_verify: print() print("in-memory compile checks through the real importer:") checks = verify(args.design, args.sensor, rigid, units) design = checks["design_route"] print(f" design route extent {np.round(design['extent_m'], 8)} m " f"low {np.round(design['low_m'], 8)} high {np.round(design['high_m'], 8)}") print(f" sdk route extent {np.round(checks['sdk_route_extent_m'], 8)} m") print(f" agreement max|diff| {design['max_abs_difference_from_sdk_route_m']:.3g} m, " f"rms {design['rms_difference_from_sdk_route_m']:.3g} m, " f"bitwise identical {design['bitwise_identical_to_sdk_route']}") print(f" trap (stored scale-bearing matrix as --transform): " f"{checks['trap_stored_matrix_as_transform']}") forced = checks["trap_double_scale_if_forced"] print(f" trap (derived unit + scale-bearing matrix, forced): extent " f"{np.round(forced['extent_m'], 9)} m, low {np.round(forced['low_m'], 6)}, " f"high {np.round(forced['high_m'], 6)}") milli = checks["trap_design_unit_called_a_millimetre"] print(f" trap (design unit called a millimetre): extent " f"{np.round(milli['extent_m'], 6)} m — " f"{milli['factor_too_large']:.5f}x too large, and accepted") record["verification"] = checks args.rigid_output.parent.mkdir(parents=True, exist_ok=True) np.save(args.rigid_output, rigid) print() print(f"wrote {args.rigid_output}") if args.report is not None: args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text( json.dumps(record, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" ) print(f"wrote {args.report}") return 0 if __name__ == "__main__": raise SystemExit(main())