id-scanner-lib 1.3.0 → 1.3.3

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.
@@ -151,6 +151,7 @@ function _mergeNamespaces(e,t){return t.forEach((function(t){t&&"string"!=typeof
151
151
  * @file 图像处理工具类
152
152
  * @description 提供图像预处理功能,用于提高OCR识别率
153
153
  * @module ImageProcessor
154
+ * @version 1.3.2
154
155
  */
155
156
  /**
156
157
  * 图像处理工具类
@@ -550,6 +551,278 @@ class ImageProcessor {
550
551
  // 获取新的ImageData
551
552
  return ctx.getImageData(0, 0, newWidth, newHeight);
552
553
  }
554
+ /**
555
+ * 边缘检测算法,用于识别图像中的边缘
556
+ * 基于Sobel算子实现
557
+ *
558
+ * @param imageData 原始图像数据,应已转为灰度图
559
+ * @param threshold 边缘阈值,默认为30
560
+ * @returns 检测到边缘的图像数据
561
+ */
562
+ static detectEdges(imageData, threshold = 30) {
563
+ // 确保输入图像是灰度图
564
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
565
+ const width = grayscaleImage.width;
566
+ const height = grayscaleImage.height;
567
+ const inputData = grayscaleImage.data;
568
+ const outputData = new Uint8ClampedArray(inputData.length);
569
+ // Sobel算子 - 水平和垂直方向
570
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
571
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
572
+ // 对每个像素应用Sobel算子
573
+ for (let y = 1; y < height - 1; y++) {
574
+ for (let x = 1; x < width - 1; x++) {
575
+ let gx = 0;
576
+ let gy = 0;
577
+ // 应用卷积
578
+ for (let ky = -1; ky <= 1; ky++) {
579
+ for (let kx = -1; kx <= 1; kx++) {
580
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
581
+ const pixelVal = inputData[pixelPos]; // 灰度值
582
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
583
+ gx += pixelVal * sobelX[kernelIdx];
584
+ gy += pixelVal * sobelY[kernelIdx];
585
+ }
586
+ }
587
+ // 计算梯度强度
588
+ let magnitude = Math.sqrt(gx * gx + gy * gy);
589
+ // 应用阈值
590
+ magnitude = magnitude > threshold ? 255 : 0;
591
+ // 设置输出像素
592
+ const pos = (y * width + x) * 4;
593
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
594
+ outputData[pos + 3] = 255; // 透明度保持完全不透明
595
+ }
596
+ }
597
+ // 处理边缘像素
598
+ for (let i = 0; i < width * 4; i++) {
599
+ // 顶部和底部行
600
+ outputData[i] = 0;
601
+ outputData[(height - 1) * width * 4 + i] = 0;
602
+ }
603
+ for (let i = 0; i < height; i++) {
604
+ // 左右两侧列
605
+ const leftPos = i * width * 4;
606
+ const rightPos = (i * width + width - 1) * 4;
607
+ for (let j = 0; j < 4; j++) {
608
+ outputData[leftPos + j] = 0;
609
+ outputData[rightPos + j] = 0;
610
+ }
611
+ }
612
+ return new ImageData(outputData, width, height);
613
+ }
614
+ /**
615
+ * 卡尼-德里奇边缘检测
616
+ * 相比Sobel更精确的边缘检测算法
617
+ *
618
+ * @param imageData 灰度图像数据
619
+ * @param lowThreshold 低阈值
620
+ * @param highThreshold 高阈值
621
+ * @returns 边缘检测结果
622
+ */
623
+ static cannyEdgeDetection(imageData, lowThreshold = 20, highThreshold = 50) {
624
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
625
+ // 1. 高斯模糊
626
+ const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
627
+ // 2. 使用Sobel算子计算梯度
628
+ const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
629
+ // 3. 非极大值抛弃
630
+ const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
631
+ // 4. 双阈值处理
632
+ const thresholdResult = this.hysteresisThresholding(nonMaxSuppressed, blurredImage.width, blurredImage.height, lowThreshold, highThreshold);
633
+ // 创建输出图像
634
+ const outputData = new Uint8ClampedArray(imageData.data.length);
635
+ // 将结果转换为ImageData
636
+ for (let i = 0; i < thresholdResult.length; i++) {
637
+ const pos = i * 4;
638
+ const value = thresholdResult[i] ? 255 : 0;
639
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
640
+ outputData[pos + 3] = 255;
641
+ }
642
+ return new ImageData(outputData, blurredImage.width, blurredImage.height);
643
+ }
644
+ /**
645
+ * 高斯模糊
646
+ */
647
+ static gaussianBlur(imageData, sigma = 1.5) {
648
+ const width = imageData.width;
649
+ const height = imageData.height;
650
+ const inputData = imageData.data;
651
+ const outputData = new Uint8ClampedArray(inputData.length);
652
+ // 生成高斯核
653
+ const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
654
+ const halfKernel = Math.floor(kernelSize / 2);
655
+ const kernel = this.generateGaussianKernel(kernelSize, sigma);
656
+ // 应用高斯核
657
+ for (let y = 0; y < height; y++) {
658
+ for (let x = 0; x < width; x++) {
659
+ let sum = 0;
660
+ let weightSum = 0;
661
+ for (let ky = -halfKernel; ky <= halfKernel; ky++) {
662
+ for (let kx = -halfKernel; kx <= halfKernel; kx++) {
663
+ const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
664
+ const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
665
+ const pixelPos = (pixelY * width + pixelX) * 4;
666
+ const kernelY = ky + halfKernel;
667
+ const kernelX = kx + halfKernel;
668
+ const weight = kernel[kernelY * kernelSize + kernelX];
669
+ sum += inputData[pixelPos] * weight;
670
+ weightSum += weight;
671
+ }
672
+ }
673
+ const pos = (y * width + x) * 4;
674
+ const value = Math.round(sum / weightSum);
675
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
676
+ outputData[pos + 3] = 255;
677
+ }
678
+ }
679
+ return new ImageData(outputData, width, height);
680
+ }
681
+ /**
682
+ * 生成高斯核
683
+ */
684
+ static generateGaussianKernel(size, sigma) {
685
+ const kernel = new Array(size * size);
686
+ const center = Math.floor(size / 2);
687
+ let sum = 0;
688
+ for (let y = 0; y < size; y++) {
689
+ for (let x = 0; x < size; x++) {
690
+ const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
691
+ const value = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
692
+ kernel[y * size + x] = value;
693
+ sum += value;
694
+ }
695
+ }
696
+ // 归一化
697
+ for (let i = 0; i < kernel.length; i++) {
698
+ kernel[i] /= sum;
699
+ }
700
+ return kernel;
701
+ }
702
+ /**
703
+ * 计算梯度强度和方向
704
+ */
705
+ static computeGradients(imageData) {
706
+ const width = imageData.width;
707
+ const height = imageData.height;
708
+ const inputData = imageData.data;
709
+ const gradientMagnitude = new Array(width * height);
710
+ const gradientDirection = new Array(width * height);
711
+ // Sobel算子
712
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
713
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
714
+ for (let y = 1; y < height - 1; y++) {
715
+ for (let x = 1; x < width - 1; x++) {
716
+ let gx = 0;
717
+ let gy = 0;
718
+ for (let ky = -1; ky <= 1; ky++) {
719
+ for (let kx = -1; kx <= 1; kx++) {
720
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
721
+ const pixelVal = inputData[pixelPos];
722
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
723
+ gx += pixelVal * sobelX[kernelIdx];
724
+ gy += pixelVal * sobelY[kernelIdx];
725
+ }
726
+ }
727
+ const idx = y * width + x;
728
+ gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
729
+ gradientDirection[idx] = Math.atan2(gy, gx);
730
+ }
731
+ }
732
+ // 处理边界
733
+ for (let y = 0; y < height; y++) {
734
+ for (let x = 0; x < width; x++) {
735
+ if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
736
+ const idx = y * width + x;
737
+ gradientMagnitude[idx] = 0;
738
+ gradientDirection[idx] = 0;
739
+ }
740
+ }
741
+ }
742
+ return { gradientMagnitude, gradientDirection };
743
+ }
744
+ /**
745
+ * 非极大值抛弃
746
+ */
747
+ static nonMaxSuppression(gradientMagnitude, gradientDirection, width, height) {
748
+ const result = new Array(width * height).fill(0);
749
+ for (let y = 1; y < height - 1; y++) {
750
+ for (let x = 1; x < width - 1; x++) {
751
+ const idx = y * width + x;
752
+ const magnitude = gradientMagnitude[idx];
753
+ const direction = gradientDirection[idx];
754
+ // 将方向转化为角度
755
+ const degrees = (direction * 180 / Math.PI + 180) % 180;
756
+ // 获取相邻像素索引
757
+ let neighbor1Idx, neighbor2Idx;
758
+ // 将方向量化为四个方向: 0°, 45°, 90°, 135°
759
+ if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
760
+ // 水平方向
761
+ neighbor1Idx = idx - 1;
762
+ neighbor2Idx = idx + 1;
763
+ }
764
+ else if (degrees >= 22.5 && degrees < 67.5) {
765
+ // 45度方向
766
+ neighbor1Idx = (y - 1) * width + (x + 1);
767
+ neighbor2Idx = (y + 1) * width + (x - 1);
768
+ }
769
+ else if (degrees >= 67.5 && degrees < 112.5) {
770
+ // 垂直方向
771
+ neighbor1Idx = (y - 1) * width + x;
772
+ neighbor2Idx = (y + 1) * width + x;
773
+ }
774
+ else {
775
+ // 135度方向
776
+ neighbor1Idx = (y - 1) * width + (x - 1);
777
+ neighbor2Idx = (y + 1) * width + (x + 1);
778
+ }
779
+ // 检查当前像素是否是最大值
780
+ if (magnitude >= gradientMagnitude[neighbor1Idx] &&
781
+ magnitude >= gradientMagnitude[neighbor2Idx]) {
782
+ result[idx] = magnitude;
783
+ }
784
+ }
785
+ }
786
+ return result;
787
+ }
788
+ /**
789
+ * 双阈值处理
790
+ */
791
+ static hysteresisThresholding(nonMaxSuppressed, width, height, lowThreshold, highThreshold) {
792
+ const result = new Array(width * height).fill(false);
793
+ const visited = new Array(width * height).fill(false);
794
+ const stack = [];
795
+ // 标记强边缘点
796
+ for (let i = 0; i < nonMaxSuppressed.length; i++) {
797
+ if (nonMaxSuppressed[i] >= highThreshold) {
798
+ result[i] = true;
799
+ stack.push(i);
800
+ visited[i] = true;
801
+ }
802
+ }
803
+ // 使用深度优先搜索连接弱边缘
804
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
805
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
806
+ while (stack.length > 0) {
807
+ const currentIdx = stack.pop();
808
+ const currentX = currentIdx % width;
809
+ const currentY = Math.floor(currentIdx / width);
810
+ // 检查88个相邻方向
811
+ for (let i = 0; i < 8; i++) {
812
+ const newX = currentX + dx[i];
813
+ const newY = currentY + dy[i];
814
+ if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
815
+ const newIdx = newY * width + newX;
816
+ if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
817
+ result[newIdx] = true;
818
+ stack.push(newIdx);
819
+ visited[newIdx] = true;
820
+ }
821
+ }
822
+ }
823
+ }
824
+ return result;
825
+ }
553
826
  }
554
827
 
555
828
  /**
@@ -715,6 +988,7 @@ function calculateImageFingerprint(imageData, size = 8) {
715
988
  * @file 身份证检测模块
716
989
  * @description 提供自动检测和定位图像中的身份证功能
717
990
  * @module IDCardDetector
991
+ * @version 1.3.2
718
992
  */
719
993
  /**
720
994
  * 身份证检测器类
@@ -922,25 +1196,24 @@ class IDCardDetector {
922
1196
  */
923
1197
  async detectIDCard(imageData) {
924
1198
  // 1. 图像预处理
925
- ImageProcessor.toGrayscale(imageData);
926
- // 2. 检测矩形和边缘(简化版实现)
927
- // 注意:实际应用中应使用OpenCV.js或其他计算机视觉库进行更精确的检测
928
- // 此处仅作为概念性实现,使用基本矩形检测逻辑
929
- // 模拟检测过程,随机判断是否找到身份证
930
- // 在实际应用中,此处应当实现实际的计算机视觉算法
1199
+ const grayscale = ImageProcessor.toGrayscale(imageData);
1200
+ // 2. 使用Sobel边缘检测算法检测边缘
1201
+ const edgeData = ImageProcessor.detectEdges(grayscale);
1202
+ // 3. 检测矩形和边缘
1203
+ // 使用基于边缘的矩形检测
1204
+ const rectangles = this.detectRectangles(edgeData);
1205
+ // 4. 评估检测结果 - 检查是否找到了合适的矩形
1206
+ const idCardRect = this.findIdCardRectangle(rectangles, imageData.width, imageData.height);
931
1207
  const detectionResult = {
932
- success: Math.random() > 0.3, // 70%的概率成功检测到
933
- message: "身份证检测完成",
1208
+ success: idCardRect !== null,
1209
+ message: idCardRect ? "身份证检测成功" : "未检测到身份证",
934
1210
  };
935
- if (detectionResult.success) {
936
- // 模拟一个身份证矩形区域
937
- const width = imageData.width;
938
- const height = imageData.height;
939
- // 大致的身份证区域(按比例)
940
- const rectWidth = Math.round(width * 0.7);
941
- const rectHeight = Math.round(rectWidth * 0.618); // 身份证是黄金比例
942
- const rectX = Math.round((width - rectWidth) / 2);
943
- const rectY = Math.round((height - rectHeight) / 2);
1211
+ if (detectionResult.success && idCardRect) {
1212
+ // 使用实际检测到的身份证区域
1213
+ const rectWidth = idCardRect.width;
1214
+ const rectHeight = idCardRect.height;
1215
+ const rectX = idCardRect.x;
1216
+ const rectY = idCardRect.y;
944
1217
  // 添加四个角点
945
1218
  detectionResult.corners = [
946
1219
  { x: rectX, y: rectY },
@@ -965,8 +1238,8 @@ class IDCardDetector {
965
1238
  ctx.drawImage(tempCanvas, rectX, rectY, rectWidth, rectHeight, 0, 0, rectWidth, rectHeight);
966
1239
  detectionResult.croppedImage = ctx.getImageData(0, 0, rectWidth, rectHeight);
967
1240
  }
968
- // 设置置信度
969
- detectionResult.confidence = 0.7 + Math.random() * 0.3;
1241
+ // 设置置信度 - 基于边缘强度和矩形形状评分
1242
+ detectionResult.confidence = this.calculateConfidence(idCardRect, edgeData);
970
1243
  }
971
1244
  return detectionResult;
972
1245
  }
@@ -985,7 +1258,123 @@ class IDCardDetector {
985
1258
  this.camera.release();
986
1259
  this.resultCache.clear();
987
1260
  }
1261
+ /**
1262
+ * 从边缘图像中检测矩形
1263
+ * @param edgeData 边缘检测后的图像数据
1264
+ * @returns 检测到的矩形数组
1265
+ */
1266
+ detectRectangles(edgeData) {
1267
+ const width = edgeData.width;
1268
+ const height = edgeData.height;
1269
+ const minSize = Math.min(width, height) * 0.2; // 最小矩形尺寸
1270
+ const rectangles = [];
1271
+ // 使用积分图像加速边缘密度计算
1272
+ const integralImg = new Uint32Array(width * height);
1273
+ // 计算积分图像
1274
+ for (let y = 0; y < height; y++) {
1275
+ for (let x = 0; x < width; x++) {
1276
+ const idx = y * width + x;
1277
+ const pixel = (edgeData.data[idx * 4] > 128) ? 1 : 0; // 边缘为白色
1278
+ // 计算积分图
1279
+ const above = y > 0 ? integralImg[(y - 1) * width + x] : 0;
1280
+ const left = x > 0 ? integralImg[y * width + (x - 1)] : 0;
1281
+ const diagonal = (x > 0 && y > 0) ? integralImg[(y - 1) * width + (x - 1)] : 0;
1282
+ integralImg[idx] = pixel + above + left - diagonal;
1283
+ }
1284
+ }
1285
+ // 滑动窗口检测矩形
1286
+ for (let h = minSize; h < height * 0.9; h += Math.max(2, Math.floor(h * 0.05))) {
1287
+ // 计算当前高度下,按照标准身份证比例的宽度
1288
+ const w = Math.round(h * IDCardDetector.ID_CARD_ASPECT_RATIO);
1289
+ if (w > width * 0.9)
1290
+ continue;
1291
+ for (let y = 0; y < height - h; y += Math.max(2, Math.floor(h * 0.1))) {
1292
+ for (let x = 0; x < width - w; x += Math.max(2, Math.floor(w * 0.1))) {
1293
+ // 计算矩形区域内的边缘密度
1294
+ const edgeCount = this.calculateRectSum(integralImg, x, y, w, h, width);
1295
+ const avgEdgeDensity = edgeCount / (w * h);
1296
+ // 计算矩形边界的边缘密度
1297
+ const perimeterEdgeCount = this.calculateRectPerimeter(integralImg, x, y, w, h, width);
1298
+ const perimeterLength = 2 * (w + h);
1299
+ const perimeterDensity = perimeterEdgeCount / perimeterLength;
1300
+ // 矩形得分 - 边界边缘密度高且内部适中
1301
+ const rectScore = perimeterDensity * 0.7 + (0.3 - Math.abs(0.15 - avgEdgeDensity)) * 0.3;
1302
+ if (rectScore > 0.4) { // 阈值可根据实际项目调整
1303
+ rectangles.push({
1304
+ x,
1305
+ y,
1306
+ width: w,
1307
+ height: h,
1308
+ confidence: rectScore
1309
+ });
1310
+ }
1311
+ }
1312
+ }
1313
+ }
1314
+ // 按得分排序
1315
+ return rectangles.sort((a, b) => b.confidence - a.confidence);
1316
+ }
1317
+ /**
1318
+ * 使用积分图计算矩形区域内的总和
1319
+ */
1320
+ calculateRectSum(integral, x, y, w, h, stride) {
1321
+ const x2 = Math.min(x + w - 1, stride - 1);
1322
+ const y2 = Math.min(y + h - 1, integral.length / stride - 1);
1323
+ const topLeft = (x > 0 && y > 0) ? integral[(y - 1) * stride + (x - 1)] : 0;
1324
+ const topRight = y > 0 ? integral[(y - 1) * stride + x2] : 0;
1325
+ const bottomLeft = x > 0 ? integral[y2 * stride + (x - 1)] : 0;
1326
+ const bottomRight = integral[y2 * stride + x2];
1327
+ return bottomRight - topRight - bottomLeft + topLeft;
1328
+ }
1329
+ /**
1330
+ * 计算矩形周长上的边缘点数量
1331
+ */
1332
+ calculateRectPerimeter(integral, x, y, w, h, stride) {
1333
+ // 上边缘
1334
+ const topEdgeSum = this.calculateRectSum(integral, x, y, w, 1, stride);
1335
+ // 下边缘
1336
+ const bottomEdgeSum = this.calculateRectSum(integral, x, y + h - 1, w, 1, stride);
1337
+ // 左边缘
1338
+ const leftEdgeSum = this.calculateRectSum(integral, x, y, 1, h, stride);
1339
+ // 右边缘
1340
+ const rightEdgeSum = this.calculateRectSum(integral, x + w - 1, y, 1, h, stride);
1341
+ return topEdgeSum + bottomEdgeSum + leftEdgeSum + rightEdgeSum;
1342
+ }
1343
+ /**
1344
+ * 从检测到的矩形中找出最可能是身份证的矩形
1345
+ */
1346
+ findIdCardRectangle(rectangles, imageWidth, imageHeight) {
1347
+ if (rectangles.length === 0)
1348
+ return null;
1349
+ // 筛选符合身份证宽高比的矩形
1350
+ const filteredRects = rectangles.filter(rect => {
1351
+ const aspectRatio = rect.width / rect.height;
1352
+ return Math.abs(aspectRatio - IDCardDetector.ID_CARD_ASPECT_RATIO) < 0.2; // 允许20%的误差
1353
+ });
1354
+ if (filteredRects.length === 0)
1355
+ return null;
1356
+ // 返回得分最高的矩形
1357
+ return filteredRects[0];
1358
+ }
1359
+ /**
1360
+ * 计算身份证检测的置信度
1361
+ */
1362
+ calculateConfidence(rect, edgeData) {
1363
+ if (!rect)
1364
+ return 0;
1365
+ // 基本得分来自矩形检测
1366
+ let score = rect.confidence;
1367
+ // 额外因素:矩形大小相对于图像
1368
+ const relativeSize = (rect.width * rect.height) / (edgeData.width * edgeData.height);
1369
+ if (relativeSize > 0.1 && relativeSize < 0.7) {
1370
+ score += 0.1; // 身份证通常占据图像的合理比例
1371
+ }
1372
+ // 范围限制在0-1之间
1373
+ return Math.min(Math.max(score, 0), 1);
1374
+ }
988
1375
  }
1376
+ // 身份证标准宽高比(近似黄金比例)
1377
+ IDCardDetector.ID_CARD_ASPECT_RATIO = 1.58; // 标准身份证宽高比
989
1378
 
990
1379
  /**
991
1380
  * @file Web Worker辅助工具类
@@ -1081,16 +1470,16 @@ async function processOCRInWorker(input) {
1081
1470
  // 加载Tesseract.js (Worker 环境下动态导入)
1082
1471
  const { createWorker } = await import('tesseract.js');
1083
1472
  // 创建OCR Worker
1084
- const worker = await createWorker(input.tessWorkerOptions || {
1085
- logger: (m) => console.log(m)
1086
- }); // 添加类型断言,避免TypeScript错误
1473
+ const worker = (await createWorker(input.tessWorkerOptions || {
1474
+ logger: (m) => console.log(m),
1475
+ })); // 添加类型断言,避免TypeScript错误
1087
1476
  try {
1088
1477
  // 初始化OCR引擎
1089
1478
  await worker.load();
1090
- await worker.loadLanguage('chi_sim');
1091
- await worker.initialize('chi_sim');
1479
+ await worker.loadLanguage("chi_sim");
1480
+ await worker.initialize("chi_sim");
1092
1481
  await worker.setParameters({
1093
- tessedit_char_whitelist: '0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期'
1482
+ tessedit_char_whitelist: "0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期",
1094
1483
  });
1095
1484
  // 识别图像
1096
1485
  const { data } = await worker.recognize(input.imageBase64);
@@ -1103,7 +1492,7 @@ async function processOCRInWorker(input) {
1103
1492
  // 返回处理结果
1104
1493
  return {
1105
1494
  idCardInfo,
1106
- processingTime
1495
+ processingTime,
1107
1496
  };
1108
1497
  }
1109
1498
  catch (error) {
@@ -1124,7 +1513,7 @@ async function processOCRInWorker(input) {
1124
1513
  function parseIDCardText(text) {
1125
1514
  const info = {};
1126
1515
  // 拆分为行
1127
- const lines = text.split('\n').filter(line => line.trim());
1516
+ const lines = text.split("\n").filter((line) => line.trim());
1128
1517
  // 解析身份证号码(最容易识别的部分)
1129
1518
  const idNumberRegex = /(\d{17}[\dX])/;
1130
1519
  const idNumberMatch = text.match(idNumberRegex);
@@ -1133,8 +1522,9 @@ function parseIDCardText(text) {
1133
1522
  }
1134
1523
  // 解析姓名
1135
1524
  for (const line of lines) {
1136
- if (line.includes('姓名') || line.length < 10 && line.length > 1 && !/\d/.test(line)) {
1137
- info.name = line.replace('姓名', '').trim();
1525
+ if (line.includes("姓名") ||
1526
+ (line.length < 10 && line.length > 1 && !/\d/.test(line))) {
1527
+ info.name = line.replace("姓名", "").trim();
1138
1528
  break;
1139
1529
  }
1140
1530
  }
@@ -1144,7 +1534,9 @@ function parseIDCardText(text) {
1144
1534
  if (genderMatch) {
1145
1535
  info.gender = genderMatch[1];
1146
1536
  const nationalityText = genderMatch[0];
1147
- info.nationality = nationalityText.substring(nationalityText.indexOf(genderMatch[1]) + 1).trim();
1537
+ info.nationality = nationalityText
1538
+ .substring(nationalityText.indexOf(genderMatch[1]) + 1)
1539
+ .trim();
1148
1540
  }
1149
1541
  // 解析出生日期
1150
1542
  const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/;
@@ -1156,19 +1548,19 @@ function parseIDCardText(text) {
1156
1548
  const addressRegex = /住址([\s\S]*?)公民身份号码/;
1157
1549
  const addressMatch = text.match(addressRegex);
1158
1550
  if (addressMatch) {
1159
- info.address = addressMatch[1].replace(/\n/g, '').trim();
1551
+ info.address = addressMatch[1].replace(/\n/g, "").trim();
1160
1552
  }
1161
1553
  // 解析签发机关
1162
1554
  const authorityRegex = /签发机关([\s\S]*?)有效期/;
1163
1555
  const authorityMatch = text.match(authorityRegex);
1164
1556
  if (authorityMatch) {
1165
- info.issuingAuthority = authorityMatch[1].replace(/\n/g, '').trim();
1557
+ info.issuingAuthority = authorityMatch[1].replace(/\n/g, "").trim();
1166
1558
  }
1167
1559
  // 解析有效期限
1168
1560
  const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/;
1169
1561
  const validPeriodMatch = text.match(validPeriodRegex);
1170
1562
  if (validPeriodMatch) {
1171
- info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim();
1563
+ info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
1172
1564
  }
1173
1565
  return info;
1174
1566
  }
@@ -1177,6 +1569,7 @@ function parseIDCardText(text) {
1177
1569
  * @file OCR处理模块
1178
1570
  * @description 提供身份证文字识别和信息提取功能
1179
1571
  * @module OCRProcessor
1572
+ * @version 1.3.2
1180
1573
  */
1181
1574
  /**
1182
1575
  * OCR处理器类
@@ -1338,57 +1731,184 @@ class OCRProcessor {
1338
1731
  * @param {string} text - OCR识别到的文本
1339
1732
  * @returns {IDCardInfo} 提取到的身份证信息对象
1340
1733
  */
1734
+ /**
1735
+ * 格式化日期字符串为标准格式 (YYYY-MM-DD)
1736
+ * @param dateStr 原始日期字符串
1737
+ * @returns 格式化后的日期字符串
1738
+ */
1739
+ formatDateString(dateStr) {
1740
+ // 先尝试提取年月日
1741
+ const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
1742
+ if (dateMatch) {
1743
+ const year = dateMatch[1];
1744
+ const month = dateMatch[2].padStart(2, '0');
1745
+ const day = dateMatch[3].padStart(2, '0');
1746
+ return `${year}-${month}-${day}`;
1747
+ }
1748
+ // 如果是纯数字格式如 20220101
1749
+ if (/^\d{8}$/.test(dateStr)) {
1750
+ const year = dateStr.substring(0, 4);
1751
+ const month = dateStr.substring(4, 6);
1752
+ const day = dateStr.substring(6, 8);
1753
+ return `${year}-${month}-${day}`;
1754
+ }
1755
+ // 如果无法格式化,返回原始字符串
1756
+ return dateStr;
1757
+ }
1758
+ /**
1759
+ * 验证身份证号是否符合规则
1760
+ * @param idNumber 身份证号
1761
+ * @returns 是否有效
1762
+ */
1763
+ validateIDNumber(idNumber) {
1764
+ // 基本验证,校验位有效性和长度
1765
+ if (!idNumber || idNumber.length !== 18) {
1766
+ return false;
1767
+ }
1768
+ // 检查格式,前17位必须为数字,最后一位可以是数字或'X'
1769
+ const pattern = /^\d{17}[\dX]$/;
1770
+ if (!pattern.test(idNumber)) {
1771
+ return false;
1772
+ }
1773
+ // 检查日期部分
1774
+ parseInt(idNumber.substr(6, 4));
1775
+ const month = parseInt(idNumber.substr(10, 2));
1776
+ const day = parseInt(idNumber.substr(12, 2));
1777
+ if (month < 1 || month > 12 || day < 1 || day > 31) {
1778
+ return false;
1779
+ }
1780
+ // 更详细的检查可以添加校验位的验证等逻辑...
1781
+ return true;
1782
+ }
1341
1783
  parseIDCardText(text) {
1342
1784
  const info = {};
1343
- // 拆分为行
1344
- const lines = text.split("\n").filter((line) => line.trim());
1345
- // 解析身份证号码(最容易识别的部分)
1785
+ // 预处理文本,清除多余空白
1786
+ const processedText = text.replace(/\s+/g, " ").trim();
1787
+ // 拆分为行,并过滤空行
1788
+ const lines = processedText.split("\n").filter((line) => line.trim());
1789
+ // 解析身份证号码 - 多种模式匹配
1790
+ // 1. 普通18位身份证号模式
1346
1791
  const idNumberRegex = /(\d{17}[\dX])/;
1347
- const idNumberMatch = text.match(idNumberRegex);
1348
- if (idNumberMatch) {
1349
- info.idNumber = idNumberMatch[1];
1350
- }
1351
- // 解析姓名
1352
- for (const line of lines) {
1353
- if (line.includes("姓名") ||
1354
- (line.length < 10 && line.length > 1 && !/\d/.test(line))) {
1355
- info.name = line.replace("姓名", "").trim();
1356
- break;
1792
+ // 2. 带前缀的模式
1793
+ const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
1794
+ // 尝试所有模式
1795
+ let idNumber = null;
1796
+ const basicMatch = processedText.match(idNumberRegex);
1797
+ const prefixMatch = processedText.match(idNumberWithPrefixRegex);
1798
+ if (prefixMatch && prefixMatch[1]) {
1799
+ idNumber = prefixMatch[1]; // 首选带前缀的匹配,因为最可靠
1800
+ }
1801
+ else if (basicMatch && basicMatch[1]) {
1802
+ idNumber = basicMatch[1]; // 其次是常规匹配
1803
+ }
1804
+ if (idNumber) {
1805
+ info.idNumber = idNumber;
1806
+ }
1807
+ // 解析姓名 - 使用多种策略
1808
+ // 1. 直接匹配姓名标签近的内容
1809
+ const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
1810
+ const nameMatch = processedText.match(nameWithLabelRegex);
1811
+ // 2. 分析行文本寻找姓名
1812
+ if (nameMatch && nameMatch[1]) {
1813
+ info.name = nameMatch[1].trim();
1814
+ }
1815
+ else {
1816
+ // 备用方案:查找短行且内容全是汉字
1817
+ for (const line of lines) {
1818
+ if (line.length >= 2 && line.length <= 5 && /^[一-龥]+$/.test(line) && !/性别|民族|住址|公民|签发|有效/.test(line)) {
1819
+ info.name = line.trim();
1820
+ break;
1821
+ }
1357
1822
  }
1358
1823
  }
1359
- // 解析性别和民族
1360
- const genderNationalityRegex = /(男|女).*(族)/;
1361
- const genderMatch = text.match(genderNationalityRegex);
1362
- if (genderMatch) {
1363
- info.gender = genderMatch[1];
1364
- const nationalityText = genderMatch[0];
1365
- info.nationality = nationalityText
1366
- .substring(nationalityText.indexOf(genderMatch[1]) + 1)
1367
- .trim();
1368
- }
1369
- // 解析出生日期
1370
- const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/;
1371
- const birthDateMatch = text.match(birthDateRegex);
1372
- if (birthDateMatch) {
1373
- info.birthDate = `${birthDateMatch[1]}-${birthDateMatch[2]}-${birthDateMatch[3]}`;
1374
- }
1375
- // 解析地址
1376
- const addressRegex = /住址([\s\S]*?)公民身份号码/;
1377
- const addressMatch = text.match(addressRegex);
1378
- if (addressMatch) {
1379
- info.address = addressMatch[1].replace(/\n/g, "").trim();
1824
+ // 解析性别和民族 - 多种模式匹配
1825
+ // 1. 标准格式匹配
1826
+ const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
1827
+ const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
1828
+ // 2. 只匹配性别
1829
+ const genderOnlyRegex = /性别[\s\:]*([男女])/;
1830
+ const genderOnlyMatch = processedText.match(genderOnlyRegex);
1831
+ // 3. 只匹配民族
1832
+ const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
1833
+ const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
1834
+ if (genderNationalityMatch) {
1835
+ info.gender = genderNationalityMatch[1];
1836
+ info.nationality = genderNationalityMatch[2];
1837
+ }
1838
+ else {
1839
+ // 分开获取
1840
+ if (genderOnlyMatch)
1841
+ info.gender = genderOnlyMatch[1];
1842
+ if (nationalityOnlyMatch)
1843
+ info.nationality = nationalityOnlyMatch[1];
1844
+ }
1845
+ // 解析出生日期 - 支持多种格式
1846
+ // 1. 标准格式:YYYY年MM月DD日
1847
+ const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
1848
+ // 2. 美式日期格式:YYYY-MM-DD或YYYY/MM/DD
1849
+ const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
1850
+ // 3. 带前缀的格式
1851
+ const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
1852
+ let birthDateMatch = processedText.match(birthDateRegex1) ||
1853
+ processedText.match(birthDateRegex2) ||
1854
+ processedText.match(birthDateRegex3);
1855
+ // 4. 从身份证号码中提取出生日期(如果上述方法失败)
1856
+ if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
1857
+ const year = info.idNumber.substring(6, 10);
1858
+ const month = info.idNumber.substring(10, 12);
1859
+ const day = info.idNumber.substring(12, 14);
1860
+ info.birthDate = `${year}-${month}-${day}`;
1861
+ }
1862
+ else if (birthDateMatch) {
1863
+ // 确保月份和日期是两位数
1864
+ const year = birthDateMatch[1];
1865
+ const month = birthDateMatch[2].padStart(2, '0');
1866
+ const day = birthDateMatch[3].padStart(2, '0');
1867
+ info.birthDate = `${year}-${month}-${day}`;
1868
+ }
1869
+ // 解析地址 - 改进的正则匹配
1870
+ // 1. 常规模式
1871
+ const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
1872
+ // 2. 更宽松的模式
1873
+ const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
1874
+ const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2);
1875
+ if (addressMatch && addressMatch[1]) {
1876
+ // 清理地址中的常见错误和多余空格
1877
+ info.address = addressMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
1878
+ // 限制地址长度并判断地址合理性
1879
+ if (info.address.length > 70) {
1880
+ info.address = info.address.substring(0, 70);
1881
+ }
1882
+ // 确保地址是合理的(不仅仅包含符号或数字)
1883
+ if (!/[一-龥]/.test(info.address)) {
1884
+ info.address = ""; // 如果没有中文字符,可能不是有效地址
1885
+ }
1380
1886
  }
1381
1887
  // 解析签发机关
1382
- const authorityRegex = /签发机关([\s\S]*?)有效期/;
1383
- const authorityMatch = text.match(authorityRegex);
1384
- if (authorityMatch) {
1385
- info.issuingAuthority = authorityMatch[1].replace(/\n/g, "").trim();
1386
- }
1387
- // 解析有效期限
1388
- const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/;
1389
- const validPeriodMatch = text.match(validPeriodRegex);
1888
+ const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
1889
+ const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
1890
+ const authorityMatch = processedText.match(authorityRegex1) || processedText.match(authorityRegex2);
1891
+ if (authorityMatch && authorityMatch[1]) {
1892
+ info.issuingAuthority = authorityMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
1893
+ }
1894
+ // 解析有效期限 - 支持多种格式
1895
+ // 1. 常规格式:YYYY.MM.DD-YYYY.MM.DD
1896
+ const validPeriodRegex1 = /有效期限[\s\:]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日\s]*)[-\s]*(至|-)[-\s]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日]*|[永久长期]*)/;
1897
+ // 2. 简化格式:YYYYMMDD-YYYYMMDD
1898
+ const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
1899
+ const validPeriodMatch = processedText.match(validPeriodRegex1) || processedText.match(validPeriodRegex2);
1390
1900
  if (validPeriodMatch) {
1391
- info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
1901
+ // 格式化为统一的有效期限形式
1902
+ if (validPeriodMatch[1] && validPeriodMatch[3]) {
1903
+ const startDate = this.formatDateString(validPeriodMatch[1]);
1904
+ const endDate = /\d/.test(validPeriodMatch[3]) ?
1905
+ this.formatDateString(validPeriodMatch[3]) :
1906
+ '长期有效';
1907
+ info.validPeriod = `${startDate}-${endDate}`;
1908
+ }
1909
+ else {
1910
+ info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim();
1911
+ }
1392
1912
  }
1393
1913
  return info;
1394
1914
  }