☰
基于Flask与SQLite的轻量级人脸识别考勤系统实现
2026/10/27 22:48:32 网站建设 项目流程

1. 项目概述:为什么我选了Flask轻量方案

1. 项目概述:为什么我选了Flask轻量方案

你要是问我现在最闹腾的考勤系统是什么,十个里八个会挂个摄像头。人脸识别考勤这几年火到什么程度——园区门禁、教室点名、工地实名制,到处都是。但绝大多数是成品设备,一套下来少则几千,多的好几万,还得整套云平台和数据后台跟着,小公司和小团队很难吃得动。

这个项目做的事情很简单:用Flask + SQLite做一个人脸识别考勤认证系统,摄像头拍一张脸,系统判断你是谁,往数据库里写一条考勤记录。不依赖任何商业云服务,人脸编码和比对全部本地完成,数据不出局域网。那一刻你就能明显感受到,很多看似“高大上”的识别系统,底层逻辑其实没你想的那么玄乎,用对工具就能搭起来。

我先说几个关键词,方便筛选读者:人脸识别、Flask、SQLite。如果你正在用Flask做Web开发,或者想给自己的小项目加一个“刷脸打卡”功能,又或者单纯好奇人脸识别从图像到“认出你是谁”这中间到底发生了什么,这篇博文会非常合你胃口。我尽量把原理讲人话,把代码贴出来,把坑提前踩给你看。

方案选型上也纠结过一阵子。人脸识别部分,网上方案很多,云端API确实省事,但按次收费,企业内部用还是心里不踏实——人脸数据往外传,合规和隐私问题迟早找上门,不如本地推理干净。Web框架上,FastAPI这两年风头很猛,但需要异步编程思想,对纯新手门槛稍高;Flask胜在简单直白,生态也老,遇到问题几乎随便搜就有答案。数据层用SQLite,一开始我也有顾虑,担心并发多写会不会挂,后来压测了一下,十万条考勤记录查询毫秒级返回,根本用不着上MySQL。

一句话总结这个项目的定位:帮你在半小时内,从零搭一套真正能跑的人脸考勤系统,并且让你彻底搞懂它每一步在做什么。

2. 核心难点拆解:人脸识别的“三步走”

2.1 人脸检测、对齐与特征提取的关系

很多初学者把人脸识别当成“一张照片进去,出来一个名字”的黑盒子,这会导致遇到问题时完全不知道从哪下手。实际上从摄像头画面到考勤记录,中间至少要过三关:检测、对齐、特征提取。

检测做的就是“人脸的框在哪里”这件事。OpenCV的Haar Cascade和基于深度学习的MTCNN、YOLO都能干,效果差异巨大。Haar Cascade速度快但漏检误检都高,对侧面脸基本放弃;MTCNN稍微重一点但准确率明显好,定位人脸的同时还会给出双眼、鼻尖、嘴角共五个关键点。这几个关键点非常有价值,因为这直接关系到下一步的“对齐”。

对齐很多人会忽略,但它恰恰决定识别率的上下限。你想,人脸歪着、仰着、侧着,特征提取网络看到的完全是不同的图像内容,拿这些特征去比对,结果必然不稳定。对齐做的事情就是把脸“扶正”:根据五个关键点的坐标,计算一个仿射变换矩阵,把眼睛拉到水平位置,把整张脸缩放到固定的尺寸(比如160x160像素)。这样网络输入的每个人的脸,都是标准“证件照”姿态,特征提取才有意义。

特征提取是整个流程的灵魂。这一步要做的是把人脸图像转换成一个高维向量——这个向量就是人脸的“数字指纹”。face_recognition库用的是dlib的ResNet模型,输入一张RGB人脸图,输出128维浮点向量;ArcFace、FaceNet这类模型输出的可能是512维甚至更高。向量里的每一个数值看起来毫无意义,但它们组合在一起,就编码了这个人的面部几何结构、纹理分布这些独特信息。后续的比对,本质上就是在比对这些向量的“远近”。

你要是没接触过上一步到这一步的转换过程,我建议耐心往下看,我在2.2里把神经网络的这趟“旅程”掰开揉碎讲一遍。

2.2 图像到高维向量:神经网络到底在算什么

要理解高维向量的含义,先说一个生活比喻。你可以把128维向量想象成存放在高维空间里的一个点,这个点有128个坐标。同一张人脸的所有角度照片,在模型理想状态下应当映射到这个点附近;不同人的脸,则是散布在高维空间里相距较远的点。识别任务就退化成“计算两个点的距离”,距离小于阈值算同一人,否则不是。

人脸图像是怎么一步步变成这128个数的?从输入开始,图片会先做归一化:把像素值从0到255的RGB,压缩到-1到1之间,同时缩放到模型要求的尺寸,比如dlib要求150x150。这一步非常重要,像素值过大或过小会让网络的初始梯度计算不稳定,影响推理结果。

进入神经网络之后,前面的卷积层有点像人的视觉皮层。第一层卷积关注的是最基础的信息:图像里的线条、边缘、色块,这些信息被编码成特征图的局部模式。随着网络层数加深,第二层、第三层的卷积开始把这些边缘拼起来,组合成眼睛、鼻子、嘴巴这些五官部件。再往后,深层网络组合出的是过分抽象的整体面部结构特征——眉骨高度、颧骨形状、眼下区域的纹理走向。

ResNet模型在做一件很聪明的事:输出层的神经元数量被设计成128个,最后通过一个全连接层把前面所有特征汇总,压缩成128个浮点数。整个网络在被训练的时候,用的是一种叫Triplet Loss(三元组损失)的思路:每次训练挑三张图,一张锚点图(某人A的照片)、一张正样本(A的另一个角度照片)、一张负样本(B的照片),网络学习的目标就是让锚点和正样本的向量拉近、同时把负样本推远,反复调整网络权重,直到“同一人聚拢、不同人分离”的空间结构稳定下来。

这些模型都是别人花无数GPU时长训练好的,我们不需要重新训练,只需把推理这一步接到我们系统中,拿现成的128维向量去做比对。这就像你不需要会造发动机,但会踩油门就行。

2.3 相似度计算与阈值设置

拿到高维向量之后,怎么判断“是不是同一个人”?最常用的相似度度量有两种:欧氏距离和余弦相似度。

欧氏距离的理解很直观,就是高维空间里两个点的直线距离,数值越小说明越相似。余弦相似度衡量的是两个向量的方向一致性,数值越接近1说明夹角越小。face_recognition库默认用欧氏距离,并推荐阈值为0.6——当两张脸的编码距离小于0.6时判断为同一个人,大于0.6则拒绝。

这个阈值是经验值,不是铁律。它需要在“安全”和“方便”之间做权衡。阈值调小(比如0.4),识别更严格,陌生人被放进来会很难,但员工本人也可能因为角度、光线、眼镜变化就被拒之门外,每天几个人卡在门口消磁就很浪费时间。阈值调大(比如0.7以上),所有人都能顺利通过,代打卡的风险也随之上升,因为别人拿你一张高清照片也能混进来。

我的建议是:先按0.6跑一周,把每天的识别失败记录和误识别率统计出来,再动态调整。不同摄像头的成像质量、现场光线环境对阈值的影响非常大。办公室LED灯下的0.6,换到室外强光下可能要调成0.55。这也解释了为什么市场上的人脸识别门禁机不让你自己调参——设备厂商早就按他们的硬件给你优化好了。

3. 数据库设计与技术选型细节

3.1 SQLite在考勤场景能扛住吗

先给一个明确的结论:考勤这个场景,SQLite完全扛得住。一个几百人的公司,一天考勤记录算四条(上班、下班、加班、加班下班),一年也才百万条级别。SQLite单文件数据库对百万行记录的读写表现,实测远超够用的水平。

我自己用一万条和十万条数据做过简单压测。在一万条记录中做带索引的按日期查询,返回结果基本在50毫秒以内;十万条记录时的查询也就上升到100毫秒左右,对考勤查询这种不需要实时响应的场景,毫无压力。很多团队一上来就上MySQL,结果还要雇人维护数据库服务、备份、权限管理,纯属杀鸡用牛刀。

SQLite真正的问题在并发写。它采用数据库级锁,同一时刻只允许一个进程写库。Flask默认的Werkzeug服务器是多线程的,如果在考勤高峰时刻,多个人同时打卡进入,写操作就可能撞车,抛出“database is locked”错误。解决办法有两个。一是把SQLite连接串上加上check_same_thread=False,允许多线程共享连接。但这样仍可能撞锁,更好的做法是设置连接超时参数:connect(timeout=10)。二是开启WAL(Write-Ahead Logging)模式,它把写操作先写入日志文件,再异步合并回主库,读写之间互不阻塞,显著降低了锁冲突的概率。两步都做了,基本不会因为锁问题丢数据。

3.2 人脸特征怎么存进数据库

很多人会问,128维浮点向量怎么进SQLite?答案很简单:存BLOB二进制字段。

具体做法是用numpy把向量转成字节数组,然后写进数据库。读取时查出来再通过numpy还原成向量。但这里有一个重要经验教训:不要直接用浮点数组的原始二进制存储。比如numpy默认的float64类型,128个数字就占1024字节,查询比对时还原倒没问题,但存储浪费。

我建议存float32,精度对人脸识别比对的影响微乎其微,存储占用直接减半。如果你连这512字节的空间也想省,还可以对特征向量做量化,把每个值映射到0到255的整数范围,代价是识别精度可能有些许下降,对考勤场景不值得。所以我最终用的是float32 + BLOB,这个组合最均衡。

表结构设计上,用户表里除了特征向量,还要存员工工号、姓名、部门ID、注册照片路径这些字段。工号唯一索引,这是业务上的人脸ID对应关系。考勤记录表则单独存放每条打卡记录,关联用户ID、打卡时间、考勤类型(上班、下班)。两张表用外键关联,查询时用JOIN,干净利落。

import numpy as np import sqlite3 # 将128维向量转为字节流(float32) def vector_to_blob(vector): return vector.astype(np.float32).tobytes() # 从字节流还原为128维向量 def blob_to_vector(blob): return np.frombuffer(blob, dtype=np.float32)

3.3 索引策略与查询优化

考勤查询最常做的操作是“查某人某天的记录”和“查某天谁没打卡”,所以时间和用户这两个维度最好都建索引。SQLite里建索引很轻量,建了之后查询性能提升明显。

我在实际项目中给attendance表建过两个索引:idx_attendance_time(打卡时间)和idx_attendance_user(用户ID)。实验对比,十万条数据无索引query要扫全表,耗时近一秒;加索引后直接降到几十毫秒。这在Web请求的响应延迟上是有感觉的。

有一种查询容易让人掉坑:查询中使用了strftime('%Y-%m-%d', check_time)来按日期分组。注意,即使check_time字段有索引,因为你对字段做了函数操作,SQLite也不会走索引,全表扫描不可避免。十万条还好,百万条就会卡顿。正确做法是查询时直接传日期边界,或者把日期单独存一列,建索引,查询走索引。

4. Flask接口与前端交互实现

4.1 路由设计:注册与考勤

整个系统最核心的API有三个:注册人脸、考勤认证、查询记录。设计API的时候我始终遵循一个原则:人脸识别和数据库操作要解耦。前端可以把摄像头拍摄的图片Base64编码传到后端,后端再运行人脸识别流程,这样前后端职责清晰,调试方便。

注册接口接收的还是原始图片。后端的处理逻辑是:先检测图片中是否有人脸,没有人脸直接返回错误提示;有人脸则进行对齐、编码,把得到的128维向量写入数据库的users表,同时保存原始照片作为存档。注册成功之后,这条人脸特征就生效了,接下来这个人就可以直接拿来打卡。

考勤认证接口是另一个核心接口。它接收待识别图片,做同样的检测、对齐、编码流程,然后取出编码向量,与users表里所有用户的特征向量逐一做距离计算。这里涉及到一个性能决策:是全表遍历比对,还是设计一种高效索引?考勤系统用户规模不大,几百人的库全表遍历也就几十毫秒,算法复杂度是O(N),完全可接受。但如果未来要扩容到几万人,就需要考虑引入faiss这类向量检索库做ANN(近似最近邻)搜索了,这个属于后话。

4.2 多线程安全与连接管理

Flask的典型开发服务器app.run()默认是单进程多线程的,每个请求会在线程中执行,如果你在模块级别创建了一个SQLite连接,然后所有线程都用它,SQLite的线程安全模式默认是serialized,一般不会崩,但并发写库时会出现锁竞争。

我的建议是每次请求独立创建连接,用完即关,或者使用flask.g来按请求管理连接。我用的是后者,在before_request钩子中创建连接,在teardown_request中关闭。这能保证每个请求用独立的SQLite连接,互不干扰。同时设置socket_timeout和WAL模式,最大化避免锁问题。

from flask import Flask, request, jsonify, g import sqlite3 DATABASE = 'attendance.db' def get_db(): if 'db' not in g: g.db = sqlite3.connect(DATABASE, timeout=10) g.db.row_factory = sqlite3.Row return g.db @app.teardown_request def close_db(exception): db = g.pop('db', None) if db is not None: db.close()

人脸识别部分很吃CPU,在Web应用里它是个耗时操作,跑一次编码大约需要0.2到0.5秒(取决于机器和模型),如果并发请求多,Flask的多线程模型还能并行处理,瓶颈只在CPU频率上。如果部署环境是树莓派之类的低性能设备,建议开启一个后台队列来串行处理识别请求,避免多个请求同时推理导致内存爆炸。

4.3 前端页面怎么做:一个极简但完整的示例

前端不用做复杂设计,一个展示摄像头画面的页面、一个“打卡”按钮,足矣。浏览器里调用摄像头用getUserMedia,但现代浏览器的安全策略要求页面必须是HTTPS或localhost环境才能调用摄像头的,所以调试时用localhost,部署时用反向代理加HTTPS证书。这点特别容易踩坑,如果你直接把Flask服务起在服务器上通过IP访问页面,摄像头是调不起来的。

前端把拍到的照片通过Canvas转成JPEG数据,用POST请求送到后端接口。后端拿到图片后开始推理识别,把结果返回给前端,前端根据返回结果弹窗提示“打卡成功”或者“未识别到人脸,请重试”。我建议把打卡成功的员工姓名、打卡时间也在前端显示出来,增强反馈感。毕竟考勤机如果能说一句“打卡成功”,员工心里踏实很多。

5. 完整部署流程与操作笔记

5.1 环境准备:Windows和Linux下的安装

这个项目跨平台没问题,Windows下开发调试,Linux服务器上部署跑服务,步骤基本一致。依赖是Flask、dlib、face_recognition、opencv-python、numpy。dlib的安装是最大的坑,对新手极其不友好,需要先装CMake和Visual Studio Build Tools(Windows下),否则pip装dlib会直接编译失败。

我强烈建议在Python 3.8到3.10之间使用,dlib和face_recognition对新版本Python的支持会滞后,硬上最新版可能要自己编译。Ubuntu下用apt装cmake和build-essential,Windows下装Visual Studio Build Tools勾选“C++桌面开发”然后pip install dlib,一步到位。

如果你不想碰dlib编译的鬼打墙,有个替代方案:用OpenCV内置的DNN模块加载OpenFace的ONNX模型来提取特征。这样能避开dlib,但OpenCV的DNN模块对ONNX的支持没有直接调用facet_recognition方便,你也需要自己实现一些预处理流程。两难的选择,我建议踩坑一次,认真装上dlib,后续项目还会复用。

5.2 启动Flask服务与生产部署

开发调试时可以直接app.run(host='0.0.0.0', port=5000)。但生产环境绝对不能跑开发服务器,性能差、并发能力弱,直接暴露还有安全隐患。正确做法是用gunicorn(Linux)或waitress(Windows)启动Flask应用。我用的gunicorn命令如下:

gunicorn -w 4 -b 0.0.0.0:5000 app:app

-w 4表示4个工作进程,每个进程独立运行Flask应用。这里注意,多个进程同时写SQLite,锁竞争更高了,必须开WAL模式,且每个进程连接数据库时设置合理的timeout。如果遇到写锁冲突频发,可以把-w降到2,或用Nginx做负载均衡同时只给SQLite一层入口。

Nginx做反向代理还能顺便解决HTTPS问题。毕竟浏览器调用getUserMedia必须HTTPS,你可以在Nginx配置SSL证书,把443端口的请求反向代理到本地的5000端口。这样前端页面就能正常工作,摄像头调用不会报错。

布署完测试时,我还有一个建议:用curl直接测试API接口是否正常返回,然后再用浏览器去访问页面。把问题切分清楚,后端问题还是前端问题,调试效率至少高一倍。

5.3 用DB Browser查看考勤数据

项目做完后,你可能想快速看数据库里存了多少用户、考勤记录长什么样。SQLite数据库是单个.db文件,用DB Browser for SQLite这个工具打开就能以表格形式浏览,像Excel一样。这个工具免费、跨平台、Windows/macOS/Linux都有安装包。

实际使用中我发现,用DB Browser排查数据问题非常方便。比如某个员工注册了好几次人脸,导致库里存在多条特征记录;或者某天的考勤数据异常,缺了一大段记录。直接在DB Browser里执行SQL语句,几秒钟就能定位问题,比在Python里写脚本查快得多。

还有一个DB Browser很实用的功能:可以把查询结果导出成CSV文件。每个月月底做考勤汇总时,直接导出交给财务核算,省得单独开发导出功能了。

6. 常见问题与排查经验

6.1 识别准确率不理想怎么办

这是一个高频问题,排查方向首先要锁定“是检测失败还是比对失败”。我建议在系统里加一个日志功能,把每一步的检测置信度、人脸框坐标、比对距离值都打进日志。如果检测阶段就没人脸框,问题在光线、角度、摄像头清晰度;如果检测正常但比对距离全在0.7以上,问题在阈值或注册照片质量上。

注册照片的影响比大多数人想的大。很多公司给员工拍照用手机随意拍,光线不均匀、角度俯仰严重,这种照片提取出来的特征本身质量就差,后面怎么调都高不了。我建议注册时使用现场摄像头拍摄,要求员工正对镜头,保持正常距离,确保面部光照均匀。一张高质量的注册照片,比十张模糊照片有用一万倍。

眼镜也是个易错的点。框架眼镜会对特征提取有轻微干扰,普通人摘下眼镜再戴上,特征距离差个0.1左右很常见。如果公司有戴眼镜和不戴眼镜状态差异大的员工,可以给同一个人注册两组特征,比对时取最小距离,能显著减少误拒。

6.2 SQLite锁导致的“database is locked”错误

前面提过这个经典问题。解决路径清晰:启用WAL模式,设置连接超时,避免多个进程高频写库。但如果问题仍然存在,那就是你同时开的写连接太多了。我排查过一个现场,用户在打卡高峰期同时有20个人请求打卡,每个请求都独立创建连接写库,虽然有WAL,但写进程之间的锁竞争还是让部分请求超时失败了。

解决办法是把写操作集中化。打卡时把识别结果先写入一个内存队列,由一个后台线程统一负责写数据库,这样同一时刻只有一个线程在写库,锁竞争彻底消失。对于考勤这个场景,写入延迟几十毫秒完全无感,但稳定性提升巨大。

6.3 摄像头画面卡顿与延迟

人脸识别卡在“摄像头每帧都识别”上,性能一定不够。视频帧率是30fps,每帧都跑一次特征提取,CPU直接拉满,页面卡死。正确做法是降低处理频率,比如每秒最多处理2帧,或者每隔一定帧数处理一次。用户在摄像头前停留两三秒完全足够完成识别,根本不需要实时识别。

再用OpenCV做人脸检测时,实际识别推理之前先对图像做缩放。把摄像头采集的1920x1080画面先压到640x360再送检测器,检测速度能提升好几倍,人脸框定位精度影响不大。如果检测器单人脸定位不准,也可以直接利用MTCNN的检测结果裁剪出人脸区域,再缩放至标准尺寸送入特征提取网络,这样精度和性能能平衡得更好。

另外一个很重要的细节:不要每次都把Base64编码传回后端做解码。这些图像数据在网络传输上开销很大。摄像头的视频流在浏览器端直接交给Canvas处理,每帧检测时只把裁剪好的人脸截图传到后端,图片小、速度也快。如果是在局域网内部用,差距还不明显;如果将来要远程部署,这种优化能显著降低卡顿。

6.4 冷启动速度和内存占用

dlib的模型文件非常大(大约100MB),模型加载耗时几秒。每次启动Flask服务时都会加载一遍,如果模型在请求时才加载,第一个请求会卡很久。正确做法是在应用启动时预加载模型,把这部分时间放在服务器启动阶段。

内存方面,face_recognition加载模型之后大约占用100到200MB内存,多个worker进程的话,内存翻倍。gunicorn如果起了4个worker,光模型就占800MB。如果你服务器内存紧张,可以把worker数降到2,或者接受略慢的启动时间,换用单worker。对考勤系统来说,4个worker其实是浪费——识别推理本身就是CPU密集操作,进程多了也不会让单个CPU跑得更好。

7. 一些可以继续扩展的玩法

系统跑通之后你很快就发现,它不止可以用于考勤。门禁、会议室预约确认、访客登记、甚至园区食堂刷脸吃饭,底层逻辑全都一样:识别出你是谁。把考勤接口抽象成通用的人脸认证服务,前端换一套UI就能复用。

我后面又做了两个小改动,效果都很好。一是给考勤记录加了照片存档功能,每次打卡保存一张现场照片,方便月底对账时人工抽查,防止有人故意遮挡或使用照片作弊。二是加了一个简单的“按日考勤异常报表”,每天下班后跑一个定时任务扫描当天的考勤记录,谁没打卡、谁迟到,自动汇总到Excel再发邮件给部门主管。

从技术角度说,这个系统的架构在功能上其实留有足够扩展性。你想加人脸识别测温功能,在识别流程前插入一个红外传感数据采集就行;你想加RESTful API给移动端调用,Flask自身提供了天然支持;你想把用户和数据从SQLite迁移到PostgreSQL,只需要改改数据库操作层。技术选型上的克制,反而给了项目最大的灵活性。

我个人在维护这套系统的过程中,最大的收获其实不是“我搭了个刷脸系统”,而是把一条数据从摄像头到屏幕的全链路给磨透了。你以为的“认脸”,链路上本质就是图像处理三件套加一个向量数据库加一层Web API,每一层都不难。你要是也正卡在“想用神经网络却不知从哪开始”的坎上,就从这套系统的注册和比对流程入手,逐段打印中间结果,亲手看一遍向量距离怎么从1.2变成0.1,比看多少教程都有用。

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

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

立即咨询