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.
@@ -279,6 +279,323 @@
279
279
  }
280
280
  return imageData;
281
281
  }
282
+ /**
283
+ * 降低图像分辨率以提高处理速度
284
+ *
285
+ * 对于OCR和图像分析,降低分辨率可以在保持识别率的同时大幅提升处理速度
286
+ *
287
+ * @param {ImageData} imageData - 原图像数据
288
+ * @param {number} [maxDimension=1000] - 目标最大尺寸(宽或高)
289
+ * @returns {ImageData} 处理后的图像数据
290
+ */
291
+ static downsampleForProcessing(imageData, maxDimension = 1000) {
292
+ const { width, height } = imageData;
293
+ // 如果图像尺寸已经小于或等于目标尺寸,则无需处理
294
+ if (width <= maxDimension && height <= maxDimension) {
295
+ return imageData;
296
+ }
297
+ // 计算缩放比例,保持宽高比
298
+ const scale = maxDimension / Math.max(width, height);
299
+ const newWidth = Math.round(width * scale);
300
+ const newHeight = Math.round(height * scale);
301
+ // 调整图像大小
302
+ return this.resize(imageData, newWidth, newHeight);
303
+ }
304
+ /**
305
+ * 转换图像为Base64格式,方便在Worker线程中传递
306
+ *
307
+ * @param {ImageData} imageData - 原图像数据
308
+ * @returns {string} base64编码的图像数据
309
+ */
310
+ static imageDataToBase64(imageData) {
311
+ const canvas = this.imageDataToCanvas(imageData);
312
+ return canvas.toDataURL('image/jpeg', 0.7); // 使用较低质量的JPEG格式减少数据量
313
+ }
314
+ /**
315
+ * 从Base64字符串还原图像数据
316
+ *
317
+ * @param {string} base64 - base64编码的图像数据
318
+ * @returns {Promise<ImageData>} 还原的图像数据
319
+ */
320
+ static async base64ToImageData(base64) {
321
+ return new Promise((resolve, reject) => {
322
+ const img = new Image();
323
+ img.onload = () => {
324
+ const canvas = document.createElement('canvas');
325
+ canvas.width = img.width;
326
+ canvas.height = img.height;
327
+ const ctx = canvas.getContext('2d');
328
+ if (!ctx) {
329
+ reject(new Error('无法创建Canvas上下文'));
330
+ return;
331
+ }
332
+ ctx.drawImage(img, 0, 0);
333
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
334
+ resolve(imageData);
335
+ };
336
+ img.onerror = () => {
337
+ reject(new Error('图像加载失败'));
338
+ };
339
+ img.src = base64;
340
+ });
341
+ }
342
+ /**
343
+ * 使用Web Worker并行处理图像
344
+ * 此方法将图像分割为多个部分,并行处理以提高性能
345
+ *
346
+ * @param {ImageData} imageData - 原图像数据
347
+ * @param {Function} processingFunction - 处理函数,接收ImageData返回ImageData
348
+ * @param {number} [chunks=4] - 分割的块数
349
+ * @returns {Promise<ImageData>} 处理后的图像数据
350
+ */
351
+ static async processImageInParallel(imageData, processingFunction, chunks = 4) {
352
+ // 如果不支持Worker或图像太小,直接处理
353
+ if (typeof Worker === 'undefined' || imageData.width * imageData.height < 100000) {
354
+ return processingFunction(imageData);
355
+ }
356
+ // 创建结果canvas
357
+ const resultCanvas = document.createElement('canvas');
358
+ resultCanvas.width = imageData.width;
359
+ resultCanvas.height = imageData.height;
360
+ const resultCtx = resultCanvas.getContext('2d');
361
+ if (!resultCtx) {
362
+ throw new Error('无法创建Canvas上下文');
363
+ }
364
+ // 根据图像特性确定分割方向和每块大小
365
+ const isWide = imageData.width > imageData.height;
366
+ const chunkSize = Math.floor((isWide ? imageData.width : imageData.height) / chunks);
367
+ // 创建Worker处理每个块
368
+ const promises = [];
369
+ for (let i = 0; i < chunks; i++) {
370
+ const chunkCanvas = document.createElement('canvas');
371
+ const chunkCtx = chunkCanvas.getContext('2d');
372
+ if (!chunkCtx)
373
+ continue;
374
+ let chunkImageData;
375
+ if (isWide) {
376
+ // 水平分割
377
+ const startX = i * chunkSize;
378
+ const width = (i === chunks - 1) ? imageData.width - startX : chunkSize;
379
+ chunkCanvas.width = width;
380
+ chunkCanvas.height = imageData.height;
381
+ // 复制原图像数据到分块
382
+ const tempCanvas = this.imageDataToCanvas(imageData);
383
+ chunkCtx.drawImage(tempCanvas, startX, 0, width, imageData.height, 0, 0, width, imageData.height);
384
+ chunkImageData = chunkCtx.getImageData(0, 0, width, imageData.height);
385
+ }
386
+ else {
387
+ // 垂直分割
388
+ const startY = i * chunkSize;
389
+ const height = (i === chunks - 1) ? imageData.height - startY : chunkSize;
390
+ chunkCanvas.width = imageData.width;
391
+ chunkCanvas.height = height;
392
+ // 复制原图像数据到分块
393
+ const tempCanvas = this.imageDataToCanvas(imageData);
394
+ chunkCtx.drawImage(tempCanvas, 0, startY, imageData.width, height, 0, 0, imageData.width, height);
395
+ chunkImageData = chunkCtx.getImageData(0, 0, imageData.width, height);
396
+ }
397
+ // 使用Worker处理
398
+ const workerCode = `
399
+ self.onmessage = function(e) {
400
+ const imageData = e.data.imageData;
401
+ const processingFunction = ${processingFunction.toString()};
402
+ const result = processingFunction(imageData);
403
+ self.postMessage({ result, index: e.data.index }, [result.data.buffer]);
404
+ }
405
+ `;
406
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
407
+ const workerUrl = URL.createObjectURL(blob);
408
+ const worker = new Worker(workerUrl);
409
+ const promise = new Promise((resolve) => {
410
+ worker.onmessage = function (e) {
411
+ resolve(e.data);
412
+ worker.terminate();
413
+ URL.revokeObjectURL(workerUrl);
414
+ };
415
+ // 传输数据
416
+ worker.postMessage({
417
+ imageData: chunkImageData,
418
+ index: i
419
+ }, [chunkImageData.data.buffer]);
420
+ });
421
+ promises.push(promise);
422
+ }
423
+ // 等待所有Worker完成并组合结果
424
+ const results = await Promise.all(promises);
425
+ // 按索引排序结果
426
+ results.sort((a, b) => a.index - b.index);
427
+ // 将处理后的块绘制到结果canvas
428
+ for (let i = 0; i < results.length; i++) {
429
+ const { result } = results[i];
430
+ const tempCanvas = this.imageDataToCanvas(result);
431
+ if (isWide) {
432
+ const startX = i * chunkSize;
433
+ resultCtx.drawImage(tempCanvas, startX, 0);
434
+ }
435
+ else {
436
+ const startY = i * chunkSize;
437
+ resultCtx.drawImage(tempCanvas, 0, startY);
438
+ }
439
+ }
440
+ return resultCtx.getImageData(0, 0, imageData.width, imageData.height);
441
+ }
442
+ }
443
+
444
+ /**
445
+ * @file 性能优化工具类
446
+ * @description 提供节流、防抖、缓存等性能优化功能
447
+ * @module PerformanceUtils
448
+ */
449
+ /**
450
+ * 节流函数:限制函数在一定时间内只能执行一次
451
+ *
452
+ * @param fn 需要节流的函数
453
+ * @param delay 延迟时间(毫秒)
454
+ * @returns 节流处理后的函数
455
+ */
456
+ function throttle(fn, delay) {
457
+ let lastCall = 0;
458
+ let timeoutId = null;
459
+ return function (...args) {
460
+ const now = Date.now();
461
+ const remaining = delay - (now - lastCall);
462
+ if (remaining <= 0) {
463
+ if (timeoutId) {
464
+ clearTimeout(timeoutId);
465
+ timeoutId = null;
466
+ }
467
+ lastCall = now;
468
+ fn.apply(this, args);
469
+ }
470
+ else if (!timeoutId) {
471
+ timeoutId = window.setTimeout(() => {
472
+ lastCall = Date.now();
473
+ timeoutId = null;
474
+ fn.apply(this, args);
475
+ }, remaining);
476
+ }
477
+ };
478
+ }
479
+ /**
480
+ * LRU缓存类 - 使用最近最少使用策略的缓存实现
481
+ */
482
+ class LRUCache {
483
+ /**
484
+ * 构造LRU缓存
485
+ * @param maxSize 缓存最大容量
486
+ */
487
+ constructor(maxSize = 100) {
488
+ this.maxSize = maxSize;
489
+ this.cache = new Map();
490
+ }
491
+ /**
492
+ * 获取缓存项
493
+ * @param key 缓存键
494
+ * @returns 缓存值或undefined
495
+ */
496
+ get(key) {
497
+ if (!this.cache.has(key)) {
498
+ return undefined;
499
+ }
500
+ // 获取值
501
+ const value = this.cache.get(key);
502
+ // 将项移至最新位置(删除后重新添加)
503
+ this.cache.delete(key);
504
+ this.cache.set(key, value);
505
+ return value;
506
+ }
507
+ /**
508
+ * 设置缓存项
509
+ * @param key 缓存键
510
+ * @param value 缓存值
511
+ */
512
+ set(key, value) {
513
+ // 如果键已存在,需要先删除
514
+ if (this.cache.has(key)) {
515
+ this.cache.delete(key);
516
+ }
517
+ // 如果缓存已满,移除最老的项
518
+ if (this.cache.size >= this.maxSize) {
519
+ const oldestKey = this.cache.keys().next().value;
520
+ this.cache.delete(oldestKey);
521
+ }
522
+ // 添加新项
523
+ this.cache.set(key, value);
524
+ }
525
+ /**
526
+ * 删除缓存项
527
+ * @param key 缓存键
528
+ * @returns 是否成功删除
529
+ */
530
+ delete(key) {
531
+ return this.cache.delete(key);
532
+ }
533
+ /**
534
+ * 清空缓存
535
+ */
536
+ clear() {
537
+ this.cache.clear();
538
+ }
539
+ /**
540
+ * 获取当前缓存大小
541
+ */
542
+ get size() {
543
+ return this.cache.size;
544
+ }
545
+ /**
546
+ * 检查键是否存在
547
+ * @param key 缓存键
548
+ */
549
+ has(key) {
550
+ return this.cache.has(key);
551
+ }
552
+ }
553
+ /**
554
+ * 图像指纹计算函数 - 用于检测相同或相似图像
555
+ *
556
+ * @param imageData 图像数据
557
+ * @param size 指纹尺寸(默认8x8)
558
+ * @returns 图像指纹字符串
559
+ */
560
+ function calculateImageFingerprint(imageData, size = 8) {
561
+ // 1. 缩小图像到指定尺寸
562
+ const canvas = document.createElement('canvas');
563
+ canvas.width = size;
564
+ canvas.height = size;
565
+ const ctx = canvas.getContext('2d');
566
+ if (!ctx) {
567
+ return '';
568
+ }
569
+ // 创建一个临时canvas来绘制原始imageData
570
+ const tempCanvas = document.createElement('canvas');
571
+ tempCanvas.width = imageData.width;
572
+ tempCanvas.height = imageData.height;
573
+ const tempCtx = tempCanvas.getContext('2d');
574
+ if (!tempCtx) {
575
+ return '';
576
+ }
577
+ tempCtx.putImageData(imageData, 0, 0);
578
+ // 缩小到目标尺寸
579
+ ctx.drawImage(tempCanvas, 0, 0, imageData.width, imageData.height, 0, 0, size, size);
580
+ // 2. 转换为灰度
581
+ const smallImgData = ctx.getImageData(0, 0, size, size);
582
+ const grayValues = [];
583
+ for (let i = 0; i < smallImgData.data.length; i += 4) {
584
+ const r = smallImgData.data[i];
585
+ const g = smallImgData.data[i + 1];
586
+ const b = smallImgData.data[i + 2];
587
+ // 转为灰度: 0.299r + 0.587g + 0.114b
588
+ const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
589
+ grayValues.push(gray);
590
+ }
591
+ // 3. 计算平均值
592
+ const avg = grayValues.reduce((sum, val) => sum + val, 0) / grayValues.length;
593
+ // 4. 比较每个像素与平均值,生成二进制指纹
594
+ let fingerprint = '';
595
+ for (const gray of grayValues) {
596
+ fingerprint += gray >= avg ? '1' : '0';
597
+ }
598
+ return fingerprint;
282
599
  }
283
600
 
284
601
  /**
@@ -320,15 +637,48 @@
320
637
  constructor(options) {
321
638
  this.detecting = false;
322
639
  this.detectTimer = null;
640
+ this.frameCount = 0;
641
+ this.lastDetectionTime = 0;
323
642
  this.camera = new Camera();
324
643
  if (typeof options === 'function') {
325
644
  // 兼容旧的构造函数方式
326
645
  this.onDetected = options;
646
+ this.options = {
647
+ detectionInterval: 200,
648
+ maxImageDimension: 800,
649
+ enableCache: true,
650
+ cacheSize: 20,
651
+ logger: console.log
652
+ };
327
653
  }
328
654
  else if (options) {
329
655
  // 使用新的选项对象方式
656
+ this.options = {
657
+ detectionInterval: 200,
658
+ maxImageDimension: 800,
659
+ enableCache: true,
660
+ cacheSize: 20,
661
+ logger: console.log,
662
+ ...options
663
+ };
330
664
  this.onDetected = options.onDetection;
665
+ this.onError = options.onError;
331
666
  }
667
+ else {
668
+ this.options = {
669
+ detectionInterval: 200,
670
+ maxImageDimension: 800,
671
+ enableCache: true,
672
+ cacheSize: 20,
673
+ logger: console.log
674
+ };
675
+ }
676
+ this.detectionInterval = this.options.detectionInterval;
677
+ this.maxImageDimension = this.options.maxImageDimension;
678
+ // 初始化结果缓存
679
+ this.resultCache = new LRUCache(this.options.cacheSize);
680
+ // 创建节流版本的检测函数
681
+ this.throttledDetect = throttle(this.performDetection.bind(this), this.detectionInterval);
332
682
  }
333
683
  /**
334
684
  * 启动身份证检测
@@ -341,98 +691,374 @@
341
691
  async start(videoElement) {
342
692
  await this.camera.initialize(videoElement);
343
693
  this.detecting = true;
694
+ this.frameCount = 0;
695
+ this.lastDetectionTime = 0;
344
696
  this.detect();
345
697
  }
346
698
  /**
347
- * 执行一次身份证检测
348
- *
349
- * 内部方法,捕获当前视频帧并尝试检测其中的身份证
699
+ * 停止身份证检测
700
+ */
701
+ stop() {
702
+ this.detecting = false;
703
+ if (this.detectTimer !== null) {
704
+ cancelAnimationFrame(this.detectTimer);
705
+ this.detectTimer = null;
706
+ }
707
+ }
708
+ /**
709
+ * 持续检测视频帧
350
710
  *
351
711
  * @private
352
712
  */
353
- async detect() {
713
+ detect() {
354
714
  if (!this.detecting)
355
715
  return;
356
- const imageData = this.camera.captureFrame();
357
- if (imageData) {
716
+ this.detectTimer = requestAnimationFrame(() => {
358
717
  try {
359
- // 简单实现,因为没有完整的OpenCV.js
360
- // 实际项目中应该使用OpenCV.js做更精确的边缘检测
361
- const result = await this.detectIDCard(imageData);
362
- if (this.onDetected) {
363
- this.onDetected(result);
718
+ this.frameCount++;
719
+ const now = performance.now();
720
+ // 帧率控制 - 只有满足时间间隔的帧才进行检测
721
+ // 这样可以显著减少CPU使用率,同时保持良好的用户体验
722
+ if (this.frameCount % 3 === 0 || now - this.lastDetectionTime >= this.detectionInterval) {
723
+ this.throttledDetect();
724
+ this.lastDetectionTime = now;
364
725
  }
726
+ // 继续下一帧检测
727
+ this.detect();
365
728
  }
366
729
  catch (error) {
730
+ if (this.onError) {
731
+ this.onError(error);
732
+ }
733
+ else {
734
+ console.error('身份证检测错误:', error);
735
+ }
736
+ // 出错后延迟重试
737
+ setTimeout(() => {
738
+ if (this.detecting) {
739
+ this.detect();
740
+ }
741
+ }, 1000);
742
+ }
743
+ });
744
+ }
745
+ /**
746
+ * 执行单帧检测
747
+ *
748
+ * @private
749
+ */
750
+ async performDetection() {
751
+ if (!this.detecting || !this.camera)
752
+ return;
753
+ // 获取当前视频帧
754
+ const frame = this.camera.captureFrame();
755
+ if (!frame)
756
+ return;
757
+ // 检查缓存
758
+ if (this.options.enableCache) {
759
+ const fingerprint = calculateImageFingerprint(frame, 16); // 使用更大的尺寸提高特征区分度
760
+ const cachedResult = this.resultCache.get(fingerprint);
761
+ if (cachedResult) {
762
+ this.options.logger?.('使用缓存的检测结果');
763
+ // 使用缓存结果,但更新图像数据以确保最新
764
+ const updatedResult = {
765
+ ...cachedResult,
766
+ imageData: frame
767
+ };
768
+ if (this.onDetected) {
769
+ this.onDetected(updatedResult);
770
+ }
771
+ return;
772
+ }
773
+ }
774
+ // 降低分辨率以提高性能
775
+ const downsampledFrame = ImageProcessor.downsampleForProcessing(frame, this.maxImageDimension);
776
+ try {
777
+ // 检测身份证
778
+ const result = await this.detectIDCard(downsampledFrame);
779
+ // 如果检测成功,将原始图像添加到结果中
780
+ if (result.success) {
781
+ result.imageData = frame;
782
+ // 缓存结果
783
+ if (this.options.enableCache) {
784
+ const fingerprint = calculateImageFingerprint(frame, 16);
785
+ this.resultCache.set(fingerprint, result);
786
+ }
787
+ }
788
+ // 处理检测结果
789
+ if (this.onDetected) {
790
+ this.onDetected(result);
791
+ }
792
+ }
793
+ catch (error) {
794
+ if (this.onError) {
795
+ this.onError(error);
796
+ }
797
+ else {
367
798
  console.error('身份证检测错误:', error);
368
799
  }
369
800
  }
370
- this.detectTimer = window.setTimeout(() => this.detect(), 200);
371
801
  }
372
802
  /**
373
- * 身份证检测核心算法
374
- *
375
- * 通过图像处理技术检测和提取图像中的身份证区域
803
+ * 检测图像中的身份证
376
804
  *
377
805
  * @private
378
- * @param {ImageData} imageData - 需要检测身份证的图像数据
379
- * @returns {Promise<DetectionResult>} 检测结果,包含成功标志和裁剪后的身份证图像
806
+ * @param {ImageData} imageData - 要分析的图像数据
807
+ * @returns {Promise<DetectionResult>} 检测结果
380
808
  */
381
809
  async detectIDCard(imageData) {
382
- // 图像预处理
383
- const grayscale = ImageProcessor.toGrayscale(imageData);
384
- ImageProcessor.adjustBrightnessContrast(grayscale, 10, 30);
385
- // 简化的身份证检测算法
386
- // 在真实项目中,这里应该使用OpenCV.js进行轮廓检测和矩形检测
387
- // 模拟检测过程
388
- const success = Math.random() > 0.7; // 模拟70%的概率检测成功
389
- if (success) {
390
- // 模拟一个身份证区域,实际项目中应该是根据检测结果
391
- const cardWidth = Math.floor(imageData.width * 0.8);
392
- const cardHeight = Math.floor(cardWidth * 0.63); // 身份证比例大约是8:5
393
- const x = Math.floor((imageData.width - cardWidth) / 2);
394
- const y = Math.floor((imageData.height - cardHeight) / 2);
395
- // 模拟四个角点
396
- const corners = [
397
- { x, y }, // 左上
398
- { x: x + cardWidth, y }, // 右上
399
- { x: x + cardWidth, y: y + cardHeight }, // 右下
400
- { x, y: y + cardHeight } // 左下
810
+ // 1. 图像预处理
811
+ ImageProcessor.toGrayscale(imageData);
812
+ // 2. 检测矩形和边缘(简化版实现)
813
+ // 注意:实际应用中应使用OpenCV.js或其他计算机视觉库进行更精确的检测
814
+ // 此处仅作为概念性实现,使用基本矩形检测逻辑
815
+ // 模拟检测过程,随机判断是否找到身份证
816
+ // 在实际应用中,此处应当实现实际的计算机视觉算法
817
+ const detectionResult = {
818
+ success: Math.random() > 0.3, // 70%的概率成功检测到
819
+ message: '身份证检测完成'
820
+ };
821
+ if (detectionResult.success) {
822
+ // 模拟一个身份证矩形区域
823
+ const width = imageData.width;
824
+ const height = imageData.height;
825
+ // 大致的身份证区域(按比例)
826
+ const rectWidth = Math.round(width * 0.7);
827
+ const rectHeight = Math.round(rectWidth * 0.618); // 身份证是黄金比例
828
+ const rectX = Math.round((width - rectWidth) / 2);
829
+ const rectY = Math.round((height - rectHeight) / 2);
830
+ // 添加四个角点
831
+ detectionResult.corners = [
832
+ { x: rectX, y: rectY },
833
+ { x: rectX + rectWidth, y: rectY },
834
+ { x: rectX + rectWidth, y: rectY + rectHeight },
835
+ { x: rectX, y: rectY + rectHeight }
401
836
  ];
402
- // 模拟裁剪图像(实际项目中应该做透视变换)
837
+ // 添加边界框
838
+ detectionResult.boundingBox = {
839
+ x: rectX,
840
+ y: rectY,
841
+ width: rectWidth,
842
+ height: rectHeight
843
+ };
844
+ // 裁剪身份证图像
403
845
  const canvas = document.createElement('canvas');
404
- canvas.width = cardWidth;
405
- canvas.height = cardHeight;
846
+ canvas.width = rectWidth;
847
+ canvas.height = rectHeight;
406
848
  const ctx = canvas.getContext('2d');
407
849
  if (ctx) {
408
- // 从原图中裁剪身份证区域
409
- const sourceCanvas = ImageProcessor.imageDataToCanvas(imageData);
410
- ctx.drawImage(sourceCanvas, x, y, cardWidth, cardHeight, 0, 0, cardWidth, cardHeight);
411
- const croppedImage = ctx.getImageData(0, 0, cardWidth, cardHeight);
412
- return {
413
- success: true,
414
- corners,
415
- croppedImage
416
- };
850
+ const tempCanvas = ImageProcessor.imageDataToCanvas(imageData);
851
+ ctx.drawImage(tempCanvas, rectX, rectY, rectWidth, rectHeight, 0, 0, rectWidth, rectHeight);
852
+ detectionResult.croppedImage = ctx.getImageData(0, 0, rectWidth, rectHeight);
417
853
  }
854
+ // 设置置信度
855
+ detectionResult.confidence = 0.7 + Math.random() * 0.3;
418
856
  }
419
- return { success: false };
857
+ return detectionResult;
420
858
  }
421
859
  /**
422
- * 停止身份证检测
423
- *
424
- * 停止检测循环并释放相机资源
860
+ * 清除检测结果缓存
425
861
  */
426
- stop() {
427
- this.detecting = false;
428
- if (this.detectTimer) {
429
- clearTimeout(this.detectTimer);
430
- this.detectTimer = null;
431
- }
862
+ clearCache() {
863
+ this.resultCache.clear();
864
+ this.options.logger?.('检测结果缓存已清除');
865
+ }
866
+ /**
867
+ * 释放资源
868
+ */
869
+ dispose() {
870
+ this.stop();
432
871
  this.camera.release();
872
+ this.resultCache.clear();
433
873
  }
434
874
  }
435
875
 
876
+ /**
877
+ * @file Web Worker辅助工具类
878
+ * @description 提供Worker线程管理功能,用于将计算密集型任务移至后台线程
879
+ * @module WorkerUtils
880
+ */
881
+ /**
882
+ * 创建Worker线程并处理消息通信
883
+ *
884
+ * @param workerFunction 要在Worker中执行的函数
885
+ * @returns 返回包含发送消息方法的Worker控制对象
886
+ */
887
+ function createWorker(workerFunction) {
888
+ // 将函数转换为字符串,然后创建一个Blob URL
889
+ const workerCode = `
890
+ self.onmessage = async function(e) {
891
+ try {
892
+ const result = await (${workerFunction.toString()})(e.data);
893
+ self.postMessage({ success: true, result });
894
+ } catch (error) {
895
+ self.postMessage({
896
+ success: false,
897
+ error: { message: error.message, stack: error.stack }
898
+ });
899
+ }
900
+ }
901
+ `;
902
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
903
+ const workerUrl = URL.createObjectURL(blob);
904
+ const worker = new Worker(workerUrl);
905
+ // 创建一个映射来存储待解析的Promise
906
+ const promiseMap = new Map();
907
+ let messageCounter = 0;
908
+ worker.onmessage = (e) => {
909
+ // 释放Blob URL
910
+ if (promiseMap.size === 0) {
911
+ URL.revokeObjectURL(workerUrl);
912
+ }
913
+ const { id, success, result, error } = e.data;
914
+ const promiseHandlers = promiseMap.get(id);
915
+ if (promiseHandlers) {
916
+ promiseMap.delete(id);
917
+ if (success) {
918
+ promiseHandlers.resolve(result);
919
+ }
920
+ else {
921
+ const workerError = new Error(error.message);
922
+ workerError.stack = error.stack;
923
+ promiseHandlers.reject(workerError);
924
+ }
925
+ }
926
+ };
927
+ return {
928
+ postMessage: (data) => {
929
+ return new Promise((resolve, reject) => {
930
+ const id = messageCounter++;
931
+ promiseMap.set(id, { resolve, reject });
932
+ worker.postMessage({ id, data });
933
+ });
934
+ },
935
+ terminate: () => {
936
+ worker.terminate();
937
+ promiseMap.clear();
938
+ URL.revokeObjectURL(workerUrl);
939
+ }
940
+ };
941
+ }
942
+ /**
943
+ * 判断浏览器是否支持Web Workers
944
+ *
945
+ * @returns 是否支持Web Workers
946
+ */
947
+ function isWorkerSupported() {
948
+ return typeof Worker !== 'undefined';
949
+ }
950
+
951
+ /**
952
+ * @file OCR Worker处理模块
953
+ * @description 用于在Web Worker中执行OCR处理
954
+ * @module OCRWorker
955
+ */
956
+ /**
957
+ * 在Web Worker中执行OCR处理的函数
958
+ *
959
+ * 该函数用于在使用 createWorker 创建的 Worker 中执行
960
+ *
961
+ * @param input OCR处理输入数据
962
+ * @returns OCR处理结果
963
+ */
964
+ async function processOCRInWorker(input) {
965
+ // 计时开始
966
+ const startTime = performance.now();
967
+ // 加载Tesseract.js (Worker 环境下动态导入)
968
+ const { createWorker } = await import('tesseract.js');
969
+ // 创建OCR Worker
970
+ const worker = createWorker(input.tessWorkerOptions || {
971
+ logger: (m) => console.log(m)
972
+ });
973
+ try {
974
+ // 初始化OCR引擎
975
+ await worker.load();
976
+ await worker.loadLanguage('chi_sim');
977
+ await worker.initialize('chi_sim');
978
+ await worker.setParameters({
979
+ tessedit_char_whitelist: '0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期'
980
+ });
981
+ // 识别图像
982
+ const { data } = await worker.recognize(input.imageBase64);
983
+ // 解析识别结果
984
+ const idCardInfo = parseIDCardText(data.text);
985
+ // 处理完成后终止worker
986
+ await worker.terminate();
987
+ // 计算处理时间
988
+ const processingTime = performance.now() - startTime;
989
+ // 返回处理结果
990
+ return {
991
+ idCardInfo,
992
+ processingTime
993
+ };
994
+ }
995
+ catch (error) {
996
+ // 确保资源被释放
997
+ await worker.terminate();
998
+ throw error;
999
+ }
1000
+ }
1001
+ /**
1002
+ * 解析身份证文本信息
1003
+ *
1004
+ * 从OCR识别到的文本中提取结构化的身份证信息
1005
+ *
1006
+ * @private
1007
+ * @param {string} text - OCR识别到的文本
1008
+ * @returns {IDCardInfo} 提取到的身份证信息对象
1009
+ */
1010
+ function parseIDCardText(text) {
1011
+ const info = {};
1012
+ // 拆分为行
1013
+ const lines = text.split('\n').filter(line => line.trim());
1014
+ // 解析身份证号码(最容易识别的部分)
1015
+ const idNumberRegex = /(\d{17}[\dX])/;
1016
+ const idNumberMatch = text.match(idNumberRegex);
1017
+ if (idNumberMatch) {
1018
+ info.idNumber = idNumberMatch[1];
1019
+ }
1020
+ // 解析姓名
1021
+ for (const line of lines) {
1022
+ if (line.includes('姓名') || line.length < 10 && line.length > 1 && !/\d/.test(line)) {
1023
+ info.name = line.replace('姓名', '').trim();
1024
+ break;
1025
+ }
1026
+ }
1027
+ // 解析性别和民族
1028
+ const genderNationalityRegex = /(男|女).*(族)/;
1029
+ const genderMatch = text.match(genderNationalityRegex);
1030
+ if (genderMatch) {
1031
+ info.gender = genderMatch[1];
1032
+ const nationalityText = genderMatch[0];
1033
+ info.nationality = nationalityText.substring(nationalityText.indexOf(genderMatch[1]) + 1).trim();
1034
+ }
1035
+ // 解析出生日期
1036
+ const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/;
1037
+ const birthDateMatch = text.match(birthDateRegex);
1038
+ if (birthDateMatch) {
1039
+ info.birthDate = `${birthDateMatch[1]}-${birthDateMatch[2]}-${birthDateMatch[3]}`;
1040
+ }
1041
+ // 解析地址
1042
+ const addressRegex = /住址([\s\S]*?)公民身份号码/;
1043
+ const addressMatch = text.match(addressRegex);
1044
+ if (addressMatch) {
1045
+ info.address = addressMatch[1].replace(/\n/g, '').trim();
1046
+ }
1047
+ // 解析签发机关
1048
+ const authorityRegex = /签发机关([\s\S]*?)有效期/;
1049
+ const authorityMatch = text.match(authorityRegex);
1050
+ if (authorityMatch) {
1051
+ info.issuingAuthority = authorityMatch[1].replace(/\n/g, '').trim();
1052
+ }
1053
+ // 解析有效期限
1054
+ const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/;
1055
+ const validPeriodMatch = text.match(validPeriodRegex);
1056
+ if (validPeriodMatch) {
1057
+ info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim();
1058
+ }
1059
+ return info;
1060
+ }
1061
+
436
1062
  /**
437
1063
  * @file OCR处理模块
438
1064
  * @description 提供身份证文字识别和信息提取功能
@@ -460,8 +1086,25 @@
460
1086
  * ```
461
1087
  */
462
1088
  class OCRProcessor {
463
- constructor() {
1089
+ /**
1090
+ * 创建OCR处理器实例
1091
+ *
1092
+ * @param options OCR处理器选项
1093
+ */
1094
+ constructor(options = {}) {
464
1095
  this.worker = null;
1096
+ this.ocrWorker = null;
1097
+ this.initialized = false;
1098
+ this.options = {
1099
+ useWorker: isWorkerSupported(),
1100
+ enableCache: true,
1101
+ cacheSize: 50,
1102
+ maxImageDimension: 1000,
1103
+ logger: console.log,
1104
+ ...options
1105
+ };
1106
+ // 初始化缓存
1107
+ this.resultCache = new LRUCache(this.options.cacheSize);
465
1108
  }
466
1109
  /**
467
1110
  * 初始化OCR引擎
@@ -471,15 +1114,28 @@
471
1114
  * @returns {Promise<void>} 初始化完成的Promise
472
1115
  */
473
1116
  async initialize() {
474
- this.worker = tesseract_js.createWorker({
475
- logger: (m) => console.log(m)
476
- });
477
- await this.worker.load();
478
- await this.worker.loadLanguage('chi_sim');
479
- await this.worker.initialize('chi_sim');
480
- await this.worker.setParameters({
481
- tessedit_char_whitelist: '0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期'
482
- });
1117
+ if (this.initialized)
1118
+ return;
1119
+ if (this.options.useWorker) {
1120
+ // 使用自定义Worker线程处理OCR
1121
+ this.ocrWorker = createWorker(processOCRInWorker);
1122
+ this.initialized = true;
1123
+ this.options.logger?.('OCR Worker 初始化完成');
1124
+ }
1125
+ else {
1126
+ // 使用主线程处理OCR
1127
+ this.worker = tesseract_js.createWorker({
1128
+ logger: this.options.logger
1129
+ });
1130
+ await this.worker.load();
1131
+ await this.worker.loadLanguage('chi_sim');
1132
+ await this.worker.initialize('chi_sim');
1133
+ await this.worker.setParameters({
1134
+ tessedit_char_whitelist: '0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期'
1135
+ });
1136
+ this.initialized = true;
1137
+ this.options.logger?.('OCR引擎初始化完成');
1138
+ }
483
1139
  }
484
1140
  /**
485
1141
  * 处理身份证图像并提取信息
@@ -487,21 +1143,57 @@
487
1143
  * @returns 提取的身份证信息
488
1144
  */
489
1145
  async processIDCard(imageData) {
490
- if (!this.worker) {
1146
+ if (!this.initialized) {
491
1147
  await this.initialize();
492
1148
  }
493
- // 图像预处理,提高OCR识别率
494
- const enhancedImage = ImageProcessor.adjustBrightnessContrast(imageData, 15, 25);
495
- // 转换ImageData为Canvas
496
- const canvas = ImageProcessor.imageDataToCanvas(enhancedImage);
1149
+ // 计算图像指纹,用于缓存查找
1150
+ if (this.options.enableCache) {
1151
+ const fingerprint = calculateImageFingerprint(imageData);
1152
+ // 检查缓存中是否有结果
1153
+ const cachedResult = this.resultCache.get(fingerprint);
1154
+ if (cachedResult) {
1155
+ this.options.logger?.('使用缓存的OCR结果');
1156
+ return cachedResult;
1157
+ }
1158
+ }
1159
+ // 图像预处理:降低分辨率和增强对比度
1160
+ const downsampledImage = ImageProcessor.downsampleForProcessing(imageData, this.options.maxImageDimension);
1161
+ const enhancedImage = ImageProcessor.adjustBrightnessContrast(downsampledImage, 15, 25);
497
1162
  // OCR识别
498
1163
  try {
499
- const { data } = await this.worker.recognize(canvas);
500
- // 解析身份证信息
501
- return this.parseIDCardText(data.text);
1164
+ let idCardInfo;
1165
+ if (this.options.useWorker && this.ocrWorker) {
1166
+ // 使用Worker线程处理
1167
+ const base64Image = ImageProcessor.imageDataToBase64(enhancedImage);
1168
+ const result = await this.ocrWorker.postMessage({
1169
+ imageBase64: base64Image,
1170
+ tessWorkerOptions: {
1171
+ logger: this.options.logger
1172
+ }
1173
+ });
1174
+ idCardInfo = result.idCardInfo;
1175
+ this.options.logger?.(`OCR处理完成,用时: ${result.processingTime.toFixed(2)}ms`);
1176
+ }
1177
+ else {
1178
+ // 使用主线程处理
1179
+ const startTime = performance.now();
1180
+ // 转换ImageData为Canvas
1181
+ const canvas = ImageProcessor.imageDataToCanvas(enhancedImage);
1182
+ const { data } = await this.worker.recognize(canvas);
1183
+ // 解析身份证信息
1184
+ idCardInfo = this.parseIDCardText(data.text);
1185
+ const processingTime = performance.now() - startTime;
1186
+ this.options.logger?.(`OCR处理完成,用时: ${processingTime.toFixed(2)}ms`);
1187
+ }
1188
+ // 缓存结果
1189
+ if (this.options.enableCache) {
1190
+ const fingerprint = calculateImageFingerprint(imageData);
1191
+ this.resultCache.set(fingerprint, idCardInfo);
1192
+ }
1193
+ return idCardInfo;
502
1194
  }
503
1195
  catch (error) {
504
- console.error('OCR识别错误:', error);
1196
+ this.options.logger?.(`OCR识别错误: ${error}`);
505
1197
  return {};
506
1198
  }
507
1199
  }
@@ -565,6 +1257,13 @@
565
1257
  }
566
1258
  return info;
567
1259
  }
1260
+ /**
1261
+ * 清除结果缓存
1262
+ */
1263
+ clearCache() {
1264
+ this.resultCache.clear();
1265
+ this.options.logger?.('OCR结果缓存已清除');
1266
+ }
568
1267
  /**
569
1268
  * 终止OCR引擎并释放资源
570
1269
  *
@@ -575,6 +1274,18 @@
575
1274
  await this.worker.terminate();
576
1275
  this.worker = null;
577
1276
  }
1277
+ if (this.ocrWorker) {
1278
+ this.ocrWorker.terminate();
1279
+ this.ocrWorker = null;
1280
+ }
1281
+ this.initialized = false;
1282
+ this.options.logger?.('OCR引擎已终止');
1283
+ }
1284
+ /**
1285
+ * 释放资源
1286
+ */
1287
+ dispose() {
1288
+ return this.terminate();
578
1289
  }
579
1290
  }
580
1291