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

171 lines
11 KiB
JavaScript

'use strict';
const { test } = require('node:test'), assert = require('node:assert/strict');
const quality = require('../utils/native-face-quality');
const h5 = require('../../h5/js/capture-quality');
// A complete synthetic dlib68 face: jaw 0..16, brows 17..26, nose 27..35,
// eyes 36..47, mouth 48..67. These are original 720x1280-frame coordinates,
// not normalized model inputs or AR106 landmarks. No real face data is used.
function face(noseShift = 0) {
const positions = [];
for (let i = 0; i < 17; i++) {
const angle = Math.PI - i * Math.PI / 16;
positions.push({ x: 360 + Math.cos(angle) * 165, y: 470 + Math.sin(angle) * 360 });
}
for (let i = 0; i < 10; i++) positions.push({ x: 245 + (i < 5 ? i : i + 2) * 18, y: 390 - Math.sin((i % 5) * Math.PI / 4) * 12 });
for (let i = 0; i < 9; i++) positions.push({ x: 360 + noseShift + (i < 4 ? 0 : (i - 6) * 12), y: 465 + i * 18 });
for (const center of [290, 430]) for (let i = 0; i < 6; i++) {
const angle = Math.PI - i * Math.PI / 3;
positions.push({ x: center + Math.cos(angle) * 30, y: 450 - Math.sin(angle) * 13 });
}
for (let i = 0; i < 20; i++) {
const angle = i * 2 * Math.PI / (i < 12 ? 12 : 8);
positions.push({ x: 360 + Math.cos(angle) * (i < 12 ? 62 : 32), y: 705 + Math.sin(angle) * (i < 12 ? 24 : 10) });
}
// Landmark semantics used by H5: eye outer corners, brow peaks, nose tip /
// base, chin. The neutral pitch ratio is (618-420)/(830-390)=0.45.
for (const [index, x, y] of [[36,260,450],[45,460,450],[19,285,390],[24,435,390],
[8,360,830],[30,360+noseShift,530],[33,360+noseShift,618]]) positions[index] = { x, y };
return { detection: { box: { x: 180, y: 250, width: 360, height: 650 }, score: .9 }, landmarks: { positions } };
}
function frame(noseShift = 0) { return { width: 720, height: 1280, elapsedMs: 71, detections: [face(noseShift)] }; }
test('complete dlib68 model output uses the H5 semantic points and never returns image geometry', () => {
const input = frame(), original = JSON.stringify(input), verdict = quality.evaluate(input, 'front');
assert.equal(input.detections[0].landmarks.positions.length, 68);
assert.equal(verdict.ok, true); assert.equal(verdict.observation.yaw, 0);
assert(Math.abs(verdict.observation.pitch) < 1e-10); assert.equal(verdict.observation.roll, 0);
assert.equal(verdict.faceCount, 1); assert.equal(verdict.frameReceived, true);
assert.equal(verdict.width, 720); assert.equal(verdict.height, 1280);
assert.equal(JSON.stringify(input), original);
assert(!/detections|landmarks|positions|capturedImage|anchors|"box"/.test(JSON.stringify(verdict)));
// A 68-point input must not be accidentally remapped through AR106 indices.
const invalidEye = frame(); invalidEye.detections[0].landmarks.positions[36].x = NaN;
assert.equal(quality.evaluate(invalidEye, 'front').ok, false);
});
test('front, right and left use the unchanged physical head-turn semantics', () => {
for (const [shift, target, sign] of [[0, 'front', 0], [35.4, 'right', 1], [-35.4, 'left', -1]]) {
const verdict = quality.evaluate(frame(shift), target);
assert.equal(verdict.ok, true, target);
if (sign) assert(verdict.observation.yaw * sign > 40 && verdict.observation.yaw * sign < 50);
}
assert.equal(quality.evaluate(frame(35.4), 'left').hint, '请向右转头');
assert.equal(quality.evaluate(frame(-35.4), 'right').hint, '请向左转头');
assert.equal(quality.evaluate(frame(35.4), 'front').hint, '请正对屏幕');
assert.equal(quality.evaluate(frame(), 'right').ok, false, 'frontal face cannot pass a side step');
});
test('mirroring is explicit and defaults to unmirrored raw-frame points', () => {
assert.equal(quality.evaluate(frame(35.4), 'right').ok, true);
assert.equal(quality.evaluate(frame(35.4), 'right', { mirrored: false }).ok, true);
const mirrored = quality.evaluate(frame(35.4), 'left', { mirrored: true });
assert.equal(mirrored.ok, true); assert(mirrored.observation.yaw < -40);
assert.equal(quality.evaluate(frame(35.4), 'right', { mirrored: true }).ok, false);
assert.equal(quality.evaluate(frame(), 'front', { mirrored: true }).ok, true);
});
test('native side steps accept a small correct turn while H5 retains its original angle gate', () => {
for (const [shift, target] of [[12, 'right'], [-12, 'left']]) {
const input = frame(shift), verdict = quality.evaluate(input, target);
assert.equal(verdict.ok, true);
assert(Math.abs(verdict.observation.yaw) >= 12 && Math.abs(verdict.observation.yaw) < 24);
assert.equal(h5.evaluate(input.detections[0], { count: 1, target, width: 720, height: 1280, offsetX: 0 }).ok, false);
assert.equal(quality.evaluate(input, target === 'left' ? 'right' : 'left').ok, false);
assert.equal(quality.evaluate(input, target, { mirrored: true }).ok, false);
}
assert.equal(quality.evaluate(frame(), 'right').ok, false);
assert.equal(quality.evaluate(frame(5), 'right').ok, false);
assert.equal(quality.evaluate(frame(), 'left').ok, false);
});
test('relaxed side angles retain single-face, framing, confidence and excessive-tilt rejection', () => {
const pitched = frame(12); pitched.detections[0].landmarks.positions[33].y += 80;
assert.equal(quality.evaluate(pitched, 'right').ok, true, 'about 33 degree pitch is tolerated on side steps');
pitched.detections[0].landmarks.positions[33].y += 40;
assert.equal(quality.evaluate(pitched, 'right').ok, false);
const tilted = frame(12); tilted.detections[0].landmarks.positions[45].y += 95;
assert.equal(quality.evaluate(tilted, 'right').ok, true, 'about 25 degree roll is tolerated on side steps');
tilted.detections[0].landmarks.positions[45].y += 45;
assert.equal(quality.evaluate(tilted, 'right').ok, false);
const multiple = frame(12); multiple.detections.push(face());
assert.equal(quality.evaluate(multiple, 'right').hint, '请保持单人出镜');
const edge = frame(12); edge.detections[0].detection.box.x = 0;
assert.equal(quality.evaluate(edge, 'right').hint, '请将脸部完整移入取景框');
const blurry = frame(12); blurry.detections[0].detection.score = .27;
assert.equal(quality.evaluate(blurry, 'right').hint, '请保持面部清晰');
});
test('missing frames and completed no-face results remain distinguishable and fail closed', () => {
for (const invalid of [null, {}, { ...frame(), width: NaN }, { ...frame(), height: 0 }, { ...frame(), detections: null }]) {
const verdict = quality.evaluate(invalid, 'front');
assert.equal(verdict.frameReceived, false); assert.equal(verdict.reason, 'frame'); assert.equal(verdict.ok, false);
assert.equal(verdict.faceCount, 0); assert.equal(verdict.width, 0); assert.equal(verdict.height, 0);
}
const empty = quality.evaluate({ ...frame(), detections: [] }, 'front');
assert.equal(empty.frameReceived, true); assert.equal(empty.faceCount, 0); assert.equal(empty.hint, '未检测到人脸');
assert.equal(quality.evaluate({ ...frame(), detections: [{}] }, 'front').ok, false);
});
test('multiple people, distance, confidence and raw-frame edges retain H5 priorities', () => {
const multiple = frame(); multiple.detections.push(face());
assert.equal(quality.evaluate(multiple, 'front').hint, '请保持单人出镜');
const distant = frame(); distant.detections[0].detection.box.height = 350;
assert.equal(quality.evaluate(distant, 'front').hint, '请靠近一点');
const close = frame(); close.detections[0].detection.box.height = 880;
assert.equal(quality.evaluate(close, 'front').hint, '请远一点');
const lowConfidence = frame(); lowConfidence.detections[0].detection.score = .24;
assert.equal(quality.evaluate(lowConfidence, 'front').hint, '请保持面部清晰');
const frontOnlyConfidence = frame(); frontOnlyConfidence.detections[0].detection.score = .27;
assert.equal(quality.evaluate(frontOnlyConfidence, 'front').ok, true);
assert.equal(quality.evaluate(frontOnlyConfidence, 'right').hint, '请保持面部清晰');
for (const box of [{ x: 0 }, { y: 0 }, { x: 365 }, { y: 650 }]) {
const edge = frame(); Object.assign(edge.detections[0].detection.box, box);
assert.equal(quality.evaluate(edge, 'front').hint, '请将脸部完整移入取景框');
}
});
test('centering uses the visible object-fit cover crop rather than raw-frame width alone', () => {
const input = frame(); input.detections[0].detection.box.x += 100;
assert.equal(quality.evaluate(input, 'front').ok, true, '100/720 is inside the raw-width centering margin');
assert.equal(quality.evaluate(input, 'front', { viewWidth: 200, viewHeight: 1000 }).hint, '请将脸部移入取景框');
assert.equal(quality.evaluate(input, 'front', { viewWidth: 200, viewHeight: 1000, mirrored: true }).ok, false);
assert.equal(quality.evaluate(frame(), 'front', { viewWidth: 200, viewHeight: 1000 }).ok, true);
assert.equal(quality.evaluate(input, 'front', { viewWidth: 0, viewHeight: NaN }).ok, true, 'missing viewport falls back to the frame');
});
test('pitch and roll failures use actual nose-base and eye-outer points', () => {
const down = frame(); down.detections[0].landmarks.positions[33].y = 490;
assert.equal(quality.evaluate(down, 'front').hint, '请正对屏幕');
const tilted = frame(); tilted.detections[0].landmarks.positions[45].y += 100;
assert.equal(quality.evaluate(tilted, 'front').hint, '请勿歪头');
const malformed = frame(); malformed.detections[0].landmarks.positions = [];
assert.equal(quality.evaluate(malformed, 'front').ok, false);
});
test('measured face brightness reaches the H5 warning rules on front and relaxed side steps', () => {
for (const [value, warning] of [[50, '建议到光线明亮处拍摄'], [240, '光线过强,建议避开强光'], [160, '']]) {
for (const [shift, target] of [[0, 'front'], [12, 'right']]) {
const verdict = quality.evaluate({ ...frame(shift), luma: value }, target);
assert.equal(verdict.ok, true); assert.equal(verdict.warning, warning); assert.equal(verdict.observation.luma, value);
}
}
const unavailable = quality.evaluate({ ...frame(), luma: NaN }, 'front');
assert.equal(unavailable.ok, true); assert.equal(unavailable.warning, ''); assert.equal(unavailable.observation.luma, null);
});
test('lighting stays advisory and stable holding directly uses the H5 implementation', () => {
const dim = quality.evaluate(frame(), 'front', { luma: 50 });
assert.equal(dim.ok, true); assert.equal(dim.warning, '建议到光线明亮处拍摄'); assert.equal(dim.observation.luma, 50);
assert.equal(quality.evaluate(frame(), 'front', { luma: 240 }).warning, '光线过强,建议避开强光');
assert.equal(quality.evaluate(frame(), 'front', { luma: NaN }).observation.luma, null);
assert.equal(quality.hold, require('../vendor/capture-quality').hold);
assert.deepEqual(quality.config, h5.config);
let held = quality.hold(null, dim.observation, 1000);
assert.equal(held.ready, false);
held = quality.hold(held.state, dim.observation, 1449); assert.equal(held.ready, false);
held = quality.hold(held.state, dim.observation, 1450); assert.equal(held.ready, true);
assert.equal(quality.hold(held.state, null, 1500).state, null);
assert.equal(quality.hold(held.state, dim.observation, 4000).ready, false);
});