【YOLO】X-Anylabeling 数据集标注

前言

本文将介绍如何使用 X-AnyLabeling 软件进行 YOLO 格式的数据集标注与导出。对于初学者来说,建议参考X-AnyLabeling QuickStart Guides进行安装与使用。特别是在第1.2节“Running from GUI”中,详细介绍了如何通过GUI进行操作。

为了快速开始,建议前往X-AnyLabeling v2.5.4页面,下载适合你系统的GUI版本。我个人下载并使用的是X-AnyLabeling-CPU.exe

参考资料:

整体思路

准备数据集–数据集标注–标签导出–数据集拆分

  1. 准备数据集:通过相机拍照或使用 X-AnyLabeling 自带的视频抽帧功能获取足够数量的数据集。

  2. 数据集标注:导入数据集,并按照分类要求逐个标注,确保每次只标注一种类别。

  3. 标签导出:完成数据标注后,将标注结果导出为 YOLO 格式,每张图像会生成一个对应的 .txt 标签文件。

  4. 数据集拆分:根据训练集、验证集、测试集的比例(8:1:1)使用 Python 脚本进行数据集拆分

准备数据集

这里不再讨论如何通过相机采集数据集,而重点介绍如何利用 X-anylableling 自带的视频抽帧功能获取数据集。

1.导入视频文件

打开 X-anylableling 后,导入你需要处理的视频文件。

2.配置抽帧参数

X-AnyLabeling 默认视频以 30 帧/秒 录制,帧率参数表示抽取的间隔。
例如设置为 1 时,每帧抽取 1 帧(1 秒视频 → 30 张图片);设置为 15 时,每 15 帧抽取 1 帧(1 秒视频 → 2 张图片);设置为 30 时,每隔 30 帧采样 1 张图片。

请根据实际需求配置帧率。

在这里插入图片描述

3. 生成图片集

点击 OK 后,X-AnyLabeling 会将视频拆分为图片,并在界面中显示生成的图片集。同时,这些图片会被保存到与原始视频相同的目录下,存放在一个新创建的文件夹中。

右下角可以查看拆分后的图片文件:
在这里插入图片描述

数据集标注

1. 导入数据集

图片集:点击“打开文件夹”即可导入。
视频抽帧:按照数据集准备步骤操作后,即可直接开始标注。

2. 修改配置

建议先开启 X-AnyLabeling 的 “自动使用上一个标签” 选项,然后每轮只标注同一种物体,这样系统会自动沿用上次的标签类型,避免每次手动选择,提高标注效率。
在这里插入图片描述

3. 开始标注

  • 按 R 键开启绘制模式(出现十字光标表示可以标注)
  • 框选目标后,输入标签并点击 OK 进行确认
    在这里插入图片描述

4. 后续标注

  • 添加标签后,后续框选会自动沿用该标签类型。
  • 若需修改标签,双击侧边栏的标签即可编辑。
    在这里插入图片描述
    建议每轮仅标注 一种物体,直至所有类别标注完成,以确保标注效率和准确性。

标签导出

1. 新建一个classes.txt文件

首先,创建一个 classes.txt 文件,用于存放所有标签的索引信息。

举例来说,假设在 X-AnyLabeling 中右侧的标签顺序如下:
在这里插入图片描述
那么我应该创建一个 classes.txt 文件,内容为:
在这里插入图片描述
注意: 标签顺序非常重要!顺序必须与 X-AnyLabeling 中的标签顺序保持一致,否则会导致导出的 YOLO 格式标签无法正确对应到实际类别。

补充解释:
在使用 X-AnyLabeling 导出YOLO格式标签时,需要先选中一个描述类别的.txt文件,该文件按行列出所有类别(例如fruit、veg、drink)。这是因为YOLO训练时只使用数字作为类别标识,而这些数字索引是依据.txt文件中类别的排列顺序自动生成的(第一行为0,第二行为1,依此类推)。这种映射关系确保了导出的标签文件中的数字能正确对应到具体类别,从而避免因类别顺序不一致或错误而影响模型训练效果。

2. 导出 YOLO 水平框标签

首先,点击 导出 YOLO 水平框标签 按钮。
在这里插入图片描述
然后,选择我们刚才创建的 classes.txt 标签文件,并指定导出的标签文件夹路径。
在这里插入图片描述
最后,点击 确认,即可完成标签的导出。

删除图片文件夹中的 .json 文件

在 X-AnyLabeling 中,标注每张图片后会自动生成对应的 .json 文件,用于保存标注信息。
为了避免可能的错误,我们使用以下 Windows 指令来删除数据集文件夹中的所有 .json 文件:

cd "C:\path\to\img_folder"
del *.json

数据集拆分

通常,我们按照 8:1:1 的比例将数据集拆分为 训练集、验证集和测试集。为了简化这一过程,可以使用 Python 脚本自动完成拆分。

创建 split.py 文件

在原始数据集的文件夹下,新建一个 split.py 文件,用于执行数据集拆分。
在这里插入图片描述

代码粘贴

将以下代码粘贴到 split.py 文件中,并确保原始数据集的结构如下:

dataset_org/
│── images/   # 存放所有图片
│── labels/   # 存放所有标签
│── split.py  # 数据集拆分脚本

以下数据集拆分脚本采用分层抽样,确保训练集、验证集和测试集中各类别的样本数量大致均衡,避免随机拆分导致的数据分布不均问题。

# 该脚本将YOLO目标检测的样本图片和标注数据按比例切分为三部分:训练集、验证集和测试集
import shutil
import random
import os
import collections

# 修改你的项目路径、原始图像数据和标签路径
cur_path = "C:/Users/agt26/OneDrive/Desktop/xf_datasets/"
image_original_path = cur_path + "images/"
label_original_path = cur_path + "labels/"
image_format = '.jpg'   # 样本照片的格式后缀 .jpg .png .bmp等等

# 标签拆分比例
train_percent = 0.8
val_percent = 0.1
test_percent = 0.1

# 训练集路径
train_image_path = os.path.join(cur_path, "datasets/train/images/")
train_label_path = os.path.join(cur_path, "datasets/train/labels/")

# 验证集路径
val_image_path = os.path.join(cur_path, "datasets/val/images/")
val_label_path = os.path.join(cur_path, "datasets/val/labels/")

# 测试集路径
test_image_path = os.path.join(cur_path, "datasets/test/images/")
test_label_path = os.path.join(cur_path, "datasets/test/labels/")

# 训练集目录
list_train = os.path.join(cur_path, "datasets/train.txt")
list_val = os.path.join(cur_path, "datasets/val.txt")
list_test = os.path.join(cur_path, "datasets/test.txt")


def del_file(path):
    for i in os.listdir(path):
        file_data = path + "\\" + i
        os.remove(file_data)


def mkdir():
    if not os.path.exists(train_image_path):
        os.makedirs(train_image_path)
    else:
        del_file(train_image_path)
    if not os.path.exists(train_label_path):
        os.makedirs(train_label_path)
    else:
        del_file(train_label_path)

    if not os.path.exists(val_image_path):
        os.makedirs(val_image_path)
    else:
        del_file(val_image_path)
    if not os.path.exists(val_label_path):
        os.makedirs(val_label_path)
    else:
        del_file(val_label_path)

    if not os.path.exists(test_image_path):
        os.makedirs(test_image_path)
    else:
        del_file(test_image_path)
    if not os.path.exists(test_label_path):
        os.makedirs(test_label_path)
    else:
        del_file(test_label_path)


def clearfile():
    if os.path.exists(list_train):
        os.remove(list_train)
    if os.path.exists(list_val):
        os.remove(list_val)
    if os.path.exists(list_test):
        os.remove(list_test)


def get_main_class(label_path):
    """
    读取YOLO格式标签文件,返回出现频率最高的类别ID
    如果标签为空或出错,返回-1
    """
    try:
        with open(label_path, 'r') as f:
            lines = f.readlines()
            if not lines:
                return -1
            
            # 统计每个类别出现的次数
            class_counts = collections.Counter()
            for line in lines:
                parts = line.strip().split()
                if len(parts) >= 5:  # YOLO格式: class x y w h
                    class_id = int(parts[0])
                    class_counts[class_id] += 1
            
            # 返回出现频率最高的类别ID
            return class_counts.most_common(1)[0][0]
    except:
        return -1


def main():
    mkdir()
    clearfile()

    file_train = open(list_train, 'w')
    file_val = open(list_val, 'w')
    file_test = open(list_test, 'w')

    total_txt = os.listdir(label_original_path)
    
    # 按类别分组
    class_images = collections.defaultdict(list)
    
    print("正在分析标签文件中的类别信息...")
    for i, txt_name in enumerate(total_txt):
        name = txt_name[:-4]
        label_path = os.path.join(label_original_path, txt_name)
        
        # 获取图片的主要类别
        main_class = get_main_class(label_path)
        if main_class >= 0:  # 有效的类别ID
            class_images[main_class].append((i, name))
    
    # 打印每个类别的图片数量
    print("数据集类别分布情况:")
    for class_id, images in class_images.items():
        print(f"类别 {class_id}: {len(images)} 张图片")
    
    # 按类别进行分层抽样
    train_indices = []
    val_indices = []
    test_indices = []
    
    # 创建计数器跟踪每个集合中各类别的图片数量
    train_class_counts = collections.Counter()
    val_class_counts = collections.Counter()
    test_class_counts = collections.Counter()
    
    for class_id, images in class_images.items():
        num_images = len(images)
        indices = list(range(num_images))
        random.shuffle(indices)
        
        num_train = int(num_images * train_percent)
        num_val = int(num_images * val_percent)
        
        # 为每个类别分配训练/验证/测试集索引
        train_idx = indices[:num_train]
        val_idx = indices[num_train:num_train+num_val]
        test_idx = indices[num_train+num_val:]
        
        # 更新每个集合中各类别的计数
        train_class_counts[class_id] = len(train_idx)
        val_class_counts[class_id] = len(val_idx)
        test_class_counts[class_id] = len(test_idx)
        
        # 将索引转换为原始图片索引
        train_indices.extend([images[idx][0] for idx in train_idx])
        val_indices.extend([images[idx][0] for idx in val_idx])
        test_indices.extend([images[idx][0] for idx in test_idx])
    
    print(f"\n训练集总数:{len(train_indices)}, 验证集总数:{len(val_indices)}, 测试集总数:{len(test_indices)}")
    
    # 打印每个数据集中各类别的分布情况
    print("\n训练集类别分布:")
    for class_id, count in sorted(train_class_counts.items()):
        print(f"类别 {class_id}: {count} 张图片")
    
    print("\n验证集类别分布:")
    for class_id, count in sorted(val_class_counts.items()):
        print(f"类别 {class_id}: {count} 张图片")
    
    print("\n测试集类别分布:")
    for class_id, count in sorted(test_class_counts.items()):
        print(f"类别 {class_id}: {count} 张图片")
    
    # 处理每个图片
    for i, txt_name in enumerate(total_txt):
        name = txt_name[:-4]
        
        srcImage = image_original_path + name + image_format
        srcLabel = label_original_path + name + ".txt"
        
        if i in train_indices:
            dst_train_Image = train_image_path + name + image_format
            dst_train_Label = train_label_path + name + '.txt'
            shutil.copyfile(srcImage, dst_train_Image)
            shutil.copyfile(srcLabel, dst_train_Label)
            file_train.write(dst_train_Image + '\n')
        elif i in val_indices:
            dst_val_Image = val_image_path + name + image_format
            dst_val_Label = val_label_path + name + '.txt'
            shutil.copyfile(srcImage, dst_val_Image)
            shutil.copyfile(srcLabel, dst_val_Label)
            file_val.write(dst_val_Image + '\n')
        elif i in test_indices:
            dst_test_Image = test_image_path + name + image_format
            dst_test_Label = test_label_path + name + '.txt'
            shutil.copyfile(srcImage, dst_test_Image)
            shutil.copyfile(srcLabel, dst_test_Label)
            file_test.write(dst_test_Image + '\n')

    file_train.close()
    file_val.close()
    file_test.close()


if __name__ == "__main__":
    main() 

注意:

  1. 只需修改 cur_path 和 image_format 即可*(无需手动调整图片尺寸,YOLO 在训练时会自动调整为 640×640)*
  2. 字符串拼接格式需严格遵循规范,尤其是路径中的 / 斜杠,否则可能会导致 split.py 运行时报错,或者图片和标签未正确存入 images 和 labels 文件夹,而是散落在其他目录中。

为避免这些问题,建议严格按照提供的格式修改路径

至此,您的数据集已成功制作完成。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值