简介:这是一套基于Python、Flask与OpenCV深度学习技术的人脸识别签到系统完整源码包,面向计算机相关专业学生、毕业设计开发者以及希望快速上手人脸识别项目的技术人员,可直接用于课程设计、毕业设计或项目初期演示,覆盖从算法实现到系统部署的完整流程。压缩包共28个文件,包含Python后端核心逻辑、Flask页面模板、OpenCV识别模型数据、SQLite数据库、环境依赖说明与使用文档,总大小约101.47MB,所有代码均经过测试并成功运行。目前已有341人在线学习参考,具备较好的实用性与参考价值。资源内附完整源码、数据集和详细设计文档,目录按用户注册、人脸特征提取、签到记录查询等模块清晰组织,便于理解系统架构与算法流程;代码具备良好的扩展性,可在现有基础上增加考勤统计、权限管理、异常预警等功能,同时也能为论文撰写和答辩演示提供充分支撑。
1. 先把人脸识别签到系统拆开:这是一套能落地的闭环,不是识别 demo
拿到这份基于 Python+Flask+OpenCV 深度学习的人脸识别签到系统资源时,我第一反应是“又一个 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) # 参数说明: # scaleFactor=1.1 每次搜索窗口缩小 10%,越小检测越细但越慢 # minNeighbors=5 每个候选框需要至少 5 个邻居确认,越大误检越少 # minSize=(64,64) 过滤小于 64x64 的候选框,避免把噪声当人脸 faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, 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, axis=0) # 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_key=True) username = db.Column(db.String(64), unique=True, nullable=False) # 特征向量转成字符串存入数据库,避免每次启动都重新提取 face_feature = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) class Attendance(db.Model): __tablename__ = "attendance" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("user.id")) check_time = db.Column(db.DateTime, default=datetime.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 目录下的模板:
| 路由 | 方法 | 渲染模板 | 作用 |
|---|---|---|---|
/或/index | GET/POST | index.html | 签到主页面,识别成功后显示用户信息 |
/login | GET/POST | login.html | 管理员登录 |
/add_user | GET/POST | add_user.html | 录入新用户并采集人脸特征 |
/edit_user | GET/POST | edit_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", user=user) 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, font=font, 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 'cv2':Python 环境与 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报Can't 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_ok=True) # 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 分钟把模型和字体路径改成绝对路径。这些改动看起来不起眼,但能把一个“只在原作者电脑上能跑”的资源,变成“在谁电脑上都能跑”的工具。希望帮到你。
本文还有配套的精品资源,点击获取