UNPKG

id-scanner-lib

Version:

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

226 lines (203 loc) 5.87 kB
/** * @file 人脸检测结果转换器 * @description 将 face-api 检测结果转换为标准格式 * @module modules/face/face-result-converter */ import { FaceDetectionResult, Rect } from '../../interfaces/face-detection'; import { generateUUID } from '../../utils'; /** * 人脸关键点索引(对应 face-api 的 68 点模型) */ export const FaceLandmarkIndex = { LEFT_EYE: 36, RIGHT_EYE: 45, NOSE: 30, MOUTH_CENTER: 57 } as const; /** * face-api 原始检测结果 */ export interface RawFaceDetection { /** 检测框 */ detection?: { box?: { x?: number; y?: number; width?: number; height?: number }; score?: number; }; /** 关键点 */ landmarks?: { positions: Array<{ x: number; y: number }>; }; /** 表情 */ expressions?: { angry?: number; disgusted?: number; fearful?: number; happy?: number; neutral?: number; sad?: number; surprised?: number; }; /** 年龄 */ age?: number; /** 性别 */ gender?: string; /** 性别概率 */ genderProbability?: number; /** 人脸描述符(特征向量) */ descriptor?: Float32Array | number[]; } /** * 结果转换器配置 */ export interface ResultConverterConfig { /** 是否检测关键点 */ detectLandmarks: boolean; /** 关键点模型类型 */ landmarksModel: 'tiny' | '68_points'; /** 是否检测表情 */ detectExpressions: boolean; /** 是否检测年龄和性别 */ detectAgeGender: boolean; /** 是否提取特征向量 */ extractEmbeddings: boolean; } /** * 人脸检测结果转换器 * * 负责将 face-api 原始检测结果转换为标准格式 */ export class FaceResultConverter { /** 配置 */ private config: ResultConverterConfig; /** * 构造函数 * @param config 转换器配置 */ constructor(config: ResultConverterConfig) { this.config = config; } /** * 批量转换检测结果 * @param detections 原始检测结果数组 * @param options 检测选项 * @param processingTime 处理时间 * @returns 标准化检测结果 */ convertBatch( detections: RawFaceDetection[], options: { maxFaces?: number; enableTracking?: boolean; }, processingTime: number ): FaceDetectionResult[] { // 限制检测数量 const maxFaces = options.maxFaces || 10; const limitedDetections = detections.slice(0, maxFaces); const results: FaceDetectionResult[] = []; for (const detection of limitedDetections) { const result = this.convertSingle(detection, processingTime); results.push(result); } return results; } /** * 转换单个检测结果 * @param detection 原始检测结果 * @param processingTime 处理时间 * @returns 标准化检测结果 */ convertSingle(detection: RawFaceDetection, processingTime: number): FaceDetectionResult { // 转换边界框 const boundingBox: Rect = { x: detection.detection?.box?.x || 0, y: detection.detection?.box?.y || 0, width: detection.detection?.box?.width || 0, height: detection.detection?.box?.height || 0 }; // 创建基本结果 const result: FaceDetectionResult = { id: generateUUID(), type: 'face', boundingBox, confidence: detection.detection?.score || 0, processingTime, timestamp: Date.now() }; // 转换关键点 if (detection.landmarks && this.config.detectLandmarks) { result.landmarks = this.convertLandmarks(detection.landmarks); } // 转换表情属性 if (detection.expressions && this.config.detectExpressions) { result.attributes = { ...result.attributes, emotion: { angry: detection.expressions.angry || 0, disgust: detection.expressions.disgusted || 0, fear: detection.expressions.fearful || 0, happy: detection.expressions.happy || 0, neutral: detection.expressions.neutral || 0, sad: detection.expressions.sad || 0, surprise: detection.expressions.surprised || 0 } }; } // 转换年龄 if (detection.age !== undefined && this.config.detectAgeGender) { result.attributes = { ...result.attributes, age: detection.age }; } // 转换性别 if (detection.gender !== undefined && detection.genderProbability !== undefined && this.config.detectAgeGender) { result.attributes = { ...result.attributes, gender: detection.gender === 'male' ? detection.genderProbability : 1 - detection.genderProbability }; } // 转换特征向量 if (detection.descriptor && this.config.extractEmbeddings) { result.embedding = { vector: Array.from(detection.descriptor), dimension: detection.descriptor.length }; } return result; } /** * 转换关键点 */ private convertLandmarks(landmarks: RawFaceDetection['landmarks']): FaceDetectionResult['landmarks'] { if (!landmarks || !landmarks.positions) { return undefined as any; } const positions = landmarks.positions; return { leftEye: { x: positions[FaceLandmarkIndex.LEFT_EYE].x, y: positions[FaceLandmarkIndex.LEFT_EYE].y }, rightEye: { x: positions[FaceLandmarkIndex.RIGHT_EYE].x, y: positions[FaceLandmarkIndex.RIGHT_EYE].y }, nose: { x: positions[FaceLandmarkIndex.NOSE].x, y: positions[FaceLandmarkIndex.NOSE].y }, mouth: { x: positions[FaceLandmarkIndex.MOUTH_CENTER].x, y: positions[FaceLandmarkIndex.MOUTH_CENTER].y }, points: positions.map((p) => ({ x: p.x, y: p.y })) }; } /** * 更新配置 */ updateConfig(config: Partial<ResultConverterConfig>): void { this.config = { ...this.config, ...config }; } }