UNPKG

id-scanner-lib

Version:

Browser-based ID card, QR code, and face recognition scanner with liveness detection

733 lines (623 loc) 21.3 kB
/** * @file 身份证防伪检测模块 * @description 提供身份证防伪特征识别功能,区分真假身份证 * @module AntiFakeDetector * @version 1.3.2 */ import { ImageProcessor } from "../../utils/image-processing" import { LRUCache, calculateImageFingerprint } from "../../utils/performance" import { Disposable } from "../../utils/resource-manager" /** * 防伪检测结果 */ export interface AntiFakeDetectionResult { isAuthentic: boolean // 是否为真实身份证 confidence: number // 置信度(0-1) detectedFeatures: string[] // 检测到的防伪特征 message: string // 结果描述 processingTime?: number // 处理时间(ms) } /** * 防伪检测器配置选项 */ export interface AntiFakeDetectorOptions { sensitivity?: number // 敏感度 (0-1),值越高越严格 enableCache?: boolean // 是否启用缓存 cacheSize?: number // 缓存大小 logger?: (message: string) => void // 日志记录器,message 类型设为 string } /** * 身份证防伪特征检测器 * * 基于图像分析技术检测身份证中的多种防伪特征,包括: * 1. 荧光油墨特征 * 2. 微缩文字 * 3. 光变图案 * 4. 雕刻凹印 * 5. 隐形图案 * * @example * ```typescript * // 创建防伪检测器 * const antiFakeDetector = new AntiFakeDetector({ * sensitivity: 0.8, * enableCache: true * }); * * // 分析身份证图像 * const imageData = await ImageProcessor.createImageDataFromFile(idCardFile); * const result = await antiFakeDetector.detect(imageData); * * if (result.isAuthentic) { * console.log('身份证真实,检测到防伪特征:', result.detectedFeatures); * } else { * console.log('警告!', result.message); * } * ``` */ export class AntiFakeDetector implements Disposable { private options: Required<AntiFakeDetectorOptions> private resultCache: LRUCache<string, AntiFakeDetectionResult> /** * 创建身份证防伪检测器实例 * * @param options 防伪检测器配置 */ constructor(options: AntiFakeDetectorOptions = {}) { this.options = { sensitivity: 0.7, enableCache: true, cacheSize: 50, logger: console.log, ...options, } // 初始化缓存 this.resultCache = new LRUCache<string, AntiFakeDetectionResult>( this.options.cacheSize ) } /** * 检测身份证图像的防伪特征 * * @param imageData 身份证图像数据 * @returns 防伪检测结果 */ async detect(imageData: ImageData): Promise<AntiFakeDetectionResult> { const startTime = performance.now() // 检查缓存 if (this.options.enableCache) { const fingerprint = calculateImageFingerprint(imageData) const cachedResult = this.resultCache.get(fingerprint) if (cachedResult) { this.options.logger("使用缓存的防伪检测结果") return cachedResult } } // 图像预处理增强防伪特征 const enhancedImage = this.enhanceAntiFakeFeatures(imageData) // 执行多种防伪特征检测 const featureResults = await Promise.all([ this.detectUVInkFeatures(enhancedImage), this.detectMicroText(enhancedImage), this.detectOpticalVariable(enhancedImage), this.detectIntaglioPrinting(enhancedImage), this.detectGhostImage(enhancedImage), ]) // 汇总检测结果 const detectedFeatures: string[] = [] let totalConfidence = 0 for (const [feature, detected, confidence] of featureResults) { if (detected && confidence > 0.5) { detectedFeatures.push(feature) totalConfidence += confidence } } // 计算最终结果 const normalizedConfidence = featureResults.length > 0 ? totalConfidence / featureResults.length : 0 // 根据敏感度和检测到的特征决定是否通过验证 const isAuthentic = normalizedConfidence >= this.options.sensitivity && detectedFeatures.length >= 2 // 生成结果消息 const message = isAuthentic ? `身份证真实,检测到${detectedFeatures.length}个防伪特征` : detectedFeatures.length > 0 ? `可疑身份证,仅检测到${detectedFeatures.length}个防伪特征,置信度不足` : "未检测到有效防伪特征,可能为伪造证件" const result: AntiFakeDetectionResult = { isAuthentic, confidence: normalizedConfidence, detectedFeatures, message, processingTime: performance.now() - startTime, } // 缓存结果 if (this.options.enableCache) { const fingerprint = calculateImageFingerprint(imageData) this.resultCache.set(fingerprint, result) } return result } /** * 增强身份证图像中的防伪特征 * * @param imageData 原始图像数据 * @returns 增强后的图像数据 * @private */ private enhanceAntiFakeFeatures(imageData: ImageData): ImageData { // 应用特定的图像处理增强防伪特征 return ImageProcessor.batchProcess(imageData, { contrast: 30, // 增强对比度 brightness: 10, // 轻微提高亮度 sharpen: true, // 锐化图像突出细节 }) } /** * 检测荧光油墨特征 * * @param imageData 图像数据 * @returns [特征名称, 是否检测到, 置信度] * @private */ private async detectUVInkFeatures( imageData: ImageData ): Promise<[string, boolean, number]> { // 在真实身份证上,荧光油墨会在特定反光条件下呈现特定颜色特征 // 在普通可见光下,我们分析蓝色和紫外色通道分布特征 // 1. 提取蓝色通道并增强对比度 const blueChannel = this.extractColorChannel(imageData, "blue") // 2. 分析蓝色通道的分布特征 const { peaks, variance } = this.analyzeChannelDistribution(blueChannel) // 3. 分析特定区域的颜色模式 const patternScore = this.detectUVColorPattern(imageData) // 4. 计算综合得分 // 特征分析:荧光油墨在蓝色通道通常有显著峰值,且分布更聚集 let score = 0 // 过多的峰值表明可能是真实身份证上的荧光特征 if (peaks > 3 && peaks < 10) { score += 0.4 } // 方差越大,表示颜色对比度越高,更可能有荧光特征 if (variance > 1000) { score += 0.3 } // 颜色模式得分 score += patternScore * 0.3 // 重要区域分析 // 身份证头像区域通常不应具有荧光特征 const hasPortraitAreaFeatures = this.analyzePortraitArea(imageData) if (hasPortraitAreaFeatures) { // 头像区域不应该有荧光特征,如果有可能是伪造的 score -= 0.2 } // 求出最终分数并限制在[0,1]范围内 const confidence = Math.max(0, Math.min(1, score)) const detected = confidence > 0.55 return ["荧光油墨", detected, confidence] } /** * 从图像数据中提取指定颜色通道 * @param imageData 原始图像数据 * @param channel 通道名称(red, green, blue) */ private extractColorChannel( imageData: ImageData, channel: "red" | "green" | "blue" ): Uint8ClampedArray { const { data, width, height } = imageData const channelOffset = channel === "red" ? 0 : channel === "green" ? 1 : 2 const channelData = new Uint8ClampedArray(width * height) for (let i = 0; i < data.length; i += 4) { const pixelIndex = i / 4 channelData[pixelIndex] = data[i + channelOffset] } return channelData } /** * 分析颜色通道分布特征 * @param channelData 颜色通道数据 */ private analyzeChannelDistribution(channelData: Uint8ClampedArray): { peaks: number variance: number } { // 计算直方图 const histogram = new Array(256).fill(0) for (let i = 0; i < channelData.length; i++) { histogram[channelData[i]]++ } // 平滑直方图以减少噪声 const smoothedHistogram = this.smoothHistogram(histogram, 3) // 计算峰值数量 let peaks = 0 for (let i = 1; i < 255; i++) { if ( smoothedHistogram[i] > smoothedHistogram[i - 1] && smoothedHistogram[i] > smoothedHistogram[i + 1] && smoothedHistogram[i] > channelData.length * 0.01 ) { // 只计算显著峰值 peaks++ } } // 计算方差 let mean = 0 for (let i = 0; i < channelData.length; i++) { mean += channelData[i] } mean /= channelData.length let variance = 0 for (let i = 0; i < channelData.length; i++) { variance += Math.pow(channelData[i] - mean, 2) } variance /= channelData.length return { peaks, variance } } /** * 平滑直方图以减少噪声 */ private smoothHistogram(histogram: number[], windowSize: number): number[] { const result = new Array(histogram.length).fill(0) const halfWindow = Math.floor(windowSize / 2) for (let i = 0; i < histogram.length; i++) { let sum = 0 let count = 0 for ( let j = Math.max(0, i - halfWindow); j <= Math.min(histogram.length - 1, i + halfWindow); j++ ) { sum += histogram[j] count++ } result[i] = sum / count } return result } /** * 检测图像中的荧光颜色模式 */ private detectUVColorPattern(imageData: ImageData): number { // 分析特定组合颜色的出现频率,荧光油墨在可见光下也有特定的颜色特征 const { data, width, height } = imageData let uvColorCount = 0 // 寻找可能为荧光油墨的特定颜色模式 // 这些颜色通常是特定的蓝紫色调和高对比度 for (let i = 0; i < data.length; i += 4) { const r = data[i] const g = data[i + 1] const b = data[i + 2] // 检查是否是荧光油墨特有的颜色范围 // 这里使用简化的追踪条件,实际应用中应使用更复杂的颜色模型 if (b > 1.5 * r && b > 1.3 * g && b > 100) { uvColorCount++ } } // 计算荧光颜色像素占比 const totalPixels = width * height const uvColorRatio = uvColorCount / totalPixels // 对于真实身份证,荧光颜色的占比应该在一定范围内 // 如果占比过高或过低,可能是伪造的 const idealRatio = 0.05 // 理想占比 const deviation = Math.abs(uvColorRatio - idealRatio) / idealRatio // 将差异转换为0-1的置信度分数 return Math.max(0, 1 - Math.min(1, deviation * 2)) } /** * 分析头像区域是否存在荧光特征 * 这个方法用于检测伪造的身份证,因为头像区域不应该有荧光特征 */ private analyzePortraitArea(imageData: ImageData): boolean { // 假设头像区域大约占据图片右上方四分之一的区域 const { width, height, data } = imageData const portraitX = Math.floor(width * 0.6) const portraitY = Math.floor(height * 0.2) const portraitWidth = Math.floor(width * 0.3) const portraitHeight = Math.floor(height * 0.3) let uvFeatureCount = 0 let totalPixels = 0 // 检查头像区域的荧光特征 for (let y = portraitY; y < portraitY + portraitHeight; y++) { for (let x = portraitX; x < portraitX + portraitWidth; x++) { if (x >= 0 && x < width && y >= 0 && y < height) { const i = (y * width + x) * 4 const r = data[i] const g = data[i + 1] const b = data[i + 2] // 使用与上面相同的荧光颜色检测标准 if (b > 1.5 * r && b > 1.3 * g && b > 100) { uvFeatureCount++ } totalPixels++ } } } // 如果头像区域的荧光特征占比过高,可能是伪造的 return totalPixels > 0 && uvFeatureCount / totalPixels > 0.1 } /** * 检测微缩文字 * * @param imageData 图像数据 * @returns [特征名称, 是否检测到, 置信度] * @private */ private async detectMicroText( imageData: ImageData ): Promise<[string, boolean, number]> { // 微缩文字检测 - 身份证上的微缩文字是重要的防伪特征 // 这些文字很小,但会呈现规则的线条和高频组件 // 1. 转换图像为灰度图 const grayscale = ImageProcessor.toGrayscale( new ImageData( new Uint8ClampedArray(imageData.data), imageData.width, imageData.height ) ) // 2. 执行边缘检测突出微缩文字 const edgeData = ImageProcessor.detectEdges(grayscale, 40) // 强化的边缘检测 // 3. 分析频率特征 - 微缩文字呈现高频的边缘过渡 const frequencyFeatures = this.analyzeFrequencyFeatures(edgeData) // 4. 检测微缩文字的具体区域 const microTextRegions = this.detectMicroTextRegions(edgeData) // 5. 综合分析结果计算置信度 let score = 0 // 频率特征分数 score += frequencyFeatures.score * 0.6 // 区域特征分数 if (microTextRegions.count > 0) { // 过多的区域也可能表示噪声,因此有一个最佳范围 const normalizedCount = Math.min(microTextRegions.count, 5) / 5 score += normalizedCount * 0.4 } // 对置信度进行最终调整 const confidence = Math.max(0, Math.min(1, score)) const detected = confidence > 0.5 return ["微缩文字", detected, confidence] } /** * 分析边缘图像的频率特征 * 微缩文字呈现高频的边缘过渡 */ private analyzeFrequencyFeatures(edgeData: ImageData): { score: number highFreqRatio: number } { const { data, width, height } = edgeData let edgeCount = 0 const totalPixels = width * height // 计算边缘像素的数量 for (let i = 0; i < data.length; i += 4) { if (data[i] > 200) { // 大于阈值的边缘像素 edgeCount++ } } // 计算高频边缘分布 // 统计边缘过渡的变化频率 let highFreqTransitions = 0 // 检测行方向的边缘变化 for (let y = 0; y < height; y++) { let prevEdge = false let transitions = 0 for (let x = 0; x < width; x++) { const i = (y * width + x) * 4 const isEdge = data[i] > 200 if (isEdge !== prevEdge) { transitions++ prevEdge = isEdge } } // 每行的过渡频率 if (transitions > width * 0.1) { // 高频过渡行 highFreqTransitions++ } } // 计算列方向的边缘变化 let colHighFreqTransitions = 0 for (let x = 0; x < width; x++) { let prevEdge = false let transitions = 0 for (let y = 0; y < height; y++) { const i = (y * width + x) * 4 const isEdge = data[i] > 200 if (isEdge !== prevEdge) { transitions++ prevEdge = isEdge } } // 每列的过渡频率 if (transitions > height * 0.1) { // 高频过渡列 colHighFreqTransitions++ } } // 综合计算高频特征比例 const rowHighFreqRatio = highFreqTransitions / height const colHighFreqRatio = colHighFreqTransitions / width const highFreqRatio = (rowHighFreqRatio + colHighFreqRatio) / 2 // 计算最终分数 // 真实的微缩文字应该有适度的高频特征,而不是极端的高或低 const idealRatio = 0.15 // 理想的高频比例 const deviationFactor = Math.abs(highFreqRatio - idealRatio) / idealRatio const score = Math.max(0, 1 - Math.min(1, deviationFactor * 3)) return { score, highFreqRatio } } /** * 检测微缩文字区域 * 微缩文字通常呈现呈现规则的组合排列 */ private detectMicroTextRegions(edgeData: ImageData): { count: number regions: Array<{ x: number; y: number; w: number; h: number }> } { const { data, width, height } = edgeData const visitedMap = new Array(width * height).fill(false) const regions: Array<{ x: number; y: number; w: number; h: number }> = [] // 使用满足条件的连通区域寻找微缩文字区域 for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const idx = y * width + x const i = idx * 4 // 如果是边缘像素且未访问过 if (data[i] > 200 && !visitedMap[idx]) { // 使用深度优先搜索找到连通的边缘区域 const regionPoints = this.floodFillEdge(edgeData, x, y, visitedMap) // 分析区域 if (regionPoints.length > 10) { // 小区域忽略 const [minX, minY, maxX, maxY] = this.getBoundingBox(regionPoints) const regionWidth = maxX - minX + 1 const regionHeight = maxY - minY + 1 // 检查区域大小和纹理特征 if ( regionWidth > 5 && regionHeight > 5 && regionWidth < width * 0.2 && regionHeight < height * 0.2 ) { // 计算区域密度 const density = regionPoints.length / (regionWidth * regionHeight) // 检查并添加符合微缩文字特征的区域 if (density > 0.1 && density < 0.5) { // 合适的密度范围 regions.push({ x: minX, y: minY, w: regionWidth, h: regionHeight, }) } } } } } } return { count: regions.length, regions } } /** * 深度优先搜索连通的边缘区域 */ private floodFillEdge( edgeData: ImageData, startX: number, startY: number, visitedMap: boolean[] ): Array<{ x: number; y: number }> { const { data, width, height } = edgeData const stack: Array<{ x: number; y: number }> = [] const points: Array<{ x: number; y: number }> = [] const dx = [-1, 0, 1, -1, 1, -1, 0, 1] const dy = [-1, -1, -1, 0, 0, 1, 1, 1] // 起始点 stack.push({ x: startX, y: startY }) visitedMap[startY * width + startX] = true while (stack.length > 0) { const { x, y } = stack.pop()! points.push({ x, y }) // 检查88个相邻方向 for (let i = 0; i < 8; i++) { const nx = x + dx[i] const ny = y + dy[i] if (nx >= 0 && nx < width && ny >= 0 && ny < height) { const nidx = ny * width + nx const ni = nidx * 4 if (data[ni] > 200 && !visitedMap[nidx]) { stack.push({ x: nx, y: ny }) visitedMap[nidx] = true } } } } return points } /** * 获取点集的外接矩形 */ private getBoundingBox( points: Array<{ x: number; y: number }> ): [number, number, number, number] { let minX = Number.MAX_SAFE_INTEGER let minY = Number.MAX_SAFE_INTEGER let maxX = 0 let maxY = 0 for (const { x, y } of points) { minX = Math.min(minX, x) minY = Math.min(minY, y) maxX = Math.max(maxX, x) maxY = Math.max(maxY, y) } return [minX, minY, maxX, maxY] } /** * 检测光变图案 * * @param imageData 图像数据 * @returns [特征名称, 是否检测到, 置信度] * @private */ private async detectOpticalVariable( imageData: ImageData ): Promise<[string, boolean, number]> { // 提取特定区域并分析颜色变化 // 在实际实现中需要定位光变图案区域并分析其特征 // 这里使用模拟实现 // 模拟检测: 65%的概率检测到,置信度0.6-0.9 const detected = Math.random() > 0.35 const confidence = detected ? 0.6 + Math.random() * 0.3 : 0 return ["光变图案", detected, confidence] } /** * 检测凹印雕刻特征 * * @param imageData 图像数据 * @returns [特征名称, 是否检测到, 置信度] * @private */ private async detectIntaglioPrinting( imageData: ImageData ): Promise<[string, boolean, number]> { // 使用特定滤镜增强凹印效果 // 在实际实现中应分析阴影和纹理模式 // 这里使用模拟实现 // 模拟检测: 75%的概率检测到,置信度0.65-0.9 const detected = Math.random() > 0.25 const confidence = detected ? 0.65 + Math.random() * 0.25 : 0 return ["雕刻凹印", detected, confidence] } /** * 检测隐形图案(幽灵图像) * * @param imageData 图像数据 * @returns [特征名称, 是否检测到, 置信度] * @private */ private async detectGhostImage( imageData: ImageData ): Promise<[string, boolean, number]> { // 调整对比度和亮度显现隐形图案 // 在实际实现中应使用特定滤镜和图像处理算法 // 这里使用模拟实现 // 模拟检测: 60%的概率检测到,置信度0.55-0.85 const detected = Math.random() > 0.4 const confidence = detected ? 0.55 + Math.random() * 0.3 : 0 return ["隐形图案", detected, confidence] } /** * 清除结果缓存 */ clearCache(): void { this.resultCache.clear() this.options.logger("防伪检测结果缓存已清除") } /** * 释放资源 */ dispose(): void { this.resultCache.clear() } }