id-scanner-lib 1.0.0 → 1.1.1
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/README.md +96 -0
- package/dist/core.d.ts +77 -0
- package/dist/id-recognition/data-extractor.d.ts +31 -0
- package/dist/id-recognition/id-detector.d.ts +25 -1
- package/dist/id-scanner-core.esm.js +10710 -0
- package/dist/id-scanner-core.esm.js.map +1 -0
- package/dist/id-scanner-core.js +10722 -0
- package/dist/id-scanner-core.js.map +1 -0
- package/dist/id-scanner-core.min.js +9 -0
- package/dist/id-scanner-core.min.js.map +1 -0
- package/dist/id-scanner-ocr.esm.js +914 -0
- package/dist/id-scanner-ocr.esm.js.map +1 -0
- package/dist/id-scanner-ocr.js +923 -0
- package/dist/id-scanner-ocr.js.map +1 -0
- package/dist/id-scanner-ocr.min.js +9 -0
- package/dist/id-scanner-ocr.min.js.map +1 -0
- package/dist/id-scanner-qr.esm.js +613 -0
- package/dist/id-scanner-qr.esm.js.map +1 -0
- package/dist/id-scanner-qr.js +622 -0
- package/dist/id-scanner-qr.js.map +1 -0
- package/dist/id-scanner-qr.min.js +9 -0
- package/dist/id-scanner-qr.min.js.map +1 -0
- package/dist/id-scanner.js +1243 -94656
- package/dist/id-scanner.js.map +1 -1
- package/dist/id-scanner.min.js +7 -7
- package/dist/id-scanner.min.js.map +1 -1
- package/dist/index-umd.d.ts +96 -0
- package/dist/index.d.ts +23 -88
- package/dist/ocr-module.d.ts +67 -0
- package/dist/qr-module.d.ts +68 -0
- package/dist/types/core.d.ts +77 -0
- package/dist/types/demo/demo.d.ts +14 -0
- package/dist/types/id-recognition/data-extractor.d.ts +105 -0
- package/dist/types/id-recognition/id-detector.d.ts +100 -0
- package/dist/types/id-recognition/ocr-processor.d.ts +64 -0
- package/dist/types/index-umd.d.ts +96 -0
- package/dist/types/index.d.ts +78 -0
- package/dist/types/ocr-module.d.ts +67 -0
- package/dist/types/qr-module.d.ts +68 -0
- package/dist/types/scanner/barcode-scanner.d.ts +90 -0
- package/dist/types/scanner/qr-scanner.d.ts +80 -0
- package/dist/types/utils/camera.d.ts +81 -0
- package/dist/types/utils/image-processing.d.ts +75 -0
- package/dist/types/utils/types.d.ts +65 -0
- package/dist/utils/camera.d.ts +18 -13
- package/dist/utils/types.d.ts +6 -6
- package/package.json +25 -4
- package/src/core.ts +138 -0
- package/src/id-recognition/data-extractor.ts +97 -0
- package/src/id-recognition/id-detector.ts +20 -2
- package/src/id-recognition/ocr-processor.ts +3 -6
- package/src/index-umd.ts +240 -0
- package/src/index.ts +125 -139
- package/src/ocr-module.ts +139 -0
- package/src/qr-module.ts +129 -0
- package/src/utils/camera.ts +61 -36
- package/src/utils/types.ts +23 -6
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
import { createWorker } from 'tesseract.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file 相机工具类
|
|
5
|
+
* @description 提供访问和控制设备摄像头的功能
|
|
6
|
+
* @module Camera
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 相机工具类
|
|
10
|
+
*
|
|
11
|
+
* 提供访问设备摄像头、获取视频流以及捕获图像帧的功能
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // 创建相机实例
|
|
16
|
+
* const camera = new Camera({
|
|
17
|
+
* width: 1280,
|
|
18
|
+
* height: 720,
|
|
19
|
+
* facingMode: 'environment' // 使用后置摄像头
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // 初始化相机
|
|
23
|
+
* const videoElement = document.getElementById('video') as HTMLVideoElement;
|
|
24
|
+
* await camera.start(videoElement);
|
|
25
|
+
*
|
|
26
|
+
* // 捕获当前视频帧
|
|
27
|
+
* const imageData = camera.captureFrame();
|
|
28
|
+
*
|
|
29
|
+
* // 使用结束后释放资源
|
|
30
|
+
* camera.stop();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
class Camera {
|
|
34
|
+
/**
|
|
35
|
+
* 创建相机实例
|
|
36
|
+
* @param {CameraOptions} [options] - 相机配置选项
|
|
37
|
+
*/
|
|
38
|
+
constructor(options = {}) {
|
|
39
|
+
this.options = options;
|
|
40
|
+
this.stream = null;
|
|
41
|
+
this.videoElement = null;
|
|
42
|
+
this.options = {
|
|
43
|
+
width: 640,
|
|
44
|
+
height: 480,
|
|
45
|
+
facingMode: 'environment',
|
|
46
|
+
...options
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 启动摄像头并将视频流绑定到视频元素
|
|
51
|
+
* @param videoElement HTML视频元素
|
|
52
|
+
* @returns Promise<void>
|
|
53
|
+
*/
|
|
54
|
+
async start(videoElement) {
|
|
55
|
+
return this.initialize(videoElement);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 停止摄像头并释放资源
|
|
59
|
+
*/
|
|
60
|
+
stop() {
|
|
61
|
+
this.release();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 初始化相机,获取视频流并绑定到视频元素
|
|
65
|
+
*
|
|
66
|
+
* @param {HTMLVideoElement} videoElement - 用于显示视频流的视频元素
|
|
67
|
+
* @returns {Promise<void>} 初始化完成的Promise
|
|
68
|
+
* @throws 如果无法访问摄像头,将抛出错误
|
|
69
|
+
*/
|
|
70
|
+
async initialize(videoElement) {
|
|
71
|
+
this.videoElement = videoElement;
|
|
72
|
+
try {
|
|
73
|
+
// 构建媒体约束
|
|
74
|
+
const constraints = {
|
|
75
|
+
video: {
|
|
76
|
+
width: { ideal: this.options.width },
|
|
77
|
+
height: { ideal: this.options.height },
|
|
78
|
+
facingMode: this.options.facingMode
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
// 获取视频流
|
|
82
|
+
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
83
|
+
// 绑定到视频元素
|
|
84
|
+
if (this.videoElement) {
|
|
85
|
+
this.videoElement.srcObject = this.stream;
|
|
86
|
+
await new Promise((resolve) => {
|
|
87
|
+
if (this.videoElement) {
|
|
88
|
+
this.videoElement.onloadedmetadata = () => {
|
|
89
|
+
if (this.videoElement) {
|
|
90
|
+
this.videoElement.play().then(() => resolve());
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error('无法访问摄像头:', error);
|
|
99
|
+
throw new Error('无法访问摄像头。请确保已授予摄像头访问权限,并且摄像头未被其他应用程序占用。');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 捕获当前视频帧
|
|
104
|
+
*
|
|
105
|
+
* @returns {ImageData|null} 视频帧的ImageData对象,如果未初始化则返回null
|
|
106
|
+
*/
|
|
107
|
+
captureFrame() {
|
|
108
|
+
if (!this.videoElement) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// 创建Canvas元素用于捕获视频帧
|
|
112
|
+
const canvas = document.createElement('canvas');
|
|
113
|
+
canvas.width = this.videoElement.videoWidth;
|
|
114
|
+
canvas.height = this.videoElement.videoHeight;
|
|
115
|
+
const context = canvas.getContext('2d');
|
|
116
|
+
if (!context) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
// 将视频内容绘制到Canvas中
|
|
120
|
+
context.drawImage(this.videoElement, 0, 0, canvas.width, canvas.height);
|
|
121
|
+
// 获取ImageData对象
|
|
122
|
+
return context.getImageData(0, 0, canvas.width, canvas.height);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 释放摄像头资源
|
|
126
|
+
*/
|
|
127
|
+
release() {
|
|
128
|
+
// 停止视频流的所有轨道
|
|
129
|
+
if (this.stream) {
|
|
130
|
+
this.stream.getTracks().forEach(track => track.stop());
|
|
131
|
+
this.stream = null;
|
|
132
|
+
}
|
|
133
|
+
// 清除视频元素绑定
|
|
134
|
+
if (this.videoElement) {
|
|
135
|
+
this.videoElement.srcObject = null;
|
|
136
|
+
this.videoElement = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @file 图像处理工具类
|
|
143
|
+
* @description 提供图像处理相关的辅助功能
|
|
144
|
+
* @module ImageProcessor
|
|
145
|
+
*/
|
|
146
|
+
/**
|
|
147
|
+
* 图像处理工具类
|
|
148
|
+
*
|
|
149
|
+
* 提供常用的图像处理功能,如亮度和对比度调整、灰度转换、图像大小调整等。
|
|
150
|
+
* 这些功能可用于增强图像质量,提高OCR和扫描的识别率。
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* // 使用图像处理功能增强图像
|
|
155
|
+
* const enhancedImage = ImageProcessor.adjustBrightnessContrast(
|
|
156
|
+
* originalImageData,
|
|
157
|
+
* 15, // 增加亮度
|
|
158
|
+
* 25 // 增加对比度
|
|
159
|
+
* );
|
|
160
|
+
*
|
|
161
|
+
* // 转换为灰度图像
|
|
162
|
+
* const grayImage = ImageProcessor.toGrayscale(originalImageData);
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
class ImageProcessor {
|
|
166
|
+
/**
|
|
167
|
+
* 将ImageData转换为Canvas元素
|
|
168
|
+
*
|
|
169
|
+
* @param {ImageData} imageData - 要转换的图像数据
|
|
170
|
+
* @returns {HTMLCanvasElement} 包含图像的Canvas元素
|
|
171
|
+
*/
|
|
172
|
+
static imageDataToCanvas(imageData) {
|
|
173
|
+
const canvas = document.createElement('canvas');
|
|
174
|
+
canvas.width = imageData.width;
|
|
175
|
+
canvas.height = imageData.height;
|
|
176
|
+
const ctx = canvas.getContext('2d');
|
|
177
|
+
if (ctx) {
|
|
178
|
+
ctx.putImageData(imageData, 0, 0);
|
|
179
|
+
}
|
|
180
|
+
return canvas;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 将Canvas转换为ImageData
|
|
184
|
+
*
|
|
185
|
+
* @param {HTMLCanvasElement} canvas - 要转换的Canvas元素
|
|
186
|
+
* @returns {ImageData|null} Canvas的图像数据,如果获取失败则返回null
|
|
187
|
+
*/
|
|
188
|
+
static canvasToImageData(canvas) {
|
|
189
|
+
const ctx = canvas.getContext('2d');
|
|
190
|
+
return ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 调整图像亮度和对比度
|
|
194
|
+
*
|
|
195
|
+
* @param {ImageData} imageData - 要处理的图像数据
|
|
196
|
+
* @param {number} [brightness=0] - 亮度调整值,正值增加亮度,负值降低亮度,范围建议为-100到100
|
|
197
|
+
* @param {number} [contrast=0] - 对比度调整值,正值增加对比度,负值降低对比度,范围建议为-100到100
|
|
198
|
+
* @returns {ImageData} 处理后的图像数据
|
|
199
|
+
*/
|
|
200
|
+
static adjustBrightnessContrast(imageData, brightness = 0, contrast = 0) {
|
|
201
|
+
const canvas = this.imageDataToCanvas(imageData);
|
|
202
|
+
const ctx = canvas.getContext('2d');
|
|
203
|
+
if (ctx) {
|
|
204
|
+
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
205
|
+
const data = imgData.data;
|
|
206
|
+
// 调整对比度算法
|
|
207
|
+
const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
|
|
208
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
209
|
+
// 红色
|
|
210
|
+
data[i] = this.truncate(factor * (data[i] - 128) + 128 + brightness);
|
|
211
|
+
// 绿色
|
|
212
|
+
data[i + 1] = this.truncate(factor * (data[i + 1] - 128) + 128 + brightness);
|
|
213
|
+
// 蓝色
|
|
214
|
+
data[i + 2] = this.truncate(factor * (data[i + 2] - 128) + 128 + brightness);
|
|
215
|
+
// Alpha不变
|
|
216
|
+
}
|
|
217
|
+
ctx.putImageData(imgData, 0, 0);
|
|
218
|
+
return imgData;
|
|
219
|
+
}
|
|
220
|
+
return imageData;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 确保值在0-255范围内
|
|
224
|
+
*
|
|
225
|
+
* @private
|
|
226
|
+
* @param {number} value - 要截断的值
|
|
227
|
+
* @returns {number} 截断后的值,范围为0-255
|
|
228
|
+
*/
|
|
229
|
+
static truncate(value) {
|
|
230
|
+
return Math.min(255, Math.max(0, value));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 将彩色图像转换为灰度图像
|
|
234
|
+
*
|
|
235
|
+
* 灰度转换可以简化图像,提高OCR和条形码识别的准确率
|
|
236
|
+
*
|
|
237
|
+
* @param {ImageData} imageData - 要转换的彩色图像
|
|
238
|
+
* @returns {ImageData} 转换后的灰度图像
|
|
239
|
+
*/
|
|
240
|
+
static toGrayscale(imageData) {
|
|
241
|
+
const canvas = this.imageDataToCanvas(imageData);
|
|
242
|
+
const ctx = canvas.getContext('2d');
|
|
243
|
+
if (ctx) {
|
|
244
|
+
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
245
|
+
const data = imgData.data;
|
|
246
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
247
|
+
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
|
248
|
+
data[i] = avg; // 红色
|
|
249
|
+
data[i + 1] = avg; // 绿色
|
|
250
|
+
data[i + 2] = avg; // 蓝色
|
|
251
|
+
// Alpha不变
|
|
252
|
+
}
|
|
253
|
+
ctx.putImageData(imgData, 0, 0);
|
|
254
|
+
return imgData;
|
|
255
|
+
}
|
|
256
|
+
return imageData;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 调整图像大小
|
|
260
|
+
*
|
|
261
|
+
* @param {ImageData} imageData - 原图像数据
|
|
262
|
+
* @param {number} newWidth - 新宽度
|
|
263
|
+
* @param {number} newHeight - 新高度
|
|
264
|
+
* @returns {ImageData} 调整大小后的图像数据
|
|
265
|
+
*/
|
|
266
|
+
static resize(imageData, newWidth, newHeight) {
|
|
267
|
+
const canvas = document.createElement('canvas');
|
|
268
|
+
canvas.width = newWidth;
|
|
269
|
+
canvas.height = newHeight;
|
|
270
|
+
const ctx = canvas.getContext('2d');
|
|
271
|
+
if (ctx) {
|
|
272
|
+
const tempCanvas = this.imageDataToCanvas(imageData);
|
|
273
|
+
ctx.drawImage(tempCanvas, 0, 0, imageData.width, imageData.height, 0, 0, newWidth, newHeight);
|
|
274
|
+
return ctx.getImageData(0, 0, newWidth, newHeight);
|
|
275
|
+
}
|
|
276
|
+
return imageData;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @file 身份证检测模块
|
|
282
|
+
* @description 提供自动检测和定位图像中的身份证功能
|
|
283
|
+
* @module IDCardDetector
|
|
284
|
+
*/
|
|
285
|
+
/**
|
|
286
|
+
* 身份证检测器类
|
|
287
|
+
*
|
|
288
|
+
* 通过图像处理和计算机视觉技术,实时检测视频流中的身份证,并提取身份证区域
|
|
289
|
+
* 注意:当前实现是简化版,实际项目中建议使用OpenCV.js进行更精确的检测
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* // 创建身份证检测器
|
|
294
|
+
* const detector = new IDCardDetector((result) => {
|
|
295
|
+
* if (result.success && result.croppedImage) {
|
|
296
|
+
* console.log('检测到身份证!');
|
|
297
|
+
* // 对裁剪出的身份证图像进行处理
|
|
298
|
+
* processIDCardImage(result.croppedImage);
|
|
299
|
+
* }
|
|
300
|
+
* });
|
|
301
|
+
*
|
|
302
|
+
* // 启动检测
|
|
303
|
+
* const videoElement = document.getElementById('video') as HTMLVideoElement;
|
|
304
|
+
* await detector.start(videoElement);
|
|
305
|
+
*
|
|
306
|
+
* // 停止检测
|
|
307
|
+
* detector.stop();
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
class IDCardDetector {
|
|
311
|
+
/**
|
|
312
|
+
* 创建身份证检测器实例
|
|
313
|
+
*
|
|
314
|
+
* @param options 身份证检测器配置选项,或者检测回调函数
|
|
315
|
+
*/
|
|
316
|
+
constructor(options) {
|
|
317
|
+
this.detecting = false;
|
|
318
|
+
this.detectTimer = null;
|
|
319
|
+
this.camera = new Camera();
|
|
320
|
+
if (typeof options === 'function') {
|
|
321
|
+
// 兼容旧的构造函数方式
|
|
322
|
+
this.onDetected = options;
|
|
323
|
+
}
|
|
324
|
+
else if (options) {
|
|
325
|
+
// 使用新的选项对象方式
|
|
326
|
+
this.onDetected = options.onDetection;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 启动身份证检测
|
|
331
|
+
*
|
|
332
|
+
* 初始化相机并开始连续检测视频帧中的身份证
|
|
333
|
+
*
|
|
334
|
+
* @param {HTMLVideoElement} videoElement - 用于显示相机画面的video元素
|
|
335
|
+
* @returns {Promise<void>} 启动完成的Promise
|
|
336
|
+
*/
|
|
337
|
+
async start(videoElement) {
|
|
338
|
+
await this.camera.initialize(videoElement);
|
|
339
|
+
this.detecting = true;
|
|
340
|
+
this.detect();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* 执行一次身份证检测
|
|
344
|
+
*
|
|
345
|
+
* 内部方法,捕获当前视频帧并尝试检测其中的身份证
|
|
346
|
+
*
|
|
347
|
+
* @private
|
|
348
|
+
*/
|
|
349
|
+
async detect() {
|
|
350
|
+
if (!this.detecting)
|
|
351
|
+
return;
|
|
352
|
+
const imageData = this.camera.captureFrame();
|
|
353
|
+
if (imageData) {
|
|
354
|
+
try {
|
|
355
|
+
// 简单实现,因为没有完整的OpenCV.js
|
|
356
|
+
// 实际项目中应该使用OpenCV.js做更精确的边缘检测
|
|
357
|
+
const result = await this.detectIDCard(imageData);
|
|
358
|
+
if (this.onDetected) {
|
|
359
|
+
this.onDetected(result);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
console.error('身份证检测错误:', error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this.detectTimer = window.setTimeout(() => this.detect(), 200);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 身份证检测核心算法
|
|
370
|
+
*
|
|
371
|
+
* 通过图像处理技术检测和提取图像中的身份证区域
|
|
372
|
+
*
|
|
373
|
+
* @private
|
|
374
|
+
* @param {ImageData} imageData - 需要检测身份证的图像数据
|
|
375
|
+
* @returns {Promise<DetectionResult>} 检测结果,包含成功标志和裁剪后的身份证图像
|
|
376
|
+
*/
|
|
377
|
+
async detectIDCard(imageData) {
|
|
378
|
+
// 图像预处理
|
|
379
|
+
const grayscale = ImageProcessor.toGrayscale(imageData);
|
|
380
|
+
ImageProcessor.adjustBrightnessContrast(grayscale, 10, 30);
|
|
381
|
+
// 简化的身份证检测算法
|
|
382
|
+
// 在真实项目中,这里应该使用OpenCV.js进行轮廓检测和矩形检测
|
|
383
|
+
// 模拟检测过程
|
|
384
|
+
const success = Math.random() > 0.7; // 模拟70%的概率检测成功
|
|
385
|
+
if (success) {
|
|
386
|
+
// 模拟一个身份证区域,实际项目中应该是根据检测结果
|
|
387
|
+
const cardWidth = Math.floor(imageData.width * 0.8);
|
|
388
|
+
const cardHeight = Math.floor(cardWidth * 0.63); // 身份证比例大约是8:5
|
|
389
|
+
const x = Math.floor((imageData.width - cardWidth) / 2);
|
|
390
|
+
const y = Math.floor((imageData.height - cardHeight) / 2);
|
|
391
|
+
// 模拟四个角点
|
|
392
|
+
const corners = [
|
|
393
|
+
{ x, y }, // 左上
|
|
394
|
+
{ x: x + cardWidth, y }, // 右上
|
|
395
|
+
{ x: x + cardWidth, y: y + cardHeight }, // 右下
|
|
396
|
+
{ x, y: y + cardHeight } // 左下
|
|
397
|
+
];
|
|
398
|
+
// 模拟裁剪图像(实际项目中应该做透视变换)
|
|
399
|
+
const canvas = document.createElement('canvas');
|
|
400
|
+
canvas.width = cardWidth;
|
|
401
|
+
canvas.height = cardHeight;
|
|
402
|
+
const ctx = canvas.getContext('2d');
|
|
403
|
+
if (ctx) {
|
|
404
|
+
// 从原图中裁剪身份证区域
|
|
405
|
+
const sourceCanvas = ImageProcessor.imageDataToCanvas(imageData);
|
|
406
|
+
ctx.drawImage(sourceCanvas, x, y, cardWidth, cardHeight, 0, 0, cardWidth, cardHeight);
|
|
407
|
+
const croppedImage = ctx.getImageData(0, 0, cardWidth, cardHeight);
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
corners,
|
|
411
|
+
croppedImage
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return { success: false };
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* 停止身份证检测
|
|
419
|
+
*
|
|
420
|
+
* 停止检测循环并释放相机资源
|
|
421
|
+
*/
|
|
422
|
+
stop() {
|
|
423
|
+
this.detecting = false;
|
|
424
|
+
if (this.detectTimer) {
|
|
425
|
+
clearTimeout(this.detectTimer);
|
|
426
|
+
this.detectTimer = null;
|
|
427
|
+
}
|
|
428
|
+
this.camera.release();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* @file OCR处理模块
|
|
434
|
+
* @description 提供身份证文字识别和信息提取功能
|
|
435
|
+
* @module OCRProcessor
|
|
436
|
+
*/
|
|
437
|
+
/**
|
|
438
|
+
* OCR处理器类
|
|
439
|
+
*
|
|
440
|
+
* 使用Tesseract.js实现对身份证图像的OCR文字识别和信息提取功能
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```typescript
|
|
444
|
+
* // 创建OCR处理器
|
|
445
|
+
* const ocrProcessor = new OCRProcessor();
|
|
446
|
+
*
|
|
447
|
+
* // 初始化OCR引擎
|
|
448
|
+
* await ocrProcessor.initialize();
|
|
449
|
+
*
|
|
450
|
+
* // 处理身份证图像
|
|
451
|
+
* const idInfo = await ocrProcessor.processIDCard(idCardImageData);
|
|
452
|
+
* console.log('识别到的身份证信息:', idInfo);
|
|
453
|
+
*
|
|
454
|
+
* // 使用结束后释放资源
|
|
455
|
+
* await ocrProcessor.terminate();
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
458
|
+
class OCRProcessor {
|
|
459
|
+
constructor() {
|
|
460
|
+
this.worker = null;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* 初始化OCR引擎
|
|
464
|
+
*
|
|
465
|
+
* 加载Tesseract OCR引擎和中文简体语言包,并设置适合身份证识别的参数
|
|
466
|
+
*
|
|
467
|
+
* @returns {Promise<void>} 初始化完成的Promise
|
|
468
|
+
*/
|
|
469
|
+
async initialize() {
|
|
470
|
+
this.worker = createWorker({
|
|
471
|
+
logger: (m) => console.log(m)
|
|
472
|
+
});
|
|
473
|
+
await this.worker.load();
|
|
474
|
+
await this.worker.loadLanguage('chi_sim');
|
|
475
|
+
await this.worker.initialize('chi_sim');
|
|
476
|
+
await this.worker.setParameters({
|
|
477
|
+
tessedit_char_whitelist: '0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期'
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* 处理身份证图像并提取信息
|
|
482
|
+
* @param imageData 要处理的身份证图像数据
|
|
483
|
+
* @returns 提取的身份证信息
|
|
484
|
+
*/
|
|
485
|
+
async processIDCard(imageData) {
|
|
486
|
+
if (!this.worker) {
|
|
487
|
+
await this.initialize();
|
|
488
|
+
}
|
|
489
|
+
// 图像预处理,提高OCR识别率
|
|
490
|
+
const enhancedImage = ImageProcessor.adjustBrightnessContrast(imageData, 15, 25);
|
|
491
|
+
// 转换ImageData为Canvas
|
|
492
|
+
const canvas = ImageProcessor.imageDataToCanvas(enhancedImage);
|
|
493
|
+
// OCR识别
|
|
494
|
+
try {
|
|
495
|
+
const { data } = await this.worker.recognize(canvas);
|
|
496
|
+
// 解析身份证信息
|
|
497
|
+
return this.parseIDCardText(data.text);
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
console.error('OCR识别错误:', error);
|
|
501
|
+
return {};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* 解析身份证文本信息
|
|
506
|
+
*
|
|
507
|
+
* 从OCR识别到的文本中提取结构化的身份证信息
|
|
508
|
+
*
|
|
509
|
+
* @private
|
|
510
|
+
* @param {string} text - OCR识别到的文本
|
|
511
|
+
* @returns {IDCardInfo} 提取到的身份证信息对象
|
|
512
|
+
*/
|
|
513
|
+
parseIDCardText(text) {
|
|
514
|
+
const info = {};
|
|
515
|
+
// 拆分为行
|
|
516
|
+
const lines = text.split('\n').filter(line => line.trim());
|
|
517
|
+
// 解析身份证号码(最容易识别的部分)
|
|
518
|
+
const idNumberRegex = /(\d{17}[\dX])/;
|
|
519
|
+
const idNumberMatch = text.match(idNumberRegex);
|
|
520
|
+
if (idNumberMatch) {
|
|
521
|
+
info.idNumber = idNumberMatch[1];
|
|
522
|
+
}
|
|
523
|
+
// 解析姓名
|
|
524
|
+
for (const line of lines) {
|
|
525
|
+
if (line.includes('姓名') || line.length < 10 && line.length > 1 && !/\d/.test(line)) {
|
|
526
|
+
info.name = line.replace('姓名', '').trim();
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// 解析性别和民族
|
|
531
|
+
const genderNationalityRegex = /(男|女).*(族)/;
|
|
532
|
+
const genderMatch = text.match(genderNationalityRegex);
|
|
533
|
+
if (genderMatch) {
|
|
534
|
+
info.gender = genderMatch[1];
|
|
535
|
+
const nationalityText = genderMatch[0];
|
|
536
|
+
info.nationality = nationalityText.substring(nationalityText.indexOf(genderMatch[1]) + 1).trim();
|
|
537
|
+
}
|
|
538
|
+
// 解析出生日期
|
|
539
|
+
const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/;
|
|
540
|
+
const birthDateMatch = text.match(birthDateRegex);
|
|
541
|
+
if (birthDateMatch) {
|
|
542
|
+
info.birthDate = `${birthDateMatch[1]}-${birthDateMatch[2]}-${birthDateMatch[3]}`;
|
|
543
|
+
}
|
|
544
|
+
// 解析地址
|
|
545
|
+
const addressRegex = /住址([\s\S]*?)公民身份号码/;
|
|
546
|
+
const addressMatch = text.match(addressRegex);
|
|
547
|
+
if (addressMatch) {
|
|
548
|
+
info.address = addressMatch[1].replace(/\n/g, '').trim();
|
|
549
|
+
}
|
|
550
|
+
// 解析签发机关
|
|
551
|
+
const authorityRegex = /签发机关([\s\S]*?)有效期/;
|
|
552
|
+
const authorityMatch = text.match(authorityRegex);
|
|
553
|
+
if (authorityMatch) {
|
|
554
|
+
info.issuingAuthority = authorityMatch[1].replace(/\n/g, '').trim();
|
|
555
|
+
}
|
|
556
|
+
// 解析有效期限
|
|
557
|
+
const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/;
|
|
558
|
+
const validPeriodMatch = text.match(validPeriodRegex);
|
|
559
|
+
if (validPeriodMatch) {
|
|
560
|
+
info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim();
|
|
561
|
+
}
|
|
562
|
+
return info;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* 终止OCR引擎并释放资源
|
|
566
|
+
*
|
|
567
|
+
* @returns {Promise<void>} 终止完成的Promise
|
|
568
|
+
*/
|
|
569
|
+
async terminate() {
|
|
570
|
+
if (this.worker) {
|
|
571
|
+
await this.worker.terminate();
|
|
572
|
+
this.worker = null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* @file 数据提取工具类
|
|
579
|
+
* @description 提供身份证信息的验证和格式化功能
|
|
580
|
+
* @module DataExtractor
|
|
581
|
+
*/
|
|
582
|
+
/**
|
|
583
|
+
* 数据提取工具类
|
|
584
|
+
*
|
|
585
|
+
* 提供身份证信息的验证、提取和增强功能,可以从身份证号码中提取出生日期、性别等信息,
|
|
586
|
+
* 并对OCR识别结果进行补充和验证
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* ```typescript
|
|
590
|
+
* // 验证身份证号码
|
|
591
|
+
* const isValid = DataExtractor.validateIDNumber('110101199001011234');
|
|
592
|
+
*
|
|
593
|
+
* // 从身份证号码提取出生日期
|
|
594
|
+
* const birthDate = DataExtractor.extractBirthDateFromID('110101199001011234');
|
|
595
|
+
* // 结果: '1990-01-01'
|
|
596
|
+
*
|
|
597
|
+
* // 增强OCR识别结果
|
|
598
|
+
* const enhancedInfo = DataExtractor.enhanceIDCardInfo({
|
|
599
|
+
* name: '张三',
|
|
600
|
+
* idNumber: '110101199001011234'
|
|
601
|
+
* });
|
|
602
|
+
* // 结果会自动补充性别和出生日期
|
|
603
|
+
* ```
|
|
604
|
+
*/
|
|
605
|
+
class DataExtractor {
|
|
606
|
+
/**
|
|
607
|
+
* 验证身份证号码格式
|
|
608
|
+
*
|
|
609
|
+
* 检查身份证号码的长度、格式和出生日期部分是否有效
|
|
610
|
+
*
|
|
611
|
+
* @param {string} idNumber - 要验证的身份证号码
|
|
612
|
+
* @returns {boolean} 是否是有效的身份证号码
|
|
613
|
+
*/
|
|
614
|
+
static validateIDNumber(idNumber) {
|
|
615
|
+
// 简单校验长度
|
|
616
|
+
if (!idNumber || idNumber.length !== 18) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
// 校验格式 (前17位必须是数字,最后一位可以是数字或X)
|
|
620
|
+
const pattern = /^\d{17}[\dX]$/;
|
|
621
|
+
if (!pattern.test(idNumber)) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
// 校验出生日期
|
|
625
|
+
const year = parseInt(idNumber.substr(6, 4));
|
|
626
|
+
const month = parseInt(idNumber.substr(10, 2));
|
|
627
|
+
const day = parseInt(idNumber.substr(12, 2));
|
|
628
|
+
const date = new Date(year, month - 1, day);
|
|
629
|
+
if (date.getFullYear() !== year ||
|
|
630
|
+
date.getMonth() + 1 !== month ||
|
|
631
|
+
date.getDate() !== day) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
// 简单的校验规则,实际项目中可以加入更完善的验证
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* 从身份证号码提取出生日期
|
|
639
|
+
*
|
|
640
|
+
* @param {string} idNumber - 身份证号码
|
|
641
|
+
* @returns {string|null} 格式化的出生日期(YYYY-MM-DD),如果身份证号码无效则返回null
|
|
642
|
+
*/
|
|
643
|
+
static extractBirthDateFromID(idNumber) {
|
|
644
|
+
if (!this.validateIDNumber(idNumber)) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
const year = idNumber.substr(6, 4);
|
|
648
|
+
const month = idNumber.substr(10, 2);
|
|
649
|
+
const day = idNumber.substr(12, 2);
|
|
650
|
+
return `${year}-${month}-${day}`;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* 从身份证号码提取性别
|
|
654
|
+
*
|
|
655
|
+
* 根据身份证号码第17位判断性别,奇数为男,偶数为女
|
|
656
|
+
*
|
|
657
|
+
* @param {string} idNumber - 身份证号码
|
|
658
|
+
* @returns {string|null} '男'或'女',如果身份证号码无效则返回null
|
|
659
|
+
*/
|
|
660
|
+
static extractGenderFromID(idNumber) {
|
|
661
|
+
if (!this.validateIDNumber(idNumber)) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
// 第17位,奇数为男,偶数为女
|
|
665
|
+
const genderCode = parseInt(idNumber.charAt(16));
|
|
666
|
+
return genderCode % 2 === 1 ? '男' : '女';
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* 从身份证号码提取地区编码
|
|
670
|
+
*
|
|
671
|
+
* @param {string} idNumber - 身份证号码
|
|
672
|
+
* @returns {string|null} 地区编码(前6位),如果身份证号码无效则返回null
|
|
673
|
+
*/
|
|
674
|
+
static extractRegionFromID(idNumber) {
|
|
675
|
+
if (!this.validateIDNumber(idNumber)) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
return idNumber.substr(0, 6);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* 合并并优化身份证信息
|
|
682
|
+
*
|
|
683
|
+
* 使用多个来源的数据进行交叉验证和补充,如果OCR识别结果缺少某些信息,
|
|
684
|
+
* 但有身份证号码,则可以从号码中提取出生日期和性别等信息
|
|
685
|
+
*
|
|
686
|
+
* @param {IDCardInfo} ocrInfo - OCR识别到的身份证信息
|
|
687
|
+
* @param {string} [idNumber] - 可选的外部提供的身份证号码,优先级高于OCR识别结果
|
|
688
|
+
* @returns {IDCardInfo} 增强后的身份证信息
|
|
689
|
+
*/
|
|
690
|
+
static enhanceIDCardInfo(ocrInfo, idNumber) {
|
|
691
|
+
const result = { ...ocrInfo };
|
|
692
|
+
// 如果OCR识别出身份证号,但没有识别出生日期或性别,则从身份证号码提取
|
|
693
|
+
if (result.idNumber && this.validateIDNumber(result.idNumber)) {
|
|
694
|
+
// 从身份证号提取出生日期
|
|
695
|
+
if (!result.birthDate) {
|
|
696
|
+
result.birthDate = this.extractBirthDateFromID(result.idNumber) || undefined;
|
|
697
|
+
}
|
|
698
|
+
// 从身份证号提取性别
|
|
699
|
+
if (!result.gender) {
|
|
700
|
+
result.gender = this.extractGenderFromID(result.idNumber) || undefined;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// 如果外部传入了身份证号,则优先使用它并提取信息
|
|
704
|
+
if (idNumber && this.validateIDNumber(idNumber)) {
|
|
705
|
+
result.idNumber = idNumber;
|
|
706
|
+
// 使用身份证号码再次验证或补充信息
|
|
707
|
+
const birthDate = this.extractBirthDateFromID(idNumber);
|
|
708
|
+
if (birthDate) {
|
|
709
|
+
result.birthDate = birthDate;
|
|
710
|
+
}
|
|
711
|
+
const gender = this.extractGenderFromID(idNumber);
|
|
712
|
+
if (gender) {
|
|
713
|
+
result.gender = gender;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return result;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* 提取并验证身份证信息
|
|
720
|
+
*
|
|
721
|
+
* @param idCardInfo 初步提取的身份证信息
|
|
722
|
+
* @returns 验证和增强后的身份证信息
|
|
723
|
+
*/
|
|
724
|
+
extractAndValidate(idCardInfo) {
|
|
725
|
+
const enhancedInfo = { ...idCardInfo };
|
|
726
|
+
// 验证和规范化身份证号
|
|
727
|
+
if (enhancedInfo.idNumber) {
|
|
728
|
+
enhancedInfo.idNumber = this.normalizeIDNumber(enhancedInfo.idNumber);
|
|
729
|
+
// 如果身份证号有效,推断出生日期
|
|
730
|
+
if (this.validateIDNumber(enhancedInfo.idNumber)) {
|
|
731
|
+
if (!enhancedInfo.birthDate) {
|
|
732
|
+
enhancedInfo.birthDate = this.extractBirthDateFromID(enhancedInfo.idNumber);
|
|
733
|
+
}
|
|
734
|
+
// 推断性别
|
|
735
|
+
if (!enhancedInfo.gender) {
|
|
736
|
+
enhancedInfo.gender = this.extractGenderFromID(enhancedInfo.idNumber);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// 规范化日期格式
|
|
741
|
+
if (enhancedInfo.birthDate) {
|
|
742
|
+
enhancedInfo.birthDate = this.normalizeDate(enhancedInfo.birthDate);
|
|
743
|
+
}
|
|
744
|
+
// 规范化地址信息
|
|
745
|
+
if (enhancedInfo.address) {
|
|
746
|
+
enhancedInfo.address = this.normalizeAddress(enhancedInfo.address);
|
|
747
|
+
}
|
|
748
|
+
return enhancedInfo;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* 规范化身份证号码
|
|
752
|
+
*/
|
|
753
|
+
normalizeIDNumber(idNumber) {
|
|
754
|
+
// 移除空格和特殊字符
|
|
755
|
+
return idNumber.replace(/[\s\-]/g, '').toUpperCase();
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* 验证身份证号码是否有效
|
|
759
|
+
*/
|
|
760
|
+
validateIDNumber(idNumber) {
|
|
761
|
+
// 简单验证身份证号码长度和格式
|
|
762
|
+
const idRegex = /(^\d{15}$)|(^\d{17}([0-9]|X)$)/;
|
|
763
|
+
return idRegex.test(idNumber);
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* 从身份证号中提取出生日期
|
|
767
|
+
*/
|
|
768
|
+
extractBirthDateFromID(idNumber) {
|
|
769
|
+
if (idNumber.length === 18) {
|
|
770
|
+
return `${idNumber.substring(6, 10)}-${idNumber.substring(10, 12)}-${idNumber.substring(12, 14)}`;
|
|
771
|
+
}
|
|
772
|
+
else if (idNumber.length === 15) {
|
|
773
|
+
return `19${idNumber.substring(6, 8)}-${idNumber.substring(8, 10)}-${idNumber.substring(10, 12)}`;
|
|
774
|
+
}
|
|
775
|
+
return '';
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* 从身份证号中提取性别信息
|
|
779
|
+
*/
|
|
780
|
+
extractGenderFromID(idNumber) {
|
|
781
|
+
let sexCode;
|
|
782
|
+
if (idNumber.length === 18) {
|
|
783
|
+
sexCode = parseInt(idNumber.charAt(16));
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
sexCode = parseInt(idNumber.charAt(14));
|
|
787
|
+
}
|
|
788
|
+
return sexCode % 2 === 1 ? '男' : '女';
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* 规范化日期格式
|
|
792
|
+
*/
|
|
793
|
+
normalizeDate(date) {
|
|
794
|
+
// 简单的日期格式化逻辑
|
|
795
|
+
return date.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* 规范化地址信息
|
|
799
|
+
*/
|
|
800
|
+
normalizeAddress(address) {
|
|
801
|
+
// 地址格式化逻辑
|
|
802
|
+
return address.trim();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* @file OCR模块入口文件
|
|
808
|
+
* @description 包含身份证OCR识别相关功能
|
|
809
|
+
* @module IDScannerOCR
|
|
810
|
+
* @version 1.0.0
|
|
811
|
+
* @license MIT
|
|
812
|
+
*/
|
|
813
|
+
/**
|
|
814
|
+
* OCR模块类
|
|
815
|
+
*
|
|
816
|
+
* 提供身份证检测和OCR文字识别功能
|
|
817
|
+
*/
|
|
818
|
+
class OCRModule {
|
|
819
|
+
/**
|
|
820
|
+
* 构造函数
|
|
821
|
+
* @param options 配置选项
|
|
822
|
+
*/
|
|
823
|
+
constructor(options = {}) {
|
|
824
|
+
this.options = options;
|
|
825
|
+
this.isRunning = false;
|
|
826
|
+
this.videoElement = null;
|
|
827
|
+
this.camera = new Camera(options.cameraOptions);
|
|
828
|
+
this.idDetector = new IDCardDetector({
|
|
829
|
+
onDetection: this.handleIDDetection.bind(this),
|
|
830
|
+
onError: this.handleError.bind(this)
|
|
831
|
+
});
|
|
832
|
+
this.ocrProcessor = new OCRProcessor();
|
|
833
|
+
this.dataExtractor = new DataExtractor();
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* 初始化OCR引擎
|
|
837
|
+
*
|
|
838
|
+
* @returns Promise<void>
|
|
839
|
+
*/
|
|
840
|
+
async initialize() {
|
|
841
|
+
try {
|
|
842
|
+
await this.ocrProcessor.initialize();
|
|
843
|
+
console.log('OCR engine initialized');
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
846
|
+
this.handleError(error);
|
|
847
|
+
throw error;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* 启动身份证扫描
|
|
852
|
+
* @param videoElement HTML视频元素
|
|
853
|
+
*/
|
|
854
|
+
async startIDCardScanner(videoElement) {
|
|
855
|
+
if (!this.ocrProcessor) {
|
|
856
|
+
throw new Error('OCR engine not initialized. Call initialize() first.');
|
|
857
|
+
}
|
|
858
|
+
this.videoElement = videoElement;
|
|
859
|
+
this.isRunning = true;
|
|
860
|
+
await this.camera.start(videoElement);
|
|
861
|
+
this.idDetector.start(videoElement);
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* 停止扫描
|
|
865
|
+
*/
|
|
866
|
+
stop() {
|
|
867
|
+
this.isRunning = false;
|
|
868
|
+
this.idDetector.stop();
|
|
869
|
+
this.camera.stop();
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* 处理身份证检测结果
|
|
873
|
+
*/
|
|
874
|
+
async handleIDDetection(result) {
|
|
875
|
+
if (!this.isRunning)
|
|
876
|
+
return;
|
|
877
|
+
try {
|
|
878
|
+
// 检查 imageData 是否存在
|
|
879
|
+
if (!result.imageData) {
|
|
880
|
+
this.handleError(new Error('无效的图像数据'));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const idCardInfo = await this.ocrProcessor.processIDCard(result.imageData);
|
|
884
|
+
const extractedInfo = this.dataExtractor.extractAndValidate(idCardInfo);
|
|
885
|
+
if (this.options.onIDCardScanned) {
|
|
886
|
+
this.options.onIDCardScanned(extractedInfo);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
this.handleError(error);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* 处理错误
|
|
895
|
+
*/
|
|
896
|
+
handleError(error) {
|
|
897
|
+
if (this.options.onError) {
|
|
898
|
+
this.options.onError(error);
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
console.error('OCRModule error:', error);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* 释放资源
|
|
906
|
+
*/
|
|
907
|
+
async terminate() {
|
|
908
|
+
this.stop();
|
|
909
|
+
await this.ocrProcessor.terminate();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export { DataExtractor, IDCardDetector, OCRModule, OCRProcessor };
|
|
914
|
+
//# sourceMappingURL=id-scanner-ocr.esm.js.map
|