Files
juzi/taobao/utils/native-face-quality.js
T
xsl 774376fa16 feat: 完善淘宝小程序拍照、报告与登录流程
接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
2026-11-04 15:07:51 +08:00

62 lines
4.2 KiB
JavaScript

// The local detector returns source-frame pixels in dlib/face-api 68-point
// order. The caller supplies the lens-specific yaw mapping verified on phone.
// Reuse H5 framing and pose geometry without AR's 106-point map.
var CaptureQuality = require('../vendor/capture-quality');
var HeadPose = require('../vendor/head-pose');
// Tiny landmarks under-estimate some phone head turns. Relax only the native
// side-pose gate; shared H5 rules and all preceding quality gates stay intact.
var sidePoseConfig = Object.freeze({ yawMin: 12, yawMax: 100, pitchMax: 40, rollMax: 30 });
function finite(value) { return typeof value === 'number' && isFinite(value); }
function relaxedSideVerdict(detection, target, options, width, height) {
var pose;
try { pose = HeadPose.estimate(detection.landmarks.positions, options.mirrored === true); } catch (_) { return null; }
if (!pose) return null;
var yaw = pose.yaw * (target === 'left' ? -1 : 1);
if (yaw < sidePoseConfig.yawMin || yaw > sidePoseConfig.yawMax ||
Math.abs(pose.pitch) > sidePoseConfig.pitchMax || Math.abs(pose.roll) > sidePoseConfig.rollMax) return null;
var box = detection.detection.box, luma = finite(options.luma) ? options.luma : null;
return { ok: true, hint: '保持不动', warning: luma !== null && luma < CaptureQuality.config.dark ? '建议到光线明亮处拍摄' :
luma !== null && luma > CaptureQuality.config.bright ? '光线过强,建议避开强光' : '',
observation: { yaw: pose.yaw, pitch: pose.pitch, roll: pose.roll, score: detection.detection.score,
luma: luma, ratio: box.height / height, center_x: (box.x + box.width / 2) / width,
center_y: (box.y + box.height / 2) / height } };
}
function evaluate(result, target, options) {
options = options || {};
if (!result || !finite(result.width) || !finite(result.height) || result.width <= 0 || result.height <= 0 || !Array.isArray(result.detections)) {
return { ok: false, reason: 'frame', hint: '等待相机真实画面…', frameReceived: false, faceCount: 0, width: 0, height: 0 };
}
var width = result.width, height = result.height, first = result.detections[0], detection = null, offsetX = 0;
if (first) {
// Keep malformed detector responses fail-closed rather than throwing into
// the camera loop. Never modify the detector's original points or box.
detection = { detection: { box: first.detection && first.detection.box,
score: first.detection && first.detection.score },
landmarks: { positions: first.landmarks && first.landmarks.positions } };
var box = detection.detection.box;
if (box && finite(box.x) && finite(box.width)) {
// H5 faceOffsetX: object-fit:cover, centered crop, preview scale 1.
// Mirroring changes the sign, which does not change the centering gate.
var scale = finite(options.viewWidth) && options.viewWidth > 0 && finite(options.viewHeight) && options.viewHeight > 0
? Math.max(options.viewWidth / width, options.viewHeight / height) / options.viewWidth : 1 / width;
offsetX = (box.x + box.width / 2 - width / 2) * scale;
}
}
var luma = finite(options.luma) ? options.luma : finite(result.luma) ? result.luma : null;
var verdict = CaptureQuality.evaluate(detection, { count: result.detections.length, target: target,
width: width, height: height, mirrored: options.mirrored === true, offsetX: offsetX,
luma: luma });
// An angle failure means confidence, framing, centering and single-face
// checks already passed. Never relax failures from those earlier gates.
if (!verdict.ok && verdict.reason === 'angle' && (target === 'left' || target === 'right')) {
verdict = relaxedSideVerdict(detection, target, { mirrored: options.mirrored, luma: luma }, width, height) || verdict;
}
// Only quality metadata may cross setData. Do not expose native frames,
// detections, boxes, landmarks, or private image fields in the verdict.
verdict.frameReceived = true; verdict.faceCount = result.detections.length;
verdict.width = width; verdict.height = height;
return verdict;
}
module.exports = { evaluate: evaluate, hold: CaptureQuality.hold, config: CaptureQuality.config,
poseConfig: HeadPose.config, sidePoseConfig: sidePoseConfig, scoreThreshold: CaptureQuality.scoreThreshold };