基于OpenCV与LBPH的人脸识别系统课程设计全解析
2026/9/12 2:32:40 网站建设 项目流程

简介:面向机器视觉课程及期末大作业场景,这套基于Python与OpenCV的人脸识别项目提供了完整的源代码与文档说明,开发者可由此了解人脸数据采集、模型训练到实时识别的全流程,代码内注释丰富,对新手较为友好。压缩包共9个文件,包含3个Python脚本(负责检测、训练与数据采集)、OpenCV级联检测器的XML配置、训练生成的YML模型、SQLite风格的DB人脸数据库、Markdown说明文档等,整体体积仅1.03MB,部署简单,适合本地快速跑通。目前已有524人学习下载,反馈良好;项目结构清晰,系统功能完善,界面直观易用,既可作为课程设计或期末大作业的参考方案,也可作为人脸识别入门的练习素材。随包附带的配置文件与说明文档,能帮助使用者梳理OpenCV人脸识别的关键步骤与参数,方便二次开发与扩展。

1. 为什么人脸识别大作业要自己写一遍而不是直接调现成接口

不少同学把OpenCV官方的人脸识别示例改个颜色就交了,答辩时被问到“训练的模型文件是什么格式”“置信度多少算通过”就答不上来。其实机器视觉课的核心并不是让你调通一个云服务,而是理解图像从像素到特征的完整链路。这个课程设计资源包含datasetCreator.pytrainer.pydetector.pyrecognizer.py等几个脚本,配合FaceBase.dbtrainingData.yml,构成了一个不依赖外网、离线可跑的人脸识别闭环,适合期末大作业、课程设计,也适合想真正搞懂Haar级联、LBPH和SQLite记录的新手。下面我按数据采集、模型训练、推理识别、数据库管理四层拆解,每个文件怎么改、参数怎么调、答辩会问哪里都会标出来。

2. 人脸数据采集与预处理:datasetCreator.py 的关键细节

2.1 初始化级联分类器与摄像头

先看采集端代码:

import cv2 import os cascade_path = "haarcascade_frontalface_default.xml" face_cascade = cv2.CascadeClassifier(cascade_path) cap = cv2.VideoCapture(0) if not cap.isOpened(): raise IOError("摄像头无法打开,请检查设备索引或驱动")

这里有两处容易踩坑。第一,CascadeClassifier读取的是OpenCV自带的人脸检测模型文件,路径最好是绝对路径或放在项目根目录,否则在PyCharm里能跑、命令行一运行就报找不到文件。第二,VideoCapture(0)表示系统默认摄像头,在笔记本上通常是内置摄像头;如果接外接USB摄像头或工业相机,设备索引会变成1或2。机器视觉检测场景里,有时候需要同时读取多个通道,那就用VideoCapture(0)VideoCapture(1)分别打开。

2.2 灰度化与直方图均衡化的参数选择

代码:

ret, frame = cap.read() if not ret: return gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray_eq = cv2.equalizeHist(gray)

灰度化是为了把三通道彩图压缩成单通道亮度图,Haar特征本身只依赖亮度差,颜色信息反而会引入噪声。equalizeHist做的是全局直方图均衡化,它把灰度分布拉伸到接近均匀,解决背光或半边脸阴影导致的人脸对比度过低问题。这个函数输入必须是uint8单通道,输出尺寸和输入一致,没有额外参数。很多人把这一步省略,结果在教室日光灯环境下检测率掉到一半以下,所以不要跳。另一个细节是:均衡化只用于检测,实际保存人脸块时用原始gray而不是gray_eq,这样可以避免训练数据和检测阶段的像素强度分布不一致,影响LBPH直方图匹配。

2.3 人脸框筛选与ROI截取

faces = face_cascade.detectMultiScale( gray_eq, scaleFactor=1.1, minNeighbors=5, minSize=(64, 64) ) if len(faces) != 1: continue for (x, y, w, h) in faces: roi = gray[y:y+h, x:x+w] roi_resized = cv2.resize(roi, (200, 200)) cv2.imwrite(f"data/user_{user_id}/{count}.jpg", roi_resized)

detectMultiScale是Haar检测的核心函数,四个参数直接影响误检率和漏检率:scaleFactor表示每次缩放倍率,1.1代表每次缩小10%,值越小计算越慢但检测越全;minNeighbors表示一个候选框周围必须存在多少个近邻框才保留,太小会误检,太大会漏检,5是课程设计场景的折中值;minSize=(64, 64)过滤掉小于该尺寸的区域,避免远处小脸或衣服纹理被当成脸。代码里限制len(faces) != 1才采集,能有效避免电视、海报等背景人脸混进样本导致标签混乱。保存前resize到200×200,统一尺寸可以减小特征向量在空间上的漂移,LBPH本身对缩放有一定容忍度,但统一的输入在计算局部二值模式时更稳定。

2.4 样本数量与目录结构规划

采集前先创建目录结构,常见做法是:

data/ user_0/1.jpg user_0/2.jpg user_1/1.jpg

其中user_0对应数据库里一个人,user_1对应另一个人。这里有个重要的索引约定:标签ID必须是整数,从0开始连续,否则recognizer读取trainingData.yml时会报标签无法解析。样本数量和质量直接影响最终分数,下面给出一组实际经验值:

每人样本数识别表现课程设计定位
1 ~ 5 张同角度同光线能用,稍一动就认错勉强及格
10 ~ 20 张日常光照下稳定,戴眼镜后偶发掉点中等偏上
30 ~ 50 张换表情、换发型也有一定鲁棒性高分作业
50 张以上接近工程可用,但采集和训练时间变长有工程价值

采集时让人脸做小幅度的左右旋转、抬头低头、靠近远离,每个姿态保持约半秒再跳到下一个。不要连续疯狂截图,相邻帧高度相似,特征冗余大。我一般把摄像头帧率控制在15FPS左右,每5帧保存一次,一个用户采集40~50张只需要两三分钟。

3. 训练自己的识别模型:trainer.py 与 LBPH 参数

3.1 从数据集读取图像和标签

训练代码的核心是遍历刚才生成的data目录,把图像路径和所属用户ID做成对照表:

import cv2 import os import numpy as np face_cascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml") faces = [] labels = [] data_dir = "data" for user_name in os.listdir(data_dir): user_path = os.path.join(data_dir, user_name) if not os.path.isdir(user_path): continue label = int(user_name.split("_")[1]) for img_name in os.listdir(user_path): img_path = os.path.join(user_path, img_name) img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) detected = face_cascade.detectMultiScale(img, scaleFactor=1.1, minNeighbors=4) if len(detected) == 1: (x, y, w, h) = detected[0] faces.append(cv2.resize(img[y:y+h, x:x+w], (200, 200))) labels.append(label) faces = np.array(faces) labels = np.array(labels)

这段代码遍历所有user_*文件夹,从目录名中解出整数ID,再对每张图执行一次人脸检测并裁出人脸区域。这里的detectMultiScale参数和采集时略有不同,minNeighbors=4放宽了一点,因为训练样本可能是在不同光线下拍的,检测框稳定性比实时视频差。这一步的作用是二次校验:哪怕采集时保存了完整人脸,训练前再裁一次,可以去掉非人脸噪声。如果某张图检测不到人脸,直接跳过,不要让空数组进入训练器。

3.2 LBPH的原理边界与参数选择

训练器最常用的是OpenCV自带的LBPH:

recognizer = cv2.face.LBPHFaceRecognizer_create( radius=1, neighbors=8, grid_x=8, grid_y=8 )

LBPH把人脸分成多个小网格,在每个像素周围取半径为radius的邻域,与中心像素比较大小,按二进制编码统计直方图。radius=1表示取3×3邻域,radius=2则覆盖5×5区域;邻域半径越大,能捕捉的纹理尺度越大,但也更容易把表情皱纹和光照阴影混进特征。neighbors=8是标准的8邻域采样点,太小特征区分度差,太大会对噪声敏感。grid_x=8, grid_y=8是把人脸划分成64个小块,每个块独立统计直方图再首尾拼接,这样能保留人脸的局部空间信息:鼻子区域的特征不会跑到额头区域。对课程设计来说,这组默认参数是经过大量样本验证的稳定组合,不建议盲目增大grid,因为小块越多维度越高,小样本下直方图会稀疏,识别率反而下降。

3.3 训练并保存 trainingData.yml

recognizer.train(faces, labels) model_path = "trainingData.yml" recognizer.write(model_path) print(f"训练完成,共 {len(faces)} 个样本,模型已保存到 {model_path}")

train接受两个参数:样本矩阵和标签数组。样本矩阵形状是(N, 200, 200),标签是长度N的整数数组。write把训练结果序列化到YAML文件,里面保存的是LBPH的直方图序列、标签映射和网格参数。答辩时如果问“模型文件里是什么”,就答:“每个标签对应一组从各网格块拼接的局部二值模式直方图,以及训练时记录的用户ID列表。”不要答成神经网络权重。这里要注意一个常见错误:直接用np.array(faces)时如果每张图尺寸不统一,train会报data type must be 8-bit或维度不匹配,所以采集端统一resize到200×200,不能只在训练时临时resize。

3.4 为什么课程设计用LBPH而不是深度模型

机器视觉课程要求在有限时间、有限数据集里跑通闭环,LBPH有三个不可替代的优势:不需要GPU,CPU上训练100张图只需几秒;模型文件只有几十到几百KB,部署简单;代码量和原理都能在答辩时讲清楚。深度学习方案比如FaceNet、InsightFace在公开数据集上准确率更高,但需要预训练权重、特征对齐和阈值调优,一套流程下来复杂度翻倍,而且期末项目里如果只有几十个样本,微调效果通常还不如LBPH。我见过有人用face_recognition库两行代码出结果,但那把特征提取和比对都封装了,老师一问“距离阈值为什么是0.45”就答不上来。LBPH是可以用纸笔推导出计算过程的算法,这也是大作业评分的隐藏加分项。

4. 识别与数据库:detector.py + recognizer.py 配合 FaceBase.db

4.1 加载模型与置信度阈值设定

识别端代码:

recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("trainingData.yml") label_map = { 0: "张三", 1: "李四", 2: "Unknown" } threshold = 80

read从YAML文件恢复训练好的模型,不需要重新训练。label_map把数字ID映射成真实姓名,这里要注意:这个映射最好和FaceBase.db中的记录保持一致。threshold是置信度阈值,LBPH的predict返回的距离值越小表示越相似,这个值的含义是“重建误差”或“直方图卡方距离”,不是一个概率。常见取值在50到100之间,具体要看训练集的个体差异。一个比较稳的做法是先收集10张已经训练过的人脸跑一遍,把最低距离和最误判距离画一条分界线,取两者中值作为阈值。

4.2 识别流程与业务逻辑

ret, frame = cap.read() gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5) for (x, y, w, h) in faces: roi = gray[y:y+h, x:x+w] roi_resized = cv2.resize(roi, (200, 200)) label, confidence = recognizer.predict(roi_resized) if confidence < threshold: name = label_map.get(label, "Unknown") color = (0, 255, 0) else: name = "Unknown" color = (0, 0, 255) cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2) cv2.putText(frame, f"{name} ({confidence:.1f})", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)

识别流程和训练时一致:转灰度、检测人脸、resize、预测。唯一区别是predict返回两个值:label是预测的标签,confidence是距离值。很多同学只看label不设阈值,导致任意一张陌生脸都被归到最近的已注册用户,这在演示时非常尴尬。加了阈值判断后,陌生脸会在“未注册”分支处理。这里的confidence大小和算法选择有关:EigenFace返回的是欧氏距离,FisherFace和LBPH返回的也是某种距离,阈值不能跨算法通用。所以答辩时如果改了识别器,阈值也要重新标定。

4.3 SQLite 记录访问日志

课程设计资源里的FaceBase.db就是SQLite数据库,常见实现是记录姓名、时间、是否通过:

import sqlite3 from datetime import datetime conn = sqlite3.connect("FaceBase.db") cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS access_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, pass TINYINT NOT NULL, time TEXT NOT NULL ) """) if name != "Unknown": cursor.execute( "INSERT INTO access_log (name, pass, time) VALUES (?, ?, ?)", (name, 1, datetime.now().strftime("%Y-%m-%d %H:%M:%S")) ) conn.commit()

FaceBase.db用SQLite管理,好处是不需要额外安装数据库服务,交给老师时整个项目一个目录拷走即可。注意SQL语句使用参数化查询,避免字符串拼接导致注入,这也是代码规范的一个加分点。如果要导出签到表,可以这样查:

SELECT name, time FROM access_log ORDER BY time DESC LIMIT 10;

4.4 在界面上显示结果

原资源中的界面逻辑通常在detector.py里通过OpenCV的imshow显示视频流,配合键盘事件退出:

cv2.imshow("Face Recognition System", frame) key = cv2.waitKey(1) if key == ord("q"): break

这是最轻量的交互方式,不依赖第三方GUI库。如果想做成更完整的课设界面,可以引入tkinterPyQt5,把视频流嵌入Label控件,同时把右侧Listbox显示数据库记录。但要注意:OpenCV的imshowwaitKey必须在同一线程调用,如果开启多线程读摄像头,要保证cap.read()不被并发抢占。我在做过的一个版本里把摄像头读取放在子线程,主线程用after()刷新UI,结果因为GIL和缓冲区竞争出现画面撕裂。后来改成单线程轮询,代码简单反而稳定。对于课程设计,imshow方案已经够用,界面美观主要靠窗口标题、字体颜色和识别框动画来体现。

5. 把作业做出区分度的验证与调试技巧

5.1 用留出法快速评估模型

训练完不要直接放到识别端看效果,先做一个离线的K折验证。常见做法是把每个人的样本随机打乱,80%训练、20%测试,统计准确率:

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( faces, labels, test_size=0.2, stratify=labels, random_state=42 ) r = cv2.face.LBPHFaceRecognizer_create() r.train(X_train, y_train) correct = 0 for img, true_label in zip(X_test, y_test): pred, conf = r.predict(img) if pred == true_label and conf < threshold: correct += 1 print(f"准确率: {correct / len(X_test):.2f}")

stratify=labels保证每个人的测试样本比例一致,防止某个人全是训练集、另一个人全是测试集的情况。这个脚本只要跑一次,就能在写报告时给出一个量化的准确率数据,比“感觉还不错”有说服力得多。

5.2 现场演示最容易翻车的三个场景

第一,现场灯光和采集时差别太大。解决办法是识别的时候对每一帧也做equalizeHist,并且尽量把采集和演示放在同一场所。第二,误检非人脸。minNeighbors从5调到7,牺牲一点帧率换稳定。第三,阈值过高导致已注册用户被拒。可以在演示时打印confidence实时值,看到通常稳定在40~60,把阈值设为100左右留出余量。不要为了追求不误识把阈值压到40,那样熟悉的人换个发型也会被拒。

5.3 输出通道扩展:从视频窗口到串口和数据库

如果想把项目从“能跑”提升到“像产品”,可以把识别结果同步写进FaceBase.db并推送一条串口指令。例如通过pyserial发送open_door给单片机:

import serial ser = serial.Serial("/dev/ttyUSB0", 9600, timeout=0.5) if name != "Unknown": ser.write(b"open_door\\n")

这个动作在答辩时非常加分,因为它把课堂上的OpenCV任务和实际门禁场景连起来了。需要注意的是,串口号在Windows下是COM3这样的形式,MAC下是/dev/cu.usbserial-*,不同机器不一致,可以写一个自动扫描端口的函数,或者把串口配置单独放在config.py里。如果不想接硬件,也可以用pygame.mixer播放声音,或者用win32api弹窗提示,核心思路是让输出多样化,展现你对机器视觉应用边界的思考。

真正决定大作业评分的不是算法本身,而是边界情况处理得有多细。下来你可以把threshold、摄像头索引、样本尺寸这些参数集中到一个配置类里,以后换数据集、换算法时,只需要改配置文件,不需要动识别主流程。

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

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

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

立即咨询