简介:这是一份基于CNN神经网络实现的人脸识别考勤演示项目,主要面向大学生课程设计与深度学习入门者。项目完整覆盖人脸注册、特征提取、识别签到等流程,后端由Python脚本驱动业务逻辑,界面采用PyQt编写,检测部分结合OpenCV的xml级联分类器,并配有Caffe的prototxt网络结构与caffemodel权重文件。资源共30个文件,包括11个py源码、6个pyc编译文件、3个ui界面文件、模型配置文件、JPG/PNG图片、TTF字体及临时buffer文件,压缩包约32.54MB。项目目录按模型、界面、脚本、素材分层组织,整体结构清晰,方便按模块阅读与调试。已有173人学习,适合想快速上手人脸识别考勤应用的同学,可直接运行体验,也能通过源码理解CNN模型部署、界面交互与考勤数据处理的衔接思路,便于在此基础上扩展功能或改写为课程设计。
1. 考勤demo不是玩具:CNN人脸识别在打卡场景里的真实边界
很多人拿到“基于CNN神经网络人脸识别实现的考勤demo项目.zip”后的第一反应是:这也能打卡?几百行代码加一个训练好的小模型权重,看起来比公司门口那台人脸识别门禁机单薄太多。但恰恰是这种demo程序,把深度学习cnn人脸识别的完整链路——人脸检测、特征提取、身份比对、考勤记录——一条龙跑通了,让你只用一台带摄像头的电脑就能验证“谁在什么时候打了卡”。它适合三类人:做毕设和课设的学生、想在公司内部先试点人脸考勤的算法或运维工程师、以及想拿一个小项目练手深度学习cnn落地细节的开发者。下面这篇笔记就按“原理是什么 → 怎么跑起来 → 哪些地方会翻车 → 怎么改成能实际用”的顺序,把这条链路讲透。
2. 先看懂人脸识别链路:CNN在这个考勤demo里到底做了什么
2.1 为什么考勤场景最终选深度学习cnn,而不是老式特征工程
最早的自动人脸识别靠的是人手工设计的特征:HOG描述梯度方向分布,LBP编码局部纹理,Eigenfaces把脸投影到主成分空间。这些方法在正脸、均匀光照下能用,但考勤现场不会配合你:走廊背光、戴眼镜、低头看手机、上午和下午的阳光角度不一样。手工特征的组合维度太多,调参调到怀疑人生,识别率还是忽高忽低。
CNN卷积神经网络的出现把“特征设计”变成了“特征学习”。训练时,网络自己决定底层卷积核去响应边缘、颜色块,深层卷积核去响应眼睛、鼻梁这些语义部件。训练数据里出现过的姿态和光照变化,都会以权重的形式沉淀进网络里。这带来一个考勤场景最看重的性质:只要训练数据覆盖到位,CNN对姿态、光照、模糊的容忍度远高于特征工程时代的方案。这也是为什么现在的人脸识别门禁机、考勤机里几乎都是深度学习cnn方案,传统方法基本退出了主流赛道。
2.2 一条完整的人脸识别流程:检测、对齐、特征、比对
先说清楚一件事:单独训练一个CNN分类模型,和“人脸识别”之间还差几步。一个可用的考勤demo,内部通常是一条四段流水线。
第一段是人脸检测。摄像头里不一定只有一个人,也可能没人,你得先知道脸在哪。demo里常见两种做法:轻量的用OpenCV自带的Haar级联,稍微靠谱的用MTCNN或OpenCV DNN的人脸检测模型。检测输出一张人脸框,交给下一段。
第二段是人脸对齐。检测框里的脸可能是歪的、侧着的,直接送进CNN会让特征质量明显下降。主流做法是检测眼睛、鼻尖等关键点,再做仿射变换,把脸转正并缩放到固定尺寸,比如112×112或224×224。
第三段是特征提取,这才是CNN的主场。人脸图经过卷积神经网络前向传播,最终压缩成一个固定长度的特征向量。好的特征向量有一个性质:同一个人的不同照片,向量距离小;不同人的照片,向量距离大。第四段是对比和决策:拿摄像头抓到的特征向量去和员工底库里的所有已知特征比相似度,超过设定的阈值就判成对应的人,同时记下时间和摄像头编号。
不少demo程序会把检测这一步简化成“每次只处理画面里最大的一张脸”,这对单人打卡场景够用;但如果你想做得扎实,检测、对齐这两段不能省,它们对最终识别率的影响经常比CNN网络本身还大。
2.3 demo里常见的两种建模思路:分类训练和度量学习
我翻过不少考勤demo的源码,也自己写过,发现训练部分基本分成两派。
第一派是“分类训练”:把每个员工当成一个类别,网络最后一层输出一个概率分布,用softmax交叉熵去训练。识别的时候,不取最后一层的分类结果,而是取倒数第二层的特征向量当身份描述。这种做法的好处是代码直观、训练速度快,几十个人的班级或公司部门,一块普通显卡十几分钟就能训完。缺点是特征不够“挤”,同一个人的特征在空间里可能比较分散,识别阈值不太好定。
第二派是“度量学习”:不直接学分类,而是让网络学会“同类靠近、异类拉远”。常见损失函数有Triplet Loss和ArcFace。ArcFace在公开人脸数据集上做预训练后,输出的512维特征在判别力上明显强于softmax特征,这也是arcface人脸识别方向在生产落地中最常见的选择。代价是训练逻辑更复杂,数据集也需要更精细地构造三元组。
下面这个表格是我在选型时常用的对比,可以直接抄:
| 思路 | 训练难度 | 特征判别力 | 对数据量要求 | demo常见程度 |
|---|---|---|---|---|
| 分类训练 + 特征向量 | 低 | 中 | 每人5~10张可用 | 最常见 |
| Triplet Loss | 中 | 中高 | 需要构造三元组 | 少 |
| ArcFace | 高 | 高 | 最好有较大数据集 | 少,多用于生产 |
如果只是把这个demo跑通,我建议先选第一派;如果目标是做出一个能和现网考勤系统拼一拼的东西,直接换ArcFace的预训练权重更划算,后面会展开讲。
2.4 demo必然省略的部分:活体检测和多人场景
绝大多数考勤demo会刻意省略两件事:活体检测和多人同时识别。省略活体检测的原因是它需要额外的网络或动作指令,比如眨眼、张嘴、左右转头,这些会显著增加演示环节的出错概率;省略多人识别是因为考勤界面大多只有一个打卡点,排队打卡是常态。省略不一定是偷懒,而是让demo聚焦到CNN人脸识别这条主链路上。你心里要先有这根弦:这套逻辑能被一张手机照片骗过,后面我会专门写一条避坑记录。
3. 解压之前先读结构:考勤demo项目.zip里通常藏着什么
3.1 典型目录:训练脚本、识别脚本、数据、权重各归其位
拿到“基于CNN神经网络人脸识别实现的考勤demo项目.zip”,解压后先别急着找exe。多数这类demo是源码包,有一个明显的分层:训练相关、识别相关、数据相关、界面相关。
一份比较常见的考勤demo目录会长这样,文件名可能各有出入,但职责基本对应:
| 路径 | 职责 |
|---|---|
| data/known/员工姓名/ | 注册底库照片,一人一个文件夹 |
| data/val/ | 训练验证集,用于看loss之外的指标 |
| weights/ | 训练好的模型权重(.pth或.h5) |
| config.py | 全局参数:路径、阈值、摄像头编号 |
| dataset.py | 读取图片、做数据增强、切分训练集 |
| model.py | CNN网络结构定义 |
| train.py | 训练入口 |
| register.py | 生成员工底库特征 |
| recognize.py | 摄像头实时识别主逻辑 |
| app.py | PyQt/Tkinter/Flask界面,展示打卡记录 |
| requirements.txt | Python依赖清单 |
为什么这个结构值得注意?因为训练脚本和识别脚本被刻意拆开了。实际部署时,识别进程是常驻的,不希望因为重训模型就重启打卡服务;分开之后,训练新模型只影响weights目录里的文件,recognize.py每次启动时重新加载一次即可。你在改代码时也要守住这条线:别把训练代码写进识别循环。
3.2 数据集是最容易被忽略的黑匣子
如果让我给这个demo排一个“对最终效果影响最大”的清单,数据排在模型前面。很多demo自带的data目录里只有几张明星照片或证件照,目的是证明流程能跑,并不是让你直接拿去给全公司用。注册底库的常识是:每个员工至少准备5~10张不同角度、不同光照的正脸照;如果某个人只有一张照片,识别率基本靠玄学,换了眼镜或发型就认不出来。
训练集和底库还要分开。底库是用来比对的“标准答案”,训练集是让CNN学会人脸表征的“教材”。如果你把要验收的那几张照片也塞进训练集,验证指标会虚高,真正上线后一塌糊涂,这是数据划分里最常犯的错。我习惯在训练前先数一遍两个目录里的人数是否一致,避免出现“有人的脸在训练集里出现过,有人没出现”这种不对称。
3.3 config.py:demo里必须看懂的那十几行参数
考勤demo的config.py常被当成“不用改的东西”,实际上它是整个项目里性价比最高的调试点。拿一份典型配置举例:
| 参数 | 示例值 | 含义 |
|---|---|---|
| IMG_SIZE | 112 | 送入CNN的图片边长,越小越快但丢细节 |
| FEATURE_DIM | 64 | 特征向量维度,小分类网络常见64,ArcFace多为512 |
| DETECT_CONF | 0.7 | 人脸检测置信度阈值 |
| MATCH_THRESH | 0.6 | 余弦相似度阈值,越大越严格 |
| CAM_ID | 0 | 摄像头编号,笔记本一般为0 |
| FRAME_INTERVAL | 0.2 | 两次识别之间的最小间隔秒数 |
| DB_PATH | attendance.db | 打卡记录SQLite存放位置 |
这些参数不是随便写的。MATCH_THRESH这一个参数就能决定你考勤是“总是认不出人”还是“经常认错人”;FRAME_INTERVAL控制CPU负载,0.2秒意味着每秒最多跑5次推理。改config.py之前,建议一次只改一个参数,并记录对应的结果,否则两个参数互相影响时,你根本不知道是谁拖累了谁。
3.4 requirements.txt先对版本,避免解压即翻车
还有个容易被忽略的点:demo的requirements.txt通常是作者本机环境的快照,里面的版本可能很老,也可能和新版不兼容。常见翻车方式是numpy版本冲突、OpenCV编译缺少libGL、PyTorch和CUDA版本对不上。我的习惯是先看清里面钉了哪些版本,再决定是照装还是装新版。如果README里没有说明Python版本,优先尝试Python 3.8到3.10之间的解释器,再老再新都容易踩坑。
4. 把考勤demo跑起来的最小路径:从训练到一次真实打卡
4.1 环境搭建:两分钟把运行环境盘干净
这里以PyTorch版本的常见demo为例。如果你拿到的是TensorFlow版本,逻辑完全一样,换掉安装命令即可。第一步是建独立环境,别直接装在系统Python里,否则后面改版本会非常痛苦:
python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install --upgrade pip pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python numpy pillow如果是NVIDIA显卡且想用GPU训练,把第三行换成对应CUDA版本的安装命令。CPU版的好处是零配置,训练小数据集也够用;GPU版训练能快十几倍,但需要先确认驱动和CUDA版本匹配。装完后跑一句python -c "import torch, cv2; print(torch.__version__, cv2.__version__)",能正常输出版本号就说明环境通了。
4.2 训练一版自己的CNN模型:最小的可运行脚本
下面的训练脚本按“分类训练”思路写,结构是我做这类demo时的习惯,可以直接保存为train.py。它做的事情是:读取data/known下按人名分文件夹的图片,在data/val上做验证,训练一个轻量CNN,保存权重。
# train.py —— 考勤demo最简训练脚本(PyTorch版) import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import transforms from torchvision.datasets import ImageFolder class TinyCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), # 第一层卷积:响应边缘和颜色 nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 112 -> 56 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 56 -> 28 ) self.avgpool = nn.AdaptiveAvgPool2d(1) self.classifier = nn.Linear(64, num_classes) def forward(self, x): x = self.features(x) x = self.avgpool(x).flatten(1) return self.classifier(x) def embedding(self, x): # 识别阶段用:输出64维特征向量,而不是分类概率 x = self.features(x) x = self.avgpool(x).flatten(1) return x def main(): train_transform = transforms.Compose([ transforms.Resize((112, 112)), transforms.RandomHorizontalFlip(p=0.5), # 水平翻转,等价于扩充角度 transforms.ColorJitter(brightness=0.2), # 亮度扰动,模拟不同光照 transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ]) val_transform = transforms.Compose([ transforms.Resize((112, 112)), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ]) train_dataset = ImageFolder("data/known", transform=train_transform) val_dataset = ImageFolder("data/val", transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False) model = TinyCNN(num_classes=len(train_dataset.classes)) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) for epoch in range(20): model.train() total_loss = 0.0 for imgs, labels in train_loader: optimizer.zero_grad() out = model(imgs) loss = criterion(out, labels) loss.backward() optimizer.step() total_loss += loss.item() model.eval() correct = 0 with torch.no_grad(): for imgs, labels in val_loader: pred = model(imgs).argmax(dim=1) correct += (pred == labels).sum().item() val_acc = correct / len(val_dataset) print(f"epoch {epoch + 1:02d} | loss {total_loss / len(train_loader):.4f} | val_acc {val_acc:.3f}") torch.save(model.state_dict(), "weights/best.pth") if __name__ == "__main__": main()这里有几个参数值得单独说。batch_size=32在CPU训练时建议降到8或16,因为小机器的内存带宽会变成瓶颈;lr=0.001是Adam的常见起点,如果loss波动剧烈可以降到0.0003;Resize((112, 112))对应低分辨率输入,训练更快,换224会让准确率微升,但显存占用是前者的四倍。RandomHorizontalFlip是最划算的数据增强,人脸左右对称,翻转不破坏语义。
训练输出里要重点看val_acc而不是loss。loss只说明网络在训练集上拟合得怎么样,val_acc才代表没见过的照片能不能认对。如果val_acc长期上不去,优先怀疑数据而不是网络。
注意:如果val_acc始终在1/员工数附近徘徊,先检查data/known和data/val是否都是“人名文件夹/图片”的两层结构,ImageFolder会把一级目录名当标签;再检查员工文件夹里是否混入了非图片文件。
4.3 注册员工底库:把照片变成特征向量
训练结束后,识别系统并不直接拿图片对比图片,而是先把每个员工的照片通过CNN变成特征向量存成底库。这一步相当于给每个员工建立一份“长相指纹”:
# register.py —— 生成员工特征底库 import numpy as np import torch from pathlib import Path from torchvision import transforms from PIL import Image from train import TinyCNN device = torch.device("cuda" if torch.cuda.is_available() else "cpu") num_classes = len(list(Path("data/known").iterdir())) model = TinyCNN(num_classes=num_classes).to(device) model.load_state_dict(torch.load("weights/best.pth", map_location=device)) model.eval() transform = transforms.Compose([ transforms.Resize((112, 112)), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ]) features = {} for person in Path("data/known").iterdir(): if not person.is_dir(): continue embs = [] for img_file in person.glob("*.jpg"): img = Image.open(img_file).convert("RGB") x = transform(img).unsqueeze(0).to(device) with torch.no_grad(): emb = model.embedding(x).cpu().numpy().flatten() embs.append(emb) features[person.name] = np.mean(embs, axis=0) # 一个员工多张照片取平均 Path("features").mkdir(exist_ok=True) np.save(f"features/{person.name}.npy", features[person.name])关键点在于调用model.embedding(x)而不是model(x):前者取的是最后一层卷积池化后的64维特征,后者输出的是分类概率,不能用来做人脸比对。np.mean(embs, axis=0)把同一个员工的几条特征平均成一条,能压制单张照片带来的噪声;如果某个员工只注册了一张照片,这一步就没有平均效果,识别率会明显下降。特征文件用.npy保存,后续识别时用np.load读取,不需要每次启动都重新跑一遍网络。
4.4 实时打卡:摄像头抓一帧,和底库比一次
最后是demo最核心的界面逻辑。最低配的版本长这样:循环读摄像头帧,用人脸检测器找到人脸,裁剪缩放后送进CNN提取特征,与所有底库特征算余弦相似度,找到最高分且超过阈值的那个人,并写一条打卡记录:
# recognize.py —— 单摄像头实时打卡(核心片段) import cv2 import numpy as np import torch import sqlite3, time from pathlib import Path from train import TinyCNN # 模型加载、底库读取与4.3节一致,变量沿用:model, features, transform, device similarity = lambda a, b: float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8)) def recognize_one(face_img): """输入一张人脸图,返回(员工名, 相似度)或(None, 最高分)""" x = transform(face_img).unsqueeze(0).to(device) with torch.no_grad(): emb = model.embedding(x).cpu().numpy().flatten() best_name, best_score = None, -1.0 for name, feat in features.items(): score = similarity(emb, feat) if score > best_score: best_name, best_score = name, score return (best_name, best_score) if best_score >= MATCH_THRESH else (None, best_score) cap = cv2.VideoCapture(CAM_ID) detector = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml") conn = sqlite3.connect(DB_PATH) while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(80, 80)) if len(faces) > 0: x, y, w, h = max(faces, key=lambda f: f[2] * f[3]) # 取画面里最大的一张脸 face_img = cv2.cvtColor(frame[y:y+h, x:x+w], cv2.COLOR_BGR2RGB) name, score = recognize_one(face_img) if name: now = time.strftime("%Y-%m-%d %H:%M:%S") conn.execute("INSERT INTO records(name, time) VALUES(?, ?)", (name, now)) conn.commit() time.sleep(FRAME_INTERVAL) # 控制推理频率,省CPU这段代码里最容易被忽略的是time.sleep(FRAME_INTERVAL)。不加这一行,摄像头会以最高帧率持续喂帧,CPU会被检测和识别塞满。minNeighbors=5控制检测严格度,调大到8可以减少误检,但也会漏掉一些侧脸;scaleFactor=1.1表示每次按1.1倍缩小搜索窗口,越小越慢但越准。SQLite写入不要放在每一帧都执行的位置,否则同一员工会在同一秒被写入多条重复记录,实际做的时候可以在写入前检查距上一条记录的时间间隔。
4.5 阈值怎么定:在误识率和拒识率之间找平衡
最后说一个真正的“参数玄学”:MATCH_THRESH。这个值设得低,陌生人容易被识别成某个员工,人脸识别领域的术语叫误识率升高;设得高,员工本人也经常被拒绝,叫拒识率升高。考勤系统通常宁可拒识率高一点,也不愿意误识,因为误识会导致替打卡,拒识顶多让人多刷一次脸。
从0.6起步,记录一段你所在环境的识别情况:如果看到陌生脸频繁触发,往上调到0.65或0.7;如果员工总说刷不过,往下降0.05一档。每次只动0.05,改完跑一天再决定下一步。这个调参过程没有标准答案,因为不同摄像头、不同光线下的特征分布完全不同,只能拿自己的数据说话。
5. 人脸识别考勤demo的排查笔记:五大踩坑现场与解法
5.1 戴个眼镜就翻车,换个发型就认不出
现象:演示时一切正常,员工戴上眼镜或换个发型后,识别分数直接掉到阈值以下,界面频繁显示“未识别”。
原因:这几乎不可能是模型坏了,而是训练数据和底库照片里没有覆盖这些变化。demo的data目录往往只有几张正脸证件照,CNN对训练分布之外的输入很脆弱,一点视觉变化都会让特征向量偏移一大截。
解决:先给底库补照片,每人大约10张,覆盖戴镜、不戴镜、仰头、低头、室内光和靠窗光。再在训练脚本里加更强的数据增强:RandomAffine(degrees=10, translate=(0.05, 0.05))和ColorJitter(brightness=0.2)能让小模型对姿态和光照更耐造。这个坑我踩得最深:当年用单张证件照注册,演示现场被同事的眼镜干翻三次。
5.2 长得像的人互相抽中:误识别到底怎么压
现象:两个脸型相近的员工,A打卡时经常显示成B,底库越大越明显。
原因:小数据集训练出的特征向量区分度不足,加上阈值定得过松。分类网络被训练成“把所有A的照片拉到一起”,但它没有像ArcFace那样强制把A和B拉开。员工基数超过20人后,这个问题会被放大。
解决:分三步走。第一步把阈值往上调0.05到0.1,先看拒识率是否可接受;第二步给每名员工的底库特征做碰撞测试,用底库内部两两比对,找出相似度高于0.8的员工对,给他们补不同角度的照片;第三步是治本,把损失函数换成ArcFace,或直接加载公开的arcface人脸识别预训练模型来做特征提取。
5.3 一开摄像头CPU就飙满,界面卡成PPT
现象:识别脚本一启动,CPU占用接近100%,摄像头预览明显掉帧,点按钮要等一秒才响应。
原因:人脸检测在整帧上跑,CNN又在裁剪出来的人脸上跑,一帧里有两段重计算;如果循环里没有限帧,摄像头以30fps喂帧,CPU一直在满负荷工作。
解决:养成三个习惯。一是加time.sleep(FRAME_INTERVAL)或按时间戳跳帧,把识别频率限制在每秒3到5次;二是先做检测再做识别,检测不到人脸就直接跳过CNN这一步,节省大量算力;三是把送入CNN的分辨率从224降到112,特征向量的质量损失很小,耗时却接近四分之一。如果再不够,就把检测换成MobileNet系列的人脸检测器,比Haar级联更准但更费时,属于进阶优化。
5.4 训练loss一路下降,验证集准确率却越来越差
现象:train.py输出的loss每轮都在降,看着很爽;但验证集准确率从0.9掉到0.8,之后越来越差,完全符合过拟合。
原因:数据量太少而网络又不够“笨”。几十个人的小数据集通常每人只有几张图,模型很快把训练照片的背景纹理、衣服颜色、固定光线背了下来,而不是人脸本身。这些在验证集里不存在,所以泛化崩盘。
解决:第一,把数据增强打开,RandomHorizontalFlip、随机亮度、随机裁剪都是零成本扩充;第二,回头检查data划分,确认验证集里没有和训练集来自同一段视频的连续帧,否则之前的val_acc本来就是虚高的;第三,给网络加上Dropout(p=0.5),或改用预训练模型固定backbone只训分类层,后者的效果在小数据集上通常是立竿见影的。
5.5 拿手机照片在镜头前晃一下就过了:活体缺失
现象:员工拿另一名同事的手机照片对准摄像头,系统直接判定为本人,打卡成功。
原因:绝大多数考勤demo没有活体检测,特征提取器只看“这张脸是谁”,不管它是真人还是照片。这在demo阶段是已知的简化,但要明确知晓:这已经不是识别准度问题,而是安全边界问题,照片攻击是这类系统最容易被钻的漏洞。
解决:如果只是演示,可以在提醒文案里注明“不含活体检测,仅限非敏感场景”;如果想堵住这个洞,给识别前端加一个动作指令,比如屏幕显示“请眨眼”,用摄像头连续帧判断眼睛开合状态;更可靠的做法是加一个轻量深度图分类器,判断画面里是立体人脸还是平面照片。后面这个方向在嵌入式门禁产品里几乎是标配,属于从demo往产品走必须补的一块。
6. 从demo到能被当回事的考勤方案:先验收,再进阶
6.1 给demo做一次严肃的验收测试
demo跑通不等于方案可用。我的习惯是,在动手优化任何东西之前,先攒一个验收集:每人拿出训练时没见过的10张照片,覆盖不同光照和角度,标好名字。然后写一段脚本,把底库特征和验收集里每张照片两两对比,输出这样一张表:
| 指标 | 计算公式 | 验收底线 |
|---|---|---|
| 识别准确率 | 正确识别数 / 测试照片总数 | 不低于90% |
| 误识率 | 被判成别人的非本人照片 / 总数 | 不高于1% |
| 拒识率 | 本人未被识别的照片 / 本人照片总数 | 不高于5% |
这三个率是互相拉扯的,压误识率必然抬高拒识率。改成什么样要看考勤场景的容忍度:宁可让员工多刷一次脸,也不能让A替B打卡。这套表格也直接决定了你要不要把MATCH_THRESH往上调。
6.2 三条进阶路线:ArcFace、多人脸跟踪、嵌入式部署
如果验收没过,别急着上模型,先补数据;如果过了,下面三条线可以按团队能力选一条走。
第一条是把特征提取升级到arcface人脸识别这条主流路线。加载公开预训练的ArcFace模型,用它的512维输出替换TinyCNN的embedding,底库和识别逻辑一行不用改,只改模型加载和特征维度参数,识别率通常会有质的提升。这也是说“分类训练”是demo路径的原因——生产路径现在基本都是ArcFace加大规模预训练。
第二条是让系统支持多人同时在画面里。做法是每帧输出多张人脸框,给每一路识别出的身份加一个简单的IOU跟踪,连续3帧判定为同一人才写考勤,避免一个员工在镜头前晃一下就刷出好几条重复记录。这条线工作量不大,但离真实考勤场景近了一大步。
第三条是往人脸识别门禁机的方向靠:把模型做小、量化到INT8,换嵌入式推理框架,再把活体检测加上。这一步的目标是让整套系统跑在低成本算力板上,而不是靠一台电脑。到这一步,当初这个demo剩下的就只有UI和考勤规则了。
我回看自己最早做考勤demo时的记录,最大的教训不是模型没训好,而是把90%的时间花在刷准确率上,却没留时间去想“谁会拿着手机照片来打卡”“摄像头装哪个角度最合理”这类系统问题。现在我做任何模型demo,都会先写验收标准再写训练代码,这个习惯救了我好几次。这个方向最值得投入的地方,恰恰是那些demo默认不作数的部分:活体、多人、数据质量。希望帮到你。
本文还有配套的精品资源,点击获取