id-scanner-lib 1.3.2 → 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.
@@ -284,6 +284,7 @@
284
284
  * @file 图像处理工具类
285
285
  * @description 提供图像预处理功能,用于提高OCR识别率
286
286
  * @module ImageProcessor
287
+ * @version 1.3.2
287
288
  */
288
289
  /**
289
290
  * 图像处理工具类
@@ -683,6 +684,278 @@
683
684
  // 获取新的ImageData
684
685
  return ctx.getImageData(0, 0, newWidth, newHeight);
685
686
  }
687
+ /**
688
+ * 边缘检测算法,用于识别图像中的边缘
689
+ * 基于Sobel算子实现
690
+ *
691
+ * @param imageData 原始图像数据,应已转为灰度图
692
+ * @param threshold 边缘阈值,默认为30
693
+ * @returns 检测到边缘的图像数据
694
+ */
695
+ static detectEdges(imageData, threshold = 30) {
696
+ // 确保输入图像是灰度图
697
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
698
+ const width = grayscaleImage.width;
699
+ const height = grayscaleImage.height;
700
+ const inputData = grayscaleImage.data;
701
+ const outputData = new Uint8ClampedArray(inputData.length);
702
+ // Sobel算子 - 水平和垂直方向
703
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
704
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
705
+ // 对每个像素应用Sobel算子
706
+ for (let y = 1; y < height - 1; y++) {
707
+ for (let x = 1; x < width - 1; x++) {
708
+ let gx = 0;
709
+ let gy = 0;
710
+ // 应用卷积
711
+ for (let ky = -1; ky <= 1; ky++) {
712
+ for (let kx = -1; kx <= 1; kx++) {
713
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
714
+ const pixelVal = inputData[pixelPos]; // 灰度值
715
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
716
+ gx += pixelVal * sobelX[kernelIdx];
717
+ gy += pixelVal * sobelY[kernelIdx];
718
+ }
719
+ }
720
+ // 计算梯度强度
721
+ let magnitude = Math.sqrt(gx * gx + gy * gy);
722
+ // 应用阈值
723
+ magnitude = magnitude > threshold ? 255 : 0;
724
+ // 设置输出像素
725
+ const pos = (y * width + x) * 4;
726
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
727
+ outputData[pos + 3] = 255; // 透明度保持完全不透明
728
+ }
729
+ }
730
+ // 处理边缘像素
731
+ for (let i = 0; i < width * 4; i++) {
732
+ // 顶部和底部行
733
+ outputData[i] = 0;
734
+ outputData[(height - 1) * width * 4 + i] = 0;
735
+ }
736
+ for (let i = 0; i < height; i++) {
737
+ // 左右两侧列
738
+ const leftPos = i * width * 4;
739
+ const rightPos = (i * width + width - 1) * 4;
740
+ for (let j = 0; j < 4; j++) {
741
+ outputData[leftPos + j] = 0;
742
+ outputData[rightPos + j] = 0;
743
+ }
744
+ }
745
+ return new ImageData(outputData, width, height);
746
+ }
747
+ /**
748
+ * 卡尼-德里奇边缘检测
749
+ * 相比Sobel更精确的边缘检测算法
750
+ *
751
+ * @param imageData 灰度图像数据
752
+ * @param lowThreshold 低阈值
753
+ * @param highThreshold 高阈值
754
+ * @returns 边缘检测结果
755
+ */
756
+ static cannyEdgeDetection(imageData, lowThreshold = 20, highThreshold = 50) {
757
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
758
+ // 1. 高斯模糊
759
+ const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
760
+ // 2. 使用Sobel算子计算梯度
761
+ const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
762
+ // 3. 非极大值抛弃
763
+ const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
764
+ // 4. 双阈值处理
765
+ const thresholdResult = this.hysteresisThresholding(nonMaxSuppressed, blurredImage.width, blurredImage.height, lowThreshold, highThreshold);
766
+ // 创建输出图像
767
+ const outputData = new Uint8ClampedArray(imageData.data.length);
768
+ // 将结果转换为ImageData
769
+ for (let i = 0; i < thresholdResult.length; i++) {
770
+ const pos = i * 4;
771
+ const value = thresholdResult[i] ? 255 : 0;
772
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
773
+ outputData[pos + 3] = 255;
774
+ }
775
+ return new ImageData(outputData, blurredImage.width, blurredImage.height);
776
+ }
777
+ /**
778
+ * 高斯模糊
779
+ */
780
+ static gaussianBlur(imageData, sigma = 1.5) {
781
+ const width = imageData.width;
782
+ const height = imageData.height;
783
+ const inputData = imageData.data;
784
+ const outputData = new Uint8ClampedArray(inputData.length);
785
+ // 生成高斯核
786
+ const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
787
+ const halfKernel = Math.floor(kernelSize / 2);
788
+ const kernel = this.generateGaussianKernel(kernelSize, sigma);
789
+ // 应用高斯核
790
+ for (let y = 0; y < height; y++) {
791
+ for (let x = 0; x < width; x++) {
792
+ let sum = 0;
793
+ let weightSum = 0;
794
+ for (let ky = -halfKernel; ky <= halfKernel; ky++) {
795
+ for (let kx = -halfKernel; kx <= halfKernel; kx++) {
796
+ const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
797
+ const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
798
+ const pixelPos = (pixelY * width + pixelX) * 4;
799
+ const kernelY = ky + halfKernel;
800
+ const kernelX = kx + halfKernel;
801
+ const weight = kernel[kernelY * kernelSize + kernelX];
802
+ sum += inputData[pixelPos] * weight;
803
+ weightSum += weight;
804
+ }
805
+ }
806
+ const pos = (y * width + x) * 4;
807
+ const value = Math.round(sum / weightSum);
808
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
809
+ outputData[pos + 3] = 255;
810
+ }
811
+ }
812
+ return new ImageData(outputData, width, height);
813
+ }
814
+ /**
815
+ * 生成高斯核
816
+ */
817
+ static generateGaussianKernel(size, sigma) {
818
+ const kernel = new Array(size * size);
819
+ const center = Math.floor(size / 2);
820
+ let sum = 0;
821
+ for (let y = 0; y < size; y++) {
822
+ for (let x = 0; x < size; x++) {
823
+ const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
824
+ const value = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
825
+ kernel[y * size + x] = value;
826
+ sum += value;
827
+ }
828
+ }
829
+ // 归一化
830
+ for (let i = 0; i < kernel.length; i++) {
831
+ kernel[i] /= sum;
832
+ }
833
+ return kernel;
834
+ }
835
+ /**
836
+ * 计算梯度强度和方向
837
+ */
838
+ static computeGradients(imageData) {
839
+ const width = imageData.width;
840
+ const height = imageData.height;
841
+ const inputData = imageData.data;
842
+ const gradientMagnitude = new Array(width * height);
843
+ const gradientDirection = new Array(width * height);
844
+ // Sobel算子
845
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
846
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
847
+ for (let y = 1; y < height - 1; y++) {
848
+ for (let x = 1; x < width - 1; x++) {
849
+ let gx = 0;
850
+ let gy = 0;
851
+ for (let ky = -1; ky <= 1; ky++) {
852
+ for (let kx = -1; kx <= 1; kx++) {
853
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
854
+ const pixelVal = inputData[pixelPos];
855
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
856
+ gx += pixelVal * sobelX[kernelIdx];
857
+ gy += pixelVal * sobelY[kernelIdx];
858
+ }
859
+ }
860
+ const idx = y * width + x;
861
+ gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
862
+ gradientDirection[idx] = Math.atan2(gy, gx);
863
+ }
864
+ }
865
+ // 处理边界
866
+ for (let y = 0; y < height; y++) {
867
+ for (let x = 0; x < width; x++) {
868
+ if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
869
+ const idx = y * width + x;
870
+ gradientMagnitude[idx] = 0;
871
+ gradientDirection[idx] = 0;
872
+ }
873
+ }
874
+ }
875
+ return { gradientMagnitude, gradientDirection };
876
+ }
877
+ /**
878
+ * 非极大值抛弃
879
+ */
880
+ static nonMaxSuppression(gradientMagnitude, gradientDirection, width, height) {
881
+ const result = new Array(width * height).fill(0);
882
+ for (let y = 1; y < height - 1; y++) {
883
+ for (let x = 1; x < width - 1; x++) {
884
+ const idx = y * width + x;
885
+ const magnitude = gradientMagnitude[idx];
886
+ const direction = gradientDirection[idx];
887
+ // 将方向转化为角度
888
+ const degrees = (direction * 180 / Math.PI + 180) % 180;
889
+ // 获取相邻像素索引
890
+ let neighbor1Idx, neighbor2Idx;
891
+ // 将方向量化为四个方向: 0°, 45°, 90°, 135°
892
+ if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
893
+ // 水平方向
894
+ neighbor1Idx = idx - 1;
895
+ neighbor2Idx = idx + 1;
896
+ }
897
+ else if (degrees >= 22.5 && degrees < 67.5) {
898
+ // 45度方向
899
+ neighbor1Idx = (y - 1) * width + (x + 1);
900
+ neighbor2Idx = (y + 1) * width + (x - 1);
901
+ }
902
+ else if (degrees >= 67.5 && degrees < 112.5) {
903
+ // 垂直方向
904
+ neighbor1Idx = (y - 1) * width + x;
905
+ neighbor2Idx = (y + 1) * width + x;
906
+ }
907
+ else {
908
+ // 135度方向
909
+ neighbor1Idx = (y - 1) * width + (x - 1);
910
+ neighbor2Idx = (y + 1) * width + (x + 1);
911
+ }
912
+ // 检查当前像素是否是最大值
913
+ if (magnitude >= gradientMagnitude[neighbor1Idx] &&
914
+ magnitude >= gradientMagnitude[neighbor2Idx]) {
915
+ result[idx] = magnitude;
916
+ }
917
+ }
918
+ }
919
+ return result;
920
+ }
921
+ /**
922
+ * 双阈值处理
923
+ */
924
+ static hysteresisThresholding(nonMaxSuppressed, width, height, lowThreshold, highThreshold) {
925
+ const result = new Array(width * height).fill(false);
926
+ const visited = new Array(width * height).fill(false);
927
+ const stack = [];
928
+ // 标记强边缘点
929
+ for (let i = 0; i < nonMaxSuppressed.length; i++) {
930
+ if (nonMaxSuppressed[i] >= highThreshold) {
931
+ result[i] = true;
932
+ stack.push(i);
933
+ visited[i] = true;
934
+ }
935
+ }
936
+ // 使用深度优先搜索连接弱边缘
937
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
938
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
939
+ while (stack.length > 0) {
940
+ const currentIdx = stack.pop();
941
+ const currentX = currentIdx % width;
942
+ const currentY = Math.floor(currentIdx / width);
943
+ // 检查88个相邻方向
944
+ for (let i = 0; i < 8; i++) {
945
+ const newX = currentX + dx[i];
946
+ const newY = currentY + dy[i];
947
+ if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
948
+ const newIdx = newY * width + newX;
949
+ if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
950
+ result[newIdx] = true;
951
+ stack.push(newIdx);
952
+ visited[newIdx] = true;
953
+ }
954
+ }
955
+ }
956
+ }
957
+ return result;
958
+ }
686
959
  }
687
960
 
688
961
  /**
@@ -1046,6 +1319,7 @@
1046
1319
  * @file 身份证检测模块
1047
1320
  * @description 提供自动检测和定位图像中的身份证功能
1048
1321
  * @module IDCardDetector
1322
+ * @version 1.3.2
1049
1323
  */
1050
1324
  /**
1051
1325
  * 身份证检测器类
@@ -1253,25 +1527,24 @@
1253
1527
  */
1254
1528
  async detectIDCard(imageData) {
1255
1529
  // 1. 图像预处理
1256
- ImageProcessor.toGrayscale(imageData);
1257
- // 2. 检测矩形和边缘(简化版实现)
1258
- // 注意:实际应用中应使用OpenCV.js或其他计算机视觉库进行更精确的检测
1259
- // 此处仅作为概念性实现,使用基本矩形检测逻辑
1260
- // 模拟检测过程,随机判断是否找到身份证
1261
- // 在实际应用中,此处应当实现实际的计算机视觉算法
1530
+ const grayscale = ImageProcessor.toGrayscale(imageData);
1531
+ // 2. 使用Sobel边缘检测算法检测边缘
1532
+ const edgeData = ImageProcessor.detectEdges(grayscale);
1533
+ // 3. 检测矩形和边缘
1534
+ // 使用基于边缘的矩形检测
1535
+ const rectangles = this.detectRectangles(edgeData);
1536
+ // 4. 评估检测结果 - 检查是否找到了合适的矩形
1537
+ const idCardRect = this.findIdCardRectangle(rectangles, imageData.width, imageData.height);
1262
1538
  const detectionResult = {
1263
- success: Math.random() > 0.3, // 70%的概率成功检测到
1264
- message: "身份证检测完成",
1539
+ success: idCardRect !== null,
1540
+ message: idCardRect ? "身份证检测成功" : "未检测到身份证",
1265
1541
  };
1266
- if (detectionResult.success) {
1267
- // 模拟一个身份证矩形区域
1268
- const width = imageData.width;
1269
- const height = imageData.height;
1270
- // 大致的身份证区域(按比例)
1271
- const rectWidth = Math.round(width * 0.7);
1272
- const rectHeight = Math.round(rectWidth * 0.618); // 身份证是黄金比例
1273
- const rectX = Math.round((width - rectWidth) / 2);
1274
- const rectY = Math.round((height - rectHeight) / 2);
1542
+ if (detectionResult.success && idCardRect) {
1543
+ // 使用实际检测到的身份证区域
1544
+ const rectWidth = idCardRect.width;
1545
+ const rectHeight = idCardRect.height;
1546
+ const rectX = idCardRect.x;
1547
+ const rectY = idCardRect.y;
1275
1548
  // 添加四个角点
1276
1549
  detectionResult.corners = [
1277
1550
  { x: rectX, y: rectY },
@@ -1296,8 +1569,8 @@
1296
1569
  ctx.drawImage(tempCanvas, rectX, rectY, rectWidth, rectHeight, 0, 0, rectWidth, rectHeight);
1297
1570
  detectionResult.croppedImage = ctx.getImageData(0, 0, rectWidth, rectHeight);
1298
1571
  }
1299
- // 设置置信度
1300
- detectionResult.confidence = 0.7 + Math.random() * 0.3;
1572
+ // 设置置信度 - 基于边缘强度和矩形形状评分
1573
+ detectionResult.confidence = this.calculateConfidence(idCardRect, edgeData);
1301
1574
  }
1302
1575
  return detectionResult;
1303
1576
  }
@@ -1316,7 +1589,123 @@
1316
1589
  this.camera.release();
1317
1590
  this.resultCache.clear();
1318
1591
  }
1592
+ /**
1593
+ * 从边缘图像中检测矩形
1594
+ * @param edgeData 边缘检测后的图像数据
1595
+ * @returns 检测到的矩形数组
1596
+ */
1597
+ detectRectangles(edgeData) {
1598
+ const width = edgeData.width;
1599
+ const height = edgeData.height;
1600
+ const minSize = Math.min(width, height) * 0.2; // 最小矩形尺寸
1601
+ const rectangles = [];
1602
+ // 使用积分图像加速边缘密度计算
1603
+ const integralImg = new Uint32Array(width * height);
1604
+ // 计算积分图像
1605
+ for (let y = 0; y < height; y++) {
1606
+ for (let x = 0; x < width; x++) {
1607
+ const idx = y * width + x;
1608
+ const pixel = (edgeData.data[idx * 4] > 128) ? 1 : 0; // 边缘为白色
1609
+ // 计算积分图
1610
+ const above = y > 0 ? integralImg[(y - 1) * width + x] : 0;
1611
+ const left = x > 0 ? integralImg[y * width + (x - 1)] : 0;
1612
+ const diagonal = (x > 0 && y > 0) ? integralImg[(y - 1) * width + (x - 1)] : 0;
1613
+ integralImg[idx] = pixel + above + left - diagonal;
1614
+ }
1615
+ }
1616
+ // 滑动窗口检测矩形
1617
+ for (let h = minSize; h < height * 0.9; h += Math.max(2, Math.floor(h * 0.05))) {
1618
+ // 计算当前高度下,按照标准身份证比例的宽度
1619
+ const w = Math.round(h * IDCardDetector.ID_CARD_ASPECT_RATIO);
1620
+ if (w > width * 0.9)
1621
+ continue;
1622
+ for (let y = 0; y < height - h; y += Math.max(2, Math.floor(h * 0.1))) {
1623
+ for (let x = 0; x < width - w; x += Math.max(2, Math.floor(w * 0.1))) {
1624
+ // 计算矩形区域内的边缘密度
1625
+ const edgeCount = this.calculateRectSum(integralImg, x, y, w, h, width);
1626
+ const avgEdgeDensity = edgeCount / (w * h);
1627
+ // 计算矩形边界的边缘密度
1628
+ const perimeterEdgeCount = this.calculateRectPerimeter(integralImg, x, y, w, h, width);
1629
+ const perimeterLength = 2 * (w + h);
1630
+ const perimeterDensity = perimeterEdgeCount / perimeterLength;
1631
+ // 矩形得分 - 边界边缘密度高且内部适中
1632
+ const rectScore = perimeterDensity * 0.7 + (0.3 - Math.abs(0.15 - avgEdgeDensity)) * 0.3;
1633
+ if (rectScore > 0.4) { // 阈值可根据实际项目调整
1634
+ rectangles.push({
1635
+ x,
1636
+ y,
1637
+ width: w,
1638
+ height: h,
1639
+ confidence: rectScore
1640
+ });
1641
+ }
1642
+ }
1643
+ }
1644
+ }
1645
+ // 按得分排序
1646
+ return rectangles.sort((a, b) => b.confidence - a.confidence);
1647
+ }
1648
+ /**
1649
+ * 使用积分图计算矩形区域内的总和
1650
+ */
1651
+ calculateRectSum(integral, x, y, w, h, stride) {
1652
+ const x2 = Math.min(x + w - 1, stride - 1);
1653
+ const y2 = Math.min(y + h - 1, integral.length / stride - 1);
1654
+ const topLeft = (x > 0 && y > 0) ? integral[(y - 1) * stride + (x - 1)] : 0;
1655
+ const topRight = y > 0 ? integral[(y - 1) * stride + x2] : 0;
1656
+ const bottomLeft = x > 0 ? integral[y2 * stride + (x - 1)] : 0;
1657
+ const bottomRight = integral[y2 * stride + x2];
1658
+ return bottomRight - topRight - bottomLeft + topLeft;
1659
+ }
1660
+ /**
1661
+ * 计算矩形周长上的边缘点数量
1662
+ */
1663
+ calculateRectPerimeter(integral, x, y, w, h, stride) {
1664
+ // 上边缘
1665
+ const topEdgeSum = this.calculateRectSum(integral, x, y, w, 1, stride);
1666
+ // 下边缘
1667
+ const bottomEdgeSum = this.calculateRectSum(integral, x, y + h - 1, w, 1, stride);
1668
+ // 左边缘
1669
+ const leftEdgeSum = this.calculateRectSum(integral, x, y, 1, h, stride);
1670
+ // 右边缘
1671
+ const rightEdgeSum = this.calculateRectSum(integral, x + w - 1, y, 1, h, stride);
1672
+ return topEdgeSum + bottomEdgeSum + leftEdgeSum + rightEdgeSum;
1673
+ }
1674
+ /**
1675
+ * 从检测到的矩形中找出最可能是身份证的矩形
1676
+ */
1677
+ findIdCardRectangle(rectangles, imageWidth, imageHeight) {
1678
+ if (rectangles.length === 0)
1679
+ return null;
1680
+ // 筛选符合身份证宽高比的矩形
1681
+ const filteredRects = rectangles.filter(rect => {
1682
+ const aspectRatio = rect.width / rect.height;
1683
+ return Math.abs(aspectRatio - IDCardDetector.ID_CARD_ASPECT_RATIO) < 0.2; // 允许20%的误差
1684
+ });
1685
+ if (filteredRects.length === 0)
1686
+ return null;
1687
+ // 返回得分最高的矩形
1688
+ return filteredRects[0];
1689
+ }
1690
+ /**
1691
+ * 计算身份证检测的置信度
1692
+ */
1693
+ calculateConfidence(rect, edgeData) {
1694
+ if (!rect)
1695
+ return 0;
1696
+ // 基本得分来自矩形检测
1697
+ let score = rect.confidence;
1698
+ // 额外因素:矩形大小相对于图像
1699
+ const relativeSize = (rect.width * rect.height) / (edgeData.width * edgeData.height);
1700
+ if (relativeSize > 0.1 && relativeSize < 0.7) {
1701
+ score += 0.1; // 身份证通常占据图像的合理比例
1702
+ }
1703
+ // 范围限制在0-1之间
1704
+ return Math.min(Math.max(score, 0), 1);
1705
+ }
1319
1706
  }
1707
+ // 身份证标准宽高比(近似黄金比例)
1708
+ IDCardDetector.ID_CARD_ASPECT_RATIO = 1.58; // 标准身份证宽高比
1320
1709
 
1321
1710
  /**
1322
1711
  * @file Web Worker辅助工具类
@@ -1511,6 +1900,7 @@
1511
1900
  * @file OCR处理模块
1512
1901
  * @description 提供身份证文字识别和信息提取功能
1513
1902
  * @module OCRProcessor
1903
+ * @version 1.3.2
1514
1904
  */
1515
1905
  /**
1516
1906
  * OCR处理器类
@@ -1672,57 +2062,184 @@
1672
2062
  * @param {string} text - OCR识别到的文本
1673
2063
  * @returns {IDCardInfo} 提取到的身份证信息对象
1674
2064
  */
2065
+ /**
2066
+ * 格式化日期字符串为标准格式 (YYYY-MM-DD)
2067
+ * @param dateStr 原始日期字符串
2068
+ * @returns 格式化后的日期字符串
2069
+ */
2070
+ formatDateString(dateStr) {
2071
+ // 先尝试提取年月日
2072
+ const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
2073
+ if (dateMatch) {
2074
+ const year = dateMatch[1];
2075
+ const month = dateMatch[2].padStart(2, '0');
2076
+ const day = dateMatch[3].padStart(2, '0');
2077
+ return `${year}-${month}-${day}`;
2078
+ }
2079
+ // 如果是纯数字格式如 20220101
2080
+ if (/^\d{8}$/.test(dateStr)) {
2081
+ const year = dateStr.substring(0, 4);
2082
+ const month = dateStr.substring(4, 6);
2083
+ const day = dateStr.substring(6, 8);
2084
+ return `${year}-${month}-${day}`;
2085
+ }
2086
+ // 如果无法格式化,返回原始字符串
2087
+ return dateStr;
2088
+ }
2089
+ /**
2090
+ * 验证身份证号是否符合规则
2091
+ * @param idNumber 身份证号
2092
+ * @returns 是否有效
2093
+ */
2094
+ validateIDNumber(idNumber) {
2095
+ // 基本验证,校验位有效性和长度
2096
+ if (!idNumber || idNumber.length !== 18) {
2097
+ return false;
2098
+ }
2099
+ // 检查格式,前17位必须为数字,最后一位可以是数字或'X'
2100
+ const pattern = /^\d{17}[\dX]$/;
2101
+ if (!pattern.test(idNumber)) {
2102
+ return false;
2103
+ }
2104
+ // 检查日期部分
2105
+ parseInt(idNumber.substr(6, 4));
2106
+ const month = parseInt(idNumber.substr(10, 2));
2107
+ const day = parseInt(idNumber.substr(12, 2));
2108
+ if (month < 1 || month > 12 || day < 1 || day > 31) {
2109
+ return false;
2110
+ }
2111
+ // 更详细的检查可以添加校验位的验证等逻辑...
2112
+ return true;
2113
+ }
1675
2114
  parseIDCardText(text) {
1676
2115
  const info = {};
1677
- // 拆分为行
1678
- const lines = text.split("\n").filter((line) => line.trim());
1679
- // 解析身份证号码(最容易识别的部分)
2116
+ // 预处理文本,清除多余空白
2117
+ const processedText = text.replace(/\s+/g, " ").trim();
2118
+ // 拆分为行,并过滤空行
2119
+ const lines = processedText.split("\n").filter((line) => line.trim());
2120
+ // 解析身份证号码 - 多种模式匹配
2121
+ // 1. 普通18位身份证号模式
1680
2122
  const idNumberRegex = /(\d{17}[\dX])/;
1681
- const idNumberMatch = text.match(idNumberRegex);
1682
- if (idNumberMatch) {
1683
- info.idNumber = idNumberMatch[1];
1684
- }
1685
- // 解析姓名
1686
- for (const line of lines) {
1687
- if (line.includes("姓名") ||
1688
- (line.length < 10 && line.length > 1 && !/\d/.test(line))) {
1689
- info.name = line.replace("姓名", "").trim();
1690
- break;
2123
+ // 2. 带前缀的模式
2124
+ const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
2125
+ // 尝试所有模式
2126
+ let idNumber = null;
2127
+ const basicMatch = processedText.match(idNumberRegex);
2128
+ const prefixMatch = processedText.match(idNumberWithPrefixRegex);
2129
+ if (prefixMatch && prefixMatch[1]) {
2130
+ idNumber = prefixMatch[1]; // 首选带前缀的匹配,因为最可靠
2131
+ }
2132
+ else if (basicMatch && basicMatch[1]) {
2133
+ idNumber = basicMatch[1]; // 其次是常规匹配
2134
+ }
2135
+ if (idNumber) {
2136
+ info.idNumber = idNumber;
2137
+ }
2138
+ // 解析姓名 - 使用多种策略
2139
+ // 1. 直接匹配姓名标签近的内容
2140
+ const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
2141
+ const nameMatch = processedText.match(nameWithLabelRegex);
2142
+ // 2. 分析行文本寻找姓名
2143
+ if (nameMatch && nameMatch[1]) {
2144
+ info.name = nameMatch[1].trim();
2145
+ }
2146
+ else {
2147
+ // 备用方案:查找短行且内容全是汉字
2148
+ for (const line of lines) {
2149
+ if (line.length >= 2 && line.length <= 5 && /^[一-龥]+$/.test(line) && !/性别|民族|住址|公民|签发|有效/.test(line)) {
2150
+ info.name = line.trim();
2151
+ break;
2152
+ }
1691
2153
  }
1692
2154
  }
1693
- // 解析性别和民族
1694
- const genderNationalityRegex = /(男|女).*(族)/;
1695
- const genderMatch = text.match(genderNationalityRegex);
1696
- if (genderMatch) {
1697
- info.gender = genderMatch[1];
1698
- const nationalityText = genderMatch[0];
1699
- info.nationality = nationalityText
1700
- .substring(nationalityText.indexOf(genderMatch[1]) + 1)
1701
- .trim();
1702
- }
1703
- // 解析出生日期
1704
- const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/;
1705
- const birthDateMatch = text.match(birthDateRegex);
1706
- if (birthDateMatch) {
1707
- info.birthDate = `${birthDateMatch[1]}-${birthDateMatch[2]}-${birthDateMatch[3]}`;
1708
- }
1709
- // 解析地址
1710
- const addressRegex = /住址([\s\S]*?)公民身份号码/;
1711
- const addressMatch = text.match(addressRegex);
1712
- if (addressMatch) {
1713
- info.address = addressMatch[1].replace(/\n/g, "").trim();
2155
+ // 解析性别和民族 - 多种模式匹配
2156
+ // 1. 标准格式匹配
2157
+ const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
2158
+ const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
2159
+ // 2. 只匹配性别
2160
+ const genderOnlyRegex = /性别[\s\:]*([男女])/;
2161
+ const genderOnlyMatch = processedText.match(genderOnlyRegex);
2162
+ // 3. 只匹配民族
2163
+ const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
2164
+ const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
2165
+ if (genderNationalityMatch) {
2166
+ info.gender = genderNationalityMatch[1];
2167
+ info.nationality = genderNationalityMatch[2];
2168
+ }
2169
+ else {
2170
+ // 分开获取
2171
+ if (genderOnlyMatch)
2172
+ info.gender = genderOnlyMatch[1];
2173
+ if (nationalityOnlyMatch)
2174
+ info.nationality = nationalityOnlyMatch[1];
2175
+ }
2176
+ // 解析出生日期 - 支持多种格式
2177
+ // 1. 标准格式:YYYY年MM月DD日
2178
+ const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
2179
+ // 2. 美式日期格式:YYYY-MM-DD或YYYY/MM/DD
2180
+ const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
2181
+ // 3. 带前缀的格式
2182
+ const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
2183
+ let birthDateMatch = processedText.match(birthDateRegex1) ||
2184
+ processedText.match(birthDateRegex2) ||
2185
+ processedText.match(birthDateRegex3);
2186
+ // 4. 从身份证号码中提取出生日期(如果上述方法失败)
2187
+ if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
2188
+ const year = info.idNumber.substring(6, 10);
2189
+ const month = info.idNumber.substring(10, 12);
2190
+ const day = info.idNumber.substring(12, 14);
2191
+ info.birthDate = `${year}-${month}-${day}`;
2192
+ }
2193
+ else if (birthDateMatch) {
2194
+ // 确保月份和日期是两位数
2195
+ const year = birthDateMatch[1];
2196
+ const month = birthDateMatch[2].padStart(2, '0');
2197
+ const day = birthDateMatch[3].padStart(2, '0');
2198
+ info.birthDate = `${year}-${month}-${day}`;
2199
+ }
2200
+ // 解析地址 - 改进的正则匹配
2201
+ // 1. 常规模式
2202
+ const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
2203
+ // 2. 更宽松的模式
2204
+ const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
2205
+ const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2);
2206
+ if (addressMatch && addressMatch[1]) {
2207
+ // 清理地址中的常见错误和多余空格
2208
+ info.address = addressMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
2209
+ // 限制地址长度并判断地址合理性
2210
+ if (info.address.length > 70) {
2211
+ info.address = info.address.substring(0, 70);
2212
+ }
2213
+ // 确保地址是合理的(不仅仅包含符号或数字)
2214
+ if (!/[一-龥]/.test(info.address)) {
2215
+ info.address = ""; // 如果没有中文字符,可能不是有效地址
2216
+ }
1714
2217
  }
1715
2218
  // 解析签发机关
1716
- const authorityRegex = /签发机关([\s\S]*?)有效期/;
1717
- const authorityMatch = text.match(authorityRegex);
1718
- if (authorityMatch) {
1719
- info.issuingAuthority = authorityMatch[1].replace(/\n/g, "").trim();
1720
- }
1721
- // 解析有效期限
1722
- const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/;
1723
- const validPeriodMatch = text.match(validPeriodRegex);
2219
+ const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
2220
+ const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
2221
+ const authorityMatch = processedText.match(authorityRegex1) || processedText.match(authorityRegex2);
2222
+ if (authorityMatch && authorityMatch[1]) {
2223
+ info.issuingAuthority = authorityMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
2224
+ }
2225
+ // 解析有效期限 - 支持多种格式
2226
+ // 1. 常规格式:YYYY.MM.DD-YYYY.MM.DD
2227
+ 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}[日]*|[永久长期]*)/;
2228
+ // 2. 简化格式:YYYYMMDD-YYYYMMDD
2229
+ const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
2230
+ const validPeriodMatch = processedText.match(validPeriodRegex1) || processedText.match(validPeriodRegex2);
1724
2231
  if (validPeriodMatch) {
1725
- info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
2232
+ // 格式化为统一的有效期限形式
2233
+ if (validPeriodMatch[1] && validPeriodMatch[3]) {
2234
+ const startDate = this.formatDateString(validPeriodMatch[1]);
2235
+ const endDate = /\d/.test(validPeriodMatch[3]) ?
2236
+ this.formatDateString(validPeriodMatch[3]) :
2237
+ '长期有效';
2238
+ info.validPeriod = `${startDate}-${endDate}`;
2239
+ }
2240
+ else {
2241
+ info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim();
2242
+ }
1726
2243
  }
1727
2244
  return info;
1728
2245
  }
@@ -1991,6 +2508,7 @@
1991
2508
  * @file 身份证防伪检测模块
1992
2509
  * @description 提供身份证防伪特征识别功能,区分真假身份证
1993
2510
  * @module AntiFakeDetector
2511
+ * @version 1.3.2
1994
2512
  */
1995
2513
  /**
1996
2514
  * 身份证防伪特征检测器
@@ -2122,23 +2640,166 @@
2122
2640
  * @private
2123
2641
  */
2124
2642
  async detectUVInkFeatures(imageData) {
2125
- // 提取蓝色通道增强UV油墨可见度
2126
- const canvas = document.createElement("canvas");
2127
- canvas.width = imageData.width;
2128
- canvas.height = imageData.height;
2129
- const ctx = canvas.getContext("2d");
2130
- if (!ctx) {
2131
- return ["荧光油墨", false, 0];
2132
- }
2133
- ctx.putImageData(imageData, 0, 0);
2134
- // 分析蓝色通道中的特定模式
2135
- // 实际实现中应使用更复杂的算法提取UV特征
2136
- // 这里使用模拟实现
2137
- // 模拟检测: 70%的概率检测到,置信度0.65-0.95
2138
- const detected = Math.random() > 0.3;
2139
- const confidence = detected ? 0.65 + Math.random() * 0.3 : 0;
2643
+ // 在真实身份证上,荧光油墨会在特定反光条件下呈现特定颜色特征
2644
+ // 在普通可见光下,我们分析蓝色和紫外色通道分布特征
2645
+ // 1. 提取蓝色通道并增强对比度
2646
+ const blueChannel = this.extractColorChannel(imageData, 'blue');
2647
+ // 2. 分析蓝色通道的分布特征
2648
+ const { peaks, variance } = this.analyzeChannelDistribution(blueChannel);
2649
+ // 3. 分析特定区域的颜色模式
2650
+ const patternScore = this.detectUVColorPattern(imageData);
2651
+ // 4. 计算综合得分
2652
+ // 特征分析:荧光油墨在蓝色通道通常有显著峰值,且分布更聚集
2653
+ let score = 0;
2654
+ // 过多的峰值表明可能是真实身份证上的荧光特征
2655
+ if (peaks > 3 && peaks < 10) {
2656
+ score += 0.4;
2657
+ }
2658
+ // 方差越大,表示颜色对比度越高,更可能有荧光特征
2659
+ if (variance > 1000) {
2660
+ score += 0.3;
2661
+ }
2662
+ // 颜色模式得分
2663
+ score += patternScore * 0.3;
2664
+ // 重要区域分析
2665
+ // 身份证头像区域通常不应具有荧光特征
2666
+ const hasPortraitAreaFeatures = this.analyzePortraitArea(imageData);
2667
+ if (hasPortraitAreaFeatures) {
2668
+ // 头像区域不应该有荧光特征,如果有可能是伪造的
2669
+ score -= 0.2;
2670
+ }
2671
+ // 求出最终分数并限制在[0,1]范围内
2672
+ const confidence = Math.max(0, Math.min(1, score));
2673
+ const detected = confidence > 0.55;
2140
2674
  return ["荧光油墨", detected, confidence];
2141
2675
  }
2676
+ /**
2677
+ * 从图像数据中提取指定颜色通道
2678
+ * @param imageData 原始图像数据
2679
+ * @param channel 通道名称(red, green, blue)
2680
+ */
2681
+ extractColorChannel(imageData, channel) {
2682
+ const { data, width, height } = imageData;
2683
+ const channelOffset = channel === 'red' ? 0 : channel === 'green' ? 1 : 2;
2684
+ const channelData = new Uint8ClampedArray(width * height);
2685
+ for (let i = 0; i < data.length; i += 4) {
2686
+ const pixelIndex = i / 4;
2687
+ channelData[pixelIndex] = data[i + channelOffset];
2688
+ }
2689
+ return channelData;
2690
+ }
2691
+ /**
2692
+ * 分析颜色通道分布特征
2693
+ * @param channelData 颜色通道数据
2694
+ */
2695
+ analyzeChannelDistribution(channelData) {
2696
+ // 计算直方图
2697
+ const histogram = new Array(256).fill(0);
2698
+ for (let i = 0; i < channelData.length; i++) {
2699
+ histogram[channelData[i]]++;
2700
+ }
2701
+ // 平滑直方图以减少噪声
2702
+ const smoothedHistogram = this.smoothHistogram(histogram, 3);
2703
+ // 计算峰值数量
2704
+ let peaks = 0;
2705
+ for (let i = 1; i < 255; i++) {
2706
+ if (smoothedHistogram[i] > smoothedHistogram[i - 1] &&
2707
+ smoothedHistogram[i] > smoothedHistogram[i + 1] &&
2708
+ smoothedHistogram[i] > channelData.length * 0.01) { // 只计算显著峰值
2709
+ peaks++;
2710
+ }
2711
+ }
2712
+ // 计算方差
2713
+ let mean = 0;
2714
+ for (let i = 0; i < channelData.length; i++) {
2715
+ mean += channelData[i];
2716
+ }
2717
+ mean /= channelData.length;
2718
+ let variance = 0;
2719
+ for (let i = 0; i < channelData.length; i++) {
2720
+ variance += Math.pow(channelData[i] - mean, 2);
2721
+ }
2722
+ variance /= channelData.length;
2723
+ return { peaks, variance };
2724
+ }
2725
+ /**
2726
+ * 平滑直方图以减少噪声
2727
+ */
2728
+ smoothHistogram(histogram, windowSize) {
2729
+ const result = new Array(histogram.length).fill(0);
2730
+ const halfWindow = Math.floor(windowSize / 2);
2731
+ for (let i = 0; i < histogram.length; i++) {
2732
+ let sum = 0;
2733
+ let count = 0;
2734
+ for (let j = Math.max(0, i - halfWindow); j <= Math.min(histogram.length - 1, i + halfWindow); j++) {
2735
+ sum += histogram[j];
2736
+ count++;
2737
+ }
2738
+ result[i] = sum / count;
2739
+ }
2740
+ return result;
2741
+ }
2742
+ /**
2743
+ * 检测图像中的荧光颜色模式
2744
+ */
2745
+ detectUVColorPattern(imageData) {
2746
+ // 分析特定组合颜色的出现频率,荧光油墨在可见光下也具有特定的颜色特征
2747
+ const { data, width, height } = imageData;
2748
+ let uvColorCount = 0;
2749
+ // 寻找可能为荧光油墨的特定颜色模式
2750
+ // 这些颜色通常是特定的蓝紫色调和高对比度
2751
+ for (let i = 0; i < data.length; i += 4) {
2752
+ const r = data[i];
2753
+ const g = data[i + 1];
2754
+ const b = data[i + 2];
2755
+ // 检查是否是荧光油墨特有的颜色范围
2756
+ // 这里使用简化的追踪条件,实际应用中应使用更复杂的颜色模型
2757
+ if (b > 1.5 * r && b > 1.3 * g && b > 100) {
2758
+ uvColorCount++;
2759
+ }
2760
+ }
2761
+ // 计算荧光颜色像素占比
2762
+ const totalPixels = width * height;
2763
+ const uvColorRatio = uvColorCount / totalPixels;
2764
+ // 对于真实身份证,荧光颜色的占比应该在一定范围内
2765
+ // 如果占比过高或过低,可能是伪造的
2766
+ const idealRatio = 0.05; // 理想占比
2767
+ const deviation = Math.abs(uvColorRatio - idealRatio) / idealRatio;
2768
+ // 将差异转换为0-1的置信度分数
2769
+ return Math.max(0, 1 - Math.min(1, deviation * 2));
2770
+ }
2771
+ /**
2772
+ * 分析头像区域是否存在荧光特征
2773
+ * 这个方法用于检测伪造的身份证,因为头像区域不应该有荧光特征
2774
+ */
2775
+ analyzePortraitArea(imageData) {
2776
+ // 假设头像区域大约占据图片右上方四分之一的区域
2777
+ const { width, height, data } = imageData;
2778
+ const portraitX = Math.floor(width * 0.6);
2779
+ const portraitY = Math.floor(height * 0.2);
2780
+ const portraitWidth = Math.floor(width * 0.3);
2781
+ const portraitHeight = Math.floor(height * 0.3);
2782
+ let uvFeatureCount = 0;
2783
+ let totalPixels = 0;
2784
+ // 检查头像区域的荧光特征
2785
+ for (let y = portraitY; y < portraitY + portraitHeight; y++) {
2786
+ for (let x = portraitX; x < portraitX + portraitWidth; x++) {
2787
+ if (x >= 0 && x < width && y >= 0 && y < height) {
2788
+ const i = (y * width + x) * 4;
2789
+ const r = data[i];
2790
+ const g = data[i + 1];
2791
+ const b = data[i + 2];
2792
+ // 使用与上面相同的荧光颜色检测标准
2793
+ if (b > 1.5 * r && b > 1.3 * g && b > 100) {
2794
+ uvFeatureCount++;
2795
+ }
2796
+ totalPixels++;
2797
+ }
2798
+ }
2799
+ }
2800
+ // 如果头像区域的荧光特征占比过高,可能是伪造的
2801
+ return totalPixels > 0 && (uvFeatureCount / totalPixels) > 0.1;
2802
+ }
2142
2803
  /**
2143
2804
  * 检测微缩文字
2144
2805
  *
@@ -2147,16 +2808,176 @@
2147
2808
  * @private
2148
2809
  */
2149
2810
  async detectMicroText(imageData) {
2150
- // 应用边缘检测突出微缩文字
2151
- ImageProcessor.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
2152
- // 寻找特定的微缩文字模式
2153
- // 实际实现中应使用计算机视觉算法寻找微小规则文字模式
2154
- // 这里使用模拟实现
2155
- // 模拟检测: 80%的概率检测到,置信度0.7-0.95
2156
- const detected = Math.random() > 0.2;
2157
- const confidence = detected ? 0.7 + Math.random() * 0.25 : 0;
2811
+ // 微缩文字检测 - 身份证上的微缩文字是重要的防伪特征
2812
+ // 这些文字很小,但会呈现规则的线条和高频组件
2813
+ // 1. 转换图像为灰度图
2814
+ const grayscale = ImageProcessor.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
2815
+ // 2. 执行边缘检测突出微缩文字
2816
+ const edgeData = ImageProcessor.detectEdges(grayscale, 40); // 强化的边缘检测
2817
+ // 3. 分析频率特征 - 微缩文字呈现高频的边缘过渡
2818
+ const frequencyFeatures = this.analyzeFrequencyFeatures(edgeData);
2819
+ // 4. 检测微缩文字的具体区域
2820
+ const microTextRegions = this.detectMicroTextRegions(edgeData);
2821
+ // 5. 综合分析结果计算置信度
2822
+ let score = 0;
2823
+ // 频率特征分数
2824
+ score += frequencyFeatures.score * 0.6;
2825
+ // 区域特征分数
2826
+ if (microTextRegions.count > 0) {
2827
+ // 过多的区域也可能表示噪声,因此有一个最佳范围
2828
+ const normalizedCount = Math.min(microTextRegions.count, 5) / 5;
2829
+ score += normalizedCount * 0.4;
2830
+ }
2831
+ // 对置信度进行最终调整
2832
+ const confidence = Math.max(0, Math.min(1, score));
2833
+ const detected = confidence > 0.5;
2158
2834
  return ["微缩文字", detected, confidence];
2159
2835
  }
2836
+ /**
2837
+ * 分析边缘图像的频率特征
2838
+ * 微缩文字呈现高频的边缘过渡
2839
+ */
2840
+ analyzeFrequencyFeatures(edgeData) {
2841
+ const { data, width, height } = edgeData;
2842
+ // 计算高频边缘分布
2843
+ // 统计边缘过渡的变化频率
2844
+ let highFreqTransitions = 0;
2845
+ // 检测行方向的边缘变化
2846
+ for (let y = 0; y < height; y++) {
2847
+ let prevEdge = false;
2848
+ let transitions = 0;
2849
+ for (let x = 0; x < width; x++) {
2850
+ const i = (y * width + x) * 4;
2851
+ const isEdge = data[i] > 200;
2852
+ if (isEdge !== prevEdge) {
2853
+ transitions++;
2854
+ prevEdge = isEdge;
2855
+ }
2856
+ }
2857
+ // 每行的过渡频率
2858
+ if (transitions > width * 0.1) { // 高频过渡行
2859
+ highFreqTransitions++;
2860
+ }
2861
+ }
2862
+ // 计算列方向的边缘变化
2863
+ let colHighFreqTransitions = 0;
2864
+ for (let x = 0; x < width; x++) {
2865
+ let prevEdge = false;
2866
+ let transitions = 0;
2867
+ for (let y = 0; y < height; y++) {
2868
+ const i = (y * width + x) * 4;
2869
+ const isEdge = data[i] > 200;
2870
+ if (isEdge !== prevEdge) {
2871
+ transitions++;
2872
+ prevEdge = isEdge;
2873
+ }
2874
+ }
2875
+ // 每列的过渡频率
2876
+ if (transitions > height * 0.1) { // 高频过渡列
2877
+ colHighFreqTransitions++;
2878
+ }
2879
+ }
2880
+ // 综合计算高频特征比例
2881
+ const rowHighFreqRatio = highFreqTransitions / height;
2882
+ const colHighFreqRatio = colHighFreqTransitions / width;
2883
+ const highFreqRatio = (rowHighFreqRatio + colHighFreqRatio) / 2;
2884
+ // 计算最终分数
2885
+ // 真实的微缩文字应该有适度的高频特征,而不是极端的高或低
2886
+ const idealRatio = 0.15; // 理想的高频比例
2887
+ const deviationFactor = Math.abs(highFreqRatio - idealRatio) / idealRatio;
2888
+ const score = Math.max(0, 1 - Math.min(1, deviationFactor * 3));
2889
+ return { score, highFreqRatio };
2890
+ }
2891
+ /**
2892
+ * 检测微缩文字区域
2893
+ * 微缩文字通常呈现呈现规则的组合排列
2894
+ */
2895
+ detectMicroTextRegions(edgeData) {
2896
+ const { data, width, height } = edgeData;
2897
+ const visitedMap = new Array(width * height).fill(false);
2898
+ const regions = [];
2899
+ // 使用满足条件的连通区域寻找微缩文字区域
2900
+ for (let y = 0; y < height; y++) {
2901
+ for (let x = 0; x < width; x++) {
2902
+ const idx = y * width + x;
2903
+ const i = idx * 4;
2904
+ // 如果是边缘像素且未访问过
2905
+ if (data[i] > 200 && !visitedMap[idx]) {
2906
+ // 使用深度优先搜索找到连通的边缘区域
2907
+ const regionPoints = this.floodFillEdge(edgeData, x, y, visitedMap);
2908
+ // 分析区域
2909
+ if (regionPoints.length > 10) { // 小区域忽略
2910
+ const [minX, minY, maxX, maxY] = this.getBoundingBox(regionPoints);
2911
+ const regionWidth = maxX - minX + 1;
2912
+ const regionHeight = maxY - minY + 1;
2913
+ // 检查区域大小和纹理特征
2914
+ if (regionWidth > 5 && regionHeight > 5 &&
2915
+ regionWidth < width * 0.2 && regionHeight < height * 0.2) {
2916
+ // 计算区域密度
2917
+ const density = regionPoints.length / (regionWidth * regionHeight);
2918
+ // 检查并添加符合微缩文字特征的区域
2919
+ if (density > 0.1 && density < 0.5) { // 合适的密度范围
2920
+ regions.push({
2921
+ x: minX,
2922
+ y: minY,
2923
+ w: regionWidth,
2924
+ h: regionHeight
2925
+ });
2926
+ }
2927
+ }
2928
+ }
2929
+ }
2930
+ }
2931
+ }
2932
+ return { count: regions.length, regions };
2933
+ }
2934
+ /**
2935
+ * 深度优先搜索连通的边缘区域
2936
+ */
2937
+ floodFillEdge(edgeData, startX, startY, visitedMap) {
2938
+ const { data, width, height } = edgeData;
2939
+ const stack = [];
2940
+ const points = [];
2941
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
2942
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
2943
+ // 起始点
2944
+ stack.push({ x: startX, y: startY });
2945
+ visitedMap[startY * width + startX] = true;
2946
+ while (stack.length > 0) {
2947
+ const { x, y } = stack.pop();
2948
+ points.push({ x, y });
2949
+ // 检查88个相邻方向
2950
+ for (let i = 0; i < 8; i++) {
2951
+ const nx = x + dx[i];
2952
+ const ny = y + dy[i];
2953
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
2954
+ const nidx = ny * width + nx;
2955
+ const ni = nidx * 4;
2956
+ if (data[ni] > 200 && !visitedMap[nidx]) {
2957
+ stack.push({ x: nx, y: ny });
2958
+ visitedMap[nidx] = true;
2959
+ }
2960
+ }
2961
+ }
2962
+ }
2963
+ return points;
2964
+ }
2965
+ /**
2966
+ * 获取点集的外接矩形
2967
+ */
2968
+ getBoundingBox(points) {
2969
+ let minX = Number.MAX_SAFE_INTEGER;
2970
+ let minY = Number.MAX_SAFE_INTEGER;
2971
+ let maxX = 0;
2972
+ let maxY = 0;
2973
+ for (const { x, y } of points) {
2974
+ minX = Math.min(minX, x);
2975
+ minY = Math.min(minY, y);
2976
+ maxX = Math.max(maxX, x);
2977
+ maxY = Math.max(maxY, y);
2978
+ }
2979
+ return [minX, minY, maxX, maxY];
2980
+ }
2160
2981
  /**
2161
2982
  * 检测光变图案
2162
2983
  *