☰
Python+Flask+OpenCV人脸签到系统:从零搭建到阈值调优
2026/9/26 7:41:04 网站建设 项目流程

简介:这份资源面向计算机相关专业的在校学生、教师及企业开发者,提供一套基于Python、Flask与OpenCV深度学习的人脸识别签到系统完整实现,可作为毕业设计、课程设计或项目立项演示的参考方案。压缩包共28个文件,约101.47MB,包含8个Python源码文件、7个HTML页面模板、4个数据文件,以及SQLite数据库、配置文件、样式表、字体文件和说明文档等,覆盖从人脸注册、识别签到到用户管理的核心模块。目前已有340人学习下载。资源内代码均经过测试运行成功,读者可获取完整项目源码、数据集与详细文档,理解Flask后端与OpenCV人脸识别模型的整合方式,并在此基础上修改扩展功能,适合具备一定Python基础、希望快速完成毕设或进阶学习人脸识别应用开发的读者。

1. 从一张签到表说起:Python+Flask+OpenCV 的人脸签到系统到底在做什么

公司行政每个月末都要对着 Excel 核对考勤,纸质签到表上字迹潦草、代签、漏签,一场培训下来统计两小时。这个标题要解决的就是这件事:用 Python 做后端逻辑,Flask 把识别能力挂到网页上,OpenCV 负责摄像头取流和人脸预处理,深度学习模型完成人脸特征提取与比对,最终形成一个能跑在局域网里的签到系统。它适合三类人:正在找毕业设计题目的学生、想给内部做轻量考勤工具的后端工程师、以及刚学完 Python 想找一个完整项目练手的人。整套方案不依赖云服务,一台带摄像头的普通电脑就能跑通,数据集自己采几十张人脸就能验证闭环。下面按「先跑通最小链路,再补工程细节,最后处理翻车点」的顺序展开,每一步都给出可复现的命令和参数。

2. 技术选型与最小可跑链路:为什么是 Flask 而不是 Django,为什么是 OpenCV 而不是直接调摄像头 SDK

2.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 Tools,Linux 下执行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 默认 BGR,face_recognition 需要 RGB rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 检测人脸位置,返回 (top, right, bottom, left) 元组列表 locations = face_recognition.face_locations(rgb, model="hog") 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" b"Content-Type: image/jpeg\r\n\r\n" + buffer.tobytes() + b"\r\n") @app.route("/video_feed") def video_feed(): return Response(gen_frames(), mimetype="multipart/x-mixed-replace; boundary=frame")

逻辑说明:multipart/x-mixed-replace告诉浏览器每收到一段就替换上一帧,形成视频效果。boundary=frame必须和 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], dtype=np.float64) for r in rows] names = [r[0] for r in rows] # 默认阈值 0.6,越小越严格 matches = face_recognition.compare_faces(known, encodings[0], tolerance=0.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_ok=True) 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, axis=0) 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里没有异常处理,摄像头读帧失败时生成器直接退出,浏览器收到不完整响应。

解决:启动时加threaded=True,命令改成app.run(host="0.0.0.0", port=5000, threaded=True)。同时在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 依赖 dlib,dlib 是 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 dlib,conda 有预编译包,省去编译步骤。

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/(TP+FN) 是召回率,TN/(TN+FP) 是特异度。签到系统要求召回率高于 95%,特异度高于 99%。如果特异度低,说明阈值太松,调小 tolerance;如果召回率低,说明采集数据不够或光照差异大,补采数据。

我自己的习惯是每次改完阈值或换模型,都跑一遍这个矩阵,把数字记在 README 里。答辩时老师问「你怎么证明系统可靠」,直接翻出这张表,比说「我试了能识别」有说服力得多。这套方案从环境搭建到阈值调优,最花时间的不是写代码,而是采集数据和反复测试。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询