简介:这是一份面向高校学生与深度学习入门者的课程设计级人脸识别考勤演示项目,基于卷积神经网络实现人脸检测与签到流程,适合作为人工智能、机器学习相关课程的实践参考或毕业设计起步模板。压缩包共30个文件,约32.54MB,以Python源码为主体,包含11个py脚本与6个pyc编译文件,另有3个ui界面文件、3张jpg测试图片、1个ttf字体、1个caffemodel模型、1个prototxt网络定义、1个xml人脸检测器及说明文档等,覆盖从模型加载、人脸搜索、摄像头采集到签到与录入的完整模块。项目结构清晰,模型与界面资源分离,便于按功能定位代码。目前已有173人学习下载。读者可借此理解CNN人脸识别在考勤场景中的落地方式,掌握模型调用、界面交互与数据保存等关键环节,并参考其目录组织与排错思路,快速搭建自己的课程设计或实验demo。
1. 从一张 zip 包说起:CNN 人脸识别考勤 demo 到底能跑出什么
很多人第一次拿到「基于cnn神经网络人脸识别实现的考勤demo项目.zip」这类压缩包,心态是矛盾的:既想赶紧跑通看效果,又怕里面是一堆跑不起来的死代码。我拆过不少类似的项目,结论很直接——这类 demo 的价值不在于它多准,而在于它把「人脸检测 → 特征提取 → 比对识别 → 考勤记录」这条链路完整串了一遍,让你知道每个环节的数据长什么样、模型在哪一步介入、打卡逻辑挂在哪里。
它适合两类人:一类是想快速理解 CNN 在人脸识别里到底干了什么的新手,另一类是想拿它当骨架、换成自己数据集做二次开发的工程师。你不需要先精通卷积神经网络,但得能看懂 Python、会装依赖、知道 OpenCV 是干嘛的。这篇笔记就按「先立住原理、再动手复现、最后讲坑」的顺序,把这类 demo 从解压到跑通再到改造的路径讲清楚,参数怎么调、哪里容易翻车,我都会给到具体值。
2. CNN 人脸识别考勤 demo 的技术骨架:从像素到工号
2.1 为什么人脸识别绕不开卷积神经网络
人脸识别本质上是一个「把同一张脸的不同照片映射到相近向量、把不同脸映射到远离向量」的问题。传统方法用 Haar 特征加 LBPH 做识别,在光照均匀、正脸、无遮挡的实验室条件下能到 90% 以上,但换到考勤场景——早上逆光、中午顶光、有人戴眼镜有人刚剪头发——准确率会断崖式下跌。卷积神经网络之所以成为主流,是因为它的卷积核能自动学到边缘、纹理、五官局部结构这些对光照和姿态更鲁棒的特征,而不是靠人工设计的规则。
一个典型的 CNN 人脸识别流程分四步:第一步用人脸检测器(常见是 MTCNN 或 OpenCV 的 DNN 模块)把画面里的人脸框出来并做对齐;第二步把对齐后的人脸裁剪缩放到固定尺寸,比如 112×112 或 160×160;第三步送进 CNN 主干网络提取特征向量,常见的有 FaceNet、ArcFace 这类结构,输出一个 128 维或 512 维的 embedding;第四步用余弦相似度或欧氏距离跟底库里已注册的人脸特征比对,超过阈值就判定为同一个人,然后写一条考勤记录。
提示:demo 项目为了降低门槛,经常把检测和识别合并成一个轻量模型,或者直接用分类网络(比如把每个员工当一个类别)来训练。分类思路在员工数量少时能跑,但新增一个人就要重新训练,工程上不推荐。
2.2 一个可复现的目录结构与依赖清单
拿到 zip 包后别急着双击运行,先看目录。这类 demo 的典型结构是:data/放人脸底库图片,按人名建子文件夹;models/放预训练权重文件;src/或根目录放detect.py、train.py、recognize.py、attendance.py;根目录有requirements.txt和README.md。如果压缩包里没有权重文件,只有代码,那你需要自己下载对应的预训练模型,这一步是最容易卡住的。
依赖方面,核心就几个:opencv-python负责图像读写和检测,numpy做矩阵运算,torch或tensorflow跑 CNN,scikit-learn做聚类或相似度计算,pandas写考勤表。下面是我一般会先执行的依赖安装命令,版本我锁在比较稳的组合上:
# 创建独立环境,避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖,torch 按自己显卡情况选 CPU 或 CUDA 版本 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3 pip install torch==2.0.1 torchvision==0.15.2 pip install scikit-learn==1.3.0 pip install pandas==2.0.3这里opencv-python用 4.8 系列是因为它的 DNN 模块对 ONNX 格式的人脸检测模型支持稳定;numpy锁 1.24 是为了避开 1.25 之后跟部分旧版 torch 的 ABI 冲突;torch选 2.0.1 是兼顾新特性和社区教程的兼容性。如果你没有 NVIDIA 显卡,把 torch 那行换成 CPU 版本即可,识别速度会慢,但跑通 demo 没问题。
2.3 人脸检测与对齐:别跳过这一步
很多 demo 翻车就翻在检测环节。我见过最省事的写法是直接用整张图送进 CNN,结果背景、衣服、灯光全被当成特征,底库稍微换张照片就认不出来。正确做法是先检测再对齐。OpenCV 自带的人脸检测器有两种常用选择:Haar 级联和 DNN 模块加载 Caffe 或 ONNX 模型。Haar 快但误检多,DNN 慢一点但准得多,考勤场景建议用 DNN。
import cv2 import numpy as np # 加载 OpenCV DNN 人脸检测器,权重文件需提前下载到 models/ 目录 net = cv2.dnn.readNetFromCaffe( "models/deploy.prototxt", "models/res10_300x300_ssd_iter_140000.caffemodel" ) def detect_faces(image_path, conf_threshold=0.6): img = cv2.imread(image_path) h, w = img.shape[:2] # 构造 300x300 的 blob,做均值减法,这是 SSD 检测器的标准输入 blob = cv2.dnn.blobFromImage( cv2.resize(img, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) net.setInput(blob) detections = net.forward() faces = [] for i in range(detections.shape[2]): confidence = detections[0, 0, i, 2] if confidence > conf_threshold: box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype("int") # 边界裁剪,防止坐标越界导致后续裁剪报错 x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w, x2), min(h, y2) faces.append((x1, y1, x2, y2)) return img, facesconf_threshold设 0.6 是经验值:低于 0.5 会把墙上的海报、相框误检成人脸;高于 0.8 在侧脸或戴口罩时会漏检。blobFromImage里的(104.0, 177.0, 123.0)是 SSD 训练时用的 BGR 均值,不能随便改,改了检测框会偏移。检测到的人脸框要再做一次对齐,通常是根据两眼坐标做仿射变换,把脸摆正,这一步能明显提升后续识别率。
3. 把 demo 跑起来:注册、训练、识别、打卡四步走
3.1 底库注册:每人至少 5 张,角度要散开
考勤系统的底库就是「谁是谁」的答案。demo 里一般要求你在data/下按人名建文件夹,每个文件夹放这个人的照片。我踩过的坑是:有人只放一张正脸证件照,结果现场打卡时稍微低头就认不出。稳妥的做法是每人至少 5 张,覆盖正面、左转 15 度、右转 15 度、轻微仰头、轻微低头,光照尽量一致。照片格式统一成 jpg,尺寸不用太大,长边 640 就够,太大只会拖慢检测。
import os import cv2 import numpy as np def build_face_database(data_dir, output_path="models/face_db.npz"): embeddings = [] labels = [] # 遍历 data/ 下每个人名文件夹 for person_name in sorted(os.listdir(data_dir)): person_dir = os.path.join(data_dir, person_name) if not os.path.isdir(person_dir): continue for img_name in os.listdir(person_dir): img_path = os.path.join(person_dir, img_name) img, faces = detect_faces(img_path) if len(faces) != 1: # 检测不到或多张脸都跳过,避免脏数据进底库 print(f"跳过 {img_path},检测到 {len(faces)} 张脸") continue x1, y1, x2, y2 = faces[0] face = img[y1:y2, x1:x2] face = cv2.resize(face, (112, 112)) # 这里调用 CNN 主干提取 128 维特征,函数见下一节 emb = extract_embedding(face) embeddings.append(emb) labels.append(person_name) np.savez(output_path, embeddings=np.array(embeddings), labels=np.array(labels)) print(f"底库构建完成,共 {len(labels)} 条记录")这段代码的关键点是「检测不到或多张脸就跳过」。很多人为了凑数量把模糊的、侧脸过度的照片也塞进去,结果底库特征分布散乱,识别时反而拉低准确率。output_path存成 npz 格式,加载快,也不用依赖数据库。如果员工超过 50 人,建议换成 FAISS 做向量检索,npz 线性比对会变慢。
3.2 特征提取:CNN 主干怎么选、输出维度怎么定
demo 里常见的 CNN 主干有三种:自己搭的小型卷积网络、MobileNet 轻量版、FaceNet 的 Inception-ResNet 结构。自己搭的网络层数少、训练快,但特征判别力弱,适合纯演示;MobileNet 在速度和精度之间平衡好,适合边缘设备;FaceNet 精度高但模型大,CPU 上跑一张脸要几百毫秒。我一般先用 MobileNet 跑通流程,再按需换。
import torch import torch.nn as nn from torchvision import models class FaceEmbedding(nn.Module): def __init__(self, embedding_dim=128): super().__init__() # 用 MobileNetV2 做主干,去掉原分类头 backbone = models.mobilenet_v2(pretrained=True) self.features = backbone.features self.pool = nn.AdaptiveAvgPool2d(1) # 全连接层把 1280 维特征压到 128 维 embedding self.fc = nn.Linear(1280, embedding_dim) self.bn = nn.BatchNorm1d(embedding_dim) def forward(self, x): x = self.features(x) x = self.pool(x) x = torch.flatten(x, 1) x = self.fc(x) x = self.bn(x) # L2 归一化,让余弦相似度等价于内积 return nn.functional.normalize(x, p=2, dim=1) def extract_embedding(face_bgr): model = FaceEmbedding().eval() # BGR 转 RGB,归一化到 [0,1],再减均值除标准差 face_rgb = cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB) / 255.0 mean = np.array([0.485, 0.456, 0.406]) std = np.array([0.229, 0.224, 0.225]) face_norm = (face_rgb - mean) / std tensor = torch.from_numpy(face_norm).permute(2, 0, 1).float().unsqueeze(0) with torch.no_grad(): emb = model(tensor).numpy().flatten() return embembedding_dim设 128 是 FaceNet 论文的经典值,再低会损失判别力,再高在小规模底库上收益不明显还费存储。pretrained=True表示用 ImageNet 预训练权重,如果你有足够的人脸数据,可以换成在人脸数据集上预训练的权重,识别率会明显提升。L2 归一化那一步不能省,否则余弦相似度计算会受向量模长干扰。
3.3 识别与打卡:阈值怎么定、考勤表怎么写
识别就是拿现场人脸的特征向量跟底库里每条记录算余弦相似度,取最高分,超过阈值就判定为对应员工。阈值定多少直接决定「认错」和「认不出」的平衡。我实测下来,128 维 MobileNet 特征在 5 张底库照片的条件下,阈值 0.6 比较稳:低于 0.55 容易把别人认成你,高于 0.7 你自己换个表情就刷不上。
import pandas as pd from datetime import datetime def recognize_and_log(face_emb, db_path="models/face_db.npz", threshold=0.6, log_path="attendance.csv"): db = np.load(db_path, allow_pickle=True) embeddings = db["embeddings"] labels = db["labels"] # 余弦相似度 = 两个 L2 归一化向量的内积 sims = embeddings @ face_emb best_idx = np.argmax(sims) best_score = sims[best_idx] if best_score < threshold: return None, best_score person = labels[best_idx] # 写考勤记录,同一天同一人只记一次 now = datetime.now() record = {"name": person, "time": now.strftime("%Y-%m-%d %H:%M:%S"), "score": round(float(best_score), 4)} df = pd.DataFrame([record]) try: old = pd.read_csv(log_path) # 去重:同一天同一人已有记录就跳过 today = now.strftime("%Y-%m-%d") if not ((old["name"] == person) & (old["time"].str.startswith(today))).any(): df.to_csv(log_path, mode="a", header=False, index=False) except FileNotFoundError: df.to_csv(log_path, index=False) return person, best_scorethreshold=0.6是起点不是终点,你要拿真实场景的照片测:让每个员工在不同光线、不同角度下刷 10 次,统计通过率和误识率,再微调。考勤表用 CSV 存,字段就name、time、score三个,score留着方便事后排查误识。去重逻辑按「同一天同一人只记一次」写,避免一个人站在摄像头前被连续识别导致刷屏。
4. 避坑与排查:demo 跑不通的 5 个真实原因
4.1 现象:一运行就报ModuleNotFoundError: No module named 'cv2'
原因通常不是没装,而是装到了系统 Python 而不是虚拟环境里,或者装了opencv-python-headless但代码里用了cv2.imshow需要 GUI 支持。解决:先which python确认当前解释器路径在 venv 下,再pip list | grep opencv看是否真的装了。如果要在无显示器的服务器上跑,把cv2.imshow相关代码注释掉,改用cv2.imwrite存图排查。
4.2 现象:检测框位置整体偏移,框到了额头或下巴
原因多半是blobFromImage的均值参数写错了,或者输入图像没有做 BGR 到 RGB 的转换。SSD 检测器训练时用的是 BGR 顺序加(104, 177, 123)均值,你如果传了 RGB 图或者均值写成(0, 0, 0),检测框就会漂。解决:严格按 2.3 节的代码写,均值不要改,输入保持cv2.imread读出来的 BGR 格式。
4.3 现象:底库照片识别率很高,现场摄像头就是认不出
原因通常是训练和推理的预处理不一致。底库照片可能是正面、均匀光照,现场摄像头有逆光、运动模糊、分辨率低。解决:第一,底库照片尽量用和现场同一摄像头拍;第二,现场识别前先做直方图均衡化,缓解光照差异;第三,把阈值从 0.6 降到 0.55 试试,但降完要重新测误识率。我一般会保留一个「调试模式」,把每次识别的最高分和次高分都打印出来,方便判断是阈值问题还是特征问题。
4.4 现象:考勤表里同一个人一天出现几十条记录
原因是去重逻辑没写对,或者识别循环没有加时间间隔。解决:按 3.3 节的写法,用pandas读已有记录,判断「同一天同一人」是否已存在。另外在摄像头循环里加time.sleep(1),别让程序每帧都识别一次,既浪费算力又容易刷记录。
4.5 现象:换了一台机器,模型加载报RuntimeError: Error(s) in loading state_dict
原因是保存模型时用了torch.save(model)整个对象,换机器后类定义路径变了或者 torch 版本不一致。解决:永远只保存model.state_dict(),加载时先实例化同样的网络结构,再load_state_dict。如果 torch 版本跨了大版本,比如 1.x 到 2.x,部分层的参数名可能变,需要手动映射或重训。
5. 从 demo 到能用:把识别准确率再往上推的技巧
demo 跑通只是起点,真要放到考勤场景用,还得解决「底库更新」和「阈值自适应」两个问题。底库更新我一般做成增量式:新员工入职时,用同一套检测对齐流程提取特征,直接追加到 npz 文件里,不用重训模型。追加时注意保持特征维度和归一化方式一致,否则新旧特征没法比对。
阈值自适应是个更实用的技巧。固定阈值在不同摄像头、不同光照下表现差异很大。我的做法是给每个员工单独存一个「类内距离」统计:注册时用他本人的多张照片两两算相似度,取最小值作为该员工的个性化阈值下限。识别时,如果最高分对应的员工是他,且分数高于他的个性化下限,就通过;如果最高分对应的是别人,但分数低于那个人的下限,就拒绝。这样能明显降低「把 A 认成 B」的概率。
def build_personal_thresholds(db_path="models/face_db.npz", output_path="models/thresholds.npz"): db = np.load(db_path, allow_pickle=True) embeddings, labels = db["embeddings"], db["labels"] thresholds = {} for name in set(labels): idxs = np.where(labels == name)[0] if len(idxs) < 2: thresholds[name] = 0.6 # 样本太少用默认值 continue # 同一人内部两两相似度 intra_sims = [] for i in range(len(idxs)): for j in range(i + 1, len(idxs)): intra_sims.append(float(embeddings[idxs[i]] @ embeddings[idxs[j]])) # 取最小类内相似度再减 0.05 作为该员工的通过阈值 thresholds[name] = max(0.5, min(intra_sims) - 0.05) np.savez(output_path, **thresholds) return thresholds这段代码的逻辑是:同一个人不同照片之间的相似度如果最低都有 0.75,那阈值设 0.7 就能通过;如果最低只有 0.62,阈值就得降到 0.57,否则本人刷不上。max(0.5, ...)是兜底,防止阈值被拉到太低导致误识。实际用的时候,识别函数里把固定threshold换成thresholds.get(person, 0.6)即可。
最后说个我自己的习惯:每次改完参数,一定拿一个「回归测试集」跑一遍——这个测试集包含每个员工的 3 张现场照和 3 张非员工的干扰照,统计通过率和误识率。没有这个习惯,调参就是玄学,今天调好了明天换个光线又翻车。这套 demo 骨架不大,但把检测、对齐、特征、比对、记录五个环节都留了接口,你顺着改比从零搭省事得多。希望帮到你。
本文还有配套的精品资源,点击获取