VOC数据集转COCO数据集工具

当我们处理目标检测任务时,很多时候我们会遇到必须将数据集从 VOC 格式转换为 COCO 格式的情况。VOC 格式和 COCO 格式是两种广泛使用的目标检测数据集格式。VOC 格式使用 XML 文件来存储每个图像的标注信息,而 COCO 格式使用 JSON 文件。这种格式转换通常是为了适应不同的深度学习框架或工具。

为了简化这个过程,我给大家分享一个 Python 脚本,可以将 VOC 格式的数据集转换为 COCO 格式,并且还支持自动复制图像到指定目录。

首先,我们需要准备以下内容:

  • VOC 格式的标注文件夹(包含 XML 文件)
  • 目标转换后的 COCO 格式 JSON 文件所在文件夹
  • 类别列表(以字符串形式表示,比如 ['cat', 'dog', 'person']
  • 图像文件夹,存放对应的图像文件

接下来,我们创建一个 VOC2COCOConverter 对象,并指定上面提到的各个参数。我们还可以选择设置 proportions 参数,以控制最后生成的 COCO 格式数据集在训练集、验证集和测试集之间的比例。默认情况下,这个参数设置为 [80, 10, 10],即将数据集划分为 80%的训练集,10%的验证集和 10%的测试集。当然如果你需要也可以设置为 [100][80, 20]

我们还可以设置 copy_images 参数来决定是否复制图像文件。如果将其设置为 True,则脚本会自动将图像复制到与生成的 COCO 格式 JSON 文件同名的文件夹中。这个功能对于数据集管理很有用,可以在用其他框架操作数据集、数据探索或模型调试时方便地使用数据集。

下面是示例代码的部分内容:

import os
import glob
import json
import shutil
import xml.etree.ElementTree as ET
from collections import defaultdict, Counter
from tqdm import tqdm

START_BOUNDING_BOX_ID = 1

class VOC2COCOConverter:
    def __init__(self, xml_dir, json_dir, classes, img_dir, proportions=[8, 1, 1], copy_images=False, min_samples_per_class=20):
        self.xml_dir = xml_dir
        self.json_dir = json_dir
        self.img_dir = img_dir
        self.classes = classes
        self.proportions = proportions
        self.copy_images = copy_images
        self.min_samples_per_class = min_samples_per_class

        self.pre_define_categories = {}
        for i, cls in enumerate(self.classes):
            self.pre_define_categories[cls] = i + 1
            
    def convert(self):
        xml_files_by_class = self._get_sorted_xml_files_by_class()
        dataset_size = len(self.proportions)
        xml_files_by_dataset = [defaultdict(list) for _ in range(dataset_size)]
        xml_files_count_by_dataset = [0] * dataset_size
        
        for cls, xml_files in xml_files_by_class.items():
            total_files = len(xml_files)
            datasets_limits = [int(total_files * p / sum(self.proportions)) for p in self.proportions]
            datasets_limits[-1] = total_files - sum(datasets_limits[:-1]) # adjust to make sure the sums are correct due to integer division

            start = 0
            for i, limit in enumerate(datasets_limits):
                xml_files_by_dataset[i][cls] = xml_files[start:start + limit]
                xml_files_count_by_dataset[i] += limit
                start += limit

        for idx, xml_files_dict in enumerate(xml_files_by_dataset):
            dataset_dir = ''
            if self.copy_images:
                dataset_dir = os.path.join(self.json_dir, f'dataset_{idx + 1}')
                os.makedirs(dataset_dir, exist_ok=True)
                
            json_file_name = f'dataset_{idx + 1}.json'
            xml_files = sum(xml_files_dict.values(), [])
            self._convert_annotation(tqdm(xml_files), os.path.join(self.json_dir, json_file_name))
            if dataset_dir:
                self._copy_images(tqdm(xml_files), dataset_dir)

            print(f"\n在数据集{idx+1}中,各个类型的样本数量分别为:")
            for cls, files in xml_files_dict.items():
                print(f"类型 {cls} 的样本数量是: {len(files)}")

        print("\n各个数据集中相同类型样本的数量比值是:")
        for cls in self.classes:
            print("\n类型 {}:".format(cls))
            for i in range(len(self.proportions) - 1):
                if len(xml_files_by_dataset[i + 1].get(cls, [])) != 0 :
                    print("数据集 {} 和 数据集 {} 的样本数量比是: {}".format(
                        i + 1, 
                        i + 2, 
                        len(xml_files_by_dataset[i].get(cls, [])) / len(xml_files_by_dataset[i + 1].get(cls, []))
                    ))

    def _get_sorted_xml_files_by_class(self):
        xml_files_by_class = defaultdict(list)
        for xml_file in glob.glob(os.path.join(self.xml_dir, "*.xml")):
            tree = ET.parse(xml_file)
            root = tree.getroot()
            for obj in root.findall('object'):
                class_name = obj.find('name').text
                if class_name in self.classes:
                    xml_files_by_class[class_name].append(xml_file)


        # Filter classes
        if self.min_samples_per_class is not None:
            xml_files_by_class = {
                cls: files 
                for cls, files in xml_files_by_class.items()
                if len(files) > self.min_samples_per_class
            }

        xml_files_by_class = dict(
            sorted(xml_files_by_class.items(), key=lambda item: len(item[1]), reverse=True))

        return xml_files_by_class

    def _copy_images(self, xml_files, dataset_dir):
        for xml_file in xml_files:
            img_file = os.path.join(self.img_dir, os.path.basename(xml_file).replace('.xml', '.jpg'))
            if os.path.exists(img_file):
                shutil.copy(img_file, dataset_dir)

    def _get_files_by_majority_class(self):
        xml_files_by_class = defaultdict(list)
        for xml_file in glob.glob(os.path.join(self.xml_dir, "*.xml")):
            tree = ET.parse(xml_file)
            root = tree.getroot()
            class_counts = defaultdict(int)
            for obj in root.findall('object'):
                class_name = obj.find('name').text
                if class_name in self.classes:
                    class_counts[class_name] += 1
            majority_class = max(class_counts, key=class_counts.get)
            xml_files_by_class[majority_class].append(xml_file)

        return dict(sorted(xml_files_by_class.items(), key=lambda item: len(item[1]), reverse=True))

    def _convert_annotation(self, xml_list, json_file):
        json_dict = {"info":['none'], "license":['none'], "images": [], "annotations": [], "categories": []}
        categories = self.pre_define_categories.copy()
        bnd_id = START_BOUNDING_BOX_ID
        all_categories = {}

        for index, line in enumerate(xml_list):
            xml_f = line
            tree = ET.parse(xml_f)
            root = tree.getroot()

            filename = os.path.basename(xml_f)[:-4] + ".jpg"

            image_id = int(filename.split('.')[0][-9:])

            size = self._get_and_check(root, 'size', 1)
            width = int(self._get_and_check(size, 'width', 1).text)
            height = int(self._get_and_check(size, 'height', 1).text)
            image = {'file_name': filename, 'height': height, 'width': width, 'id':image_id}
            json_dict['images'].append(image)

            for obj in self._get(root, 'object'):
                category = self._get_and_check(obj, 'name', 1).text
                if category in all_categories:
                    all_categories[category] += 1
                else:
                    all_categories[category] = 1

                if category not in categories:
                    new_id = len(categories) + 1
                    print(filename)
                    print("[warning] 类别 '{}' 不在 'pre_define_categories'({})中,将自动创建新的id: {}".format(category, self.pre_define_categories, new_id))
                    categories[category] = new_id

                category_id = categories[category]
                bndbox = self._get_and_check(obj, 'bndbox', 1)
                xmin = int(float(self._get_and_check(bndbox, 'xmin', 1).text))
                ymin = int(float(self._get_and_check(bndbox, 'ymin', 1).text))
                xmax = int(float(self._get_and_check(bndbox, 'xmax', 1).text))
                ymax = int(float(self._get_and_check(bndbox, 'ymax', 1).text))
                o_width = abs(xmax - xmin)
                o_height = abs(ymax - ymin)

                ann = {'area': o_width*o_height, 'iscrowd': 0, 'image_id': image_id, 'bbox':[xmin, ymin, o_width, o_height],
                       'category_id': category_id, 'id': bnd_id, 'ignore': 0, 'segmentation': []}
                json_dict['annotations'].append(ann)
                bnd_id = bnd_id + 1

        for cate, cid in categories.items():
            cat = {'supercategory': 'none', 'id': cid, 'name': cate}
            json_dict['categories'].append(cat)
        json_fp = open(json_file, 'w')
        json_str = json.dumps(json_dict)
        json_fp.write(json_str)
        json_fp.close()
        print("------------已完成创建 {}--------------".format(json_file))
        print("找到 {} 类别: {} -->>> 你的预定类别 {}: {}".format(len(all_categories), all_categories.keys(), len(self.pre_define_categories), self.pre_define_categories.keys()))
        print("类别: id --> {}".format(categories))
        
    def _get(self, root, name):
        return root.findall(name)

    def _get_and_check(self, root, name, length):
        vars = root.findall(name)
        if len(vars) == 0:
            raise NotImplementedError('Can not find %s in %s.'%(name, root.tag))
        if length > 0 and len(vars) != length:
            raise NotImplementedError('The size of %s is supposed to be %d, but is %d.'%(name, length, len(vars)))
        if length == 1:
            vars = vars[0]
        return vars

if __name__ == '__main__':
    # xml标注文件夹   
    xml_dir = 'path/to/xml/directory'
    # JSON文件所在文件夹
    json_dir = 'path/to/json/directory'  
    # 类别列表,以字符串形式表示
    classes = ['cat', 'dog', 'person']
    # 图片所在文件夹
    img_dir = 'path/to/image/directory'
    # 类别在数据集中的比例
    proportions = [80, 10, 10]
    # 创建VOC2COCOConverter对象并进行转换
    converter = VOC2COCOConverter(xml_dir, json_dir, classes, img_dir, proportions, copy_images=True)
    converter.convert()

上面的代码只是将voc格式转换成coco格式,并不会指定具体哪个数据集的作用,因此在进行转换后需要手动给每个文件夹与注释文件命名,其中test数据集不是必要的,可以根据你的需要进行调整。

这是COCO数据集的基本目录结构:

|-- annotations
|   |-- instances_train.json
|   |-- instances_val.json
|   |-- instances_test.json
|-- train
|   |-- image1.jpg
|   |-- image2.jpg
|   |-- ...
|-- val
|   |-- image1.jpg
|   |-- image2.jpg
|   |-- ...
|-- test
|   |-- image1.jpg
|   |-- image2.jpg
|   |-- ...

annotations文件夹:存放标注文件,如instances_train.json、instances_val.json等。

train文件夹:存放训练集的图像文件。

val文件夹:存放验证集的图像文件。

test文件夹:存放测试集的图像文件。

以上是这个 VOC 到 COCO 格式转换脚本的简单介绍。你可以根据自己的实际需求进行适当修改和优化。这个脚本可以帮助你高效地转换数据集格式,并且支持自动复制图像文件,方便管理和使用数据集。

注意:本文及代码完全由 GPT 4 自动生成,经测试代码可以正常使用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值