id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
736 lines (622 loc) • 23.1 kB
text/typescript
/* eslint-disable */
/**
* @file 人脸检测模块
* @description 提供人脸检测、跟踪和分析功能
* @module modules/face/face-detector
*/
import * as tf from '@tensorflow/tfjs';
import * as faceapi from '@vladmandic/face-api';
import { BaseScannerModule, ModuleCapabilities, ModuleEvent, ModuleInitOptions, ModuleStatus, ModuleType } from '../../interfaces/scanner-module';
import { FaceDetectionOptions, FaceDetectionResult, LivenessDetectionType, Rect } from '../../interfaces/face-detection';
import { ConfigManager } from '../../core/config';
import { Logger } from '../../core/logger';
import { ResourceManager } from '../../core/resource-manager';
import { CameraManager, CameraEvent } from '../../core/camera-manager';
import { Result } from '../../core/result';
import { FaceDetectionError, FaceComparisonError, InitializationError, LivenessDetectionError, ResourceLoadError } from '../../core/errors';
import { generateUUID } from '../../utils';
import { FaceModelLoader } from './face-model-loader';
import { FaceTracker } from './face-tracker';
import { FaceComparator } from './face-comparator';
import { FaceResultConverter } from './face-result-converter';
import { FaceDetectorOptionsFactory } from './face-detector-options';
/**
* 人脸检测模型类型
*/
export enum FaceModelType {
/** SSD MobileNet V1 模型 */
SSD_MOBILENET = 'ssd_mobilenetv1',
/** Tiny Face 模型 */
TINY_FACE = 'tiny_face',
/** MTCNN 模型 */
MTCNN = 'mtcnn',
/** BlazeFace 模型 */
BLAZE_FACE = 'blazeface'
}
/**
* 68点人脸关键点索引 (face-api 68-point model)
* 参考: https://github.com/justadudewhohacks/face-api.js/issues/175
*/
const FaceLandmarkIndex = {
LEFT_EYE: 36,
RIGHT_EYE: 45,
NOSE: 30,
MOUTH: 48, // 上唇中心
LEFT_EYE_CORNER: 36,
RIGHT_EYE_CORNER: 45,
NOSE_TIP: 30,
MOUTH_CENTER: 57,
} as const;
/**
* 人脸检测模块配置
*/
export interface FaceDetectorConfig {
/** 是否启用 */
enabled: boolean;
/** 检测模型类型 */
detectionModel: FaceModelType;
/** 置信度阈值 */
minConfidence: number;
/** 最大检测人脸数 */
maxFaces: number;
/** 是否检测关键点 */
detectLandmarks: boolean;
/** 关键点模型类型 */
landmarksModel: 'tiny' | '68_points';
/** 是否检测表情 */
detectExpressions: boolean;
/** 是否检测年龄和性别 */
detectAgeGender: boolean;
/** 是否提取人脸特征向量 */
extractEmbeddings: boolean;
/** 人脸匹配阈值(0-1) */
matchThreshold: number;
/** 是否启用跟踪 */
enableTracking: boolean;
/** 活体检测类型 */
livenessDetection: LivenessDetectionType | 'none';
/** 模型路径 */
modelPath: string;
}
/**
* 人脸检测模块
*/
export class FaceDetector extends BaseScannerModule {
/** 模块类型 */
readonly type: ModuleType = ModuleType.FACE;
/** 模块配置 */
protected config: FaceDetectorConfig;
/** 默认配置 */
private static readonly DEFAULT_CONFIG: FaceDetectorConfig = {
enabled: true,
detectionModel: FaceModelType.SSD_MOBILENET,
minConfidence: 0.5,
maxFaces: 10,
detectLandmarks: true,
landmarksModel: 'tiny',
detectExpressions: false,
detectAgeGender: false,
extractEmbeddings: false,
matchThreshold: 0.6,
enableTracking: false,
livenessDetection: 'none',
modelPath: '/models/face-api'
};
/** 模型加载器 */
private modelLoader: FaceModelLoader;
/** 人脸跟踪器 */
private faceTracker: FaceTracker;
/** 人脸比对器 */
private faceComparator: FaceComparator;
/** 结果转换器 */
private resultConverter: FaceResultConverter;
/** 处理计时器ID */
private processingTimerId: number | null = null;
/** 处理间隔(ms) */
private processingInterval: number = 100;
/** 摄像头管理器 */
private cameraManager: CameraManager;
/** 配置管理器 */
private configManager: ConfigManager;
/** 资源管理器 */
private resourceManager: ResourceManager;
/** 日志记录器 */
private logger: Logger;
/** 画布元素,用于处理帧 */
private canvas: HTMLCanvasElement;
/** 画布渲染上下文 */
private canvasCtx: CanvasRenderingContext2D | null = null;
/** 最后一次检测结果 */
private lastDetectionResult: FaceDetectionResult[] = [];
/**
* 构造函数
* @param config 初始配置
*/
constructor(config: Partial<FaceDetectorConfig> = {}) {
super({
enabled: true,
...config
});
this.configManager = ConfigManager.getInstance();
this.cameraManager = CameraManager.getInstance();
this.resourceManager = ResourceManager.getInstance();
this.logger = Logger.getInstance();
// 合并配置
this.config = {
...FaceDetector.DEFAULT_CONFIG,
...config
};
// 初始化组件
this.modelLoader = new FaceModelLoader({
modelPath: this.config.modelPath,
detectionModel: this.config.detectionModel,
landmarksModel: this.config.landmarksModel,
detectLandmarks: this.config.detectLandmarks,
detectExpressions: this.config.detectExpressions,
detectAgeGender: this.config.detectAgeGender,
extractEmbeddings: this.config.extractEmbeddings
});
this.faceTracker = new FaceTracker({ trackTimeout: 1000 });
this.faceComparator = new FaceComparator({ matchThreshold: this.config.matchThreshold });
this.resultConverter = new FaceResultConverter({
detectLandmarks: this.config.detectLandmarks,
landmarksModel: this.config.landmarksModel,
detectExpressions: this.config.detectExpressions,
detectAgeGender: this.config.detectAgeGender,
extractEmbeddings: this.config.extractEmbeddings
});
// 创建画布
this.canvas = document.createElement('canvas');
this.canvasCtx = this.canvas.getContext('2d');
}
/**
* 获取模块能力
*/
get capabilities(): ModuleCapabilities {
return {
supportsVideo: true,
supportsImage: true,
supportsBatch: false,
supportsRealtime: true,
supportsWebWorker: false,
supportedMediaTypes: ['image/jpeg', 'image/png', 'image/webp']
};
}
/**
* 初始化模块
* @param options 初始化选项
*/
async initialize(options?: ModuleInitOptions): Promise<void> {
if (this._status === ModuleStatus.INITIALIZING) {
throw new Error('人脸检测模块正在初始化中');
}
if (this._status === ModuleStatus.READY) {
this.logger.debug('FaceDetector', '人脸检测模块已初始化');
return;
}
this.setStatus(ModuleStatus.INITIALIZING);
this.emit(ModuleEvent.INIT_START);
try {
// 应用配置选项
if (options?.config) {
this.updateConfig(options.config);
}
// 设置调试模式
if (options?.debug !== undefined) {
this.debug = options.debug;
}
const modelPath = options?.modelPath || this.config.modelPath;
// 加载模型
this.logger.info('FaceDetector', `正在加载人脸检测模型,路径:${modelPath}`);
// 设置模型路径
faceapi.env.monkeyPatch({
Canvas: HTMLCanvasElement,
Image: HTMLImageElement,
ImageData: ImageData,
Video: HTMLVideoElement,
createCanvasElement: () => document.createElement('canvas'),
createImageElement: () => document.createElement('img')
});
// 确保TensorFlow.js已初始化
await tf.ready();
// 设置模型路径并加载模型
await this.loadModels(modelPath);
// 绑定摄像头事件
if (options?.bindCamera) {
this.cameraManager.on(CameraEvent.FRAME, this.handleCameraFrame.bind(this));
}
this.setStatus(ModuleStatus.READY);
this.emit(ModuleEvent.INIT_COMPLETE);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('FaceDetector', `初始化失败: ${errorMessage}`, error as Error);
this.setStatus(ModuleStatus.ERROR);
this.emit(ModuleEvent.INIT_ERROR, { error });
throw new Error(`人脸检测模块初始化失败: ${errorMessage}`);
}
}
// ==================== 模型加载(委托给 FaceModelLoader) ====================
/**
* 懒加载模型 - 委托给 FaceModelLoader
* @deprecated 使用 modelLoader.lazyLoadModel 代替
*/
private async lazyLoadModel(modelType: string, modelPath: string): Promise<void> {
await this.modelLoader.lazyLoadModel(modelType, modelPath);
}
/**
* 根据需求加载模型 - 委托给 FaceModelLoader
* @deprecated 使用 modelLoader.loadModelsOnDemand 代替
*/
private async loadModelsOnDemand(options: FaceDetectionOptions, modelPath: string): Promise<void> {
await this.modelLoader.loadModelsOnDemand({
withLandmarks: options.withLandmarks,
withAttributes: options.withAttributes,
withEmbedding: options.withEmbedding
});
}
/**
* 加载人脸检测模型 - 委托给 FaceModelLoader
* @deprecated 使用 modelLoader.ensureModelsLoaded 代替
*/
private async loadModels(modelPath: string): Promise<void> {
await this.modelLoader.ensureModelsLoaded();
}
/**
* 处理图片
* @param image 图片源
* @param options 处理选项
*/
async processImage(
image: string | HTMLImageElement | HTMLCanvasElement | ImageData,
options: FaceDetectionOptions = {}
): Promise<Result<FaceDetectionResult[]>> {
this.checkInitialized();
if (this._status === ModuleStatus.PROCESSING) {
return Result.failure(new FaceDetectionError('另一个处理操作正在进行中'));
}
this.setStatus(ModuleStatus.PROCESSING);
this.emit(ModuleEvent.PROCESS_START);
try {
// 懒加载所需的模型
const modelPath = this.config.modelPath || '/models';
await this.loadModelsOnDemand(options, modelPath);
// 合并选项和配置
const processOptions: FaceDetectionOptions = {
minConfidence: this.config.minConfidence,
maxFaces: this.config.maxFaces,
withLandmarks: this.config.detectLandmarks,
withAttributes: this.config.detectExpressions || this.config.detectAgeGender,
withEmbedding: this.config.extractEmbeddings,
...options
};
// 加载图片
let imgElement: HTMLImageElement | HTMLCanvasElement;
if (typeof image === 'string') {
imgElement = await this.loadImage(image);
} else if (image instanceof HTMLImageElement || image instanceof HTMLCanvasElement) {
imgElement = image;
} else if (image instanceof ImageData) {
// 将ImageData转换为Canvas
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx?.putImageData(image, 0, 0);
imgElement = canvas;
} else {
throw new FaceDetectionError('不支持的图像格式');
}
// 开始计时
const startTime = Date.now();
// 执行人脸检测
const results = await this.detectFaces(imgElement, processOptions);
// 计算处理时间
const processingTime = Date.now() - startTime;
this.setStatus(ModuleStatus.READY);
this.emit(ModuleEvent.PROCESS_COMPLETE, { results, processingTime });
return Result.success(results);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('FaceDetector', `图片处理失败: ${errorMessage}`, error as Error);
this.setStatus(ModuleStatus.ERROR);
this.emit(ModuleEvent.PROCESS_ERROR, { error });
return Result.failure(new FaceDetectionError(`图片处理失败: ${errorMessage}`));
}
}
/**
* 开始实时处理
* @param videoElement 视频元素
* @param options 处理选项
*/
async startRealtime(
videoElement?: HTMLVideoElement,
options: FaceDetectionOptions = {}
): Promise<Result<boolean>> {
this.checkInitialized();
if (this._status === ModuleStatus.PROCESSING) {
return Result.failure(new FaceDetectionError('实时处理已在进行中'));
}
try {
// 停止现有处理
this.stopRealtime();
// 获取视频元素
const video = videoElement || this.cameraManager.getVideoElement();
if (!video) {
throw new FaceDetectionError('未提供视频元素且摄像头未初始化');
}
// 如果视频未播放,尝试启动摄像头
if (!this.cameraManager.isActive() && !videoElement) {
const cameraResult = await this.cameraManager.init({ autoStart: true });
if (!cameraResult.isSuccess()) {
throw new Error('无法启动摄像头');
}
}
// 设置处理间隔
this.processingInterval = options.processingInterval || 100;
// 设置状态
this.setStatus(ModuleStatus.PROCESSING);
// 启动处理循环
this.processingTimerId = window.setInterval(() => {
this.processVideoFrame(video, options);
}, this.processingInterval);
return Result.success(true);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('FaceDetector', `启动实时处理失败: ${errorMessage}`, error as Error);
this.setStatus(ModuleStatus.ERROR);
return Result.failure(new FaceDetectionError(`启动实时处理失败: ${errorMessage}`));
}
}
/**
* 停止实时处理
*/
stopRealtime(): void {
if (this.processingTimerId !== null) {
window.clearInterval(this.processingTimerId);
this.processingTimerId = null;
}
if (this._status === ModuleStatus.PROCESSING) {
this.setStatus(ModuleStatus.READY);
}
// 清除人脸跟踪状态
this.faceTracker.reset();
this.lastDetectionResult = [];
}
/**
* 释放资源
*/
async dispose(): Promise<void> {
// 停止实时处理
this.stopRealtime();
// 释放模型(通过 FaceModelLoader)
await this.modelLoader.dispose();
// 重置跟踪器
this.faceTracker.reset();
// 移除事件监听
this.cameraManager.off(CameraEvent.FRAME, this.handleCameraFrame.bind(this));
this._status = ModuleStatus.NOT_INITIALIZED;
}
/**
* 加载图片
* @param src 图片URL
*/
private async loadImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`无法加载图片: ${src}`));
img.src = src;
});
}
/**
* 处理视频帧
* @param video 视频元素
* @param options 处理选项
*/
private async processVideoFrame(
video: HTMLVideoElement,
options: FaceDetectionOptions = {}
): Promise<void> {
if (this._status !== ModuleStatus.PROCESSING || !video || video.paused || video.ended) {
return;
}
try {
// 检查视频是否准备好
if (video.readyState < 2) { // HAVE_CURRENT_DATA
return;
}
// 检查视频尺寸
if (video.videoWidth === 0 || video.videoHeight === 0) {
return;
}
// 调整画布大小
if (this.canvas.width !== video.videoWidth || this.canvas.height !== video.videoHeight) {
this.canvas.width = video.videoWidth;
this.canvas.height = video.videoHeight;
}
// 将视频帧绘制到画布
if (this.canvasCtx) {
this.canvasCtx.drawImage(video, 0, 0);
}
// 执行人脸检测
const startTime = Date.now();
const results = await this.detectFaces(video, options);
const processingTime = Date.now() - startTime;
// 更新最后的检测结果
this.lastDetectionResult = results;
// 发出实时结果事件
this.emit(ModuleEvent.REALTIME_RESULT, {
results,
processingTime,
timestamp: Date.now()
});
} catch (error) {
this.logger.error('FaceDetector', `处理视频帧失败: ${error}`);
}
}
/**
* 处理摄像头帧
*/
private handleCameraFrame(event: any): void {
if (this._status !== ModuleStatus.PROCESSING || !event.frameData) {
return;
}
const { frameData } = event;
// 调整画布大小
if (this.canvas.width !== frameData.width || this.canvas.height !== frameData.height) {
this.canvas.width = frameData.width;
this.canvas.height = frameData.height;
}
// 将帧数据绘制到画布
if (this.canvasCtx) {
this.canvasCtx.putImageData(frameData, 0, 0);
// 执行人脸检测
this.detectFaces(this.canvas).then(results => {
// 更新最后的检测结果
this.lastDetectionResult = results;
// 发出实时结果事件
this.emit(ModuleEvent.REALTIME_RESULT, {
results,
timestamp: Date.now()
});
}).catch(error => {
this.logger.error('FaceDetector', `处理摄像头帧失败: ${error}`);
});
}
}
/**
* 执行人脸检测
* @param input 输入图像
* @param options 检测选项
*/
private async detectFaces(
input: HTMLImageElement | HTMLCanvasElement | HTMLVideoElement,
options: FaceDetectionOptions = {}
): Promise<FaceDetectionResult[]> {
try {
// 检查模型是否已加载
if (!this.modelLoader.isModelsLoaded()) {
throw new FaceDetectionError('人脸检测模型尚未加载');
}
// 合并选项和配置
const detectOptions = FaceDetectorOptionsFactory.mergeOptions(this.config, options);
// 创建 face-api 检测选项
const faceapiOptions = FaceDetectorOptionsFactory.createFaceAPIOptions(
this.config.detectionModel,
detectOptions.minConfidence ?? 0.5
);
// 进行检测
const startTime = Date.now();
const detections = await FaceDetectorOptionsFactory.detect(
input,
faceapiOptions,
detectOptions,
this.config.landmarksModel
);
// 限制检测数量
const maxFaces = detectOptions.maxFaces || this.config.maxFaces;
const detectionsArray: any[] = Array.isArray(detections) ? detections : [detections];
if (detectionsArray.length > maxFaces) {
detectionsArray.length = maxFaces;
}
// 将结果转换为标准格式
const processingTime = Date.now() - startTime;
// 临时更新转换器配置
this.resultConverter.updateConfig({
detectLandmarks: !!detectOptions.withLandmarks,
detectExpressions: !!detectOptions.withAttributes,
detectAgeGender: !!detectOptions.withAttributes,
extractEmbeddings: !!detectOptions.withEmbedding
});
const results = this.resultConverter.convertBatch(detectionsArray, { maxFaces }, processingTime);
// 处理人脸跟踪(使用 FaceTracker)
if (detectOptions.enableTracking) {
const trackedResults = this.faceTracker.update(results);
for (let i = 0; i < trackedResults.length; i++) {
if (trackedResults[i].trackId) {
results[i].trackId = trackedResults[i].trackId;
}
}
}
return results;
} catch (error) {
this.logger.error('FaceDetector', `人脸检测失败: ${error}`);
throw new FaceDetectionError(`人脸检测失败: ${error}`);
}
}
/**
* 比对两个人脸
* @param source 源人脸
* @param target 目标人脸
*/
async compareFaces(
source: string | HTMLImageElement | FaceDetectionResult,
target: string | HTMLImageElement | FaceDetectionResult
): Promise<Result<{ similarity: number; isMatch: boolean; threshold: number }>> {
this.checkInitialized();
try {
// 获取源人脸的特征向量
let sourceEmbedding: number[];
if (typeof source === 'string' || source instanceof HTMLImageElement) {
// 处理图片源
const result = await this.processImage(source, { withEmbedding: true });
const resultData = result.getData();
if (!result.isSuccess() || !resultData || resultData.length === 0) {
throw new FaceComparisonError('无法从源图像检测人脸');
}
if (!resultData[0].embedding) {
throw new FaceComparisonError('源图像未提取特征向量');
}
sourceEmbedding = resultData[0].embedding.vector;
} else {
// 使用现有检测结果
if (!source.embedding || !source.embedding.vector) {
throw new FaceComparisonError('源人脸未提取特征向量');
}
sourceEmbedding = source.embedding.vector;
}
// 获取目标人脸的特征向量
let targetEmbedding: number[];
if (typeof target === 'string' || target instanceof HTMLImageElement) {
// 处理图片源
const result = await this.processImage(target, { withEmbedding: true });
const resultData = result.getData();
if (!result.isSuccess() || !resultData || resultData.length === 0) {
throw new FaceComparisonError('无法从目标图像检测人脸');
}
if (!resultData[0].embedding) {
throw new FaceComparisonError('目标图像未提取特征向量');
}
targetEmbedding = resultData[0].embedding.vector;
} else {
// 使用现有检测结果
if (!target.embedding || !target.embedding.vector) {
throw new FaceComparisonError('目标人脸未提取特征向量');
}
targetEmbedding = target.embedding.vector;
}
// 计算相似度(使用 FaceComparator)
const comparisonResult = this.faceComparator.compare(sourceEmbedding, targetEmbedding);
if (comparisonResult.isFailure()) {
throw comparisonResult.getError() || new FaceComparisonError('人脸比对失败');
}
const { similarity, isMatch, threshold } = comparisonResult.getData()!;
return Result.success({
similarity,
isMatch,
threshold
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('FaceDetector', `人脸比对失败: ${errorMessage}`, error as Error);
return Result.failure(new FaceComparisonError(`人脸比对失败: ${errorMessage}`));
}
}
/**
* 获取最近的检测结果
*/
getLatestResults(): FaceDetectionResult[] {
return [...this.lastDetectionResult];
}
}