用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.npy和name.txtcamera_use.py和Face_login负责打开摄像头、检测人脸、比对并触发签到file_processing.py负责把签到结果写成本地记录供后续查询和导出。三层的切分方式比较适合真实项目即使以后把 GUI 换成 Web 接口识别和记录模块仍然可以复用。下面从第一段的特征库开始讲。2. 先建底库用 create_dataset.py 把人脸照片转成 faceEmbedding.npy 与 name.txtface_recognition底层依赖 dlib它把输入人脸归一化到 128 维浮点向量。这个向量不是某个像素的简单组合而是通过深度神经网络提取的通用面部特征。两个不同人的向量在特征空间里距离通常比较大同一个人的不同照片则向量距离很小。所以“建库”这步的任务就是批量把这些向量从图片中抽出来按照固定顺序落盘供后面实时识别加载。2.1 工程文件结构哪些是运行时产物哪些是代码拿到解压后的源码目录先看清边界避免把运行生成的.npy当成代码改。下面的表格列出本项目里最常见的文件职责文件/目录职责使用方式create_dataset.py遍历照片目录生成特征库命令行运行一次faceEmbedding.npy每人一个 128 维向量的矩阵由程序写入识别时读取name.txt与矩阵行号对齐的姓名列表由程序写入识别时读取camera_use.py摄像头采集、抽帧、人脸检测入口开始签到前启动Face_login识别主程序/模块包含签到状态控制接入 GUI 或单独运行common.py公共配置阈值、路径、摄像头编号全局统一修改util.py校验、日志、文件名处理等辅助函数被其他模块 importfile_processing.py签到记录读写、导出表格识别成功后调用test_images测试图像集验证模型效果调参时手动运行推荐的运行顺序是在 Python 3.6 环境里安装好opencv-python、numpy、face_recognition准备一批单人正脸照片先跑create_dataset.py确认生成的faceEmbedding.npy和name.txt行数一致后再启动camera_use.py或Face_login。emb/目录如果存在通常存储单张照片的中间编码方便新增人员时只增量更新某一行不用每次把全量照片重新跑一遍。2.2 建立特征库的代码逻辑均值编码与持久化常见做法是把每个人的照片放在一个独立的目录下目录名里带姓名例如images/001_zhangsan。create_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_jitters10 表示对同一张脸做 10 次抖动重采样 encodings face_recognition.face_encodings(img, num_jitters10) if encodings: # 如果照片里有多张脸这里只取检测到的第一张 face_vectors.append(encodings[0]) if face_vectors: # 同一人的多张照片编码取平均得到一条更稳定的底库向量 embeddings.append(np.mean(face_vectors, axis0)) names.append(person_name) np.save(faceEmbedding.npy, np.array(embeddings)) with open(name.txt, w, encodingutf-8) as f: f.write(\n.join(names)) print(f完成{len(names)} 人特征维度 {np.array(embeddings).shape[1]})num_jitters10是我习惯的折中值数值越大编码耗时越长但抗轻微表情变化的效果更好。如果只有一张照片num_jitters1也能跑只是后面误识风险略高。face_encodings里的人脸检测模型默认是hogCPU 上速度尚可如果底库照片分辨率很低可以显式加modelcnn但耗时会显著增加不适合大批量离线处理。这里最容易踩的坑是顺序错位。name.txt里第一行的人名必须对应矩阵第一行向量后续识别时所有索引都是从矩阵行号反查姓名一旦顺序错乱签到记录会全部张冠李戴。所以在上面的代码里我用同一个列表同步追加避免两个文件由不同循环写入导致的错位。2.3 用 common.py 和 util.py 封装特征库加载与校验底库文件生成后识别程序每次启动都要加载一次。如果直接写np.load遇到文件缺失或特征维度不对报错信息往往很晦涩。资源包里的common.py和util.py在这个环节起的是“兜底校验”作用。常见做法是抽一个load_face_db函数放到util.py统一检查维度、姓名长度、是否存在无效空行。import numpy as np def load_face_db(npy_pathfaceEmbedding.npy, name_pathname.txt): embeddings np.load(npy_path) with open(name_path, r, encodingutf-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, namesload_face_db里的两个校验非常值得保留。第一个校验防的是中途追加人员时只改了矩阵、忘记改name.txt第二个校验防的是误把别的模型导出文件当成faceEmbedding.npy加载。embeddings通常以float64保存与face_recognition的face_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 试试。取帧后的第一步是缩小常用fx0.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 # 缩放后转成 RGBface_recognition 内部使用 RGB 顺序 small_frame cv2.resize(frame, (0, 0), fx0.5, fy0.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, modelhog) face_encodings face_recognition.face_encodings(rgb_small_frame, face_locations) # 识别和签到判断逻辑在这里调用cv2.CAP_DSHOW是 Windows 上比较常见的摄像头后端参数可以缓解 DirectShow 掉帧问题Linux 下不需要传。process_every_n2相当于每两帧做一次识别假设摄像头 30fps实际识别频率约 15fps对人脸签到这种“人站在摄像头前等结果”的场景足够。得到face_locations时坐标对应的是缩放后的图像如果要画框到原帧需要把 top/right/bottom/left 四个值全部乘 2。这是新手最容易漏的坐标映射漏了之后画框位置会偏移。3.2 比对算法用 face_distance 代替 compare_faces保留可调细节face_recognition.compare_faces(known_encodings, unknown_encoding, tolerance0.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 UnknownMATCH_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 中所有date和name字段拼成同样的日期_姓名键放进去。多人脸同时出现时两个人脸框会各自走一遍上面的逻辑但由于signed_set是按姓名去重同一个人同时被左右两个摄像头框识别到也不会重复记录。阈值、摄像头编号、跳帧参数这些配置都不应该散落在各个脚本里。把它们集中到common.py例如VIDEO_INDEX 0、MATCH_THRESHOLD 0.45、PROCESS_EVERY_N 2。这样切换摄像头或调整识别灵敏度时只改一个文件不用在camera_use.py和Face_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, pathsign_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, encodingutf-8) as f: f.write(line)threading.Lock()在这里很重要。如果识别模块放在独立线程里两个识别线程同时write_record可能互相写入半行数据。加锁之后写文件变成原子操作不会出现同一行里两个人名交错的情况。encodingutf-8是另一个隐藏坑如果读用 GBK、写用 UTF-8Excel 打开 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(textf识别结果: {matched_name}) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil_img Image.fromarray(rgb) imgtk ImageTk.PhotoImage(pil_img) lbl_video.configure(imageimgtk) 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, pathsign_records.csv): result [] with open(path, r, encodingutf-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(fthreshold{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(maxsize2) 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))maxsize2是关键队列太深会导致处理的是几秒前的画面看起来像“慢动作实时监控”。这里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_CONFIRM、MATCH_THRESHOLD、PROCESS_EVERY_N三组参数放在同一个配置区调光照或摄像头角度时只改这几个数值就能在“灵敏”和“稳定”之间来回切换。本文还有配套的精品资源点击获取