175 lines
7.6 KiB
Python
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())
|