🍓 第十一章 AI 实验范例(一):计算机视觉+AI 识别-OpenCV+YOLO
📄 一句话概括

用 Haar 级联找脸:先掰正语义——OpenCV 级联做的是【人脸检测】(这里有一张脸),不是【人脸识别】(这是谁的脸);十行代码让摄像头在画面里框出人脸。

📝 本节和前面几节的关系

11-3 的基本功(灰度、数组、画框用的坐标规矩)在这里合体:转灰度 → 级联检测 → 画框,三个动作各是学过的一把刀。11-1 的环境、10-4 的 picamera2 供帧继续当基建。这也是全课第一次【模型文件】登场——它在硬盘上、由 OpenCV 读进内存跑推理,思路与 11-5 的 YOLO 一脉相承。

一、先掰正语义:检测 ≠ 识别

教材标题说【人脸识别】,但 OpenCV 级联干的事严格说是人脸检测(face detection):

为什么要掰正:所有报错排查、所有进阶资料的搜索词都依赖这个区分;拿【识别】的教程去解决【检测】的问题,方向就错了。同 8-4 掰正【用 Python 开发 Arduino】一样:语义不清,后面的坑全讲不通。

二、Haar 级联:二十岁的老算法怎么找脸

Haar 级联分类器(2001 年提出)的原理一句话:脸有稳定的亮暗模式(眼窝比脸颊暗、鼻梁比两侧亮),算法用几百个小【亮暗模板】在图片的各个位置、各个大小上滑动比对,命中足够多就报【这里有脸】。

三个要点:

三、模型文件在哪:路径的正确打开方式

OpenCV 自带十几个训练好的级联文件(人脸、眼睛、猫脸、车身……)。找它们的正确姿势:

import cv2
print(cv2.data.haarcascades)
# apt 安装版通常输出:/usr/share/opencv4/haarcascades/
# 终端确认文件真实存在
ls /usr/share/opencv4/haarcascades/ | grep frontal

坑在哪:老教程常硬编码一个绝对路径(/usr/share/opencv/haarcascades/...),不同系统、不同安装方式路径千差万别——一律改用 cv2.data.haarcascades 拼接文件名,OpenCV 自己知道数据在哪。加载后用 .empty() 判断是否加载成功(路径错时它不报异常,只安静地返回空分类器)。

四、核心 API:detectMultiScale 三参数

faces = face_cascade.detectMultiScale(
    gray,            # 输入:灰度图(11-3 的第一把刀)
    scaleFactor=1.1, # 每轮把搜索窗口放大 10%(图里远近不同的脸各归各档)
    minNeighbors=5,  # 至少 5 个邻居窗口都喊【是脸】才算数——越大越严、误报越少
    minSize=(30, 30) # 比 30×30 像素还小的目标不找——过滤远处噪声
)
# 返回 [[x, y, w, h], ...]:每个脸框的左上角坐标与宽高

返回值直接能画框:cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)——绿色(BGR 的 0,255,0!)粗 2 像素。

调参手感:乱报(没有脸也画框)→ 加大 minNeighbors;漏报(有脸不画)→ 减小它或降低 minSize。检测是概率游戏,参数在【严】与【松】之间找平衡。

五、动手 Demo:照片版与实时版

照片版(拿 10-3 拍的 snap.jpg 或自备一张带正脸的照片):

# face_photo.py
import cv2

cascade = cv2.CascadeClassifier(
    cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
if cascade.empty():
    raise SystemExit("级联文件加载失败,先查 cv2.data.haarcascades 路径")

img = cv2.imread("snap.jpg")
if img is None:
    raise SystemExit("照片没读到,检查路径(imread 失败返回 None,11-3 的坑)")

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = cascade.detectMultiScale(gray, scaleFactor=1.1,
                                 minNeighbors=5, minSize=(30, 30))
print("检测到", len(faces), "张脸")
for (x, y, w, h) in faces:
    cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.imwrite("snap_faces.jpg", img)

实时版(picamera2 供帧,循环检测):

# face_live.py
import cv2
from picamera2 import Picamera2

cascade = cv2.CascadeClassifier(
    cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
picam2 = Picamera2()
picam2.configure(picam2.create_video_configuration())
picam2.start()

for i in range(20):                      # 无头环境:检测 20 帧打印结果
    frame = picam2.capture_array()
    frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    faces = cascade.detectMultiScale(gray, 1.1, 5, minSize=(30, 30))
    print("第", i, "帧:", len(faces), "张脸",
          faces.tolist() if len(faces) else "")
picam2.stop()

看到什么算成功:照片版打印【检测到 N 张脸】(N ≥ 1,对着镜头正脸拍一般 N=1),snap_faces.jpg 里脸上多了绿框;实时版对着镜头跑,脸入画时每帧打印 1 组坐标,离开画面变 0。

读结果:坐标 faces 每帧会轻微跳动——级联逐帧独立判断,没有记忆;【框抖】是正常现象,要平滑得自己加滤波(进阶话题)。检测不到时按第 3、4 节排:正脸吗?光线够吗?minNeighbors 是不是调太严了?

六、老教程哪里容易翻车

  1. 硬编码级联文件路径 → 换 cv2.data.haarcascades 拼接(本节第三节)。
  2. pip 装的 OpenCV 有时不带级联数据 → 用 apt 版(11-2 的正路自动配齐);ls 一眼确认。
  3. CascadeClassifier 路径错不报异常、只安静返回空分类器,直到 detectMultiScale 才炸——加载后立刻 .empty() 检查。
  4. 喂彩色图给 detectMultiScale → 它要灰度图;不转换在新版会报错或行为异常。
  5. 把【检测不到脸】当故障 → 先确认正脸、光线、距离(minSize 过滤);再动参数。

七、30 秒实验:实时检测的帧率账

实测本节实时版在树莓派上单帧检测约 0.2 秒:

  1. 每秒能处理几帧?
  2. 想做【有人靠近就拍照】的门铃,够用吗?

参考答案:1. 1 ÷ 0.2 = 5 FPS——比摄像头 30 帧的产出慢得多,所以实时程序常【隔帧检测】(每 3~5 帧检一次,中间帧沿用上次结果)。2. 够用:门铃这类【人有没有在】的判断对延迟不敏感,5 FPS 绰绰有余;要【流畅画框】才需要提帧率(缩小分辨率是最快的提速手段)。

八、常见误区

  1. 【这叫人脸识别】 —— 严格说是人脸检测【这里有脸】;识别【这是谁】需要另外训练身份模型。
  2. 【检测不到=代码错了】 —— 级联只认正脸;侧脸、逆光、太远(小于 minSize)都检不出,先摆好再怀疑代码。
  3. 【路径随便写】 —— 硬编码路径是老教程毒瘤;永远 cv2.data.haarcascades 拼接,并加载后 .empty() 检查。
  4. 【minNeighbors 越大越好】 —— 越大误报越少但漏报越多;门铃要【宁可错杀】调小、考勤要【绝不误报】调大,按场景调参。
  5. 【级联是最好的找脸算法】 —— 它是最快上手的老算法;深度学习方案(如 OpenCV 自带的 DNN 人脸模型)精度更高但更吃算力——入门用级联,产品级再升级。
自测三问(点开对答案) 1. 检测和识别的区别?本节做的是哪个? 2. `detectMultiScale` 返回什么结构?怎么用它画框? 3. 实时版画面里框为什么会抖? **参考答案** 1. 检测=【有没有、在哪】(框);识别=【是谁】(身份)。本节是检测。 2. 返回 NumPy 数组,每行一个 `[x, y, w, h]`;`cv2.rectangle(img, (x, y), (x+w, y+h), (0,255,0), 2)` 逐个画。 3. 级联**逐帧独立判断、没有记忆**,每帧的框位置各自算各自的最优,轻微跳动正常;平滑要自己加滤波。

相关笔记