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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @file 图像处理工具类
|
|
3
3
|
* @description 提供图像预处理功能,用于提高OCR识别率
|
|
4
4
|
* @module ImageProcessor
|
|
5
|
+
* @version 1.3.2
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import imageCompression from "browser-image-compression"
|
|
@@ -543,4 +544,367 @@ export class ImageProcessor {
|
|
|
543
544
|
// 获取新的ImageData
|
|
544
545
|
return ctx.getImageData(0, 0, newWidth, newHeight)
|
|
545
546
|
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* 边缘检测算法,用于识别图像中的边缘
|
|
550
|
+
* 基于Sobel算子实现
|
|
551
|
+
*
|
|
552
|
+
* @param imageData 原始图像数据,应已转为灰度图
|
|
553
|
+
* @param threshold 边缘阈值,默认为30
|
|
554
|
+
* @returns 检测到边缘的图像数据
|
|
555
|
+
*/
|
|
556
|
+
static detectEdges(imageData: ImageData, threshold: number = 30): ImageData {
|
|
557
|
+
// 确保输入图像是灰度图
|
|
558
|
+
const grayscaleImage = this.toGrayscale(
|
|
559
|
+
new ImageData(
|
|
560
|
+
new Uint8ClampedArray(imageData.data),
|
|
561
|
+
imageData.width,
|
|
562
|
+
imageData.height
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const width = grayscaleImage.width;
|
|
567
|
+
const height = grayscaleImage.height;
|
|
568
|
+
const inputData = grayscaleImage.data;
|
|
569
|
+
const outputData = new Uint8ClampedArray(inputData.length);
|
|
570
|
+
|
|
571
|
+
// Sobel算子 - 水平和垂直方向
|
|
572
|
+
const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
|
|
573
|
+
const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
|
|
574
|
+
|
|
575
|
+
// 对每个像素应用Sobel算子
|
|
576
|
+
for (let y = 1; y < height - 1; y++) {
|
|
577
|
+
for (let x = 1; x < width - 1; x++) {
|
|
578
|
+
let gx = 0;
|
|
579
|
+
let gy = 0;
|
|
580
|
+
|
|
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
|
+
|
|
587
|
+
const kernelIdx = (ky + 1) * 3 + (kx + 1);
|
|
588
|
+
gx += pixelVal * sobelX[kernelIdx];
|
|
589
|
+
gy += pixelVal * sobelY[kernelIdx];
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 计算梯度强度
|
|
594
|
+
let magnitude = Math.sqrt(gx * gx + gy * gy);
|
|
595
|
+
|
|
596
|
+
// 应用阈值
|
|
597
|
+
magnitude = magnitude > threshold ? 255 : 0;
|
|
598
|
+
|
|
599
|
+
// 设置输出像素
|
|
600
|
+
const pos = (y * width + x) * 4;
|
|
601
|
+
outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
|
|
602
|
+
outputData[pos + 3] = 255; // 透明度保持完全不透明
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// 处理边缘像素
|
|
607
|
+
for (let i = 0; i < width * 4; i++) {
|
|
608
|
+
// 顶部和底部行
|
|
609
|
+
outputData[i] = 0;
|
|
610
|
+
outputData[(height - 1) * width * 4 + i] = 0;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
for (let i = 0; i < height; i++) {
|
|
614
|
+
// 左右两侧列
|
|
615
|
+
const leftPos = i * width * 4;
|
|
616
|
+
const rightPos = (i * width + width - 1) * 4;
|
|
617
|
+
|
|
618
|
+
for (let j = 0; j < 4; j++) {
|
|
619
|
+
outputData[leftPos + j] = 0;
|
|
620
|
+
outputData[rightPos + j] = 0;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return new ImageData(outputData, width, height);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* 卡尼-德里奇边缘检测
|
|
629
|
+
* 相比Sobel更精确的边缘检测算法
|
|
630
|
+
*
|
|
631
|
+
* @param imageData 灰度图像数据
|
|
632
|
+
* @param lowThreshold 低阈值
|
|
633
|
+
* @param highThreshold 高阈值
|
|
634
|
+
* @returns 边缘检测结果
|
|
635
|
+
*/
|
|
636
|
+
static cannyEdgeDetection(
|
|
637
|
+
imageData: ImageData,
|
|
638
|
+
lowThreshold: number = 20,
|
|
639
|
+
highThreshold: number = 50
|
|
640
|
+
): ImageData {
|
|
641
|
+
const grayscaleImage = this.toGrayscale(
|
|
642
|
+
new ImageData(
|
|
643
|
+
new Uint8ClampedArray(imageData.data),
|
|
644
|
+
imageData.width,
|
|
645
|
+
imageData.height
|
|
646
|
+
)
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// 1. 高斯模糊
|
|
650
|
+
const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
|
|
651
|
+
|
|
652
|
+
// 2. 使用Sobel算子计算梯度
|
|
653
|
+
const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
|
|
654
|
+
|
|
655
|
+
// 3. 非极大值抛弃
|
|
656
|
+
const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
|
|
657
|
+
|
|
658
|
+
// 4. 双阈值处理
|
|
659
|
+
const thresholdResult = this.hysteresisThresholding(
|
|
660
|
+
nonMaxSuppressed,
|
|
661
|
+
blurredImage.width,
|
|
662
|
+
blurredImage.height,
|
|
663
|
+
lowThreshold,
|
|
664
|
+
highThreshold
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
// 创建输出图像
|
|
668
|
+
const outputData = new Uint8ClampedArray(imageData.data.length);
|
|
669
|
+
|
|
670
|
+
// 将结果转换为ImageData
|
|
671
|
+
for (let i = 0; i < thresholdResult.length; i++) {
|
|
672
|
+
const pos = i * 4;
|
|
673
|
+
const value = thresholdResult[i] ? 255 : 0;
|
|
674
|
+
outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
|
|
675
|
+
outputData[pos + 3] = 255;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return new ImageData(outputData, blurredImage.width, blurredImage.height);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* 高斯模糊
|
|
683
|
+
*/
|
|
684
|
+
private static gaussianBlur(imageData: ImageData, sigma: number = 1.5): ImageData {
|
|
685
|
+
const width = imageData.width;
|
|
686
|
+
const height = imageData.height;
|
|
687
|
+
const inputData = imageData.data;
|
|
688
|
+
const outputData = new Uint8ClampedArray(inputData.length);
|
|
689
|
+
|
|
690
|
+
// 生成高斯核
|
|
691
|
+
const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
|
|
692
|
+
const halfKernel = Math.floor(kernelSize / 2);
|
|
693
|
+
const kernel = this.generateGaussianKernel(kernelSize, sigma);
|
|
694
|
+
|
|
695
|
+
// 应用高斯核
|
|
696
|
+
for (let y = 0; y < height; y++) {
|
|
697
|
+
for (let x = 0; x < width; x++) {
|
|
698
|
+
let sum = 0;
|
|
699
|
+
let weightSum = 0;
|
|
700
|
+
|
|
701
|
+
for (let ky = -halfKernel; ky <= halfKernel; ky++) {
|
|
702
|
+
for (let kx = -halfKernel; kx <= halfKernel; kx++) {
|
|
703
|
+
const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
|
|
704
|
+
const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
|
|
705
|
+
const pixelPos = (pixelY * width + pixelX) * 4;
|
|
706
|
+
|
|
707
|
+
const kernelY = ky + halfKernel;
|
|
708
|
+
const kernelX = kx + halfKernel;
|
|
709
|
+
const weight = kernel[kernelY * kernelSize + kernelX];
|
|
710
|
+
|
|
711
|
+
sum += inputData[pixelPos] * weight;
|
|
712
|
+
weightSum += weight;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const pos = (y * width + x) * 4;
|
|
717
|
+
const value = Math.round(sum / weightSum);
|
|
718
|
+
outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
|
|
719
|
+
outputData[pos + 3] = 255;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return new ImageData(outputData, width, height);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 生成高斯核
|
|
728
|
+
*/
|
|
729
|
+
private static generateGaussianKernel(size: number, sigma: number): number[] {
|
|
730
|
+
const kernel = new Array(size * size);
|
|
731
|
+
const center = Math.floor(size / 2);
|
|
732
|
+
let sum = 0;
|
|
733
|
+
|
|
734
|
+
for (let y = 0; y < size; y++) {
|
|
735
|
+
for (let x = 0; x < size; x++) {
|
|
736
|
+
const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
|
|
737
|
+
const value = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
|
|
738
|
+
|
|
739
|
+
kernel[y * size + x] = value;
|
|
740
|
+
sum += value;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// 归一化
|
|
745
|
+
for (let i = 0; i < kernel.length; i++) {
|
|
746
|
+
kernel[i] /= sum;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return kernel;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* 计算梯度强度和方向
|
|
754
|
+
*/
|
|
755
|
+
private static computeGradients(imageData: ImageData): {
|
|
756
|
+
gradientMagnitude: number[],
|
|
757
|
+
gradientDirection: number[]
|
|
758
|
+
} {
|
|
759
|
+
const width = imageData.width;
|
|
760
|
+
const height = imageData.height;
|
|
761
|
+
const inputData = imageData.data;
|
|
762
|
+
|
|
763
|
+
const gradientMagnitude = new Array(width * height);
|
|
764
|
+
const gradientDirection = new Array(width * height);
|
|
765
|
+
|
|
766
|
+
// Sobel算子
|
|
767
|
+
const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
|
|
768
|
+
const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
|
|
769
|
+
|
|
770
|
+
for (let y = 1; y < height - 1; y++) {
|
|
771
|
+
for (let x = 1; x < width - 1; x++) {
|
|
772
|
+
let gx = 0;
|
|
773
|
+
let gy = 0;
|
|
774
|
+
|
|
775
|
+
for (let ky = -1; ky <= 1; ky++) {
|
|
776
|
+
for (let kx = -1; kx <= 1; kx++) {
|
|
777
|
+
const pixelPos = ((y + ky) * width + (x + kx)) * 4;
|
|
778
|
+
const pixelVal = inputData[pixelPos];
|
|
779
|
+
|
|
780
|
+
const kernelIdx = (ky + 1) * 3 + (kx + 1);
|
|
781
|
+
gx += pixelVal * sobelX[kernelIdx];
|
|
782
|
+
gy += pixelVal * sobelY[kernelIdx];
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const idx = y * width + x;
|
|
787
|
+
gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
|
|
788
|
+
gradientDirection[idx] = Math.atan2(gy, gx);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// 处理边界
|
|
793
|
+
for (let y = 0; y < height; y++) {
|
|
794
|
+
for (let x = 0; x < width; x++) {
|
|
795
|
+
if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
|
|
796
|
+
const idx = y * width + x;
|
|
797
|
+
gradientMagnitude[idx] = 0;
|
|
798
|
+
gradientDirection[idx] = 0;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return { gradientMagnitude, gradientDirection };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* 非极大值抛弃
|
|
808
|
+
*/
|
|
809
|
+
private static nonMaxSuppression(
|
|
810
|
+
gradientMagnitude: number[],
|
|
811
|
+
gradientDirection: number[],
|
|
812
|
+
width: number,
|
|
813
|
+
height: number
|
|
814
|
+
): number[] {
|
|
815
|
+
const result = new Array(width * height).fill(0);
|
|
816
|
+
|
|
817
|
+
for (let y = 1; y < height - 1; y++) {
|
|
818
|
+
for (let x = 1; x < width - 1; x++) {
|
|
819
|
+
const idx = y * width + x;
|
|
820
|
+
const magnitude = gradientMagnitude[idx];
|
|
821
|
+
const direction = gradientDirection[idx];
|
|
822
|
+
|
|
823
|
+
// 将方向转化为角度
|
|
824
|
+
const degrees = (direction * 180 / Math.PI + 180) % 180;
|
|
825
|
+
|
|
826
|
+
// 获取相邻像素索引
|
|
827
|
+
let neighbor1Idx, neighbor2Idx;
|
|
828
|
+
|
|
829
|
+
// 将方向量化为四个方向: 0°, 45°, 90°, 135°
|
|
830
|
+
if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
|
|
831
|
+
// 水平方向
|
|
832
|
+
neighbor1Idx = idx - 1;
|
|
833
|
+
neighbor2Idx = idx + 1;
|
|
834
|
+
} else if (degrees >= 22.5 && degrees < 67.5) {
|
|
835
|
+
// 45度方向
|
|
836
|
+
neighbor1Idx = (y - 1) * width + (x + 1);
|
|
837
|
+
neighbor2Idx = (y + 1) * width + (x - 1);
|
|
838
|
+
} else if (degrees >= 67.5 && degrees < 112.5) {
|
|
839
|
+
// 垂直方向
|
|
840
|
+
neighbor1Idx = (y - 1) * width + x;
|
|
841
|
+
neighbor2Idx = (y + 1) * width + x;
|
|
842
|
+
} else {
|
|
843
|
+
// 135度方向
|
|
844
|
+
neighbor1Idx = (y - 1) * width + (x - 1);
|
|
845
|
+
neighbor2Idx = (y + 1) * width + (x + 1);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// 检查当前像素是否是最大值
|
|
849
|
+
if (magnitude >= gradientMagnitude[neighbor1Idx] &&
|
|
850
|
+
magnitude >= gradientMagnitude[neighbor2Idx]) {
|
|
851
|
+
result[idx] = magnitude;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* 双阈值处理
|
|
861
|
+
*/
|
|
862
|
+
private static hysteresisThresholding(
|
|
863
|
+
nonMaxSuppressed: number[],
|
|
864
|
+
width: number,
|
|
865
|
+
height: number,
|
|
866
|
+
lowThreshold: number,
|
|
867
|
+
highThreshold: number
|
|
868
|
+
): boolean[] {
|
|
869
|
+
const result = new Array(width * height).fill(false);
|
|
870
|
+
const visited = new Array(width * height).fill(false);
|
|
871
|
+
const stack = [];
|
|
872
|
+
|
|
873
|
+
// 标记强边缘点
|
|
874
|
+
for (let i = 0; i < nonMaxSuppressed.length; i++) {
|
|
875
|
+
if (nonMaxSuppressed[i] >= highThreshold) {
|
|
876
|
+
result[i] = true;
|
|
877
|
+
stack.push(i);
|
|
878
|
+
visited[i] = true;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// 使用深度优先搜索连接弱边缘
|
|
883
|
+
const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
|
|
884
|
+
const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
|
|
885
|
+
|
|
886
|
+
while (stack.length > 0) {
|
|
887
|
+
const currentIdx: number = stack.pop()!;
|
|
888
|
+
const currentX: number = currentIdx % width;
|
|
889
|
+
const currentY: number = Math.floor(currentIdx / width);
|
|
890
|
+
|
|
891
|
+
// 检查88个相邻方向
|
|
892
|
+
for (let i = 0; i < 8; i++) {
|
|
893
|
+
const newX: number = currentX + dx[i];
|
|
894
|
+
const newY: number = currentY + dy[i];
|
|
895
|
+
|
|
896
|
+
if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
|
|
897
|
+
const newIdx: number = newY * width + newX;
|
|
898
|
+
|
|
899
|
+
if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
|
|
900
|
+
result[newIdx] = true;
|
|
901
|
+
stack.push(newIdx);
|
|
902
|
+
visited[newIdx] = true;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return result;
|
|
909
|
+
}
|
|
546
910
|
}
|