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