UNPKG

id-scanner-lib

Version:

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

151 lines (130 loc) 3.65 kB
/** * @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; } }