简介:这是一套基于OpenCV与深度学习模型实现的Python人脸识别签到系统源码包,面向具备Python基础、希望完成课堂或会议自动签到的开发者,覆盖图像采集、人脸特征提取、身份匹配、考勤记录与数据导出等完整流程。压缩包共23个文件,约1.93MB,其中包含3个Python主程序、2个UI界面文件、4个XML配置、5张图片以及Markdown说明文档、CSV考勤数据等,便于直接查看逻辑、修改界面或扩展功能。资源已吸引1483人学习,适合作为毕业设计、课程项目或企业考勤系统改造的参考原型。除核心代码外,还提供项目说明、界面资源与运行配置文件,可帮助理解FaceNet/MTCNN等模型在本地应用中的落地方式,以及Tkinter/PyQt界面与考勤数据管理模块的整合思路。
1. Python人脸识别签到:从摄像头到考勤记录,我为什么建议先做最小闭环
人脸识别签到的需求场景太常见了:小团队考勤、培训签到、实验室门禁,甚至教室点名。买一台人脸识别门禁机要几百上千,还要配云端管理后台,数据还不一定导得出来。而用Python加一个普通USB摄像头,几百行代码就能跑通从人脸检测到签到记录的全流程,成本低、逻辑透明,还能随时改规则。这个方案不是要硬刚大厂的商用系统,而是给“刷脸签个到,记录谁几点来了”这种需求一个可控的落地路径。这篇文章会按“选型→注册→识别→记录→避坑”的顺序讲,每一步都给可复现代码,你照着抄就能跑通第一版。
2. 人脸识别技术选型:OpenCV、dlib 还是 face_recognition
2.1 先搞清楚人脸识别链路的四个步骤
任何一个人脸识别系统,本质上都是四步走:人脸检测、人脸对齐、特征提取、特征比对。人脸检测负责在画面里把人脸框出来;对齐是把脸摆正,防止歪头影响精度;特征提取是把人脸转换成一组数值向量;比对则是拿这组向量和库里提前存好的向量算距离。很多人只盯着“识别”两个字,实际上前三步决定了最后的准确率,第四步只是一个余弦相似度或欧氏距离的计算。
理解了链路,你就能明白为什么不能直接从网上找一个模型就开始用。模型只是特征提取的一部分,人脸检测用的是另一个模型,对齐可能是几何变换,比对阈值也需要根据实际场景调。把这些零部件拼起来,才是Python人脸识别签到的完整方案。如果只想做个带签到记录的demo,最省事的路径就是直接用封装好的库,把中间细节包起来。
2.2 三种常用方案的优缺点和适用规模
| 方案 | 特征提取方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| OpenCV LBPH | 局部二值模式直方图 | 安装简单、CPU快、无需额外模型 | 对光照和角度极敏感,准确率低 | 10人以内、固定角度、环境稳定的功能验证 |
| dlib + ResNet | 深度学习网络,输出128维向量 | 准确率高,离线可用,可微调阈值 | 安装麻烦,需要CMake和编译器,CPU推理慢 | 中小团队、离线环境、对精度有要求 |
| face_recognition | 封装dlib,直接提供接口 | API友好,几行代码完成检测和特征提取 | 依赖dlib,模型文件约100MB,速度一般 | 快速原型、50人左右签到、开发效率优先 |
我一般会直接推荐face_recognition,尤其是你只有半天时间想要跑通签到流程的时候。它把人脸检测和128维特征提取都封装好了,调一个函数就能拿到人脸向量。缺点是它比较吃资源,如果你要在树莓派这种设备上跑,那就改成dlib的HOG检测加上ResNet特征,或者干脆用OpenCV自带的LBPH——但LBPH的准确率在签到场景里真的会翻车,光线一变就认不出人。
2.3 环境安装:一条命令解决大部分依赖
选型定了以后,环境安装是第一个门槛。face_recognition底层依赖dlib,而dlib在Windows上装起来有不少玄学。我的建议是用Python 3.8或3.9,配合pip直接装,成功率最高。
python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install opencv-python face_recognition numpy这段命令创建虚拟环境并安装依赖。opencv-python提供摄像头捕获和图像处理,face_recognition提供人脸识别核心能力,numpy用来处理特征向量。参数说明:如果你在Windows上遇到dlib编译错误,先装Visual Studio Build Tools,勾选C++桌面开发组件;或者直接安装预编译版,比如用pip install dlib==19.24.4指定版本,这个版本在多数环境下有预编译wheel。装完以后跑一下python -c "import face_recognition",不报错就说明环境通了。这一步是你后面所有代码的地基,地基没打好,后面全是坑。
3. 人脸注册与特征入库:采集照片、提取128维向量、存成文件
3.1 人脸注册要解决的核心问题
签到前必须先把人的“脸”存进库里。很多人以为注册就是拍一张照片保存下来,等到签到时拿照片对比原始图片。这种做法的最大问题是每次比对都要重新提取特征,速度慢,而且存原图有隐私风险。正确的做法是注册时就把人脸转换成一个128维的特征向量,库里只存向量和对应的姓名、工号。比对时也是把摄像头抓到的脸转换成向量,然后算向量距离。这样不仅速度快,而且即使原始照片泄露,也无法还原人脸图像。
这里还要注意一张脸可能在不同角度下特征不同,所以注册时最好多拍几张。常见做法是一个人拍三到五张,取平均特征。这样能减少姿态和表情带来的干扰。如果是临时测试,先用一张高清正面照也行,但真实使用我建议至少三张。
3.2 编写注册脚本:自动检测人脸并提取特征
下面这个脚本会遍历photos/目录下以人名为文件夹存放的照片,提取每张脸的特征,把同一个人名的所有特征取平均,最后保存到face_db.npz。
import os import cv2 import numpy as np import face_recognition face_db = {} data_dir = "photos" for person_name in os.listdir(data_dir): person_dir = os.path.join(data_dir, person_name) if not os.path.isdir(person_dir): continue encodings = [] for img_name in os.listdir(person_dir): img_path = os.path.join(person_dir, img_name) # 用OpenCV读图,再转成RGB,face_recognition需要RGB格式 img = cv2.imread(img_path) # 中文路径下OpenCV可能会读取失败,下面这行是健壮性处理 if img is None: with open(img_path, "rb") as f: data = np.frombuffer(f.read(), dtype=np.uint8) img = cv2.imdecode(data, cv2.IMREAD_COLOR) img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 检测人脸并提取特征,location传空默认检测最大人脸 face_locations = face_recognition.face_locations(img_rgb, model="hog") if len(face_locations) != 1: print(f"{img_path}: 检测到 {len(face_locations)} 张脸,跳过") continue encoding = face_recognition.face_encodings( img_rgb, known_face_locations=face_locations, num_jitters=10 )[0] encodings.append(encoding) if encodings: avg_encoding = np.mean(encodings, axis=0) face_db[person_name] = avg_encoding print(f"已注册 {person_name},用了 {len(encodings)} 张照片") np.savez_compressed("face_db.npz", **face_db)这段代码先遍历人名目录,然后对每张照片调用face_recognition.face_locations做人脸检测,再用face_encodings提取128维向量。关键参数:model="hog"表示用HOG模型,CPU就能跑;如果换成"cnn"更准但慢,适合GPU或离线处理。num_jitters=10表示对图片进行10次轻微抖动采样后取平均特征,数值越大越稳定,但耗时成倍增加,注册时设为10,识别时通常设为1就够了。
3.3 特征库的存储与加载
上面脚本生成的是压缩的npz文件,打开以后是一个字典,键是人名,值是128维浮点向量。这个文件就是你的签到系统“黑匣子”之外那部分——注册和识别共用同一个库,所以必须确保注册质量。如果某张照片拍到一半脸在外面,检测到0张脸会跳过;如果拍到多个人,也跳过,避免特征乱套。
加载的时候用np.load("face_db.npz", allow_pickle=True),得到的是一个类似字典的对象,可以直接用键访问。我建议在正式使用前写个小脚本,把库里每个人的特征和名字打印出来,确认数量对得上。特征库文件很小,一百人也就几十KB,复制迁移都很方便,不像存照片那样占空间。
4. 签到主流程实现:摄像头识别、数据库记录、去重防刷
4.1 签到逻辑的边界条件
识别出“这个人是谁”只是半程,另半程是把“谁在什么时候打了卡”记下来。签到场景有几个边界条件必须处理:同一人同一天不能重复签到、非库内人员要提示“未注册”、无人脸时不误报、摄像头画面里出现多张脸时怎么处理。我的常见做法是每次识别到脸后,先比对特征库,如果距离小于阈值就判定为签到成功;然后查数据库,看此人今天是否已经有记录,有则提示“已签到”,没有则插入一条新记录。
阈值设置很关键。face_recognition默认的比对阈值是0.6(欧氏距离,越小越严格)。实战中0.5到0.55更常见,因为签到画质通常一般,阈值太松会导致不同人被误认成同一人。这个值不是玄学,最好拿你们团队的人脸数据跑一遍,画出相同人和不同人的距离分布,然后取分界点。后文会在避坑章节详细说。
4.2 实时识别数据库写入代码
下面这个脚本从face_db.npz加载特征库,打开摄像头,逐帧检测人脸并识别,识别成功后写入SQLite数据库。
import sqlite3 import datetime import numpy as np import cv2 import face_recognition # 加载特征库 data = np.load("face_db.npz", allow_pickle=True) known_names = list(data.keys()) known_encodings = [data[name] for name in known_names] # 初始化SQLite,建表 conn = sqlite3.connect("attendance.db") conn.execute("""CREATE TABLE IF NOT EXISTS checkin ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, check_time TEXT NOT NULL, date TEXT NOT NULL )""") conn.commit() def is_checked_in_today(name): today = datetime.date.today().isoformat() cur = conn.execute( "SELECT 1 FROM checkin WHERE name=? AND date=?", (name, today) ) return cur.fetchone() is not None def record_checkin(name): now = datetime.datetime.now() today = now.date().isoformat() time_str = now.strftime("%H:%M:%S") conn.execute( "INSERT INTO checkin (name, check_time, date) VALUES (?,?,?)", (name, time_str, today) ) conn.commit() print(f"{name} 签到成功,时间 {time_str}") # 摄像头识别循环 cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame = cap.read() if not ret: print("摄像头读取失败") break # 缩小画面加快处理,同时保持比例 small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb_small = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) # 检测人脸位置 face_locations = face_recognition.face_locations(rgb_small, model="hog") if face_locations: face_encodings = face_recognition.face_encodings(rgb_small, face_locations) for face_encoding in face_encodings: # 与库中所有人比对,拿到最小距离 distances = face_recognition.face_distance(known_encodings, face_encoding) min_index = np.argmin(distances) min_distance = distances[min_index] if min_distance < 0.5: name = known_names[min_index] if is_checked_in_today(name): print(f"{name} 今天已签到") else: record_checkin(name) else: print("未注册人脸") else: print("未检测到人脸") if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() conn.close()这段代码的逻辑很直白:先缩小画面到一半尺寸,因为face_recognition处理大图很慢,缩小后速度提升明显。face_locations返回每个检测到的人脸位置,然后对每个位置提取特征。face_distance计算当前脸与库中所有人脸的欧氏距离,取最小距离的那个作为候选,和0.5的阈值比较。为防同一天重复签到,先查SQLite里今天有没有记录,有则忽略,没有则插入。
参数说明:CAP_PROP_FRAME_WIDTH和CAP_PROP_FRAME_HEIGHT分别设置摄像头分辨率,640x480比较平衡画质和速度。fx=0.5表示缩小一半,识别速度大约是原图的四倍。识别的阈值0.5是硬编码的,实际使用建议根据测试调整到0.45~0.55之间。这里的SQLite表结构提供了date字段,专门用于日期去重。
4.3 为什么用SQLite而不是Excel
签到记录至少要支持查询、去重、统计,用Excel文件直接append看似简单,但并发写入时会锁文件,而且查询某个人今天是否已签到需要遍历整个表格。SQLite是单文件数据库,Python内置,不需要额外服务,完美匹配单人签到场景。后面如果要导出Excel,直接用pandas读SQLite再写excel,几分钟就能搞定。
如果你需要多人同时从不同摄像头签到,那SQLite也可以,只要写入操作串行化即可。但像“同一时间多个摄像头同时人脸识别”这种需求,最好还是用MySQL或PostgreSQL,SQLite在并发写入多的时候会报database locked。好在多数签到场景是单人单机,SQLite足够。
5. 常见问题与排查:光线、乱码、装不上、识别不准的4个实例
5.1 dlib安装失败导致face_recognition无法导入
现象:执行pip install face_recognition时,控制台报错Failed to build dlib,或者导入face_recognition时提示找不到_dlib_pybind11。
原因:dlib需要C++编译,Windows上缺少Visual Studio Build Tools,或者Python版本太新(比如3.12)没有预编译wheel。
解决:先降级Python到3.8或3.9,这版本dlib的预编译支持最成熟。然后执行pip install dlib==19.24.4,如果还不行,去下载dlib的预编译wheel文件,用pip install 下载的文件.whl离线安装。装好后再装face_recognition。Linux上则先装build-essential和cmake。这是整个方案里最容易劝退的一步,但只要装过一次,后面就顺畅了。
5.2 中文用户名和照片路径乱码导致特征提取失败
现象:注册脚本遍历photos/张三时,cv2.imread返回None,或者os.listdir出来的文件名在打印时乱码。
原因:OpenCV的imread不支持中文路径,Windows下尤其严重。os.listdir本身没问题,但终端编码不一致会导致显示乱码,进而干扰判断。
解决:统一用cv2.imdecode读图,也就是先读二进制数据再解码。我在注册脚本里已经写了这个健壮性处理。路径和文件名不要直接用中文,建议用拼音或工号作为目录名,显示名存在数据库里。比如目录叫zhangsan,库里映射到“张三”。这样既避开编码坑,又方便后续扩展。
5.3 识别阈值设错导致“张冠李戴”
现象:A来签到,结果系统提示B已签到;或者B今天明明没来,库里却有了B的签到记录。
原因:face_distance返回的最小距离小于阈值,但可能A和B长得像,或者摄像头画质差导致特征混淆。阈值设成0.6时,误报率明显升高。
解决:做一次阈值校准。拿库里每个人的一张新照片,计算它与库中所有人的距离,观察“同一个人”的距离分布和“不同人”的距离分布。一般同人距离在0.25~0.45之间,不同人通常大于0.55。取0.45~0.5作为阈值比较安全。如果想让识别更谨慎,宁可拒识也不误报,把阈值降到0.4。
5.4 摄像头画面正常但识别时间和实际签到时间差几秒
现象:人已经走到摄像头前,屏幕上要过1~2秒才显示签到成功,而且连续几张画面都提示“未注册”。
原因:face_recognition的HOG检测在CPU上每帧大约60~150毫秒,但如果画面里有多个脸,或者分辨率设置过高,循环会卡顿。还有一个隐藏点:代码里每次识别都对整帧所有人脸做特征提取,如果人站在远处脸很小,特征质量差,距离会变大,导致提示未注册。
解决:把摄像头分辨率降到640x480,缩小帧到0.5倍,识别速度能提升到每秒10帧左右。同时限制只处理画面中心区域,比如只对距离摄像头最近的人脸做识别,避免多人互相干扰。如果还嫌慢,可以跳帧——每第3帧才做一次完整识别,其余帧只显示画面。签到不需要实时连续识别,一两秒的延迟完全能接受。
6. 进阶:用质量检测和阈值配置把误报率压到最低
6.1 给签到加一个人脸质量门槛
很多人不知道,识别不准的核心原因往往不是模型,而是输入的人脸太模糊。摄像头帧中人或走动、或低头,抓到的脸可能只有10像素宽,特征自然不稳定。我建议在识别前增加一个“人脸质量检测”,只有清晰度达标的画面才进入比对流程。
常见做法是用cv2.Laplacian计算人脸区域的方差,方差低于某个值就判定为模糊。下面的代码片段可以直接插入识别循环:
def is_face_sharp(face_image, threshold=50): # face_image是人脸区域BGR图像,转为灰度后计算拉普拉斯方差 gray = cv2.cvtColor(face_image, cv2.COLOR_BGR2GRAY) variance = cv2.Laplacian(gray, cv2.CV_64F).var() return variance > threshold这段代码的关键参数是threshold,我一般在50到100之间取,光线暗或摄像头低端时降低到30,否则会误杀很多正常脸。把这个函数放在特征提取之前,如果返回False就直接跳过这一帧。这个步骤能显著减少“晃一下脸就被误识别”的情况,尤其是有人在镜头前经过时,不会因为闪过半张脸就打了卡。
6.2 用多线程把摄像头采集和识别分开
单线程下,摄像头读取和模型识别是串行的,识别慢时画面会卡顿,而且可能会丢帧。我现在习惯用两个线程:一个线程只负责cap.read(),把最新帧放入队列;另一个线程从队列取帧做人脸识别。这样即使识别耗时200毫秒,视频流依然流畅。
import threading import queue frame_queue = queue.Queue(maxsize=2) def capture_loop(cap): while True: ret, frame = cap.read() if ret and not frame_queue.full(): frame_queue.put(frame) # 在主循环里改成从队列取帧 def recognition_loop(): while True: frame = frame_queue.get() # 后面走人脸检测逻辑这里maxsize=2限制了队列长度,避免生产速度大于消费速度时内存无限增长。如果队满了就直接丢弃旧帧,保证识别的是最新画面。这个模式也方便后面扩展成多摄像头,每个摄像头一个采集线程,识别线程共享一个特征库和数据库。对签到这种低并发场景,双线程已经足够,不用上复杂的并发框架。
6.3 把误签到的“后悔药”做好
最后想提醒你的是,无论识别准不准,一定要给管理员留一个手动撤销或补签的入口。哪怕识别准确率99%,每天几百人次签到下来也会出个把错。我的做法是在SQLite里加一个is_valid字段,默认1,如果管理员发现某条记录是误签,就把它更新为0,统计时只统计is_valid=1的记录。这比删数据好,记录留痕,可追溯。
conn.execute("ALTER TABLE checkin ADD COLUMN is_valid INTEGER DEFAULT 1")这条DDL给已存在的表加字段,不影响已有数据。使用中多一个这样的软删除机制,能避免很多尴尬。我曾经因为没做这个设计,某天误签了一个人,结果月底对账时怎么都解释不通,最后只能手动改数据库。那次之后我所有签到项目都保留撤销接口,这也是我踩坑踩出来的习惯。识别阈值、环境安装、摄像头参数都有可能随设备变化,但数据可修正是系统设计层面的底线。希望这篇笔记能帮你把一个能跑、好用、不坑的Python人脸识别签到系统搭起来,少走我走过的弯路。
本文还有配套的精品资源,点击获取