id-scanner-lib 1.6.5 → 1.6.7
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 +107 -67
- package/dist/id-scanner-lib.esm.js.map +1 -1
- package/dist/id-scanner-lib.js +107 -66
- package/dist/id-scanner-lib.js.map +1 -1
- package/package.json +1 -1
- package/src/core/event-emitter.ts +9 -0
- package/src/core/logger.ts +52 -34
- package/src/core/resource-manager.ts +23 -9
- package/src/core/result.ts +3 -3
- package/src/index.ts +5 -5
- package/src/modules/face/face-detector.ts +8 -6
- package/src/modules/face/liveness-detector.ts +3 -2
- package/src/modules/id-card/index.ts +10 -8
- package/src/utils/image-processing.ts +22 -31
- package/src/core/plugin-manager.ts +0 -429
- package/src/core/scanner-factory.ts +0 -236
package/package.json
CHANGED
|
@@ -52,6 +52,15 @@ export class EventEmitter {
|
|
|
52
52
|
this.eventHandlers.delete(eventName);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 取消订阅事件 (off的别名)
|
|
58
|
+
* @param eventName 事件名称
|
|
59
|
+
* @param handler 事件处理器
|
|
60
|
+
*/
|
|
61
|
+
removeListener(eventName: string, handler: EventHandler): void {
|
|
62
|
+
this.off(eventName, handler);
|
|
63
|
+
}
|
|
55
64
|
|
|
56
65
|
/**
|
|
57
66
|
* 订阅事件,但只触发一次
|
package/src/core/logger.ts
CHANGED
|
@@ -9,19 +9,24 @@ import { ConfigManager } from './config';
|
|
|
9
9
|
/**
|
|
10
10
|
* 日志级别枚举
|
|
11
11
|
*/
|
|
12
|
-
export enum
|
|
12
|
+
export enum LoggerLevel {
|
|
13
13
|
DEBUG = 'debug',
|
|
14
14
|
INFO = 'info',
|
|
15
15
|
WARN = 'warn',
|
|
16
16
|
ERROR = 'error'
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated 使用 LoggerLevel 代替
|
|
21
|
+
*/
|
|
22
|
+
export const LogLevel = LoggerLevel;
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* 日志条目接口
|
|
21
26
|
*/
|
|
22
27
|
export interface LogEntry {
|
|
23
28
|
/** 日志级别 */
|
|
24
|
-
level:
|
|
29
|
+
level: LoggerLevel;
|
|
25
30
|
/** 日志标签 */
|
|
26
31
|
tag: string;
|
|
27
32
|
/** 日志消息 */
|
|
@@ -59,16 +64,16 @@ export class ConsoleLogHandler implements LogHandler {
|
|
|
59
64
|
const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
|
|
60
65
|
|
|
61
66
|
switch (entry.level) {
|
|
62
|
-
case
|
|
67
|
+
case LoggerLevel.DEBUG:
|
|
63
68
|
console.debug(prefix, entry.message, entry.error || '');
|
|
64
69
|
break;
|
|
65
|
-
case
|
|
70
|
+
case LoggerLevel.INFO:
|
|
66
71
|
console.info(prefix, entry.message, entry.error || '');
|
|
67
72
|
break;
|
|
68
|
-
case
|
|
73
|
+
case LoggerLevel.WARN:
|
|
69
74
|
console.warn(prefix, entry.message, entry.error || '');
|
|
70
75
|
break;
|
|
71
|
-
case
|
|
76
|
+
case LoggerLevel.ERROR:
|
|
72
77
|
console.error(prefix, entry.message, entry.error || '');
|
|
73
78
|
break;
|
|
74
79
|
default:
|
|
@@ -119,7 +124,7 @@ export class MemoryLogHandler implements LogHandler {
|
|
|
119
124
|
* 根据级别过滤日志条目
|
|
120
125
|
* @param level 日志级别
|
|
121
126
|
*/
|
|
122
|
-
getEntriesByLevel(level:
|
|
127
|
+
getEntriesByLevel(level: LoggerLevel): LogEntry[] {
|
|
123
128
|
return this.entries.filter(entry => entry.level === level);
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -154,6 +159,8 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
154
159
|
private flushInterval: number;
|
|
155
160
|
/** 定时发送的计时器ID */
|
|
156
161
|
private timerId: number | null = null;
|
|
162
|
+
/** 是否在浏览器环境 */
|
|
163
|
+
private readonly isBrowser: boolean;
|
|
157
164
|
|
|
158
165
|
/**
|
|
159
166
|
* 构造函数
|
|
@@ -165,14 +172,17 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
165
172
|
this.endpoint = endpoint;
|
|
166
173
|
this.maxQueueSize = maxQueueSize;
|
|
167
174
|
this.flushInterval = flushInterval;
|
|
175
|
+
this.isBrowser = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
|
|
168
176
|
|
|
169
177
|
// 设置定时发送
|
|
170
178
|
this.startTimer();
|
|
171
179
|
|
|
172
180
|
// 页面卸载前尝试发送剩余日志
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
if (this.isBrowser) {
|
|
182
|
+
window.addEventListener('beforeunload', () => {
|
|
183
|
+
this.flush();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
/**
|
|
@@ -181,7 +191,7 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
181
191
|
*/
|
|
182
192
|
handle(entry: LogEntry): void {
|
|
183
193
|
// 只处理INFO以上级别的日志
|
|
184
|
-
if (entry.level >=
|
|
194
|
+
if (entry.level >= LoggerLevel.INFO) {
|
|
185
195
|
this.queue.push(entry);
|
|
186
196
|
|
|
187
197
|
// 如果队列满了,立即发送
|
|
@@ -249,6 +259,7 @@ export class RemoteLogHandler implements LogHandler {
|
|
|
249
259
|
* 开始定时发送
|
|
250
260
|
*/
|
|
251
261
|
startTimer(): void {
|
|
262
|
+
if (!this.isBrowser) return;
|
|
252
263
|
if (this.timerId !== null) return;
|
|
253
264
|
|
|
254
265
|
this.timerId = window.setInterval(() => {
|
|
@@ -281,7 +292,7 @@ export class Logger {
|
|
|
281
292
|
/** 默认标签 */
|
|
282
293
|
private defaultTag: string = 'IDScanner';
|
|
283
294
|
/** 日志级别 */
|
|
284
|
-
private logLevel:
|
|
295
|
+
private logLevel: LoggerLevel = LoggerLevel.INFO;
|
|
285
296
|
|
|
286
297
|
/**
|
|
287
298
|
* 私有构造函数,防止直接实例化
|
|
@@ -293,7 +304,7 @@ export class Logger {
|
|
|
293
304
|
this.addHandler(new ConsoleLogHandler());
|
|
294
305
|
|
|
295
306
|
// 监听配置变化
|
|
296
|
-
this.config.onConfigChange('logLevel', (level:
|
|
307
|
+
this.config.onConfigChange('logLevel', (level: LoggerLevel) => {
|
|
297
308
|
this.debug('Logger', `Log level changed to ${level}`);
|
|
298
309
|
});
|
|
299
310
|
}
|
|
@@ -307,6 +318,13 @@ export class Logger {
|
|
|
307
318
|
}
|
|
308
319
|
return Logger.instance;
|
|
309
320
|
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 重置单例实例(主要用于测试)
|
|
324
|
+
*/
|
|
325
|
+
public static resetInstance(): void {
|
|
326
|
+
Logger.instance = undefined as any;
|
|
327
|
+
}
|
|
310
328
|
|
|
311
329
|
/**
|
|
312
330
|
* 添加日志处理器
|
|
@@ -349,7 +367,7 @@ export class Logger {
|
|
|
349
367
|
* @param error 错误
|
|
350
368
|
*/
|
|
351
369
|
debug(tag: string, message: string, error?: Error): void {
|
|
352
|
-
this.log(
|
|
370
|
+
this.log(LoggerLevel.DEBUG, tag, message, error);
|
|
353
371
|
}
|
|
354
372
|
|
|
355
373
|
/**
|
|
@@ -359,7 +377,7 @@ export class Logger {
|
|
|
359
377
|
* @param error 错误
|
|
360
378
|
*/
|
|
361
379
|
info(tag: string, message: string, error?: Error): void {
|
|
362
|
-
this.log(
|
|
380
|
+
this.log(LoggerLevel.INFO, tag, message, error);
|
|
363
381
|
}
|
|
364
382
|
|
|
365
383
|
/**
|
|
@@ -369,7 +387,7 @@ export class Logger {
|
|
|
369
387
|
* @param error 错误
|
|
370
388
|
*/
|
|
371
389
|
warn(tag: string, message: string, error?: Error): void {
|
|
372
|
-
this.log(
|
|
390
|
+
this.log(LoggerLevel.WARN, tag, message, error);
|
|
373
391
|
}
|
|
374
392
|
|
|
375
393
|
/**
|
|
@@ -379,7 +397,7 @@ export class Logger {
|
|
|
379
397
|
* @param error 错误
|
|
380
398
|
*/
|
|
381
399
|
error(tag: string, message: string, error?: Error): void {
|
|
382
|
-
this.log(
|
|
400
|
+
this.log(LoggerLevel.ERROR, tag, message, error);
|
|
383
401
|
}
|
|
384
402
|
|
|
385
403
|
/**
|
|
@@ -397,7 +415,7 @@ export class Logger {
|
|
|
397
415
|
* @param message 消息
|
|
398
416
|
* @param error 错误
|
|
399
417
|
*/
|
|
400
|
-
private log(level:
|
|
418
|
+
private log(level: LoggerLevel, tag: string, message: string, error?: Error): void {
|
|
401
419
|
// 检查日志级别
|
|
402
420
|
const levelValue = this.getLevelValue(level);
|
|
403
421
|
const currentLevelValue = this.getLevelValue(this.logLevel);
|
|
@@ -439,16 +457,16 @@ export class Logger {
|
|
|
439
457
|
const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
|
|
440
458
|
|
|
441
459
|
switch (entry.level) {
|
|
442
|
-
case
|
|
460
|
+
case LoggerLevel.DEBUG:
|
|
443
461
|
console.debug(`${prefix} ${entry.message}`, entry.error || '');
|
|
444
462
|
break;
|
|
445
|
-
case
|
|
463
|
+
case LoggerLevel.INFO:
|
|
446
464
|
console.info(`${prefix} ${entry.message}`, entry.error || '');
|
|
447
465
|
break;
|
|
448
|
-
case
|
|
466
|
+
case LoggerLevel.WARN:
|
|
449
467
|
console.warn(`${prefix} ${entry.message}`, entry.error || '');
|
|
450
468
|
break;
|
|
451
|
-
case
|
|
469
|
+
case LoggerLevel.ERROR:
|
|
452
470
|
console.error(`${prefix} ${entry.message}`, entry.error || '');
|
|
453
471
|
break;
|
|
454
472
|
}
|
|
@@ -458,15 +476,15 @@ export class Logger {
|
|
|
458
476
|
* 获取日志级别值
|
|
459
477
|
* @param level 日志级别
|
|
460
478
|
*/
|
|
461
|
-
private getLevelValue(level:
|
|
479
|
+
private getLevelValue(level: LoggerLevel): number {
|
|
462
480
|
switch (level) {
|
|
463
|
-
case
|
|
481
|
+
case LoggerLevel.DEBUG:
|
|
464
482
|
return 0;
|
|
465
|
-
case
|
|
483
|
+
case LoggerLevel.INFO:
|
|
466
484
|
return 1;
|
|
467
|
-
case
|
|
485
|
+
case LoggerLevel.WARN:
|
|
468
486
|
return 2;
|
|
469
|
-
case
|
|
487
|
+
case LoggerLevel.ERROR:
|
|
470
488
|
return 3;
|
|
471
489
|
default:
|
|
472
490
|
return 1; // 默认INFO级别
|
|
@@ -477,23 +495,23 @@ export class Logger {
|
|
|
477
495
|
* 设置日志级别
|
|
478
496
|
* @param level 日志级别
|
|
479
497
|
*/
|
|
480
|
-
public setLevel(level:
|
|
498
|
+
public setLevel(level: LoggerLevel | string): void {
|
|
481
499
|
if (typeof level === 'string') {
|
|
482
500
|
switch (level) {
|
|
483
501
|
case 'debug':
|
|
484
|
-
this.logLevel =
|
|
502
|
+
this.logLevel = LoggerLevel.DEBUG;
|
|
485
503
|
break;
|
|
486
504
|
case 'info':
|
|
487
|
-
this.logLevel =
|
|
505
|
+
this.logLevel = LoggerLevel.INFO;
|
|
488
506
|
break;
|
|
489
507
|
case 'warn':
|
|
490
|
-
this.logLevel =
|
|
508
|
+
this.logLevel = LoggerLevel.WARN;
|
|
491
509
|
break;
|
|
492
510
|
case 'error':
|
|
493
|
-
this.logLevel =
|
|
511
|
+
this.logLevel = LoggerLevel.ERROR;
|
|
494
512
|
break;
|
|
495
513
|
default:
|
|
496
|
-
this.logLevel =
|
|
514
|
+
this.logLevel = LoggerLevel.INFO;
|
|
497
515
|
}
|
|
498
516
|
} else {
|
|
499
517
|
this.logLevel = level;
|
|
@@ -506,7 +524,7 @@ export class Logger {
|
|
|
506
524
|
* 获取当前日志级别
|
|
507
525
|
* @returns 当前日志级别
|
|
508
526
|
*/
|
|
509
|
-
public getLevel():
|
|
527
|
+
public getLevel(): LoggerLevel {
|
|
510
528
|
return this.logLevel;
|
|
511
529
|
}
|
|
512
530
|
}
|
|
@@ -118,6 +118,8 @@ export class ResourceManager extends EventEmitter {
|
|
|
118
118
|
/** 加载中的资源请求 */
|
|
119
119
|
private pendingRequests: Map<string, Promise<any>> = new Map();
|
|
120
120
|
private initialized: boolean = false;
|
|
121
|
+
/** 是否在浏览器环境 */
|
|
122
|
+
private readonly isBrowser: boolean;
|
|
121
123
|
|
|
122
124
|
/**
|
|
123
125
|
* 私有构造函数
|
|
@@ -126,16 +128,19 @@ export class ResourceManager extends EventEmitter {
|
|
|
126
128
|
super();
|
|
127
129
|
this.config = ConfigManager.getInstance();
|
|
128
130
|
this.logger = Logger.getInstance();
|
|
131
|
+
this.isBrowser = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
|
|
129
132
|
|
|
130
133
|
// 初始化资源清理定时器
|
|
131
134
|
this.setupCleanupTimer();
|
|
132
135
|
|
|
133
136
|
// 页面卸载时尝试释放资源
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
if (this.isBrowser) {
|
|
138
|
+
window.addEventListener('beforeunload', () => {
|
|
139
|
+
if (this.config.get('autoReleaseResources', true)) {
|
|
140
|
+
this.releaseAll();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
/**
|
|
@@ -147,6 +152,13 @@ export class ResourceManager extends EventEmitter {
|
|
|
147
152
|
}
|
|
148
153
|
return ResourceManager.instance;
|
|
149
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 重置单例实例(主要用于测试)
|
|
158
|
+
*/
|
|
159
|
+
public static resetInstance(): void {
|
|
160
|
+
ResourceManager.instance = undefined as any;
|
|
161
|
+
}
|
|
150
162
|
|
|
151
163
|
/**
|
|
152
164
|
* 设置基础路径
|
|
@@ -402,10 +414,10 @@ export class ResourceManager extends EventEmitter {
|
|
|
402
414
|
const promises = resources.map(async ({ id, url, options }) => {
|
|
403
415
|
const result = await this.load(id, url, options);
|
|
404
416
|
|
|
405
|
-
if (result.isSuccess() && result.
|
|
406
|
-
results[id] = result.
|
|
407
|
-
} else if (result.isFailure() && result.
|
|
408
|
-
errors.push({ id, error: result.
|
|
417
|
+
if (result.isSuccess() && result.getData() !== undefined) {
|
|
418
|
+
results[id] = result.getData();
|
|
419
|
+
} else if (result.isFailure() && result.getError()) {
|
|
420
|
+
errors.push({ id, error: result.getError()! });
|
|
409
421
|
}
|
|
410
422
|
});
|
|
411
423
|
|
|
@@ -668,6 +680,8 @@ export class ResourceManager extends EventEmitter {
|
|
|
668
680
|
* 设置资源清理定时器
|
|
669
681
|
*/
|
|
670
682
|
private setupCleanupTimer(): void {
|
|
683
|
+
if (!this.isBrowser) return;
|
|
684
|
+
|
|
671
685
|
const interval = 60000; // 每分钟检查一次
|
|
672
686
|
|
|
673
687
|
this.cleanupTimerId = window.setInterval(() => {
|
package/src/core/result.ts
CHANGED
|
@@ -72,21 +72,21 @@ export class Result<T = any> {
|
|
|
72
72
|
/**
|
|
73
73
|
* 获取结果数据
|
|
74
74
|
*/
|
|
75
|
-
|
|
75
|
+
getData(): T | undefined {
|
|
76
76
|
return this._data;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* 获取错误对象
|
|
81
81
|
*/
|
|
82
|
-
|
|
82
|
+
getError(): Error | undefined {
|
|
83
83
|
return this._error;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* 获取元数据
|
|
88
88
|
*/
|
|
89
|
-
|
|
89
|
+
getMeta(): Record<string, any> | undefined {
|
|
90
90
|
return this._meta;
|
|
91
91
|
}
|
|
92
92
|
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { IDCardModule } from './modules/id-card';
|
|
|
10
10
|
import { QRCodeModule } from './modules/qrcode';
|
|
11
11
|
import { FaceModule } from './modules/face';
|
|
12
12
|
import { VERSION, BUILD_DATE } from './version';
|
|
13
|
-
import { Logger,
|
|
13
|
+
import { Logger, LoggerLevel } from './core/logger';
|
|
14
14
|
import { IDCardModuleOptions } from './modules/id-card/types';
|
|
15
15
|
import { QRCodeModuleOptions } from './modules/qrcode/types';
|
|
16
16
|
import { FaceModuleOptions } from './modules/face/types';
|
|
@@ -20,7 +20,7 @@ import { FaceModuleOptions } from './modules/face/types';
|
|
|
20
20
|
*/
|
|
21
21
|
export interface IDScannerOptions {
|
|
22
22
|
/** 日志级别 */
|
|
23
|
-
logLevel?:
|
|
23
|
+
logLevel?: LoggerLevel;
|
|
24
24
|
/** 是否启用身份证识别模块 */
|
|
25
25
|
enableIDCard?: boolean;
|
|
26
26
|
/** 是否启用二维码识别模块 */
|
|
@@ -66,15 +66,15 @@ export class IDScanner {
|
|
|
66
66
|
this.moduleManager = ModuleManager.getInstance();
|
|
67
67
|
|
|
68
68
|
// 注册模块
|
|
69
|
-
if (options.enableIDCard
|
|
69
|
+
if (options.enableIDCard === true) {
|
|
70
70
|
this.moduleManager.register(new IDCardModule(options.idCard));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
if (options.enableQRCode
|
|
73
|
+
if (options.enableQRCode === true) {
|
|
74
74
|
this.moduleManager.register(new QRCodeModule(options.qrCode));
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
if (options.enableFace
|
|
77
|
+
if (options.enableFace === true) {
|
|
78
78
|
this.moduleManager.register(new FaceModule(options.face));
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -898,13 +898,14 @@ export class FaceDetector extends BaseScannerModule {
|
|
|
898
898
|
if (typeof source === 'string' || source instanceof HTMLImageElement) {
|
|
899
899
|
// 处理图片源
|
|
900
900
|
const result = await this.processImage(source, { withEmbedding: true });
|
|
901
|
-
|
|
901
|
+
const resultData = result.getData();
|
|
902
|
+
if (!result.isSuccess() || !resultData || resultData.length === 0) {
|
|
902
903
|
throw new FaceComparisonError('无法从源图像检测人脸');
|
|
903
904
|
}
|
|
904
|
-
if (!
|
|
905
|
+
if (!resultData[0].embedding) {
|
|
905
906
|
throw new FaceComparisonError('源图像未提取特征向量');
|
|
906
907
|
}
|
|
907
|
-
sourceEmbedding =
|
|
908
|
+
sourceEmbedding = resultData[0].embedding.vector;
|
|
908
909
|
} else {
|
|
909
910
|
// 使用现有检测结果
|
|
910
911
|
if (!source.embedding || !source.embedding.vector) {
|
|
@@ -919,13 +920,14 @@ export class FaceDetector extends BaseScannerModule {
|
|
|
919
920
|
if (typeof target === 'string' || target instanceof HTMLImageElement) {
|
|
920
921
|
// 处理图片源
|
|
921
922
|
const result = await this.processImage(target, { withEmbedding: true });
|
|
922
|
-
|
|
923
|
+
const resultData = result.getData();
|
|
924
|
+
if (!result.isSuccess() || !resultData || resultData.length === 0) {
|
|
923
925
|
throw new FaceComparisonError('无法从目标图像检测人脸');
|
|
924
926
|
}
|
|
925
|
-
if (!
|
|
927
|
+
if (!resultData[0].embedding) {
|
|
926
928
|
throw new FaceComparisonError('目标图像未提取特征向量');
|
|
927
929
|
}
|
|
928
|
-
targetEmbedding =
|
|
930
|
+
targetEmbedding = resultData[0].embedding.vector;
|
|
929
931
|
} else {
|
|
930
932
|
// 使用现有检测结果
|
|
931
933
|
if (!target.embedding || !target.embedding.vector) {
|
|
@@ -227,13 +227,14 @@ export class LivenessDetector extends BaseScannerModule {
|
|
|
227
227
|
withLandmarks: true,
|
|
228
228
|
withAttributes: true
|
|
229
229
|
});
|
|
230
|
+
const faceData = faceResult.getData();
|
|
230
231
|
|
|
231
|
-
if (!faceResult.isSuccess() || !
|
|
232
|
+
if (!faceResult.isSuccess() || !faceData || faceData.length === 0) {
|
|
232
233
|
throw new LivenessDetectionError('未在图像中检测到人脸');
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
// 获取检测到的第一个人脸
|
|
236
|
-
const face =
|
|
237
|
+
const face = faceData[0];
|
|
237
238
|
|
|
238
239
|
// 执行被动式活体检测
|
|
239
240
|
const livenessResult = this.performPassiveLivenessDetection(face);
|
|
@@ -129,21 +129,22 @@ export class IDCardModule extends BaseModule {
|
|
|
129
129
|
try {
|
|
130
130
|
// 检测身份证
|
|
131
131
|
const detectionResult = await this.detector.processImage(image);
|
|
132
|
+
const detectionData = detectionResult.getData();
|
|
132
133
|
|
|
133
|
-
if (!detectionResult.isSuccess() || !
|
|
134
|
+
if (!detectionResult.isSuccess() || !detectionData) {
|
|
134
135
|
throw new Error('未检测到身份证');
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
// 创建结果对象
|
|
138
139
|
const idCardInfo: IDCardInfo = {
|
|
139
|
-
type:
|
|
140
|
-
confidence:
|
|
140
|
+
type: detectionData.type || IDCardType.FRONT,
|
|
141
|
+
confidence: detectionData.confidence
|
|
141
142
|
};
|
|
142
143
|
|
|
143
144
|
// 如果启用OCR且OCR处理器已初始化
|
|
144
145
|
if (this.options.detector?.enableOCR && this.ocrProcessor) {
|
|
145
146
|
// 裁剪并处理图像
|
|
146
|
-
const processedImage =
|
|
147
|
+
const processedImage = detectionData.image || this.convertToImageData(image);
|
|
147
148
|
|
|
148
149
|
// 识别文本信息
|
|
149
150
|
const ocrResult = await this.ocrProcessor.processIDCard(processedImage);
|
|
@@ -364,15 +365,16 @@ export class IDCardModule extends BaseModule {
|
|
|
364
365
|
// 调用检测器处理图像
|
|
365
366
|
const result = await this.detector.processImage(image);
|
|
366
367
|
|
|
367
|
-
if (!result.isSuccess() || !result.
|
|
368
|
+
if (!result.isSuccess() || !result.getData()) {
|
|
368
369
|
return { success: false, confidence: 0 };
|
|
369
370
|
}
|
|
370
371
|
|
|
372
|
+
const data = result.getData()!;
|
|
371
373
|
return {
|
|
372
374
|
success: true,
|
|
373
|
-
type:
|
|
374
|
-
confidence:
|
|
375
|
-
croppedImage:
|
|
375
|
+
type: data.type,
|
|
376
|
+
confidence: data.confidence || 0,
|
|
377
|
+
croppedImage: data.image
|
|
376
378
|
};
|
|
377
379
|
} catch (error) {
|
|
378
380
|
this.logger.error(this.name, '身份证检测失败', error instanceof Error ? error : new Error(String(error)));
|
|
@@ -174,17 +174,20 @@ export class ImageProcessor {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
177
|
+
// 处理边缘像素(仅遍历四条边,而非全图 O(width×height) → O(width+height))
|
|
178
|
+
// 上边 + 下边
|
|
179
|
+
for (let x = 0; x < width; x++) {
|
|
180
|
+
const topPos = x * 4
|
|
181
|
+
const bottomPos = ((height - 1) * width + x) * 4
|
|
182
|
+
outputData[topPos] = data[topPos]; outputData[topPos + 1] = data[topPos + 1]; outputData[topPos + 2] = data[topPos + 2]; outputData[topPos + 3] = data[topPos + 3]
|
|
183
|
+
outputData[bottomPos] = data[bottomPos]; outputData[bottomPos + 1] = data[bottomPos + 1]; outputData[bottomPos + 2] = data[bottomPos + 2]; outputData[bottomPos + 3] = data[bottomPos + 3]
|
|
184
|
+
}
|
|
185
|
+
// 左边 + 右边(排除四角,它们已在上下一行处理)
|
|
186
|
+
for (let y = 1; y < height - 1; y++) {
|
|
187
|
+
const leftPos = y * width * 4
|
|
188
|
+
const rightPos = (y * width + width - 1) * 4
|
|
189
|
+
outputData[leftPos] = data[leftPos]; outputData[leftPos + 1] = data[leftPos + 1]; outputData[leftPos + 2] = data[leftPos + 2]; outputData[leftPos + 3] = data[leftPos + 3]
|
|
190
|
+
outputData[rightPos] = data[rightPos]; outputData[rightPos + 1] = data[rightPos + 1]; outputData[rightPos + 2] = data[rightPos + 2]; outputData[rightPos + 3] = data[rightPos + 3]
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
// 创建新的ImageData对象
|
|
@@ -199,19 +202,12 @@ export class ImageProcessor {
|
|
|
199
202
|
* @returns 处理后的图像数据
|
|
200
203
|
*/
|
|
201
204
|
static threshold(imageData: ImageData, threshold: number = 128): ImageData {
|
|
202
|
-
//
|
|
203
|
-
const grayscaleImage = this.toGrayscale(
|
|
204
|
-
new ImageData(
|
|
205
|
-
new Uint8ClampedArray(imageData.data),
|
|
206
|
-
imageData.width,
|
|
207
|
-
imageData.height
|
|
208
|
-
)
|
|
209
|
-
)
|
|
205
|
+
// 先转换为灰度图(toGrayscale 内部已创建新 ImageData,无需外部拷贝)
|
|
206
|
+
const grayscaleImage = this.toGrayscale(imageData)
|
|
210
207
|
|
|
211
208
|
const data = grayscaleImage.data
|
|
212
|
-
const length = data.length
|
|
213
209
|
|
|
214
|
-
for (let i = 0; i < length; i += 4) {
|
|
210
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
215
211
|
// 二值化处理
|
|
216
212
|
const value = data[i] < threshold ? 0 : 255
|
|
217
213
|
data[i] = data[i + 1] = data[i + 2] = value
|
|
@@ -227,14 +223,8 @@ export class ImageProcessor {
|
|
|
227
223
|
* @returns 二值化后的图像数据
|
|
228
224
|
*/
|
|
229
225
|
static toBinaryImage(imageData: ImageData): ImageData {
|
|
230
|
-
//
|
|
231
|
-
const grayscaleImage = this.toGrayscale(
|
|
232
|
-
new ImageData(
|
|
233
|
-
new Uint8ClampedArray(imageData.data),
|
|
234
|
-
imageData.width,
|
|
235
|
-
imageData.height
|
|
236
|
-
)
|
|
237
|
-
)
|
|
226
|
+
// 先转换为灰度图(toGrayscale 内部已创建新 ImageData,无需外部拷贝)
|
|
227
|
+
const grayscaleImage = this.toGrayscale(imageData)
|
|
238
228
|
|
|
239
229
|
// 使用OTSU算法自动确定阈值
|
|
240
230
|
const threshold = this.getOtsuThreshold(grayscaleImage)
|
|
@@ -250,9 +240,10 @@ export class ImageProcessor {
|
|
|
250
240
|
*/
|
|
251
241
|
private static getOtsuThreshold(imageData: ImageData): number {
|
|
252
242
|
const data = imageData.data
|
|
253
|
-
|
|
243
|
+
// 使用 Uint8Array 替代 Array<number>,避免 boxing 开销,提升直方图统计性能
|
|
244
|
+
const histogram = new Uint32Array(256)
|
|
254
245
|
|
|
255
|
-
//
|
|
246
|
+
// 统计灰度直方图(每4字节取R通道,即灰度值)
|
|
256
247
|
for (let i = 0; i < data.length; i += 4) {
|
|
257
248
|
histogram[data[i]]++
|
|
258
249
|
}
|