COCO目标检测标签转YOLO格式+按需实现标签筛选

参考文章:详细!正确!COCO数据集(.json)训练格式转换成YOLO格式(.txt)

YOLOv8官方转换代码:ultralytics/JSON2YOLO

COCO官方转换代码:cocodataset/cocoapi

COCO标签转YOLO:将下载的训练集标签instances_train2017.json验证集标签instances_val2017.json转成YOLO格式。

转换的坑:

1.坐标转换:COCO坐标是左上+宽高。

2.iscrowd:一条标注信息中该key对应的值为1,则存在覆盖问题。如下图红色箭头所指,由于人过多,用一个大的框覆盖了所有人,因而未对所有目标进行详细标注。

3.不是所有图片都用于训练or验证由第二条坑,存在覆盖问题的图片,应该不加入有效图片列表,这一点在image_info[image_id]['valid']中实现,save_yolo_labels默认获取了全部标签在获取自己想要的类别数据时,则去除了含有覆盖问题的图片。

4.关于第3条坑,我的建议是重新复制保存自己所需图片,因为自己用公共数据集不需要所有类别图片,有效图片(存在目标类别,且不存在覆盖问题)作为正样本,无效的图(不存在目标类别)作为负样本。

转换代码:

import json
import os
from collections import defaultdict

def tlwh2xywhn(xywh, shape, precision=8):
    """左上+宽高 => 归一化的中心+宽高"""
    x, y, w, h = xywh[:4]
    x_center = round((x + w / 2.0) / shape[1], precision)
    y_center = round((y + h / 2.0) / shape[0], precision)
    box_width = round(w / shape[1], precision)
    box_height = round(h / shape[0], precision)

    return [x_center, y_center, box_width, box_height]


def coco2yolo_get_dict(coco_json_path):
    """
    读取coco检测标签文件地址,将其转化成字典信息
    :param coco_json_path: 标签地址
    :return: 字典信息
    """
    coco_data = json.loads(open(coco_json_path).read())
    image_list = coco_data['images']        # 列表存放字典,需要用到的key{'file_name', 'height', 'width', 'id'}
    annotations = coco_data['annotations']  # 列表存放字典,需要用到的key{'bbox': xywh, 'category_id', 'id'}
    categories = coco_data['categories']    # 列表存放字典,需要用到的key{'supercategory', 'id', name}

    print(f"INFO:读取到的图片总共有: [{len(image_list)}] 张,获取的标签条目总共有: [{len(annotations)}] 个。")
    print(f"INFO:第一个图片条目:{image_list[0]}")
    print(f"INFO:第一个标注条目:{annotations[0]}")

    # name2index = {}     # {类别名称:标签索引}
    # index2name = {}     # {标签索引:类别名称},用于生成标签文件
    id2index = {}       # {id:标签索引}
    # index2id = {}       # {标签索引:id}

    for i, category in enumerate(categories):  # 构建类别名和序号的映射关系
        # name2index[category['name']] = i
        # index2name[i] = category['name']
        id2index[category['id']] = i
        # index2id[i] = category['id']

    image_info = defaultdict(dict)     # 存储图片信息

    # 先遍历图片,为所有图片建立一个字典条目,用于储存信息
    for image in image_list:
        image_id = image['id']    # coco_data['images']的'id'对应coco_data['annotations']的'image_id'
        file_name = image['file_name']
        shape = (image['height'], image['width'])   # (高度,宽度)

        image_info[image_id]['file_name'] = file_name   # 一张图片的文件名
        image_info[image_id]['shape'] = shape           # 一张图片的形状
        image_info[image_id]['yolo_data'] = []          # 一张图片的yolo格式数据
        image_info[image_id]['yolo_label'] = set()      # 一张图片还有的标签字典,用于筛选数据,放在自己的数据集中
        image_info[image_id]['valid'] = True            # 一张图是否有用,原数据有‘iscrowd’属性,表示覆盖,去除改类图片

    # 然后遍历标注信息,因为一张图片可能有多个标注条目信息,所以需要用哈希映射到对应图片
    for anno in annotations:
        image_id = anno['image_id']     # 图片id
        bbox_xywh = anno['bbox']        # 原始xywh,需要归一化
        category_id = anno['category_id']   # 类别id,需要映射成标签索引
        is_crowd = anno['iscrowd']      # 是否有类别覆盖

        index = id2index[category_id]   # 获取标签索引
        bbox = tlwh2xywhn(xywh=bbox_xywh, shape=image_info[image_id]['shape'])  # 获取归一化坐标

        image_info[image_id]['yolo_data'].append([index] + bbox)
        image_info[image_id]['yolo_label'].add(index)
        image_info[image_id]['valid'] = image_info[image_id]['valid'] and not is_crowd

    # 给标签排个序
    for _, image in image_info.items():
        image['yolo_data'].sort(key=lambda x: x[0])     # 按类别序号排序

    print(f"INFO:提取数据成功,获取的第一条数据:{image_info[image_list[0]['id']]}")

    return image_info


def save_yolo_labels(image_info, output_dir, image_root, txt_path):
    """
    将字典信息里的yolo坐标保存到指定文件夹下
    :param image_info: 字典信息
    :param output_dir: 保存路径
    :param image_root: 图片根路径
    :param txt_path: 保存图片路径的txt的保存路径
    :return:
    """
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    i = 0   # 标注框个数
    file_list = []  # 文件列表

    # 遍历每张图片的信息
    for image_id, info in image_info.items():
        file_name = info['file_name']
        yolo_data = info['yolo_data']

        txt_file_path = os.path.join(output_dir, f"{os.path.splitext(file_name)[0]}.txt")   # 构建输出文件路径
        file_list.append(os.path.join(image_root, file_name))

        with open(txt_file_path, 'w') as f:     # 写入YOLO格式数据到TXT文件
            for data in yolo_data:
                line = ' '.join(map(str, data))     # 将列表中的数据转换成字符串并写入文件
                f.write(line + '\n')
                i += 1

    print(f"INFO:成功将YOLO标签保存到目录:{output_dir},共{i}个标注。")

    # 生成包含所有图片路径的文件
    with open(txt_path, 'w') as f:
        for file_path in file_list:
            f.write(file_path + '\n')

    print(f"INFO:所有文件路径已保存到:{txt_path}")


def save_yolo_labels_valid(image_info, output_dir, image_root, txt_path, valid_label, label_map):
    """
    只保留给定标签的图片。
    :param image_info: 字典信息
    :param output_dir: 保存路径
    :param image_root: 图片根路径
    :param txt_path: 保存图片路径的txt的保存路径
    :param valid_label: 有效的标签列表
    :param label_map: 标签映射成新的标签
    :return:
    """
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    i = 0   # 标注框个数
    file_list = []  # 文件列表

    # 遍历每张图片的信息
    for image_id, info in image_info.items():
        file_name = info['file_name']
        yolo_data = info['yolo_data']
        yolo_label = info['yolo_label']
        valid = info['valid']

        if not valid or not set(valid_label) & yolo_label:    # 有效标签和图片含有的标签交集为空,则跳过
            continue

        txt_file_path = os.path.join(output_dir, f"{os.path.splitext(file_name)[0]}.txt")   # 构建输出文件路径
        file_list.append(os.path.join(image_root, file_name))

        valid_yolo_data = []
        for data in yolo_data:
            label = data[0]
            if label in valid_label:
                new_label = label_map[label]    # 将标签映射成新的标签
                valid_yolo_data.append([new_label] + data[1:])

        with open(txt_file_path, 'w') as f:     # 写入YOLO格式数据到TXT文件
            for data in valid_yolo_data:
                line = ' '.join(map(str, data))     # 将列表中的数据转换成字符串并写入文件
                f.write(line + '\n')
                i += 1

    print(f"INFO:成功将YOLO标签保存到目录:{output_dir},共{i}个标注。")

    # 生成包含所有图片路径的文件
    with open(txt_path, 'w') as f:
        for file_path in file_list:
            f.write(file_path + '\n')

    print(f"INFO:所有文件路径已保存到:{txt_path}")


def save_yolo_negative(image_info, image_root, txt_path, exclude_label):
    """
    只保留不含exclude_label标签的图片
    :param image_info: 字典信息
    :param image_root: 图片根路径
    :param txt_path: 保存图片路径的txt的保存路径
    :param exclude_label: 需要排除的标签
    :return:
    """
    file_list = []  # 文件列表

    # 遍历每张图片的信息
    for image_id, info in image_info.items():
        file_name = info['file_name']
        yolo_label = info['yolo_label']

        if set(exclude_label) & yolo_label:    # 有效标签和图片含有的标签存在交集,则跳过
            continue

        file_list.append(os.path.join(image_root, file_name))

    # 生成包含所有图片路径的文件
    with open(txt_path, 'w') as f:
        for file_path in file_list:
            f.write(file_path + '\n')

    print(f"INFO:所有文件路径已保存到:{txt_path}")


if __name__ == '__main__':
    json_path = r'./COCO2017/annotations_trainval2017/instances_val2017.json'
    image_info_all = coco2yolo_get_dict(json_path)

    # ######################################## 处理所有标签 #######################################################
    # save_path = r'./COCO2017/labels_det/val2017'
    # txt_path = r'./COCO2017/val2017_det.txt'
    # save_yolo_labels(image_info_all, save_path, './images/val2017', txt_path)

    # ######################################## 获取目标标签 #######################################################
    # save_path_kx = r'./COCO2017/det_box/labels/train2017'
    # txt_path_kx = r'./COCO2017/train2017_det_kx.txt'
    # valid_label_kx = {28}
    # label_map_kx = {28: 1}
    #
    # save_yolo_labels_valid(image_info_all, save_path_kx, './images/train2017', txt_path_kx, valid_label_kx,
    #                       label_map_kx)

    # save_path_ren = r'./COCO2017/det_ren/labels/train2017'
    # txt_path_ren = r'./COCO2017/train2017_det_ren.txt'
    # valid_label_ren = {0}
    # label_map_ren = {0: 0}
    #
    # save_yolo_labels_valid(image_info_all, save_path_ren, './images/train2017', txt_path_ren, valid_label_ren,
    #                        label_map_ren)

    # ######################################## 获取负样本图 #######################################################
    # txt_path_neg = r'./COCO2017/val2017_det_neg.txt'
    # save_yolo_negative(image_info_all, './images/val2017', txt_path_neg, exclude_label={0, 24, 26, 28})

转换结果:

只获取和人相关的图片:

只获取和箱子相关的图片,并设置箱子新的ID为1: