YOLO数据集处理工具:与make-sense搭配使用,辅助划分训练集和验证集,简化标注数据的整理与划分

背景介绍

在使用YOLO进行目标检测任务时,我们经常会遇到这样的场景:使用make-sense等工具标注完数据后,需要将杂乱的数据整理成YOLO训练所需的格式,并合理划分训练集和验证集。这个过程如果手动操作,不仅繁琐还容易出错。

为了解决这个问题,我开发了一个命令行工具,它可以帮助我们:

  1. 类别整理和预览标注的图片
  2. 方便地选择验证集样本
  3. 自动构建符合YOLO格式的数据集

工具特点

  1. 按类别预览
  • 自动将标注后的图片按类别分类展示
  • 支持同一张图片包含多个类别的情况
  • 使用软链接(Linux)或复制(Windows)方式,节省存储空间
  1. 灵活的验证集选择
  • 可以直观地查看每个类别的图片
  • 通过简单的复制操作选择验证集
  • 确保验证集样本的代表性
  1. 自动构建数据集
  • 自动创建YOLO所需的目录结构
  • 正确处理图片和标注文件的对应关系
  • 保证数据集的完整性
  1. 安全可靠
  • 不会删除既有文件
  • 对已存在的目录进行提示
  • 详细的操作日志

使用方法

  1. 安装依赖:
pip install pyyaml
  1. 准备配置文件(dataset.yaml):
path: ./dataset
train: images/train
val: images/val
names:
  0: person
  1: car
  2: traffic_light
  1. 基本使用流程:
# 第一步:组织预览
python yolo_dataset.py organize --yaml dataset.yaml --images raw_data/images --labels raw_data/labels

# 第二步:手动选择测试集图片

# 第三步:构建数据集
python yolo_dataset.py build --yaml dataset.yaml --images raw_data/images --labels raw_data/labels

使用建议

  1. 验证集选择:
  • 每个类别选择约20%的图片
  • 选择具有代表性的样本
  • 尽量覆盖不同场景和角度
  1. 目录组织:
  • 保持原始数据的完整性
  • 使用相对路径以便项目迁移
  • 及时备份重要数据

工具优势

  1. 提高效率
  • 自动化的文件组织过程
  • 直观的验证集选择方式
  • 快速构建标准数据集
  1. 降低错误
  • 自动验证数据集格式
  • 保证标签和图片的对应关系
  • 避免人工操作失误
  1. 易于使用
  • 命令行界面简单直观
  • 详细的帮助信息
  • 完整的错误提示

总结

这个工具极大地简化了YOLO数据集的准备过程,使研究人员可以将更多精力放在模型训练和优化上。无论是个人项目还是团队协作,它都能帮助你更高效地管理数据集。

源码获取

import os
import shutil
import yaml
import argparse
from collections import defaultdict

def parse_args():
    """解析命令行参数"""
    # 创建带有详细描述的解析器
    example_text = """
使用示例:

1. 显示帮助信息:
    %(prog)s -h                # 显示主帮助
    %(prog)s organize -h       # 显示organize命令帮助
    %(prog)s build -h          # 显示build命令帮助

2. 标准流程:
    # 第一步:组织预览
    %(prog)s organize --yaml dataset.yaml --images raw_data/images --labels raw_data/labels

    # 第二步:手动选择测试集图片(将图片复制到test_selected目录)

    # 第三步:构建数据集
    %(prog)s build --yaml dataset.yaml --images raw_data/images --labels raw_data/labels

3. 自定义路径示例:
    # 组织预览
    %(prog)s organize \\
        --yaml custom_dataset.yaml \\
        --images /data/my_images \\
        --labels /data/my_labels \\
        --preview sorted_preview \\
        --test-selected my_test_set

    # 构建数据集
    %(prog)s build \\
        --yaml custom_dataset.yaml \\
        --images /data/my_images \\
        --labels /data/my_labels \\
        --test-selected my_test_set
    """
    
    parser = argparse.ArgumentParser(
        description='YOLO数据集组织和构建工具',
        epilog=example_text,
        formatter_class=argparse.RawDescriptionHelpFormatter  # 保持原始格式
    )
    
    subparsers = parser.add_subparsers(dest='command', help='可用命令')
    
    # organize 子命令
    organize_parser = subparsers.add_parser(
        'organize', 
        help='按类别组织数据集预览',
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    organize_parser.add_argument('--yaml', 
                              default='dataset.yaml',
                              help='数据集YAML配置文件路径 (默认: %(default)s)')
    organize_parser.add_argument('--images',
                              default='raw_data/images',
                              help='原始图片目录路径 (默认: %(default)s)')
    organize_parser.add_argument('--labels',
                              default='raw_data/labels',
                              help='原始标注文件目录路径 (默认: %(default)s)')
    organize_parser.add_argument('--preview',
                              default='preview',
                              help='预览目录名 (默认: %(default)s)')
    organize_parser.add_argument('--test-selected',
                              default='test_selected',
                              help='测试集选择目录名 (默认: %(default)s)')
    
    # build 子命令
    build_parser = subparsers.add_parser(
        'build', 
        help='构建最终数据集',
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    build_parser.add_argument('--yaml',
                           default='dataset.yaml',
                           help='数据集YAML配置文件路径 (默认: %(default)s)')
    build_parser.add_argument('--images',
                           default='raw_data/images',
                           help='原始图片目录路径 (默认: %(default)s)')
    build_parser.add_argument('--labels',
                           default='raw_data/labels',
                           help='原始标注文件目录路径 (默认: %(default)s)')
    build_parser.add_argument('--test-selected',
                           default='test_selected',
                           help='测试集选择目录名 (默认: %(default)s)')
    
    args = parser.parse_args()
    
    if args.command is None:
        parser.print_help()
        exit(1)
        
    return args

def load_and_validate_yaml(yaml_path):
    """加载并验证dataset.yaml"""
    try:
        with open(yaml_path, 'r', encoding='utf-8') as f:
            config = yaml.safe_load(f)
        
        required_fields = ['path', 'train', 'val', 'names']
        for field in required_fields:
            if field not in config:
                raise ValueError(f"dataset.yaml 缺少必需字段: {field}")
        
        class_names = config['names'].values()
        if len(class_names) != len(set(class_names)):
            raise ValueError("类别名称必须唯一!")
        
        class_ids = config['names'].keys()
        if list(map(int, class_ids)) != list(range(len(class_ids))):
            raise ValueError("类别索引必须从0开始连续!")
        
        return config
    
    except FileNotFoundError:
        raise FileNotFoundError(f"找不到配置文件: {yaml_path}")
    except yaml.YAMLError as e:
        raise ValueError(f"YAML格式错误: {e}")

def check_existing_dirs(dirs):
    """检查目录是否已存在并包含文件"""
    existing_dirs = []
    
    for dir_path in dirs.values():
        if os.path.exists(dir_path):
            files = []
            for root, _, filenames in os.walk(dir_path):
                files.extend(os.path.join(root, f) for f in filenames)
            if files:
                existing_dirs.append((dir_path, len(files)))
    
    if existing_dirs:
        print("\n警告: 以下目录已存在并包含文件:")
        for dir_path, file_count in existing_dirs:
            print(f"- {dir_path}: {file_count}个文件")
        print("这些目录不会被删除,新文件将直接添加,同名文件将被覆盖")
        input("按Enter键继续,或Ctrl+C取消...")

def create_dir_structure(args, dataset_config):
    """创建目录结构"""
    dirs = {
        'images': args.images,
        'labels': args.labels,
        'dataset_root': dataset_config['path'],
        'dataset_train_images': os.path.join(dataset_config['path'], dataset_config['train']),
        'dataset_val_images': os.path.join(dataset_config['path'], dataset_config['val']),
        'dataset_train_labels': os.path.join(dataset_config['path'], 'labels/train'),
        'dataset_val_labels': os.path.join(dataset_config['path'], 'labels/val')
    }
    
    if hasattr(args, 'preview'):
        dirs.update({
            'preview': args.preview,
            'test_selected': args.test_selected
        })
    else:
        dirs['test_selected'] = args.test_selected
    
    check_existing_dirs(dirs)
    
    for dir_path in dirs.values():
        os.makedirs(dir_path, exist_ok=True)
    
    return dirs

def organize_by_class(dirs, config):
    """按类别组织图片预览"""
    class_to_images = defaultdict(list)
    
    for txt_file in os.listdir(dirs['labels']):
        if not txt_file.endswith('.txt'):
            continue
        
        image_base = txt_file.replace('.txt', '')
        txt_path = os.path.join(dirs['labels'], txt_file)
        
        with open(txt_path, 'r') as f:
            for line in f:
                class_id = int(line.split()[0])
                class_name = config['names'][str(class_id)]
                class_to_images[class_name].append(image_base)
    
    for class_name, images in class_to_images.items():
        class_dir = os.path.join(dirs['preview'], class_name)
        os.makedirs(class_dir, exist_ok=True)
        
        for img_base in images:
            for ext in ['.jpg', '.jpeg', '.png', '.bmp']:
                src_path = os.path.join(dirs['images'], img_base + ext)
                if os.path.exists(src_path):
                    dst_path = os.path.join(class_dir, img_base + ext)
                    if os.name == 'nt':  # Windows
                        shutil.copy2(src_path, dst_path)
                    else:  # Linux/Mac
                        if os.path.exists(dst_path):
                            os.remove(dst_path)
                        os.symlink(os.path.abspath(src_path), dst_path)
                    break
    
    return class_to_images

def build_dataset(dirs):
    """构建最终数据集"""
    test_images = set()
    if os.path.exists(dirs['test_selected']):
        for img_name in os.listdir(dirs['test_selected']):
            if any(img_name.endswith(ext) for ext in ['.jpg', '.jpeg', '.png', '.bmp']):
                test_images.add(os.path.splitext(img_name)[0])
    
    processed_count = {'train': 0, 'val': 0}
    
    for img_name in os.listdir(dirs['images']):
        if not any(img_name.endswith(ext) for ext in ['.jpg', '.jpeg', '.png', '.bmp']):
            continue
            
        base_name = os.path.splitext(img_name)[0]
        is_test = base_name in test_images
        
        target_dir = 'val' if is_test else 'train'
        target_img_dir = dirs[f'dataset_{target_dir}_images']
        target_label_dir = dirs[f'dataset_{target_dir}_labels']
        
        # 复制图片
        shutil.copy2(
            os.path.join(dirs['images'], img_name),
            os.path.join(target_img_dir, img_name)
        )
        
        # 复制标注文件
        label_name = f"{base_name}.txt"
        label_path = os.path.join(dirs['labels'], label_name)
        if os.path.exists(label_path):
            shutil.copy2(
                label_path,
                os.path.join(target_label_dir, label_name)
            )
        
        processed_count[target_dir] += 1
    
    return processed_count

def command_organize(args):
    """organize命令的实现"""
    config = load_and_validate_yaml(args.yaml)
    dirs = create_dir_structure(args, config)
    
    print("\n目录结构:")
    print(f"""
    {dirs['images']}/          # 原始图片目录
    {dirs['labels']}/          # 原始标注文件目录
    {dirs['preview']}/         # 按类别组织的预览目录
        ├── <类别1>/
        ├── <类别2>/
        └── ...
    {dirs['test_selected']}/   # 测试集选择目录
    {dirs['dataset_root']}/    # 最终数据集目录
        ├── images/
        │   ├── train/
        │   └── val/
        └── labels/
            ├── train/
            └── val/
    """)
    
    class_to_images = organize_by_class(dirs, config)
    
    print("\n类别统计:")
    for class_name, images in sorted(class_to_images.items()):
        print(f"{class_name}: {len(images)}张图片")
    
    print("\n后续步骤:")
    print("1. 在预览目录中检查各个类别的图片")
    print("2. 将选作测试集的图片复制到测试集选择目录")
    print(f"3. 运行 '{os.path.basename(__file__)} build' 构建最终数据集")

def command_build(args):
    """build命令的实现"""
    config = load_and_validate_yaml(args.yaml)
    dirs = create_dir_structure(args, config)
    
    print("\n正在构建数据集...")
    processed = build_dataset(dirs)
    
    print("\n数据集构建完成!")
    print(f"训练集: {processed['train']}张图片")
    print(f"验证集: {processed['val']}张图片")

def main():
    args = parse_args()
    
    try:
        if args.command == 'organize':
            command_organize(args)
        elif args.command == 'build':
            command_build(args)
    except Exception as e:
        print(f"\n错误: {str(e)}")
        exit(1)

if __name__ == "__main__":
    main()

后记

希望这个小工具能帮助到你

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Deng_Xian_Shemg

捐助1元钱

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

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

打赏作者

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

抵扣说明:

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

余额充值