fix: 拍照引导检测链路校准——模拟摄像头闭环修复四个实测问题

用户真机症状:一直『未检测到人脸』;脸进框却提示太远;凑近又超框。用 C3 三视图生成 Y4M 模拟摄像头闭环复现并定位:

- 检测可靠性:tinyFaceDetector 320/0.4 → 416/0.3(正脸置信度 0.67→0.99,框更准);连续漏检 ≥3 帧才报未检测到人脸(单帧抖动不再刷屏)
- 距离阈值:0.35/0.8 → 0.26/0.58(以取景框≈42% 屏高、416 输入实测框 0.40–0.44 标定;原阈值下脸进框即误判太远)
- HeadPose:check() 双重镜像符号 bug(estimate 已翻转 yaw,check 又翻一次→侧脸引导方向相反);YAW_COEFF 2.2→3.8(±45° 侧脸实测只读 ±25°);PITCH_BASELINE 0.32→0.45(正脸误读 29° 被判低头)
- head-pose 自测夹具按新系数数值标定,全部通过
- e2e 新增 E7:模拟摄像头全自动三连拍(正脸→右脸→左脸,姿态引导+倒计时+抓帧,分析请求拦截不烧次数包),全量绿
This commit is contained in:
Ubuntu
2026-09-27 00:47:22 +08:00
parent f52b52a954
commit eae22d18a1
5 changed files with 39 additions and 28 deletions
+22 -21
View File
@@ -52,13 +52,13 @@
CHIN: 8
};
// ---- 经验系数(⚠ 真机需校准) ----
// ---- 经验系数(2026-09-27 用 C3 三视图(+45°侧脸)模拟摄像头校准) ----
// yaw = atan((dLeft-dRight)/(dLeft+dRight)) × YAW_COEFF。
// atan 输出 ∈ (-45°,45°),取 2.2 使真实 ~45° 侧脸读数落在 40° 上下。
var YAW_COEFF = 2.2;
// pitch:鼻底相对「眉眼连线中点」的纵向偏移 / 脸高(眉 19/24 — 下巴 8),
// 校准:±45° 实拍侧脸读数 ±25°(2.2 偏小)→ 3.8 使读数落在 44° 上下;正脸 ~4°,仍 <12° 阈值
var YAW_COEFF = 3.8;
// pitch:鼻底相对「眉眼连线中点」的纵向偏移 / 脸高(眉 19/24 — 下巴 8),
// 减去中性正脸基线后线性映射为角度。
var PITCH_BASELINE = 0.32; // 正脸时 offset/faceHeight 的典型值
var PITCH_BASELINE = 0.45; // 校准:0.32 时正脸误读 ~29°(被误判低头),实测基线 0.48,留 0.03 余量
var PITCH_COEFF = 180; // 归一化偏移 → 角度 的增益
// ---- check 阈值(角度制) ----
@@ -170,8 +170,9 @@
}
if (!e) return { ok: false, hint: HINT_FRONT }; // 退化特征点(重合/脸高过小 → NaN/Infinity)
// 语义 yaw:>0 = 被摄者向自己的左侧转头,与预览是否镜像无关
var sYaw = mirrored ? -e.yaw : e.yaw;
// 语义 yaw:>0 = 被摄者向自己的左侧转头。estimate(mirrored) 内部已按镜像翻转过符号,
// 这里直接用 e.yaw(此前再翻一次是双重镜像 bug:侧脸引导方向完全相反)
var sYaw = e.yaw;
var pitch = e.pitch;
if (target === 'front') {
@@ -247,15 +248,15 @@ if (typeof module !== 'undefined' && typeof require !== 'undefined' && require.m
pts[24] = { x: 230, y: 70 };
pts[8] = { x: 200, y: 240 }; // 下巴 → 脸高 240-70=170
pts[30] = { x: 200, y: 140 }; // 鼻尖居中
pts[33] = { x: 200, y: 139.4 }; // (139.4-85)/170 = 0.32 = PITCH_BASELINE
pts[33] = { x: 200, y: 161.5 }; // (161.5-85)/170 = 0.45 = PITCH_BASELINE
return pts;
}
// 把鼻点横向偏移,模拟 ~45° 侧脸(向画面右侧偏 = 被摄者向左转)
// 把鼻点横向偏移 17.7px,模拟 ~45° 侧脸(向画面右侧偏 = 被摄者向左转;偏移量按 YAW_COEFF=3.8 数值标定)
function makeSide() {
var pts = makeFrontal();
pts[30] = { x: 235, y: 140 };
pts[33] = { x: 235, y: 139.4 };
pts[30] = { x: 217.7, y: 140 };
pts[33] = { x: 217.7, y: 161.5 };
return pts;
}
@@ -279,16 +280,16 @@ if (typeof module !== 'undefined' && typeof require !== 'undefined' && require.m
ok(rc.ok === true, '侧脸(被摄者左转)target=right ok');
ok(lc.ok === false && lc.hint === '请向右转头', '同侧脸 target=left 拒绝并引导向右');
// 5) mirrored=true 翻转 yaw 符号,但 check 语义不变
// 5) mirrored=true 翻转 yaw 符号,check 语义随之翻转(同一点集在镜像预览下对应相反的物理转头)
var sideM = HeadPose.estimate(makeSide(), true);
ok(Math.abs(sideM.yaw + side.yaw) < 0.001, 'mirrored=true 时 yaw 符号翻转');
ok(HeadPose.check(makeSide(), 'right', true).ok === true, 'mirrored 下 check(right) 仍 ok');
ok(HeadPose.check(makeSide(), 'left', true).ok === false, 'mirrored 下 check(left) 仍拒绝');
ok(HeadPose.check(makeSide(), 'right', true).ok === false, 'mirrored 语义翻转:check(right) 拒绝');
ok(HeadPose.check(makeSide(), 'left', true).ok === true, 'mirrored 语义翻转:check(left) 通过');
// 6) 反方向侧脸:鼻尖偏画面左 → target 'left' 通过
// 6) 反方向侧脸:鼻尖偏画面左 → target 'left' 通过
var back = makeSide();
back[30] = { x: 165, y: 140 };
back[33] = { x: 165, y: 139.4 };
back[30] = { x: 182.3, y: 140 };
back[33] = { x: 182.3, y: 161.5 };
var bc = HeadPose.check(back, 'left', false);
ok(bc.ok === true, '反向侧脸 target=left ok');
ok(HeadPose.check(back, 'right', false).ok === false, '反向侧脸 target=right 拒绝');
@@ -330,15 +331,15 @@ if (typeof module !== 'undefined' && typeof require !== 'undefined' && require.m
// 13) 正脸阈值收紧到 |yaw|≤12°(Face++ 口径 <10° + 2° 余量):yaw≈13.7°(旧阈值 15 内)应拒绝
ok(HeadPose.config.FRONT_YAW_MAX === 12, 'config.FRONT_YAW_MAX = 12');
var slight = makeFrontal();
slight[30] = { x: 209, y: 140 };
slight[33] = { x: 209, y: 139.4 };
slight[30] = { x: 205.18, y: 140 }; // 偏移 5.18 → yaw≈13.7°(YAW_COEFF=3.8 标定)
slight[33] = { x: 205.18, y: 161.5 };
var se = HeadPose.estimate(slight, false);
ok(se.yaw > 12 && se.yaw < 15, '轻微偏头 yaw∈(12,15) (got ' + se.yaw.toFixed(2) + ')');
var sc2 = HeadPose.check(slight, 'front', false);
ok(sc2.ok === false, 'yaw≈13.7° front 拒绝(旧阈值 15 会放行)');
var slight2 = makeFrontal();
slight2[30] = { x: 207, y: 140 };
slight2[33] = { x: 207, y: 139.4 };
slight2[30] = { x: 204.04, y: 140 }; // 偏移 4.04 → yaw≈10.7°
slight2[33] = { x: 204.04, y: 161.5 };
ok(HeadPose.check(slight2, 'front', false).ok === true, 'yaw≈10.7°(≤12°)front 通过');
if (failures > 0) {