基于Django与人脸识别的考勤系统设计与落地实践

发布时间:2026/10/22 13:55:57
基于Django与人脸识别的考勤系统设计与落地实践
几年前接到一个考勤需求时对方单位还在用最原始的刷卡机。机器本身倒没坏问题是总有人让同事捎带刷月底统计一堆迟到早退扯皮。那会我恰好对本地人脸识别技术比较感兴趣就想着能不能把它和 Django 串起来直接从摄像头识别员工身份把打卡这件事做得更自动、更可靠。现在这套系统已经在内部试运行了一整个学期主流程稳定常规环境下识别成功率能到九成以上。这篇文章我就把从零设计到落地踩坑的过程完整拆开讲为什么选择 Django 这种“重框架”打底人脸识别链路怎么一步步搭考勤业务逻辑怎么设计以及真正跑起来之后一定会遇到的那些问题。给想自己动手做一套“基于 Django 和人脸识别的考勤系统”的开发者做参考。1. 项目整体设计与方案选型1.1 为什么是“Django 人脸识别”而不是别的最新技术先回答一个几乎每个刚看到这个题目的人都会问的问题都什么年代了人脸识别不是有现成的 SDK 和云服务吗为什么还要自己造轮子我的看法是考勤这件事的本质不是“认出那张脸”而是“认出脸之后的整个数据流”。云服务识别虽然省事但人脸数据要传到第三方服务器不少单位在这一关就直接摇头而且考勤这种场景流量不高真去按次调用云接口单价不便宜还得年年续费。相比之下本地跑 dlib 或 OpenCV 模型一台普通 PC 就能扛住几百人的底库后续每一条打卡记录都是自己的数据可控性会好很多。至于为什么选 Django而不是 Flask、FastAPI不是说 Django 一定最强而是考勤系统有太多“表单、列表、权限、统计”这类很不性感但躲不掉的功能。Django 自带的 ORM、Admin、Auth、表单校验和数据库迁移让我能把一个学期的功能从原型追加到上线只用几分之一的时间。打个比方Flask 像工具包需要什么自己拼Django 像带精装修的样板房墙壁、衣柜、灶台都给你备好了你只需要调整格局。对一门想着“快快落地”的项目后者香得多。除了框架选型底层算法我最终敲定的是 dlib face_recognition 这个组合。face_recognition 严格说是基于 dlib 的人脸识别封装它会先把人脸检测出来再提取一个 128 维的特征向量最后用欧氏距离判断相似度。选它是因为对中小型考勤项目来说效果够用、代码简单、纯离线。内部测试时底库存到 300 人左右单张图片的比对耗时稳定在几十毫秒完全够打卡这种低频请求。如果以后底库继续涨到一万人再上 FAISS 或者 PostgreSQL 向量检索也来得及模型选型留一条升级路就行。1.2 系统功能架构与数据流转整个系统我拆成了五块员工管理、人脸注册与识别、打卡接口、考勤规则、统计报表。员工管理直接复用 Django Admin人脸注册是一个独立页面管理员上传员工照片后自动完成特征提取识别模块封装成一个 Python 包不让业务代码跟算法细节直接纠缠打卡接口用 Django 原生 JSON 视图暴露统计报表则用 Django 的 ORM 聚合加 Excel 导出。数据流转是这样走的。员工第一次使用管理员在后台录入员工编号和姓名然后让员工正对摄像头拍 3 到 5 张照片。后台每张照片先做人脸检测确保照片里确实有且只有一张人脸再做人脸编码把 128 维向量存进人脸特征表。打卡的时候员工在装有摄像头的平板或电脑前按一下按钮摄像头抓拍一帧前端把图片压缩后交给后端后端调用识别模块比对出“这张脸属于谁”然后根据考勤规则写入记录。这里有个容易忽略的点识别模块和 Django 业务逻辑一定要解耦。最开始我把识别函数直接写进 view 里测了几次没毛病可一旦同一个 view 同时有几十个请求进来人脸检测本身就很占 CPU结果一个打卡请求卡住后面排队全卡死。后来我把识别逻辑独立成一个服务模块Django 只负责接收图片、调用识别、落库整个请求耗时从原来的随机几秒变成了稳定控制在 1 秒以内。哪怕以后要把识别服务单独抽成 gRPC 接口迁移成本也小。2. 核心细节解析与难点突破2.1 人脸识别链路检测—对齐—编码—比对人脸识别放到考勤场景不是简简单单“用 OpenCV 找脸再比一比”。完整链路被我分成了四步。第一步是人脸检测。face_recognition 的 face_locations 函数默认用 HOG 特征检测人脸CPU 上速度不错如果检测不到再考虑用 CNN 模型降级。不过实测下来考勤摄像头通常固定机位、正面人脸HOG 基本够用。第二步是对齐。这里很多人会跳过但我觉得不能省。同一个人的脸如果偏头 15 度直接用原图提取特征和正面照片的欧氏距离会明显变大用 68 点关键点检测出眼睛、鼻子、嘴角坐标后做一个仿射变换把人脸扭正距离就能稳定很多。你可以把这一步理解成“先把证件照摆正了再拿放大镜端详”。第三步是编码。face_recognition.face_encodings 会把规整后的人脸转换成一个 128 维的浮点向量本质是把一张脸压缩成数学坐标。第四步是比对。对库里的向量逐一算欧氏距离距离越小越像。默认阈值 0.6 是作者给的一个通用参考值但我建议自己采样调优后面我单独讲。这里补充一个经验底库不要只存一张照片。考勤环境的光线、眼镜、发型变化都很大一个人注册时我就要求至少上传 3 张不同光线、不同角度、有无眼镜的照片比对时把候选员工的几张特征向量都算一遍取最小距离。这样既不明显拖慢速度误拒率又能降一档。不要嫌这种活粗糙识别率往往就是这样一点点抠出来的。2.2 阈值到底怎么定才不容易误判阈值选多少是整个系统里最容易被糊弄过去的问题。我们内部做过一次小实验请 20 名志愿者每人采集了 8 张不同条件下的照片把一个人的 4 张放入底库另外 4 张当作“打卡测试图”然后用同一个算法算距离。结果同一人测试图和底库的距离大多在 0.22 到 0.52 之间不同人的距离基本在 0.48 到 0.85 之间。可以看到两组数据中间有一个交叉区大体在 0.45 到 0.55 这个范围。如果阈值设得太严比如 0.35好处是不会认错人但稍微换个光线就可能把本人拒之门外如果太松比如 0.65隔壁同事穿件同款工服、侧个脸就可能被认成同一个人。最后我把阈值定在 0.55并在代码里留了个配置项方便部署时根据摄像头环境微调。如果发现交叉区间特别大说明底库照片拍摄质量不稳定这时候应该回头去补注册照片而不是把阈值调来调去。阈值只是最后一道闸照片质量才是根子上的事。2.3 Django 侧的模型设计与接口规划数据库设计我建了四张核心表。员工表存编号、姓名、部门、状态人脸特征表通过外键关联员工存 128 维向量考勤记录表保存某人在某天的签到、签退时间和判定状态考勤规则表配置上下班时间和宽限分钟。模型大致是这样class Employee(models.Model): employee_no models.CharField(max_length20, uniqueTrue) name models.CharField(max_length50) department models.CharField(max_length100, blankTrue) is_active models.BooleanField(defaultTrue) class FaceEncoding(models.Model): employee models.ForeignKey(Employee, on_deletemodels.CASCADE, related_nameface_encodings) encoding models.JSONField() class AttendanceRule(models.Model): name models.CharField(max_length50) start_time models.TimeField() end_time models.TimeField() late_grace_minutes models.IntegerField(default0) leave_early_grace_minutes models.IntegerField(default0) class AttendanceRecord(models.Model): employee models.ForeignKey(Employee, on_deletemodels.PROTECT) date models.DateField() check_in models.DateTimeField(nullTrue, blankTrue) check_out models.DateTimeField(nullTrue, blankTrue) status models.CharField(max_length20, defaultnormal)为什么用 JSONField 存向量而不是 BLOB主要是方便调试后台直接能看到数据迁移也不容易出问题。向量一共才 128 个浮点数就算 1 万个人也就几 MB完全不是瓶颈。如果你想追求极致性能以后可以改成 PostgreSQL 的 vector 字段或者接 FAISS这个后面再说。接口方面最核心的是打卡接口前端把摄像头抓到的那一帧图片 base64 编码后 POST 过来后端解码、调用识别模块、判断员工是否有效再根据当前时间写入 AttendanceRecord。返回值统一给一个 JSON 结构code、message、employee_no、name、photo_time。其他接口包括员工注册、特征重录、考勤记录查询、月度导出都属于常规 CRUD不展开再说了。3. 实操过程与关键环节实现3.1 环境准备与基础依赖安装开始动工前先把环境备好。Python 我用 3.10安装以下依赖Django 4.xdlibface_recognitionopencv-pythonnumpydlib 是这里面最让人头疼的一个。Windows 下直接 pip install dlib十有八九会因为缺 CMake 和编译工具报错。我的建议是有条件就在 Ubuntu 服务器上跑先执行apt install build-essential cmake再pip install dlibWindows 下可以搜预编译的 dlib wheel或者用 conda 装。face_recognition 相对好装但依赖 dlib如果 dlib 没装好它也会跟着报错。项目结构我建议这样组织attendance/ manage.py apps/ employees/ attendance/ recognition/ recognition/ detector.py encoder.py matcher.py static/ templates/把 recognition 单独放一个包里面封装 detector、encoder、matcherDjango 的视图层只能调用 matcher.match()不要直接碰 face_recognition 的细节。这样以后想换模型改动范围能锁住。3.2 人脸注册模块先把底库建好注册模块的工作流是管理员在页面上选择员工上传照片后端保存照片并提取特征。下面是核心代码的简化版本def register_face(request): employee get_object_or_404(Employee, idrequest.POST[employee_id]) image_file request.FILES[face_image] image face_recognition.load_image_file(image_file) locations face_recognition.face_locations(image) if len(locations) ! 1: return JsonResponse({code: 40002, message: 照片中必须且只能有一张人脸}) encodings face_recognition.face_encodings(image, locations) if not encodings: return JsonResponse({code: 40003, message: 未提取到人脸特征}) FaceEncoding.objects.create(employeeemployee, encodingencodings[0].tolist()) return JsonResponse({code: 0, message: 注册成功})这里必须检查人脸数量。有人会想当然地认为“反正我传的是单人照”实际上员工上传的照片里经常出现桌面上的照片摆件、身后同事半张脸检测到多张脸时我们要直接拒绝而不是随机选一张。注册时我会额外叮嘱现场人员员工正对摄像头不要侧脸不要低头背景不要有强光如果是戴眼镜的最好摘掉眼镜拍一张、戴上眼镜拍一张。这些经验看似琐碎却直接决定后续识别率的上限。另外如果发现某位员工反复注册失败大概率是照片质量问题不要硬调阈值先让现场把照片拍清楚。3.3 打卡接口从摄像头画面到考勤记录打卡页面我用一个简单的 HTML 页面调用浏览器的 getUserMedia 打开摄像头用户在页面上看到自己后点击“打卡”前端把 canvas 截图的 base64 发到后端。这里有一个小细节相机拍出来的原始分辨率很高直接传原图既慢又占带宽。我在前端先对图像做了压缩把长边限制在 640 像素然后再把质量压到 0.7这样一张图只有几十 KB后端识别速度也快了不少。后端的核心逻辑如下def check_in(request): image_data request.POST.get(image_base64) image_bytes base64.b64decode(image_data.split(,)[-1]) image face_recognition.load_image_file(io.BytesIO(image_bytes)) locations face_recognition.face_locations(image) if len(locations) ! 1: return JsonResponse({code: 40004, message: 请确保正对摄像头保持画面中只有一张脸}) encodings face_recognition.face_encodings(image, locations) if not encodings: return JsonResponse({code: 40005, message: 未提取到人脸特征}) match matcher.match(encodings[0]) if not match: return JsonResponse({code: 40006, message: 未识别到本人}) employee, distance match today timezone.localdate() now timezone.now() record, created AttendanceRecord.objects.get_or_create( employeeemployee, datetoday, defaults{check_in: now, status: normal} ) if not created: return JsonResponse({code: 0, message: 今天已打过卡, employee: employee.name}) ...这里我用 get_or_create 来避免同一天重复插入多条记录再用时间间隔限制短时间内重复点击。要注意的是打卡时间一定要用 Django 的 timezone不要在前端用浏览器时间判断因为浏览器时间可以被员工手动改服务器时间更可靠。考勤系统说到底是个数据系统时间可信度是底线。关于并发我额外做了两层防护。第一层在应用层识别模块内部用一个 Lock 保证同一时刻只有一个线程在跑 dlib 的编码计算第二层在数据库层AttendanceRecord 表对 (employee, date) 建唯一约束就算双请求同时过来数据库也会帮你挡住脏数据。不是所有系统都会遇到并发但这种边界问题一旦出了就是数据事故提前防住没坏处。3.4 考勤统计与数据导出的小工具统计模块是我最后补的但实用度一点也不低。原本可以只用 Django Admin 查看每一天的记录可 Admin 的列表看多了眼睛疼还做不了月底汇总。我写了一个简单的月度报表页面按部门显示每个人当月出勤天数、迟到次数、早退次数和缺勤天数再提供一个“导出 Excel”的按钮。导出部分只讲思路用一个视图接收 year 和 month查 AttendanceRecord 表按员工编号 group_by逐条判定状态最后用 openpyxl 写 xlsx直接返回 HttpResponse 设置好 content_type 和文件名就行。判定迟到/早退的逻辑不复杂但一定要处理一个边界情况员工当天只打了一次卡是算迟到还是算缺勤我的做法是只要今天有打卡记录就按“有考勤”算只是状态可能是迟到如果一条记录都没有才算缺勤。统计报表必须考虑“加班”和“调休”这个需求往往是在开发中临时被加进来的。我在 AttendanceRule 里预留了字段比如是否允许加班、加班最小小时数但说实话考勤系统的业务规则每个单位都不太一样与其把规则做得无穷复杂不如先把核心打卡跑稳再在 Admin 里允许管理员手动补录和调整状态。实际使用下来手动补录的比例大概在 5% 以内这在实际应用中是完全可以接受的。4. 常见问题与排查技巧实录4.1 高频问题速查把我在项目里真实遇到过的坑整理成一张表格方便你直接对着排查现象原因解决办法一打开页面摄像头画面是黑的浏览器摄像头权限未开或没走 HTTPS开发环境用 localhost部署用 HTTPS检查是否申请 getUserMedia 权限dlib 安装失败报 CMake 或 MSVC 错误Windows 下缺少编译工具链用预编译 wheel或切到 Ubuntu 部署先装 build-essential cmake识别耗时长页面经常卡每帧都做全量检测和编码打卡只取一帧识别逻辑放到独立服务降低图片分辨率同一人上午正常下午被拒光照变化、阴影注册时采集多张不同光照照片调整阈值有条件加补光灯两个人交叉误认阈值设太松或底库照片太相似调低阈值检查底库照片删掉模糊和过曝的图片同一天出现重复打卡记录前端没做防抖后端没有唯一约束前端按钮防抖后端 get_or_create 数据库唯一索引导出 Excel 打开乱码用了 csv 但编码设置不对用 openpyxl 输出 xlsx或 csv 时用 utf-8-sig这张表是我在开发中一条一条记下来的每一条都真实发生过。其中“调阈值”其实就是改 matcher 里的一个常量但改完一定要跑一遍回归测试否则可能把原本正确的识别也搞挂。建议把阈值配置放在 Django settings 或环境变量里而不是硬编码在代码中。4.2 几个容易踩的坑和我最终的解决思路第一个坑数据库里到底存什么。刚开始我想省事直接把员工原始照片存到服务器后台列表里也能看。后来想想脸部照片属于个人敏感信息一旦服务器被拖库责任很大。后来调整为默认不保存原始照片只在识别不通过时把当前帧临时保存下来供管理员人工复核并自动在 7 天后清理。人脸特征表里只存 128 维向量向量本身不能还原成人脸图像这个设计在内部过审时要干净很多。第二个坑时区处理。有一次我发现晚上 11 点打下班卡记录里日期变成了第二天因为前端传过来的是浏览器本地时间而后端用的又是服务器 UTC。后来统一改用 Django timezone 下的 now()并且明确签到签退都以服务器时间为准再也没出过这种跨天错乱。考勤数据涉及工资结算日期错一天月底全对不上这种低级错误最致命。第三个坑比对前一定要做对齐。我在最开始为了省时间跳过对齐步骤结果同一人脸稍侧一点距离直接波动 0.1 以上。后来把关键点检测和仿射变换补上距离分布明显收敛。这个动作在代码里只多了不到二十行后面测试时却很意外地发现误拒率降了接近一半。做人脸识别细节真的都在这些不起眼的预处理里。第四个坑识别服务的内存占用。face_recognition 依赖的模型初次加载会占几百 MB 内存如果每次请求都重新加载模型性能和内存都扛不住。我把识别模型做成模块级单例第一次调用时加载后续复用部署时在 uwsgi 或 gunicorn 里保持 worker 数不要太多否则几个 worker 各加载一份模型内存会直接爆炸。我的做法是 4GB 内存的机器只跑 2 个 worker识别排队的时间和打卡体验都在可接受范围。最后再分享一点个人在实际操作中的体会是这类系统真正难的不是把识别模型跑通而是把“识别结果”和“考勤业务规则”串顺。作为一个开发者先把流程跑通让一个真人能从注册、打卡、看报表完整走一遍比钻在模型调优里更有价值。后面如果要扩展可以再加入实时视频流识别、多设备打卡终端同步、移动端 H5甚至换成更轻量的嵌入式端侧模型。无论如何先把当前这一版稳定运行起来再去谈下一步怎么升级这总不会出错。