id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
537 lines (468 loc) • 15.9 kB
text/typescript
/**
* @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();
}
}