YOLOv5/v7/v8改进实验(二)之数据增强和格式转换篇

本文介绍了在YOLO系列实验中数据增强的重要性和常用方法,包括几何和像素变换,以及Mixup、Cutout、Cutmix和Mosaic等策略。同时,详细阐述了数据转换的过程,包括VOC转YOLO、YOLO转VOC和YOLO转COCO的代码实现,以提升模型的泛化能力和训练效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


在这里插入图片描述


🚀🚀 前言 🚀🚀

我们常常会遇到数据不足的情况。比如,你遇到的一个任务,目前只有小几百的数据,然而,你知道目前现在流行的最先进的神经网络都是成千上万的图片数据。你知道有人提及大的数据集是效果好的保证,那么这个时候就需要对自己的数据集进行数据增强。


🔥🔥 YOLO系列实验实战篇:

📖 YOLOv5/v7/v8改进实验(一)之数据准备篇
📖 YOLOv5/v7/v8改进实验(二)之数据增强篇
📖 YOLOv5/v7/v8改进实验(三)之训练技巧篇

更新中…


一、数据增强

数据增强(data augmentation)是在训练神经网络时对原始数据进行一系列变换操作以产生新的样本的技术。它在深度学习中具有重要的意义和重要性,主要体现在以下几个方面:

  • 扩充数据集:数据增强通过对原始数据应用不同的变换操作,可以生成更多、更多样化的训练样本。这样可以有效扩充原始数据集,增加训练样本的多样性和数量,从而减轻模型的过拟合问题。

  • 提高模型泛化能力:数据增强可以在不改变数据标签的情况下引入多样性,使得模型更加鲁棒,能够更好地适应不同的场景和数据变化。通过引入噪声、旋转、平移、缩放等变换,模型可以学习到更加鲁棒的特征表示,提高泛化能力。

  • 减少过拟合风险:在深度学习中,当训练样本有限时,模型容易发生过拟合现象,即在训练集上表现良好但在测试集上表现较差。数据增强通过引入变换操作,增加了样本空间的多样性,可以使得模型更好地捕捉数据的统计规律,减少过拟合的风险。

  • 对抗噪声和变化:在真实世界的应用中,数据常常面临各种噪声和变化,如光照变化、仿射变换、几何扭曲等。数据增强可以模拟这些变化,并使得模型在训练中更好地学习如何应对这些挑战,提高模型的鲁棒性和稳定性。

1.1 常用方法

  • 比较常用的几何变换方法主要有:翻转旋转裁剪缩放平移抖动。值得注意的是,在某些具体的任务中,当使用这些方法时需要主要标签数据的变化,如目标检测中若使用翻转,则需要将gt框进行相应的调整。
  • 比较常用的像素变换方法有:加椒盐噪声高斯噪声,进行高斯模糊,调整HSV对比度,调节亮度饱和度直方图均衡化,调整白平衡等。
  • 还有以下数据增强方式:
    • Mixup:将随机的两张样本按比例混合,分类的结果按比例分配。只适合分类任务。
    • Cutout:随机的将样本中的部分区域cut掉,并且填充0像素值,分类的结果不变。
    • Cutmix:将一部分区域cut掉但不填充0像素而是随机填充训练集中的其他数据的区域像素值,分类结果按一定的比例分配。
    • Mosaic:将4张图片按一定比例组合成一张图片。

1.2 代码实现

二、数据转换

据集的分布会影响模型的性能和泛化能力。如果训练数据和实际应用场景的数据分布不一致,模型可能会出现过拟合或欠拟合的问题。了解数据分布可以帮助判断是否存在类别不平衡问题,有助于选择适当的算法和模型,以提高模型的泛化能力。

2.1 多线程处理

大量数据的处理需要消耗大量的时间,所以这里选择使用多线程转换速度,提高效率!具体代码如下:

from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor


NUM_THREADS = min(8, max(1, os.cpu_count() - 1))

def run(func, this_iter, desc="Processing"):
    with ThreadPoolExecutor(max_workers=NUM_THREADS, thread_name_prefix='MyThread') as executor:
        results = list(
            tqdm(executor.map(func, this_iter), total=len(this_iter), desc=desc)
        )
    return results

2.2 VOC转YOLO

您的数据集存放格式可以如下所示:

- mydata
	- Annotations
	- images 

运行脚本后会生成labels文件夹(用于存放txt文件)和classes.txt文件(记录种类)

- mydata
	- Annotations
	- images
	- labels
	- classes.txt

实现代码

import glob
import os
import re
import xml.etree.ElementTree as ET
from pathlib import Path
import cv2
import numpy as np
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor

NUM_THREADS = min(8, max(1, os.cpu_count() - 1))

def run(func, this_iter, desc="Processing"):
    with ThreadPoolExecutor(max_workers=NUM_THREADS, thread_name_prefix='MyThread') as executor:
        results = list(
            tqdm(executor.map(func, this_iter), total=len(this_iter), desc=desc)
        )
    return results


# XML坐标格式转换成yolo坐标格式
def convert(size, box):
    dw = 1.0 / size[0]
    dh = 1.0 / size[1]
    x = (box[0] + box[1]) / 2.0
    y = (box[2] + box[3]) / 2.0
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh
    return (x, y, w, h)


def get_xml_classes(xml_path):
    f = open(xml_path)  # xml文件路径
    xml_text = f.read()
    root = ET.fromstring(xml_text)
    f.close()
    for obj in root.iter("object"):
        cls = obj.find("name").text
        if cls not in xml_classes:
            classes_file.write(cls + "\n")
            xml_classes.append(cls)


# 标记文件格式转换
def convert_xml2yolo(img_path):
    img_path = Path(img_path)

    xml_name = re.sub(r"\.(jpg|png|jpeg)$", ".xml", img_path.name)
    txt_name = re.sub(r"\.(jpg|png|jpeg)$", ".txt", img_path.name)
    xml_path = Path(xml_target_path) / xml_name
    txt_path = Path(save_path) / txt_name

    if xml_path.exists():
        out_file = open(txt_path, "w")  # 转换后的txt文件存放路径
        f = open(xml_path)  # xml文件路径
        xml_text = f.read()
        root = ET.fromstring(xml_text)
        f.close()
        size = root.find("size")
        w = int(size.find("width").text)
        h = int(size.find("height").text)
        if w == 0 or h == 0:
            # problem_xml.append(str(img_path.name))
            img = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), 1)
            h, w, _ = img.shape
        for obj in root.iter("object"):
            cls = obj.find("name").text
            if cls not in xml_classes:
                print(cls)
                continue
            cls_id = xml_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),
            )
            try:
                bbox = convert((w, h), b)
            except:
                print(img_path)
            out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bbox]) + "\n")
    else:
        print(f"{xml_path}不存在!")


if __name__ == "__main__":
    xml_target_path = r"data\Annotations"  # xml文件夹
    save_path = r"data\labels"  # 转换后的txt文件存放文件夹
    images_path = r"data\images"  # 图片文件夹
    classes_file = open(Path(xml_target_path).parents[0] / "classes.txt", "w")
    # -------------------------------------------- #
    # 第一步 获得xml所有种类
    # -------------------------------------------- #
    assert (Path(xml_target_path)).exists(), "Annotations文件夹不存在"
    xml_classes = []
    xml_list = glob.glob(os.path.join(xml_target_path, "*.[x][m][l]*"))
    run(get_xml_classes, xml_list)
    print(Path(xml_target_path).parents[0])
    print(xml_classes)
    # -------------------------------------------- #
    # 第二步 转换成YOLO txt
    # -------------------------------------------- #
    if not Path(save_path).exists():
        Path(save_path).mkdir(parents=True)
    file_list = glob.glob(os.path.join(images_path, "*.[jp][pn][gg]*"))
    run(convert_xml2yolo, file_list)
    

2.3 YOLO转VOC

您的数据集存放格式可以如下所示:

- mydata
	- images 
	- labels

运行脚本后会生成Annotations文件夹(用于存放xml文件)

- mydata
	- Annotations
	- images 
	- labels

实现代码

import glob
from pathlib import Path
from xml.dom.minidom import Document
import os
import cv2
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor

NUM_THREADS = min(8, max(1, os.cpu_count() - 1))

def run(func, this_iter, desc="Processing"):
    with ThreadPoolExecutor(max_workers=NUM_THREADS, thread_name_prefix='MyThread') as executor:
        results = list(
            tqdm(executor.map(func, this_iter), total=len(this_iter), desc=desc)
        )
    return results

def makexml(file_name):
    try:
        name = Path(file_name).name
        xmlBuilder = Document()
        annotation = xmlBuilder.createElement("annotation")  # 创建annotation标签
        xmlBuilder.appendChild(annotation)
        txtFile = open(txtPath + name)
        txtList = txtFile.readlines()
        img = cv2.imread(picPath + name[0:-4] + ".jpg")
        Pheight, Pwidth, Pdepth = img.shape

        folder = xmlBuilder.createElement("folder")  # folder标签
        foldercontent = xmlBuilder.createTextNode(folder_name)
        folder.appendChild(foldercontent)
        annotation.appendChild(folder)  # folder标签结束

        filename = xmlBuilder.createElement("filename")  # filename标签
        filenamecontent = xmlBuilder.createTextNode(name[0:-4] + ".jpg")
        filename.appendChild(filenamecontent)
        annotation.appendChild(filename)  # filename标签结束

        size = xmlBuilder.createElement("size")  # size标签
        width = xmlBuilder.createElement("width")  # size子标签width
        widthcontent = xmlBuilder.createTextNode(str(Pwidth))
        width.appendChild(widthcontent)
        size.appendChild(width)  # size子标签width结束

        height = xmlBuilder.createElement("height")  # size子标签height
        heightcontent = xmlBuilder.createTextNode(str(Pheight))
        height.appendChild(heightcontent)
        size.appendChild(height)  # size子标签height结束

        depth = xmlBuilder.createElement("depth")  # size子标签depth
        depthcontent = xmlBuilder.createTextNode(str(Pdepth))
        depth.appendChild(depthcontent)
        size.appendChild(depth)  # size子标签depth结束

        annotation.appendChild(size)  # size标签结束

        for j in txtList:
            oneline = j.strip().split(" ")
            object = xmlBuilder.createElement("object")  # object 标签
            picname = xmlBuilder.createElement("name")  # name标签
            namecontent = xmlBuilder.createTextNode(dic[oneline[0]])
            picname.appendChild(namecontent)
            object.appendChild(picname)  # name标签结束

            pose = xmlBuilder.createElement("pose")  # pose标签
            posecontent = xmlBuilder.createTextNode("Unspecified")
            pose.appendChild(posecontent)
            object.appendChild(pose)  # pose标签结束

            truncated = xmlBuilder.createElement("truncated")  # truncated标签
            truncatedContent = xmlBuilder.createTextNode("0")
            truncated.appendChild(truncatedContent)
            object.appendChild(truncated)  # truncated标签结束

            difficult = xmlBuilder.createElement("difficult")  # difficult标签
            difficultcontent = xmlBuilder.createTextNode("0")
            difficult.appendChild(difficultcontent)
            object.appendChild(difficult)  # difficult标签结束

            bndbox = xmlBuilder.createElement("bndbox")  # bndbox标签
            xmin = xmlBuilder.createElement("xmin")  # xmin标签
            mathData = int(((float(oneline[1])) * Pwidth + 1) - (float(oneline[3])) * 0.5 * Pwidth)
            xminContent = xmlBuilder.createTextNode(str(mathData))
            xmin.appendChild(xminContent)
            bndbox.appendChild(xmin)  # xmin标签结束

            ymin = xmlBuilder.createElement("ymin")  # ymin标签
            mathData = int(((float(oneline[2])) * Pheight + 1) - (float(oneline[4])) * 0.5 * Pheight)
            yminContent = xmlBuilder.createTextNode(str(mathData))
            ymin.appendChild(yminContent)
            bndbox.appendChild(ymin)  # ymin标签结束

            xmax = xmlBuilder.createElement("xmax")  # xmax标签
            mathData = int(((float(oneline[1])) * Pwidth + 1) + (float(oneline[3])) * 0.5 * Pwidth)
            xmaxContent = xmlBuilder.createTextNode(str(mathData))
            xmax.appendChild(xmaxContent)
            bndbox.appendChild(xmax)  # xmax标签结束

            ymax = xmlBuilder.createElement("ymax")  # ymax标签
            mathData = int(((float(oneline[2])) * Pheight + 1) + (float(oneline[4])) * 0.5 * Pheight)
            ymaxContent = xmlBuilder.createTextNode(str(mathData))
            ymax.appendChild(ymaxContent)
            bndbox.appendChild(ymax)  # ymax标签结束

            object.appendChild(bndbox)  # bndbox标签结束

            annotation.appendChild(object)  # object标签结束

        f = open(xmlPath + name[0:-4] + ".xml", 'w')
        xmlBuilder.writexml(f, indent='\t', newl='\n', addindent='\t', encoding='utf-8')
        f.close()
    except Exception as e:
        print(e)

def main(txtPath):  # txt所在文件夹路径,xml文件保存路径,图片所在文件夹路径
    """此函数用于将yolo格式txt标注文件转换为voc格式xml标注文件
    """
    # files = os.listdir(txtPath)
    files = glob.glob(os.path.join(txtPath, '*.[t][x][t]*'))

    run(makexml, files)
    

if __name__ == "__main__":
    dic = {
        '0': "Dead tree",  # 创建字典用来对类型进行转换
        '1': "Sick tree",  # 此处的字典要与自己的classes.txt文件中的类对应,且顺序要一致
        }
    folder_name = "JPEGImages" # # folder标签,可更改
    picPath = r"data/images/"  # 图片所在文件夹路径,后面的/一定要带上
    txtPath = r"data/labels/"  # txt所在文件夹路径,后面的/一定要带上
    xmlPath = r"data/Annotations/"  # xml文件保存路径,后面的/一定要带上

    assert (Path(picPath)).exists() or (Path(txtPath)).exists(), f"{picPath}{txtPath}文件夹不存在"
    if not Path(xmlPath).exists():
        Path(xmlPath).mkdir(parents=True)
    main(txtPath)

2.4 YOLO转COCO

您的数据集存放格式可以如下所示:

- mydata
	- test
		- images
		- labels
	- classes.txt

classes.txt存放目标类别信息,注意顺序要对应。运行脚本后会在当前根目录下生成instances_val2017.json文件夹

实现代码

'''
Date: 2023-10-18 10:41:52
LastEditors: xujiayue
LastEditTime: 2023-10-18 10:46:18
'''
import os
import cv2
import json
from tqdm import tqdm
# from sklearn.model_selection import train_test_split
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--root_dir', default=r'F:\ObjectDetection\Datasets\Experiment Datasets\archive721', type=str,
                    help="root path of images and labels, include ./images and ./labels and classes.txt")
parser.add_argument('--save_path', type=str, default='instances_val2017.json',
                    help="if not split the dataset, give a path to a json file")

arg = parser.parse_args()


def yolo2coco(arg):
    root_path = arg.root_dir
    print("Loading data from ", root_path)

    assert os.path.exists(root_path)
    originLabelsDir = os.path.join(root_path, 'test/labels')
    originImagesDir = os.path.join(root_path, 'test/images')
    with open(os.path.join(root_path, 'classes.txt')) as f:
        classes = list(map(lambda x: x.strip(), f.readlines()))
    # images dir name
    indexes = os.listdir(originImagesDir)

    dataset = {'categories': [], 'annotations': [], 'images': []}
    for i, cls in enumerate(classes, 0):
        dataset['categories'].append({'id': i, 'name': cls, 'supercategory': 'mark'})

    # 标注的id
    ann_id_cnt = 0
    for k, index in enumerate(tqdm(indexes)):
        # 支持 png jpg 格式的图片。
        txtFile = index.replace('images', 'txt').replace('.jpg', '.txt').replace('.png', '.txt')
        # 读取图像的宽和高
        im = cv2.imread(os.path.join(originImagesDir, index))
        height, width, _ = im.shape
        # 添加图像的信息
        if not os.path.exists(os.path.join(originLabelsDir, txtFile)):
            # 如没标签,跳过,只保留图片信息。
            continue
        dataset['images'].append({'file_name': index,
                                  'id': int(index[:-4]) if index[:-4].isnumeric() else index[:-4],
                                  'width': width,
                                  'height': height})
        with open(os.path.join(originLabelsDir, txtFile), 'r') as fr:
            labelList = fr.readlines()
            for label in labelList:
                label = label.strip().split()
                x = float(label[1])
                y = float(label[2])
                w = float(label[3])
                h = float(label[4])

                # convert x,y,w,h to x1,y1,x2,y2
                H, W, _ = im.shape
                x1 = (x - w / 2) * W
                y1 = (y - h / 2) * H
                x2 = (x + w / 2) * W
                y2 = (y + h / 2) * H
                # 标签序号从0开始计算, coco2017数据集标号混乱,不管它了。
                cls_id = int(label[0])
                width = max(0, x2 - x1)
                height = max(0, y2 - y1)
                dataset['annotations'].append({
                    'area': width * height,
                    'bbox': [x1, y1, width, height],
                    'category_id': cls_id,
                    'id': ann_id_cnt,
                    'image_id': int(index[:-4]) if index[:-4].isnumeric() else index[:-4],
                    'iscrowd': 0,
                    # mask, 矩形是从左上角点按顺时针的四个顶点
                    'segmentation': [[x1, y1, x2, y1, x2, y2, x1, y2]]
                })
                ann_id_cnt += 1

    # 保存结果
    with open(arg.save_path, 'w') as f:
        json.dump(dataset, f)
        print('Save annotation to {}'.format(arg.save_path))


if __name__ == "__main__":
    yolo2coco(arg)

在这里插入图片描述

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

w94ghz

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值