UNPKG

id-scanner-lib

Version:

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

208 lines (178 loc) 5.2 kB
/** * @file 人脸跟踪器 * @description 提供人脸跟踪功能,基于 IOU 匹配算法 * @module modules/face/face-tracker */ import { FaceDetectionResult, Rect } from '../../interfaces/face-detection'; import { generateUUID } from '../../utils'; /** * 人脸跟踪状态 */ interface FaceTrackerState { /** 跟踪ID */ trackId: string; /** 最后检测到的时间戳 */ lastSeen: number; /** 检测结果 */ detection: FaceDetectionResult; /** 连续跟踪帧数 */ consecutiveFrames: number; } /** * 人脸跟踪器配置 */ export interface FaceTrackerConfig { /** 跟踪超时时间(ms),超过此时间未检测到则移除 */ trackTimeout?: number; /** 匹配阈值,值越小越严格 */ matchThreshold?: number; } /** * 人脸跟踪器 * * 基于 IOU(交并比)和中心点距离的人脸跟踪算法 */ export class FaceTracker { /** 跟踪状态映射 */ private trackers: Map<string, FaceTrackerState> = new Map(); /** 配置 */ private config: Required<FaceTrackerConfig>; /** * 构造函数 * @param config 跟踪器配置 */ constructor(config: FaceTrackerConfig = {}) { this.config = { trackTimeout: config.trackTimeout ?? 1000, matchThreshold: config.matchThreshold ?? 0.3 }; } /** * 计算两个矩形的 IOU(交并比) */ private computeIOU(a: Rect, b: Rect): number { const x1 = Math.max(a.x, b.x); const y1 = Math.max(a.y, b.y); const x2 = Math.min(a.x + a.width, b.x + b.width); const y2 = Math.min(a.y + a.height, b.y + b.height); if (x2 <= x1 || y2 <= y1) { return 0; } const intersection = (x2 - x1) * (y2 - y1); const areaA = a.width * a.height; const areaB = b.width * b.height; const union = areaA + areaB - intersection; return union > 0 ? intersection / union : 0; } /** * 计算两个矩形的中心点距离 */ private computeCenterDistance(a: Rect, b: Rect): number { const centerA = { x: a.x + a.width / 2, y: a.y + a.height / 2 }; const centerB = { x: b.x + b.width / 2, y: b.y + b.height / 2 }; return Math.sqrt( Math.pow(centerA.x - centerB.x, 2) + Math.pow(centerA.y - centerB.y, 2) ); } /** * 匹配检测结果到现有跟踪器 * @param detections 当前帧的检测结果 * @returns 更新后的跟踪结果(带有 trackId) */ update(detections: FaceDetectionResult[]): FaceDetectionResult[] { const now = Date.now(); const results: FaceDetectionResult[] = []; // 清理过期的跟踪器 for (const [id, tracker] of this.trackers) { if (now - tracker.lastSeen > this.config.trackTimeout) { this.trackers.delete(id); } } // 标记所有检测结果为"未匹配" const unmatchedDetections = [...detections]; // 尝试将检测结果匹配到现有跟踪器 for (const [trackId, tracker] of this.trackers) { let bestMatchIdx = -1; let bestScore = Infinity; for (let i = 0; i < unmatchedDetections.length; i++) { const detection = unmatchedDetections[i]; const iou = this.computeIOU(tracker.detection.boundingBox, detection.boundingBox); const centerDist = this.computeCenterDistance( tracker.detection.boundingBox, detection.boundingBox ); // 计算综合分数:IOU 权重 0.4,中心距离权重 0.6 const maxDim = Math.max(detection.boundingBox.width, detection.boundingBox.height); const score = iou * 0.4 + (1 - centerDist / (maxDim * 2)) * 0.6; if (score > 0.5 && score < bestScore) { bestScore = score; bestMatchIdx = i; } } if (bestMatchIdx !== -1) { // 匹配成功,更新跟踪器 const matched = unmatchedDetections[bestMatchIdx]; matched.trackId = trackId; results.push(matched); // 更新跟踪状态 tracker.lastSeen = now; tracker.detection = matched; tracker.consecutiveFrames++; // 移除已匹配的检测 unmatchedDetections.splice(bestMatchIdx, 1); } } // 剩余未匹配的检测创建新的跟踪器 for (const detection of unmatchedDetections) { const trackId = generateUUID(); detection.trackId = trackId; results.push(detection); this.trackers.set(trackId, { trackId, lastSeen: now, detection, consecutiveFrames: 1 }); } return results; } /** * 获取当前跟踪状态 */ getTrackerStates(): Map<string, FaceTrackerState> { return this.trackers; } /** * 获取活跃跟踪器数量 */ getActiveCount(): number { const now = Date.now(); let count = 0; for (const [, tracker] of this.trackers) { if (now - tracker.lastSeen <= this.config.trackTimeout) { count++; } } return count; } /** * 重置跟踪器 */ reset(): void { this.trackers.clear(); } /** * 移除指定跟踪器 * @param trackId 跟踪ID */ remove(trackId: string): boolean { return this.trackers.delete(trackId); } }