id-scanner-lib 1.6.6 → 1.7.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.
@@ -17,6 +17,11 @@ import { CameraManager, CameraEvent } from '../../core/camera-manager';
17
17
  import { Result } from '../../core/result';
18
18
  import { FaceDetectionError, FaceComparisonError, InitializationError, LivenessDetectionError, ResourceLoadError } from '../../core/errors';
19
19
  import { generateUUID } from '../../utils';
20
+ import { FaceModelLoader } from './face-model-loader';
21
+ import { FaceTracker } from './face-tracker';
22
+ import { FaceComparator } from './face-comparator';
23
+ import { FaceResultConverter } from './face-result-converter';
24
+ import { FaceDetectorOptionsFactory } from './face-detector-options';
20
25
 
21
26
  /**
22
27
  * 人脸检测模型类型
@@ -106,66 +111,87 @@ export class FaceDetector extends BaseScannerModule {
106
111
  modelPath: '/models/face-api'
107
112
  };
108
113
 
109
- /** 模型加载状态 */
110
- private modelsLoaded: boolean = false;
111
- private loadedModels: Set<string> = new Set();
112
-
114
+ /** 模型加载器 */
115
+ private modelLoader: FaceModelLoader;
116
+
117
+ /** 人脸跟踪器 */
118
+ private faceTracker: FaceTracker;
119
+
120
+ /** 人脸比对器 */
121
+ private faceComparator: FaceComparator;
122
+
123
+ /** 结果转换器 */
124
+ private resultConverter: FaceResultConverter;
125
+
113
126
  /** 处理计时器ID */
114
127
  private processingTimerId: number | null = null;
115
-
128
+
116
129
  /** 处理间隔(ms) */
117
130
  private processingInterval: number = 100;
118
-
131
+
119
132
  /** 摄像头管理器 */
120
133
  private cameraManager: CameraManager;
121
-
134
+
122
135
  /** 配置管理器 */
123
136
  private configManager: ConfigManager;
124
-
137
+
125
138
  /** 资源管理器 */
126
139
  private resourceManager: ResourceManager;
127
-
140
+
128
141
  /** 日志记录器 */
129
142
  private logger: Logger;
130
-
143
+
131
144
  /** 画布元素,用于处理帧 */
132
145
  private canvas: HTMLCanvasElement;
133
-
146
+
134
147
  /** 画布渲染上下文 */
135
148
  private canvasCtx: CanvasRenderingContext2D | null = null;
136
-
149
+
137
150
  /** 最后一次检测结果 */
138
151
  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
- /**
152
+
153
+ /**
149
154
  * 构造函数
150
155
  * @param config 初始配置
151
156
  */
152
157
  constructor(config: Partial<FaceDetectorConfig> = {}) {
153
- super({
154
- enabled: true,
155
- ...config
158
+ super({
159
+ enabled: true,
160
+ ...config
156
161
  });
157
-
162
+
158
163
  this.configManager = ConfigManager.getInstance();
159
164
  this.cameraManager = CameraManager.getInstance();
160
165
  this.resourceManager = ResourceManager.getInstance();
161
166
  this.logger = Logger.getInstance();
162
-
167
+
163
168
  // 合并配置
164
169
  this.config = {
165
170
  ...FaceDetector.DEFAULT_CONFIG,
166
171
  ...config
167
172
  };
168
-
173
+
174
+ // 初始化组件
175
+ this.modelLoader = new FaceModelLoader({
176
+ modelPath: this.config.modelPath,
177
+ detectionModel: this.config.detectionModel,
178
+ landmarksModel: this.config.landmarksModel,
179
+ detectLandmarks: this.config.detectLandmarks,
180
+ detectExpressions: this.config.detectExpressions,
181
+ detectAgeGender: this.config.detectAgeGender,
182
+ extractEmbeddings: this.config.extractEmbeddings
183
+ });
184
+
185
+ this.faceTracker = new FaceTracker({ trackTimeout: 1000 });
186
+ this.faceComparator = new FaceComparator({ matchThreshold: this.config.matchThreshold });
187
+ this.resultConverter = new FaceResultConverter({
188
+ detectLandmarks: this.config.detectLandmarks,
189
+ landmarksModel: this.config.landmarksModel,
190
+ detectExpressions: this.config.detectExpressions,
191
+ detectAgeGender: this.config.detectAgeGender,
192
+ extractEmbeddings: this.config.extractEmbeddings
193
+ });
194
+
169
195
  // 创建画布
170
196
  this.canvas = document.createElement('canvas');
171
197
  this.canvasCtx = this.canvas.getContext('2d');
@@ -252,98 +278,34 @@ export class FaceDetector extends BaseScannerModule {
252
278
  }
253
279
  }
254
280
 
281
+ // ==================== 模型加载(委托给 FaceModelLoader) ====================
282
+
255
283
  /**
256
- * 懒加载模型 - 仅在需要时加载特定模型
257
- * @param modelType 模型类型
258
- * @param modelPath 模型路径
284
+ * 懒加载模型 - 委托给 FaceModelLoader
285
+ * @deprecated 使用 modelLoader.lazyLoadModel 代替
259
286
  */
260
287
  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
- }
288
+ await this.modelLoader.lazyLoadModel(modelType, modelPath);
304
289
  }
305
290
 
306
291
  /**
307
- * 根据需求加载模型
308
- * @param options 检测选项
309
- * @param modelPath 模型路径
292
+ * 根据需求加载模型 - 委托给 FaceModelLoader
293
+ * @deprecated 使用 modelLoader.loadModelsOnDemand 代替
310
294
  */
311
295
  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;
296
+ await this.modelLoader.loadModelsOnDemand({
297
+ withLandmarks: options.withLandmarks,
298
+ withAttributes: options.withAttributes,
299
+ withEmbedding: options.withEmbedding
300
+ });
339
301
  }
340
302
 
341
303
  /**
342
- * 加载人脸检测模型 (旧版 - 保留兼容性)
343
- * @param modelPath 模型路径
304
+ * 加载人脸检测模型 - 委托给 FaceModelLoader
305
+ * @deprecated 使用 modelLoader.ensureModelsLoaded 代替
344
306
  */
345
307
  private async loadModels(modelPath: string): Promise<void> {
346
- await this.loadModelsOnDemand({}, modelPath);
308
+ await this.modelLoader.ensureModelsLoaded();
347
309
  }
348
310
 
349
311
  /**
@@ -492,7 +454,7 @@ export class FaceDetector extends BaseScannerModule {
492
454
  }
493
455
 
494
456
  // 清除人脸跟踪状态
495
- this.faceTrackers.clear();
457
+ this.faceTracker.reset();
496
458
  this.lastDetectionResult = [];
497
459
  }
498
460
 
@@ -502,20 +464,16 @@ export class FaceDetector extends BaseScannerModule {
502
464
  async dispose(): Promise<void> {
503
465
  // 停止实时处理
504
466
  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
-
467
+
468
+ // 释放模型(通过 FaceModelLoader)
469
+ await this.modelLoader.dispose();
470
+
471
+ // 重置跟踪器
472
+ this.faceTracker.reset();
473
+
516
474
  // 移除事件监听
517
475
  this.cameraManager.off(CameraEvent.FRAME, this.handleCameraFrame.bind(this));
518
-
476
+
519
477
  this._status = ModuleStatus.NOT_INITIALIZED;
520
478
  }
521
479
 
@@ -634,66 +592,27 @@ export class FaceDetector extends BaseScannerModule {
634
592
  ): Promise<FaceDetectionResult[]> {
635
593
  try {
636
594
  // 检查模型是否已加载
637
- if (!this.modelsLoaded) {
595
+ if (!this.modelLoader.isModelsLoaded()) {
638
596
  throw new FaceDetectionError('人脸检测模型尚未加载');
639
597
  }
640
598
 
641
599
  // 合并选项和配置
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
-
600
+ const detectOptions = FaceDetectorOptionsFactory.mergeOptions(this.config, options);
601
+
602
+ // 创建 face-api 检测选项
603
+ const faceapiOptions = FaceDetectorOptionsFactory.createFaceAPIOptions(
604
+ this.config.detectionModel,
605
+ detectOptions.minConfidence ?? 0.5
606
+ );
607
+
669
608
  // 进行检测
670
- let detections;
671
609
  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
- }
610
+ const detections = await FaceDetectorOptionsFactory.detect(
611
+ input,
612
+ faceapiOptions,
613
+ detectOptions,
614
+ this.config.landmarksModel
615
+ );
697
616
 
698
617
  // 限制检测数量
699
618
  const maxFaces = detectOptions.maxFaces || this.config.maxFaces;
@@ -701,105 +620,30 @@ export class FaceDetector extends BaseScannerModule {
701
620
  if (detectionsArray.length > maxFaces) {
702
621
  detectionsArray.length = maxFaces;
703
622
  }
704
-
623
+
705
624
  // 将结果转换为标准格式
706
- const results: FaceDetectionResult[] = [];
707
625
  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;
626
+
627
+ // 临时更新转换器配置
628
+ this.resultConverter.updateConfig({
629
+ detectLandmarks: !!detectOptions.withLandmarks,
630
+ detectExpressions: !!detectOptions.withAttributes,
631
+ detectAgeGender: !!detectOptions.withAttributes,
632
+ extractEmbeddings: !!detectOptions.withEmbedding
633
+ });
634
+
635
+ const results = this.resultConverter.convertBatch(detectionsArray, { maxFaces }, processingTime);
636
+
637
+ // 处理人脸跟踪(使用 FaceTracker)
638
+ if (detectOptions.enableTracking) {
639
+ const trackedResults = this.faceTracker.update(results);
640
+ for (let i = 0; i < trackedResults.length; i++) {
641
+ if (trackedResults[i].trackId) {
642
+ results[i].trackId = trackedResults[i].trackId;
797
643
  }
798
644
  }
799
-
800
- results.push(result);
801
645
  }
802
-
646
+
803
647
  return results;
804
648
  } catch (error) {
805
649
  this.logger.error('FaceDetector', `人脸检测失败: ${error}`);
@@ -807,79 +651,6 @@ export class FaceDetector extends BaseScannerModule {
807
651
  }
808
652
  }
809
653
 
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
654
  /**
884
655
  * 比对两个人脸
885
656
  * @param source 源人脸
@@ -936,11 +707,13 @@ export class FaceDetector extends BaseScannerModule {
936
707
  targetEmbedding = target.embedding.vector;
937
708
  }
938
709
 
939
- // 计算相似度
940
- const similarity = this.calculateSimilarity(sourceEmbedding, targetEmbedding);
941
- const threshold = this.config.matchThreshold;
942
- const isMatch = similarity >= threshold;
943
-
710
+ // 计算相似度(使用 FaceComparator)
711
+ const comparisonResult = this.faceComparator.compare(sourceEmbedding, targetEmbedding);
712
+ if (comparisonResult.isFailure()) {
713
+ throw comparisonResult.getError() || new FaceComparisonError('人脸比对失败');
714
+ }
715
+ const { similarity, isMatch, threshold } = comparisonResult.getData()!;
716
+
944
717
  return Result.success({
945
718
  similarity,
946
719
  isMatch,
@@ -949,39 +722,11 @@ export class FaceDetector extends BaseScannerModule {
949
722
  } catch (error) {
950
723
  const errorMessage = error instanceof Error ? error.message : String(error);
951
724
  this.logger.error('FaceDetector', `人脸比对失败: ${errorMessage}`, error as Error);
952
-
725
+
953
726
  return Result.failure(new FaceComparisonError(`人脸比对失败: ${errorMessage}`));
954
727
  }
955
728
  }
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
-
729
+
985
730
  /**
986
731
  * 获取最近的检测结果
987
732
  */