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.
- package/README.md +55 -460
- package/dist/id-scanner-lib.esm.js +4641 -0
- package/dist/id-scanner-lib.esm.js.map +1 -0
- package/dist/id-scanner-lib.js +14755 -0
- package/dist/id-scanner-lib.js.map +1 -0
- package/dist/types/core/base-module.d.ts +44 -0
- package/dist/types/core/camera-manager.d.ts +258 -0
- package/dist/types/core/config.d.ts +88 -0
- package/dist/types/core/errors.d.ts +111 -0
- package/dist/types/core/event-emitter.d.ts +55 -0
- package/dist/types/core/logger.d.ts +277 -0
- package/dist/types/core/module-manager.d.ts +78 -0
- package/dist/types/core/plugin-manager.d.ts +158 -0
- package/dist/types/core/resource-manager.d.ts +246 -0
- package/dist/types/core/result.d.ts +83 -0
- package/dist/types/core/scanner-factory.d.ts +93 -0
- package/dist/types/index.bundle.d.ts +1303 -0
- package/dist/types/index.d.ts +86 -0
- package/dist/types/interfaces/external-types.d.ts +174 -0
- package/dist/types/interfaces/face-detection.d.ts +293 -0
- package/dist/types/interfaces/scanner-module.d.ts +280 -0
- package/dist/types/modules/face/face-detector.d.ts +170 -0
- package/dist/types/modules/face/index.d.ts +56 -0
- package/dist/types/modules/face/liveness-detector.d.ts +177 -0
- package/dist/types/modules/face/types.d.ts +136 -0
- package/dist/types/modules/id-card/anti-fake-detector.d.ts +170 -0
- package/dist/types/modules/id-card/id-card-detector.d.ts +131 -0
- package/dist/types/modules/id-card/index.d.ts +89 -0
- package/dist/types/modules/id-card/ocr-processor.d.ts +110 -0
- package/dist/types/modules/id-card/ocr-worker.d.ts +31 -0
- package/dist/types/modules/id-card/types.d.ts +181 -0
- package/dist/types/modules/qrcode/index.d.ts +51 -0
- package/dist/types/modules/qrcode/qr-code-scanner.d.ts +64 -0
- package/dist/types/modules/qrcode/types.d.ts +67 -0
- package/dist/types/utils/camera.d.ts +81 -0
- package/dist/types/utils/image-processing.d.ts +176 -0
- package/dist/types/utils/index.d.ts +175 -0
- package/dist/types/utils/performance.d.ts +81 -0
- package/dist/types/utils/resource-manager.d.ts +53 -0
- package/dist/types/utils/types.d.ts +166 -0
- package/dist/types/utils/worker.d.ts +52 -0
- package/dist/types/version.d.ts +7 -0
- package/package.json +76 -75
- package/src/core/base-module.ts +78 -0
- package/src/core/camera-manager.ts +798 -0
- package/src/core/config.ts +268 -0
- package/src/core/errors.ts +174 -0
- package/src/core/event-emitter.ts +110 -0
- package/src/core/logger.ts +549 -0
- package/src/core/module-manager.ts +165 -0
- package/src/core/plugin-manager.ts +429 -0
- package/src/core/resource-manager.ts +762 -0
- package/src/core/result.ts +163 -0
- package/src/core/scanner-factory.ts +237 -0
- package/src/index.ts +113 -936
- package/src/interfaces/external-types.ts +200 -0
- package/src/interfaces/face-detection.ts +309 -0
- package/src/interfaces/scanner-module.ts +384 -0
- package/src/modules/face/face-detector.ts +931 -0
- package/src/modules/face/index.ts +208 -0
- package/src/modules/face/liveness-detector.ts +908 -0
- package/src/modules/face/types.ts +133 -0
- package/src/{id-recognition → modules/id-card}/anti-fake-detector.ts +273 -239
- package/src/modules/id-card/id-card-detector.ts +474 -0
- package/src/modules/id-card/index.ts +425 -0
- package/src/{id-recognition → modules/id-card}/ocr-processor.ts +149 -92
- package/src/modules/id-card/ocr-worker.ts +259 -0
- package/src/modules/id-card/types.ts +178 -0
- package/src/modules/qrcode/index.ts +175 -0
- package/src/modules/qrcode/qr-code-scanner.ts +230 -0
- package/src/modules/qrcode/types.ts +65 -0
- package/src/types/tesseract.d.ts +265 -22
- package/src/utils/image-processing.ts +68 -49
- package/src/utils/index.ts +426 -0
- package/src/utils/performance.ts +168 -131
- package/src/utils/resource-manager.ts +65 -146
- package/src/utils/types.ts +90 -2
- package/src/utils/worker.ts +123 -84
- package/src/version.ts +11 -0
- package/tools/scaffold.js +543 -0
- package/dist/id-scanner-core.esm.js +0 -11349
- package/dist/id-scanner-core.js +0 -11361
- package/dist/id-scanner-core.min.js +0 -1
- package/dist/id-scanner-ocr.esm.js +0 -2319
- package/dist/id-scanner-ocr.js +0 -2328
- package/dist/id-scanner-ocr.min.js +0 -1
- package/dist/id-scanner-qr.esm.js +0 -1296
- package/dist/id-scanner-qr.js +0 -1305
- package/dist/id-scanner-qr.min.js +0 -1
- package/dist/id-scanner.js +0 -4561
- package/dist/id-scanner.min.js +0 -1
- package/src/core.ts +0 -138
- package/src/demo/demo.ts +0 -204
- package/src/id-recognition/data-extractor.ts +0 -262
- package/src/id-recognition/id-detector.ts +0 -510
- package/src/id-recognition/ocr-worker.ts +0 -156
- package/src/index-umd.ts +0 -477
- package/src/ocr-module.ts +0 -187
- package/src/qr-module.ts +0 -179
- package/src/scanner/barcode-scanner.ts +0 -251
- 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 "
|
|
9
|
-
import { LRUCache, calculateImageFingerprint } from "
|
|
10
|
-
import { Disposable } from "
|
|
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:
|
|
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,
|
|
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(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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): {
|
|
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 (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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 (
|
|
302
|
-
|
|
303
|
-
|
|
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 &&
|
|
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): {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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): {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
536
|
-
const
|
|
537
|
-
const
|
|
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 (
|
|
541
|
-
|
|
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(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
let
|
|
609
|
-
let
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
/**
|