简介:一套基于CNN的人脸识别考勤系统完整源码包,面向计算机视觉初学者、毕业设计开发者及需要快速落地考勤场景的工程师。资源包含可直接运行的预训练模型(h5文件)与配套源码,省去从零训练的时间与算力成本;8个Python脚本覆盖数据集加载、人脸特征提取、匹配判定、考勤记录等核心环节,另有UI界面文件与XML配置文件,便于二次开发与界面调整。压缩包共4848个文件,其中以4835张JPG人脸样本图片为主,包含不同角度、光照与表情的样本,可支撑模型微调与效果验证;整体108.06MB,目录结构清晰,上手门槛较低。已有306人学习下载。借助该资源可系统掌握CNN人脸识别在考勤场景中的完整落地流程,理解预训练模型迁移、特征比对与实时识别逻辑,适合课程设计、毕业设计或企业原型系统参考。
1. 能直接运行的 CNN 人脸识别考勤系统,先验三件事
拿到一套带预训练模型、号称能直接运行的 CNN 人脸识别考勤系统,最常见的情况不是打不开,而是摄像头亮了、画面里也框出了人脸,但刷谁都"不认识"。反直觉的点在于:预训练模型能跑通和考勤能落地是两回事。前者只代表 ONNX 权重被正确加载,后者还需要确认检测模型和特征提取模型的输入输出是否匹配、比对阈值是否被默认值带偏、底库里的特征向量是什么时候用哪一版权重生成的。本文按选型、首跑、业务表设计、参数校正的顺序,把这套系统从"能出框"调成"能打卡"。
2. 拆解 CNN 人脸识别考勤系统的识别链路与预训练模型选型
2.1 一个 CNN 不够:检测、对齐、特征提取是三个模型接力
人脸识别考勤系统里的"CNN"不是一个大网络,而是一条流水线。第一步是人脸检测,常见的是 RetinaFace、SCRFD、MTCNN 这类轻量检测网络,输出人脸框和五个关键点(双眼、鼻尖、左右嘴角);第二步是对齐,根据五个关键点做相似变换,把图片里歪着、侧着的脸归一化成标准正脸图;第三步才是真正意义上的"人脸识别",也就是特征提取网络,常见的是 ResNet50、ResNet100、MobileFaceNet 这一类 backbone 加上 ArcFace 或 CosFace 的损失函数训练出来的特征模型。
这个区分很重要,因为"预训练模型"在这条流水线里通常指的是第三步的特征模型,而检测和对齐可能来自另一个预训练权重。判断一套系统能不能直接跑,第一件事就是看它有没有把所有阶段的权重文件都带齐,而不是只给一个*.onnx就当交付。
特征提取模型的工作方式是把对齐后的 112×112 或 160×160 的彩色人脸图映射到一个高维向量。以 ArcFace 系列为例,输出一般是 512 维的浮点特征,并且已经做了 L2 归一化。后面的比对不再依赖 CNN,而是对特征向量做余弦相似度计算,相似度超过阈值就算同一个人。
2.2 为什么"直接用预训练模型"比重训更划算
很多人拿到考勤系统的第一步是想着再训练一轮,想把员工照片 finetune 进去。实际上对于人脸识别这种任务,直接使用开源免费商用的预训练模型,效果通常远好于基于几百张员工照片做微调。原因在于预训练模型是在百万级甚至千万级人脸库上训练出来的,backbone 已经学到了"光照变化、姿态变化、年龄变化"下的鲁棒表示。考勤场景里摄像头位置固定、光线相对可控,属于预训练模型覆盖范围内的常见分布。
真正需要做的不是重新训练 CNN,而是选择一个合适的预训练模型,并保证底库里的特征和线上识别用的是同一套权重。如果底库是旧权重生成的 512 维向量,识别端换成了新权重,那么两个人的相似度会普遍偏低,表现为"谁都不认识"。这种情况在工程里非常常见,却不是模型的问题,而是特征版本不一致。
另外要注意"免费商用"的边界。开源的人脸识别模型大多只开放了推理权重和推理代码,训练数据和使用限制要看具体许可证。落地到企业内部考勤问题不大,但如果要做成对外销售的人脸识别门禁机或 SaaS 产品,就得核对授权条款。选型时我一般会把"可商用性"和"输入输出格式"放在同等位置。
2.3 预训练模型选型参数:输入尺寸、特征维度和损失函数
下表列出做考勤系统时常见的预训练模型选型参考维度,不涉及具体下载地址,只说明业界常见的几种配置倾向:
| 模型系列 | 输入尺寸 | 特征维度 | 适用场景说明 |
|---|---|---|---|
| ArcFace(ResNet100 backbone) | 112×112 | 512 | 精度优先,适合闸机、门禁一体机,CPU 推理偏慢 |
| ArcFace(MobileFaceNet) | 112×112 | 128 | 轻量设备或边缘盒子,识别速度更快,精度略低 |
| FaceNet(Inception ResNet v1) | 160×160 | 128 | 经典方案,部署资料多,适合快速验证原型 |
| SCRFD 检测 + ArcFace 识别组合 | 检测 640×640,识别 112×112 | 512 | 考勤系统最常见的组合,兼顾检测召回和识别精度 |
选型时除了看准确率,还要看模型文件大小和推理耗时。考勤场景是"多人排队刷脸",单帧识别时间最好控制在 100ms 以内,所以 MobileFaceNet 这类轻量模型在普通 USB 摄像头方案里更常见;如果现场配的是专用人脸识别门禁机,设备内部往往已经跑了一个嵌入式优化过的模型,这时候考勤系统要做的只是接收设备传来的特征值或者人脸 ID。
2.4 比对逻辑的最小代码:相似度怎么算、阈值怎么给
预训练模型输出的是归一化特征向量,代码里比对两人的相似度时,不需要重算余弦公式里的除法,直接点乘即可。下面这段是识别逻辑的最小代码,也是后续考勤系统判定的基础:
import numpy as np def cosine_similarity(feat_a: np.ndarray, feat_b: np.ndarray) -> float: # 预训练模型输出通常已经做 L2 归一化,点乘即余弦相似度 if feat_a.shape != feat_b.shape: raise ValueError(f"特征维度不一致: {feat_a.shape} vs {feat_b.shape}") score = float(np.dot(feat_a, feat_b)) # 防御性检查:超过 1.0 的浮点误差裁回 1.0,避免影响后续阈值判断 return min(max(score, -1.0), 1.0) def is_same_person(score: float, threshold: float = 0.5) -> bool: # 阈值只影响误识率(FAR)和拒识率(FRR)的取舍,不改变特征本身 return score >= threshold逻辑说明:np.dot在这里等同于余弦相似度,因为两个向量都已经做过 L2 归一化,模长为 1。阈值 0.5 是经验起点,不是通用答案。调高到 0.55 以上,误识率下降但容易拒识;调到 0.45 以下,员工刷脸通过率提高但可能把长得像的两个人判成同一人。后面第 5 章会说怎么用真实数据把阈值标定出来。
3. 预训练模型本地跑通:注册、比对、摄像头识别的第一版代码
3.1 拿到预训练模型先核对四处配置
"可以直接运行"的模型包通常只包含权重文件,而权重对输入数据有严格约定。我在接入一个新模型包时,最先核对的是四件事:输入图像的通道顺序是 RGB 还是 BGR;输入尺寸是 112×112、160×160 还是其他;像素归一化是除以 255 还是减均值除以标准差;输出特征是否需要再做一次 L2 归一化。这四项里任何一项不一致,识别准确率都会大幅下降,而且表现很像"系统坏了"——因为不是逻辑错误,是数据分布整体错位。
最常见的错误发生在通道顺序上。OpenCV 读进来的是 BGR,而大部分预训练模型是用 RGB 图像训练的。如果加载模型后没有cv2.cvtColor(img, cv2.COLOR_BGR2RGB),识别率会掉到接近随机猜。另一个隐蔽问题是关键点坐标的尺度,检测模型输出的五个关键点坐标是在原图分辨率下的像素坐标,对齐时如果换算倍数错了,切出来的人脸就是歪的,特征质量自然差。
3.2 用 ONNX Runtime 写一个特征提取引擎
下面是基于 ONNX Runtime 和 OpenCV 的通用特征提取实现,兼容以 ONNX 格式分发的预训练检测模型和识别模型。检测模型输出人脸框和五个关键点,识别模型接收对齐后的标准人脸图并输出特征向量。
import cv2 import numpy as np import onnxruntime as ort class FaceEngine: def __init__(self, det_model: str, rec_model: str, input_size: int = 112): # 优先用 CUDA,不存在则回退 CPU,避免在无 GPU 的考勤机上直接报错 providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] self.det_session = ort.InferenceSession(det_model, providers=providers) self.rec_session = ort.InferenceSession(rec_model, providers=providers) self.input_size = input_size # 记录模型要求的输入名,不同预训练模型的输入节点名并不统一 self.det_input = self.det_session.get_inputs()[0].name self.rec_input = self.rec_session.get_inputs()[0].name print(f"检测模型输入: {self.det_input}, 识别模型输入: {self.rec_input}") def detect_faces(self, bgr_img: np.ndarray): """返回人脸框和五点的粗略实现,省略 NMS 细节以保证可读性。""" # 检测模型输入通常要求固定尺寸,这里按 640x640 缩放并保持原图比例不变 h, w = bgr_img.shape[:2] scale = 640 / max(h, w) resized = cv2.resize(bgr_img, (int(w * scale), int(h * scale))) # 构造 NCHW 输入,归一化到 [0,1] blob = cv2.dnn.blobFromImage(resized, 1.0 / 255, (640, 640), (0, 0, 0), swapRB=True) outputs = self.det_session.run(None, {self.det_input: blob}) # outputs 的具体格式取决于模型,需要按实际权重解析 bbox 和 landmark return outputs def get_embedding(self, bgr_img: np.ndarray) -> np.ndarray: """检测并裁剪出最大人脸,对齐后提取 512 维特征。""" dets = self.detect_faces(bgr_img) # 实际项目中这里要解析出 bbox 与五个关键点,并按相似变换把脸对齐到 input_size face_align = cv2.resize(bgr_img, (self.input_size, self.input_size)) # 识别模型同样要求 RGB 顺序与 [0,1] 归一化 blob = cv2.dnn.blobFromImage(face_align, 1.0 / 255, (self.input_size, self.input_size), (0, 0, 0), swapRB=True) feat = self.rec_session.run(None, {self.rec_input: blob})[0].flatten() # L2 归一化后再入库,保证后面点乘即余弦相似度 feat = feat / np.linalg.norm(feat) return feat逻辑说明:blobFromImage里的swapRB=True是因为 ONNX 模型大多数按 RGB 训练,而 OpenCV 默认 BGR,这一步负责把通道顺序转换掉。检测结果解析我没有展开,因为不同预训练模型的输出设计差异很大,有的直接输出 bbox 和关键点坐标,有的输出的是 stride 网格,需要按模型文档解码。实际落地时,我会先用一张单人照片打印outputs的 shape 和数值范围,确认输出结构后再写解析逻辑。
3.3 注册第一位员工,完成"认人"闭环
from pathlib import Path import numpy as np import cv2 engine = FaceEngine("det.onnx", "rec.onnx") def register_employee(emp_id: str, photo_path: str, db_dir: str = "face_db"): """把一张干净的正面照变成特征向量,以 npy 文件落库。""" img = cv2.imread(photo_path) feat = engine.get_embedding(img) db_dir = Path(db_dir) db_dir.mkdir(exist_ok=True) np.save(db_dir / f"{emp_id}.npy", feat) print(f"员工 {emp_id} 注册完成,特征维度: {feat.shape[0]}") def recognize(photo_path: str, db_dir: str = "face_db", threshold: float = 0.5): """识别一张照片属于哪个员工,返回员工ID与相似度。""" img = cv2.imread(photo_path) feat = engine.get_embedding(img) best_id, best_score = None, -1.0 for npy_file in Path(db_dir).glob("*.npy"): db_feat = np.load(npy_file) score = float(np.dot(feat, db_feat)) if score > best_score: best_id, best_score = npy_file.stem, score if best_score < threshold: return None, best_score return best_id, best_score # 注册一个测试员工,然后用同一张照片验证识别 register_employee("E1001", "photos/zhangsan.jpg") emp_id, score = recognize("photos/zhangsan_live.jpg", threshold=0.5) print(f"识别结果: {emp_id}, 相似度: {score:.3f}")参数说明:threshold=0.5是初始值,实际部署时应该用第 5 章的方法重新标定。db_dir用 npy 文件存特征,适合几十人到几百人的规模;超过 1000 人时逐个加载 npy 的耗时不可忽略,这一段的瓶颈就从 CNN 推理转移到了 IO 和向量比对,需要一个更快的索引结构。
3.4 首次运行最容易卡住的三个位置
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 模型加载报错 "No such file or directory" | 路径含中文或模型文件缺失 | 把模型放到纯英文路径,确认det.onnx、rec.onnx都存在 |
| 识别率极低,所有人的分数都在 0.2 以下 | 通道顺序反了,或输入尺寸与模型要求不一致 | 检查swapRB参数和 resize 尺寸是否匹配模型输入 |
| 同一个人的两次抓拍相似度只有 0.3 | 对齐环节没生效,直接resize整张图当人脸 | 把对齐后的人脸图保存出来看一眼,确认五官位置居中 |
这三类问题占了模型接入阶段八成以上的故障时间。第 2 类是权重配置问题,第 3 类是对齐逻辑问题,这也是为什么我坚持把"保存中间结果"写进工程代码——没有中间人脸图,调试时就只能靠猜。
4. 考勤数据落地:人脸识别的结果怎么变成迟到早退记录
4.1 考勤表设计:人员、特征、打卡记录三张表
识别链路跑通后,下一个核心问题是数据模型。很多自研考勤系统只建了一张打卡表,存员工 ID、时间和照片路径,等要出月度报表时发现"迟到"没法算,因为没有基准排班表。常见的做法是拆三张表:employees存人员基础信息,face_features存特征向量和版本号,attendance存每次识别事件。特征单独成表而不是并进人员表,是为了将来更换识别模型时能通过版本字段批量重算。
CREATE TABLE employees ( emp_id TEXT PRIMARY KEY, name TEXT NOT NULL, department TEXT, hire_date TEXT, status INTEGER DEFAULT 1 ); CREATE TABLE face_features ( emp_id TEXT PRIMARY KEY, feature BLOB NOT NULL, model_version TEXT NOT NULL, updated_at TEXT DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (emp_id) REFERENCES employees(emp_id) ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, emp_id TEXT NOT NULL, clock_time TEXT NOT NULL, device_id TEXT, similarity REAL, status TEXT, FOREIGN KEY (emp_id) REFERENCES employees(emp_id) ); CREATE INDEX idx_attendance_emp_time ON attendance(emp_id, clock_time);建表逻辑说明:feature字段存的是特征向量的二进制序列化结果,而不是 Base64 字符串,因为 BLOB 检索和存储开销更小。model_version字段记录这次特征是用哪套权重生成的,识别端加载模型后会把模型文件名或版本号带进来,只有同版本的特征才能直接比对。attendance表只存原始事件,迟到、早退的判定不写在这里,而是靠视图或统计 SQL 计算,这样排班规则改动时不需要重写历史记录。
4.2 打卡事件判定:迟到、早退、缺卡的去重规则
考勤业务里容易出问题的不是识别,而是同一个员工在闸机前反复刷脸导致产生多条记录。判定逻辑通常先做去重,再打状态标签。常见做法是:同一个员工 5 分钟内的连续识别只保留第一次,这样进公司时刷两次不会重复;但下班时同一个人可能只刷一次就走,因此去重窗口不能设太长。
下面的 SQL 把当天打卡记录按员工分组,取出每个人当天最早的打卡和最后的打卡,再根据班次信息标记状态:
WITH daily_records AS ( SELECT emp_id, date(clock_time) AS work_day, MIN(clock_time) AS first_in, MAX(clock_time) AS last_out, COUNT(*) AS punch_count FROM attendance WHERE clock_time >= date('now', 'localtime', '-1 day') GROUP BY emp_id, date(clock_time) ) SELECT emp_id, work_day, CASE WHEN time(first_in) > '09:15:00' THEN '迟到' ELSE '正常' END AS morning_status, CASE WHEN punch_count = 1 THEN '缺少下班记录' ELSE '已打卡' END AS evening_status FROM daily_records;这段 SQL 将一个人当天上午第一次有效识别作为上班时间,晚上最后一次识别作为下班时间。MIN(clock_time)在去重后的表上做才能保证准确,如果直接在原始表上取最小值,员工在闸机前多刷几次就会把上班时间提前。实际系统里我会把去重放在写入阶段完成,而不是查询阶段,这样报表查询时不需要重复做窗口去重。
4.3 识别通过后如何写入考勤记录
import sqlite3 from datetime import datetime def punch(emp_id: str, similarity: float, device_id: str, conn: sqlite3.Connection): """写入一次打卡事件,5 分钟内同一个人重复识别直接丢弃。""" now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") dup = conn.execute( """ SELECT COUNT(*) FROM attendance WHERE emp_id = ? AND clock_time >= datetime(?, '-5 minutes') """, (emp_id, now), ).fetchone()[0] if dup: return False conn.execute( "INSERT INTO attendance (emp_id, clock_time, device_id, similarity) VALUES (?, ?, ?, ?)", (emp_id, now, device_id, similarity), ) conn.commit() return True写入逻辑说明:去重查询里用datetime(?, '-5 minutes')生成窗口起点,避免在代码里做时间字符串拼接。similarity字段保留识别时的相似度,方便后续排查阈值过低导致的误打卡。写入失败时整个事务回滚,避免摄像头端显示成功但库里没记录的"幽灵打卡"。
4.4 上班高峰的并发压力:先别怪模型,用 JMeter 压一下接口
人脸识别考勤系统在早上 8:50 到 9:10 之间会迎来瞬时并发,几十个人同时站在摄像头前刷脸。这时候设备端的推理耗时是一方面,更大的瓶颈往往是识别服务接收图片、写库、返回结果的 HTTP 链路。拿 JMeter 测试人脸识别接口时,我会把注册照片变成 Base64 字符串作为 POST 参数,设置线程组模拟 20 个并发用户循环打卡,观察接口的响应时间分布。
如果发现 P95 响应时间超过 1 秒,先看是不是 SQLite 写锁冲突,再看识别服务里有没有把图片解码放到请求线程里。这两个问题通常比 CNN 推理更先暴露。SQLite 在高并发写入场景下表现一般,如果打卡设备数量超过 10 台,我会把 attendance 表迁到 MySQL 或 PostgreSQL,face_features 表因为写入频率低,留在 SQLite 反而更简单。
5. "越用越准":线上考勤系统最常调的 4 个识别参数
5.1 识别阈值:0.4 到 0.55 之间怎么量着定
默认阈值 0.5 只适合 Demo。考勤系统上线前,我会收集三类样本:同一员工不同时段的抓拍、不同员工的抓拍、员工拿工牌照片刷脸的抓拍,分别算相似度,画出分布。正常情况是同一个人相似度集中在 0.55~0.8,不同人集中在 0.1~0.35,中间的空档就是阈值可调区间。如果两类分布重叠严重,说明特征质量差,调阈值没用,要先查对齐或图像质量。
import numpy as np def pick_threshold(pos_scores: list[float], neg_scores: list[float], max_far: float = 0.001) -> float: """在满足误识率上限的前提下,选使通过率最高的阈值。""" best_t, best_tar = 0.5, 0.0 for t in np.arange(0.3, 0.7, 0.005): far = sum(1 for s in neg_scores if s >= t) / len(neg_scores) tar = sum(1 for s in pos_scores if s >= t) / len(pos_scores) if far <= max_far and tar > best_tar: best_t, best_tar = round(t, 3), tar return best_t, best_tar参数说明:max_far表示允许的最大误识率,考勤场景我建议定在 0.1% 以下,也就是一万次刷脸最多误通过一次;pos_scores是同一人两张照片的相似度集合,neg_scores是不同人照片的相似度集合。这套方法把阈值从拍脑袋变成了数据决策,而且每次重录底库后都能重新跑一遍。
5.2 摄像头安装距离决定检测参数
人脸识别考勤系统最常见的部署问题是摄像头装得太高或者太远,导致人脸在画面里只有几十个像素宽。检测模型看的是整张图,det_size决定内部缩放后的分辨率,det_thresh决定最低置信度。摄像头装在 1.2 米高度、人脸宽度占画面 1/5 时,det_thresh用 0.5 没问题;如果是吸顶安装仰拍,det_thresh要降到 0.3 左右,同时把det_size从 640×640 提到 960×960,否则漏检率会很高。
这里有个常见误区:det_thresh调低会增加误检框,表现为画面里出现乱七八糟的框,这时候不是继续调低阈值,而是检查是不是把墙上的海报、显示器里的照片当成了人脸。考勤机和人脸识别门禁机的差异就在这体现——门禁机通常用结构光或双目摄像头限制只能识别真人,而普通 USB 摄像头方案只能靠算法过滤。
5.3 底库超过 1000 人后,特征存储要换索引
用 npy 文件存特征,加载 100 人时每张 npy 只有 4KB,全部读一遍只要十几毫秒;到了 1000 人,每次识别都要遍历 1000 个文件,磁盘 IO 时间可能超过 CNN 推理时间。这时候常见做法是把特征全部加载到内存,用 NumPy 矩阵一次算完所有点乘,或者引入 FAISS 这类向量索引。在写考勤系统的批量导入功能时也要注意:不要逐个人调用识别接口提取特征,而是一次性把照片路径列表喂给批量任务,先把特征算完再统一写入数据库。
def batch_extract(photo_dir: str, conn: sqlite3.Connection, engine): """批量提取特征并写入 face_features 表,适合初始化底库或换模型后重算。""" rows = [] for photo_path in Path(photo_dir).glob("*.jpg"): emp_id = photo_path.stem img = cv2.imread(str(photo_path)) feat = engine.get_embedding(img) rows.append((emp_id, feat.tobytes(), "v2")) conn.executemany( "INSERT OR REPLACE INTO face_features (emp_id, feature, model_version) VALUES (?, ?, ?)", rows, ) conn.commit()逻辑说明:feat.tobytes()把 float32 数组序列化成 BLOB,读取时用np.frombuffer(blob, dtype=np.float32)还原。批量提取的价值不只是快,更重要的是保证底库里所有特征出自同一个model_version——否则第 2 章说的"新旧权重混用导致全都不认识"就会出现。换模型后,只需要把v2改成新模型的版本号,全表重跑一遍即可。
5.4 高频误报的三类样本:镜子、工牌照片、远处的侧脸
考勤系统上线后收到的"打卡异常"投诉,大部分不是误识,而是漏识。漏识最多的三种情况:一是员工戴着帽子或口罩,五官被遮挡严重;二是画面里有镜子,检测模型在镜子里也检测到了人脸;三是员工从摄像头侧面快步走过,侧脸角度太大。针对第 2 种,我不会去优化检测模型,而是在业务层加限制——考勤机通常只取画面里宽度最大的那张脸作为打卡对象;针对第 1 种和第 3 种,靠的是"连续多帧确认"策略:同一人在 1 秒内的 5 帧里至少 3 帧识别为同一员工,才写入打卡记录,而不是单帧通过就打卡。
6. 收尾动作:写一个自检脚本,把阈值和误识率都变成看得见的报告
系统上线后最容易被质问的一个问题就是"你凭什么把阈值设在 0.5"。与其解释,不如把自检脚本写进工程里,定期生成一份识别质量报告。做法是维护一个测试集:每个员工注册照之外再采集两张现场照作为正样本对,再随机组合不同员工的照片作为负样本对,然后批量计算相似度,输出报告。
import csv import random from pathlib import Path import numpy as np from face_engine import FaceEngine engine = FaceEngine("det.onnx", "rec.onnx") def build_pairs(db_dir: str, live_dir: str, n_neg: int = 500): """构造正负样本对:正样本=同一人注册照vs现场照,负样本=随机双人组合。""" emps = list(Path(db_dir).glob("*.npy")) pairs = [] for npy in emps: # 现场照命名约定: {emp_id}_live.jpg 放在 live_dir live_path = Path(live_dir) / f"{npy.stem}_live.jpg" if live_path.exists(): pairs.append(("pos", npy.stem, str(live_path))) all_ids = [p.stem for p in emps] for _ in range(n_neg): a, b = random.sample(all_ids, 2) pairs.append(("neg", a, b)) return pairs def generate_report(pairs, out_csv: str = "similarity_report.csv"): """跑完全部样本对,输出相似度分布和推荐阈值。""" rows, pos_scores, neg_scores = [], [], [] for label, id_a, id_b in pairs: if label == "pos": # 现场照实时提特征,与注册底库特征比较 feat_live = engine.get_embedding(cv2.imread(id_b)) feat_db = np.load(Path("face_db") / f"{id_a}.npy") score = float(np.dot(feat_live, feat_db)) pos_scores.append(score) else: feat_a = np.load(Path("face_db") / f"{id_a}.npy") feat_b = np.load(Path("face_db") / f"{id_b}.npy") score = float(np.dot(feat_a, feat_b)) neg_scores.append(score) rows.append((label, id_a, id_b, round(score, 4))) with open(out_csv, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["label", "id_a", "id_b", "similarity"]) writer.writerows(rows) # 推荐阈值的判定标准:负样本误识率不高于 0.1% best_t, best_tar = pick_threshold(pos_scores, neg_scores, max_far=0.001) print(f"正样本对数量: {len(pos_scores)}, 负样本对数量: {len(neg_scores)}") print(f"推荐阈值: {best_t}, 对应通过率: {best_tar:.2%}") return best_t这个脚本解决的是"更新底库后要不要调阈值"的维护问题。每次新增员工、更换摄像头或换预训练模型,就重新生成一次similarity_report.csv并归档;如果推荐阈值和线上阈值偏差超过 0.03,说明上线后的环境发生了变化,需要把新阈值同步到考勤识别服务里。报告文件本身也应该按日期命名留存,比如similarity_report_20250115.csv,这样事后追溯"某天开始误识率变高"时,可以直接对照报告数据定位是阈值漂移还是底库特征问题。
本文还有配套的精品资源,点击获取