@sssxyd/face-liveness-detector 0.4.1-beta.7 → 0.4.1-beta.8

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 CHANGED
@@ -1633,8 +1633,11 @@ class MotionLivenessDetector {
1633
1633
  getOptions() {
1634
1634
  return this.config;
1635
1635
  }
1636
- isReady() {
1637
- return this.normalizedLandmarksHistory.length >= 5; // 只需要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.isReady()) {
1688
+ if (!this.collectedMinFrames()) {
1686
1689
  return this.createEmptyResult({
1687
1690
  reason: '数据收集中,帧数不足',
1688
1691
  collectedFrames: this.normalizedLandmarksHistory.length
@@ -2518,26 +2521,40 @@ class MotionLivenessDetector {
2518
2521
  // 更多的点对会给出更准确的单应性矩阵估计
2519
2522
  const errors = [];
2520
2523
  const homographyMatrices = [];
2521
- // 计算相邻帧的变换误差
2522
- for (let i = 1; i < this.faceLandmarksHistory.length; i++) {
2524
+ let lastSrcPoints = []; // 保存最后一组点对,用于计算特征尺度
2525
+ // 【策略改进】优先使用最近的帧对(尽早检测)
2526
+ // 照片的几何约束是瞬间的,2帧就足够;多帧用来验证一致性
2527
+ const recentFrameCount = Math.min(5, this.faceLandmarksHistory.length);
2528
+ // 计算最近帧对的变换误差(重点在最近的帧)
2529
+ for (let i = Math.max(1, this.faceLandmarksHistory.length - recentFrameCount); i < this.faceLandmarksHistory.length; i++) {
2523
2530
  const frame1 = this.faceLandmarksHistory[i - 1];
2524
2531
  const frame2 = this.faceLandmarksHistory[i];
2525
- if (frame1.length < 468 || frame2.length < 468)
2526
- continue;
2532
+ if (frame1.length < 100 || frame2.length < 100)
2533
+ continue; // 至少100个有效点
2527
2534
  // 【改进】收集所有有效的点对(而不是只采样10个点)
2528
2535
  // 这给出更好的H矩阵估计
2529
2536
  const srcPoints = [];
2530
2537
  const dstPoints = [];
2531
- for (let ptIdx = 0; ptIdx < frame1.length; ptIdx++) {
2538
+ for (let ptIdx = 0; ptIdx < Math.min(frame1.length, frame2.length); ptIdx++) {
2532
2539
  if (frame1[ptIdx] && frame2[ptIdx] &&
2533
2540
  frame1[ptIdx].length >= 2 && frame2[ptIdx].length >= 2) {
2534
- // 使用x, y原始坐标
2535
- srcPoints.push([frame1[ptIdx][0], frame1[ptIdx][1]]);
2536
- dstPoints.push([frame2[ptIdx][0], frame2[ptIdx][1]]);
2541
+ const x1 = frame1[ptIdx][0];
2542
+ const y1 = frame1[ptIdx][1];
2543
+ const x2 = frame2[ptIdx][0];
2544
+ const y2 = frame2[ptIdx][1];
2545
+ // 【修复】只排除明显的异常值(移动过大),保留所有其他点
2546
+ // 包括静止的点和微妙移动的点,DLT需要全局点分布
2547
+ const displacement = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
2548
+ if (displacement < 200) { // 仅排除极端异常
2549
+ srcPoints.push([x1, y1]);
2550
+ dstPoints.push([x2, y2]);
2551
+ }
2537
2552
  }
2538
2553
  }
2539
- if (srcPoints.length < 4)
2540
- continue;
2554
+ if (srcPoints.length < 10)
2555
+ continue; // 至少10个匹配点对(DLT最少需要4个)
2556
+ // 保存这一组点对(用于后面计算特征尺度)
2557
+ lastSrcPoints = srcPoints;
2541
2558
  // 【新增】使用DLT算法计算完整的3x3单应性矩阵
2542
2559
  const H = this.estimateHomographyDLT(srcPoints, dstPoints);
2543
2560
  if (!H)
@@ -2568,38 +2585,51 @@ class MotionLivenessDetector {
2568
2585
  if (homographyMatrices.length > 1) {
2569
2586
  matrixConsistency = this.checkHomographyConsistency(homographyMatrices);
2570
2587
  }
2571
- // 平面得分 = 误差低 且 H矩阵一致
2572
- // avgError < 0.01 非常可能是平面
2573
- // avgError > 0.05 → 可能是立体(活体)
2574
- const errorScore = Math.max(0, 1 - avgError / 0.03);
2588
+ // 【改进】使用相对误差而不是绝对误差
2589
+ // 相对误差 = avgError / 点集特征尺度
2590
+ // 这样对不同分辨率和点集大小更鲁棒
2591
+ const characteristicScale = lastSrcPoints.length > 0 ? this.computeCharacteristicScale(lastSrcPoints) : 1;
2592
+ const relativeError = characteristicScale > 0.1 ? avgError / characteristicScale : avgError;
2593
+ // 平面性判决:
2594
+ // relativeError < 0.05 → 很可能是平面(照片)
2595
+ // relativeError > 0.1 → 很可能是立体(活体)
2596
+ const errorScore = Math.max(0, 1 - relativeError / 0.1);
2575
2597
  const planarScore = errorScore * matrixConsistency;
2576
2598
  console.debug('[HomographyConstraint]', {
2599
+ recentFrameCount,
2577
2600
  frameCount: errors.length,
2578
2601
  avgError: avgError.toFixed(4),
2579
2602
  errorScore: errorScore.toFixed(3),
2580
2603
  matrixConsistency: matrixConsistency.toFixed(3),
2581
- planarScore: planarScore.toFixed(3)
2604
+ planarScore: planarScore.toFixed(3),
2605
+ homographyMatrixCount: homographyMatrices.length
2582
2606
  });
2583
2607
  return { planarScore: Math.min(planarScore, 1), error: avgError };
2584
2608
  }
2585
2609
  /**
2586
- * 估计仿射变换矩阵
2587
- * 【改进】使用完整的6参数仿射变换(包含剪切分量)
2588
- *
2589
- * 变换形式: x' = ax + by + c, y' = dx + ey + f
2590
- * 其中 [a, b] 和 [d, e] 可以包含旋转、缩放、剪切分量
2591
- /**
2592
- * 【新增】使用DLT算法估计单应性矩阵
2610
+ * 使用DLT算法估计单应性矩阵(Homography Estimation using DLT)
2593
2611
  *
2594
2612
  * DLT (Direct Linear Transform) 是估计射影变换的标准算法
2595
- * 输入:源点和目标点对 (至少4对)
2596
- * 输出:3x3单应性矩阵H,使得 p' = H * p (齐次坐标)
2597
2613
  *
2598
- * 相比仿射变换:
2599
- * - 仿射:6参数,处理旋转+缩放+平移+剪切
2600
- * - 单应性:8参数,处理完整的射影变换(包括透视)
2614
+ * 输入:
2615
+ * - src: 源点坐标数组 (至少4对)
2616
+ * - dst: 目标点坐标数组
2617
+ *
2618
+ * 输出:
2619
+ * - 3x3单应性矩阵H,使得 p' = H * p (齐次坐标表示)
2620
+ *
2621
+ * 关键特性:
2622
+ * - 与仿射变换的区别:
2623
+ * * 仿射:6参数,处理旋转+缩放+平移+剪切
2624
+ * * 单应性:8参数,处理完整的射影变换(包括透视失真)
2625
+ * - 适用场景:照片倾斜拍摄、相机透视变换
2626
+ * - 数值稳定性:使用点集归一化提高数值精度
2601
2627
  *
2602
- * 照片倾斜拍摄时,需要完整的单应性矩阵!
2628
+ * 算法步骤:
2629
+ * 1. 归一化源点和目标点(改进数值稳定性)
2630
+ * 2. 构建 2n×9 的 A 矩阵(n为点对数)
2631
+ * 3. 使用最小二乘法求解 Ah = 0
2632
+ * 4. 反演应化矩阵到原始坐标系
2603
2633
  */
2604
2634
  estimateHomographyDLT(src, dst) {
2605
2635
  if (src.length < 4 || dst.length < 4 || src.length !== dst.length)
@@ -2610,33 +2640,72 @@ class MotionLivenessDetector {
2610
2640
  const dstNorm = this.normalizePoints(dst);
2611
2641
  if (!srcNorm || !dstNorm)
2612
2642
  return null;
2613
- // 构建DLT方程矩阵 A (2n x 9)
2614
- // 对每对点,构建2行方程:
2615
- // [-x, -y, -1, 0, 0, 0, x*x', y*x', x']
2616
- // [0, 0, 0, -x, -y, -1, x*y', y*y', y']
2643
+ // 构建DLT方程矩阵 A (2n × 9)
2644
+ //
2645
+ // 标准DLT形式推导:
2646
+ // 已知齐次坐标关系:p' = H * p
2647
+ // 即:[x', y', 1]^T = H * [x, y, 1]^T / w
2648
+ // 其中:w = h7*x + h8*y + h9 (齐次化分母)
2649
+ //
2650
+ // 展开得到:
2651
+ // x' = (h1*x + h2*y + h3) / (h7*x + h8*y + h9)
2652
+ // y' = (h4*x + h5*y + h6) / (h7*x + h8*y + h9)
2653
+ //
2654
+ // 交叉相乘消去分母:
2655
+ // x'*(h7*x + h8*y + h9) = h1*x + h2*y + h3
2656
+ // y'*(h7*x + h8*y + h9) = h4*x + h5*y + h6
2657
+ //
2658
+ // 整理为齐次线性方程 Ah = 0:
2659
+ // h1*x + h2*y + h3 - xp*h7*x - xp*h8*y - xp*h9 = 0
2660
+ // h4*x + h5*y + h6 - yp*h7*x - yp*h8*y - yp*h9 = 0
2661
+ //
2662
+ // 矩阵形式(每对点产生2行方程):
2663
+ // [x, y, 1, 0, 0, 0, -xp*x, -xp*y, -xp] [h1]
2664
+ // [0, 0, 0, x, y, 1, -yp*x, -yp*y, -yp] * [h2] = 0
2665
+ // [...]
2666
+ // [h9]
2617
2667
  const A = [];
2618
2668
  for (let i = 0; i < n; i++) {
2619
2669
  const x = srcNorm.points[i][0];
2620
2670
  const y = srcNorm.points[i][1];
2621
2671
  const xp = dstNorm.points[i][0];
2622
2672
  const yp = dstNorm.points[i][1];
2623
- // 第一行
2624
- A.push([-x, -y, -1, 0, 0, 0, x * xp, y * xp, xp]);
2625
- // 第二行
2626
- A.push([0, 0, 0, -x, -y, -1, x * yp, y * yp, yp]);
2673
+ // 第一行:h1*x + h2*y + h3 - xp*h7*x - xp*h8*y - xp*h9 = 0
2674
+ A.push([x, y, 1, 0, 0, 0, -xp * x, -xp * y, -xp]);
2675
+ // 第二行:h4*x + h5*y + h6 - yp*h7*x - yp*h8*y - yp*h9 = 0
2676
+ A.push([0, 0, 0, x, y, 1, -yp * x, -yp * y, -yp]);
2627
2677
  }
2628
- // 使用SVD求解 Ah = 0
2629
- // h 是最小奇异值对应的右奇异向量
2678
+ // 使用最小二乘法求解 Ah = 0
2679
+ // h 是 A^T*A 最小特征值对应的特征向量
2630
2680
  const h = this.solveHomographyLSQ(A);
2631
2681
  if (!h)
2632
2682
  return null;
2633
- // 反演应化矩阵(从归一化坐标回到原始坐标)
2683
+ // 反演应化:从归一化坐标回到原始图像坐标
2684
+ // H_orig = T_dst^(-1) * H_norm * T_src
2634
2685
  const H = this.denormalizeHomography(h, srcNorm, dstNorm);
2635
2686
  return H;
2636
2687
  }
2637
2688
  /**
2638
- * 点集归一化(提高数值稳定性)
2639
- * 变换点使得重心在原点,平均距离为sqrt(2)
2689
+ * 点集归一化(Hartley标准化)- 提高DLT数值稳定性
2690
+ *
2691
+ * 目的:
2692
+ * - DLT算法对坐标尺度敏感,归一化可显著改善数值稳定性
2693
+ * - 避免矩阵条件数过大,减少数值误差
2694
+ *
2695
+ * 方法:
2696
+ * 1. 计算点集的重心 (cx, cy)
2697
+ * 2. 计算点到重心的平均距离
2698
+ * 3. 缩放使得平均距离为 √2
2699
+ *
2700
+ * 变换:T = [s, 0, -s*cx; 0, s, -s*cy; 0, 0, 1]
2701
+ * 其中 s = √2 / avgDistance
2702
+ *
2703
+ * 好处:
2704
+ * - 点集的重心在原点
2705
+ * - 点到原点的平均距离为 √2(标准化)
2706
+ * - A^T*A 矩阵条件数接近最优
2707
+ *
2708
+ * 逆操作:在得到矩阵后需要反归一化回原始坐标
2640
2709
  */
2641
2710
  normalizePoints(points) {
2642
2711
  if (points.length === 0)
@@ -2676,15 +2745,26 @@ class MotionLivenessDetector {
2676
2745
  return { points: normalized, T };
2677
2746
  }
2678
2747
  /**
2679
- * 最小二乘法求解 Ah = 0
2680
- * 其中 h 是3x3矩阵的向量化形式
2748
+ * 最小二乘法求解齐次线性方程 Ah = 0
2749
+ *
2750
+ * 问题:找到使 ||Ah|| 最小的单位向量 h
2751
+ *
2752
+ * 标准解法:
2753
+ * - 完整SVD:对 A 进行 SVD 分解,h 是最小奇异值的右奇异向量
2754
+ * - 特征向量法(此处使用):
2755
+ * 1. 计算 A^T * A(9×9对称矩阵)
2756
+ * 2. 求 A^T*A 的最小特征值对应的特征向量
2757
+ * 3. 该特征向量即为所求的 h
2758
+ *
2759
+ * 说明:
2760
+ * - h 是 3×3 单应性矩阵 H 的向量化形式
2761
+ * - 返回的特征向量已归一化
2681
2762
  */
2682
2763
  solveHomographyLSQ(A) {
2683
- // 简化的SVD求解:找使 ||Ah|| 最小的 h
2684
- // 为了简化,使用迭代最小二乘法或对称矩阵的特征向量
2685
2764
  if (A.length < 8)
2686
2765
  return null;
2687
- // A^T * A 矩阵 (9x9)
2766
+ // 构造 A^T * A 矩阵 (9×9)
2767
+ // 这是一个对称半正定矩阵
2688
2768
  const ATA = Array(9).fill(0).map(() => Array(9).fill(0));
2689
2769
  for (let i = 0; i < 9; i++) {
2690
2770
  for (let j = 0; j < 9; j++) {
@@ -2693,44 +2773,82 @@ class MotionLivenessDetector {
2693
2773
  }
2694
2774
  }
2695
2775
  }
2696
- // 求ATA的最小特征向量(对应最小特征值)
2776
+ // 求 A^T*A 的最小特征向量
2777
+ // 最小特征向量对应最小特征值,使 ||Ah|| 最小
2697
2778
  const eigenVec = this.getSmallestEigenvector(ATA);
2698
2779
  return eigenVec;
2699
2780
  }
2700
2781
  /**
2701
- * 求3x3对称矩阵的最小特征向量(简化版本)
2702
- * 使用幂迭代法或直接求解
2782
+ * 求9x9对称矩阵(A^T*A)的最小特征向量
2783
+ * 使用改进的迭代方法
2784
+ *
2785
+ * 原理:
2786
+ * - A^T*A 是对称半正定矩阵
2787
+ * - 最小特征值对应的特征向量是最小二乘解
2788
+ * - 使用迭代幂法(Power Iteration)的变种求最小特征向量
2703
2789
  */
2704
2790
  getSmallestEigenvector(mat) {
2705
2791
  if (mat.length !== 9)
2706
2792
  return null;
2707
- // 为了简化,使用初步估计:取行和最小的方向
2708
- // 或者使用固定迭代次数的幂法
2709
- // 简化:返回一个初步猜测的向量
2710
- // 实际应用应该使用完整的SVD或特征值分解库
2711
- // 这里使用Power Iteration的反向版本(找最小特征值)
2712
- let v = [1, 1, 1, 1, 1, 1, 1, 1, 1];
2713
- for (let iter = 0; iter < 10; iter++) {
2714
- // v_new = A * v
2715
- const v_new = [0, 0, 0, 0, 0, 0, 0, 0, 0];
2793
+ // 初始随机向量
2794
+ let v = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // 初始值更稳定
2795
+ let prevEigenvalue = Infinity;
2796
+ // 迭代求解最小特征值对应的特征向量
2797
+ for (let iter = 0; iter < 50; iter++) {
2798
+ // 计算 A*v
2799
+ const Av = [0, 0, 0, 0, 0, 0, 0, 0, 0];
2716
2800
  for (let i = 0; i < 9; i++) {
2717
2801
  for (let j = 0; j < 9; j++) {
2718
- v_new[i] += mat[i][j] * v[j];
2802
+ Av[i] += mat[i][j] * v[j];
2719
2803
  }
2720
2804
  }
2721
- // 归一化
2722
- const norm = Math.sqrt(v_new.reduce((a, b) => a + b * b, 0));
2723
- if (norm < 0.0001)
2805
+ // 计算 Rayleigh 商(特征值估计)
2806
+ let vTAv = 0;
2807
+ let vTv = 0;
2808
+ for (let i = 0; i < 9; i++) {
2809
+ vTAv += v[i] * Av[i];
2810
+ vTv += v[i] * v[i];
2811
+ }
2812
+ const eigenvalue = vTv > 1e-10 ? vTAv / vTv : 0;
2813
+ // 标准化 Av
2814
+ const AvNorm = Math.sqrt(Av.reduce((a, b) => a + b * b, 0));
2815
+ if (AvNorm < 1e-10) {
2816
+ break;
2817
+ }
2818
+ // 更新 v
2819
+ for (let i = 0; i < 9; i++) {
2820
+ v[i] = Av[i] / AvNorm;
2821
+ }
2822
+ // 收敛判断:特征值变化很小
2823
+ if (Math.abs(eigenvalue - prevEigenvalue) < 1e-8) {
2724
2824
  break;
2825
+ }
2826
+ prevEigenvalue = eigenvalue;
2827
+ }
2828
+ // 最后做一次归一化
2829
+ const norm = Math.sqrt(v.reduce((a, b) => a + b * b, 0));
2830
+ if (norm > 1e-10) {
2725
2831
  for (let i = 0; i < 9; i++) {
2726
- v[i] = v_new[i] / norm;
2832
+ v[i] = v[i] / norm;
2727
2833
  }
2728
2834
  }
2729
2835
  return v;
2730
2836
  }
2731
2837
  /**
2732
- * 反演应化矩阵
2733
- * H_orig = T_dst^-1 * H_norm * T_src
2838
+ * 反演应化矩阵 - 从归一化坐标回到原始图像坐标
2839
+ *
2840
+ * 原理:
2841
+ * - 在归一化坐标系中估算的H矩阵需要转换回原始坐标
2842
+ *
2843
+ * 公式:H_orig = T_dst^(-1) * H_norm * T_src
2844
+ *
2845
+ * 说明:
2846
+ * - T_src:源点的归一化变换矩阵
2847
+ * - T_dst:目标点的归一化变换矩阵
2848
+ * - H_norm:在归一化坐标中计算得到的3×3矩阵
2849
+ * - H_orig:最终的单应性矩阵(用于原始图像坐标)
2850
+ *
2851
+ * 验证:p'_orig = H_orig * p_src_orig
2734
2852
  */
2735
2853
  denormalizeHomography(h, srcNorm, dstNorm) {
2736
2854
  // 将向量h转换为3x3矩阵
@@ -2750,6 +2868,30 @@ class MotionLivenessDetector {
2750
2868
  const H = this.multiplyMatrix3x3(temp, T_src);
2751
2869
  return H;
2752
2870
  }
2871
+ /**
2872
+ * 检查点集的分布范围(防止点集集中在小区域)
2873
+ * 返回值:0-1,值越大说明分布越分散
2874
+ */
2875
+ checkPointSpread(points) {
2876
+ if (points.length < 2)
2877
+ return 0;
2878
+ let minX = Infinity, maxX = -Infinity;
2879
+ let minY = Infinity, maxY = -Infinity;
2880
+ for (const p of points) {
2881
+ minX = Math.min(minX, p[0]);
2882
+ maxX = Math.max(maxX, p[0]);
2883
+ minY = Math.min(minY, p[1]);
2884
+ maxY = Math.max(maxY, p[1]);
2885
+ }
2886
+ const rangeX = maxX - minX;
2887
+ const rangeY = maxY - minY;
2888
+ const area = rangeX * rangeY;
2889
+ // 点集的相对面积(相对于整个图像,假设1920x1080)
2890
+ // 面积越大,点的分布越分散
2891
+ const imageArea = 1920 * 1080;
2892
+ const relativeArea = Math.min(area / imageArea, 1);
2893
+ return relativeArea;
2894
+ }
2753
2895
  /**
2754
2896
  * 3x3矩阵求逆
2755
2897
  */
@@ -2796,23 +2938,59 @@ class MotionLivenessDetector {
2796
2938
  return result;
2797
2939
  }
2798
2940
  /**
2799
- * 应用单应性变换(齐次坐标)
2800
- * p' = H * p / (H * p 的 Z 分量)
2941
+ * 应用单应性变换 - 齐次坐标变换与反齐次化
2942
+ *
2943
+ * 操作步骤:
2944
+ * 1. 输入:(x, y) → 齐次坐标 [x, y, 1]
2945
+ * 2. 矩阵乘法:H * p 得到齐次结果 [x'w, y'w, w]
2946
+ * 3. 反齐次化:[x'w/w, y'w/w] = [x', y']
2947
+ *
2948
+ * 公式:
2949
+ * [x'] [h11 h12 h13] [x]
2950
+ * [y' ] = [h21 h22 h23] [y]
2951
+ * [w'] [h31 h32 h33] [1]
2952
+ *
2953
+ * 最后:x' = x'w / w, y' = y'w / w
2954
+ *
2955
+ * 注意:如果 w' ≈ 0,点被投影到无穷远(退化情况)
2801
2956
  */
2802
2957
  applyHomography(H, x, y) {
2803
- // 齐次坐标
2958
+ // 齐次坐标表示
2804
2959
  const p = [x, y, 1];
2805
- // H * p
2960
+ // 矩阵乘法:H * p
2806
2961
  const Hp = [
2807
2962
  H[0][0] * p[0] + H[0][1] * p[1] + H[0][2] * p[2],
2808
2963
  H[1][0] * p[0] + H[1][1] * p[1] + H[1][2] * p[2],
2809
2964
  H[2][0] * p[0] + H[2][1] * p[1] + H[2][2] * p[2]
2810
2965
  ];
2811
- // 反齐次化
2966
+ // 反齐次化:除以齐次坐标的 Z 分量
2967
+ // 如果 w ≈ 0,则点在无穷远,返回齐次结果
2812
2968
  if (Math.abs(Hp[2]) < 0.0001) {
2813
- return [Hp[0], Hp[1]];
2969
+ return [Hp[0], Hp[1]]; // 异常情况:接近无穷
2814
2970
  }
2815
- return [Hp[0] / Hp[2], Hp[1] / Hp[2]];
2971
+ return [Hp[0] / Hp[2], Hp[1] / Hp[2]]; // 正常情况:反齐次化
2972
+ }
2973
+ /**
2974
+ * 【新增】计算点集的特征尺度(点间的平均距离)
2975
+ *
2976
+ * 用于相对误差计算,使算法对不同分辨率和点集大小更鲁棒
2977
+ */
2978
+ computeCharacteristicScale(points) {
2979
+ if (points.length < 2)
2980
+ return 1;
2981
+ let totalDist = 0;
2982
+ let count = 0;
2983
+ // 采样计算点间距离,避免 O(n²) 复杂度
2984
+ const sampleSize = Math.min(points.length, 30);
2985
+ for (let i = 0; i < sampleSize; i++) {
2986
+ for (let j = i + 1; j < sampleSize; j++) {
2987
+ const dx = points[i][0] - points[j][0];
2988
+ const dy = points[i][1] - points[j][1];
2989
+ totalDist += Math.sqrt(dx * dx + dy * dy);
2990
+ count++;
2991
+ }
2992
+ }
2993
+ return count > 0 ? totalDist / count : 1;
2816
2994
  }
2817
2995
  /**
2818
2996
  * 【新增】检查单应性矩阵的一致性
@@ -3017,7 +3195,7 @@ class MotionLivenessDetector {
3017
3195
  * 逆向检测优先级更高,因为照片几何约束是物理定律,无法伪造
3018
3196
  */
3019
3197
  makeLivenessDecision(eyeActivity, mouthActivity, muscleActivity, photoGeometry) {
3020
- if (!this.isReady()) {
3198
+ if (!this.collectedMinFrames()) {
3021
3199
  return true; // 数据不足,默认通过
3022
3200
  }
3023
3201
  // ============ 逆向检测(照片几何特征)============
@@ -4246,8 +4424,8 @@ class FaceDetectionEngine extends SimpleEventEmitter {
4246
4424
  }
4247
4425
  // 静默活体检测
4248
4426
  const motionResult = this.detectionState.motionDetector.analyzeMotion(face, faceBox);
4249
- // 只有ready状态的检测器的结果才可信
4250
- if (this.detectionState.motionDetector.isReady()) {
4427
+ if (this.detectionState.motionDetector.collectedMinFrames()) {
4428
+ // 采集到最小帧数后,否定性判定才可信
4251
4429
  if (!motionResult.isLively) {
4252
4430
  this.emitDebug('motion-detection', 'Motion liveness check failed - possible photo attack', {
4253
4431
  details: motionResult.details,
@@ -4261,17 +4439,20 @@ class FaceDetectionEngine extends SimpleEventEmitter {
4261
4439
  this.partialResetDetectionState();
4262
4440
  return;
4263
4441
  }
4264
- this.emitDebug('motion-detection', 'Motion liveness check passed', {
4265
- debug: motionResult.debug,
4266
- details: motionResult.details,
4267
- }, 'warn');
4268
- this.detectionState.liveness = true;
4269
- }
4270
- else {
4271
- this.emitDebug('motion-detection', 'Motion liveness detector not ready yet', {
4272
- debug: motionResult.debug,
4273
- details: motionResult.details,
4274
- }, 'warn');
4442
+ // 采集到足够帧数后,肯定性判定才可信
4443
+ if (this.detectionState.motionDetector.collectedFullFrames()) {
4444
+ this.emitDebug('motion-detection', 'Motion liveness check passed', {
4445
+ debug: motionResult.debug,
4446
+ details: motionResult.details,
4447
+ }, 'warn');
4448
+ this.detectionState.liveness = true;
4449
+ }
4450
+ else {
4451
+ this.emitDebug('motion-detection', 'Motion liveness check ongoing - collecting more frames', {
4452
+ debug: motionResult.debug,
4453
+ details: motionResult.details,
4454
+ }, 'warn');
4455
+ }
4275
4456
  }
4276
4457
  // 动作活体检测阶段处理
4277
4458
  if (this.detectionState.period === DetectionPeriod.VERIFY) {