YOLOv8目标检测(一)_检测流程梳理:YOLOv8目标检测(一)_检测流程梳理_yolo检测流程-CSDN博客

YOLOv8目标检测(二)_准备数据集:YOLOv8目标检测(二)_准备数据集_yolov8 数据集准备-CSDN博客

YOLOv8目标检测(三)_训练模型:YOLOv8目标检测(三)_训练模型_yolo data.yaml-CSDN博客

YOLOv8目标检测(三*)_最佳超参数训练:YOLOv8目标检测(三*)_最佳超参数训练_yolo 为什么要选择yolov8m.pt进行训练-CSDN博客

YOLOv8目标检测(四)_图片推理:YOLOv8目标检测(四)_图片推理-CSDN博客

YOLOv8目标检测(五)_结果文件(run/detrct/train)详解:YOLOv8目标检测(五)_结果文件(run/detrct/train)详解_yolov8 yolov8m.pt可以训练什么-CSDN博客

YOLOv8目标检测(六)_封装API接口:YOLOv8目标检测(六)_封装API接口-CSDN博客

YOLOv8目标检测(七)_AB压力测试:YOLOv8目标检测(七)_AB压力测试-CSDN博客

对整个YOLO目标检测流程有一个大体思路之后,从零开始整理自己需要的数据集

先回顾下YOLOv8训练集文件层级

dataset/
├── train/
│   ├── images/
│   └── labels/
└── val/
    ├── images/
    └── labels/

1.收集视频数据

对于笔者而言,收集视频数据,更容易获得高质量的图片。当然各位读者也可以选择直接用图片。

mixkit官网:Mixkit - Awesome free assets for your next video project

选择自己需要的视频,下载

2.视频抽帧

(1)准备原始视频文件夹

  • 注:最好全英文命名

(2)视频重命名

收集的视频名字千奇百怪,不利于我们检查数据及后期的调优,建议大家规范名字。

(3)运行抽帧脚本

将下载好的视频抽帧成图片,下面是笔者常用的视频抽帧脚本

  • 注:要修改原始视频文件夹路径
# -----------------------------------------------------------
# 说明:对文件夹中视频进行抽帧,显示当前处理进度.
# -----------------------------------------------------------

import cv2
import os
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm

video_folder = r"D:\\Desktop\\play_phone"  # 视频文件夹路径
output_folder = r"D:\\Desktop\\play_phone_images"  # 保存帧的文件夹路径
save_step = 10  # 间隔帧

# 如果输出文件夹不存在,则创建
if not os.path.exists(output_folder):
    os.makedirs(output_folder)

def process_video(video_path):
    video = cv2.VideoCapture(video_path)
    video_name = os.path.splitext(os.path.basename(video_path))[0]  # 提取视频文件名(不包含扩展名)
    num = 0  # 计数器
    total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))  # 获取视频总帧数

    with tqdm(total=total_frames // save_step, desc=f"Processing {video_name}") as pbar:
        while True:
            ret, frame = video.read()
            if not ret:
                break
            num += 1
            if num % save_step == 0:
                output_path = os.path.join(output_folder, f"{video_name}_{num}.jpg")
                cv2.imwrite(output_path, frame)
                pbar.update(1)  # 更新进度条

    video.release()  # 释放视频捕获对象

# 遍历文件夹中的所有视频文件
video_files = [os.path.join(video_folder, filename) for filename in os.listdir(video_folder) if filename.endswith(".mp4")]

# 使用多线程处理视频文件
with ThreadPoolExecutor(max_workers=4) as executor:  # 根据 CPU 核心数调整 max_workers
    executor.map(process_video, video_files)

cv2.destroyAllWindows()  # 关闭所有 OpenCV 窗口

运行后文件内容

3.图片梳理

剔除低质量的图片,即模糊、部分无目标、混乱的图片。

  • 注:本次选择的视频只是举例,一个视频是远远不够的,场景、动作、角度都太少。

4.图片标注

YOLOv8训练的标签格式为txt格式,如果标注类别较少建议选择YOLO格式即txt格式,如果PascalVOC格式即xml格式。

本次选择PascalVOC格式,为了给各位读者演示更详细数据处理的流程。

(1)安装labelimg

推荐读者先创建个虚拟环境再安装labelimg

1)创建环境

#创建环境labelimg,前提安装了anaconda,没有需安装
conda create -n labelimg python=3.8 -y

2)激活环境

#激活labelimg环境
conda activate labelimg

3)安装

pip install labelimg

4)命令行打开

labelimg

(2)打开labelimg

(3)打开目录

打开准备好的图片

(4)改变存放目录

选择你保存标签的位置

(5)确认格式和自动保存

确认PascalVOC格式,并勾选自动保存

(6)开始标注

(7)检查标签

5.标签格式转化

将xml格式标签转为txt格式

为什么要一开始选择xml格式而不是txt格式呢?

对于笔者而言,xml格式标签兼容性和通用性好能被许多标注工具支持、能存储更详细的信息、便于后期格式转换。如果类别单一直接选txt也可以。

# ------------------------------------------------------------------------------------
# 说明:标签将xml转为yolo格式,并将坐标解析失败和不在namelist中的名字放到新的文件夹中
# ------------------------------------------------------------------------------------
import xml.etree.ElementTree as ET
import os
import shutil
from tqdm import tqdm
import sys

class XmlParse:
    def __init__(self, file_path):
        self.tree = None
        self.root = None
        self.xml_file_path = file_path

    def ReadXml(self):
        try:
            self.tree = ET.parse(self.xml_file_path)
            self.root = self.tree.getroot()
        except Exception as e:
            print(f"Parse XML failed for {self.xml_file_path}! Error: {e}")
            return None
        return self.tree

def parse_dimension(value, file_name, field_name):
    """解析尺寸字段,确保其为浮点数,并打印调试信息"""
    try:
        value = value.strip()
        return float(value)
    except ValueError:
        print(f"Warning: Could not parse dimension '{field_name}' with value '{value}' in file {file_name}.")
        return None

def xml2txt(xml, labels, name_list, unmatched_xml_dir):
    xml_files = sorted(os.listdir(xml))

    if not os.path.exists(labels):
        os.mkdir(labels)

    if not os.path.exists(unmatched_xml_dir):
        os.mkdir(unmatched_xml_dir)

    total_files = len(xml_files)
    for i in tqdm(xml_files, total=total_files, desc="Processing files", unit="file"):
        p = os.path.join(xml, i)
        xml_file = os.path.abspath(p)
        parse = XmlParse(xml_file)
        tree = parse.ReadXml()

        # 如果 XML 解析失败,直接移动文件到 unmatched_xml_dir 文件夹
        if tree is None:
            shutil.move(xml_file, os.path.join(unmatched_xml_dir, i))
            continue

        root = tree.getroot()

        # 解析宽度和高度
        W = parse_dimension(root.find('size').find('width').text, i, "width")
        H = parse_dimension(root.find('size').find('height').text, i, "height")

        # 如果尺寸解析失败,直接移动文件到 unmatched_xml_dir 文件夹
        if W is None or H is None:
            shutil.move(xml_file, os.path.join(unmatched_xml_dir, i))
            continue

        fil_name = os.path.splitext(i)[0]
        out_path = os.path.join(labels, fil_name + '.txt')
        with open(out_path, 'w+') as out:

            objects_found = False
            has_unmatched = False

            for obj in root.iter('object'):
                objects_found = True
                class_name = obj.find('name').text

                # 检查类别名是否在 name_list 中
                if class_name not in name_list:
                    has_unmatched = True
                    print(f"Class '{class_name}' not in name list in file {i}, moving XML to unmatched folder.")
                    break

                # 解析边界框坐标
                x_min = parse_dimension(obj.find('bndbox').find('xmin').text, i, "xmin")
                x_max = parse_dimension(obj.find('bndbox').find('xmax').text, i, "xmax")
                y_min = parse_dimension(obj.find('bndbox').find('ymin').text, i, "ymin")
                y_max = parse_dimension(obj.find('bndbox').find('ymax').text, i, "ymax")

                # 如果边界框解析失败,移动文件到 unmatched_xml_dir 文件夹
                if None in [x_min, x_max, y_min, y_max]:
                    has_unmatched = True
                    break

                # 计算中心点和宽高,并写入标签文件
                xcenter = x_min + (x_max - x_min) / 2
                ycenter = y_min + (y_max - y_min) / 2
                w = x_max - x_min
                h = y_max - y_min

                xcenter = round(xcenter / W, 6)
                ycenter = round(ycenter / H, 6)
                w = round(w / W, 6)
                h = round(h / H, 6)

                class_dict = dict(zip(name_list, range(len(name_list))))
                class_id = class_dict[class_name]
                out.write(f"{class_id} {xcenter} {ycenter} {w} {h}\\n")

        # 如果有未匹配类别或解析错误,将文件移动到 unmatched_xml_dir
        if has_unmatched or not objects_found:
            shutil.move(xml_file, os.path.join(unmatched_xml_dir, i))

def folder_Path():
    xml_path = 'D:/Desktop/play_phone_labels'
    labels = 'D:/Desktop/labels'
    unmatched_xml_dir = 'D:/Desktop/unmatched_xml'
    name_list = ['play_phone']

    xml2txt(xml_path, labels, name_list, unmatched_xml_dir)

if __name__ == '__main__':
    folder_Path()

转换后格式如下:

6.训练集验证集划分

(1)准备好图片和标签

将图片和标签放到一个文件夹中

(2)运行切分脚本

import os
import random
import shutil

def split_dataset(srcDir, trainDir, valDir, split_ratio=0.9):
    """
    将数据集划分为训练集和验证集,并保存到相应的文件夹中。
    """
    os.makedirs(os.path.join(trainDir, 'images'), exist_ok=True)
    os.makedirs(os.path.join(trainDir, 'labels'), exist_ok=True)
    os.makedirs(os.path.join(valDir, 'images'), exist_ok=True)
    os.makedirs(os.path.join(valDir, 'labels'), exist_ok=True)
    # 获取数据集中所有文件的列表
    file_list = os.listdir(srcDir)
    random.shuffle(file_list)
    print(file_list)

    split_index = int(len(file_list) * split_ratio)
    train_files = file_list[:split_index]
    val_files = file_list[split_index:]

    for file in train_files:
        #print(train_files)
        #print(file)
        if file.endswith('.jpg'):
            #print('3ok')
            img_src = os.path.join(srcDir, file)
            label_src = os.path.join(srcDir, file[:-4] + '.txt')
            #print('ok')
            #print(img_src)
            shutil.move(img_src, os.path.join(trainDir, 'images', file))
            if os.path.exists(label_src):
                shutil.move(label_src, os.path.join(trainDir, 'labels', file[:-4] + '.txt'))
                
        if file.endswith('.png'):
            img_src = os.path.join(srcDir, file)
            label_src = os.path.join(srcDir, file[:-4] + '.txt')
            shutil.move(img_src, os.path.join(trainDir, 'images', file))
            if os.path.exists(label_src):
                shutil.move(label_src, os.path.join(trainDir, 'labels', file[:-4] + '.txt')) 

    for file in val_files:
        if file.endswith('.jpg'):
            img_src = os.path.join(srcDir, file)
            label_src = os.path.join(srcDir, file[:-4] + '.txt')
            shutil.move(img_src, os.path.join(valDir, 'images', file))
            if os.path.exists(label_src):
                shutil.move(label_src, os.path.join(valDir, 'labels', file[:-4] + '.txt'))

if __name__ == '__main__':
    # 输入文件夹路径
    srcDir = 'D:/Desktop/play_phone'  
    trainDir = 'D:/Desktop/datesets/train'  
    valDir = 'D:/Desktop/datesets/val'  

    # 调用函数划分数据集
    split_dataset(srcDir, trainDir, valDir)

成功转换如下图

恭喜你成功制作了自己的数据集!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐