id-scanner-lib
Version:
一款纯前端实现的TypeScript身份证&二维码识别库,无需后端支持,所有处理在浏览器端完成,新增图像批处理与优化
364 lines (323 loc) • 9.7 kB
text/typescript
/**
* @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()
}
}