動画を使ったYOLO用の学習データを作成します。汎用性を持たせるために、動画からはPascalVOC形式で学習データを作成し、その後、YOLO用の学習データに変換します。ただし、プログラムでは、検出する物体は固定されている物体とします。
フォルダ構成
フォルダ構成を次に示します。「result」フォルダ直下に置かれるファイルについては「Google Colaboratoryを使ってYOLOv4-tinyで学習」を参照してください。
video
動画ファイル
result
>tree /f result \RESULT │ classes.txt │ obj.data │ test.txt │ train.txt │ yolov4-tiny-my.cfg │ ├─annotation │ video_0000.xml │ video_0001.xml │ video_0002.xml │ video_0003.xml │ video_0004.xml │ ・・・l │ video_0094.xml │ video_0095.xml │ video_0096.xml │ video_0097.xml │ video_0098.xml │ video_0099.xml │ ├─image │ video_0000.jpg │ video_0000.txt │ video_0001.jpg │ video_0001.txt │ video_0002.jpg │ video_0002.txt │ video_0003.jpg │ video_0003.txt │ video_0004.jpg │ video_0004.txt │ ・・・・・ │ │ video_0096.jpg │ video_0096.txt │ video_0097.jpg │ video_0097.txt │ video_0098.jpg │ video_0098.txt │ video_0099.jpg │ video_0099.txt │ └─train train.txt
動画からPascalVOC形式の学習データの作成
動画からPascalVOC形式の学習データを作成するプログラムを示します。
- bounding boxを表す座標系には、画像の左上を原点とし、右側にx座標正方向、下側にy座標正方向としたものが利用されています。bounding boxは、boxの左上の点のx座標(xmin)、y座標(ymin)、boxの右下の点のx座標(xmax)、y座標(ymax)として与えられています。
- 12行目と13行目のコメントを外すと、3か所のbounding boxが作成されます。
- 14行目でbounding boxの左上の原点を指定し、16行目でbounding boxののサイズを指定します。
- 15行目でクラスの名前を指定し、[0, 0]は名前の終了を示します。
- 19行目で動画のパスを設定します。
- 37行目で動画から切り取った画像に名前を付けて保存します。
- 38行目から90行目で作成したbounding boxの情報をPascalVOC形式で作成して保存します。
- 118行目で学習データを個数を判断します。
vocFormat/main.py
import cv2 import os import xml.etree.cElementTree as ET import random # bounding boxを表す座標系には、画像の左上を原点とし、右側にx座標正方向、下側にy座標正方向としたものが利用されています。 # bounding boxは、boxの左上の点のx座標(xmin)、y座標(ymin)、boxの右下の点のx座標(xmax)、y座標(ymax)として与えられています。 # box_m = [234, 246], [769, 61], [1287, 171] # bounding boxは3箇所 # name = ["m1", "m2", "m3" ] box_m = [234, 246], [0, 0] # bounding boxは3箇所 name = ["m1"] img_size = 416 # video_path = 'video/WIN_20220118_17_19_13_Pro.mp4' video_path = 'video/WIN_20220210_18_25_55_Pro.mp4' class TrafficLights: annotation_dir = 'result/annotation/' images_dir = 'result/image/' train_dir = 'result/train' ext = 'jpg' basename = "video" jpeg_filenames_list = [] def __init__(self, name, box): self.name = name self.box = box def setimage(self, num, frame): cv2.imwrite('{}_{}.{}'.format(TrafficLights.images_dir + TrafficLights.basename, num, TrafficLights.ext), frame) filename = '{}_{}'.format(TrafficLights.basename, num) jpeg_filename = filename + TrafficLights.ext # テキストファイルの作成 TrafficLights.jpeg_filenames_list.append(filename) # XMLファイルの保存 xml_filename = filename + '.xml' print(xml_filename) new_root = ET.Element('annotation') new_filename = ET.SubElement(new_root, 'filename') new_filename.text = jpeg_filename Size = ET.SubElement(new_root, 'size') Width = ET.SubElement(Size, 'width') Height = ET.SubElement(Size, 'height') Depth = ET.SubElement(Size, 'depth') Width.text = str(frame.shape[1]) Height.text = str(frame.shape[0]) Depth.text = str(frame.shape[2]) i = 0 for index in self.box: if index[0] == 0: break Object = ET.SubElement(new_root, 'object') Name = ET.SubElement(Object, 'name') Name.text = self.name[i] Difficult = ET.SubElement(Object, 'difficult') Difficult.text = '0' Bndbox = ET.SubElement(Object, 'bndbox') Xmin = ET.SubElement(Bndbox, 'xmin') Ymin = ET.SubElement(Bndbox, 'ymin') Xmax = ET.SubElement(Bndbox, 'xmax') Ymax = ET.SubElement(Bndbox, 'ymax') x = random.randint(0, 32) - 16 y = random.randint(0, 32) - 16 Xmin.text = str(index[0] + x) Ymin.text = str(index[1] + y) Xmax.text = str(index[0] + x + img_size) Ymax.text = str(index[1] + y + img_size) i += 1 new_tree = ET.ElementTree(new_root) new_tree.write(os.path.join(TrafficLights.annotation_dir, xml_filename)) @staticmethod def cleatetrain(): # テキストファイルの保存 text = "\n".join(TrafficLights.jpeg_filenames_list) with open(os.path.join(TrafficLights.train_dir, 'train.txt'), "w") as f: f.write(text) def main(): trafficlights_m = TrafficLights(name, box_m) cap = cv2.VideoCapture(video_path) if not cap.isOpened(): return digit = len(str(int(cap.get(cv2.CAP_PROP_FRAME_COUNT)))) n = 0 while True: ret, frame = cap.read() if ret: num = str(n).zfill(digit) trafficlights_m.setimage(num, frame) n += 1 if n == 100: break else: break TrafficLights.cleatetrain() if __name__ == '__main__': main()
実行すると次のようなPascalVOC形式のファイルが「RESULT/annotation」フォルダに作成されます。
PascalVOC形式学習データをYOLO用学習データに変換
PascalVOC形式学習データをYOLO用学習データに変換するプログラムを示します。
- 34行目のvoc2yoloメソッドで作成したPascalVOC形式学習データの「annotation」フォルダからYOLO用学習データに変換します。
- 77行目のimglist2fileメソッドで作成したリストをシャッフルしてから、学習用のデータリストを「train.txt」ファイルに、評価用のデータリストを「test.txt」に保存します。
vocFormat/pascalVOC2yolov3.py
# coding:utf-8 from __future__ import print_function import os import random import glob import xml.etree.ElementTree as ET base_dir = 'result/' annotation_dir = base_dir + 'annotation/' images_dir = base_dir + 'image/' yolo_dir = base_dir + 'image' def xml_reader(filename): """ Parse a PASCAL VOC xml file """ tree = ET.parse(filename) size = tree.find('size') width = int(size.find('width').text) height = int(size.find('height').text) objects = [] for obj in tree.findall('object'): obj_struct = {} obj_struct['name'] = obj.find('name').text bbox = obj.find('bndbox') obj_struct['bbox'] = [int(bbox.find('xmin').text), int(bbox.find('ymin').text), int(bbox.find('xmax').text), int(bbox.find('ymax').text)] objects.append(obj_struct) return width, height, objects def voc2yolo(filename): classes_dict = {} with open("result/classes.txt") as f: for idx, line in enumerate(f.readlines()): class_name = line.strip() classes_dict[class_name] = idx width, height, objects = xml_reader(filename) lines = [] for obj in objects: x, y, x2, y2 = obj['bbox'] class_name = obj['name'] label = classes_dict[class_name] cx = (x2 + x) * 0.5 / width cy = (y2 + y) * 0.5 / height w = (x2 - x) * 1. / width h = (y2 - y) * 1. / height line = "%s %.6f %.6f %.6f %.6f\n" % (label, cx, cy, w, h) lines.append(line) txt_name = filename.replace(".xml", ".txt").replace("annotation", "image") with open(txt_name, "w") as f: f.writelines(lines) def get_image_list(image_dir, suffix=['jpg', 'jpeg', 'JPG', 'JPEG', 'png']): '''get all image path ends with suffix''' if not os.path.exists(image_dir): print("PATH:%s not exists" % image_dir) return [] imglist = [] for root, sdirs, files in os.walk(image_dir): if not files: continue for filename in files: filepath = os.path.join(root, filename) + "\n" if filename.split('.')[-1] in suffix: print(filepath) imglist.append(filepath) return imglist def imglist2file(imglist): random.shuffle(imglist) # size = 100 size = 20 train_list = imglist[:-size] valid_list = imglist[-size:] with open(base_dir + "train.txt", "w") as f: f.writelines(train_list) with open(base_dir + "test.txt", "w") as f: f.writelines(valid_list) if __name__ == "__main__": xml_path_list = glob.glob(annotation_dir + "*.xml") for xml_path in xml_path_list: voc2yolo(xml_path) imglist = get_image_list(images_dir) imglist2file(imglist)
実行すると次のように変換されたYOLO用学習データのファイルが、画像データと組み合わせて「RESULT/image」フォルダに作成されます。
video_0000.txt
0 0.226562 0.418519 0.216667 0.385185