id-scanner-lib 1.0.0 → 1.2.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 +173 -0
- package/dist/core.d.ts +77 -0
- package/dist/id-recognition/data-extractor.d.ts +31 -0
- package/dist/id-recognition/id-detector.d.ts +25 -1
- package/dist/id-scanner-core.esm.js +10870 -0
- package/dist/id-scanner-core.esm.js.map +1 -0
- package/dist/id-scanner-core.js +10882 -0
- package/dist/id-scanner-core.js.map +1 -0
- package/dist/id-scanner-core.min.js +9 -0
- package/dist/id-scanner-core.min.js.map +1 -0
- package/dist/id-scanner-ocr.esm.js +1625 -0
- package/dist/id-scanner-ocr.esm.js.map +1 -0
- package/dist/id-scanner-ocr.js +1634 -0
- package/dist/id-scanner-ocr.js.map +1 -0
- package/dist/id-scanner-ocr.min.js +9 -0
- package/dist/id-scanner-ocr.min.js.map +1 -0
- package/dist/id-scanner-qr.esm.js +773 -0
- package/dist/id-scanner-qr.esm.js.map +1 -0
- package/dist/id-scanner-qr.js +782 -0
- package/dist/id-scanner-qr.js.map +1 -0
- package/dist/id-scanner-qr.min.js +9 -0
- package/dist/id-scanner-qr.min.js.map +1 -0
- package/dist/id-scanner.js +1954 -94656
- package/dist/id-scanner.js.map +1 -1
- package/dist/id-scanner.min.js +7 -7
- package/dist/id-scanner.min.js.map +1 -1
- package/dist/index-umd.d.ts +96 -0
- package/dist/index.d.ts +23 -88
- package/dist/ocr-module.d.ts +67 -0
- package/dist/qr-module.d.ts +68 -0
- package/dist/types/core.d.ts +77 -0
- package/dist/types/demo/demo.d.ts +14 -0
- package/dist/types/id-recognition/data-extractor.d.ts +105 -0
- package/dist/types/id-recognition/id-detector.d.ts +100 -0
- package/dist/types/id-recognition/ocr-processor.d.ts +64 -0
- package/dist/types/index-umd.d.ts +96 -0
- package/dist/types/index.d.ts +78 -0
- package/dist/types/ocr-module.d.ts +67 -0
- package/dist/types/qr-module.d.ts +68 -0
- package/dist/types/scanner/barcode-scanner.d.ts +90 -0
- package/dist/types/scanner/qr-scanner.d.ts +80 -0
- package/dist/types/utils/camera.d.ts +81 -0
- package/dist/types/utils/image-processing.d.ts +75 -0
- package/dist/types/utils/types.d.ts +65 -0
- package/dist/utils/camera.d.ts +18 -13
- package/dist/utils/types.d.ts +6 -6
- package/package.json +25 -4
- package/src/core.ts +138 -0
- package/src/id-recognition/data-extractor.ts +97 -0
- package/src/id-recognition/id-detector.ts +230 -67
- package/src/id-recognition/ocr-processor.ts +145 -27
- package/src/id-recognition/ocr-worker.ts +146 -0
- package/src/index-umd.ts +240 -0
- package/src/index.ts +125 -139
- package/src/ocr-module.ts +139 -0
- package/src/qr-module.ts +129 -0
- package/src/utils/camera.ts +61 -36
- package/src/utils/image-processing.ts +204 -0
- package/src/utils/performance.ts +208 -0
- package/src/utils/resource-manager.ts +198 -0
- package/src/utils/types.ts +23 -6
- package/src/utils/worker.ts +173 -0
package/src/utils/camera.ts
CHANGED
|
@@ -34,13 +34,13 @@ export interface CameraOptions {
|
|
|
34
34
|
*
|
|
35
35
|
* // 初始化相机
|
|
36
36
|
* const videoElement = document.getElementById('video') as HTMLVideoElement;
|
|
37
|
-
* await camera.
|
|
37
|
+
* await camera.start(videoElement);
|
|
38
38
|
*
|
|
39
39
|
* // 捕获当前视频帧
|
|
40
40
|
* const imageData = camera.captureFrame();
|
|
41
41
|
*
|
|
42
42
|
* // 使用结束后释放资源
|
|
43
|
-
* camera.
|
|
43
|
+
* camera.stop();
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
export class Camera {
|
|
@@ -49,7 +49,6 @@ export class Camera {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* 创建相机实例
|
|
52
|
-
*
|
|
53
52
|
* @param {CameraOptions} [options] - 相机配置选项
|
|
54
53
|
*/
|
|
55
54
|
constructor(private options: CameraOptions = {}) {
|
|
@@ -62,75 +61,101 @@ export class Camera {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
/**
|
|
65
|
-
*
|
|
64
|
+
* 启动摄像头并将视频流绑定到视频元素
|
|
65
|
+
* @param videoElement HTML视频元素
|
|
66
|
+
* @returns Promise<void>
|
|
67
|
+
*/
|
|
68
|
+
async start(videoElement: HTMLVideoElement): Promise<void> {
|
|
69
|
+
return this.initialize(videoElement);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 停止摄像头并释放资源
|
|
74
|
+
*/
|
|
75
|
+
stop(): void {
|
|
76
|
+
this.release();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 初始化相机,获取视频流并绑定到视频元素
|
|
66
81
|
*
|
|
67
|
-
* @param {HTMLVideoElement} videoElement -
|
|
82
|
+
* @param {HTMLVideoElement} videoElement - 用于显示视频流的视频元素
|
|
68
83
|
* @returns {Promise<void>} 初始化完成的Promise
|
|
69
|
-
* @throws
|
|
84
|
+
* @throws 如果无法访问摄像头,将抛出错误
|
|
70
85
|
*/
|
|
71
86
|
async initialize(videoElement: HTMLVideoElement): Promise<void> {
|
|
87
|
+
this.videoElement = videoElement;
|
|
88
|
+
|
|
72
89
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
// 构建媒体约束
|
|
75
91
|
const constraints: MediaStreamConstraints = {
|
|
76
92
|
video: {
|
|
77
|
-
width: this.options.width,
|
|
78
|
-
height: this.options.height,
|
|
93
|
+
width: { ideal: this.options.width },
|
|
94
|
+
height: { ideal: this.options.height },
|
|
79
95
|
facingMode: this.options.facingMode
|
|
80
96
|
}
|
|
81
97
|
};
|
|
82
98
|
|
|
99
|
+
// 获取视频流
|
|
83
100
|
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
84
|
-
this.videoElement.srcObject = this.stream;
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
// 绑定到视频元素
|
|
103
|
+
if (this.videoElement) {
|
|
104
|
+
this.videoElement.srcObject = this.stream;
|
|
105
|
+
await new Promise<void>((resolve) => {
|
|
106
|
+
if (this.videoElement) {
|
|
107
|
+
this.videoElement.onloadedmetadata = () => {
|
|
108
|
+
if (this.videoElement) {
|
|
109
|
+
this.videoElement.play().then(() => resolve());
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
96
115
|
} catch (error) {
|
|
97
|
-
console.error('
|
|
98
|
-
throw new Error('
|
|
116
|
+
console.error('无法访问摄像头:', error);
|
|
117
|
+
throw new Error('无法访问摄像头。请确保已授予摄像头访问权限,并且摄像头未被其他应用程序占用。');
|
|
99
118
|
}
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* 捕获当前视频画面并转换为ImageData对象,可用于图像处理和分析
|
|
122
|
+
* 捕获当前视频帧
|
|
106
123
|
*
|
|
107
|
-
* @returns {ImageData|null}
|
|
124
|
+
* @returns {ImageData|null} 视频帧的ImageData对象,如果未初始化则返回null
|
|
108
125
|
*/
|
|
109
126
|
captureFrame(): ImageData | null {
|
|
110
|
-
if (!this.videoElement)
|
|
127
|
+
if (!this.videoElement) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
111
130
|
|
|
131
|
+
// 创建Canvas元素用于捕获视频帧
|
|
112
132
|
const canvas = document.createElement('canvas');
|
|
113
|
-
const ctx = canvas.getContext('2d');
|
|
114
|
-
if (!ctx) return null;
|
|
115
|
-
|
|
116
133
|
canvas.width = this.videoElement.videoWidth;
|
|
117
134
|
canvas.height = this.videoElement.videoHeight;
|
|
118
|
-
ctx.drawImage(this.videoElement, 0, 0);
|
|
119
135
|
|
|
120
|
-
|
|
136
|
+
const context = canvas.getContext('2d');
|
|
137
|
+
if (!context) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 将视频内容绘制到Canvas中
|
|
142
|
+
context.drawImage(this.videoElement, 0, 0, canvas.width, canvas.height);
|
|
143
|
+
|
|
144
|
+
// 获取ImageData对象
|
|
145
|
+
return context.getImageData(0, 0, canvas.width, canvas.height);
|
|
121
146
|
}
|
|
122
147
|
|
|
123
148
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* 停止所有视频流轨道并清理资源。在不再需要相机时应调用此方法。
|
|
149
|
+
* 释放摄像头资源
|
|
127
150
|
*/
|
|
128
151
|
release(): void {
|
|
152
|
+
// 停止视频流的所有轨道
|
|
129
153
|
if (this.stream) {
|
|
130
154
|
this.stream.getTracks().forEach(track => track.stop());
|
|
131
155
|
this.stream = null;
|
|
132
156
|
}
|
|
133
157
|
|
|
158
|
+
// 清除视频元素绑定
|
|
134
159
|
if (this.videoElement) {
|
|
135
160
|
this.videoElement.srcObject = null;
|
|
136
161
|
this.videoElement = null;
|
|
@@ -154,4 +154,208 @@ export class ImageProcessor {
|
|
|
154
154
|
|
|
155
155
|
return imageData;
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 降低图像分辨率以提高处理速度
|
|
160
|
+
*
|
|
161
|
+
* 对于OCR和图像分析,降低分辨率可以在保持识别率的同时大幅提升处理速度
|
|
162
|
+
*
|
|
163
|
+
* @param {ImageData} imageData - 原图像数据
|
|
164
|
+
* @param {number} [maxDimension=1000] - 目标最大尺寸(宽或高)
|
|
165
|
+
* @returns {ImageData} 处理后的图像数据
|
|
166
|
+
*/
|
|
167
|
+
static downsampleForProcessing(imageData: ImageData, maxDimension: number = 1000): ImageData {
|
|
168
|
+
const { width, height } = imageData;
|
|
169
|
+
|
|
170
|
+
// 如果图像尺寸已经小于或等于目标尺寸,则无需处理
|
|
171
|
+
if (width <= maxDimension && height <= maxDimension) {
|
|
172
|
+
return imageData;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 计算缩放比例,保持宽高比
|
|
176
|
+
const scale = maxDimension / Math.max(width, height);
|
|
177
|
+
const newWidth = Math.round(width * scale);
|
|
178
|
+
const newHeight = Math.round(height * scale);
|
|
179
|
+
|
|
180
|
+
// 调整图像大小
|
|
181
|
+
return this.resize(imageData, newWidth, newHeight);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 转换图像为Base64格式,方便在Worker线程中传递
|
|
186
|
+
*
|
|
187
|
+
* @param {ImageData} imageData - 原图像数据
|
|
188
|
+
* @returns {string} base64编码的图像数据
|
|
189
|
+
*/
|
|
190
|
+
static imageDataToBase64(imageData: ImageData): string {
|
|
191
|
+
const canvas = this.imageDataToCanvas(imageData);
|
|
192
|
+
return canvas.toDataURL('image/jpeg', 0.7); // 使用较低质量的JPEG格式减少数据量
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 从Base64字符串还原图像数据
|
|
197
|
+
*
|
|
198
|
+
* @param {string} base64 - base64编码的图像数据
|
|
199
|
+
* @returns {Promise<ImageData>} 还原的图像数据
|
|
200
|
+
*/
|
|
201
|
+
static async base64ToImageData(base64: string): Promise<ImageData> {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const img = new Image();
|
|
204
|
+
img.onload = () => {
|
|
205
|
+
const canvas = document.createElement('canvas');
|
|
206
|
+
canvas.width = img.width;
|
|
207
|
+
canvas.height = img.height;
|
|
208
|
+
const ctx = canvas.getContext('2d');
|
|
209
|
+
|
|
210
|
+
if (!ctx) {
|
|
211
|
+
reject(new Error('无法创建Canvas上下文'));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
ctx.drawImage(img, 0, 0);
|
|
216
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
217
|
+
resolve(imageData);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
img.onerror = () => {
|
|
221
|
+
reject(new Error('图像加载失败'));
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
img.src = base64;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 使用Web Worker并行处理图像
|
|
230
|
+
* 此方法将图像分割为多个部分,并行处理以提高性能
|
|
231
|
+
*
|
|
232
|
+
* @param {ImageData} imageData - 原图像数据
|
|
233
|
+
* @param {Function} processingFunction - 处理函数,接收ImageData返回ImageData
|
|
234
|
+
* @param {number} [chunks=4] - 分割的块数
|
|
235
|
+
* @returns {Promise<ImageData>} 处理后的图像数据
|
|
236
|
+
*/
|
|
237
|
+
static async processImageInParallel(
|
|
238
|
+
imageData: ImageData,
|
|
239
|
+
processingFunction: (imgData: ImageData) => ImageData,
|
|
240
|
+
chunks: number = 4
|
|
241
|
+
): Promise<ImageData> {
|
|
242
|
+
// 如果不支持Worker或图像太小,直接处理
|
|
243
|
+
if (typeof Worker === 'undefined' || imageData.width * imageData.height < 100000) {
|
|
244
|
+
return processingFunction(imageData);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 创建结果canvas
|
|
248
|
+
const resultCanvas = document.createElement('canvas');
|
|
249
|
+
resultCanvas.width = imageData.width;
|
|
250
|
+
resultCanvas.height = imageData.height;
|
|
251
|
+
const resultCtx = resultCanvas.getContext('2d');
|
|
252
|
+
|
|
253
|
+
if (!resultCtx) {
|
|
254
|
+
throw new Error('无法创建Canvas上下文');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 根据图像特性确定分割方向和每块大小
|
|
258
|
+
const isWide = imageData.width > imageData.height;
|
|
259
|
+
const chunkSize = Math.floor((isWide ? imageData.width : imageData.height) / chunks);
|
|
260
|
+
|
|
261
|
+
// 创建Worker处理每个块
|
|
262
|
+
const promises = [];
|
|
263
|
+
|
|
264
|
+
for (let i = 0; i < chunks; i++) {
|
|
265
|
+
const chunkCanvas = document.createElement('canvas');
|
|
266
|
+
const chunkCtx = chunkCanvas.getContext('2d');
|
|
267
|
+
|
|
268
|
+
if (!chunkCtx) continue;
|
|
269
|
+
|
|
270
|
+
let chunkImageData;
|
|
271
|
+
|
|
272
|
+
if (isWide) {
|
|
273
|
+
// 水平分割
|
|
274
|
+
const startX = i * chunkSize;
|
|
275
|
+
const width = (i === chunks - 1) ? imageData.width - startX : chunkSize;
|
|
276
|
+
|
|
277
|
+
chunkCanvas.width = width;
|
|
278
|
+
chunkCanvas.height = imageData.height;
|
|
279
|
+
|
|
280
|
+
// 复制原图像数据到分块
|
|
281
|
+
const tempCanvas = this.imageDataToCanvas(imageData);
|
|
282
|
+
chunkCtx.drawImage(
|
|
283
|
+
tempCanvas,
|
|
284
|
+
startX, 0, width, imageData.height,
|
|
285
|
+
0, 0, width, imageData.height
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
chunkImageData = chunkCtx.getImageData(0, 0, width, imageData.height);
|
|
289
|
+
} else {
|
|
290
|
+
// 垂直分割
|
|
291
|
+
const startY = i * chunkSize;
|
|
292
|
+
const height = (i === chunks - 1) ? imageData.height - startY : chunkSize;
|
|
293
|
+
|
|
294
|
+
chunkCanvas.width = imageData.width;
|
|
295
|
+
chunkCanvas.height = height;
|
|
296
|
+
|
|
297
|
+
// 复制原图像数据到分块
|
|
298
|
+
const tempCanvas = this.imageDataToCanvas(imageData);
|
|
299
|
+
chunkCtx.drawImage(
|
|
300
|
+
tempCanvas,
|
|
301
|
+
0, startY, imageData.width, height,
|
|
302
|
+
0, 0, imageData.width, height
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
chunkImageData = chunkCtx.getImageData(0, 0, imageData.width, height);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 使用Worker处理
|
|
309
|
+
const workerCode = `
|
|
310
|
+
self.onmessage = function(e) {
|
|
311
|
+
const imageData = e.data.imageData;
|
|
312
|
+
const processingFunction = ${processingFunction.toString()};
|
|
313
|
+
const result = processingFunction(imageData);
|
|
314
|
+
self.postMessage({ result, index: e.data.index }, [result.data.buffer]);
|
|
315
|
+
}
|
|
316
|
+
`;
|
|
317
|
+
|
|
318
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
319
|
+
const workerUrl = URL.createObjectURL(blob);
|
|
320
|
+
const worker = new Worker(workerUrl);
|
|
321
|
+
|
|
322
|
+
const promise = new Promise<{ result: ImageData, index: number }>((resolve) => {
|
|
323
|
+
worker.onmessage = function(e) {
|
|
324
|
+
resolve(e.data);
|
|
325
|
+
worker.terminate();
|
|
326
|
+
URL.revokeObjectURL(workerUrl);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// 传输数据
|
|
330
|
+
worker.postMessage({
|
|
331
|
+
imageData: chunkImageData,
|
|
332
|
+
index: i
|
|
333
|
+
}, [chunkImageData.data.buffer]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
promises.push(promise);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 等待所有Worker完成并组合结果
|
|
340
|
+
const results = await Promise.all(promises);
|
|
341
|
+
|
|
342
|
+
// 按索引排序结果
|
|
343
|
+
results.sort((a, b) => a.index - b.index);
|
|
344
|
+
|
|
345
|
+
// 将处理后的块绘制到结果canvas
|
|
346
|
+
for (let i = 0; i < results.length; i++) {
|
|
347
|
+
const { result } = results[i];
|
|
348
|
+
const tempCanvas = this.imageDataToCanvas(result);
|
|
349
|
+
|
|
350
|
+
if (isWide) {
|
|
351
|
+
const startX = i * chunkSize;
|
|
352
|
+
resultCtx.drawImage(tempCanvas, startX, 0);
|
|
353
|
+
} else {
|
|
354
|
+
const startY = i * chunkSize;
|
|
355
|
+
resultCtx.drawImage(tempCanvas, 0, startY);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return resultCtx.getImageData(0, 0, imageData.width, imageData.height);
|
|
360
|
+
}
|
|
157
361
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 性能优化工具类
|
|
3
|
+
* @description 提供节流、防抖、缓存等性能优化功能
|
|
4
|
+
* @module PerformanceUtils
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 节流函数:限制函数在一定时间内只能执行一次
|
|
9
|
+
*
|
|
10
|
+
* @param fn 需要节流的函数
|
|
11
|
+
* @param delay 延迟时间(毫秒)
|
|
12
|
+
* @returns 节流处理后的函数
|
|
13
|
+
*/
|
|
14
|
+
export function throttle<T extends (...args: any[]) => any>(
|
|
15
|
+
fn: T,
|
|
16
|
+
delay: number
|
|
17
|
+
): (...args: Parameters<T>) => void {
|
|
18
|
+
let lastCall = 0;
|
|
19
|
+
let timeoutId: number | null = null;
|
|
20
|
+
|
|
21
|
+
return function(...args: Parameters<T>) {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const remaining = delay - (now - lastCall);
|
|
24
|
+
|
|
25
|
+
if (remaining <= 0) {
|
|
26
|
+
if (timeoutId) {
|
|
27
|
+
clearTimeout(timeoutId);
|
|
28
|
+
timeoutId = null;
|
|
29
|
+
}
|
|
30
|
+
lastCall = now;
|
|
31
|
+
fn.apply(this, args);
|
|
32
|
+
} else if (!timeoutId) {
|
|
33
|
+
timeoutId = window.setTimeout(() => {
|
|
34
|
+
lastCall = Date.now();
|
|
35
|
+
timeoutId = null;
|
|
36
|
+
fn.apply(this, args);
|
|
37
|
+
}, remaining);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 防抖函数:函数在最后一次调用后延迟指定时间执行
|
|
44
|
+
*
|
|
45
|
+
* @param fn 需要防抖的函数
|
|
46
|
+
* @param delay 延迟时间(毫秒)
|
|
47
|
+
* @returns 防抖处理后的函数
|
|
48
|
+
*/
|
|
49
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
50
|
+
fn: T,
|
|
51
|
+
delay: number
|
|
52
|
+
): (...args: Parameters<T>) => void {
|
|
53
|
+
let timeoutId: number | null = null;
|
|
54
|
+
|
|
55
|
+
return function(...args: Parameters<T>) {
|
|
56
|
+
if (timeoutId) {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
timeoutId = window.setTimeout(() => {
|
|
61
|
+
fn.apply(this, args);
|
|
62
|
+
timeoutId = null;
|
|
63
|
+
}, delay);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* LRU缓存类 - 使用最近最少使用策略的缓存实现
|
|
69
|
+
*/
|
|
70
|
+
export class LRUCache<K, V> {
|
|
71
|
+
private cache = new Map<K, V>();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 构造LRU缓存
|
|
75
|
+
* @param maxSize 缓存最大容量
|
|
76
|
+
*/
|
|
77
|
+
constructor(private maxSize: number = 100) {}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 获取缓存项
|
|
81
|
+
* @param key 缓存键
|
|
82
|
+
* @returns 缓存值或undefined
|
|
83
|
+
*/
|
|
84
|
+
get(key: K): V | undefined {
|
|
85
|
+
if (!this.cache.has(key)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 获取值
|
|
90
|
+
const value = this.cache.get(key)!;
|
|
91
|
+
|
|
92
|
+
// 将项移至最新位置(删除后重新添加)
|
|
93
|
+
this.cache.delete(key);
|
|
94
|
+
this.cache.set(key, value);
|
|
95
|
+
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 设置缓存项
|
|
101
|
+
* @param key 缓存键
|
|
102
|
+
* @param value 缓存值
|
|
103
|
+
*/
|
|
104
|
+
set(key: K, value: V): void {
|
|
105
|
+
// 如果键已存在,需要先删除
|
|
106
|
+
if (this.cache.has(key)) {
|
|
107
|
+
this.cache.delete(key);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 如果缓存已满,移除最老的项
|
|
111
|
+
if (this.cache.size >= this.maxSize) {
|
|
112
|
+
const oldestKey = this.cache.keys().next().value;
|
|
113
|
+
this.cache.delete(oldestKey);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 添加新项
|
|
117
|
+
this.cache.set(key, value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 删除缓存项
|
|
122
|
+
* @param key 缓存键
|
|
123
|
+
* @returns 是否成功删除
|
|
124
|
+
*/
|
|
125
|
+
delete(key: K): boolean {
|
|
126
|
+
return this.cache.delete(key);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 清空缓存
|
|
131
|
+
*/
|
|
132
|
+
clear(): void {
|
|
133
|
+
this.cache.clear();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 获取当前缓存大小
|
|
138
|
+
*/
|
|
139
|
+
get size(): number {
|
|
140
|
+
return this.cache.size;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 检查键是否存在
|
|
145
|
+
* @param key 缓存键
|
|
146
|
+
*/
|
|
147
|
+
has(key: K): boolean {
|
|
148
|
+
return this.cache.has(key);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 图像指纹计算函数 - 用于检测相同或相似图像
|
|
154
|
+
*
|
|
155
|
+
* @param imageData 图像数据
|
|
156
|
+
* @param size 指纹尺寸(默认8x8)
|
|
157
|
+
* @returns 图像指纹字符串
|
|
158
|
+
*/
|
|
159
|
+
export function calculateImageFingerprint(imageData: ImageData, size: number = 8): string {
|
|
160
|
+
// 1. 缩小图像到指定尺寸
|
|
161
|
+
const canvas = document.createElement('canvas');
|
|
162
|
+
canvas.width = size;
|
|
163
|
+
canvas.height = size;
|
|
164
|
+
const ctx = canvas.getContext('2d');
|
|
165
|
+
|
|
166
|
+
if (!ctx) {
|
|
167
|
+
return '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 创建一个临时canvas来绘制原始imageData
|
|
171
|
+
const tempCanvas = document.createElement('canvas');
|
|
172
|
+
tempCanvas.width = imageData.width;
|
|
173
|
+
tempCanvas.height = imageData.height;
|
|
174
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
175
|
+
|
|
176
|
+
if (!tempCtx) {
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
tempCtx.putImageData(imageData, 0, 0);
|
|
181
|
+
|
|
182
|
+
// 缩小到目标尺寸
|
|
183
|
+
ctx.drawImage(tempCanvas, 0, 0, imageData.width, imageData.height, 0, 0, size, size);
|
|
184
|
+
|
|
185
|
+
// 2. 转换为灰度
|
|
186
|
+
const smallImgData = ctx.getImageData(0, 0, size, size);
|
|
187
|
+
const grayValues = [];
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < smallImgData.data.length; i += 4) {
|
|
190
|
+
const r = smallImgData.data[i];
|
|
191
|
+
const g = smallImgData.data[i + 1];
|
|
192
|
+
const b = smallImgData.data[i + 2];
|
|
193
|
+
// 转为灰度: 0.299r + 0.587g + 0.114b
|
|
194
|
+
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
|
195
|
+
grayValues.push(gray);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 3. 计算平均值
|
|
199
|
+
const avg = grayValues.reduce((sum, val) => sum + val, 0) / grayValues.length;
|
|
200
|
+
|
|
201
|
+
// 4. 比较每个像素与平均值,生成二进制指纹
|
|
202
|
+
let fingerprint = '';
|
|
203
|
+
for (const gray of grayValues) {
|
|
204
|
+
fingerprint += gray >= avg ? '1' : '0';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return fingerprint;
|
|
208
|
+
}
|