@sssxyd/face-liveness-detector 0.4.1-beta.8 → 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 CHANGED
@@ -2525,12 +2525,23 @@ class MotionLivenessDetector {
2525
2525
  // 【策略改进】优先使用最近的帧对(尽早检测)
2526
2526
  // 照片的几何约束是瞬间的,2帧就足够;多帧用来验证一致性
2527
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
+ });
2528
2533
  // 计算最近帧对的变换误差(重点在最近的帧)
2529
2534
  for (let i = Math.max(1, this.faceLandmarksHistory.length - recentFrameCount); i < this.faceLandmarksHistory.length; i++) {
2530
2535
  const frame1 = this.faceLandmarksHistory[i - 1];
2531
2536
  const frame2 = this.faceLandmarksHistory[i];
2532
- if (frame1.length < 100 || frame2.length < 100)
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)`);
2533
2543
  continue; // 至少100个有效点
2544
+ }
2534
2545
  // 【改进】收集所有有效的点对(而不是只采样10个点)
2535
2546
  // 这给出更好的H矩阵估计
2536
2547
  const srcPoints = [];
@@ -2551,30 +2562,61 @@ class MotionLivenessDetector {
2551
2562
  }
2552
2563
  }
2553
2564
  }
2554
- if (srcPoints.length < 10)
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)`);
2555
2571
  continue; // 至少10个匹配点对(DLT最少需要4个)
2572
+ }
2556
2573
  // 保存这一组点对(用于后面计算特征尺度)
2557
2574
  lastSrcPoints = srcPoints;
2558
2575
  // 【新增】使用DLT算法计算完整的3x3单应性矩阵
2559
2576
  const H = this.estimateHomographyDLT(srcPoints, dstPoints);
2560
- if (!H)
2577
+ if (!H) {
2578
+ console.debug(`[HomographyConstraint] DLT failed to estimate homography`);
2561
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
+ });
2562
2588
  homographyMatrices.push(H);
2563
2589
  // 【改进】使用单应性矩阵计算误差(而不是仿射变换)
2564
2590
  let frameError = 0;
2565
2591
  let validCount = 0;
2592
+ const sampleErrors = [];
2566
2593
  for (let j = 0; j < srcPoints.length; j++) {
2567
2594
  const transformed = this.applyHomography(H, srcPoints[j][0], srcPoints[j][1]);
2568
2595
  const actual = dstPoints[j];
2569
2596
  const error = Math.sqrt((transformed[0] - actual[0]) ** 2 + (transformed[1] - actual[1]) ** 2);
2570
2597
  frameError += error;
2571
2598
  validCount++;
2599
+ // 记录前5个误差用于调试
2600
+ if (j < 5) {
2601
+ sampleErrors.push(error);
2602
+ }
2572
2603
  }
2573
2604
  if (validCount > 0) {
2574
- errors.push(frameError / validCount);
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
+ });
2575
2612
  }
2576
2613
  }
2614
+ console.debug(`[HomographyConstraint] Error collection complete`, {
2615
+ totalErrors: errors.length,
2616
+ matrixCount: homographyMatrices.length
2617
+ });
2577
2618
  if (errors.length === 0) {
2619
+ console.debug(`[HomographyConstraint] No errors computed, returning 0`);
2578
2620
  return { planarScore: 0, error: 0 };
2579
2621
  }
2580
2622
  // 计算所有帧的平均误差
@@ -2595,10 +2637,12 @@ class MotionLivenessDetector {
2595
2637
  // relativeError > 0.1 → 很可能是立体(活体)
2596
2638
  const errorScore = Math.max(0, 1 - relativeError / 0.1);
2597
2639
  const planarScore = errorScore * matrixConsistency;
2598
- console.debug('[HomographyConstraint]', {
2640
+ console.debug('[HomographyConstraint] FINAL RESULT', {
2599
2641
  recentFrameCount,
2600
2642
  frameCount: errors.length,
2601
2643
  avgError: avgError.toFixed(4),
2644
+ characteristicScale: characteristicScale.toFixed(4),
2645
+ relativeError: relativeError.toFixed(4),
2602
2646
  errorScore: errorScore.toFixed(3),
2603
2647
  matrixConsistency: matrixConsistency.toFixed(3),
2604
2648
  planarScore: planarScore.toFixed(3),
@@ -2632,14 +2676,23 @@ class MotionLivenessDetector {
2632
2676
  * 4. 反演应化矩阵到原始坐标系
2633
2677
  */
2634
2678
  estimateHomographyDLT(src, dst) {
2635
- 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 });
2636
2681
  return null;
2682
+ }
2637
2683
  const n = src.length;
2638
2684
  // 【关键】对点进行归一化,提高数值稳定性
2639
2685
  const srcNorm = this.normalizePoints(src);
2640
2686
  const dstNorm = this.normalizePoints(dst);
2641
- if (!srcNorm || !dstNorm)
2687
+ if (!srcNorm || !dstNorm) {
2688
+ console.debug('[DLT] Point normalization failed');
2642
2689
  return null;
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
+ });
2643
2696
  // 构建DLT方程矩阵 A (2n × 9)
2644
2697
  //
2645
2698
  // 标准DLT形式推导:
@@ -2780,59 +2833,113 @@ class MotionLivenessDetector {
2780
2833
  }
2781
2834
  /**
2782
2835
  * 求9x9对称矩阵(A^T*A)的最小特征向量
2783
- * 使用改进的迭代方法
2784
2836
  *
2785
- * 原理:
2786
- * - A^T*A 是对称半正定矩阵
2787
- * - 最小特征值对应的特征向量是最小二乘解
2788
- * - 使用迭代幂法(Power Iteration)的变种求最小特征向量
2837
+ * 【关键】使用 Jacobi 特征值分解而不是幂法
2838
+ *
2839
+ * 问题分析:
2840
+ * - 迭代幂法(Power Iteration)求的是**最大**特征值的特征向量
2841
+ * - 我们需要**最小**特征值的特征向量
2842
+ * - 之前的幂法实现虽然标记为最小,但实际求的是最大 → 算法失败!
2843
+ *
2844
+ * 解决方案:
2845
+ * 1. 使用 QR 算法或 Jacobi 方法求完整的特征值分解
2846
+ * 2. 选择最小特征值对应的特征向量
2847
+ *
2848
+ * 此处实现简化的 Jacobi 迭代:
2849
+ * - 对称矩阵对角化
2850
+ * - 提取最小特征值的特征向量
2789
2851
  */
2790
2852
  getSmallestEigenvector(mat) {
2791
2853
  if (mat.length !== 9)
2792
2854
  return null;
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];
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
+ // 找非对角元素中绝对值最大的
2800
2868
  for (let i = 0; i < 9; i++) {
2801
- for (let j = 0; j < 9; j++) {
2802
- Av[i] += mat[i][j] * v[j];
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
+ }
2803
2875
  }
2804
2876
  }
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) {
2877
+ // 收敛判断
2878
+ if (maxOffDiag < 1e-10) {
2816
2879
  break;
2817
2880
  }
2818
- // 更新 v
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列的其他元素
2819
2899
  for (let i = 0; i < 9; i++) {
2820
- v[i] = Av[i] / AvNorm;
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
+ }
2821
2908
  }
2822
- // 收敛判断:特征值变化很小
2823
- if (Math.abs(eigenvalue - prevEigenvalue) < 1e-8) {
2824
- break;
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;
2825
2919
  }
2826
- prevEigenvalue = eigenvalue;
2827
2920
  }
2828
- // 最后做一次归一化
2829
- const norm = Math.sqrt(v.reduce((a, b) => a + b * b, 0));
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));
2830
2937
  if (norm > 1e-10) {
2831
2938
  for (let i = 0; i < 9; i++) {
2832
- v[i] = v[i] / norm;
2939
+ eigenvector[i] = eigenvector[i] / norm;
2833
2940
  }
2834
2941
  }
2835
- return v;
2942
+ return eigenvector;
2836
2943
  }
2837
2944
  /**
2838
2945
  * 反演应化矩阵 - 从归一化坐标回到原始图像坐标