简介:这一项目将人脸识别技术与考勤打卡场景结合,为具备一定Python基础、希望掌握计算机视觉与Web开发集成的开发者,提供了一套结构完整、可直接参考的实践案例。资源共118个文件,约99.59MB,主体为52个Python源码文件,另有14个pyc编译文件、13个txt文本、多张jpg/png图片以及pb模型、TensorBoard日志等,涵盖人脸检测、特征提取、前端界面和后端服务等模块。目前已有585人学习下载。包内提供facenet相关模型与脚本、多张测试人像及TensorBoard日志等,便于对照源码理解MTCNN/FaceNet等模型的应用方式;后端结合Flask等框架,展现出数据库交互与API设计的常见写法,对学习人脸识别落地和Web系统搭建都有直接参考价值。对于希望快速搭建类似系统或深入探究深度学习人脸识别落地细节的读者,这份资源兼具代码参考与排错价值。
1. 人脸识别打卡系统:从 facenet-master 到一个能用的刷脸考勤
这几年人脸识别门禁机到处都在装,但真自己动手做一套“人脸识别门禁系统设计”,拿到的资料多半跑不通。这个“人脸识别打卡.zip”不是那种大而全的企业级平台,而是一套把 FaceNet 深度模型、OpenCV 图像处理和前后端打卡逻辑串起来的最小可运行系统。压缩包里没有完整源代码,核心是 facenet-master 模型库加一批员工人脸样本:宗.jpg、苏.jpg、58.jpg、47.jpg,还有测试用的 show.jpg 和 cam.jpg,面向的是“算法落地”这个环节——你要做的是把模型跑起来,让摄像头拍到的人脸能对上号、记下时间。适合哪些人?有一定 Python 基础、想从“会调库”跨到“能跑通一个完整人脸识别算法流程”的从业者,以及正在做考勤类毕业设计的人。
2. 把 facenet-master 变成可运行环境:依赖选型与最小实现
2.1 为什么用 FaceNet 而不是 OpenCV 的 Haar 级联
打开压缩包你会看到 .iml 文件,这是 PyCharm 的工程标记,但也直接说明了项目作者用的是 Python 开发环境。整个识别链路里最关键的一个选择是:人脸检测用 OpenCV 的 Haar Cascade,特征提取用 FaceNet。
这两个东西不是替代关系,而是分工关系。Haar Cascade 是传统视觉算法,速度快、开销小,用来在画面上框出人脸的位置;FaceNet 是深度学习模型,把检测到的人脸区域转换成 128 维的特征向量。为什么非要用这种方法组合?因为直接拿 Haar Cascade 做识别根本不可靠,光照稍微变一下,同一个人的检测结果就天差地别。而 FaceNet 不同,它训练时的核心逻辑是让同一个人的特征向量距离尽量小、不同人的向量距离尽量大。所以即使照片角度偏了、光线暗了,只要特征向量距离在阈值范围内,依然能识别出来。
常见做法是配合 MTCNN 做人脸对齐,FaceNet 官方仓库里也集成了 MTCNN 的调用接口。但在这个项目里, MTCNN 不是必须的——样本照片是正面照居多,OpenCV 的检测器够用,还能省掉一大笔推理耗时。我的做法是先用 OpenCV 框人脸,再把裁剪后的人脸区域送入 FaceNet 生成向量,整套流程在普通笔记本 CPU 上也能跑到每秒 2 帧左右的识别频率。
2.2 环境搭建:Python 版本、依赖包与测试推理
先把解压后的目录结构理顺。你拿到的 zip 解压后会看到 facenet-master 这个 PyTorch(或 TensorFlow)版本的模型目录,还有若干 jpg 图片。注意,facenet-master.iml 只是 IDE 的工程配置文件,不具备在命令行下直接运行的能力。我一般会新建一个 app.py 放在外层目录,然后手动安装依赖。
常见依赖组合是:
pip install opencv-python==4.5.5.64 pip install torch==1.10.0 pip install facenet-pytorch==2.5.3 pip install numpy flask flask-cors这里我用的是 facenet-pytorch 这个库,而不是从源码编译 FaceNet,理由很简单:它把 Inception ResNet v1 网络结构、预训练权重和 MTCNN 检测器都封装好了,直接 load 进来就能用。torch 版本 1.10.0 是目前兼容性最稳的一档,高版本 torch 在部分 Windows 机器上会跟 OpenCV 的依赖产生 DLL 冲突,没必要追新。
装完之后,先别急着接摄像头,我习惯先跑一张静态图验证模型链路:
from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image # 初始化检测器和特征提取器 mtcnn = MTCNN(image_size=160, margin=0, keep_all=False) model = InceptionResnetV1(pretrained='vggface2').eval() # 读取测试图 img = Image.open('show.jpg') # 检测人脸并返回裁剪后的 160x160 张量 face_tensor = mtcnn(img) if face_tensor is not None: # 生成 512 维特征向量(这里实际是 512 维) embedding = model(face_tensor.unsqueeze(0)) print('Embedding shape:', embedding.shape)这一步的关键参数有两个:MTCNN 的 image_size 设为 160,是 FaceNet 论文中规定的输入尺寸,改大会导致向量质量下降;keep_all 设为 False,表示只取画面中最大的一张人脸,适合打卡这种单人工位场景。如果你的考勤机要同时识别多个人,这里改成 True,后面做匹配时就要多套一层循环。
跑通之后你会看到输出一个如[1, 512]的张量,这就是特征向量。到这一步,模型链路已经通了,接下来要解决的问题是:怎么把员工照片变成可查询的向量库。
3. 照片预处理与员工注册:从单人照片到可用的 embedding 库
3.1 批量提取图片中的人脸向量:中文文件名与路径处理
项目压缩包里的照片有两种命名形式:中文名(宗.jpg、苏.jpg)和数字名(58.jpg、47.jpg、101.jpg)。中文文件名在 OpenCV 里有坑——cv2.imread 遇到中文路径会直接返回 None,而且不报错,很多新手在这里翻车。我平时处理这种情况会绕开 cv2.imread,用 np.fromfile + cv2.imdecode 组合代替。
批量生成员工 embedding 的脚本可以这样写:
import os import numpy as np import cv2 from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image mtcnn = MTCNN(image_size=160, margin=0, keep_all=False) model = InceptionResnetV1(pretrained='vggface2').eval() # 存放照片的目录 img_dir = 'faces/' # 返回 {文件名: 512维向量} embeddings = {} for fname in os.listdir(img_dir): if not fname.endswith('.jpg'): continue # 用 np.fromfile 解决中文名路径读不到的问题 img_bgr = cv2.imdecode(np.fromfile(os.path.join(img_dir, fname), dtype=np.uint8), cv2.IMREAD_COLOR) # BGR 转 RGB,PIL 打开 img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(img_rgb) face_tensor = mtcnn(pil_img) if face_tensor is None: print(f'Warning: no face detected in {fname}') continue emb = model(face_tensor.unsqueeze(0)).detach().numpy().flatten() # 用文件名去扩展名作为人员标识 name = os.path.splitext(fname)[0] embeddings[name] = emb print(f'{name}: embedding dim={emb.shape[0]}') # 保存为字典文件,后续打卡查询直接用 np.save('embeddings.npy', embeddings)逻辑说明:第一段把 OpenCV 读到的 BGR 图像转成 RGB,因为 PIL 和 PyTorch 模型都按 RGB 通道输入;第二段调用 MTCNN 检测人脸并裁剪,返回一个 1×3×160×160 的张量;第三段把它喂给 FaceNet 模型,输出 512 维向量后转成 numpy 数组存储。文件名直接用人员姓名,后续做匹配时能直接映射到人。
参数上需要注意的是,margin 参数控制人脸周围保留多少像素边缘。在打卡场景下我一般设为 0 或 20,不大会影响识别,但 margin 太大容易混入背景噪声。如果后续发现识别不稳定,可以优先调这个参数。
3.2 建立一个简单的“人脸底库”结构
生成的 embeddings.npy 是一个 Python 字典序列化后的文件,键是人员名,值是 512 维向量。这种存储方式对原型系统够用,但如果你打算做成一个长期跑的服务,我建议你把向量存进 SQLite 或 MySQL。常见做法是建一张表:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER PRIMARY KEY | 自增主键 |
| name | VARCHAR(64) | 人员姓名 |
| embedding | BLOB | 512 维 float32 二进制 |
| created_at | DATETIME | 注册时间 |
把 numpy 向量转成二进制存入 BLOB 字段,查询时取出来用 np.frombuffer 还原成数组。这个做法的好处是:后续新增员工不需要重新训练模型,只要拍一张新照片,跑一遍上面的向量提取,插入一行记录即可。整个注册流程走下来,单人耗时不到 2 秒。
另外一个实务提醒:embedding 文件很安静,它不会告诉你哪张照片质量差。我每次批量处理完会顺手打印每张图的向量标准差,如果某个人的向量标准差特别小(比如接近 0),说明照片可能是一张纯色图或检测到了错误区域,需要重新采集。这个细节能省下后面排查“为什么这个人总是识别失败”的很多时间。
4. 实时打卡链路:cam.jpg 到考勤记录,阈值怎么设才算稳
4.1 摄像头捕获与推理:cv2.VideoCapture 的帧处理循环
打包里的 cam.jpg 是一张从摄像头采集的测试帧,用于在没有物理摄像头的情况下验证识别流程。从单张图到实时视频流,核心改动是把读取图片这一行换成 VideoCapture,并加入帧抽稀逻辑,避免每帧都跑推理导致 CPU 占用拉满。
import cv2 import numpy as np from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image # 载入底库 embeddings = np.load('embeddings.npy', allow_pickle=True).item() # 反向映射:向量 -> 姓名 known_names = list(embeddings.keys()) known_embs = np.array([embeddings[n] for n in known_names]) mtcnn = MTCNN(image_size=160, margin=0, keep_all=False) model = InceptionResnetV1(pretrained='vggface2').eval() cap = cv2.VideoCapture(0) # 把摄像头分辨率设为 640x480,够用且推理压力小 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) frame_count = 0 while True: ret, frame = cap.read() if not ret: break # 抽帧:每 3 帧处理一次,留出推理时间 frame_count += 1 if frame_count % 3 != 0: continue rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(rgb) face = mtcnn(pil_img) if face is not None: emb = model(face.unsqueeze(0)).detach().numpy().flatten() # 用余弦相似度或欧氏距离做比对,这里用欧氏距离 dists = np.linalg.norm(known_embs - emb, axis=1) min_idx = np.argmin(dists) # 阈值 0.8,小于阈值才算匹配成功 if dists[min_idx] < 0.8: name = known_names[min_idx] print(f'识别成功: {name}, 距离={dists[min_idx]:.3f}') else: print(f'未匹配到员工, 最小距离={dists[min_idx]:.3f}') # 在画面上把检测到的人脸框出来(示例略) cap.release()参数设定上,这里最核心是阈值 0.8。我用的是欧氏距离,FaceNet 训练出的特征向量经过 L2 归一化后,同一人不同照片的距离一般在 0.4~0.8 之间,不同人通常大于 1.0。但具体阈值还是要按你的底库实测,先用收集的员工照片两两比对,画出最远类内距离和最近类间距离,阈值取二者中间偏宽松的位置。
4.2 从识别结果到考勤记录:Flask 接口与时间戳落库
单机版的识别循环能跑通,但真正打卡系统需要把结果暴露给前端或门禁硬件。我在项目里常在南端接一个 Flask 服务,通过 POST 请求上传一张人脸图片,服务端返回识别姓名和当前时间戳。用 Flask 的好处是开发快,跟现有代码同属 Python 技术栈,不用像 Django 那样配一堆中间件。
from flask import Flask, request, jsonify from datetime import datetime import numpy as np import sqlite3 app = Flask(__name__) # 全局加载底库与模型(略),伪代码示意 # embs_dict = np.load('embeddings.npy', allow_pickle=True).item() @app.route('/attendance', methods=['POST']) def attendance(): # 接收前端上传的图片文件 file = request.files['image'] # 交给识别函数,返回姓名 name = recognize(file.read()) now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 写入 SQLite 考勤表 conn = sqlite3.connect('attendance.db') conn.execute('INSERT INTO records (name, time) VALUES (?, ?)', (name, now)) conn.commit() conn.close() return jsonify({'name': name, 'time': now})这里有几个工程细节值得说。第一,实际项目里不能用图片文件名当员工姓名的主键,因为重名或改名会导致历史记录断裂,建议用自增 ID。第二,记录打卡时间用的 now 是服务器时间,如果前端和服务器不在同一时区,需要统一用 UTC 存储再在展示层转本地时间。第三,重复打卡逻辑没写进去,实际还要加一个时间窗口判断,比如同一员工 5 分钟内不允许重复打卡。
从识别到落库的完整链路到这里就通了。但我必须提醒你:识别成功和识别准确是两回事。很多项目卡在阈值校准上,接下来我把这几类高频问题集中梳理一遍。
5. 避坑:人脸识别打卡项目里最常见的五个翻车现场
5.1 中文文件名导致 OpenCV 静默失败,连报错都不给
现象:读入宗.jpg 时,变量变成 None,程序继续往下跑但识别永远失败,或者直接抛出cv2.error: !_src.empty()。
原因:OpenCV 的 imread 底层调用的是 C 标准库的 fopen,不支持 Windows 下的中文路径编码。它失败后返回空指针。
解决:改用cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)读入,写文件则用cv2.imencode('.jpg', img).tofile(path)。这个替代方案可以同时在读和写两个方向规避中文路径问题。
5.2 MTCNN 把非人脸区域也框了进来
现象:一张桌面上放了个人形立牌的照片,被 MTCNN 当作真正的人脸提取,注册时生成了一条错误的 embedding。
原因:MTCNN 的边界框回归在遮挡、侧面、暗光条件下会给出低置信度的检测结果,而默认的置信度阈值 0.9 挡不住所有误检。
解决:批量处理时打印 MTCNN 返回的概率值,把低于 0.95 的样本过滤掉。正对镜头的高清照片通常能到 0.98 以上,取 0.95 作为硬阈值是常见做法。还可以在 MTCNN 初始化时传入select_largest=False,避免误导框选到背景里的远距离人脸。
5.3 欧氏距离阈值设死,换光线就误识别人
现象:上午测试识别率 100%,下午换了工位背光,出现陌生访客被识别成老员工。
原因:FaceNet 的向量在光照变化下会漂移,你在暗环境下算出来的类内距离可能是 0.5,到了逆光环境变成 1.2。固定阈值 0.8 在这种场景下必然出错。
解决:采集每个员工至少 3 个角度、3 种光照下的照片各算一次 embedding 存入底库,匹配时取与底库所有向量的最小距离。这比单一阈值要鲁棒得多,相当于给每个人建了一个小型的向量簇。
5.4 只处理单张最大人脸,多人同框导致识别错乱
现象:摄像头画面上同时出现两个人,远一点的那位拿到了打卡结果,站在镜头前的人反而被忽略。
原因:MTCNN 默认只返回置信度最高的人脸,而置信度又跟脸的大小、清晰度强相关,不是你想象中“最近的人优先”。
解决:把 keep_all 设为 True,遍历返回的所有人脸分别提取向量并匹配。打卡场景通常还要接一个简单的面积排序逻辑,取画面中面积最大的脸作为目标。这个方法在门禁机上很常见。
5.5 误用了未压缩的 zip 内隐藏文件
现象:解压“人脸识别打卡.zip”后找不到源代码文件,只有 .iml 和 jpg,怀疑资源不完整。
原因:部分开发者在打包时会误把 .idea 目录或 PyCharm 工程标记包含进来,而真正的核心代码可能在另一个文件夹里,或者压根就没打包进去。
解决:先用unzip -l查看完整清单,确认有没有被子目录隐藏的 py 文件。如果确实没有源码,这个资源提供的是模型依赖和测试素材,你需要按上面的流程自己补齐业务代码,这反而比拿来就跑更锻炼人。
6. 复现验证与进阶方向:用批量回放代替盲改参数
整个项目跑通之后,我建议你做一次“批量回放验证”,而不是靠肉眼盯着摄像头试。把 160608131423034.jpg、show.jpg、cam.jpg 这些测试图统一编号,写一个循环脚本,把每张图的识别结果和期望结果做成对照表:
| 测试文件 | 期望结果 | 实际结果 | 最小欧氏距离 |
|---|---|---|---|
| 58.jpg | 员工A | 员工A | 0.43 |
| 47.jpg | 员工B | 员工B | 0.52 |
| show.jpg | 未注册 | 未匹配 | 1.37 |
| cam.jpg | 员工B | 员工B | 0.61 |
如果 show.jpg 被误识别成某个已注册员工,说明阈值偏大,往下调;如果 cam.jpg 识别失败,说明你的底库照片与实际环境光差异太大,要补采集。这一步的意义在于:把“感觉准不准”变成“数字准不准”,后续调参有据可依,不用靠玄学。
再往深走一步,可以关注两个方向。一是为每张打卡照片保存当时的视频帧,用于人工复核异常记录,这能显著提升考勤数据的可信度;二是把 FaceNet 换成 ArcFace 或 CosFace,这类角度损失函数在近两年的公开数据集上 accuracy 普遍比 FaceNet 高 2~3 个百分点,代价是模型文件更大、推理耗时多几十毫秒,对门禁一体机的算力要求更高。
从那以后,我每次拿到这类打包资源,都会强制自己先做一遍“清单核对 + 小样本验证”再上手改代码。人脸识别项目最大的风险不是算法选错,而是你根本不确定拿到的数据能不能跑出一条有效链路。希望帮到你。
本文还有配套的精品资源,点击获取