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.
- package/dist/id-scanner-lib.esm.js +994 -1139
- package/dist/id-scanner-lib.esm.js.map +1 -1
- package/dist/id-scanner-lib.js +995 -1144
- package/dist/id-scanner-lib.js.map +1 -1
- package/package.json +1 -1
- package/src/compat/index.ts +7 -0
- package/src/compat/v1-adapter.ts +84 -0
- package/src/core/camera-manager.ts +43 -76
- package/src/core/camera-stream-manager.ts +318 -0
- package/src/core/config.ts +113 -267
- package/src/core/errors.ts +68 -117
- package/src/core/logger.ts +158 -81
- package/src/core/resource-manager.ts +150 -0
- package/src/core/scanner.ts +109 -0
- package/src/core/utils/browser.ts +7 -0
- package/src/core/utils/canvas-pool.ts +171 -0
- package/src/core/utils/canvas.ts +7 -0
- package/src/core/utils/image.ts +7 -0
- package/src/core/utils/index.ts +9 -0
- package/src/core/utils/resource-manager.ts +155 -0
- package/src/core/utils/validate.ts +7 -0
- package/src/core/utils/worker.ts +130 -0
- package/src/modules/face/comparator/comparator.ts +45 -0
- package/src/modules/face/comparator/index.ts +1 -0
- package/src/modules/face/detector/detector.ts +83 -0
- package/src/modules/face/detector/index.ts +2 -0
- package/src/modules/face/detector/types.ts +80 -0
- package/src/modules/face/face-comparator.ts +150 -0
- package/src/modules/face/face-detector-options.ts +104 -0
- package/src/modules/face/face-detector.ts +121 -376
- package/src/modules/face/face-detector.ts.bak +991 -0
- package/src/modules/face/face-model-loader.ts +222 -0
- package/src/modules/face/face-result-converter.ts +225 -0
- package/src/modules/face/face-tracker.ts +207 -0
- package/src/modules/face/liveness/index.ts +7 -0
- package/src/modules/face/liveness-detector.ts +2 -2
- package/src/modules/face/tracker/index.ts +7 -0
- package/src/modules/id-card/anti-fake/index.ts +7 -0
- package/src/modules/id-card/detector/index.ts +7 -0
- package/src/modules/id-card/id-card-text-parser.ts +151 -0
- package/src/modules/id-card/ocr-processor.ts +20 -257
- package/src/modules/id-card/ocr-worker.ts +2 -183
- package/src/modules/id-card/parser/index.ts +7 -0
- package/src/modules/qr/scanner/index.ts +7 -0
- package/src/utils/canvas-pool.ts +273 -0
- package/src/utils/edge-detector.ts +232 -0
- package/src/utils/image-processing.ts +92 -419
- 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.
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
|
116
|
-
const 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 =
|
|
121
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 =
|
|
213
|
-
|
|
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
|
-
//
|
|
247
|
+
// 先转换为灰度图(返回新 ImageData,不修改原图)
|
|
227
248
|
const grayscaleImage = this.toGrayscale(imageData)
|
|
228
249
|
|
|
229
250
|
// 使用OTSU算法自动确定阈值
|
|
230
251
|
const threshold = this.getOtsuThreshold(grayscaleImage)
|
|
231
252
|
|
|
232
|
-
|
|
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
|
-
//
|
|
385
|
-
const 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
|
-
|
|
420
|
+
context.drawImage(img, 0, 0);
|
|
398
421
|
|
|
399
422
|
// 获取图像数据
|
|
400
|
-
const imageData =
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
446
|
-
canvas
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|