From 7fc0210ce61032eb777ee80bb6f7c7bb251c8ade Mon Sep 17 00:00:00 2001 From: xsl Date: Tue, 21 Jul 2026 16:46:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(pose):=20=E4=BF=AE=E6=AD=A3=E6=AD=A3?= =?UTF-8?q?=E9=9D=A2=E7=85=A7=E8=A2=AB=E8=AF=AF=E5=88=A4=E4=B8=BA1003?= =?UTF-8?q?=EF=BC=88solvePnP=E7=BF=BB=E8=BD=AC=E8=A7=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ITERATIVE 偶发收敛到相机后方(tz<0),roll≈±180° 超阈值, 把正面照误判为非正面。检测到负深度时回退 SQPNP 重解正深度解。 补充回归测试。 Co-authored-by: Cursor --- face_analysis/pose.py | 11 ++++++++++- tests/test_pose.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/face_analysis/pose.py b/face_analysis/pose.py index 423af00..57d385c 100644 --- a/face_analysis/pose.py +++ b/face_analysis/pose.py @@ -50,12 +50,21 @@ def estimate_head_pose(landmarks, image_width, image_height): [0, 0, 1]], dtype=np.float64) dist = np.zeros((4, 1)) # 假设无畸变 - success, rvec, _tvec = cv2.solvePnP( + success, rvec, tvec = cv2.solvePnP( _MODEL_POINTS, image_points, cam_matrix, dist, flags=cv2.SOLVEPNP_ITERATIVE, ) if not success: 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) # 在「相机坐标系」(x右 y下 z内) 下抽取 Tait-Bryan 欧拉角,物理含义对齐: # yaw = 绕 Y(竖轴)转 → 左右扭头 diff --git a/tests/test_pose.py b/tests/test_pose.py index 11b5695..663ac9e 100644 --- a/tests/test_pose.py +++ b/tests/test_pose.py @@ -62,3 +62,41 @@ def test_threshold_gating_rejects_when_zeroed(): 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