ITERATIVE 偶发收敛到相机后方(tz<0),roll≈±180° 超阈值, 把正面照误判为非正面。检测到负深度时回退 SQPNP 重解正深度解。 补充回归测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
3.6 KiB
Python
103 lines
3.6 KiB
Python
"""姿态校验测试:真实正面图通过 + 合成大 yaw 拒绝 + 阈值门控。"""
|
|
import cv2
|
|
import numpy as np
|
|
from conftest import fixture, _LM
|
|
|
|
from face_analysis import pose
|
|
from face_analysis.detector import detector
|
|
from face_analysis.pose import (
|
|
estimate_head_pose, check_frontal_face, _MODEL_POINTS,
|
|
)
|
|
from face_analysis.face_mesh_landmarks import PNP_INDICES
|
|
|
|
|
|
def _project_model_with_yaw(yaw_deg, W=1000, H=1000, tz=1000.0):
|
|
"""把 _MODEL_POINTS 绕 Y 轴旋转 yaw 后投影回像素,构造伪 landmarks。
|
|
|
|
与 pose.estimate_head_pose 使用同一相机模型,故应能近似反解出该 yaw。
|
|
"""
|
|
a = np.radians(yaw_deg)
|
|
Ry = np.array([[np.cos(a), 0, np.sin(a)],
|
|
[0, 1, 0],
|
|
[-np.sin(a), 0, np.cos(a)]])
|
|
focal = float(W)
|
|
cx, cy = W / 2, H / 2
|
|
lm = [_LM(0.5, 0.5) for _ in range(478)]
|
|
for idx, X in zip(PNP_INDICES, _MODEL_POINTS):
|
|
Xc = Ry @ X + np.array([0, 0, tz])
|
|
u = focal * Xc[0] / Xc[2] + cx
|
|
v = focal * Xc[1] / Xc[2] + cy
|
|
lm[idx] = _LM(u / W, v / H)
|
|
|
|
class _Holder:
|
|
landmark = lm
|
|
return _Holder()
|
|
|
|
|
|
def test_frontal_image_passes():
|
|
img = cv2.imread(fixture("frontal.jpg"))
|
|
h, w = img.shape[:2]
|
|
lms = detector.detect(img)
|
|
assert lms is not None
|
|
assert check_frontal_face(lms, w, h) is True
|
|
|
|
|
|
def test_synthetic_large_yaw_recovered_and_rejected():
|
|
holder = _project_model_with_yaw(40.0)
|
|
yaw, pitch, roll = estimate_head_pose(holder, 1000, 1000)
|
|
# 反解出的 yaw 量级应接近 40°(符号取决于约定)
|
|
assert abs(yaw) > 30
|
|
# 默认阈值(30°)下应判为非正面
|
|
assert check_frontal_face(holder, 1000, 1000) is False
|
|
|
|
|
|
def test_threshold_gating_rejects_when_zeroed():
|
|
"""阈值门控逻辑:阈值压到 0,则任何非零角度都应被拒。"""
|
|
img = cv2.imread(fixture("frontal.jpg"))
|
|
h, w = img.shape[:2]
|
|
lms = detector.detect(img)
|
|
assert check_frontal_face(lms, w, h, yaw_thr=0, pitch_thr=0, roll_thr=0) is False
|
|
|
|
|
|
def test_pose_none_is_not_blocked():
|
|
"""solvePnP 失败(返回 None)时不拦截,check_frontal_face 返回 True。"""
|
|
assert pose.estimate_head_pose.__doc__ # 占位,确保导入
|
|
|
|
|
|
def test_iterative_flipped_solution_falls_back_to_sqpnp():
|
|
"""回归:部分正面照上 ITERATIVE 会解出 tz<0、roll≈±180°,应回退 SQPNP。
|
|
|
|
像素点取自一张真实正面短发照(720×945);裸跑 ITERATIVE 会得到负深度。
|
|
"""
|
|
W, H = 720, 945
|
|
# 鼻尖 / 下巴 / 左眼外 / 右眼外 / 左嘴角 / 右嘴角(像素)
|
|
px = [
|
|
(358.32715988, 600.98652095),
|
|
(346.83344364, 779.63507116),
|
|
(242.94779778, 466.76155195),
|
|
(478.43703747, 480.10321766),
|
|
(284.13277388, 679.95527387),
|
|
(422.19510555, 684.31899190),
|
|
]
|
|
lm = [_LM(0.5, 0.5) for _ in range(478)]
|
|
for idx, (u, v) in zip(PNP_INDICES, px):
|
|
lm[idx] = _LM(u / W, v / H)
|
|
|
|
class _Holder:
|
|
landmark = lm
|
|
|
|
holder = _Holder()
|
|
# 确认裸 ITERATIVE 确实是翻转解(否则本回归失去意义)
|
|
image_points = np.array(px, dtype=np.float64)
|
|
cam = np.array([[float(W), 0, W / 2], [0, float(W), H / 2], [0, 0, 1]],
|
|
dtype=np.float64)
|
|
ok, rvec, tvec = cv2.solvePnP(
|
|
_MODEL_POINTS, image_points, cam, np.zeros((4, 1)),
|
|
flags=cv2.SOLVEPNP_ITERATIVE,
|
|
)
|
|
assert ok and float(tvec[2, 0]) < 0
|
|
|
|
yaw, pitch, roll = estimate_head_pose(holder, W, H)
|
|
assert abs(roll) < 30, f"roll 应被纠正,实际 roll={roll}"
|
|
assert check_frontal_face(holder, W, H) is True
|