☰
Python人脸识别写字楼考勤打卡系统设计与实践
2026/10/19 13:45:12 网站建设 项目流程

写字楼早晚高峰期,前台打卡排队的情况我想大家都不陌生。刷卡怕代刷,指纹怕干手指识别不了,纸质签到更是一笔糊涂账。我当时接到这个需求时,客户的核心要求就一句话:让员工刷脸进门的同时,把考勤记录自动留档。于是就有了这个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维特征中小规模考勤(百人内)、快速落地
自训练CNN(Facenet/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,零配置,一个文件搞定,适合中小规模项目;如果后续要部署到多人并发环境,可以换成MySQL,DAO层的代码改动很小。

员工表和打卡记录表的结构如下:

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 pandas

Windows用户如果卡在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), fx=0.25, fy=0.25) rgb = cv2.cvtColor(small, cv2.COLOR_BGR2RGB) # 在缩小的图上检测人脸 face_locations = face_recognition.face_locations(rgb, model="hog") 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", index=False) 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, threshold=15.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(target=worker, daemon=True) thread.start() # 识别成功时入队 checkin_queue.put(("INSERT INTO attendance ...", (emp_id, ...)))

这个模式看起来简单,但解决了很多并发问题。如果换用MySQL,这个问题基本不存在,因为MySQL对并发的支持比SQLite强得多。

5.5 部署到写字楼前台的几个细节

前台设备我建议用专门的迷你主机,不要用前台办公人员的日常电脑,否则一个后台文件传输就能把CPU占满,打卡体验会很差。设备选好后,摄像头高度和角度要调整好,保证员工正常走过来的距离上人脸在画面中部,而不是需要踮脚或者弯腰去凑镜头。

另外一定要保证系统时间准确。考勤记录依赖系统时间,如果主机时间偏差超过一分钟,早晚会出现争议。我部署时直接给主机配置了时间同步服务,让它在后台自己校准。断网状态下本地识别功能不受影响,就是时间同步做不了,恢复网络后会自然校准。

隐私方面的建议也要提一下:人脸特征属于敏感的生物识别数据,项目默认所有数据保存在本地数据库,不主动上传云端;删除员工时连同它的特征向量一起删除,并且提供一个“清理全部人脸数据”的功能,方便到期或政策需要时一键处理。

6. 写在最后:踩过的坑和一点经验

这个项目从第一行代码到能稳定跑完一个月的考勤周期,我前后大概花了一周多。回过头看,最折腾我的不是识别算法,反而是dlib的安装、摄像头分辨率不一致导致的内存问题、以及PyQt界面线程卡顿这些工程问题。算法本身用现成库就能做得很稳,工程化才是真正拉开差距的地方。

另一个经验是关于效果的预期管理。人脸识别不是100%不出错的,再好的系统也会在极端光线或者重度遮挡下发生拒识。我在文档说明里特意加了一行:系统允许“识别失败时人工辅助确认”。也就是当前台遇到刷不上脸的情况,管理员可以从后台直接手动补录一条考勤记录。保留这条人工兜底通道,比把所有希望都押在算法上靠谱得多。

最后分享一个小技巧:如果你在这个项目基础上继续扩展,优先做“当天未打卡人员提醒”。每天上午十点自动导出截止当前还没打卡的员工名单,这一个小小的功能,能让整个系统在行政眼里从“刷脸玩具”变成真正的考勤管理工具。这个项目的代码和文档结构都是围绕“可复现、可扩展”来组织的,拿去做二次开发或者学习参考,应该能帮你省下不少弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询