news 2026/9/11 10:22:18

用Python构建本地人脸识别签到系统:从特征库到实时识别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python构建本地人脸识别签到系统:从特征库到实时识别

简介:一套基于Python的人脸识别签到系统完整源码,面向计算机视觉入门及中级开发者,适用于课堂考勤、会议签到、门禁打卡等场景。系统结合OpenCV、dlib与Face_recognition库实现人脸检测、特征提取和比对识别,并用Tkinter等GUI框架构建交互界面,解决传统签到效率低、易代签的问题。资源以rar压缩包提供,共20个文件,仅95KB,包含6个Python脚本(覆盖人脸数据采集、特征向量提取、实时识别、文件处理、工具函数等模块)、4个pyc编译文件、4个xml配置文件,以及npy特征向量、readme说明文档和测试图片。已有3985人学习浏览,具备一定参考价值。读者可获得完整可运行的人脸签到项目:从摄像头采集人脸、构建特征库、到GUI实时签到与记录导出,目录结构清晰,配合说明文档便于快速部署和二次开发,也能帮助理解人脸识别系统的整体架构与关键实现细节。

1. 本地化的人脸识别签到系统:从摄像头到特征库,一条完整的工程链路

考勤场景里,刷卡和指纹的痛点不在识别速度,而在“代刷”和“设备绑定”。买一台成品人脸识别门禁机,硬件质量没问题,但后台数据通常走厂商云平台,想导出到公司内部 OA 还要再看厂商接口的脸色。用 Python 自己搭一套人脸识别签到系统,数据全程留在本机,识别逻辑可以随时改,成本也基本只有一套普通 USB 摄像头的钱。

这套资源把工程拆成了三段:create_dataset.py负责把照片转换成 128 维人脸特征,生成faceEmbedding.npyname.txtcamera_use.pyFace_login负责打开摄像头、检测人脸、比对并触发签到;file_processing.py负责把签到结果写成本地记录,供后续查询和导出。三层的切分方式比较适合真实项目:即使以后把 GUI 换成 Web 接口,识别和记录模块仍然可以复用。下面从第一段的特征库开始讲。

2. 先建底库:用 create_dataset.py 把人脸照片转成 faceEmbedding.npy 与 name.txt

face_recognition底层依赖 dlib,它把输入人脸归一化到 128 维浮点向量。这个向量不是某个像素的简单组合,而是通过深度神经网络提取的通用面部特征。两个不同人的向量在特征空间里距离通常比较大,同一个人的不同照片则向量距离很小。所以“建库”这步的任务,就是批量把这些向量从图片中抽出来,按照固定顺序落盘,供后面实时识别加载。

2.1 工程文件结构:哪些是运行时产物,哪些是代码

拿到解压后的源码目录,先看清边界,避免把运行生成的.npy当成代码改。下面的表格列出本项目里最常见的文件职责:

文件/目录职责使用方式
create_dataset.py遍历照片目录,生成特征库命令行运行一次
faceEmbedding.npy每人一个 128 维向量的矩阵由程序写入,识别时读取
name.txt与矩阵行号对齐的姓名列表由程序写入,识别时读取
camera_use.py摄像头采集、抽帧、人脸检测入口开始签到前启动
Face_login识别主程序/模块,包含签到状态控制接入 GUI 或单独运行
common.py公共配置:阈值、路径、摄像头编号全局统一修改
util.py校验、日志、文件名处理等辅助函数被其他模块 import
file_processing.py签到记录读写、导出表格识别成功后调用
test_images测试图像集,验证模型效果调参时手动运行

推荐的运行顺序是:在 Python 3.6+ 环境里安装好opencv-pythonnumpyface_recognition,准备一批单人正脸照片,先跑create_dataset.py;确认生成的faceEmbedding.npyname.txt行数一致后,再启动camera_use.pyFace_loginemb/目录如果存在,通常存储单张照片的中间编码,方便新增人员时只增量更新某一行,不用每次把全量照片重新跑一遍。

2.2 建立特征库的代码逻辑:均值编码与持久化

常见做法是把每个人的照片放在一个独立的目录下,目录名里带姓名,例如images/001_zhangsancreate_dataset.py会扫描这些目录,对每个目录里所有照片抽取编码并取平均。取平均是因为单张照片的光照、侧脸程度都不一样,多张平均后的向量更能代表这个人的稳定外观。

import os import numpy as np import face_recognition DATA_ROOT = "images" embeddings = [] names = [] for person_dir in sorted(os.listdir(DATA_ROOT)): person_path = os.path.join(DATA_ROOT, person_dir) if not os.path.isdir(person_path): continue # 目录名适当清洗后作为显示姓名,例如 001_zhangsan -> zhangsan person_name = person_dir.split("_", 1)[1] face_vectors = [] for img_file in sorted(os.listdir(person_path)): if not img_file.lower().endswith((".jpg", ".jpeg", ".png")): continue img = face_recognition.load_image_file(os.path.join(person_path, img_file)) # num_jitters=10 表示对同一张脸做 10 次抖动重采样 encodings = face_recognition.face_encodings(img, num_jitters=10) if encodings: # 如果照片里有多张脸,这里只取检测到的第一张 face_vectors.append(encodings[0]) if face_vectors: # 同一人的多张照片编码取平均,得到一条更稳定的底库向量 embeddings.append(np.mean(face_vectors, axis=0)) names.append(person_name) np.save("faceEmbedding.npy", np.array(embeddings)) with open("name.txt", "w", encoding="utf-8") as f: f.write("\n".join(names)) print(f"完成:{len(names)} 人,特征维度 {np.array(embeddings).shape[1]}")

num_jitters=10是我习惯的折中值:数值越大编码耗时越长,但抗轻微表情变化的效果更好。如果只有一张照片,num_jitters=1也能跑,只是后面误识风险略高。face_encodings里的人脸检测模型默认是hog,CPU 上速度尚可;如果底库照片分辨率很低,可以显式加model="cnn",但耗时会显著增加,不适合大批量离线处理。

这里最容易踩的坑是顺序错位。name.txt里第一行的人名必须对应矩阵第一行向量,后续识别时所有索引都是从矩阵行号反查姓名,一旦顺序错乱,签到记录会全部张冠李戴。所以在上面的代码里,我用同一个列表同步追加,避免两个文件由不同循环写入导致的错位。

2.3 用 common.py 和 util.py 封装特征库加载与校验

底库文件生成后,识别程序每次启动都要加载一次。如果直接写np.load,遇到文件缺失或特征维度不对,报错信息往往很晦涩。资源包里的common.pyutil.py在这个环节起的是“兜底校验”作用。常见做法是抽一个load_face_db函数放到util.py,统一检查维度、姓名长度、是否存在无效空行。

import numpy as np def load_face_db(npy_path="faceEmbedding.npy", name_path="name.txt"): embeddings = np.load(npy_path) with open(name_path, "r", encoding="utf-8") as f: names = [line.strip() for line in f if line.strip()] if len(names) != len(embeddings): raise ValueError(f"特征数量 {len(embeddings)} 与姓名数量 {len(names)} 不一致") # face_recognition 的编码是 128 维,检查维度可以在启动时快速暴露问题 if embeddings.ndim != 2 or embeddings.shape[1] != 128: raise ValueError(f"特征维度异常,预期矩阵形状为 (N, 128),实际为 {embeddings.shape}") return embeddings, names

load_face_db里的两个校验非常值得保留。第一个校验防的是中途追加人员时只改了矩阵、忘记改name.txt;第二个校验防的是误把别的模型导出文件当成faceEmbedding.npy加载。embeddings通常以float64保存,与face_recognitionface_distance兼容。把这样的函数放到util.pyFace_login里只需一行known_faces, names = load_face_db()就能拿到干净数据,后续调阈值时也不会被脏数据干扰。

3. 识别与签到判定:camera_use.py 和 Face_login 的实时比对链路

真正跑起来以后,摄像头以每秒 20-30 帧的速度产生画面,而人脸检测在 CPU 上单帧耗时可能就要两三百毫秒。所以实时模块不能死板地“来一帧处理一帧”,而是要把检测频率降下来、把参与检测的图像缩小,再把识别结果映射回原画面。

3.1 摄像头取帧到人脸编码:缩小、转色、跳帧

camera_use.py打开摄像头后,通常先初始化cv2.VideoCapture(0)。这里的参数 0 是系统默认摄像头,普通笔记本内摄像头或 USB 外接摄像头一般都对应 0;如果设备没图像,可以换成 1、2 试试。取帧后的第一步是缩小,常用fx=0.5缩放比例,把 1920x1080 的帧降到 960x540,检测速度能快一倍以上,但识别精度下降不明显,因为人脸检测器的输入本身不需要超高分辨率。

import cv2 import face_recognition video_capture = cv2.VideoCapture(0, cv2.CAP_DSHOW) # Windows 下避免 MSMF 延迟 process_every_n = 2 frame_count = 0 while True: ret, frame = video_capture.read() if not ret: break frame_count += 1 # 缩放后转成 RGB,face_recognition 内部使用 RGB 顺序 small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) if frame_count % process_every_n == 0: face_locations = face_recognition.face_locations(rgb_small_frame, model="hog") face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations) # 识别和签到判断逻辑在这里调用

cv2.CAP_DSHOW是 Windows 上比较常见的摄像头后端参数,可以缓解 DirectShow 掉帧问题,Linux 下不需要传。process_every_n=2相当于每两帧做一次识别,假设摄像头 30fps,实际识别频率约 15fps,对人脸签到这种“人站在摄像头前等结果”的场景足够。得到face_locations时,坐标对应的是缩放后的图像;如果要画框到原帧,需要把 top/right/bottom/left 四个值全部乘 2。这是新手最容易漏的坐标映射,漏了之后画框位置会偏移。

3.2 比对算法:用 face_distance 代替 compare_faces,保留可调细节

face_recognition.compare_faces(known_encodings, unknown_encoding, tolerance=0.6)返回布尔列表,但真实项目里我更喜欢用face_distance,因为它给出的是具体的欧氏距离,方便对阈值做微调。tolerance越小,识别越严格,漏识率会上升;越大,越容易误判为同一人。资源里常用的匹配逻辑如下:

unknown_encoding = face_encodings[0] # 假设画面里只有一个人 known_encodings, names = load_face_db() distances = face_recognition.face_distance(known_encodings, unknown_encoding) min_idx = int(np.argmin(distances)) min_dist = float(distances[min_idx]) if min_dist <= 0.45: matched_name = names[min_idx] else: matched_name = "Unknown"

MATCH_THRESHOLD = 0.45是参考起点,不是固定标准。当底库照片都是在室内固定光源下拍摄时,阈值可以放到 0.5;当现场有强背光或者员工经常戴口罩、帽子,阈值建议收紧到 0.4 以下,宁可提示 “Unknown”,也不要刷成另一个人。face_distance返回的是长度为 N 的数组,N 等于底库人数;它内部会把两个向量转成归一化特征再算欧氏距离,所以不同底库之间比对结果不能横向比较,每次加载后重新算距离。

如果觉得只看最近距离不够稳,还可以记录第二小距离作为参考。当第二小距离离最小距离非常近时,说明这个人撞脸了,此时最好不直接签到,而是弹一个二次确认。这个思路在第五章的阈值标定脚本里可以继续扩展。

3.3 签到状态锁:如何在多人脸和连续帧中避免重复签到

识别到姓名后,直接写文件是最简单的方案,但会带来重复签到:人站在摄像头前 5 秒,识别成功 20 次,就会刷出 20 条记录。业务上正确逻辑是“每人每天只记第一次”。Face_login里一般用一个signed_set保存已经签过的“日期_姓名”键。

import time today = time.strftime("%Y-%m-%d") signed_key = f"{today}_{matched_name}" if matched_name != "Unknown" and signed_key not in signed_set: file_processing.write_record(matched_name) signed_set.add(signed_key) print(f"{matched_name} 签到成功")

signed_set在程序启动时应从历史记录文件里预加载,否则程序重启一次,之前签过到的人又能再签一次。预加载很简单:读取 CSV 中所有datename字段,拼成同样的日期_姓名键放进去。多人脸同时出现时,两个人脸框会各自走一遍上面的逻辑,但由于signed_set是按姓名去重,同一个人同时被左右两个摄像头框识别到也不会重复记录。

阈值、摄像头编号、跳帧参数这些配置都不应该散落在各个脚本里。把它们集中到common.py,例如VIDEO_INDEX = 0MATCH_THRESHOLD = 0.45PROCESS_EVERY_N = 2。这样切换摄像头或调整识别灵敏度时,只改一个文件,不用在camera_use.pyFace_login里交叉查找。

4. 从识别到可查:file_processing.py 的签到记录与 Tkinter 界面

识别链路再漂亮,最后还是要落到“谁几点几分签到了”这一行数据。file_processing.py承担的就是把内存中的姓名转成持久化记录,并提供给 GUI 查询和下载。

4.1 记录文件结构:CSV 字段设计与线程安全写入

签到系统记录格式很简单,但设计时也要想清楚后续报表要什么字段。建议至少包含date、time、name、status四列,其中status保留给迟到、早退等判定使用。CSV 是通用性最好的纯文本格式,Excel、pandas 都能直接读,不需要额外数据库依赖。

import threading from datetime import datetime _write_lock = threading.Lock() def write_record(name, path="sign_records.csv", status="正常"): now = datetime.now() line = f"{now:%Y-%m-%d},{now:%H:%M:%S},{name},{status}\n" with _write_lock: with open(path, "a", encoding="utf-8") as f: f.write(line)

threading.Lock()在这里很重要。如果识别模块放在独立线程里,两个识别线程同时write_record,可能互相写入半行数据。加锁之后,写文件变成原子操作,不会出现同一行里两个人名交错的情况。encoding="utf-8"是另一个隐藏坑,如果读用 GBK、写用 UTF-8,Excel 打开 CSV 时中文会乱码;更省心的做法是统一存 UTF-8,导出时再按需加 BOM。

4.2 Tkinter 视频画布与识别联动的写法

GUI 部分资源里用的是 Tkinter,它是 Python 标准库,免安装,适合这种体量的桌面端。Tkinter 的陷阱是主事件循环是单线程的,如果把while True的摄像头循环直接放进主线程,窗口会整个卡死,点任何按钮都没反应。正确做法是用root.after周期调度刷新函数。

import tkinter as tk from PIL import Image, ImageTk root = tk.Tk() lbl_video = tk.Label(root) lbl_status = tk.Label(root, text="等待识别...", font=("微软雅黑", 14)) lbl_video.pack() lbl_status.pack() cap = cv2.VideoCapture(0) def update_frame(): ret, frame = cap.read() if not ret: root.after(30, update_frame) return # 在这里调用上文的识别模块,返回 matched_name frame, matched_name = Face_login.run_on_frame(frame) lbl_status.config(text=f"识别结果: {matched_name}") rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(rgb) imgtk = ImageTk.PhotoImage(pil_img) lbl_video.configure(image=imgtk) lbl_video.image = imgtk # 防止被垃圾回收 root.after(30, update_frame) update_frame() root.mainloop()

lbl_video.image = imgtk这行必须保留。Tkinter 的PhotoImage对象如果只保存在局部变量里,函数一退出就会被垃圾回收,界面上只剩空白。after(30, update_frame)相当于是和mainloop协商的定时器,每次刷新后重新注册自己,才形成连续的视频显示。界面上加“开始签到”按钮时,不要用线程去sleep推迟识别,直接在摄像头循环里做状态开关即可。

4.3 查询、导出与编码处理

查询功能给运维和行政用的多。一种实现是没有数据库直接用 CSV 过滤,代码足够短,也不破坏原工程结构。导出 Excel 时要注意openpyxl需要额外安装,放在requirements.txt里即可。

def query_records(date_str, path="sign_records.csv"): result = [] with open(path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split(",") if len(parts) >= 3 and parts[0] == date_str: result.append(parts) return result def export_excel(rows, out_path="签到表.xlsx"): import openpyxl wb = openpyxl.Workbook() ws = wb.active ws.append(["日期", "时间", "姓名", "状态"]) # 标题行 for row in rows: ws.append(row) wb.save(out_path)

query_records按行字符串startswith也可以,但显式split后判断日期列,能避免姓名或时间里恰好包含同一天的巧合。导出到 Excel 前,如果 CSV 是用 UTF-8 保存的,建议先转成utf-8-sig写一个中间文件,或者让openpyxl直接写xlsx格式,这样 Windows 用户下载后双击用 Excel 打开,中文表头不会乱码。

5. 用 test_images 做阈值标定,再把摄像头线程拆开提升帧率

最后落几个实际调试操作。很多签到系统上线后被人诟病“认不出”或“乱认人”,根源往往不是模型不行,而是阈值和图像输入没有针对现场调过。下面三个操作可以直接在生产系统里改。

5.1 用 test_images 自动标定阈值

test_images目录不应只是放着看,可以写一个脚本,把已知人名的正样本照片和无关人脸的负样本照片全部跑一遍face_distance,统计不同阈值下的误识率和漏识率。简单做法是遍历所有测试图,把“识别成正确人名”算 TP,“识别成错误人名”算 FP。

for threshold in np.arange(0.35, 0.56, 0.05): tp = fp = fn = 0 for image_path, true_name in test_cases: pred = predict_with_threshold(image_path, threshold) if pred == true_name: tp += 1 elif pred != "Unknown": fp += 1 elif true_name != "Unknown": fn += 1 print(f"threshold={threshold:.2f} 误识={fp} 漏识={fn} 正确={tp}")

这里predict_with_threshold就是把第三章里那段face_distance逻辑封装成函数。和人脸识别门禁机对照之后你会发现,多数室内考勤场景在0.45~0.50之间比较平衡,但如果现场有戴帽子和口罩的,必须把漏识率压到最低,阈值宁愿低于0.42

5.2 生产者消费者线程:让画面不卡在识别上

单线程里识别一次两三百毫秒,视频显示会明显掉帧。改进思路是两线程:主线程只读帧,放到queue.Queue;识别线程从队列取帧并计算,把结果放回另一个队列供 GUI 展示。当队列满时,新帧替代旧帧,保证显示延迟始终可控。

import queue frame_queue = queue.Queue(maxsize=2) def capture_loop(): cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: continue if frame_queue.full(): try: frame_queue.get_nowait() except queue.Empty: pass frame_queue.put(frame) def recognition_loop(): while True: frame = frame_queue.get() matched_name = Face_login.run_on_frame(frame) result_queue.put((frame, matched_name))

maxsize=2是关键,队列太深会导致处理的是几秒前的画面,看起来像“慢动作实时监控”。这里get_nowait是丢旧帧的兜底,比清空队列更节省代码。

5.3 多人脸连续帧确认

如果现场经常同时出现两个人,瞬时识别很可能因为侧脸或遮挡出现跳变。常见做法不是一识别到就签到,而是给每个人一个候选帧计数:同一姓名连续或累计出现 3 帧,才真正写入签到记录。这样也能避免一个人从画面边缘快速经过时被误签到。

candidate_counter = {} FRAME_CONFIRM = 3 if matched_name != "Unknown": candidate_counter[matched_name] = candidate_counter.get(matched_name, 0) + 1 if candidate_counter[matched_name] >= FRAME_CONFIRM: do_sign_in(matched_name) else: candidate_counter.clear()

common.py里把FRAME_CONFIRMMATCH_THRESHOLDPROCESS_EVERY_N三组参数放在同一个配置区,调光照或摄像头角度时只改这几个数值,就能在“灵敏”和“稳定”之间来回切换。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 10:19:41

macOS虚拟机安装与性能优化全攻略

1. macOS虚拟机安装方案全景解析在跨平台开发测试或特定软件兼容性验证场景中&#xff0c;macOS虚拟机的需求持续增长。根据实际硬件环境和性能需求&#xff0c;目前主流方案可分为三大技术路线&#xff1a;1.1 基于Intel平台的虚拟化方案VMware Fusion和Parallels Desktop作为…

作者头像 李华
网站建设 2026/9/11 10:17:39

光伏系统中LC功率均衡器设计与应用

1. 项目背景与核心价值光伏发电系统正从传统的交流并网架构向中压直流架构演进&#xff0c;这种转变带来了更高的传输效率和更简单的系统结构。但在独立式光伏系统中&#xff0c;多个光伏组串间的功率不均衡问题会显著影响系统整体效率——实测数据显示&#xff0c;当组串间功率…

作者头像 李华
网站建设 2026/9/11 10:08:57

云服务器选型避坑:长期成本与免费组网实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华