UNPKG

id-scanner-lib

Version:

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

537 lines (468 loc) 15.9 kB
/** * @file 身份证检测器 * @description 提供身份证检测和解析功能 * @module modules/id-card/id-card-detector */ import { EventEmitter } from '../../core/event-emitter'; import { Logger } from '../../core/logger'; import { Result } from '../../core/result'; import { IDCardType, IDCardEdge, IDCardInfo } from './types'; /** * 身份证检测器配置选项 */ export interface IDCardDetectorOptions { /** 是否启用 */ enabled?: boolean; /** 最小置信度 */ minConfidence?: number; /** 是否检测身份证类型 */ detectType?: boolean; /** 是否检测边缘 */ detectEdge?: boolean; /** 是否启用边缘检测(用于更精确的边缘检测) */ enableEdgeDetection?: boolean; /** 是否启用OCR识别 */ enableOCR?: boolean; /** 是否裁剪并校正图像 */ cropAndAlign?: boolean; /** 是否启用防伪检测 */ enableAntiFake?: boolean; /** 是否返回原始图像 */ returnImage?: boolean; /** 模型路径 */ modelPath?: string; } /** * 图像处理配置选项 */ export interface ImageProcessOptions { /** 是否进行预处理 */ preprocess?: boolean; /** 是否校正图像 */ correctPerspective?: boolean; /** 是否增强图像 */ enhance?: boolean; /** 是否去噪 */ denoise?: boolean; /** 是否二值化 */ binarize?: boolean; } /** * 身份证检测器类 */ export class IDCardDetector extends EventEmitter { private options: IDCardDetectorOptions; private logger: Logger; private initialized: boolean = false; private models: { detection?: any; ocr?: any; antiFake?: any; } = {}; /** 重用的 Canvas 元素,用于减少内存分配 */ private reusableCanvas: HTMLCanvasElement | null = null; private reusableContext: CanvasRenderingContext2D | null = null; /** * 构造函数 * @param options 配置选项 */ constructor(options: IDCardDetectorOptions = {}) { super(); this.options = { enabled: true, minConfidence: 0.7, detectType: true, detectEdge: true, enableEdgeDetection: false, enableOCR: true, cropAndAlign: true, enableAntiFake: false, returnImage: false, modelPath: '/models/id-card', ...options }; this.logger = Logger.getInstance(); } /** * 初始化检测器 */ public async initialize(): Promise<void> { if (this.initialized || !this.options.enabled) { return; } this.logger.debug('IDCardDetector', '初始化身份证检测器'); try { // 加载检测模型 await this.loadDetectionModel(); // 如果启用OCR,加载OCR模型 if (this.options.enableOCR) { await this.loadOCRModel(); } // 如果启用防伪检测,加载防伪模型 if (this.options.enableAntiFake) { await this.loadAntiFakeModel(); } this.initialized = true; this.emit('detector:initialized', {}); this.logger.debug('IDCardDetector', '身份证检测器初始化完成'); } catch (error) { this.logger.error('IDCardDetector', '身份证检测器初始化失败', error as Error); throw error; } } /** * 加载检测模型 * @private */ private async loadDetectionModel(): Promise<void> { // 实际项目中,这里应该加载检测模型 this.logger.debug('IDCardDetector', '加载身份证检测模型'); // 模拟加载模型的延迟 await new Promise(resolve => setTimeout(resolve, 100)); // 设置模型 this.models.detection = { loaded: true, name: 'id-card-detection' }; } /** * 加载OCR模型 * @private */ private async loadOCRModel(): Promise<void> { // 实际项目中,这里应该加载OCR模型 this.logger.debug('IDCardDetector', '加载身份证OCR模型'); // 模拟加载模型的延迟 await new Promise(resolve => setTimeout(resolve, 100)); // 设置模型 this.models.ocr = { loaded: true, name: 'id-card-ocr' }; } /** * 加载防伪模型 * @private */ private async loadAntiFakeModel(): Promise<void> { // 实际项目中,这里应该加载防伪模型 this.logger.debug('IDCardDetector', '加载身份证防伪模型'); // 模拟加载模型的延迟 await new Promise(resolve => setTimeout(resolve, 100)); // 设置模型 this.models.antiFake = { loaded: true, name: 'id-card-anti-fake' }; } /** * 处理图像 * @param image 图像源(可以是ImageData、HTMLImageElement、HTMLCanvasElement等) * @param processOptions 图像处理选项 * @returns 处理结果 */ public async processImage( image: ImageData | HTMLImageElement | HTMLCanvasElement, processOptions: ImageProcessOptions = {} ): Promise<Result<IDCardInfo>> { if (!this.initialized) { return Result.failure(new Error('身份证检测器未初始化')); } // 输入验证 if (!image) { return Result.failure(new Error('图像源不能为空')); } // 验证 HTMLImageElement 是否已加载 if (image instanceof HTMLImageElement && !image.complete) { return Result.failure(new Error('图像尚未加载完成')); } // 验证 ImageData 尺寸 if (image instanceof ImageData) { if (image.width === 0 || image.height === 0) { return Result.failure(new Error('图像尺寸无效')); } } // 验证 Canvas 尺寸 if (image instanceof HTMLCanvasElement) { if (image.width === 0 || image.height === 0) { return Result.failure(new Error('Canvas尺寸无效')); } } try { this.logger.debug('IDCardDetector', '开始处理图像'); // 预处理图像 const processedImage = await this.preprocessImage(image, processOptions); // 检测身份证 const detectionResult = await this.detectIDCard(processedImage); if (!detectionResult || detectionResult.confidence < (this.options.minConfidence || 0.7)) { return Result.failure(new Error('未检测到身份证或置信度过低')); } let idCardInfo: IDCardInfo = { type: detectionResult.type, edge: detectionResult.edge, confidence: detectionResult.confidence }; // 如果启用OCR识别,提取文字信息 if (this.options.enableOCR && this.models.ocr) { // 裁剪并校正图像 const alignedImage = this.options.cropAndAlign ? await this.cropAndAlign(processedImage, detectionResult.edge) : processedImage; // 识别文字 const ocrResult = await this.recognizeText(alignedImage, detectionResult.type); // 合并结果 idCardInfo = { ...idCardInfo, ...ocrResult }; } // 如果启用防伪检测,进行防伪检测 if (this.options.enableAntiFake && this.models.antiFake) { const antiFakeResult = await this.detectAntiFake(processedImage, detectionResult); idCardInfo.antiFake = antiFakeResult; } // 如果需要返回原始图像 if (this.options.returnImage) { // 根据图像类型获取ImageData if (image instanceof ImageData) { idCardInfo.image = image; } else if (image instanceof HTMLCanvasElement) { const context = image.getContext('2d'); if (context) { idCardInfo.image = context.getImageData(0, 0, image.width, image.height); } } else if (image instanceof HTMLImageElement && image.complete) { const canvas = this.getReusableCanvas(image.naturalWidth, image.naturalHeight); const context = canvas.getContext('2d'); if (context) { context.drawImage(image, 0, 0); idCardInfo.image = context.getImageData(0, 0, canvas.width, canvas.height); } } } this.logger.debug('IDCardDetector', '图像处理完成'); this.emit('detector:result', { result: idCardInfo }); return Result.success(idCardInfo); } catch (error) { this.logger.error('IDCardDetector', '图像处理失败', error as Error); return Result.failure(error as Error); } } /** * 获取可重用的 Canvas 元素 * @param width 宽度 * @param height 高度 * @returns CanvasRenderingContext2D */ private getReusableCanvas(width: number, height: number): HTMLCanvasElement { // 如果存在可重用的 canvas 且尺寸匹配,直接返回 if (this.reusableCanvas && this.reusableCanvas.width === width && this.reusableCanvas.height === height) { return this.reusableCanvas; } // 创建新的 canvas this.reusableCanvas = document.createElement('canvas'); this.reusableCanvas.width = width; this.reusableCanvas.height = height; this.reusableContext = this.reusableCanvas.getContext('2d'); return this.reusableCanvas; } /** * 预处理图像 * @param image 图像源 * @param options 处理选项 * @returns 处理后的图像 * @private */ private async preprocessImage( image: ImageData | HTMLImageElement | HTMLCanvasElement, options: ImageProcessOptions ): Promise<ImageData> { this.logger.debug('IDCardDetector', '预处理图像'); // 创建ImageData对象 let imageData: ImageData; if (image instanceof ImageData) { imageData = image; } else { const width = image instanceof HTMLImageElement ? image.naturalWidth : image.width; const height = image instanceof HTMLImageElement ? image.naturalHeight : image.height; const canvas = this.getReusableCanvas(width, height); const context = canvas.getContext('2d'); if (!context) { throw new Error('无法获取Canvas上下文'); } if (image instanceof HTMLImageElement) { context.drawImage(image, 0, 0); } else { context.drawImage(image, 0, 0); } imageData = context.getImageData(0, 0, width, height); } // 应用图像处理选项 // 实际项目中,这里应该根据options进行相应的图像处理 return imageData; } /** * 检测身份证 * @param image 图像数据 * @returns 检测结果 * @private */ private async detectIDCard(image: ImageData): Promise<{ type: IDCardType; edge: IDCardEdge; confidence: number; } | null> { // 实际项目中,这里应该调用模型进行身份证检测 this.logger.debug('IDCardDetector', '检测身份证'); // 模拟检测结果 // 在实际应用中,这里应该使用机器学习模型进行推理 return { type: IDCardType.FRONT, edge: { topLeft: { x: 10, y: 10 }, topRight: { x: image.width - 10, y: 10 }, bottomRight: { x: image.width - 10, y: image.height - 10 }, bottomLeft: { x: 10, y: image.height - 10 } }, confidence: 0.95 }; } /** * 裁剪并校正图像 * @param image 图像数据 * @param edge 边缘信息 * @returns 校正后的图像 * @private */ private async cropAndAlign(image: ImageData, edge: IDCardEdge): Promise<ImageData> { this.logger.debug('IDCardDetector', '裁剪并校正图像'); // 设置标准身份证尺寸比例 const standardWidth = 428; const standardHeight = 270; const canvas = this.getReusableCanvas(standardWidth, standardHeight); const context = canvas.getContext('2d'); if (!context) { throw new Error('无法获取Canvas上下文'); } // 创建临时Canvas用于源图像 const tempCanvas = document.createElement('canvas'); tempCanvas.width = image.width; tempCanvas.height = image.height; const tempContext = tempCanvas.getContext('2d'); if (!tempContext) { throw new Error('无法获取临时Canvas上下文'); } // 将ImageData绘制到临时Canvas tempContext.putImageData(image, 0, 0); // 在实际应用中,这里应该使用透视变换算法 // 例如使用Canvas的transform或WebGL进行变换 // 简化处理:直接裁剪 context.drawImage( tempCanvas, edge.topLeft.x, edge.topLeft.y, edge.topRight.x - edge.topLeft.x, edge.bottomLeft.y - edge.topLeft.y, 0, 0, standardWidth, standardHeight ); return context.getImageData(0, 0, standardWidth, standardHeight); } /** * 识别文字 * * @note 此方法返回模拟数据,用于框架开发和测试 * 实际使用时需要替换为真实的 OCR 模型集成 * * @param image 图像数据 * @param type 身份证类型 * @returns 识别结果 * @private */ private async recognizeText(image: ImageData, type: IDCardType): Promise<Partial<IDCardInfo>> { this.logger.debug('IDCardDetector', '识别文字'); // 模拟OCR结果 // 注意:这是框架的占位实现,真实场景需要接入实际的 OCR 服务 // 可选的方案包括: // - TensorFlow.js + 自定义 OCR 模型 // - 第三方 OCR API (如百度OCR、腾讯OCR) // - Tesseract.js WASM 版本 // if (type === IDCardType.FRONT) { return { name: '张三', // TODO: 替换为真实OCR结果 gender: '男', // TODO: 替换为真实OCR结果 ethnicity: '汉', // TODO: 替换为真实OCR结果 birthDate: '1990-01-01', // TODO: 替换为真实OCR结果 address: '北京市朝阳区某某街道某某社区1号楼1单元101', // TODO: 替换为真实OCR结果 idNumber: '110101199001010001', // TODO: 替换为真实OCR结果 photoRegion: { x: 300, y: 40, width: 100, height: 130 } }; } else if (type === IDCardType.BACK) { return { issueAuthority: '北京市公安局朝阳分局', // TODO: 替换为真实OCR结果 validFrom: '2015-01-01', // TODO: 替换为真实OCR结果 validTo: '2035-01-01' // TODO: 替换为真实OCR结果 }; } return {}; } /** * 检测防伪特征 * * @note 此方法返回模拟数据,用于框架开发和测试 * 实际使用时需要替换为真实的防伪检测模型 * * @param image 图像数据 * @param detectionResult 检测结果 * @returns 防伪检测结果 * @private */ private async detectAntiFake( image: ImageData, detectionResult: { type: IDCardType; edge: IDCardEdge; confidence: number } ): Promise<IDCardInfo['antiFake']> { this.logger.debug('IDCardDetector', '检测防伪特征'); // 模拟防伪检测结果 // 注意:这是框架的占位实现,真实场景需要接入实际的防伪检测模型 // 可选的方案包括: // - 紫外光特征检测 // - 红外光特征检测 // - 微缩文字检测 // - 光学变色特征检测 return { passed: true, score: 0.92, features: { fluorescent: true, // TODO: 替换为真实检测结果 microtext: true, // TODO: 替换为真实检测结果 opticalVariable: true, // TODO: 替换为真实检测结果 texture: true, // TODO: 替换为真实检测结果 watermark: true // TODO: 替换为真实检测结果 } }; } /** * 释放资源 */ public dispose(): void { this.logger.debug('IDCardDetector', '释放资源'); // 清理模型 this.models = {}; this.initialized = false; // 清理可重用的 Canvas this.reusableCanvas = null; this.reusableContext = null; // 清理事件监听 this.removeAllListeners(); } }