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.
- package/README.md +55 -460
- package/dist/id-scanner-lib.esm.js +4641 -0
- package/dist/id-scanner-lib.esm.js.map +1 -0
- package/dist/id-scanner-lib.js +14755 -0
- package/dist/id-scanner-lib.js.map +1 -0
- package/dist/types/core/base-module.d.ts +44 -0
- package/dist/types/core/camera-manager.d.ts +258 -0
- package/dist/types/core/config.d.ts +88 -0
- package/dist/types/core/errors.d.ts +111 -0
- package/dist/types/core/event-emitter.d.ts +55 -0
- package/dist/types/core/logger.d.ts +277 -0
- package/dist/types/core/module-manager.d.ts +78 -0
- package/dist/types/core/plugin-manager.d.ts +158 -0
- package/dist/types/core/resource-manager.d.ts +246 -0
- package/dist/types/core/result.d.ts +83 -0
- package/dist/types/core/scanner-factory.d.ts +93 -0
- package/dist/types/index.bundle.d.ts +1303 -0
- package/dist/types/index.d.ts +86 -0
- package/dist/types/interfaces/external-types.d.ts +174 -0
- package/dist/types/interfaces/face-detection.d.ts +293 -0
- package/dist/types/interfaces/scanner-module.d.ts +280 -0
- package/dist/types/modules/face/face-detector.d.ts +170 -0
- package/dist/types/modules/face/index.d.ts +56 -0
- package/dist/types/modules/face/liveness-detector.d.ts +177 -0
- package/dist/types/modules/face/types.d.ts +136 -0
- package/dist/types/modules/id-card/anti-fake-detector.d.ts +170 -0
- package/dist/types/modules/id-card/id-card-detector.d.ts +131 -0
- package/dist/types/modules/id-card/index.d.ts +89 -0
- package/dist/types/modules/id-card/ocr-processor.d.ts +110 -0
- package/dist/types/modules/id-card/ocr-worker.d.ts +31 -0
- package/dist/types/modules/id-card/types.d.ts +181 -0
- package/dist/types/modules/qrcode/index.d.ts +51 -0
- package/dist/types/modules/qrcode/qr-code-scanner.d.ts +64 -0
- package/dist/types/modules/qrcode/types.d.ts +67 -0
- package/dist/types/utils/camera.d.ts +81 -0
- package/dist/types/utils/image-processing.d.ts +176 -0
- package/dist/types/utils/index.d.ts +175 -0
- package/dist/types/utils/performance.d.ts +81 -0
- package/dist/types/utils/resource-manager.d.ts +53 -0
- package/dist/types/utils/types.d.ts +166 -0
- package/dist/types/utils/worker.d.ts +52 -0
- package/dist/types/version.d.ts +7 -0
- package/package.json +76 -77
- package/src/core/base-module.ts +78 -0
- package/src/core/camera-manager.ts +798 -0
- package/src/core/config.ts +268 -0
- package/src/core/errors.ts +174 -0
- package/src/core/event-emitter.ts +110 -0
- package/src/core/logger.ts +549 -0
- package/src/core/module-manager.ts +165 -0
- package/src/core/plugin-manager.ts +429 -0
- package/src/core/resource-manager.ts +762 -0
- package/src/core/result.ts +163 -0
- package/src/core/scanner-factory.ts +237 -0
- package/src/index.ts +113 -936
- package/src/interfaces/external-types.ts +200 -0
- package/src/interfaces/face-detection.ts +309 -0
- package/src/interfaces/scanner-module.ts +384 -0
- package/src/modules/face/face-detector.ts +931 -0
- package/src/modules/face/index.ts +208 -0
- package/src/modules/face/liveness-detector.ts +908 -0
- package/src/modules/face/types.ts +133 -0
- package/src/modules/id-card/anti-fake-detector.ts +732 -0
- package/src/modules/id-card/id-card-detector.ts +474 -0
- package/src/modules/id-card/index.ts +425 -0
- package/src/modules/id-card/ocr-processor.ts +538 -0
- package/src/modules/id-card/ocr-worker.ts +259 -0
- package/src/modules/id-card/types.ts +178 -0
- package/src/modules/qrcode/index.ts +175 -0
- package/src/modules/qrcode/qr-code-scanner.ts +230 -0
- package/src/modules/qrcode/types.ts +65 -0
- package/src/types/browser-image-compression.d.ts +19 -0
- package/src/types/tesseract.d.ts +280 -0
- package/src/utils/image-processing.ts +432 -49
- package/src/utils/index.ts +426 -0
- package/src/utils/performance.ts +168 -131
- package/src/utils/resource-manager.ts +65 -146
- package/src/utils/types.ts +90 -2
- package/src/utils/worker.ts +123 -84
- package/src/version.ts +11 -0
- package/tools/scaffold.js +543 -0
- package/dist/id-scanner-core.esm.js +0 -11076
- package/dist/id-scanner-core.esm.js.map +0 -1
- package/dist/id-scanner-core.js +0 -11088
- package/dist/id-scanner-core.js.map +0 -1
- package/dist/id-scanner-core.min.js +0 -1
- package/dist/id-scanner-core.min.js.map +0 -1
- package/dist/id-scanner-ocr.esm.js +0 -1802
- package/dist/id-scanner-ocr.esm.js.map +0 -1
- package/dist/id-scanner-ocr.js +0 -1811
- package/dist/id-scanner-ocr.js.map +0 -1
- package/dist/id-scanner-ocr.min.js +0 -1
- package/dist/id-scanner-ocr.min.js.map +0 -1
- package/dist/id-scanner-qr.esm.js +0 -1023
- package/dist/id-scanner-qr.esm.js.map +0 -1
- package/dist/id-scanner-qr.js +0 -1032
- package/dist/id-scanner-qr.js.map +0 -1
- package/dist/id-scanner-qr.min.js +0 -1
- package/dist/id-scanner-qr.min.js.map +0 -1
- package/dist/id-scanner.js +0 -3740
- package/dist/id-scanner.js.map +0 -1
- package/dist/id-scanner.min.js +0 -1
- package/dist/id-scanner.min.js.map +0 -1
- package/src/core.ts +0 -138
- package/src/demo/demo.ts +0 -204
- package/src/id-recognition/anti-fake-detector.ts +0 -317
- package/src/id-recognition/data-extractor.ts +0 -262
- package/src/id-recognition/id-detector.ts +0 -363
- package/src/id-recognition/ocr-processor.ts +0 -334
- package/src/id-recognition/ocr-worker.ts +0 -156
- package/src/index-umd.ts +0 -477
- package/src/ocr-module.ts +0 -187
- package/src/qr-module.ts +0 -179
- package/src/scanner/barcode-scanner.ts +0 -251
- 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
|
|
489
|
-
* @returns
|
|
489
|
+
* @param keepAspectRatio 是否保持宽高比
|
|
490
|
+
* @returns 调整后的图像
|
|
490
491
|
*/
|
|
491
|
-
static resizeImage(
|
|
492
|
-
|
|
492
|
+
public static resizeImage(
|
|
493
|
+
image: ImageData | HTMLImageElement | HTMLCanvasElement,
|
|
493
494
|
maxWidth: number,
|
|
494
495
|
maxHeight: number,
|
|
495
|
-
|
|
496
|
+
keepAspectRatio: boolean = true
|
|
496
497
|
): ImageData {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
505
|
-
let
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
550
|
+
throw new Error('无法创建临时Canvas上下文');
|
|
532
551
|
}
|
|
533
552
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
//
|
|
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
|
}
|