@sssxyd/face-liveness-detector 0.4.1-beta.4 → 0.4.1-beta.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 +110 -69
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +110 -69
- package/dist/index.js.map +1 -1
- package/dist/types/enums.d.ts +3 -1
- package/dist/types/enums.d.ts.map +1 -1
- package/dist/types/face-detection-engine.d.ts +4 -2
- package/dist/types/face-detection-engine.d.ts.map +1 -1
- package/dist/types/types.d.ts +1 -0
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -10,8 +10,10 @@ var LivenessAction;
|
|
|
10
10
|
LivenessAction["BLINK"] = "blink";
|
|
11
11
|
// Mouth open
|
|
12
12
|
LivenessAction["MOUTH_OPEN"] = "mouth_open";
|
|
13
|
-
// Nod
|
|
14
|
-
LivenessAction["
|
|
13
|
+
// Nod down (look down)
|
|
14
|
+
LivenessAction["NOD_DOWN"] = "nod_down";
|
|
15
|
+
// Nod up (look up)
|
|
16
|
+
LivenessAction["NOD_UP"] = "nod_up";
|
|
15
17
|
})(LivenessAction || (LivenessAction = {}));
|
|
16
18
|
/**
|
|
17
19
|
* Liveness action status enumeration
|
|
@@ -20,6 +22,7 @@ var LivenessActionStatus;
|
|
|
20
22
|
(function (LivenessActionStatus) {
|
|
21
23
|
LivenessActionStatus["STARTED"] = "started";
|
|
22
24
|
LivenessActionStatus["COMPLETED"] = "completed";
|
|
25
|
+
LivenessActionStatus["MISMATCH"] = "mismatch";
|
|
23
26
|
LivenessActionStatus["TIMEOUT"] = "timeout";
|
|
24
27
|
})(LivenessActionStatus || (LivenessActionStatus = {}));
|
|
25
28
|
var DetectionPeriod;
|
|
@@ -103,7 +106,7 @@ const DEFAULT_OPTIONS$1 = {
|
|
|
103
106
|
min_blur_score: 0.6
|
|
104
107
|
},
|
|
105
108
|
// action Liveness Settings
|
|
106
|
-
action_liveness_action_list: [LivenessAction.BLINK, LivenessAction.MOUTH_OPEN, LivenessAction.
|
|
109
|
+
action_liveness_action_list: [LivenessAction.BLINK, LivenessAction.MOUTH_OPEN, LivenessAction.NOD_DOWN, LivenessAction.NOD_UP],
|
|
107
110
|
action_liveness_action_count: 1,
|
|
108
111
|
action_liveness_action_randomize: true,
|
|
109
112
|
action_liveness_verify_timeout: 60000,
|
|
@@ -3962,19 +3965,17 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
3962
3965
|
this.stopDetection(false);
|
|
3963
3966
|
return;
|
|
3964
3967
|
}
|
|
3965
|
-
let bgrFrame = null;
|
|
3966
|
-
let grayFrame = null;
|
|
3967
3968
|
try {
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3969
|
+
// 面部区域占比计算
|
|
3970
|
+
const faceRatio = (faceBox[2] * faceBox[3]) / (this.actualVideoWidth * this.actualVideoHeight);
|
|
3971
|
+
// 面部区域过小则跳过当前帧
|
|
3972
|
+
if (faceRatio <= this.options.collect_min_face_ratio) {
|
|
3973
|
+
this.emitDetectorInfo({ code: DetectionCode.FACE_TOO_SMALL, faceRatio: faceRatio });
|
|
3974
|
+
this.emitDebug('detection', 'Face is too small', { ratio: faceRatio.toFixed(4), minRatio: this.options.collect_min_face_ratio, maxRatio: this.options.collect_max_face_ratio }, 'info');
|
|
3973
3975
|
return;
|
|
3974
3976
|
}
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
const motionResult = this.detectionState.motionDetector.analyzeMotion(grayFrame, faceBox);
|
|
3977
|
+
// 静默活体检测
|
|
3978
|
+
const motionResult = this.detectionState.motionDetector.analyzeMotion(face, faceBox);
|
|
3978
3979
|
// 只有ready状态的检测器的结果才可信
|
|
3979
3980
|
if (this.detectionState.motionDetector.isReady()) {
|
|
3980
3981
|
if (!motionResult.isLively) {
|
|
@@ -4002,18 +4003,27 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4002
4003
|
details: motionResult.details,
|
|
4003
4004
|
}, 'warn');
|
|
4004
4005
|
}
|
|
4005
|
-
//
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
this.emitDetectorInfo({ code: DetectionCode.FACE_TOO_SMALL, faceRatio: faceRatio });
|
|
4009
|
-
this.emitDebug('detection', 'Face is too small', { ratio: faceRatio.toFixed(4), minRatio: this.options.collect_min_face_ratio, maxRatio: this.options.collect_max_face_ratio }, 'info');
|
|
4006
|
+
// 动作活体检测阶段处理
|
|
4007
|
+
if (this.detectionState.period === DetectionPeriod.VERIFY) {
|
|
4008
|
+
this.handleVerifyPhase(gestures);
|
|
4010
4009
|
return;
|
|
4011
4010
|
}
|
|
4011
|
+
// 面部区域过大则跳过当前帧
|
|
4012
4012
|
if (faceRatio >= this.options.collect_max_face_ratio) {
|
|
4013
4013
|
this.emitDetectorInfo({ code: DetectionCode.FACE_TOO_LARGE, faceRatio: faceRatio });
|
|
4014
4014
|
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');
|
|
4015
4015
|
return;
|
|
4016
4016
|
}
|
|
4017
|
+
// 捕获并准备帧数据
|
|
4018
|
+
const frameData = this.captureAndPrepareFrames();
|
|
4019
|
+
if (!frameData) {
|
|
4020
|
+
this.emitDebug('detection', '帧采集失败,无法继续检测', {
|
|
4021
|
+
frameIndex: this.frameIndex
|
|
4022
|
+
}, 'warn');
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
const bgrFrame = frameData.bgrFrame;
|
|
4026
|
+
const grayFrame = frameData.grayFrame;
|
|
4017
4027
|
let frontal = 1;
|
|
4018
4028
|
// 计算面部正对度,不达标则跳过当前帧
|
|
4019
4029
|
if (this.detectionState.needFrontalFace()) {
|
|
@@ -4025,6 +4035,7 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4025
4035
|
return;
|
|
4026
4036
|
}
|
|
4027
4037
|
}
|
|
4038
|
+
// 计算图像质量分数,不达标则跳过当前帧
|
|
4028
4039
|
const qualityResult = calcImageQuality(this.cv, grayFrame, this.options.collect_image_quality_features, this.options.collect_min_image_quality);
|
|
4029
4040
|
if (!qualityResult.passed || qualityResult.score < this.options.collect_min_image_quality) {
|
|
4030
4041
|
this.emitDetectorInfo({ code: DetectionCode.FACE_LOW_QUALITY, faceRatio: faceRatio, faceFrontal: frontal, imageQuality: qualityResult.score });
|
|
@@ -4039,15 +4050,15 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4039
4050
|
}
|
|
4040
4051
|
// 当前帧通过常规检查
|
|
4041
4052
|
this.emitDetectorInfo({ passed: true, code: DetectionCode.FACE_CHECK_PASS, faceRatio: faceRatio, faceFrontal: frontal, imageQuality: qualityResult.score });
|
|
4042
|
-
// 处理不同检测阶段的逻辑
|
|
4043
4053
|
// 检测阶段,图像各方面合规,进入采集阶段
|
|
4044
4054
|
if (this.detectionState.period === DetectionPeriod.DETECT) {
|
|
4045
4055
|
this.handleDetectPhase();
|
|
4046
4056
|
}
|
|
4047
|
-
//
|
|
4057
|
+
// 采集阶段,采集当前帧图像
|
|
4048
4058
|
if (this.detectionState.period === DetectionPeriod.COLLECT) {
|
|
4049
4059
|
this.handleCollectPhase(bgrFrame, qualityResult.score, faceBox);
|
|
4050
4060
|
}
|
|
4061
|
+
// 采集到足够的图像,并且静默活体通过,进入动作验证阶段
|
|
4051
4062
|
if (this.detectionState.isReadyToVerify(this.options.collect_min_collect_count)) {
|
|
4052
4063
|
this.emitDebug('detection', 'Ready to enter action verification phase', {
|
|
4053
4064
|
collectCount: this.detectionState.collectCount,
|
|
@@ -4062,9 +4073,6 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4062
4073
|
return;
|
|
4063
4074
|
}
|
|
4064
4075
|
}
|
|
4065
|
-
if (this.detectionState.period === DetectionPeriod.VERIFY) {
|
|
4066
|
-
this.handleVerifyPhase(gestures);
|
|
4067
|
-
}
|
|
4068
4076
|
}
|
|
4069
4077
|
catch (error) {
|
|
4070
4078
|
const errorInfo = this.extractErrorInfo(error);
|
|
@@ -4091,12 +4099,14 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4091
4099
|
this.collectHighQualityImage(bgrFrame, qualityScore, faceBox);
|
|
4092
4100
|
}
|
|
4093
4101
|
/**
|
|
4094
|
-
*
|
|
4102
|
+
* 验证动作阶段处理
|
|
4103
|
+
* @param gestures - Detected gestures from Human.js
|
|
4095
4104
|
*/
|
|
4096
4105
|
handleVerifyPhase(gestures) {
|
|
4097
|
-
// No action set yet, will continue after setting
|
|
4098
4106
|
if (!this.detectionState.currentAction) {
|
|
4107
|
+
// 当前无动作,选择下一个动作
|
|
4099
4108
|
if (!this.selectNextAction()) {
|
|
4109
|
+
// 下一个动作不可用,内部错误
|
|
4100
4110
|
this.emit('detector-error', {
|
|
4101
4111
|
code: ErrorCode.INTERNAL_ERROR,
|
|
4102
4112
|
message: 'No available actions to perform for liveness verification'
|
|
@@ -4106,32 +4116,47 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4106
4116
|
}
|
|
4107
4117
|
return;
|
|
4108
4118
|
}
|
|
4109
|
-
//
|
|
4110
|
-
const
|
|
4111
|
-
if (
|
|
4119
|
+
// 检测实际动作
|
|
4120
|
+
const detectedActions = this.detectAction(gestures);
|
|
4121
|
+
if (detectedActions.length === 0) {
|
|
4122
|
+
// 没有任何动作,继续检测
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4125
|
+
// 验证检测到的动作:只有检测到期望的动作才算成功
|
|
4126
|
+
// 如果同时检测到多个动作(如NOD_DOWN和NOD_UP),只要包含期望的动作即可
|
|
4127
|
+
if (!detectedActions.includes(this.detectionState.currentAction)) {
|
|
4128
|
+
this.emitDebug('liveness', 'Action mismatch', {
|
|
4129
|
+
expected: this.detectionState.currentAction,
|
|
4130
|
+
detected: detectedActions
|
|
4131
|
+
}, 'warn');
|
|
4132
|
+
this.emit('detector-action', {
|
|
4133
|
+
action: this.detectionState.currentAction,
|
|
4134
|
+
detected: detectedActions,
|
|
4135
|
+
status: LivenessActionStatus.MISMATCH
|
|
4136
|
+
});
|
|
4137
|
+
this.stopDetection(false);
|
|
4112
4138
|
return;
|
|
4113
4139
|
}
|
|
4114
|
-
|
|
4140
|
+
// 动作验证成功
|
|
4141
|
+
this.emit('detector-action', {
|
|
4115
4142
|
action: this.detectionState.currentAction,
|
|
4143
|
+
detected: detectedActions,
|
|
4116
4144
|
status: LivenessActionStatus.COMPLETED
|
|
4117
|
-
};
|
|
4118
|
-
// Action completed
|
|
4119
|
-
this.emit('detector-action', actionComplete);
|
|
4145
|
+
});
|
|
4120
4146
|
this.emitDebug('liveness', 'Action detected', { action: this.detectionState.currentAction });
|
|
4121
4147
|
this.detectionState.onActionCompleted();
|
|
4122
|
-
//
|
|
4148
|
+
// 检查是否完成所有动作
|
|
4123
4149
|
if (this.detectionState.completedActions.size >= this.getPerformActionCount()) {
|
|
4124
4150
|
this.stopDetection(true);
|
|
4125
4151
|
return;
|
|
4126
4152
|
}
|
|
4127
|
-
//
|
|
4153
|
+
// 选择下一个动作
|
|
4128
4154
|
if (!this.selectNextAction()) {
|
|
4129
4155
|
this.emit('detector-error', {
|
|
4130
4156
|
code: ErrorCode.INTERNAL_ERROR,
|
|
4131
4157
|
message: 'No available actions to perform for liveness verification'
|
|
4132
4158
|
});
|
|
4133
4159
|
this.stopDetection(false);
|
|
4134
|
-
return;
|
|
4135
4160
|
}
|
|
4136
4161
|
}
|
|
4137
4162
|
/**
|
|
@@ -4215,6 +4240,7 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4215
4240
|
}
|
|
4216
4241
|
const actionStart = {
|
|
4217
4242
|
action: nextAction,
|
|
4243
|
+
detected: [],
|
|
4218
4244
|
status: LivenessActionStatus.STARTED
|
|
4219
4245
|
};
|
|
4220
4246
|
this.emit('detector-action', actionStart);
|
|
@@ -4226,6 +4252,7 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4226
4252
|
}, 'warn');
|
|
4227
4253
|
this.emit('detector-action', {
|
|
4228
4254
|
action: nextAction,
|
|
4255
|
+
detected: [],
|
|
4229
4256
|
status: LivenessActionStatus.TIMEOUT
|
|
4230
4257
|
});
|
|
4231
4258
|
this.partialResetDetectionState();
|
|
@@ -4233,49 +4260,63 @@ class FaceDetectionEngine extends SimpleEventEmitter {
|
|
|
4233
4260
|
return true;
|
|
4234
4261
|
}
|
|
4235
4262
|
/**
|
|
4236
|
-
* Detect
|
|
4263
|
+
* Detect all actions from gestures
|
|
4264
|
+
* @returns Array of detected actions, empty array if none detected
|
|
4237
4265
|
*/
|
|
4238
|
-
detectAction(
|
|
4266
|
+
detectAction(gestures) {
|
|
4267
|
+
const detectedActions = [];
|
|
4239
4268
|
if (!gestures || gestures.length === 0) {
|
|
4240
|
-
this.emitDebug('liveness', 'No gestures detected for action verification', {
|
|
4241
|
-
return
|
|
4269
|
+
this.emitDebug('liveness', 'No gestures detected for action verification', { gestureCount: gestures?.length ?? 0 }, 'info');
|
|
4270
|
+
return detectedActions;
|
|
4242
4271
|
}
|
|
4243
4272
|
try {
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
const percent = parseInt(percentMatch[1]) / 100; // Convert to 0-1 range
|
|
4260
|
-
return percent > (this.options.action_liveness_min_mouth_open_percent);
|
|
4261
|
-
});
|
|
4262
|
-
case LivenessAction.NOD:
|
|
4263
|
-
return gestures.some(g => {
|
|
4264
|
-
if (!g.gesture)
|
|
4265
|
-
return false;
|
|
4266
|
-
// Check for continuous head movement (up -> down or down -> up)
|
|
4267
|
-
const headPattern = g.gesture.match(/head\s+(up|down)/i);
|
|
4268
|
-
return !!headPattern && !!headPattern[1];
|
|
4269
|
-
});
|
|
4270
|
-
default:
|
|
4271
|
-
this.emitDebug('liveness', 'Unknown action type in detection', { action }, 'warn');
|
|
4273
|
+
// Check for BLINK
|
|
4274
|
+
if (gestures.some(g => {
|
|
4275
|
+
if (!g.gesture)
|
|
4276
|
+
return false;
|
|
4277
|
+
return g.gesture.includes('blink');
|
|
4278
|
+
})) {
|
|
4279
|
+
detectedActions.push(LivenessAction.BLINK);
|
|
4280
|
+
}
|
|
4281
|
+
// Check for MOUTH_OPEN
|
|
4282
|
+
if (gestures.some(g => {
|
|
4283
|
+
const gestureStr = g.gesture;
|
|
4284
|
+
if (!gestureStr || !gestureStr.includes('mouth'))
|
|
4285
|
+
return false;
|
|
4286
|
+
const percentMatch = gestureStr.match(/mouth\s+(\d+)%\s+open/);
|
|
4287
|
+
if (!percentMatch || !percentMatch[1])
|
|
4272
4288
|
return false;
|
|
4289
|
+
const percent = parseInt(percentMatch[1]) / 100; // Convert to 0-1 range
|
|
4290
|
+
return percent > (this.options.action_liveness_min_mouth_open_percent);
|
|
4291
|
+
})) {
|
|
4292
|
+
detectedActions.push(LivenessAction.MOUTH_OPEN);
|
|
4293
|
+
}
|
|
4294
|
+
// Check for NOD_DOWN (head down)
|
|
4295
|
+
if (gestures.some(g => {
|
|
4296
|
+
if (!g.gesture)
|
|
4297
|
+
return false;
|
|
4298
|
+
const headPattern = g.gesture.match(/head\s+down/i);
|
|
4299
|
+
return !!headPattern;
|
|
4300
|
+
})) {
|
|
4301
|
+
detectedActions.push(LivenessAction.NOD_DOWN);
|
|
4302
|
+
}
|
|
4303
|
+
// Check for NOD_UP (head up)
|
|
4304
|
+
if (gestures.some(g => {
|
|
4305
|
+
if (!g.gesture)
|
|
4306
|
+
return false;
|
|
4307
|
+
const headPattern = g.gesture.match(/head\s+up/i);
|
|
4308
|
+
return !!headPattern;
|
|
4309
|
+
})) {
|
|
4310
|
+
detectedActions.push(LivenessAction.NOD_UP);
|
|
4311
|
+
}
|
|
4312
|
+
if (detectedActions.length > 0) {
|
|
4313
|
+
this.emitDebug('liveness', 'Actions detected', { detectedActions }, 'info');
|
|
4273
4314
|
}
|
|
4274
4315
|
}
|
|
4275
4316
|
catch (error) {
|
|
4276
|
-
this.emitDebug('liveness', 'Error during action detection', {
|
|
4277
|
-
return false;
|
|
4317
|
+
this.emitDebug('liveness', 'Error during action detection', { error: error.message }, 'error');
|
|
4278
4318
|
}
|
|
4319
|
+
return detectedActions;
|
|
4279
4320
|
}
|
|
4280
4321
|
/**
|
|
4281
4322
|
* Emit debug event
|