id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
151 lines (130 loc) • 3.65 kB
text/typescript
/**
* @file 人脸比对器
* @description 提供人脸特征向量比对功能
* @module modules/face/face-comparator
*/
import { FaceDetectionResult } from '../../interfaces/face-detection';
import { Result } from '../../core/result';
import { FaceComparisonError } from '../../core/errors';
import { Logger } from '../../core/logger';
/**
* 人脸比对结果
*/
export interface ComparisonResult {
/** 相似度 (0-1) */
similarity: number;
/** 是否匹配(基于阈值) */
isMatch: boolean;
/** 使用的阈值 */
threshold: number;
}
/**
* 人脸比对器配置
*/
export interface FaceComparatorConfig {
/** 人脸匹配阈值 (0-1) */
matchThreshold?: number;
}
/**
* 人脸比对器
*
* 负责计算两个人脸特征向量的相似度
*/
export class FaceComparator {
/** 日志记录器 */
private logger: Logger;
/** 匹配阈值 */
private matchThreshold: number;
/**
* 构造函数
* @param config 比对器配置
*/
constructor(config: FaceComparatorConfig = {}) {
this.logger = Logger.getInstance();
this.matchThreshold = config.matchThreshold ?? 0.6;
}
/**
* 计算两个特征向量的余弦相似度
*
* @param v1 特征向量1
* @param v2 特征向量2
* @returns 相似度 (0-1)
*/
computeSimilarity(v1: number[], v2: number[]): number {
if (v1.length !== v2.length) {
throw new Error('特征向量维度不匹配');
}
let dotProduct = 0;
let norm1 = 0;
let norm2 = 0;
for (let i = 0; i < v1.length; i++) {
dotProduct += v1[i] * v2[i];
norm1 += v1[i] * v1[i];
norm2 += v2[i] * v2[i];
}
// 确保长度非零
if (norm1 === 0 || norm2 === 0) {
return 0;
}
return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
/**
* 比对两个人脸
*
* @param source 源人脸(特征向量或检测结果)
* @param target 目标人脸(特征向量或检测结果)
* @returns 比对结果
*/
compare(
source: number[] | FaceDetectionResult,
target: number[] | FaceDetectionResult
): Result<ComparisonResult> {
try {
// 提取特征向量
const sourceEmbedding = this.extractEmbedding(source);
const targetEmbedding = this.extractEmbedding(target);
if (!sourceEmbedding || !targetEmbedding) {
return Result.failure(new FaceComparisonError('无法获取特征向量'));
}
// 计算相似度
const similarity = this.computeSimilarity(sourceEmbedding, targetEmbedding);
const isMatch = similarity >= this.matchThreshold;
return Result.success({
similarity,
isMatch,
threshold: this.matchThreshold
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('FaceComparator', `人脸比对失败: ${errorMessage}`, error as Error);
return Result.failure(new FaceComparisonError(`人脸比对失败: ${errorMessage}`));
}
}
/**
* 从输入中提取特征向量
*/
private extractEmbedding(input: number[] | FaceDetectionResult): number[] | null {
if (Array.isArray(input)) {
return input;
}
if (input.embedding?.vector) {
return input.embedding.vector;
}
return null;
}
/**
* 设置匹配阈值
*/
setThreshold(threshold: number): void {
if (threshold < 0 || threshold > 1) {
throw new Error('阈值必须在 0-1 范围内');
}
this.matchThreshold = threshold;
}
/**
* 获取当前阈值
*/
getThreshold(): number {
return this.matchThreshold;
}
}