人脸识别大概是计算机视觉里最容易让初学者产生"我好像会了"错觉的方向。网上很多教程把人脸识别拆成十行代码,看起来确实简单——cv2.CascadeClassifier 加载一个 xml,detectMultiScale 框出人脸,predict 给出一个序号,完事。然而一旦把代码从教程页面搬到自己的电脑上,摄像头、模型路径、版本兼容这些环节随便卡一个,demo 就会当场翻车。这篇文章是我实际跑通一个人脸识别项目的完整记录,用的就是 OpenCV 和 Python,覆盖了从环境安装、人脸检测、模型训练到实时识别整条链路,也把运行中遇到的报错和解决过程整理了一遍。适合刚接触人脸识别、又不想一上来就啃深度学习框架的同学参考。
1. 先想明白:人脸检测和人脸识别是两回事
1.1 从需求反推方案
很多人上来就搜"人脸识别代码",然后抄一段就开始跑。但真正动手之前,得先把需求拆清楚。所谓"人脸识别"在工程上其实是两个子任务:
- 人脸检测:判断画面里有没有人脸,以及人脸在哪个位置,输出一个矩形框。
- 人脸识别:把人脸框内的图像和注册过的库比对,判断"这个人是张三还是李四"。
这两个任务经常被混在一起说,但实现思路完全不同。检测解决的是"人在哪",识别解决的是"人是谁"。门禁、考勤、会议签到这类场景,两个都要做;相册自动分类只需要识别;监控区域有人出现的告警只需要检测。
这个项目我定位成一个完整的门禁签到式 demo:摄像头实时画面中检测人脸,识别出身份,在画面上叠加姓名和相似度。目标用户是刚学完 Python 基础、想用 OpenCV 做点实际项目的人。
1.2 为什么选 OpenCV 而不是从零写神经网络
这几年深度学习框架很火,很多人一提到人脸识别就想到 PyTorch、TensorFlow。但我的建议是:第一版项目先用 OpenCV 把整条链路跑通。
原因很实在。OpenCV 本身已经内置了完善的检测和识别 API,不需要自己搭网络、不需要准备大量训练数据、不需要 GPU,装一个包就能干活。它既能用传统的 Haar 级联、LBPH 这类轻量算法,也能通过 DNN 模块直接加载 ONNX 格式的深度学习模型,比如 YuNet 检测器和 SFace 识别器。这就意味着你可以用同一套 OpenCV 环境,先跑轻量版理解原理,再切到深度学习模型提升效果,而不用换技术栈。
另外,OpenCV 的 Python 绑定把图像采集、预处理、显示、视频流读取这些周边工作全包了。人脸识别项目真正的工程难点往往不在模型本身,而在摄像头读取、图像对齐、阈值判断这些环节,OpenCV 能让你把精力集中在核心逻辑上。
1.3 这个项目适合什么场景
我建议把它定位在"单机本地人脸识别"方向,这类需求其实很常见:
- 实验室/办公室的门禁打卡
- 个人相册的自动归档
- 课堂签到系统
- 嵌入式设备上的简单人脸识别终端
这类场景的共同特点是:用户数量不大(几十个人以内)、可以提前采集注册样本、对识别延迟不敏感。它们不太需要动辄百万级数据训练的商用模型,OpenCV 提供的方案在成本、开发速度和可控性方面已经足够。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境安装:搞不定 opencv 一切白搭
2.1 安装命令与版本选择
先说明一个最容易踩的坑:OpenCV 的 pip 包分好几种,装错了后面会非常难受。
大多数人第一次跑人脸识别,只需要装基础包:
bash复制pip install opencv-python
但如果要使用 cv2.face.LBPHFaceRecognizer 这个经典的识别器,基础包是没有这个模块的。人脸识别相关的扩展功能在 opencv-contrib-python 里,安装方式:
bash复制pip install opencv-contrib-python
注意两点。
第一,opencv-python 和 opencv-contrib-python 不能同时存在。后装的那个会把先装的覆盖掉,表面上没事,但一些版本组合会出诡异报错。建议直接只装 contrib 版,省心。
第二,如果用的是服务器或容器环境,没有 GUI 界面,建议装 opencv-contrib-python-headless。否则 import cv2 的时候可能报 libGL.so.1: cannot open shared object file 这类错误,因为 OpenCV 默认版本依赖了图形库。headless 版本是把 GUI 相关功能去掉的编译版,适合跑服务端程序。
我通常的做法是建一个干净的虚拟环境:
bash复制python -m venv venv
source venv/bin/activate # Windows 用 venv\Scripts\activate
pip install opencv-contrib-python numpy
2.2 常见安装报错的排查链路
安装阶段最常见的报错是 ModuleNotFoundError: No module named 'cv2'。这个报错八成是环境混了。排查思路按顺序来:
- 检查包到底装没装:
pip list | grep opencv(Windows 用findstr)。 - 确认当前解释器路径:
python -c "import sys; print(sys.executable)"。 - 确认 pip 和 python 是否指向同一个环境:
pip --version和python --version对照。 - 如果之前用的 conda,检查有没有激活对应的 conda 环境。
很多人的问题是:在 PyCharm 里用的是项目的虚拟环境,但在终端用 pip 装的包装到了系统 Python,两边对不上,就出现"明明装了还是提示找不到"。
还有一个高频问题:pip install opencv-python 下载太慢甚至超时。解决办法是用国内镜像源:
bash复制pip install opencv-contrib-python -i https://pypi.tuna.tsinghua.edu.cn/simple
Windows 上如果 import cv2 报 DLL load failed,先看一下 Python 版本。opencv-python 新版对 Python 版本有要求,太老的 Python(3.7 以下)装新版本会直接失败。建议升到 3.8 以上,然后用 pip install --upgrade opencv-contrib-python 升级到最新版。另外,Windows 下缺 VC++ 运行库也会报 DLL 相关错误,装一下微软官方的 VC_redist.x64.exe 基本能解决。
2.3 验证安装是否可用
装完之后做一个快速验证:
python复制import cv2
print(cv2.__version__)
print(cv2.data.haarcascades)
能输出版本号和级联分类器目录,说明安装成功。
再验证一下摄像头能不能打开:
python复制cap = cv2.VideoCapture(0)
ret, frame = cap.read()
print(ret, frame.shape if ret else 'no frame')
cap.release()
输出 True (480, 640, 3) 这类结果,就说明环境完全 OK,可以进入下一步。
3. 人脸检测:从Haar到YuNet
3.1 Haar级联分类器为什么能检测人脸
Haar 级联是 OpenCV 里最老牌的人脸检测算法,直到现在很多入门教程还在用它。它的原理并不复杂:Haar 特征本质上是一组黑白矩形区域的像素差,比如眼睛区域比额头暗、鼻梁区域比两侧亮,这些亮度差异可以用特定形状的矩形模板快速衡量。
但单个 Haar 特征非常弱,单独用几乎不能判断是不是人脸。AdaBoost 算法把大量弱分类器组合成一个强分类器,每个弱分类器负责筛选一种简单的局部特征。关键是后面还接了一个级联结构:早期几层用最便宜的特征快速排除大部分非人脸区域,只有通过所有层的区域才被判为人脸。这个"先粗筛后精判"的设计,让 Haar 在 CPU 上也能跑得飞快。
不过 Haar 的缺陷也很明显:它对正脸效果好,侧脸、遮挡、暗光环境下漏检率会大幅上升。如果你做的是"人站在摄像头正前方、光线可控"的门禁打卡,Haar 够用;如果是稍微复杂的场景,建议直接用下面的 YuNet。
3.2 用Haar做静态图片检测的完整代码
Haar 的模型文件是一堆 xml,OpenCV 安装包自带下载好的版本,直接通过 cv2.data.haarcascades 拼接路径即可,不用手动去网站下载:
python复制import cv2
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
face_cascade = cv2.CascadeClassifier(cascade_path)
img = cv2.imread('friends.jpg')
if img is None:
print('图片读取失败,检查路径')
exit()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30)
)
for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.imshow('detected', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
detectMultiScale 的几个参数值得解释一下:
scaleFactor=1.1:每次缩放窗口的倍率,越大检测越快但容易漏检小脸。minNeighbors=5:每个候选矩形至少需要被多少个相邻矩形确认才保留,值越大误检越少,但也可能漏检。minSize=(30, 30):最小人脸尺寸,小于这个尺寸的框直接忽略。
3.3 更推荐的方式:YuNet ONNX模型
如果用 Haar 检测出的框去喂给识别模型,精度很容易拖后腿。我实际项目里更推荐用 OpenCV DNN 模块加载 YuNet 模型,这是 OpenCV 官方模型库中的一个轻量人脸检测器,输入一张图,直接输出人脸框和五个关键点(左眼、右眼、鼻尖、左嘴角、右嘴角),这对于后面做人脸对齐非常有价值。
先下载模型文件 face_detection_yunet_2023mar.onnx,放到项目的 model 目录。加载和检测代码:
python复制import cv2
detector = cv2.FaceDetectorYN.create(
'model/face_detection_yunet_2023mar.onnx',
'',
(320, 320),
score_threshold=0.9,
nms_threshold=0.3,
top_k=5000
)
img = cv2.imread('friends.jpg')
h, w = img.shape[:2]
detector.setInputSize((w, h))
retval, faces = detector.detect(img)
# faces 是 N x 15 的数组
# 前4个是 x, y, w, h
# 之后10个是5个关键点的坐标
# 最后1个是置信度
if faces is not None:
for face in faces:
x, y, fw, fh = face[:4].astype(int)
score = face[-1]
cv2.rectangle(img, (x, y), (x + fw, y + fh), (0, 255, 0), 2)
cv2.putText(img, f'{score:.2f}', (x, y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
setInputSize 这一步很重要,模型默认输入是 320x320,但实际检测前必须把输入尺寸设成当前图片的宽高,否则检测结果坐标会错乱。score_threshold=0.9 是置信度阈值,低于这个分数的人脸框会被过滤,调低可以检出更多低质量人脸,但误检也会增加。
3.4 检测环节的漏检和误检
实际跑起来,检测环节最常见的三个问题:漏检、误检、检测框抖动。
漏检通常出现在人脸太小、太偏、暗光或遮挡的场景。解决思路是用 YuNet 并适当降低 score_threshold 到 0.7 左右;如果还是漏,可以在送入检测器前对图像做直方图均衡化,增强对比度。但要注意,降阈值后误检概率会上升。
检测框抖动在实时视频里很常见,因为每帧的人脸框都有一点随机波动。简单的处理办法是加一个轻量的平滑,比如对前后两帧的矩形坐标做指数移动平均:
python复制smooth_box = 0.7 * prev_box + 0.3 * current_box
这在嵌入式或低帧率设备上特别有效。
4. 人脸识别:准备数据、训练模型、两套方案对比
4.1 数据采集该注意什么
识别模型的训练数据直接决定最终效果,这一块偷懒,后面全白搭。
我见过很多新手采集训练样本时,对着摄像头同一个位置连拍 20 张,然后拿这 20 张几乎一样的图去训练。结果一测试就翻车:稍微偏一下头就认不出来了。正确做法是每个用户采集 20 到 30 张图,覆盖不同角度(左右轻微转头)、不同表情、不同光照条件,有条件的话戴不戴眼镜各采一部分。
采集脚本的核心逻辑是:打开摄像头,用 YuNet 检测人脸,按空格键把当前人脸框区域裁下来保存:
python复制import cv2
import os
def collect_face(name, save_dir='dataset', target_count=30):
save_dir = os.path.join(save_dir, name)
os.makedirs(save_dir, exist_ok=True)
detector = cv2.FaceDetectorYN.create(
'model/face_detection_yunet_2023mar.onnx', '', (320, 320), 0.9
)
cap = cv2.VideoCapture(0)
count = 0
while count < target_count:
ret, frame = cap.read()
if not ret:
continue
h, w = frame.shape[:2]
detector.setInputSize((w, h))
_, faces = detector.detect(frame)
if faces is not None and len(faces) > 0:
x, y, fw, fh = faces[0][:4].astype(int)
x, y = max(0, x), max(0, y)
cv2.rectangle(frame, (x, y), (x + fw, y + fh), (0, 255, 0), 2)
cv2.putText(frame, f'{count}/{target_count}', (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.imshow('collect', frame)
key = cv2.waitKey(1) & 0xFF
if key == ord(' ') and faces is not None:
x, y, fw, fh = faces[0][:4].astype(int)
face_roi = frame[y:y + fh, x:x + fw]
face_roi = cv2.resize(face_roi, (160, 160))
cv2.imwrite(os.path.join(save_dir, f'{count}.jpg'), face_roi)
count += 1
elif key == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
采集完之后,目录结构是这样的:
code复制dataset/
├── zhang_san/
│ ├── 0.jpg
│ ├── 1.jpg
│ └── ...
└── li_si/
├── 0.jpg
└── ...
4.2 LBPH识别器的原理与训练代码
LBPH 是 Local Binary Pattern Histogram(局部二值模式直方图)的缩写。它的思路是:把图像分成网格,对每个像素和它周围邻域的像素做大小比较,得到一个二进制编码,然后统计这些编码的分布直方图作为该人脸的特征向量。
识别时,把待识别图片的直方图特征和训练集里的人脸特征做距离度量,距离越小说明越相似。这个距离在 OpenCV 里被称作 confidence。所以 LBPH 的输出逻辑和其他模型相反:confidence 越小,相似度越高。
LBPH 对光照很敏感,所以训练之前一定要把图片转灰度,并且建议做直方图均衡化:
python复制import cv2
import os
import glob
images = []
labels = []
names = []
dataset_dir = 'dataset'
for label, name in enumerate(sorted(os.listdir(dataset_dir))):
names.append(name)
pattern = os.path.join(dataset_dir, name, '*.jpg')
for path in glob.glob(pattern):
img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
img = cv2.equalizeHist(img)
img = cv2.resize(img, (200, 200))
images.append(img)
labels.append(label)
recognizer = cv2.face.LBPHFaceRecognizer_create()
recognizer.train(images, labels)
recognizer.write('model/lbph_model.yml')
print('训练完成,类别映射:', names)
cv2.face 模块只有 contrib 版才有,如果这里报 AttributeError: module 'cv2' has no attribute 'face',说明你装的是基础版 opencv-python,改用 opencv-contrib-python 即可。
预测时:
python复制recognizer = cv2.face.LBPHFaceRecognizer_create()
recognizer.read('model/lbph_model.yml')
label, confidence = recognizer.predict(gray_face)
print(names[label], confidence)
如果 confidence 小于经验阈值(通常 80 到 100 之间),可以认为识别成功;如果大于这个范围,说明这个人不在库里,应归类为 unknown。
4.3 深度学习方案:SFACE的完整用法
如果 LBPH 的精度满足不了需求,可以直接切换到 OpenCV 官方另一个模型 SFace。SFace 是 OpenCV 人脸识别模型,输入对齐后的人脸图,输出一个 256 维的特征向量,然后通过比对向量的余弦相似度来判断是不是同一个人。
SFace 配合 YuNet 使用效果很好:YuNet 检测出人脸并给出关键点,SFace 的 alignCrop 会根据关键点将人脸对齐到标准姿态,再做特征提取。整体流程:
python复制import cv2
import numpy as np
sface = cv2.FaceRecognizerSF.create(
'model/face_recognition_sface_2021dec.onnx', ''
)
detector = cv2.FaceDetectorYN.create(
'model/face_detection_yunet_2023mar.onnx', '', (320, 320), 0.9
)
img = cv2.imread('test.jpg')
h, w = img.shape[:2]
detector.setInputSize((w, h))
_, faces = detector.detect(img)
if faces is not None:
for face in faces:
box = face[:4].astype(int)
aligned = sface.alignCrop(img, box) # 关键:人脸对齐
feature = sface.feature(aligned) # 提取256维特征
print(feature.shape)
注册阶段,对每个用户的多张人脸图提取特征,可以存成一个 numpy 数组,或者多个特征求平均后存一份。识别阶段,把当前帧的特征和库里所有特征逐一算余弦相似度,取最高分。SFace 的 match 方法内置了两种距离/相似度计算方式:FR_COSINE 是余弦相似度,值越大越相似;FR_NORM_L2 是归一化欧氏距离,值越小越相似。我这里用余弦相似度:
python复制similarity = sface.match(
feature_current,
feature_registered,
cv2.face.FaceRecognizerSF_FR_COSINE
)
4.4 两套方案怎么选
我整理了 LBPH 和 SFace 的对比,方便你做技术选型:
| 对比项 | LBPH | YuNet + SFace |
|---|---|---|
| 训练环节 | 需要采集数据并训练模型 | 不需要训练,提取特征直接比对 |
| 特征表示 | 局部纹理直方图 | 256维深度学习特征向量 |
| 对光照/角度鲁棒性 | 弱 | 强 |
| CPU实时性 | 非常快 | 较快,普通CPU可跑 |
| 模型体积 | 几十KB到几百KB | 单个模型约几百KB到几MB |
| 适合场景 | 入门学习、低资源设备 | 门禁、考勤、真实产品 |
我的建议是:如果你只是学习流程、在嵌入式小设备上做 demo,用 LBPH;如果是想交付一个能真实用的门禁或考勤系统,直接上 YuNet + SFace。两条链路我都跑过,SFace 在稍微复杂的光照和角度下的识别成功率明显高一个档次,而且省去了训练环节,维护成本反而更低。
5. 完整项目:摄像头实时人脸识别
5.1 项目结构设计
项目代码如下,我按功能模块区分文件,方便后续扩展和维护:
code复制face_access/
├── model/ # 存放 onnx 模型和训练好的 yml
│ ├── face_detection_yunet_2023mar.onnx
│ └── face_recognition_sface_2021dec.onnx
├── dataset/ # 原始采集数据,按姓名分目录
├── features/ # 注册的 SFace 特征,按姓名存 npy
├── collect.py # 人脸采集
├── register.py # 生成注册特征库
├── recognize.py # 实时识别
└── config.py # 公共配置
5.2 核心代码实现
config.py 里统一管理路径和阈值:
python复制DETECTOR_PATH = 'model/face_detection_yunet_2023mar.onnx'
RECOGNIZER_PATH = 'model/face_recognition_sface_2021dec.onnx'
SCORE_THRESHOLD = 0.9
SIMILARITY_THRESHOLD = 0.363
register.py 的核心流程:读取 dataset 下的每个用户图片,检测并对齐,提取特征,保存到 features/:
python复制import cv2
import numpy as np
import os
detector = cv2.FaceDetectorYN.create(
'model/face_detection_yunet_2023mar.onnx', '', (320, 320), 0.9
)
sface = cv2.FaceRecognizerSF.create(
'model/face_recognition_sface_2021dec.onnx', ''
)
os.makedirs('features', exist_ok=True)
for name in os.listdir('dataset'):
user_dir = os.path.join('dataset', name)
if not os.path.isdir(user_dir):
continue
feats = []
for img_name in os.listdir(user_dir):
path = os.path.join(user_dir, img_name)
img = cv2.imread(path)
if img is None:
continue
h, w = img.shape[:2]
detector.setInputSize((w, h))
_, faces = detector.detect(img)
if faces is None:
continue
box = faces[0][:4].astype(int)
aligned = sface.alignCrop(img, box)
feat = sface.feature(aligned)
feats.append(feat)
if feats:
avg_feat = np.mean(feats, axis=0)
np.save(os.path.join('features', f'{name}.npy'), avg_feat)
print(f'{name} 注册完成,使用 {len(feats)} 张图')
注意这里只取了检测到的第一张脸,采集时尽量单人入镜,避免误注册。
recognize.py 是实时识别主程序:
python复制import cv2
import numpy as np
import os
detector = cv2.FaceDetectorYN.create(
'model/face_detection_yunet_2023mar.onnx', '', (320, 320), 0.9
)
sface = cv2.FaceRecognizerSF.create(
'model/face_recognition_sface_2021dec.onnx', ''
)
embeddings = {}
for f in os.listdir('features'):
if f.endswith('.npy'):
embeddings[f[:-4]] = np.load(os.path.join('features', f))
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
h, w = frame.shape[:2]
detector.setInputSize((w, h))
_, faces = detector.detect(frame)
if faces is not None:
for face in faces:
x, y, fw, fh = face[:4].astype(int)
x, y = max(0, x), max(0, y)
aligned = sface.alignCrop(frame, face[:4].astype(int))
feat = sface.feature(aligned)
best_name = 'unknown'
best_score = -1
for name, emb in embeddings.items():
score = sface.match(feat, emb, cv2.face.FaceRecognizerSF_FR_COSINE)
if score > best_score:
best_score = score
best_name = name
if best_score < 0.363:
best_name = 'unknown'
cv2.rectangle(frame, (x, y), (x + fw, y + fh), (0, 255, 0), 2)
label_text = f'{best_name} {best_score:.2f}'
cv2.putText(frame, label_text, (x, y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv2.imshow('face access', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
跑起来后,站在摄像头前能看到自己的名字和相似度分数,整体体验已经接近一个真实门禁的雏形。
5.3 阈值怎么调到靠谱
阈值的设定是整个项目中最容易被忽略、却最影响体验的部分。太严会把本人识别成 unknown,太松会把陌生人放进来。
我的调参思路是:准备一组正样本(本人现场采集若干帧)和一组负样本(同事或陌生人若干帧),计算出所有正样本的相似度分布和负样本的相似度分布,然后取两个分布之间的一个值作为阈值。如果正样本的相似度集中在 0.4 到 0.55,负样本集中在 0.15 到 0.25,那么 0.35 就是一个比较合理的初始阈值。
实际项目中,OpenCV 官方 SFace demo 用的阈值是 0.363,但不同摄像头、不同光线、不同人种都会让这个数值浮动,所以不能无脑照搬。建议把识别结果实时打印出来,观察 20 到 30 次识别中"本人最低分"和"陌生人最高分"分别在哪,再确定阈值。
5.4 实时性能优化
这套流程在普通笔记本 CPU 上跑到 20 到 30 fps 问题不大,但如果是性能较弱的设备,有几个优化手段:
- 降分辨率:摄像头输出设成 640x480,已经足够个人门禁使用。
- 跳帧检测:每 2 到 3 帧做一次完整检测和识别,中间帧沿用上一帧的结果,能显著降低 CPU 占用。
- 只识别检测框内区域:比如画面上同时出现 5 个人脸,但只有 1 个需要识别,可以根据业务逻辑只对最靠近画面中心的人脸做特征提取。
- 用 headless 方案跑服务:如果不需要
imshow显示画面,直接跑在无 GUI 环境,减少图形渲染开销。
6. 实战踩坑:环境、模型与应用中的细节
6.1 cv2.error: The function/feature is not implemented
这是搜索热度非常高的一个报错。它的完整信息一般是:
code复制cv2.error: OpenCV(4.x.x) ... The function/feature is not implemented ...
我排查下来,绝大多数情况是模块缺失。比如在安装基础版 opencv-python 的前提下调用了 cv2.face 模块,OpenCV 内部根本没有这个功能,就会抛这个错误。解决方式前面说过:换成 opencv-contrib-python 或 headless 对应版本。
另一种情况是 DNN 模块加载特定后端失败。它在某些编译版本里确实不包含某些加速推理后端(如 CUDA 或 OpenVINO),代码本身没写错,但函数报 not implemented。遇到时先把后端切回 CPU 默认后端,确认流程没问题后再考虑加速。
还有一个容易混淆的版本问题:旧版 API 里创建 LBPH 识别器用的是 cv2.face.createLBPHFaceRecognizer(),新版变成了 cv2.face.LBPHFaceRecognizer_create()。如果你在网上看到的老教程代码直接搬过来,会报 module has no attribute。注意看当前环境的 OpenCV 版本,按新版 API 写。
6.2 摄像头打不开与画面异常
cap = cv2.VideoCapture(0) 之后 cap.isOpened() 返回 False,通常有三个原因。
第一个是设备索引不对。笔记本内置摄像头一般是 0,外接 USB 摄像头可能是 1 或 2。你可以写个循环依次尝试 0 到 4,直到某个索引能读到帧。
第二个是 Windows 下某些摄像头被其他程序占用,或者 V4L2 协议不兼容。Windows 上可以指定后端:
python复制cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
加上 CAP_DSHOW 后,很多 DirectShow 兼容性问题的概率会降低。Linux 下如果打不开,先确认 /dev/video* 设备文件存在,以及当前用户是否有权限访问。
第三个是摄像头可以打开但帧率很低。设置分辨率时注意,有的设备不支持任意分辨率,取最近的可用值:
python复制cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
设置完读一下实际值,验证是否生效。
6.3 LBPH置信度忽高忽低
用 LBPH 识别时,同一个人的 confidence 有时会从 50 跳到 120,导致一会识别成功一会识别失败。这个问题几乎都和训练数据、图像预处理有关。
首先,训练集必须覆盖不同的面部姿态,不能全是同一个位置的截图。第二,预测前一定要做和训练时一致的预处理:灰度化、直方图均衡化、缩放对齐。如果训练时先把图缩到 200x200 再训练,预测时也要缩放成 200x200,尺寸不一致会直接影响直方图统计。
第三,LBPH 本身对光照非常敏感,如果识别现场的光线和训练时差异太大,confidence 会大幅波动。我后来在 LBPH 链路里加了 cv2.equalizeHist,情况有明显改善,但不能完全消除。真要做实际项目,还是建议切到 SFace。
6.4 实时识别卡顿
如果识别画面肉眼可见地掉帧,先排查是不是在高分辨率图上做了检测。imshow 全屏输出本身也会占资源,先把窗口调小,再看 CPU 占用的位置。
另一个常见瓶颈是特征比对循环。如果注册用户很多,每个用户还保存多张特征图,每次识别都要做几十甚至是上百次 alignCrop 和 feature 提取,这会非常慢。优化方式是把每个用户的多张特征预先取平均,注册阶段直接存一份特征向量,识别时做 1 次提取和 N 次 match。match 本身是轻量向量运算,几十个用户不会有明显卡顿。
还有一点,OpenCV 的 DNN 检测器在每帧调用时会重新做前向推理,如果在性能很弱的设备上跑,可以把检测间隔拉长,比如每 3 帧检测一次,中间帧复用上一次的人脸框坐标,这样能明显提升流畅度。
做这个项目时我最大的感受是:如果你只是跑通官网示例,人脸识别的成功率看着很高,但一旦放上真实摄像头,光照、角度、遮挡、设备差异每个都会让成功率往下掉。想把它当产品交付,时间至少要留一半在采集样本和调阈值上,模型代码反而是最简单的部分。
