物体検出タスクを扱うとき、データセットを 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 によって完全に自動生成されており、テストされたコードは正常に使用できます。