id-scanner-lib 1.3.3 → 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.
Files changed (101) hide show
  1. package/README.md +55 -460
  2. package/dist/id-scanner-lib.esm.js +4641 -0
  3. package/dist/id-scanner-lib.esm.js.map +1 -0
  4. package/dist/id-scanner-lib.js +14755 -0
  5. package/dist/id-scanner-lib.js.map +1 -0
  6. package/dist/types/core/base-module.d.ts +44 -0
  7. package/dist/types/core/camera-manager.d.ts +258 -0
  8. package/dist/types/core/config.d.ts +88 -0
  9. package/dist/types/core/errors.d.ts +111 -0
  10. package/dist/types/core/event-emitter.d.ts +55 -0
  11. package/dist/types/core/logger.d.ts +277 -0
  12. package/dist/types/core/module-manager.d.ts +78 -0
  13. package/dist/types/core/plugin-manager.d.ts +158 -0
  14. package/dist/types/core/resource-manager.d.ts +246 -0
  15. package/dist/types/core/result.d.ts +83 -0
  16. package/dist/types/core/scanner-factory.d.ts +93 -0
  17. package/dist/types/index.bundle.d.ts +1303 -0
  18. package/dist/types/index.d.ts +86 -0
  19. package/dist/types/interfaces/external-types.d.ts +174 -0
  20. package/dist/types/interfaces/face-detection.d.ts +293 -0
  21. package/dist/types/interfaces/scanner-module.d.ts +280 -0
  22. package/dist/types/modules/face/face-detector.d.ts +170 -0
  23. package/dist/types/modules/face/index.d.ts +56 -0
  24. package/dist/types/modules/face/liveness-detector.d.ts +177 -0
  25. package/dist/types/modules/face/types.d.ts +136 -0
  26. package/dist/types/modules/id-card/anti-fake-detector.d.ts +170 -0
  27. package/dist/types/modules/id-card/id-card-detector.d.ts +131 -0
  28. package/dist/types/modules/id-card/index.d.ts +89 -0
  29. package/dist/types/modules/id-card/ocr-processor.d.ts +110 -0
  30. package/dist/types/modules/id-card/ocr-worker.d.ts +31 -0
  31. package/dist/types/modules/id-card/types.d.ts +181 -0
  32. package/dist/types/modules/qrcode/index.d.ts +51 -0
  33. package/dist/types/modules/qrcode/qr-code-scanner.d.ts +64 -0
  34. package/dist/types/modules/qrcode/types.d.ts +67 -0
  35. package/dist/types/utils/camera.d.ts +81 -0
  36. package/dist/types/utils/image-processing.d.ts +176 -0
  37. package/dist/types/utils/index.d.ts +175 -0
  38. package/dist/types/utils/performance.d.ts +81 -0
  39. package/dist/types/utils/resource-manager.d.ts +53 -0
  40. package/dist/types/utils/types.d.ts +166 -0
  41. package/dist/types/utils/worker.d.ts +52 -0
  42. package/dist/types/version.d.ts +7 -0
  43. package/package.json +76 -75
  44. package/src/core/base-module.ts +78 -0
  45. package/src/core/camera-manager.ts +798 -0
  46. package/src/core/config.ts +268 -0
  47. package/src/core/errors.ts +174 -0
  48. package/src/core/event-emitter.ts +110 -0
  49. package/src/core/logger.ts +549 -0
  50. package/src/core/module-manager.ts +165 -0
  51. package/src/core/plugin-manager.ts +429 -0
  52. package/src/core/resource-manager.ts +762 -0
  53. package/src/core/result.ts +163 -0
  54. package/src/core/scanner-factory.ts +237 -0
  55. package/src/index.ts +113 -936
  56. package/src/interfaces/external-types.ts +200 -0
  57. package/src/interfaces/face-detection.ts +309 -0
  58. package/src/interfaces/scanner-module.ts +384 -0
  59. package/src/modules/face/face-detector.ts +931 -0
  60. package/src/modules/face/index.ts +208 -0
  61. package/src/modules/face/liveness-detector.ts +908 -0
  62. package/src/modules/face/types.ts +133 -0
  63. package/src/{id-recognition → modules/id-card}/anti-fake-detector.ts +273 -239
  64. package/src/modules/id-card/id-card-detector.ts +474 -0
  65. package/src/modules/id-card/index.ts +425 -0
  66. package/src/{id-recognition → modules/id-card}/ocr-processor.ts +149 -92
  67. package/src/modules/id-card/ocr-worker.ts +259 -0
  68. package/src/modules/id-card/types.ts +178 -0
  69. package/src/modules/qrcode/index.ts +175 -0
  70. package/src/modules/qrcode/qr-code-scanner.ts +230 -0
  71. package/src/modules/qrcode/types.ts +65 -0
  72. package/src/types/tesseract.d.ts +265 -22
  73. package/src/utils/image-processing.ts +68 -49
  74. package/src/utils/index.ts +426 -0
  75. package/src/utils/performance.ts +168 -131
  76. package/src/utils/resource-manager.ts +65 -146
  77. package/src/utils/types.ts +90 -2
  78. package/src/utils/worker.ts +123 -84
  79. package/src/version.ts +11 -0
  80. package/tools/scaffold.js +543 -0
  81. package/dist/id-scanner-core.esm.js +0 -11349
  82. package/dist/id-scanner-core.js +0 -11361
  83. package/dist/id-scanner-core.min.js +0 -1
  84. package/dist/id-scanner-ocr.esm.js +0 -2319
  85. package/dist/id-scanner-ocr.js +0 -2328
  86. package/dist/id-scanner-ocr.min.js +0 -1
  87. package/dist/id-scanner-qr.esm.js +0 -1296
  88. package/dist/id-scanner-qr.js +0 -1305
  89. package/dist/id-scanner-qr.min.js +0 -1
  90. package/dist/id-scanner.js +0 -4561
  91. package/dist/id-scanner.min.js +0 -1
  92. package/src/core.ts +0 -138
  93. package/src/demo/demo.ts +0 -204
  94. package/src/id-recognition/data-extractor.ts +0 -262
  95. package/src/id-recognition/id-detector.ts +0 -510
  96. package/src/id-recognition/ocr-worker.ts +0 -156
  97. package/src/index-umd.ts +0 -477
  98. package/src/ocr-module.ts +0 -187
  99. package/src/qr-module.ts +0 -179
  100. package/src/scanner/barcode-scanner.ts +0 -251
  101. package/src/scanner/qr-scanner.ts +0 -167
@@ -5,9 +5,9 @@
5
5
  * @version 1.3.2
6
6
  */
7
7
 
8
- import { ImageProcessor } from "../utils/image-processing"
9
- import { LRUCache, calculateImageFingerprint } from "../utils/performance"
10
- import { Disposable } from "../utils/resource-manager"
8
+ import { ImageProcessor } from "../../utils/image-processing"
9
+ import { LRUCache, calculateImageFingerprint } from "../../utils/performance"
10
+ import { Disposable } from "../../utils/resource-manager"
11
11
 
12
12
  /**
13
13
  * 防伪检测结果
@@ -27,7 +27,7 @@ export interface AntiFakeDetectorOptions {
27
27
  sensitivity?: number // 敏感度 (0-1),值越高越严格
28
28
  enableCache?: boolean // 是否启用缓存
29
29
  cacheSize?: number // 缓存大小
30
- logger?: (message: any) => void // 日志记录器
30
+ logger?: (message: string) => void // 日志记录器,message 类型设为 string
31
31
  }
32
32
 
33
33
  /**
@@ -187,199 +187,212 @@ export class AntiFakeDetector implements Disposable {
187
187
  ): Promise<[string, boolean, number]> {
188
188
  // 在真实身份证上,荧光油墨会在特定反光条件下呈现特定颜色特征
189
189
  // 在普通可见光下,我们分析蓝色和紫外色通道分布特征
190
-
190
+
191
191
  // 1. 提取蓝色通道并增强对比度
192
- const blueChannel = this.extractColorChannel(imageData, 'blue');
193
-
192
+ const blueChannel = this.extractColorChannel(imageData, "blue")
193
+
194
194
  // 2. 分析蓝色通道的分布特征
195
- const { peaks, variance } = this.analyzeChannelDistribution(blueChannel);
196
-
195
+ const { peaks, variance } = this.analyzeChannelDistribution(blueChannel)
196
+
197
197
  // 3. 分析特定区域的颜色模式
198
- const patternScore = this.detectUVColorPattern(imageData);
199
-
198
+ const patternScore = this.detectUVColorPattern(imageData)
199
+
200
200
  // 4. 计算综合得分
201
201
  // 特征分析:荧光油墨在蓝色通道通常有显著峰值,且分布更聚集
202
- let score = 0;
203
-
202
+ let score = 0
203
+
204
204
  // 过多的峰值表明可能是真实身份证上的荧光特征
205
205
  if (peaks > 3 && peaks < 10) {
206
- score += 0.4;
206
+ score += 0.4
207
207
  }
208
-
208
+
209
209
  // 方差越大,表示颜色对比度越高,更可能有荧光特征
210
210
  if (variance > 1000) {
211
- score += 0.3;
211
+ score += 0.3
212
212
  }
213
-
213
+
214
214
  // 颜色模式得分
215
- score += patternScore * 0.3;
216
-
215
+ score += patternScore * 0.3
216
+
217
217
  // 重要区域分析
218
218
  // 身份证头像区域通常不应具有荧光特征
219
- const hasPortraitAreaFeatures = this.analyzePortraitArea(imageData);
219
+ const hasPortraitAreaFeatures = this.analyzePortraitArea(imageData)
220
220
  if (hasPortraitAreaFeatures) {
221
221
  // 头像区域不应该有荧光特征,如果有可能是伪造的
222
- score -= 0.2;
222
+ score -= 0.2
223
223
  }
224
-
224
+
225
225
  // 求出最终分数并限制在[0,1]范围内
226
- const confidence = Math.max(0, Math.min(1, score));
227
- const detected = confidence > 0.55;
228
-
229
- return ["荧光油墨", detected, confidence];
226
+ const confidence = Math.max(0, Math.min(1, score))
227
+ const detected = confidence > 0.55
228
+
229
+ return ["荧光油墨", detected, confidence]
230
230
  }
231
-
231
+
232
232
  /**
233
233
  * 从图像数据中提取指定颜色通道
234
234
  * @param imageData 原始图像数据
235
235
  * @param channel 通道名称(red, green, blue)
236
236
  */
237
- private extractColorChannel(imageData: ImageData, channel: 'red' | 'green' | 'blue'): Uint8ClampedArray {
238
- const { data, width, height } = imageData;
239
- const channelOffset = channel === 'red' ? 0 : channel === 'green' ? 1 : 2;
240
- const channelData = new Uint8ClampedArray(width * height);
241
-
237
+ private extractColorChannel(
238
+ imageData: ImageData,
239
+ channel: "red" | "green" | "blue"
240
+ ): Uint8ClampedArray {
241
+ const { data, width, height } = imageData
242
+ const channelOffset = channel === "red" ? 0 : channel === "green" ? 1 : 2
243
+ const channelData = new Uint8ClampedArray(width * height)
244
+
242
245
  for (let i = 0; i < data.length; i += 4) {
243
- const pixelIndex = i / 4;
244
- channelData[pixelIndex] = data[i + channelOffset];
246
+ const pixelIndex = i / 4
247
+ channelData[pixelIndex] = data[i + channelOffset]
245
248
  }
246
-
247
- return channelData;
249
+
250
+ return channelData
248
251
  }
249
-
252
+
250
253
  /**
251
254
  * 分析颜色通道分布特征
252
255
  * @param channelData 颜色通道数据
253
256
  */
254
- private analyzeChannelDistribution(channelData: Uint8ClampedArray): { peaks: number, variance: number } {
257
+ private analyzeChannelDistribution(channelData: Uint8ClampedArray): {
258
+ peaks: number
259
+ variance: number
260
+ } {
255
261
  // 计算直方图
256
- const histogram = new Array(256).fill(0);
262
+ const histogram = new Array(256).fill(0)
257
263
  for (let i = 0; i < channelData.length; i++) {
258
- histogram[channelData[i]]++;
264
+ histogram[channelData[i]]++
259
265
  }
260
-
266
+
261
267
  // 平滑直方图以减少噪声
262
- const smoothedHistogram = this.smoothHistogram(histogram, 3);
263
-
268
+ const smoothedHistogram = this.smoothHistogram(histogram, 3)
269
+
264
270
  // 计算峰值数量
265
- let peaks = 0;
271
+ let peaks = 0
266
272
  for (let i = 1; i < 255; i++) {
267
- if (smoothedHistogram[i] > smoothedHistogram[i-1] &&
268
- smoothedHistogram[i] > smoothedHistogram[i+1] &&
269
- smoothedHistogram[i] > channelData.length * 0.01) { // 只计算显著峰值
270
- peaks++;
273
+ if (
274
+ smoothedHistogram[i] > smoothedHistogram[i - 1] &&
275
+ smoothedHistogram[i] > smoothedHistogram[i + 1] &&
276
+ smoothedHistogram[i] > channelData.length * 0.01
277
+ ) {
278
+ // 只计算显著峰值
279
+ peaks++
271
280
  }
272
281
  }
273
-
282
+
274
283
  // 计算方差
275
- let mean = 0;
284
+ let mean = 0
276
285
  for (let i = 0; i < channelData.length; i++) {
277
- mean += channelData[i];
286
+ mean += channelData[i]
278
287
  }
279
- mean /= channelData.length;
280
-
281
- let variance = 0;
288
+ mean /= channelData.length
289
+
290
+ let variance = 0
282
291
  for (let i = 0; i < channelData.length; i++) {
283
- variance += Math.pow(channelData[i] - mean, 2);
292
+ variance += Math.pow(channelData[i] - mean, 2)
284
293
  }
285
- variance /= channelData.length;
286
-
287
- return { peaks, variance };
294
+ variance /= channelData.length
295
+
296
+ return { peaks, variance }
288
297
  }
289
-
298
+
290
299
  /**
291
300
  * 平滑直方图以减少噪声
292
301
  */
293
302
  private smoothHistogram(histogram: number[], windowSize: number): number[] {
294
- const result = new Array(histogram.length).fill(0);
295
- const halfWindow = Math.floor(windowSize / 2);
296
-
303
+ const result = new Array(histogram.length).fill(0)
304
+ const halfWindow = Math.floor(windowSize / 2)
305
+
297
306
  for (let i = 0; i < histogram.length; i++) {
298
- let sum = 0;
299
- let count = 0;
300
-
301
- for (let j = Math.max(0, i - halfWindow); j <= Math.min(histogram.length - 1, i + halfWindow); j++) {
302
- sum += histogram[j];
303
- count++;
307
+ let sum = 0
308
+ let count = 0
309
+
310
+ for (
311
+ let j = Math.max(0, i - halfWindow);
312
+ j <= Math.min(histogram.length - 1, i + halfWindow);
313
+ j++
314
+ ) {
315
+ sum += histogram[j]
316
+ count++
304
317
  }
305
-
306
- result[i] = sum / count;
318
+
319
+ result[i] = sum / count
307
320
  }
308
-
309
- return result;
321
+
322
+ return result
310
323
  }
311
-
324
+
312
325
  /**
313
326
  * 检测图像中的荧光颜色模式
314
327
  */
315
328
  private detectUVColorPattern(imageData: ImageData): number {
316
- // 分析特定组合颜色的出现频率,荧光油墨在可见光下也具有特定的颜色特征
317
- const { data, width, height } = imageData;
318
- let uvColorCount = 0;
319
-
329
+ // 分析特定组合颜色的出现频率,荧光油墨在可见光下也有特定的颜色特征
330
+ const { data, width, height } = imageData
331
+ let uvColorCount = 0
332
+
320
333
  // 寻找可能为荧光油墨的特定颜色模式
321
334
  // 这些颜色通常是特定的蓝紫色调和高对比度
322
335
  for (let i = 0; i < data.length; i += 4) {
323
- const r = data[i];
324
- const g = data[i + 1];
325
- const b = data[i + 2];
326
-
336
+ const r = data[i]
337
+ const g = data[i + 1]
338
+ const b = data[i + 2]
339
+
327
340
  // 检查是否是荧光油墨特有的颜色范围
328
341
  // 这里使用简化的追踪条件,实际应用中应使用更复杂的颜色模型
329
342
  if (b > 1.5 * r && b > 1.3 * g && b > 100) {
330
- uvColorCount++;
343
+ uvColorCount++
331
344
  }
332
345
  }
333
-
346
+
334
347
  // 计算荧光颜色像素占比
335
- const totalPixels = width * height;
336
- const uvColorRatio = uvColorCount / totalPixels;
337
-
348
+ const totalPixels = width * height
349
+ const uvColorRatio = uvColorCount / totalPixels
350
+
338
351
  // 对于真实身份证,荧光颜色的占比应该在一定范围内
339
352
  // 如果占比过高或过低,可能是伪造的
340
- const idealRatio = 0.05; // 理想占比
341
- const deviation = Math.abs(uvColorRatio - idealRatio) / idealRatio;
342
-
353
+ const idealRatio = 0.05 // 理想占比
354
+ const deviation = Math.abs(uvColorRatio - idealRatio) / idealRatio
355
+
343
356
  // 将差异转换为0-1的置信度分数
344
- return Math.max(0, 1 - Math.min(1, deviation * 2));
357
+ return Math.max(0, 1 - Math.min(1, deviation * 2))
345
358
  }
346
-
359
+
347
360
  /**
348
361
  * 分析头像区域是否存在荧光特征
349
362
  * 这个方法用于检测伪造的身份证,因为头像区域不应该有荧光特征
350
363
  */
351
364
  private analyzePortraitArea(imageData: ImageData): boolean {
352
365
  // 假设头像区域大约占据图片右上方四分之一的区域
353
- const { width, height, data } = imageData;
354
- const portraitX = Math.floor(width * 0.6);
355
- const portraitY = Math.floor(height * 0.2);
356
- const portraitWidth = Math.floor(width * 0.3);
357
- const portraitHeight = Math.floor(height * 0.3);
358
-
359
- let uvFeatureCount = 0;
360
- let totalPixels = 0;
361
-
366
+ const { width, height, data } = imageData
367
+ const portraitX = Math.floor(width * 0.6)
368
+ const portraitY = Math.floor(height * 0.2)
369
+ const portraitWidth = Math.floor(width * 0.3)
370
+ const portraitHeight = Math.floor(height * 0.3)
371
+
372
+ let uvFeatureCount = 0
373
+ let totalPixels = 0
374
+
362
375
  // 检查头像区域的荧光特征
363
376
  for (let y = portraitY; y < portraitY + portraitHeight; y++) {
364
377
  for (let x = portraitX; x < portraitX + portraitWidth; x++) {
365
378
  if (x >= 0 && x < width && y >= 0 && y < height) {
366
- const i = (y * width + x) * 4;
367
- const r = data[i];
368
- const g = data[i + 1];
369
- const b = data[i + 2];
370
-
379
+ const i = (y * width + x) * 4
380
+ const r = data[i]
381
+ const g = data[i + 1]
382
+ const b = data[i + 2]
383
+
371
384
  // 使用与上面相同的荧光颜色检测标准
372
385
  if (b > 1.5 * r && b > 1.3 * g && b > 100) {
373
- uvFeatureCount++;
386
+ uvFeatureCount++
374
387
  }
375
-
376
- totalPixels++;
388
+
389
+ totalPixels++
377
390
  }
378
391
  }
379
392
  }
380
-
393
+
381
394
  // 如果头像区域的荧光特征占比过高,可能是伪造的
382
- return totalPixels > 0 && (uvFeatureCount / totalPixels) > 0.1;
395
+ return totalPixels > 0 && uvFeatureCount / totalPixels > 0.1
383
396
  }
384
397
 
385
398
  /**
@@ -394,7 +407,7 @@ export class AntiFakeDetector implements Disposable {
394
407
  ): Promise<[string, boolean, number]> {
395
408
  // 微缩文字检测 - 身份证上的微缩文字是重要的防伪特征
396
409
  // 这些文字很小,但会呈现规则的线条和高频组件
397
-
410
+
398
411
  // 1. 转换图像为灰度图
399
412
  const grayscale = ImageProcessor.toGrayscale(
400
413
  new ImageData(
@@ -402,220 +415,241 @@ export class AntiFakeDetector implements Disposable {
402
415
  imageData.width,
403
416
  imageData.height
404
417
  )
405
- );
406
-
418
+ )
419
+
407
420
  // 2. 执行边缘检测突出微缩文字
408
- const edgeData = ImageProcessor.detectEdges(grayscale, 40); // 强化的边缘检测
409
-
421
+ const edgeData = ImageProcessor.detectEdges(grayscale, 40) // 强化的边缘检测
422
+
410
423
  // 3. 分析频率特征 - 微缩文字呈现高频的边缘过渡
411
- const frequencyFeatures = this.analyzeFrequencyFeatures(edgeData);
412
-
424
+ const frequencyFeatures = this.analyzeFrequencyFeatures(edgeData)
425
+
413
426
  // 4. 检测微缩文字的具体区域
414
- const microTextRegions = this.detectMicroTextRegions(edgeData);
415
-
427
+ const microTextRegions = this.detectMicroTextRegions(edgeData)
428
+
416
429
  // 5. 综合分析结果计算置信度
417
- let score = 0;
418
-
430
+ let score = 0
431
+
419
432
  // 频率特征分数
420
- score += frequencyFeatures.score * 0.6;
421
-
433
+ score += frequencyFeatures.score * 0.6
434
+
422
435
  // 区域特征分数
423
436
  if (microTextRegions.count > 0) {
424
437
  // 过多的区域也可能表示噪声,因此有一个最佳范围
425
- const normalizedCount = Math.min(microTextRegions.count, 5) / 5;
426
- score += normalizedCount * 0.4;
438
+ const normalizedCount = Math.min(microTextRegions.count, 5) / 5
439
+ score += normalizedCount * 0.4
427
440
  }
428
-
441
+
429
442
  // 对置信度进行最终调整
430
- const confidence = Math.max(0, Math.min(1, score));
431
- const detected = confidence > 0.5;
432
-
433
- return ["微缩文字", detected, confidence];
443
+ const confidence = Math.max(0, Math.min(1, score))
444
+ const detected = confidence > 0.5
445
+
446
+ return ["微缩文字", detected, confidence]
434
447
  }
435
-
448
+
436
449
  /**
437
450
  * 分析边缘图像的频率特征
438
451
  * 微缩文字呈现高频的边缘过渡
439
452
  */
440
- private analyzeFrequencyFeatures(edgeData: ImageData): { score: number, highFreqRatio: number } {
441
- const { data, width, height } = edgeData;
442
- let edgeCount = 0;
443
- let totalPixels = width * height;
444
-
453
+ private analyzeFrequencyFeatures(edgeData: ImageData): {
454
+ score: number
455
+ highFreqRatio: number
456
+ } {
457
+ const { data, width, height } = edgeData
458
+ let edgeCount = 0
459
+ let totalPixels = width * height
460
+
445
461
  // 计算边缘像素的数量
446
462
  for (let i = 0; i < data.length; i += 4) {
447
- if (data[i] > 200) { // 大于阈值的边缘像素
448
- edgeCount++;
463
+ if (data[i] > 200) {
464
+ // 大于阈值的边缘像素
465
+ edgeCount++
449
466
  }
450
467
  }
451
-
468
+
452
469
  // 计算高频边缘分布
453
470
  // 统计边缘过渡的变化频率
454
- let highFreqTransitions = 0;
455
-
471
+ let highFreqTransitions = 0
472
+
456
473
  // 检测行方向的边缘变化
457
474
  for (let y = 0; y < height; y++) {
458
- let prevEdge = false;
459
- let transitions = 0;
460
-
475
+ let prevEdge = false
476
+ let transitions = 0
477
+
461
478
  for (let x = 0; x < width; x++) {
462
- const i = (y * width + x) * 4;
463
- const isEdge = data[i] > 200;
464
-
479
+ const i = (y * width + x) * 4
480
+ const isEdge = data[i] > 200
481
+
465
482
  if (isEdge !== prevEdge) {
466
- transitions++;
467
- prevEdge = isEdge;
483
+ transitions++
484
+ prevEdge = isEdge
468
485
  }
469
486
  }
470
-
487
+
471
488
  // 每行的过渡频率
472
- if (transitions > width * 0.1) { // 高频过渡行
473
- highFreqTransitions++;
489
+ if (transitions > width * 0.1) {
490
+ // 高频过渡行
491
+ highFreqTransitions++
474
492
  }
475
493
  }
476
-
494
+
477
495
  // 计算列方向的边缘变化
478
- let colHighFreqTransitions = 0;
496
+ let colHighFreqTransitions = 0
479
497
  for (let x = 0; x < width; x++) {
480
- let prevEdge = false;
481
- let transitions = 0;
482
-
498
+ let prevEdge = false
499
+ let transitions = 0
500
+
483
501
  for (let y = 0; y < height; y++) {
484
- const i = (y * width + x) * 4;
485
- const isEdge = data[i] > 200;
486
-
502
+ const i = (y * width + x) * 4
503
+ const isEdge = data[i] > 200
504
+
487
505
  if (isEdge !== prevEdge) {
488
- transitions++;
489
- prevEdge = isEdge;
506
+ transitions++
507
+ prevEdge = isEdge
490
508
  }
491
509
  }
492
-
510
+
493
511
  // 每列的过渡频率
494
- if (transitions > height * 0.1) { // 高频过渡列
495
- colHighFreqTransitions++;
512
+ if (transitions > height * 0.1) {
513
+ // 高频过渡列
514
+ colHighFreqTransitions++
496
515
  }
497
516
  }
498
-
517
+
499
518
  // 综合计算高频特征比例
500
- const rowHighFreqRatio = highFreqTransitions / height;
501
- const colHighFreqRatio = colHighFreqTransitions / width;
502
- const highFreqRatio = (rowHighFreqRatio + colHighFreqRatio) / 2;
503
-
519
+ const rowHighFreqRatio = highFreqTransitions / height
520
+ const colHighFreqRatio = colHighFreqTransitions / width
521
+ const highFreqRatio = (rowHighFreqRatio + colHighFreqRatio) / 2
522
+
504
523
  // 计算最终分数
505
524
  // 真实的微缩文字应该有适度的高频特征,而不是极端的高或低
506
- const idealRatio = 0.15; // 理想的高频比例
507
- const deviationFactor = Math.abs(highFreqRatio - idealRatio) / idealRatio;
508
- const score = Math.max(0, 1 - Math.min(1, deviationFactor * 3));
509
-
510
- return { score, highFreqRatio };
525
+ const idealRatio = 0.15 // 理想的高频比例
526
+ const deviationFactor = Math.abs(highFreqRatio - idealRatio) / idealRatio
527
+ const score = Math.max(0, 1 - Math.min(1, deviationFactor * 3))
528
+
529
+ return { score, highFreqRatio }
511
530
  }
512
-
531
+
513
532
  /**
514
533
  * 检测微缩文字区域
515
534
  * 微缩文字通常呈现呈现规则的组合排列
516
535
  */
517
- private detectMicroTextRegions(edgeData: ImageData): { count: number, regions: Array<{x: number, y: number, w: number, h: number}> } {
518
- const { data, width, height } = edgeData;
519
- const visitedMap = new Array(width * height).fill(false);
520
- const regions: Array<{x: number, y: number, w: number, h: number}> = [];
521
-
536
+ private detectMicroTextRegions(edgeData: ImageData): {
537
+ count: number
538
+ regions: Array<{ x: number; y: number; w: number; h: number }>
539
+ } {
540
+ const { data, width, height } = edgeData
541
+ const visitedMap = new Array(width * height).fill(false)
542
+ const regions: Array<{ x: number; y: number; w: number; h: number }> = []
543
+
522
544
  // 使用满足条件的连通区域寻找微缩文字区域
523
545
  for (let y = 0; y < height; y++) {
524
546
  for (let x = 0; x < width; x++) {
525
- const idx = y * width + x;
526
- const i = idx * 4;
527
-
547
+ const idx = y * width + x
548
+ const i = idx * 4
549
+
528
550
  // 如果是边缘像素且未访问过
529
551
  if (data[i] > 200 && !visitedMap[idx]) {
530
552
  // 使用深度优先搜索找到连通的边缘区域
531
- const regionPoints = this.floodFillEdge(edgeData, x, y, visitedMap);
532
-
553
+ const regionPoints = this.floodFillEdge(edgeData, x, y, visitedMap)
554
+
533
555
  // 分析区域
534
- if (regionPoints.length > 10) { // 小区域忽略
535
- const [minX, minY, maxX, maxY] = this.getBoundingBox(regionPoints);
536
- const regionWidth = maxX - minX + 1;
537
- const regionHeight = maxY - minY + 1;
538
-
556
+ if (regionPoints.length > 10) {
557
+ // 小区域忽略
558
+ const [minX, minY, maxX, maxY] = this.getBoundingBox(regionPoints)
559
+ const regionWidth = maxX - minX + 1
560
+ const regionHeight = maxY - minY + 1
561
+
539
562
  // 检查区域大小和纹理特征
540
- if (regionWidth > 5 && regionHeight > 5 &&
541
- regionWidth < width * 0.2 && regionHeight < height * 0.2) {
542
-
563
+ if (
564
+ regionWidth > 5 &&
565
+ regionHeight > 5 &&
566
+ regionWidth < width * 0.2 &&
567
+ regionHeight < height * 0.2
568
+ ) {
543
569
  // 计算区域密度
544
- const density = regionPoints.length / (regionWidth * regionHeight);
545
-
570
+ const density = regionPoints.length / (regionWidth * regionHeight)
571
+
546
572
  // 检查并添加符合微缩文字特征的区域
547
- if (density > 0.1 && density < 0.5) { // 合适的密度范围
573
+ if (density > 0.1 && density < 0.5) {
574
+ // 合适的密度范围
548
575
  regions.push({
549
576
  x: minX,
550
577
  y: minY,
551
578
  w: regionWidth,
552
- h: regionHeight
553
- });
579
+ h: regionHeight,
580
+ })
554
581
  }
555
582
  }
556
583
  }
557
584
  }
558
585
  }
559
586
  }
560
-
561
- return { count: regions.length, regions };
587
+
588
+ return { count: regions.length, regions }
562
589
  }
563
-
590
+
564
591
  /**
565
592
  * 深度优先搜索连通的边缘区域
566
593
  */
567
- private floodFillEdge(edgeData: ImageData, startX: number, startY: number, visitedMap: boolean[]): Array<{x: number, y: number}> {
568
- const { data, width, height } = edgeData;
569
- const stack: Array<{x: number, y: number}> = [];
570
- const points: Array<{x: number, y: number}> = [];
571
- const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
572
- const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
573
-
594
+ private floodFillEdge(
595
+ edgeData: ImageData,
596
+ startX: number,
597
+ startY: number,
598
+ visitedMap: boolean[]
599
+ ): Array<{ x: number; y: number }> {
600
+ const { data, width, height } = edgeData
601
+ const stack: Array<{ x: number; y: number }> = []
602
+ const points: Array<{ x: number; y: number }> = []
603
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1]
604
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1]
605
+
574
606
  // 起始点
575
- stack.push({x: startX, y: startY});
576
- visitedMap[startY * width + startX] = true;
577
-
607
+ stack.push({ x: startX, y: startY })
608
+ visitedMap[startY * width + startX] = true
609
+
578
610
  while (stack.length > 0) {
579
- const {x, y} = stack.pop()!;
580
- points.push({x, y});
581
-
611
+ const { x, y } = stack.pop()!
612
+ points.push({ x, y })
613
+
582
614
  // 检查88个相邻方向
583
615
  for (let i = 0; i < 8; i++) {
584
- const nx = x + dx[i];
585
- const ny = y + dy[i];
586
-
616
+ const nx = x + dx[i]
617
+ const ny = y + dy[i]
618
+
587
619
  if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
588
- const nidx = ny * width + nx;
589
- const ni = nidx * 4;
590
-
620
+ const nidx = ny * width + nx
621
+ const ni = nidx * 4
622
+
591
623
  if (data[ni] > 200 && !visitedMap[nidx]) {
592
- stack.push({x: nx, y: ny});
593
- visitedMap[nidx] = true;
624
+ stack.push({ x: nx, y: ny })
625
+ visitedMap[nidx] = true
594
626
  }
595
627
  }
596
628
  }
597
629
  }
598
-
599
- return points;
630
+
631
+ return points
600
632
  }
601
-
633
+
602
634
  /**
603
635
  * 获取点集的外接矩形
604
636
  */
605
- private getBoundingBox(points: Array<{x: number, y: number}>): [number, number, number, number] {
606
- let minX = Number.MAX_SAFE_INTEGER;
607
- let minY = Number.MAX_SAFE_INTEGER;
608
- let maxX = 0;
609
- let maxY = 0;
610
-
611
- for (const {x, y} of points) {
612
- minX = Math.min(minX, x);
613
- minY = Math.min(minY, y);
614
- maxX = Math.max(maxX, x);
615
- maxY = Math.max(maxY, y);
637
+ private getBoundingBox(
638
+ points: Array<{ x: number; y: number }>
639
+ ): [number, number, number, number] {
640
+ let minX = Number.MAX_SAFE_INTEGER
641
+ let minY = Number.MAX_SAFE_INTEGER
642
+ let maxX = 0
643
+ let maxY = 0
644
+
645
+ for (const { x, y } of points) {
646
+ minX = Math.min(minX, x)
647
+ minY = Math.min(minY, y)
648
+ maxX = Math.max(maxX, x)
649
+ maxY = Math.max(maxY, y)
616
650
  }
617
-
618
- return [minX, minY, maxX, maxY];
651
+
652
+ return [minX, minY, maxX, maxY]
619
653
  }
620
654
 
621
655
  /**