id-scanner-lib 1.6.7 → 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 +885 -823
- package/dist/id-scanner-lib.esm.js.map +1 -1
- package/dist/id-scanner-lib.js +885 -823
- 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 +92 -419
- package/src/utils/index.ts +1 -0
package/src/core/logger.ts
CHANGED
|
@@ -62,19 +62,22 @@ export class ConsoleLogHandler implements LogHandler {
|
|
|
62
62
|
handle(entry: LogEntry): void {
|
|
63
63
|
const timestamp = new Date(entry.timestamp).toISOString();
|
|
64
64
|
const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
|
|
65
|
-
|
|
65
|
+
|
|
66
|
+
// 优先使用 error,其次使用 data
|
|
67
|
+
const extra = entry.error || entry.data || '';
|
|
68
|
+
|
|
66
69
|
switch (entry.level) {
|
|
67
70
|
case LoggerLevel.DEBUG:
|
|
68
|
-
console.debug(prefix, entry.message,
|
|
71
|
+
console.debug(prefix, entry.message, extra);
|
|
69
72
|
break;
|
|
70
73
|
case LoggerLevel.INFO:
|
|
71
|
-
console.info(prefix, entry.message,
|
|
74
|
+
console.info(prefix, entry.message, extra);
|
|
72
75
|
break;
|
|
73
76
|
case LoggerLevel.WARN:
|
|
74
|
-
console.warn(prefix, entry.message,
|
|
77
|
+
console.warn(prefix, entry.message, extra);
|
|
75
78
|
break;
|
|
76
79
|
case LoggerLevel.ERROR:
|
|
77
|
-
console.error(prefix, entry.message,
|
|
80
|
+
console.error(prefix, entry.message, extra);
|
|
78
81
|
break;
|
|
79
82
|
default:
|
|
80
83
|
// 输出什么也不做
|
|
@@ -158,10 +161,14 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
158
161
|
/** 发送间隔(毫秒) */
|
|
159
162
|
private flushInterval: number;
|
|
160
163
|
/** 定时发送的计时器ID */
|
|
161
|
-
private timerId:
|
|
164
|
+
private timerId: ReturnType<typeof setInterval> | null = null;
|
|
162
165
|
/** 是否在浏览器环境 */
|
|
163
166
|
private readonly isBrowser: boolean;
|
|
164
|
-
|
|
167
|
+
/** 最大连续失败次数,超过则丢弃日志 */
|
|
168
|
+
private readonly maxConsecutiveFailures: number;
|
|
169
|
+
/** 当前连续失败计数 */
|
|
170
|
+
private consecutiveFailures: number = 0;
|
|
171
|
+
|
|
165
172
|
/**
|
|
166
173
|
* 构造函数
|
|
167
174
|
* @param endpoint 远程服务器URL
|
|
@@ -173,10 +180,11 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
173
180
|
this.maxQueueSize = maxQueueSize;
|
|
174
181
|
this.flushInterval = flushInterval;
|
|
175
182
|
this.isBrowser = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
|
|
176
|
-
|
|
183
|
+
this.maxConsecutiveFailures = 10;
|
|
184
|
+
|
|
177
185
|
// 设置定时发送
|
|
178
186
|
this.startTimer();
|
|
179
|
-
|
|
187
|
+
|
|
180
188
|
// 页面卸载前尝试发送剩余日志
|
|
181
189
|
if (this.isBrowser) {
|
|
182
190
|
window.addEventListener('beforeunload', () => {
|
|
@@ -184,7 +192,7 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
184
192
|
});
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
|
-
|
|
195
|
+
|
|
188
196
|
/**
|
|
189
197
|
* 处理日志条目
|
|
190
198
|
* @param entry 日志条目
|
|
@@ -193,86 +201,96 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
193
201
|
// 只处理INFO以上级别的日志
|
|
194
202
|
if (entry.level >= LoggerLevel.INFO) {
|
|
195
203
|
this.queue.push(entry);
|
|
196
|
-
|
|
204
|
+
|
|
197
205
|
// 如果队列满了,立即发送
|
|
198
206
|
if (this.queue.length >= this.maxQueueSize) {
|
|
199
207
|
this.flush();
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
210
|
}
|
|
203
|
-
|
|
211
|
+
|
|
204
212
|
/**
|
|
205
213
|
* 发送队列中的日志
|
|
206
214
|
*/
|
|
207
215
|
flush(): void {
|
|
208
216
|
if (this.queue.length === 0) return;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
this.
|
|
212
|
-
|
|
213
|
-
// 防止在 fetch 失败时无限重试
|
|
214
|
-
const sendCount = (this as any)._sendCount || 0;
|
|
215
|
-
(this as any)._sendCount = sendCount + 1;
|
|
216
|
-
|
|
217
|
-
// 如果发送次数过多,停止发送以防止无限循环
|
|
218
|
-
if (sendCount > 10) {
|
|
219
|
-
console.warn('RemoteLogHandler: Too many failed sends, stopping. Clear queue.');
|
|
217
|
+
|
|
218
|
+
// 如果连续失败次数过多,停止发送以防止无限循环
|
|
219
|
+
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
220
|
+
console.warn('RemoteLogHandler: Too many consecutive failures, stopping. Clear queue.');
|
|
220
221
|
this.queue = [];
|
|
221
|
-
|
|
222
|
+
this.consecutiveFailures = 0;
|
|
222
223
|
return;
|
|
223
224
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'Content-Type': 'application/json'
|
|
230
|
-
},
|
|
231
|
-
body: JSON.stringify(entriesToSend),
|
|
232
|
-
keepalive: true
|
|
233
|
-
}).catch((err: Error) => {
|
|
234
|
-
console.error('Failed to send logs to remote server:', err);
|
|
235
|
-
|
|
236
|
-
// 防止无限重试 - 如果失败次数过多,丢弃日志
|
|
237
|
-
if ((this as any)._sendCount > 10) {
|
|
238
|
-
console.warn('RemoteLogHandler: Max retry exceeded, discarding logs');
|
|
239
|
-
this.queue = []; // 清空队列,避免内存泄漏
|
|
240
|
-
(this as any)._sendCount = 0;
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 失败时把日志放回队列,但防止无限增长
|
|
245
|
-
if (this.queue.length < this.maxQueueSize) {
|
|
246
|
-
const maxReturn = Math.min(entriesToSend.length, this.maxQueueSize - this.queue.length);
|
|
247
|
-
const returnedEntries = entriesToSend.slice(0, maxReturn);
|
|
248
|
-
this.queue = [...returnedEntries, ...this.queue];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
(this as any)._sendCount = 0;
|
|
252
|
-
});
|
|
253
|
-
} catch (error) {
|
|
254
|
-
console.error('Error sending logs:', error);
|
|
255
|
-
}
|
|
225
|
+
|
|
226
|
+
const entriesToSend = [...this.queue];
|
|
227
|
+
this.queue = [];
|
|
228
|
+
|
|
229
|
+
this.sendLogEntries(entriesToSend);
|
|
256
230
|
}
|
|
257
|
-
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 发送日志条目到远程服务器
|
|
234
|
+
* @param entries 日志条目数组
|
|
235
|
+
*/
|
|
236
|
+
private sendLogEntries(entries: LogEntry[]): void {
|
|
237
|
+
if (entries.length === 0) return;
|
|
238
|
+
|
|
239
|
+
const controller = new AbortController();
|
|
240
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s 超时
|
|
241
|
+
|
|
242
|
+
fetch(this.endpoint, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: {
|
|
245
|
+
'Content-Type': 'application/json'
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify(entries),
|
|
248
|
+
keepalive: true,
|
|
249
|
+
signal: controller.signal
|
|
250
|
+
}).then(() => {
|
|
251
|
+
clearTimeout(timeoutId);
|
|
252
|
+
this.consecutiveFailures = 0; // 发送成功,重置失败计数
|
|
253
|
+
}).catch((err: Error) => {
|
|
254
|
+
clearTimeout(timeoutId);
|
|
255
|
+
console.error('Failed to send logs to remote server:', err);
|
|
256
|
+
|
|
257
|
+
this.consecutiveFailures++;
|
|
258
|
+
|
|
259
|
+
// 如果失败次数过多,丢弃日志防止内存泄漏
|
|
260
|
+
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
261
|
+
console.warn('RemoteLogHandler: Max consecutive failures exceeded, discarding logs');
|
|
262
|
+
this.queue = [];
|
|
263
|
+
this.consecutiveFailures = 0;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 失败时把日志放回队列,但防止无限增长
|
|
268
|
+
const maxReturn = Math.min(entries.length, this.maxQueueSize - this.queue.length);
|
|
269
|
+
if (maxReturn > 0) {
|
|
270
|
+
const returnedEntries = entries.slice(0, maxReturn);
|
|
271
|
+
this.queue = [...returnedEntries, ...this.queue];
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
258
276
|
/**
|
|
259
277
|
* 开始定时发送
|
|
260
278
|
*/
|
|
261
279
|
startTimer(): void {
|
|
262
280
|
if (!this.isBrowser) return;
|
|
263
281
|
if (this.timerId !== null) return;
|
|
264
|
-
|
|
265
|
-
this.timerId =
|
|
282
|
+
|
|
283
|
+
this.timerId = setInterval(() => {
|
|
266
284
|
this.flush();
|
|
267
285
|
}, this.flushInterval);
|
|
268
286
|
}
|
|
269
|
-
|
|
287
|
+
|
|
270
288
|
/**
|
|
271
289
|
* 停止定时发送
|
|
272
290
|
*/
|
|
273
291
|
stopTimer(): void {
|
|
274
292
|
if (this.timerId !== null) {
|
|
275
|
-
|
|
293
|
+
clearInterval(this.timerId);
|
|
276
294
|
this.timerId = null;
|
|
277
295
|
}
|
|
278
296
|
}
|
|
@@ -364,40 +382,56 @@ export class Logger {
|
|
|
364
382
|
* 记录调试级别日志
|
|
365
383
|
* @param tag 标签
|
|
366
384
|
* @param message 消息
|
|
367
|
-
* @param
|
|
385
|
+
* @param errorOrData 错误对象或结构化数据
|
|
368
386
|
*/
|
|
369
|
-
debug(tag: string, message: string,
|
|
370
|
-
|
|
387
|
+
debug(tag: string, message: string, errorOrData?: Error | object): void {
|
|
388
|
+
if (errorOrData instanceof Error) {
|
|
389
|
+
this.log(LoggerLevel.DEBUG, tag, message, errorOrData);
|
|
390
|
+
} else {
|
|
391
|
+
this.logWithData(LoggerLevel.DEBUG, tag, message, errorOrData);
|
|
392
|
+
}
|
|
371
393
|
}
|
|
372
|
-
|
|
394
|
+
|
|
373
395
|
/**
|
|
374
396
|
* 记录信息级别日志
|
|
375
397
|
* @param tag 标签
|
|
376
398
|
* @param message 消息
|
|
377
|
-
* @param
|
|
399
|
+
* @param errorOrData 错误对象或结构化数据
|
|
378
400
|
*/
|
|
379
|
-
info(tag: string, message: string,
|
|
380
|
-
|
|
401
|
+
info(tag: string, message: string, errorOrData?: Error | object): void {
|
|
402
|
+
if (errorOrData instanceof Error) {
|
|
403
|
+
this.log(LoggerLevel.INFO, tag, message, errorOrData);
|
|
404
|
+
} else {
|
|
405
|
+
this.logWithData(LoggerLevel.INFO, tag, message, errorOrData);
|
|
406
|
+
}
|
|
381
407
|
}
|
|
382
|
-
|
|
408
|
+
|
|
383
409
|
/**
|
|
384
410
|
* 记录警告级别日志
|
|
385
411
|
* @param tag 标签
|
|
386
412
|
* @param message 消息
|
|
387
|
-
* @param
|
|
413
|
+
* @param errorOrData 错误对象或结构化数据
|
|
388
414
|
*/
|
|
389
|
-
warn(tag: string, message: string,
|
|
390
|
-
|
|
415
|
+
warn(tag: string, message: string, errorOrData?: Error | object): void {
|
|
416
|
+
if (errorOrData instanceof Error) {
|
|
417
|
+
this.log(LoggerLevel.WARN, tag, message, errorOrData);
|
|
418
|
+
} else {
|
|
419
|
+
this.logWithData(LoggerLevel.WARN, tag, message, errorOrData);
|
|
420
|
+
}
|
|
391
421
|
}
|
|
392
|
-
|
|
422
|
+
|
|
393
423
|
/**
|
|
394
424
|
* 记录错误级别日志
|
|
395
425
|
* @param tag 标签
|
|
396
426
|
* @param message 消息
|
|
397
|
-
* @param
|
|
427
|
+
* @param errorOrData 错误对象或结构化数据
|
|
398
428
|
*/
|
|
399
|
-
error(tag: string, message: string,
|
|
400
|
-
|
|
429
|
+
error(tag: string, message: string, errorOrData?: Error | object): void {
|
|
430
|
+
if (errorOrData instanceof Error) {
|
|
431
|
+
this.log(LoggerLevel.ERROR, tag, message, errorOrData);
|
|
432
|
+
} else {
|
|
433
|
+
this.logWithData(LoggerLevel.ERROR, tag, message, errorOrData);
|
|
434
|
+
}
|
|
401
435
|
}
|
|
402
436
|
|
|
403
437
|
/**
|
|
@@ -448,6 +482,46 @@ export class Logger {
|
|
|
448
482
|
}
|
|
449
483
|
}
|
|
450
484
|
|
|
485
|
+
/**
|
|
486
|
+
* 记录日志(支持结构化数据)
|
|
487
|
+
* @param level 日志级别
|
|
488
|
+
* @param tag 标签
|
|
489
|
+
* @param message 消息
|
|
490
|
+
* @param data 结构化数据
|
|
491
|
+
*/
|
|
492
|
+
private logWithData(level: LoggerLevel, tag: string, message: string, data?: object): void {
|
|
493
|
+
// 检查日志级别
|
|
494
|
+
const levelValue = this.getLevelValue(level);
|
|
495
|
+
const currentLevelValue = this.getLevelValue(this.logLevel);
|
|
496
|
+
|
|
497
|
+
if (levelValue < currentLevelValue) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 创建日志条目
|
|
502
|
+
const entry: LogEntry = {
|
|
503
|
+
timestamp: Date.now(),
|
|
504
|
+
level: level,
|
|
505
|
+
tag: tag || this.defaultTag,
|
|
506
|
+
message,
|
|
507
|
+
data
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// 分发到所有处理程序
|
|
511
|
+
for (const handler of this.handlers) {
|
|
512
|
+
try {
|
|
513
|
+
handler.handle(entry);
|
|
514
|
+
} catch (handlerError) {
|
|
515
|
+
console.error(`[Logger] 处理程序错误:`, handlerError);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 如果没有处理程序,使用控制台
|
|
520
|
+
if (this.handlers.length === 0) {
|
|
521
|
+
this.consoleOutput(entry);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
451
525
|
/**
|
|
452
526
|
* 控制台输出
|
|
453
527
|
* @param entry 日志条目
|
|
@@ -455,19 +529,22 @@ export class Logger {
|
|
|
455
529
|
private consoleOutput(entry: LogEntry): void {
|
|
456
530
|
const timestamp = new Date(entry.timestamp).toISOString();
|
|
457
531
|
const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
|
|
458
|
-
|
|
532
|
+
|
|
533
|
+
// 构造日志内容:错误对象或数据对象
|
|
534
|
+
const extra = entry.error || entry.data || '';
|
|
535
|
+
|
|
459
536
|
switch (entry.level) {
|
|
460
537
|
case LoggerLevel.DEBUG:
|
|
461
|
-
console.debug(`${prefix} ${entry.message}`,
|
|
538
|
+
console.debug(`${prefix} ${entry.message}`, extra);
|
|
462
539
|
break;
|
|
463
540
|
case LoggerLevel.INFO:
|
|
464
|
-
console.info(`${prefix} ${entry.message}`,
|
|
541
|
+
console.info(`${prefix} ${entry.message}`, extra);
|
|
465
542
|
break;
|
|
466
543
|
case LoggerLevel.WARN:
|
|
467
|
-
console.warn(`${prefix} ${entry.message}`,
|
|
544
|
+
console.warn(`${prefix} ${entry.message}`, extra);
|
|
468
545
|
break;
|
|
469
546
|
case LoggerLevel.ERROR:
|
|
470
|
-
console.error(`${prefix} ${entry.message}`,
|
|
547
|
+
console.error(`${prefix} ${entry.message}`, extra);
|
|
471
548
|
break;
|
|
472
549
|
}
|
|
473
550
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 人脸比对器
|
|
3
|
+
* @description 提供人脸特征向量比对功能
|
|
4
|
+
* @module modules/face/face-comparator
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { FaceDetectionResult } from '../../interfaces/face-detection';
|
|
8
|
+
import { Result } from '../../core/result';
|
|
9
|
+
import { FaceComparisonError } from '../../core/errors';
|
|
10
|
+
import { Logger } from '../../core/logger';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 人脸比对结果
|
|
14
|
+
*/
|
|
15
|
+
export interface ComparisonResult {
|
|
16
|
+
/** 相似度 (0-1) */
|
|
17
|
+
similarity: number;
|
|
18
|
+
/** 是否匹配(基于阈值) */
|
|
19
|
+
isMatch: boolean;
|
|
20
|
+
/** 使用的阈值 */
|
|
21
|
+
threshold: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 人脸比对器配置
|
|
26
|
+
*/
|
|
27
|
+
export interface FaceComparatorConfig {
|
|
28
|
+
/** 人脸匹配阈值 (0-1) */
|
|
29
|
+
matchThreshold?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 人脸比对器
|
|
34
|
+
*
|
|
35
|
+
* 负责计算两个人脸特征向量的相似度
|
|
36
|
+
*/
|
|
37
|
+
export class FaceComparator {
|
|
38
|
+
/** 日志记录器 */
|
|
39
|
+
private logger: Logger;
|
|
40
|
+
|
|
41
|
+
/** 匹配阈值 */
|
|
42
|
+
private matchThreshold: number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 构造函数
|
|
46
|
+
* @param config 比对器配置
|
|
47
|
+
*/
|
|
48
|
+
constructor(config: FaceComparatorConfig = {}) {
|
|
49
|
+
this.logger = Logger.getInstance();
|
|
50
|
+
this.matchThreshold = config.matchThreshold ?? 0.6;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 计算两个特征向量的余弦相似度
|
|
55
|
+
*
|
|
56
|
+
* @param v1 特征向量1
|
|
57
|
+
* @param v2 特征向量2
|
|
58
|
+
* @returns 相似度 (0-1)
|
|
59
|
+
*/
|
|
60
|
+
computeSimilarity(v1: number[], v2: number[]): number {
|
|
61
|
+
if (v1.length !== v2.length) {
|
|
62
|
+
throw new Error('特征向量维度不匹配');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let dotProduct = 0;
|
|
66
|
+
let norm1 = 0;
|
|
67
|
+
let norm2 = 0;
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < v1.length; i++) {
|
|
70
|
+
dotProduct += v1[i] * v2[i];
|
|
71
|
+
norm1 += v1[i] * v1[i];
|
|
72
|
+
norm2 += v2[i] * v2[i];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 确保长度非零
|
|
76
|
+
if (norm1 === 0 || norm2 === 0) {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 比对两个人脸
|
|
85
|
+
*
|
|
86
|
+
* @param source 源人脸(特征向量或检测结果)
|
|
87
|
+
* @param target 目标人脸(特征向量或检测结果)
|
|
88
|
+
* @returns 比对结果
|
|
89
|
+
*/
|
|
90
|
+
compare(
|
|
91
|
+
source: number[] | FaceDetectionResult,
|
|
92
|
+
target: number[] | FaceDetectionResult
|
|
93
|
+
): Result<ComparisonResult> {
|
|
94
|
+
try {
|
|
95
|
+
// 提取特征向量
|
|
96
|
+
const sourceEmbedding = this.extractEmbedding(source);
|
|
97
|
+
const targetEmbedding = this.extractEmbedding(target);
|
|
98
|
+
|
|
99
|
+
if (!sourceEmbedding || !targetEmbedding) {
|
|
100
|
+
return Result.failure(new FaceComparisonError('无法获取特征向量'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 计算相似度
|
|
104
|
+
const similarity = this.computeSimilarity(sourceEmbedding, targetEmbedding);
|
|
105
|
+
const isMatch = similarity >= this.matchThreshold;
|
|
106
|
+
|
|
107
|
+
return Result.success({
|
|
108
|
+
similarity,
|
|
109
|
+
isMatch,
|
|
110
|
+
threshold: this.matchThreshold
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
114
|
+
this.logger.error('FaceComparator', `人脸比对失败: ${errorMessage}`, error as Error);
|
|
115
|
+
return Result.failure(new FaceComparisonError(`人脸比对失败: ${errorMessage}`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 从输入中提取特征向量
|
|
121
|
+
*/
|
|
122
|
+
private extractEmbedding(input: number[] | FaceDetectionResult): number[] | null {
|
|
123
|
+
if (Array.isArray(input)) {
|
|
124
|
+
return input;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (input.embedding?.vector) {
|
|
128
|
+
return input.embedding.vector;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 设置匹配阈值
|
|
136
|
+
*/
|
|
137
|
+
setThreshold(threshold: number): void {
|
|
138
|
+
if (threshold < 0 || threshold > 1) {
|
|
139
|
+
throw new Error('阈值必须在 0-1 范围内');
|
|
140
|
+
}
|
|
141
|
+
this.matchThreshold = threshold;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 获取当前阈值
|
|
146
|
+
*/
|
|
147
|
+
getThreshold(): number {
|
|
148
|
+
return this.matchThreshold;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 人脸检测选项工厂
|
|
3
|
+
* @description 创建 face-api 检测选项和处理选项
|
|
4
|
+
* @module modules/face/face-detector-options
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as faceapi from '@vladmandic/face-api';
|
|
8
|
+
import { FaceDetectionOptions } from '../../interfaces/face-detection';
|
|
9
|
+
import { FaceModelType, FaceDetectorConfig } from './face-detector';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 人脸检测选项工厂
|
|
13
|
+
*
|
|
14
|
+
* 负责创建 face-api 兼容的检测选项和处理选项
|
|
15
|
+
*/
|
|
16
|
+
export class FaceDetectorOptionsFactory {
|
|
17
|
+
/**
|
|
18
|
+
* 创建 face-api 检测选项
|
|
19
|
+
* @param detectionModel 检测模型类型
|
|
20
|
+
* @param minConfidence 最小置信度
|
|
21
|
+
* @returns face-api 检测选项
|
|
22
|
+
*/
|
|
23
|
+
static createFaceAPIOptions(
|
|
24
|
+
detectionModel: FaceModelType,
|
|
25
|
+
minConfidence: number
|
|
26
|
+
): faceapi.SsdMobilenetv1Options | faceapi.TinyFaceDetectorOptions | faceapi.MtcnnOptions {
|
|
27
|
+
switch (detectionModel) {
|
|
28
|
+
case FaceModelType.SSD_MOBILENET:
|
|
29
|
+
return new faceapi.SsdMobilenetv1Options({ minConfidence });
|
|
30
|
+
case FaceModelType.TINY_FACE:
|
|
31
|
+
return new faceapi.TinyFaceDetectorOptions({ scoreThreshold: minConfidence });
|
|
32
|
+
case FaceModelType.MTCNN:
|
|
33
|
+
return new faceapi.MtcnnOptions({ minConfidence });
|
|
34
|
+
default:
|
|
35
|
+
return new faceapi.SsdMobilenetv1Options({ minConfidence });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 合并检测选项和配置
|
|
41
|
+
* @param config 人脸检测器配置
|
|
42
|
+
* @param options 用户提供的选项
|
|
43
|
+
* @returns 合并后的检测选项
|
|
44
|
+
*/
|
|
45
|
+
static mergeOptions(
|
|
46
|
+
config: FaceDetectorConfig,
|
|
47
|
+
options: FaceDetectionOptions = {}
|
|
48
|
+
): FaceDetectionOptions {
|
|
49
|
+
return {
|
|
50
|
+
minConfidence: config.minConfidence,
|
|
51
|
+
maxFaces: config.maxFaces,
|
|
52
|
+
withLandmarks: config.detectLandmarks,
|
|
53
|
+
withAttributes: config.detectExpressions || config.detectAgeGender,
|
|
54
|
+
withEmbedding: config.extractEmbeddings,
|
|
55
|
+
enableTracking: config.enableTracking,
|
|
56
|
+
...options
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 执行人脸检测
|
|
62
|
+
* @param input 输入图像/视频/画布
|
|
63
|
+
* @param faceapiOptions face-api 检测选项
|
|
64
|
+
* @param detectOptions 检测选项
|
|
65
|
+
* @param landmarksModel 关键点模型类型
|
|
66
|
+
* @returns face-api 检测结果
|
|
67
|
+
*/
|
|
68
|
+
static async detect(
|
|
69
|
+
input: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement,
|
|
70
|
+
faceapiOptions: faceapi.SsdMobilenetv1Options | faceapi.TinyFaceDetectorOptions | faceapi.MtcnnOptions,
|
|
71
|
+
detectOptions: FaceDetectionOptions,
|
|
72
|
+
landmarksModel: 'tiny' | '68_points'
|
|
73
|
+
): Promise<any> {
|
|
74
|
+
const withLandmarks = detectOptions.withLandmarks;
|
|
75
|
+
const withAttributes = detectOptions.withAttributes;
|
|
76
|
+
const withEmbedding = detectOptions.withEmbedding;
|
|
77
|
+
|
|
78
|
+
// 根据选项组合选择检测方法链
|
|
79
|
+
if (withLandmarks && withAttributes && withEmbedding) {
|
|
80
|
+
// 全功能检测
|
|
81
|
+
return await faceapi
|
|
82
|
+
.detectAllFaces(input, faceapiOptions)
|
|
83
|
+
.withFaceLandmarks(landmarksModel === 'tiny')
|
|
84
|
+
.withFaceExpressions()
|
|
85
|
+
.withAgeAndGender()
|
|
86
|
+
.withFaceDescriptors();
|
|
87
|
+
} else if (withLandmarks && withAttributes) {
|
|
88
|
+
// 检测带关键点和属性
|
|
89
|
+
return await faceapi
|
|
90
|
+
.detectAllFaces(input, faceapiOptions)
|
|
91
|
+
.withFaceLandmarks(landmarksModel === 'tiny')
|
|
92
|
+
.withFaceExpressions()
|
|
93
|
+
.withAgeAndGender();
|
|
94
|
+
} else if (withLandmarks) {
|
|
95
|
+
// 检测带关键点
|
|
96
|
+
return await faceapi
|
|
97
|
+
.detectAllFaces(input, faceapiOptions)
|
|
98
|
+
.withFaceLandmarks(landmarksModel === 'tiny');
|
|
99
|
+
} else {
|
|
100
|
+
// 仅检测
|
|
101
|
+
return await faceapi.detectAllFaces(input, faceapiOptions);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|