4.9 基于深度学习的目标检测
使用YOLO、卷积神经网络(CNN)和循环神经网络(RNN)等机器学习或狠毒学习技术进行目标检测和跟踪。这些模型可以学习目标的特征,并在视频帧序列中进行目标跟踪。
实例4-8:基于YOLOv8的汽车追踪系统(codes/2/yolov8-for-car-tracking.ipynb)
4.9.1 项目介绍
本项目是一个基于 YOLO 模型的目标检测和目标跟踪系统,本项目结合了目标检测和目标跟踪的任务,通过 YOLO 模型实现了对车辆的检测和跟踪。本项目使用的 YOLOv8 模型是YOLO 模型中的一版本,是一个常用的目标检测模型,通过训练和调整参数,可以适应不同的数据集和任务。整个流程展示了从数据准备到模型训练、推断和结果分析的完整项目流程。
本项目的主要实现步骤和功能如下所示:
- 数据集准备:项目开始时,使用了一个数据集,该数据集包含了视频中的车辆,并提供了每个帧中车辆的位置和类别标签。数据集通过解析 XML 格式的注释文件进行读取,包括每个帧的文件路径、宽度、高度以及每个车辆框的坐标和标签信息。
- 数据集转换为 YOLO 格式:为了适应 YOLO 模型的训练需求,数据集被转换为 YOLO 格式。这包括将车辆的位置信息转换为 YOLO 框的格式,并将数据集划分为训练、验证和测试集。
- 模型训练:使用 YOLO 模型进行目标检测和目标跟踪。项目中使用了 YOLOv8 模型,通过加载预训练权重进行模型初始化,并在准备好的数据集上进行了训练。模型训练的参数包括训练轮数、图像大小、批次大小等。
- 目标检测:使用训练好的 YOLO 模型对测试集中的图像进行目标检测。检测结果包括边界框和类别标签,将结果绘制在图像上。
- 目标跟踪:在整个测试集上使用 YOLO 模型进行目标跟踪。通过跟踪算法,每个目标在不同帧上被唯一标识,并在每一帧上进行目标跟踪。
- 后处理和结果分析:对跟踪结果进行后处理,确保每个跟踪ID被分配为一个最常见的类别。最后,通过在图像上绘制跟踪结果,进行结果可视化。
4.9.2 具体实现
实例文件yolov8-for-car-tracking.ipynb的具体实现流程如下所示。
(1)设置目标检测项目的配置参数,这些配置参数用于定义训练和评估目标检测模型的设置。具体实现代码如下所示。
# 数据集目录
DATASET_DIR = "input/cars-video-object-tracking"
# 验证集和测试集的百分比
VAL_PERCENT, TEST_PERCENT = 0.1, 0.05 # 根据这些值的乘以100,将数据集划分为训练、验证和测试集。TRAIN_PERCENT = 1 - (VAL_PERCENT + TEST_PERCENT)
# YOLO格式的数据集保存目录
YOLO_DATASET_DIR = "working/yolo_dataset" # 用于保存以YOLO格式存储的数据集的目录
# YOLO模型的检查点名称
YOLO_CHECKPOINT = "yolov8s"
# 训练的轮数
EPOCHS = 8 # 训练轮数,一轮是对整个数据集的一次完整遍历。使用的轮数取决于数据集的大小。太多轮可能导致过拟合,可以通过监控训练过程中的验证损失来检测。太少轮可能导致欠拟合,可以通过损失的持续"急剧"下降来识别。
# 用于前向传播的图像大小
IMGSZ = 640 # 用于前向传播的图像的大小。较大的大小意味着对小细节的更多关注,因此在对象很小的图像中有更好的质量。大图像大小在训练期间需要大量计算资源。
# 每次迭代在一个GPU上的训练批次大小
TRAIN_BATCH_SIZE = 16 # 每次迭代在一个GPU上的训练批次大小。最好是2的幂次方(2、4、8)
# 名义批次的大小(由训练批次大小划分)
NOMINAL_BATCH_SIZE = 64 # 名义批次的大小(通过训练批次大小进行划分)
(2)定义两个字典,用于将目标检测任务中的类别标签(label)映射到整数标识(id)以及反向映射。这样的映射通常用于将类别标签转换为模型可以处理的格式,或者在评估模型时将模型输出的整数标识转换为易读的类别标签。
label2id = {"car": 0, "minivan": 1}
id2label = {v: k for k, v in label2id.items()}
对上述代码的具体说明如下:
- label2id:这是一个字典,将目标检测任务中的类别标签映射到整数标识。在这个例子中,"car" 对应的整数标识是 0,"minivan" 对应的整数标识是 1。这种映射通常在训练和评估模型时用于处理类别标签。
- id2label:这是 label2id 字典的反向映射。通过使用字典推导式,它将整数标识映射回原始的类别标签。例如,0 对应于 "car",1 对应于 "minivan"。这对于在模型的输出中将整数标识转换回类别标签是很有用的。
(3)解析目标检测数据集的注释文件(assumed to be in XML format),并将注释信息存储到一个字典中。
# 初始化一个空的字典用于存储注释信息
data = dict()
# 使用ElementTree库解析XML格式的注释文件
annotations = etree.parse(opj(DATASET_DIR, "annotations.xml")).getroot()
# 遍历所有的图像标注
for image in annotations.findall('image'):
# 获取图像文件路径,并将文件扩展名改为大写(可能是为了匹配文件扩展名的大小写)
filepath = opj(DATASET_DIR, image.attrib["name"]).replace(".png", ".PNG")
# 获取图像的宽度和高度
w_img, h_img = float(image.attrib['width']), float(image.attrib['height'])
# 初始化一个列表用于存储每个图像的目标框信息
boxes = []
# 遍历图像中的每个目标框
for box in image.findall('box'):
# 获取目标框的标签(类别)
label = box.attrib['label']
# 计算目标框的归一化坐标(相对于图像宽度和高度)
xtl, ytl = float(box.attrib["xtl"]) / w_img, float(box.attrib["ytl"]) / h_img
xbr, ybr = float(box.attrib["xbr"]) / w_img, float(box.attrib["ybr"]) / h_img
# 计算目标框的中心坐标和宽度、高度(YOLO格式)
xc, yc = (xbr + xtl) / 2, (ybr + ytl) / 2
yolo_box = [xc, yc, xbr - xtl, ybr - ytl]
# 将目标框的信息存储为列表,并添加到当前图像的目标框列表中
box_data = [label2id[label]] + yolo_box
boxes.append(box_data)
# 将当前图像的注释信息存储到数据字典中
data[filepath] = boxes
(4)将数据集划分为训练集、验证集和测试集,划分过程是根据预先定义的百分比进行的,且考虑到了数据集中的时间序列关系。这样的划分方式可以根据时间序列将验证集和测试集选择为顺序帧,以更好地反映实际使用情境。
# 获取所有图像文件路径
paths = list(data.keys())
# 计算训练集的划分点
train_split = int((1 - (VAL_PERCENT + TEST_PERCENT)) * len(paths))
# 划分训练集和非训练集
train_paths, nontrain_paths = paths[:train_split], paths[train_split:]
# 计算验证集和测试集的划分点
nontrain_split = int((VAL_PERCENT / (VAL_PERCENT + TEST_PERCENT)) * len(nontrain_paths))
# 划分验证集和测试集
eval_paths, test_paths = nontrain_paths[:nontrain_split], nontrain_paths[nontrain_split:]
(5)打印输出训练集、验证集和测试集的数量,具体实现代码如下所示。
len(train_paths), len(eval_paths), len(test_paths)
执行后可以查看数据集在划分后的规模大小:
(255, 30, 16)
(6)将数据集的图像和相应的注释保存到 YOLO 数据集的文件夹中,这样将数据集整理为符合 YOLO 模型训练所需的文件结构,包括图像和相应的标签。
# 遍历训练集、验证集和测试集
for data_type, paths in [("train", train_paths), ("val", eval_paths), ("test", test_paths)]:
# 定义图像和注释保存的路径
images_path = opj(YOLO_DATASET_DIR, "images", data_type)
annotations_path = opj(YOLO_DATASET_DIR, "labels", data_type)
# 创建保存图像和注释的目录(如果不存在)
os.makedirs(images_path, exist_ok=True)
os.makedirs(annotations_path, exist_ok=True)
# 遍历每个图像路径
for path in tqdm(paths):
# 获取图像的目标框信息
boxes = data[path]
# 获取图像文件名
name = path.replace("\\", "/").split("/")[-1]
# 将目标框信息转换为 YOLO 格式的注释内容
annotation_content = "\n".join(map(lambda box: " ".join(map(str, box)), boxes))
# 复制图像到相应的目录
shutil.copy(path, opj(images_path, name))
# 将注释内容写入对应的文本文件
with open(opj(annotations_path, name.split(".")[0] + ".txt"), "w") as f:
f.write(annotation_content)
执行后会输出:
100%
255/255 [00:14<00:00, 14.10it/s]
100%
30/30 [00:01<00:00, 17.18it/s]
100%
16/16 [00:01<00:00, 13.42it/s]
(7)创建 YOLO 模型训练所需的 dataset.yaml 配置文件,该文件包含了数据集的路径、训练、验证和测试集的子目录,以及类别的名称。
# 构建类别名称的字符串
names_content = "\n".join([f" {label_id}: {label}" for label, label_id in label2id.items()])
# 构建 dataset.yaml 文件的内容
dataset_content = f"""
path: "{YOLO_DATASET_DIR}/"
train: "images/train"
val: "images/val"
test: "images/test"
names:
{names_content}
"""
# 将配置内容写入 dataset.yaml 文件
with open(opj(YOLO_DATASET_DIR, "dataset.yaml"), "w") as f:
f.write(dataset_content)
对上述代码的具体说明如下 :
- names_content:通过遍历类别标签的字典 label2id,创建了一个字符串,其中包含了每个类别的名称和对应的整数标识。
- dataset_content:构建了 dataset.yaml 文件的内容,包括数据集的路径、训练、验证和测试集的子目录,以及类别名称。
- with open(...) 块:将配置内容写入 dataset.yaml 文件。
配置文件dataset.yaml是为 YOLO 模型提供数据集信息的配置文件,以便在训练时正确加载数据。
(8)根据指定的百分比将数据集分为训练集、验证集和测试集。划分过程考虑到了数据集中帧的时序关系。
# 获取所有图像文件路径
paths = list(data.keys())
# 计算训练集的划分点
train_split = int((1 - (VAL_PERCENT + TEST_PERCENT)) * len(paths))
# 划分训练集和非训练集
train_paths, nontrain_paths = paths[:train_split], paths[train_split:]
# 计算验证集和测试集的划分点
nontrain_split = int((VAL_PERCENT / (VAL_PERCENT + TEST_PERCENT)) * len(nontrain_paths))
# 划分验证集和测试集
eval_paths, test_paths = nontrain_paths[:nontrain_split], nontrain_paths[nontrain_split:]
(9)打印输出训练集、验证集和测试集的数量,具体实现代码如下所示。
len(train_paths), len(eval_paths), len(test_paths)
执行后会输出:
(255, 30, 16)
(10)将数据集的图像和相应的注释保存到 YOLO 数据集的文件夹中,目的是将数据集整理为符合 YOLO 模型训练所需的文件结构,包括图像和相应的标签。
# 遍历训练集、验证集和测试集
for data_type, paths in [("train", train_paths), ("val", eval_paths), ("test", test_paths)]:
# 定义图像和注释保存的路径
images_path = opj(YOLO_DATASET_DIR, "images", data_type)
annotations_path = opj(YOLO_DATASET_DIR, "labels", data_type)
# 创建保存图像和注释的目录(如果不存在)
os.makedirs(images_path, exist_ok=True)
os.makedirs(annotations_path, exist_ok=True)
# 遍历每个图像路径
for path in tqdm(paths):
# 获取图像的目标框信息
boxes = data[path]
# 获取图像文件名
name = path.replace("\\", "/").split("/")[-1]
# 将目标框信息转换为 YOLO 格式的注释内容
annotation_content = "\n".join(map(lambda box: " ".join(map(str, box)), boxes))
# 复制图像到相应的目录
shutil.copy(path, opj(images_path, name))
# 将注释内容写入对应的文本文件
with open(opj(annotations_path, name.split(".")[0] + ".txt"), "w") as f:
f.write(annotation_content)
对上述代码的具体说明如下:
- data_type表示当前是训练集、验证集还是测试集。
- images_path 和 annotations_path用于定义保存图像和注释的文件夹路径。
- 首先,创建对应的文件夹(如果不存在)。然后遍历每个图像路径,获取图像的目标框信息。
- 最后将图像复制到相应的目录中,将目标框信息保存为 YOLO 格式的注释文件(.txt 文件)。
执行后会输出:
100%
255/255 [00:14<00:00, 14.10it/s]
100%
30/30 [00:01<00:00, 17.18it/s]
100%
16/16 [00:01<00:00, 13.42it/s]
(11)创建 YOLO 模型训练所需的 dataset.yaml 配置文件,该文件包含了数据集的路径、训练、验证和测试集的子目录,以及类别的名称。
# 构建类别名称的字符串
names_content = "\n".join([f" {label_id}: {label}" for label, label_id in label2id.items()])
# 构建 dataset.yaml 文件的内容
dataset_content = f"""
path: "{YOLO_DATASET_DIR}/"
train: "images/train"
val: "images/val"
test: "images/test"
names:
{names_content}
"""
# 将配置内容写入 dataset.yaml 文件
with open(opj(YOLO_DATASET_DIR, "dataset.yaml"), "w") as f:
f.write(dataset_content)