出处https://blog.csdn.net/m0_66307842/article/details/132119646?spm=1001.2014.3001.5501
作者:流继承

前言:本篇是关于如何使用YoloV5+Deepsort训练自己的数据集,从而实现目标检测与目标追踪,并绘制出物体的运动轨迹。本章讲解的为第二部分内容:训练集的采集与划分,Yolov5模型的训练。本文中用到的数据集均为自采,实验动物为斑马鱼。

环境&配置:RTX 3060、CUDA Version: 11.1、torch_version:1.9.1+cu111、python:3.8

源码如下

GitHub - mikel-brostrom/yolo_tracking: A collection of SOTA real-time, multi-object tracking algorithms for object detectors

GitHub - Sharpiless/Yolov5-Deepsort: 最新版本yolov5+deepsort目标检测和追踪,能够显示目标类别,支持5.0版本可训练自己数据集

如果想进一步了解Yolov5+Deepsort中的算法,猛戳这里:【Yolov5+Deepsort】训练自己的数据集(1)| 目标检测&追踪 | 轨迹绘制

Ⅰ准备数据集

0x00 数据集的采集

使用USB3.0工业相机对运动的斑马鱼进行照片的抓拍采集,共采集到照片1w+。

数据集示例:

为什么使用工业相机呢:

  • 可以提供高分辨率和高质量的图像,确保准确的视觉分析和检测。
  • 具备高帧率和快速曝光时间,能够在高速运动或快速生产线上捕获清晰的图像,确保高效的生产过程。
  • 工业相机经过严格的测试和质量控制,具有高度的稳定性和可靠性,能够长时间稳定工作。
    由于工业相机的成本等问题,不使用工业相机也可以采集到质量较高的图片,但是采集的图片一般要满足以下要求:
  1. 图像质量:图像应该具有足够的清晰度和图像质量,以确保模型能够正确地提取特征并进行准确的预测。低质量或模糊的图像可能会导致模型性能下降。
  2. 统一尺寸:数据集中的图像应该具有统一的尺寸。在训练过程中,通常需要将图像调整为相同的大小,以便于批量处理。
  3. 多样性:数据集应该包含多样性的图像样本,涵盖不同的场景、角度、光照条件、背景等。这样可以确保模型在各种情况下都能表现良好。
  4. 平衡类别:如果数据集是分类任务,每个类别的样本应该尽量保持平衡。不平衡的类别分布可能导致模型对少数类别的表现不佳。

0x01 数据集的标注

在机器学习和计算机视觉领域,有许多常用的图像数据标注软件,用于对图像数据进行标注和注释。

我们使用LabelImg对抓拍的图片进行标记:

由于我们后续要使用Yolov5作为目标检测的模型,故我们的数据集需采用YOLO的格式进行标记。

得到标记后的txt格式

为了方便读者的实际操作,在这里给出VOC格式转YOLO格式(txt格式)的代码:

import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join


def convert(size, box):
    x_center = (box[0] + box[1]) / 2.0
    y_center = (box[2] + box[3]) / 2.0
    x = x_center / size[0]
    y = y_center / size[1]
    w = (box[1] - box[0]) / size[0]
    h = (box[3] - box[2]) / size[1]
    return (x, y, w, h)


def convert_annotation(xml_files_path, save_txt_files_path, classes):
    xml_files = os.listdir(xml_files_path)
    print(xml_files)
    for xml_name in xml_files:
        print(xml_name)
        xml_file = os.path.join(xml_files_path, xml_name)
        out_txt_path = os.path.join(save_txt_files_path, xml_name.split('.')[0] + '.txt')
        out_txt_f = open(out_txt_path, 'w')
        tree = ET.parse(xml_file)
        root = tree.getroot()
        size = root.find('size')
        w = int(size.find('width').text)
        h = int(size.find('height').text)

        for obj in root.iter('object'):
            difficult = obj.find('difficult').text
            cls = obj.find('name').text
            if cls not in classes or int(difficult) == 1:
                continue
            cls_id = classes.index(cls)
            xmlbox = obj.find('bndbox')
            b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
                 float(xmlbox.find('ymax').text))
            # b=(xmin, xmax, ymin, ymax)
            print(w, h, b)
            bb = convert((w, h), b)
            out_txt_f.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')


if __name__ == "__main__":
    # 需要转换的类别,需要一一对应
    classes1 = ['helmet']
    # 2、voc格式的xml标签文件路径
    xml_files1 = r'path'
    # 3、转化为yolo格式的txt标签文件存储路径
    save_txt_files1 = r'path'

    convert_annotation(xml_files1, save_txt_files1, classes1)

因为有多种标记工具可以用于标记,若使用LabelMe进行标记,则需要将json格式转为txt格式

import os
import json
from PIL import Image


json_dir = 'path'  # json文件路径
out_dir = 'path'  # 输出的 txt 文件路径


def get_json(json_file, filename):
    # 读取 json 文件数据
    with open(json_file, 'r') as load_f:
        content = json.load(load_f)

    file_path = 'D:/train/image/'+filename+'.jpg' # 每个json文件对应的图片文件路径
    img = Image.open(file_path)
    # imgSize = img.size  # 大小/尺寸
    image_width = img.width  # 图片的宽
    image_height = img.height  # 图片的高

    filename_txt = out_dir + filename+'.txt'

    # 创建txt文件
    fp = open(filename_txt, mode="w", encoding="utf-8")
    # 将数据写入文件
    #fp.close()
    for t in content:
        if(t['type']<=7):
            # 计算 yolo 数据格式所需要的中心点的 相对 x, y 坐标, w,h 的值
            x = (t['x'] + t['width']/ 2) / image_width #归一化
            y = (t['y']+ t['height']/ 2 )/ image_height #归一化
            w = t['width']/ image_width #归一化
            h = t['height'] / image_height #归一化
            fp = open(filename_txt, mode="r+", encoding="utf-8")
            file_str = str(t['type']-1) + ' ' + str(round(x, 6)) + ' ' + str(round(y, 6)) + ' ' + str(round(w, 6)) + \
                       ' ' + str(round(h, 6))
            line_data = fp.readlines()

            if len(line_data) != 0:
                fp.write('\n' + file_str)
            else:
                fp.write(file_str)
            fp.close()

def main():
    files = os.listdir(json_dir)  # 得到文件夹下的所有文件名称
    s = []
    for file in files:  # 遍历文件夹
        filename = file.split('.')[0]
        get_json(json_dir+'/'+file, filename)


if __name__ == '__main__':
    main()

0x02 数据集的划分

数据集划分是在机器学习和深度学习任务中至关重要的步骤,用于将数据集分成训练集、验证集和测试集。以下是简单的数据集划分方法:

  • 训练集(Training set):用于训练模型的数据集。训练集占据整个数据集的大部分,通常约为总数据集的60-80%。模型通过训练集来学习数据的特征和模式。
  • 验证集(Validation set):用于调整模型的超参数和选择最佳模型。验证集是用来评估模型在训练过程中的性能,并帮助确定哪些超参数设置最优。验证集通常约占数据集的10-20%。
  • 测试集(Test set):用于评估模型的泛化能力和性能。测试集是在训练和调参完成后,用来验证模型在新数据上的表现。测试集应该与训练集和验证集没有重叠,通常约占数据集的10-20%。
    数据集划分应该尽量保持数据的随机性,避免训练集、验证集和测试集之间的数据分布差异过大。

若数据集划分不当,则容易出现过拟合和欠拟合:

1.过拟合(Overfitting)

  • 当训练集过小,无法充分代表整个数据分布时,模型可能会在训练集上表现得很好,但在未见过的数据上表现不佳,这称为过拟合。
  • 过拟合问题会导致模型过度记忆训练集中的噪声和细节,而无法泛化到新数据上。
  • 过拟合通常在验证集和测试集上表现较差,但在训练集上表现优秀。
    2.欠拟合(Underfitting)

  • 当训练集过大或模型复杂度不够高时,模型可能会无法充分学习数据的规律,而表现不佳,这称为欠拟合。

  • 欠拟合问题会导致模型无法学习数据的真实分布和特征,表现较差且泛化能力差。
  • 欠拟合通常在训练集、验证集和测试集上表现均较差。
    划分好的格式如下:

可以手动进行数据集的划分,也可以使用如下代码进行数据集的划分:

import os
import shutil
import random

random.seed(0)


def split_data(file_path,xml_path, new_file_path, train_rate, val_rate, test_rate):
    each_class_image = []
    each_class_label = []
    for image in os.listdir(file_path):
        each_class_image.append(image)
    for label in os.listdir(xml_path):
        each_class_label.append(label)
    data=list(zip(each_class_image,each_class_label))
    total = len(each_class_image)
    random.shuffle(data)
    each_class_image,each_class_label=zip(*data)
    train_images = each_class_image[0:int(train_rate * total)]
    val_images = each_class_image[int(train_rate * total):int((train_rate + val_rate) * total)]
    test_images = each_class_image[int((train_rate + val_rate) * total):]
    train_labels = each_class_label[0:int(train_rate * total)]
    val_labels = each_class_label[int(train_rate * total):int((train_rate + val_rate) * total)]
    test_labels = each_class_label[int((train_rate + val_rate) * total):]

    for image in train_images:
        print(image)
        old_path = file_path + '/' + image
        new_path1 = new_file_path + '/' + 'train' + '/' + 'images'
        if not os.path.exists(new_path1):
            os.makedirs(new_path1)
        new_path = new_path1 + '/' + image
        shutil.copy(old_path, new_path)

    for label in train_labels:
        print(label)
        old_path = xml_path + '/' + label
        new_path1 = new_file_path + '/' + 'train' + '/' + 'labels'
        if not os.path.exists(new_path1):
            os.makedirs(new_path1)
        new_path = new_path1 + '/' + label
        shutil.copy(old_path, new_path)

    for image in val_images:
        old_path = file_path + '/' + image
        new_path1 = new_file_path + '/' + 'val' + '/' + 'images'
        if not os.path.exists(new_path1):
            os.makedirs(new_path1)
        new_path = new_path1 + '/' + image
        shutil.copy(old_path, new_path)

    for label in val_labels:
        old_path = xml_path + '/' + label
        new_path1 = new_file_path + '/' + 'val' + '/' + 'labels'
        if not os.path.exists(new_path1):
            os.makedirs(new_path1)
        new_path = new_path1 + '/' + label
        shutil.copy(old_path, new_path)

    for image in test_images:
        old_path = file_path + '/' + image
        new_path1 = new_file_path + '/' + 'test' + '/' + 'images'
        if not os.path.exists(new_path1):
            os.makedirs(new_path1)
        new_path = new_path1 + '/' + image
        shutil.copy(old_path, new_path)

    for label in test_labels:
        old_path = xml_path + '/' + label
        new_path1 = new_file_path + '/' + 'test' + '/' + 'labels'
        if not os.path.exists(new_path1):
            os.makedirs(new_path1)
        new_path = new_path1 + '/' + label
        shutil.copy(old_path, new_path)


if __name__ == '__main__':
    file_path = "D:/Files/dataSet/drone_images"
    xml_path = 'D:/Files/dataSet/drone_labels'
    new_file_path = "D:/Files/dataSet/droneData"
    split_data(file_path,xml_path, new_file_path, train_rate=0.7, val_rate=0.1, test_rate=0.2)

Ⅱ Yolov5模型训练

0x00 修改配置文件

在data中找到coco128.yaml并打开


其中,nc是标签名个数,names就是标签的名字。

train是在path绝对路径条件下的训练集路径,val同上,但是是验证集,这里为了方便,合并训练集和验证集。

0x01 选择预训练模型

yolov5共有4中配置,本次演示选择yolov5s,这个版本对显存的要求较低,但效果一般。

在yolov5下找到train.py

只需要修改以下参数即可:

—weights:训练的初始权重的位置,以.pt结尾的文件,可在官网上下载权重。

—cfg:训练模型文件,在本项目中对应yolov5s.yaml。

—data:数据集参数文件,在本项目中对应coco128.yaml

—epochs:训练的轮数,这里设置为1000,可根据需要修改

—batch-size:每次迭代(或称为训练步骤)中模型处理的样本数量,决定了训练的速度,要根据自己电脑的显存选择合理的batch。

0x02 训练结果

可在如下路径下找到最后的结果(在train中的最后一个exp文件夹中,训练次数越多,exp的数量越多):

可以看到训练结果:

可在如下路径下找到训练后得到的权重:


best.pt和last.pt是我们训练出来的权重文件

其中last是最后一次的训练结果,best是效果最好的训练结果。

可能随着不同的训练设定和实验有所变化,因此在不同的实验中,得到的最佳模型参数可能不同。
只是在训练过程中在验证集上表现最好的一个模型快照,但并不能保证它在所有情况下都是最佳的。

0x03 训练结果浅析

1. F1_curve.png —— F1曲线

  • F1分数与置信度阈值(x轴)之间的关系。F1分数是分类的一个衡量标准,是精确率和召回率的调和平均数,介于0,1之间。越大越好。
  • 若F1曲线很“宽敞”且顶部接近1,说明在训练数据集上表现得很好的置信度阈值区间很大。

R_curve.png —— 单一类召回率(置信度阈值 - 召回率曲线图)

  • 当置信度越小的时候,类别检测的越全面(不容易被漏掉,但容易误判)。
    P_curve.png —— 单一类准确率(置信度阈值 - 准确率曲线图)

  • 当判定概率超过置信度阈值时,各个类别识别的准确率。当置信度越大时,类别检测越准确。
    PR_curve.png —— 精确率和召回率的关系图

  • 在准确率很高的前提下,尽可能的检测到全部的类别。因此希望我曲线接近(1,1),即希望mAP曲线的面积尽可能接近1。

    0x04 替换权重文件

    将训练后得到的best.pt替换到track.py中:

至此,yolov5部分已经全部结束。

 END


因为作者的能力有限,所以文章可能会存在一些错误和不准确之处,恳请大家指出!

 参考文献:

[1] Simple Online and Realtime Tracking with a Deep Association Metric

[1703.07402] Simple Online and Realtime Tracking with a Deep Association Metric (arxiv.org)