@sssxyd/face-liveness-detector 0.4.0-alpha.1 → 0.4.0-alpha.3
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 +146 -106
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +146 -106
- package/dist/index.js.map +1 -1
- package/dist/types/face-detection-engine.d.ts +1 -4
- package/dist/types/face-detection-engine.d.ts.map +1 -1
- package/dist/types/face-detection-state.d.ts +36 -0
- package/dist/types/face-detection-state.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -1632,6 +1632,46 @@ function getOpenCVVersion() {
|
|
|
1632
1632
|
}
|
|
1633
1633
|
}
|
|
1634
1634
|
|
|
1635
|
+
/**
|
|
1636
|
+
* 将绘制图片的canvas转换为mat
|
|
1637
|
+
* @param {any} cv OpenCV实例
|
|
1638
|
+
* @param {HTMLCanvasElement} canvas canvas元素
|
|
1639
|
+
* @param {boolean} gray 是否转换为灰度图像
|
|
1640
|
+
* @returns {any | null} - 转换后的Mat对象,如果转换失败则返回null
|
|
1641
|
+
*/
|
|
1642
|
+
function drawCanvasToMat(cv, canvas, gray) {
|
|
1643
|
+
try {
|
|
1644
|
+
if (!cv || !canvas) {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
// Convert canvas directly to cv.Mat
|
|
1648
|
+
const mat = cv.imread(canvas);
|
|
1649
|
+
if (!mat || mat.empty()) {
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
if (gray) {
|
|
1653
|
+
cv.cvtColor(mat, mat, cv.COLOR_BGR2GRAY);
|
|
1654
|
+
}
|
|
1655
|
+
return mat;
|
|
1656
|
+
}
|
|
1657
|
+
catch (e) {
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
function matToGray(cv, mat) {
|
|
1662
|
+
try {
|
|
1663
|
+
if (!cv || !mat || mat.empty()) {
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
const grayMat = new cv.Mat();
|
|
1667
|
+
cv.cvtColor(mat, grayMat, cv.COLOR_BGR2GRAY);
|
|
1668
|
+
return grayMat;
|
|
1669
|
+
}
|
|
1670
|
+
catch (e) {
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1635
1675
|
/**
|
|
1636
1676
|
* 运动和活体检测 - 防止照片攻击
|
|
1637
1677
|
* 检测微妙的面部运动和运动模式,以区分真实面孔和高质量照片
|
|
@@ -2380,46 +2420,6 @@ class MotionLivenessDetector {
|
|
|
2380
2420
|
}
|
|
2381
2421
|
}
|
|
2382
2422
|
|
|
2383
|
-
/**
|
|
2384
|
-
* 将绘制图片的canvas转换为mat
|
|
2385
|
-
* @param {any} cv OpenCV实例
|
|
2386
|
-
* @param {HTMLCanvasElement} canvas canvas元素
|
|
2387
|
-
* @param {boolean} gray 是否转换为灰度图像
|
|
2388
|
-
* @returns {any | null} - 转换后的Mat对象,如果转换失败则返回null
|
|
2389
|
-
*/
|
|
2390
|
-
function drawCanvasToMat(cv, canvas, gray) {
|
|
2391
|
-
try {
|
|
2392
|
-
if (!cv || !canvas) {
|
|
2393
|
-
return null;
|
|
2394
|
-
}
|
|
2395
|
-
// Convert canvas directly to cv.Mat
|
|
2396
|
-
const mat = cv.imread(canvas);
|
|
2397
|
-
if (!mat || mat.empty()) {
|
|
2398
|
-
return null;
|
|
2399
|
-
}
|
|
2400
|
-
if (gray) {
|
|
2401
|
-
cv.cvtColor(mat, mat, cv.COLOR_BGR2GRAY);
|
|
2402
|
-
}
|
|
2403
|
-
return mat;
|
|
2404
|
-
}
|
|
2405
|
-
catch (e) {
|
|
2406
|
-
return null;
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
function matToGray(cv, mat) {
|
|
2410
|
-
try {
|
|
2411
|
-
if (!cv || !mat || mat.empty()) {
|
|
2412
|
-
return null;
|
|
2413
|
-
}
|
|
2414
|
-
const grayMat = new cv.Mat();
|
|
2415
|
-
cv.cvtColor(mat, grayMat, cv.COLOR_BGR2GRAY);
|
|
2416
|
-
return grayMat;
|
|
2417
|
-
}
|
|
2418
|
-
catch (e) {
|
|
2419
|
-
return null;
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
2423
|
/**
|
|
2424
2424
|
* 屏幕色彩特征检测模块
|
|
2425
2425
|
*
|
|
@@ -3991,10 +3991,6 @@ class ScreenCaptureDetector {
|
|
|
3991
3991
|
}
|
|
3992
3992
|
}
|
|
3993
3993
|
|
|
3994
|
-
/**
|
|
3995
|
-
* Face Detection Engine - Core Detection Engine
|
|
3996
|
-
* Framework-agnostic face liveness detection engine
|
|
3997
|
-
*/
|
|
3998
3994
|
/**
|
|
3999
3995
|
* Internal detection state interface
|
|
4000
3996
|
*/
|
|
@@ -4017,6 +4013,7 @@ class DetectionState {
|
|
|
4017
4013
|
Object.assign(this, options);
|
|
4018
4014
|
}
|
|
4019
4015
|
reset() {
|
|
4016
|
+
this.clearActionVerifyTimeout();
|
|
4020
4017
|
const savedMotionDetector = this.motionDetector;
|
|
4021
4018
|
const savedScreenDetector = this.screenDetector;
|
|
4022
4019
|
savedMotionDetector?.reset();
|
|
@@ -4037,7 +4034,71 @@ class DetectionState {
|
|
|
4037
4034
|
}
|
|
4038
4035
|
return false;
|
|
4039
4036
|
}
|
|
4037
|
+
onActionStarted(nextAction, timeoutMills, timeoutCallback) {
|
|
4038
|
+
if (nextAction === null) {
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
this.currentAction = nextAction;
|
|
4042
|
+
this.clearActionVerifyTimeout();
|
|
4043
|
+
this.actionVerifyTimeout = setTimeout(timeoutCallback, timeoutMills);
|
|
4044
|
+
}
|
|
4045
|
+
onActionCompleted() {
|
|
4046
|
+
if (this.currentAction === null) {
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
this.clearActionVerifyTimeout();
|
|
4050
|
+
this.completedActions.add(this.currentAction);
|
|
4051
|
+
this.currentAction = null;
|
|
4052
|
+
}
|
|
4053
|
+
setCVInstance(cvInstance) {
|
|
4054
|
+
this.motionDetector?.setCVInstance(cvInstance);
|
|
4055
|
+
this.screenDetector?.setCVInstance(cvInstance);
|
|
4056
|
+
}
|
|
4057
|
+
/**
|
|
4058
|
+
* Clear action verify timeout
|
|
4059
|
+
*/
|
|
4060
|
+
clearActionVerifyTimeout() {
|
|
4061
|
+
if (this.actionVerifyTimeout !== null) {
|
|
4062
|
+
clearTimeout(this.actionVerifyTimeout);
|
|
4063
|
+
this.actionVerifyTimeout = null;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4040
4066
|
}
|
|
4067
|
+
// <-- Add this import at the top if ResolvedEngineOptions is defined in types.ts
|
|
4068
|
+
function createDetectionState(options) {
|
|
4069
|
+
const detectionState = new DetectionState({});
|
|
4070
|
+
detectionState.motionDetector = new MotionLivenessDetector({
|
|
4071
|
+
minMotionThreshold: options.motion_liveness_min_motion_score,
|
|
4072
|
+
minKeypointVariance: options.motion_liveness_min_keypoint_variance,
|
|
4073
|
+
frameBufferSize: options.motion_liveness_frame_buffer_size,
|
|
4074
|
+
eyeAspectRatioThreshold: options.motion_liveness_eye_aspect_ratio_threshold
|
|
4075
|
+
});
|
|
4076
|
+
detectionState.screenDetector = new ScreenCaptureDetector({
|
|
4077
|
+
confidenceThreshold: options.screen_capture_confidence_threshold,
|
|
4078
|
+
detectionStrategy: stringToDetectionStrategy(options.screen_capture_detection_strategy),
|
|
4079
|
+
moireThreshold: options.screen_moire_pattern_threshold,
|
|
4080
|
+
moireEnableDCT: options.screen_moire_pattern_enable_dct,
|
|
4081
|
+
moireEnableEdgeDetection: options.screen_moire_pattern_enable_edge_detection,
|
|
4082
|
+
colorSaturationThreshold: options.screen_color_saturation_threshold,
|
|
4083
|
+
colorRgbCorrelationThreshold: options.screen_color_rgb_correlation_threshold,
|
|
4084
|
+
colorPixelEntropyThreshold: options.screen_color_pixel_entropy_threshold,
|
|
4085
|
+
colorConfidenceThreshold: options.screen_color_confidence_threshold,
|
|
4086
|
+
rgbLowFreqStartPercent: options.screen_rgb_low_freq_start_percent,
|
|
4087
|
+
rgbLowFreqEndPercent: options.screen_rgb_low_freq_end_percent,
|
|
4088
|
+
rgbEnergyRatioNormalizationFactor: options.screen_rgb_energy_ratio_normalization_factor,
|
|
4089
|
+
rgbChannelDifferenceNormalizationFactor: options.screen_rgb_channel_difference_normalization_factor,
|
|
4090
|
+
rgbEnergyScoreWeight: options.screen_rgb_energy_score_weight,
|
|
4091
|
+
rgbAsymmetryScoreWeight: options.screen_rgb_asymmetry_score_weight,
|
|
4092
|
+
rgbDifferenceFactorWeight: options.screen_rgb_difference_factor_weight,
|
|
4093
|
+
rgbConfidenceThreshold: options.screen_rgb_confidence_threshold
|
|
4094
|
+
});
|
|
4095
|
+
return detectionState;
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
/**
|
|
4099
|
+
* Face Detection Engine - Core Detection Engine
|
|
4100
|
+
* Framework-agnostic face liveness detection engine
|
|
4101
|
+
*/
|
|
4041
4102
|
/**
|
|
4042
4103
|
* Framework-agnostic face liveness detection engine
|
|
4043
4104
|
* Provides core detection logic without UI dependencies
|
|
@@ -4064,32 +4125,15 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4064
4125
|
this.actualVideoWidth = 0;
|
|
4065
4126
|
this.actualVideoHeight = 0;
|
|
4066
4127
|
this.options = mergeOptions(options);
|
|
4067
|
-
this.detectionState =
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
this.detectionState
|
|
4075
|
-
|
|
4076
|
-
detectionStrategy: stringToDetectionStrategy(this.options.screen_capture_detection_strategy),
|
|
4077
|
-
moireThreshold: this.options.screen_moire_pattern_threshold,
|
|
4078
|
-
moireEnableDCT: this.options.screen_moire_pattern_enable_dct,
|
|
4079
|
-
moireEnableEdgeDetection: this.options.screen_moire_pattern_enable_edge_detection,
|
|
4080
|
-
colorSaturationThreshold: this.options.screen_color_saturation_threshold,
|
|
4081
|
-
colorRgbCorrelationThreshold: this.options.screen_color_rgb_correlation_threshold,
|
|
4082
|
-
colorPixelEntropyThreshold: this.options.screen_color_pixel_entropy_threshold,
|
|
4083
|
-
colorConfidenceThreshold: this.options.screen_color_confidence_threshold,
|
|
4084
|
-
rgbLowFreqStartPercent: this.options.screen_rgb_low_freq_start_percent,
|
|
4085
|
-
rgbLowFreqEndPercent: this.options.screen_rgb_low_freq_end_percent,
|
|
4086
|
-
rgbEnergyRatioNormalizationFactor: this.options.screen_rgb_energy_ratio_normalization_factor,
|
|
4087
|
-
rgbChannelDifferenceNormalizationFactor: this.options.screen_rgb_channel_difference_normalization_factor,
|
|
4088
|
-
rgbEnergyScoreWeight: this.options.screen_rgb_energy_score_weight,
|
|
4089
|
-
rgbAsymmetryScoreWeight: this.options.screen_rgb_asymmetry_score_weight,
|
|
4090
|
-
rgbDifferenceFactorWeight: this.options.screen_rgb_difference_factor_weight,
|
|
4091
|
-
rgbConfidenceThreshold: this.options.screen_rgb_confidence_threshold
|
|
4092
|
-
});
|
|
4128
|
+
this.detectionState = createDetectionState(this.options);
|
|
4129
|
+
}
|
|
4130
|
+
updateOptions(options) {
|
|
4131
|
+
if (this.engineState == EngineState.DETECTING) {
|
|
4132
|
+
this.stopDetection(false);
|
|
4133
|
+
}
|
|
4134
|
+
this.options = mergeOptions(options);
|
|
4135
|
+
this.detectionState = createDetectionState(this.options);
|
|
4136
|
+
this.detectionState.setCVInstance(this.cv);
|
|
4093
4137
|
}
|
|
4094
4138
|
getEngineState() {
|
|
4095
4139
|
return this.engineState;
|
|
@@ -4128,10 +4172,8 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4128
4172
|
console.log('[FaceDetectionEngine] OpenCV loaded successfully', {
|
|
4129
4173
|
version: cv_version
|
|
4130
4174
|
});
|
|
4131
|
-
// Inject OpenCV instance into motion detector
|
|
4132
|
-
|
|
4133
|
-
this.detectionState.motionDetector.cv = cv;
|
|
4134
|
-
}
|
|
4175
|
+
// Inject OpenCV instance into motion detector and screen detector
|
|
4176
|
+
this.detectionState.setCVInstance(this.cv);
|
|
4135
4177
|
// Load Human.js
|
|
4136
4178
|
console.log('[FaceDetectionEngine] Loading Human.js models...');
|
|
4137
4179
|
this.emitDebug('initialization', 'Loading Human.js...');
|
|
@@ -4749,7 +4791,14 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4749
4791
|
handleVerifyPhase(gestures) {
|
|
4750
4792
|
// No action set yet, will continue after setting
|
|
4751
4793
|
if (!this.detectionState.currentAction) {
|
|
4752
|
-
this.selectNextAction()
|
|
4794
|
+
if (!this.selectNextAction()) {
|
|
4795
|
+
this.emit('detector-error', {
|
|
4796
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
4797
|
+
message: 'No available actions to perform for liveness verification'
|
|
4798
|
+
});
|
|
4799
|
+
this.stopDetection(false);
|
|
4800
|
+
return;
|
|
4801
|
+
}
|
|
4753
4802
|
this.scheduleNextDetection(this.options.detect_frame_delay * 3);
|
|
4754
4803
|
return;
|
|
4755
4804
|
}
|
|
@@ -4766,16 +4815,21 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4766
4815
|
// Action completed
|
|
4767
4816
|
this.emit('detector-action', actionComplete);
|
|
4768
4817
|
this.emitDebug('liveness', 'Action detected', { action: this.detectionState.currentAction });
|
|
4769
|
-
this.
|
|
4770
|
-
this.detectionState.completedActions.add(this.detectionState.currentAction);
|
|
4771
|
-
this.detectionState.currentAction = null;
|
|
4818
|
+
this.detectionState.onActionCompleted();
|
|
4772
4819
|
// Check if all required actions completed
|
|
4773
4820
|
if (this.detectionState.completedActions.size >= this.getPerformActionCount()) {
|
|
4774
4821
|
this.stopDetection(true);
|
|
4775
4822
|
return;
|
|
4776
4823
|
}
|
|
4777
4824
|
// Select next action
|
|
4778
|
-
this.selectNextAction()
|
|
4825
|
+
if (!this.selectNextAction()) {
|
|
4826
|
+
this.emit('detector-error', {
|
|
4827
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
4828
|
+
message: 'No available actions to perform for liveness verification'
|
|
4829
|
+
});
|
|
4830
|
+
this.stopDetection(false);
|
|
4831
|
+
return;
|
|
4832
|
+
}
|
|
4779
4833
|
this.scheduleNextDetection();
|
|
4780
4834
|
}
|
|
4781
4835
|
/**
|
|
@@ -4844,7 +4898,7 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4844
4898
|
const availableActions = (this.options.action_liveness_action_list ?? []).filter(action => !this.detectionState.completedActions.has(action));
|
|
4845
4899
|
if (availableActions.length === 0) {
|
|
4846
4900
|
this.emitDebug('liveness', 'No available actions to perform', { completedActions: Array.from(this.detectionState.completedActions), totalActions: this.options.action_liveness_action_list?.length ?? 0 }, 'warn');
|
|
4847
|
-
return;
|
|
4901
|
+
return false;
|
|
4848
4902
|
}
|
|
4849
4903
|
let nextAction = availableActions[0];
|
|
4850
4904
|
if (this.options.action_liveness_action_randomize) {
|
|
@@ -4852,38 +4906,24 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4852
4906
|
const randomIndex = Math.floor(Math.random() * availableActions.length);
|
|
4853
4907
|
nextAction = availableActions[randomIndex];
|
|
4854
4908
|
}
|
|
4855
|
-
this.detectionState.currentAction = nextAction;
|
|
4856
4909
|
const actionStart = {
|
|
4857
4910
|
action: nextAction,
|
|
4858
4911
|
status: LivenessActionStatus.STARTED
|
|
4859
4912
|
};
|
|
4860
4913
|
this.emit('detector-action', actionStart);
|
|
4861
4914
|
this.emitDebug('liveness', 'Action selected', { action: this.detectionState.currentAction });
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
this.resetDetectionState();
|
|
4875
|
-
}
|
|
4876
|
-
}, this.options.action_liveness_verify_timeout);
|
|
4877
|
-
return;
|
|
4878
|
-
}
|
|
4879
|
-
/**
|
|
4880
|
-
* Clear action verify timeout
|
|
4881
|
-
*/
|
|
4882
|
-
clearActionVerifyTimeout() {
|
|
4883
|
-
if (this.detectionState.actionVerifyTimeout !== null) {
|
|
4884
|
-
clearTimeout(this.detectionState.actionVerifyTimeout);
|
|
4885
|
-
this.detectionState.actionVerifyTimeout = null;
|
|
4886
|
-
}
|
|
4915
|
+
this.detectionState.onActionStarted(nextAction, this.options.action_liveness_verify_timeout, () => {
|
|
4916
|
+
this.emitDebug('liveness', 'Action verify timeout', {
|
|
4917
|
+
action: nextAction,
|
|
4918
|
+
timeout: this.options.action_liveness_verify_timeout
|
|
4919
|
+
}, 'warn');
|
|
4920
|
+
this.emit('detector-action', {
|
|
4921
|
+
action: nextAction,
|
|
4922
|
+
status: LivenessActionStatus.TIMEOUT
|
|
4923
|
+
});
|
|
4924
|
+
this.resetDetectionState();
|
|
4925
|
+
});
|
|
4926
|
+
return true;
|
|
4887
4927
|
}
|
|
4888
4928
|
/**
|
|
4889
4929
|
* Detect specific action
|