id-scanner-lib 1.3.2 → 1.3.3

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.
@@ -2,6 +2,7 @@
2
2
  * @file 身份证防伪检测模块
3
3
  * @description 提供身份证防伪特征识别功能,区分真假身份证
4
4
  * @module AntiFakeDetector
5
+ * @version 1.3.2
5
6
  */
6
7
 
7
8
  import { ImageProcessor } from "../utils/image-processing"
@@ -184,27 +185,201 @@ export class AntiFakeDetector implements Disposable {
184
185
  private async detectUVInkFeatures(
185
186
  imageData: ImageData
186
187
  ): Promise<[string, boolean, number]> {
187
- // 提取蓝色通道增强UV油墨可见度
188
- const canvas = document.createElement("canvas")
189
- canvas.width = imageData.width
190
- canvas.height = imageData.height
191
- const ctx = canvas.getContext("2d")
192
-
193
- if (!ctx) {
194
- return ["荧光油墨", false, 0]
188
+ // 在真实身份证上,荧光油墨会在特定反光条件下呈现特定颜色特征
189
+ // 在普通可见光下,我们分析蓝色和紫外色通道分布特征
190
+
191
+ // 1. 提取蓝色通道并增强对比度
192
+ const blueChannel = this.extractColorChannel(imageData, 'blue');
193
+
194
+ // 2. 分析蓝色通道的分布特征
195
+ const { peaks, variance } = this.analyzeChannelDistribution(blueChannel);
196
+
197
+ // 3. 分析特定区域的颜色模式
198
+ const patternScore = this.detectUVColorPattern(imageData);
199
+
200
+ // 4. 计算综合得分
201
+ // 特征分析:荧光油墨在蓝色通道通常有显著峰值,且分布更聚集
202
+ let score = 0;
203
+
204
+ // 过多的峰值表明可能是真实身份证上的荧光特征
205
+ if (peaks > 3 && peaks < 10) {
206
+ score += 0.4;
195
207
  }
196
-
197
- ctx.putImageData(imageData, 0, 0)
198
-
199
- // 分析蓝色通道中的特定模式
200
- // 实际实现中应使用更复杂的算法提取UV特征
201
- // 这里使用模拟实现
202
-
203
- // 模拟检测: 70%的概率检测到,置信度0.65-0.95
204
- const detected = Math.random() > 0.3
205
- const confidence = detected ? 0.65 + Math.random() * 0.3 : 0
206
-
207
- return ["荧光油墨", detected, confidence]
208
+
209
+ // 方差越大,表示颜色对比度越高,更可能有荧光特征
210
+ if (variance > 1000) {
211
+ score += 0.3;
212
+ }
213
+
214
+ // 颜色模式得分
215
+ score += patternScore * 0.3;
216
+
217
+ // 重要区域分析
218
+ // 身份证头像区域通常不应具有荧光特征
219
+ const hasPortraitAreaFeatures = this.analyzePortraitArea(imageData);
220
+ if (hasPortraitAreaFeatures) {
221
+ // 头像区域不应该有荧光特征,如果有可能是伪造的
222
+ score -= 0.2;
223
+ }
224
+
225
+ // 求出最终分数并限制在[0,1]范围内
226
+ const confidence = Math.max(0, Math.min(1, score));
227
+ const detected = confidence > 0.55;
228
+
229
+ return ["荧光油墨", detected, confidence];
230
+ }
231
+
232
+ /**
233
+ * 从图像数据中提取指定颜色通道
234
+ * @param imageData 原始图像数据
235
+ * @param channel 通道名称(red, green, blue)
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
+
242
+ for (let i = 0; i < data.length; i += 4) {
243
+ const pixelIndex = i / 4;
244
+ channelData[pixelIndex] = data[i + channelOffset];
245
+ }
246
+
247
+ return channelData;
248
+ }
249
+
250
+ /**
251
+ * 分析颜色通道分布特征
252
+ * @param channelData 颜色通道数据
253
+ */
254
+ private analyzeChannelDistribution(channelData: Uint8ClampedArray): { peaks: number, variance: number } {
255
+ // 计算直方图
256
+ const histogram = new Array(256).fill(0);
257
+ for (let i = 0; i < channelData.length; i++) {
258
+ histogram[channelData[i]]++;
259
+ }
260
+
261
+ // 平滑直方图以减少噪声
262
+ const smoothedHistogram = this.smoothHistogram(histogram, 3);
263
+
264
+ // 计算峰值数量
265
+ let peaks = 0;
266
+ 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++;
271
+ }
272
+ }
273
+
274
+ // 计算方差
275
+ let mean = 0;
276
+ for (let i = 0; i < channelData.length; i++) {
277
+ mean += channelData[i];
278
+ }
279
+ mean /= channelData.length;
280
+
281
+ let variance = 0;
282
+ for (let i = 0; i < channelData.length; i++) {
283
+ variance += Math.pow(channelData[i] - mean, 2);
284
+ }
285
+ variance /= channelData.length;
286
+
287
+ return { peaks, variance };
288
+ }
289
+
290
+ /**
291
+ * 平滑直方图以减少噪声
292
+ */
293
+ private smoothHistogram(histogram: number[], windowSize: number): number[] {
294
+ const result = new Array(histogram.length).fill(0);
295
+ const halfWindow = Math.floor(windowSize / 2);
296
+
297
+ 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++;
304
+ }
305
+
306
+ result[i] = sum / count;
307
+ }
308
+
309
+ return result;
310
+ }
311
+
312
+ /**
313
+ * 检测图像中的荧光颜色模式
314
+ */
315
+ private detectUVColorPattern(imageData: ImageData): number {
316
+ // 分析特定组合颜色的出现频率,荧光油墨在可见光下也具有特定的颜色特征
317
+ const { data, width, height } = imageData;
318
+ let uvColorCount = 0;
319
+
320
+ // 寻找可能为荧光油墨的特定颜色模式
321
+ // 这些颜色通常是特定的蓝紫色调和高对比度
322
+ 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
+
327
+ // 检查是否是荧光油墨特有的颜色范围
328
+ // 这里使用简化的追踪条件,实际应用中应使用更复杂的颜色模型
329
+ if (b > 1.5 * r && b > 1.3 * g && b > 100) {
330
+ uvColorCount++;
331
+ }
332
+ }
333
+
334
+ // 计算荧光颜色像素占比
335
+ const totalPixels = width * height;
336
+ const uvColorRatio = uvColorCount / totalPixels;
337
+
338
+ // 对于真实身份证,荧光颜色的占比应该在一定范围内
339
+ // 如果占比过高或过低,可能是伪造的
340
+ const idealRatio = 0.05; // 理想占比
341
+ const deviation = Math.abs(uvColorRatio - idealRatio) / idealRatio;
342
+
343
+ // 将差异转换为0-1的置信度分数
344
+ return Math.max(0, 1 - Math.min(1, deviation * 2));
345
+ }
346
+
347
+ /**
348
+ * 分析头像区域是否存在荧光特征
349
+ * 这个方法用于检测伪造的身份证,因为头像区域不应该有荧光特征
350
+ */
351
+ private analyzePortraitArea(imageData: ImageData): boolean {
352
+ // 假设头像区域大约占据图片右上方四分之一的区域
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
+
362
+ // 检查头像区域的荧光特征
363
+ for (let y = portraitY; y < portraitY + portraitHeight; y++) {
364
+ for (let x = portraitX; x < portraitX + portraitWidth; x++) {
365
+ 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
+
371
+ // 使用与上面相同的荧光颜色检测标准
372
+ if (b > 1.5 * r && b > 1.3 * g && b > 100) {
373
+ uvFeatureCount++;
374
+ }
375
+
376
+ totalPixels++;
377
+ }
378
+ }
379
+ }
380
+
381
+ // 如果头像区域的荧光特征占比过高,可能是伪造的
382
+ return totalPixels > 0 && (uvFeatureCount / totalPixels) > 0.1;
208
383
  }
209
384
 
210
385
  /**
@@ -217,24 +392,230 @@ export class AntiFakeDetector implements Disposable {
217
392
  private async detectMicroText(
218
393
  imageData: ImageData
219
394
  ): Promise<[string, boolean, number]> {
220
- // 应用边缘检测突出微缩文字
395
+ // 微缩文字检测 - 身份证上的微缩文字是重要的防伪特征
396
+ // 这些文字很小,但会呈现规则的线条和高频组件
397
+
398
+ // 1. 转换图像为灰度图
221
399
  const grayscale = ImageProcessor.toGrayscale(
222
400
  new ImageData(
223
401
  new Uint8ClampedArray(imageData.data),
224
402
  imageData.width,
225
403
  imageData.height
226
404
  )
227
- )
228
-
229
- // 寻找特定的微缩文字模式
230
- // 实际实现中应使用计算机视觉算法寻找微小规则文字模式
231
- // 这里使用模拟实现
232
-
233
- // 模拟检测: 80%的概率检测到,置信度0.7-0.95
234
- const detected = Math.random() > 0.2
235
- const confidence = detected ? 0.7 + Math.random() * 0.25 : 0
236
-
237
- return ["微缩文字", detected, confidence]
405
+ );
406
+
407
+ // 2. 执行边缘检测突出微缩文字
408
+ const edgeData = ImageProcessor.detectEdges(grayscale, 40); // 强化的边缘检测
409
+
410
+ // 3. 分析频率特征 - 微缩文字呈现高频的边缘过渡
411
+ const frequencyFeatures = this.analyzeFrequencyFeatures(edgeData);
412
+
413
+ // 4. 检测微缩文字的具体区域
414
+ const microTextRegions = this.detectMicroTextRegions(edgeData);
415
+
416
+ // 5. 综合分析结果计算置信度
417
+ let score = 0;
418
+
419
+ // 频率特征分数
420
+ score += frequencyFeatures.score * 0.6;
421
+
422
+ // 区域特征分数
423
+ if (microTextRegions.count > 0) {
424
+ // 过多的区域也可能表示噪声,因此有一个最佳范围
425
+ const normalizedCount = Math.min(microTextRegions.count, 5) / 5;
426
+ score += normalizedCount * 0.4;
427
+ }
428
+
429
+ // 对置信度进行最终调整
430
+ const confidence = Math.max(0, Math.min(1, score));
431
+ const detected = confidence > 0.5;
432
+
433
+ return ["微缩文字", detected, confidence];
434
+ }
435
+
436
+ /**
437
+ * 分析边缘图像的频率特征
438
+ * 微缩文字呈现高频的边缘过渡
439
+ */
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
+
445
+ // 计算边缘像素的数量
446
+ for (let i = 0; i < data.length; i += 4) {
447
+ if (data[i] > 200) { // 大于阈值的边缘像素
448
+ edgeCount++;
449
+ }
450
+ }
451
+
452
+ // 计算高频边缘分布
453
+ // 统计边缘过渡的变化频率
454
+ let highFreqTransitions = 0;
455
+
456
+ // 检测行方向的边缘变化
457
+ for (let y = 0; y < height; y++) {
458
+ let prevEdge = false;
459
+ let transitions = 0;
460
+
461
+ for (let x = 0; x < width; x++) {
462
+ const i = (y * width + x) * 4;
463
+ const isEdge = data[i] > 200;
464
+
465
+ if (isEdge !== prevEdge) {
466
+ transitions++;
467
+ prevEdge = isEdge;
468
+ }
469
+ }
470
+
471
+ // 每行的过渡频率
472
+ if (transitions > width * 0.1) { // 高频过渡行
473
+ highFreqTransitions++;
474
+ }
475
+ }
476
+
477
+ // 计算列方向的边缘变化
478
+ let colHighFreqTransitions = 0;
479
+ for (let x = 0; x < width; x++) {
480
+ let prevEdge = false;
481
+ let transitions = 0;
482
+
483
+ for (let y = 0; y < height; y++) {
484
+ const i = (y * width + x) * 4;
485
+ const isEdge = data[i] > 200;
486
+
487
+ if (isEdge !== prevEdge) {
488
+ transitions++;
489
+ prevEdge = isEdge;
490
+ }
491
+ }
492
+
493
+ // 每列的过渡频率
494
+ if (transitions > height * 0.1) { // 高频过渡列
495
+ colHighFreqTransitions++;
496
+ }
497
+ }
498
+
499
+ // 综合计算高频特征比例
500
+ const rowHighFreqRatio = highFreqTransitions / height;
501
+ const colHighFreqRatio = colHighFreqTransitions / width;
502
+ const highFreqRatio = (rowHighFreqRatio + colHighFreqRatio) / 2;
503
+
504
+ // 计算最终分数
505
+ // 真实的微缩文字应该有适度的高频特征,而不是极端的高或低
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 };
511
+ }
512
+
513
+ /**
514
+ * 检测微缩文字区域
515
+ * 微缩文字通常呈现呈现规则的组合排列
516
+ */
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
+
522
+ // 使用满足条件的连通区域寻找微缩文字区域
523
+ for (let y = 0; y < height; y++) {
524
+ for (let x = 0; x < width; x++) {
525
+ const idx = y * width + x;
526
+ const i = idx * 4;
527
+
528
+ // 如果是边缘像素且未访问过
529
+ if (data[i] > 200 && !visitedMap[idx]) {
530
+ // 使用深度优先搜索找到连通的边缘区域
531
+ const regionPoints = this.floodFillEdge(edgeData, x, y, visitedMap);
532
+
533
+ // 分析区域
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
+
539
+ // 检查区域大小和纹理特征
540
+ if (regionWidth > 5 && regionHeight > 5 &&
541
+ regionWidth < width * 0.2 && regionHeight < height * 0.2) {
542
+
543
+ // 计算区域密度
544
+ const density = regionPoints.length / (regionWidth * regionHeight);
545
+
546
+ // 检查并添加符合微缩文字特征的区域
547
+ if (density > 0.1 && density < 0.5) { // 合适的密度范围
548
+ regions.push({
549
+ x: minX,
550
+ y: minY,
551
+ w: regionWidth,
552
+ h: regionHeight
553
+ });
554
+ }
555
+ }
556
+ }
557
+ }
558
+ }
559
+ }
560
+
561
+ return { count: regions.length, regions };
562
+ }
563
+
564
+ /**
565
+ * 深度优先搜索连通的边缘区域
566
+ */
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
+
574
+ // 起始点
575
+ stack.push({x: startX, y: startY});
576
+ visitedMap[startY * width + startX] = true;
577
+
578
+ while (stack.length > 0) {
579
+ const {x, y} = stack.pop()!;
580
+ points.push({x, y});
581
+
582
+ // 检查88个相邻方向
583
+ for (let i = 0; i < 8; i++) {
584
+ const nx = x + dx[i];
585
+ const ny = y + dy[i];
586
+
587
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
588
+ const nidx = ny * width + nx;
589
+ const ni = nidx * 4;
590
+
591
+ if (data[ni] > 200 && !visitedMap[nidx]) {
592
+ stack.push({x: nx, y: ny});
593
+ visitedMap[nidx] = true;
594
+ }
595
+ }
596
+ }
597
+ }
598
+
599
+ return points;
600
+ }
601
+
602
+ /**
603
+ * 获取点集的外接矩形
604
+ */
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);
616
+ }
617
+
618
+ return [minX, minY, maxX, maxY];
238
619
  }
239
620
 
240
621
  /**
@@ -2,6 +2,7 @@
2
2
  * @file 身份证检测模块
3
3
  * @description 提供自动检测和定位图像中的身份证功能
4
4
  * @module IDCardDetector
5
+ * @version 1.3.2
5
6
  */
6
7
 
7
8
  import { Camera } from "../utils/camera"
@@ -53,6 +54,8 @@ export interface IDCardDetectorOptions {
53
54
  * ```
54
55
  */
55
56
  export class IDCardDetector implements Disposable {
57
+ // 身份证标准宽高比(近似黄金比例)
58
+ private static readonly ID_CARD_ASPECT_RATIO = 1.58 // 标准身份证宽高比
56
59
  private camera: Camera
57
60
  private detecting = false
58
61
  private detectTimer: number | null = null
@@ -270,28 +273,32 @@ export class IDCardDetector implements Disposable {
270
273
  private async detectIDCard(imageData: ImageData): Promise<DetectionResult> {
271
274
  // 1. 图像预处理
272
275
  const grayscale = ImageProcessor.toGrayscale(imageData)
273
-
274
- // 2. 检测矩形和边缘(简化版实现)
275
- // 注意:实际应用中应使用OpenCV.js或其他计算机视觉库进行更精确的检测
276
- // 此处仅作为概念性实现,使用基本矩形检测逻辑
277
-
278
- // 模拟检测过程,随机判断是否找到身份证
279
- // 在实际应用中,此处应当实现实际的计算机视觉算法
276
+
277
+ // 2. 使用Sobel边缘检测算法检测边缘
278
+ const edgeData = ImageProcessor.detectEdges(grayscale)
279
+
280
+ // 3. 检测矩形和边缘
281
+ // 使用基于边缘的矩形检测
282
+ const rectangles = this.detectRectangles(edgeData)
283
+
284
+ // 4. 评估检测结果 - 检查是否找到了合适的矩形
285
+ const idCardRect = this.findIdCardRectangle(rectangles, imageData.width, imageData.height)
286
+
280
287
  const detectionResult: DetectionResult = {
281
- success: Math.random() > 0.3, // 70%的概率成功检测到
282
- message: "身份证检测完成",
288
+ success: idCardRect !== null,
289
+ message: idCardRect ? "身份证检测成功" : "未检测到身份证",
283
290
  }
284
291
 
285
- if (detectionResult.success) {
286
- // 模拟一个身份证矩形区域
292
+ if (detectionResult.success && idCardRect) {
293
+ // 使用检测到的身份证矩形区域
287
294
  const width = imageData.width
288
295
  const height = imageData.height
289
-
290
- // 大致的身份证区域(按比例)
291
- const rectWidth = Math.round(width * 0.7)
292
- const rectHeight = Math.round(rectWidth * 0.618) // 身份证是黄金比例
293
- const rectX = Math.round((width - rectWidth) / 2)
294
- const rectY = Math.round((height - rectHeight) / 2)
296
+
297
+ // 使用实际检测到的身份证区域
298
+ const rectWidth = idCardRect.width
299
+ const rectHeight = idCardRect.height
300
+ const rectX = idCardRect.x
301
+ const rectY = idCardRect.y
295
302
 
296
303
  // 添加四个角点
297
304
  detectionResult.corners = [
@@ -337,8 +344,8 @@ export class IDCardDetector implements Disposable {
337
344
  )
338
345
  }
339
346
 
340
- // 设置置信度
341
- detectionResult.confidence = 0.7 + Math.random() * 0.3
347
+ // 设置置信度 - 基于边缘强度和矩形形状评分
348
+ detectionResult.confidence = this.calculateConfidence(idCardRect, edgeData)
342
349
  }
343
350
 
344
351
  return detectionResult
@@ -360,4 +367,144 @@ export class IDCardDetector implements Disposable {
360
367
  this.camera.release()
361
368
  this.resultCache.clear()
362
369
  }
370
+
371
+ /**
372
+ * 从边缘图像中检测矩形
373
+ * @param edgeData 边缘检测后的图像数据
374
+ * @returns 检测到的矩形数组
375
+ */
376
+ private detectRectangles(edgeData: ImageData): Array<{
377
+ x: number;
378
+ y: number;
379
+ width: number;
380
+ height: number;
381
+ confidence: number;
382
+ }> {
383
+ const width = edgeData.width;
384
+ const height = edgeData.height;
385
+ const minSize = Math.min(width, height) * 0.2; // 最小矩形尺寸
386
+ const rectangles = [];
387
+
388
+ // 使用积分图像加速边缘密度计算
389
+ const integralImg = new Uint32Array(width * height);
390
+
391
+ // 计算积分图像
392
+ for (let y = 0; y < height; y++) {
393
+ for (let x = 0; x < width; x++) {
394
+ const idx = y * width + x;
395
+ const pixel = (edgeData.data[idx * 4] > 128) ? 1 : 0; // 边缘为白色
396
+
397
+ // 计算积分图
398
+ const above = y > 0 ? integralImg[(y - 1) * width + x] : 0;
399
+ const left = x > 0 ? integralImg[y * width + (x - 1)] : 0;
400
+ const diagonal = (x > 0 && y > 0) ? integralImg[(y - 1) * width + (x - 1)] : 0;
401
+
402
+ integralImg[idx] = pixel + above + left - diagonal;
403
+ }
404
+ }
405
+
406
+ // 滑动窗口检测矩形
407
+ for (let h = minSize; h < height * 0.9; h += Math.max(2, Math.floor(h * 0.05))) {
408
+ // 计算当前高度下,按照标准身份证比例的宽度
409
+ const w = Math.round(h * IDCardDetector.ID_CARD_ASPECT_RATIO);
410
+ if (w > width * 0.9) continue;
411
+
412
+ for (let y = 0; y < height - h; y += Math.max(2, Math.floor(h * 0.1))) {
413
+ for (let x = 0; x < width - w; x += Math.max(2, Math.floor(w * 0.1))) {
414
+ // 计算矩形区域内的边缘密度
415
+ const edgeCount = this.calculateRectSum(integralImg, x, y, w, h, width);
416
+ const avgEdgeDensity = edgeCount / (w * h);
417
+
418
+ // 计算矩形边界的边缘密度
419
+ const perimeterEdgeCount = this.calculateRectPerimeter(integralImg, x, y, w, h, width);
420
+ const perimeterLength = 2 * (w + h);
421
+ const perimeterDensity = perimeterEdgeCount / perimeterLength;
422
+
423
+ // 矩形得分 - 边界边缘密度高且内部适中
424
+ const rectScore = perimeterDensity * 0.7 + (0.3 - Math.abs(0.15 - avgEdgeDensity)) * 0.3;
425
+
426
+ if (rectScore > 0.4) { // 阈值可根据实际项目调整
427
+ rectangles.push({
428
+ x,
429
+ y,
430
+ width: w,
431
+ height: h,
432
+ confidence: rectScore
433
+ });
434
+ }
435
+ }
436
+ }
437
+ }
438
+
439
+ // 按得分排序
440
+ return rectangles.sort((a, b) => b.confidence - a.confidence);
441
+ }
442
+
443
+ /**
444
+ * 使用积分图计算矩形区域内的总和
445
+ */
446
+ private calculateRectSum(integral: Uint32Array, x: number, y: number, w: number, h: number, stride: number): number {
447
+ const x2 = Math.min(x + w - 1, stride - 1);
448
+ const y2 = Math.min(y + h - 1, integral.length / stride - 1);
449
+
450
+ const topLeft = (x > 0 && y > 0) ? integral[(y - 1) * stride + (x - 1)] : 0;
451
+ const topRight = y > 0 ? integral[(y - 1) * stride + x2] : 0;
452
+ const bottomLeft = x > 0 ? integral[y2 * stride + (x - 1)] : 0;
453
+ const bottomRight = integral[y2 * stride + x2];
454
+
455
+ return bottomRight - topRight - bottomLeft + topLeft;
456
+ }
457
+
458
+ /**
459
+ * 计算矩形周长上的边缘点数量
460
+ */
461
+ private calculateRectPerimeter(integral: Uint32Array, x: number, y: number, w: number, h: number, stride: number): number {
462
+ // 上边缘
463
+ const topEdgeSum = this.calculateRectSum(integral, x, y, w, 1, stride);
464
+ // 下边缘
465
+ const bottomEdgeSum = this.calculateRectSum(integral, x, y + h - 1, w, 1, stride);
466
+ // 左边缘
467
+ const leftEdgeSum = this.calculateRectSum(integral, x, y, 1, h, stride);
468
+ // 右边缘
469
+ const rightEdgeSum = this.calculateRectSum(integral, x + w - 1, y, 1, h, stride);
470
+
471
+ return topEdgeSum + bottomEdgeSum + leftEdgeSum + rightEdgeSum;
472
+ }
473
+
474
+ /**
475
+ * 从检测到的矩形中找出最可能是身份证的矩形
476
+ */
477
+ private findIdCardRectangle(rectangles: Array<{x: number; y: number; width: number; height: number; confidence: number}>, imageWidth: number, imageHeight: number): {x: number; y: number; width: number; height: number; confidence: number} | null {
478
+ if (rectangles.length === 0) return null;
479
+
480
+ // 筛选符合身份证宽高比的矩形
481
+ const filteredRects = rectangles.filter(rect => {
482
+ const aspectRatio = rect.width / rect.height;
483
+ return Math.abs(aspectRatio - IDCardDetector.ID_CARD_ASPECT_RATIO) < 0.2; // 允许20%的误差
484
+ });
485
+
486
+ if (filteredRects.length === 0) return null;
487
+
488
+ // 返回得分最高的矩形
489
+ return filteredRects[0];
490
+ }
491
+
492
+ /**
493
+ * 计算身份证检测的置信度
494
+ */
495
+ private calculateConfidence(rect: {x: number; y: number; width: number; height: number; confidence: number} | null, edgeData: ImageData): number {
496
+ if (!rect) return 0;
497
+
498
+ // 基本得分来自矩形检测
499
+ let score = rect.confidence;
500
+
501
+ // 额外因素:矩形大小相对于图像
502
+ const relativeSize = (rect.width * rect.height) / (edgeData.width * edgeData.height);
503
+ if (relativeSize > 0.1 && relativeSize < 0.7) {
504
+ score += 0.1; // 身份证通常占据图像的合理比例
505
+ }
506
+
507
+ // 范围限制在0-1之间
508
+ return Math.min(Math.max(score, 0), 1);
509
+ }
363
510
  }