fix(pose): 修正正面照被误判为1003(solvePnP翻转解)

ITERATIVE 偶发收敛到相机后方(tz<0),roll≈±180° 超阈值,
把正面照误判为非正面。检测到负深度时回退 SQPNP 重解正深度解。
补充回归测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
xsl
2026-07-21 16:46:05 +08:00
co-authored by Cursor
parent 6d9e92f710
commit d4aae9722c
2 changed files with 48 additions and 1 deletions
+10 -1
View File
@@ -50,12 +50,21 @@ def estimate_head_pose(landmarks, image_width, image_height):
[0, 0, 1]], dtype=np.float64) [0, 0, 1]], dtype=np.float64)
dist = np.zeros((4, 1)) # 假设无畸变 dist = np.zeros((4, 1)) # 假设无畸变
success, rvec, _tvec = cv2.solvePnP( success, rvec, tvec = cv2.solvePnP(
_MODEL_POINTS, image_points, cam_matrix, dist, _MODEL_POINTS, image_points, cam_matrix, dist,
flags=cv2.SOLVEPNP_ITERATIVE, flags=cv2.SOLVEPNP_ITERATIVE,
) )
if not success: if not success:
return None return None
# ITERATIVE 偶发收敛到相机后方的翻转解(tz<0),此时 roll 落在 ±180° 附近,
# 会把真正的正面照误判为 1003。改用 SQPNP 重解正深度解。
if float(tvec[2, 0]) < 0:
ok2, rvec2, tvec2 = cv2.solvePnP(
_MODEL_POINTS, image_points, cam_matrix, dist,
flags=cv2.SOLVEPNP_SQPNP,
)
if ok2 and float(tvec2[2, 0]) > 0:
rvec = rvec2
rot, _ = cv2.Rodrigues(rvec) rot, _ = cv2.Rodrigues(rvec)
# 在「相机坐标系」(x右 y下 z内) 下抽取 Tait-Bryan 欧拉角,物理含义对齐: # 在「相机坐标系」(x右 y下 z内) 下抽取 Tait-Bryan 欧拉角,物理含义对齐:
# yaw = 绕 Y(竖轴)转 → 左右扭头 # yaw = 绕 Y(竖轴)转 → 左右扭头
+38
View File
@@ -62,3 +62,41 @@ def test_threshold_gating_rejects_when_zeroed():
def test_pose_none_is_not_blocked(): def test_pose_none_is_not_blocked():
"""solvePnP 失败(返回 None)时不拦截,check_frontal_face 返回 True。""" """solvePnP 失败(返回 None)时不拦截,check_frontal_face 返回 True。"""
assert pose.estimate_head_pose.__doc__ # 占位,确保导入 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