Open8
CD-COCOをCOCO-Handにマージ
まずは COCO-Hand 自力再アノテーションデータセットと CD-COCO の画像セットをマッチングして、COCO-Hand に存在する画像のみを CD-COCO から抽出する。 結果 4,351枚 が残った。COCO-Handはアスペクト比を無視して 480x360 へリサイズしてしまっているゴミデータセットなので、CD-COCO の画像と COCO-Hand の画像サイズは一致していない。ただ、YOLOのアノテーションフォーマットが [classid, cx, cy, w, h]
の画像全体に対するスケール値管理なので、画像サイズの変化によってアノテーションが影響を受けることは無い想定。
01_cocohand_cdcoco_matching.py
import os
from tqdm import tqdm
# Bフォルダの画像のファイル名をリスト化
b_folder = 'train_val_cocohand'
b_files = set(os.listdir(b_folder))
# Aフォルダのファイル数を取得
a_folder = 'train_val_distorted'
original_a_count = len(os.listdir(a_folder))
# Aフォルダをループし、Bフォルダのリストにないファイルを削除
deleted_count = 0
for file in tqdm(os.listdir(a_folder), dynamic_ncols=True):
if file not in b_files:
os.remove(os.path.join(a_folder, file))
deleted_count += 1
# 最終的にAフォルダに残ったファイルの数を表示
remaining_count = original_a_count - deleted_count
print(f"Aフォルダに残ったファイルの数: {remaining_count}")
COCO-Hand のアノテーションを CD-COCO 側にコピーする
Aフォルダの画像枚数:4,351枚
.txtをAフォルダにコピーしたあとの想定ファイル数:8,702枚
02_cocohand_anno_copy_to_cdcoco.sh
!#/bin/bash
cd train_val_distorted
# Aフォルダの画像ファイルの拡張子を除くファイル名を取得
for img_file in *.{jpg,jpeg,png,JPG,JPEG,PNG}; do
if [[ -f "$img_file" ]]; then
base_name="${img_file%.*}"
# 拡張子を除くファイル名がBフォルダの *.txt ファイルと一致するか確認
if [ -f "../train_val_cocohand/$base_name.txt" ]; then
# 一致する場合、BフォルダからAフォルダにコピー
cp "../train_val_cocohand/$base_name.txt" .
fi
fi
done
CD-COCO の画像ファイルとアノテーションファイルを一括リネームして COCO-Hand と重複しないようにする。
03_cdcoco_rename.sh
!#/bin/bash
cd train_val_distorted
for file in *; do
mv "$file" "dist_$file"
done
COCO-Hand 側の画像とアノテーションファイルを CD-COCO 側にすべてコピーする。
処理後の想定ファイル数:9,776 + 8,702 -> 18,478
04_copy_cocohand_to_cdcoco.sh
!#/bin/bash
cd train_val_cocohand
for file in *; do
cp "$file" ../train_val_distorted/
done
-
train_val_distorted
フォルダの画像ファイルのみをZIPに圧縮してCVATへアップロードする 9,239枚 になるはず - CVATからYOLOフォーマットのアノテーションデータテンプレートのみをダウンロードする
- CVATからダウンロードしたアノテーションテンプレートに
train_val_distorted
の.txt
を一括上書き更新する - 上書き更新したアノテーションテンプレートをZIPに圧縮してCVATへアップロードする
- CVATからVOC形式でアノテーションをダウンロードする
-
convert_voc_to_coco.py
で VOC形式 から COCO形式 のXMLファイルを生成する
train, val への振り向け
05_voc_to_coco_prep.py
import os
dataset_directory = 'body_head_hand_face_dist_voc'
# 学習/検証データパス(train/validation data path)
train_directory = './train'
validation_directory = './validation'
# 学習データ格納ディレクトリ作成(Create training data storage directory)
os.makedirs(train_directory, exist_ok=True)
# 検証データ格納ディレクトリ作成(Create verification data storage directory)
os.makedirs(validation_directory, exist_ok=True)
import glob
import shutil
import random
# 学習データの割合(Percentage of training data)
train_ratio = 0.8
# コピー元ファイルリスト取得(Get copy source file list)
annotation_list = sorted(glob.glob(dataset_directory + '/*.xml'))
image_list = sorted(glob.glob(dataset_directory + '/*.jpg')+glob.glob(dataset_directory + '/*.PNG')+glob.glob(dataset_directory + '/*.png'))
file_num = len(annotation_list)
# インデックスシャッフル(shuffle)
index_list = list(range(file_num - 1))
random.shuffle(index_list)
for count, index in enumerate(index_list):
if count < int(file_num * train_ratio):
# 学習用データ(Training Data)
shutil.copy2(annotation_list[index], train_directory)
shutil.copy2(image_list[index], train_directory)
else:
# 検証用データ(Validation Data)
shutil.copy2(annotation_list[index], validation_directory)
shutil.copy2(image_list[index], validation_directory)
convert_voc_to_coco.py
によるフォーマット変換を実行
06_convert_voc_to_coco.py
import os
import json
import glob
import argparse
from tqdm import tqdm
import xml.etree.ElementTree as element_tree
def get_args():
parser = argparse.ArgumentParser(
description="Convert Pascal VOC annotation to COCO format.")
parser.add_argument(
"xml_dir",
help="Directory path to xml files.",
type=str,
default='Annotations',
)
parser.add_argument(
"json_file",
help="Output COCO format json file.",
type=str,
default='output.json',
)
parser.add_argument(
"--start_image_id",
type=int,
default=None,
)
parser.add_argument(
"--start_bbox_id",
help="Bounding Box start ID.",
type=int,
default=1,
)
parser.add_argument(
"--category",
help="Specify a category list.",
type=str,
default=None,
)
parser.add_argument(
"--indent",
help="COCO format json indent.",
type=int,
default=None,
)
parser.add_argument(
"--bbox_offset",
help="Bounding Box offset.",
type=int,
default=-1,
)
parser.add_argument(
"--reserve_category_num",
type=int,
default=None,
)
args = parser.parse_args()
return args
def main():
# 引数取得
args = get_args()
xml_dir = args.xml_dir
json_file = args.json_file
start_image_id = args.start_image_id
start_bbox_id = args.start_bbox_id
category_txt_path = args.category
indent = args.indent
bbox_offset = args.bbox_offset
reserve_category_num = args.reserve_category_num
# Pascal VOC XMLファイルリスト取得
xml_files = glob.glob(os.path.join(xml_dir, "*.xml"))
# 事前定義のカテゴリーリストを取得
predefine_categories = None
if category_txt_path is not None:
with open(category_txt_path, 'r') as f:
category_list = f.readlines()
predefine_categories = {
name.rstrip('\n'): i
for i, name in enumerate(category_list)
}
# カテゴリーリスト生成
if predefine_categories is not None:
categories = predefine_categories
else:
categories = get_categories(xml_files)
# Pascal VOC → COCO 変換
print("Number of xml files: {}".format(len(xml_files)))
json_dict = convert_xml_to_json(
xml_files,
categories,
start_image_id,
start_bbox_id,
bbox_offset,
reserve_category_num,
)
# JSON保存
if os.path.dirname(json_file):
os.makedirs(os.path.dirname(json_file), exist_ok=True)
with open(json_file, "w") as fp:
json_text = json.dumps(json_dict, indent=indent)
fp.write(json_text)
print("Success: {}".format(json_file))
def get_categories(xml_files):
classes_names = []
# 全XMLのobjectからnameを取得
for xml_file in xml_files:
tree = element_tree.parse(xml_file)
root = tree.getroot()
for member in root.findall("object"):
classes_names.append(member[0].text)
# 重複を削除してソート
classes_names = list(set(classes_names))
classes_names.sort()
# Dict形式に変換
# categories = {name: i + 1 for i, name in enumerate(classes_names)}
categories = {'body': 1, 'head': 2, 'hand': 3, 'face': 4}
return categories
def get_element(root, name, length=None):
# 指定タグの値を取得
vars = root.findall(name)
# 長さチェック
if length is not None:
if len(vars) == 0:
raise ValueError("Can not find %s in %s." % (name, root.tag))
if length > 0 and len(vars) != length:
raise ValueError(
"The size of %s is supposed to be %d, but is %d." %
(name, length, len(vars)))
if length == 1:
vars = vars[0]
return vars
def get_basename_without_ext(filename):
# 拡張子を含まないファイル名を取得
basename_without_ext = filename.replace("\\", "/")
basename_without_ext = os.path.splitext(os.path.basename(filename))[0]
return str(basename_without_ext)
def get_basename_with_ext(filename):
# 拡張子を含むファイル名を取得
basename_with_ext = filename.replace("\\", "/")
basename_with_ext = os.path.basename(basename_with_ext)
return str(basename_with_ext)
def convert_xml_to_json(
xml_files,
categories=None,
start_image_id=None,
start_bbox_id=1,
bbox_offset=-1,
reserve_category_num=None,
):
json_dict = {
"images": [],
"type": "instances",
"annotations": [],
"categories": []
}
count_dict = {}
bbox_id = start_bbox_id
if start_image_id is not None:
image_id_count = start_image_id
for xml_file in tqdm(xml_files, "Convert XML to JSON"):
# ルート要素取得
tree = element_tree.parse(xml_file)
root = tree.getroot()
# 画像ファイル名取得
path = get_element(root, "path")
if len(path) == 1:
filename = os.path.basename(path[0].text)
elif len(path) == 0:
filename = get_element(root, "filename", 1).text
else:
raise ValueError("%d paths found in %s" % (len(path), xml_file))
# 画像情報取得
size = get_element(root, "size", 1)
width = int(get_element(size, "width", 1).text)
height = int(get_element(size, "height", 1).text)
if start_image_id is None:
image_id = get_basename_without_ext(filename)
else:
image_id = image_id_count
# JSON Dict追加
image_info = {
"file_name": filename,
"height": height,
"width": width,
"id": image_id,
}
json_dict["images"].append(image_info)
# object情報取得
for obj in get_element(root, "object"):
# バウンディングボックス情報取得
bbox = get_element(obj, "bndbox", 1)
xmin = int(float(get_element(bbox, "xmin", 1).text)) + bbox_offset
ymin = int(float(get_element(bbox, "ymin", 1).text)) + bbox_offset
xmax = int(float(get_element(bbox, "xmax", 1).text))
ymax = int(float(get_element(bbox, "ymax", 1).text))
if xmin > xmax:
continue
if ymin > ymax:
continue
# カテゴリー名取得
category = get_element(obj, "name", 1).text
# 初出のカテゴリー名の場合、リストに追加
if category not in categories:
new_id = len(categories)
categories[category] = new_id
# カテゴリー数カウント
if category not in count_dict:
count_dict[category] = 0
else:
count_dict[category] += 1
# カテゴリーID取得
category_id = categories[category]
# print(f'{filename} {xmin}-{xmax} {ymin}-{ymax}')
# assert xmax > xmin
# assert ymax > ymin
bbox_width = abs(xmax - xmin)
bbox_height = abs(ymax - ymin)
# JSON Dict追加
annotation_info = {
"area": bbox_width * bbox_height,
"iscrowd": 0,
"image_id": image_id,
"bbox": [xmin, ymin, bbox_width, bbox_height],
"category_id": category_id,
"id": bbox_id,
"ignore": 0,
"segmentation": [],
}
json_dict["annotations"].append(annotation_info)
bbox_id = bbox_id + 1
if start_image_id is not None:
image_id_count = image_id_count + 1
# カテゴリー情報
max_category_id = 0
for category_name, category_id in categories.items():
category_info = {
"supercategory": "none",
"id": category_id,
"name": category_name
}
json_dict["categories"].append(category_info)
if max_category_id <= category_id:
max_category_id = category_id + 1
if reserve_category_num is not None:
for index in range(len(json_dict["categories"]), reserve_category_num):
category_info = {
"supercategory": "none",
"id": max_category_id,
"name": 'reserve_' + str(index).zfill(4)
}
json_dict["categories"].append(category_info)
max_category_id += 1
print(count_dict)
return json_dict
if __name__ == "__main__":
main()
python 06_convert_voc_to_coco.py \
train train/train_annotations.json \
--start_image_id=0
python 06_convert_voc_to_coco.py \
validation validation/validation_annotations.json \
--start_image_id=10000000
YOLOXトレーニング用データ階層の作成
mkdir -p dataset/images/train2017
mkdir -p dataset/images/val2017
mkdir -p dataset/images/annotations
cp -rf train/*.jpg dataset/images/train2017
cp -rf train/*.PNG dataset/images/train2017
cp -rf train/*.png dataset/images/train2017
cp -rf validation/*.jpg dataset/images/val2017
cp -rf validation/*.PNG dataset/images/val2017
cp -rf validation/*.png dataset/images/val2017
cp -rf train/train_annotations.json dataset/images/annotations
cp -rf validation/validation_annotations.json dataset/images/annotations
-
epoch=130
あたりがベストっぽい
python train.py \
-f x.py \
-d 1 \
-b 32 \
--fp16 \
-o \
-c yolox_x.pth
- 155 epochの結果
Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.568
Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.837
Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.611
Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.396
Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.746
Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.882
Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 1 ] = 0.181
Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 10 ] = 0.512
Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.605
Average Recall (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.457
Average Recall (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.781
Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.901
per class AP:
| class | AP | class | AP | class | AP |
|:--------|:-------|:--------|:-------|:--------|:-------|
| body | 62.314 | head | 59.383 | hand | 53.498 |
| face | 51.892 | | | | |
per class AR:
| class | AR | class | AR | class | AR |
|:--------|:-------|:--------|:-------|:--------|:-------|
| body | 65.385 | head | 62.703 | hand | 57.922 |
| face | 56.005 | | | | |
- データセット強化前の結果
Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.526
Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.800
Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.555
Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.374
Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.745
Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.884
Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 1 ] = 0.166
Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets= 10 ] = 0.465
Average Recall (AR) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.574
Average Recall (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.446
Average Recall (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.785
Average Recall (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.902
per class AP:
| class | AP | class | AP | class | AP |
|:--------|:-------|:--------|:-------|:--------|:-------|
| body | 56.340 | head | 54.669 | hand | 47.280 |
| face | 51.928 | | | | |
per class AR:
| class | AR | class | AR | class | AR |
|:--------|:-------|:--------|:-------|:--------|:-------|
| body | 61.122 | head | 58.182 | hand | 53.339 |
| face | 56.775 | | | | |