VOCデータをCOCOデータセットツールにセット

物体検出タスクを扱うとき、データセットを VOC 形式から COCO 形式に変換しなければならない状況に何度も遭遇します。VOC 形式と COCO 形式は、広く使用されている 2 つのターゲット検出データ セット形式です。VOC 形式は XML ファイルを使用して各画像の注釈情報を保存しますが、COCO 形式は JSON ファイルを使用します。この形式の変換は、通常、さまざまな深層学習フレームワークまたはツールに対応するために行われます。

このプロセスを簡素化するために、VOC 形式のデータ セットを COCO 形式に変換でき、指定されたディレクトリへの画像の自動コピーもサポートする Python スクリプトを共有します。

まず、次のものを準備する必要があります。

  • 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 形式に変換するだけであり、どのデータ セットが使用されるかは指定されていません。したがって、変換後、各フォルダーと注釈ファイルに手動で名前を付ける必要があります。テスト データ セットは必要なく、Adjust に基づいて作成できます。あなたのニーズに合わせて。

これは 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
|   |-- ...

注釈フォルダー:instances_train.json、instances_val.json などの注釈ファイルを保存します。

train フォルダー: トレーニング セットの画像ファイルを保存します。

val フォルダー: 検証セットのイメージ ファイルが格納されます。

テスト フォルダー: テスト セットの画像ファイルを保存します。

上記は、この VOC から COCO 形式への変換スクリプトの簡単な紹介です。実際のニーズに応じて、適切な変更や最適化を行うことができます。このスクリプトは、データ セット形式を効率的に変換するのに役立ち、データ セットの管理と使用を容易にする画像ファイルの自動コピーをサポートします。

注: この記事とコードは GPT 4 によって完全に自動生成されており、テストされたコードは正常に使用できます。

おすすめ

転載: blog.csdn.net/Lc_001/article/details/132698406