@sssxyd/face-liveness-detector 0.4.1-beta.7 → 0.4.1-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.js +384 -96
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +384 -96
- package/dist/index.js.map +1 -1
- package/dist/types/face-detection-engine.d.ts.map +1 -1
- package/dist/types/motion-liveness-detector.d.ts +111 -24
- package/dist/types/motion-liveness-detector.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1633,8 +1633,11 @@ class MotionLivenessDetector {
|
|
|
1633
1633
|
getOptions() {
|
|
1634
1634
|
return this.config;
|
|
1635
1635
|
}
|
|
1636
|
-
|
|
1637
|
-
return this.normalizedLandmarksHistory.length >= 5;
|
|
1636
|
+
collectedMinFrames() {
|
|
1637
|
+
return this.normalizedLandmarksHistory.length >= 5;
|
|
1638
|
+
}
|
|
1639
|
+
collectedFullFrames() {
|
|
1640
|
+
return this.normalizedLandmarksHistory.length >= this.config.frameBufferSize;
|
|
1638
1641
|
}
|
|
1639
1642
|
reset() {
|
|
1640
1643
|
this.eyeAspectRatioHistory = [];
|
|
@@ -1682,7 +1685,7 @@ class MotionLivenessDetector {
|
|
|
1682
1685
|
});
|
|
1683
1686
|
}
|
|
1684
1687
|
// 数据不足时,继续收集
|
|
1685
|
-
if (!this.
|
|
1688
|
+
if (!this.collectedMinFrames()) {
|
|
1686
1689
|
return this.createEmptyResult({
|
|
1687
1690
|
reason: '数据收集中,帧数不足',
|
|
1688
1691
|
collectedFrames: this.normalizedLandmarksHistory.length
|
|
@@ -2518,46 +2521,102 @@ class MotionLivenessDetector {
|
|
|
2518
2521
|
// 更多的点对会给出更准确的单应性矩阵估计
|
|
2519
2522
|
const errors = [];
|
|
2520
2523
|
const homographyMatrices = [];
|
|
2521
|
-
//
|
|
2522
|
-
|
|
2524
|
+
let lastSrcPoints = []; // 保存最后一组点对,用于计算特征尺度
|
|
2525
|
+
// 【策略改进】优先使用最近的帧对(尽早检测)
|
|
2526
|
+
// 照片的几何约束是瞬间的,2帧就足够;多帧用来验证一致性
|
|
2527
|
+
const recentFrameCount = Math.min(5, this.faceLandmarksHistory.length);
|
|
2528
|
+
console.debug('[HomographyConstraint] Starting analysis', {
|
|
2529
|
+
totalFrames: this.faceLandmarksHistory.length,
|
|
2530
|
+
recentFrameCount,
|
|
2531
|
+
startIndex: Math.max(1, this.faceLandmarksHistory.length - recentFrameCount)
|
|
2532
|
+
});
|
|
2533
|
+
// 计算最近帧对的变换误差(重点在最近的帧)
|
|
2534
|
+
for (let i = Math.max(1, this.faceLandmarksHistory.length - recentFrameCount); i < this.faceLandmarksHistory.length; i++) {
|
|
2523
2535
|
const frame1 = this.faceLandmarksHistory[i - 1];
|
|
2524
2536
|
const frame2 = this.faceLandmarksHistory[i];
|
|
2525
|
-
|
|
2526
|
-
|
|
2537
|
+
console.debug(`[HomographyConstraint] Processing frame pair [${i - 1}, ${i}]`, {
|
|
2538
|
+
frame1Length: frame1.length,
|
|
2539
|
+
frame2Length: frame2.length
|
|
2540
|
+
});
|
|
2541
|
+
if (frame1.length < 100 || frame2.length < 100) {
|
|
2542
|
+
console.debug(`[HomographyConstraint] Frame pair skipped (insufficient points)`);
|
|
2543
|
+
continue; // 至少100个有效点
|
|
2544
|
+
}
|
|
2527
2545
|
// 【改进】收集所有有效的点对(而不是只采样10个点)
|
|
2528
2546
|
// 这给出更好的H矩阵估计
|
|
2529
2547
|
const srcPoints = [];
|
|
2530
2548
|
const dstPoints = [];
|
|
2531
|
-
for (let ptIdx = 0; ptIdx < frame1.length; ptIdx++) {
|
|
2549
|
+
for (let ptIdx = 0; ptIdx < Math.min(frame1.length, frame2.length); ptIdx++) {
|
|
2532
2550
|
if (frame1[ptIdx] && frame2[ptIdx] &&
|
|
2533
2551
|
frame1[ptIdx].length >= 2 && frame2[ptIdx].length >= 2) {
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2552
|
+
const x1 = frame1[ptIdx][0];
|
|
2553
|
+
const y1 = frame1[ptIdx][1];
|
|
2554
|
+
const x2 = frame2[ptIdx][0];
|
|
2555
|
+
const y2 = frame2[ptIdx][1];
|
|
2556
|
+
// 【修复】只排除明显的异常值(移动过大),保留所有其他点
|
|
2557
|
+
// 包括静止的点和微妙移动的点,DLT需要全局点分布
|
|
2558
|
+
const displacement = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
|
2559
|
+
if (displacement < 200) { // 仅排除极端异常
|
|
2560
|
+
srcPoints.push([x1, y1]);
|
|
2561
|
+
dstPoints.push([x2, y2]);
|
|
2562
|
+
}
|
|
2537
2563
|
}
|
|
2538
2564
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2565
|
+
console.debug(`[HomographyConstraint] Collected points`, {
|
|
2566
|
+
srcPointsCount: srcPoints.length,
|
|
2567
|
+
dstPointsCount: dstPoints.length
|
|
2568
|
+
});
|
|
2569
|
+
if (srcPoints.length < 10) {
|
|
2570
|
+
console.debug(`[HomographyConstraint] Point pair skipped (only ${srcPoints.length} < 10 points)`);
|
|
2571
|
+
continue; // 至少10个匹配点对(DLT最少需要4个)
|
|
2572
|
+
}
|
|
2573
|
+
// 保存这一组点对(用于后面计算特征尺度)
|
|
2574
|
+
lastSrcPoints = srcPoints;
|
|
2541
2575
|
// 【新增】使用DLT算法计算完整的3x3单应性矩阵
|
|
2542
2576
|
const H = this.estimateHomographyDLT(srcPoints, dstPoints);
|
|
2543
|
-
if (!H)
|
|
2577
|
+
if (!H) {
|
|
2578
|
+
console.debug(`[HomographyConstraint] DLT failed to estimate homography`);
|
|
2544
2579
|
continue;
|
|
2580
|
+
}
|
|
2581
|
+
console.debug(`[HomographyConstraint] H matrix estimated:`, {
|
|
2582
|
+
h00: H[0][0].toFixed(4),
|
|
2583
|
+
h11: H[1][1].toFixed(4),
|
|
2584
|
+
h22: H[2][2].toFixed(4),
|
|
2585
|
+
det: (H[0][0] * H[1][1] * H[2][2] + H[0][1] * H[1][2] * H[2][0] + H[0][2] * H[1][0] * H[2][1]
|
|
2586
|
+
- H[0][2] * H[1][1] * H[2][0] - H[0][0] * H[1][2] * H[2][1] - H[0][1] * H[1][0] * H[2][2]).toFixed(4)
|
|
2587
|
+
});
|
|
2545
2588
|
homographyMatrices.push(H);
|
|
2546
2589
|
// 【改进】使用单应性矩阵计算误差(而不是仿射变换)
|
|
2547
2590
|
let frameError = 0;
|
|
2548
2591
|
let validCount = 0;
|
|
2592
|
+
const sampleErrors = [];
|
|
2549
2593
|
for (let j = 0; j < srcPoints.length; j++) {
|
|
2550
2594
|
const transformed = this.applyHomography(H, srcPoints[j][0], srcPoints[j][1]);
|
|
2551
2595
|
const actual = dstPoints[j];
|
|
2552
2596
|
const error = Math.sqrt((transformed[0] - actual[0]) ** 2 + (transformed[1] - actual[1]) ** 2);
|
|
2553
2597
|
frameError += error;
|
|
2554
2598
|
validCount++;
|
|
2599
|
+
// 记录前5个误差用于调试
|
|
2600
|
+
if (j < 5) {
|
|
2601
|
+
sampleErrors.push(error);
|
|
2602
|
+
}
|
|
2555
2603
|
}
|
|
2556
2604
|
if (validCount > 0) {
|
|
2557
|
-
|
|
2605
|
+
const avgFrameError = frameError / validCount;
|
|
2606
|
+
errors.push(avgFrameError);
|
|
2607
|
+
console.debug(`[HomographyConstraint] Frame error computed`, {
|
|
2608
|
+
pointCount: validCount,
|
|
2609
|
+
avgFrameError: avgFrameError.toFixed(4),
|
|
2610
|
+
sampleErrors: sampleErrors.map(e => e.toFixed(4)).join(', ')
|
|
2611
|
+
});
|
|
2558
2612
|
}
|
|
2559
2613
|
}
|
|
2614
|
+
console.debug(`[HomographyConstraint] Error collection complete`, {
|
|
2615
|
+
totalErrors: errors.length,
|
|
2616
|
+
matrixCount: homographyMatrices.length
|
|
2617
|
+
});
|
|
2560
2618
|
if (errors.length === 0) {
|
|
2619
|
+
console.debug(`[HomographyConstraint] No errors computed, returning 0`);
|
|
2561
2620
|
return { planarScore: 0, error: 0 };
|
|
2562
2621
|
}
|
|
2563
2622
|
// 计算所有帧的平均误差
|
|
@@ -2568,75 +2627,138 @@ class MotionLivenessDetector {
|
|
|
2568
2627
|
if (homographyMatrices.length > 1) {
|
|
2569
2628
|
matrixConsistency = this.checkHomographyConsistency(homographyMatrices);
|
|
2570
2629
|
}
|
|
2571
|
-
//
|
|
2572
|
-
//
|
|
2573
|
-
//
|
|
2574
|
-
const
|
|
2630
|
+
// 【改进】使用相对误差而不是绝对误差
|
|
2631
|
+
// 相对误差 = avgError / 点集特征尺度
|
|
2632
|
+
// 这样对不同分辨率和点集大小更鲁棒
|
|
2633
|
+
const characteristicScale = lastSrcPoints.length > 0 ? this.computeCharacteristicScale(lastSrcPoints) : 1;
|
|
2634
|
+
const relativeError = characteristicScale > 0.1 ? avgError / characteristicScale : avgError;
|
|
2635
|
+
// 平面性判决:
|
|
2636
|
+
// relativeError < 0.05 → 很可能是平面(照片)
|
|
2637
|
+
// relativeError > 0.1 → 很可能是立体(活体)
|
|
2638
|
+
const errorScore = Math.max(0, 1 - relativeError / 0.1);
|
|
2575
2639
|
const planarScore = errorScore * matrixConsistency;
|
|
2576
|
-
console.debug('[HomographyConstraint]', {
|
|
2640
|
+
console.debug('[HomographyConstraint] FINAL RESULT', {
|
|
2641
|
+
recentFrameCount,
|
|
2577
2642
|
frameCount: errors.length,
|
|
2578
2643
|
avgError: avgError.toFixed(4),
|
|
2644
|
+
characteristicScale: characteristicScale.toFixed(4),
|
|
2645
|
+
relativeError: relativeError.toFixed(4),
|
|
2579
2646
|
errorScore: errorScore.toFixed(3),
|
|
2580
2647
|
matrixConsistency: matrixConsistency.toFixed(3),
|
|
2581
|
-
planarScore: planarScore.toFixed(3)
|
|
2648
|
+
planarScore: planarScore.toFixed(3),
|
|
2649
|
+
homographyMatrixCount: homographyMatrices.length
|
|
2582
2650
|
});
|
|
2583
2651
|
return { planarScore: Math.min(planarScore, 1), error: avgError };
|
|
2584
2652
|
}
|
|
2585
2653
|
/**
|
|
2586
|
-
*
|
|
2587
|
-
* 【改进】使用完整的6参数仿射变换(包含剪切分量)
|
|
2588
|
-
*
|
|
2589
|
-
* 变换形式: x' = ax + by + c, y' = dx + ey + f
|
|
2590
|
-
* 其中 [a, b] 和 [d, e] 可以包含旋转、缩放、剪切分量
|
|
2591
|
-
/**
|
|
2592
|
-
* 【新增】使用DLT算法估计单应性矩阵
|
|
2654
|
+
* 使用DLT算法估计单应性矩阵(Homography Estimation using DLT)
|
|
2593
2655
|
*
|
|
2594
2656
|
* DLT (Direct Linear Transform) 是估计射影变换的标准算法
|
|
2595
|
-
* 输入:源点和目标点对 (至少4对)
|
|
2596
|
-
* 输出:3x3单应性矩阵H,使得 p' = H * p (齐次坐标)
|
|
2597
2657
|
*
|
|
2598
|
-
*
|
|
2599
|
-
* -
|
|
2600
|
-
* -
|
|
2658
|
+
* 输入:
|
|
2659
|
+
* - src: 源点坐标数组 (至少4对)
|
|
2660
|
+
* - dst: 目标点坐标数组
|
|
2601
2661
|
*
|
|
2602
|
-
*
|
|
2662
|
+
* 输出:
|
|
2663
|
+
* - 3x3单应性矩阵H,使得 p' = H * p (齐次坐标表示)
|
|
2664
|
+
*
|
|
2665
|
+
* 关键特性:
|
|
2666
|
+
* - 与仿射变换的区别:
|
|
2667
|
+
* * 仿射:6参数,处理旋转+缩放+平移+剪切
|
|
2668
|
+
* * 单应性:8参数,处理完整的射影变换(包括透视失真)
|
|
2669
|
+
* - 适用场景:照片倾斜拍摄、相机透视变换
|
|
2670
|
+
* - 数值稳定性:使用点集归一化提高数值精度
|
|
2671
|
+
*
|
|
2672
|
+
* 算法步骤:
|
|
2673
|
+
* 1. 归一化源点和目标点(改进数值稳定性)
|
|
2674
|
+
* 2. 构建 2n×9 的 A 矩阵(n为点对数)
|
|
2675
|
+
* 3. 使用最小二乘法求解 Ah = 0
|
|
2676
|
+
* 4. 反演应化矩阵到原始坐标系
|
|
2603
2677
|
*/
|
|
2604
2678
|
estimateHomographyDLT(src, dst) {
|
|
2605
|
-
if (src.length < 4 || dst.length < 4 || src.length !== dst.length)
|
|
2679
|
+
if (src.length < 4 || dst.length < 4 || src.length !== dst.length) {
|
|
2680
|
+
console.debug('[DLT] Invalid input', { srcLen: src.length, dstLen: dst.length });
|
|
2606
2681
|
return null;
|
|
2682
|
+
}
|
|
2607
2683
|
const n = src.length;
|
|
2608
2684
|
// 【关键】对点进行归一化,提高数值稳定性
|
|
2609
2685
|
const srcNorm = this.normalizePoints(src);
|
|
2610
2686
|
const dstNorm = this.normalizePoints(dst);
|
|
2611
|
-
if (!srcNorm || !dstNorm)
|
|
2687
|
+
if (!srcNorm || !dstNorm) {
|
|
2688
|
+
console.debug('[DLT] Point normalization failed');
|
|
2612
2689
|
return null;
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2690
|
+
}
|
|
2691
|
+
console.debug('[DLT] Normalization success', {
|
|
2692
|
+
pointCount: n,
|
|
2693
|
+
srcScale: srcNorm.T[0][0].toFixed(4),
|
|
2694
|
+
dstScale: dstNorm.T[0][0].toFixed(4)
|
|
2695
|
+
});
|
|
2696
|
+
// 构建DLT方程矩阵 A (2n × 9)
|
|
2697
|
+
//
|
|
2698
|
+
// 标准DLT形式推导:
|
|
2699
|
+
// 已知齐次坐标关系:p' = H * p
|
|
2700
|
+
// 即:[x', y', 1]^T = H * [x, y, 1]^T / w
|
|
2701
|
+
// 其中:w = h7*x + h8*y + h9 (齐次化分母)
|
|
2702
|
+
//
|
|
2703
|
+
// 展开得到:
|
|
2704
|
+
// x' = (h1*x + h2*y + h3) / (h7*x + h8*y + h9)
|
|
2705
|
+
// y' = (h4*x + h5*y + h6) / (h7*x + h8*y + h9)
|
|
2706
|
+
//
|
|
2707
|
+
// 交叉相乘消去分母:
|
|
2708
|
+
// x'*(h7*x + h8*y + h9) = h1*x + h2*y + h3
|
|
2709
|
+
// y'*(h7*x + h8*y + h9) = h4*x + h5*y + h6
|
|
2710
|
+
//
|
|
2711
|
+
// 整理为齐次线性方程 Ah = 0:
|
|
2712
|
+
// h1*x + h2*y + h3 - xp*h7*x - xp*h8*y - xp*h9 = 0
|
|
2713
|
+
// h4*x + h5*y + h6 - yp*h7*x - yp*h8*y - yp*h9 = 0
|
|
2714
|
+
//
|
|
2715
|
+
// 矩阵形式(每对点产生2行方程):
|
|
2716
|
+
// [x, y, 1, 0, 0, 0, -xp*x, -xp*y, -xp] [h1]
|
|
2717
|
+
// [0, 0, 0, x, y, 1, -yp*x, -yp*y, -yp] * [h2] = 0
|
|
2718
|
+
// [...]
|
|
2719
|
+
// [h9]
|
|
2617
2720
|
const A = [];
|
|
2618
2721
|
for (let i = 0; i < n; i++) {
|
|
2619
2722
|
const x = srcNorm.points[i][0];
|
|
2620
2723
|
const y = srcNorm.points[i][1];
|
|
2621
2724
|
const xp = dstNorm.points[i][0];
|
|
2622
2725
|
const yp = dstNorm.points[i][1];
|
|
2623
|
-
//
|
|
2624
|
-
A.push([
|
|
2625
|
-
//
|
|
2626
|
-
A.push([0, 0, 0,
|
|
2726
|
+
// 第一行:h1*x + h2*y + h3 - xp*h7*x - xp*h8*y - xp*h9 = 0
|
|
2727
|
+
A.push([x, y, 1, 0, 0, 0, -xp * x, -xp * y, -xp]);
|
|
2728
|
+
// 第二行:h4*x + h5*y + h6 - yp*h7*x - yp*h8*y - yp*h9 = 0
|
|
2729
|
+
A.push([0, 0, 0, x, y, 1, -yp * x, -yp * y, -yp]);
|
|
2627
2730
|
}
|
|
2628
|
-
//
|
|
2629
|
-
// h
|
|
2731
|
+
// 使用最小二乘法求解 Ah = 0
|
|
2732
|
+
// h 是 A^T*A 最小特征值对应的特征向量
|
|
2630
2733
|
const h = this.solveHomographyLSQ(A);
|
|
2631
2734
|
if (!h)
|
|
2632
2735
|
return null;
|
|
2633
|
-
//
|
|
2736
|
+
// 反演应化:从归一化坐标回到原始图像坐标
|
|
2737
|
+
// H_orig = T_dst^(-1) * H_norm * T_src
|
|
2634
2738
|
const H = this.denormalizeHomography(h, srcNorm, dstNorm);
|
|
2635
2739
|
return H;
|
|
2636
2740
|
}
|
|
2637
2741
|
/**
|
|
2638
|
-
*
|
|
2639
|
-
*
|
|
2742
|
+
* 点集归一化(Hartley标准化)- 提高DLT数值稳定性
|
|
2743
|
+
*
|
|
2744
|
+
* 目的:
|
|
2745
|
+
* - DLT算法对坐标尺度敏感,归一化可显著改善数值稳定性
|
|
2746
|
+
* - 避免矩阵条件数过大,减少数值误差
|
|
2747
|
+
*
|
|
2748
|
+
* 方法:
|
|
2749
|
+
* 1. 计算点集的重心 (cx, cy)
|
|
2750
|
+
* 2. 计算点到重心的平均距离
|
|
2751
|
+
* 3. 缩放使得平均距离为 √2
|
|
2752
|
+
*
|
|
2753
|
+
* 变换:T = [s, 0, -s*cx; 0, s, -s*cy; 0, 0, 1]
|
|
2754
|
+
* 其中 s = √2 / avgDistance
|
|
2755
|
+
*
|
|
2756
|
+
* 好处:
|
|
2757
|
+
* - 点集的重心在原点
|
|
2758
|
+
* - 点到原点的平均距离为 √2(标准化)
|
|
2759
|
+
* - A^T*A 矩阵条件数接近最优
|
|
2760
|
+
*
|
|
2761
|
+
* 逆操作:在得到矩阵后需要反归一化回原始坐标
|
|
2640
2762
|
*/
|
|
2641
2763
|
normalizePoints(points) {
|
|
2642
2764
|
if (points.length === 0)
|
|
@@ -2676,15 +2798,26 @@ class MotionLivenessDetector {
|
|
|
2676
2798
|
return { points: normalized, T };
|
|
2677
2799
|
}
|
|
2678
2800
|
/**
|
|
2679
|
-
*
|
|
2680
|
-
*
|
|
2801
|
+
* 最小二乘法求解齐次线性方程 Ah = 0
|
|
2802
|
+
*
|
|
2803
|
+
* 问题:找到使 ||Ah|| 最小的单位向量 h
|
|
2804
|
+
*
|
|
2805
|
+
* 标准解法:
|
|
2806
|
+
* - 完整SVD:对 A 进行 SVD 分解,h 是最小奇异值的右奇异向量
|
|
2807
|
+
* - 特征向量法(此处使用):
|
|
2808
|
+
* 1. 计算 A^T * A(9×9对称矩阵)
|
|
2809
|
+
* 2. 求 A^T*A 的最小特征值对应的特征向量
|
|
2810
|
+
* 3. 该特征向量即为所求的 h
|
|
2811
|
+
*
|
|
2812
|
+
* 说明:
|
|
2813
|
+
* - h 是 3×3 单应性矩阵 H 的向量化形式
|
|
2814
|
+
* - 返回的特征向量已归一化
|
|
2681
2815
|
*/
|
|
2682
2816
|
solveHomographyLSQ(A) {
|
|
2683
|
-
// 简化的SVD求解:找使 ||Ah|| 最小的 h
|
|
2684
|
-
// 为了简化,使用迭代最小二乘法或对称矩阵的特征向量
|
|
2685
2817
|
if (A.length < 8)
|
|
2686
2818
|
return null;
|
|
2687
|
-
// A^T * A 矩阵 (
|
|
2819
|
+
// 构造 A^T * A 矩阵 (9×9)
|
|
2820
|
+
// 这是一个对称半正定矩阵
|
|
2688
2821
|
const ATA = Array(9).fill(0).map(() => Array(9).fill(0));
|
|
2689
2822
|
for (let i = 0; i < 9; i++) {
|
|
2690
2823
|
for (let j = 0; j < 9; j++) {
|
|
@@ -2693,44 +2826,136 @@ class MotionLivenessDetector {
|
|
|
2693
2826
|
}
|
|
2694
2827
|
}
|
|
2695
2828
|
}
|
|
2696
|
-
// 求
|
|
2829
|
+
// 求 A^T*A 的最小特征向量
|
|
2830
|
+
// 最小特征向量对应最小特征值,使 ||Ah|| 最小
|
|
2697
2831
|
const eigenVec = this.getSmallestEigenvector(ATA);
|
|
2698
2832
|
return eigenVec;
|
|
2699
2833
|
}
|
|
2700
2834
|
/**
|
|
2701
|
-
* 求
|
|
2702
|
-
*
|
|
2835
|
+
* 求9x9对称矩阵(A^T*A)的最小特征向量
|
|
2836
|
+
*
|
|
2837
|
+
* 【关键】使用 Jacobi 特征值分解而不是幂法
|
|
2838
|
+
*
|
|
2839
|
+
* 问题分析:
|
|
2840
|
+
* - 迭代幂法(Power Iteration)求的是**最大**特征值的特征向量
|
|
2841
|
+
* - 我们需要**最小**特征值的特征向量
|
|
2842
|
+
* - 之前的幂法实现虽然标记为最小,但实际求的是最大 → 算法失败!
|
|
2843
|
+
*
|
|
2844
|
+
* 解决方案:
|
|
2845
|
+
* 1. 使用 QR 算法或 Jacobi 方法求完整的特征值分解
|
|
2846
|
+
* 2. 选择最小特征值对应的特征向量
|
|
2847
|
+
*
|
|
2848
|
+
* 此处实现简化的 Jacobi 迭代:
|
|
2849
|
+
* - 对称矩阵对角化
|
|
2850
|
+
* - 提取最小特征值的特征向量
|
|
2703
2851
|
*/
|
|
2704
2852
|
getSmallestEigenvector(mat) {
|
|
2705
2853
|
if (mat.length !== 9)
|
|
2706
2854
|
return null;
|
|
2707
|
-
//
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2855
|
+
// 复制矩阵,Jacobi方法会修改原矩阵
|
|
2856
|
+
const A = mat.map(row => [...row]);
|
|
2857
|
+
const V = Array(9).fill(0).map((_, i) => {
|
|
2858
|
+
const row = Array(9).fill(0);
|
|
2859
|
+
row[i] = 1;
|
|
2860
|
+
return row;
|
|
2861
|
+
}); // 9×9 单位矩阵,用于存储特征向量
|
|
2862
|
+
// Jacobi迭代(简化版)
|
|
2863
|
+
// 目标:将 A 对角化为 D = V^T * A * V
|
|
2864
|
+
for (let iteration = 0; iteration < 20; iteration++) {
|
|
2865
|
+
let maxOffDiag = 0;
|
|
2866
|
+
let p = 0, q = 1;
|
|
2867
|
+
// 找非对角元素中绝对值最大的
|
|
2716
2868
|
for (let i = 0; i < 9; i++) {
|
|
2717
|
-
for (let j =
|
|
2718
|
-
|
|
2869
|
+
for (let j = i + 1; j < 9; j++) {
|
|
2870
|
+
if (Math.abs(A[i][j]) > maxOffDiag) {
|
|
2871
|
+
maxOffDiag = Math.abs(A[i][j]);
|
|
2872
|
+
p = i;
|
|
2873
|
+
q = j;
|
|
2874
|
+
}
|
|
2719
2875
|
}
|
|
2720
2876
|
}
|
|
2721
|
-
//
|
|
2722
|
-
|
|
2723
|
-
if (norm < 0.0001)
|
|
2877
|
+
// 收敛判断
|
|
2878
|
+
if (maxOffDiag < 1e-10) {
|
|
2724
2879
|
break;
|
|
2880
|
+
}
|
|
2881
|
+
// 计算Givens旋转角
|
|
2882
|
+
const Aqq = A[q][q];
|
|
2883
|
+
const App = A[p][p];
|
|
2884
|
+
const Apq = A[p][q];
|
|
2885
|
+
let theta = 0;
|
|
2886
|
+
if (Math.abs(Aqq - App) < 1e-10) {
|
|
2887
|
+
theta = Math.PI / 4;
|
|
2888
|
+
}
|
|
2889
|
+
else {
|
|
2890
|
+
theta = 0.5 * Math.atan2(2 * Apq, Aqq - App);
|
|
2891
|
+
}
|
|
2892
|
+
const c = Math.cos(theta);
|
|
2893
|
+
const s = Math.sin(theta);
|
|
2894
|
+
// 更新矩阵 A(2×2 子块旋转)
|
|
2895
|
+
const App_new = c * c * App - 2 * s * c * Apq + s * s * Aqq;
|
|
2896
|
+
const Aqq_new = s * s * App + 2 * s * c * Apq + c * c * Aqq;
|
|
2897
|
+
const Apq_new = 0; // 旋转后的非对角元素为0
|
|
2898
|
+
// 更新第p行第q列的其他元素
|
|
2725
2899
|
for (let i = 0; i < 9; i++) {
|
|
2726
|
-
|
|
2900
|
+
if (i !== p && i !== q) {
|
|
2901
|
+
const Aip = A[i][p];
|
|
2902
|
+
const Aiq = A[i][q];
|
|
2903
|
+
A[i][p] = c * Aip - s * Aiq;
|
|
2904
|
+
A[p][i] = A[i][p];
|
|
2905
|
+
A[i][q] = s * Aip + c * Aiq;
|
|
2906
|
+
A[q][i] = A[i][q];
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
A[p][p] = App_new;
|
|
2910
|
+
A[q][q] = Aqq_new;
|
|
2911
|
+
A[p][q] = Apq_new;
|
|
2912
|
+
A[q][p] = Apq_new;
|
|
2913
|
+
// 更新特征向量矩阵 V
|
|
2914
|
+
for (let i = 0; i < 9; i++) {
|
|
2915
|
+
const Vip = V[i][p];
|
|
2916
|
+
const Viq = V[i][q];
|
|
2917
|
+
V[i][p] = c * Vip - s * Viq;
|
|
2918
|
+
V[i][q] = s * Vip + c * Viq;
|
|
2727
2919
|
}
|
|
2728
2920
|
}
|
|
2729
|
-
|
|
2921
|
+
// 找到最小特征值的位置
|
|
2922
|
+
let minIdx = 0;
|
|
2923
|
+
let minEigenvalue = A[0][0];
|
|
2924
|
+
for (let i = 1; i < 9; i++) {
|
|
2925
|
+
if (A[i][i] < minEigenvalue) {
|
|
2926
|
+
minEigenvalue = A[i][i];
|
|
2927
|
+
minIdx = i;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
// 提取对应的特征向量(V的第 minIdx 列)
|
|
2931
|
+
const eigenvector = Array(9);
|
|
2932
|
+
for (let i = 0; i < 9; i++) {
|
|
2933
|
+
eigenvector[i] = V[i][minIdx];
|
|
2934
|
+
}
|
|
2935
|
+
// 归一化
|
|
2936
|
+
const norm = Math.sqrt(eigenvector.reduce((a, b) => a + b * b, 0));
|
|
2937
|
+
if (norm > 1e-10) {
|
|
2938
|
+
for (let i = 0; i < 9; i++) {
|
|
2939
|
+
eigenvector[i] = eigenvector[i] / norm;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
return eigenvector;
|
|
2730
2943
|
}
|
|
2731
2944
|
/**
|
|
2732
|
-
* 反演应化矩阵
|
|
2733
|
-
*
|
|
2945
|
+
* 反演应化矩阵 - 从归一化坐标回到原始图像坐标
|
|
2946
|
+
*
|
|
2947
|
+
* 原理:
|
|
2948
|
+
* - 在归一化坐标系中估算的H矩阵需要转换回原始坐标
|
|
2949
|
+
*
|
|
2950
|
+
* 公式:H_orig = T_dst^(-1) * H_norm * T_src
|
|
2951
|
+
*
|
|
2952
|
+
* 说明:
|
|
2953
|
+
* - T_src:源点的归一化变换矩阵
|
|
2954
|
+
* - T_dst:目标点的归一化变换矩阵
|
|
2955
|
+
* - H_norm:在归一化坐标中计算得到的3×3矩阵
|
|
2956
|
+
* - H_orig:最终的单应性矩阵(用于原始图像坐标)
|
|
2957
|
+
*
|
|
2958
|
+
* 验证:p'_orig = H_orig * p_src_orig
|
|
2734
2959
|
*/
|
|
2735
2960
|
denormalizeHomography(h, srcNorm, dstNorm) {
|
|
2736
2961
|
// 将向量h转换为3x3矩阵
|
|
@@ -2750,6 +2975,30 @@ class MotionLivenessDetector {
|
|
|
2750
2975
|
const H = this.multiplyMatrix3x3(temp, T_src);
|
|
2751
2976
|
return H;
|
|
2752
2977
|
}
|
|
2978
|
+
/**
|
|
2979
|
+
* 检查点集的分布范围(防止点集集中在小区域)
|
|
2980
|
+
* 返回值:0-1,值越大说明分布越分散
|
|
2981
|
+
*/
|
|
2982
|
+
checkPointSpread(points) {
|
|
2983
|
+
if (points.length < 2)
|
|
2984
|
+
return 0;
|
|
2985
|
+
let minX = Infinity, maxX = -Infinity;
|
|
2986
|
+
let minY = Infinity, maxY = -Infinity;
|
|
2987
|
+
for (const p of points) {
|
|
2988
|
+
minX = Math.min(minX, p[0]);
|
|
2989
|
+
maxX = Math.max(maxX, p[0]);
|
|
2990
|
+
minY = Math.min(minY, p[1]);
|
|
2991
|
+
maxY = Math.max(maxY, p[1]);
|
|
2992
|
+
}
|
|
2993
|
+
const rangeX = maxX - minX;
|
|
2994
|
+
const rangeY = maxY - minY;
|
|
2995
|
+
const area = rangeX * rangeY;
|
|
2996
|
+
// 点集的相对面积(相对于整个图像,假设1920x1080)
|
|
2997
|
+
// 面积越大,点的分布越分散
|
|
2998
|
+
const imageArea = 1920 * 1080;
|
|
2999
|
+
const relativeArea = Math.min(area / imageArea, 1);
|
|
3000
|
+
return relativeArea;
|
|
3001
|
+
}
|
|
2753
3002
|
/**
|
|
2754
3003
|
* 3x3矩阵求逆
|
|
2755
3004
|
*/
|
|
@@ -2796,23 +3045,59 @@ class MotionLivenessDetector {
|
|
|
2796
3045
|
return result;
|
|
2797
3046
|
}
|
|
2798
3047
|
/**
|
|
2799
|
-
*
|
|
2800
|
-
*
|
|
3048
|
+
* 应用单应性变换 - 齐次坐标变换与反齐次化
|
|
3049
|
+
*
|
|
3050
|
+
* 操作步骤:
|
|
3051
|
+
* 1. 输入:(x, y) → 齐次坐标 [x, y, 1]
|
|
3052
|
+
* 2. 矩阵乘法:H * p 得到齐次结果 [x'w, y'w, w]
|
|
3053
|
+
* 3. 反齐次化:[x'w/w, y'w/w] = [x', y']
|
|
3054
|
+
*
|
|
3055
|
+
* 公式:
|
|
3056
|
+
* [x'] [h11 h12 h13] [x]
|
|
3057
|
+
* [y' ] = [h21 h22 h23] [y]
|
|
3058
|
+
* [w'] [h31 h32 h33] [1]
|
|
3059
|
+
*
|
|
3060
|
+
* 最后:x' = x'w / w, y' = y'w / w
|
|
3061
|
+
*
|
|
3062
|
+
* 注意:如果 w' ≈ 0,点被投影到无穷远(退化情况)
|
|
2801
3063
|
*/
|
|
2802
3064
|
applyHomography(H, x, y) {
|
|
2803
|
-
//
|
|
3065
|
+
// 齐次坐标表示
|
|
2804
3066
|
const p = [x, y, 1];
|
|
2805
|
-
// H * p
|
|
3067
|
+
// 矩阵乘法:H * p
|
|
2806
3068
|
const Hp = [
|
|
2807
3069
|
H[0][0] * p[0] + H[0][1] * p[1] + H[0][2] * p[2],
|
|
2808
3070
|
H[1][0] * p[0] + H[1][1] * p[1] + H[1][2] * p[2],
|
|
2809
3071
|
H[2][0] * p[0] + H[2][1] * p[1] + H[2][2] * p[2]
|
|
2810
3072
|
];
|
|
2811
|
-
//
|
|
3073
|
+
// 反齐次化:除以齐次坐标的 Z 分量
|
|
3074
|
+
// 如果 w ≈ 0,则点在无穷远,返回齐次结果
|
|
2812
3075
|
if (Math.abs(Hp[2]) < 0.0001) {
|
|
2813
|
-
return [Hp[0], Hp[1]];
|
|
3076
|
+
return [Hp[0], Hp[1]]; // 异常情况:接近无穷
|
|
2814
3077
|
}
|
|
2815
|
-
return [Hp[0] / Hp[2], Hp[1] / Hp[2]];
|
|
3078
|
+
return [Hp[0] / Hp[2], Hp[1] / Hp[2]]; // 正常情况:反齐次化
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* 【新增】计算点集的特征尺度(点间的平均距离)
|
|
3082
|
+
*
|
|
3083
|
+
* 用于相对误差计算,使算法对不同分辨率和点集大小更鲁棒
|
|
3084
|
+
*/
|
|
3085
|
+
computeCharacteristicScale(points) {
|
|
3086
|
+
if (points.length < 2)
|
|
3087
|
+
return 1;
|
|
3088
|
+
let totalDist = 0;
|
|
3089
|
+
let count = 0;
|
|
3090
|
+
// 采样计算点间距离,避免 O(n²) 复杂度
|
|
3091
|
+
const sampleSize = Math.min(points.length, 30);
|
|
3092
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
3093
|
+
for (let j = i + 1; j < sampleSize; j++) {
|
|
3094
|
+
const dx = points[i][0] - points[j][0];
|
|
3095
|
+
const dy = points[i][1] - points[j][1];
|
|
3096
|
+
totalDist += Math.sqrt(dx * dx + dy * dy);
|
|
3097
|
+
count++;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
return count > 0 ? totalDist / count : 1;
|
|
2816
3101
|
}
|
|
2817
3102
|
/**
|
|
2818
3103
|
* 【新增】检查单应性矩阵的一致性
|
|
@@ -3017,7 +3302,7 @@ class MotionLivenessDetector {
|
|
|
3017
3302
|
* 逆向检测优先级更高,因为照片几何约束是物理定律,无法伪造
|
|
3018
3303
|
*/
|
|
3019
3304
|
makeLivenessDecision(eyeActivity, mouthActivity, muscleActivity, photoGeometry) {
|
|
3020
|
-
if (!this.
|
|
3305
|
+
if (!this.collectedMinFrames()) {
|
|
3021
3306
|
return true; // 数据不足,默认通过
|
|
3022
3307
|
}
|
|
3023
3308
|
// ============ 逆向检测(照片几何特征)============
|
|
@@ -4246,8 +4531,8 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4246
4531
|
}
|
|
4247
4532
|
// 静默活体检测
|
|
4248
4533
|
const motionResult = this.detectionState.motionDetector.analyzeMotion(face, faceBox);
|
|
4249
|
-
|
|
4250
|
-
|
|
4534
|
+
if (this.detectionState.motionDetector.collectedMinFrames()) {
|
|
4535
|
+
// 采集到最小帧数后,否定性判定才可信
|
|
4251
4536
|
if (!motionResult.isLively) {
|
|
4252
4537
|
this.emitDebug('motion-detection', 'Motion liveness check failed - possible photo attack', {
|
|
4253
4538
|
details: motionResult.details,
|
|
@@ -4261,17 +4546,20 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4261
4546
|
this.partialResetDetectionState();
|
|
4262
4547
|
return;
|
|
4263
4548
|
}
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4549
|
+
// 采集到足够帧数后,肯定性判定才可信
|
|
4550
|
+
if (this.detectionState.motionDetector.collectedFullFrames()) {
|
|
4551
|
+
this.emitDebug('motion-detection', 'Motion liveness check passed', {
|
|
4552
|
+
debug: motionResult.debug,
|
|
4553
|
+
details: motionResult.details,
|
|
4554
|
+
}, 'warn');
|
|
4555
|
+
this.detectionState.liveness = true;
|
|
4556
|
+
}
|
|
4557
|
+
else {
|
|
4558
|
+
this.emitDebug('motion-detection', 'Motion liveness check ongoing - collecting more frames', {
|
|
4559
|
+
debug: motionResult.debug,
|
|
4560
|
+
details: motionResult.details,
|
|
4561
|
+
}, 'warn');
|
|
4562
|
+
}
|
|
4275
4563
|
}
|
|
4276
4564
|
// 动作活体检测阶段处理
|
|
4277
4565
|
if (this.detectionState.period === DetectionPeriod.VERIFY) {
|