id-scanner-lib 1.3.3 → 1.5.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/README.md +55 -460
- package/dist/id-scanner-lib.esm.js +4641 -0
- package/dist/id-scanner-lib.esm.js.map +1 -0
- package/dist/id-scanner-lib.js +14755 -0
- package/dist/id-scanner-lib.js.map +1 -0
- package/dist/types/core/base-module.d.ts +44 -0
- package/dist/types/core/camera-manager.d.ts +258 -0
- package/dist/types/core/config.d.ts +88 -0
- package/dist/types/core/errors.d.ts +111 -0
- package/dist/types/core/event-emitter.d.ts +55 -0
- package/dist/types/core/logger.d.ts +277 -0
- package/dist/types/core/module-manager.d.ts +78 -0
- package/dist/types/core/plugin-manager.d.ts +158 -0
- package/dist/types/core/resource-manager.d.ts +246 -0
- package/dist/types/core/result.d.ts +83 -0
- package/dist/types/core/scanner-factory.d.ts +93 -0
- package/dist/types/index.bundle.d.ts +1303 -0
- package/dist/types/index.d.ts +86 -0
- package/dist/types/interfaces/external-types.d.ts +174 -0
- package/dist/types/interfaces/face-detection.d.ts +293 -0
- package/dist/types/interfaces/scanner-module.d.ts +280 -0
- package/dist/types/modules/face/face-detector.d.ts +170 -0
- package/dist/types/modules/face/index.d.ts +56 -0
- package/dist/types/modules/face/liveness-detector.d.ts +177 -0
- package/dist/types/modules/face/types.d.ts +136 -0
- package/dist/types/modules/id-card/anti-fake-detector.d.ts +170 -0
- package/dist/types/modules/id-card/id-card-detector.d.ts +131 -0
- package/dist/types/modules/id-card/index.d.ts +89 -0
- package/dist/types/modules/id-card/ocr-processor.d.ts +110 -0
- package/dist/types/modules/id-card/ocr-worker.d.ts +31 -0
- package/dist/types/modules/id-card/types.d.ts +181 -0
- package/dist/types/modules/qrcode/index.d.ts +51 -0
- package/dist/types/modules/qrcode/qr-code-scanner.d.ts +64 -0
- package/dist/types/modules/qrcode/types.d.ts +67 -0
- package/dist/types/utils/camera.d.ts +81 -0
- package/dist/types/utils/image-processing.d.ts +176 -0
- package/dist/types/utils/index.d.ts +175 -0
- package/dist/types/utils/performance.d.ts +81 -0
- package/dist/types/utils/resource-manager.d.ts +53 -0
- package/dist/types/utils/types.d.ts +166 -0
- package/dist/types/utils/worker.d.ts +52 -0
- package/dist/types/version.d.ts +7 -0
- package/package.json +76 -75
- package/src/core/base-module.ts +78 -0
- package/src/core/camera-manager.ts +798 -0
- package/src/core/config.ts +268 -0
- package/src/core/errors.ts +174 -0
- package/src/core/event-emitter.ts +110 -0
- package/src/core/logger.ts +549 -0
- package/src/core/module-manager.ts +165 -0
- package/src/core/plugin-manager.ts +429 -0
- package/src/core/resource-manager.ts +762 -0
- package/src/core/result.ts +163 -0
- package/src/core/scanner-factory.ts +237 -0
- package/src/index.ts +113 -936
- package/src/interfaces/external-types.ts +200 -0
- package/src/interfaces/face-detection.ts +309 -0
- package/src/interfaces/scanner-module.ts +384 -0
- package/src/modules/face/face-detector.ts +931 -0
- package/src/modules/face/index.ts +208 -0
- package/src/modules/face/liveness-detector.ts +908 -0
- package/src/modules/face/types.ts +133 -0
- package/src/{id-recognition → modules/id-card}/anti-fake-detector.ts +273 -239
- package/src/modules/id-card/id-card-detector.ts +474 -0
- package/src/modules/id-card/index.ts +425 -0
- package/src/{id-recognition → modules/id-card}/ocr-processor.ts +149 -92
- package/src/modules/id-card/ocr-worker.ts +259 -0
- package/src/modules/id-card/types.ts +178 -0
- package/src/modules/qrcode/index.ts +175 -0
- package/src/modules/qrcode/qr-code-scanner.ts +230 -0
- package/src/modules/qrcode/types.ts +65 -0
- package/src/types/tesseract.d.ts +265 -22
- package/src/utils/image-processing.ts +68 -49
- package/src/utils/index.ts +426 -0
- package/src/utils/performance.ts +168 -131
- package/src/utils/resource-manager.ts +65 -146
- package/src/utils/types.ts +90 -2
- package/src/utils/worker.ts +123 -84
- package/src/version.ts +11 -0
- package/tools/scaffold.js +543 -0
- package/dist/id-scanner-core.esm.js +0 -11349
- package/dist/id-scanner-core.js +0 -11361
- package/dist/id-scanner-core.min.js +0 -1
- package/dist/id-scanner-ocr.esm.js +0 -2319
- package/dist/id-scanner-ocr.js +0 -2328
- package/dist/id-scanner-ocr.min.js +0 -1
- package/dist/id-scanner-qr.esm.js +0 -1296
- package/dist/id-scanner-qr.js +0 -1305
- package/dist/id-scanner-qr.min.js +0 -1
- package/dist/id-scanner.js +0 -4561
- package/dist/id-scanner.min.js +0 -1
- package/src/core.ts +0 -138
- package/src/demo/demo.ts +0 -204
- package/src/id-recognition/data-extractor.ts +0 -262
- package/src/id-recognition/id-detector.ts +0 -510
- package/src/id-recognition/ocr-worker.ts +0 -156
- package/src/index-umd.ts +0 -477
- package/src/ocr-module.ts +0 -187
- package/src/qr-module.ts +0 -179
- package/src/scanner/barcode-scanner.ts +0 -251
- package/src/scanner/qr-scanner.ts +0 -167
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file 身份证检测模块
|
|
3
|
-
* @description 提供自动检测和定位图像中的身份证功能
|
|
4
|
-
* @module IDCardDetector
|
|
5
|
-
* @version 1.3.2
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { Camera } from "../utils/camera"
|
|
9
|
-
import { ImageProcessor } from "../utils/image-processing"
|
|
10
|
-
import { DetectionResult } from "../utils/types"
|
|
11
|
-
import {
|
|
12
|
-
throttle,
|
|
13
|
-
LRUCache,
|
|
14
|
-
calculateImageFingerprint,
|
|
15
|
-
} from "../utils/performance"
|
|
16
|
-
import { Disposable } from "../utils/resource-manager"
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* IDCardDetector配置选项
|
|
20
|
-
*/
|
|
21
|
-
export interface IDCardDetectorOptions {
|
|
22
|
-
onDetection?: (result: DetectionResult) => void
|
|
23
|
-
onError?: (error: Error) => void
|
|
24
|
-
detectionInterval?: number
|
|
25
|
-
maxImageDimension?: number
|
|
26
|
-
enableCache?: boolean
|
|
27
|
-
cacheSize?: number
|
|
28
|
-
logger?: (message: any) => void
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 身份证检测器类
|
|
33
|
-
*
|
|
34
|
-
* 通过图像处理和计算机视觉技术,实时检测视频流中的身份证,并提取身份证区域
|
|
35
|
-
* 注意:当前实现是简化版,实际项目中建议使用OpenCV.js进行更精确的检测
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```typescript
|
|
39
|
-
* // 创建身份证检测器
|
|
40
|
-
* const detector = new IDCardDetector((result) => {
|
|
41
|
-
* if (result.success && result.croppedImage) {
|
|
42
|
-
* console.log('检测到身份证!');
|
|
43
|
-
* // 对裁剪出的身份证图像进行处理
|
|
44
|
-
* processIDCardImage(result.croppedImage);
|
|
45
|
-
* }
|
|
46
|
-
* });
|
|
47
|
-
*
|
|
48
|
-
* // 启动检测
|
|
49
|
-
* const videoElement = document.getElementById('video') as HTMLVideoElement;
|
|
50
|
-
* await detector.start(videoElement);
|
|
51
|
-
*
|
|
52
|
-
* // 停止检测
|
|
53
|
-
* detector.stop();
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
|
-
export class IDCardDetector implements Disposable {
|
|
57
|
-
// 身份证标准宽高比(近似黄金比例)
|
|
58
|
-
private static readonly ID_CARD_ASPECT_RATIO = 1.58 // 标准身份证宽高比
|
|
59
|
-
private camera: Camera
|
|
60
|
-
private detecting = false
|
|
61
|
-
private detectTimer: number | null = null
|
|
62
|
-
private onDetected?: (result: DetectionResult) => void
|
|
63
|
-
private onError?: (error: Error) => void
|
|
64
|
-
private detectionInterval: number
|
|
65
|
-
private maxImageDimension: number
|
|
66
|
-
private resultCache: LRUCache<string, DetectionResult>
|
|
67
|
-
private throttledDetect: ReturnType<typeof throttle>
|
|
68
|
-
private frameCount: number = 0
|
|
69
|
-
private lastDetectionTime: number = 0
|
|
70
|
-
private options: IDCardDetectorOptions
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 创建身份证检测器实例
|
|
74
|
-
*
|
|
75
|
-
* @param options 身份证检测器配置选项,或者检测回调函数
|
|
76
|
-
*/
|
|
77
|
-
constructor(
|
|
78
|
-
options?: IDCardDetectorOptions | ((result: DetectionResult) => void)
|
|
79
|
-
) {
|
|
80
|
-
this.camera = new Camera()
|
|
81
|
-
|
|
82
|
-
if (typeof options === "function") {
|
|
83
|
-
// 兼容旧的构造函数方式
|
|
84
|
-
this.onDetected = options
|
|
85
|
-
this.options = {
|
|
86
|
-
detectionInterval: 200,
|
|
87
|
-
maxImageDimension: 800,
|
|
88
|
-
enableCache: true,
|
|
89
|
-
cacheSize: 20,
|
|
90
|
-
logger: console.log,
|
|
91
|
-
}
|
|
92
|
-
} else if (options) {
|
|
93
|
-
// 使用新的选项对象方式
|
|
94
|
-
this.options = {
|
|
95
|
-
detectionInterval: 200,
|
|
96
|
-
maxImageDimension: 800,
|
|
97
|
-
enableCache: true,
|
|
98
|
-
cacheSize: 20,
|
|
99
|
-
logger: console.log,
|
|
100
|
-
...options,
|
|
101
|
-
}
|
|
102
|
-
this.onDetected = options.onDetection
|
|
103
|
-
this.onError = options.onError
|
|
104
|
-
} else {
|
|
105
|
-
this.options = {
|
|
106
|
-
detectionInterval: 200,
|
|
107
|
-
maxImageDimension: 800,
|
|
108
|
-
enableCache: true,
|
|
109
|
-
cacheSize: 20,
|
|
110
|
-
logger: console.log,
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
this.detectionInterval = this.options.detectionInterval!
|
|
115
|
-
this.maxImageDimension = this.options.maxImageDimension!
|
|
116
|
-
|
|
117
|
-
// 初始化结果缓存
|
|
118
|
-
this.resultCache = new LRUCache<string, DetectionResult>(
|
|
119
|
-
this.options.cacheSize
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
// 创建节流版本的检测函数
|
|
123
|
-
this.throttledDetect = throttle(
|
|
124
|
-
this.performDetection.bind(this),
|
|
125
|
-
this.detectionInterval
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* 启动身份证检测
|
|
131
|
-
*
|
|
132
|
-
* 初始化相机并开始连续检测视频帧中的身份证
|
|
133
|
-
*
|
|
134
|
-
* @param {HTMLVideoElement} videoElement - 用于显示相机画面的video元素
|
|
135
|
-
* @returns {Promise<void>} 启动完成的Promise
|
|
136
|
-
*/
|
|
137
|
-
async start(videoElement: HTMLVideoElement): Promise<void> {
|
|
138
|
-
await this.camera.initialize(videoElement)
|
|
139
|
-
this.detecting = true
|
|
140
|
-
this.frameCount = 0
|
|
141
|
-
this.lastDetectionTime = 0
|
|
142
|
-
this.detect()
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* 停止身份证检测
|
|
147
|
-
*/
|
|
148
|
-
stop(): void {
|
|
149
|
-
this.detecting = false
|
|
150
|
-
if (this.detectTimer !== null) {
|
|
151
|
-
cancelAnimationFrame(this.detectTimer)
|
|
152
|
-
this.detectTimer = null
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* 持续检测视频帧
|
|
158
|
-
*
|
|
159
|
-
* @private
|
|
160
|
-
*/
|
|
161
|
-
private detect(): void {
|
|
162
|
-
if (!this.detecting) return
|
|
163
|
-
|
|
164
|
-
this.detectTimer = requestAnimationFrame(() => {
|
|
165
|
-
try {
|
|
166
|
-
this.frameCount++
|
|
167
|
-
const now = performance.now()
|
|
168
|
-
|
|
169
|
-
// 帧率控制 - 只有满足时间间隔的帧才进行检测
|
|
170
|
-
// 这样可以显著减少CPU使用率,同时保持良好的用户体验
|
|
171
|
-
if (
|
|
172
|
-
this.frameCount % 3 === 0 ||
|
|
173
|
-
now - this.lastDetectionTime >= this.detectionInterval
|
|
174
|
-
) {
|
|
175
|
-
this.throttledDetect()
|
|
176
|
-
this.lastDetectionTime = now
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 继续下一帧检测
|
|
180
|
-
this.detect()
|
|
181
|
-
} catch (error) {
|
|
182
|
-
if (this.onError) {
|
|
183
|
-
this.onError(error as Error)
|
|
184
|
-
} else {
|
|
185
|
-
console.error("身份证检测错误:", error)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 出错后延迟重试
|
|
189
|
-
setTimeout(() => {
|
|
190
|
-
if (this.detecting) {
|
|
191
|
-
this.detect()
|
|
192
|
-
}
|
|
193
|
-
}, 1000)
|
|
194
|
-
}
|
|
195
|
-
})
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* 执行单帧检测
|
|
200
|
-
*
|
|
201
|
-
* @private
|
|
202
|
-
*/
|
|
203
|
-
private async performDetection(): Promise<void> {
|
|
204
|
-
if (!this.detecting || !this.camera) return
|
|
205
|
-
|
|
206
|
-
// 获取当前视频帧
|
|
207
|
-
const frame = this.camera.captureFrame()
|
|
208
|
-
if (!frame) return
|
|
209
|
-
|
|
210
|
-
// 检查缓存
|
|
211
|
-
if (this.options.enableCache) {
|
|
212
|
-
const fingerprint = calculateImageFingerprint(frame, 16) // 使用更大的尺寸提高特征区分度
|
|
213
|
-
const cachedResult = this.resultCache.get(fingerprint)
|
|
214
|
-
|
|
215
|
-
if (cachedResult) {
|
|
216
|
-
this.options.logger?.("使用缓存的检测结果")
|
|
217
|
-
|
|
218
|
-
// 使用缓存结果,但更新图像数据以确保最新
|
|
219
|
-
const updatedResult = {
|
|
220
|
-
...cachedResult,
|
|
221
|
-
imageData: frame,
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (this.onDetected) {
|
|
225
|
-
this.onDetected(updatedResult)
|
|
226
|
-
}
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// 降低分辨率以提高性能
|
|
232
|
-
const downsampledFrame = ImageProcessor.resizeImage(
|
|
233
|
-
frame,
|
|
234
|
-
this.maxImageDimension,
|
|
235
|
-
this.maxImageDimension
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
// 检测身份证
|
|
240
|
-
const result = await this.detectIDCard(downsampledFrame)
|
|
241
|
-
|
|
242
|
-
// 如果检测成功,将原始图像添加到结果中
|
|
243
|
-
if (result.success) {
|
|
244
|
-
result.imageData = frame
|
|
245
|
-
|
|
246
|
-
// 缓存结果
|
|
247
|
-
if (this.options.enableCache) {
|
|
248
|
-
const fingerprint = calculateImageFingerprint(frame, 16)
|
|
249
|
-
this.resultCache.set(fingerprint, result)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// 处理检测结果
|
|
254
|
-
if (this.onDetected) {
|
|
255
|
-
this.onDetected(result)
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
if (this.onError) {
|
|
259
|
-
this.onError(error as Error)
|
|
260
|
-
} else {
|
|
261
|
-
console.error("身份证检测错误:", error)
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* 检测图像中的身份证
|
|
268
|
-
*
|
|
269
|
-
* @private
|
|
270
|
-
* @param {ImageData} imageData - 要分析的图像数据
|
|
271
|
-
* @returns {Promise<DetectionResult>} 检测结果
|
|
272
|
-
*/
|
|
273
|
-
private async detectIDCard(imageData: ImageData): Promise<DetectionResult> {
|
|
274
|
-
// 1. 图像预处理
|
|
275
|
-
const grayscale = ImageProcessor.toGrayscale(imageData)
|
|
276
|
-
|
|
277
|
-
// 2. 使用Sobel边缘检测算法检测边缘
|
|
278
|
-
const edgeData = ImageProcessor.detectEdges(grayscale)
|
|
279
|
-
|
|
280
|
-
// 3. 检测矩形和边缘
|
|
281
|
-
// 使用基于边缘的矩形检测
|
|
282
|
-
const rectangles = this.detectRectangles(edgeData)
|
|
283
|
-
|
|
284
|
-
// 4. 评估检测结果 - 检查是否找到了合适的矩形
|
|
285
|
-
const idCardRect = this.findIdCardRectangle(rectangles, imageData.width, imageData.height)
|
|
286
|
-
|
|
287
|
-
const detectionResult: DetectionResult = {
|
|
288
|
-
success: idCardRect !== null,
|
|
289
|
-
message: idCardRect ? "身份证检测成功" : "未检测到身份证",
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (detectionResult.success && idCardRect) {
|
|
293
|
-
// 使用检测到的身份证矩形区域
|
|
294
|
-
const width = imageData.width
|
|
295
|
-
const height = imageData.height
|
|
296
|
-
|
|
297
|
-
// 使用实际检测到的身份证区域
|
|
298
|
-
const rectWidth = idCardRect.width
|
|
299
|
-
const rectHeight = idCardRect.height
|
|
300
|
-
const rectX = idCardRect.x
|
|
301
|
-
const rectY = idCardRect.y
|
|
302
|
-
|
|
303
|
-
// 添加四个角点
|
|
304
|
-
detectionResult.corners = [
|
|
305
|
-
{ x: rectX, y: rectY },
|
|
306
|
-
{ x: rectX + rectWidth, y: rectY },
|
|
307
|
-
{ x: rectX + rectWidth, y: rectY + rectHeight },
|
|
308
|
-
{ x: rectX, y: rectY + rectHeight },
|
|
309
|
-
]
|
|
310
|
-
|
|
311
|
-
// 添加边界框
|
|
312
|
-
detectionResult.boundingBox = {
|
|
313
|
-
x: rectX,
|
|
314
|
-
y: rectY,
|
|
315
|
-
width: rectWidth,
|
|
316
|
-
height: rectHeight,
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// 裁剪身份证图像
|
|
320
|
-
const canvas = document.createElement("canvas")
|
|
321
|
-
canvas.width = rectWidth
|
|
322
|
-
canvas.height = rectHeight
|
|
323
|
-
const ctx = canvas.getContext("2d")
|
|
324
|
-
|
|
325
|
-
if (ctx) {
|
|
326
|
-
const tempCanvas = ImageProcessor.imageDataToCanvas(imageData)
|
|
327
|
-
ctx.drawImage(
|
|
328
|
-
tempCanvas,
|
|
329
|
-
rectX,
|
|
330
|
-
rectY,
|
|
331
|
-
rectWidth,
|
|
332
|
-
rectHeight,
|
|
333
|
-
0,
|
|
334
|
-
0,
|
|
335
|
-
rectWidth,
|
|
336
|
-
rectHeight
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
detectionResult.croppedImage = ctx.getImageData(
|
|
340
|
-
0,
|
|
341
|
-
0,
|
|
342
|
-
rectWidth,
|
|
343
|
-
rectHeight
|
|
344
|
-
)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// 设置置信度 - 基于边缘强度和矩形形状评分
|
|
348
|
-
detectionResult.confidence = this.calculateConfidence(idCardRect, edgeData)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return detectionResult
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* 清除检测结果缓存
|
|
356
|
-
*/
|
|
357
|
-
clearCache(): void {
|
|
358
|
-
this.resultCache.clear()
|
|
359
|
-
this.options.logger?.("检测结果缓存已清除")
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* 释放资源
|
|
364
|
-
*/
|
|
365
|
-
dispose(): void {
|
|
366
|
-
this.stop()
|
|
367
|
-
this.camera.release()
|
|
368
|
-
this.resultCache.clear()
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* 从边缘图像中检测矩形
|
|
373
|
-
* @param edgeData 边缘检测后的图像数据
|
|
374
|
-
* @returns 检测到的矩形数组
|
|
375
|
-
*/
|
|
376
|
-
private detectRectangles(edgeData: ImageData): Array<{
|
|
377
|
-
x: number;
|
|
378
|
-
y: number;
|
|
379
|
-
width: number;
|
|
380
|
-
height: number;
|
|
381
|
-
confidence: number;
|
|
382
|
-
}> {
|
|
383
|
-
const width = edgeData.width;
|
|
384
|
-
const height = edgeData.height;
|
|
385
|
-
const minSize = Math.min(width, height) * 0.2; // 最小矩形尺寸
|
|
386
|
-
const rectangles = [];
|
|
387
|
-
|
|
388
|
-
// 使用积分图像加速边缘密度计算
|
|
389
|
-
const integralImg = new Uint32Array(width * height);
|
|
390
|
-
|
|
391
|
-
// 计算积分图像
|
|
392
|
-
for (let y = 0; y < height; y++) {
|
|
393
|
-
for (let x = 0; x < width; x++) {
|
|
394
|
-
const idx = y * width + x;
|
|
395
|
-
const pixel = (edgeData.data[idx * 4] > 128) ? 1 : 0; // 边缘为白色
|
|
396
|
-
|
|
397
|
-
// 计算积分图
|
|
398
|
-
const above = y > 0 ? integralImg[(y - 1) * width + x] : 0;
|
|
399
|
-
const left = x > 0 ? integralImg[y * width + (x - 1)] : 0;
|
|
400
|
-
const diagonal = (x > 0 && y > 0) ? integralImg[(y - 1) * width + (x - 1)] : 0;
|
|
401
|
-
|
|
402
|
-
integralImg[idx] = pixel + above + left - diagonal;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// 滑动窗口检测矩形
|
|
407
|
-
for (let h = minSize; h < height * 0.9; h += Math.max(2, Math.floor(h * 0.05))) {
|
|
408
|
-
// 计算当前高度下,按照标准身份证比例的宽度
|
|
409
|
-
const w = Math.round(h * IDCardDetector.ID_CARD_ASPECT_RATIO);
|
|
410
|
-
if (w > width * 0.9) continue;
|
|
411
|
-
|
|
412
|
-
for (let y = 0; y < height - h; y += Math.max(2, Math.floor(h * 0.1))) {
|
|
413
|
-
for (let x = 0; x < width - w; x += Math.max(2, Math.floor(w * 0.1))) {
|
|
414
|
-
// 计算矩形区域内的边缘密度
|
|
415
|
-
const edgeCount = this.calculateRectSum(integralImg, x, y, w, h, width);
|
|
416
|
-
const avgEdgeDensity = edgeCount / (w * h);
|
|
417
|
-
|
|
418
|
-
// 计算矩形边界的边缘密度
|
|
419
|
-
const perimeterEdgeCount = this.calculateRectPerimeter(integralImg, x, y, w, h, width);
|
|
420
|
-
const perimeterLength = 2 * (w + h);
|
|
421
|
-
const perimeterDensity = perimeterEdgeCount / perimeterLength;
|
|
422
|
-
|
|
423
|
-
// 矩形得分 - 边界边缘密度高且内部适中
|
|
424
|
-
const rectScore = perimeterDensity * 0.7 + (0.3 - Math.abs(0.15 - avgEdgeDensity)) * 0.3;
|
|
425
|
-
|
|
426
|
-
if (rectScore > 0.4) { // 阈值可根据实际项目调整
|
|
427
|
-
rectangles.push({
|
|
428
|
-
x,
|
|
429
|
-
y,
|
|
430
|
-
width: w,
|
|
431
|
-
height: h,
|
|
432
|
-
confidence: rectScore
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// 按得分排序
|
|
440
|
-
return rectangles.sort((a, b) => b.confidence - a.confidence);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* 使用积分图计算矩形区域内的总和
|
|
445
|
-
*/
|
|
446
|
-
private calculateRectSum(integral: Uint32Array, x: number, y: number, w: number, h: number, stride: number): number {
|
|
447
|
-
const x2 = Math.min(x + w - 1, stride - 1);
|
|
448
|
-
const y2 = Math.min(y + h - 1, integral.length / stride - 1);
|
|
449
|
-
|
|
450
|
-
const topLeft = (x > 0 && y > 0) ? integral[(y - 1) * stride + (x - 1)] : 0;
|
|
451
|
-
const topRight = y > 0 ? integral[(y - 1) * stride + x2] : 0;
|
|
452
|
-
const bottomLeft = x > 0 ? integral[y2 * stride + (x - 1)] : 0;
|
|
453
|
-
const bottomRight = integral[y2 * stride + x2];
|
|
454
|
-
|
|
455
|
-
return bottomRight - topRight - bottomLeft + topLeft;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* 计算矩形周长上的边缘点数量
|
|
460
|
-
*/
|
|
461
|
-
private calculateRectPerimeter(integral: Uint32Array, x: number, y: number, w: number, h: number, stride: number): number {
|
|
462
|
-
// 上边缘
|
|
463
|
-
const topEdgeSum = this.calculateRectSum(integral, x, y, w, 1, stride);
|
|
464
|
-
// 下边缘
|
|
465
|
-
const bottomEdgeSum = this.calculateRectSum(integral, x, y + h - 1, w, 1, stride);
|
|
466
|
-
// 左边缘
|
|
467
|
-
const leftEdgeSum = this.calculateRectSum(integral, x, y, 1, h, stride);
|
|
468
|
-
// 右边缘
|
|
469
|
-
const rightEdgeSum = this.calculateRectSum(integral, x + w - 1, y, 1, h, stride);
|
|
470
|
-
|
|
471
|
-
return topEdgeSum + bottomEdgeSum + leftEdgeSum + rightEdgeSum;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* 从检测到的矩形中找出最可能是身份证的矩形
|
|
476
|
-
*/
|
|
477
|
-
private findIdCardRectangle(rectangles: Array<{x: number; y: number; width: number; height: number; confidence: number}>, imageWidth: number, imageHeight: number): {x: number; y: number; width: number; height: number; confidence: number} | null {
|
|
478
|
-
if (rectangles.length === 0) return null;
|
|
479
|
-
|
|
480
|
-
// 筛选符合身份证宽高比的矩形
|
|
481
|
-
const filteredRects = rectangles.filter(rect => {
|
|
482
|
-
const aspectRatio = rect.width / rect.height;
|
|
483
|
-
return Math.abs(aspectRatio - IDCardDetector.ID_CARD_ASPECT_RATIO) < 0.2; // 允许20%的误差
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
if (filteredRects.length === 0) return null;
|
|
487
|
-
|
|
488
|
-
// 返回得分最高的矩形
|
|
489
|
-
return filteredRects[0];
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* 计算身份证检测的置信度
|
|
494
|
-
*/
|
|
495
|
-
private calculateConfidence(rect: {x: number; y: number; width: number; height: number; confidence: number} | null, edgeData: ImageData): number {
|
|
496
|
-
if (!rect) return 0;
|
|
497
|
-
|
|
498
|
-
// 基本得分来自矩形检测
|
|
499
|
-
let score = rect.confidence;
|
|
500
|
-
|
|
501
|
-
// 额外因素:矩形大小相对于图像
|
|
502
|
-
const relativeSize = (rect.width * rect.height) / (edgeData.width * edgeData.height);
|
|
503
|
-
if (relativeSize > 0.1 && relativeSize < 0.7) {
|
|
504
|
-
score += 0.1; // 身份证通常占据图像的合理比例
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// 范围限制在0-1之间
|
|
508
|
-
return Math.min(Math.max(score, 0), 1);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file OCR Worker处理模块
|
|
3
|
-
* @description 用于在Web Worker中执行OCR处理
|
|
4
|
-
* @module OCRWorker
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { IDCardInfo } from "../utils/types"
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* OCR处理输入接口
|
|
11
|
-
*/
|
|
12
|
-
export interface OCRProcessInput {
|
|
13
|
-
imageBase64: string
|
|
14
|
-
tessWorkerOptions?: any
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* OCR处理输出接口
|
|
19
|
-
*/
|
|
20
|
-
export interface OCRProcessOutput {
|
|
21
|
-
idCardInfo: IDCardInfo
|
|
22
|
-
processingTime: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 在Web Worker中执行OCR处理的函数
|
|
27
|
-
*
|
|
28
|
-
* 该函数用于在使用 createWorker 创建的 Worker 中执行
|
|
29
|
-
*
|
|
30
|
-
* @param input OCR处理输入数据
|
|
31
|
-
* @returns OCR处理结果
|
|
32
|
-
*/
|
|
33
|
-
export async function processOCRInWorker(
|
|
34
|
-
input: OCRProcessInput
|
|
35
|
-
): Promise<OCRProcessOutput> {
|
|
36
|
-
// 计时开始
|
|
37
|
-
const startTime = performance.now()
|
|
38
|
-
|
|
39
|
-
// 加载Tesseract.js (Worker 环境下动态导入)
|
|
40
|
-
const { createWorker } = await import("tesseract.js")
|
|
41
|
-
|
|
42
|
-
// 创建OCR Worker
|
|
43
|
-
const worker = (await createWorker(
|
|
44
|
-
input.tessWorkerOptions || {
|
|
45
|
-
logger: (m: any) => console.log(m),
|
|
46
|
-
}
|
|
47
|
-
)) as any // 添加类型断言,避免TypeScript错误
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
// 初始化OCR引擎
|
|
51
|
-
await worker.load()
|
|
52
|
-
await worker.loadLanguage("chi_sim")
|
|
53
|
-
await worker.initialize("chi_sim")
|
|
54
|
-
await worker.setParameters({
|
|
55
|
-
tessedit_char_whitelist:
|
|
56
|
-
"0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期",
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
// 识别图像
|
|
60
|
-
const { data } = await worker.recognize(input.imageBase64)
|
|
61
|
-
|
|
62
|
-
// 解析识别结果
|
|
63
|
-
const idCardInfo = parseIDCardText(data.text)
|
|
64
|
-
|
|
65
|
-
// 处理完成后终止worker
|
|
66
|
-
await worker.terminate()
|
|
67
|
-
|
|
68
|
-
// 计算处理时间
|
|
69
|
-
const processingTime = performance.now() - startTime
|
|
70
|
-
|
|
71
|
-
// 返回处理结果
|
|
72
|
-
return {
|
|
73
|
-
idCardInfo,
|
|
74
|
-
processingTime,
|
|
75
|
-
}
|
|
76
|
-
} catch (error) {
|
|
77
|
-
// 确保资源被释放
|
|
78
|
-
await worker.terminate()
|
|
79
|
-
throw error
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 解析身份证文本信息
|
|
85
|
-
*
|
|
86
|
-
* 从OCR识别到的文本中提取结构化的身份证信息
|
|
87
|
-
*
|
|
88
|
-
* @private
|
|
89
|
-
* @param {string} text - OCR识别到的文本
|
|
90
|
-
* @returns {IDCardInfo} 提取到的身份证信息对象
|
|
91
|
-
*/
|
|
92
|
-
function parseIDCardText(text: string): IDCardInfo {
|
|
93
|
-
const info: IDCardInfo = {}
|
|
94
|
-
|
|
95
|
-
// 拆分为行
|
|
96
|
-
const lines = text.split("\n").filter((line) => line.trim())
|
|
97
|
-
|
|
98
|
-
// 解析身份证号码(最容易识别的部分)
|
|
99
|
-
const idNumberRegex = /(\d{17}[\dX])/
|
|
100
|
-
const idNumberMatch = text.match(idNumberRegex)
|
|
101
|
-
if (idNumberMatch) {
|
|
102
|
-
info.idNumber = idNumberMatch[1]
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 解析姓名
|
|
106
|
-
for (const line of lines) {
|
|
107
|
-
if (
|
|
108
|
-
line.includes("姓名") ||
|
|
109
|
-
(line.length < 10 && line.length > 1 && !/\d/.test(line))
|
|
110
|
-
) {
|
|
111
|
-
info.name = line.replace("姓名", "").trim()
|
|
112
|
-
break
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// 解析性别和民族
|
|
117
|
-
const genderNationalityRegex = /(男|女).*(族)/
|
|
118
|
-
const genderMatch = text.match(genderNationalityRegex)
|
|
119
|
-
if (genderMatch) {
|
|
120
|
-
info.gender = genderMatch[1]
|
|
121
|
-
const nationalityText = genderMatch[0]
|
|
122
|
-
info.nationality = nationalityText
|
|
123
|
-
.substring(nationalityText.indexOf(genderMatch[1]) + 1)
|
|
124
|
-
.trim()
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 解析出生日期
|
|
128
|
-
const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/
|
|
129
|
-
const birthDateMatch = text.match(birthDateRegex)
|
|
130
|
-
if (birthDateMatch) {
|
|
131
|
-
info.birthDate = `${birthDateMatch[1]}-${birthDateMatch[2]}-${birthDateMatch[3]}`
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 解析地址
|
|
135
|
-
const addressRegex = /住址([\s\S]*?)公民身份号码/
|
|
136
|
-
const addressMatch = text.match(addressRegex)
|
|
137
|
-
if (addressMatch) {
|
|
138
|
-
info.address = addressMatch[1].replace(/\n/g, "").trim()
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// 解析签发机关
|
|
142
|
-
const authorityRegex = /签发机关([\s\S]*?)有效期/
|
|
143
|
-
const authorityMatch = text.match(authorityRegex)
|
|
144
|
-
if (authorityMatch) {
|
|
145
|
-
info.issuingAuthority = authorityMatch[1].replace(/\n/g, "").trim()
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 解析有效期限
|
|
149
|
-
const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/
|
|
150
|
-
const validPeriodMatch = text.match(validPeriodRegex)
|
|
151
|
-
if (validPeriodMatch) {
|
|
152
|
-
info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim()
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return info
|
|
156
|
-
}
|