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.
- package/dist/id-scanner-lib.esm.js +915 -838
- package/dist/id-scanner-lib.esm.js.map +1 -1
- package/dist/id-scanner-lib.js +915 -838
- package/dist/id-scanner-lib.js.map +1 -1
- package/package.json +1 -1
- package/src/core/camera-manager.ts +43 -76
- package/src/core/camera-stream-manager.ts +318 -0
- package/src/core/logger.ts +158 -81
- package/src/modules/face/face-comparator.ts +150 -0
- package/src/modules/face/face-detector-options.ts +104 -0
- package/src/modules/face/face-detector.ts +121 -376
- package/src/modules/face/face-detector.ts.bak +991 -0
- package/src/modules/face/face-model-loader.ts +222 -0
- package/src/modules/face/face-result-converter.ts +225 -0
- package/src/modules/face/face-tracker.ts +207 -0
- package/src/modules/face/liveness-detector.ts +2 -2
- package/src/modules/id-card/id-card-text-parser.ts +151 -0
- package/src/modules/id-card/ocr-processor.ts +20 -257
- package/src/modules/id-card/ocr-worker.ts +2 -183
- package/src/utils/canvas-pool.ts +273 -0
- package/src/utils/edge-detector.ts +232 -0
- package/src/utils/image-processing.ts +110 -446
- package/src/utils/index.ts +1 -0
- package/src/core/plugin-manager.ts +0 -429
|
@@ -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
|
|
111
|
-
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
309
|
-
* @param modelPath 模型路径
|
|
292
|
+
* 根据需求加载模型 - 委托给 FaceModelLoader
|
|
293
|
+
* @deprecated 使用 modelLoader.loadModelsOnDemand 代替
|
|
310
294
|
*/
|
|
311
295
|
private async loadModelsOnDemand(options: FaceDetectionOptions, modelPath: string): Promise<void> {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
* @
|
|
304
|
+
* 加载人脸检测模型 - 委托给 FaceModelLoader
|
|
305
|
+
* @deprecated 使用 modelLoader.ensureModelsLoaded 代替
|
|
344
306
|
*/
|
|
345
307
|
private async loadModels(modelPath: string): Promise<void> {
|
|
346
|
-
await this.
|
|
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.
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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.
|
|
595
|
+
if (!this.modelLoader.isModelsLoaded()) {
|
|
638
596
|
throw new FaceDetectionError('人脸检测模型尚未加载');
|
|
639
597
|
}
|
|
640
598
|
|
|
641
599
|
// 合并选项和配置
|
|
642
|
-
const detectOptions
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
|
941
|
-
|
|
942
|
-
|
|
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
|
*/
|