【精选优质专栏推荐】
- 《AI 技术前沿》 —— 紧跟 AI 最新趋势与应用
- 《网络安全新手快速入门(附漏洞挖掘案例)》 —— 零基础安全入门必看
- 《BurpSuite 入门教程(附实战图文)》 —— 渗透测试必备工具详解
- 《网安渗透工具使用教程(全)》 —— 一站式工具手册
- 《CTF 新手入门实战教程》 —— 从题目讲解到实战技巧
- 《前后端项目开发(新手必知必会)》 —— 实战驱动快速上手
每个专栏均配有案例与图文讲解,循序渐进,适合新手与进阶学习者,欢迎订阅。

使用 OpenCV 中的 Haar 级联分类器很简单。你只需提供一个 XML 文件中的训练模型即可创建分类器。然而,从零开始训练一个分类器并不是那么直接。在本教程中,你将看到训练过程应该是怎样的。具体来说,你将学习到:
- 在 OpenCV 中训练 Haar 级联的工具是什么
- 如何准备训练数据
- 如何执行训练
本文分为五个部分:
- OpenCV 中训练级联分类器的问题
- 环境设置
- 级联分类器训练概述
- 准备训练数据
- 训练 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 对图像进行检测,检测效果受训练数据、窗口大小和训练参数影响。通过调整数据集和训练参数,可以提高检测准确率,但增加阶段会降低检测速度。

308

被折叠的 条评论
为什么被折叠?



