動画を使った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