UNPKG

id-scanner-lib

Version:

一款纯前端实现的TypeScript身份证&二维码识别库,无需后端支持,所有处理在浏览器端完成,新增图像批处理与优化

364 lines (323 loc) 9.7 kB
/** * @file 身份证检测模块 * @description 提供自动检测和定位图像中的身份证功能 * @module IDCardDetector */ import { Camera } from "../utils/camera" import { ImageProcessor } from "../utils/image-processing" import { DetectionResult } from "../utils/types" import { throttle, LRUCache, calculateImageFingerprint, } from "../utils/performance" import { Disposable } from "../utils/resource-manager" /** * IDCardDetector配置选项 */ export interface IDCardDetectorOptions { onDetection?: (result: DetectionResult) => void onError?: (error: Error) => void detectionInterval?: number maxImageDimension?: number enableCache?: boolean cacheSize?: number logger?: (message: any) => void } /** * 身份证检测器类 * * 通过图像处理和计算机视觉技术,实时检测视频流中的身份证,并提取身份证区域 * 注意:当前实现是简化版,实际项目中建议使用OpenCV.js进行更精确的检测 * * @example * ```typescript * // 创建身份证检测器 * const detector = new IDCardDetector((result) => { * if (result.success && result.croppedImage) { * console.log('检测到身份证!'); * // 对裁剪出的身份证图像进行处理 * processIDCardImage(result.croppedImage); * } * }); * * // 启动检测 * const videoElement = document.getElementById('video') as HTMLVideoElement; * await detector.start(videoElement); * * // 停止检测 * detector.stop(); * ``` */ export class IDCardDetector implements Disposable { private camera: Camera private detecting = false private detectTimer: number | null = null private onDetected?: (result: DetectionResult) => void private onError?: (error: Error) => void private detectionInterval: number private maxImageDimension: number private resultCache: LRUCache<string, DetectionResult> private throttledDetect: ReturnType<typeof throttle> private frameCount: number = 0 private lastDetectionTime: number = 0 private options: IDCardDetectorOptions /** * 创建身份证检测器实例 * * @param options 身份证检测器配置选项,或者检测回调函数 */ constructor( options?: IDCardDetectorOptions | ((result: DetectionResult) => void) ) { this.camera = new Camera() if (typeof options === "function") { // 兼容旧的构造函数方式 this.onDetected = options this.options = { detectionInterval: 200, maxImageDimension: 800, enableCache: true, cacheSize: 20, logger: console.log, } } else if (options) { // 使用新的选项对象方式 this.options = { detectionInterval: 200, maxImageDimension: 800, enableCache: true, cacheSize: 20, logger: console.log, ...options, } this.onDetected = options.onDetection this.onError = options.onError } else { this.options = { detectionInterval: 200, maxImageDimension: 800, enableCache: true, cacheSize: 20, logger: console.log, } } this.detectionInterval = this.options.detectionInterval! this.maxImageDimension = this.options.maxImageDimension! // 初始化结果缓存 this.resultCache = new LRUCache<string, DetectionResult>( this.options.cacheSize ) // 创建节流版本的检测函数 this.throttledDetect = throttle( this.performDetection.bind(this), this.detectionInterval ) } /** * 启动身份证检测 * * 初始化相机并开始连续检测视频帧中的身份证 * * @param {HTMLVideoElement} videoElement - 用于显示相机画面的video元素 * @returns {Promise<void>} 启动完成的Promise */ async start(videoElement: HTMLVideoElement): Promise<void> { await this.camera.initialize(videoElement) this.detecting = true this.frameCount = 0 this.lastDetectionTime = 0 this.detect() } /** * 停止身份证检测 */ stop(): void { this.detecting = false if (this.detectTimer !== null) { cancelAnimationFrame(this.detectTimer) this.detectTimer = null } } /** * 持续检测视频帧 * * @private */ private detect(): void { if (!this.detecting) return this.detectTimer = requestAnimationFrame(() => { try { this.frameCount++ const now = performance.now() // 帧率控制 - 只有满足时间间隔的帧才进行检测 // 这样可以显著减少CPU使用率,同时保持良好的用户体验 if ( this.frameCount % 3 === 0 || now - this.lastDetectionTime >= this.detectionInterval ) { this.throttledDetect() this.lastDetectionTime = now } // 继续下一帧检测 this.detect() } catch (error) { if (this.onError) { this.onError(error as Error) } else { console.error("身份证检测错误:", error) } // 出错后延迟重试 setTimeout(() => { if (this.detecting) { this.detect() } }, 1000) } }) } /** * 执行单帧检测 * * @private */ private async performDetection(): Promise<void> { if (!this.detecting || !this.camera) return // 获取当前视频帧 const frame = this.camera.captureFrame() if (!frame) return // 检查缓存 if (this.options.enableCache) { const fingerprint = calculateImageFingerprint(frame, 16) // 使用更大的尺寸提高特征区分度 const cachedResult = this.resultCache.get(fingerprint) if (cachedResult) { this.options.logger?.("使用缓存的检测结果") // 使用缓存结果,但更新图像数据以确保最新 const updatedResult = { ...cachedResult, imageData: frame, } if (this.onDetected) { this.onDetected(updatedResult) } return } } // 降低分辨率以提高性能 const downsampledFrame = ImageProcessor.resizeImage( frame, this.maxImageDimension, this.maxImageDimension ) try { // 检测身份证 const result = await this.detectIDCard(downsampledFrame) // 如果检测成功,将原始图像添加到结果中 if (result.success) { result.imageData = frame // 缓存结果 if (this.options.enableCache) { const fingerprint = calculateImageFingerprint(frame, 16) this.resultCache.set(fingerprint, result) } } // 处理检测结果 if (this.onDetected) { this.onDetected(result) } } catch (error) { if (this.onError) { this.onError(error as Error) } else { console.error("身份证检测错误:", error) } } } /** * 检测图像中的身份证 * * @private * @param {ImageData} imageData - 要分析的图像数据 * @returns {Promise<DetectionResult>} 检测结果 */ private async detectIDCard(imageData: ImageData): Promise<DetectionResult> { // 1. 图像预处理 const grayscale = ImageProcessor.toGrayscale(imageData) // 2. 检测矩形和边缘(简化版实现) // 注意:实际应用中应使用OpenCV.js或其他计算机视觉库进行更精确的检测 // 此处仅作为概念性实现,使用基本矩形检测逻辑 // 模拟检测过程,随机判断是否找到身份证 // 在实际应用中,此处应当实现实际的计算机视觉算法 const detectionResult: DetectionResult = { success: Math.random() > 0.3, // 70%的概率成功检测到 message: "身份证检测完成", } if (detectionResult.success) { // 模拟一个身份证矩形区域 const width = imageData.width const height = imageData.height // 大致的身份证区域(按比例) const rectWidth = Math.round(width * 0.7) const rectHeight = Math.round(rectWidth * 0.618) // 身份证是黄金比例 const rectX = Math.round((width - rectWidth) / 2) const rectY = Math.round((height - rectHeight) / 2) // 添加四个角点 detectionResult.corners = [ { x: rectX, y: rectY }, { x: rectX + rectWidth, y: rectY }, { x: rectX + rectWidth, y: rectY + rectHeight }, { x: rectX, y: rectY + rectHeight }, ] // 添加边界框 detectionResult.boundingBox = { x: rectX, y: rectY, width: rectWidth, height: rectHeight, } // 裁剪身份证图像 const canvas = document.createElement("canvas") canvas.width = rectWidth canvas.height = rectHeight const ctx = canvas.getContext("2d") if (ctx) { const tempCanvas = ImageProcessor.imageDataToCanvas(imageData) ctx.drawImage( tempCanvas, rectX, rectY, rectWidth, rectHeight, 0, 0, rectWidth, rectHeight ) detectionResult.croppedImage = ctx.getImageData( 0, 0, rectWidth, rectHeight ) } // 设置置信度 detectionResult.confidence = 0.7 + Math.random() * 0.3 } return detectionResult } /** * 清除检测结果缓存 */ clearCache(): void { this.resultCache.clear() this.options.logger?.("检测结果缓存已清除") } /** * 释放资源 */ dispose(): void { this.stop() this.camera.release() this.resultCache.clear() } }