id-scanner-lib 1.6.7 → 2.0.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 (48) hide show
  1. package/dist/id-scanner-lib.esm.js +994 -1139
  2. package/dist/id-scanner-lib.esm.js.map +1 -1
  3. package/dist/id-scanner-lib.js +995 -1144
  4. package/dist/id-scanner-lib.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/compat/index.ts +7 -0
  7. package/src/compat/v1-adapter.ts +84 -0
  8. package/src/core/camera-manager.ts +43 -76
  9. package/src/core/camera-stream-manager.ts +318 -0
  10. package/src/core/config.ts +113 -267
  11. package/src/core/errors.ts +68 -117
  12. package/src/core/logger.ts +158 -81
  13. package/src/core/resource-manager.ts +150 -0
  14. package/src/core/scanner.ts +109 -0
  15. package/src/core/utils/browser.ts +7 -0
  16. package/src/core/utils/canvas-pool.ts +171 -0
  17. package/src/core/utils/canvas.ts +7 -0
  18. package/src/core/utils/image.ts +7 -0
  19. package/src/core/utils/index.ts +9 -0
  20. package/src/core/utils/resource-manager.ts +155 -0
  21. package/src/core/utils/validate.ts +7 -0
  22. package/src/core/utils/worker.ts +130 -0
  23. package/src/modules/face/comparator/comparator.ts +45 -0
  24. package/src/modules/face/comparator/index.ts +1 -0
  25. package/src/modules/face/detector/detector.ts +83 -0
  26. package/src/modules/face/detector/index.ts +2 -0
  27. package/src/modules/face/detector/types.ts +80 -0
  28. package/src/modules/face/face-comparator.ts +150 -0
  29. package/src/modules/face/face-detector-options.ts +104 -0
  30. package/src/modules/face/face-detector.ts +121 -376
  31. package/src/modules/face/face-detector.ts.bak +991 -0
  32. package/src/modules/face/face-model-loader.ts +222 -0
  33. package/src/modules/face/face-result-converter.ts +225 -0
  34. package/src/modules/face/face-tracker.ts +207 -0
  35. package/src/modules/face/liveness/index.ts +7 -0
  36. package/src/modules/face/liveness-detector.ts +2 -2
  37. package/src/modules/face/tracker/index.ts +7 -0
  38. package/src/modules/id-card/anti-fake/index.ts +7 -0
  39. package/src/modules/id-card/detector/index.ts +7 -0
  40. package/src/modules/id-card/id-card-text-parser.ts +151 -0
  41. package/src/modules/id-card/ocr-processor.ts +20 -257
  42. package/src/modules/id-card/ocr-worker.ts +2 -183
  43. package/src/modules/id-card/parser/index.ts +7 -0
  44. package/src/modules/qr/scanner/index.ts +7 -0
  45. package/src/utils/canvas-pool.ts +273 -0
  46. package/src/utils/edge-detector.ts +232 -0
  47. package/src/utils/image-processing.ts +92 -419
  48. package/src/utils/index.ts +1 -0
@@ -2,11 +2,13 @@
2
2
  * @file 图像处理工具类
3
3
  * @description 提供图像预处理功能,用于提高OCR识别率
4
4
  * @module ImageProcessor
5
- * @version 1.3.2
5
+ * @version 1.4.0
6
6
  */
7
7
 
8
8
  import imageCompression from "browser-image-compression"
9
9
  import { Point, Rect, ImageProcessingOptions } from './types';
10
+ import { CanvasPool } from './canvas-pool';
11
+ import { EdgeDetector } from './edge-detector';
10
12
 
11
13
  /**
12
14
  * 图像处理器配置选项
@@ -18,6 +20,8 @@ export interface ImageProcessorOptions {
18
20
  invert?: boolean // 是否反转颜色
19
21
  blur?: number // 模糊程度 (0-10)
20
22
  sharpen?: boolean // 是否锐化
23
+ /** 是否使用 Canvas 对象池(减少内存分配) */
24
+ usePool?: boolean;
21
25
  }
22
26
 
23
27
  /**
@@ -44,17 +48,27 @@ export class ImageProcessor {
44
48
  * @param {ImageData} imageData - 要转换的图像数据
45
49
  * @returns {HTMLCanvasElement} 包含图像的Canvas元素
46
50
  */
47
- static imageDataToCanvas(imageData: ImageData): HTMLCanvasElement {
48
- const canvas = document.createElement("canvas")
49
- canvas.width = imageData.width
50
- canvas.height = imageData.height
51
- const ctx = canvas.getContext("2d")
51
+ static imageDataToCanvas(imageData: ImageData, usePool: boolean = true): HTMLCanvasElement {
52
+ let canvas: HTMLCanvasElement;
53
+ let context: CanvasRenderingContext2D;
54
+
55
+ if (usePool) {
56
+ ({ canvas, context } = CanvasPool.getInstance().acquire(imageData.width, imageData.height));
57
+ } else {
58
+ canvas = document.createElement("canvas");
59
+ canvas.width = imageData.width;
60
+ canvas.height = imageData.height;
61
+ context = canvas.getContext("2d")!;
62
+ }
52
63
 
53
- if (ctx) {
54
- ctx.putImageData(imageData, 0, 0)
64
+ context.putImageData(imageData, 0, 0);
65
+
66
+ if (usePool) {
67
+ // 立即释放回池中,用户保留 canvas 引用即可
68
+ CanvasPool.getInstance().release(canvas);
55
69
  }
56
70
 
57
- return canvas
71
+ return canvas;
58
72
  }
59
73
 
60
74
  /**
@@ -106,22 +120,26 @@ export class ImageProcessor {
106
120
  }
107
121
 
108
122
  /**
109
- * 将图像转换为灰度图
123
+ * 将图像转换为灰度图(返回新 ImageData,不修改原图)
110
124
  *
111
125
  * @param imageData 原始图像数据
112
- * @returns 灰度图像数据
126
+ * @returns 灰度图像数据(新对象)
113
127
  */
114
128
  static toGrayscale(imageData: ImageData): ImageData {
115
- const data = imageData.data
116
- const length = data.length
129
+ const srcData = imageData.data
130
+ const length = srcData.length
131
+ // 创建新数组,避免修改原图
132
+ const destData = new Uint8ClampedArray(srcData)
117
133
 
118
134
  for (let i = 0; i < length; i += 4) {
119
135
  // 使用加权平均法将 RGB 转换为灰度值
120
- const gray = data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11
121
- data[i] = data[i + 1] = data[i + 2] = gray
136
+ const gray = srcData[i] * 0.3 + srcData[i + 1] * 0.59 + srcData[i + 2] * 0.11
137
+ destData[i] = destData[i + 1] = destData[i + 2] = gray
138
+ // Alpha 通道保持不变
139
+ destData[i + 3] = srcData[i + 3]
122
140
  }
123
141
 
124
- return imageData
142
+ return new ImageData(destData, imageData.width, imageData.height)
125
143
  }
126
144
 
127
145
  /**
@@ -195,41 +213,55 @@ export class ImageProcessor {
195
213
  }
196
214
 
197
215
  /**
198
- * 对图像应用阈值操作,增强对比度
216
+ * 对图像应用阈值操作,增强对比度(二值化)
199
217
  *
200
218
  * @param imageData 原始图像数据
201
219
  * @param threshold 阈值 (0-255)
202
- * @returns 处理后的图像数据
220
+ * @returns 处理后的图像数据(新对象,不修改原图)
203
221
  */
204
222
  static threshold(imageData: ImageData, threshold: number = 128): ImageData {
205
- // 先转换为灰度图(toGrayscale 内部已创建新 ImageData,无需外部拷贝)
223
+ // 先转换为灰度图(返回新 ImageData,不修改原图)
206
224
  const grayscaleImage = this.toGrayscale(imageData)
225
+ const srcData = grayscaleImage.data
226
+ const length = srcData.length
227
+ // 创建新数组存储二值化结果
228
+ const destData = new Uint8ClampedArray(length)
207
229
 
208
- const data = grayscaleImage.data
209
-
210
- for (let i = 0; i < data.length; i += 4) {
230
+ for (let i = 0; i < length; i += 4) {
211
231
  // 二值化处理
212
- const value = data[i] < threshold ? 0 : 255
213
- data[i] = data[i + 1] = data[i + 2] = value
232
+ const value = srcData[i] < threshold ? 0 : 255
233
+ destData[i] = destData[i + 1] = destData[i + 2] = value
234
+ destData[i + 3] = srcData[i + 3] // 保持透明度
214
235
  }
215
236
 
216
- return grayscaleImage
237
+ return new ImageData(destData, grayscaleImage.width, grayscaleImage.height)
217
238
  }
218
239
 
219
240
  /**
220
- * 将图像转换为黑白图像(二值化)
241
+ * 将图像转换为黑白图像(二值化,使用OTSU自动阈值)
221
242
  *
222
243
  * @param imageData 原始图像数据
223
- * @returns 二值化后的图像数据
244
+ * @returns 二值化后的图像数据(新对象,不修改原图)
224
245
  */
225
246
  static toBinaryImage(imageData: ImageData): ImageData {
226
- // 先转换为灰度图(toGrayscale 内部已创建新 ImageData,无需外部拷贝)
247
+ // 先转换为灰度图(返回新 ImageData,不修改原图)
227
248
  const grayscaleImage = this.toGrayscale(imageData)
228
249
 
229
250
  // 使用OTSU算法自动确定阈值
230
251
  const threshold = this.getOtsuThreshold(grayscaleImage)
231
252
 
232
- return this.threshold(grayscaleImage, threshold)
253
+ // 直接对灰度图进行二值化,避免再次调用 toGrayscale
254
+ const srcData = grayscaleImage.data
255
+ const length = srcData.length
256
+ const destData = new Uint8ClampedArray(length)
257
+
258
+ for (let i = 0; i < length; i += 4) {
259
+ const value = srcData[i] < threshold ? 0 : 255
260
+ destData[i] = destData[i + 1] = destData[i + 2] = value
261
+ destData[i + 3] = srcData[i + 3] // 保持透明度
262
+ }
263
+
264
+ return new ImageData(destData, grayscaleImage.width, grayscaleImage.height)
233
265
  }
234
266
 
235
267
  /**
@@ -381,48 +413,38 @@ export class ImageProcessor {
381
413
 
382
414
  img.onload = () => {
383
415
  try {
384
- // 创建canvas元素
385
- const canvas = document.createElement("canvas")
386
- const ctx = canvas.getContext("2d")
387
-
388
- if (!ctx) {
389
- reject(new Error("无法创建2D上下文"))
390
- return
391
- }
392
-
393
- canvas.width = img.width
394
- canvas.height = img.height
416
+ // 使用 Canvas 池获取 canvas
417
+ const { canvas, context } = CanvasPool.getInstance().acquire(img.width, img.height);
395
418
 
396
419
  // 绘制图片到canvas
397
- ctx.drawImage(img, 0, 0)
420
+ context.drawImage(img, 0, 0);
398
421
 
399
422
  // 获取图像数据
400
- const imageData = ctx.getImageData(
401
- 0,
402
- 0,
403
- canvas.width,
404
- canvas.height
405
- )
423
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
424
+
425
+ // 释放回池
426
+ CanvasPool.getInstance().release(canvas);
406
427
 
407
428
  // 释放资源
408
- URL.revokeObjectURL(url)
429
+ URL.revokeObjectURL(url);
409
430
 
410
- resolve(imageData)
431
+ resolve(imageData);
411
432
  } catch (e) {
412
- reject(e)
433
+ URL.revokeObjectURL(url);
434
+ reject(e);
413
435
  }
414
- }
436
+ };
415
437
 
416
438
  img.onerror = () => {
417
- URL.revokeObjectURL(url)
418
- reject(new Error("图片加载失败"))
419
- }
439
+ URL.revokeObjectURL(url);
440
+ reject(new Error("图片加载失败"));
441
+ };
420
442
 
421
- img.src = url
443
+ img.src = url;
422
444
  } catch (error) {
423
- reject(error)
445
+ reject(error);
424
446
  }
425
- })
447
+ });
426
448
  }
427
449
 
428
450
  /**
@@ -442,25 +464,21 @@ export class ImageProcessor {
442
464
  ): Promise<File> {
443
465
  return new Promise((resolve, reject) => {
444
466
  try {
445
- const canvas = document.createElement("canvas")
446
- canvas.width = imageData.width
447
- canvas.height = imageData.height
448
-
449
- const ctx = canvas.getContext("2d")
450
- if (!ctx) {
451
- reject(new Error("无法创建2D上下文"))
452
- return
453
- }
467
+ // 使用 Canvas
468
+ const { canvas, context } = CanvasPool.getInstance().acquire(imageData.width, imageData.height);
454
469
 
455
- ctx.putImageData(imageData, 0, 0)
470
+ context.putImageData(imageData, 0, 0);
456
471
 
457
472
  canvas.toBlob(
458
473
  (blob) => {
474
+ // 释放回池
475
+ CanvasPool.getInstance().release(canvas);
476
+
459
477
  if (!blob) {
460
- reject(new Error("无法创建图片Blob"))
461
- return
478
+ reject(new Error("无法创建图片Blob"));
479
+ return;
462
480
  }
463
- const file = new File([blob], fileName, { type: fileType })
481
+ const file = new File([blob], fileName, { type: fileType });
464
482
  resolve(file)
465
483
  },
466
484
  fileType,
@@ -556,365 +574,20 @@ export class ImageProcessor {
556
574
  }
557
575
 
558
576
  /**
559
- * 边缘检测算法,用于识别图像中的边缘
560
- * 基于Sobel算子实现
561
- *
562
- * @param imageData 原始图像数据,应已转为灰度图
563
- * @param threshold 边缘阈值,默认为30
564
- * @returns 检测到边缘的图像数据
577
+ * @deprecated 请使用 EdgeDetector.detectEdges()
565
578
  */
566
579
  static detectEdges(imageData: ImageData, threshold: number = 30): ImageData {
567
- // 确保输入图像是灰度图
568
- const grayscaleImage = this.toGrayscale(
569
- new ImageData(
570
- new Uint8ClampedArray(imageData.data),
571
- imageData.width,
572
- imageData.height
573
- )
574
- );
575
-
576
- const width = grayscaleImage.width;
577
- const height = grayscaleImage.height;
578
- const inputData = grayscaleImage.data;
579
- const outputData = new Uint8ClampedArray(inputData.length);
580
-
581
- // Sobel算子 - 水平和垂直方向
582
- const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
583
- const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
584
-
585
- // 对每个像素应用Sobel算子
586
- for (let y = 1; y < height - 1; y++) {
587
- for (let x = 1; x < width - 1; x++) {
588
- let gx = 0;
589
- let gy = 0;
590
-
591
- // 应用卷积
592
- for (let ky = -1; ky <= 1; ky++) {
593
- for (let kx = -1; kx <= 1; kx++) {
594
- const pixelPos = ((y + ky) * width + (x + kx)) * 4;
595
- const pixelVal = inputData[pixelPos]; // 灰度值
596
-
597
- const kernelIdx = (ky + 1) * 3 + (kx + 1);
598
- gx += pixelVal * sobelX[kernelIdx];
599
- gy += pixelVal * sobelY[kernelIdx];
600
- }
601
- }
602
-
603
- // 计算梯度强度
604
- let magnitude = Math.sqrt(gx * gx + gy * gy);
605
-
606
- // 应用阈值
607
- magnitude = magnitude > threshold ? 255 : 0;
608
-
609
- // 设置输出像素
610
- const pos = (y * width + x) * 4;
611
- outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
612
- outputData[pos + 3] = 255; // 透明度保持完全不透明
613
- }
614
- }
615
-
616
- // 处理边缘像素
617
- for (let i = 0; i < width * 4; i++) {
618
- // 顶部和底部行
619
- outputData[i] = 0;
620
- outputData[(height - 1) * width * 4 + i] = 0;
621
- }
622
-
623
- for (let i = 0; i < height; i++) {
624
- // 左右两侧列
625
- const leftPos = i * width * 4;
626
- const rightPos = (i * width + width - 1) * 4;
627
-
628
- for (let j = 0; j < 4; j++) {
629
- outputData[leftPos + j] = 0;
630
- outputData[rightPos + j] = 0;
631
- }
632
- }
633
-
634
- return new ImageData(outputData, width, height);
580
+ return EdgeDetector.detectEdges(imageData, threshold);
635
581
  }
636
582
 
637
583
  /**
638
- * 卡尼-德里奇边缘检测
639
- * 相比Sobel更精确的边缘检测算法
640
- *
641
- * @param imageData 灰度图像数据
642
- * @param lowThreshold 低阈值
643
- * @param highThreshold 高阈值
644
- * @returns 边缘检测结果
584
+ * @deprecated 请使用 EdgeDetector.cannyEdgeDetection()
645
585
  */
646
586
  static cannyEdgeDetection(
647
587
  imageData: ImageData,
648
588
  lowThreshold: number = 20,
649
589
  highThreshold: number = 50
650
590
  ): ImageData {
651
- const grayscaleImage = this.toGrayscale(
652
- new ImageData(
653
- new Uint8ClampedArray(imageData.data),
654
- imageData.width,
655
- imageData.height
656
- )
657
- );
658
-
659
- // 1. 高斯模糊
660
- const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
661
-
662
- // 2. 使用Sobel算子计算梯度
663
- const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
664
-
665
- // 3. 非极大值抛弃
666
- const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
667
-
668
- // 4. 双阈值处理
669
- const thresholdResult = this.hysteresisThresholding(
670
- nonMaxSuppressed,
671
- blurredImage.width,
672
- blurredImage.height,
673
- lowThreshold,
674
- highThreshold
675
- );
676
-
677
- // 创建输出图像
678
- const outputData = new Uint8ClampedArray(imageData.data.length);
679
-
680
- // 将结果转换为ImageData
681
- for (let i = 0; i < thresholdResult.length; i++) {
682
- const pos = i * 4;
683
- const value = thresholdResult[i] ? 255 : 0;
684
- outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
685
- outputData[pos + 3] = 255;
686
- }
687
-
688
- return new ImageData(outputData, blurredImage.width, blurredImage.height);
689
- }
690
-
691
- /**
692
- * 高斯模糊
693
- */
694
- private static gaussianBlur(imageData: ImageData, sigma: number = 1.5): ImageData {
695
- const width = imageData.width;
696
- const height = imageData.height;
697
- const inputData = imageData.data;
698
- const outputData = new Uint8ClampedArray(inputData.length);
699
-
700
- // 生成高斯核
701
- const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
702
- const halfKernel = Math.floor(kernelSize / 2);
703
- const kernel = this.generateGaussianKernel(kernelSize, sigma);
704
-
705
- // 应用高斯核
706
- for (let y = 0; y < height; y++) {
707
- for (let x = 0; x < width; x++) {
708
- let sum = 0;
709
- let weightSum = 0;
710
-
711
- for (let ky = -halfKernel; ky <= halfKernel; ky++) {
712
- for (let kx = -halfKernel; kx <= halfKernel; kx++) {
713
- const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
714
- const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
715
- const pixelPos = (pixelY * width + pixelX) * 4;
716
-
717
- const kernelY = ky + halfKernel;
718
- const kernelX = kx + halfKernel;
719
- const weight = kernel[kernelY * kernelSize + kernelX];
720
-
721
- sum += inputData[pixelPos] * weight;
722
- weightSum += weight;
723
- }
724
- }
725
-
726
- const pos = (y * width + x) * 4;
727
- const value = Math.round(sum / weightSum);
728
- outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
729
- outputData[pos + 3] = 255;
730
- }
731
- }
732
-
733
- return new ImageData(outputData, width, height);
734
- }
735
-
736
- /**
737
- * 生成高斯核
738
- */
739
- private static generateGaussianKernel(size: number, sigma: number): number[] {
740
- const kernel = new Array(size * size);
741
- const center = Math.floor(size / 2);
742
- let sum = 0;
743
-
744
- for (let y = 0; y < size; y++) {
745
- for (let x = 0; x < size; x++) {
746
- const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
747
- const value = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
748
-
749
- kernel[y * size + x] = value;
750
- sum += value;
751
- }
752
- }
753
-
754
- // 归一化
755
- for (let i = 0; i < kernel.length; i++) {
756
- kernel[i] /= sum;
757
- }
758
-
759
- return kernel;
760
- }
761
-
762
- /**
763
- * 计算梯度强度和方向
764
- */
765
- private static computeGradients(imageData: ImageData): {
766
- gradientMagnitude: number[],
767
- gradientDirection: number[]
768
- } {
769
- const width = imageData.width;
770
- const height = imageData.height;
771
- const inputData = imageData.data;
772
-
773
- const gradientMagnitude = new Array(width * height);
774
- const gradientDirection = new Array(width * height);
775
-
776
- // Sobel算子
777
- const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
778
- const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
779
-
780
- for (let y = 1; y < height - 1; y++) {
781
- for (let x = 1; x < width - 1; x++) {
782
- let gx = 0;
783
- let gy = 0;
784
-
785
- for (let ky = -1; ky <= 1; ky++) {
786
- for (let kx = -1; kx <= 1; kx++) {
787
- const pixelPos = ((y + ky) * width + (x + kx)) * 4;
788
- const pixelVal = inputData[pixelPos];
789
-
790
- const kernelIdx = (ky + 1) * 3 + (kx + 1);
791
- gx += pixelVal * sobelX[kernelIdx];
792
- gy += pixelVal * sobelY[kernelIdx];
793
- }
794
- }
795
-
796
- const idx = y * width + x;
797
- gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
798
- gradientDirection[idx] = Math.atan2(gy, gx);
799
- }
800
- }
801
-
802
- // 处理边界
803
- for (let y = 0; y < height; y++) {
804
- for (let x = 0; x < width; x++) {
805
- if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
806
- const idx = y * width + x;
807
- gradientMagnitude[idx] = 0;
808
- gradientDirection[idx] = 0;
809
- }
810
- }
811
- }
812
-
813
- return { gradientMagnitude, gradientDirection };
814
- }
815
-
816
- /**
817
- * 非极大值抛弃
818
- */
819
- private static nonMaxSuppression(
820
- gradientMagnitude: number[],
821
- gradientDirection: number[],
822
- width: number,
823
- height: number
824
- ): number[] {
825
- const result = new Array(width * height).fill(0);
826
-
827
- for (let y = 1; y < height - 1; y++) {
828
- for (let x = 1; x < width - 1; x++) {
829
- const idx = y * width + x;
830
- const magnitude = gradientMagnitude[idx];
831
- const direction = gradientDirection[idx];
832
-
833
- // 将方向转化为角度
834
- const degrees = (direction * 180 / Math.PI + 180) % 180;
835
-
836
- // 获取相邻像素索引
837
- let neighbor1Idx, neighbor2Idx;
838
-
839
- // 将方向量化为四个方向: 0°, 45°, 90°, 135°
840
- if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
841
- // 水平方向
842
- neighbor1Idx = idx - 1;
843
- neighbor2Idx = idx + 1;
844
- } else if (degrees >= 22.5 && degrees < 67.5) {
845
- // 45度方向
846
- neighbor1Idx = (y - 1) * width + (x + 1);
847
- neighbor2Idx = (y + 1) * width + (x - 1);
848
- } else if (degrees >= 67.5 && degrees < 112.5) {
849
- // 垂直方向
850
- neighbor1Idx = (y - 1) * width + x;
851
- neighbor2Idx = (y + 1) * width + x;
852
- } else {
853
- // 135度方向
854
- neighbor1Idx = (y - 1) * width + (x - 1);
855
- neighbor2Idx = (y + 1) * width + (x + 1);
856
- }
857
-
858
- // 检查当前像素是否是最大值
859
- if (magnitude >= gradientMagnitude[neighbor1Idx] &&
860
- magnitude >= gradientMagnitude[neighbor2Idx]) {
861
- result[idx] = magnitude;
862
- }
863
- }
864
- }
865
-
866
- return result;
867
- }
868
-
869
- /**
870
- * 双阈值处理
871
- */
872
- private static hysteresisThresholding(
873
- nonMaxSuppressed: number[],
874
- width: number,
875
- height: number,
876
- lowThreshold: number,
877
- highThreshold: number
878
- ): boolean[] {
879
- const result = new Array(width * height).fill(false);
880
- const visited = new Array(width * height).fill(false);
881
- const stack = [];
882
-
883
- // 标记强边缘点
884
- for (let i = 0; i < nonMaxSuppressed.length; i++) {
885
- if (nonMaxSuppressed[i] >= highThreshold) {
886
- result[i] = true;
887
- stack.push(i);
888
- visited[i] = true;
889
- }
890
- }
891
-
892
- // 使用深度优先搜索连接弱边缘
893
- const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
894
- const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
895
-
896
- while (stack.length > 0) {
897
- const currentIdx: number = stack.pop()!;
898
- const currentX: number = currentIdx % width;
899
- const currentY: number = Math.floor(currentIdx / width);
900
-
901
- // 检查88个相邻方向
902
- for (let i = 0; i < 8; i++) {
903
- const newX: number = currentX + dx[i];
904
- const newY: number = currentY + dy[i];
905
-
906
- if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
907
- const newIdx: number = newY * width + newX;
908
-
909
- if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
910
- result[newIdx] = true;
911
- stack.push(newIdx);
912
- visited[newIdx] = true;
913
- }
914
- }
915
- }
916
- }
917
-
918
- return result;
591
+ return EdgeDetector.cannyEdgeDetection(imageData, lowThreshold, highThreshold);
919
592
  }
920
593
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  export * from './error-handler';
8
8
  export * from './retry';
9
+ export * from './canvas-pool';
9
10
 
10
11
  /**
11
12
  * 创建延迟Promise