☰
Python人脸识别签到系统实战:从原理到代码完整实现
2026/9/28 11:55:05 网站建设 项目流程

简介:基于Python的人脸识别签到系统完整工程代码包,面向计算机视觉与GUI开发的初中级开发者,解决课堂考勤、会议签到等手工登记效率低的问题。工程覆盖人脸检测、特征提取、人脸识别、GUI交互与签到记录下载五个模块,使用OpenCV、dlib、face_recognition等库,并附readme文档,便于理清项目结构。压缩包共20个文件,以py源码为主,辅以pyc编译文件、xml配置文件、人脸特征npy数据、示例图片及说明文档,整体仅95KB,轻量便于下载与二次修改。目前已有3988人学习浏览,适合掌握人脸识别落地思路的读者。阅读源码可学到摄像头实时取流、人脸特征向量比对、Tkinter界面搭建、签到信息存储等关键细节,对GUI人脸签到感兴趣的入门者可获得可直接运行的参考工程。

1. python人脸识别签到系统:为什么说这是最适合练手的AI落地项目

如果你刚把python环境配好,又不想只写爬虫和爱心代码,那「python人脸识别签到系统」几乎是性价比最高的下一个目标:它同时碰到摄像头取流、图像处理、模型调用、数据库读写和业务逻辑,难度又刚好卡在“能跑起来”和“能讲清楚”之间。市面上人脸识别门禁机动辄上千元,而用python搭一套局域网内可用的签到系统,成本就是一台带摄像头的电脑加几十行核心代码。这套方案适合三类人:给学生点名发愁的老师、想给团队做个轻量考勤工具的后勤同学,以及准备把简历里“熟悉AI应用”写成“做过完整视觉项目”的求职者。本文按我从零搭这套系统的顺序来写:先讲选型和原理,再给可抄的代码,最后把参数和坑一次性说透。

2. 技术选型与识别流程:先想清楚在哪一步省钱

2.1 人脸识别签到系统的四段链路

一个完整的python人脸识别签到系统,从摄像头画面到你excel里的签到记录,中间隔着四段固定的链路:人脸检测、人脸对齐、特征提取、特征比对。

人脸检测解决“脸在哪”的问题。常见做法是用OpenCV的Haar级联分类器,或者用face_recognition库内置的HOG(方向梯度直方图)检测器。Haar速度快但在侧脸和暗光下容易漏检,HOG对正面和轻微侧脸更稳,代价是每帧多花几十毫秒。对签到场景来说,人通常是正对摄像头刷卡式采集,HOG足够。

人脸对齐解决“脸歪了”的问题。检测到人脸后,算法会定位左眼、右眼、鼻尖、嘴角等关键点,把脸旋转矫正到统一尺度。face_recognition库把这步封装成了face_landmarks,不需要你手写仿射变换。特征提取是关键一步:它把一张脸压缩成一个128维的浮点向量,这个向量也叫人脸编码。同一个人的不同照片,向量距离很小,不同人的向量距离很大。比对就是算欧氏距离,距离小于阈值判为同一人。

这套流程里,特征提取是唯一绕不开模型的环节。face_recognition库底层用的是dlib的ResNet模型,模型文件约80MB,第一次运行会自动下载。整体思路是:注册时提取人脸向量存库,签到时再提取当前向量,和库里向量逐一算距离,命中就写入签到记录。

2.2 为什么选face_recognition而不是OpenCV硬刚

最容易走偏的路是自己用OpenCV的LBPH人脸识别器做一个签到系统。LBPH的置信度很不稳定,换角度、换光线后同一个人的置信度波动极大,而且它本质上是把整张脸的纹理编码成直方图,对眼镜、刘海、胡子的干扰几乎没有抵抗力。我见过有人用LBPH做签到,戴个帽子就从“识别成功”变成“识别失败”,这种方案投入生产就是给自己找事。

相比之下,face_recognition库的优势是封装完整:检测、对齐、提取、比对四步全有,调用方式接近傻瓜式。而且它的识别精度在普通办公光照下足够用,CPU模式下处理一帧约0.3到0.5秒,对签到这种“人站定后识别”的场景完全能接受。需要说明的是,它也有软肋:模型文件大、启动慢、纯CPU跑视频流做不到实时,但这些对签到系统都不致命。

另一个选择是走云端API,比如调用现成的人脸识别服务。好处是精度高、有活体检测,但代价是每次签到都要上传照片,单位内部用会涉及隐私问题,而且依赖公网和按量计费。做局域网签到系统,自建本地模型才是可控的方案。

2.3 自建模型与云端API的取舍:小项目别被供应商绑架

我一般会建议:先本地跑通,再考虑要不要接云端。本地方案的好处是你随时能改逻辑——比如今天想加一个“代签限制”,明天想把签到照片存到本地目录留作审计,这些在本地模型上都是几行代码的事。而云端API的计费模式、调用限额和数据合规问题,会让你在项目中期陷入被动。

预算上的差距也值得算一笔账:云端API按次计费,100人每天签两次,一个月就是6000次调用,一年下来成本不低。本地方案是一次性投入,一台旧笔记本就能当服务器。你省的不仅是钱,还有每次调试时反复上传照片的等待时间。

3. 搭一个能跑的签到系统:从采集人脸到写入SQLite

3.1 项目结构与数据库设计

先约定目录结构,避免代码写到一半开始乱放文件。常见做法是分成两个核心脚本:一个管注册,一个管签到,再加一个公共模块负责数据库操作。

face_sign/ ├── requirements.txt ├── db.py # 数据库连接与操作 ├── register.py # 注册新用户 ├── check_in.py # 签到主程序 ├── known_faces/ # 注册时采集的照片留档 └── attendance.db # SQLite数据库文件

使用SQLite的原因很简单:签到系统是单机或局域网小规模使用,SQLite不需要安装服务端,一个文件就是整个数据库,备份时复制文件即可。表结构就两张,一张存人,一张存记录。

CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, face_encoding BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS sign_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, sign_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) );

face_encoding字段存的是128维向量的二进制序列化结果。比对的时候读出BLOB,用numpy.frombuffer还原成数组。这种设计让用户表单和识别数据放在同一个库里,查询打卡记录时一条JOIN就能带出姓名和时间。

3.2 注册阶段:多角度采集与特征入库

注册脚本要完成三件事:打开摄像头,连续采集多帧人脸,提取特征后存入数据库。重点是不能只拍一帧,因为单帧照片受姿势和光线影响太大,后面签到容易失败。

python代码如下,核心逻辑和参数说明紧随其后:

import cv2 import face_recognition import numpy as np import pickle import sqlite3 from db import get_connection def register_user(name, camera_id=0, sample_frames=10): # 打开摄像头,0表示默认USB摄像头 cap = cv2.VideoCapture(camera_id) if not cap.isOpened(): raise RuntimeError("无法打开摄像头,请检查设备索引和驱动") face_encodings = [] frame_count = 0 CAPTURED = 0 print(f"请正对摄像头,开始采集 {name} 的人脸…") while CAPTURED < sample_frames: ok, frame = cap.read() if not ok: continue frame_count += 1 # 每5帧检测一次,避免连续重复帧浪费计算 if frame_count % 5 != 0: continue # HOG检测人脸,不缩小图像以保证检出率 small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) boxes = face_recognition.face_locations(rgb_frame, model="hog") if len(boxes) != 1: print("画面中必须且只能有一个人脸,当前检测到", len(boxes)) continue # 提取128维特征向量 encodings = face_recognition.face_encodings(rgb_frame, boxes) if encodings: face_encodings.append(encodings[0]) CAPTURED += 1 print(f"已采集 {CAPTURED}/{sample_frames} 帧") # 显示画面方便人调整姿势 cv2.imshow("Register", small_frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() if len(face_encodings) == 0: raise RuntimeError("没有采集到有效人脸,请检查光线和角度") # 多帧特征做平均,降低单帧噪声 avg_encoding = np.mean(face_encodings, axis=0) # 序列化存入数据库 blob = pickle.dumps(avg_encoding) conn = get_connection() conn.execute( "INSERT INTO users (name, face_encoding) VALUES (?, ?)", (name, blob) ) conn.commit() conn.close() print(f"用户 {name} 注册完成")

脚本里有两个值得注意的参数。一个是sample_frames设为10,采集10帧做均值,这是我在实测后确定的平衡点:少于6帧,个别帧的眨眼、歪头会把特征拉偏;多于15帧,采集耗时太长,注册体验变差。另一个是每隔5帧才处理一帧,目的是跳过摄像头连续输出时高度相似的帧,让采样点更分散。

注册时我还会保留一张原始照片到known_faces目录,用于事后人工核对“这个账号到底是谁注册的”。有人会注册好友的脸去代打卡,留档照片是唯一的追溯手段。

3.3 签到阶段:实时比对与打卡去重

签到脚本是系统的门面。它持续读取摄像头画面,检测到人脸后立即与数据库里的所有人脸编码比对。比对命中且距离小于阈值,就写入签到记录。

import cv2 import face_recognition import numpy as np import pickle import sqlite3 from datetime import datetime from db import get_connection def load_known_faces(): conn = get_connection() rows = conn.execute("SELECT id, name, face_encoding FROM users").fetchall() conn.close() known_ids = [] known_names = [] known_encodings = [] for uid, name, blob in rows: encoding = pickle.loads(blob) # 还原为numpy数组 known_encodings.append(encoding) known_names.append(name) known_ids.append(uid) return known_ids, known_names, known_encodings def check_in(camera_id=0, tolerance=0.45, cooldown_seconds=300): ids, names, encodings = load_known_faces() if len(encodings) == 0: raise RuntimeError("数据库里没有人脸,请先运行注册脚本") cap = cv2.VideoCapture(camera_id) last_sign_time = {} # 记录每个用户最近一次签到时间,用于防重复 while True: ok, frame = cap.read() if not ok: continue # 缩小帧处理,人脸检测在缩放图上做,速度更快 small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) boxes = face_recognition.face_locations(rgb_frame, model="hog") current_encodings = face_recognition.face_encodings(rgb_frame, boxes) for encoding in current_encodings: # 与库里所有特征算欧氏距离 distances = face_recognition.face_distance(encodings, encoding) min_idx = np.argmin(distances) min_dist = distances[min_idx] if min_dist < tolerance: uid = ids[min_idx] name = names[min_idx] now = datetime.now() # 冷却期内不重复打卡,默认5分钟 last = last_sign_time.get(uid) if last and (now - last).total_seconds() < cooldown_seconds: print(f"{name} 已在冷却期内,不重复签到") continue last_sign_time[uid] = now conn = get_connection() conn.execute( "INSERT INTO sign_log (user_id) VALUES (?)", (uid,) ) conn.commit() conn.close() print(f"[{now.strftime('%H:%M:%S')}] {name} 签到成功") else: print(f"检测到未注册人脸,最近距离:{min_dist:.3f}") cv2.imshow("Check-in", small_frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

这段代码的逻辑是:每一帧都做人脸检测,但只对检测到的人脸提取特征并比对。face_distance返回一个数组,长度和数据库里的用户数一致,取最小值后与tolerance比较。比较结果是二进制判断——小于阈值就认定是库里的人,否则视为陌生人。

cooldown_seconds参数是防重复打卡的关键。没有它,一个人站在摄像头前5秒会被写入5条签到记录。300秒的冷却时间适合绝大多数场景,如果签到点是上下班打卡,这个值甚至可以缩到60秒;如果是会议室扫码签到,建议拉到600秒以上。

3.4 一键启动脚本与摄像头自动探测

每次手动敲register.py和check_in.py很麻烦,尤其是在Windows上还要先激活虚拟环境。我一般在项目根目录放一个start.py,提供命令行参数选择模式。

import sys import cv2 from register import register_user from check_in import check_in def find_camera(): """自动探测可用的摄像头索引,避免用户手动试0还是1""" for idx in range(3): cap = cv2.VideoCapture(idx) if cap.isOpened(): cap.release() return idx raise RuntimeError("未找到可用摄像头") if __name__ == "__main__": mode = sys.argv[1] if len(sys.argv) > 1 else "check" camera = find_camera() if mode == "register": name = sys.argv[2] register_user(name, camera_id=camera) elif mode == "check": check_in(camera_id=camera) else: print("用法: python start.py register 张三 / python start.py check")

find_camera利用了OpenCV的VideoCapture特性:摄像头被占用或不存在时,isOpened返回False。USB摄像头在某些笔记本上索引是1而不是0,自动探测能消掉这个玄学问题。

4. 让识别准起来:三个比换模型更管用的参数

4.1 比对阈值:0.4和0.5之间的玄学

很多照着教程抄作业的人,一路跑通后发现两个问题:要么陌生人频频被识别成库里的人,要么本人站了半天签不上。这两个问题通常指向同一个参数——tolerance。face_recognition库默认的tolerance是0.6,这个值偏向宽松,适合“只要大概像就认出来”的场景。但签到系统必须严格,0.6会导致误识别率明显上升。

我实测过的经验值是0.45。在办公室普通光照条件下,同一个人的不同帧距离通常在0.3到0.42之间,不同人的距离通常在0.55以上,0.45给两边都留了余量。如果你发现库里的人经常被拒,先别急着调阈值,检查注册时的照片质量——照片太暗、闭眼、角度偏大,都会让本人距离变大。阈值调到0.5以上,意味着开始接受“长得像的人”,这是签到系统的红线。

调整方法很直接:把check_in函数里的tolerance参数从0.45改成0.5再跑几天对比误识率。不要反复横跳,一次只动一个参数。

4.2 抽帧间隔与检测模型:别让CPU白烧

face_recognition的CPU模式跑一帧大约需要0.3到0.5秒,如果每帧都处理,CPU占用率会顶到100%而且画面会肉眼可见地卡顿。签到场景里人站定后画面基本静止,处理每一帧纯属浪费。

常见做法是每5帧处理1帧,也就是每秒约处理4到6次。这个频率对人脸签到足够,人不可能在0.2秒内从正脸变成完全侧脸。如果你用的是树莓派之类的低性能设备,可以把间隔拉大到10帧,或者把detection模型换成hog(已经是默认),不要用cnn模型——cnn精度高但CPU下每帧要好几秒,只有GPU机器才需要考虑。

4.3 摄像头分辨率与曝光:签到机的物理基础

软件参数调得再好,摄像头画面糊成一片也是白搭。人脸检测需要至少画出80x80像素的人脸区域。以常见的1280x720分辨率摄像头为例,人脸距镜头0.5到1米时检测效果最好。分辨率低于640x480的摄像头,建议把采集帧resize到0.5倍后的尺寸由640x320改为原尺寸处理,不要盲目缩小。

曝光补偿是另一个容易翻车的点。背对窗户坐时,人脸会变成逆光剪影,HOG检测器找不到五官边界。解决方法是把摄像头安装位置改成正对光源,或开启摄像头的自动曝光。OpenCV里可以通过cap.set(cv2.CAP_PROP_EXPOSURE, -6)强制压低曝光补偿,但不同品牌摄像头对曝光值的响应差异很大,我没法给一个通用数值。我的习惯是先用cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25)关闭自动曝光,然后手动调整到画面亮度合适为止。

5. 排障与避坑:5个让我半夜爬起来改代码的问题

5.1 入库之后无法识别:pickle序列化与BLOB类型不匹配

现象:注册时一切正常,签到脚本运行时face_distance全部报错,打印出来全是nan。

原因:注册脚本用pickle.dumps把numpy数组序列化成bytes,但SQLite的BLOB字段在读出时可能被dlib封装成了PIL图像类型。实际上更隐蔽的问题在于,face_recognition的face_encodings返回的是numpy数组,但你没有强制转成float64类型。

解决:注册阶段写入前加一行avg_encoding = np.asarray(avg_encoding, dtype=np.float64)。读出时用pickle.loads后也做同样转换。顺带说一下,face_recognition库在Windows上容易出现依赖冲突,建议用conda安装dlib,再pip install face_recognition,顺序反了经常撞库。

5.2 同一个人隔天签不上:单一注册帧导致特征漂移

现象:上午注册成功,下午签到失败,距离值在0.5附近徘徊。

原因:注册时只采了一帧正面照,特征只覆盖了正脸状态。下午光线角度变化后,当前帧特征偏移到0.5以上。

解决:这是我在3.2节把采集帧数定成10帧的原因。如果你已经在用单帧注册,最快的补救办法是重新注册,这次转头、低头、微微侧脸各拍几帧。也可以考虑在数据库里为每个用户存多组特征,比对时取最小距离。后者的改动量不大,签到脚本里比对逻辑从“一个用户一个向量”改成“一个用户多个向量取最小”,对陌生人误识率影响很小。

5.3 显示器上的人脸照片也能签到:没有活体检测

现象:把手机或打印的照片举到摄像头前,签到成功。

原因:face_recognition只看静态纹理,不管画面里的人是不是活的。对签到系统来说这是最严重的漏洞。

解决:低成本方案是加动作验证——检测到人脸后,弹窗要求用户眨眼或转头。可以用face_recognition的face_landmarks检测眼睛纵横比,连续两帧纵横比低于阈值判定为一次眨眼。这个方案挡不住视频重放,但能挡住“一张照片签两次”的低级代签。要彻底解决得换红外活体摄像头,用近红外光下皮肤和照片的反射差异来判定,这是硬件层面的投入。

5.4 SQLite操作频繁导致数据库锁死

现象:签到脚本连续跑几小时后报“database is locked”。

原因:SQLite在Windows上的默认锁粒度较大,而签到脚本每次写入都新建连接、提交后关闭,多线程或多进程同时操作时容易撞锁。另外,如果你的签到脚本和其他进程(比如Flask后端)同时在写同一个db文件,锁的概率更高。

解决:把所有写操作收敛到一个连接里,程序启动时建长连接,退出时关闭。如果并发压力确实大,开启WAL模式可以显著改善读写并发。

conn = sqlite3.connect('attendance.db', timeout=10) conn.execute("PRAGMA journal_mode=WAL;")

timeout设为10秒表示等待锁的时长,WAL模式让读操作不阻塞写操作。这两行代码加上后,我再没遇到过database is locked。

5.5 笔记本摄像头黑屏但系统显示占用中

现象:脚本报“无法打开摄像头0”,或画面全黑但任务管理器显示软件正在访问相机。

原因:Windows的隐私设置里禁用了桌面应用的相机访问权限。这个跟你的代码无关,属于操作系统层面的坑。

解决:先打开系统设置里的相机隐私权限,允许桌面应用访问相机。如果权限没问题,用find_camera函数多试几个设备索引——有的笔记本内建摄像头索引是1,外接USB摄像头是0。挂载了虚拟摄像头软件的机器还会出现多个索引的情况,按设备顺序逐个探测最稳。

6. 往下走:活体检测、IPC摄像头接入与多机部署的落地思路

签到系统从能用到好用,还差三个进阶改造点。

第一个是活体检测的工程化。我在5.3节提到了眨眼检测的思路,这里补一句参数:眼睛纵横比EAR的阈值通常设在0.2,低于0.2判定为闭眼。连续检测到闭眼再睁开的循环算一次眨眼,要求3秒内至少完成2次眨眼才算活体。这个方案实现代码量不大,但对硬件有个隐性要求——帧率不能低于10fps,否则眨眼过程拍不到。帧率达不到就先降分辨率到320x240再调。

第二个是接入IPC网络摄像头。办公区自助签到机没必要用USB线连着电脑,常见做法是海康或大华的网络摄像头接入局域网,python这边直接把VideoCapture的路径换成RTSP地址。一条关键经验:RTSP地址要选主码流还是子码流,建议用子码流。主码流分辨率高但网络抖动时掉帧严重,子码流在小分辨率下检测够用且延迟低。代码改动只有一行:cv2.VideoCapture("rtsp://用户名:密码@IP:554/Streaming/Channels/101")。

第三个是从单机改多机。100人以上的团队,签到机可能要放两台,但签到记录要汇总到一个表里。最省事的做法是共用SQLite文件放到局域网共享盘——不推荐这么做,并发写容易出5.4那种锁问题。我用过的可靠方案是加一个Flask中间层:签到脚本识别成功后把user_id和timestamp通过HTTP POST上报到中心服务器,中心服务器统一写SQLite或MySQL。签到脚本本身保持不变,只是把本地写库换成远程调用。这样加第二台签到机时,代码一行不用改。

最后分享一个习惯:我每次改签到系统的阈值、冷却时间或抽帧参数,都会把改动记在项目根目录的CHANGES.md里。人脸识别有个黑匣子特性——同样的参数,换一间办公室、换一盏灯,结果都可能不同。没有改动记录,参数调漂了只能靠回忆往回找,这个坑我踩过不止一次。这套方案做好了,后续朝人脸门禁、访客登记方向扩展都很顺,值得你投入时间把它跑透。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询