ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenCV人脸识别源码实战:Haar+LBPH从环境到部署全流程

OpenCV人脸识别源码实战:Haar+LBPH从环境到部署全流程 简介基于Python与OpenCV的人脸识别系统源码定位明确面向具备一定Python语法基础、渴望通过实战掌握人脸检测与识别技术的开发者可直接服务于高校课程设计、毕业设计或竞赛项目。压缩包共十四份文件整体仅有148KB核心包括可直接运行的主程序main.py、参数配置文件config.txt、基于Haar特征的正面人脸检测器XML模型以及十个已经训练好的YML识别器另外附带README说明文档方便快速上手使用。目前已经有一千零八十二人学习下载是入门计算机视觉领域值得参考的小型学习样本。整套源码覆盖了从加载图片、人脸框定、图像预处理到特征提取与模型匹配的完整链路读者既可以逐行阅读核心逻辑也可以在现有YML识别器基础上替换自己的训练数据多种模型文件便于对比不同样本规模下的识别差异适合作为二次开发或实验验证的起点。1. 一套能跑通的 OpenCV 人脸识别源码它到底值不值得你花时间很多找我咨询的人第一句往往是“我下载了一个 Python 基于 OpenCV 的人脸识别系统源码接下来该干什么”。这个标题代表的并不是神秘商业代码而是 Haar 级联检测加 LBPH 识别的经典组合不需要 GPU、不需要深度学习框架核心代码不到两百行普通笔记本摄像头就能在低算力设备上实时认出人脸。人脸识别门禁 demo、课堂签到、宿舍查寝这类场景它都够用。它最大的优点是跑得通环境配好十分钟内就能看到摄像头框住你的脸并打出名字。缺点也很明显——精度不如深度学习方案对光照、角度、遮挡都很敏感。它适合学生作业、毕业设计和中小型项目起步不适合高精度安防。下文按从零到能用的顺序把环境、采集、训练、识别和踩坑一次讲完。2. 环境与原理为什么 Haar 检测加 LBPH 识别是入门最优解2.1 三个候选方案对比dlib、face_recognition、OpenCV 怎么选在做人脸识别之前先想清楚一个问题网上有 dlib、face_recognition、OpenCV 三条路为什么这个标题最终落在 OpenCV 上答案不是 OpenCV 识别率最高而是它的依赖最轻、封装最直接。方案依赖复杂度识别方式适合场景dlib需要编译 C 库Windows 下配置麻烦深度学习特征向量 距离比较精度要求高、能接受复杂环境face_recognition底层还是 dlibpip 安装容易踩编译坑128 维特征向量精度要求高、不介意慢OpenCV 自带pip 一个包搞定Haar 检测 LBPH 识别课程设计、门禁 demo、低算力设备我一般会把 OpenCV 作为首选原因有三个第一opencv-contrib-python 这个包把检测器、识别器、摄像头读取全部打包不需要额外安装任何深度学习推理框架第二LBPH 训练时间以秒计一个人 100 张样本训练完只要一两秒第三模型文件就是一个几百 KB 的 yml拷到树莓派上也能直接跑。对比之下dlib 和 face_recognition 在 Windows 上编译容易翻车模型文件动辄几十 MB识别精度确实高但对入门项目来说学习成本不成比例。如果你的目标是先把一条完整链路跑通OpenCV 是最稳的选择。2.2 环境准备从零装出能跑 cv2.face 的 Python 环境网上 Python 安装教程和 OpenCV 教程很多但最容易翻车的不是安装本身而是版本搭配。很多人装的是 opencv-python这个包不带人脸识别模块代码一跑就报 ModuleNotFoundError。必须装 opencv-contrib-python扩展模块里才有 cv2.face。建议用 Anaconda 建一个干净环境避免把系统 Python 搞乱。命令如下conda create -n face python3.9 -y conda activate face pip install opencv-contrib-python numpy -i https://pypi.tuna.tsinghua.edu.cn/simple python -c import cv2; print(cv2.__version__); print(cv2.face.LBPHFaceRecognizer_create)看到输出版本号且没有报错环境就通了。第三条命令里的LBPHFaceRecognizer_create是 OpenCV 的人脸识别器工厂函数能正常打印出来说明 contrib 模块装对了。如果最后一行报module cv2 has no attribute face就是装了不带扩展的 opencv-python卸载后换装 opencv-contrib-python 即可。提示不要同时装 opencv-python 和 opencv-contrib-python两个包会互相覆盖同名文件造成各种诡异报错。装之前先pip uninstall opencv-python。2.3 Haar 级联与 LBPH 原理两个黑匣子的使用边界OpenCV 做人脸识别是两段式先用检测器从画面里找出人脸的位置再把裁出来的人脸图交给识别器判断是谁。检测用的是 Haar 级联分类器识别用的是 LBPH。Haar 检测的原理是训练一个 Adaboost 级联分类器用人脸局部明暗特征做判断。这些特征预先训练好存在 xml 文件里OpenCV 自带的haarcascade_frontalface_default.xml是通用正脸模型。调用detectMultiScale时会有一堆参数最核心的是scaleFactor和minNeighborsscaleFactor 是每轮检测缩小的比例越大越快但越容易漏检常见取 1.1 到 1.2minNeighbors 是每个候选框需要经过多少邻居框确认才算数越大误检越少但越容易漏掉真脸。LBPH 全称 Local Binary Pattern Histogram原理是把灰度图划分成网格每个像素和周围像素比较得到二进制模式最后统计直方图作为人脸特征。训练时把每个人的特征存进模型识别时计算当前人脸特征和所有已存特征的相似度得到 (label, confidence) 二元组。这里有个关键认知LBPH 的置信度是距离值越小越像不是越大越像正好和很多人直觉相反。这套组合的边界在于Haar 对侧脸、低头、暗光非常脆弱LBPH 对表情和角度变化敏感。理解这两个黑匣子的脾气后面踩坑时才能对症下药。3. 造数据与训练用摄像头采集自己的人脸训练出可用的识别模型3.1 数据集目录设计一份可以被脚本直接读取的结构训练前先想好数据怎么放。标准做法是创建dataset目录每个人的名字建一个子目录目录名就是标签。注意一个原则目录名和文件名不用中文。OpenCV 的imread和imwrite在 Windows 下对中文路径支持不完整文件路径里带中文会直接翻车表现为图片读出来是 None 或写入失败。用拼音或英文最省心。目录结构是这样dataset/ └── zhang_san/ ├── 001.jpg ├── 002.jpg ├── ... └── 100.jpg训练脚本会遍历这个目录把zhang_san映射成标签 0li_si映射成标签 1依次类推。一人一个文件夹的好处是扩展简单想加人就新建一个文件夹不用改代码。每类样本建议至少 40 张少于 20 张时 LBPH 特征统计不稳定识别时误识率会明显上升。3.2 采集脚本用摄像头造一百张人脸样本的完整代码采集的核心逻辑是循环读帧、检测人脸、裁剪灰度图、缩放到统一尺寸、写盘。下面是完整的采集脚本import cv2 import os person_name zhang_san save_dir os.path.join(dataset, person_name) os.makedirs(save_dir, exist_okTrue) face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError(摄像头打不开检查索引或占用情况) count 0 while count 100: ok, frame cap.read() if not ok: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5, minSize(64, 64)) for (x, y, w, h) in faces: face gray[y:y h, x:x w] face cv2.resize(face, (200, 200)) count 1 cv2.imwrite(os.path.join(save_dir, f{count:03d}.jpg), face) cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.imshow(capture, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() print(f已保存 {count} 张人脸到 {save_dir})逻辑说明VideoCapture(0)打开默认摄像头索引 0 表示第一个摄像头笔记本一般就是它。每次检测到人脸就裁剪出灰度图缩放到 200x200 后写盘同时在画面里画绿框做可视化反馈。minSize(64, 64)过滤掉太小的误检框避免把远处的人或噪声存进数据集。参数说明scaleFactor1.1是精度优先的选择检测慢一点但漏检少minNeighbors5能压住大部分误检。如果发现存进去的图片很多不是人脸把 minNeighbors 调到 6 或 7如果发现正脸经常检测不到把 scaleFactor 调到 1.05 并接受更慢的速度。采集时要注意镜头里只留目标人不要出现第二张脸头左右转动、上下微抬、远近移动着拍保证样本覆盖不同角度和距离识别时就不容易翻车。连续帧之间人如果完全不动存的 100 张几乎一样后面训练出的模型泛化性很差。3.3 训练脚本均衡化、标签映射和模型保存数据采完就可以训练了。我的训练脚本固定做四件事遍历目录建立标签映射、读图、直方图均衡化、训练并保存模型import cv2 import os import json import numpy as np data_dir dataset images, labels [], [] name_map {} for label, name in enumerate(sorted(os.listdir(data_dir))): person_dir os.path.join(data_dir, name) if not os.path.isdir(person_dir): continue name_map[label] name for fname in os.listdir(person_dir): if not fname.lower().endswith((.jpg, .jpeg, .png)): continue img cv2.imread(os.path.join(person_dir, fname), cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (200, 200)) img cv2.equalizeHist(img) images.append(img) labels.append(label) if len(images) 0: raise RuntimeError(dataset 目录下没有找到任何图片) recognizer cv2.face.LBPHFaceRecognizer_create( radius1, neighbors8, grid_x8, grid_y8 ) recognizer.train(images, np.array(labels)) recognizer.save(trainer.yml) with open(name_map.json, w, encodingutf-8) as f: json.dump(name_map, f, ensure_asciiFalse, indent2) print(训练完成标签映射:, name_map)逻辑说明enumerate(sorted(os.listdir(data_dir)))保证了标签按目录名字典序分配模型训练多少次标签都不会乱变。equalizeHist是直方图均衡化作用是把过于亮或过暗的图像对比度拉开让 LBPH 特征提取更稳定这是对抗光照变化最便宜的手段。参数说明radius1是 LBP 算子的采样半径neighbors8是圆周采样点数这两个值控制特征粒度grid_x8, grid_y8把图像切分成 8x8 的网格网格越细特征越局部但太细会过拟合8x8 是 OpenCV 默认值应对 100 张样本的规模是合适的。训练完成后会生成trainer.yml和name_map.json前者是识别模型后者是标签到人名的映射两个文件推理时都要用到。注意name_map.json是很容易被忽略的文件。很多人只保存了 yml识别时拿到的 label 是数字不知道对应谁。json 文件几 KB压缩包解压后不要删。4. 实时识别用摄像头判断面前的人是谁代码和参数怎么调4.1 识别主循环检测、裁剪、预测、画框的完整流程训练完进入识别环节。识别程序的骨架和采集脚本很像区别在于把imwrite换成了recognizer.predict并根据置信度决定显示谁的名字import cv2 import json recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(trainer.yml) with open(name_map.json, r, encodingutf-8) as f: name_map {int(k): v for k, v in json.load(f).items()} face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) CONF_THRESHOLD 70 while True: ok, frame cap.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5, minSize(64, 64)) for (x, y, w, h) in faces: face cv2.resize(gray[y:y h, x:x w], (200, 200)) face cv2.equalizeHist(face) label, confidence recognizer.predict(face) if confidence CONF_THRESHOLD: name name_map.get(label, unknown) status f{name} {confidence:.0f} else: status unknown cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(frame, status, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow(recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明每一帧先转灰度检测人脸对每个检测框裁出人脸图做和训练时完全相同的预处理resize 到 200x200 加均衡化然后喂给predict。这里的关键是预处理必须和训练时保持一致哪怕差一步特征分布就对不上识别率会明显下降。confidence是 LBPH 的距离值越小越可信。CONF_THRESHOLD 70是我常用的起始值实际应该按你自己的数据去校准下一节讲方法。画框和文字时注意cv2.putText不支持中文渲染所以显示的名字必须用英文或拼音这就是前面强调数据集目录用拼音的原因。4.2 置信度阈值那个决定“认识不认识”的数值怎么定置信度阈值是整个系统里最像玄学的参数。LBPH 返回的 confidence 理论上没有上限实际运行中不同人的距离分布差异很大。有的人本人识别稳定在 40 到 60陌生人能到 120 以上阈值取 70 很舒服有的人本人就和陌生人距离接近阈值怎么取都会误识。所以不要抄别人的阈值。我一般会在正式使用前做一次阈值观察把识别脚本里predict的结果直接打印出来分别用自己的脸和陌生人的脸在摄像头前站几秒记录两边的 confidence 范围。如果本人分布在 45 到 65陌生人分布在 90 以上取中间值 75 到 80 就有安全边界。如果两个区间有重叠说明数据质量有问题先回去补样本而不是硬调阈值。# 临时观察脚本观察本人和陌生人的置信度分布 label, confidence recognizer.predict(face) print(f预测标签{label}, 置信度{confidence:.2f})一个更系统的做法是写一个离线评估脚本把训练集里每张照片重新预测一遍统计每类的最小、最大、平均置信度。这比对着摄像头拍几十秒靠谱得多因为你能控制变量。这个脚本我在第 6 章给出完整版本。4.3 低算力设备上怎么跑帧率、分辨率与跳帧策略很多同学的目标平台不是笔记本而是人脸识别门禁机或树莓派这类设备算力有限。LBPH 本身很快瓶颈在 Haar 检测和图像缩放。我的经验是三步限制输入分辨率、跳帧检测、检测到人脸再做预测。frame cv2.resize(frame, (320, 240)) # 降低分辨率检测耗时明显下降 frame_counter (frame_counter 1) % 3 if frame_counter ! 0: continue # 每 3 帧做一次完整检测中间帧直接跳过逻辑说明把摄像头原始帧先缩到 320x240Haar 检测的耗时能降到原来的三分之一以下对精度影响不大因为人脸检测只需要大致位置最终还是会裁出来缩放到 200x200。跳帧的意思是不必每一帧都做检测每 3 帧检测一次中间 2 帧直接显示上一帧的结果人眼感知不到延迟CPU 占用却降下来了。真正识别的时候predict只在faces列表非空时才执行也就是画面上有人脸才做特征比较。这套组合在树莓派 4B 上能跑到 15 帧左右做门禁签到是完全够用的。5. 避坑指南五个最容易翻车的点及对应解法5.1 环境类cv2.face 报错、OpenCV 版本互相覆盖、中文路径读不出图第一个高频问题ModuleNotFoundError: No module named cv2.face。现象是 import cv2 成功但访问cv2.face报错。原因是装的是 opencv-python这个包只包含核心模块人脸识别相关代码在扩展模块里。解决办法是卸载后安装 opencv-contrib-python具体命令在 2.2 节已经给了。判断标准很简单安装完执行一次cv2.face.LBPHFaceRecognizer_create能打印出对象说明模块可用。第二个问题之前能跑的项目突然 import 报错或者 cv2 版本变了。常见原因是 conda 和 pip 混装系统里存在多个 OpenCV 副本Python 加载了其中一个另一个被覆盖。排查方法是在命令行执行python -c import cv2; print(cv2.__file__); print(cv2.__version__)看打印出的路径是哪个环境下的再检查该路径下是否存在opencv_python和opencv_contrib_python两个 dist-info 目录。如果同时存在就是版本互相覆盖留下的残留建议把两个都卸掉重新只装 contrib 版本。第三个问题cv2.imread返回 None。我在第 3 章提过Windows 下 OpenCV 对中文路径支持不完整。解决思路有两个一是所有目录文件名用英文这是最省事的二是用 numpy 和 imdecode 绕过底层文件读取代码如下import cv2 import numpy as np def cv_imread(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_GRAYSCALE)5.2 数据类光照一变就认不出、100 张样本全是重复的、人员一多就误识很多人训练完发现一个尴尬现象训练的时候识别正常换了个房间开灯或者拉上窗帘人脸就认不出来了。原因是采集时样本太单一全是一个角度、一种光照、一种表情。LBPH 提取的是局部纹理特征光照一变纹理细节全变。解决思路是采集阶段就制造变化开灯关灯各拍几十张左右转头、远近移动、戴不戴眼镜都拍几张。另一个补强手段是训练和识别前都做equalizeHist这个操作能显著减弱光照影响也是我前面代码里固定加这一步的原因。还有个隐蔽问题采集脚本存下来的 100 张可能几乎一模一样。原因是摄像头帧率高人坐着不动相邻帧的差异微乎其微。特征学习需要的是“多样性”而不是“数量”。最简单的办法是用差分判断当前帧和上一帧人脸区域的像素差异小于阈值就丢弃if last_face is not None: diff cv2.absdiff(face, last_face) if cv2.mean(diff)[0] 5: continue last_face face人员变多、每类样本数量不均衡时LBPH 会偏向样本多的那一类。表现为只认某个人其他人都判成他。解决方法是每类样本数量尽量接近建议下限 40 张训练前检查一下各类数量差距太悬殊就补数据。5.3 运行类摄像头打不开、人脸框乱跳、识别卡顿cap.isOpened()返回 False摄像头打不开常见原因有三个索引不对、摄像头被其他程序占用、笔记本摄像头驱动问题。先换索引for idx in range(3): cap cv2.VideoCapture(idx) if cap.isOpened(): print(f摄像头索引 {idx} 可用) break索引从 0 开始外接摄像头经常落在 1 或 2。如果索引都对但打不开检查是不是微信、浏览器等程序占用了摄像头Windows 下摄像头同一时间只能被一个进程独占。人脸框乱跳是因为 Haar 检测本身不稳定某几帧漏检了下一帧又检测到框会闪。常见做法是记录上一帧的检测结果当前帧没检测到人脸就沿用历史框检测到了再更新last_box None if len(faces) 0: last_box faces[0] elif last_box is not None: x, y, w, h last_box识别卡顿则优先检查两件事摄像头分辨率是否过高处理帧是否做了缩放。把cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)这类设置加上同时参照 4.3 节做降分辨率和跳帧。6. 进阶把检测器换成 YuNet并加一个自检脚本校准阈值6.1 用 YuNet 替换 Haar侧脸和暗光不再看运气Haar 检测器最大的短板是侧脸和暗光下漏检严重这是级联分类器本身的局限。OpenCV 4.5.4 之后内置了 YuNet 神经网络检测器模型文件face_detection_yunet_2023mar.onnx可以从 OpenCV Zoo 项目里下载放到运行目录即可。detector cv2.FaceDetectorYN_create( face_detection_yunet_2023mar.onnx, , (320, 320), 0.6, # score_threshold低于这个分数的框被丢掉 0.3, # nms_threshold非极大值抑制的 IoU 阈值 5000 # 最多保留的框数 ) detector.setInputSize((frame.shape[1], frame.shape[0])) _, faces detector.detect(frame)替换后的检测流程不同直接传 BGR 彩色图不用手动转灰度faces 为空时是 None有检测结果时每个元素是 15 维数组前四位是 x, y, w, h后面还有 5 个关键点坐标和 1 个置信度分数。裁剪人脸时仍用前四维。YuNet 对侧脸、低头、暗光、逆光都有明显改善代价是模型文件几十 MB树莓派上帧率略降但在笔记本上体验差异很小。6.2 离线自检脚本训练完先测一遍再谈阈值我现在的习惯是训练完先跑一遍离线自检而不是直接开摄像头。这个脚本把训练集里每张照片重新预测一遍统计每个名字的置信度范围用来校准阈值和发现数据问题import cv2 import os import json recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(trainer.yml) with open(name_map.json, r, encodingutf-8) as f: name_map {int(k): v for k, v in json.load(f).items()} for label, name in name_map.items(): confs [] for fname in os.listdir(os.path.join(dataset, name)): img cv2.imread(os.path.join(dataset, name, fname), cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (200, 200)) img cv2.equalizeHist(img) _, conf recognizer.predict(img) confs.append(conf) print(f{name}: min{min(confs):.1f} max{max(confs):.1f} fmean{sum(confs)/len(confs):.1f})逻辑说明这是把识别链路里除了摄像头外的所有环节验证一遍。如果某个名字的 min 值已经很高说明训练数据本身有问题先检查采集时有没有混入别人的脸。如果所有类的 max 值都集中在 80 到 90阈值取 100 以上基本没法用优先回去补数据而不是硬调阈值。这个脚本只在训练和验证时运行部署时不带也不影响实时识别性能。这套方案做到这里已经覆盖了从环境到部署的完整闭环。说实话OpenCV 人脸识别天花板不高但它是我见过把整条链路讲清楚成本最低的方案——对新手友好对入门项目够用踩完这些坑以后换深度学习方案你至少知道问题出在检测还是识别。希望帮到你。本文还有配套的精品资源点击获取
返回列表