从零开始训练 OpenCV Haar 级联分类器:猫脸检测实战指南

【精选优质专栏推荐】


每个专栏均配有案例与图文讲解,循序渐进,适合新手与进阶学习者,欢迎订阅。

在这里插入图片描述

使用 OpenCV 中的 Haar 级联分类器很简单。你只需提供一个 XML 文件中的训练模型即可创建分类器。然而,从零开始训练一个分类器并不是那么直接。在本教程中,你将看到训练过程应该是怎样的。具体来说,你将学习到:

  • 在 OpenCV 中训练 Haar 级联的工具是什么
  • 如何准备训练数据
  • 如何执行训练

本文分为五个部分:

  1. OpenCV 中训练级联分类器的问题
  2. 环境设置
  3. 级联分类器训练概述
  4. 准备训练数据
  5. 训练 Haar 级联分类器

OpenCV 中训练级联分类器的问题

OpenCV 已经存在多年,并有多个版本。在撰写本文时,OpenCV 5 正在开发中,推荐使用 OpenCV 4,确切来说是 4.8.0 版本。

OpenCV 3 与 OpenCV 4 之间进行了大量清理工作,最显著的是大量代码被重写。改动非常大,很多函数都发生了变化,其中包括用于训练 Haar 级联分类器的工具。

级联分类器不是像 SVM 那样可以轻松训练的简单模型。它是一个使用 AdaBoost 的集成模型。因此,训练涉及多个步骤。OpenCV 3 提供了命令行工具来帮助完成此类训练,但该工具在 OpenCV 4 中已经损坏,尚未有修复方法。

因此,只能使用 OpenCV 3 来训练 Haar 级联分类器。幸运的是,训练完成后你可以弃用它,并在保存模型为 XML 文件后回到 OpenCV 4。本教程就是按这种方式进行的。

在 Python 中,OpenCV 3 和 OpenCV 4 不能共存。因此,建议为训练创建一个单独的环境。在 Python 中,可以使用 venv 模块创建虚拟环境,即创建一套独立的已安装模块。其他方案是使用 Anaconda 或 Pyenv,它们的架构不同但理念相同。在这些方案中,Anaconda 环境是最便捷的选择。

环境设置

如果使用 Anaconda,会更简单。可以使用以下命令创建并使用一个新环境,命名为 cvtrain:

conda create -n cvtrain python 'opencv>=3,<4'
conda activate cvtrain

如果命令 opencv_traincascade 可用,则说明环境准备就绪:

$ opencv_traincascade
Usage: opencv_traincascade
  -data <cascade_dir_name>
  -vec <vec_file_name>
  -bg <background_file_name>
  [-numPos <number_of_positive_samples = 2000>]
  [-numNeg <number_of_negative_samples = 1000>]
  [-numStages <number_of_stages = 20>]
  [-precalcValBufSize <precalculated_vals_buffer_size_in_Mb = 1024>]
  [-precalcIdxBufSize <precalculated_idxs_buffer_size_in_Mb = 1024>]
  [-baseFormatSave]
  [-numThreads <max_number_of_threads = 16>]
  [-acceptanceRatioBreakValue <value> = -1>]
--cascadeParams--
  [-stageType <BOOST(default)>]
  [-featureType <{HAAR(default), LBP, HOG}>]
  [-w <sampleWidth = 24>]
  [-h <sampleHeight = 24>]
--boostParams--
  [-bt <{DAB, RAB, LB, GAB(default)}>]
  [-minHitRate <min_hit_rate> = 0.995>]
  [-maxFalseAlarmRate <max_false_alarm_rate = 0.5>]
  [-weightTrimRate <weight_trim_rate = 0.95>]
  [-maxDepth <max_depth_of_weak_tree = 1>]
  [-maxWeakCount <max_weak_tree_count = 100>]
--haarFeatureParams--
  [-mode <BASIC(default) | CORE | ALL
--lbpFeatureParams--
--HOGFeatureParams--

如果使用 pyenv 或 venv,需要更多步骤。首先,创建环境并安装 OpenCV(注意与 Anaconda 的包名不同):

# 创建环境并安装 OpenCV 3
pyenv virtualenv 3.11 cvtrain
pyenv activate cvtrain
pip install 'opencv-python>=3,<4'

这允许你使用 OpenCV 运行 Python 程序,但没有训练的命令行工具。要获取这些工具,需要从源码编译:

1.下载 OpenCV 源码并切换到 3.4 分支:

git clone https://github.com/opencv/opencv
cd opencv
git checkout 3.4
cd ..

2.创建独立于源码目录的构建目录:

mkdir build
cd build

3.使用 cmake 准备构建目录,并指向 OpenCV 仓库:

cmake ../opencv

4.编译(可能需要先安装开发库):

make
ls bin

所需工具会在 bin/ 目录下。命令行工具包括 opencv_traincascade 和 opencv_createsamples。本文后续假设这些工具可用。

级联分类器训练概述

你将使用 OpenCV 工具训练一个级联分类器。该分类器是一个使用 AdaBoost 的集成模型。简单来说,会创建多个弱分类器,每个单独分类能力较弱,但组合后可以成为精度和召回率都较高的强分类器。

每个弱分类器都是二分类器。训练时,需要正样本和负样本。负样本很容易:提供一些随机图片,让 OpenCV 选择矩形区域(最好图片中没有目标对象)。正样本需要提供图像及其对象所在的边界框,使对象正好位于框内。

提供数据集后,OpenCV 会从正负样本中提取 Haar 特征并用它们训练多个分类器。Haar 特征来源于将样本划分为矩形区域,划分方式带有一定随机性,因此 OpenCV 需要时间找到最佳划分方式以提取 Haar 特征。

在 OpenCV 中,只需提供图像文件格式的训练数据(如 JPEG 或 PNG)。负样本只需一个包含文件名的文本文件。正样本需要“信息文件”,即包含文件名、图像中对象数量及对应边界框的纯文本文件。

正样本应为二进制格式。OpenCV 提供 opencv_createsamples 工具将“信息文件”生成二进制格式。然后,将正样本与负样本一起提供给 opencv_traincascade 工具进行训练,并生成 XML 文件格式的模型,这就是可以加载到 OpenCV Haar 级联分类器的文件。

准备训练数据

以创建猫脸检测器为例。训练此检测器前,需要数据集。可使用 Oxford-IIIT Pet Dataset:

https://www.robots.ox.ac.uk/~vgg/data/pets/

这是一个 800MB 的数据集,对于计算机视觉数据集来说算小。图片采用 Pascal VOC 格式标注,每张图片有对应的 XML 文件,例如:

<?xml version="1.0"?>
<annotation>
  <folder>OXIIIT</folder>
  <filename>Abyssinian_100.jpg</filename>
  <source>
    <database>OXFORD-IIIT Pet Dataset</database>
    <annotation>OXIIIT</annotation>
    <image>flickr</image>
  </source>
  <size>
    <width>394</width>
    <height>500</height>
    <depth>3</depth>
  </size>
  <segmented>0</segmented>
  <object>
    <name>cat</name>
    <pose>Frontal</pose>
    <truncated>0</truncated>
    <occluded>0</occluded>
    <bndbox>
      <xmin>151</xmin>
      <ymin>71</ymin>
      <xmax>335</xmax>
      <ymax>267</ymax>
    </bndbox>
    <difficult>0</difficult>
  </object>
</annotation>

XML 文件说明了图片文件名(示例中为 Abyssinian_100.jpg)及其中对象,边界框在 <bndbox></bndbox> 标签内。

提取 XML 文件中的边界框可使用以下函数:

import xml.etree.ElementTree as ET

def read_voc_xml(xmlfile: str) -> dict:
    root = ET.parse(xmlfile).getroot()
    boxes = {"filename": root.find("filename").text,
             "objects": []}
    for box in root.iter('object'):
        bb = box.find('bndbox')
        obj = {
            "name": box.find('name').text,
            "xmin": int(bb.find("xmin").text),
            "ymin": int(bb.find("ymin").text),
            "xmax": int(bb.find("xmax").text),
            "ymax": int(bb.find("ymax").text),
        }
        boxes["objects"].append(obj)

    return boxes

函数返回的字典示例:

{'filename': 'yorkshire_terrier_160.jpg',
 'objects': [{'name': 'dog', 'xmax': 290, 'xmin': 97, 'ymax': 245, 'ymin': 18}]}

有了这些数据,可以轻松创建训练数据集:Oxford-IIIT Pet 数据集中照片为猫或狗,可将狗照片作为负样本,猫照片作为正样本并设置合适的边界框。

OpenCV 对正样本的“信息文件”要求为纯文本,每行格式如下:

filename N x0 y0 w0 h0 x1 y1 w1 h1 ...

文件名后面的数字 N 表示该图像中边界框的数量,每个边界框是一个正样本。随后依次为每个边界框的像素坐标(左上角)及宽度和高度。为了 Haar 级联分类器效果最佳,边界框应与模型预期的纵横比一致。

假设你下载的 Pet 数据集位于 dataset/ 目录下,文件组织如下:

dataset
|-- annotations
|   |-- README
|   |-- list.txt
|   |-- test.txt
|   |-- trainval.txt
|   |-- trimaps
|   |   |-- Abyssinian_1.png
|   |   |-- Abyssinian_10.png
|   |   ...
|   |   |-- yorkshire_terrier_98.png
|   |   `-- yorkshire_terrier_99.png
|   `-- xmls
|       |-- Abyssinian_1.xml
|       |-- Abyssinian_10.xml
|       ...
|       |-- yorkshire_terrier_189.xml
|       `-- yorkshire_terrier_190.xml
`-- images
    |-- Abyssinian_1.jpg
    |-- Abyssinian_10.jpg
    ...
    |-- yorkshire_terrier_98.jpg
    `-- yorkshire_terrier_99.jpg

有了这些文件,可以轻松创建正样本的“信息文件”和负样本文件列表,使用如下程序:

import pathlib
import xml.etree.ElementTree as ET
import numpy as np

def read_voc_xml(xmlfile: str) -> dict:
    """读取 Pascal VOC XML 并返回 (filename, 对象名称, 边界框)
    边界框是 (xmin, ymin, xmax, ymax) 向量,像素坐标为 1 起始。
    """
    root = ET.parse(xmlfile).getroot()
    boxes = {"filename": root.find("filename").text,
             "objects": []}
    for box in root.iter('object'):
        bb = box.find('bndbox')
        obj = {
            "name": box.find('name').text,
            "xmin": int(bb.find("xmin").text),
            "ymin": int(bb.find("ymin").text),
            "xmax": int(bb.find("xmax").text),
            "ymax": int(bb.find("ymax").text),
        }
        boxes["objects"].append(obj)
    return boxes

# 读取 Pascal VOC 并写入数据
base_path = pathlib.Path("dataset")
img_src = base_path / "images"
ann_src = base_path / "annotations" / "xmls"

negative = []
positive = []

for xmlfile in ann_src.glob("*.xml"):
    ann = read_voc_xml(str(xmlfile))
    if ann['objects'][0]['name'] == 'dog':
        negative.append(str(img_src / ann['filename']))
    else:
        bbox = []
        for obj in ann['objects']:
            x = obj['xmin']
            y = obj['ymin']
            w = obj['xmax'] - obj['xmin']
            h = obj['ymax'] - obj['ymin']
            bbox.append(f"{x} {y} {w} {h}")
        line = f"{str(img_src/ann['filename'])} {len(bbox)} {' '.join(bbox)}"
        positive.append(line)

# 写入 `negative.dat` 和 `positive.dat`
with open("negative.dat", "w") as fp:
    fp.write("\n".join(negative))

with open("positive.dat", "w") as fp:
    fp.write("\n".join(positive))

该程序扫描数据集中的所有 XML 文件,提取猫照片的边界框。negative 列表保存狗照片路径,positive 列表保存猫照片路径及边界框,每行一条记录。循环完成后,将这两个列表写入磁盘,分别生成 negative.dat 和 positive.dat。

negative.dat 内容简单,positive.dat 示例:

dataset/images/Siamese_102.jpg 1 154 92 194 176
dataset/images/Bengal_152.jpg 1 84 8 187 201
dataset/images/Abyssinian_195.jpg 1 8 6 109 115
dataset/images/Russian_Blue_135.jpg 1 228 90 103 117
dataset/images/Persian_122.jpg 1 60 16 230 228

在运行训练前,需要将 positive.dat 转换为二进制格式,命令如下:

opencv_createsamples -info positive.dat -vec positive.vec -w 30 -h 30

该命令应在 positive.dat 同目录下运行,以便找到数据集图片。输出为 positive.vec 文件,也称“vec 文件”。-w 和 -h 参数指定窗口宽高,用于将边界框裁剪的图像调整为此尺寸,训练时也需保持一致。

训练 Haar 级联分类器

训练分类器需要时间,分多个阶段进行。每个阶段会写入中间文件,完成所有阶段后,训练好的模型会保存为 XML 文件。OpenCV 要求将所有生成文件存放在一个目录中。

训练过程很直接。创建一个新目录 cat_detect 存放生成文件:

mkdir cat_detect
opencv_traincascade -data cat_detect -vec positive.vec -bg negative.dat -numPos 900 -numNeg 2000 -numStages 10 -w 30 -h 30

注意使用 positive.vec 作为正样本,negative.dat 作为负样本,-w 和 -h 与之前 opencv_createsamples 命令一致。其他参数说明:

  • -data <dirname>:训练好的分类器存放目录,需事先创建
  • -vec <filename>:正样本 vec 文件
  • -bg <filename>:负样本列表,也称背景图像
  • -numPos <N>:每阶段训练使用的正样本数量
  • -numNeg <N>:每阶段训练使用的负样本数量
  • -numStages <N>:训练的级联阶段数
  • -w <width>-h <height>:对象像素尺寸,需与创建训练样本时一致
  • -minHitRate <rate>:每阶段期望的最小真实率
  • -maxFalseAlarmRate <rate>:每阶段期望的最大误报率
  • -maxDepth <N>:弱分类树最大深度
  • -maxWeakCount <N>:每阶段弱分类树最大数量

训练期间输出示例:

$ opencv_traincascade -data cat_detect -vec positive.vec -bg negative.dat -numPos 900 -numNeg 2000 -numStages 10 -w 30 -h 30
PARAMETERS:
cascadeDirName: cat_detect
vecFileName: positive.vec
bgFileName: negative.dat
numPos: 900
numNeg: 2000
numStages: 10
precalcValBufSize[Mb] : 1024
precalcIdxBufSize[Mb] : 1024
acceptanceRatioBreakValue : -1
stageType: BOOST
featureType: HAAR
sampleWidth: 30
sampleHeight: 30
boostType: GAB
minHitRate: 0.995
maxFalseAlarmRate: 0.5
weightTrimRate: 0.95
maxDepth: 1
maxWeakCount: 100
mode: BASIC
Number of unique features given windowSize [30,30] : 394725
...

训练阶段编号从 0 到 N-1,每阶段可能训练时间不同。每阶段会打印表格,包括特征编号 N、命中率 HR(真阳性率)、误报率 FA(假阳性率)。训练时,每阶段会找到大量 Haar 特征,以确保命中率 ≥ 0.995,误报率 ≤ 0.5。理论上,n 阶段的整体命中率约为 pⁿ,整体误报率约为 qⁿ。

在现代电脑上,训练命令完成约需 3 小时,输出文件名为 cascade.xml。可使用以下示例代码验证结果:

import cv2

image = 'dataset/images/Abyssinian_88.jpg'
model = 'cat_detect/cascade.xml'

classifier = cv2.CascadeClassifier(model)
img = cv2.imread(image)

# 转为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 对象检测
objects = classifier.detectMultiScale(gray,
                                      scaleFactor=1.1, minNeighbors=5,
                                      minSize=(30, 30))

# 绘制矩形框
for (x, y, w, h) in objects:
    cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2)

# 显示结果
cv2.imshow('Object Detection', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

检测结果取决于训练模型的效果,以及传入 detectMultiScale() 的参数。

上面的代码会在数据集中的一张图像上运行检测器,你可能会看到如下结果:

在这里插入图片描述

你会看到一些误报,但猫的脸已经被检测出来。有多种方法可以提高检测质量。例如,上面使用的训练数据集并未使用正方形边界框,而你在训练和检测时使用了正方形。调整数据集可能会有所改善。同样,训练命令行中使用的其他参数也会影响结果。然而,你需要注意,Haar 级联检测器虽然非常快速,但使用的阶段越多,速度就会越慢。

总结

Haar 级联分类器训练的流程包括准备正负样本,将正样本整理为信息文件并转换为二进制 .vec 文件,然后使用 opencv_traincascade 进行多阶段训练,每个阶段生成中间文件并提取 Haar 特征,最终保存为 XML 模型文件。训练完成后可以用 CascadeClassifier 对图像进行检测,检测效果受训练数据、窗口大小和训练参数影响。通过调整数据集和训练参数,可以提高检测准确率,但增加阶段会降低检测速度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋说

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值