@sssxyd/face-liveness-detector 0.4.2-alpha.2 → 0.4.3-alpha.1

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
@@ -41,11 +41,11 @@ var DetectionCode;
41
41
  DetectionCode["FACE_TOO_SMALL"] = "FACE_TOO_SMALL";
42
42
  DetectionCode["FACE_TOO_LARGE"] = "FACE_TOO_LARGE";
43
43
  DetectionCode["FACE_NOT_FRONTAL"] = "FACE_NOT_FRONTAL";
44
- DetectionCode["FACE_NOT_LIVE"] = "FACE_NOT_LIVE";
45
44
  DetectionCode["FACE_LOW_QUALITY"] = "FACE_LOW_QUALITY";
46
45
  DetectionCode["FACE_CHECK_PASS"] = "FACE_CHECK_PASS";
47
- DetectionCode["PLEASE_MOVING_FACE"] = "PLEASE_MOVING_FACE";
46
+ DetectionCode["FACE_NOT_MOVING"] = "FACE_NOT_MOVING";
48
47
  DetectionCode["PHOTO_ATTACK_DETECTED"] = "PHOTO_ATTACK_DETECTED";
48
+ DetectionCode["SCREEN_ATTACK_DETECTED"] = "SCREEN_ATTACK_DETECTED";
49
49
  })(DetectionCode || (DetectionCode = {}));
50
50
  /**
51
51
  * Error code enumeration
@@ -84,6 +84,9 @@ const DEFAULT_OPTIONS$2 = {
84
84
  debug_log_level: 'info',
85
85
  debug_log_stages: undefined, // undefined 表示所有阶段
86
86
  debug_log_throttle: 100, // 默认 100ms 节流,防止过于频繁
87
+ enable_face_moving_detection: true,
88
+ enable_photo_attack_detection: true,
89
+ enable_screen_attack_detection: true,
87
90
  // Detection Settings
88
91
  detect_video_ideal_width: 1280,
89
92
  detect_video_ideal_height: 720,
@@ -1562,11 +1565,13 @@ function matToBase64Jpeg(cv, mat, quality = 0.9) {
1562
1565
  class FaceMovingDetectionResult {
1563
1566
  isMoving;
1564
1567
  details;
1565
- debug;
1566
- constructor(isMoving, details, debug = {}) {
1568
+ available = false;
1569
+ trusted = false;
1570
+ constructor(isMoving, details, available = true, trusted = false) {
1567
1571
  this.isMoving = isMoving;
1568
1572
  this.details = details;
1569
- this.debug = debug;
1573
+ this.available = available;
1574
+ this.trusted = trusted;
1570
1575
  }
1571
1576
  getMessage() {
1572
1577
  if (this.details.frameCount < 2) {
@@ -1682,7 +1687,7 @@ class FaceMovingDetector {
1682
1687
  details.lastCentroidShift = this.calculateCentroidShift(lastFrame.result);
1683
1688
  // 计算中心化坐标的变化速率(基于实际时间)
1684
1689
  details.centroidShiftRate = this.calculateCentroidShiftRate();
1685
- return new FaceMovingDetectionResult(details.isMoving, details);
1690
+ return new FaceMovingDetectionResult(details.isMoving, details, true, true);
1686
1691
  }
1687
1692
  /**
1688
1693
  * 计算两帧之间的运动强度
@@ -1840,17 +1845,6 @@ class FaceMovingDetector {
1840
1845
  const variance = squaredDiffs.reduce((a, b) => a + b) / values.length;
1841
1846
  return Math.sqrt(variance);
1842
1847
  }
1843
- /**
1844
- * 检查当前实例是否可用
1845
- *
1846
- * @description 通过检查帧缓冲区长度来判断实例是否处于可用状态
1847
- * 当帧缓冲区长度大于等于2时,认为实例可用
1848
- *
1849
- * @returns {boolean} 如果实例可用返回true,否则返回false
1850
- */
1851
- isAvailable() {
1852
- return this.frameBuffer.length >= 2;
1853
- }
1854
1848
  /**
1855
1849
  * 重置检测器
1856
1850
  */
@@ -1903,17 +1897,13 @@ class FaceMovingDetector {
1903
1897
  class PhotoAttackDetectionResult {
1904
1898
  isPhoto;
1905
1899
  details;
1906
- debug;
1907
- constructor(isPhoto, details, debug = {}) {
1900
+ available = false;
1901
+ trusted = false;
1902
+ constructor(isPhoto, details, available = false, trusted = false) {
1908
1903
  this.isPhoto = isPhoto;
1909
1904
  this.details = details;
1910
- this.debug = debug;
1911
- }
1912
- isAvailable() {
1913
- return this.details.frameCount >= 3;
1914
- }
1915
- isTrusted() {
1916
- return this.details.frameCount >= 15;
1905
+ this.available = available;
1906
+ this.trusted = trusted;
1917
1907
  }
1918
1908
  getMessage() {
1919
1909
  if (this.details.frameCount < 3) {
@@ -1928,9 +1918,9 @@ class PhotoAttackDetectionResult {
1928
1918
  reasons.push(`深度方差极小(${depthVar})`);
1929
1919
  }
1930
1920
  if (this.details.perspectiveScore > 0.5) {
1931
- this.details.motionDisplacementVariance.toFixed(3);
1921
+ const motionVar = this.details.motionDisplacementVariance.toFixed(3);
1932
1922
  const consistency = (this.details.motionDirectionConsistency * 100).toFixed(0);
1933
- reasons.push(`运动一致性过高(${consistency}%)`);
1923
+ reasons.push(`运动一致性过高(${consistency}%),位移方差(${motionVar})`);
1934
1924
  }
1935
1925
  const reasonStr = reasons.length > 0 ? `(${reasons.join('、')})` : '';
1936
1926
  return `检测到照片攻击${reasonStr},置信度 ${confidence}%`;
@@ -1938,8 +1928,9 @@ class PhotoAttackDetectionResult {
1938
1928
  }
1939
1929
  const DEFAULT_OPTIONS = {
1940
1930
  frameBufferSize: 15, // 15帧 (0.5秒@30fps)
1941
- depthVarianceThreshold: 0.001, // 深度方差阈值:真实人脸 > 0.005,照片 < 0.001
1942
- motionVarianceThreshold: 0.01, // 运动方差阈值:真实人脸 > 0.02,照片 < 0.01
1931
+ requiredFrameCount: 15, // 可信赖所需的最小帧数
1932
+ depthVarianceThreshold: 0.003, // 深度方差阈值:真实人脸 > 0.005,照片 < 0.001
1933
+ motionVarianceThreshold: 0.015, // 运动方差阈值:真实人脸 > 0.02,照片 < 0.01
1943
1934
  perspectiveRatioThreshold: 0.85, // 透视比率阈值:真实人脸 > 0.95,照片 < 0.85
1944
1935
  motionConsistencyThreshold: 0.8, // 运动一致性阈值:真实人脸 < 0.5,照片 > 0.8
1945
1936
  };
@@ -2033,7 +2024,7 @@ class PhotoAttackDetector {
2033
2024
  else {
2034
2025
  details.dominantFeature = 'perspective';
2035
2026
  }
2036
- return new PhotoAttackDetectionResult(details.isPhoto, details);
2027
+ return new PhotoAttackDetectionResult(details.isPhoto, details, true, this.frameBuffer.length >= this.config.requiredFrameCount);
2037
2028
  }
2038
2029
  /**
2039
2030
  * 方案一:3D 深度方差分析
@@ -2356,11 +2347,382 @@ class PhotoAttackDetector {
2356
2347
  reset() {
2357
2348
  this.frameBuffer = [];
2358
2349
  }
2350
+ }
2351
+
2352
+ /**
2353
+ * 屏幕攻击检测结果
2354
+ */
2355
+ class ScreenAttackDetectionResult {
2356
+ isScreenAttack;
2357
+ details;
2358
+ available = false;
2359
+ trusted = false;
2360
+ constructor(isScreenAttack, details, available = false, trusted = false) {
2361
+ this.isScreenAttack = isScreenAttack;
2362
+ this.details = details;
2363
+ this.available = available;
2364
+ this.trusted = trusted;
2365
+ }
2366
+ getMessage() {
2367
+ if (this.details.frameCount < 1) {
2368
+ return '未获得足够数据,无法进行屏幕攻击检测';
2369
+ }
2370
+ if (!this.isScreenAttack)
2371
+ return '';
2372
+ const confidence = (this.details.screenAttackConfidence * 100).toFixed(2);
2373
+ const reasons = [];
2374
+ if (this.details.moireConfidence > 0.6) {
2375
+ const moireScore = (this.details.moireScore * 100).toFixed(2);
2376
+ reasons.push(`摩尔纹特征明显(${moireScore})`);
2377
+ }
2378
+ const reasonStr = reasons.length > 0 ? `(${reasons.join('、')})` : '';
2379
+ return `检测到屏幕攻击${reasonStr},置信度 ${confidence}%`;
2380
+ }
2381
+ }
2382
+ const DEFAULT_SCREEN_OPTIONS = {
2383
+ moireThreshold: 0.65,
2384
+ pixelGridSensitivity: 0.75,
2385
+ requiredFrameCount: 12,
2386
+ earlyDetectionThreshold: 0.8
2387
+ };
2388
+ /**
2389
+ * 屏幕攻击检测器
2390
+ *
2391
+ * 检测方案:
2392
+ * 摩尔纹/像素网格分析(检测屏幕特有的周期性图案)
2393
+ */
2394
+ class ScreenAttackDetector {
2395
+ config;
2396
+ opencv = null;
2397
+ frameCount = 0;
2398
+ emitDebug = () => { }; // 默认空实现
2399
+ constructor(options) {
2400
+ this.config = { ...DEFAULT_SCREEN_OPTIONS, ...options };
2401
+ }
2359
2402
  /**
2360
- * 获取当前缓冲区中的帧数
2403
+ * 设置 OpenCV 实例
2404
+ * @param opencv - TechStark opencv.js 实例
2361
2405
  */
2362
- getFrameCount() {
2363
- return this.frameBuffer.length;
2406
+ setOpencv(opencv) {
2407
+ this.opencv = opencv;
2408
+ }
2409
+ /**
2410
+ * 设置 emitDebug 方法(依赖注入)
2411
+ * @param emitDebugFn - 来自 FaceDetectionEngine 的 emitDebug 方法
2412
+ */
2413
+ setEmitDebug(emitDebugFn) {
2414
+ this.emitDebug = emitDebugFn;
2415
+ }
2416
+ /**
2417
+ * 检测屏幕攻击
2418
+ * @param faceBox - 人脸区域框
2419
+ * @param colorMat - 彩色图像矩阵
2420
+ * @param grayMat - 灰度图像矩阵
2421
+ * @returns 检测结果
2422
+ */
2423
+ detect(colorMat, grayMat) {
2424
+ this.frameCount += 1;
2425
+ const details = {
2426
+ frameCount: this.frameCount,
2427
+ moireScore: 0,
2428
+ pixelGridStrength: 0,
2429
+ moireConfidence: 0,
2430
+ isScreenAttack: false,
2431
+ screenAttackConfidence: 0,
2432
+ feature: 'moire',
2433
+ debugInfo: {}
2434
+ };
2435
+ // 检查OpenCV是否已设置
2436
+ if (!this.opencv) {
2437
+ this.emitDebug('screen-attack', 'OpenCV未初始化', {}, 'warn');
2438
+ return new ScreenAttackDetectionResult(false, details);
2439
+ }
2440
+ try {
2441
+ // ============ 摩尔纹/像素网格分析 ============
2442
+ const moireAnalysis = this.analyzeMoirePattern(grayMat);
2443
+ details.moireScore = moireAnalysis.score;
2444
+ details.pixelGridStrength = moireAnalysis.pixelGridStrength;
2445
+ details.moireConfidence = moireAnalysis.confidence;
2446
+ details.debugInfo.moireAnalysis = moireAnalysis.debug;
2447
+ // 早期检测:如果摩尔纹分数过高,直接判定为屏幕攻击
2448
+ if (details.moireScore > this.config.earlyDetectionThreshold) {
2449
+ details.isScreenAttack = true;
2450
+ details.screenAttackConfidence = details.moireScore;
2451
+ details.feature = 'moire';
2452
+ this.emitDebug('screen-attack', '摩尔纹分析早期检测触发', {
2453
+ moireScore: details.moireScore,
2454
+ earlyThreshold: this.config.earlyDetectionThreshold
2455
+ });
2456
+ return new ScreenAttackDetectionResult(true, details, true, true);
2457
+ }
2458
+ // 基于摩尔纹置信度的判定
2459
+ details.isScreenAttack = details.moireConfidence > this.config.moireThreshold;
2460
+ details.screenAttackConfidence = details.moireScore;
2461
+ this.emitDebug('screen-attack', '摩尔纹分析完成', {
2462
+ moireScore: details.moireScore,
2463
+ isScreenAttack: details.isScreenAttack
2464
+ });
2465
+ return new ScreenAttackDetectionResult(details.isScreenAttack, details, true, this.frameCount >= this.config.requiredFrameCount);
2466
+ }
2467
+ catch (error) {
2468
+ this.emitDebug('screen-attack', `检测过程中发生错误: ${error}`, {}, 'error');
2469
+ return new ScreenAttackDetectionResult(false, details, false, false);
2470
+ }
2471
+ }
2472
+ /**
2473
+ * 摩尔纹/像素网格分析
2474
+ *
2475
+ * 原理:
2476
+ * - 真实人脸:纹理在频域中分布较为随机
2477
+ * - 屏幕显示:像素阵列在频域中产生特有的摩尔纹和周期性图案
2478
+ * - 摄像头采样与屏幕像素网格的频率混叠效应
2479
+ */
2480
+ analyzeMoirePattern(grayMat) {
2481
+ const cv = this.opencv;
2482
+ let debugInfo = {};
2483
+ try {
2484
+ // 分析整个图像,而不是特定区域
2485
+ const analysisMat = grayMat;
2486
+ // 确保图像是浮点类型用于FFT
2487
+ const floatMat = new cv.Mat();
2488
+ analysisMat.convertTo(floatMat, cv.CV_32F);
2489
+ // 创建复数矩阵用于FFT
2490
+ const planes = new cv.MatVector();
2491
+ const zeros = cv.Mat.zeros(floatMat.rows, floatMat.cols, cv.CV_32F);
2492
+ planes.push_back(floatMat);
2493
+ planes.push_back(zeros);
2494
+ // 执行2D FFT
2495
+ const complexI = new cv.Mat();
2496
+ cv.merge(planes, complexI);
2497
+ cv.dft(complexI, complexI, cv.DFT_COMPLEX_OUTPUT);
2498
+ // 计算幅度谱
2499
+ const mag = new cv.Mat();
2500
+ const planesVec = new cv.MatVector();
2501
+ cv.split(complexI, planesVec);
2502
+ const real = planesVec.get(0);
2503
+ const imag = planesVec.get(1);
2504
+ // 计算幅度: sqrt(real^2 + imag^2)
2505
+ cv.magnitude(real, imag, mag);
2506
+ // 转换为对数尺度以便可视化
2507
+ const matShift = new cv.Mat();
2508
+ mag.convertTo(matShift, cv.CV_32F);
2509
+ // 对数变换
2510
+ cv.add(cv.ones(mag.rows, mag.cols, cv.CV_32F), matShift, matShift);
2511
+ cv.log(matShift, matShift);
2512
+ // 重新排列四象限,使零频率分量位于中心
2513
+ const cx = Math.floor(matShift.cols / 2);
2514
+ const cy = Math.floor(matShift.rows / 2);
2515
+ const q0 = matShift.roi(new cv.Rect(0, 0, cx, cy));
2516
+ const q1 = matShift.roi(new cv.Rect(cx, 0, cx, cy));
2517
+ const q2 = matShift.roi(new cv.Rect(0, cy, cx, cy));
2518
+ const q3 = matShift.roi(new cv.Rect(cx, cy, cx, cy));
2519
+ const tmp = new cv.Mat();
2520
+ q0.copyTo(tmp);
2521
+ q3.copyTo(q0);
2522
+ tmp.copyTo(q3);
2523
+ q1.copyTo(tmp);
2524
+ q2.copyTo(q1);
2525
+ tmp.copyTo(q2);
2526
+ // 专门检测摩尔纹特征
2527
+ // 屏幕像素网格在频域中产生同心圆状的规律性模式
2528
+ const moireScore = this.analyzeMoireCharacteristics(matShift);
2529
+ const pixelGridStrength = this.calculatePixelGridStrength(matShift);
2530
+ // 综合分析摩尔纹特征
2531
+ let finalScore = 0;
2532
+ let patternStrength = 0;
2533
+ // 基于同心圆模式的分析
2534
+ if (moireScore > 0.3) {
2535
+ finalScore += moireScore * 0.6;
2536
+ }
2537
+ // 基于像素网格强度的分析
2538
+ if (pixelGridStrength > 0.4) {
2539
+ finalScore += pixelGridStrength * 0.4;
2540
+ }
2541
+ // 检查是否有清晰的周期性峰值
2542
+ const peaks = this.findMoirePeaks(matShift);
2543
+ const peakCount = peaks.length;
2544
+ if (peakCount > 15) { // 高峰值数量表明存在明显的摩尔纹
2545
+ finalScore += Math.min(0.3, peakCount / 100);
2546
+ }
2547
+ patternStrength = Math.max(moireScore, pixelGridStrength);
2548
+ // 限制分数范围
2549
+ finalScore = Math.min(1, Math.max(0, finalScore));
2550
+ // 计算置信度
2551
+ const confidence = finalScore * this.config.pixelGridSensitivity;
2552
+ debugInfo = {
2553
+ moireScore,
2554
+ pixelGridStrength,
2555
+ peakCount,
2556
+ patternStrength,
2557
+ rawScore: finalScore
2558
+ };
2559
+ // 释放内存
2560
+ floatMat.delete();
2561
+ complexI.delete();
2562
+ mag.delete();
2563
+ matShift.delete();
2564
+ planes.delete();
2565
+ zeros.delete();
2566
+ planesVec.delete();
2567
+ real.delete();
2568
+ imag.delete();
2569
+ tmp.delete();
2570
+ return {
2571
+ score: finalScore,
2572
+ pixelGridStrength,
2573
+ confidence,
2574
+ debug: debugInfo
2575
+ };
2576
+ }
2577
+ catch (error) {
2578
+ this.emitDebug('screen-attack-moire', `摩尔纹分析出错: ${error}`, {}, 'error');
2579
+ return { score: 0, pixelGridStrength: 0, confidence: 0, debug: {} };
2580
+ }
2581
+ }
2582
+ /**
2583
+ * 分析频域中的摩尔纹特征
2584
+ */
2585
+ analyzeMoireCharacteristics(spectrum) {
2586
+ this.opencv;
2587
+ const centerX = Math.floor(spectrum.cols / 2);
2588
+ const centerY = Math.floor(spectrum.rows / 2);
2589
+ // 计算径向剖面图以检测同心圆模式
2590
+ const radialProfile = this.computeRadialProfile(spectrum, centerX, centerY);
2591
+ // 检查径向剖面的规律性(同心圆特征)
2592
+ let regularityScore = 0;
2593
+ Math.max(1, Math.floor(radialProfile.length / 10));
2594
+ // 计算径向剖面的自相关性来检测规律性
2595
+ const autoCorrelation = this.calculateAutoCorrelation(radialProfile);
2596
+ // 寻找主要的周期性模式
2597
+ let maxPeak = 0;
2598
+ for (let i = 1; i < autoCorrelation.length / 4; i++) {
2599
+ if (autoCorrelation[i] > maxPeak) {
2600
+ maxPeak = autoCorrelation[i];
2601
+ }
2602
+ }
2603
+ // 根据最大峰值确定摩尔纹特征强度
2604
+ regularityScore = Math.min(1, maxPeak / 10.0); // 归一化
2605
+ return regularityScore;
2606
+ }
2607
+ /**
2608
+ * 计算像素网格强度
2609
+ */
2610
+ calculatePixelGridStrength(spectrum) {
2611
+ const cv = this.opencv;
2612
+ // 计算频谱的标准差 - 高标准差可能表示周期性结构
2613
+ const meanStddev = { mean: new cv.Scalar(), stddev: new cv.Scalar() };
2614
+ cv.meanStdDev(spectrum, meanStddev.mean, meanStddev.stddev);
2615
+ const spectrumStdDev = meanStddev.stddev.data64F[0];
2616
+ // 归一化标准差到0-1范围
2617
+ const normalizedStdDev = Math.min(1, spectrumStdDev / 5.0);
2618
+ return normalizedStdDev;
2619
+ }
2620
+ /**
2621
+ * 查找频域中的摩尔纹峰值
2622
+ */
2623
+ findMoirePeaks(spectrum) {
2624
+ const cv = this.opencv;
2625
+ const peaks = [];
2626
+ // 使用局部最大值检测
2627
+ const kernel = cv.Mat.ones(3, 3, cv.CV_8UC1);
2628
+ // 膨胀操作以找到局部最大值
2629
+ const dilated = new cv.Mat();
2630
+ cv.dilate(spectrum, dilated, kernel);
2631
+ // 比较原图和膨胀后的图,相等的位置即为局部最大值
2632
+ const localMaxMask = new cv.Mat();
2633
+ cv.compare(spectrum, dilated, localMaxMask, cv.CMP_EQ);
2634
+ // 查找非零点(即峰值位置)
2635
+ for (let y = 1; y < spectrum.rows - 1; y++) {
2636
+ for (let x = 1; x < spectrum.cols - 1; x++) {
2637
+ if (localMaxMask.ucharPtr(y, x)[0] !== 0) {
2638
+ try {
2639
+ // 使用floatPtr访问浮点值
2640
+ const ptrValue = spectrum.floatPtr(y, x);
2641
+ if (ptrValue && ptrValue.length > 0) {
2642
+ const val = ptrValue[0];
2643
+ // 只保留显著的峰值
2644
+ if (val > 3.0) { // 阈值可根据实际调试调整
2645
+ peaks.push({ x, y, value: val });
2646
+ }
2647
+ }
2648
+ }
2649
+ catch (e) {
2650
+ // fallback to other method
2651
+ try {
2652
+ const val = spectrum.data32F[y * spectrum.cols + x];
2653
+ if (val > 3.0) {
2654
+ peaks.push({ x, y, value: val });
2655
+ }
2656
+ }
2657
+ catch (e2) {
2658
+ console.warn("Could not access pixel value at", x, y);
2659
+ }
2660
+ }
2661
+ }
2662
+ }
2663
+ }
2664
+ // 释放内存
2665
+ kernel.delete();
2666
+ dilated.delete();
2667
+ localMaxMask.delete();
2668
+ return peaks;
2669
+ }
2670
+ /**
2671
+ * 计算径向剖面图
2672
+ */
2673
+ computeRadialProfile(spectrum, centerX, centerY) {
2674
+ this.opencv;
2675
+ const profile = [];
2676
+ const maxRadius = Math.min(centerX, centerY);
2677
+ for (let r = 0; r < maxRadius; r++) {
2678
+ let sum = 0;
2679
+ let count = 0;
2680
+ for (let angle = 0; angle < 360; angle += 5) { // 每5度采样一次
2681
+ const rad = angle * Math.PI / 180;
2682
+ const x = Math.round(centerX + r * Math.cos(rad));
2683
+ const y = Math.round(centerY + r * Math.sin(rad));
2684
+ if (x >= 0 && x < spectrum.cols && y >= 0 && y < spectrum.rows) {
2685
+ try {
2686
+ const value = spectrum.floatPtr(y, x)[0];
2687
+ sum += value;
2688
+ count++;
2689
+ }
2690
+ catch (e) {
2691
+ try {
2692
+ const value = spectrum.data32F[y * spectrum.cols + x];
2693
+ sum += value;
2694
+ count++;
2695
+ }
2696
+ catch (e2) {
2697
+ // 忽略错误
2698
+ }
2699
+ }
2700
+ }
2701
+ }
2702
+ profile.push(count > 0 ? sum / count : 0);
2703
+ }
2704
+ return profile;
2705
+ }
2706
+ /**
2707
+ * 计算数组的自相关
2708
+ */
2709
+ calculateAutoCorrelation(data) {
2710
+ const n = data.length;
2711
+ const result = new Array(n).fill(0);
2712
+ for (let lag = 0; lag < n; lag++) {
2713
+ let sum = 0;
2714
+ for (let i = 0; i < n - lag; i++) {
2715
+ sum += data[i] * data[i + lag];
2716
+ }
2717
+ result[lag] = sum / (n - lag);
2718
+ }
2719
+ return result;
2720
+ }
2721
+ /**
2722
+ * 重置检测器
2723
+ */
2724
+ reset() {
2725
+ this.frameCount = 0;
2364
2726
  }
2365
2727
  }
2366
2728
 
@@ -2380,7 +2742,9 @@ class DetectionState {
2380
2742
  lastFrontalScore = 1;
2381
2743
  faceMovingDetector = null;
2382
2744
  photoAttackDetector = null;
2745
+ screenAttachDetector = null;
2383
2746
  liveness = false;
2747
+ realness = false;
2384
2748
  constructor(options) {
2385
2749
  Object.assign(this, options);
2386
2750
  }
@@ -2388,11 +2752,17 @@ class DetectionState {
2388
2752
  this.clearActionVerifyTimeout();
2389
2753
  const savedFaceMovingDetector = this.faceMovingDetector;
2390
2754
  const savedPhotoAttackDetector = this.photoAttackDetector;
2755
+ const savedScreenAttackDetector = this.screenAttachDetector;
2391
2756
  savedFaceMovingDetector?.reset();
2392
2757
  savedPhotoAttackDetector?.reset();
2758
+ savedScreenAttackDetector?.reset();
2393
2759
  Object.assign(this, new DetectionState({}));
2394
2760
  this.faceMovingDetector = savedFaceMovingDetector;
2395
2761
  this.photoAttackDetector = savedPhotoAttackDetector;
2762
+ this.screenAttachDetector = savedScreenAttackDetector;
2763
+ }
2764
+ setOpenCv(opencv) {
2765
+ this.screenAttachDetector?.setOpencv(opencv);
2396
2766
  }
2397
2767
  // 默认方法
2398
2768
  needFrontalFace() {
@@ -2401,7 +2771,7 @@ class DetectionState {
2401
2771
  // 是否准备好进行动作验证
2402
2772
  isReadyToVerify(minCollectCount) {
2403
2773
  if (this.period === DetectionPeriod.COLLECT
2404
- && this.liveness
2774
+ && this.liveness && this.realness
2405
2775
  && this.collectCount >= minCollectCount) {
2406
2776
  return true;
2407
2777
  }
@@ -2439,6 +2809,9 @@ function createDetectionState(engine) {
2439
2809
  detectionState.faceMovingDetector.setEmitDebug(engine.emitDebug.bind(engine));
2440
2810
  detectionState.photoAttackDetector = new PhotoAttackDetector();
2441
2811
  detectionState.photoAttackDetector.setEmitDebug(engine.emitDebug.bind(engine));
2812
+ detectionState.screenAttachDetector = new ScreenAttackDetector();
2813
+ detectionState.screenAttachDetector.setOpencv(cv);
2814
+ detectionState.screenAttachDetector.setEmitDebug(engine.emitDebug.bind(engine));
2442
2815
  return detectionState;
2443
2816
  }
2444
2817
 
@@ -2552,6 +2925,7 @@ class FaceDetectionEngine extends SimpleEventEmitter {
2552
2925
  }
2553
2926
  this.options = mergeOptions(options);
2554
2927
  this.detectionState = createDetectionState(this);
2928
+ this.detectionState.setOpenCv(this.cv);
2555
2929
  this.emitDebug('config', 'Engine options updated', { wasDetecting }, 'info');
2556
2930
  }
2557
2931
  getEngineState() {
@@ -2790,6 +3164,7 @@ class FaceDetectionEngine extends SimpleEventEmitter {
2790
3164
  if (!this.transitionEngineState(EngineState.READY, 'initialize() success')) {
2791
3165
  throw new Error('Failed to transition to READY state');
2792
3166
  }
3167
+ this.detectionState.setOpenCv(this.cv);
2793
3168
  const loadedData = {
2794
3169
  success: true,
2795
3170
  opencv_version: getOpenCVVersion(),
@@ -3282,6 +3657,16 @@ class FaceDetectionEngine extends SimpleEventEmitter {
3282
3657
  this.stopDetection(false);
3283
3658
  return;
3284
3659
  }
3660
+ if (!this.detectionState.screenAttachDetector) {
3661
+ this.emit('detector-error', {
3662
+ code: ErrorCode.INTERNAL_ERROR,
3663
+ message: 'Screen attack detector is not initialized'
3664
+ });
3665
+ // Clear the detecting flag before stopping to avoid deadlock
3666
+ this.isDetectingFrameActive = false;
3667
+ this.stopDetection(false);
3668
+ return;
3669
+ }
3285
3670
  try {
3286
3671
  // 动作活体检测阶段处理
3287
3672
  if (this.detectionState.period === DetectionPeriod.VERIFY) {
@@ -3302,55 +3687,51 @@ class FaceDetectionEngine extends SimpleEventEmitter {
3302
3687
  this.emitDebug('detection', 'Face is too large', { ratio: faceRatio.toFixed(4), minRatio: this.options.collect_min_face_ratio, maxRatio: this.options.collect_max_face_ratio }, 'info');
3303
3688
  return;
3304
3689
  }
3305
- this.detectionState.faceMovingDetector.addFrame(face, timestamp);
3306
- if (!this.detectionState.faceMovingDetector.isAvailable()) {
3307
- // 面部移动数据尚不可用,等待更多帧
3308
- this.emitDebug('motion-detection', 'Face moving data not yet available - collecting more frames', {
3309
- collectedFrames: this.detectionState.faceMovingDetector.getFrameCount()
3310
- }, 'info');
3311
- return;
3312
- }
3313
- const faceMovingResult = this.detectionState.faceMovingDetector.detect();
3314
- if (!faceMovingResult.isMoving) {
3315
- // 面部移动检测失败,可能为照片攻击
3316
- this.emitDebug('motion-detection', 'Face moving detection failed - possible photo attack', {
3317
- details: faceMovingResult.details,
3318
- debug: faceMovingResult.debug
3319
- }, 'warn');
3320
- this.emitDetectorInfo({
3321
- code: DetectionCode.PLEASE_MOVING_FACE,
3322
- message: '请动一动您的脸部',
3323
- });
3324
- this.partialResetDetectionState();
3325
- return;
3326
- }
3327
- this.detectionState.photoAttackDetector.addFrame(face);
3328
- const photoAttackResult = this.detectionState.photoAttackDetector.detect();
3329
- if (photoAttackResult.isAvailable()) {
3330
- // 照片攻击检测可用(仅当判定为照片攻击时)
3331
- if (photoAttackResult.isPhoto) {
3332
- this.emitDetectorInfo({
3333
- code: DetectionCode.PHOTO_ATTACK_DETECTED,
3334
- message: photoAttackResult.getMessage(),
3335
- });
3336
- this.emitDebug('motion-detection', 'Photo attack detected', {
3337
- details: photoAttackResult.details,
3338
- debug: photoAttackResult.debug,
3339
- }, 'warn');
3340
- this.partialResetDetectionState();
3341
- return;
3690
+ // 开启面部移动检测
3691
+ if (this.options.enable_face_moving_detection) {
3692
+ this.detectionState.faceMovingDetector.addFrame(face, timestamp);
3693
+ const faceMovingResult = this.detectionState.faceMovingDetector.detect();
3694
+ if (faceMovingResult.available) {
3695
+ if (!faceMovingResult.isMoving) {
3696
+ // 面部移动检测失败,可能为照片攻击
3697
+ this.emitDebug('motion-detection', 'Face moving detection failed - possible photo attack', faceMovingResult.details, 'warn');
3698
+ this.emitDetectorInfo({
3699
+ code: DetectionCode.FACE_NOT_MOVING,
3700
+ message: faceMovingResult.getMessage(),
3701
+ });
3702
+ this.partialResetDetectionState();
3703
+ return;
3704
+ }
3342
3705
  }
3343
- else {
3344
- if (photoAttackResult.isTrusted()) {
3345
- // 仅当采集到足够帧,且判定为非照片攻击时,才采信
3346
- this.detectionState.liveness = true;
3347
- this.emitDebug('motion-detection', 'Photo attack detection passed - face is live', {
3348
- debug: photoAttackResult.debug,
3349
- details: photoAttackResult.details,
3350
- }, 'warn');
3706
+ }
3707
+ // 开启照片攻击检测
3708
+ if (this.options.enable_photo_attack_detection) {
3709
+ this.detectionState.photoAttackDetector.addFrame(face);
3710
+ const photoAttackResult = this.detectionState.photoAttackDetector.detect();
3711
+ if (photoAttackResult.available) {
3712
+ // 照片攻击检测可用(仅当判定为照片攻击时)
3713
+ if (photoAttackResult.isPhoto) {
3714
+ this.emitDetectorInfo({
3715
+ code: DetectionCode.PHOTO_ATTACK_DETECTED,
3716
+ message: photoAttackResult.getMessage(),
3717
+ });
3718
+ this.emitDebug('motion-detection', 'Photo attack detected', photoAttackResult.details, 'warn');
3719
+ this.partialResetDetectionState();
3720
+ return;
3721
+ }
3722
+ else {
3723
+ if (photoAttackResult.trusted) {
3724
+ // 仅当采集到足够帧,且判定为非照片攻击时,才采信
3725
+ this.detectionState.liveness = true;
3726
+ this.emitDebug('motion-detection', 'Photo attack detection passed - face is live', photoAttackResult.details, 'warn');
3727
+ }
3351
3728
  }
3352
3729
  }
3353
3730
  }
3731
+ else {
3732
+ // 未启用照片攻击检测,默认活体为活体
3733
+ this.detectionState.liveness = true;
3734
+ }
3354
3735
  // 捕获并准备帧数据
3355
3736
  const frameData = this.captureAndPrepareFrames();
3356
3737
  if (!frameData) {
@@ -3361,6 +3742,31 @@ class FaceDetectionEngine extends SimpleEventEmitter {
3361
3742
  }
3362
3743
  const bgrFrame = frameData.bgrFrame;
3363
3744
  const grayFrame = frameData.grayFrame;
3745
+ if (this.options.enable_screen_attack_detection) {
3746
+ const screenAttackResult = this.detectionState.screenAttachDetector.detect(bgrFrame, grayFrame);
3747
+ if (screenAttackResult.available) {
3748
+ if (screenAttackResult.isScreenAttack) {
3749
+ this.emitDetectorInfo({
3750
+ code: DetectionCode.SCREEN_ATTACK_DETECTED,
3751
+ message: screenAttackResult.getMessage(),
3752
+ });
3753
+ this.emitDebug('motion-detection', 'Screen attack detected', screenAttackResult.details, 'warn');
3754
+ this.partialResetDetectionState();
3755
+ return;
3756
+ }
3757
+ else {
3758
+ if (screenAttackResult.trusted) {
3759
+ // 仅当采集到足够帧,且判定为非屏幕攻击时,才采信
3760
+ this.detectionState.realness = true;
3761
+ this.emitDebug('motion-detection', 'Screen attack detection passed - face is real', screenAttackResult.details, 'warn');
3762
+ }
3763
+ }
3764
+ }
3765
+ }
3766
+ else {
3767
+ // 未启用屏幕攻击检测,默认真实性为真实
3768
+ this.detectionState.realness = true;
3769
+ }
3364
3770
  let frontal = 1;
3365
3771
  // 计算面部正对度,不达标则跳过当前帧
3366
3772
  if (this.detectionState.needFrontalFace()) {