56 lines
3.9 KiB
JavaScript
56 lines
3.9 KiB
JavaScript
/* Local-only capture gates; no request, scoring, or report recommendation logic. */
|
|
(function(root, factory) {
|
|
if (typeof module === 'object' && module.exports) module.exports = factory(require('./head-pose.js'));
|
|
else root.CaptureQuality = factory(root.HeadPose);
|
|
})(typeof window !== 'undefined' ? window : this, function(HeadPose) {
|
|
'use strict';
|
|
// Real-phone feedback: ease alignment and normal hand/head movement while
|
|
// retaining two checked frames, one face, framing and final JPEG validation.
|
|
var config = Object.freeze({ version: '2026-10-15-angle-relaxed-20-v3', score: 0.4,
|
|
near: 0.30, far: 0.68, center: 0.16, edge: 0.01,
|
|
dark: 65, bright: 235, holdMs: 450, minFrames: 2, maxGapMs: 2000,
|
|
yawDrift: 8, pitchDrift: 8, centerDrift: 0.055, sizeDrift: 0.065 });
|
|
function finite(v) { return typeof v === 'number' && isFinite(v); }
|
|
function evaluate(d, options) {
|
|
var failed = function(hint, reason) { return { ok: false, hint: hint, reason: reason || 'quality' }; };
|
|
if (!d) return failed('未检测到人脸');
|
|
if (options.count !== 1) return failed('请保持单人出镜');
|
|
var box = d.detection.box, w = options.width, h = options.height;
|
|
if (!box || ![box.x, box.y, box.width, box.height, w, h].every(finite) || w <= 0 || h <= 0 || box.width <= 0 || box.height <= 0) return failed('请将脸部完整移入取景框');
|
|
var ratio = box.height / h;
|
|
if (!finite(d.detection.score) || d.detection.score < config.score) return failed('请保持面部清晰');
|
|
if (ratio < config.near) return failed('请靠近一点');
|
|
if (ratio > config.far) return failed('请远一点');
|
|
if (box.x < w * config.edge || box.y < h * config.edge || box.x + box.width > w * (1 - config.edge) || box.y + box.height > h * (1 - config.edge)) return failed('请将脸部完整移入取景框');
|
|
if (!finite(options.offsetX) || Math.abs(options.offsetX) > config.center) return failed('请将脸部移入取景框');
|
|
var pose;
|
|
try { pose = HeadPose.estimate(d.landmarks.positions, options.mirrored); } catch (_) { return failed('请正对屏幕'); }
|
|
if (!pose) return failed('请正对屏幕');
|
|
// System-camera files may already be mirrored. Retain the established
|
|
// direction-agnostic fallback, but apply the same angle/pitch/roll limits.
|
|
var target = options.target;
|
|
if (options.ignoreDirection && target !== 'front') target = pose.yaw < 0 ? 'left' : 'right';
|
|
var check = HeadPose.check(d.landmarks.positions, target, options.mirrored);
|
|
if (!check.ok) return failed(check.hint, finite(check.yaw) ? 'angle' : 'quality');
|
|
if (!finite(options.luma)) return failed('请保持面部清晰');
|
|
if (options.luma < config.dark) return failed('请到光线充足处');
|
|
if (options.luma > config.bright) return failed('光线过强,请避开强光');
|
|
return { ok: true, hint: '保持不动', observation: { yaw: pose.yaw, pitch: pose.pitch, roll: pose.roll,
|
|
score: d.detection.score, luma: options.luma, ratio: ratio,
|
|
center_x: (box.x + box.width / 2) / w, center_y: (box.y + box.height / 2) / h } };
|
|
}
|
|
function hold(state, observation, now) {
|
|
if (!observation) return { ready: false, state: null };
|
|
var reset = !state || now - state.lastAt > config.maxGapMs || now < state.lastAt;
|
|
if (!reset) {
|
|
var a = state.anchor, b = observation;
|
|
reset = Math.abs(a.yaw - b.yaw) > config.yawDrift || Math.abs(a.pitch - b.pitch) > config.pitchDrift ||
|
|
Math.abs(a.center_x - b.center_x) > config.centerDrift || Math.abs(a.center_y - b.center_y) > config.centerDrift || Math.abs(a.ratio - b.ratio) > config.sizeDrift;
|
|
}
|
|
var next = reset ? { since: now, lastAt: now, frames: 1, anchor: observation } :
|
|
{ since: state.since, lastAt: now, frames: state.frames + 1, anchor: state.anchor };
|
|
return { state: next, ready: now - next.since >= config.holdMs && next.frames >= config.minFrames };
|
|
}
|
|
return { config: config, evaluate: evaluate, hold: hold };
|
|
});
|