id-scanner-lib 1.6.7 → 1.7.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.
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { IDCardInfo, IDCardType } from './types';
8
+ import { IDCardTextParser } from './id-card-text-parser';
8
9
  import { LoggerMessage } from 'tesseract.js';
9
10
 
10
11
  /**
@@ -57,7 +58,7 @@ export async function processOCRInWorker(
57
58
  const { data } = await worker.recognize(input.imageBase64);
58
59
 
59
60
  // 解析身份证信息
60
- const idCardInfo = parseIDCardText(data.text);
61
+ const idCardInfo = IDCardTextParser.parse(data.text);
61
62
 
62
63
  // 释放Worker资源
63
64
  await worker.terminate();
@@ -74,186 +75,4 @@ export async function processOCRInWorker(
74
75
  }
75
76
  }
76
77
 
77
- /**
78
- * 解析身份证文本
79
- * @param text OCR识别的文本
80
- * @returns 解析后的身份证信息
81
- */
82
- function parseIDCardText(text: string): IDCardInfo {
83
- const info: IDCardInfo = {};
84
-
85
- // 预处理文本,清除多余空白
86
- const processedText = text.replace(/\s+/g, ' ').trim();
87
-
88
- // 解析身份证号码
89
- const idNumberRegex = /(\d{17}[\dX])/;
90
- const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
91
-
92
- const basicMatch = processedText.match(idNumberRegex);
93
- const prefixMatch = processedText.match(idNumberWithPrefixRegex);
94
-
95
- if (prefixMatch && prefixMatch[1]) {
96
- info.idNumber = prefixMatch[1];
97
- } else if (basicMatch && basicMatch[1]) {
98
- info.idNumber = basicMatch[1];
99
- }
100
-
101
- // 解析姓名
102
- const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
103
- const nameMatch = processedText.match(nameWithLabelRegex);
104
-
105
- if (nameMatch && nameMatch[1]) {
106
- info.name = nameMatch[1].trim();
107
- } else {
108
- // 备用方案:查找短行且内容全是汉字
109
- const lines = processedText.split('\n').filter(line => line.trim());
110
- for (const line of lines) {
111
- if (
112
- line.length >= 2 &&
113
- line.length <= 5 &&
114
- /^[一-龥]+$/.test(line) &&
115
- !/性别|民族|住址|公民|签发|有效/.test(line)
116
- ) {
117
- info.name = line.trim();
118
- break;
119
- }
120
- }
121
- }
122
-
123
- // 解析性别和民族
124
- const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
125
- const genderOnlyRegex = /性别[\s\:]*([男女])/;
126
- const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
127
-
128
- const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
129
- const genderOnlyMatch = processedText.match(genderOnlyRegex);
130
- const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
131
-
132
- if (genderNationalityMatch) {
133
- info.gender = genderNationalityMatch[1];
134
- info.ethnicity = genderNationalityMatch[2];
135
- } else {
136
- if (genderOnlyMatch) info.gender = genderOnlyMatch[1];
137
- if (nationalityOnlyMatch) info.ethnicity = nationalityOnlyMatch[1];
138
- }
139
-
140
- // 根据内容判断身份证类型
141
- if (processedText.includes('出生') || processedText.includes('公民身份号码')) {
142
- info.type = IDCardType.FRONT; // 确保类型为枚举值而不是字符串
143
- } else if (processedText.includes('签发机关') || processedText.includes('有效期')) {
144
- info.type = IDCardType.BACK; // 确保类型为枚举值而不是字符串
145
- }
146
-
147
- // 解析出生日期
148
- const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
149
- const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
150
- const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
151
-
152
- const birthDateMatch =
153
- processedText.match(birthDateRegex1) ||
154
- processedText.match(birthDateRegex2) ||
155
- processedText.match(birthDateRegex3);
156
-
157
- if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
158
- const year = info.idNumber.substring(6, 10);
159
- const month = info.idNumber.substring(10, 12);
160
- const day = info.idNumber.substring(12, 14);
161
- info.birthDate = `${year}-${month}-${day}`;
162
- } else if (birthDateMatch) {
163
- const year = birthDateMatch[1];
164
- const month = birthDateMatch[2].padStart(2, '0');
165
- const day = birthDateMatch[3].padStart(2, '0');
166
- info.birthDate = `${year}-${month}-${day}`;
167
- }
168
-
169
- // 解析地址
170
- const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
171
- const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
172
-
173
- const addressMatch =
174
- processedText.match(addressRegex1) || processedText.match(addressRegex2);
175
78
 
176
- if (addressMatch && addressMatch[1]) {
177
- info.address = addressMatch[1]
178
- .replace(/\s+/g, '')
179
- .replace(/\n/g, '')
180
- .trim();
181
-
182
- if (info.address.length > 70) {
183
- info.address = info.address.substring(0, 70);
184
- }
185
-
186
- if (!/[一-龥]/.test(info.address)) {
187
- info.address = '';
188
- }
189
- }
190
-
191
- // 解析签发机关
192
- const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
193
- const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
194
-
195
- const authorityMatch =
196
- processedText.match(authorityRegex1) ||
197
- processedText.match(authorityRegex2);
198
-
199
- if (authorityMatch && authorityMatch[1]) {
200
- info.issueAuthority = authorityMatch[1]
201
- .replace(/\s+/g, '')
202
- .replace(/\n/g, '')
203
- .trim();
204
- }
205
-
206
- // 解析有效期限
207
- const validPeriodRegex1 = /有效期限[\s\:]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日\s]*)[-\s]*(至|-)[-\s]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日]*|[永久长期]*)/;
208
- const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
209
-
210
- const validPeriodMatch =
211
- processedText.match(validPeriodRegex1) ||
212
- processedText.match(validPeriodRegex2);
213
-
214
- if (validPeriodMatch) {
215
- if (validPeriodMatch[1] && validPeriodMatch[3]) {
216
- const startDate = formatDateString(validPeriodMatch[1]);
217
- const endDate = /\d/.test(validPeriodMatch[3])
218
- ? formatDateString(validPeriodMatch[3])
219
- : '长期有效';
220
-
221
- info.validFrom = startDate;
222
- info.validTo = endDate;
223
- info.validPeriod = `${startDate}-${endDate}`;
224
- } else {
225
- info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim();
226
- }
227
- }
228
-
229
- return info;
230
- }
231
-
232
- /**
233
- * 格式化日期字符串
234
- * @param dateStr 原始日期字符串
235
- * @returns 格式化后的日期字符串
236
- */
237
- function formatDateString(dateStr: string): string {
238
- // 提取年月日
239
- const dateMatch = dateStr.match(
240
- /(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/
241
- );
242
- if (dateMatch) {
243
- const year = dateMatch[1];
244
- const month = dateMatch[2].padStart(2, '0');
245
- const day = dateMatch[3].padStart(2, '0');
246
- return `${year}-${month}-${day}`;
247
- }
248
-
249
- // 纯数字格式如 20220101
250
- if (/^\d{8}$/.test(dateStr)) {
251
- const year = dateStr.substring(0, 4);
252
- const month = dateStr.substring(4, 6);
253
- const day = dateStr.substring(6, 8);
254
- return `${year}-${month}-${day}`;
255
- }
256
-
257
- // 无法格式化,返回原始字符串
258
- return dateStr;
259
- }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * @file Canvas 对象池
3
+ * @description 提供 Canvas 元素的复用机制,减少内存分配和 GC 压力
4
+ * @module utils/canvas-pool
5
+ */
6
+
7
+ /**
8
+ * Canvas 池条目
9
+ */
10
+ interface CanvasPoolItem {
11
+ /** Canvas 元素 */
12
+ canvas: HTMLCanvasElement;
13
+ /** Canvas 2D 上下文 */
14
+ context: CanvasRenderingContext2D;
15
+ /** 是否正在使用 */
16
+ inUse: boolean;
17
+ /** 最近使用时间戳 */
18
+ lastUsed: number;
19
+ /** Canvas 尺寸标识 */
20
+ sizeKey: string;
21
+ }
22
+
23
+ /**
24
+ * Canvas 对象池
25
+ *
26
+ * 复用 Canvas 元素,避免频繁创建和销毁导致的内存抖动
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const pool = CanvasPool.getInstance();
31
+ * const { canvas, context } = pool.acquire(100, 200);
32
+ * // 使用 canvas 进行绘制...
33
+ * pool.release(canvas);
34
+ * ```
35
+ */
36
+ export class CanvasPool {
37
+ /** 单例实例 */
38
+ private static instance: CanvasPool | null = null;
39
+
40
+ /** Canvas 池存储 */
41
+ private pool: Map<string, CanvasPoolItem[]> = new Map();
42
+
43
+ /** 已借出的 Canvas */
44
+ private borrowed: Map<HTMLCanvasElement, CanvasPoolItem> = new Map();
45
+
46
+ /** 最大池大小(每个尺寸) */
47
+ private maxPoolSize: number = 4;
48
+
49
+ /** Canvas 尺寸容差(允许一定范围的尺寸复用) */
50
+ private sizeTolerance: number = 10;
51
+
52
+ /**
53
+ * 获取单例实例
54
+ */
55
+ public static getInstance(): CanvasPool {
56
+ if (!CanvasPool.instance) {
57
+ CanvasPool.instance = new CanvasPool();
58
+ }
59
+ return CanvasPool.instance;
60
+ }
61
+
62
+ /**
63
+ * 重置单例实例(主要用于测试)
64
+ */
65
+ public static resetInstance(): void {
66
+ if (CanvasPool.instance) {
67
+ CanvasPool.instance.dispose();
68
+ CanvasPool.instance = null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 私有构造函数
74
+ */
75
+ private constructor() {
76
+ // 页面卸载前清理
77
+ if (typeof window !== 'undefined') {
78
+ window.addEventListener('beforeunload', () => this.dispose());
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 生成尺寸键
84
+ * @param width 宽度
85
+ * @param height 高度
86
+ */
87
+ private getSizeKey(width: number, height: number): string {
88
+ return `${width}x${height}`;
89
+ }
90
+
91
+ /**
92
+ * 查找匹配的尺寸键(考虑容差)
93
+ * @param width 宽度
94
+ * @param height 高度
95
+ */
96
+ private findMatchingSizeKey(width: number, height: number): string | null {
97
+ for (const [key, items] of this.pool.entries()) {
98
+ const [w, h] = key.split('x').map(Number);
99
+ if (Math.abs(w - width) <= this.sizeTolerance &&
100
+ Math.abs(h - height) <= this.sizeTolerance) {
101
+ // 找到可用的
102
+ const available = items.filter(item => !item.inUse);
103
+ if (available.length > 0) {
104
+ return key;
105
+ }
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * 从池中获取 Canvas
113
+ *
114
+ * @param width 宽度
115
+ * @param height 高度
116
+ * @returns Canvas 和其上下文
117
+ */
118
+ public acquire(width: number, height: number): {
119
+ canvas: HTMLCanvasElement;
120
+ context: CanvasRenderingContext2D;
121
+ } {
122
+ // 先尝试精确匹配
123
+ let sizeKey = this.getSizeKey(width, height);
124
+ let items = this.pool.get(sizeKey);
125
+
126
+ // 如果没有精确匹配,尝试模糊匹配
127
+ if (!items || items.every(item => item.inUse)) {
128
+ const matchedKey = this.findMatchingSizeKey(width, height);
129
+ if (matchedKey) {
130
+ sizeKey = matchedKey;
131
+ items = this.pool.get(sizeKey);
132
+ }
133
+ }
134
+
135
+ // 如果没有可用的,创建一个新的
136
+ if (!items || items.every(item => item.inUse)) {
137
+ const canvas = document.createElement('canvas');
138
+ canvas.width = width;
139
+ canvas.height = height;
140
+ const context = canvas.getContext('2d')!;
141
+
142
+ const item: CanvasPoolItem = {
143
+ canvas,
144
+ context,
145
+ inUse: true,
146
+ lastUsed: Date.now(),
147
+ sizeKey: this.getSizeKey(width, height)
148
+ };
149
+
150
+ // 如果池已满,移除最老的
151
+ if (!items) {
152
+ items = [];
153
+ this.pool.set(sizeKey, items);
154
+ } else if (items.length >= this.maxPoolSize) {
155
+ // 找到最老的未使用项并移除
156
+ let oldestIdx = 0;
157
+ let oldestTime = Infinity;
158
+ items.forEach((item, idx) => {
159
+ if (!item.inUse && item.lastUsed < oldestTime) {
160
+ oldestTime = item.lastUsed;
161
+ oldestIdx = idx;
162
+ }
163
+ });
164
+ const removed = items.splice(oldestIdx, 1)[0];
165
+ this.borrowed.delete(removed.canvas);
166
+ }
167
+
168
+ items.push(item);
169
+ this.borrowed.set(canvas, item);
170
+
171
+ return { canvas, context };
172
+ }
173
+
174
+ // 找到一个空闲的
175
+ const available = items.find(item => !item.inUse)!;
176
+ available.inUse = true;
177
+ available.lastUsed = Date.now();
178
+
179
+ // 如果尺寸变化,更新 canvas
180
+ if (available.canvas.width !== width || available.canvas.height !== height) {
181
+ available.canvas.width = width;
182
+ available.canvas.height = height;
183
+ available.sizeKey = sizeKey;
184
+ }
185
+
186
+ // 清除之前的上下文状态
187
+ available.context.setTransform(1, 0, 0, 1, 0, 0);
188
+ available.context.clearRect(0, 0, width, height);
189
+
190
+ this.borrowed.set(available.canvas, available);
191
+
192
+ return { canvas: available.canvas, context: available.context };
193
+ }
194
+
195
+ /**
196
+ * 释放 Canvas 回池中
197
+ *
198
+ * @param canvas 要释放的 Canvas
199
+ */
200
+ public release(canvas: HTMLCanvasElement): void {
201
+ const item = this.borrowed.get(canvas);
202
+ if (!item) {
203
+ // 不属于我们管理的 Canvas,忽略
204
+ return;
205
+ }
206
+
207
+ item.inUse = false;
208
+ item.lastUsed = Date.now();
209
+ this.borrowed.delete(canvas);
210
+ }
211
+
212
+ /**
213
+ * 批量释放所有借出的 Canvas
214
+ */
215
+ public releaseAll(): void {
216
+ for (const [, item] of this.borrowed) {
217
+ item.inUse = false;
218
+ item.lastUsed = Date.now();
219
+ }
220
+ this.borrowed.clear();
221
+ }
222
+
223
+ /**
224
+ * 预热池(预创建指定尺寸的 Canvas)
225
+ *
226
+ * @param sizes 尺寸数组,每项为 [width, height]
227
+ */
228
+ public warmup(sizes: Array<[number, number]>): void {
229
+ for (const [width, height] of sizes) {
230
+ this.acquire(width, height);
231
+ // 立即释放,让它们进入池中
232
+ const sizeKey = this.getSizeKey(width, height);
233
+ const items = this.pool.get(sizeKey);
234
+ if (items && items.length > 0) {
235
+ const item = items[items.length - 1];
236
+ item.inUse = false;
237
+ this.borrowed.delete(item.canvas);
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * 获取池统计信息
244
+ */
245
+ public getStats(): {
246
+ totalItems: number;
247
+ borrowedCount: number;
248
+ poolSizes: Record<string, { total: number; available: number }>;
249
+ } {
250
+ let totalItems = 0;
251
+ let borrowedCount = 0;
252
+ const poolSizes: Record<string, { total: number; available: number }> = {};
253
+
254
+ for (const [key, items] of this.pool.entries()) {
255
+ totalItems += items.length;
256
+ borrowedCount += items.filter(i => i.inUse).length;
257
+ poolSizes[key] = {
258
+ total: items.length,
259
+ available: items.filter(i => !i.inUse).length
260
+ };
261
+ }
262
+
263
+ return { totalItems, borrowedCount, poolSizes };
264
+ }
265
+
266
+ /**
267
+ * 清理并释放所有资源
268
+ */
269
+ public dispose(): void {
270
+ this.pool.clear();
271
+ this.borrowed.clear();
272
+ }
273
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @file 边缘检测器
3
+ * @description 提供边缘检测算法(Sobel、Canny等)
4
+ * @module utils/edge-detector
5
+ */
6
+
7
+ /**
8
+ * 边缘检测器类
9
+ * 提供各种边缘检测算法用于图像处理
10
+ */
11
+ export class EdgeDetector {
12
+ /**
13
+ * 使用Sobel算子进行边缘检测
14
+ * @param imageData 灰度图像数据
15
+ * @param threshold 边缘阈值,默认为30
16
+ * @returns 检测到边缘的图像数据
17
+ */
18
+ static detectEdges(imageData: ImageData, threshold: number = 30): ImageData {
19
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
20
+ const width = grayscaleImage.width;
21
+ const height = grayscaleImage.height;
22
+ const inputData = grayscaleImage.data;
23
+ const outputData = new Uint8ClampedArray(inputData.length);
24
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
25
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
26
+
27
+ for (let y = 1; y < height - 1; y++) {
28
+ for (let x = 1; x < width - 1; x++) {
29
+ let gx = 0, gy = 0;
30
+ for (let ky = -1; ky <= 1; ky++) {
31
+ for (let kx = -1; kx <= 1; kx++) {
32
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
33
+ const pixelVal = inputData[pixelPos];
34
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
35
+ gx += pixelVal * sobelX[kernelIdx];
36
+ gy += pixelVal * sobelY[kernelIdx];
37
+ }
38
+ }
39
+ let magnitude = Math.sqrt(gx * gx + gy * gy);
40
+ magnitude = magnitude > threshold ? 255 : 0;
41
+ const pos = (y * width + x) * 4;
42
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
43
+ outputData[pos + 3] = 255;
44
+ }
45
+ }
46
+
47
+ // 处理边缘
48
+ for (let i = 0; i < width * 4; i++) {
49
+ outputData[i] = 0;
50
+ outputData[(height - 1) * width * 4 + i] = 0;
51
+ }
52
+ for (let i = 0; i < height; i++) {
53
+ const leftPos = i * width * 4;
54
+ const rightPos = (i * width + width - 1) * 4;
55
+ for (let j = 0; j < 4; j++) {
56
+ outputData[leftPos + j] = 0;
57
+ outputData[rightPos + j] = 0;
58
+ }
59
+ }
60
+
61
+ return new ImageData(outputData, width, height);
62
+ }
63
+
64
+ /**
65
+ * 卡尼-德里奇边缘检测
66
+ */
67
+ static cannyEdgeDetection(imageData: ImageData, lowThreshold: number = 20, highThreshold: number = 50): ImageData {
68
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
69
+ const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
70
+ const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
71
+ const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
72
+ const thresholdResult = this.hysteresisThresholding(nonMaxSuppressed, blurredImage.width, blurredImage.height, lowThreshold, highThreshold);
73
+
74
+ const outputData = new Uint8ClampedArray(imageData.data.length);
75
+ for (let i = 0; i < thresholdResult.length; i++) {
76
+ const pos = i * 4;
77
+ const value = thresholdResult[i] ? 255 : 0;
78
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
79
+ outputData[pos + 3] = 255;
80
+ }
81
+ return new ImageData(outputData, blurredImage.width, blurredImage.height);
82
+ }
83
+
84
+ private static toGrayscale(imageData: ImageData): ImageData {
85
+ const srcData = imageData.data;
86
+ const destData = new Uint8ClampedArray(srcData);
87
+ for (let i = 0; i < srcData.length; i += 4) {
88
+ const gray = srcData[i] * 0.3 + srcData[i + 1] * 0.59 + srcData[i + 2] * 0.11;
89
+ destData[i] = destData[i + 1] = destData[i + 2] = gray;
90
+ destData[i + 3] = srcData[i + 3];
91
+ }
92
+ return new ImageData(destData, imageData.width, imageData.height);
93
+ }
94
+
95
+ private static gaussianBlur(imageData: ImageData, sigma: number = 1.5): ImageData {
96
+ const width = imageData.width, height = imageData.height;
97
+ const inputData = imageData.data, outputData = new Uint8ClampedArray(inputData.length);
98
+ const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
99
+ const halfKernel = Math.floor(kernelSize / 2);
100
+ const kernel = this.generateGaussianKernel(kernelSize, sigma);
101
+
102
+ for (let y = 0; y < height; y++) {
103
+ for (let x = 0; x < width; x++) {
104
+ let sum = 0, weightSum = 0;
105
+ for (let ky = -halfKernel; ky <= halfKernel; ky++) {
106
+ for (let kx = -halfKernel; kx <= halfKernel; kx++) {
107
+ const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
108
+ const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
109
+ const pixelPos = (pixelY * width + pixelX) * 4;
110
+ const kernelY = ky + halfKernel, kernelX = kx + halfKernel;
111
+ const weight = kernel[kernelY * kernelSize + kernelX];
112
+ sum += inputData[pixelPos] * weight;
113
+ weightSum += weight;
114
+ }
115
+ }
116
+ const pos = (y * width + x) * 4;
117
+ const value = Math.round(sum / weightSum);
118
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
119
+ outputData[pos + 3] = 255;
120
+ }
121
+ }
122
+ return new ImageData(outputData, width, height);
123
+ }
124
+
125
+ private static generateGaussianKernel(size: number, sigma: number): number[] {
126
+ const kernel = new Array(size * size);
127
+ const center = Math.floor(size / 2);
128
+ let sum = 0;
129
+ for (let y = 0; y < size; y++) {
130
+ for (let x = 0; x < size; x++) {
131
+ const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
132
+ kernel[y * size + x] = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
133
+ sum += kernel[y * size + x];
134
+ }
135
+ }
136
+ for (let i = 0; i < kernel.length; i++) kernel[i] /= sum;
137
+ return kernel;
138
+ }
139
+
140
+ private static computeGradients(imageData: ImageData): { gradientMagnitude: number[], gradientDirection: number[] } {
141
+ const width = imageData.width, height = imageData.height;
142
+ const inputData = imageData.data;
143
+ const gradientMagnitude = new Array(width * height);
144
+ const gradientDirection = new Array(width * height);
145
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
146
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
147
+
148
+ for (let y = 1; y < height - 1; y++) {
149
+ for (let x = 1; x < width - 1; x++) {
150
+ let gx = 0, gy = 0;
151
+ for (let ky = -1; ky <= 1; ky++) {
152
+ for (let kx = -1; kx <= 1; kx++) {
153
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
154
+ const pixelVal = inputData[pixelPos];
155
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
156
+ gx += pixelVal * sobelX[kernelIdx];
157
+ gy += pixelVal * sobelY[kernelIdx];
158
+ }
159
+ }
160
+ const idx = y * width + x;
161
+ gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
162
+ gradientDirection[idx] = Math.atan2(gy, gx);
163
+ }
164
+ }
165
+ return { gradientMagnitude, gradientDirection };
166
+ }
167
+
168
+ private static nonMaxSuppression(gradientMagnitude: number[], gradientDirection: number[], width: number, height: number): number[] {
169
+ const result = new Array(width * height).fill(0);
170
+ for (let y = 1; y < height - 1; y++) {
171
+ for (let x = 1; x < width - 1; x++) {
172
+ const idx = y * width + x;
173
+ const magnitude = gradientMagnitude[idx];
174
+ const direction = gradientDirection[idx];
175
+ const degrees = (direction * 180 / Math.PI + 180) % 180;
176
+ let neighbor1Idx: number, neighbor2Idx: number;
177
+
178
+ if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
179
+ neighbor1Idx = idx - 1; neighbor2Idx = idx + 1;
180
+ } else if (degrees >= 22.5 && degrees < 67.5) {
181
+ neighbor1Idx = (y - 1) * width + (x + 1); neighbor2Idx = (y + 1) * width + (x - 1);
182
+ } else if (degrees >= 67.5 && degrees < 112.5) {
183
+ neighbor1Idx = (y - 1) * width + x; neighbor2Idx = (y + 1) * width + x;
184
+ } else {
185
+ neighbor1Idx = (y - 1) * width + (x - 1); neighbor2Idx = (y + 1) * width + (x + 1);
186
+ }
187
+
188
+ if (magnitude >= gradientMagnitude[neighbor1Idx] && magnitude >= gradientMagnitude[neighbor2Idx]) {
189
+ result[idx] = magnitude;
190
+ }
191
+ }
192
+ }
193
+ return result;
194
+ }
195
+
196
+ private static hysteresisThresholding(nonMaxSuppressed: number[], width: number, height: number, lowThreshold: number, highThreshold: number): boolean[] {
197
+ const result = new Array(width * height).fill(false);
198
+ const visited = new Array(width * height).fill(false);
199
+ const stack: number[] = [];
200
+
201
+ for (let i = 0; i < nonMaxSuppressed.length; i++) {
202
+ if (nonMaxSuppressed[i] >= highThreshold) {
203
+ result[i] = true;
204
+ stack.push(i);
205
+ visited[i] = true;
206
+ }
207
+ }
208
+
209
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
210
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
211
+
212
+ while (stack.length > 0) {
213
+ const currentIdx = stack.pop()!;
214
+ const currentX = currentIdx % width;
215
+ const currentY = Math.floor(currentIdx / width);
216
+
217
+ for (let i = 0; i < 8; i++) {
218
+ const newX = currentX + dx[i];
219
+ const newY = currentY + dy[i];
220
+ if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
221
+ const newIdx = newY * width + newX;
222
+ if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
223
+ result[newIdx] = true;
224
+ stack.push(newIdx);
225
+ visited[newIdx] = true;
226
+ }
227
+ }
228
+ }
229
+ }
230
+ return result;
231
+ }
232
+ }