☰
Python人脸识别签到系统:从摄像头采集到特征匹配的完整实现
2026/9/28 3:01:33 网站建设 项目流程

简介:基于Python与OpenCV、dlib、face_recognition构建的人脸识别签到系统资源,面向Python开发者、AI入门者及有考勤签到需求的学生,提供从人脸录入、特征提取到实时签到与记录导出的完整工程实现。资源共含20个文件,压缩包大小约95KB:6个py源码对应数据采集、通用处理、工具函数、摄像头调用等核心模块,附pyc编译文件、xml配置文件、测试图片、npy特征文件与说明文档,压缩包虽小但结构清晰,适合学习与实践。已有3988人学习下载,属于同类资源中较受关注的一份。具体内容包括create_dataset.py、camera_use.py、file_processing.py、util.py等脚本,以及预生成的faceEmbedding.npy,可快速跑通基于GUI的人脸签到流程,并在此基础上理解人脸检测算法选型、特征向量比对、多线程实时视频处理与签到结果本地存储等关键技术点,为二次开发或毕业设计提供可直接复用的代码基线。

1. python人脸识别签到系统:一个能直接跑通的本地摄像头签到方案

人脸识别签到在Python里并不是只有大厂才能做的产品级方案,用OpenCV做人脸检测、face_recognition提取特征向量、Tkinter搭GUI,一套下来完全可以支撑办公室或小班课的日常签到需求。这套ggg.rar里的源码包正好是这么个结构,里面有create_dataset.py建人脸库、camera_use.py开摄像头识别、file_processing.py做文件处理,还预置了一个faceEmbedding.npy特征文件和common.py公共模块。我拆完整个包之后最直观的感受是:它把"采集人脸→训练特征→实时签到→记录下载"这一条链路做得很完整,适合拿来改一改就上生产,而不是只有个识别demo就完事。适合谁用?有Python基础、想快速落地一个人脸签到工具的人,或者做课程设计、毕设需要一份完整可运行代码的学生。

2. 整个人脸签到系统的模块划分:从create_dataset到camera_use的完整闭环

2.1 先看懂这份源码包的目录结构

拿到ggg.rar解压之后,第一件事不是急着跑,而是把文件清单过一遍。我从包里整理出的核心文件如下:

文件作用
create_dataset.py人脸数据采集脚本,通过摄像头抓取人脸图像并保存
Face_login/camera_use.py签到主程序,打开摄像头实时识别并记录签到
file_processing.py处理签到记录文件,比如读取、写入、导出
common.py公共工具模块,通常放路径处理、常量定义等
util.py辅助函数,比如特征向量保存/加载
faceEmbedding.npy存储所有人脸特征向量的numpy文件
name.txt存放与特征向量对应的人名列表
readme.md项目说明文档
test_images/测试图片目录
test/teset.py测试脚本

这里最关键的是name.txt和faceEmbedding.npy这两个文件的配对关系。faceEmbedding.npy是一个numpy数组,每一行对应一个人脸的特征向量;name.txt里的每一行名字则与该向量按索引一一对应。也就是说,第i行特征向量对应第i个名字,这个映射关系一旦错位,整个识别就会张冠李戴。我在看代码时特别注意了文件处理逻辑,有没有做长度校验、有没有容错,这决定了你新增人脸时会不会把整个库搞崩。

2.2 create_dataset.py的工作机制:一张张脸是怎么变成特征向量的

create_dataset.py负责的是系统最前置的一步:建立人脸特征库。它通过调用摄像头采集人脸图片,然后用face_recognition库对检测到的人脸编码,得到128维的特征向量,最后把向量存进faceEmbedding.npy,把名字写进name.txt。整体流程分三步走:第一步,调用摄像头获取视频帧;第二步,在帧里定位人脸区域并截取;第三步,把人脸图传给face_recognition.face_encodings函数生成特征向量。

import face_recognition import cv2 import numpy as np from common import config_path def add_new_face(name: str, frame: np.ndarray): # 检测当前帧里的人脸位置 face_locations = face_recognition.face_locations(frame) if len(face_locations) != 1: return False, "需要且仅需要一张人脸出现在画面中" # 提取人脸特征向量,默认是128维 face_encoding = face_recognition.face_encodings(frame, face_locations)[0] # 加载已有特征库,若文件不存在则新建 try: embeddings = np.load(config_path("faceEmbedding.npy")) names = open(config_path("name.txt"), encoding="utf-8").read().splitlines() except FileNotFoundError: embeddings = np.empty((0, 128)) names = [] # 检查是否与已有特征重复,避免重复录入 if len(embeddings) > 0: distances = np.linalg.norm(embeddings - face_encoding, axis=1) if np.min(distances) < 0.45: return False, "该人脸已存在于特征库中" # 追加新特征并保存 embeddings = np.vstack([embeddings, face_encoding.reshape(1, -1)]) names.append(name) np.save(config_path("faceEmbedding.npy"), embeddings) with open(config_path("name.txt"), "w", encoding="utf-8") as f: f.write("\n".join(names)) return True, "录入成功"

这段代码里有几个点需要重点说。face_encodings返回的向量是128个浮点数,这是face_recognition基于dlib的预训练模型算出来的,模型本身是在大规模人脸数据集上训练好的,不需要自己训练,这也是这个库对新手友好的根本原因。np.linalg.norm(embeddings - face_encoding, axis=1)计算的是欧氏距离,face_recognition官方文档建议以0.6作为阈值,但实际场景里我会压缩到0.45到0.5之间,因为0.6在摄像头角度变换大的时候容易把不同人判成同一个人。保存特征时为什么不直接存图片?一个是体积问题,一张128维的float数组只占512字节,而一张jpg随便几十KB;另一个是隐私问题,特征向量理论上无法还原成人脸图像。

2.3 核心识别逻辑:欧氏距离阈值与容错设计

人脸识别签到系统的主程序camera_use.py里,最关键的一段就是实时帧的识别比对。它的做法很直接:把摄像头当前帧里的人脸位置全部框出来,然后对每个框提取特征向量,和faceEmbedding.npy里的所有人算距离,取最小的那个作为识别结果。如果最小的距离都超过了预设阈值,就判定为"陌生人"。

import face_recognition import cv2 import numpy as np def match_face(frame, embeddings, names, tolerance=0.5): unknown_encodings = face_recognition.face_encodings(frame) if not unknown_encodings: return None, None for unknown_encoding in unknown_encodings: distances = np.linalg.norm(embeddings - unknown_encoding, axis=1) min_index = int(np.argmin(distances)) min_distance = distances[min_index] if min_distance <= tolerance: return names[min_index], round(float(min_distance), 4) return "陌生人", None

注意这段代码里的tolerance参数,这就是整个系统最容易产生"翻车"体验的地方。tolerance设得太大,比如0.65,不同的人脸会很频繁地被互相误认;设得太小,比如0.35,同一个人换个光线角度就有可能被拒之门外。我一般建议在0.45到0.55之间调,先拿3到5个人做一轮测试,统计同一人和不同人的距离分布,再取两者的中间值作为阈值——这是一种很朴素但有效的校准方式。另外,np.linalg.norm默认计算的是L2范数即欧氏距离,你也可以换成face_recognition.compare_faces内置函数,但那个函数内部同样是距离比较,只是封装得更好读罢了。实时摄像头场景里每帧都做全库比对,如果库里人很多(超过500人),特征矩阵在内存里会变得很大,需要考虑用向量检索工具,而这份源码包的规模下numpy直接算完全够用。

2.4 GUI界面与签到记录:Tkinter如何把识别结果变成一条可追溯的考勤数据

签到系统没有界面就没有产品形态。这个项目用的是Tkinter,Python标准库自带的GUI框架,优点是零依赖、打包发布时少背一个库的锅,缺点是界面风格比较朴素。主窗口通常包含几个部分:视频显示区(用Label组件实时刷新帧)、签到按钮、结果显示区、以及签到记录导出按钮。视频流由OpenCV的VideoCapture读取,通过Tkinter的after方法周期性刷新画面,这样才能让摄像头画面"动"起来。

import tkinter as tk import cv2 from PIL import Image, ImageTk class SignInApp: def __init__(self, root): self.root = root self.cap = cv2.VideoCapture(0) self.video_label = tk.Label(root) self.video_label.pack() self.record_text = tk.Text(root, height=8, width=40) self.record_text.pack() self.update_frame() def update_frame(self): ret, frame = self.cap.read() if ret: rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(rgb_frame) img_tk = ImageTk.PhotoImage(img) self.video_label.config(image=img_tk) self.video_label.image = img_tk self.root.after(30, self.update_frame) # 约33fps def on_sign_in(self, name, timestamp): self.record_text.insert(tk.END, f"{name} {timestamp}\n")

这里有个容易被忽略的点:self.video_label.image = img_tk这一行不可少。Tkinter的Label如果只调用config(image=...)而不保留引用,图片会被垃圾回收机制回收掉,画面会直接黑屏或者闪退——这是我跑类似GUI程序时踩过最典型的坑。root.after(30, self.update_frame)也就是每秒大约33帧,对实时预览和识别来说是个合理的刷新间隔;如果你CPU比较弱,可以调整到50毫秒,降低识别频率来换流畅度。签到的数据记录,一般推荐直接用csv或者sqlite,而不是用txt文件。csv的好处是Excel能直接打开,sqlite的好处是并发读写和数据查询更可靠。

3. 特征库的构建与管理:让签到系统从"认识一个人"到"认识所有人"

3.1 为什么用npy文件存特征而非数据库存图片

这个问题的答案直接决定了整个系统的架构。包里用faceEmbedding.npy和name.txt两个文件就把人脸特征库管理起来了,够用且简单。npy是numpy的二进制序列化格式,加载速度非常快,np.load一下整个矩阵就进内存了,而如果存在sqlite或者mysql里,每次识别都要查库再反序列化,实时性就差很远。还有一点是图片的原始像素数据占空间,一个特征向量压缩成人脸的关键信息后,不仅体积小,比对时算距离也快。但npy方案也有明显的边界:它不支持增量写入的并发安全,如果两个程序同时写这个文件,文件会损坏;它也不带索引,当特征数量上万时,全量距离计算会变慢。所以这个架构只适合中小规模(几十到几百人),这点一定要有清晰认知。

3.2 批量建档的正确姿势:采集多少张图才算够

建档这个环节决定了整个签到系统到底好不好用。每录入一个新成员,我一般会让他坐在摄像头前面,缓慢转动头部,系统自动采集10到20张不同角度的帧,然后每张帧都做一次特征提取,最后把这些特征向量求平均或者全部存进去。为什么要多角度采集?因为face_recognition虽然是深度学习模型,但小角度的侧脸、低头、仰头都会影响特征向量的稳定输出,如果只采一张正脸照片,实际签到时角度稍微偏一点距离就会飙升,非常容易误判为陌生人。下面是典型的批量建档脚本写法:

import cv2, face_recognition, numpy as np, os def collect_dataset(person_name: str, save_dir: str, frames_to_collect: int = 20): cap = cv2.VideoCapture(0) collected = [] while len(collected) < frames_to_collected: ret, frame = cap.read() if not ret: continue # 每帧都尝试提取特征,检测到人脸才算有效 locs = face_recognition.face_locations(frame) if len(locs) == 1: encoding = face_recognition.face_encodings(frame, locs)[0] collected.append(encoding) cv2.imwrite(os.path.join(save_dir, f"{person_name}_{len(collected)}.jpg"), frame) # 实时显示剩余采集数量 cv2.putText(frame, f"Collected: {len(collected)}", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow("Collect", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() return np.array(collected)

这段代码的采集思路是"宁缺毋滥":只有画面里恰好出现一张人脸时才采集,避免多人乱入污染特征。if len(locs) == 1这个条件是大多数人写采集脚本时最容易漏的,如果你不限制,背景里有路人经过时也会被当作数据采进去,特征库就会混入杂音。采集完之后怎么处理这些向量?简单的做法是把它们全丢进特征库里,对同一个人的多个向量和名字做一一映射;稍微讲究一点的做法是对同一组向量求平均,得到一个更稳定的"平均脸特征",降低单帧噪声。我实践下来的经验是:3到5张不同角度的特征融合效果足够好,超过10张提升就非常有限了。

3.3 特征库更新的坑:直接在原npy上追加会毁掉整个数据库

如果你试图用np.load加载一个npy文件、append一行又np.save保存回去,版本不兼容或者维度不一致的问题会让你头皮发麻。一个特别常见的坑是:刚初始化时faceEmbedding.npy还不存在,代码第一次运行时np.load直接报FileNotFoundError;或者特征库是空的(0行128列),但np.vstack的第一维是0,拼接时不注意reshape就会得到(0,)的向量而不是(1,128)。所以保存新特征时,务必确认形状是(1,128)再拼接,我通常会在保存前打印一下矩阵shape做一个断言:

assert new_embedding.shape == (1, 128), f"shape mismatch: {new_embedding.shape}"

另外一个容易踩的问题就是编码一致性的问题。如果创建特征库时用的是GBK编码保存的name.txt,而识别主程序用UTF-8读,中文名字会变成乱码,然后识别成功却显示一个乱码名,签到记录完全没法用。整个项目里凡是涉及中文的地方,编码格式必须全局统一,我习惯在readme里直接写明"所有文件一律UTF-8"。

4. 常见问题与避坑:拿这套源码跑起来必看的排错手册

4.1 摄像头打不开:cap.read()永远返回False

现象:运行camera_use.py后界面能起来,但视频区域是黑的或者提示"摄像头初始化失败"。 原因:最常见是cv2.VideoCapture(0)的摄像头索引号不对。笔记本自带摄像头一般是0,外接USB摄像头可能是1或者2;还有可能是摄像头正被其他程序(比如微信、腾讯会议)占用,OpenCV抢不到设备。 解决:先用一个Python测试脚本暴力枚举索引0到2,每个索引尝试打开并读取一帧;如果索引都正确还是打不开,关闭所有占用摄像头的软件再试。另外注意Linux下需要检查/dev/video*权限,Windows下如果装了多个虚拟摄像头驱动也会有干扰。

4.2 装了face_recognition却import失败

现象:import face_recognition直接抛ModuleNotFoundError或ImportError,有时伴随dlib相关的报错。 原因:face_recognition依赖于dlib,而dlib需要CMake和C++编译器才能从源码构建。Windows用户如果在Python 3.10以上直接pip install dlib,大概率会卡在编译步骤,因为官方wheel只发布到特定版本,后面的版本需要自己编译。Linux用户同样需要apt install build-essential cmake来准备环境。 解决:Windows下推荐先安装Visual Studio Build Tools,勾选"使用C++的桌面开发",然后pip install dlib,成功后再pip install face_recognition;或者用conda安装conda install -c conda-forge dlib,省去编译痛苦。Python版本尽量用3.7到3.9,踩坑最少。

4.3 同一个人识别结果不稳定:时而认出时而陌生人

现象:同一个同事坐在同一个位置,上午签到正常,下午阳光照进来就报陌生人;人往左偏一点就识别失败。 原因:光照和角度变化是face_recognition这类2D模型的通病。人脸特征是从图像像素里提取的,光照变了像素值分布就变了;纯侧脸时模型可能提取不到完整的面部关键点,特征向量自然会漂移。 解决:在签到场景架设固定的正面摄像头,尽量避免强烈侧光;控制人脸在画面中的大小,录入时的脸部像素宽度和识别时保持相近(我一般让脸宽占画面1/4到1/3左右);实测把阈值从0.5放宽到0.55能吸收一部分波动。

4.4 库里明明有人,却频繁把A认成B

现象:两人同时出现在画面里,系统把其中一人签成了另一人,或者来回跳名字。 原因:阈值过宽导致不同人的特征距离小于阈值,误接受了"错误匹配"。也可能是建档时采到的样本质量差,比如模糊、遮挡、光线暗,导致特征向量本身代表性就不足。 解决:调低阈值到0.4左右做一轮回归测试,输出同一人和不同人的距离分布,取两者之间的中位值做最终阈值;同时把不合格的建档样本删除重新采集,确保录入时正面、清晰、光线均匀。

4.5 打包exe后识别和采集全部失灵

现象:代码在pycharm里跑好好的,用PyInstaller打包成exe后,摄像头也能开,但识别永远不返回结果,或者直接闪退。 原因:face_recognition模型文件(dlib的shape_predictor_5_face_landmarks.dat、dlib_face_recognition_resnet_model_v1.dat)没有被打包进exe资源目录。PyInstaller默认只打包py文件,dat文件经常漏掉;运行时模型加载失败,特征提取就会静默出错。 解决:在spec文件里用datas参数把模型目录加进去,并且在代码里改用相对资源路径,比如sys._MEIPASS方式定位模型。具体做法我后面展开,这里先记住核心是"模型文件必须和exe一起带上"。

5. 进阶:从单个摄像头到"多人同时签到"和"迟到识别"

签到系统的价值不在"能识别一个人",而在"多人同时经过时一个不漏,时间戳准确记录"。原始源码在处理多人时用的是循环遍历,人脸多于两个时容易出现漏检。我的做法是多线程加队列:主线程负责读帧,识别线程负责对帧做人脸检测和特征比对,然后把结果推入队列,GUI线程只管消费结果渲染,这样识别耗时不会阻塞画面刷新。

import threading, queue, face_recognition, cv2, numpy as np frame_queue = queue.Queue(maxsize=2) result_queue = queue.Queue() def recognition_worker(embeddings, names, tolerance=0.5): while True: frame = frame_queue.get() # 缩小尺寸加速检测,feature匹配用原图精度 small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb_small = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) locs = face_recognition.face_locations(rgb_small, model="hog") encodings = face_recognition.face_encodings(frame, locs) results = [] for encoding in encodings: dists = np.linalg.norm(embeddings - encoding, axis=1) idx = int(np.argmin(dists)) if dists[idx] <= tolerance: results.append((names[idx], round(float(dists[idx]), 4))) else: results.append(("陌生人", round(float(dists[idx]), 4))) result_queue.put(results) def capture_loop(cap): while True: ret, frame = cap.read() if ret and not frame_queue.full(): frame_queue.put(frame)

这里有几个细节值得展开。face_locations用model="hog"而不是cnn,在CPU上HOG的速度快很多,准确率对正面人脸足够;face_encodings我仍然传原图frame而不是缩小图,因为缩放会影响关键点定位精度。frame_queue设置了maxsize=2,如果识别线程来不及处理,就直接丢旧帧,保证画面永远是新的,而不是越积越久越来越卡。多人签到还有一个边界问题:同一个人站在摄像头前超过5秒,系统会不会重复签到?我一般会维护一个已签到名单的字典,记录签到时间后10分钟内不重复记录,这样既防止重复,又能保留迟到判断的逻辑。

验证整个系统是否达标其实有一套标准的做法:录20个人的人脸库,让这20个人每人分别签到5次,统计识别率和平均响应延迟;再找5个不在库里的陌生人来测试误识别率。我自己的验收线是:识别率95%以上,误识率低于1%,单人识别延迟低于500毫秒。达不到这个指标,先查光照均匀度,再调阈值,最后才怀疑模型本身——大多数情况下都是现场环境的问题。

关于exe打包,我再补充一个整体流程:先把模型文件复制到项目的models/目录,然后修改代码里的路径引用为os.path.join(getattr(sys, '_MEIPASS', os.path.dirname(__file__)), 'models/xxx.dat'),最后在spec文件中的datas里写[('models/', 'models')]。这样打出来的exe即使拷到没有Python环境的机器上也能运行。从那以后我每次搭建人脸签到系统,都会强制先把建档、识别、导出三件事跑一遍验收流程再交付,以防交付后一天到晚被叫去"看下怎么回事"。希望帮到你。

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

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

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

立即咨询