id-scanner-lib 1.6.7 → 2.0.0

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 (48) hide show
  1. package/dist/id-scanner-lib.esm.js +994 -1139
  2. package/dist/id-scanner-lib.esm.js.map +1 -1
  3. package/dist/id-scanner-lib.js +995 -1144
  4. package/dist/id-scanner-lib.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/compat/index.ts +7 -0
  7. package/src/compat/v1-adapter.ts +84 -0
  8. package/src/core/camera-manager.ts +43 -76
  9. package/src/core/camera-stream-manager.ts +318 -0
  10. package/src/core/config.ts +113 -267
  11. package/src/core/errors.ts +68 -117
  12. package/src/core/logger.ts +158 -81
  13. package/src/core/resource-manager.ts +150 -0
  14. package/src/core/scanner.ts +109 -0
  15. package/src/core/utils/browser.ts +7 -0
  16. package/src/core/utils/canvas-pool.ts +171 -0
  17. package/src/core/utils/canvas.ts +7 -0
  18. package/src/core/utils/image.ts +7 -0
  19. package/src/core/utils/index.ts +9 -0
  20. package/src/core/utils/resource-manager.ts +155 -0
  21. package/src/core/utils/validate.ts +7 -0
  22. package/src/core/utils/worker.ts +130 -0
  23. package/src/modules/face/comparator/comparator.ts +45 -0
  24. package/src/modules/face/comparator/index.ts +1 -0
  25. package/src/modules/face/detector/detector.ts +83 -0
  26. package/src/modules/face/detector/index.ts +2 -0
  27. package/src/modules/face/detector/types.ts +80 -0
  28. package/src/modules/face/face-comparator.ts +150 -0
  29. package/src/modules/face/face-detector-options.ts +104 -0
  30. package/src/modules/face/face-detector.ts +121 -376
  31. package/src/modules/face/face-detector.ts.bak +991 -0
  32. package/src/modules/face/face-model-loader.ts +222 -0
  33. package/src/modules/face/face-result-converter.ts +225 -0
  34. package/src/modules/face/face-tracker.ts +207 -0
  35. package/src/modules/face/liveness/index.ts +7 -0
  36. package/src/modules/face/liveness-detector.ts +2 -2
  37. package/src/modules/face/tracker/index.ts +7 -0
  38. package/src/modules/id-card/anti-fake/index.ts +7 -0
  39. package/src/modules/id-card/detector/index.ts +7 -0
  40. package/src/modules/id-card/id-card-text-parser.ts +151 -0
  41. package/src/modules/id-card/ocr-processor.ts +20 -257
  42. package/src/modules/id-card/ocr-worker.ts +2 -183
  43. package/src/modules/id-card/parser/index.ts +7 -0
  44. package/src/modules/qr/scanner/index.ts +7 -0
  45. package/src/utils/canvas-pool.ts +273 -0
  46. package/src/utils/edge-detector.ts +232 -0
  47. package/src/utils/image-processing.ts +92 -419
  48. package/src/utils/index.ts +1 -0
@@ -0,0 +1,991 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * @file 人脸检测模块
4
+ * @description 提供人脸检测、跟踪和分析功能
5
+ * @module modules/face/face-detector
6
+ */
7
+
8
+ import * as tf from '@tensorflow/tfjs';
9
+ import * as faceapi from '@vladmandic/face-api';
10
+
11
+ import { BaseScannerModule, ModuleCapabilities, ModuleEvent, ModuleInitOptions, ModuleStatus, ModuleType } from '../../interfaces/scanner-module';
12
+ import { FaceDetectionOptions, FaceDetectionResult, LivenessDetectionType, Rect } from '../../interfaces/face-detection';
13
+ import { ConfigManager } from '../../core/config';
14
+ import { Logger } from '../../core/logger';
15
+ import { ResourceManager } from '../../core/resource-manager';
16
+ import { CameraManager, CameraEvent } from '../../core/camera-manager';
17
+ import { Result } from '../../core/result';
18
+ import { FaceDetectionError, FaceComparisonError, InitializationError, LivenessDetectionError, ResourceLoadError } from '../../core/errors';
19
+ import { generateUUID } from '../../utils';
20
+
21
+ /**
22
+ * 人脸检测模型类型
23
+ */
24
+ export enum FaceModelType {
25
+ /** SSD MobileNet V1 模型 */
26
+ SSD_MOBILENET = 'ssd_mobilenetv1',
27
+ /** Tiny Face 模型 */
28
+ TINY_FACE = 'tiny_face',
29
+ /** MTCNN 模型 */
30
+ MTCNN = 'mtcnn',
31
+ /** BlazeFace 模型 */
32
+ BLAZE_FACE = 'blazeface'
33
+ }
34
+
35
+ /**
36
+ * 68点人脸关键点索引 (face-api 68-point model)
37
+ * 参考: https://github.com/justadudewhohacks/face-api.js/issues/175
38
+ */
39
+ const FaceLandmarkIndex = {
40
+ LEFT_EYE: 36,
41
+ RIGHT_EYE: 45,
42
+ NOSE: 30,
43
+ MOUTH: 48, // 上唇中心
44
+ LEFT_EYE_CORNER: 36,
45
+ RIGHT_EYE_CORNER: 45,
46
+ NOSE_TIP: 30,
47
+ MOUTH_CENTER: 57,
48
+ } as const;
49
+
50
+ /**
51
+ * 人脸检测模块配置
52
+ */
53
+ export interface FaceDetectorConfig {
54
+ /** 是否启用 */
55
+ enabled: boolean;
56
+ /** 检测模型类型 */
57
+ detectionModel: FaceModelType;
58
+ /** 置信度阈值 */
59
+ minConfidence: number;
60
+ /** 最大检测人脸数 */
61
+ maxFaces: number;
62
+ /** 是否检测关键点 */
63
+ detectLandmarks: boolean;
64
+ /** 关键点模型类型 */
65
+ landmarksModel: 'tiny' | '68_points';
66
+ /** 是否检测表情 */
67
+ detectExpressions: boolean;
68
+ /** 是否检测年龄和性别 */
69
+ detectAgeGender: boolean;
70
+ /** 是否提取人脸特征向量 */
71
+ extractEmbeddings: boolean;
72
+ /** 人脸匹配阈值(0-1) */
73
+ matchThreshold: number;
74
+ /** 是否启用跟踪 */
75
+ enableTracking: boolean;
76
+ /** 活体检测类型 */
77
+ livenessDetection: LivenessDetectionType | 'none';
78
+ /** 模型路径 */
79
+ modelPath: string;
80
+ }
81
+
82
+ /**
83
+ * 人脸检测模块
84
+ */
85
+ export class FaceDetector extends BaseScannerModule {
86
+ /** 模块类型 */
87
+ readonly type: ModuleType = ModuleType.FACE;
88
+
89
+ /** 模块配置 */
90
+ protected config: FaceDetectorConfig;
91
+
92
+ /** 默认配置 */
93
+ private static readonly DEFAULT_CONFIG: FaceDetectorConfig = {
94
+ enabled: true,
95
+ detectionModel: FaceModelType.SSD_MOBILENET,
96
+ minConfidence: 0.5,
97
+ maxFaces: 10,
98
+ detectLandmarks: true,
99
+ landmarksModel: 'tiny',
100
+ detectExpressions: false,
101
+ detectAgeGender: false,
102
+ extractEmbeddings: false,
103
+ matchThreshold: 0.6,
104
+ enableTracking: false,
105
+ livenessDetection: 'none',
106
+ modelPath: '/models/face-api'
107
+ };
108
+
109
+ /** 模型加载状态 */
110
+ private modelsLoaded: boolean = false;
111
+ private loadedModels: Set<string> = new Set();
112
+
113
+ /** 处理计时器ID */
114
+ private processingTimerId: number | null = null;
115
+
116
+ /** 处理间隔(ms) */
117
+ private processingInterval: number = 100;
118
+
119
+ /** 摄像头管理器 */
120
+ private cameraManager: CameraManager;
121
+
122
+ /** 配置管理器 */
123
+ private configManager: ConfigManager;
124
+
125
+ /** 资源管理器 */
126
+ private resourceManager: ResourceManager;
127
+
128
+ /** 日志记录器 */
129
+ private logger: Logger;
130
+
131
+ /** 画布元素,用于处理帧 */
132
+ private canvas: HTMLCanvasElement;
133
+
134
+ /** 画布渲染上下文 */
135
+ private canvasCtx: CanvasRenderingContext2D | null = null;
136
+
137
+ /** 最后一次检测结果 */
138
+ private lastDetectionResult: FaceDetectionResult[] = [];
139
+
140
+ /** 人脸跟踪状态 */
141
+ private faceTrackers: Map<string, {
142
+ trackId: string;
143
+ lastSeen: number;
144
+ detection: FaceDetectionResult;
145
+ consecutiveFrames: number;
146
+ }> = new Map();
147
+
148
+ /**
149
+ * 构造函数
150
+ * @param config 初始配置
151
+ */
152
+ constructor(config: Partial<FaceDetectorConfig> = {}) {
153
+ super({
154
+ enabled: true,
155
+ ...config
156
+ });
157
+
158
+ this.configManager = ConfigManager.getInstance();
159
+ this.cameraManager = CameraManager.getInstance();
160
+ this.resourceManager = ResourceManager.getInstance();
161
+ this.logger = Logger.getInstance();
162
+
163
+ // 合并配置
164
+ this.config = {
165
+ ...FaceDetector.DEFAULT_CONFIG,
166
+ ...config
167
+ };
168
+
169
+ // 创建画布
170
+ this.canvas = document.createElement('canvas');
171
+ this.canvasCtx = this.canvas.getContext('2d');
172
+ }
173
+
174
+ /**
175
+ * 获取模块能力
176
+ */
177
+ get capabilities(): ModuleCapabilities {
178
+ return {
179
+ supportsVideo: true,
180
+ supportsImage: true,
181
+ supportsBatch: false,
182
+ supportsRealtime: true,
183
+ supportsWebWorker: false,
184
+ supportedMediaTypes: ['image/jpeg', 'image/png', 'image/webp']
185
+ };
186
+ }
187
+
188
+ /**
189
+ * 初始化模块
190
+ * @param options 初始化选项
191
+ */
192
+ async initialize(options?: ModuleInitOptions): Promise<void> {
193
+ if (this._status === ModuleStatus.INITIALIZING) {
194
+ throw new Error('人脸检测模块正在初始化中');
195
+ }
196
+
197
+ if (this._status === ModuleStatus.READY) {
198
+ this.logger.debug('FaceDetector', '人脸检测模块已初始化');
199
+ return;
200
+ }
201
+
202
+ this.setStatus(ModuleStatus.INITIALIZING);
203
+ this.emit(ModuleEvent.INIT_START);
204
+
205
+ try {
206
+ // 应用配置选项
207
+ if (options?.config) {
208
+ this.updateConfig(options.config);
209
+ }
210
+
211
+ // 设置调试模式
212
+ if (options?.debug !== undefined) {
213
+ this.debug = options.debug;
214
+ }
215
+
216
+ const modelPath = options?.modelPath || this.config.modelPath;
217
+
218
+ // 加载模型
219
+ this.logger.info('FaceDetector', `正在加载人脸检测模型,路径:${modelPath}`);
220
+
221
+ // 设置模型路径
222
+ faceapi.env.monkeyPatch({
223
+ Canvas: HTMLCanvasElement,
224
+ Image: HTMLImageElement,
225
+ ImageData: ImageData,
226
+ Video: HTMLVideoElement,
227
+ createCanvasElement: () => document.createElement('canvas'),
228
+ createImageElement: () => document.createElement('img')
229
+ });
230
+
231
+ // 确保TensorFlow.js已初始化
232
+ await tf.ready();
233
+
234
+ // 设置模型路径并加载模型
235
+ await this.loadModels(modelPath);
236
+
237
+ // 绑定摄像头事件
238
+ if (options?.bindCamera) {
239
+ this.cameraManager.on(CameraEvent.FRAME, this.handleCameraFrame.bind(this));
240
+ }
241
+
242
+ this.setStatus(ModuleStatus.READY);
243
+ this.emit(ModuleEvent.INIT_COMPLETE);
244
+ } catch (error) {
245
+ const errorMessage = error instanceof Error ? error.message : String(error);
246
+ this.logger.error('FaceDetector', `初始化失败: ${errorMessage}`, error as Error);
247
+
248
+ this.setStatus(ModuleStatus.ERROR);
249
+ this.emit(ModuleEvent.INIT_ERROR, { error });
250
+
251
+ throw new Error(`人脸检测模块初始化失败: ${errorMessage}`);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * 懒加载模型 - 仅在需要时加载特定模型
257
+ * @param modelType 模型类型
258
+ * @param modelPath 模型路径
259
+ */
260
+ private async lazyLoadModel(modelType: string, modelPath: string): Promise<void> {
261
+ // 检查模型是否已加载
262
+ const loadedModels = this.loadedModels || new Set();
263
+ if (loadedModels.has(modelType)) {
264
+ return;
265
+ }
266
+
267
+ this.logger.info('FaceDetector', `懒加载模型: ${modelType}`);
268
+
269
+ try {
270
+ switch (modelType) {
271
+ case 'ssdMobilenetv1':
272
+ await faceapi.nets.ssdMobilenetv1.loadFromUri(modelPath);
273
+ break;
274
+ case 'tinyFaceDetector':
275
+ await faceapi.nets.tinyFaceDetector.loadFromUri(modelPath);
276
+ break;
277
+ case 'faceLandmark68Net':
278
+ await faceapi.nets.faceLandmark68Net.loadFromUri(modelPath);
279
+ break;
280
+ case 'faceLandmark68TinyNet':
281
+ await faceapi.nets.faceLandmark68TinyNet.loadFromUri(modelPath);
282
+ break;
283
+ case 'faceExpressionNet':
284
+ await faceapi.nets.faceExpressionNet.loadFromUri(modelPath);
285
+ break;
286
+ case 'ageGenderNet':
287
+ await faceapi.nets.ageGenderNet.loadFromUri(modelPath);
288
+ break;
289
+ case 'faceRecognitionNet':
290
+ await faceapi.nets.faceRecognitionNet.loadFromUri(modelPath);
291
+ break;
292
+ default:
293
+ this.logger.warn('FaceDetector', `未知模型类型: ${modelType}`);
294
+ return;
295
+ }
296
+
297
+ loadedModels.add(modelType);
298
+ this.loadedModels = loadedModels;
299
+ this.logger.info('FaceDetector', `模型加载完成: ${modelType}`);
300
+ } catch (error) {
301
+ this.logger.error('FaceDetector', `模型加载失败: ${modelType}`, error instanceof Error ? error : undefined);
302
+ throw new ResourceLoadError(modelType, `模型加载失败: ${error}`);
303
+ }
304
+ }
305
+
306
+ /**
307
+ * 根据需求加载模型
308
+ * @param options 检测选项
309
+ * @param modelPath 模型路径
310
+ */
311
+ private async loadModelsOnDemand(options: FaceDetectionOptions, modelPath: string): Promise<void> {
312
+ // 基础检测模型
313
+ await this.lazyLoadModel(this.config.detectionModel, modelPath);
314
+
315
+ // 关键点模型
316
+ if (options.withLandmarks || this.config.detectLandmarks) {
317
+ await this.lazyLoadModel(
318
+ this.config.landmarksModel === '68_points' ? 'faceLandmark68Net' : 'faceLandmark68TinyNet',
319
+ modelPath
320
+ );
321
+ }
322
+
323
+ // 表情模型
324
+ if (options.withExpressions || this.config.detectExpressions) {
325
+ await this.lazyLoadModel('faceExpressionNet', modelPath);
326
+ }
327
+
328
+ // 年龄性别模型
329
+ if (options.withAgeAndGender || this.config.detectAgeGender) {
330
+ await this.lazyLoadModel('ageGenderNet', modelPath);
331
+ }
332
+
333
+ // 人脸识别模型
334
+ if (options.withEmbedding || this.config.extractEmbeddings) {
335
+ await this.lazyLoadModel('faceRecognitionNet', modelPath);
336
+ }
337
+
338
+ this.modelsLoaded = true;
339
+ }
340
+
341
+ /**
342
+ * 加载人脸检测模型 (旧版 - 保留兼容性)
343
+ * @param modelPath 模型路径
344
+ */
345
+ private async loadModels(modelPath: string): Promise<void> {
346
+ await this.loadModelsOnDemand({}, modelPath);
347
+ }
348
+
349
+ /**
350
+ * 处理图片
351
+ * @param image 图片源
352
+ * @param options 处理选项
353
+ */
354
+ async processImage(
355
+ image: string | HTMLImageElement | HTMLCanvasElement | ImageData,
356
+ options: FaceDetectionOptions = {}
357
+ ): Promise<Result<FaceDetectionResult[]>> {
358
+ this.checkInitialized();
359
+
360
+ if (this._status === ModuleStatus.PROCESSING) {
361
+ return Result.failure(new FaceDetectionError('另一个处理操作正在进行中'));
362
+ }
363
+
364
+ this.setStatus(ModuleStatus.PROCESSING);
365
+ this.emit(ModuleEvent.PROCESS_START);
366
+
367
+ try {
368
+ // 懒加载所需的模型
369
+ const modelPath = this.config.modelPath || '/models';
370
+ await this.loadModelsOnDemand(options, modelPath);
371
+
372
+ // 合并选项和配置
373
+ const processOptions: FaceDetectionOptions = {
374
+ minConfidence: this.config.minConfidence,
375
+ maxFaces: this.config.maxFaces,
376
+ withLandmarks: this.config.detectLandmarks,
377
+ withAttributes: this.config.detectExpressions || this.config.detectAgeGender,
378
+ withEmbedding: this.config.extractEmbeddings,
379
+ ...options
380
+ };
381
+
382
+ // 加载图片
383
+ let imgElement: HTMLImageElement | HTMLCanvasElement;
384
+
385
+ if (typeof image === 'string') {
386
+ imgElement = await this.loadImage(image);
387
+ } else if (image instanceof HTMLImageElement || image instanceof HTMLCanvasElement) {
388
+ imgElement = image;
389
+ } else if (image instanceof ImageData) {
390
+ // 将ImageData转换为Canvas
391
+ const canvas = document.createElement('canvas');
392
+ canvas.width = image.width;
393
+ canvas.height = image.height;
394
+ const ctx = canvas.getContext('2d');
395
+ ctx?.putImageData(image, 0, 0);
396
+ imgElement = canvas;
397
+ } else {
398
+ throw new FaceDetectionError('不支持的图像格式');
399
+ }
400
+
401
+ // 开始计时
402
+ const startTime = Date.now();
403
+
404
+ // 执行人脸检测
405
+ const results = await this.detectFaces(imgElement, processOptions);
406
+
407
+ // 计算处理时间
408
+ const processingTime = Date.now() - startTime;
409
+
410
+ this.setStatus(ModuleStatus.READY);
411
+ this.emit(ModuleEvent.PROCESS_COMPLETE, { results, processingTime });
412
+
413
+ return Result.success(results);
414
+ } catch (error) {
415
+ const errorMessage = error instanceof Error ? error.message : String(error);
416
+ this.logger.error('FaceDetector', `图片处理失败: ${errorMessage}`, error as Error);
417
+
418
+ this.setStatus(ModuleStatus.ERROR);
419
+ this.emit(ModuleEvent.PROCESS_ERROR, { error });
420
+
421
+ return Result.failure(new FaceDetectionError(`图片处理失败: ${errorMessage}`));
422
+ }
423
+ }
424
+
425
+ /**
426
+ * 开始实时处理
427
+ * @param videoElement 视频元素
428
+ * @param options 处理选项
429
+ */
430
+ async startRealtime(
431
+ videoElement?: HTMLVideoElement,
432
+ options: FaceDetectionOptions = {}
433
+ ): Promise<Result<boolean>> {
434
+ this.checkInitialized();
435
+
436
+ if (this._status === ModuleStatus.PROCESSING) {
437
+ return Result.failure(new FaceDetectionError('实时处理已在进行中'));
438
+ }
439
+
440
+ try {
441
+ // 停止现有处理
442
+ this.stopRealtime();
443
+
444
+ // 获取视频元素
445
+ const video = videoElement || this.cameraManager.getVideoElement();
446
+
447
+ if (!video) {
448
+ throw new FaceDetectionError('未提供视频元素且摄像头未初始化');
449
+ }
450
+
451
+ // 如果视频未播放,尝试启动摄像头
452
+ if (!this.cameraManager.isActive() && !videoElement) {
453
+ const cameraResult = await this.cameraManager.init({ autoStart: true });
454
+ if (!cameraResult.isSuccess()) {
455
+ throw new Error('无法启动摄像头');
456
+ }
457
+ }
458
+
459
+ // 设置处理间隔
460
+ this.processingInterval = options.processingInterval || 100;
461
+
462
+ // 设置状态
463
+ this.setStatus(ModuleStatus.PROCESSING);
464
+
465
+ // 启动处理循环
466
+ this.processingTimerId = window.setInterval(() => {
467
+ this.processVideoFrame(video, options);
468
+ }, this.processingInterval);
469
+
470
+ return Result.success(true);
471
+ } catch (error) {
472
+ const errorMessage = error instanceof Error ? error.message : String(error);
473
+ this.logger.error('FaceDetector', `启动实时处理失败: ${errorMessage}`, error as Error);
474
+
475
+ this.setStatus(ModuleStatus.ERROR);
476
+
477
+ return Result.failure(new FaceDetectionError(`启动实时处理失败: ${errorMessage}`));
478
+ }
479
+ }
480
+
481
+ /**
482
+ * 停止实时处理
483
+ */
484
+ stopRealtime(): void {
485
+ if (this.processingTimerId !== null) {
486
+ window.clearInterval(this.processingTimerId);
487
+ this.processingTimerId = null;
488
+ }
489
+
490
+ if (this._status === ModuleStatus.PROCESSING) {
491
+ this.setStatus(ModuleStatus.READY);
492
+ }
493
+
494
+ // 清除人脸跟踪状态
495
+ this.faceTrackers.clear();
496
+ this.lastDetectionResult = [];
497
+ }
498
+
499
+ /**
500
+ * 释放资源
501
+ */
502
+ async dispose(): Promise<void> {
503
+ // 停止实时处理
504
+ this.stopRealtime();
505
+
506
+ // 释放模型
507
+ if (this.modelsLoaded) {
508
+ try {
509
+ await faceapi.tf.dispose();
510
+ this.modelsLoaded = false;
511
+ } catch (error) {
512
+ this.logger.error('FaceDetector', `释放模型失败: ${error}`);
513
+ }
514
+ }
515
+
516
+ // 移除事件监听
517
+ this.cameraManager.off(CameraEvent.FRAME, this.handleCameraFrame.bind(this));
518
+
519
+ this._status = ModuleStatus.NOT_INITIALIZED;
520
+ }
521
+
522
+ /**
523
+ * 加载图片
524
+ * @param src 图片URL
525
+ */
526
+ private async loadImage(src: string): Promise<HTMLImageElement> {
527
+ return new Promise((resolve, reject) => {
528
+ const img = new Image();
529
+ img.crossOrigin = 'anonymous';
530
+ img.onload = () => resolve(img);
531
+ img.onerror = () => reject(new Error(`无法加载图片: ${src}`));
532
+ img.src = src;
533
+ });
534
+ }
535
+
536
+ /**
537
+ * 处理视频帧
538
+ * @param video 视频元素
539
+ * @param options 处理选项
540
+ */
541
+ private async processVideoFrame(
542
+ video: HTMLVideoElement,
543
+ options: FaceDetectionOptions = {}
544
+ ): Promise<void> {
545
+ if (this._status !== ModuleStatus.PROCESSING || !video || video.paused || video.ended) {
546
+ return;
547
+ }
548
+
549
+ try {
550
+ // 检查视频是否准备好
551
+ if (video.readyState < 2) { // HAVE_CURRENT_DATA
552
+ return;
553
+ }
554
+
555
+ // 检查视频尺寸
556
+ if (video.videoWidth === 0 || video.videoHeight === 0) {
557
+ return;
558
+ }
559
+
560
+ // 调整画布大小
561
+ if (this.canvas.width !== video.videoWidth || this.canvas.height !== video.videoHeight) {
562
+ this.canvas.width = video.videoWidth;
563
+ this.canvas.height = video.videoHeight;
564
+ }
565
+
566
+ // 将视频帧绘制到画布
567
+ if (this.canvasCtx) {
568
+ this.canvasCtx.drawImage(video, 0, 0);
569
+ }
570
+
571
+ // 执行人脸检测
572
+ const startTime = Date.now();
573
+ const results = await this.detectFaces(video, options);
574
+ const processingTime = Date.now() - startTime;
575
+
576
+ // 更新最后的检测结果
577
+ this.lastDetectionResult = results;
578
+
579
+ // 发出实时结果事件
580
+ this.emit(ModuleEvent.REALTIME_RESULT, {
581
+ results,
582
+ processingTime,
583
+ timestamp: Date.now()
584
+ });
585
+ } catch (error) {
586
+ this.logger.error('FaceDetector', `处理视频帧失败: ${error}`);
587
+ }
588
+ }
589
+
590
+ /**
591
+ * 处理摄像头帧
592
+ */
593
+ private handleCameraFrame(event: any): void {
594
+ if (this._status !== ModuleStatus.PROCESSING || !event.frameData) {
595
+ return;
596
+ }
597
+
598
+ const { frameData } = event;
599
+
600
+ // 调整画布大小
601
+ if (this.canvas.width !== frameData.width || this.canvas.height !== frameData.height) {
602
+ this.canvas.width = frameData.width;
603
+ this.canvas.height = frameData.height;
604
+ }
605
+
606
+ // 将帧数据绘制到画布
607
+ if (this.canvasCtx) {
608
+ this.canvasCtx.putImageData(frameData, 0, 0);
609
+
610
+ // 执行人脸检测
611
+ this.detectFaces(this.canvas).then(results => {
612
+ // 更新最后的检测结果
613
+ this.lastDetectionResult = results;
614
+
615
+ // 发出实时结果事件
616
+ this.emit(ModuleEvent.REALTIME_RESULT, {
617
+ results,
618
+ timestamp: Date.now()
619
+ });
620
+ }).catch(error => {
621
+ this.logger.error('FaceDetector', `处理摄像头帧失败: ${error}`);
622
+ });
623
+ }
624
+ }
625
+
626
+ /**
627
+ * 执行人脸检测
628
+ * @param input 输入图像
629
+ * @param options 检测选项
630
+ */
631
+ private async detectFaces(
632
+ input: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement,
633
+ options: FaceDetectionOptions = {}
634
+ ): Promise<FaceDetectionResult[]> {
635
+ try {
636
+ // 检查模型是否已加载
637
+ if (!this.modelsLoaded) {
638
+ throw new FaceDetectionError('人脸检测模型尚未加载');
639
+ }
640
+
641
+ // 合并选项和配置
642
+ const detectOptions: FaceDetectionOptions = {
643
+ minConfidence: this.config.minConfidence,
644
+ maxFaces: this.config.maxFaces,
645
+ withLandmarks: this.config.detectLandmarks,
646
+ withAttributes: this.config.detectExpressions || this.config.detectAgeGender,
647
+ withEmbedding: this.config.extractEmbeddings,
648
+ enableTracking: this.config.enableTracking,
649
+ ...options
650
+ };
651
+
652
+ // 创建检测选项
653
+ let faceapiOptions;
654
+
655
+ switch (this.config.detectionModel) {
656
+ case FaceModelType.SSD_MOBILENET:
657
+ faceapiOptions = new faceapi.SsdMobilenetv1Options({ minConfidence: detectOptions.minConfidence });
658
+ break;
659
+ case FaceModelType.TINY_FACE:
660
+ faceapiOptions = new faceapi.TinyFaceDetectorOptions({ scoreThreshold: detectOptions.minConfidence });
661
+ break;
662
+ case FaceModelType.MTCNN:
663
+ faceapiOptions = new faceapi.MtcnnOptions({ minConfidence: detectOptions.minConfidence });
664
+ break;
665
+ default:
666
+ faceapiOptions = new faceapi.SsdMobilenetv1Options({ minConfidence: detectOptions.minConfidence });
667
+ }
668
+
669
+ // 进行检测
670
+ let detections;
671
+ const startTime = Date.now();
672
+
673
+ if (detectOptions.withLandmarks && detectOptions.withAttributes && detectOptions.withEmbedding) {
674
+ // 全功能检测
675
+ detections = await faceapi
676
+ .detectAllFaces(input, faceapiOptions)
677
+ .withFaceLandmarks(this.config.landmarksModel === 'tiny')
678
+ .withFaceExpressions()
679
+ .withAgeAndGender()
680
+ .withFaceDescriptors();
681
+ } else if (detectOptions.withLandmarks && detectOptions.withAttributes) {
682
+ // 检测带关键点和属性
683
+ detections = await faceapi
684
+ .detectAllFaces(input, faceapiOptions)
685
+ .withFaceLandmarks(this.config.landmarksModel === 'tiny')
686
+ .withFaceExpressions()
687
+ .withAgeAndGender();
688
+ } else if (detectOptions.withLandmarks) {
689
+ // 检测带关键点
690
+ detections = await faceapi
691
+ .detectAllFaces(input, faceapiOptions)
692
+ .withFaceLandmarks(this.config.landmarksModel === 'tiny');
693
+ } else {
694
+ // 仅检测
695
+ detections = await faceapi.detectAllFaces(input, faceapiOptions);
696
+ }
697
+
698
+ // 限制检测数量
699
+ const maxFaces = detectOptions.maxFaces || this.config.maxFaces;
700
+ const detectionsArray: any[] = Array.isArray(detections) ? detections : [detections];
701
+ if (detectionsArray.length > maxFaces) {
702
+ detectionsArray.length = maxFaces;
703
+ }
704
+
705
+ // 将结果转换为标准格式
706
+ const results: FaceDetectionResult[] = [];
707
+ const processingTime = Date.now() - startTime;
708
+
709
+ for (const detection of detectionsArray) {
710
+ const boundingBox: Rect = {
711
+ x: detection.detection?.box.x || 0,
712
+ y: detection.detection?.box.y || 0,
713
+ width: detection.detection?.box.width || 0,
714
+ height: detection.detection?.box.height || 0
715
+ };
716
+
717
+ // 创建基本结果
718
+ const result: FaceDetectionResult = {
719
+ id: generateUUID(),
720
+ type: 'face',
721
+ boundingBox,
722
+ confidence: detection.detection?.score || 0,
723
+ processingTime,
724
+ timestamp: Date.now()
725
+ };
726
+
727
+ // 添加关键点
728
+ if (detection.landmarks) {
729
+ const positions = detection.landmarks.positions;
730
+
731
+ result.landmarks = {
732
+ leftEye: {
733
+ x: positions[FaceLandmarkIndex.LEFT_EYE].x,
734
+ y: positions[FaceLandmarkIndex.LEFT_EYE].y
735
+ },
736
+ rightEye: {
737
+ x: positions[FaceLandmarkIndex.RIGHT_EYE].x,
738
+ y: positions[FaceLandmarkIndex.RIGHT_EYE].y
739
+ },
740
+ nose: {
741
+ x: positions[FaceLandmarkIndex.NOSE].x,
742
+ y: positions[FaceLandmarkIndex.NOSE].y
743
+ },
744
+ mouth: {
745
+ x: positions[FaceLandmarkIndex.MOUTH_CENTER].x,
746
+ y: positions[FaceLandmarkIndex.MOUTH_CENTER].y
747
+ },
748
+ points: positions.map((p: { x: any; y: any; }) => ({ x: p.x, y: p.y }))
749
+ };
750
+ }
751
+
752
+ // 添加表情属性
753
+ if (detection.expressions) {
754
+ const emotion = {
755
+ angry: detection.expressions.angry,
756
+ disgust: detection.expressions.disgusted,
757
+ fear: detection.expressions.fearful,
758
+ happy: detection.expressions.happy,
759
+ neutral: detection.expressions.neutral,
760
+ sad: detection.expressions.sad,
761
+ surprise: detection.expressions.surprised
762
+ };
763
+ result.attributes = {
764
+ ...result.attributes,
765
+ emotion
766
+ };
767
+ }
768
+
769
+ // 添加年龄和性别
770
+ if (detection.age !== undefined) {
771
+ result.attributes = {
772
+ ...result.attributes,
773
+ age: detection.age
774
+ };
775
+ }
776
+
777
+ if (detection.gender !== undefined && detection.genderProbability !== undefined) {
778
+ result.attributes = {
779
+ ...result.attributes,
780
+ gender: detection.gender === 'male' ? detection.genderProbability : 1 - detection.genderProbability
781
+ };
782
+ }
783
+
784
+ // 添加特征向量
785
+ if (detection.descriptor) {
786
+ result.embedding = {
787
+ vector: Array.from(detection.descriptor),
788
+ dimension: detection.descriptor.length
789
+ };
790
+ }
791
+
792
+ // 处理人脸跟踪
793
+ if (detectOptions.enableTracking) {
794
+ const trackId = this.trackFace(result);
795
+ if (trackId) {
796
+ result.trackId = trackId;
797
+ }
798
+ }
799
+
800
+ results.push(result);
801
+ }
802
+
803
+ return results;
804
+ } catch (error) {
805
+ this.logger.error('FaceDetector', `人脸检测失败: ${error}`);
806
+ throw new FaceDetectionError(`人脸检测失败: ${error}`);
807
+ }
808
+ }
809
+
810
+ /**
811
+ * 跟踪人脸
812
+ * @param detection 人脸检测结果
813
+ */
814
+ private trackFace(detection: FaceDetectionResult): string {
815
+ const now = Date.now();
816
+ const box = detection.boundingBox;
817
+ const boxCenter = {
818
+ x: box.x + box.width / 2,
819
+ y: box.y + box.height / 2
820
+ };
821
+
822
+ // 查找最匹配的跟踪器
823
+ let bestMatchId: string | null = null;
824
+ let bestMatchScore = Number.MAX_VALUE;
825
+
826
+ // 清理过期的跟踪器
827
+ const expireTime = 1000; // 1秒未检测到则过期
828
+ for (const [id, tracker] of this.faceTrackers) {
829
+ if (now - tracker.lastSeen > expireTime) {
830
+ this.faceTrackers.delete(id);
831
+ }
832
+ }
833
+
834
+ // 查找最佳匹配
835
+ for (const [id, tracker] of this.faceTrackers) {
836
+ const trackerBox = tracker.detection.boundingBox;
837
+ const trackerCenter = {
838
+ x: trackerBox.x + trackerBox.width / 2,
839
+ y: trackerBox.y + trackerBox.height / 2
840
+ };
841
+
842
+ // 计算中心点距离
843
+ const distance = Math.sqrt(
844
+ Math.pow(boxCenter.x - trackerCenter.x, 2) +
845
+ Math.pow(boxCenter.y - trackerCenter.y, 2)
846
+ );
847
+
848
+ // 计算大小差异
849
+ const sizeDiff = Math.abs(
850
+ (box.width * box.height) - (trackerBox.width * trackerBox.height)
851
+ ) / (box.width * box.height);
852
+
853
+ // 计算综合匹配分数
854
+ const score = distance * 0.7 + sizeDiff * 0.3;
855
+
856
+ // 找到最佳匹配
857
+ if (score < bestMatchScore && score < 0.3 * Math.max(box.width, box.height)) {
858
+ bestMatchScore = score;
859
+ bestMatchId = id;
860
+ }
861
+ }
862
+
863
+ if (bestMatchId) {
864
+ // 更新现有跟踪器
865
+ const tracker = this.faceTrackers.get(bestMatchId)!;
866
+ tracker.lastSeen = now;
867
+ tracker.detection = detection;
868
+ tracker.consecutiveFrames++;
869
+ return bestMatchId;
870
+ } else {
871
+ // 创建新的跟踪器
872
+ const trackId = generateUUID();
873
+ this.faceTrackers.set(trackId, {
874
+ trackId,
875
+ lastSeen: now,
876
+ detection,
877
+ consecutiveFrames: 1
878
+ });
879
+ return trackId;
880
+ }
881
+ }
882
+
883
+ /**
884
+ * 比对两个人脸
885
+ * @param source 源人脸
886
+ * @param target 目标人脸
887
+ */
888
+ async compareFaces(
889
+ source: string | HTMLImageElement | FaceDetectionResult,
890
+ target: string | HTMLImageElement | FaceDetectionResult
891
+ ): Promise<Result<{ similarity: number; isMatch: boolean; threshold: number }>> {
892
+ this.checkInitialized();
893
+
894
+ try {
895
+ // 获取源人脸的特征向量
896
+ let sourceEmbedding: number[];
897
+
898
+ if (typeof source === 'string' || source instanceof HTMLImageElement) {
899
+ // 处理图片源
900
+ const result = await this.processImage(source, { withEmbedding: true });
901
+ const resultData = result.getData();
902
+ if (!result.isSuccess() || !resultData || resultData.length === 0) {
903
+ throw new FaceComparisonError('无法从源图像检测人脸');
904
+ }
905
+ if (!resultData[0].embedding) {
906
+ throw new FaceComparisonError('源图像未提取特征向量');
907
+ }
908
+ sourceEmbedding = resultData[0].embedding.vector;
909
+ } else {
910
+ // 使用现有检测结果
911
+ if (!source.embedding || !source.embedding.vector) {
912
+ throw new FaceComparisonError('源人脸未提取特征向量');
913
+ }
914
+ sourceEmbedding = source.embedding.vector;
915
+ }
916
+
917
+ // 获取目标人脸的特征向量
918
+ let targetEmbedding: number[];
919
+
920
+ if (typeof target === 'string' || target instanceof HTMLImageElement) {
921
+ // 处理图片源
922
+ const result = await this.processImage(target, { withEmbedding: true });
923
+ const resultData = result.getData();
924
+ if (!result.isSuccess() || !resultData || resultData.length === 0) {
925
+ throw new FaceComparisonError('无法从目标图像检测人脸');
926
+ }
927
+ if (!resultData[0].embedding) {
928
+ throw new FaceComparisonError('目标图像未提取特征向量');
929
+ }
930
+ targetEmbedding = resultData[0].embedding.vector;
931
+ } else {
932
+ // 使用现有检测结果
933
+ if (!target.embedding || !target.embedding.vector) {
934
+ throw new FaceComparisonError('目标人脸未提取特征向量');
935
+ }
936
+ targetEmbedding = target.embedding.vector;
937
+ }
938
+
939
+ // 计算相似度
940
+ const similarity = this.calculateSimilarity(sourceEmbedding, targetEmbedding);
941
+ const threshold = this.config.matchThreshold;
942
+ const isMatch = similarity >= threshold;
943
+
944
+ return Result.success({
945
+ similarity,
946
+ isMatch,
947
+ threshold
948
+ });
949
+ } catch (error) {
950
+ const errorMessage = error instanceof Error ? error.message : String(error);
951
+ this.logger.error('FaceDetector', `人脸比对失败: ${errorMessage}`, error as Error);
952
+
953
+ return Result.failure(new FaceComparisonError(`人脸比对失败: ${errorMessage}`));
954
+ }
955
+ }
956
+
957
+ /**
958
+ * 计算两个特征向量的余弦相似度
959
+ * @param v1 特征向量1
960
+ * @param v2 特征向量2
961
+ */
962
+ private calculateSimilarity(v1: number[], v2: number[]): number {
963
+ if (v1.length !== v2.length) {
964
+ throw new Error('特征向量维度不匹配');
965
+ }
966
+
967
+ let dotProduct = 0;
968
+ let norm1 = 0;
969
+ let norm2 = 0;
970
+
971
+ for (let i = 0; i < v1.length; i++) {
972
+ dotProduct += v1[i] * v2[i];
973
+ norm1 += v1[i] * v1[i];
974
+ norm2 += v2[i] * v2[i];
975
+ }
976
+
977
+ // 确保长度非零
978
+ if (norm1 === 0 || norm2 === 0) {
979
+ return 0;
980
+ }
981
+
982
+ return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
983
+ }
984
+
985
+ /**
986
+ * 获取最近的检测结果
987
+ */
988
+ getLatestResults(): FaceDetectionResult[] {
989
+ return [...this.lastDetectionResult];
990
+ }
991
+ }