
简介一套基于Python与OpenCV、dlib及Face_recognition库构建的人脸识别签到系统源码包适合计算机视觉初学者、Python开发者以及需要快速实现考勤或门禁场景的在校学生与工程技术人员。系统涵盖人脸检测、特征提取、实时识别、GUI界面、签到数据记录与导出等完整流程并包含create_dataset.py、Face_login等模块文件可帮助理解从数据集构建到实时签到落地的全链路实现思路。资源共20个文件以py源码为主辅以pyc编译文件、xml配置文件、jpg/jpeg示例图片、txt说明和md文档整体压缩包仅95KB结构紧凑、易于查阅。目前已有3988人学习下载适合希望对照完整代码进行二次开发或课程设计的读者。通过阅读readme.md与运行示例能够快速掌握摄像头调用、人脸特征向量保存npy文件以及签到信息处理等关键技巧为后续优化误识率、多目标识别和实时性能提供基础。1. 签到这事值得从点名换成一套人脸识别系统一个 30 人的班级或小团队早上的签到能花掉五分钟纸质名单要传、代签挡不住、HR 事后还得手动录入 Excel。刷卡和指纹能解决一部分但替刷、指纹磨损、设备成本都是实际问题。人脸识别签到系统的价值在于用一台普通笔记本加一个摄像头就能把「看一眼屏幕」变成一条带时间戳的签到记录全程离线、数据在自己手里、单条记录成本几乎为零。它不是什么高深课题本质就是「人脸特征比对 签到状态管理」两件事。这套东西特别适合三类人做毕设或课设的学生需要一个能演示、能写进论文的完整项目小公司或培训机构的行政不想买几千块的门禁机以及想入门人脸识别应用的 Python 开发者——因为 OpenCV 和 face_recognition 的组合已经把最难的识别部分封装成了几行调用。前提是你愿意花一个晚上把环境踩平再花一个下午调阈值。接下来的内容按落地顺序走先讲方案选型再给注册和签到的完整代码然后处理签到记录、去重和迟到判定最后是我实际跑项目时踩过的坑和一点进阶思路。2. 选型与环境离线人脸识别为什么比云 API 更适合签到场景2.1 三种主流方案的取舍做签到系统的第一步不是写代码而是选识别方案。市面上能直接用的方案大致分三类传统 OpenCV 人脸识别、基于 dlib 的深度学习特征提取、以及云厂商的人脸识别 API。传统 OpenCV 方案常见做法是 Haar Cascade 检测人脸再用 LBPH 或 EigenFace 训练分类器。优点是依赖少、代码短缺点是模型容量非常有限同一个人换个发型、戴个眼镜相似度就明显下降注册样本少的时候识别率低到没法实际使用。我在早期原型里试过 LBPH结论是只适合正面、固定光线的演示不适合直接做考勤。云 API百度、阿里、虹软等识别精度确实好多角度、遮挡、光线变化都能处理。但签到系统往往是每天高频调用的内部工具把所有人脸数据传到云端首先数据隐私这关就过不去其次每次识别都有网络延迟断网时整个签到流程直接瘫痪再者按次计费长期用也是一笔开销。所以我最后选定的是 dlib 的 ResNet 模型通过 face_recognition 库调用。这个方案离线运行、CPU 就能跑、对角度和光线变化的容忍度比传统方法高出一个档次识别一个人脸在普通 i5 上大约 0.1 到 0.2 秒签到场景完全够用。签到的核心逻辑不是「训练一个模型认出所有人」而是「提取每个人的 128 维人脸特征向量然后做距离比对」这也是 dlib 方案比传统分类器更灵活的地方新增一个人只需要往库里插入一条特征向量不用重新训练。2.2 Python 环境与依赖安装先装好 dlib 再装 face_recognition这套方案的依赖只有四个Python 3.8 到 3.11 均可OpenCV、dlib、face_recognition。如果你还没有 Python 环境直接去官网下载安装包安装时勾上「Add Python to PATH」这一条能省掉后面非常多的环境变量问题。Windows 上安装 dlib 是第一个坎。face_recognition 依赖 dlib而 dlib 在 Windows 上默认没有预编译 wheelpip 会尝试从源码编译这时如果你的机器没有 Visual Studio Build Tools 和 CMake会直接报错。我见过太多人卡在这一步最后换了 Python 版本才装上。在 Windows 上按这个顺序装pip install cmake pip install dlib pip install face_recognition pip install opencv-python关键点是要先装 cmake再装 dlib。如果你装 dlib 时看到error: Microsoft Visual C 14.0 or greater is required说明缺少 C 编译工具链去 Visual Studio 官网下载 Build Tools勾选「使用 C 的桌面开发」装完重启终端再执行上面的命令。macOS 和 Linux 会顺利很多直接pip install face_recognition opencv-python即可前提是系统里有 cmake。装完后用一段代码验证环境是否完整这一步能提前暴露 80% 的问题import cv2 import face_recognition import dlib print(OpenCV version:, cv2.__version__) print(dlib version:, dlib.__version__) print(face_recognition loaded:, face_recognition.__version__)这段验证代码的逻辑很简单三个库能同时 import 且打印出版本号说明底层依赖都齐了。最容易出问题的是 dlib它不打印版本而是直接抛 ImportError 时基本就是编译产物缺失或 VC 运行库没装好。2.3 两个核心参数先定下来比对阈值与签到时间窗环境装完先别急着写功能把参数定下来。人脸识别签到有两个关键参数比对阈值和签到时间窗口。比对阈值控制「多像才算同一个人」。face_recognition 的compare_faces默认 threshold 是 0.6距离越小越相似。但默认值是针对通用场景的签到场景要求低误识率我一般会把阈值收紧到 0.5 左右。具体做法是注册完一个人后用同一摄像头采集他不同角度的 10 张脸打印出每个角度到注册向量的距离值观察分布再定阈值。我自己测试时同一个人正常光线下的距离通常在 0.35 到 0.45 之间不同人在 0.6 以上所以 0.5 是一个比较安全的中间值。签到时间窗口解决的是「同一张脸在摄像头前停留 10 秒系统该记几次」的问题。常见做法是每天每人只允许一条签到记录第一次识别成功就锁定状态当天重复识别不再插入。另一种做法是设定一个冷却时间比如 5 分钟内不重复签到适合需要记录进出次数的场景。签到的业务是「一次有效」所以我用的是前者。这两个参数不要拍脑袋定等项目跑通后一定要用自己的摄像头采集真实样本重新校准这是人脸签到系统从「能跑」到「能用」的关键分界线。3. 把注册和签到跑通最小可复现的人脸识别签到流程3.1 人脸注册从摄像头抓脸并保存 128 维特征向量系统分为注册和签到两个阶段。注册阶段要做的事是打开摄像头检测画面里的人脸截取最大的一张提取 128 维特征向量连同姓名一起存入数据库。下面是注册脚本的核心部分import cv2 import face_recognition import sqlite3 import pickle import numpy as np def register_face(name): # 1. 打开摄像头0 代表默认摄像头 cap cv2.VideoCapture(0, cv2.CAP_DSHOW) if not cap.isOpened(): print(无法打开摄像头请检查设备索引) return ret, frame cap.read() cap.release() if not ret: print(读取摄像头画面失败) return # 2. OpenCV 读到的颜色是 BGR人脸识别库需要 RGB rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 3. 检测人脸并提取特征num_jitters5 表示对每张脸做 5 次抖动增强 face_locations face_recognition.face_locations(rgb_frame) if len(face_locations) 0: print(画面中没有检测到人脸请正对摄像头) return # 4. 取画面中面积最大的人脸 largest_face max(face_locations, keylambda loc: (loc[2]-loc[0]) * (loc[1]-loc[3])) face_encodings face_recognition.face_encodings( rgb_frame, known_face_locations[largest_face], num_jitters5) if len(face_encodings) 0: print(人脸特征提取失败) return # 5. 存入 SQLite特征向量用 pickle 序列化成 BLOB feature_bytes pickle.dumps(face_encodings[0].astype(np.float32)) conn sqlite3.connect(attendance.db) conn.execute(INSERT INTO students (name, face_encoding) VALUES (?, ?), (name, feature_bytes)) conn.commit() conn.close() print(f注册成功{name}特征向量维度 {face_encodings[0].shape})这段代码的流程是读一帧画面 → 转颜色格式 → 检测人脸 → 提取特征 → 入库。num_jitters5是为了提特征时对图像做轻微扰动再取平均得到的向量更稳定代价是耗时增加但注册是低频操作值得等这几秒。注意face_encodings接受known_face_locations参数意思是「我已经知道人脸在哪你只需要提取特征」。这样避免重复调用face_locations省掉一次检测开销。特征向量是 numpy 数组SQLite 不认所以用pickle序列化成字节串再存 BLOB读出来时再反序列化。cv2.CAP_DSHOW是 Windows 上打开摄像头的 DirectShow 后端能明显减少摄像头打开延迟Linux 和 macOS 上可以不传这个参数。3.2 执行签到比对特征并写数据库签到阶段的逻辑要复杂一些循环读取视频流每隔 N 帧做一次人脸检测检测到人脸后计算与库里所有注册向量的距离距离小于阈值的那一个就是匹配结果匹配成功则写入签到记录。import cv2 import face_recognition import sqlite3 import pickle import datetime def load_known_faces(): conn sqlite3.connect(attendance.db) rows conn.execute(SELECT id, name, face_encoding FROM students).fetchall() conn.close() known_faces [] for stu_id, name, blob in rows: encoding pickle.loads(blob) known_faces.append((stu_id, name, encoding)) return known_faces def check_in(): known_faces load_known_faces() if not known_faces: print(学生库为空请先注册) return cap cv2.VideoCapture(0, cv2.CAP_DSHOW) check_interval 5 # 每 5 帧检测一次 frame_count 0 while True: ret, frame cap.read() if not ret: break frame_count 1 if frame_count % check_interval ! 0: continue rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) face_locations face_recognition.face_locations(rgb_frame) for face_location in face_locations: query_encoding face_recognition.face_encodings( rgb_frame, known_face_locations[face_location])[0] for stu_id, name, known_encoding in known_faces: distance face_recognition.face_distance( [known_encoding], query_encoding)[0] if distance 0.50: mark_attendance(stu_id, name, distance) print(f签到成功{name}距离 {distance:.3f}) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里face_distance是核心它计算两张 128 维向量之间的欧氏距离返回一个列表下标对应传入的人脸编码顺序。这里有个容易踩坑的设计——我没有用compare_faces而是直接用face_distance拿原始距离值。原因是我需要打印距离来做调参依据compare_faces返回的是布尔值会丢掉距离信息。check_interval 5的意思是每 5 帧才做一次检测。摄像头一般 30 帧每秒也就是每秒检测 6 次已经足够覆盖正常走近摄像头的速度。这个参数别设成 1会让 CPU 占用率直接拉满而且连续帧之间人脸位置几乎没变化纯属浪费算力。签名函数mark_attendance是业务层的事下一章详细讲。3.3 签到判定参数阈值、检测间隔、重复签到怎么算上一段代码里出现的0.50就是比对阈值。它直接决定两件事低于它认为是同一个人高于它认为是陌生人。阈值设置不合理时可能会出现一个人识别出多个身份或者注册的人死活识别不了的玄学情况——实际上那不是玄学只是阈值和你的摄像头画质不匹配。检测间隔同样需要根据 CPU 性能调整。如果你用的是带核显的老笔记本check_interval可以提到 10牺牲一点识别延迟换来流畅的画面如果你用的是台式机5 是体验比较好的值。注意face_locations的计算量正比于画面尺寸VideoCapture默认的分辨率通常是 640x480这已经够用不要为了「看得更清楚」去调高分辨率人脸识别只需要 128 维特征不需要看清毛孔。重复签到的问题在代码层面是这样处理的mark_attendance内部检查「今天是否已有该学生的记录」有则直接丢弃本次识别结果。识别是一回事签到记录是另一回事两者必须分离——我在每个循环里还在print识别结果但只有第一次才真正写入数据库。这个设计在下一章展开。4. 签到记录与业务逻辑从识别成功到真正可用的签到表4.1 SQLite 表结构与签到状态机人脸识别只是拿到了「这个人是谁」签到系统的核心价值在于把「谁」和「什么时候」记录下来并算出签到状态。我用 SQLite 做存储不引入 MySQL因为单机使用、并发极低SQLite 足够且零运维。建表语句如下CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, face_encoding BLOB NOT NULL, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE IF NOT EXISTS attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, check_date TEXT NOT NULL, check_time TEXT NOT NULL, status TEXT NOT NULL DEFAULT present, UNIQUE(student_id, check_date) );两张表的设计要点在attendance表唯一的(student_id, check_date)约束。这个唯一索引是防重复签到的最后一道保险哪怕业务层漏判了数据库层面也会把第二条同人同日记录拒绝掉。签到状态我用三个值present表示正常签到late表示迟到absent表示缺勤。缺勤不是签到动作产生的是当天签到结束后批量补算的。状态机很简单一个人当天只能有一条记录状态在写入时根据签到时间判定如果没有记录且当天签到已结束就是absent。4.2 防止重复签到与迟到判定现在补齐上一章卖关子的mark_attendance函数def mark_attendance(student_id, name, distance): now datetime.datetime.now() date_str now.strftime(%Y-%m-%d) time_str now.strftime(%H:%M:%S) # 09:00 之后算迟到 late_time datetime.time(9, 0, 0) status late if now.time() late_time else present conn sqlite3.connect(attendance.db) try: conn.execute( INSERT INTO attendance (student_id, check_date, check_time, status) VALUES (?, ?, ?, ?), (student_id, date_str, time_str, status) ) conn.commit() print(f已记录{name} {date_str} {time_str} 状态{status}) except sqlite3.IntegrityError: # 唯一约束触发说明今天已经签过 print(f{name} 今日已签到跳过) finally: conn.close()逻辑上是三件事生成当前日期和时间、判定是否超过迟到时间、写入记录。IntegrityError是数据库层的最后一道防线双人同时出现在画面里导致业务层两个线程同时写入时这个异常能保证同一个人不会被写入两次。迟到判定不该硬编码在代码里。我一般会把「迟到时间」抽出来放到配置文件或数据库设置表里因为不同公司的上班时间不一样写死在代码里意味着每次改时间都要改代码。这个函数只是演示最小实现生产化时需要把datetime.time(9, 0, 0)替换成配置读取。4.3 多班级与按日检索签到系统一旦投入真实使用问题马上会从一个班的点名变成「今天谁来了、哪个班迟到多、这一周出勤率怎么样」。students表里没有班级字段所以第一步是加一列group_id注册时指定班级。ALTER TABLE students ADD COLUMN group_id TEXT DEFAULT class_a;然后按日检索出勤情况SELECT s.group_id, COUNT(DISTINCT s.id) AS total_students, COUNT(DISTINCT a.student_id) AS checked_in, ROUND(1.0 * COUNT(DISTINCT a.student_id) / COUNT(DISTINCT s.id), 2) AS rate FROM students s LEFT JOIN attendance a ON a.student_id s.id AND a.check_date 2024-05-20 GROUP BY s.group_id;注意LEFT JOIN的过滤条件写在ON里而不是WHERE里。如果把a.check_date 2024-05-20写进WHERE未签到的学生会因为关联不到记录而被过滤掉统计结果会变成「已签到学生的出勤率」这是一个非常容易犯的 SQL 逻辑错误。多班级场景下签到界面上一般会先选择班级再启动摄像头识别。识别成功后先查该学生是否属于当前选中班级属于才写入不属于则提示「请到对应班级签到」。这个校验放在mark_attendance调用之前避免跨班代签。5. 避坑与常见问题从环境安装到实际签到的 5 个典型翻车点5.1 安装 face_recognition 时报错卡在 dlib 编译现象pip install dlib执行到一半报错提示缺少 Visual C 14.0或者直接卡在Building wheel for dlib长时间不动。原因Windows 没有官方预编译的 dlib wheelpip 需要本地编译源码而编译依赖 CMake 和 MSVC 编译工具链。很多人直接跳过报错去搜索「如何安装 face_recognition」最后陷入换 Python 版本、降级依赖的循环。解决先安装 Visual Studio Build Tools勾选「使用 C 的桌面开发」工作负载再安装 CMake最后重新执行pip install dlib。装完 dlib 再装 face_recognition顺序不要颠倒。这一步是 Windows 上跑人脸识别最典型的血泪经验没有捷径。5.2 摄像头打不开或画面全黑现象cap.isOpened()返回 False或者打开后画面黑屏偶尔还会遇到摄像头指示灯亮了但程序无响应。原因OpenCV 默认使用的 MSMF 后端在部分摄像头驱动上兼容性差导致打开失败或读帧卡死。另一个常见原因是 0 号摄像头被占用比如浏览器或会议软件还开着。我在测试时遇到过多次程序里调用VideoCapture(0)之前没有释放摄像头第二次运行就打不开。解决创建VideoCapture时显式指定 DirectShow 后端即cv2.VideoCapture(0, cv2.CAP_DSHOW)同时确保没有其他程序占用摄像头。排查多摄像头时从 0 开始逐个索引尝试用循环打印每个索引的isOpened()结果来定位可用设备。另外加上cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)和cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)可以减少部分摄像头在默认分辨率下的读帧异常。5.3 识别成功但在摄像头画面里显示不出中文名字现象OpenCV 用putText显示识别结果时中文全部变成问号或乱码。原因OpenCV 的putText只支持 ASCII 字符集不支持中文渲染。这是很多人把签到系统跑通后遇到的第一个「显示层」问题。解决不要在putText里直接写中文。常见做法是识别到的人名先用 ASCII 拼音或 ID 显示在实时画面上同时用内存队列把中文名异步推给 GUI 界面的标签控件显示。如果必须在画面上显示中文就用 Pillow 库把中文渲染到 PIL 图像上再转换回 OpenCV 的 BGR 格式叠加。这个兼容问题每隔一段时间就会被重新踩一次但没有任何玄学就是字符集限制。5.4 同一个人被重复签到多次现象一个人站在摄像头前 10 秒数据库里插入了 5 条签到记录。原因签到循环每秒检测多次只要人还站在画面里每帧都会识别成功一次。业务层没有做重复签到检查。解决三层防线缺一不可。第一层是业务层检查当天是否已有记录有则跳过第二层是数据库唯一约束UNIQUE(student_id, check_date)第三层是识别结果缓存记录当前画面里最近 3 秒已经签到过的名字重复出现在画面上时直接不调用签到函数。第三层能减少无谓的数据库查询前两层保证数据不脏。5.5 阈值太严导致注册的人都识别不出来现象把阈值从 0.6 调到 0.4 之后原来能识别的人全部变成「陌生人」怎么调整灯光和角度都没用。原因阈值定得太严把同一人在不同光线、不同角度的正常波动也过滤掉了。人脸特征向量不是固定不变的光线变化会让距离产生约 0.1 的漂移阈值低于 0.45 后这种漂移就会导致拒识。解决用实际样本做校准不要抄网上的推荐值。我的做法是采集 10 个人、每人 5 个角度的照片两两计算与注册向量的距离画出一组分布数据然后取「同人最大距离」和「异人最小距离」的中间值作为阈值。如果两个区间重叠说明摄像头画质或注册质量不行需要重新注册而不是继续调阈值。签到系统最忌讳的就是把阈值当成唯一旋钮识别不出来先查注册环节的人脸质量。6. 把控制台脚本升级成一套值班可用的签到工具6.1 用队列解耦识别与 UI 卡顿把签到脚本做成 GUI 时最常见的问题是把视频预览、人脸识别和界面刷新写在一个线程里导致界面一卡一卡。我的做法是开三个线程采集线程负责读帧识别线程负责隔帧检测UI 线程只负责显示结果。识别线程通过queue.Queue把签到的姓名和距离发给 UI 线程UI 收到后再刷新表格和标签。采集线程和识别线程之间用queue传帧识别线程从队列取帧时如果积压了就丢弃旧帧只处理最新一帧保证延迟可控。6.2 校准脚本是整套系统最值得留着的工具我养成的一个习惯是每换一台机器或摄像头先跑一遍距离分布校准脚本把上面说的同人距离和异人距离打印出来再决定阈值。没有这步系统在上一个教室好好的换个光照环境就翻车。运行校准脚本时让三个人轮流出镜记录下各自的同人最大距离和互相之间的最小距离只要两个区间不重叠签到系统的参数就基本可信。6.3 验证签到数据是否可信项目快交付时我最担心的问题不是识别不了而是识别错了还被记录成对的人。所以务必做一轮抽样核验从当天的签到记录里随机抽 5 条找到对应时间点的视频帧肉眼确认画面里的人和数据库名字一致。另外给签到记录加上照片留痕字段在attendance表加一列snapshot BLOB每次签到成功时截取当前帧存进去。这样事后回溯时每一笔签到都有据可查——对个人是倒查凭证对行政是考勤纠纷的后悔药。这套代码从环境搭建到数据库记录我已经在不同机器上重写过好几版最深的体会是人脸识别只占整个系统 30% 的工作量剩下的 70% 都在解决「识别对了之后怎么把它变成一条可信的签到记录」。先把环境踩平再把阈值校准跑通然后是去重和状态判定最后才是加摄像头、加界面。按这个顺序做一个晚上就能见到能用的签到记录希望帮到你。本文还有配套的精品资源点击获取