基于Python+Flask+OpenCV的人脸识别签到系统搭建与调参实战
简介这是一套基于Python、Flask与OpenCV深度学习技术的人脸识别签到系统完整源码包面向计算机相关专业学生、毕业设计开发者以及希望快速上手人脸识别项目的技术人员可直接用于课程设计、毕业设计或项目初期演示覆盖从算法实现到系统部署的完整流程。压缩包共28个文件包含Python后端核心逻辑、Flask页面模板、OpenCV识别模型数据、SQLite数据库、环境依赖说明与使用文档总大小约101.47MB所有代码均经过测试并成功运行。目前已有341人在线学习参考具备较好的实用性与参考价值。资源内附完整源码、数据集和详细设计文档目录按用户注册、人脸特征提取、签到记录查询等模块清晰组织便于理解系统架构与算法流程代码具备良好的扩展性可在现有基础上增加考勤统计、权限管理、异常预警等功能同时也能为论文撰写和答辩演示提供充分支撑。1. 先把人脸识别签到系统拆开这是一套能落地的闭环不是识别 demo拿到这份基于 PythonFlaskOpenCV 深度学习的人脸识别签到系统资源时我第一反应是“又一个 OpenCV 识别 demo”。翻完 faceRegister-master 目录才发现没那么简单里面有 app.py、api.py、functions.py、migrations 数据库迁移、templates 页面模板、faceRecognitonModels 模型目录还带了一份现成的 data.sqlite。它不是只给你一个“能框住人脸”的演示而是把“人脸检测 → 特征提取 → 身份比对 → 签到落库 → 前端展示”整条链路都串好了。对正在做毕设、课设或者想给部门做内部刷脸签到的开发者来说这个资源值钱的地方就在于你不需要从零拼装照着它的架构改就行。2. 从摄像头到特征向量OpenCV 检测与深度学习特征提取的落地分工人脸识别签到系统的第一步不是“认人”而是“找到脸在哪”。这一步通常由 OpenCV 负责之后再交给深度学习模型提特征。这套资源里 functions.py 和 faceRecognitonModels 目录对应的就是这个过程。把这两件事拆开看整个系统的技术路线就清晰了。2.1 先做人脸检测Haar 级联与 OpenCV DNN 检测器怎么选打开 functions.py里面大概率有一段类似下面的代码用 OpenCV 的级联分类器检测人脸。这是最经典、也是复现成本最低的做法import cv2 # 常见做法加载 OpenCV 自带的 Haar 级联分类器 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) def detect_faces(frame): # 转灰度级联分类器只处理单通道灰度图速度更快 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 参数说明 # scaleFactor1.1 每次搜索窗口缩小 10%越小检测越细但越慢 # minNeighbors5 每个候选框需要至少 5 个邻居确认越大误检越少 # minSize(64,64) 过滤小于 64x64 的候选框避免把噪声当人脸 faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(64, 64) ) # 返回格式[[x, y, w, h], ...] 的列表 return faces这段代码的返回值是矩形坐标列表。签到系统拿到这些坐标后会从原图上把对应区域裁出来作为后续特征提取的输入。选型时要注意Haar 级联适合正面人脸、光线稳定的场景比如室内签到如果你的摄像头角度刁钻或者人脸经常侧转我会建议改成 OpenCV 的 DNN 人脸检测器用cv2.dnn.readNet加载 SSD 模型召回率会好很多。有人问过 halcon 和 OpenCV 怎么选halcon 商用且封闭毕设和中小型项目直接用 OpenCV 开源链路就够了这套资源走的也是 OpenCV。2.2 深度学习模型提取特征faceRecognitonModels 里放着什么检测出人脸只是开始。签到系统要回答的是“这张脸是谁”需要把裁出来的人脸图像转换成特征向量再去数据库里比对。faceRecognitonModels 目录里放的就是负责这一步的深度学习模型文件可能是训练好的权重也可能是预训练的骨干网络。常见做法是这样用的import numpy as np # 加载训练好的模型注意文件名要跟目录里实际文件一致 from tensorflow import keras face_model keras.models.load_model(faceRecognitonModels/你的模型文件.h5) def get_feature(face_crop): # 1. 统一尺寸签到场景常用 112x112 或 160x160 face_resized cv2.resize(face_crop, (112, 112)) # 2. OpenCV 默认是 BGR模型训练时通常用 RGB必须转通道 face_rgb cv2.cvtColor(face_resized, cv2.COLOR_BGR2RGB) # 3. 加 batch 维度并做归一化到 [0, 1] input_tensor np.expand_dims(face_rgb.astype(np.float32) / 255.0, axis0) # 4. 提取特征向量常见长度是 128 或 512 维 feature face_model.predict(input_tensor)[0] # 5. 归一化后续算距离时不同维度才有可比性 return feature / np.linalg.norm(feature)get_feature返回的是一个归一化后的向量128 维或 512 维具体维度取决于模型结构。这个向量就是“这张脸的数字指纹”。如果发现识别效果差优先检查第 2 步的通道顺序和第 5 步有没有做归一化这两处是最容易改错的地方。人脸识别门禁机这类产品里核心逻辑也是这一套检测 特征提取 比对只是模型和硬件更工程化。2.3 比对逻辑距离度量、阈值与“能不能认出陌生人”特征提取完毕签到系统在 functions.py 或 api.py 里会做最后一步——比对。比对不靠“两张脸看起来像不像”而是靠特征向量的距离。这个认知很重要很多新手在这块翻车以为识别是分类问题实际上签到场景是“验证问题”def compare_features(feature_a, feature_b): # 欧氏距离值越小越相似 dist np.linalg.norm(feature_a - feature_b) # 映射成 0~1 的分数方便前端展示 score max(0, 1 - dist / 2.0) return score # 如果项目用的是余弦相似度则是 # sim np.dot(feature_a, feature_b)这里的核心是“阈值”的选取。阈值设得太大陌生人也被认成已注册用户签到系统形同虚设设得太小本人光线偏一点就被拒签天天要手动补卡。主流做法是收集一批“本人签到”和“陌生人路过”的特征对画两个分数分布取中间值偏严的位置。ArcFace 这类算法在公开数据集上动辄 99% 的准确率但那是在固定阈值下的离线指标落到实际场景光线、角度、摄像头分辨率都会让分数漂移阈值必须自己标定。这套资源的 api.py 里一般会有类似/api/recognize的接口接收图片返回“最匹配的用户名 相似度分数”。前端拿到分数后再决定是显示“签到成功”还是提示“未识别”。这一步把“识别”变成了“业务动作”也是这个资源比纯模型 demo 完整的地方。3. Flask 与数据库的闭合链路路由、迁移、模板怎么串起来识别算法只是内核签到系统能对外服务靠的是 Flask 这一层。这套资源里的 app.py、models、migrations、templates 组成了一个标准的 Flask 工程。看懂了这套骨架你不仅能复现还能往里加新功能。3.1 models、migrations 与 data.sqlite从数据库迁移开始看先看 models 目录里面定义了数据库表结构。常见的设计是两张表用户表和签到记录表对应签到系统的“注册信息”和“考勤流水”。典型代码如下from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) # 特征向量转成字符串存入数据库避免每次启动都重新提取 face_feature db.Column(db.Text, nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow) class Attendance(db.Model): __tablename__ attendance id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id)) check_time db.Column(db.DateTime, defaultdatetime.utcnow)face_feature这个字段我特别说明一下它存的是特征向量被序列化后的文本通常是 Python 列表或 numpy 数组转成的字符串。签到识别时系统把摄像头拍到的人脸特征提取出来跟表里所有 feature 逐个算距离取最高分的那个比对。这样设计的坑是如果用户多了全表扫描会变慢但在毕设和小规模场景几十到几百人下完全够用。migrations 目录是 Alembic 的迁移脚本配合alembic.ini和env.py工作。数据文件 data.sqlite 里已经有初始化好的内容可以直接跑。我建议复现时先备份它再走一遍迁移流程操作命令说明初始化迁移环境flask db init生成 migrations 目录资源里已带可跳过生成迁移脚本flask db migrate -m init tables自动比对 models 和当前库结构应用迁移flask db upgrade head让数据库更新到最新版本提示如果跑flask db upgrade报错多半是 data.sqlite 里已有旧结构先备份再删掉重建别硬修。script.py.mako 是 Alembic 生成迁移脚本时的模板文件一般不用动哪天你需要自定义迁移才去改它。初学阶段碰都不要碰。3.2 app.py 与 api.py 的路由分工页面和接口各管一摊Flask 项目里最容易让人迷糊的是“到底哪个文件处理请求”。这套资源的分工很明确app.py 管页面路由api.py 管接口。app.py 里的路由大致对应 templates 目录下的模板路由方法渲染模板作用/或/indexGET/POSTindex.html签到主页面识别成功后显示用户信息/loginGET/POSTlogin.html管理员登录/add_userGET/POSTadd_user.html录入新用户并采集人脸特征/edit_userGET/POSTedit_user.html修改或删除用户典型页面路由代码from flask import render_template, request app.route(/index, methods[GET, POST]) def index(): if request.method POST: # 前端上传一张图片或摄像头帧后端调 functions 识别 frame request.files.get(image) user recognize_user(frame) # 内部检测 - 提特征 - 比对 - 落库 return render_template(index.html, useruser) return render_template(index.html)这里的recognize_user是封装好的核心函数它内部做了三步调用 detect_faces 找到人脸调用 get_feature 提取特征再遍历用户表算距离并判断阈值。识别成功后写一条 Attendance 记录完成签到动作。api.py 则暴露纯 JSON 接口比如/api/recognize接收图片返回“用户名 分数 签到时间”方便小程序或 App 接入。有人问这个项目为什么用 Flask 而不是 FastAPI。这个场景是“页面 接口”一把抓Flask 的render_template直接渲染后端页面模板继承也成熟FastAPI 更适合纯 API 服务。如果以后要拆前端把 api.py 单独剥离再迁到 FastAPI 也不晚眼下没必要折腾。3.3 前端模板与中文渲染templates 里的页面流和 simsun.ttc 的用处templates 目录下五个 HTML 文件构成了完整操作流login.html 登录管理员 → add_user.html 录入新用户 → index.html 日常签到 → edit_user.html 管理用户 → base.html 提供统一的页面外壳404/500 页面做兜底。static/styles.css 控制整体样式。这是非常典型的“后台管理 签到终端”结构。资源里 font 目录和 fontToImg.py、simsun.ttc初看很突兀实际是用来解决 OpenCV 一个经典毛病cv2.putText写不了中文。它在画矩形框或者往图片上写用户名时遇到中文就变成问号或乱码。所以项目里引入了 PIL 加载系统的宋体字体来画字from PIL import Image, ImageDraw, ImageFont import numpy as np def draw_text_cn(img_bgr, text, pos): # OpenCV 图像转成 PIL 图片才能渲染中文字体 img_rgb cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) pil_img Image.fromarray(img_rgb) draw ImageDraw.Draw(pil_img) # 加载项目中自带的宋体注意路径别写错 font ImageFont.truetype(font/simsun.ttc, 28) # pos 是 (x, y) 画字位置 draw.text(pos, text, fontfont, fill(0, 255, 0)) # 画完再转回 BGR 给 OpenCV 用 return cv2.cvtColor(np.asarray(pil_img), cv2.COLOR_RGB2BGR)fontToImg.py 这个脚本名我推测就是“把文字渲染到图片”的辅助工具比如批量生成带中文姓名的测试图或者把特征可视化输出成图片配合 simsun.ttc 使用。这条链路提醒了我凡是 OpenCV 项目里出现中文字体文件八成都是 putText 中文乱码的补救方案排错时有印象就够了。4. 复现与排查环境、中文乱码、阈值调参与模型路径的坑这节内容来自我拆这种项目时的血泪积累每条都是现场翻车记录。建议你在复现这套人脸识别签到系统时一条一条对照走能省下不少时间。4.1 No module named cv2Python 环境与 OpenCV 版本的第一道坎现象python app.py直接报ModuleNotFoundError: No module named cv2或者 import cv2 时弹窗提示找不到 DLL。原因机器上装的是官方 Python没有安装 OpenCV 包或者是装了 opencv-contrib-python 和 opencv-python 两个版本把环境搞混了。解决重新建一个干净的虚拟环境按顺序安装python -m venv venv source venv/bin/activate # Windows 是 venv\Scripts\activate pip install -r requirements.txt pip install opencv-python python -c import cv2; print(cv2.__version__)requirements.txt是项目打包好的依赖清单第一次复现直接用它装别自己一个个手动装。装了 opencv-python 之后再用-c参数验证版本能跑通再往后走。如果 cv2 在 import 阶段报 DLL 错误多半是 numpy 版本不兼容pip install numpy --upgrade能解决大多数情况。4.2 画框正常但中文全变问号OpenCV 的 putText 不支持中文现象摄像头实时画面的矩形框跟着人脸框住了但姓名显示成“????”或者空白方块。原因cv2.putText内部用的是 Hershey 字体库只支持 ASCII 字符中文直接画不出来。这个方法本身就不是给多语言场景准备的。解决用第 3.3 节讲的 PIL simsun.ttc 方案替换。另外一个隐藏坑是字体文件路径直接写simsun.ttc时如果启动命令不在项目根目录下执行就会报字体文件找不到。建议用绝对路径拼import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) FONT_PATH os.path.join(BASE_DIR, font, simsun.ttc)从那以后我每次在这种项目里看到字体文件都会条件反射地检查路径是不是死了。4.3 模型文件明明在却加载失败工作目录与相对路径的坑现象faceRecognitonModels 目录在项目里躺着但程序启动时报FileNotFoundError: [Errno 2] ... faceRecognitonModels/xxx.h5或者加载权重时ValueError: No such file or directory。原因Python 里用相对路径faceRecognitonModels/xxx.h5加载文件时是相对于“当前工作目录”去找的。你用python /home/user/faceRegister/app.py启动和cd /home/user/faceRegister python app.py启动工作目录不一样结果完全不同。解决所有涉及模型、字体、配置文件加载的地方都改成基于文件自身位置拼路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) MODEL_PATH os.path.join(BASE_DIR, faceRecognitonModels, 你的模型文件.h5) model keras.models.load_model(MODEL_PATH)__file__是指当前这个 .py 文件所在路径这是最稳定的锚点。这个坑在 test.py 里同样容易出现因为测试脚本往往放在项目根目录直接跑一换目录就露馅。4.4 陌生人被“认领”签到阈值过宽和特征比对作弊现象一个完全没注册过的人走到摄像头前系统提示“签到成功王某某”而且分数还挺高比如 0.82。原因阈值设得太宽松。很多项目为了演示效果把识别阈值压到 0.5 以下导致任何一张脸都能匹配上分数最高的人。另外如果比对前没有做特征归一化不同用户的特征向量模长差异很大欧氏距离的结果就是无意义的。解决跑一轮离线校准把“真本人”和“陌生人”的分数分布统计出来再定阈值。简单逻辑import numpy as np # 已知本人特征列表和注册特征列表 scores_same [compare_features(f, registered) for f in my_features] scores_diff [compare_features(f, registered) for f in stranger_features] # 取本人最低分和陌生人最高分的中间点作为初始阈值 threshold (min(scores_same) max(scores_diff)) / 2 print(建议阈值:, threshold)拿到的阈值先测三天再微调一次。注意不要贪高阈值设到 0.9 以上光线偏一点人就签不上补卡成本更高。4.5 Alembic 迁移版本对不上升级报错或表结构对不上现象flask db upgrade报Cant locate revision identified by xxx或者启动项目时提示sqlalchemy.exc.InvalidRequestError新增字段死活写不进去。原因资源自带的 data.sqlite 里的 schema 版本和 migrations/versions 里记录的迁移链不一致。最常见的是别人提交代码时把自己的迁移历史也带进了 zip导致版本号错位。解决最稳的办法是不保留旧库状态重新初始化# 备份旧数据重要里面有调试期加的用户 sqlite3 data.sqlite .backup data_bak.sqlite # 删除旧库后重新迁移 rm data.sqlite flask db upgrade head如果是想保留已录入的用户就在迁移前用备份库跑通如果只是复现功能直接重建库再通过 add_user 页面重新录入两个测试人脸即可。这种迁移错位的问题在压缩包类资源里非常常见因为原始作者的本地环境和你的环境几乎不可能完全一致。5. 让签到系统更稳离线批量造样本与识别阈值校准的小技巧上面把系统的坑都填平了最后给你一个改变复现效率的小技巧集。很多从这套项目起步的开发者第一步是手动跑 add_user 页面录入自己一张照片就开始试。这么做不是不行但一张样本搞不定阈值调参。我自己的习惯是先量产测试样本再校准阈值最后才接摄像头。5.1 用 OpenCV 批量裁人脸造训练样本把手机或相机里的正面照全部丢进一个文件夹用 already 写好的检测函数批量裁脸import os import cv2 input_dir own_photos # 放你自己的照片多角度、多光线 output_dir face_samples # 裁出的人脸统一放这里 os.makedirs(output_dir, exist_okTrue) # detect_faces 是第 2.1 节写好的函数 for img_name in os.listdir(input_dir): img cv2.imread(os.path.join(input_dir, img_name)) if img is None: continue for i, (x, y, w, h) in enumerate(detect_faces(img)): # 在检测框基础上略微外扩避免头发和下颌被截断 margin int(0.1 * w) x0, y0 max(0, x - margin), max(0, y - margin) x1, y1 min(img.shape[1], x w margin), min(img.shape[0], y h margin) face img[y0:y1, x0:x1] face cv2.resize(face, (112, 112)) cv2.imwrite(os.path.join(output_dir, f{img_name[:-4]}_{i}.jpg), face)这里我习惯加一个margin外扩操作。检测框通常贴着脸直接把框裁出来会丢失耳朵和发际线信息特征提取时反而不好。外扩 10% 再把尺寸统一到 112x112和模型训练时的输入分布更接近。批量样本造完你可以用这几十张脸逐一跑get_feature再和 add_user 注册的特征做比对把分数分布打印出来。这个过程逼着你验证“检测会不会漏”“特征提取稳不稳”而不是等真人站到摄像头前才发现问题。5.2 阈值校准用自拍照片跑一轮离线测试再上真实摄像头我自己固定用这一套流程给签到系统定阈值以后你拿到任何一套刷脸签到代码都可以照抄把每个测试对象的脸裁出 10 张以上7 张用来模拟真实签到正面、左右侧、戴不戴眼镜3 张用来当“陌生人负样本”分开存。用get_feature提取所有特征两两配对算分数分成“同人分数”和“异人分数”两个列表。统计两组的最小值、最大值、均值。合理阈值一般落在 “同人最低分” 和 “异人最高分” 的中间再往同人方向压一点。测试时如果出现误判把误判的图片存到一个 debug 目录复现后看是光照问题还是检测框偏移。这套方法特别适合交作业前自测老师现场拍一张照片分数能稳定在阈值以上演示就不翻车。从那以后我每拿到一个刷脸签到项目都会强制走一遍“批量裁脸 → 离线打分 → 定阈值”这三步再花 10 分钟把模型和字体路径改成绝对路径。这些改动看起来不起眼但能把一个“只在原作者电脑上能跑”的资源变成“在谁电脑上都能跑”的工具。希望帮到你。本文还有配套的精品资源点击获取