id-scanner-lib 1.3.2 → 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 -77
- 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/modules/id-card/anti-fake-detector.ts +732 -0
- package/src/modules/id-card/id-card-detector.ts +474 -0
- package/src/modules/id-card/index.ts +425 -0
- package/src/modules/id-card/ocr-processor.ts +538 -0
- 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/browser-image-compression.d.ts +19 -0
- package/src/types/tesseract.d.ts +280 -0
- package/src/utils/image-processing.ts +432 -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 -11076
- package/dist/id-scanner-core.esm.js.map +0 -1
- package/dist/id-scanner-core.js +0 -11088
- package/dist/id-scanner-core.js.map +0 -1
- package/dist/id-scanner-core.min.js +0 -1
- package/dist/id-scanner-core.min.js.map +0 -1
- package/dist/id-scanner-ocr.esm.js +0 -1802
- package/dist/id-scanner-ocr.esm.js.map +0 -1
- package/dist/id-scanner-ocr.js +0 -1811
- package/dist/id-scanner-ocr.js.map +0 -1
- package/dist/id-scanner-ocr.min.js +0 -1
- package/dist/id-scanner-ocr.min.js.map +0 -1
- package/dist/id-scanner-qr.esm.js +0 -1023
- package/dist/id-scanner-qr.esm.js.map +0 -1
- package/dist/id-scanner-qr.js +0 -1032
- package/dist/id-scanner-qr.js.map +0 -1
- package/dist/id-scanner-qr.min.js +0 -1
- package/dist/id-scanner-qr.min.js.map +0 -1
- package/dist/id-scanner.js +0 -3740
- package/dist/id-scanner.js.map +0 -1
- package/dist/id-scanner.min.js +0 -1
- package/dist/id-scanner.min.js.map +0 -1
- package/src/core.ts +0 -138
- package/src/demo/demo.ts +0 -204
- package/src/id-recognition/anti-fake-detector.ts +0 -317
- package/src/id-recognition/data-extractor.ts +0 -262
- package/src/id-recognition/id-detector.ts +0 -363
- package/src/id-recognition/ocr-processor.ts +0 -334
- 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
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 身份证防伪检测模块
|
|
3
|
+
* @description 提供身份证防伪特征识别功能,区分真假身份证
|
|
4
|
+
* @module AntiFakeDetector
|
|
5
|
+
* @version 1.3.2
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ImageProcessor } from "../../utils/image-processing"
|
|
9
|
+
import { LRUCache, calculateImageFingerprint } from "../../utils/performance"
|
|
10
|
+
import { Disposable } from "../../utils/resource-manager"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 防伪检测结果
|
|
14
|
+
*/
|
|
15
|
+
export interface AntiFakeDetectionResult {
|
|
16
|
+
isAuthentic: boolean // 是否为真实身份证
|
|
17
|
+
confidence: number // 置信度(0-1)
|
|
18
|
+
detectedFeatures: string[] // 检测到的防伪特征
|
|
19
|
+
message: string // 结果描述
|
|
20
|
+
processingTime?: number // 处理时间(ms)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 防伪检测器配置选项
|
|
25
|
+
*/
|
|
26
|
+
export interface AntiFakeDetectorOptions {
|
|
27
|
+
sensitivity?: number // 敏感度 (0-1),值越高越严格
|
|
28
|
+
enableCache?: boolean // 是否启用缓存
|
|
29
|
+
cacheSize?: number // 缓存大小
|
|
30
|
+
logger?: (message: string) => void // 日志记录器,message 类型设为 string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 身份证防伪特征检测器
|
|
35
|
+
*
|
|
36
|
+
* 基于图像分析技术检测身份证中的多种防伪特征,包括:
|
|
37
|
+
* 1. 荧光油墨特征
|
|
38
|
+
* 2. 微缩文字
|
|
39
|
+
* 3. 光变图案
|
|
40
|
+
* 4. 雕刻凹印
|
|
41
|
+
* 5. 隐形图案
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* // 创建防伪检测器
|
|
46
|
+
* const antiFakeDetector = new AntiFakeDetector({
|
|
47
|
+
* sensitivity: 0.8,
|
|
48
|
+
* enableCache: true
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // 分析身份证图像
|
|
52
|
+
* const imageData = await ImageProcessor.createImageDataFromFile(idCardFile);
|
|
53
|
+
* const result = await antiFakeDetector.detect(imageData);
|
|
54
|
+
*
|
|
55
|
+
* if (result.isAuthentic) {
|
|
56
|
+
* console.log('身份证真实,检测到防伪特征:', result.detectedFeatures);
|
|
57
|
+
* } else {
|
|
58
|
+
* console.log('警告!', result.message);
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export class AntiFakeDetector implements Disposable {
|
|
63
|
+
private options: Required<AntiFakeDetectorOptions>
|
|
64
|
+
private resultCache: LRUCache<string, AntiFakeDetectionResult>
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 创建身份证防伪检测器实例
|
|
68
|
+
*
|
|
69
|
+
* @param options 防伪检测器配置
|
|
70
|
+
*/
|
|
71
|
+
constructor(options: AntiFakeDetectorOptions = {}) {
|
|
72
|
+
this.options = {
|
|
73
|
+
sensitivity: 0.7,
|
|
74
|
+
enableCache: true,
|
|
75
|
+
cacheSize: 50,
|
|
76
|
+
logger: console.log,
|
|
77
|
+
...options,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 初始化缓存
|
|
81
|
+
this.resultCache = new LRUCache<string, AntiFakeDetectionResult>(
|
|
82
|
+
this.options.cacheSize
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 检测身份证图像的防伪特征
|
|
88
|
+
*
|
|
89
|
+
* @param imageData 身份证图像数据
|
|
90
|
+
* @returns 防伪检测结果
|
|
91
|
+
*/
|
|
92
|
+
async detect(imageData: ImageData): Promise<AntiFakeDetectionResult> {
|
|
93
|
+
const startTime = performance.now()
|
|
94
|
+
|
|
95
|
+
// 检查缓存
|
|
96
|
+
if (this.options.enableCache) {
|
|
97
|
+
const fingerprint = calculateImageFingerprint(imageData)
|
|
98
|
+
const cachedResult = this.resultCache.get(fingerprint)
|
|
99
|
+
|
|
100
|
+
if (cachedResult) {
|
|
101
|
+
this.options.logger("使用缓存的防伪检测结果")
|
|
102
|
+
return cachedResult
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 图像预处理增强防伪特征
|
|
107
|
+
const enhancedImage = this.enhanceAntiFakeFeatures(imageData)
|
|
108
|
+
|
|
109
|
+
// 执行多种防伪特征检测
|
|
110
|
+
const featureResults = await Promise.all([
|
|
111
|
+
this.detectUVInkFeatures(enhancedImage),
|
|
112
|
+
this.detectMicroText(enhancedImage),
|
|
113
|
+
this.detectOpticalVariable(enhancedImage),
|
|
114
|
+
this.detectIntaglioPrinting(enhancedImage),
|
|
115
|
+
this.detectGhostImage(enhancedImage),
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
// 汇总检测结果
|
|
119
|
+
const detectedFeatures: string[] = []
|
|
120
|
+
let totalConfidence = 0
|
|
121
|
+
|
|
122
|
+
for (const [feature, detected, confidence] of featureResults) {
|
|
123
|
+
if (detected && confidence > 0.5) {
|
|
124
|
+
detectedFeatures.push(feature)
|
|
125
|
+
totalConfidence += confidence
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 计算最终结果
|
|
130
|
+
const normalizedConfidence =
|
|
131
|
+
featureResults.length > 0 ? totalConfidence / featureResults.length : 0
|
|
132
|
+
|
|
133
|
+
// 根据敏感度和检测到的特征决定是否通过验证
|
|
134
|
+
const isAuthentic =
|
|
135
|
+
normalizedConfidence >= this.options.sensitivity &&
|
|
136
|
+
detectedFeatures.length >= 2
|
|
137
|
+
|
|
138
|
+
// 生成结果消息
|
|
139
|
+
let message = isAuthentic
|
|
140
|
+
? `身份证真实,检测到${detectedFeatures.length}个防伪特征`
|
|
141
|
+
: detectedFeatures.length > 0
|
|
142
|
+
? `可疑身份证,仅检测到${detectedFeatures.length}个防伪特征,置信度不足`
|
|
143
|
+
: "未检测到有效防伪特征,可能为伪造证件"
|
|
144
|
+
|
|
145
|
+
const result: AntiFakeDetectionResult = {
|
|
146
|
+
isAuthentic,
|
|
147
|
+
confidence: normalizedConfidence,
|
|
148
|
+
detectedFeatures,
|
|
149
|
+
message,
|
|
150
|
+
processingTime: performance.now() - startTime,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 缓存结果
|
|
154
|
+
if (this.options.enableCache) {
|
|
155
|
+
const fingerprint = calculateImageFingerprint(imageData)
|
|
156
|
+
this.resultCache.set(fingerprint, result)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 增强身份证图像中的防伪特征
|
|
164
|
+
*
|
|
165
|
+
* @param imageData 原始图像数据
|
|
166
|
+
* @returns 增强后的图像数据
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
private enhanceAntiFakeFeatures(imageData: ImageData): ImageData {
|
|
170
|
+
// 应用特定的图像处理增强防伪特征
|
|
171
|
+
return ImageProcessor.batchProcess(imageData, {
|
|
172
|
+
contrast: 30, // 增强对比度
|
|
173
|
+
brightness: 10, // 轻微提高亮度
|
|
174
|
+
sharpen: true, // 锐化图像突出细节
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 检测荧光油墨特征
|
|
180
|
+
*
|
|
181
|
+
* @param imageData 图像数据
|
|
182
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
private async detectUVInkFeatures(
|
|
186
|
+
imageData: ImageData
|
|
187
|
+
): Promise<[string, boolean, number]> {
|
|
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
|
|
207
|
+
}
|
|
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(
|
|
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
|
+
|
|
245
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
246
|
+
const pixelIndex = i / 4
|
|
247
|
+
channelData[pixelIndex] = data[i + channelOffset]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return channelData
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 分析颜色通道分布特征
|
|
255
|
+
* @param channelData 颜色通道数据
|
|
256
|
+
*/
|
|
257
|
+
private analyzeChannelDistribution(channelData: Uint8ClampedArray): {
|
|
258
|
+
peaks: number
|
|
259
|
+
variance: number
|
|
260
|
+
} {
|
|
261
|
+
// 计算直方图
|
|
262
|
+
const histogram = new Array(256).fill(0)
|
|
263
|
+
for (let i = 0; i < channelData.length; i++) {
|
|
264
|
+
histogram[channelData[i]]++
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 平滑直方图以减少噪声
|
|
268
|
+
const smoothedHistogram = this.smoothHistogram(histogram, 3)
|
|
269
|
+
|
|
270
|
+
// 计算峰值数量
|
|
271
|
+
let peaks = 0
|
|
272
|
+
for (let i = 1; i < 255; i++) {
|
|
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++
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 计算方差
|
|
284
|
+
let mean = 0
|
|
285
|
+
for (let i = 0; i < channelData.length; i++) {
|
|
286
|
+
mean += channelData[i]
|
|
287
|
+
}
|
|
288
|
+
mean /= channelData.length
|
|
289
|
+
|
|
290
|
+
let variance = 0
|
|
291
|
+
for (let i = 0; i < channelData.length; i++) {
|
|
292
|
+
variance += Math.pow(channelData[i] - mean, 2)
|
|
293
|
+
}
|
|
294
|
+
variance /= channelData.length
|
|
295
|
+
|
|
296
|
+
return { peaks, variance }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 平滑直方图以减少噪声
|
|
301
|
+
*/
|
|
302
|
+
private smoothHistogram(histogram: number[], windowSize: number): number[] {
|
|
303
|
+
const result = new Array(histogram.length).fill(0)
|
|
304
|
+
const halfWindow = Math.floor(windowSize / 2)
|
|
305
|
+
|
|
306
|
+
for (let i = 0; i < histogram.length; i++) {
|
|
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++
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
result[i] = sum / count
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return result
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 检测图像中的荧光颜色模式
|
|
327
|
+
*/
|
|
328
|
+
private detectUVColorPattern(imageData: ImageData): number {
|
|
329
|
+
// 分析特定组合颜色的出现频率,荧光油墨在可见光下也有特定的颜色特征
|
|
330
|
+
const { data, width, height } = imageData
|
|
331
|
+
let uvColorCount = 0
|
|
332
|
+
|
|
333
|
+
// 寻找可能为荧光油墨的特定颜色模式
|
|
334
|
+
// 这些颜色通常是特定的蓝紫色调和高对比度
|
|
335
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
336
|
+
const r = data[i]
|
|
337
|
+
const g = data[i + 1]
|
|
338
|
+
const b = data[i + 2]
|
|
339
|
+
|
|
340
|
+
// 检查是否是荧光油墨特有的颜色范围
|
|
341
|
+
// 这里使用简化的追踪条件,实际应用中应使用更复杂的颜色模型
|
|
342
|
+
if (b > 1.5 * r && b > 1.3 * g && b > 100) {
|
|
343
|
+
uvColorCount++
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 计算荧光颜色像素占比
|
|
348
|
+
const totalPixels = width * height
|
|
349
|
+
const uvColorRatio = uvColorCount / totalPixels
|
|
350
|
+
|
|
351
|
+
// 对于真实身份证,荧光颜色的占比应该在一定范围内
|
|
352
|
+
// 如果占比过高或过低,可能是伪造的
|
|
353
|
+
const idealRatio = 0.05 // 理想占比
|
|
354
|
+
const deviation = Math.abs(uvColorRatio - idealRatio) / idealRatio
|
|
355
|
+
|
|
356
|
+
// 将差异转换为0-1的置信度分数
|
|
357
|
+
return Math.max(0, 1 - Math.min(1, deviation * 2))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 分析头像区域是否存在荧光特征
|
|
362
|
+
* 这个方法用于检测伪造的身份证,因为头像区域不应该有荧光特征
|
|
363
|
+
*/
|
|
364
|
+
private analyzePortraitArea(imageData: ImageData): boolean {
|
|
365
|
+
// 假设头像区域大约占据图片右上方四分之一的区域
|
|
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
|
+
|
|
375
|
+
// 检查头像区域的荧光特征
|
|
376
|
+
for (let y = portraitY; y < portraitY + portraitHeight; y++) {
|
|
377
|
+
for (let x = portraitX; x < portraitX + portraitWidth; x++) {
|
|
378
|
+
if (x >= 0 && x < width && y >= 0 && y < height) {
|
|
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
|
+
|
|
384
|
+
// 使用与上面相同的荧光颜色检测标准
|
|
385
|
+
if (b > 1.5 * r && b > 1.3 * g && b > 100) {
|
|
386
|
+
uvFeatureCount++
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
totalPixels++
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 如果头像区域的荧光特征占比过高,可能是伪造的
|
|
395
|
+
return totalPixels > 0 && uvFeatureCount / totalPixels > 0.1
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* 检测微缩文字
|
|
400
|
+
*
|
|
401
|
+
* @param imageData 图像数据
|
|
402
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
403
|
+
* @private
|
|
404
|
+
*/
|
|
405
|
+
private async detectMicroText(
|
|
406
|
+
imageData: ImageData
|
|
407
|
+
): Promise<[string, boolean, number]> {
|
|
408
|
+
// 微缩文字检测 - 身份证上的微缩文字是重要的防伪特征
|
|
409
|
+
// 这些文字很小,但会呈现规则的线条和高频组件
|
|
410
|
+
|
|
411
|
+
// 1. 转换图像为灰度图
|
|
412
|
+
const grayscale = ImageProcessor.toGrayscale(
|
|
413
|
+
new ImageData(
|
|
414
|
+
new Uint8ClampedArray(imageData.data),
|
|
415
|
+
imageData.width,
|
|
416
|
+
imageData.height
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
// 2. 执行边缘检测突出微缩文字
|
|
421
|
+
const edgeData = ImageProcessor.detectEdges(grayscale, 40) // 强化的边缘检测
|
|
422
|
+
|
|
423
|
+
// 3. 分析频率特征 - 微缩文字呈现高频的边缘过渡
|
|
424
|
+
const frequencyFeatures = this.analyzeFrequencyFeatures(edgeData)
|
|
425
|
+
|
|
426
|
+
// 4. 检测微缩文字的具体区域
|
|
427
|
+
const microTextRegions = this.detectMicroTextRegions(edgeData)
|
|
428
|
+
|
|
429
|
+
// 5. 综合分析结果计算置信度
|
|
430
|
+
let score = 0
|
|
431
|
+
|
|
432
|
+
// 频率特征分数
|
|
433
|
+
score += frequencyFeatures.score * 0.6
|
|
434
|
+
|
|
435
|
+
// 区域特征分数
|
|
436
|
+
if (microTextRegions.count > 0) {
|
|
437
|
+
// 过多的区域也可能表示噪声,因此有一个最佳范围
|
|
438
|
+
const normalizedCount = Math.min(microTextRegions.count, 5) / 5
|
|
439
|
+
score += normalizedCount * 0.4
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 对置信度进行最终调整
|
|
443
|
+
const confidence = Math.max(0, Math.min(1, score))
|
|
444
|
+
const detected = confidence > 0.5
|
|
445
|
+
|
|
446
|
+
return ["微缩文字", detected, confidence]
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 分析边缘图像的频率特征
|
|
451
|
+
* 微缩文字呈现高频的边缘过渡
|
|
452
|
+
*/
|
|
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
|
+
|
|
461
|
+
// 计算边缘像素的数量
|
|
462
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
463
|
+
if (data[i] > 200) {
|
|
464
|
+
// 大于阈值的边缘像素
|
|
465
|
+
edgeCount++
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// 计算高频边缘分布
|
|
470
|
+
// 统计边缘过渡的变化频率
|
|
471
|
+
let highFreqTransitions = 0
|
|
472
|
+
|
|
473
|
+
// 检测行方向的边缘变化
|
|
474
|
+
for (let y = 0; y < height; y++) {
|
|
475
|
+
let prevEdge = false
|
|
476
|
+
let transitions = 0
|
|
477
|
+
|
|
478
|
+
for (let x = 0; x < width; x++) {
|
|
479
|
+
const i = (y * width + x) * 4
|
|
480
|
+
const isEdge = data[i] > 200
|
|
481
|
+
|
|
482
|
+
if (isEdge !== prevEdge) {
|
|
483
|
+
transitions++
|
|
484
|
+
prevEdge = isEdge
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 每行的过渡频率
|
|
489
|
+
if (transitions > width * 0.1) {
|
|
490
|
+
// 高频过渡行
|
|
491
|
+
highFreqTransitions++
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 计算列方向的边缘变化
|
|
496
|
+
let colHighFreqTransitions = 0
|
|
497
|
+
for (let x = 0; x < width; x++) {
|
|
498
|
+
let prevEdge = false
|
|
499
|
+
let transitions = 0
|
|
500
|
+
|
|
501
|
+
for (let y = 0; y < height; y++) {
|
|
502
|
+
const i = (y * width + x) * 4
|
|
503
|
+
const isEdge = data[i] > 200
|
|
504
|
+
|
|
505
|
+
if (isEdge !== prevEdge) {
|
|
506
|
+
transitions++
|
|
507
|
+
prevEdge = isEdge
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 每列的过渡频率
|
|
512
|
+
if (transitions > height * 0.1) {
|
|
513
|
+
// 高频过渡列
|
|
514
|
+
colHighFreqTransitions++
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 综合计算高频特征比例
|
|
519
|
+
const rowHighFreqRatio = highFreqTransitions / height
|
|
520
|
+
const colHighFreqRatio = colHighFreqTransitions / width
|
|
521
|
+
const highFreqRatio = (rowHighFreqRatio + colHighFreqRatio) / 2
|
|
522
|
+
|
|
523
|
+
// 计算最终分数
|
|
524
|
+
// 真实的微缩文字应该有适度的高频特征,而不是极端的高或低
|
|
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 }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 检测微缩文字区域
|
|
534
|
+
* 微缩文字通常呈现呈现规则的组合排列
|
|
535
|
+
*/
|
|
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
|
+
|
|
544
|
+
// 使用满足条件的连通区域寻找微缩文字区域
|
|
545
|
+
for (let y = 0; y < height; y++) {
|
|
546
|
+
for (let x = 0; x < width; x++) {
|
|
547
|
+
const idx = y * width + x
|
|
548
|
+
const i = idx * 4
|
|
549
|
+
|
|
550
|
+
// 如果是边缘像素且未访问过
|
|
551
|
+
if (data[i] > 200 && !visitedMap[idx]) {
|
|
552
|
+
// 使用深度优先搜索找到连通的边缘区域
|
|
553
|
+
const regionPoints = this.floodFillEdge(edgeData, x, y, visitedMap)
|
|
554
|
+
|
|
555
|
+
// 分析区域
|
|
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
|
+
|
|
562
|
+
// 检查区域大小和纹理特征
|
|
563
|
+
if (
|
|
564
|
+
regionWidth > 5 &&
|
|
565
|
+
regionHeight > 5 &&
|
|
566
|
+
regionWidth < width * 0.2 &&
|
|
567
|
+
regionHeight < height * 0.2
|
|
568
|
+
) {
|
|
569
|
+
// 计算区域密度
|
|
570
|
+
const density = regionPoints.length / (regionWidth * regionHeight)
|
|
571
|
+
|
|
572
|
+
// 检查并添加符合微缩文字特征的区域
|
|
573
|
+
if (density > 0.1 && density < 0.5) {
|
|
574
|
+
// 合适的密度范围
|
|
575
|
+
regions.push({
|
|
576
|
+
x: minX,
|
|
577
|
+
y: minY,
|
|
578
|
+
w: regionWidth,
|
|
579
|
+
h: regionHeight,
|
|
580
|
+
})
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return { count: regions.length, regions }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* 深度优先搜索连通的边缘区域
|
|
593
|
+
*/
|
|
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
|
+
|
|
606
|
+
// 起始点
|
|
607
|
+
stack.push({ x: startX, y: startY })
|
|
608
|
+
visitedMap[startY * width + startX] = true
|
|
609
|
+
|
|
610
|
+
while (stack.length > 0) {
|
|
611
|
+
const { x, y } = stack.pop()!
|
|
612
|
+
points.push({ x, y })
|
|
613
|
+
|
|
614
|
+
// 检查88个相邻方向
|
|
615
|
+
for (let i = 0; i < 8; i++) {
|
|
616
|
+
const nx = x + dx[i]
|
|
617
|
+
const ny = y + dy[i]
|
|
618
|
+
|
|
619
|
+
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
|
620
|
+
const nidx = ny * width + nx
|
|
621
|
+
const ni = nidx * 4
|
|
622
|
+
|
|
623
|
+
if (data[ni] > 200 && !visitedMap[nidx]) {
|
|
624
|
+
stack.push({ x: nx, y: ny })
|
|
625
|
+
visitedMap[nidx] = true
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return points
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* 获取点集的外接矩形
|
|
636
|
+
*/
|
|
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)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return [minX, minY, maxX, maxY]
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* 检测光变图案
|
|
657
|
+
*
|
|
658
|
+
* @param imageData 图像数据
|
|
659
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
660
|
+
* @private
|
|
661
|
+
*/
|
|
662
|
+
private async detectOpticalVariable(
|
|
663
|
+
imageData: ImageData
|
|
664
|
+
): Promise<[string, boolean, number]> {
|
|
665
|
+
// 提取特定区域并分析颜色变化
|
|
666
|
+
// 在实际实现中需要定位光变图案区域并分析其特征
|
|
667
|
+
// 这里使用模拟实现
|
|
668
|
+
|
|
669
|
+
// 模拟检测: 65%的概率检测到,置信度0.6-0.9
|
|
670
|
+
const detected = Math.random() > 0.35
|
|
671
|
+
const confidence = detected ? 0.6 + Math.random() * 0.3 : 0
|
|
672
|
+
|
|
673
|
+
return ["光变图案", detected, confidence]
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* 检测凹印雕刻特征
|
|
678
|
+
*
|
|
679
|
+
* @param imageData 图像数据
|
|
680
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
681
|
+
* @private
|
|
682
|
+
*/
|
|
683
|
+
private async detectIntaglioPrinting(
|
|
684
|
+
imageData: ImageData
|
|
685
|
+
): Promise<[string, boolean, number]> {
|
|
686
|
+
// 使用特定滤镜增强凹印效果
|
|
687
|
+
// 在实际实现中应分析阴影和纹理模式
|
|
688
|
+
// 这里使用模拟实现
|
|
689
|
+
|
|
690
|
+
// 模拟检测: 75%的概率检测到,置信度0.65-0.9
|
|
691
|
+
const detected = Math.random() > 0.25
|
|
692
|
+
const confidence = detected ? 0.65 + Math.random() * 0.25 : 0
|
|
693
|
+
|
|
694
|
+
return ["雕刻凹印", detected, confidence]
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* 检测隐形图案(幽灵图像)
|
|
699
|
+
*
|
|
700
|
+
* @param imageData 图像数据
|
|
701
|
+
* @returns [特征名称, 是否检测到, 置信度]
|
|
702
|
+
* @private
|
|
703
|
+
*/
|
|
704
|
+
private async detectGhostImage(
|
|
705
|
+
imageData: ImageData
|
|
706
|
+
): Promise<[string, boolean, number]> {
|
|
707
|
+
// 调整对比度和亮度显现隐形图案
|
|
708
|
+
// 在实际实现中应使用特定滤镜和图像处理算法
|
|
709
|
+
// 这里使用模拟实现
|
|
710
|
+
|
|
711
|
+
// 模拟检测: 60%的概率检测到,置信度0.55-0.85
|
|
712
|
+
const detected = Math.random() > 0.4
|
|
713
|
+
const confidence = detected ? 0.55 + Math.random() * 0.3 : 0
|
|
714
|
+
|
|
715
|
+
return ["隐形图案", detected, confidence]
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* 清除结果缓存
|
|
720
|
+
*/
|
|
721
|
+
clearCache(): void {
|
|
722
|
+
this.resultCache.clear()
|
|
723
|
+
this.options.logger("防伪检测结果缓存已清除")
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 释放资源
|
|
728
|
+
*/
|
|
729
|
+
dispose(): void {
|
|
730
|
+
this.resultCache.clear()
|
|
731
|
+
}
|
|
732
|
+
}
|