
简介人脸识别打卡.zip提供了一套基于Python的考勤系统完整实现面向计算机视觉初学者、Web后端开发者以及需要快速搭建人脸打卡场景的工程人员。系统整合前端交互界面与后端处理逻辑覆盖OpenCV图像采集、FaceNet/MTCNN模型特征提取、Flask构建RESTful API、SQLAlchemy操作关系数据库等关键环节清晰展示了从人脸检测、身份匹配到打卡记录持久化的完整链路。压缩包共118个文件大小约99.59MB以52个Python源码、14个pyc编译文件、13个txt配置说明为主另含16张jpg与7张png图片样本、3个npy特征文件及pb模型文件便于直接在本地运行、调试或二次开发目录结构清晰前端页面与后端代码分层明确。资源当前已有585人学习目录内还保留有训练模型与特征数据适合通过实战项目系统掌握人脸识别技术与Python Web应用开发。1. 先弄清楚这个“人脸识别打卡”包到底能干什么我见过太多小团队在考勤这件事上卡住几十号人的公司上下班打卡靠一台旧手机钉钉月底导出 Excel 对半天还总有“我明明打了卡怎么没记录”的扯皮。而人脸识别门禁机方案呢要么是整套硬件采购价格不低要么是网上“基于 STM32 的人脸识别门禁系统设计”那类课设跑在开发板上识别率感人。这个叫“人脸识别打卡.zip”的包解压之后其实就是一条比较务实的中间路线用普通 USB 摄像头 一台 Windows 或 Linux 电脑跑通人脸检测、特征提取、比对、打卡记录落库这一整条链路识别和考勤都在本地完成不依赖云端。真正把包打开之后你会发现它不是一个几百行的玩具脚本而是包含模型文件、配置、数据库表结构、三个核心模块的完整方案。这个方案能解决“谁在什么时间打了卡”这件事同时也能作为后续做门禁机联调的前置实验台——很多做基于 RK 或海思方案的人脸识别门禁系统设计的同学第一版原型也是这么搭出来的。适合谁适合有 20500 人考勤需求的小企业 IT适合想在本地把一套人脸识别流程吃透的嵌入式或算法工程师也适合正在做人脸识别门禁系统毕业设计、不想只交一个 PPT 的学生。不适合谁不适合要活体检测、防代打卡的严格安保场景那个后面我会讲清楚为什么。先给一个反直觉的结论这套方案里最容易翻车的不是模型选型而是比对阈值。很多人拿到手就把阈值往低了调觉得“识别不准是因为阈值太高”结果代打卡一打一个准。后面我会专门用一节讲这个黑匣子该怎么调。2. 先跑通最小闭环目录结构与依赖安装这套东西不是双击 exe 就能用的第一步是把工程结构看明白。很多新手一上来就python main.py报错之后连是缺模型还是缺库都分不清浪费一晚上。我一般建议按下面这个顺序来。2.1 解压后先读这几个文件别急着跑主程序一个合格的人脸识别打卡工程文件组织通常是“配置、模型、业务逻辑”三分离。解压后你大概率会看到这样的结构路径作用需要关注的点config.yaml摄像头、模型路径、阈值、数据库等全部参数后面调参基本都改这里models/检测模型、特征提取模型的权重文件看是 ONNX 还是 Caffe 格式决定依赖detect.py人脸检测模块负责在画面里框出人脸face_embedding.py特征提取与比对模块把脸变成一串 512 维浮点数attendance.py打卡逻辑与数据库写入判重、补卡、生成报表都在这data/attendance.dbSQLite 数据库文件第一次跑完自动生成samples/注册人脸照片样本每人的正脸照片放这里先打开config.yaml确认这三项大概率存在摄像头编号、模型文件相对路径、比对阈值。如果你拿到的是个残缺压缩包缺模型文件是常有的事别慌后面我会给替代下载思路。提示不管你是不是准备改代码第一次跑通之前不要动任何 Python 文件。只改配置能解决 80% 的问题。2.2 创建虚拟环境与安装依赖这套方案最常用的依赖组合是 OpenCV 做图像处理、ONNX Runtime 做模型推理、PyYAML 读配置SQLite 直接用 Python 标准库。我习惯用 conda 建独立环境避免和别的项目打架。conda create -n face_attendance python3.9 -y conda activate face_attendance pip install opencv-python opencv-contrib-python onnxruntime pyyaml numpy逻辑说明opencv-python是基础图像处理库负责读摄像头帧、图像缩放、画框opencv-contrib-python额外带了一些 DNN 模块依赖的工具函数缺少它某些版本的cv2.dnn接口会报No module named cv2.dnnonnxruntime是推理引擎比直接装 TensorFlow/PyTorch 轻得多CPU 也能跑pyyaml用来解析config.yamlnumpy是所有特征向量运算的底层。参数说明Python 版本不要高于 3.10OpenCV 有些较新版本在 3.11 下编译的 wheel 可能缺失python3.9 是踩过坑之后发现的比较稳的组合。装完依赖先跑一行命令验证环境和模型都正常python -c import cv2, onnxruntime; print(cv2.__version__, onnxruntime.__version__)如果打印出版本号就说明依赖到位了。如果报错大概率是两个原因conda 环境没激活或者 pip 装到了 base 环境——用conda activate face_attendance激活后再试。2.3 用一张公开照片验证模型没有装错主程序依赖摄像头但在服务器或没有摄像头的开发机上可以先用人脸公开数据集里的照片验证模型路径是否正确。WIDER Face 数据集随便找一张含人脸的图放到samples/test.jpg然后执行python -c from detect import DetectFace; d DetectFace(config.yaml); print(d.run(samples/test.jpg))逻辑说明这段命令是在 import 检测模块读取config.yaml里的模型路径和阈值参数对samples/test.jpg做一次前向推理返回的print结果里应该包含人脸框坐标和置信度。如果此处报FileNotFoundError说明detect.py里默认的模型相对路径和实际解压路径不一致直接修改config.yaml中的模型路径字段而不是去改 Python 源码。参数说明DetectFace.run内部一般会做一步预处理——把图像缩放到模型输入尺寸检测模型的内置输入分辨率通常是 320x320 或 640x640缩放策略是保持宽高比后补边这一步不需要你手动写但你要知道如果图片里的人脸太小比如几十人的合照检测框置信度会低于阈值被过滤这是正常的。跑通这一行就说明“图像读入 → 模型推理 → 输出坐标”这条链路没有断。下一章开始拆人脸识别最核心的部分怎么把检测到的人脸变成一串可以和库里比对的特征数字。3. 把人脸变成一串数字检测、对齐与特征提取人脸识别打卡系统的核心能力概括起来是两件事先找到脸再认出是谁。“找到脸”靠检测模型“认出是谁”靠特征提取模型。这两步任何一个出错后面打卡记录全是垃圾。这一章我们把两件事拆开讲透。3.1 为什么选 YuNet 而不是 Haar 或 OpenFace早期基于 OpenCV 的老教程喜欢用 Haar Cascade 做人脸检测但这套方案里基本不会出现原因很简单Haar 对侧脸、遮挡、暗光极度敏感实测在办公室逆光工位上的误检率能把打卡记录搞成一团浆糊。这个包的解压目录里models/下大概率放的是一个 ONNX 格式的 YuNet 检测模型OpenCV Zoo 项目里最常用的那个也可能是 SCRFD。两者都是现代深度学习检测器对角度和人脸大小的容忍度比 Haar 高一整个量级。我个人更推荐 YuNet 作为入门选择因为它输入尺寸小默认 320x320CPU 单帧推理大概 1020ms而且 OpenCV 从 4.5.4 开始直接内置了cv2.FaceDetectorYN接口不需要额外装依赖。SCRFD 精度更高一点但模型更大推理更慢适合后续做门禁机高精度版本时再考虑。对比 Haar 的“滑动窗口 手工特征”YuNet 是“锚点 回归 非极大值抑制”的标准目标检测范式本质上一个轻量 Backbone 加两个输出头一个回归人脸框和五个关键点双眼、鼻尖、嘴角两端一个分类置信度。五个关键点直接服务于下一步人脸对齐。人脸对齐这件事容易被忽略但它直接影响特征提取的准确率。同一个人的脸如果一张是正的、一张歪了 20 度不做对齐直接送进特征提取网络提取出的特征向量能差出 0.2 的余弦距离这可能就导致误判为两个人。YuNet 输出的五个关键点足够我们做相似变换把关键点映射到标准位置裁剪出 112x112 的正脸图再送进特征提取模型。这套“检测 → 对齐 → 裁剪 → 提特征”的流程在所有开源人脸识别项目里几乎是标准工序。3.2 特征提取与比对最重要的三个参数特征提取模型决定了一次打卡最终能不能认出人。这个包里用的多半是一个在 MS1M 或 Glint360K 上训练过的 ResNet 或 MobileFaceNet 结构输出是 512 维的归一化特征向量。这种模型体积小、CPU 推理快最重要的一点它对“域泛化”更友好——训练时候用的是清晰人脸数据集测试时候是办公室监控头的低照度画面MobileFaceNet 这类轻量模型在这种场景下的掉点反而比参数量巨大的 ResNet 更小。配置里你需要关心的参数主要集中在config.yaml我列一个常用的推荐值和它背后的逻辑参数推荐值说明与踩坑点detect_threshold0.60.7检测框置信度。低于 0.5 会出现大量误检把人形、海报当人脸recognition_threshold0.450.6最关键的参数。余弦相似度阈值低于 0.4 基本等同“谁都能打卡”embedding_batch_size14一次推理几张脸。打卡场景通常 1 就够批量注册时可以提高max_face_size160x160送入检测模型前图像不缩放的最小人脸尺寸input_size320x320YuNet 固定输入。改大会更准但显著变慢recognition_threshold是这套系统里唯一需要你反复实拍的参数。它的含义是两张脸的 512 维特征计算余弦相似度如果结果大于这个值判定为同一个人。0.5 是一个相对安全的起点同一个人的不同照片通常能到 0.60.75不同人大多在 0.20.4 之间通常不会越过 0.5。但如果你们办公室灯光极差、摄像头角度歪同一个人不同时间段的特征相似度可能掉到 0.5 以下这时候把阈值降到 0.42 左右可以救回来但代价是误识率上升。我见过把阈值调到 0.3 的团队两个长得像的同事互相能打卡成功这是最典型的翻车场景。3.3 特征提取与比对的最小代码这一节给一个可以直接抄进项目、替换掉原有特征提取核心逻辑的简化实现。假设你已经用 YuNet 检测到人脸框face_rect和关键点landmarks下面的代码完成对齐、提特征、比对三件事import cv2 import numpy as np import onnxruntime as ort class FaceEmbedder: def __init__(self, model_path, input_size(112, 112)): self.session ort.InferenceSession(model_path) self.input_size input_size # 标准参考关键点左眼、右眼、鼻尖、左嘴角、右嘴角 self.ref_landmarks np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041] ], dtypenp.float32) def align_face(self, img, landmarks): # 用相似变换把检测到的关键点对齐到标准位置 M, _ cv2.estimateAffinePartial2D(landmarks, self.ref_landmarks) aligned cv2.warpAffine(img, M, self.input_size, borderValue0.0) return aligned def embed(self, aligned_face): # 归一化并转为 NCHW 格式送入 ONNX 模型 blob cv2.dnn.blobFromImage(aligned_face, 1.0 / 255.0, self.input_size, (0.5, 0.5, 0.5), swapRBTrue) pred self.session.run(None, {self.session.get_inputs()[0].name: blob})[0] embedding pred.flatten() norm np.linalg.norm(embedding) return embedding / norm # 归一化到单位向量 def match(self, emb1, emb2): return np.dot(emb1, emb2)逻辑说明align_face函数的核心是cv2.estimateAffinePartial2D它根据检测到的五个关键点和标准参考点计算一个仿射变换矩阵再用warpAffine把人脸裁剪并矫正到 112x112。embed函数里用了cv2.dnn.blobFromImage这一步做的事情是缩放、减均值、通道转换RGB 转 BGR 再转 NCHW这些细节如果自己手写很容易写错直接用 OpenCV 封装好的接口最稳。match返回的是两个单位向量的点积也就是余弦相似度范围在 [-1, 1]实际场景里同一个人通常在 0.5 以上。参数说明input_size(112, 112)必须和训练特征提取模型时候对齐的尺寸一致MobileFaceNet 系列基本都是 112x112blobFromImage的mean(0.5, 0.5, 0.5)是固定的归一化参数不能省否则特征分布会偏移比对结果整体偏小最后一步归一化不能丢特征提取模型输出后做 L2 归一化是训练时的标准操作不归一化直接算点积结果会受向量长度干扰。4. 打卡逻辑从特征比对到判重与补卡检测和特征提取解决的是“认人”问题打卡逻辑解决的是“记录”问题。很多初学这套系统的人把注意力全放在模型上实际上线之后被吐槽最多的反而是业务逻辑同一个人一天打四次卡、迟到早退算错、补卡没有留痕。这一章把打卡流程完整走一遍。4.1 一天四段考勤怎么判重不打架标准考勤通常分四个时间点上午上班、上午下班、下午上班、下午下班。如果你不做判重一个员工站在摄像头前多刷几次脸数据库里就多出好几条一模一样的记录。判重逻辑我在生产环境里见过最简单的做法也是这套包大概率内置的做法按user_id record_date record_type四字段唯一索引。record_type morning_in # 上午上班 # 打卡时间按上下午自动判断例如 12:00 前算 morning_in判重时要特别注意跨天问题。一个员工加班到第二天凌晨 2 点他离开时的打卡记录应该归属哪一天常见做法是下班打卡时间在凌晨 0:006:00 之间的归属到前一天的下班记录。这个逻辑如果写错月底报表对账的时候会多出一堆“幽灵加班记录”。4.2 打卡核心代码比对、写入、事务保护下面这段代码是打卡主流程的精简版包含读取特征库、实时比对、写库三步。它可以直接替换attendance.py里的核心函数import sqlite3 import yaml from datetime import datetime, timedelta def do_check_in(embedding, config): # 加载特征库 conn sqlite3.connect(config[db_path]) rows conn.execute(SELECT user_id, embedding FROM face_features).fetchall() best_id, best_score None, -1.0 for user_id, emb_blob in rows: emb_db np.frombuffer(emb_blob, dtypenp.float32) score np.dot(embedding, emb_db) if score best_score: best_id, best_score user_id, score if best_score config[recognition_threshold]: conn.close() return None, best_score # 判重同一人在同一打卡类型下只记一条 now datetime.now() today now.strftime(%Y-%m-%d) if now.hour 12: rec_type morning_in else: rec_type morning_out if now.hour 13 else afternoon_in try: conn.execute( INSERT INTO attendance (user_id, record_date, record_type, record_time) VALUES (?, ?, ?, ?), (best_id, today, rec_type, now.strftime(%H:%M:%S)) ) conn.commit() except sqlite3.IntegrityError: # 唯一索引冲突说明已打过卡直接丢弃 pass finally: conn.close() return best_id, best_score逻辑说明这个函数的执行顺序很重要。先遍历特征库里所有人脸特征用余弦相似度找到最高分的人如果最高分低于阈值直接返回None表示“没认出是谁”不写任何记录。如果认出来了再根据当前时间判断打卡类型插入数据库。sqlite3.IntegrityError的捕捉是关键——如果表结构建了唯一索引重复打卡会在插入时报冲突此时直接忽略就是最简单可靠的判重方案。参数说明recognition_threshold和第三章是同一个值不要在这里单独设置保持全局走config.yaml。record_type的时间切分边界可以按你公司实际作息调整比如 13:00 是下午上班开始那么 12:0013:00 之间打卡算上午下班13:00 之后算下午上班。embedding存储用np.frombuffer从小数二进制还原注意dtype.float32和入库时完全一致否则特征值全错。入库前要np.ascontiguousarray(embedding).tobytes()这是新手最容易漏的一步。4.3 正确性验证拿真实样本算一遍接受率写完了不能直接装摄像头。我一般会用一批样本来验证系统整体可用性。做法是先准备一个包含 10 个人的注册库每人 3 张正脸照片再准备测试集每人的 5 张不同表情、角度照片作为正样本再加 20 张无关人脸作为负样本。跑一遍后统计两个指标python verify.py --positive_dir samples/test/known/ --negative_dir samples/test/unknown/逻辑说明这个脚本的作用是批量跑完所有比对输出两个数字正样本接受率TPR应该越高越好和负样本接受率FPR应该越低越好。如果 TPR 低于 95%说明阈值设得过高或注册照片质量太差如果 FPR 高于 5%说明阈值设得太低或注册库里有两个人长得过于接近——后者需要换更高质量的特征提取模型。参数说明测试的时候尽量用和实际部署环境同一台摄像头拍的照片不要用网上下载的明星脸照片。现实中的摄像头畸变、白平衡差异会让实验室指标大打折扣。提示验证结果不要只看最终分数把识别失败的样本单独输出到debug/failed/目录肉眼判断是“角度太偏”还是“光线过暗”。这能省下后面大量盲调时间。5. 避坑清单黑屏、误判、重复打卡和高延迟到这一章就是真正的血泪经验了。这套系统在我手里跑过三个现场也帮别人排查过不少问题。按出现频率排序下面这 6 个坑是九成团队都会遇到的每条按现象、原因、解决写清楚。5.1 摄像头打不开程序直接黑屏闪退现象启动时 OpenCV 报cant open camera 0或者窗口是黑的但程序没退出。原因最常见的是笔记本内置摄像头编号不是 0是 1因为驱动把虚拟摄像头排在了前面其次是程序脚本所在目录没有摄像头权限Windows 上独立控制台没有相机访问权限也会静默失败。解决先跑一行命令枚举所有可用摄像头python -c import cv2; [print(i, cv2.VideoCapture(i).isOpened()) for i in range(4)]看哪个编号返回 True把config.yaml里的camera_index改成对应的值。如果全是 False检查系统相机权限设置给 Python 进程授予摄像头访问权限。5.2 阈值设太低两个同事互刷成功现象A 的脸能打出 B 的卡晚上翻记录发现同一个人一天出现 8 条打卡。原因recognition_threshold被调到了 0.3 以下。不同人的余弦相似度通常在 0.20.4 之间0.3 的阈值等于把相似的人全部放进来。解决先把阈值恢复到 0.5 跑一天观察日志里每条打卡的相似度分数。如果某个人的分数稳定在 0.45 附近单独为这个人降低阈值而不是全局降低——实现方式是特征库里给每个用户单独存一个threshold_override字段比对时优先用这个值。5.3 逆光和侧脸导致的误判漏判现象坐在窗边背光工位的员工上午能打卡下午逆光就识别失败。原因检测模块能框住脸但光线对比度过强导致关键点定位偏移对齐后的人脸图半边是黑的特征提取结果严重偏离正常分布。解决在送入检测模型前先做一次直方图均衡化gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) equalized cv2.equalizeHist(gray) frame cv2.cvtColor(equalized, cv2.COLOR_GRAY2BGR)注意equalizeHist只对灰度图生效不要直接对彩色图做否则会出现偏色。实测这一行代码能挽回约 15% 的逆光漏识别。同时把检测框的置信度阈值下调到 0.55给低质量画面留一点余量。5.4 打卡时间错乱凌晨记录算到前一天头上现象第二天看报表凌晨 1 点走的人下班记录出现在当天而不是前一天。原因判重逻辑里下班类型的时间判断只写了if hour 12没有区分凌晨段。解决在判断打卡类型前增加一段凌晨归属if now.hour 6: # 凌晨 0:00-6:00 的记录归属到前一天 today (now - timedelta(days1)).strftime(%Y-%m-%d)这种做法只对“下班补打”有意义如果你们公司根本没有夜班可以不处理这段逻辑。但只要有一个人是凌晨下班的这个 bug 迟早会让你重新翻数据库。5.5 识别慢到卡屏CPU 占用 100%现象1080P 摄像头画面流畅但只要有人脸出现在画面里帧率掉到 5 帧以下界面卡顿。原因特征提取模型输入是 112x112但每帧都执行完整流程。检测和特征提取都是有成本的一帧 10ms 一帧 20ms看起来不高但叠加摄像头缓冲、画框、写日志CPU 就被吃满了。解决两条路。第一设置检测间隔只每 3 帧做一次完整推理中间帧直接复用上一次结果if frame_count % 3 0: result pipeline.run(frame)第二摄像头采集分辨率降到 640x480。打卡场景根本不需要 1080P人脸检测模型会把输入缩到 320x320高分辨率除了增加处理延迟没有半点收益。5.6 注册照片和实拍照片差距大特征识别率骤降现象员工入职注册时用手机自拍的正脸到现场使用摄像头拍摄角度偏上识别率断崖式下跌。原因注册照片和现场照片的“人脸域”不一致。特征提取模型虽然做了一定程度的域泛化但俯拍 vs 平视、室内暖光 vs 冷白灯仍然会显著影响特征。解决注册入库时用现场摄像头连拍 3 张取互相相似度最高的两张作为该用户的双模板。比对时有一个模板相似度过阈值就通过。这个做法能把现场的“玄学失败”变成可解释的日志。注意第 5.6 条是最容易被忽略但影响最大的。代码写得好坏是次要的数据分布决定上限。6. 跑通之后把方案搬进人脸识别门禁机如果你把这套 PC 端方案跑通之后想往人脸识别门禁机上迁那要做的不是暴力移植代码而是重新核算性能预算。门禁机最常见的硬件是海思 Hi3516 系列和瑞芯微 RV1109/RK3568这类芯片的算力在 0.51.0 TOPS 之间运行的是 Linux跑不了完整 OpenCV DNN 那一套推理要用厂商的 SDK。PC 端跑一个 MobileFaceNet 不在乎那 3MB 模型体积门禁机上闪存每 MB 都金贵。常见做法是把 ONNX 模型用厂商工具链转成 INT8 量化模型RKNN Toolkit 或者海思 ive 工具链都能转精度损失大约 1%3%换来推理时间从 40ms 降到 12ms。性能预算要重新分账YuNet 检测 MobileFaceNet 特征提取INT8 化之后单帧总延迟控制在 50ms 以内才能达到门禁机的“即刷即过”体验。检测间隔在门禁机上可以缩短到 1 帧 1 次检测因为门禁机场景人脸的通过速度比较快不做每帧检测会出现“头都过去了框还没出来”的尴尬。另外门禁机上一定要加活体检测——PC 端可以用照片直接刷过去门禁机如果也这样员工拿张工牌照片就能进公司。轻量活体检测一般用 3D 结构光或双目摄像头方案纯 2D 的话至少加一个“眼睛开闭检测 随机点头动作”的挑战式校验。最后讲一个我自己的教训。第一版方案上线的时候我把recognition_threshold从 0.5 调到了 0.35理由是“让识别更灵敏”结果一周内出现 3 次代打卡事件两个长得像的同事互相刷成功HR 部门差点把考勤系统退回手工模式。后来我把阈值调回 0.52同时在日志里记录每次比对的分数再遇到纠纷就能拿数据说话。这是这套系统最值得养成的习惯别只看“打卡成功”四个字要把分数面板留好它是你排查一切问题的第一入口。希望帮到你。本文还有配套的精品资源点击获取