id-scanner-lib 1.3.2 → 1.5.0

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.
Files changed (115) hide show
  1. package/README.md +55 -460
  2. package/dist/id-scanner-lib.esm.js +4641 -0
  3. package/dist/id-scanner-lib.esm.js.map +1 -0
  4. package/dist/id-scanner-lib.js +14755 -0
  5. package/dist/id-scanner-lib.js.map +1 -0
  6. package/dist/types/core/base-module.d.ts +44 -0
  7. package/dist/types/core/camera-manager.d.ts +258 -0
  8. package/dist/types/core/config.d.ts +88 -0
  9. package/dist/types/core/errors.d.ts +111 -0
  10. package/dist/types/core/event-emitter.d.ts +55 -0
  11. package/dist/types/core/logger.d.ts +277 -0
  12. package/dist/types/core/module-manager.d.ts +78 -0
  13. package/dist/types/core/plugin-manager.d.ts +158 -0
  14. package/dist/types/core/resource-manager.d.ts +246 -0
  15. package/dist/types/core/result.d.ts +83 -0
  16. package/dist/types/core/scanner-factory.d.ts +93 -0
  17. package/dist/types/index.bundle.d.ts +1303 -0
  18. package/dist/types/index.d.ts +86 -0
  19. package/dist/types/interfaces/external-types.d.ts +174 -0
  20. package/dist/types/interfaces/face-detection.d.ts +293 -0
  21. package/dist/types/interfaces/scanner-module.d.ts +280 -0
  22. package/dist/types/modules/face/face-detector.d.ts +170 -0
  23. package/dist/types/modules/face/index.d.ts +56 -0
  24. package/dist/types/modules/face/liveness-detector.d.ts +177 -0
  25. package/dist/types/modules/face/types.d.ts +136 -0
  26. package/dist/types/modules/id-card/anti-fake-detector.d.ts +170 -0
  27. package/dist/types/modules/id-card/id-card-detector.d.ts +131 -0
  28. package/dist/types/modules/id-card/index.d.ts +89 -0
  29. package/dist/types/modules/id-card/ocr-processor.d.ts +110 -0
  30. package/dist/types/modules/id-card/ocr-worker.d.ts +31 -0
  31. package/dist/types/modules/id-card/types.d.ts +181 -0
  32. package/dist/types/modules/qrcode/index.d.ts +51 -0
  33. package/dist/types/modules/qrcode/qr-code-scanner.d.ts +64 -0
  34. package/dist/types/modules/qrcode/types.d.ts +67 -0
  35. package/dist/types/utils/camera.d.ts +81 -0
  36. package/dist/types/utils/image-processing.d.ts +176 -0
  37. package/dist/types/utils/index.d.ts +175 -0
  38. package/dist/types/utils/performance.d.ts +81 -0
  39. package/dist/types/utils/resource-manager.d.ts +53 -0
  40. package/dist/types/utils/types.d.ts +166 -0
  41. package/dist/types/utils/worker.d.ts +52 -0
  42. package/dist/types/version.d.ts +7 -0
  43. package/package.json +76 -77
  44. package/src/core/base-module.ts +78 -0
  45. package/src/core/camera-manager.ts +798 -0
  46. package/src/core/config.ts +268 -0
  47. package/src/core/errors.ts +174 -0
  48. package/src/core/event-emitter.ts +110 -0
  49. package/src/core/logger.ts +549 -0
  50. package/src/core/module-manager.ts +165 -0
  51. package/src/core/plugin-manager.ts +429 -0
  52. package/src/core/resource-manager.ts +762 -0
  53. package/src/core/result.ts +163 -0
  54. package/src/core/scanner-factory.ts +237 -0
  55. package/src/index.ts +113 -936
  56. package/src/interfaces/external-types.ts +200 -0
  57. package/src/interfaces/face-detection.ts +309 -0
  58. package/src/interfaces/scanner-module.ts +384 -0
  59. package/src/modules/face/face-detector.ts +931 -0
  60. package/src/modules/face/index.ts +208 -0
  61. package/src/modules/face/liveness-detector.ts +908 -0
  62. package/src/modules/face/types.ts +133 -0
  63. package/src/modules/id-card/anti-fake-detector.ts +732 -0
  64. package/src/modules/id-card/id-card-detector.ts +474 -0
  65. package/src/modules/id-card/index.ts +425 -0
  66. package/src/modules/id-card/ocr-processor.ts +538 -0
  67. package/src/modules/id-card/ocr-worker.ts +259 -0
  68. package/src/modules/id-card/types.ts +178 -0
  69. package/src/modules/qrcode/index.ts +175 -0
  70. package/src/modules/qrcode/qr-code-scanner.ts +230 -0
  71. package/src/modules/qrcode/types.ts +65 -0
  72. package/src/types/browser-image-compression.d.ts +19 -0
  73. package/src/types/tesseract.d.ts +280 -0
  74. package/src/utils/image-processing.ts +432 -49
  75. package/src/utils/index.ts +426 -0
  76. package/src/utils/performance.ts +168 -131
  77. package/src/utils/resource-manager.ts +65 -146
  78. package/src/utils/types.ts +90 -2
  79. package/src/utils/worker.ts +123 -84
  80. package/src/version.ts +11 -0
  81. package/tools/scaffold.js +543 -0
  82. package/dist/id-scanner-core.esm.js +0 -11076
  83. package/dist/id-scanner-core.esm.js.map +0 -1
  84. package/dist/id-scanner-core.js +0 -11088
  85. package/dist/id-scanner-core.js.map +0 -1
  86. package/dist/id-scanner-core.min.js +0 -1
  87. package/dist/id-scanner-core.min.js.map +0 -1
  88. package/dist/id-scanner-ocr.esm.js +0 -1802
  89. package/dist/id-scanner-ocr.esm.js.map +0 -1
  90. package/dist/id-scanner-ocr.js +0 -1811
  91. package/dist/id-scanner-ocr.js.map +0 -1
  92. package/dist/id-scanner-ocr.min.js +0 -1
  93. package/dist/id-scanner-ocr.min.js.map +0 -1
  94. package/dist/id-scanner-qr.esm.js +0 -1023
  95. package/dist/id-scanner-qr.esm.js.map +0 -1
  96. package/dist/id-scanner-qr.js +0 -1032
  97. package/dist/id-scanner-qr.js.map +0 -1
  98. package/dist/id-scanner-qr.min.js +0 -1
  99. package/dist/id-scanner-qr.min.js.map +0 -1
  100. package/dist/id-scanner.js +0 -3740
  101. package/dist/id-scanner.js.map +0 -1
  102. package/dist/id-scanner.min.js +0 -1
  103. package/dist/id-scanner.min.js.map +0 -1
  104. package/src/core.ts +0 -138
  105. package/src/demo/demo.ts +0 -204
  106. package/src/id-recognition/anti-fake-detector.ts +0 -317
  107. package/src/id-recognition/data-extractor.ts +0 -262
  108. package/src/id-recognition/id-detector.ts +0 -363
  109. package/src/id-recognition/ocr-processor.ts +0 -334
  110. package/src/id-recognition/ocr-worker.ts +0 -156
  111. package/src/index-umd.ts +0 -477
  112. package/src/ocr-module.ts +0 -187
  113. package/src/qr-module.ts +0 -179
  114. package/src/scanner/barcode-scanner.ts +0 -251
  115. package/src/scanner/qr-scanner.ts +0 -167
@@ -2,9 +2,11 @@
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"
9
+ import { Point, Rect, ImageProcessingOptions } from './types';
8
10
 
9
11
  /**
10
12
  * 图像处理器配置选项
@@ -480,67 +482,448 @@ export class ImageProcessor {
480
482
  }
481
483
 
482
484
  /**
483
- * 调整图像大小
484
- *
485
- * @param imageData 原始图像数据
485
+ * 将图像调整到指定大小
486
+ * @param image 输入图像
486
487
  * @param maxWidth 最大宽度
487
488
  * @param maxHeight 最大高度
488
- * @param maintainAspectRatio 是否保持宽高比
489
- * @returns ImageData 调整大小后的图像数据
489
+ * @param keepAspectRatio 是否保持宽高比
490
+ * @returns 调整后的图像
490
491
  */
491
- static resizeImage(
492
- imageData: ImageData,
492
+ public static resizeImage(
493
+ image: ImageData | HTMLImageElement | HTMLCanvasElement,
493
494
  maxWidth: number,
494
495
  maxHeight: number,
495
- maintainAspectRatio: boolean = true
496
+ keepAspectRatio: boolean = true
496
497
  ): ImageData {
497
- const { width, height } = imageData
498
-
499
- // 如果图像已经小于指定大小,则不需要调整
500
- if (width <= maxWidth && height <= maxHeight) {
501
- return imageData
498
+ // 创建canvas元素
499
+ const canvas = document.createElement('canvas');
500
+ const ctx = canvas.getContext('2d');
501
+
502
+ if (!ctx) {
503
+ throw new Error('无法创建Canvas上下文');
502
504
  }
503
-
504
- let newWidth = maxWidth
505
- let newHeight = maxHeight
506
-
507
- // 计算新的尺寸,保持宽高比
508
- if (maintainAspectRatio) {
509
- const ratio = Math.min(maxWidth / width, maxHeight / height)
510
- newWidth = Math.floor(width * ratio)
511
- newHeight = Math.floor(height * ratio)
505
+
506
+ // 获取图像尺寸
507
+ let width: number;
508
+ let height: number;
509
+
510
+ if (image instanceof ImageData) {
511
+ width = image.width;
512
+ height = image.height;
513
+ } else {
514
+ width = image.width;
515
+ height = image.height;
512
516
  }
513
-
514
- // 创建用于调整大小的Canvas
515
- const canvas = document.createElement("canvas")
516
- canvas.width = newWidth
517
- canvas.height = newHeight
518
-
519
- const ctx = canvas.getContext("2d")
520
- if (!ctx) {
521
- throw new Error("无法创建2D上下文")
517
+
518
+ // 计算调整后的尺寸
519
+ let newWidth = width;
520
+ let newHeight = height;
521
+
522
+ if (keepAspectRatio) {
523
+ if (width > height) {
524
+ if (width > maxWidth) {
525
+ newHeight = Math.round(height * (maxWidth / width));
526
+ newWidth = maxWidth;
527
+ }
528
+ } else {
529
+ if (height > maxHeight) {
530
+ newWidth = Math.round(width * (maxHeight / height));
531
+ newHeight = maxHeight;
532
+ }
533
+ }
534
+ } else {
535
+ newWidth = Math.min(width, maxWidth);
536
+ newHeight = Math.min(height, maxHeight);
522
537
  }
523
-
524
- // 创建临时Canvas绘制原始ImageData
525
- const tempCanvas = document.createElement("canvas")
526
- tempCanvas.width = width
527
- tempCanvas.height = height
528
-
529
- const tempCtx = tempCanvas.getContext("2d")
538
+
539
+ // 设置canvas尺寸
540
+ canvas.width = newWidth;
541
+ canvas.height = newHeight;
542
+
543
+ // 绘制调整后的图像
544
+ if (image instanceof ImageData) {
545
+ // 创建临时canvas存储ImageData
546
+ const tempCanvas = document.createElement('canvas');
547
+ const tempCtx = tempCanvas.getContext('2d');
548
+
530
549
  if (!tempCtx) {
531
- throw new Error("无法创建临时2D上下文")
550
+ throw new Error('无法创建临时Canvas上下文');
532
551
  }
533
552
 
534
- tempCtx.putImageData(imageData, 0, 0)
535
-
536
- // 使用缩放平滑算法
537
- ctx.imageSmoothingEnabled = true
538
- ctx.imageSmoothingQuality = "high"
539
-
540
- // 绘制调整大小的图像
541
- ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, newWidth, newHeight)
553
+ tempCanvas.width = image.width;
554
+ tempCanvas.height = image.height;
555
+ tempCtx.putImageData(image, 0, 0);
556
+
557
+ // 绘制调整后的图像
558
+ ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, newWidth, newHeight);
559
+ } else {
560
+ ctx.drawImage(image, 0, 0, width, height, 0, 0, newWidth, newHeight);
561
+ }
542
562
 
543
- // 获取新的ImageData
544
- return ctx.getImageData(0, 0, newWidth, newHeight)
563
+ // 返回调整后的ImageData
564
+ return ctx.getImageData(0, 0, newWidth, newHeight);
565
+ }
566
+
567
+ /**
568
+ * 边缘检测算法,用于识别图像中的边缘
569
+ * 基于Sobel算子实现
570
+ *
571
+ * @param imageData 原始图像数据,应已转为灰度图
572
+ * @param threshold 边缘阈值,默认为30
573
+ * @returns 检测到边缘的图像数据
574
+ */
575
+ static detectEdges(imageData: ImageData, threshold: number = 30): ImageData {
576
+ // 确保输入图像是灰度图
577
+ const grayscaleImage = this.toGrayscale(
578
+ new ImageData(
579
+ new Uint8ClampedArray(imageData.data),
580
+ imageData.width,
581
+ imageData.height
582
+ )
583
+ );
584
+
585
+ const width = grayscaleImage.width;
586
+ const height = grayscaleImage.height;
587
+ const inputData = grayscaleImage.data;
588
+ const outputData = new Uint8ClampedArray(inputData.length);
589
+
590
+ // Sobel算子 - 水平和垂直方向
591
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
592
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
593
+
594
+ // 对每个像素应用Sobel算子
595
+ for (let y = 1; y < height - 1; y++) {
596
+ for (let x = 1; x < width - 1; x++) {
597
+ let gx = 0;
598
+ let gy = 0;
599
+
600
+ // 应用卷积
601
+ for (let ky = -1; ky <= 1; ky++) {
602
+ for (let kx = -1; kx <= 1; kx++) {
603
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
604
+ const pixelVal = inputData[pixelPos]; // 灰度值
605
+
606
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
607
+ gx += pixelVal * sobelX[kernelIdx];
608
+ gy += pixelVal * sobelY[kernelIdx];
609
+ }
610
+ }
611
+
612
+ // 计算梯度强度
613
+ let magnitude = Math.sqrt(gx * gx + gy * gy);
614
+
615
+ // 应用阈值
616
+ magnitude = magnitude > threshold ? 255 : 0;
617
+
618
+ // 设置输出像素
619
+ const pos = (y * width + x) * 4;
620
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
621
+ outputData[pos + 3] = 255; // 透明度保持完全不透明
622
+ }
623
+ }
624
+
625
+ // 处理边缘像素
626
+ for (let i = 0; i < width * 4; i++) {
627
+ // 顶部和底部行
628
+ outputData[i] = 0;
629
+ outputData[(height - 1) * width * 4 + i] = 0;
630
+ }
631
+
632
+ for (let i = 0; i < height; i++) {
633
+ // 左右两侧列
634
+ const leftPos = i * width * 4;
635
+ const rightPos = (i * width + width - 1) * 4;
636
+
637
+ for (let j = 0; j < 4; j++) {
638
+ outputData[leftPos + j] = 0;
639
+ outputData[rightPos + j] = 0;
640
+ }
641
+ }
642
+
643
+ return new ImageData(outputData, width, height);
644
+ }
645
+
646
+ /**
647
+ * 卡尼-德里奇边缘检测
648
+ * 相比Sobel更精确的边缘检测算法
649
+ *
650
+ * @param imageData 灰度图像数据
651
+ * @param lowThreshold 低阈值
652
+ * @param highThreshold 高阈值
653
+ * @returns 边缘检测结果
654
+ */
655
+ static cannyEdgeDetection(
656
+ imageData: ImageData,
657
+ lowThreshold: number = 20,
658
+ highThreshold: number = 50
659
+ ): ImageData {
660
+ const grayscaleImage = this.toGrayscale(
661
+ new ImageData(
662
+ new Uint8ClampedArray(imageData.data),
663
+ imageData.width,
664
+ imageData.height
665
+ )
666
+ );
667
+
668
+ // 1. 高斯模糊
669
+ const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
670
+
671
+ // 2. 使用Sobel算子计算梯度
672
+ const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
673
+
674
+ // 3. 非极大值抛弃
675
+ const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
676
+
677
+ // 4. 双阈值处理
678
+ const thresholdResult = this.hysteresisThresholding(
679
+ nonMaxSuppressed,
680
+ blurredImage.width,
681
+ blurredImage.height,
682
+ lowThreshold,
683
+ highThreshold
684
+ );
685
+
686
+ // 创建输出图像
687
+ const outputData = new Uint8ClampedArray(imageData.data.length);
688
+
689
+ // 将结果转换为ImageData
690
+ for (let i = 0; i < thresholdResult.length; i++) {
691
+ const pos = i * 4;
692
+ const value = thresholdResult[i] ? 255 : 0;
693
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
694
+ outputData[pos + 3] = 255;
695
+ }
696
+
697
+ return new ImageData(outputData, blurredImage.width, blurredImage.height);
698
+ }
699
+
700
+ /**
701
+ * 高斯模糊
702
+ */
703
+ private static gaussianBlur(imageData: ImageData, sigma: number = 1.5): ImageData {
704
+ const width = imageData.width;
705
+ const height = imageData.height;
706
+ const inputData = imageData.data;
707
+ const outputData = new Uint8ClampedArray(inputData.length);
708
+
709
+ // 生成高斯核
710
+ const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
711
+ const halfKernel = Math.floor(kernelSize / 2);
712
+ const kernel = this.generateGaussianKernel(kernelSize, sigma);
713
+
714
+ // 应用高斯核
715
+ for (let y = 0; y < height; y++) {
716
+ for (let x = 0; x < width; x++) {
717
+ let sum = 0;
718
+ let weightSum = 0;
719
+
720
+ for (let ky = -halfKernel; ky <= halfKernel; ky++) {
721
+ for (let kx = -halfKernel; kx <= halfKernel; kx++) {
722
+ const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
723
+ const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
724
+ const pixelPos = (pixelY * width + pixelX) * 4;
725
+
726
+ const kernelY = ky + halfKernel;
727
+ const kernelX = kx + halfKernel;
728
+ const weight = kernel[kernelY * kernelSize + kernelX];
729
+
730
+ sum += inputData[pixelPos] * weight;
731
+ weightSum += weight;
732
+ }
733
+ }
734
+
735
+ const pos = (y * width + x) * 4;
736
+ const value = Math.round(sum / weightSum);
737
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
738
+ outputData[pos + 3] = 255;
739
+ }
740
+ }
741
+
742
+ return new ImageData(outputData, width, height);
743
+ }
744
+
745
+ /**
746
+ * 生成高斯核
747
+ */
748
+ private static generateGaussianKernel(size: number, sigma: number): number[] {
749
+ const kernel = new Array(size * size);
750
+ const center = Math.floor(size / 2);
751
+ let sum = 0;
752
+
753
+ for (let y = 0; y < size; y++) {
754
+ for (let x = 0; x < size; x++) {
755
+ const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
756
+ const value = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
757
+
758
+ kernel[y * size + x] = value;
759
+ sum += value;
760
+ }
761
+ }
762
+
763
+ // 归一化
764
+ for (let i = 0; i < kernel.length; i++) {
765
+ kernel[i] /= sum;
766
+ }
767
+
768
+ return kernel;
769
+ }
770
+
771
+ /**
772
+ * 计算梯度强度和方向
773
+ */
774
+ private static computeGradients(imageData: ImageData): {
775
+ gradientMagnitude: number[],
776
+ gradientDirection: number[]
777
+ } {
778
+ const width = imageData.width;
779
+ const height = imageData.height;
780
+ const inputData = imageData.data;
781
+
782
+ const gradientMagnitude = new Array(width * height);
783
+ const gradientDirection = new Array(width * height);
784
+
785
+ // Sobel算子
786
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
787
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
788
+
789
+ for (let y = 1; y < height - 1; y++) {
790
+ for (let x = 1; x < width - 1; x++) {
791
+ let gx = 0;
792
+ let gy = 0;
793
+
794
+ for (let ky = -1; ky <= 1; ky++) {
795
+ for (let kx = -1; kx <= 1; kx++) {
796
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
797
+ const pixelVal = inputData[pixelPos];
798
+
799
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
800
+ gx += pixelVal * sobelX[kernelIdx];
801
+ gy += pixelVal * sobelY[kernelIdx];
802
+ }
803
+ }
804
+
805
+ const idx = y * width + x;
806
+ gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
807
+ gradientDirection[idx] = Math.atan2(gy, gx);
808
+ }
809
+ }
810
+
811
+ // 处理边界
812
+ for (let y = 0; y < height; y++) {
813
+ for (let x = 0; x < width; x++) {
814
+ if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
815
+ const idx = y * width + x;
816
+ gradientMagnitude[idx] = 0;
817
+ gradientDirection[idx] = 0;
818
+ }
819
+ }
820
+ }
821
+
822
+ return { gradientMagnitude, gradientDirection };
823
+ }
824
+
825
+ /**
826
+ * 非极大值抛弃
827
+ */
828
+ private static nonMaxSuppression(
829
+ gradientMagnitude: number[],
830
+ gradientDirection: number[],
831
+ width: number,
832
+ height: number
833
+ ): number[] {
834
+ const result = new Array(width * height).fill(0);
835
+
836
+ for (let y = 1; y < height - 1; y++) {
837
+ for (let x = 1; x < width - 1; x++) {
838
+ const idx = y * width + x;
839
+ const magnitude = gradientMagnitude[idx];
840
+ const direction = gradientDirection[idx];
841
+
842
+ // 将方向转化为角度
843
+ const degrees = (direction * 180 / Math.PI + 180) % 180;
844
+
845
+ // 获取相邻像素索引
846
+ let neighbor1Idx, neighbor2Idx;
847
+
848
+ // 将方向量化为四个方向: 0°, 45°, 90°, 135°
849
+ if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
850
+ // 水平方向
851
+ neighbor1Idx = idx - 1;
852
+ neighbor2Idx = idx + 1;
853
+ } else if (degrees >= 22.5 && degrees < 67.5) {
854
+ // 45度方向
855
+ neighbor1Idx = (y - 1) * width + (x + 1);
856
+ neighbor2Idx = (y + 1) * width + (x - 1);
857
+ } else if (degrees >= 67.5 && degrees < 112.5) {
858
+ // 垂直方向
859
+ neighbor1Idx = (y - 1) * width + x;
860
+ neighbor2Idx = (y + 1) * width + x;
861
+ } else {
862
+ // 135度方向
863
+ neighbor1Idx = (y - 1) * width + (x - 1);
864
+ neighbor2Idx = (y + 1) * width + (x + 1);
865
+ }
866
+
867
+ // 检查当前像素是否是最大值
868
+ if (magnitude >= gradientMagnitude[neighbor1Idx] &&
869
+ magnitude >= gradientMagnitude[neighbor2Idx]) {
870
+ result[idx] = magnitude;
871
+ }
872
+ }
873
+ }
874
+
875
+ return result;
876
+ }
877
+
878
+ /**
879
+ * 双阈值处理
880
+ */
881
+ private static hysteresisThresholding(
882
+ nonMaxSuppressed: number[],
883
+ width: number,
884
+ height: number,
885
+ lowThreshold: number,
886
+ highThreshold: number
887
+ ): boolean[] {
888
+ const result = new Array(width * height).fill(false);
889
+ const visited = new Array(width * height).fill(false);
890
+ const stack = [];
891
+
892
+ // 标记强边缘点
893
+ for (let i = 0; i < nonMaxSuppressed.length; i++) {
894
+ if (nonMaxSuppressed[i] >= highThreshold) {
895
+ result[i] = true;
896
+ stack.push(i);
897
+ visited[i] = true;
898
+ }
899
+ }
900
+
901
+ // 使用深度优先搜索连接弱边缘
902
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
903
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
904
+
905
+ while (stack.length > 0) {
906
+ const currentIdx: number = stack.pop()!;
907
+ const currentX: number = currentIdx % width;
908
+ const currentY: number = Math.floor(currentIdx / width);
909
+
910
+ // 检查88个相邻方向
911
+ for (let i = 0; i < 8; i++) {
912
+ const newX: number = currentX + dx[i];
913
+ const newY: number = currentY + dy[i];
914
+
915
+ if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
916
+ const newIdx: number = newY * width + newX;
917
+
918
+ if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
919
+ result[newIdx] = true;
920
+ stack.push(newIdx);
921
+ visited[newIdx] = true;
922
+ }
923
+ }
924
+ }
925
+ }
926
+
927
+ return result;
545
928
  }
546
929
  }