OpenCV-Python人脸识别实战:从LBPH训练到门禁系统原型
发布时间:2026/9/26 3:16:45来源:尧图网络
简介基于OpenCV-Python的人脸模型训练与识别完整代码包面向正在学习Python图像处理与计算机视觉的初学者。程序支持从输入人脸图片中提取特征进行模型训练生成识别模型既可完成单张图片的识别与展示也能调用本地摄像头实现实时人脸识别的简单验证训练过程涵盖图像预处理、特征提取与模型生成代码结构清晰便于二次修改和学习。压缩包共含341个文件核心为305个Python脚本另附模型权重pth、配置文件cfg/xml、示例图片及说明文档并打包了Python虚拟环境相关组件可快速部署运行整体约30.27MB。目前已有1870人学习下载。通过配套博客讲解与演示读者可掌握OpenCV人脸检测、模型训练、识别调用及摄像头实时处理的全流程思路同时了解常见排错方法对觉得Python入门吃力、希望在实战中提升的读者这份资料能提供直观有效的学习路径。1. 基于 OpenCV-Python 的人脸识别从一台笔记本跑到门禁机原型如果你跟我一样手里只有一台笔记本和一个 USB 摄像头却想写一个“人脸识别门禁机”原型那 OpenCV-Python 加 LBPH 识别器是最快的一条路。很多人以为人脸识别一定要上深度学习其实 OpenCV-Python 自带的人脸模型训练与识别已经能让你在一下午跑通采集样本、训练模型、摄像头实时识别全流程。这篇笔记用基于 OpenCV-Python 的人脸模型训练与识别项目当例子从算法选择、参数设置写到避坑经验适合想自己动手做一个小玩意的人也适合后续要把流程迁到门禁机或嵌入式设备上的读者。2. 三种人脸识别算法怎么选LBPH 的取舍与参数边界把 OpenCV 的 face 模块打开你会看到三个识别器EigenFace、FisherFace、LBPH。很多教程直接说“用 LBPH”但如果你不清楚三者的边界后面调参数就是玄学。我当年第一次做的时候直接把 EigenFace 跑起来结果白天训练、晚上测试识别结果完全没法看。这里把原理和选型理由放在前面省得你踩同样的坑。2.1 先分清人脸识别器不是人脸检测器人脸检测器和人脸识别器是两个东西。检测器Haar 级联、DNN 检测器只回答“画面里有没有人脸、脸在哪”给出一个矩形框识别器回答“这张脸是谁”。我们口头说的“人脸识别算法”在 OpenCV 的世界里其实是两步先用CascadeClassifier在整帧里检测人脸矩形再把矩形区域裁出来交给识别器做predict。LBPH 的全称是 Local Binary Patterns Histograms也就是局部二值模式直方图。它不是神经网络核心思路是对每个像素拿它和周围邻居比大小把结果拼成一个二进制串再转成 0 到 255 的十进制值整张脸被划分为grid_x × grid_y个格子每个格子单独统计直方图最后拼成一个大的特征向量。训练时算法把每个身份的特征向量保存下来预测时提取同一张脸的特征向量和保存的向量算距离距离越小越像。OpenCV 把这个距离叫作 confidence体现在predict的返回值里。这一步很多人会忽略LBPH 训练用的图如果是灰度图预测时的输入也必须是灰度图。我见过有人训练用cv2.imread(..., cv2.IMREAD_GRAYSCALE)到了摄像头那一步却直接把彩色帧丢给predict然后发现 confidence 永远很大。统一预处理是整个流程的生命线。这个两步结构的直接后果是识别器的正确与否很大程度上取决于检测框有没有把脸完整框进来。框太紧额头或下巴被切掉直方图少一块框太松背景和头发占了大半特征被噪音污染。我一般会把 Haar 返回的矩形向外扩 10% 再裁剪效果比直接用原始矩形好不少。2.2 LBPH 的三个参数和一把尺子LBPHFaceRecognizer_create有四个躲不开的参数radius、neighbors、grid_x、grid_y外面还得配一个阈值。radius 是采样半径radius1 时只看 3×3 邻域里的 8 个像素radius2 时范围更大对光照包容度好一点但脸上的细小特征也会被抹掉。我一般先用 radius1等测试集上细纹特征不够了再考虑调大。neighbors 是采样点个数OpenCV 默认是 8。你可以改成 16更接近原始论文的设定但训练和预测都会变慢对这个“小玩意”项目来说收益不大。grid 决定把脸分成多少格子8×8 是很多示例的默认值。网格越密表情和角度的差异越容易被放大网格越疏对没对齐的脸越宽容可真假脸的区分能力也会下降。阈值就是判断“是不是本人”的那把尺子。predict返回的 confidence 小于阈值判定为本人否则判为陌生人。OpenCV 不同版本对 LBPH 的默认阈值并不完全一致示例代码里常见的写法是setThreshold(80)我也习惯从 80 开始试然后按“熟人不会被拒、陌生人不会放进来”的原则来回调。这个值没有标准答案和你的光照、摄像头、样本量都有关系。另外LBPH 在训练和预测时并不会自动做直方图均衡化。样本和测试画面光照不同时预先做一次均衡化能把 confidence 整体压下去一截。我建议把均衡化放到预处理函数里和 resize 放在一起不要写一半落一处。2.3 EigenFace 与 FisherFace 为什么不适合我的场景EigenFace 用 PCA 把脸压缩成一组特征脸FisherFace 用 LDA 找类别可分性最大的投影方向。这两个算法都比 LBPH 历史悠久但输入要求也更苛刻人脸必须对齐瞳孔位置要大致一样光照不能差太多。我用 EigenFace 做过一次实验白天在办公室采样本晚上回家测试十次有五次把同事的脸认成陌生人。FisherFace 的问题更实际它要求至少两个类别的样本而且每个类别样本数不能太少否则类内散布矩阵的结果不可靠。对“拿着笔记本临时采集一批人脸就跑”的场景这两个算法都容易翻车。LBPH 统计的是局部纹理直方图光照变化不会整体改变直方图形状所以更适合摄像头画面随机、样本数量有限的原型。表三种识别器在实际项目里的边界算法原理光照敏感度最小组类要求适合场景EigenFacePCA 特征脸很高1 类起人脸对齐良好、光照可控的实验室环境FisherFaceLDA 线性判别中等至少 2 类样本充足、类别可分性强的数据库LBPH局部二值模式直方图较低1 类起门禁机、打卡机、嵌入式原型如果你后面要做基于 STM32 的人脸识别门禁系统设计LBPH 还有一个额外的好处直方图特征可以提前算好存成模板识别端只做比对。相比之下深度学习模型要挤进单片机就是另一场硬仗了。还要注意这三个识别器都是全脸匹配不是局部比对。如果你想要戴口罩也能识别OpenCV 这套传统流程就做不到了那得换 DNN 特征提取。3. 准备人脸样本采集脚本与数据清洗的三条军规样本质量决定模型上限这句话放在 OpenCV 的人脸模型训练里一点不夸张。LBPH 再能扛光照也扛不住模糊、闭眼、半张脸的样本。这一章先把样本的目录结构和采集脚本写好再说清洗。3.1 目录结构和样本命名给训练脚本铺路训练时要按人分文件夹我用的结构是face_data/ zhangsan/ 0000.jpg 0001.jpg ... lisi/ 0000.jpg ...文件夹名就是人名训练脚本会自己读文件夹名生成数字标签不需要手工改文件名。图片统一存成 200×200 的灰度 JPEG比 PNG 省空间训练速度也快。每个类别我一般采集 50 到 100 张少了模型容易过拟合多了采集和清洗都累。为什么用灰度而不是彩色LBPH 的直方图建立在灰度像素比较上彩色信息本来就不参与。存彩色 JPEG 除了浪费磁盘没有任何收益。另一个细节是文件夹命名不要带空格和中文OpenCV 在部分平台对中文路径支持有问题直接用拼音最省心。3.2 用 Haar 级联写采集脚本30 行跑通采集脚本的核心是detectMultiScale。下面这段我用 PyCharm 直接跑过注意摄像头索引和count的上限按自己需求改。import cv2 import os # 用 OpenCV 自带的 Haar 级联模型路径不要自己拼 cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) name zhangsan # 文件夹名就是身份名 save_dir fface_data/{name} os.makedirs(save_dir, exist_okTrue) cap cv2.VideoCapture(0) count 0 while count 100: # 每个类别采集 100 张 ret, frame cap.read() if not ret: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(100, 100) ) for (x, y, w, h) in faces: face gray[y:yh, x:xw] face cv2.resize(face, (200, 200)) face cv2.equalizeHist(face) # 先均衡化再存 cv2.imwrite(f{save_dir}/{count:04d}.jpg, face) count 1 cap.release()这段逻辑是每帧先转灰度让 Haar 检测人脸检测到的人脸矩形裁出来缩放到 200×200做一次直方图均衡化然后写盘。count到 100 就自动停止适合采集的时候人坐摄像头前面自己看着进度。scaleFactor1.1表示每一轮检测窗口缩小 10%值越小检测越细但越慢minNeighbors5是邻居矩形合并条件太小容易把显示器、钥匙串误检成人脸太大又会漏检小脸。minSize(100, 100)用来过滤画面里太小的脸否则远处走动的路人也会被存进样本。3.3 数据清洗模糊、遮挡、角度这三关采集完不能直接开训。我一般会循环读一遍样本用 Laplacian 方差筛掉模糊帧方差低于某个值就删。常见做法是低于 50 就删但这个阈值和摄像头有关我会先打印一批样本的均值再定。import cv2 import os root face_data/zhangsan for file in os.listdir(root): img cv2.imread(f{root}/{file}, cv2.IMREAD_GRAYSCALE) var cv2.Laplacian(img, cv2.CV_64F).var() if var 50: os.remove(f{root}/{file}) print(f删除模糊样本: {file})除了模糊还要检查两类问题一是遮挡比如手挡着脸、刘海遮住眼睛这种样本会让直方图跑偏二是角度如果你只存纯正面模型一遇到侧脸就大概率找不着北。我采集时会让对方稍微左右转头、仰头、低头各来一阵保证每个类别有一定角度变化。戴眼镜的样本最好单独保留因为镜框反光会改变局部纹理特征。清洗完成后我会再随机抽出 5 张图用cv2.imshow肉眼扫一遍看有没有漏网的半张脸和反光。这一步听起来土但确实救过我一次当时有一批样本下半张脸全被衣领挡住confidence 怎么调都不对。4. 训练与保存人脸模型LBPH 训练脚本逐行拆解样本清洗完接着就是把face_data文件夹变成face_model.yml。这一章给完整训练脚本并解释每个参数为什么这么设。4.1 遍历样本目录并生成标签训练脚本要解决的核心问题文件夹名是字符串模型只认数字标签。所以建一个label_map字典把名字映射成从 0 开始的数字。import os import cv2 import numpy as np import json image_paths [] labels [] label_map {} current_label 0 root face_data for name in sorted(os.listdir(root)): if name.startswith(.): continue label_map[name] current_label for file in sorted(os.listdir(f{root}/{name})): if not file.endswith(.jpg): continue image_paths.append(f{root}/{name}/{file}) labels.append(current_label) current_label 1 faces [] for path in image_paths: gray cv2.imread(path, cv2.IMREAD_GRAYSCALE) faces.append(gray) labels np.asarray(labels, dtypenp.int32)这里sorted是保证同一个人的样本按文件名顺序排列调试时方便。labels必须是 numpy 的 int32 数组OpenCV 不接受 Python 原生 list这个是很多人踩过的坑。faces是一个 list里面每个元素是一张灰度图尺寸必须一致否则训练时会报尺寸不匹配。如果你的face_data里混入了一个空文件夹os.listdir扫不到 jpg 就不会给这个类别加 label但label_map里已经占了一个数字。这会导致训练完的标签不连续实时识别时永远识别不到某些人。我一般会在遍历前先过滤掉空目录。4.2 训练参数怎么设默认值与边界recognizer cv2.face.LBPHFaceRecognizer_create( radius1, neighbors8, grid_x8, grid_y8, ) recognizer.train(faces, labels)训练本身只有一行但参数含义要心里有数参数我用的值影响radius1值越大特征越糙对光照越钝但细节损失越明显neighbors8采样点越多计算越慢默认 8 足够grid_x / grid_y8 / 8网格越密越能区分表情和角度但也越容易误杀图片尺寸200×200训练与预测必须一致否则 confidence 会失真threshold训练后再调predict 返回的置信度小于它才判为本人把 radius 调到 2 或者把 grid 调到 10×10 都不会报错但到实时识别时你会明显感觉到 FPS 往下掉。如果你的样本里有大量角度变化grid 保持 8×8 其实是安全做法如果你全是正面照想提高区分度再考虑 10×10。4.3 保存模型与标签映射一个 YAML 加一个 JSON训练完成后把模型写到磁盘同时把label_map存成 JSON。下次启动程序直接read模型和 JSON不用重新训练。recognizer.write(face_model.yml) with open(labels.json, w, encodingutf-8) as f: json.dump(label_map, f, ensure_asciiFalse, indent2) print(训练完成模型已保存)这里有个小习惯模型文件用.ymlOpenCV 只认扩展名决定写 YAML 还是 XML不要乱改。labels.json是识别端把数字标签解释回人名的依据丢了它模型就是一堆没有名字的数字。我在第一次做的时候图省事只存了模型结果第二天识别出来全是 0、1、2还得重新猜映射白白浪费时间。重新打开模型时用recognizer.read(face_model.yml)就能加载。注意read不会校验标签映射所以 JSON 文件必须和模型文件一起备份不然数字标签就是一堆黑匣子。5. 避坑人脸识别训练与识别里最常见的五个翻车现场这一章全部来自我自己的血泪经验每一条都按“现象 → 原因 → 解决”写你照着排查能省很多时间。5.1 cv2.face 模块报不存在现象在 PyCharm 里 import cv2 正常但执行cv2.face.LBPHFaceRecognizer_create()直接报module cv2 has no attribute face。原因face 模块在 opencv-contrib-python 里而不是在 opencv-python 里。只安装 opencv-python 的话cv2.face根本不存在。尤其容易翻车的是 PyCharm 的情况你用系统终端 pip 安装了包但 PyCharm 解释器指向的是另一个虚拟环境两者互不相通。解决先卸载干净再装避免两个包同时存在互相冲突。在 PyCharm 底部 Terminal 里执行pip uninstall opencv-python opencv-contrib-python pip install opencv-contrib-python装完后在 Python 控制台里敲print(cv2.__version__)确认能用再看cv2.face是否能访问。记住opencv-contrib-python本身就包含 main modules不需要再装 opencv-python。PyCharm 里如果安装完还是报错去 File Settings Project Python Interpreter 确认解释器路径别让两个 Python 环境打架。5.2 预测时 confidence 大到离谱现象模型训练成功识别时返回的 label 是对的但 confidence 普遍超过 200阈值怎么调都压不下来。原因最典型的错误是测试图和训练图预处理不一致。训练时用灰度、200×200、做了均衡化预测时直接把彩色帧或者不同尺寸的图丢进predict。LBPH 直方图对尺寸和通道极敏感差一步结果就完全对不上。解决把预处理抽成同一个函数训练和识别都用它def preprocess_face(face_roi): gray cv2.cvtColor(face_roi, cv2.COLOR_BGR2GRAY) if face_roi.ndim 3 else face_roi gray cv2.resize(gray, (200, 200)) return cv2.equalizeHist(gray)从那以后我每次接到别人项目第一件事就是看他的训练脚本和识别脚本里预处理是不是同一个函数。这是这个项目里最常见的坑没有之一。5.3 换个环境就认不出人现象在办公室灯光下训练的模型拿到家里或者白天户外的摄像头下识别率断崖式下降。原因样本单一。采集时全是在同一个时刻、同一个角度、同一个亮度下拍的LBPH 对光照的抗性被高估了。它只是比 EigenFace 更能扛局部变化不代表它能应对绝对光照差异。解决采集阶段就覆盖白天和夜晚、正脸和左右转、戴不戴眼镜等变化。每个类别至少 50 张宁缺毋滥。如果实在没法多采可以在预处理时用 CLAHE 代替equalizeHistclahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) face clahe.apply(face)CLAHE 对低照度画面的提升比普通均衡化明显代价是计算量略高在实时识别时每帧多花几毫秒。5.4 Haar 级联检测不到人脸或乱框现象摄像头对着人脸detectMultiScale返回空列表或者把显示器、海报上的脸误框出来。原因scaleFactor、minNeighbors、minSize三个参数没有配合好。scaleFactor1.1是常见值但画面本来就暗的情况下Haar 本身检测率就会掉minNeighbors设太大会把连续检测框拆散设太小又会保留大量误检。另一个常见原因是级联文件路径写错手写绝对路径在别人机器上一跑就崩。解决检测前先对灰度图做一次equalizeHist参数从scaleFactor1.1, minNeighbors5, minSize(80, 80)开始调。路径用cv2.data.haarcascades haarcascade_frontalface_default.xml拼接不要写死盘符。如果检测还是时好时坏可以直接换 OpenCV 的 DNN 人脸检测器但那是另一个话题原型阶段 Haar 够用。5.5 实时识别卡成幻灯片现象训练和静态识别都没问题一到摄像头实时画面FPS 只有个位数画面像是慢放。原因每帧都跑detectMultiScale加predictCPU 全耗在检测上再加上模型网格设太大、分辨率拉满性能自然就崩了。解决模型在程序启动时只read一次不要放进循环里识别帧率抽到每 2 帧或每 3 帧处理一次摄像头分辨率用 640×480而不是 1920×1080。还可以加一个逻辑先检测只有检测到人脸且人脸框足够大才调用predict没有人脸就不做识别。如果你做的是门禁机原型完全可以用“检测到人脸后抓拍一帧识别完再决定能不能过”的串行流程不需要每帧都推。基于 STM32 的人脸识别门禁系统设计里也常见这种“采集-识别-动作”串行流程只是把 PC 摄像头换成了摄像头模组。门禁场景里还有一个现实问题摄像头摆位。如果摄像头装在门侧Haar 经常只能检测到侧脸识别率自然低。原型阶段把摄像头放在正对门的位置离地高度约 1.2 米会可靠很多。6. 把模型接到摄像头实时识别与置信度验证技巧模型训练好之后别急着上摄像头先在离线测试集上过一遍。我会准备 10 张本人和 10 张陌生人的照片统一走preprocess_face用模型预测并记录 confidence然后看两块分布熟人的 confidence 通常在一百以下陌生人的通常在几百以上。中间的重叠区就是你要用阈值切掉的风险区我通常把阈值放在两者的分界线上。import cv2 import json recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(face_model.yml) label_map json.load(open(labels.json, encodingutf-8)) label_name {v: k for k, v in label_map.items()} # 每帧代码里把检测到的人脸框传入 preprocess_face label, conf recognizer.predict(face_roi_gray) if conf 80: name label_name[label] else: name unknown实时识别时我会在画面左上角把 confidence 一起打出来不只是打名字。这样一旦有人因为角度问题被判成陌生人我能马上看到是距离本身很大还是阈值太严。这个习惯帮我省了大量来回跑代码的时间。从那以后我每次训练完都强制走一遍流程先看face_model.yml和labels.json有没有生成再跑一遍离线验证最后才接摄像头。这套流程也能自然迁到门禁机场景验证通过就给出一个开关信号验证不通过就继续等下一帧。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网