写字楼早晚高峰期前台打卡排队的情况我想大家都不陌生。刷卡怕代刷指纹怕干手指识别不了纸质签到更是一笔糊涂账。我当时接到这个需求时客户的核心要求就一句话让员工刷脸进门的同时把考勤记录自动留档。于是就有了这个Python基于人脸识别的写字楼打卡软件代码和文档说明都整理齐全可以直接部署试用也可以作为练手项目去研究人脸识别在真实场景里怎么落地。这个项目不是一个简单的“扫脸提示ok”的玩具。它包含了摄像头采集、人脸检测、128维特征提取、员工库比对、打卡记录入SQLite数据库、PyQt界面展示和考勤数据导出这几大块。适合三类人看一是写字楼或中小企业的行政人员想找一个低成本的考勤替代方案二是Python开发者看完能懂人脸识别打卡系统的完整数据流三是正在做课程设计或毕业设计的同学这个项目比普通的单文件demo完整得多源码结构和文档说明都符合“做完能答辩”的规格。1. 项目概述为什么做人脸打卡1.1 写字楼场景的痛点写字楼和工厂车间、学校教室的打卡场景很不一样。写字楼的人流量集中在早晚高峰人员身份以固定员工为主但来访者也多。传统打卡方式在这里的问题很典型刷卡和NFC手环会被代刷一张卡可以复制出好几张行政要定期查代打卡记录指纹考勤遇到冬天干燥、手指蜕皮、或者保洁阿姨手上带水识别率会明显下降更重要的是以上几种方式都属于“凭证验证”不是“身份验证”。你刷的卡是别人的系统就认为你是别人。人脸识别的本质是直接对人本身做特征提取不需要携带任何实体凭证也就绕开了“卡被借走”这个长期痛点。加上写字楼的摄像头环境相对固定光照可控非常适合做人脸识别考勤的试点。我当时判断这个项目能成立核心逻辑就在这里。1.2 项目目标和适用人群这个项目的目标是员工站在摄像头前系统自动识别出身份记录上下班时间管理员可以事后查看和导出考勤报表。整个识别过程应该在1秒内有反馈不需要员工刻意配合摆姿势。从技术实现角度看它拆成了四个子目标人脸注册批量导入员工照片提取特征向量实时识别摄像头画面中检测出人脸与员工特征库比对考勤记录识别成功后自动写入当日打卡记录同一个人5分钟内不重复打卡数据查询管理员界面可以按部门、按日期筛选导出Excel事实上这个系统的很多设计细节是可以沿用到其他场景的。比如把打卡记录表换成访客登记表就变成了访客刷脸登记系统把比对结果接到门禁控制器的串口上就成了一道人脸闸机。这也是我推荐想学计算机视觉的同学从这个项目入手的核心原因——它麻雀虽小五脏俱全。2. 技术选型从库到方案的取舍2.1 为什么选face_recognition而不是其他方案做实时人脸识别这块市面上的Python方案其实不少我当时在项目里简单列了个对比表直接决定了选型方向方案部署难度准确率训练需求适合场景OpenCV Haar Cascade LBPH低低到中等需要为每个员工收集多张训练样本教学演示、小型实验光线和角度敏感face_recognition封装库低中高零训练一张照片即可提取128维特征中小规模考勤百人内、快速落地自训练CNNFacenet/Insightface路线高高需要大量数据和GPU训练千人以上规模、对误识率有严苛要求的场景选face_recognition的原因很实际它封装了dlib的深度学习人脸检测模型和预训练的面部特征提取模型用的时候不需要自己准备数据集去训练一个深度网络。我只需要对每张员工照片调用一次模型拿到一个128维的特征向量后续实时识别时跟这个向量做距离比对就行。对于写字楼一百人左右的规模这种“预训练模型特征向量检索”的思路在效率上完全够用而且开发周期可以压缩到一周以内。如果换成自训练CNN先不说需要准备多少张员工照片光是把训练环境搭好、调一轮loss时间成本就不是这个项目能承受的。当然face_recognition并不是没有缺点后面我会专门讲它慢在哪里、怎么规避——这种“预设方案解决80%需求剩余20%用工程技巧补”的思路我觉得比单纯追求高精度模型更贴近实际项目。2.2 硬件和开发环境准备硬件方面没有太高的门槛。我测试时用的是一个普通的1080p USB摄像头笔记本自带摄像头也能用。建议摄像头的摆放高度略高于人脸微微向下俯视5到10度这样拍出来的人脸角度比较正识别率会比仰视视角高很多。开发环境我推荐Python 3.7到3.10之间不建议直接用最新版本。主要原因在于dlib的预编译wheel对新版Python支持往往滞后Windows上如果装的是Python 3.11或3.12经常遇到dlib需要本地编译的情况一台没有Visual Studio Build Tools的机器当场卡死。我踩过这个坑所以把环境要求写进了项目文档的第一页。依赖如下face_recognition核心的人脸检测和特征提取库opencv-python摄像头采集、图像缩放和显示numpy特征向量处理和距离计算PyQt5桌面界面视频流显示和打卡记录表格pandas考勤记录的筛选、统计和Excel导出安装命令就是常规的pip install但Windows用户特别注意先装dlib再装face_recognition装dlib时如果pip直接拉源码编译失败去下载对应Python版本的dlib预编译whl文件来装。这是整个项目里最折腾的一环很多新人卡在这里就放弃了所以我在文档说明里特别标注了步骤。2.3 活体检测方案的选择任何上线的刷脸考勤系统都绕不开一个灵魂拷问拿一张打印照片放在摄像头前能不能骗过系统我的回答是纯静态的人脸比对一定骗得过。项目里我做了两层防伪。第一层是运动检测很简单比对摄像头连续几帧画面中人脸区域的灰度变化。真实的人脸总有微小的表情变化和头部晃动照片不会。当变化幅度低于某个阈值时判定为静态照片不进入打卡流程。第二层是动作配合屏幕提示用户做出“眨眨眼”或者“左右转头”的动作系统检测到动作完成才记录打卡。办公室场景经常有人排队动作配合的体验稍微差一点但安全系数高很多。商用设备上用的红外结构光方案当然更可靠但普通USB摄像头做不了这个我也在文档里如实写了。我个人的建议是中小型写字楼考勤用“运动检测动作随机抽检”的组合就可以了。每次打卡不强制做动作但系统会随机要求十分之一的打卡操作配合一个动作这样兼顾了通行效率和防照片攻击的基本需求。3. 系统架构与数据流设计3.1 整体模块划分整个项目按照功能拆成了六个模块每个模块的职责非常清晰这也是文档说明里我花了最多篇幅的部分主控模块负责启动摄像头、初始化识别模型、驱动主循环图像采集模块从摄像头读取帧按设定分辨率缩放人脸检测与特征提取模块定位人脸位置计算128维特征向量特征比对模块在员工库中查找距离最近的特征向量联合阈值判断身份打卡记录模块判断是否重复打卡写入数据库界面与报表模块展示视频画面、签到记录表格、导出考勤Excel核心数据流是这样的摄像头每读取一帧先缩放到四分之一大小在缩放后的帧上做人脸检测小图检测速度快得到人脸位置后映射回原图坐标再从原图对应区域提取特征向量拿这个向量跟员工库里的所有向量比距离找到最小距离且小于阈值的人脸就认为是该员工。然后查询这个人当天是否已经打过卡如果没有就插入一条考勤记录同时刷新界面上的打卡状态栏。这里的人脸检测和特征提取是两个步骤很多新手会忽略它们之间的性能差异。人脸检测用的是HOG特征或者CNN检测器主要做“人脸在哪”的粗定位特征提取模型做的是“这个人是谁”的精细计算。两者分开执行的好处是检测可以在小分辨率图上快速完成只对找到的人脸区域做一次高成本的特征提取。如果把整个大图直接丢进特征提取模型速度会慢到没法用。3.2 数据库设计数据库我用的是SQLite零配置一个文件搞定适合中小规模项目如果后续要部署到多人并发环境可以换成MySQLDAO层的代码改动很小。员工表和打卡记录表的结构如下CREATE TABLE employee ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, department TEXT, photo_path TEXT, face_encoding TEXT NOT NULL, created_at TEXT DEFAULT (datetime(now,localtime)) ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id INTEGER NOT NULL, check_time TEXT DEFAULT (datetime(now,localtime)), check_type TEXT, source_photo TEXT, score REAL, FOREIGN KEY (employee_id) REFERENCES employee(id) );face_encoding字段存的是128个浮点数我用逗号拼接成字符串写入。读取的时候再用numpy拆回数组。这张表就是识别系统的底库注册员工的过程就是往这张表里插数据的过程。attendance表里我特意加了source_photo和score两个字段。source_photo保存员工打卡时摄像头截取的人脸小图路径score保存特征距离值。这两列在平时没什么用但一旦有人质疑“我那天没迟到系统认错人了”管理员可以直接翻出当时的照片和分数来核对。这种审计性的设计在考勤系统里非常加分很多商业产品也保留了类似能力。3.3 识别阈值怎么定face_recognition默认用欧氏距离衡量两个128维特征向量的相似度距离越小表示越可能是同一个人。库的默认阈值是0.6但实际项目里绝对不能直接拿来就用。阈值设置的核心矛盾是误识率和拒识率的权衡。阈值设得太低比如0.4意味着要求比对距离非常近识别会非常保守员工换个发型或者光线暗一点就刷不上脸了阈值设得太高比如0.6又会出现两个长相接近的同事互相误识别的情况。我在项目里做了一次简单的阈值标定收集了50个员工的各5张照片两两组合计算同人距离和不同人距离把两组距离的分布画出来取中间值偏保守的位置。结果在这个数据集中同人距离大多在0.35到0.5之间不同人距离基本在0.6以上所以阈值取0.5左右比较合适。实际运行时我还加了“连续两帧识别出同一个员工才确认打卡”的策略进一步降低随机误报。这个思路不复杂但很管用建议部署到新环境时都做一遍。4. 实操过程一步步实现打卡系统4.1 环境搭建本地开发环境我建议先建一个虚拟环境不要直接装在系统Python里避免不同项目的依赖互相干扰。python -m venv venv venv\Scripts\activate # Windows # source venv/bin/activate # macOS/Linux pip install numpy opencv-python pip install dlib pip install face-recognition pip install PyQt5 pandasWindows用户如果卡在dlib这一步下载对应版本的预编译whl文件然后pip install那个文件即可。macOS用户需要注意face_recognition依赖的dlib在Apple Silicon上编译比较慢同样建议优先找预编译版本。装完之后跑一句简单的测试代码能输出摄像头画面就算成功import cv2 cap cv2.VideoCapture(0) ret, frame cap.read() print(摄像头读取成功画面尺寸:, frame.shape) cap.release()4.2 人脸样本录入录入是整个系统能否稳定工作的基石这一步做砸了后面识别率再高也没用。我给客户做现场注册时要求每人至少提供3张照片一张完全正脸、一张向左偏转15度左右、一张向右偏转15度左右并且最好在两种不同光照环境下拍。这样注册出来的特征向量泛化能力会比较强适应日常上班时站的位置、朝向和光线变化。录入脚本的核心逻辑很简单加载图片、定位人脸、提取特征、写入数据库。让我贴一下核心函数import face_recognition import sqlite3 conn sqlite3.connect(attendance.db) cursor conn.cursor() def add_employee(name, department, photo_path): image face_recognition.load_image_file(photo_path) face_locations face_recognition.face_locations(image) if not face_locations: # 图片里没有检测到人脸可能是照片太模糊或者不是正脸 raise ValueError(照片里没有检测到人脸请更换照片) encoding face_recognition.face_encodings(image, face_locations)[0] encoding_str ,.join(str(x) for x in encoding) cursor.execute( INSERT INTO employee (name, department, photo_path, face_encoding) VALUES (?, ?, ?, ?), (name, department, photo_path, encoding_str), ) conn.commit() print(f已录入{name}特征维度{len(encoding)}) # 示例批量录入 if __name__ __main__: add_employee(张三, 行政部, photos/zhangsan.jpg) add_employee(李四, 技术部, photos/lisi.jpg)这里有个细节face_recognition.face_locations返回的是一组人脸边框坐标一个人脸对应一个边框。如果照片里有两个人会返回两个框我写的是取第一个人脸的编码批量录入时照片最好只包含目标员工一个人。我实际运营时发现很多模糊照片会导致face_locations返回空数组所以在录入阶段就加了这个判断宁可让管理员换照片也不要让一个无效特征混进员工库否则后面识别时会产生莫名其妙的“幽灵匹配”。4.3 核心识别流程的实现实时识别的主循环比录入稍微复杂一点但核心就这几个步骤。先加载员工库里的所有特征到内存然后循环读取摄像头帧缩放后检测人脸对检测到的每个人脸提取特征并比对。import cv2 import numpy as np import face_recognition from datetime import datetime def load_employees(): rows cursor.execute(SELECT name, department, face_encoding FROM employee).fetchall() known [] for name, dept, enc_str in rows: enc np.array([float(x) for x in enc_str.split(,)]) known.append((name, dept, enc)) return known known_employees load_employees() def process_frame(frame): 处理一帧画面返回识别到的打卡人员列表 small cv2.resize(frame, (0, 0), fx0.25, fy0.25) rgb cv2.cvtColor(small, cv2.COLOR_BGR2RGB) # 在缩小的图上检测人脸 face_locations face_recognition.face_locations(rgb, modelhog) face_encodings face_recognition.face_encodings(rgb, face_locations) results [] for face_encoding in face_encodings: # 与所有员工特征比对 distances face_recognition.face_distance( [k[2] for k in known_employees], face_encoding ) best_idx np.argmin(distances) best_distance distances[best_idx] if best_distance 0.5: name, dept known_employees[best_idx][0], known_employees[best_idx][1] results.append((name, dept, float(best_distance))) return results识别准确率的关键在于“在缩小的帧上提取特征”是否靠谱。我的做法是小图上只做人脸检测拿到坐标然后把坐标放大回原图从原图上裁出人脸区域再对这块高分辨率区域提取特征。上面的简化代码是直接用缩小图提取特征的对720p摄像头问题不大但1080p下建议改成“小图检测大图提取”的方式。实际项目中我使用的是这个优化版本。打卡记录还要处理一个重要问题重复打卡。一个人在摄像头前停了三秒系统是以帧为单位识别的可能同一秒钟就识别出十几次不可能全部写入数据库。我用一个字典记录每个员工的最近打卡时间间隔小于60秒的重复识别直接忽略last_check_time {} def should_check_in(employee_name): now datetime.now() last last_check_time.get(employee_name) if last and (now - last).total_seconds() 60: return False last_check_time[employee_name] now return True配合这个函数process_frame识别到员工后先查一次重再插入attendance表。上下班的类型判断我放在记录时做上午11点前的打卡记为上班下午1点后的记为下班。如果公司有午休打卡需求改成“按当天已有记录数判断是第几次打卡”也很简单。4.4 UI界面和考勤记录模块界面我用的PyQt5整体的布局是一左一右左边显示摄像头实时画面同时把识别到的表情直接画在画面里右边是一个竖排的打卡状态栏从上到下依次显示“当前识别员工姓名、部门、打卡时间”刚打卡成功时状态栏会高亮几秒方便员工确认自己有没有打上卡。视频显示和界面刷新之间用QTimer定时器驱动每隔30毫秒从摄像头取一帧更新画面。识别计算放在一个独立的工作线程里不能放在UI主线程否则画面会卡成幻灯片——把识别线程算出来的结果通过信号量传给主线程主线程负责刷新界面。这个“界面线程与识别线程分离”的设计是我在文档里反复强调的工程要点很多第一次写PyQt的人都会栽在这里。考勤记录的查询页我用了一个QTableWidget可以从数据库加载今天的全部打卡记录按时间倒序排列。旁边放一个日期选择器和部门下拉框筛选条件变了就重新查一次。导出Excel直接调pandas一行代码搞定import pandas as pd def export_attendance(date_str): rows cursor.execute( SELECT e.name, e.department, a.check_time, a.check_type FROM attendance a JOIN employee e ON e.id a.employee_id WHERE substr(a.check_time,1,10) ? ORDER BY a.check_time, (date_str,) ).fetchall() df pd.DataFrame(rows, columns[姓名, 部门, 打卡时间, 打卡类型]) df.to_excel(f考勤记录_{date_str}.xlsx, indexFalse) print(导出成功)4.5 考勤报表与可扩展方向文档说明里我额外写了两个扩展模块的思路一个是“迟到早退统计”一个是“跨部门汇总”。迟到早退统计在SQL层面就能实现。比如公司规定9点上班那么当天第一条打卡记录时间晚于9点的就是迟到18点下班最后一条记录早于18点的就是早退。把这些按部门分组聚合就能看到不同部门当天的考勤概览。汇总报表可以做成一个简单的柱状图某部门在一周内的人均迟到次数、平均下班时间。虽然项目本身没有强制要求但既然数据库里存了这些数据呈现出来只是顺手的事。我的经验是考勤系统的价值一半在采集另一半在报表。哪怕只是简单地把迟到人员名单导出给行政系统的实用性也比只有“刷脸成功”提示高出两个量级。5. 常见问题与排查技巧实录5.1 程序启动和识别响应慢第一次启动时程序会加载dlib的两个预训练模型这一步大概需要1到2秒属于正常现象。真正的问题通常出在识别循环里如果发现摄像头画面有明显的延迟感先检查是不是在每一帧都执行了完整的人脸检测和特征提取。正确的做法是每隔3到5帧做一次识别中间帧直接跳过画面保持流畅识别结果也不会受影响。要是现场员工数量上了两三百每个人的特征比对都要算一遍距离逐帧比对还是会感到吃力。可以把员工特征向量转换成numpy矩阵、一次性做矩阵距离计算numpy的向量化运算比Python循环快很多。再不够就用GPU版的dlib不过那要重新编译一般部署时用不到。5.2 识别率忽高忽低识别率不稳定的原因九成出在光线和角度上。正对窗户的员工工位逆光严重摄像头拍出来的人脸黑乎乎一片特征提取自然不准。处理办法是在图像送入模型前加一个直方图均衡化让暗部细节亮一些或者调整摄像头的曝光补偿参数这个可以在OpenCV里设置。还有一种情况是员工注册时只有一张正脸照入职后换了发型、戴了眼镜识别距离一下子变大。解决办法很粗暴让员工重新录一次或者把原来的单张注册照补齐到多角度3张。我在项目里提供了一条“重新录入员工”的命令删掉旧特征重新提取全程不到一分钟。5.3 照片骗过系统和活体检测的兜底前面提过纯静态比对会被照片骗过这里补充一下项目里运动检测的具体实现逻辑。连续取两帧画面裁剪出人脸区域转成灰度图后计算像素差的绝对值和。如果这个值小于设定阈值说明画面里那张“脸”纹丝不动极有可能是照片直接拒绝打卡import cv2 def is_static_photo(face_frame_a, face_frame_b, threshold15.0): gray_a cv2.cvtColor(face_frame_a, cv2.COLOR_BGR2GRAY) gray_b cv2.cvtColor(face_frame_b, cv2.COLOR_BGR2GRAY) diff cv2.absdiff(gray_a, gray_b) score diff.mean() return score threshold注意不要让阈值设得过高否则员工正经站着正常呼吸也会被误判为照片。项目文档里推荐15到20之间实际用的时候可以根据摄像头画质微调。5.4 多人同时出现在镜头前怎么处理写字楼打卡经常遇到员工结伴走到摄像头前的情况。face_recognition.face_locations返回的是画面里所有人脸的列表上面的process_frame函数会依次处理每个人脸理论上都能识别。但实际中会出现两个人同时识别成功、几乎同秒写入数据库的场景。SQLite在同一时间并发写入时偶尔会报“database is locked”。我的处理方式是把所有打卡写入操作放进一个线程队列串行执行保证任何时刻只有一个写操作from queue import Queue from threading import Thread checkin_queue Queue() def worker(): while True: sql, params checkin_queue.get() cursor.execute(sql, params) conn.commit() thread Thread(targetworker, daemonTrue) thread.start() # 识别成功时入队 checkin_queue.put((INSERT INTO attendance ..., (emp_id, ...)))这个模式看起来简单但解决了很多并发问题。如果换用MySQL这个问题基本不存在因为MySQL对并发的支持比SQLite强得多。5.5 部署到写字楼前台的几个细节前台设备我建议用专门的迷你主机不要用前台办公人员的日常电脑否则一个后台文件传输就能把CPU占满打卡体验会很差。设备选好后摄像头高度和角度要调整好保证员工正常走过来的距离上人脸在画面中部而不是需要踮脚或者弯腰去凑镜头。另外一定要保证系统时间准确。考勤记录依赖系统时间如果主机时间偏差超过一分钟早晚会出现争议。我部署时直接给主机配置了时间同步服务让它在后台自己校准。断网状态下本地识别功能不受影响就是时间同步做不了恢复网络后会自然校准。隐私方面的建议也要提一下人脸特征属于敏感的生物识别数据项目默认所有数据保存在本地数据库不主动上传云端删除员工时连同它的特征向量一起删除并且提供一个“清理全部人脸数据”的功能方便到期或政策需要时一键处理。6. 写在最后踩过的坑和一点经验这个项目从第一行代码到能稳定跑完一个月的考勤周期我前后大概花了一周多。回过头看最折腾我的不是识别算法反而是dlib的安装、摄像头分辨率不一致导致的内存问题、以及PyQt界面线程卡顿这些工程问题。算法本身用现成库就能做得很稳工程化才是真正拉开差距的地方。另一个经验是关于效果的预期管理。人脸识别不是100%不出错的再好的系统也会在极端光线或者重度遮挡下发生拒识。我在文档说明里特意加了一行系统允许“识别失败时人工辅助确认”。也就是当前台遇到刷不上脸的情况管理员可以从后台直接手动补录一条考勤记录。保留这条人工兜底通道比把所有希望都押在算法上靠谱得多。最后分享一个小技巧如果你在这个项目基础上继续扩展优先做“当天未打卡人员提醒”。每天上午十点自动导出截止当前还没打卡的员工名单这一个小小的功能能让整个系统在行政眼里从“刷脸玩具”变成真正的考勤管理工具。这个项目的代码和文档结构都是围绕“可复现、可扩展”来组织的拿去做二次开发或者学习参考应该能帮你省下不少弯路。