@sssxyd/face-liveness-detector 0.4.0-alpha.4 → 0.4.0-alpha.6
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 +80 -49
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +80 -49
- package/dist/index.js.map +1 -1
- package/dist/types/face-detection-engine.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -137,16 +137,16 @@
|
|
|
137
137
|
motion_liveness_motion_consistency_threshold: 0.3,
|
|
138
138
|
motion_liveness_strict_photo_detection: false,
|
|
139
139
|
// Screen Capture Detection Settings
|
|
140
|
-
screen_capture_confidence_threshold: 0.
|
|
140
|
+
screen_capture_confidence_threshold: 0.7,
|
|
141
141
|
screen_capture_detection_strategy: 'adaptive',
|
|
142
142
|
screen_moire_pattern_threshold: 0.65,
|
|
143
143
|
screen_moire_pattern_enable_dct: true,
|
|
144
144
|
screen_moire_pattern_enable_edge_detection: true,
|
|
145
145
|
screen_color_saturation_threshold: 40,
|
|
146
|
-
screen_color_rgb_correlation_threshold: 0.
|
|
146
|
+
screen_color_rgb_correlation_threshold: 0.75,
|
|
147
147
|
screen_color_pixel_entropy_threshold: 6.5,
|
|
148
148
|
screen_color_gradient_smoothness_threshold: 0.7,
|
|
149
|
-
screen_color_confidence_threshold: 0.
|
|
149
|
+
screen_color_confidence_threshold: 0.65,
|
|
150
150
|
screen_rgb_low_freq_start_percent: 0.15,
|
|
151
151
|
screen_rgb_low_freq_end_percent: 0.35,
|
|
152
152
|
screen_rgb_energy_ratio_normalization_factor: 10,
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
screen_rgb_energy_score_weight: 0.40,
|
|
155
155
|
screen_rgb_asymmetry_score_weight: 0.40,
|
|
156
156
|
screen_rgb_difference_factor_weight: 0.20,
|
|
157
|
-
screen_rgb_confidence_threshold: 0.
|
|
157
|
+
screen_rgb_confidence_threshold: 0.65,
|
|
158
158
|
};
|
|
159
159
|
/**
|
|
160
160
|
* Merge user configuration with defaults
|
|
@@ -4538,9 +4538,16 @@
|
|
|
4538
4538
|
};
|
|
4539
4539
|
if (this.videoElement) {
|
|
4540
4540
|
this.videoElement.addEventListener('canplay', onCanPlay, { once: true });
|
|
4541
|
-
this.videoElement.play().catch(err => {
|
|
4541
|
+
this.videoElement.play().catch((err) => {
|
|
4542
4542
|
clearTimeout(timeout);
|
|
4543
4543
|
cleanup();
|
|
4544
|
+
const errorInfo = this.extractErrorInfo(err);
|
|
4545
|
+
this.emitDebug('video-setup', 'Failed to play video', {
|
|
4546
|
+
error: errorInfo.message,
|
|
4547
|
+
stack: errorInfo.stack,
|
|
4548
|
+
name: errorInfo.name,
|
|
4549
|
+
cause: errorInfo.cause
|
|
4550
|
+
}, 'error');
|
|
4544
4551
|
reject(err);
|
|
4545
4552
|
});
|
|
4546
4553
|
}
|
|
@@ -4604,6 +4611,7 @@
|
|
|
4604
4611
|
* Reset detection state
|
|
4605
4612
|
*/
|
|
4606
4613
|
resetDetectionState() {
|
|
4614
|
+
this.emitDebug('detection', 'Resetting detection state...');
|
|
4607
4615
|
this.detectionState.reset();
|
|
4608
4616
|
this.actualVideoWidth = 0;
|
|
4609
4617
|
this.actualVideoHeight = 0;
|
|
@@ -4724,34 +4732,34 @@
|
|
|
4724
4732
|
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4725
4733
|
return;
|
|
4726
4734
|
}
|
|
4727
|
-
//
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
return;
|
|
4733
|
-
}
|
|
4734
|
-
// 当前帧灰度图片
|
|
4735
|
-
const grayFrame = matToGray(this.cv, bgrFrame);
|
|
4736
|
-
if (!grayFrame) {
|
|
4737
|
-
bgrFrame.delete();
|
|
4738
|
-
this.emitDebug('detection', 'Failed to convert frame Mat to grayscale', {}, 'warn');
|
|
4739
|
-
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4740
|
-
return;
|
|
4741
|
-
}
|
|
4742
|
-
// 提取人脸区域图片及灰度图片
|
|
4743
|
-
const bgrFace = bgrFrame.roi(new this.cv.Rect(faceBox[0], faceBox[1], faceBox[2], faceBox[3]));
|
|
4744
|
-
const grayFace = matToGray(this.cv, bgrFace);
|
|
4745
|
-
if (!grayFace) {
|
|
4746
|
-
bgrFrame.delete();
|
|
4747
|
-
bgrFace.delete();
|
|
4748
|
-
this.emitDebug('detection', 'Failed to convert face Mat to grayscale', {}, 'warn');
|
|
4749
|
-
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4750
|
-
return;
|
|
4751
|
-
}
|
|
4752
|
-
// 释放不再需要的Mat
|
|
4753
|
-
bgrFrame.delete();
|
|
4735
|
+
// 所有需要删除的 Mat 对象
|
|
4736
|
+
let bgrFrame = null;
|
|
4737
|
+
let grayFrame = null;
|
|
4738
|
+
let bgrFace = null;
|
|
4739
|
+
let grayFace = null;
|
|
4754
4740
|
try {
|
|
4741
|
+
// 当前帧图片
|
|
4742
|
+
bgrFrame = drawCanvasToMat(this.cv, frameCanvas, false);
|
|
4743
|
+
if (!bgrFrame) {
|
|
4744
|
+
this.emitDebug('detection', 'Failed to convert canvas to OpenCV Mat', {}, 'warn');
|
|
4745
|
+
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
// 当前帧灰度图片
|
|
4749
|
+
grayFrame = matToGray(this.cv, bgrFrame);
|
|
4750
|
+
if (!grayFrame) {
|
|
4751
|
+
this.emitDebug('detection', 'Failed to convert frame Mat to grayscale', {}, 'warn');
|
|
4752
|
+
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
// 提取人脸区域图片及灰度图片
|
|
4756
|
+
bgrFace = bgrFrame.roi(new this.cv.Rect(faceBox[0], faceBox[1], faceBox[2], faceBox[3]));
|
|
4757
|
+
grayFace = matToGray(this.cv, bgrFace);
|
|
4758
|
+
if (!grayFace) {
|
|
4759
|
+
this.emitDebug('detection', 'Failed to convert face Mat to grayscale', {}, 'warn');
|
|
4760
|
+
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4761
|
+
return;
|
|
4762
|
+
}
|
|
4755
4763
|
if (!this.detectionState.screenDetector) {
|
|
4756
4764
|
this.emit('detector-error', {
|
|
4757
4765
|
code: exports.ErrorCode.INTERNAL_ERROR,
|
|
@@ -4770,15 +4778,37 @@
|
|
|
4770
4778
|
}
|
|
4771
4779
|
// 屏幕捕获检测, 只关心脸部区域
|
|
4772
4780
|
const screenResult = this.detectionState.screenDetector.detectAuto(bgrFace, grayFace);
|
|
4773
|
-
bgrFace.delete();
|
|
4774
|
-
grayFace.delete();
|
|
4775
4781
|
// 屏幕捕获检测器已经准备就绪,其验证结果可信
|
|
4776
4782
|
if (screenResult.isScreenCapture) {
|
|
4777
|
-
|
|
4783
|
+
// 从 executedMethods 提取各检测器的置信度
|
|
4784
|
+
const methodConfidences = screenResult.executedMethods.reduce((acc, method) => {
|
|
4785
|
+
if (method.method.includes('Moiré')) {
|
|
4786
|
+
acc.moireConfidence = method.confidence;
|
|
4787
|
+
}
|
|
4788
|
+
else if (method.method.includes('Color')) {
|
|
4789
|
+
acc.colorConfidence = method.confidence;
|
|
4790
|
+
}
|
|
4791
|
+
else if (method.method.includes('RGB')) {
|
|
4792
|
+
acc.rgbConfidence = method.confidence;
|
|
4793
|
+
}
|
|
4794
|
+
return acc;
|
|
4795
|
+
}, {});
|
|
4778
4796
|
this.emitDetectorInfo({ code: exports.DetectionCode.FACE_NOT_REAL, message: screenResult.getMessage(), screenConfidence: screenResult.confidenceScore });
|
|
4779
4797
|
this.emitDebug('screen-capture-detection', 'Screen capture detected - possible video replay attack', {
|
|
4780
|
-
|
|
4781
|
-
|
|
4798
|
+
overallConfidence: screenResult.confidenceScore,
|
|
4799
|
+
minConfidenceThreshold: this.options.screen_capture_confidence_threshold,
|
|
4800
|
+
moireConfidence: methodConfidences.moireConfidence ?? 'N/A',
|
|
4801
|
+
colorConfidence: methodConfidences.colorConfidence ?? 'N/A',
|
|
4802
|
+
rgbConfidence: methodConfidences.rgbConfidence ?? 'N/A',
|
|
4803
|
+
detectionStrategy: screenResult.strategy,
|
|
4804
|
+
riskLevel: screenResult.riskLevel,
|
|
4805
|
+
processingTimeMs: screenResult.processingTimeMs,
|
|
4806
|
+
executedMethods: screenResult.executedMethods.map((m) => ({
|
|
4807
|
+
method: m.method,
|
|
4808
|
+
isScreenCapture: m.isScreenCapture,
|
|
4809
|
+
confidence: m.confidence
|
|
4810
|
+
})),
|
|
4811
|
+
skippedMethods: screenResult.skippedMethods
|
|
4782
4812
|
}, 'warn');
|
|
4783
4813
|
this.resetDetectionState();
|
|
4784
4814
|
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
@@ -4789,13 +4819,18 @@
|
|
|
4789
4819
|
if (this.detectionState.motionDetector.isReady()) {
|
|
4790
4820
|
// 运动检测器已经准备就绪,其验证结果可信
|
|
4791
4821
|
if (!motionResult.isLively) {
|
|
4792
|
-
grayFrame.delete();
|
|
4793
4822
|
this.emitDebug('motion-detection', 'Motion liveness check failed - possible photo attack', {
|
|
4794
4823
|
motionScore: motionResult.motionScore,
|
|
4795
4824
|
keypointVariance: motionResult.keypointVariance,
|
|
4825
|
+
opticalFlowMagnitude: motionResult.opticalFlowMagnitude,
|
|
4826
|
+
eyeMotionScore: motionResult.eyeMotionScore,
|
|
4827
|
+
mouthMotionScore: motionResult.mouthMotionScore,
|
|
4796
4828
|
motionType: motionResult.motionType,
|
|
4797
4829
|
minMotionScore: this.options.motion_liveness_min_motion_score,
|
|
4798
|
-
minKeypointVariance: this.options.motion_liveness_min_keypoint_variance
|
|
4830
|
+
minKeypointVariance: this.options.motion_liveness_min_keypoint_variance,
|
|
4831
|
+
minOpticalFlowThreshold: this.options.motion_liveness_min_optical_flow_threshold,
|
|
4832
|
+
minMotionConsistencyThreshold: this.options.motion_liveness_motion_consistency_threshold,
|
|
4833
|
+
details: motionResult.details
|
|
4799
4834
|
}, 'warn');
|
|
4800
4835
|
this.emitDetectorInfo({
|
|
4801
4836
|
code: exports.DetectionCode.FACE_NOT_LIVE,
|
|
@@ -4844,7 +4879,6 @@
|
|
|
4844
4879
|
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4845
4880
|
return;
|
|
4846
4881
|
}
|
|
4847
|
-
grayFrame.delete();
|
|
4848
4882
|
// 当前帧通过常规检查
|
|
4849
4883
|
this.emitDetectorInfo({ passed: true, code: exports.DetectionCode.FACE_CHECK_PASS, faceRatio: faceRatio, faceFrontal: frontal, imageQuality: qualityResult.score });
|
|
4850
4884
|
// 处理不同检测阶段的逻辑
|
|
@@ -4890,18 +4924,15 @@
|
|
|
4890
4924
|
this.scheduleNextDetection(this.options.detect_error_retry_delay);
|
|
4891
4925
|
}
|
|
4892
4926
|
finally {
|
|
4893
|
-
|
|
4927
|
+
// 统一在 finally 块中删除所有 Mat 对象
|
|
4928
|
+
if (grayFrame)
|
|
4894
4929
|
grayFrame.delete();
|
|
4895
|
-
|
|
4896
|
-
if (bgrFrame) {
|
|
4930
|
+
if (bgrFrame)
|
|
4897
4931
|
bgrFrame.delete();
|
|
4898
|
-
|
|
4899
|
-
if (bgrFace) {
|
|
4932
|
+
if (bgrFace)
|
|
4900
4933
|
bgrFace.delete();
|
|
4901
|
-
|
|
4902
|
-
if (grayFace) {
|
|
4934
|
+
if (grayFace)
|
|
4903
4935
|
grayFace.delete();
|
|
4904
|
-
}
|
|
4905
4936
|
}
|
|
4906
4937
|
}
|
|
4907
4938
|
/**
|
|
@@ -5161,7 +5192,7 @@
|
|
|
5161
5192
|
return null;
|
|
5162
5193
|
}
|
|
5163
5194
|
this.frameCanvasContext.drawImage(this.videoElement, 0, 0, videoWidth_actual, videoHeight_actual);
|
|
5164
|
-
this.emitDebug('capture', 'Frame drawn to canvas');
|
|
5195
|
+
this.emitDebug('capture', 'Frame drawn to canvas as ' + videoHeight_actual + 'x' + videoWidth_actual);
|
|
5165
5196
|
return this.frameCanvasElement;
|
|
5166
5197
|
}
|
|
5167
5198
|
catch (e) {
|