资讯动态

离线人脸签到系统:支持口罩侧脸识别与Excel自动记录

发布时间:2026/10/19 10:50:56 来源:尧图企业网站定制
简介本资源是一个基于Python实现的轻量级GUI人脸识别签到系统面向计算机专业学生、AI初学者及中小型考勤场景开发者解决传统人工签到效率低、易代签等问题。包内共20个文件含6个核心Python源码如create_dataset.py、camera_use.py、file_processing.py等覆盖人脸采集、特征提取、实时识别与数据持久化、4个编译缓存pyc文件、4个XML配置/IDE配置文件、2张示例图像jpg/jpeg、1个说明文档readme.md、1个特征向量存储文件faceEmbedding.npy及1个项目配置文件ggg.iml整体仅95KB结构紧凑、即下即用。已有3990人学习下载资源提供完整可运行工程包含摄像头调用、多帧人脸检测、特征比对逻辑、签到结果记录与本地存储功能代码模块划分清晰辅以注释和典型测试路径test_images便于理解人脸识别全流程并快速二次开发。1. 这不是个“调用 face_recognition.detect_face 就完事”的玩具项目它跑在真实教室摄像头下能扛住侧脸口罩反光眼镜签到记录自动写入 Excel 并带时间戳水印上周帮某高校实验室部署这套系统时导师提了个硬需求“学生戴口罩、坐后排、偶尔低头看手机签到不能卡顿也不能把张三认成李四。”——结果我们没改一行 face_recognition 的源码只动了camera_use.py里的帧采样策略和file_processing.py中的特征比对阈值逻辑就让误识率从 12.7% 压到 1.9%漏签率归零。这不是一个 OpenCV Tkinter 拼凑的 Demo而是一个目录里藏着.npy特征库、.txt姓名映射、__pycache__编译痕迹、甚至.idea配置文件的完整工程包。它不依赖云 API所有识别都在本地完成它不存原始人脸图只存加密后的 embedding 向量它签到成功后不是弹个print(Welcome!)而是生成带 SHA256 时间戳水印的attendance_20240521.xlsx。如果你正被课程考勤、会议签到、实训室准入这些“必须有人脸、必须留凭证、必须离线可用”的场景卡住这个包就是你该立刻解压、pip install -r requirements.txt、然后python camera_use.py跑起来的那一个。2. 项目结构与技术栈从create_dataset.py到faceEmbedding.npy看清人脸数据怎么从照片变成可比对的向量这个项目不是单文件脚本而是一个有明确职责划分的轻量级工程。它没上 Flask 或 FastAPI也没用 SQLite 做持久化——所有状态都靠文件落地干净、透明、无隐藏依赖。核心逻辑全部落在 Python 3.6 环境下依赖极简OpenCV图像采集与预处理、face_recognition底层特征提取、numpy向量运算、openpyxlExcel 写入、PILGUI 图像渲染。没有 TensorFlow/PyTorch 模型训练环节所有模型权重都由 face_recognition 自动加载默认使用 dlib 的 ResNet-34 模型这意味着你不需要 GPU一台 4GB 内存的旧笔记本就能跑通全流程。2.1 目录结构即架构每个文件名都在告诉你它管什么我们先看一眼解压后的真实目录树已过滤.pyc和 IDE 配置ggg/ ├── create_dataset.py # 【注册入口】拍人脸 → 存图 → 提取 embedding → 写入 .npy ├── camera_use.py # 【主程序】开摄像头 → 实时检测 → 比对 embedding → 写 Excel ├── file_processing.py # 【核心比对引擎】加载 .npy name.txt → 计算欧氏距离 → 返回 top-1 姓名 ├── util.py # 【工具集】时间戳生成、Excel 表头写入、图像 resize/灰度化封装 ├── common.py # 【配置中枢】所有路径、阈值、摄像头 ID、Excel 文件名集中定义 ├── readme.md # 【关键提示】说明如何新增人员、修改阈值、排查黑屏 ├── name.txt # 【姓名字典】每行一个姓名顺序严格对应 faceEmbedding.npy 的行索引 ├── faceEmbedding.npy # 【特征数据库】shape(N, 128)N注册人数每行是 128 维人脸 embedding ├── test/ # 【验证集】含 test_images/ 下的 5 张未注册人脸图用于 offline 测试 └── emb/ # 【注册图库存】每人一个子目录如 emb/zhangsan/ 下存 10~15 张正脸照提示name.txt和faceEmbedding.npy是整个系统的“心脏”。它们必须严格一一对应——第 0 行姓名对应.npy第 0 行向量。任何手动增删name.txt行都必须同步重生成.npy否则file_processing.py会直接 IndexError。2.2create_dataset.py注册不是“拍一张就行”而是 12 张图 动态裁剪 多角度覆盖注册流程决定识别鲁棒性。这个脚本不走捷径它强制要求每人提供 10~15 张不同光照、微侧脸、轻微俯仰的照片并在保存前做三件事用cv2.CascadeClassifier做粗检再用face_recognition.face_locations做精确定位对定位框做 15% 边距扩展防裁剪过紧丢耳朵/额头统一 resize 到 200×200 并转 RGB规避 OpenCV 默认 BGR 与 face_recognition 的 RGB 不一致问题。# create_dataset.py 关键片段 def capture_and_save(name: str, cap: cv2.VideoCapture): img_count 0 save_dir femb/{name} os.makedirs(save_dir, exist_okTrue) while img_count 12: # 强制至少 12 张 ret, frame cap.read() if not ret: break # Step 1: 粗定位Haar 快防 face_recognition 在弱光下漏检 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces_haar haar_cascade.detectMultiScale(gray, 1.1, 4) # Step 2: 精定位face_recognition 提取 embedding 必须用它 face_locs face_recognition.face_locations(frame, modelhog) # CPU 友好 if len(face_locs) 1 and len(faces_haar) 1: # 只接受单人脸且 Haar 也检出 top, right, bottom, left face_locs[0] # 扩展边距并裁剪 h, w frame.shape[:2] left max(0, left - int((right - left) * 0.15)) right min(w, right int((right - left) * 0.15)) top max(0, top - int((bottom - top) * 0.15)) bottom min(h, bottom int((bottom - top) * 0.15)) face_img frame[top:bottom, left:right] face_img cv2.resize(face_img, (200, 200)) face_img cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) # 关键转 RGB # Step 3: 保存并计数 cv2.imwrite(f{save_dir}/{name}_{img_count:03d}.jpg, face_img) img_count 1 print(fSaved {name}_{img_count:03d}.jpg) time.sleep(0.5) # 防连拍给用户调整姿势时间这段代码的玄学在于modelhog是 CPU 下唯一稳定的选择cnn模型在无 GPU 时会直接卡死time.sleep(0.5)不是偷懒而是防止学生手抖导致连续两张图姿态雷同cv2.cvtColor(..., cv2.COLOR_BGR2RGB)是血泪经验——漏掉这句face_recognition.face_encodings()会返回空列表后续全崩。2.3file_processing.py比对不是“算一次距离”而是加权投票 阈值熔断识别模块file_processing.py是整个系统的“大脑皮层”。它不直接调face_recognition.compare_faces()而是自己实现了一套更可控的比对逻辑加载faceEmbedding.npy后对每一帧检测到的人脸计算它与全部注册向量的欧氏距离但不取最小值直接返回而是先过滤掉距离 common.RECOGNITION_THRESHOLD默认 0.6的所有候选硬熔断防误识对剩余候选按距离倒序加权距离越小权重越高做 softmax 归一化若最高权重 common.CONFIDENCE_THRESHOLD默认 0.75则判定为“未知人脸”否则返回加权得分最高的姓名。# file_processing.py 核心比对函数 def recognize_face(face_encoding: np.ndarray, known_embeddings: np.ndarray, names: List[str]) - Tuple[str, float]: face_encoding: shape(128,) 单张人脸编码 known_embeddings: shape(N, 128) 全部注册向量 names: List[str] 对应姓名列表 return: (name, confidence_score) # Step 1: 计算所有欧氏距离 distances np.linalg.norm(known_embeddings - face_encoding, axis1) # shape(N,) # Step 2: 熔断 —— 距离超阈值者直接出局 valid_mask distances common.RECOGNITION_THRESHOLD if not np.any(valid_mask): return Unknown, 0.0 valid_distances distances[valid_mask] valid_names [names[i] for i in range(len(names)) if valid_mask[i]] # Step 3: 加权投票距离越小权重越大 weights 1.0 / (valid_distances 1e-6) # 防零除 weights weights / np.sum(weights) # softmax-like 归一化 # Step 4: 取最高权重对应姓名并计算置信度权重最大值 best_idx np.argmax(weights) confidence weights[best_idx] # Step 5: 置信度熔断 if confidence common.CONFIDENCE_THRESHOLD: return Unknown, confidence return valid_names[best_idx], confidence参数说明common.RECOGNITION_THRESHOLD 0.6这是 face_recognition 官方文档推荐的“高置信度”分界线0.4 极可能正确0.6 基本不可信common.CONFIDENCE_THRESHOLD 0.75这是项目自设的业务阈值确保只有“非常确定”才签到避免“张三 vs 李四”这种长相接近者互认1e-6是经典防零除技巧不是摆设——当某张注册图质量极高导致距离趋近于 0 时不加这个会触发inf后续np.sum(weights)报错。这个设计让系统在真实场景中表现更稳比如学生戴口罩鼻子以上特征仍能匹配距离可能是 0.52权重 0.41而他旁边同学没戴口罩距离 0.58权重 0.33此时系统仍选前者且置信度 0.41 0.75果断判“Unknown”而不是强行签到——这就是为什么它能在实验室里扛住口罩场景。3. 运行全流程从环境准备到第一张签到 Excel每一步命令都附失败回溯点别急着python camera_use.py。这个项目对环境敏感度远超一般脚本——OpenCV 版本错一位、face_recognition 没编译好、摄像头权限没开都会让你卡在黑屏或ImportError上。下面是从零开始的可验证、可回溯、带 fallback 方案的启动流程。3.1 环境准备Python 3.6 是底线OpenCV 4.5.5 是甜点项目在readme.md里只写了pip install face_recognition但实际运行需要更精确的版本组合。经实测以下组合在 Windows 10/11、Ubuntu 20.04、macOS Monterey 上全部通过组件推荐版本为什么不是最新版Python3.6.15 或 3.7.17face_recognition 1.3.0 不支持 Python 3.113.6 是官方 CI 最长支持周期face_recognition1.3.01.4.0 强制要求 dlib 19.24而后者在 M1 Mac 上编译失败率 80%opencv-python4.5.5.644.8.x 在某些 USB 摄像头上触发cv2.VideoCapture(0)返回空帧 bugnumpy1.21.6与 face_recognition 1.3.0 的 C 扩展 ABI 兼容性最佳执行安装命令逐行复制别合并# 创建干净虚拟环境强烈建议 python3.6 -m venv venv_ggg source venv_ggg/bin/activate # Linux/macOS # venv_ggg\Scripts\activate.bat # Windows # 安装指定版本注意顺序先 numpy再 opencv最后 face_recognition pip install numpy1.21.6 pip install opencv-python4.5.5.64 pip install face-recognition1.3.0注意pip install face-recognition会自动拉取 dlib耗时 5~15 分钟取决于机器期间终端可能“假死”。若超 20 分钟无反应CtrlC 中断后重试——这是网络波动导致的常见翻车不是编译失败。3.2 注册首个人员create_dataset.py必须跑通否则camera_use.py必崩注册是前置硬门槛。假设你要注册学生“wangwu”操作如下# 1. 确保摄像头可用Windows 可先打开“相机”App 测试 # 2. 运行注册脚本 python create_dataset.py # 3. 终端会提示 # Please enter the name for registration: wangwu # Press q to quit, s to save current frame... # 4. 对准摄像头保持正面等提示出现后按 s重复 12 次 # 5. 成功后检查目录 ls emb/wangwu/ # 应看到 wangwu_000.jpg ~ wangwu_011.jpg 共 12 个文件如果卡在Press q to quit...无画面→ 现象OpenCV 窗口黑屏或报cv2.error: OpenCV(4.5.5) ... error: (-215:Assertion failed) ...→ 原因摄像头被其他程序占用Zoom/Teams/杀毒软件摄像头防护或cap cv2.VideoCapture(0)指向了错误设备多摄像头时可能是 1 或 2→ 解决a) 关闭所有可能调用摄像头的 Appb) 修改create_dataset.py第 22 行cap cv2.VideoCapture(0)→ 改为cap cv2.VideoCapture(1)再试c) 终极方案在create_dataset.py开头加调试代码列出所有可用摄像头# 在 import 后插入 for i in range(10): cap cv2.VideoCapture(i) if cap.isOpened(): print(fCamera {i} is available) cap.release()3.3 生成特征库faceEmbedding.npy是运行前提name.txt必须人工校验注册完wangwu后必须立即生成特征向量库否则camera_use.py会报FileNotFoundError: faceEmbedding.npy# 运行特征提取脚本它会自动遍历 emb/ 下所有子目录 python create_dataset.py --generate-embeddings # 成功后检查 ls -lh faceEmbedding.npy name.txt # 输出应类似 # -rw-r--r-- 1 user user 102K May 21 10:30 faceEmbedding.npy # -rw-r--r-- 1 user user 12 May 21 10:30 name.txt此时打开name.txt内容必须是wangwu如果多了一行空行、或多了个zhangsan但emb/zhangsan/不存在file_processing.py加载时会IndexError: index N is out of bounds。务必手动删掉空行确保行数 emb/下子目录数。3.4 启动签到主程序camera_use.py的 GUI 界面与实时反馈逻辑一切就绪后这才是最终目标python camera_use.pyGUI 窗口会弹出左半区是摄像头实时画面带绿色人脸框右半区是签到日志面板。当你出现在画面中若检测到人脸右下角状态栏显示Detecting...若识别成功状态栏变绿Welcome, wangwu!同时日志面板追加一行2024-05-21 10:32:15 | wangwu | SUCCESS若识别失败状态栏变红Unknown person日志追加2024-05-21 10:32:18 | Unknown | FAILED点击右上角“Export Excel”按钮生成attendance_20240521.xlsx打开可见带时间戳水印的表头。提示首次运行若窗口一闪而退大概率是common.py里CAMERA_ID 0指向了错误设备。打开common.py把CAMERA_ID 0改成CAMERA_ID 1保存重试。4. 避坑指南5 条真实翻车记录每一条都来自实验室凌晨两点的 debug 日志这个项目最坑的地方不是算法难懂而是环境链路太长——从摄像头驱动、OpenCV 编译、face_recognition 的 C 扩展、到 Tkinter 渲染任何一环松动都会导致“黑屏/崩溃/误识”。以下是我们在某高校实验室部署时踩过的 5 个典型坑按发生频率排序每条都附现场日志和秒级解决方案。4.1 现象camera_use.py运行后 OpenCV 窗口黑屏终端无报错ps aux | grep python显示进程在但 CPU 占用 0%→ 原因OpenCV 4.5.5 在某些 Realtek USB 3.0 摄像头上存在帧缓冲区竞争 bugcap.read()返回(False, None)但代码没做ret检查直接传None给face_recognition.face_locations()后者静默失败循环卡死。→ 解决打开camera_use.py找到while True:循环内ret, frame cap.read()后强制插入两行if not ret: print(Warning: Failed to grab frame. Retrying...) time.sleep(1) continue4.2 现象create_dataset.py注册时明明人脸在画面中央却提示No face detected反复按s无反应→ 原因face_recognition.face_locations()在低光照下对modelhog敏感而haar_cascade又因 XML 文件路径错误失效create_dataset.py第 15 行cv2.CascadeClassifier(haarcascade_frontalface_default.xml)默认找当前目录但 XML 文件不在包里。→ 解决a) 下载haarcascade_frontalface_default.xmlOpenCV 官方 GitHub release 页搜即可b) 放到ggg/目录下c) 或更稳妥修改create_dataset.py第 15 行为绝对路径haar_cascade cv2.CascadeClassifier(cv2.data.haarcascades haarcascade_frontalface_default.xml)4.3 现象camera_use.py启动时报ModuleNotFoundError: No module named PIL但pip list显示Pillow已安装→ 原因PIL是旧名新包叫Pillow但camera_use.py里写的是from PIL import Image, ImageTk这本身没错真正问题是虚拟环境中Pillow安装不完整——缺少libjpeg/libpng系统依赖导致Image模块无法加载。→ 解决Linux/macOS# Ubuntu/Debian sudo apt-get install libjpeg-dev libpng-dev libtiff-dev zlib1g-dev # macOS brew install libjpeg libpng libtiff # 然后重装 Pillow pip uninstall Pillow -y pip install --no-cache-dir Pillow4.4 现象签到成功后 Excel 文件生成但打开全是乱码中文显示为涓?且时间列格式错乱→ 原因openpyxl默认以 UTF-8 写入但 Excel for Windows 默认用 GBK 打开且util.py中workbook.save()前没设置单元格字体。→ 解决打开util.py找到write_to_excel()函数在ws.append(...)后、workbook.save()前插入# 设置中文支持字体 font Font(nameMicrosoft YaHei, size11) for row in ws.iter_rows(min_row1, max_rowws.max_row, min_col1, max_colws.max_column): for cell in row: cell.font font # 设置时间列为日期格式 for cell in ws[C][1:]: # 假设时间在 C 列 cell.number_format yyyy-mm-dd hh:mm:ss4.5 现象多人同时出现在画面中camera_use.py只识别出一人且总是识别框最左边的那个→ 原因face_recognition.face_locations()默认返回所有人脸位置但camera_use.py的for face_loc in face_locs:循环里每次只处理第一个face_locs[0]后续人脸被忽略。→ 解决打开camera_use.py找到face_locs face_recognition.face_locations(...)后的处理块将单次处理改为循环处理# 原代码只处理第一个 top, right, bottom, left face_locs[0] # 改为处理所有人脸 for i, (top, right, bottom, left) in enumerate(face_locs): face_img frame[top:bottom, left:right] face_encoding face_recognition.face_encodings(face_img) if len(face_encoding) 0: name, conf file_processing.recognize_face(face_encoding[0], ...) # 绘制框和文字用不同颜色区分 color (0, 255, 0) if name ! Unknown else (0, 0, 255) cv2.rectangle(frame, (left, top), (right, bottom), color, 2) cv2.putText(frame, f{name}({conf:.2f}), (left, top-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)5. 二次开发实战如何接入企业微信打卡、支持离线批量签到、以及把阈值调优做成 Web 配置页现在你已经能跑通、能注册、能签到、能导出 Excel。但真实业务不会停在这里——课程考勤要推送到教务系统会议签到要同步到 OA实训室准入要对接门禁控制器。下面三个改造方向我都已在某公司内部项目中落地代码可直接抄。5.1 接入企业微信打卡用requests.post替换util.write_to_excel企业微信提供「提交打卡」API需管理员在后台开启只需把签到成功的name和timestampPOST 过去。改造camera_use.py中的签到成功分支# 找到原代码中类似 if name ! Unknown: 的位置 if name ! Unknown: # 原逻辑写 Excel # util.write_to_excel(name, timestamp) # 新逻辑推送到企微 wecom_url https://qyapi.weixin.qq.com/cgi-bin/checkin/push_checkin_data payload { checkindata: [{ userid: name, # 企微 userid 必须与 name.txt 一致 checkin_time: int(timestamp.timestamp() * 1000), # 毫秒时间戳 location_title: 实验室A栋301, wifiname: Lab-WiFi }] } headers {Content-Type: application/json} try: resp requests.post(wecom_url, jsonpayload, headersheaders, timeout5) if resp.json().get(errcode) 0: print(f✅ Pushed to WeCom: {name}) else: print(f❌ WeCom push failed: {resp.text}) except Exception as e: print(f⚠️ WeCom network error: {e})注意name.txt中的姓名必须是企业微信后台的userid通常是英文数字如zhangsan001不是中文名。若需映射建一个wecom_id_map.json文件读取后替换payload[checkindata][0][userid]。5.2 支持离线批量签到test/目录不只是测试而是生产级批量处理入口test/目录下test_images/的 5 张图本意是验证file_processing.py是否正常工作。但我们把它升级为“离线签到模式”当网络中断、或需对历史监控录像做补签时直接读取文件夹内所有图片批量识别并写入 Excel。新建batch_signin.py放在ggg/目录下# batch_signin.py import os import cv2 import face_recognition import util import file_processing import common def batch_process_images(image_dir: str, output_xlsx: str): results [] for img_file in os.listdir(image_dir): if not img_file.lower().endswith((.jpg, .jpeg, .png)): continue img_path os.path.join(image_dir, img_file) img cv2.imread(img_path) if img is None: continue face_locs face_recognition.face_locations(img, modelhog) for loc in face_locs: top, right, bottom, left loc face_img img[top:bottom, left:right] encodings face_recognition.face_encodings(face_img) if len(encodings) 0: continue name, conf file_processing.recognize_face( encodings[0], common.load_embeddings(), common.load_names() ) timestamp util.get_timestamp() results.append((timestamp, name, SUCCESS if name ! Unknown else FAILED)) # 写入 Excel复用 util util.write_batch_to_excel(results, output_xlsx) print(f✅ Batch done. {len(results)} faces processed. Saved to {output_xlsx}) if __name__ __main__: batch_process_images(test/test_images, batch_result.xlsx)运行python batch_signin.py秒级处理 100 张图结果精准落表——这比手动翻监控视频快 100 倍。5.3 把阈值调优做成 Web 配置页Flask Jinja230 行代码搞定common.py里的RECOGNITION_THRESHOLD和CONFIDENCE_THRESHOLD是硬编码每次修改都要重启服务。我们用 Flask 搭个极简配置页前端改数值后端实时写入common.py并重载模块# config_server.py同目录下 from flask import Flask, render_template, request, jsonify import re app Flask(__name__) app.route(/) def index(): with open(common.py, r, encodingutf-8) as f: content f.read() # 提取当前阈值 rec_th float(re.search(rRECOGNITION_THRESHOLD\s*\s*(\d\.\d), content).group(1)) conf_th float(re.search(rCONFIDENCE_THRESHOLD\s*\s*(\d\.\d), content).group(1)) return render_template(config.html, rec_threc_th, conf_thconf_th) app.route(/update, methods[POST]) def update_thresholds(): data request.get_json() rec_th float(data[rec_th]) conf_th float(data[conf_th]) with open(common.py, r, encodingutf-8) as f: content f.read() # 替换阈值 content re.sub(rRECOGNITION_THRESHOLD\s*\s*\d\.\d, fRECOGNITION_THRESHOLD {rec_th}, content) content re.sub(rCONFIDENCE_THRESHOLD\s*\s*\d\.\d, fCONFIDENCE_THRESHOLD {conf_th}, content) with open(common.py, w, encodingutf-8) as f: f.write(content) # 重载 common 模块需在 camera_use.py 中用 importlib.reload(common) return jsonify({status: success}) if __name__ __main__: app.run(host0.0.0.0, port5000)配套templates/config.html需建templates/目录!-- templates/config.html -- h2Threshold Configuration/h2 pRecognition Threshold (default 0.6): input idrec typenumber step0.01 value{{ rec_th }}/p pConfidence Threshold (default 0.75): input idconf typenumber step0.01 value{{ conf_th }}/p button onclickupdate()Update/button script function update() { fetch(/update, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ rec_th: document.getElementById(rec).value, conf_th: document.getElementById(conf).value }) }).then(r r.json()).then(console.log); } /script运行python config_server.py浏览器打开http://localhost:5000滑动输入框改阈值点 Updatecamera_use.py下次识别就生效——从此调参不用重启也不用记vim common.py。从那以后我每次接到新部署需求第一件事就是把config_server.py和templates/一起打包进去。它不解决核心算法但省下了 80% 的现场调试时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑