简介:基于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 | 底层还是 dlib,pip 安装容易踩编译坑 | 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 python=3.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和minNeighbors:scaleFactor 是每轮检测缩小的比例,越大越快但越容易漏检,常见取 1.1 到 1.2;minNeighbors 是每个候选框需要经过多少邻居框确认才算数,越大误检越少但越容易漏掉真脸。
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映射成标签 0,li_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_ok=True) 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, scaleFactor=1.1, minNeighbors=5, 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)过滤掉太小的误检框,避免把远处的人或噪声存进数据集。
参数说明:scaleFactor=1.1是精度优先的选择,检测慢一点但漏检少;minNeighbors=5能压住大部分误检。如果发现存进去的图片很多不是人脸,把 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( radius=1, neighbors=8, grid_x=8, grid_y=8 ) recognizer.train(images, np.array(labels)) recognizer.save("trainer.yml") with open("name_map.json", "w", encoding="utf-8") as f: json.dump(name_map, f, ensure_ascii=False, indent=2) print("训练完成,标签映射:", name_map)逻辑说明:enumerate(sorted(os.listdir(data_dir)))保证了标签按目录名字典序分配,模型训练多少次标签都不会乱变。equalizeHist是直方图均衡化,作用是把过于亮或过暗的图像对比度拉开,让 LBPH 特征提取更稳定,这是对抗光照变化最便宜的手段。
参数说明:radius=1是 LBP 算子的采样半径,neighbors=8是圆周采样点数,这两个值控制特征粒度;grid_x=8, grid_y=8把图像切分成 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", encoding="utf-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, scaleFactor=1.1, minNeighbors=5, 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 帧做一次完整检测,中间帧直接跳过逻辑说明:把摄像头原始帧先缩到 320x240,Haar 检测的耗时能降到原来的三分之一以下,对精度影响不大,因为人脸检测只需要大致位置,最终还是会裁出来缩放到 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, dtype=np.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", encoding="utf-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} " f"mean={sum(confs)/len(confs):.1f}")逻辑说明:这是把识别链路里除了摄像头外的所有环节验证一遍。如果某个名字的 min 值已经很高,说明训练数据本身有问题,先检查采集时有没有混入别人的脸。如果所有类的 max 值都集中在 80 到 90,阈值取 100 以上基本没法用,优先回去补数据而不是硬调阈值。这个脚本只在训练和验证时运行,部署时不带,也不影响实时识别性能。
这套方案做到这里,已经覆盖了从环境到部署的完整闭环。说实话,OpenCV 人脸识别天花板不高,但它是我见过把整条链路讲清楚成本最低的方案——对新手友好,对入门项目够用,踩完这些坑以后换深度学习方案,你至少知道问题出在检测还是识别。希望帮到你。
本文还有配套的精品资源,点击获取