#!/usr/bin/env python3 """Rebuild the UV/unroll map for a design export, using the SDK's own builder. ``orisys-design export`` now builds this map itself (see ``src/orisys_sensor_design/uv_map.py``); this tool is the standalone diagnostic for the same core, and exists so a map can be rebuilt or cross-checked without re-running Blender: * the Blender export writes **no mask** (``blender/scripts/export.py`` writes geometry, optics, intrinsics and pose only); * the **SDK builds it** — ``orisys.unroll.precompute.build_or_load_domain`` rasterises the mesh's authored UV faces through the camera with a depth buffer, and already knows how to spot a stale cache; * the map **records its own parameters** (``out_width``/``out_height``/``flip_v``/ ``source_scale``/``edge_band``/``edge_expand``/``projection_K``), so a rebuild can be tied to the same inputs. So this is a thin wrapper: resolve the paths, take the parameters from a profile and/or an existing map and/or the command line (never guess them), call the SDK, and report what came out. python tools/build_uv_map.py \ --export-dir build/export/finger/r01/compute \ --profile sensors/finger/r01/profiles/compute.yaml \ --output build/export/finger/r01/compute Then compare it with what ships: python D:/orisys_ws/orisys-app/tools/scene_assets/compare_uv_map.py \ --map build/export/finger/r01/compute/unroll_map_uv_s0.4.npz \ --against D:/orisys_ws/orisys-sdk/sensors/finger/assets/30fps-0703/unroll_map_uv_s0.4.npz """ from __future__ import annotations import argparse import sys from pathlib import Path try: # an installed design package wins from orisys_sensor_design.uv_map import ( PARAM_KEYS, UvMapError, build_uv_map, load_uv_map_params, map_compare_hint, params_from_map, ) except ModuleNotFoundError: # running from a checkout without `pip install -e .` sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from orisys_sensor_design.uv_map import ( PARAM_KEYS, UvMapError, build_uv_map, load_uv_map_params, map_compare_hint, params_from_map, ) #: Keys the map cannot be built without. ``source_scale`` may be absent: it is then derived #: from the output grid and the export's own camera (the documented floor), and the run says #: so. Everything else has to be stated. REQUIRED_KEYS = tuple(key for key in PARAM_KEYS if key != "source_scale") def params_from_profile_file(path: Path) -> dict: """Read the ``uv_map:`` block out of a profile yaml, without PyYAML. Hand-parsed on purpose: the block is six scalars, and this tool must keep working with the interpreter that runs the SDK/map build rather than depending on PyYAML being installed there. Validation (missing block, missing key, unknown key, types) lives in ``load_uv_map_params``, so there is one definition of a valid block. """ if not path.is_file(): raise SystemExit(f"no such profile: {path}") found: dict = {} inside = False for raw in path.read_text(encoding="utf-8").splitlines(): line = raw.split("#", 1)[0].rstrip() if not line.strip(): continue if not line.startswith((" ", "\t")): inside = line.strip() == "uv_map:" continue if not inside or ":" not in line: continue key, _, value = line.strip().partition(":") value = value.strip() if value.lower() in ("true", "false"): found[key] = value.lower() == "true" else: try: found[key] = float(value) if "." in value else int(value) except ValueError: found[key] = value return found def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--export-dir", type=Path, required=True, help="directory holding mesh.npz, camera_intrinsics.npz and world_to_cam.npy") parser.add_argument("--profile", type=Path, default=None, help="profile yaml carrying the uv_map: parameters (preferred)") parser.add_argument("--params-from", type=Path, default=None, help="an existing map to take the parameters from (cross-check)") parser.add_argument("--output", type=Path, default=None, help="directory to write into (default: the export dir)") parser.add_argument("--force", action="store_true", help="delete an existing map first, so the SDK must rebuild rather than load") parser.add_argument("--edge-expand", type=float, default=None, help="override; > 1 pins it, otherwise the SDK resolves it from distortion") for key in ("out-width", "out-height", "source-width", "source-height"): parser.add_argument(f"--{key}", type=int, default=None) parser.add_argument("--flip-v", dest="flip_v", action="store_true", default=None) parser.add_argument("--no-flip-v", dest="flip_v", action="store_false") parser.add_argument("--source-scale", type=float, default=None) parser.add_argument("--edge-band", type=float, default=None) args = parser.parse_args(argv) export_dir: Path = args.export_dir params: dict = {} if args.profile is not None: block = params_from_profile_file(args.profile) try: params.update(load_uv_map_params({"uv_map": block}, source=str(args.profile))) except UvMapError as exc: raise SystemExit(f"ERROR: {exc}") from exc if args.params_from is not None: if not args.params_from.is_file(): raise SystemExit(f"no such map to take parameters from: {args.params_from}") recorded = params_from_map(args.params_from) # A disagreement is the interesting case: the profile is the source of truth, but a # map that was built with different numbers has to be visible, not resolved silently. disagree = { key: (recorded.get(key), params.get(key)) for key in params if key in recorded and recorded[key] != params[key] } if disagree: print(f"NOTE: {args.params_from.name} records different parameters than the profile " f"(map, profile): {disagree}") params.update({key: value for key, value in recorded.items() if key not in params}) overrides = { "out_width": args.out_width, "out_height": args.out_height, "flip_v": args.flip_v, "source_scale": args.source_scale, "edge_band": args.edge_band, "edge_expand": args.edge_expand, "source_width": args.source_width, "source_height": args.source_height, } params.update({key: value for key, value in overrides.items() if value is not None}) absent = [key for key in REQUIRED_KEYS if key not in params] if absent: raise SystemExit( f"no value for {absent}: pass --profile, --params-from an existing map, or give " "them on the command line. The importer never guesses what a source unit or an " "output size is." ) if "source_scale" not in params: print("note: no source_scale given; deriving it from the output grid and the " "export's own camera") out_dir: Path = args.output or export_dir try: target = build_uv_map(export_dir, params, out_dir, force=args.force) except UvMapError as exc: print(f"ERROR: {exc}", file=sys.stderr) return 2 print("\ncompare it with the shipped map:") print(map_compare_hint(target)) return 0 if __name__ == "__main__": raise SystemExit(main())