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