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