
简介这是一份面向Python入门者的OpenCV人脸识别系统源码包适合希望在真实项目中理解人脸检测、特征提取与比对流程的学习者。源码基于OpenCV传统机器学习方案包含主程序、Haar级联分类器与多个已训练好的yml识别模型可帮助读者快速搭建可运行的人脸识别演示环境。包体共14个文件以yml识别器配置、py脚本、xml检测器、txt配置及md说明文档为主要类型整体仅148KB结构精简、便于逐文件研读。目前已有1082人学习下载尤其适合作为计算机视觉课程设计或毕业设计的前期参考。读者可从清晰的目录中直接定位主程序与模型文件结合README说明理解训练与识别逻辑快速上手二次开发。1. 从零搭一个人脸识别系统这个经典组合为什么还有生命力收到“做一个带人脸识别的门禁/考勤系统”这类需求时很多人第一反应是上深度学习模型、接 GPU。但真到交付现场你会发现大部分场景跑在一台普通办公电脑甚至嵌入式小主机上数据量只有几十个人环境相对固定。Python 基于 OpenCV 的人脸识别系统正是为这种需求准备的不依赖 GPU依赖少检测、训练、识别三段代码能单独调试出了问题也看得见、改得动。它的价值不是跟大模型比精度而是在低成本、可解释的前提下把“识别率够用”这件事办成。适合课程设计、小型考勤、访客登记也是人脸识别门禁系统设计的前期原型。这篇文章把选型、核心实现、常见坑位和调优手段完整讲一遍。2. 人脸检测与识别的选型基础Haar、DNN 与 LBPH 怎么搭先把概念拆清楚一套人脸识别系统在代码上要完成两个独立任务——检测和识别。检测回答“脸在哪里”识别回答“这张脸是谁”。OpenCV 在检测侧提供 Haar 级联、LBP 级联和 DNN 模型三种主流方案识别侧最常见的是 LBPH 识别器。不少新手拿到源码第一反应是“训练怎么这么快”其实 LBPH 的训练本质是统计特征直方图识别效果的上限反而往往被检测环节卡住。下面先解决环境再讲清楚每种方案在源码里的定位和取舍。2.1 先把环境固定下来Python 虚拟环境与 OpenCV 安装不管翻哪篇 python 安装教程或 opencv 安装教程最容易忽略的问题都是环境不干净。系统 Python 里残留的包会把版本搅乱尤其是 opencv-python 和 opencv-contrib-python 混装时装错一个就找不到 cv2.face 模块。我习惯在项目目录下新建虚拟环境把 OpenCV、numpy 固定在一个空间里避免后面排查问题时先排除一堆无关干扰。# 创建并激活虚拟环境 # Windows: python -m venv .venv .venv\Scripts\activate # Linux / macOS: # python3 -m venv .venv # source .venv/bin/activate # 安装依赖opencv-python 只含基础模块 # opencv-contrib-python 才提供 cv2.faceLBPH 等识别器 pip install opencv-python opencv-contrib-python numpy注意如果只装 opencv-python源码里用到cv2.face.LBPHFaceRecognizer_create()的地方会直接报AttributeError: module cv2 has no attribute face。两个包必须一起装这也是第 4 章里要展开的常见坑。装完之后用下面这段验证环境是否就绪# 验证 OpenCV 版本与关键模块 import cv2 print(OpenCV 版本:, cv2.__version__) print(级联分类器路径:, cv2.data.haarcascades) # 能成功创建 LBPH 模型说明 cv2.face 模块可用 recognizer cv2.face.LBPHFaceRecognizer_create() print(LBPH 创建成功:, type(recognizer).__name__)这里说明一下cv2.data.haarcascades是 OpenCV 自带级联模型的目录Haar 人脸检测器的 XML 文件就在里面。源码在调用CascadeClassifier时一般会拼接这个路径如果你的项目里没有自带 XML就用这一行定位系统文件。2.2 检测器怎么选Haar 级联仍是轻量项目的第一选择Haar 级联检测器的原理是通过滑动窗口在不同尺度上扫描灰度图用级联的强分类器逐层判断窗口内是否包含人脸。训练好之后就是一个 XML 文件比如haarcascade_frontalface_default.xml大小只有 1MB 左右加载后 CPU 上处理一帧只需要几十毫秒。对于机位固定、正脸为主的门禁和考勤场景Haar 依然是性价比最高的选择没有之一。有人质疑 Haar 虚检多、对小脸不敏感。这个质疑成立但可以通过参数调整化解scaleFactor1.1表示每层金字塔缩小 1.1 倍数值越小检测越精细但越慢minNeighbors5表示一个区域至少命中 5 次才算数调大能过滤虚检但也会漏掉真正的小脸。这两个参数是检测环节最值得反复试的两个旋钮后面第 3 章的代码会直接用它们。如果你的源码里已经换成 DNN 检测器那走的通常是 OpenCV DNN 模块加载 SSD 模型需要准备deploy.prototxt和res10_300x300_ssd_iter_140000.caffemodel两个文件。调用方式比 Haar 多一步预处理# DNN 人脸检测器的常见调用方式 import cv2 net cv2.dnn.readNetFromCaffe( deploy.prototxt, res10_300x300_ssd_iter_140000.caffemodel ) img cv2.imread(test.jpg) h, w img.shape[:2] # 将图片缩放到模型输入尺寸并减去训练时的通道均值 blob cv2.dnn.blobFromImage( cv2.resize(img, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) net.setInput(blob) detections net.forward() for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: box (detections[0, 0, i, 3:7] * [w, h, w, h]).astype(int) x1, y1, x2, y2 box cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(dnn_result.jpg, img)这段代码里最容易被忽略的是(104.0, 177.0, 123.0)这三个通道均值它必须和模型训练时的预处理一致不能按自己的习惯乱改。迁移到别的机器时还要确认两个模型文件已经放进项目目录否则会报“无法解析 prototxt 文件”之类的加载错误。DNN 的精度确实比 Haar 高尤其是小尺寸人脸的召回率代价是模型文件大、推理慢。在学生宿舍门禁、会议室签到这类场景我个人还是倾向先跑通 Haar 再决定要不要换。2.3 识别器为什么用 LBPH原理与应用边界检测到人脸框之后识别器负责给这张脸贴姓名。LBPHLocal Binary Pattern Histogram把一张人脸图划分为若干网格区域在每个网格内计算局部二值模式的直方图最后把这些直方图拼成一个特征向量。训练时每个人的样本算出一组特征模板预测时计算输入特征与所有模板的距离距离最小者胜出。LBPH 对光照变化有一定容忍度运算量小在几十人规模的数据集上稳定工作所以成了 OpenCV 人脸识别源码中最常见的识别器。它有几个直接影响识别效果的参数参数名默认值作用调参习惯radius1LBP 圆形邻域半径光照复杂时调到 2neighbors8圆形邻域上的采样点数保持 8不轻易动grid_x8水平分块数人脸分辨率低于 100px 时调小grid_y8垂直分块数同上参数不是越大越好。radius增大时会丢失细节grid_x太大虽然空间定位更强但也会放大对齐误差。理解边界比改参数更重要LBPH 是整体纹理匹配器对遮挡敏感戴口罩、墨镜都会明显拉低识别率它也不做姿态矫正侧脸超过 30 度就开始退化。只有在环境相对可控的考勤、门禁场景它才真正有交付价值。3. 复现源码核心流程从人脸裁剪、模型训练到实时识别一套经典的人脸识别系统源码目录结构一般是dataset/按人分目录存原始照片prepare_faces.py负责裁剪人脸train_lbph.py训练识别器最后recognize.py打开摄像头做实时识别。三个脚本串起来就是“采集 → 训练 → 推理”的完整闭环。下面按这个顺序把每一步拆开讲。3.1 数据准备做扎实人脸裁剪脚本识别系统最终效果的上限早在数据准备阶段就定死了。常见做法是从每个人的几张照片里自动检测人脸裁剪后统一缩放到 200×200 灰度图再按人名放进子目录。dataset/ ├── alice/ │ ├── alice_000.jpg │ └── alice_001.jpg └── bob/ └── bob_000.jpg裁剪脚本如下注意它依赖前面装好的 Haar 级联文件# prepare_faces.py # 从原始照片中检测人脸裁剪并统一尺寸后保存 import os import cv2 cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) def crop_faces_from_image(image_path, output_dir, label, target_size(200, 200)): img cv2.imread(image_path) if img is None: print(无法读取:, image_path) return 0 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(100, 100) ) out_dir os.path.join(output_dir, label) os.makedirs(out_dir, exist_okTrue) for idx, (x, y, w, h) in enumerate(faces): face gray[y:y h, x:x w] # 截取人脸区域 face cv2.resize(face, target_size) # 统一尺寸 cv2.imwrite( os.path.join(out_dir, f{label}_{idx:03d}.jpg), face ) return len(faces)逻辑说明先用级联检测器在灰度图上找到所有人脸框通过 numpy 切片gray[y:yh, x:xw]截取 ROI最后 resize 成 200×200 保存。cv2.imwrite会自动根据扩展名决定编码格式所以这里统一用 jpg 没有兼容性问题。参数说明scaleFactor1.1是金字塔缩放比例值越小检测越精细耗时也越长minNeighbors5控制连续命中次数越大越不容易误检minSize(100, 100)过滤掉太小的框因为最终要放大到 200×200原始区域太小强行放大只会得到模糊特征。给同一个人准备照片时尽量覆盖正脸、微侧脸、不同光线条件每类 10 张以上是比较稳的起点。低于 5 张时识别效果会开始不稳定后面用数据增强也很难完全补回来。3.2 训练 LBPH 识别器与模型落盘样本准备好之后训练脚本要做两件事把每个人的图片和整数标签对应起来再调用 LBPH 识别器训练。标签是整数所以需要保存一份人名到 ID 的映射否则推理阶段只知道数字不知道数字对应谁。# train_lbph.py # 从 dataset 目录加载图片训练 LBPH 识别器并保存模型 import os import cv2 import numpy as np def load_dataset(data_dir): images, labels, label_map [], [], {} current_id 0 for person_name in sorted(os.listdir(data_dir)): person_dir os.path.join(data_dir, person_name) if not os.path.isdir(person_dir): continue if person_name not in label_map: label_map[person_name] current_id current_id 1 for file in sorted(os.listdir(person_dir)): if not file.lower().endswith((.jpg, .jpeg, .png)): continue path os.path.join(person_dir, file) img cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: print(跳过无法读取的文件:, path) continue images.append(img) # 收集灰度图 labels.append(label_map[person_name]) # 收集对应标签 return images, np.array(labels), label_map images, labels, label_map load_dataset(dataset) recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.train(images, labels) recognizer.save(lbph_model.yml) print(标签映射:, label_map) print(参与训练的样本数:, len(images))逻辑说明load_dataset遍历 dataset 下的每个人名子目录用label_map把人名转成从 0 开始的整数标签recognizer.train(images, labels)接收两个 list内部完成特征提取和建模save把模型写到磁盘。模型文件是 YAML 格式后面识别脚本直接read加载。参数说明训练阶段需要关心的不是迭代次数而是数据集均衡性。alice 有 50 张、bob 只有 5 张时识别结果会偏向 alice这是样本不均衡直接导致的。我一般会让每类数量接近实在不够就用数据增强补齐。还要强调一点train要求图片尺寸一致建议在裁剪阶段就固定到 200×200训练阶段不需要再 resize。图片尺寸参差不齐时 LBPH 会尝试内部归一化但效果不稳定距离值也容易漂移。3.3 实时识别摄像头循环与置信度阈值识别脚本是最终交付物。它每帧做三件事检测人脸、预处理、调用recognizer.predict得到标签和距离值。距离值在 LBPH 里是置信度的反义词值越小表示越相似。利用这个值可以区分“认识的人”和“陌生人”这是人脸识别门禁系统设计里的基本逻辑。# recognize.py # 打开摄像头实时识别展示姓名与距离值 import cv2 MODEL_PATH lbph_model.yml THRESHOLD 80 # 距离阈值低于该值才显示姓名 label_map {0: alice, 1: bob} # 与训练时保持一致 recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(MODEL_PATH) cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces cascade.detectMultiScale(gray, 1.1, 5) for (x, y, w, h) in faces: face cv2.resize(gray[y:y h, x:x w], (200, 200)) label, confidence recognizer.predict(face) name label_map.get(label, unknown) if confidence THRESHOLD: # 距离太大不再当作已知人 name unknown cv2.rectangle(frame, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(frame, f{name} {confidence:.1f}, (x, y - 10), 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()逻辑说明detectMultiScale返回的(x, y, w, h)是边界框参数用它在灰度图中裁出人脸并 resize 成统一尺寸。predict(face)返回两个值预测标签和距离值。把距离和THRESHOLD比较才能把陌生人挡在外面。显示时把距离一起打出来方便现场调试。参数说明THRESHOLD的默认值 80 不是一个万能值它必须实测。通常的做法是找几个不在训练集里的陌生人采集他们的距离值分布再统计训练集里人员正常情况下的距离范围两个分布的分界处就是最优阈值。阈值设小了会把熟人误判成陌生人设大了陌生人也能混进来这个值必须在正式运行前调好。4. 避坑与排查真实项目中毁掉识别率的四个高频问题4.1 白天能识别、晚上就失灵光照变化让 LBPH 翻车现象识别系统白天识别率 90% 以上到了晚上或室内逆光条件下稳定识别的人开始频繁被判成 unknown甚至标签乱跳。原因LBPH 的特征是局部二值模式直方图光照会改变灰度分布。侧光在脸上形成半亮半暗的渐变直方图距离被拉大一旦超过阈值就识别失败。这不是模型坏了而是训练和推理时的输入分布不一致。解决在训练和识别两端统一加上直方图均衡化封装一个预处理函数训练裁剪和推理 ROI 都走同一入口# preprocess.py # 统一的人脸预处理入口 import cv2 def preprocess_face(face_gray, size(200, 200)): face_gray cv2.resize(face_gray, size) face_gray cv2.equalizeHist(face_gray) # 灰度直方图均衡 return face_gray在训练脚本和识别脚本里拿到灰度图之后先过一遍preprocess_face再交给train或predict。改完后稳定性通常会有明显改善。另一个补充手段是关闭摄像头的自动白平衡或把曝光锁在一组固定值上。摄像头自动调参导致帧间亮度漂移时识别结果就会时好时坏这在固定机位的门禁场景尤其重要。4.2 训练读取 0 张图片中文路径还有这种坑现象训练脚本在 Windows 上运行路径里带了“人脸数据”这类中文目录名程序不报错但len(images)输出很小的数字甚至直接把所有文件跳过。原因OpenCV 的imread在部分环境下对中文路径不兼容返回的img是None程序只能跳过。这是 OpenCV 与 Windows API 编码不一致导致的不是一个纯图像处理问题。解决最直接的办法是把项目路径统一改成纯英文。为了保险在读取文件时用numpy配合imdecode替代imread# 兼容中文路径的图片读取 import cv2 import numpy as np def imread_unicode(path, flagscv2.IMREAD_GRAYSCALE): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, flags)说明np.fromfile按二进制读取整个文件imdecode再根据文件头识别编码格式绕开了imread对路径编码的假设。训练集里有中文字符也不会被跳过这是人脸识别系统源码在 Windows 上最常见的改动之一。4.3 检测框抖动、识别结果反复横跳现象摄像头静止不动画面里的人脸框一会儿大一会儿小识别结果在同一秒内从 alice 跳到 unknown 又跳回来。打印距离值会发现它在一个范围内剧烈波动。原因Haar 检测器对每一帧独立检测受噪声和轻微头部摆动影响边界框位置会漂移。200×200 的裁剪图只要框水平偏移 10 像素直方图就会明显变化LBPH 距离值也随之大幅波动。解决三招组合。第一把minSize调到检测区域的实际大小避免检测框去追最小的人脸。第二对结果做时间投票连续 N 帧中出现次数最多的标签作为最终输出。第三对confidence做滑动平均。投票代码可以很短from collections import deque votes deque(maxlen5) # 保存最近 5 帧的标签 # 在识别循环内每帧预测后执行 votes.append(predicted_label) if votes.count(votes[-1]) 3: final_label votes[-1] # 连续 3 帧一致才更新显示这个技巧对门禁场景尤其重要人脸在画面里通常停留好几秒牺牲几帧响应时间换来一路稳定的输出值得。这是我在交付识别系统时一定会加的逻辑。4.4 运行报错AttributeError: module cv2 has no attribute face现象导入 cv2 成功cv2.__version__正常但一行cv2.face.LBPHFaceRecognizer_create()直接报错提示没有 face 属性。原因cv2.face属于 opencv-contrib-python 包。很多人按常规安装教程只装了 opencv-python基础模块覆盖了但社区贡献的算子没有带进来。另一种翻车情况是机器上同时装了 opencv-python 和 opencv-contrib-python两个包互相覆盖最后只剩一个残缺模块。解决把两个包卸载干净再重装 contrib 版pip uninstall opencv-python opencv-contrib-python -y pip install opencv-contrib-python numpy注意在无人值守的部署环境里最好把版本号固定在 requirements.txt 里避免新版兼容性突然变化。另外可以在代码开头用hasattr(cv2, face)做软检查缺失时直接提示“缺少 contrib 模块”比让用户翻堆栈更快定位。5. 进阶让“能跑”的人脸识别变成“能交付”5.1 比对前先对齐眼睛对齐的收益LBPH 对空间位置非常敏感脸稍微偏一点裁剪出来的纹理位置就错位了。检测框稳定之后我习惯在进入识别前先做一次眼部对齐定位左右眼中心计算旋转角再用仿射变换把脸转到水平位置。角度偏差在 5 度以内时识别率提升非常明显。实现起来就是一次getRotationMatrix2D加warpAffine的事成本低收益大适合作为交付前的固定步骤。5.2 阈值不能一刀切陌生人拒绝与动态阈值固定阈值在固定场景里够用但场景一变就要重新标定。我后来习惯在代码里保存一组正常距离值的滑动统计比如最近 30 帧confidence的 80 分位数作为自适应阈值的基准。光线缓慢变化的环境里阈值会跟随漂移而不是死守 80。这个做法不需要额外依赖只是在识别循环里多维护一个队列建议你在正式部署时加上。5.3 用数据增强补足训练集翻转让模型更稳训练样本不够时最省事的数据增强是水平翻转和亮度扰动。LBPH 本质是直方图统计翻转不会破坏特征分布却能凭空把样本数翻倍。把增强代码放在训练脚本的加载阶段样本不够的问题能缓解一半。def augment(face_gray): yield face_gray # 原图 yield cv2.flip(face_gray, 1) # 水平翻转 yield cv2.convertScaleAbs(face_gray, alpha1.1, beta10) # 变亮我现在的习惯是不管有没有新增样本都先跑一轮增强再用同一组测试集对比增强前后的距离均值。如果距离没有明显下降说明模型到了瓶颈该去补真实样本而不是继续增强。人脸识别系统的交付不是“能跑”就行而是要在一个采样周期内稳定准确地报出人名。把检测框、预处理、阈值、投票这四件事理清这套基于 OpenCV 的方案就真正立住了。希望帮到你。本文还有配套的精品资源点击获取