id-scanner-lib 1.1.1 → 1.2.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.
@@ -7,6 +7,8 @@
7
7
  import { Camera } from '../utils/camera';
8
8
  import { ImageProcessor } from '../utils/image-processing';
9
9
  import { DetectionResult } from '../utils/types';
10
+ import { throttle, LRUCache, calculateImageFingerprint } from '../utils/performance';
11
+ import { Disposable } from '../utils/resource-manager';
10
12
 
11
13
  /**
12
14
  * IDCardDetector配置选项
@@ -15,6 +17,10 @@ export interface IDCardDetectorOptions {
15
17
  onDetection?: (result: DetectionResult) => void;
16
18
  onError?: (error: Error) => void;
17
19
  detectionInterval?: number;
20
+ maxImageDimension?: number;
21
+ enableCache?: boolean;
22
+ cacheSize?: number;
23
+ logger?: (message: any) => void;
18
24
  }
19
25
 
20
26
  /**
@@ -42,11 +48,19 @@ export interface IDCardDetectorOptions {
42
48
  * detector.stop();
43
49
  * ```
44
50
  */
45
- export class IDCardDetector {
51
+ export class IDCardDetector implements Disposable {
46
52
  private camera: Camera;
47
53
  private detecting = false;
48
54
  private detectTimer: number | null = null;
49
55
  private onDetected?: (result: DetectionResult) => void;
56
+ private onError?: (error: Error) => void;
57
+ private detectionInterval: number;
58
+ private maxImageDimension: number;
59
+ private resultCache: LRUCache<string, DetectionResult>;
60
+ private throttledDetect: ReturnType<typeof throttle>;
61
+ private frameCount: number = 0;
62
+ private lastDetectionTime: number = 0;
63
+ private options: IDCardDetectorOptions;
50
64
 
51
65
  /**
52
66
  * 创建身份证检测器实例
@@ -59,10 +73,43 @@ export class IDCardDetector {
59
73
  if (typeof options === 'function') {
60
74
  // 兼容旧的构造函数方式
61
75
  this.onDetected = options;
76
+ this.options = {
77
+ detectionInterval: 200,
78
+ maxImageDimension: 800,
79
+ enableCache: true,
80
+ cacheSize: 20,
81
+ logger: console.log
82
+ };
62
83
  } else if (options) {
63
84
  // 使用新的选项对象方式
85
+ this.options = {
86
+ detectionInterval: 200,
87
+ maxImageDimension: 800,
88
+ enableCache: true,
89
+ cacheSize: 20,
90
+ logger: console.log,
91
+ ...options
92
+ };
64
93
  this.onDetected = options.onDetection;
94
+ this.onError = options.onError;
95
+ } else {
96
+ this.options = {
97
+ detectionInterval: 200,
98
+ maxImageDimension: 800,
99
+ enableCache: true,
100
+ cacheSize: 20,
101
+ logger: console.log
102
+ };
65
103
  }
104
+
105
+ this.detectionInterval = this.options.detectionInterval!;
106
+ this.maxImageDimension = this.options.maxImageDimension!;
107
+
108
+ // 初始化结果缓存
109
+ this.resultCache = new LRUCache<string, DetectionResult>(this.options.cacheSize);
110
+
111
+ // 创建节流版本的检测函数
112
+ this.throttledDetect = throttle(this.performDetection.bind(this), this.detectionInterval);
66
113
  }
67
114
 
68
115
  /**
@@ -76,114 +123,212 @@ export class IDCardDetector {
76
123
  async start(videoElement: HTMLVideoElement): Promise<void> {
77
124
  await this.camera.initialize(videoElement);
78
125
  this.detecting = true;
126
+ this.frameCount = 0;
127
+ this.lastDetectionTime = 0;
79
128
  this.detect();
80
129
  }
81
130
 
82
131
  /**
83
- * 执行一次身份证检测
84
- *
85
- * 内部方法,捕获当前视频帧并尝试检测其中的身份证
132
+ * 停止身份证检测
133
+ */
134
+ stop(): void {
135
+ this.detecting = false;
136
+ if (this.detectTimer !== null) {
137
+ cancelAnimationFrame(this.detectTimer);
138
+ this.detectTimer = null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 持续检测视频帧
86
144
  *
87
145
  * @private
88
146
  */
89
- private async detect(): Promise<void> {
147
+ private detect(): void {
90
148
  if (!this.detecting) return;
91
149
 
92
- const imageData = this.camera.captureFrame();
93
-
94
- if (imageData) {
150
+ this.detectTimer = requestAnimationFrame(() => {
95
151
  try {
96
- // 简单实现,因为没有完整的OpenCV.js
97
- // 实际项目中应该使用OpenCV.js做更精确的边缘检测
98
- const result = await this.detectIDCard(imageData);
152
+ this.frameCount++;
153
+ const now = performance.now();
99
154
 
100
- if (this.onDetected) {
101
- this.onDetected(result);
155
+ // 帧率控制 - 只有满足时间间隔的帧才进行检测
156
+ // 这样可以显著减少CPU使用率,同时保持良好的用户体验
157
+ if (this.frameCount % 3 === 0 || now - this.lastDetectionTime >= this.detectionInterval) {
158
+ this.throttledDetect();
159
+ this.lastDetectionTime = now;
102
160
  }
161
+
162
+ // 继续下一帧检测
163
+ this.detect();
103
164
  } catch (error) {
104
- console.error('身份证检测错误:', error);
165
+ if (this.onError) {
166
+ this.onError(error as Error);
167
+ } else {
168
+ console.error('身份证检测错误:', error);
169
+ }
170
+
171
+ // 出错后延迟重试
172
+ setTimeout(() => {
173
+ if (this.detecting) {
174
+ this.detect();
175
+ }
176
+ }, 1000);
177
+ }
178
+ });
179
+ }
180
+
181
+ /**
182
+ * 执行单帧检测
183
+ *
184
+ * @private
185
+ */
186
+ private async performDetection(): Promise<void> {
187
+ if (!this.detecting || !this.camera) return;
188
+
189
+ // 获取当前视频帧
190
+ const frame = this.camera.captureFrame();
191
+ if (!frame) return;
192
+
193
+ // 检查缓存
194
+ if (this.options.enableCache) {
195
+ const fingerprint = calculateImageFingerprint(frame, 16); // 使用更大的尺寸提高特征区分度
196
+ const cachedResult = this.resultCache.get(fingerprint);
197
+
198
+ if (cachedResult) {
199
+ this.options.logger?.('使用缓存的检测结果');
200
+
201
+ // 使用缓存结果,但更新图像数据以确保最新
202
+ const updatedResult = {
203
+ ...cachedResult,
204
+ imageData: frame
205
+ };
206
+
207
+ if (this.onDetected) {
208
+ this.onDetected(updatedResult);
209
+ }
210
+ return;
105
211
  }
106
212
  }
107
213
 
108
- this.detectTimer = window.setTimeout(() => this.detect(), 200);
214
+ // 降低分辨率以提高性能
215
+ const downsampledFrame = ImageProcessor.downsampleForProcessing(frame, this.maxImageDimension);
216
+
217
+ try {
218
+ // 检测身份证
219
+ const result = await this.detectIDCard(downsampledFrame);
220
+
221
+ // 如果检测成功,将原始图像添加到结果中
222
+ if (result.success) {
223
+ result.imageData = frame;
224
+
225
+ // 缓存结果
226
+ if (this.options.enableCache) {
227
+ const fingerprint = calculateImageFingerprint(frame, 16);
228
+ this.resultCache.set(fingerprint, result);
229
+ }
230
+ }
231
+
232
+ // 处理检测结果
233
+ if (this.onDetected) {
234
+ this.onDetected(result);
235
+ }
236
+ } catch (error) {
237
+ if (this.onError) {
238
+ this.onError(error as Error);
239
+ } else {
240
+ console.error('身份证检测错误:', error);
241
+ }
242
+ }
109
243
  }
110
244
 
111
245
  /**
112
- * 身份证检测核心算法
113
- *
114
- * 通过图像处理技术检测和提取图像中的身份证区域
246
+ * 检测图像中的身份证
115
247
  *
116
248
  * @private
117
- * @param {ImageData} imageData - 需要检测身份证的图像数据
118
- * @returns {Promise<DetectionResult>} 检测结果,包含成功标志和裁剪后的身份证图像
249
+ * @param {ImageData} imageData - 要分析的图像数据
250
+ * @returns {Promise<DetectionResult>} 检测结果
119
251
  */
120
252
  private async detectIDCard(imageData: ImageData): Promise<DetectionResult> {
121
- // 图像预处理
253
+ // 1. 图像预处理
122
254
  const grayscale = ImageProcessor.toGrayscale(imageData);
123
- const enhanced = ImageProcessor.adjustBrightnessContrast(grayscale, 10, 30);
124
255
 
125
- // 简化的身份证检测算法
126
- // 在真实项目中,这里应该使用OpenCV.js进行轮廓检测和矩形检测
256
+ // 2. 检测矩形和边缘(简化版实现)
257
+ // 注意:实际应用中应使用OpenCV.js或其他计算机视觉库进行更精确的检测
258
+ // 此处仅作为概念性实现,使用基本矩形检测逻辑
127
259
 
128
- // 模拟检测过程
129
- const success = Math.random() > 0.7; // 模拟70%的概率检测成功
260
+ // 模拟检测过程,随机判断是否找到身份证
261
+ // 在实际应用中,此处应当实现实际的计算机视觉算法
262
+ const detectionResult: DetectionResult = {
263
+ success: Math.random() > 0.3, // 70%的概率成功检测到
264
+ message: '身份证检测完成'
265
+ };
130
266
 
131
- if (success) {
132
- // 模拟一个身份证区域,实际项目中应该是根据检测结果
133
- const cardWidth = Math.floor(imageData.width * 0.8);
134
- const cardHeight = Math.floor(cardWidth * 0.63); // 身份证比例大约是8:5
135
- const x = Math.floor((imageData.width - cardWidth) / 2);
136
- const y = Math.floor((imageData.height - cardHeight) / 2);
267
+ if (detectionResult.success) {
268
+ // 模拟一个身份证矩形区域
269
+ const width = imageData.width;
270
+ const height = imageData.height;
137
271
 
138
- // 模拟四个角点
139
- const corners = [
140
- { x, y }, // 左上
141
- { x: x + cardWidth, y }, // 右上
142
- { x: x + cardWidth, y: y + cardHeight }, // 右下
143
- { x, y: y + cardHeight } // 左下
272
+ // 大致的身份证区域(按比例)
273
+ const rectWidth = Math.round(width * 0.7);
274
+ const rectHeight = Math.round(rectWidth * 0.618); // 身份证是黄金比例
275
+ const rectX = Math.round((width - rectWidth) / 2);
276
+ const rectY = Math.round((height - rectHeight) / 2);
277
+
278
+ // 添加四个角点
279
+ detectionResult.corners = [
280
+ { x: rectX, y: rectY },
281
+ { x: rectX + rectWidth, y: rectY },
282
+ { x: rectX + rectWidth, y: rectY + rectHeight },
283
+ { x: rectX, y: rectY + rectHeight }
144
284
  ];
145
285
 
146
- // 模拟裁剪图像(实际项目中应该做透视变换)
286
+ // 添加边界框
287
+ detectionResult.boundingBox = {
288
+ x: rectX,
289
+ y: rectY,
290
+ width: rectWidth,
291
+ height: rectHeight
292
+ };
293
+
294
+ // 裁剪身份证图像
147
295
  const canvas = document.createElement('canvas');
148
- canvas.width = cardWidth;
149
- canvas.height = cardHeight;
296
+ canvas.width = rectWidth;
297
+ canvas.height = rectHeight;
150
298
  const ctx = canvas.getContext('2d');
151
299
 
152
300
  if (ctx) {
153
- // 从原图中裁剪身份证区域
154
- const sourceCanvas = ImageProcessor.imageDataToCanvas(imageData);
301
+ const tempCanvas = ImageProcessor.imageDataToCanvas(imageData);
155
302
  ctx.drawImage(
156
- sourceCanvas,
157
- x, y, cardWidth, cardHeight,
158
- 0, 0, cardWidth, cardHeight
303
+ tempCanvas,
304
+ rectX, rectY, rectWidth, rectHeight,
305
+ 0, 0, rectWidth, rectHeight
159
306
  );
160
307
 
161
- const croppedImage = ctx.getImageData(0, 0, cardWidth, cardHeight);
162
-
163
- return {
164
- success: true,
165
- corners,
166
- croppedImage
167
- };
308
+ detectionResult.croppedImage = ctx.getImageData(0, 0, rectWidth, rectHeight);
168
309
  }
310
+
311
+ // 设置置信度
312
+ detectionResult.confidence = 0.7 + Math.random() * 0.3;
169
313
  }
170
314
 
171
- return { success: false };
315
+ return detectionResult;
172
316
  }
173
317
 
174
318
  /**
175
- * 停止身份证检测
176
- *
177
- * 停止检测循环并释放相机资源
319
+ * 清除检测结果缓存
178
320
  */
179
- stop(): void {
180
- this.detecting = false;
181
-
182
- if (this.detectTimer) {
183
- clearTimeout(this.detectTimer);
184
- this.detectTimer = null;
185
- }
186
-
321
+ clearCache(): void {
322
+ this.resultCache.clear();
323
+ this.options.logger?.('检测结果缓存已清除');
324
+ }
325
+
326
+ /**
327
+ * 释放资源
328
+ */
329
+ dispose(): void {
330
+ this.stop();
187
331
  this.camera.release();
332
+ this.resultCache.clear();
188
333
  }
189
334
  }
@@ -7,6 +7,26 @@
7
7
  import { createWorker } from 'tesseract.js';
8
8
  import { IDCardInfo } from '../utils/types';
9
9
  import { ImageProcessor } from '../utils/image-processing';
10
+ import { LRUCache, calculateImageFingerprint } from '../utils/performance';
11
+ import { isWorkerSupported, createWorker as createCustomWorker } from '../utils/worker';
12
+ import { processOCRInWorker, OCRProcessInput } from './ocr-worker';
13
+ import { Disposable } from '../utils/resource-manager';
14
+
15
+ /**
16
+ * OCR处理器选项接口
17
+ */
18
+ export interface OCRProcessorOptions {
19
+ /** 是否使用Worker线程 */
20
+ useWorker?: boolean;
21
+ /** 是否启用结果缓存 */
22
+ enableCache?: boolean;
23
+ /** 缓存容量 */
24
+ cacheSize?: number;
25
+ /** 图像处理前的最大尺寸 */
26
+ maxImageDimension?: number;
27
+ /** 日志回调函数 */
28
+ logger?: (message: any) => void;
29
+ }
10
30
 
11
31
  /**
12
32
  * OCR处理器类
@@ -29,10 +49,31 @@ import { ImageProcessor } from '../utils/image-processing';
29
49
  * await ocrProcessor.terminate();
30
50
  * ```
31
51
  */
32
- export class OCRProcessor {
52
+ export class OCRProcessor implements Disposable {
33
53
  private worker: any = null;
54
+ private ocrWorker: ReturnType<typeof createCustomWorker<OCRProcessInput, { idCardInfo: IDCardInfo, processingTime: number }>> | null = null;
55
+ private initialized: boolean = false;
56
+ private resultCache: LRUCache<string, IDCardInfo>;
57
+ private options: OCRProcessorOptions;
34
58
 
35
- constructor() {}
59
+ /**
60
+ * 创建OCR处理器实例
61
+ *
62
+ * @param options OCR处理器选项
63
+ */
64
+ constructor(options: OCRProcessorOptions = {}) {
65
+ this.options = {
66
+ useWorker: isWorkerSupported(),
67
+ enableCache: true,
68
+ cacheSize: 50,
69
+ maxImageDimension: 1000,
70
+ logger: console.log,
71
+ ...options
72
+ };
73
+
74
+ // 初始化缓存
75
+ this.resultCache = new LRUCache<string, IDCardInfo>(this.options.cacheSize);
76
+ }
36
77
 
37
78
  /**
38
79
  * 初始化OCR引擎
@@ -42,16 +83,29 @@ export class OCRProcessor {
42
83
  * @returns {Promise<void>} 初始化完成的Promise
43
84
  */
44
85
  async initialize(): Promise<void> {
45
- this.worker = createWorker({
46
- logger: (m: any) => console.log(m)
47
- } as any);
48
-
49
- await this.worker.load();
50
- await this.worker.loadLanguage('chi_sim');
51
- await this.worker.initialize('chi_sim');
52
- await this.worker.setParameters({
53
- tessedit_char_whitelist: '0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期'
54
- });
86
+ if (this.initialized) return;
87
+
88
+ if (this.options.useWorker) {
89
+ // 使用自定义Worker线程处理OCR
90
+ this.ocrWorker = createCustomWorker<OCRProcessInput, { idCardInfo: IDCardInfo, processingTime: number }>(processOCRInWorker);
91
+ this.initialized = true;
92
+ this.options.logger?.('OCR Worker 初始化完成');
93
+ } else {
94
+ // 使用主线程处理OCR
95
+ this.worker = createWorker({
96
+ logger: this.options.logger
97
+ } as any);
98
+
99
+ await this.worker.load();
100
+ await this.worker.loadLanguage('chi_sim');
101
+ await this.worker.initialize('chi_sim');
102
+ await this.worker.setParameters({
103
+ tessedit_char_whitelist: '0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期'
104
+ });
105
+
106
+ this.initialized = true;
107
+ this.options.logger?.('OCR引擎初始化完成');
108
+ }
55
109
  }
56
110
 
57
111
  /**
@@ -60,24 +114,68 @@ export class OCRProcessor {
60
114
  * @returns 提取的身份证信息
61
115
  */
62
116
  async processIDCard(imageData: ImageData): Promise<IDCardInfo> {
63
- if (!this.worker) {
117
+ if (!this.initialized) {
64
118
  await this.initialize();
65
119
  }
66
120
 
67
- // 图像预处理,提高OCR识别率
68
- const enhancedImage = ImageProcessor.adjustBrightnessContrast(imageData, 15, 25);
121
+ // 计算图像指纹,用于缓存查找
122
+ if (this.options.enableCache) {
123
+ const fingerprint = calculateImageFingerprint(imageData);
124
+
125
+ // 检查缓存中是否有结果
126
+ const cachedResult = this.resultCache.get(fingerprint);
127
+ if (cachedResult) {
128
+ this.options.logger?.('使用缓存的OCR结果');
129
+ return cachedResult;
130
+ }
131
+ }
69
132
 
70
- // 转换ImageData为Canvas
71
- const canvas = ImageProcessor.imageDataToCanvas(enhancedImage);
133
+ // 图像预处理:降低分辨率和增强对比度
134
+ const downsampledImage = ImageProcessor.downsampleForProcessing(imageData, this.options.maxImageDimension);
135
+ const enhancedImage = ImageProcessor.adjustBrightnessContrast(downsampledImage, 15, 25);
72
136
 
73
137
  // OCR识别
74
138
  try {
75
- const { data } = await this.worker.recognize(canvas);
139
+ let idCardInfo: IDCardInfo;
76
140
 
77
- // 解析身份证信息
78
- return this.parseIDCardText(data.text);
141
+ if (this.options.useWorker && this.ocrWorker) {
142
+ // 使用Worker线程处理
143
+ const base64Image = ImageProcessor.imageDataToBase64(enhancedImage);
144
+
145
+ const result = await this.ocrWorker.postMessage({
146
+ imageBase64: base64Image,
147
+ tessWorkerOptions: {
148
+ logger: this.options.logger
149
+ }
150
+ });
151
+
152
+ idCardInfo = result.idCardInfo;
153
+ this.options.logger?.(`OCR处理完成,用时: ${result.processingTime.toFixed(2)}ms`);
154
+ } else {
155
+ // 使用主线程处理
156
+ const startTime = performance.now();
157
+
158
+ // 转换ImageData为Canvas
159
+ const canvas = ImageProcessor.imageDataToCanvas(enhancedImage);
160
+
161
+ const { data } = await this.worker.recognize(canvas);
162
+
163
+ // 解析身份证信息
164
+ idCardInfo = this.parseIDCardText(data.text);
165
+
166
+ const processingTime = performance.now() - startTime;
167
+ this.options.logger?.(`OCR处理完成,用时: ${processingTime.toFixed(2)}ms`);
168
+ }
169
+
170
+ // 缓存结果
171
+ if (this.options.enableCache) {
172
+ const fingerprint = calculateImageFingerprint(imageData);
173
+ this.resultCache.set(fingerprint, idCardInfo);
174
+ }
175
+
176
+ return idCardInfo;
79
177
  } catch (error) {
80
- console.error('OCR识别错误:', error);
178
+ this.options.logger?.(`OCR识别错误: ${error}`);
81
179
  return {} as IDCardInfo;
82
180
  }
83
181
  }
@@ -152,6 +250,14 @@ export class OCRProcessor {
152
250
  return info;
153
251
  }
154
252
 
253
+ /**
254
+ * 清除结果缓存
255
+ */
256
+ clearCache(): void {
257
+ this.resultCache.clear();
258
+ this.options.logger?.('OCR结果缓存已清除');
259
+ }
260
+
155
261
  /**
156
262
  * 终止OCR引擎并释放资源
157
263
  *
@@ -162,5 +268,20 @@ export class OCRProcessor {
162
268
  await this.worker.terminate();
163
269
  this.worker = null;
164
270
  }
271
+
272
+ if (this.ocrWorker) {
273
+ this.ocrWorker.terminate();
274
+ this.ocrWorker = null;
275
+ }
276
+
277
+ this.initialized = false;
278
+ this.options.logger?.('OCR引擎已终止');
279
+ }
280
+
281
+ /**
282
+ * 释放资源
283
+ */
284
+ dispose(): Promise<void> {
285
+ return this.terminate();
165
286
  }
166
287
  }