ARTICLE DETAIL

资讯详情

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

十个YML文件背后:OpenCV传统人脸识别系统源码拆解

十个YML文件背后:OpenCV传统人脸识别系统源码拆解 简介Python基于OpenCV的人脸识别系统源码为一整套可直接运行的入门级实战工程面向具备基础Python语法、希望快速上手OpenCV图像处理与人脸识别应用的开发者也适合作为课程设计或毕业设计的参考模板。压缩包共有14个文件约148KB核心由main.py、config.txt、README.md及10个yml识别器模型构成同时附带haarcascade_frontalface_default.xml人脸检测分类器能清晰展示数据准备、模型训练、实时识别与参数配置的完整流程。目前已有1081人学习下载资源结构紧凑、无冗余素材便于初学者按文档说明快速搭建环境并验证效果。通过研读源码与配置文件可掌握OpenCV人脸检测原理、LBPH或EigenFace等识别器用法、多模型组织方式以及摄像头调用与图像预处理的关键代码为后续二次开发或算法改进提供坚实起点。1. 十个 yml 文件才是这份人脸识别源码的关键下载这个 zip 之后第一眼大概率会失望main.py 只有几百行config.txt 也就几行剩下的是一堆数字开头的 yml 文件和 haarcascade_frontalface_default.xml。但恰恰是这十个 yml决定了这套系统能不能认出人、会不会把陌生人当熟人放行。它背后的结构是 OpenCV 传统人脸识别里的 LBPH局部二值模式直方图识别器每个 yml 保存一个人的特征模型main.py 通过 Haar 级联检测人脸再把检测到的人脸送进这些模型里比对。这套方案不依赖 GPU、不需要安装 TensorFlow 或 PyTorch一台普通笔记本甚至树莓派都能实时跑。适合做课程设计、毕业设计、门禁 Demo、课堂点名这类场景也适合拿来理解“检测识别”两段式管线后再往深度学习方案迁移。如果你已经在用 Dlib 或 FaceNet回头看这套代码反而能把传统视觉的基本功补扎实。2. Haar 人脸检测与 LBPH 识别器的源码结构拆解2.1 两段式管线检测与识别为什么必须分开很多人拿到源码第一反应是找“识别”在哪结果发现核心循环里先调了一次 CascadeClassifier再调了一次 predict。这不是多此一举而是传统人脸识别的基本架构先确定画面里有没有人脸、人脸在哪个坐标再把这块区域单独交给识别器。检测用的是 haarcascade_frontalface_default.xml这是 OpenCV 自带的 Haar 级联分类器基于 Viola-Jones 框架训练而成。它的核心是滑动窗口加积分图在每一帧图像上用不同大小的窗口滑动每个窗口经过 AdaBoost 训练出来的一组弱分类器逐级筛选级联结构让大部分非人脸窗口在前几级就被丢弃所以能做到实时。detectMultiScale 返回的是人脸矩形框列表每个框是 (x, y, w, h) 四元组。识别阶段用的是 LBPHLocal Binary Pattern Histogram。它把一张人脸图划分成若干个网格对每个像素计算局部二值模式再统计每个网格的直方图最后把各网格直方图拼接成一个特征向量。预测时计算当前人脸特征向量与已训练样本的直方图距离距离越小说明越相似。相比 EigenFace 和 FisherFaceLBPH 对光照变化更鲁棒也不需要把训练图片全部保存在模型里所以它的 yml 模型文件都很小1 号到 10 号每个也就几十 KB。这就是为什么这套源码只有十个 yml 却能支撑一个完整身份识别系统的原因。2.2 逐文件拆解与 main.py 可复现版本zip 里的文件结构很清晰去掉 README 之后真正参与运行的就三类东西haarcascade_frontalface_default.xml检测器负责找脸1.yml 到 10.yml十个 LBPH 识别器每个存一个人的人脸特征模型config.txt main.py配置与主循环把上面两样串起来config.txt 在这套源码里扮演的是“参数闸门”一般存输出窗口尺寸、人的名字列表、检测最小人脸尺寸。main.py 的逻辑我拆开重写了一份可读性更好的版本功能与原包一致import cv2 import os detector cv2.CascadeClassifier(haarcascade_frontalface_default.xml) def load_config(pathconfig.txt): names [] size (640, 480) with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line or line.startswith(#): continue if line.startswith(size): parts line.split() size (int(parts[1]), int(parts[2])) elif line.startswith(names): names line.split(:, 1)[1].strip().split(,) return names, size names, (out_w, out_h) load_config() # 提前加载 10 个识别器避免每帧重复 I/O recognizers [] for i in range(1, 11): yml_path f{i}.yml if os.path.exists(yml_path): recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(yml_path) recognizers.append((i, recognizer)) cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) faces detector.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(60, 60) ) for (x, y, w, h) in faces: face_roi gray[y:y h, x:x w] face_roi cv2.resize(face_roi, (200, 200)) best_label, best_conf None, float(inf) for label, recognizer in recognizers: pred_label, conf recognizer.predict(face_roi) if conf best_conf: best_conf conf # 这里 label 只在读取时用作 id不清楚时可以打印调试 best_label label name names[best_label - 1] if best_label else Unknown cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(frame, f{name} {best_conf:.2f}, (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(face recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里有个容易被忽略的细节主循环里对每一帧都重新cvtColor和equalizeHist。equalizeHist是直方图均衡化用来压制光照不均但它是全局操作如果画面一侧强光一侧暗效果会打折。所以实际部署时很多人会在这一行之前先做一次白平衡或者高斯模糊。另一个重点是 predict 的输入尺寸必须统一。我在这里把所有人脸 ROI resize 成 200x200这是在 train 阶段就定好的。如果训练时用的是 200x200预测时传入 100x100 或 320x240LBPH 的网格划分grid_x/grid_y不会报错但特征空间已经对不上confidence 会整体漂移误识率明显上升。原包里的 yml 是按 OpenCV 3.x 的 LBPH 格式保存的读取时用cv2.face.LBPHFaceRecognizer_create()就行不需要额外指定参数但如果你用 OpenCV 4.5 以上版本cv2.face模块可能需要单独装opencv-contrib-python否则会报module cv2 has no attribute face。3. config.txt 与置信度阈值陌生人识别靠的不是模型而是参数3.1 confidence 是距离不是概率LBPH 的predict返回两个值label 和 confidence。很多人第一次看到confidence45.7会误以为是“45.7% 像这个人”这个理解是错的。LBPH 的 confidence 是当前人脸直方图与训练样本直方图之间的距离度量数值越小说明越接近而不是越大越可靠。这个反直觉的设定是配置陌生人拒识Unknown逻辑时最容易翻车的地方。如果阈值设成threshold60那么 confidence 小于 60 判为本人大于等于 60 判为陌生人。实践中不同类型数据的置信度分布差异很大室内均匀光照下同一个人的 confidence 通常在 30 到 60 之间而换个角度或换个光线同一个人的 confidence 可能直接跳到 80 以上。反过来某些脸型相近的人不同人的 confidence 也可能低于 50。所以阈值不是一个通用常量必须在实际摄像头环境下用样张标定。confidence 区间建议处理说明0 - 45直接放行高度匹配正常识别45 - 70二次确认可能光线有变化结合多帧投票70 - 100判为陌生人基本不属于库内人员100 以上拒绝特征差异过大常见于非人脸输入3.2 config.txt 里应该装什么原包的 config.txt 内容很精简但一个可维护的配置至少要包含输出尺寸、人员名单、检测参数和置信度阈值。我一般会改成这样# 输出窗口宽高 size: 960 720 # 人名顺序与 1.yml ~ 10.yml 一一对应 names: Alice,Bob,Chen,David,Ella,Fang,Gang,Helen,Iris,Jack # 检测最小人脸尺寸小于该尺寸的人脸不识别 min_size: 60 60 # LBPH 置信度阈值低于该值才显示姓名 threshold: 62然后主循环里的判断逻辑要改成threshold 62 # 从 config.txt 读取 if best_conf threshold: display_name names[best_label - 1] else: display_name Unknown值得注意的一个坑是 names 的索引对齐。原包里 yml 文件是 1.yml 到 10.yml训练时如果 label 是从 1 开始的那么 names 列表的第 0 项对应的就是 label1也就是 1.yml。names[best_label - 1]这个下标换算在 label 从 0 开始的老项目里会普遍错一位识别成功率看着正常但名字永远错位。遇到这种情况先打印best_label再对照文件编号比对着代码猜要快得多。另外如果你用的是贡献版 OpenCVopencv-contrib-pythoncv2.face.LBPHFaceRecognizer_create的默认参数是radius1, neighbors8, grid_x8, grid_y8。这套参数对应 200x200 的人脸输入特征维度是grid_x * grid_y * (2^neighbors)也就是 88256 16384 维。如果训练时改过这些参数预测时也要用同一套参数创建识别器否则读 yml 不报错但置信度没有可比性。4. 自己训练识别器从人脸采集到生成 yml4.1 先用摄像头采集样本目录就是标签要把这套系统用在自己项目里第一步不是改代码而是重新采集训练数据。每个需要识别的人建一个目录目录名用数字编号我习惯用1/、2/、3/这种纯数字目录因为 OpenCV 的 label 必须是整数字符串目录名还得额外维护一份映射表没必要。采集脚本用一个循环检测到人脸就扣出来存成 200x200 灰度图按a.png、b.png这样的序号递增命名。核心逻辑如下import cv2 import os person_dir 1 # 第 1 个人 os.makedirs(person_dir, exist_okTrue) detector cv2.CascadeClassifier(haarcascade_frontalface_default.xml) cap cv2.VideoCapture(0) count 0 while count 200: ok, frame cap.read() if not ok: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) if len(faces) 1: # 单人场景避免混入其他人 x, y, w, h faces[0] face cv2.resize(gray[y:yh, x:xw], (200, 200)) cv2.imwrite(f{person_dir}/{count}.png, face) count 1 cv2.putText(frame, f{count}/200, (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow(collect, frame) if cv2.waitKey(1) 0xFF ord(q) or count 200: break cap.release() cv2.destroyAllWindows()注意minSize(80, 80)这里我提高了检测门槛宁可少存一点也不要存那种距离摄像头三米外占屏幕不到 5% 的模糊小脸。采集时让人脸在画面中间、小幅左右转动比固定正脸的效果好很多。LBPH 对轻微角度变化有一定容忍度但如果训练集全是正脸实际使用时有 15 度以上的偏头confidence 就会上涨明显。4.2 训练 LBPH 并保存成 yml训练脚本的核心是把所有图片和 label 组合成两个数组喂给LBPHFaceRecognizer_create最后save成 yml 文件。下面是完整逻辑import cv2 import os face_list [] label_list [] # 目录结构1/xxx.png, 2/xxx.png ... for person_id in range(1, 11): person_dir str(person_id) if not os.path.exists(person_dir): continue for img_name in os.listdir(person_dir): if not img_name.endswith(.png): continue img_path os.path.join(person_dir, img_name) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (200, 200)) face_list.append(img) label_list.append(person_id) recognizer cv2.face.LBPHFaceRecognizer_create( radius1, neighbors8, grid_x8, grid_y8 ) recognizer.train(face_list, label_list) recognizer.save(10.yml) print(ftrained with {len(face_list)} samples)训练完成后生成的 yml 直接覆盖 zip 里的对应文件。这里有个容易踩的坑如果某个人只采集了 20 张图train不会报错但识别效果会非常差confidence 普遍在 80 以上你会以为阈值没调好。经验值是每人至少 100 张最好在 150 到 260 张之间。低于 60 张时LBPH 学到的直方图统计意义不足换一天的光照基本就认不出来了。训练集数量与效果的对应关系大致如下每人样本数同人 confidence 典型区间推荐阈值场景适用性30 - 5070 - 120难以界定仅实验不能上线80 - 15045 - 7565课程设计、Demo200 以上30 - 6060门禁、考勤等受控场景训练时 label 从 1 开始对应保存为 1.yml。如果你要新增第 11 个人目录建11/训练后保存为11.yml再把 config.txt 的 names 里加上第 11 个名字。main.py 里的加载循环for i in range(1, 11)要改成range(1, 12)否则新模型不会被载入。5. 识别失败与误识Haar 参数、光照与置信度诊断5.1 检测不到人脸先查 detectMultiScale 三个参数摄像头前明明有人但画面里没有任何绿框问题基本都出在detectMultiScale的参数上。scaleFactor 控制每次缩放的比例越接近 1 检测越精细但速度越慢minNeighbors 控制一个区域被判定为人脸需要多少个相邻检测框。参数调整前先记住这个方向误检多就调大 minNeighbors漏检多就调小 minNeighbors 或调大 minSize 的下限。现象调整方式原因把墙上的图案、衣服纹理当成人脸minNeighbors 从 5 调到 8相邻框数量不足时被误判离摄像头 1 米外的人脸检测不到minSize 从 60 降到 40人脸在画面里尺寸低于下限人脸轻微转动就丢失scaleFactor 从 1.1 调到 1.05缩放步长太大跨过了匹配尺度检测数量多但框乱跳对相邻帧的矩形中心做平滑Haar 检测结果本身有抖动5.2 用诊断脚本标定 threshold而不是猜最直接的验证办法是录一段视频分别记录“本人”和“陌生人”的 confidence 分布观察两者的分界线再定阈值。我通常会用下面这个诊断脚本输入一段视频文件输出每一帧的最小 confidence 和对应的名字import cv2 recognizers [] for i in range(1, 11): rec cv2.face.LBPHFaceRecognizer_create() rec.read(f{i}.yml) recognizers.append((i, rec)) cap cv2.VideoCapture(test_alice.mp4) while True: ok, frame cap.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) faces cv2.CascadeClassifier( haarcascade_frontalface_default.xml ).detectMultiScale(gray, 1.1, 5, minSize(60, 60)) for (x, y, w, h) in faces: roi cv2.resize(gray[y:yh, x:xw], (200, 200)) best_conf float(inf) for label, rec in recognizers: _, conf rec.predict(roi) if conf best_conf: best_conf conf print(fframe best_conf{best_conf:.2f})把这段输出的最大值和最小值画出来如果本人视频里最大是 58陌生人视频里最小是 74阈值取 65 左右就是安全的。如果你看到本人和陌生人的 confidence 区间大面积重叠那问题不在阈值而是训练样本太少或光照变化太大调 threshold 没有意义。5.3 光照不稳时先处理输入而不是换模型LBPH 对光照有一定的鲁棒性但它的鲁棒性体现在纹理特征上而不是亮度整体迁移上。白天窗边和晚上开灯时的同一个人confidence 差 20 是很常见的。处理办法是在送入 predict 之前做一个带cv2.normalize的预处理或者像这样先用 CLAHE 代替全局equalizeHistclahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) gray clahe.apply(gray)这是 OpenCV 自带的局部直方图均衡比equalizeHist更适合理光照不均的门禁场景。代价是每帧多几毫秒计算在实际工程里是划算的。多人同框时主循环里本来就对每个框单独 predict不需要额外处理但如果同一个人脸框在连续帧里被反复识别成不同的人最简单的策略是加一个滑动窗口投票取最近 10 帧中出现最多的名字作为结果比单帧置信度判断稳定得多。本文还有配套的精品资源点击获取
返回列表