ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+Flask+OpenCV人脸签到系统:从零搭建到阈值调优

Python+Flask+OpenCV人脸签到系统:从零搭建到阈值调优 简介这份资源面向计算机相关专业的在校学生、教师及企业开发者提供一套基于Python、Flask与OpenCV深度学习的人脸识别签到系统完整实现可作为毕业设计、课程设计或项目立项演示的参考方案。压缩包共28个文件约101.47MB包含8个Python源码文件、7个HTML页面模板、4个数据文件以及SQLite数据库、配置文件、样式表、字体文件和说明文档等覆盖从人脸注册、识别签到到用户管理的核心模块。目前已有340人学习下载。资源内代码均经过测试运行成功读者可获取完整项目源码、数据集与详细文档理解Flask后端与OpenCV人脸识别模型的整合方式并在此基础上修改扩展功能适合具备一定Python基础、希望快速完成毕设或进阶学习人脸识别应用开发的读者。1. 从一张签到表说起PythonFlaskOpenCV 的人脸签到系统到底在做什么公司行政每个月末都要对着 Excel 核对考勤纸质签到表上字迹潦草、代签、漏签一场培训下来统计两小时。这个标题要解决的就是这件事用 Python 做后端逻辑Flask 把识别能力挂到网页上OpenCV 负责摄像头取流和人脸预处理深度学习模型完成人脸特征提取与比对最终形成一个能跑在局域网里的签到系统。它适合三类人正在找毕业设计题目的学生、想给内部做轻量考勤工具的后端工程师、以及刚学完 Python 想找一个完整项目练手的人。整套方案不依赖云服务一台带摄像头的普通电脑就能跑通数据集自己采几十张人脸就能验证闭环。下面按「先跑通最小链路再补工程细节最后处理翻车点」的顺序展开每一步都给出可复现的命令和参数。2. 技术选型与最小可跑链路为什么是 Flask 而不是 Django为什么是 OpenCV 而不是直接调摄像头 SDK2.1 四个组件的职责边界与选型理由先把职责分清楚后面排错才不会互相甩锅。OpenCV 负责三件事打开摄像头、把 BGR 帧转成模型要的 RGB、做人脸检测框的裁剪。深度学习模型负责把裁剪后的人脸图映射成一个 128 维或 512 维的特征向量。Flask 负责把「注册人脸」「发起签到」「查询记录」暴露成 HTTP 接口并渲染一个带 video 标签的页面。数据库用 SQLite 就够签到系统并发量低没必要上 MySQL。选 Flask 而不是 Django 的原因很实际这个项目不需要后台管理、不需要 ORM 迁移、不需要用户权限体系Flask 一个 app.py 加几个路由就能跑调试时改完代码自动重载对毕业设计这种「要讲清楚每一行」的场景更友好。选 OpenCV 而不是厂商 SDK是因为 SDK 通常绑定特定硬件换一台电脑就废了而 OpenCV 的 VideoCapture 在 Windows、Linux、macOS 上接口一致代码可移植。深度学习部分常见做法是用 face_recognition 库底层是 dlib 的 ResNet或者 InsightFace 的 ArcFace 模型。前者安装简单、文档多适合快速验证后者精度更高但依赖 onnxruntime部署时要注意版本。我一般先用 face_recognition 跑通确认链路没问题后再决定要不要换。2.2 环境搭建从零到能 import cv2 的最小命令先建虚拟环境避免污染系统 Python。以下命令在 Windows 和 Linux 下都适用macOS 把 python 换成 python3 即可。python -m venv venv # Windows 激活 venv\Scripts\activate # Linux/macOS 激活 source venv/bin/activate pip install flask opencv-python face_recognition numpy安装 face_recognition 时如果报 dlib 编译错误Windows 下需要先装 CMake 和 Visual Studio Build ToolsLinux 下执行sudo apt install cmake build-essential libopenblas-dev liblapack-dev。这是第一个高频翻车点后面避坑章节会展开。验证安装是否成功import cv2 import face_recognition import flask print(cv2.__version__) print(flask.__version__)如果 cv2 能打印版本号说明 OpenCV 装好了。注意 opencv-python 和 opencv-contrib-python 不要同时装否则会出现函数找不到的玄学问题。2.3 最小闭环打开摄像头、检测人脸、画框显示在写 Flask 之前先用一个独立脚本确认摄像头和检测模型能工作。这一步不涉及 Web纯本地验证。import cv2 # 0 表示默认摄像头如果有多摄像头可改成 1、2 cap cv2.VideoCapture(0) # 设置分辨率太高会导致帧率下降 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame cap.read() if not ret: print(读取帧失败检查摄像头是否被占用) break # OpenCV 默认 BGRface_recognition 需要 RGB rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 检测人脸位置返回 (top, right, bottom, left) 元组列表 locations face_recognition.face_locations(rgb, modelhog) for top, right, bottom, left in locations: cv2.rectangle(frame, (left, top), (right, bottom), (0, 255, 0), 2) cv2.imshow(face detect, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明face_locations的model参数有两个取值hog在 CPU 上快但小脸容易漏cnn精度高但需要 GPU 或等待时间较长。签到场景人脸离摄像头近hog足够。cv2.waitKey(1)里的 1 是毫秒数太小会导致窗口无响应太大画面卡顿1 到 30 之间按机器性能调。参数说明分辨率 640x480 是速度和清晰度的平衡点1280x720 会让每帧检测时间翻倍。如果摄像头读不到帧先检查是否被其他程序占用Windows 下可以在设备管理器里看摄像头状态。3. Flask 把识别能力挂到网页路由设计、视频流传输与签到接口3.1 三个核心路由注册、签到、记录查询Flask 这边不需要复杂设计三个路由覆盖全部功能。/register接收姓名和人脸图提取特征存库/checkin接收摄像头帧比对特征后写签到记录/records返回签到列表。数据库用 SQLite两张表users存姓名和特征向量records存姓名和时间戳。import sqlite3 import numpy as np import face_recognition from flask import Flask, request, jsonify, render_template app Flask(__name__) def get_db(): conn sqlite3.connect(checkin.db) conn.execute(CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, encoding BLOB NOT NULL)) conn.execute(CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, checkin_time TEXT NOT NULL)) return conn app.route(/register, methods[POST]) def register(): name request.form[name] file request.files[face] # 把上传的图片转成 numpy 数组 img face_recognition.load_image_file(file) encodings face_recognition.face_encodings(img) if len(encodings) 0: return jsonify({ok: False, msg: 未检测到人脸}) if len(encodings) 1: return jsonify({ok: False, msg: 图片中有多张人脸}) conn get_db() conn.execute(INSERT INTO users (name, encoding) VALUES (?, ?), (name, encodings[0].tobytes())) conn.commit() conn.close() return jsonify({ok: True})逻辑说明face_encodings返回的是一个列表每张脸对应一个 128 维向量。存库时用tobytes()转成二进制读出来用np.frombuffer还原。这里必须校验人脸数量0 张说明图片不合格多张说明用户传了合照两种情况都要拒绝否则后续比对会错乱。参数说明request.form[name]要求前端用 multipart/form-data 提交如果用 JSON 提交会取不到值。这是 Flask 新手最常见的类型错误后面避坑章节会讲怎么排查。3.2 视频流传输把摄像头帧推给浏览器浏览器不能直接读服务器摄像头所以要用 Flask 做一个 MJPEG 流。原理是服务端不断把 JPEG 帧拼成 multipart 响应前端用 img 标签的 src 指向这个路由。import cv2 from flask import Response def gen_frames(): cap cv2.VideoCapture(0) while True: success, frame cap.read() if not success: break # 编码成 JPEG质量 70 平衡带宽和清晰度 ret, buffer cv2.imencode(.jpg, frame, [cv2.IMWRITE_JPEG_QUALITY, 70]) if not ret: continue yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n buffer.tobytes() b\r\n) app.route(/video_feed) def video_feed(): return Response(gen_frames(), mimetypemultipart/x-mixed-replace; boundaryframe)逻辑说明multipart/x-mixed-replace告诉浏览器每收到一段就替换上一帧形成视频效果。boundaryframe必须和 yield 里的--frame一致否则浏览器解析失败页面一片空白。JPEG 质量 70 是经验值调到 90 画面更清晰但局域网带宽占用翻倍签到场景 70 够用。参数说明cv2.imencode第二个参数是质量范围 0 到 100。如果页面卡顿先降质量再降分辨率。注意gen_frames里的 cap 没有 release实际部署时要在生成器结束时释放或者用全局摄像头对象避免反复打开。3.3 签到比对阈值怎么设为什么 0.6 不是万能值签到接口拿到一帧后提取特征和库里所有用户比对距离小于阈值就判定为同一个人。app.route(/checkin, methods[POST]) def checkin(): file request.files[frame] img face_recognition.load_image_file(file) encodings face_recognition.face_encodings(img) if len(encodings) 0: return jsonify({ok: False, msg: 未检测到人脸}) conn get_db() rows conn.execute(SELECT name, encoding FROM users).fetchall() known [np.frombuffer(r[1], dtypenp.float64) for r in rows] names [r[0] for r in rows] # 默认阈值 0.6越小越严格 matches face_recognition.compare_faces(known, encodings[0], tolerance0.6) if True in matches: idx matches.index(True) name names[idx] conn.execute(INSERT INTO records (name, checkin_time) VALUES (?, datetime(now, localtime)), (name,)) conn.commit() conn.close() return jsonify({ok: True, name: name}) conn.close() return jsonify({ok: False, msg: 未匹配到用户})逻辑说明compare_faces内部计算欧氏距离小于 tolerance 返回 True。0.6 是 face_recognition 的默认值但在实际签到场景中光线变化大、摄像头质量差时同一个人可能距离到 0.65导致签到失败。我一般会先用一批测试图跑一遍统计同一个人的距离分布和不同人之间的最小距离取两者中间值。如果同人距离普遍在 0.5 以下异人距离在 0.8 以上阈值设 0.65 更稳。参数说明tolerance越小越严格误识率降低但拒识率升高。签到场景宁可让用户多试一次也不要让陌生人签到成功所以可以设 0.5。但如果是考勤场景员工排队签到拒识会导致体验差可以放宽到 0.65。这个值没有标准答案必须用自己采集的数据测。4. 数据集采集与模型微调几十张人脸怎么撑起一个可演示的系统4.1 采集脚本每个用户 20 张覆盖 5 种角度face_recognition 用的是预训练模型不需要自己训练但需要采集注册照。采集时要注意多样性否则用户换个角度就识别不了。import cv2 import os name input(输入姓名拼音: ) save_dir os.path.join(dataset, name) os.makedirs(save_dir, exist_okTrue) cap cv2.VideoCapture(0) count 0 while count 20: ret, frame cap.read() if not ret: break cv2.imshow(collect, frame) key cv2.waitKey(1) 0xFF if key ord(s): cv2.imwrite(os.path.join(save_dir, f{count}.jpg), frame) count 1 print(f已保存 {count}/20) elif key ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明按 s 保存按 q 退出。20 张的分配建议正面 5 张、左转 4 张、右转 4 张、抬头 3 张、低头 4 张。不要只采正面否则侧脸签到必失败。采集环境要和实际签到环境一致如果在办公室签到就在办公室采不要在家里的暖光灯下采完拿去冷白光环境用。参数说明保存格式用 jpg 即可face_recognition 内部会统一尺寸。如果采集时人脸太小检测不到让用户靠近摄像头保证人脸占画面三分之一以上。4.2 特征库构建批量提取并写入数据库采集完所有用户后用一个脚本批量注册避免手动一个个传。import os import sqlite3 import face_recognition import numpy as np conn sqlite3.connect(checkin.db) conn.execute(CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, encoding BLOB NOT NULL)) for name in os.listdir(dataset): user_dir os.path.join(dataset, name) if not os.path.isdir(user_dir): continue encodings [] for img_name in os.listdir(user_dir): img_path os.path.join(user_dir, img_name) img face_recognition.load_image_file(img_path) enc face_recognition.face_encodings(img) if len(enc) 1: encodings.append(enc[0]) if len(encodings) 0: print(f{name} 没有可用人脸跳过) continue # 取平均向量降低单张噪声影响 avg np.mean(encodings, axis0) conn.execute(INSERT INTO users (name, encoding) VALUES (?, ?), (name, avg.tobytes())) print(f{name} 注册成功使用 {len(encodings)} 张) conn.commit() conn.close()逻辑说明对每个用户的多张图提取特征后取平均比只用一张更稳。注意face_encodings返回的向量是 float64存库和读库要保持一致否则np.frombuffer解析出来是乱码。如果某个用户所有图都检测不到人脸说明采集时光线太暗或角度太偏需要重新采集。参数说明平均向量会损失一些角度信息如果用户侧脸和正脸差异大平均后可能两边都不像。替代方案是每个用户存多条特征比对时取最小距离。数据量小的时候多存几条更稳。4.3 用测试集验证阈值画一张距离分布图阈值不能拍脑袋定用采集的数据跑一遍看同一个人的距离和不同人之间的距离分布。import face_recognition import numpy as np import os # 加载所有特征 known {} for name in os.listdir(dataset): user_dir os.path.join(dataset, name) if not os.path.isdir(user_dir): continue encs [] for img_name in os.listdir(user_dir): img face_recognition.load_image_file(os.path.join(user_dir, img_name)) enc face_recognition.face_encodings(img) if len(enc) 1: encs.append(enc[0]) if encs: known[name] encs # 计算同人距离和异人距离 same_dist, diff_dist [], [] names list(known.keys()) for i, n1 in enumerate(names): for j, n2 in enumerate(names): for e1 in known[n1]: for e2 in known[n2]: d np.linalg.norm(e1 - e2) if i j: same_dist.append(d) else: diff_dist.append(d) print(f同人距离: 均值 {np.mean(same_dist):.3f}, 最大 {np.max(same_dist):.3f}) print(f异人距离: 均值 {np.mean(diff_dist):.3f}, 最小 {np.min(diff_dist):.3f})逻辑说明同人距离最大值和异人距离最小值之间的区间就是安全阈值区间。如果同人最大 0.55异人最小 0.75阈值设 0.65 最稳。如果两个区间重叠说明采集数据质量不够需要增加角度多样性或改善光照。参数说明np.linalg.norm计算欧氏距离和 face_recognition 内部一致。如果同人距离均值超过 0.6说明采集时人脸对齐不好检查图片是否模糊或角度过大。5. 避坑与排查那些让签到系统当场翻车的细节5.1 现象页面视频流一直转圈控制台报 ERR_INCOMPLETE_CHUNKED_ENCODING原因Flask 开发服务器默认单线程视频流路由一直占用连接其他请求被阻塞。另外gen_frames里没有异常处理摄像头读帧失败时生成器直接退出浏览器收到不完整响应。解决启动时加threadedTrue命令改成app.run(host0.0.0.0, port5000, threadedTrue)。同时在gen_frames里加 try/except读帧失败时 yield 一张占位图而不是 break。生产环境用 waitress 或 gunicorn 替代开发服务器。5.2 现象注册接口报 400日志显示 KeyError: name原因前端提交时 Content-Type 是 application/json而 Flask 的request.form只解析 multipart/form-data 和 urlencoded。这是 Flask 新手最常踩的类型坑。解决要么前端改成 FormData 提交要么后端用request.get_json()取。排查时在路由第一行加print(request.content_type)一眼就能看出问题。如果用的是 axios默认 JSON需要手动构造 FormData。5.3 现象同一个人有时能签到有时提示未匹配原因摄像头自动曝光导致画面亮度波动人脸特征向量跟着变。另外用户站位远近不同人脸在画面中的像素数差异大检测框裁剪后缩放细节丢失。解决在检测前做直方图均衡化cv2.equalizeHist对灰度图处理后再转回 RGB。同时固定摄像头位置和用户站位距离地上贴个脚印标记。如果还是不稳把 tolerance 从 0.5 放宽到 0.6但要用异人测试确认不会误识。5.4 现象dlib 安装报错CMake 找不到编译器原因face_recognition 依赖 dlibdlib 是 C 库pip 安装时需要本地编译。Windows 下没有 Visual Studio Build Tools 就会失败Linux 下缺 build-essential 也会失败。解决Windows 先装 Visual Studio Build Tools勾选「使用 C 的桌面开发」再装 CMake 并加入 PATH。Linux 执行sudo apt install cmake build-essential libopenblas-dev liblapack-dev。如果还是失败用 conda 安装conda install -c conda-forge dlibconda 有预编译包省去编译步骤。5.5 现象签到记录时间比实际时间晚 8 小时原因SQLite 的datetime(now)返回 UTC 时间没有转本地时区。解决用datetime(now, localtime)或者在 Python 层用datetime.now().strftime(%Y-%m-%d %H:%M:%S)生成时间字符串再插入。注意服务器时区设置Linux 下用timedatectl确认。6. 进阶技巧把签到系统从能跑变成好用6.1 用多帧投票降低误识率单帧比对容易受瞬时噪声影响改成连续取 5 帧每帧独立比对超过 3 帧匹配同一个人才判定签到成功。这个逻辑在/checkin里加一个循环前端每隔 200 毫秒传一帧后端累积结果。代价是签到速度变慢但误识率显著下降。实测在光线不稳的环境下单帧误识率约 3%5 帧投票后降到 0.5% 以下。6.2 活体检测的轻量替代方案严格活体检测需要红外或 3D 结构光普通摄像头做不了。但可以用一个简单技巧要求用户在签到前眨眼或转头前端连续采集后端检测人脸关键点变化。face_recognition 不提供关键点需要换用 dlib 的 shape_predictor 或 mediapipe。如果只是防照片可以在签到页面加一个随机动作提示比如「请向左转头」用户照做时采集静态照片无法完成。6.3 用 Flask 蓝图拆分代码方便答辩时讲结构单文件 app.py 写到 500 行后很难维护。用 Blueprint 拆成auth.py、checkin.py、records.py每个蓝图负责一组路由。答辩时老师问「你的模块怎么划分的」能说清楚职责边界比堆功能更加分。拆分后主文件只保留 app 初始化和数据库连接代码可读性提升明显。# checkin.py from flask import Blueprint, request, jsonify checkin_bp Blueprint(checkin, __name__) checkin_bp.route(/checkin, methods[POST]) def checkin(): # 具体逻辑 pass # app.py from checkin import checkin_bp app.register_blueprint(checkin_bp)逻辑说明蓝图让路由按业务分组URL 前缀可以统一加比如url_prefix/api。参数说明蓝图注册时如果指定 url_prefix蓝图内的路由会自动带上前缀前端请求路径要对应修改。6.4 验证方法用混淆矩阵确认系统真实水平不要只看「能识别」要统计。准备 10 个用户每人 5 张测试图跑一遍签到记录四种结果本人签到成功TP、本人签到失败FN、陌生人签到成功FP、陌生人被拒绝TN。TP/(TPFN) 是召回率TN/(TNFP) 是特异度。签到系统要求召回率高于 95%特异度高于 99%。如果特异度低说明阈值太松调小 tolerance如果召回率低说明采集数据不够或光照差异大补采数据。我自己的习惯是每次改完阈值或换模型都跑一遍这个矩阵把数字记在 README 里。答辩时老师问「你怎么证明系统可靠」直接翻出这张表比说「我试了能识别」有说服力得多。这套方案从环境搭建到阈值调优最花时间的不是写代码而是采集数据和反复测试。希望帮到你。本文还有配套的精品资源点击获取
返回列表