@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 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["NOD"] = "nod";
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.NOD],
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
- const frameData = this.captureAndPrepareFrames();
3969
- if (!frameData) {
3970
- this.emitDebug('detection', '帧采集失败,无法继续检测', {
3971
- frameIndex: this.frameIndex
3972
- }, 'warn');
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
- bgrFrame = frameData.bgrFrame;
3976
- grayFrame = frameData.grayFrame;
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
- const faceRatio = (faceBox[2] * faceBox[3]) / (this.actualVideoWidth * this.actualVideoHeight);
4007
- if (faceRatio <= this.options.collect_min_face_ratio) {
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
- * Handle verify phase
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
- // Check if action detected
4110
- const detected = this.detectAction(this.detectionState.currentAction, gestures);
4111
- if (!detected) {
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
- const actionComplete = {
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
- // Check if all required actions completed
4148
+ // 检查是否完成所有动作
4123
4149
  if (this.detectionState.completedActions.size >= this.getPerformActionCount()) {
4124
4150
  this.stopDetection(true);
4125
4151
  return;
4126
4152
  }
4127
- // Select next action
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 specific action
4263
+ * Detect all actions from gestures
4264
+ * @returns Array of detected actions, empty array if none detected
4237
4265
  */
4238
- detectAction(action, gestures) {
4266
+ detectAction(gestures) {
4267
+ const detectedActions = [];
4239
4268
  if (!gestures || gestures.length === 0) {
4240
- this.emitDebug('liveness', 'No gestures detected for action verification', { action, gestureCount: gestures?.length ?? 0 }, 'info');
4241
- return false;
4269
+ this.emitDebug('liveness', 'No gestures detected for action verification', { gestureCount: gestures?.length ?? 0 }, 'info');
4270
+ return detectedActions;
4242
4271
  }
4243
4272
  try {
4244
- switch (action) {
4245
- case LivenessAction.BLINK:
4246
- return gestures.some(g => {
4247
- if (!g.gesture)
4248
- return false;
4249
- return g.gesture.includes('blink');
4250
- });
4251
- case LivenessAction.MOUTH_OPEN:
4252
- return gestures.some(g => {
4253
- const gestureStr = g.gesture;
4254
- if (!gestureStr || !gestureStr.includes('mouth'))
4255
- return false;
4256
- const percentMatch = gestureStr.match(/mouth\s+(\d+)%\s+open/);
4257
- if (!percentMatch || !percentMatch[1])
4258
- return false;
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', { action, error: error.message }, 'error');
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