Files
2026-09-25 17:10:05 +08:00

175 lines
7.6 KiB
Python

#!/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())