Silhouette IoU is invariant to which face of the shoe points at the camera, so the optimizer drifted from the upright init into sole-facing (up-axis tilted ~90deg) or upside-down (~180deg) poses that matched the mask outline equally well. Add an upright prior to solve_pose's loss: upright_weight * (1 - cos angle between the model up axis and gravity-derived world up). up_world is threaded from calibrate(); exposed as the 'upright_strength' node input (default 1.0). Validated: from a pitch=70 start, weight 0 leaves up.worldup=0.17 (sole-facing), weight 1.5 -> 0.99 (sole-down). Adds up_axis_world/upright_penalty helpers (closed-form, pyrender-free) with unit + solver regression tests.
93 lines
3.7 KiB
Python
93 lines
3.7 KiB
Python
import os
|
|
|
|
os.environ.setdefault("PYOPENGL_PLATFORM", "egl")
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import trimesh
|
|
|
|
pyrender = pytest.importorskip("pyrender")
|
|
pytest.importorskip("scipy")
|
|
|
|
import numpy as np
|
|
|
|
from cxn.pose_solve import render_silhouette, solve_pose, silhouette_iou, up_axis_world
|
|
|
|
|
|
def _egl_ok():
|
|
try:
|
|
r = pyrender.OffscreenRenderer(8, 8)
|
|
r.delete()
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
pytestmark = pytest.mark.skipif(not _egl_ok(), reason="EGL/pyrender offscreen unavailable")
|
|
|
|
|
|
def test_solver_recovers_known_pose_from_silhouette():
|
|
# Use an asymmetric mesh so the silhouette constrains rotation.
|
|
scene = trimesh.Scene(trimesh.creation.box(extents=(0.5, 0.2, 0.3)))
|
|
size = 96
|
|
truth = dict(pitch=0.0, yaw=20.0, roll=0.0, cam_x=0.0, cam_y=0.0, cam_z=2.0)
|
|
target = render_silhouette(
|
|
scene, truth["pitch"], truth["yaw"], truth["roll"],
|
|
(truth["cam_x"], truth["cam_y"], truth["cam_z"]), 45.0, size, size,
|
|
)
|
|
init = dict(pitch=5.0, yaw=5.0, roll=0.0, cam_x=0.0, cam_y=0.0, cam_z=2.2)
|
|
out = solve_pose(scene, target, init, iterations=300, render_size=size)
|
|
rec = render_silhouette(
|
|
scene, out["pitch"], out["yaw"], out["roll"],
|
|
(out["cam_x"], out["cam_y"], out["cam_z"]), 45.0, size, size,
|
|
)
|
|
assert silhouette_iou(rec, target) > 0.9
|
|
|
|
|
|
def test_solver_respects_fov_and_aspect():
|
|
# Regression for the fixed-fov(45)/square-aspect bug: solve_pose must fit in
|
|
# the SAME vertical fov and aspect ratio that the output will be rendered
|
|
# with. Starting AT the true pose, the correct fov keeps a near-perfect fit,
|
|
# while assuming the wrong fov pulls the fit measurably away.
|
|
scene = trimesh.Scene(trimesh.creation.box(extents=(0.5, 0.2, 0.3)))
|
|
w, h, fov = 160, 120, 62.0 # non-square, non-45 vertical fov
|
|
truth = dict(pitch=3.0, yaw=18.0, roll=0.0, cam_x=0.05, cam_y=0.0, cam_z=2.0)
|
|
target = render_silhouette(
|
|
scene, truth["pitch"], truth["yaw"], truth["roll"],
|
|
(truth["cam_x"], truth["cam_y"], truth["cam_z"]), fov, w, h,
|
|
)
|
|
|
|
def fit_iou(fov_solve):
|
|
# render_size == max image side => the solver fits at full resolution,
|
|
# so the fit space matches the evaluation space (no resize quantization).
|
|
out = solve_pose(scene, target, truth, fov_deg=fov_solve, iterations=120, render_size=160)
|
|
rec = render_silhouette(
|
|
scene, out["pitch"], out["yaw"], out["roll"],
|
|
(out["cam_x"], out["cam_y"], out["cam_z"]), fov, w, h,
|
|
)
|
|
return silhouette_iou(rec, target)
|
|
|
|
iou_correct = fit_iou(fov)
|
|
iou_wrong = fit_iou(45.0)
|
|
assert iou_correct > 0.95 # correct fov + aspect keeps the true pose
|
|
assert iou_correct > iou_wrong + 0.02 # the wrong fov assumption is worse
|
|
|
|
|
|
def test_upright_prior_keeps_shoe_sole_down():
|
|
# Regression for the sole-facing / upside-down failures: starting from a
|
|
# tilted pose, the gravity upright prior must pull the model back to
|
|
# sole-down, whereas silhouette IoU alone leaves it tilted.
|
|
scene = trimesh.Scene(trimesh.creation.box(extents=(0.5, 0.18, 0.28)))
|
|
fov, size = 50.0, 96
|
|
up = np.array([0.0, 1.0, 0.0])
|
|
target = render_silhouette(scene, 0, 25, 0, (0.0, 0.0, 2.0), fov, size, size)
|
|
tilted_init = dict(pitch=70.0, yaw=10.0, roll=0.0, cam_x=0.0, cam_y=0.0, cam_z=2.4)
|
|
|
|
def dot_up(weight):
|
|
out = solve_pose(scene, target, tilted_init, fov_deg=fov, up_world=up,
|
|
upright_weight=weight, iterations=250, render_size=size)
|
|
return float(np.dot(up_axis_world(out["pitch"], out["yaw"], out["roll"]), up))
|
|
|
|
assert dot_up(0.0) < 0.6 # no prior: stays tilted (sole-facing)
|
|
assert dot_up(1.5) > 0.9 # prior: sole-down
|