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.
- package/README.md +223 -25
- package/dist/id-scanner-core.esm.js +273 -0
- package/dist/id-scanner-core.js +273 -0
- package/dist/id-scanner-core.min.js +1 -1
- package/dist/id-scanner-ocr.esm.js +596 -76
- package/dist/id-scanner-ocr.js +596 -76
- package/dist/id-scanner-ocr.min.js +1 -1
- package/dist/id-scanner-qr.esm.js +273 -0
- package/dist/id-scanner-qr.js +273 -0
- package/dist/id-scanner-qr.min.js +1 -1
- package/dist/id-scanner.js +1268 -87
- package/dist/id-scanner.min.js +1 -1
- package/package.json +2 -2
- package/src/id-recognition/anti-fake-detector.ts +698 -0
- package/src/id-recognition/id-detector.ts +166 -19
- package/src/id-recognition/ocr-processor.ts +188 -41
- package/src/id-recognition/ocr-worker.ts +82 -72
- package/src/index.ts +189 -15
- package/src/types/browser-image-compression.d.ts +19 -0
- package/src/types/tesseract.d.ts +37 -0
- package/src/utils/image-processing.ts +364 -0
- package/dist/id-scanner-core.esm.js.map +0 -1
- package/dist/id-scanner-core.js.map +0 -1
- package/dist/id-scanner-core.min.js.map +0 -1
- package/dist/id-scanner-ocr.esm.js.map +0 -1
- package/dist/id-scanner-ocr.js.map +0 -1
- package/dist/id-scanner-ocr.min.js.map +0 -1
- package/dist/id-scanner-qr.esm.js.map +0 -1
- package/dist/id-scanner-qr.js.map +0 -1
- package/dist/id-scanner-qr.min.js.map +0 -1
- package/dist/id-scanner.js.map +0 -1
- package/dist/id-scanner.min.js.map +0 -1
|
@@ -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
|
-
|
|
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:
|
|
933
|
-
message: "
|
|
1208
|
+
success: idCardRect !== null,
|
|
1209
|
+
message: idCardRect ? "身份证检测成功" : "未检测到身份证",
|
|
934
1210
|
};
|
|
935
|
-
if (detectionResult.success) {
|
|
936
|
-
//
|
|
937
|
-
const
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
const
|
|
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 =
|
|
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(
|
|
1091
|
-
await worker.initialize(
|
|
1479
|
+
await worker.loadLanguage("chi_sim");
|
|
1480
|
+
await worker.initialize("chi_sim");
|
|
1092
1481
|
await worker.setParameters({
|
|
1093
|
-
tessedit_char_whitelist:
|
|
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(
|
|
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(
|
|
1137
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
-
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
-
|
|
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
|
}
|