id-scanner-lib 1.3.3 → 1.6.2

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.
Files changed (80) hide show
  1. package/README.md +324 -410
  2. package/dist/id-scanner-lib.esm.js +4826 -0
  3. package/dist/id-scanner-lib.esm.js.map +1 -0
  4. package/dist/id-scanner-lib.js +4858 -0
  5. package/dist/id-scanner-lib.js.map +1 -0
  6. package/dist/types/browser-image-compression.d.ts +19 -0
  7. package/dist/types/tesseract.d.ts +280 -0
  8. package/package.json +89 -78
  9. package/src/core/base-module.ts +78 -0
  10. package/src/core/camera-manager.ts +813 -0
  11. package/src/core/config.ts +305 -0
  12. package/src/core/errors.ts +174 -0
  13. package/src/core/event-emitter.test.ts +42 -0
  14. package/src/core/event-emitter.ts +110 -0
  15. package/src/core/loading-state.test.ts +67 -0
  16. package/src/core/loading-state.ts +156 -0
  17. package/src/core/logger.test.ts +49 -0
  18. package/src/core/logger.ts +549 -0
  19. package/src/core/module-manager.ts +163 -0
  20. package/src/core/plugin-manager.ts +429 -0
  21. package/src/core/resource-manager.ts +762 -0
  22. package/src/core/result.ts +163 -0
  23. package/src/core/scanner-factory.ts +236 -0
  24. package/src/index.ts +117 -939
  25. package/src/interfaces/external-types.ts +200 -0
  26. package/src/interfaces/face-detection.ts +309 -0
  27. package/src/interfaces/scanner-module.ts +384 -0
  28. package/src/modules/face/face-detector.ts +988 -0
  29. package/src/modules/face/index.ts +208 -0
  30. package/src/modules/face/liveness-detector.ts +908 -0
  31. package/src/modules/face/types.ts +133 -0
  32. package/src/{id-recognition → modules/id-card}/anti-fake-detector.ts +274 -240
  33. package/src/modules/id-card/id-card-detector.ts +474 -0
  34. package/src/modules/id-card/index.ts +425 -0
  35. package/src/{id-recognition → modules/id-card}/ocr-processor.ts +149 -92
  36. package/src/modules/id-card/ocr-worker.ts +259 -0
  37. package/src/modules/id-card/types.ts +178 -0
  38. package/src/modules/qrcode/index.ts +175 -0
  39. package/src/modules/qrcode/qr-code-scanner.ts +231 -0
  40. package/src/modules/qrcode/types.ts +169 -0
  41. package/src/types/common.test.ts +99 -0
  42. package/src/types/common.ts +166 -0
  43. package/src/types/tesseract.d.ts +265 -22
  44. package/src/utils/camera.test.ts +30 -0
  45. package/src/utils/camera.ts +4 -1
  46. package/src/utils/error-handler.test.ts +137 -0
  47. package/src/utils/error-handler.ts +110 -0
  48. package/src/utils/image-processing.ts +68 -49
  49. package/src/utils/index.test.ts +186 -0
  50. package/src/utils/index.ts +429 -0
  51. package/src/utils/performance.ts +168 -131
  52. package/src/utils/resource-manager.ts +65 -146
  53. package/src/utils/retry.test.ts +142 -0
  54. package/src/utils/retry.ts +282 -0
  55. package/src/utils/types.ts +90 -2
  56. package/src/utils/utils.test.ts +171 -0
  57. package/src/utils/worker.ts +123 -84
  58. package/src/version.ts +11 -0
  59. package/tools/scaffold.js +543 -0
  60. package/dist/id-scanner-core.esm.js +0 -11349
  61. package/dist/id-scanner-core.js +0 -11361
  62. package/dist/id-scanner-core.min.js +0 -1
  63. package/dist/id-scanner-ocr.esm.js +0 -2319
  64. package/dist/id-scanner-ocr.js +0 -2328
  65. package/dist/id-scanner-ocr.min.js +0 -1
  66. package/dist/id-scanner-qr.esm.js +0 -1296
  67. package/dist/id-scanner-qr.js +0 -1305
  68. package/dist/id-scanner-qr.min.js +0 -1
  69. package/dist/id-scanner.js +0 -4561
  70. package/dist/id-scanner.min.js +0 -1
  71. package/src/core.ts +0 -138
  72. package/src/demo/demo.ts +0 -204
  73. package/src/id-recognition/data-extractor.ts +0 -262
  74. package/src/id-recognition/id-detector.ts +0 -510
  75. package/src/id-recognition/ocr-worker.ts +0 -156
  76. package/src/index-umd.ts +0 -477
  77. package/src/ocr-module.ts +0 -187
  78. package/src/qr-module.ts +0 -179
  79. package/src/scanner/barcode-scanner.ts +0 -251
  80. package/src/scanner/qr-scanner.ts +0 -167
@@ -0,0 +1,908 @@
1
+ /**
2
+ * @file 活体检测模块
3
+ * @description 提供人脸活体检测功能
4
+ * @module modules/face/liveness-detector
5
+ */
6
+
7
+ import { BaseScannerModule, ModuleCapabilities, ModuleEvent, ModuleInitOptions, ModuleStatus, ModuleType } from '../../interfaces/scanner-module';
8
+ import { FaceDetectionOptions, FaceDetectionResult, LivenessAction, LivenessDetectionResult, LivenessDetectionType, LivenessSession, Point } from '../../interfaces/face-detection';
9
+ import { ConfigManager } from '../../core/config';
10
+ import { Logger } from '../../core/logger';
11
+ import { ResourceManager } from '../../core/resource-manager';
12
+ import { CameraManager, CameraEvent } from '../../core/camera-manager';
13
+ import { Result } from '../../core/result';
14
+ import { FaceDetector } from './face-detector';
15
+ import { InitializationError, LivenessDetectionError } from '../../core/errors';
16
+ import { debounce, generateUUID } from '../../utils';
17
+
18
+ /**
19
+ * 眨眼检测阈值配置
20
+ */
21
+ interface BlinkThresholds {
22
+ /** 眼睛闭合阈值 */
23
+ eyeClosedThreshold: number;
24
+ /** 眼睛张开阈值 */
25
+ eyeOpenThreshold: number;
26
+ /** 检测阈值 */
27
+ detectionThreshold: number;
28
+ }
29
+
30
+ /**
31
+ * 活体检测器配置
32
+ */
33
+ export interface LivenessDetectorConfig {
34
+ /** 是否启用 */
35
+ enabled: boolean;
36
+ /** 检测类型 */
37
+ detectionType: LivenessDetectionType;
38
+ /** 检测置信度阈值 */
39
+ confidenceThreshold: number;
40
+ /** 眨眼检测阈值 */
41
+ blinkThresholds: BlinkThresholds;
42
+ /** 活体挑战超时(毫秒) */
43
+ challengeTimeout: number;
44
+ /** 活体挑战动作 */
45
+ challengeActions: LivenessAction[];
46
+ }
47
+
48
+ /**
49
+ * 活体检测模块
50
+ * 提供人脸活体检测功能,支持被动式和主动式活体检测
51
+ */
52
+ export class LivenessDetector extends BaseScannerModule {
53
+ /** 模块类型 */
54
+ readonly type: ModuleType = ModuleType.FACE;
55
+
56
+ /** 模块配置 */
57
+ protected config: LivenessDetectorConfig;
58
+
59
+ /** 默认配置 */
60
+ private static readonly DEFAULT_CONFIG: LivenessDetectorConfig = {
61
+ enabled: true,
62
+ detectionType: LivenessDetectionType.PASSIVE,
63
+ confidenceThreshold: 0.7,
64
+ blinkThresholds: {
65
+ eyeClosedThreshold: 0.23,
66
+ eyeOpenThreshold: 0.30,
67
+ detectionThreshold: 0.85
68
+ },
69
+ challengeTimeout: 30000, // 30秒
70
+ challengeActions: [
71
+ LivenessAction.BLINK,
72
+ LivenessAction.NOD,
73
+ LivenessAction.SMILE
74
+ ]
75
+ };
76
+
77
+ /** 配置管理器 */
78
+ private configManager: ConfigManager;
79
+
80
+ /** 日志记录器 */
81
+ private logger: Logger;
82
+
83
+ /** 资源管理器 */
84
+ private resourceManager: ResourceManager;
85
+
86
+ /** 摄像头管理器 */
87
+ private cameraManager: CameraManager;
88
+
89
+ /** 人脸检测器 */
90
+ private faceDetector: FaceDetector;
91
+
92
+ /** 当前活体会话 */
93
+ private currentSession: LivenessSession | null = null;
94
+
95
+ /** 检测历史记录,用于分析眨眼等动作 */
96
+ private detectionHistory: Array<{
97
+ timestamp: number;
98
+ eyeState: 'open' | 'closed' | 'unknown';
99
+ faceResult: FaceDetectionResult;
100
+ }> = [];
101
+
102
+ /** 最大历史记录长度 */
103
+ private readonly MAX_HISTORY_LENGTH = 30;
104
+
105
+ /** 防抖处理函数 */
106
+ private debouncedProcessFrame: (frameData: ImageData) => void;
107
+
108
+ /**
109
+ * 构造函数
110
+ * @param config 初始配置
111
+ * @param faceDetector 可选的人脸检测器实例
112
+ */
113
+ constructor(config: Partial<LivenessDetectorConfig> = {}, faceDetector?: FaceDetector) {
114
+ super({ enabled: true, ...config });
115
+
116
+ this.configManager = ConfigManager.getInstance();
117
+ this.logger = Logger.getInstance();
118
+ this.resourceManager = ResourceManager.getInstance();
119
+ this.cameraManager = CameraManager.getInstance();
120
+
121
+ // 合并配置
122
+ this.config = {
123
+ ...LivenessDetector.DEFAULT_CONFIG,
124
+ ...config
125
+ };
126
+
127
+ // 使用传入或创建新的人脸检测器
128
+ this.faceDetector = faceDetector || new FaceDetector({
129
+ detectLandmarks: true,
130
+ detectExpressions: true
131
+ });
132
+
133
+ // 创建防抖处理函数
134
+ this.debouncedProcessFrame = debounce(this.processFrame.bind(this), 100);
135
+ }
136
+
137
+ /**
138
+ * 获取模块能力
139
+ */
140
+ get capabilities(): ModuleCapabilities {
141
+ return {
142
+ supportsVideo: true,
143
+ supportsImage: true,
144
+ supportsBatch: false,
145
+ supportsRealtime: true,
146
+ supportsWebWorker: false,
147
+ supportedMediaTypes: ['image/jpeg', 'image/png', 'image/webp']
148
+ };
149
+ }
150
+
151
+ /**
152
+ * 初始化模块
153
+ * @param options 初始化选项
154
+ */
155
+ async initialize(options?: ModuleInitOptions): Promise<void> {
156
+ if (this._status === ModuleStatus.INITIALIZING) {
157
+ throw new Error('活体检测模块正在初始化中');
158
+ }
159
+
160
+ if (this._status === ModuleStatus.READY) {
161
+ this.logger.debug('LivenessDetector', '活体检测模块已初始化');
162
+ return;
163
+ }
164
+
165
+ this.setStatus(ModuleStatus.INITIALIZING);
166
+ this.emit(ModuleEvent.INIT_START);
167
+
168
+ try {
169
+ // 应用配置选项
170
+ if (options?.config) {
171
+ this.updateConfig(options.config);
172
+ }
173
+
174
+ // 设置调试模式
175
+ if (options?.debug !== undefined) {
176
+ this.debug = options.debug;
177
+ }
178
+
179
+ // 确保人脸检测器已初始化
180
+ if (this.faceDetector.getStatus() !== ModuleStatus.READY) {
181
+ await this.faceDetector.initialize(options);
182
+ }
183
+
184
+ // 监听人脸检测器结果
185
+ this.faceDetector.on(ModuleEvent.REALTIME_RESULT, this.handleFaceDetectionResult.bind(this));
186
+
187
+ // 绑定摄像头事件
188
+ if (options?.bindCamera) {
189
+ this.cameraManager.on(CameraEvent.FRAME, this.handleCameraFrame.bind(this));
190
+ }
191
+
192
+ this.setStatus(ModuleStatus.READY);
193
+ this.emit(ModuleEvent.INIT_COMPLETE);
194
+ } catch (error) {
195
+ const errorMessage = error instanceof Error ? error.message : String(error);
196
+ this.logger.error('LivenessDetector', `初始化失败: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
197
+
198
+ this.setStatus(ModuleStatus.ERROR);
199
+ this.emit(ModuleEvent.INIT_ERROR, { error: error instanceof Error ? error : new Error(errorMessage) });
200
+
201
+ throw new Error(`活体检测模块初始化失败: ${errorMessage}`);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * 处理图片
207
+ * @param image 图片源
208
+ * @param options 处理选项
209
+ */
210
+ async processImage(
211
+ image: string | HTMLImageElement | HTMLCanvasElement | ImageData,
212
+ options: Record<string, any> = {}
213
+ ): Promise<Result<LivenessDetectionResult>> {
214
+ this.checkInitialized();
215
+
216
+ if (this._status === ModuleStatus.PROCESSING) {
217
+ return Result.failure(new LivenessDetectionError('另一个处理操作正在进行中'));
218
+ }
219
+
220
+ this.setStatus(ModuleStatus.PROCESSING);
221
+ this.emit(ModuleEvent.PROCESS_START);
222
+
223
+ try {
224
+ // 首先使用人脸检测器处理图片
225
+ const faceResult = await this.faceDetector.processImage(image, {
226
+ withLandmarks: true,
227
+ withAttributes: true
228
+ });
229
+
230
+ if (!faceResult.isSuccess() || !faceResult.data || faceResult.data.length === 0) {
231
+ throw new LivenessDetectionError('未在图像中检测到人脸');
232
+ }
233
+
234
+ // 获取检测到的第一个人脸
235
+ const face = faceResult.data[0];
236
+
237
+ // 执行被动式活体检测
238
+ const livenessResult = this.performPassiveLivenessDetection(face);
239
+
240
+ this.setStatus(ModuleStatus.READY);
241
+ this.emit(ModuleEvent.PROCESS_COMPLETE, { result: livenessResult });
242
+
243
+ return Result.success(livenessResult);
244
+ } catch (error) {
245
+ const errorMessage = error instanceof Error ? error.message : String(error);
246
+ this.logger.error('LivenessDetector', `图片处理失败: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
247
+
248
+ this.setStatus(ModuleStatus.ERROR);
249
+ this.emit(ModuleEvent.PROCESS_ERROR, { error: error instanceof Error ? error : new Error(errorMessage) });
250
+
251
+ return Result.failure(new LivenessDetectionError(`活体检测失败: ${errorMessage}`));
252
+ }
253
+ }
254
+
255
+ /**
256
+ * 开始活体检测会话
257
+ * @param type 活体检测类型
258
+ */
259
+ startSession(type: LivenessDetectionType = this.config.detectionType): Result<LivenessSession> {
260
+ this.checkInitialized();
261
+
262
+ // 如果已有会话,先停止
263
+ if (this.currentSession) {
264
+ this.stopSession();
265
+ }
266
+
267
+ // 生成会话
268
+ let requiredActions: LivenessAction[] = [];
269
+
270
+ if (type === LivenessDetectionType.ACTIVE || type === LivenessDetectionType.HYBRID) {
271
+ // 从配置的动作中随机选择2-3个
272
+ const availableActions = [...this.config.challengeActions];
273
+ const actionCount = Math.min(availableActions.length, Math.floor(Math.random() * 2) + 2); // 2-3个动作
274
+
275
+ // 打乱动作顺序
276
+ for (let i = availableActions.length - 1; i > 0; i--) {
277
+ const j = Math.floor(Math.random() * (i + 1));
278
+ [availableActions[i], availableActions[j]] = [availableActions[j], availableActions[i]];
279
+ }
280
+
281
+ requiredActions = availableActions.slice(0, actionCount);
282
+
283
+ // 确保至少包含眨眼动作
284
+ if (!requiredActions.includes(LivenessAction.BLINK)) {
285
+ requiredActions[0] = LivenessAction.BLINK;
286
+ }
287
+ }
288
+
289
+ // 创建会话
290
+ this.currentSession = {
291
+ id: generateUUID(),
292
+ type,
293
+ requiredActions,
294
+ currentActionIndex: 0,
295
+ startTime: Date.now(),
296
+ timeout: this.config.challengeTimeout,
297
+ status: 'active',
298
+ completedActions: []
299
+ };
300
+
301
+ // 清空历史记录
302
+ this.detectionHistory = [];
303
+
304
+ this.logger.debug('LivenessDetector', `开始活体检测会话,类型: ${type}`, {
305
+ session: {
306
+ id: this.currentSession.id,
307
+ requiredActions
308
+ }
309
+ } as any);
310
+
311
+ return Result.success(this.currentSession);
312
+ }
313
+
314
+ /**
315
+ * 停止当前会话
316
+ */
317
+ stopSession(): void {
318
+ if (this.currentSession) {
319
+ if (this.currentSession.status === 'active') {
320
+ this.currentSession.status = 'failed';
321
+ }
322
+
323
+ this.logger.debug('LivenessDetector', '停止活体检测会话', {
324
+ session: {
325
+ id: this.currentSession.id,
326
+ status: this.currentSession.status
327
+ }
328
+ } as any );
329
+
330
+ this.currentSession = null;
331
+ }
332
+
333
+ // 清空历史记录
334
+ this.detectionHistory = [];
335
+ }
336
+
337
+ /**
338
+ * 获取当前活体检测会话
339
+ */
340
+ getCurrentSession(): LivenessSession | null {
341
+ return this.currentSession ? { ...this.currentSession } : null;
342
+ }
343
+
344
+ /**
345
+ * 开始实时处理
346
+ * @param videoElement 视频元素
347
+ * @param options 处理选项
348
+ */
349
+ async startRealtime(
350
+ videoElement?: HTMLVideoElement,
351
+ options: Record<string, any> = {}
352
+ ): Promise<Result<boolean>> {
353
+ this.checkInitialized();
354
+
355
+ if (this._status === ModuleStatus.PROCESSING) {
356
+ return Result.failure(new LivenessDetectionError('实时处理已在进行中'));
357
+ }
358
+
359
+ try {
360
+ // 停止现有会话
361
+ this.stopSession();
362
+
363
+ // 启动新会话
364
+ const sessionType = options.livenessType || this.config.detectionType;
365
+ this.startSession(sessionType as LivenessDetectionType);
366
+
367
+ // 启动人脸检测
368
+ const faceDetectorResult = await this.faceDetector.startRealtime(videoElement, {
369
+ withLandmarks: true,
370
+ withAttributes: true,
371
+ processingInterval: options.processingInterval || 100
372
+ });
373
+
374
+ if (!faceDetectorResult.isSuccess()) {
375
+ throw new Error('无法启动人脸检测');
376
+ }
377
+
378
+ this.setStatus(ModuleStatus.PROCESSING);
379
+
380
+ this.logger.debug('LivenessDetector', '开始实时活体检测');
381
+
382
+ return Result.success(true);
383
+ } catch (error) {
384
+ const errorMessage = error instanceof Error ? error.message : String(error);
385
+ this.logger.error('LivenessDetector', `启动实时活体检测失败: ${errorMessage}`, error instanceof Error ? error : new Error(errorMessage));
386
+
387
+ return Result.failure(new LivenessDetectionError(`启动实时活体检测失败: ${errorMessage}`));
388
+ }
389
+ }
390
+
391
+ /**
392
+ * 停止实时处理
393
+ */
394
+ stopRealtime(): void {
395
+ // 停止人脸检测
396
+ this.faceDetector.stopRealtime();
397
+
398
+ // 停止活体会话
399
+ this.stopSession();
400
+
401
+ if (this._status === ModuleStatus.PROCESSING) {
402
+ this.setStatus(ModuleStatus.READY);
403
+ }
404
+ }
405
+
406
+ /**
407
+ * 释放资源
408
+ */
409
+ async dispose(): Promise<void> {
410
+ // 停止实时处理
411
+ this.stopRealtime();
412
+
413
+ // 移除事件监听
414
+ this.faceDetector.off(ModuleEvent.REALTIME_RESULT, this.handleFaceDetectionResult.bind(this));
415
+ this.cameraManager.off(CameraEvent.FRAME, this.handleCameraFrame.bind(this));
416
+
417
+ // 释放人脸检测器资源
418
+ await this.faceDetector.dispose();
419
+
420
+ this._status = ModuleStatus.NOT_INITIALIZED;
421
+ }
422
+
423
+ /**
424
+ * 处理摄像头帧
425
+ */
426
+ private handleCameraFrame(event: any): void {
427
+ if (this._status !== ModuleStatus.PROCESSING || !event.frameData) {
428
+ return;
429
+ }
430
+
431
+ // 使用防抖函数处理帧,减少计算负担
432
+ this.debouncedProcessFrame(event.frameData);
433
+ }
434
+
435
+ /**
436
+ * 处理视频帧
437
+ */
438
+ private processFrame(frameData: ImageData): void {
439
+ // 实际处理逻辑委托给人脸检测器
440
+ // 我们将通过handleFaceDetectionResult接收结果
441
+ }
442
+
443
+ /**
444
+ * 处理人脸检测结果
445
+ */
446
+ private handleFaceDetectionResult(event: any): void {
447
+ if (!event.results || !Array.isArray(event.results) || event.results.length === 0) {
448
+ return;
449
+ }
450
+
451
+ // 仅处理最大的人脸
452
+ let bestFace = event.results[0];
453
+ let maxArea = bestFace.boundingBox.width * bestFace.boundingBox.height;
454
+
455
+ for (let i = 1; i < event.results.length; i++) {
456
+ const face = event.results[i];
457
+ const area = face.boundingBox.width * face.boundingBox.height;
458
+ if (area > maxArea) {
459
+ bestFace = face;
460
+ maxArea = area;
461
+ }
462
+ }
463
+
464
+ // 如果没有活跃的会话,不进行处理
465
+ if (!this.currentSession || this.currentSession.status !== 'active') {
466
+ return;
467
+ }
468
+
469
+ // 检查会话是否超时
470
+ const now = Date.now();
471
+ if (now - this.currentSession.startTime > this.currentSession.timeout) {
472
+ this.currentSession.status = 'timeout';
473
+ this.emit('liveness:timeout', { session: this.currentSession });
474
+ return;
475
+ }
476
+
477
+ // 根据活体检测类型进行处理
478
+ switch (this.currentSession.type) {
479
+ case LivenessDetectionType.PASSIVE:
480
+ this.handlePassiveLivenessDetection(bestFace);
481
+ break;
482
+ case LivenessDetectionType.ACTIVE:
483
+ this.handleActiveLivenessDetection(bestFace);
484
+ break;
485
+ case LivenessDetectionType.HYBRID:
486
+ this.handleHybridLivenessDetection(bestFace);
487
+ break;
488
+ }
489
+ }
490
+
491
+ /**
492
+ * 处理被动式活体检测
493
+ */
494
+ private handlePassiveLivenessDetection(face: FaceDetectionResult): void {
495
+ // 添加到历史记录
496
+ if (face.landmarks) {
497
+ // 添加眼睛状态到历史记录
498
+ this.addToHistory(face, 'unknown');
499
+
500
+ // 执行被动式活体检测
501
+ const result = this.performPassiveLivenessDetection(face);
502
+
503
+ // 如果检测到活体,完成会话
504
+ if (result.isLive && this.currentSession) {
505
+ this.currentSession.result = result;
506
+ this.currentSession.status = 'completed';
507
+
508
+ this.emit('liveness:detected', {
509
+ result,
510
+ session: this.currentSession
511
+ });
512
+ }
513
+ }
514
+ }
515
+
516
+ /**
517
+ * 处理主动式活体检测
518
+ */
519
+ private handleActiveLivenessDetection(face: FaceDetectionResult): void {
520
+ if (!this.currentSession || !this.currentSession.requiredActions ||
521
+ this.currentSession.currentActionIndex === undefined) {
522
+ return;
523
+ }
524
+
525
+ // 添加到历史记录
526
+ this.addToHistory(face, 'unknown');
527
+
528
+ // 获取当前要执行的动作
529
+ const currentAction = this.currentSession.requiredActions[this.currentSession.currentActionIndex];
530
+
531
+ // 检测动作是否完成
532
+ let actionCompleted = false;
533
+
534
+ switch (currentAction) {
535
+ case LivenessAction.BLINK:
536
+ actionCompleted = this.detectBlink(face);
537
+ break;
538
+ case LivenessAction.NOD:
539
+ actionCompleted = this.detectNod(face);
540
+ break;
541
+ case LivenessAction.SHAKE:
542
+ actionCompleted = this.detectHeadShake(face);
543
+ break;
544
+ case LivenessAction.SMILE:
545
+ actionCompleted = this.detectSmile(face);
546
+ break;
547
+ case LivenessAction.MOUTH_OPEN:
548
+ actionCompleted = this.detectMouthOpen(face);
549
+ break;
550
+ }
551
+
552
+ if (actionCompleted) {
553
+ // 记录完成的动作
554
+ if (!this.currentSession.completedActions) {
555
+ this.currentSession.completedActions = [];
556
+ }
557
+ this.currentSession.completedActions.push(currentAction);
558
+
559
+ // 进入下一个动作
560
+ this.currentSession.currentActionIndex++;
561
+
562
+ // 发出动作完成事件
563
+ this.emit('liveness:action:completed', {
564
+ action: currentAction,
565
+ session: this.currentSession
566
+ });
567
+
568
+ // 检查是否所有动作都已完成
569
+ if (this.currentSession.currentActionIndex >= this.currentSession.requiredActions.length) {
570
+ // 所有动作完成,执行最终的活体检测
571
+ const result: LivenessDetectionResult = {
572
+ isLive: true,
573
+ score: 1.0,
574
+ type: LivenessDetectionType.ACTIVE,
575
+ actions: this.currentSession.completedActions,
576
+ processingTime: Date.now() - this.currentSession.startTime
577
+ };
578
+
579
+ // 更新会话状态
580
+ this.currentSession.result = result;
581
+ this.currentSession.status = 'completed';
582
+
583
+ // 发出活体检测完成事件
584
+ this.emit('liveness:detected', {
585
+ result,
586
+ session: this.currentSession
587
+ });
588
+ }
589
+ }
590
+ }
591
+
592
+ /**
593
+ * 处理混合式活体检测
594
+ */
595
+ private handleHybridLivenessDetection(face: FaceDetectionResult): void {
596
+ // 同时执行被动和主动检测
597
+ this.handlePassiveLivenessDetection(face);
598
+ this.handleActiveLivenessDetection(face);
599
+ }
600
+
601
+ /**
602
+ * 执行被动式活体检测
603
+ */
604
+ private performPassiveLivenessDetection(face: FaceDetectionResult): LivenessDetectionResult {
605
+ // 初始分数
606
+ let livenessScore = 0.5;
607
+
608
+ // 检查人脸关键点和属性
609
+ if (face.landmarks) {
610
+ // 眨眼检测提高活体可能性
611
+ const hasBlinkHistory = this.detectionHistory.length > 5;
612
+ if (hasBlinkHistory) {
613
+ const blinkDetected = this.detectBlink(face);
614
+ if (blinkDetected) {
615
+ livenessScore += 0.3;
616
+ }
617
+ }
618
+
619
+ // 检查面部表情变化
620
+ const hasExpressionChange = this.detectionHistory.length > 5 && face.attributes?.emotion;
621
+ if (hasExpressionChange && face.attributes && face.attributes.emotion) {
622
+ const emotions = face.attributes.emotion;
623
+ const emotionValues = Object.values(emotions || {});
624
+ const emotionVariance = emotionValues.reduce((sum, val) => sum + Math.pow(val! - 0.5, 2), 0) / emotionValues.length;
625
+
626
+ // 表情变化提高活体可能性
627
+ if (emotionVariance > 0.1) {
628
+ livenessScore += 0.15;
629
+ }
630
+ }
631
+
632
+ // 检查微小的头部移动
633
+ if (this.detectionHistory.length > 3) {
634
+ const recentFaces = this.detectionHistory.slice(-3).map(h => h.faceResult);
635
+ const hasMovement = recentFaces.some((f, i) => {
636
+ if (i === 0) return false;
637
+ const prev = recentFaces[i - 1];
638
+ // 计算人脸中心点的移动
639
+ const prevCenter = {
640
+ x: prev.boundingBox.x + prev.boundingBox.width / 2,
641
+ y: prev.boundingBox.y + prev.boundingBox.height / 2
642
+ };
643
+ const currCenter = {
644
+ x: f.boundingBox.x + f.boundingBox.width / 2,
645
+ y: f.boundingBox.y + f.boundingBox.height / 2
646
+ };
647
+ // 小幅度移动更像真人
648
+ const dist = this.distance(prevCenter, currCenter);
649
+ return dist > 1 && dist < 20; // 小幅度移动
650
+ });
651
+
652
+ if (hasMovement) {
653
+ livenessScore += 0.1;
654
+ }
655
+ }
656
+ }
657
+
658
+ // 限制分数范围在0-1之间
659
+ livenessScore = Math.max(0, Math.min(1, livenessScore));
660
+
661
+ // 根据阈值确定是否为活体
662
+ const isLive = livenessScore >= this.config.confidenceThreshold;
663
+
664
+ return {
665
+ isLive,
666
+ score: livenessScore,
667
+ type: LivenessDetectionType.PASSIVE,
668
+ processingTime: 0
669
+ };
670
+ }
671
+
672
+ /**
673
+ * 添加检测结果到历史记录
674
+ */
675
+ private addToHistory(face: FaceDetectionResult, eyeState: 'open' | 'closed' | 'unknown'): void {
676
+ // 添加到历史记录
677
+ this.detectionHistory.push({
678
+ timestamp: Date.now(),
679
+ eyeState,
680
+ faceResult: face
681
+ });
682
+
683
+ // 限制历史长度
684
+ if (this.detectionHistory.length > this.MAX_HISTORY_LENGTH) {
685
+ this.detectionHistory.shift();
686
+ }
687
+ }
688
+
689
+ /**
690
+ * 计算眼睛纵横比(EAR)
691
+ * EAR是一种度量眼睛开合程度的指标
692
+ */
693
+ private calculateEyeAspectRatio(face: FaceDetectionResult): number | null {
694
+ if (!face.landmarks || !face.landmarks.points || face.landmarks.points.length < 68) {
695
+ return null;
696
+ }
697
+
698
+ const points = face.landmarks.points;
699
+
700
+ // 68点人脸模型中的眼睛索引
701
+ // 左眼:36-41, 右眼:42-47
702
+ const leftEye = [36, 37, 38, 39, 40, 41].map(i => points[i]);
703
+ const rightEye = [42, 43, 44, 45, 46, 47].map(i => points[i]);
704
+
705
+ // 计算左眼EAR
706
+ const leftEAR = (
707
+ this.distance(leftEye[1], leftEye[5]) + this.distance(leftEye[2], leftEye[4])
708
+ ) / (2 * this.distance(leftEye[0], leftEye[3]));
709
+
710
+ // 计算右眼EAR
711
+ const rightEAR = (
712
+ this.distance(rightEye[1], rightEye[5]) + this.distance(rightEye[2], rightEye[4])
713
+ ) / (2 * this.distance(rightEye[0], rightEye[3]));
714
+
715
+ // 返回平均EAR
716
+ return (leftEAR + rightEAR) / 2;
717
+ }
718
+
719
+ /**
720
+ * 计算两点之间的距离
721
+ */
722
+ private distance(p1: { x: number, y: number }, p2: { x: number, y: number }): number {
723
+ return Math.sqrt(
724
+ Math.pow(p2.x - p1.x, 2) +
725
+ Math.pow(p2.y - p1.y, 2)
726
+ );
727
+ }
728
+
729
+ /**
730
+ * 检测眨眼动作
731
+ */
732
+ private detectBlink(face: FaceDetectionResult): boolean {
733
+ // 如果历史记录不足,无法检测
734
+ if (this.detectionHistory.length < 5) {
735
+ return false;
736
+ }
737
+
738
+ // 计算当前帧的EAR
739
+ const currentEAR = this.calculateEyeAspectRatio(face);
740
+ if (currentEAR === null) {
741
+ return false;
742
+ }
743
+
744
+ // 获取短时间窗口内的帧
745
+ const recentHistory = this.detectionHistory.slice(-10);
746
+ const historyEARs = recentHistory
747
+ .map(h => this.calculateEyeAspectRatio(h.faceResult))
748
+ .filter(ear => ear !== null) as number[];
749
+
750
+ if (historyEARs.length < 3) {
751
+ return false;
752
+ }
753
+
754
+ // 计算眼睛状态序列
755
+ const eyeStates = historyEARs.map(ear => {
756
+ if (ear < this.config.blinkThresholds.eyeClosedThreshold) {
757
+ return 'closed';
758
+ } else if (ear > this.config.blinkThresholds.eyeOpenThreshold) {
759
+ return 'open';
760
+ } else {
761
+ return 'transition';
762
+ }
763
+ });
764
+
765
+ // 检查是否存在从"open"到"closed"再到"open"的序列
766
+ let hasOpenState = false;
767
+ let hasClosedState = false;
768
+ let hasOpenStateAfterClosed = false;
769
+
770
+ for (const state of eyeStates) {
771
+ if (state === 'open') {
772
+ if (!hasClosedState) {
773
+ hasOpenState = true;
774
+ } else {
775
+ hasOpenStateAfterClosed = true;
776
+ break;
777
+ }
778
+ } else if (state === 'closed' && hasOpenState) {
779
+ hasClosedState = true;
780
+ }
781
+ }
782
+
783
+ return hasOpenState && hasClosedState && hasOpenStateAfterClosed;
784
+ }
785
+
786
+ /**
787
+ * 检测点头动作
788
+ */
789
+ private detectNod(face: FaceDetectionResult): boolean {
790
+ // 如果历史记录不足,无法检测
791
+ if (this.detectionHistory.length < 8) {
792
+ return false;
793
+ }
794
+
795
+ // 跟踪鼻尖位置的垂直变化
796
+ if (!face.landmarks || !face.landmarks.nose) {
797
+ return false;
798
+ }
799
+
800
+ const nosePositions = this.detectionHistory
801
+ .slice(-8)
802
+ .map(h => h.faceResult.landmarks?.nose?.y);
803
+
804
+ if (nosePositions.some(y => y === undefined)) {
805
+ return false;
806
+ }
807
+
808
+ // 计算垂直位移的差异
809
+ const deltas = [];
810
+ for (let i = 1; i < nosePositions.length; i++) {
811
+ deltas.push(nosePositions[i]! - nosePositions[i - 1]!);
812
+ }
813
+
814
+ // 检查上下移动的模式
815
+ // 我们寻找垂直方向的位移符号变化,表示向上/向下移动
816
+ let directionChanges = 0;
817
+ for (let i = 1; i < deltas.length; i++) {
818
+ if ((deltas[i] > 0 && deltas[i - 1] < 0) || (deltas[i] < 0 && deltas[i - 1] > 0)) {
819
+ directionChanges++;
820
+ }
821
+ }
822
+
823
+ // 至少需要2次方向变化,表示下点头动作
824
+ return directionChanges >= 2;
825
+ }
826
+
827
+ /**
828
+ * 检测摇头动作
829
+ */
830
+ private detectHeadShake(face: FaceDetectionResult): boolean {
831
+ // 如果历史记录不足,无法检测
832
+ if (this.detectionHistory.length < 8) {
833
+ return false;
834
+ }
835
+
836
+ // 跟踪鼻尖位置的水平变化
837
+ if (!face.landmarks || !face.landmarks.nose) {
838
+ return false;
839
+ }
840
+
841
+ const nosePositions = this.detectionHistory
842
+ .slice(-8)
843
+ .map(h => h.faceResult.landmarks?.nose?.x);
844
+
845
+ if (nosePositions.some(x => x === undefined)) {
846
+ return false;
847
+ }
848
+
849
+ // 计算水平位移的差异
850
+ const deltas = [];
851
+ for (let i = 1; i < nosePositions.length; i++) {
852
+ deltas.push(nosePositions[i]! - nosePositions[i - 1]!);
853
+ }
854
+
855
+ // 检查左右移动的模式
856
+ // 我们寻找水平方向的位移符号变化,表示左/右移动
857
+ let directionChanges = 0;
858
+ for (let i = 1; i < deltas.length; i++) {
859
+ if ((deltas[i] > 0 && deltas[i - 1] < 0) || (deltas[i] < 0 && deltas[i - 1] > 0)) {
860
+ directionChanges++;
861
+ }
862
+ }
863
+
864
+ // 至少需要2次方向变化,表示摇头动作
865
+ return directionChanges >= 2;
866
+ }
867
+
868
+ /**
869
+ * 检测微笑动作
870
+ */
871
+ private detectSmile(face: FaceDetectionResult): boolean {
872
+ if (!face.attributes || !face.attributes.emotion) {
873
+ return false;
874
+ }
875
+
876
+ // 检查高兴情绪值
877
+ const happyScore = face.attributes.emotion.happy;
878
+
879
+ // 阈值设为0.7,高于此值认为是微笑
880
+ return happyScore !== undefined && happyScore > 0.7;
881
+ }
882
+
883
+ /**
884
+ * 检测张嘴动作
885
+ */
886
+ private detectMouthOpen(face: FaceDetectionResult): boolean {
887
+ if (!face.landmarks || !face.landmarks.points || face.landmarks.points.length < 68) {
888
+ return false;
889
+ }
890
+
891
+ const points = face.landmarks.points;
892
+
893
+ // 68点人脸模型中的嘴巴索引
894
+ // 上唇:50-53, 下唇:56-59
895
+ const topLip = points[51]; // 上唇中心
896
+ const bottomLip = points[57]; // 下唇中心
897
+
898
+ // 嘴巴高度
899
+ const mouthHeight = this.distance(topLip, bottomLip);
900
+
901
+ // 计算嘴巴相对于人脸高度的比例
902
+ const faceHeight = face.boundingBox.height;
903
+ const mouthRatio = mouthHeight / faceHeight;
904
+
905
+ // 嘴巴开度阈值(约占人脸高度的10%)
906
+ return mouthRatio > 0.1;
907
+ }
908
+ }