2018/10/31 updated by

VOC2012 のデータを keras-yolo3 用に変換する

The PASCAL Visual Object Classes

http://host.robots.ox.ac.uk/pascal/VOC/

VOC2012

http://host.robots.ox.ac.uk/pascal/VOC/voc2012/

認識すべきオブジェクトのクラスは次の通り

  • Person: person
  • Animal: bird, cat, cow, dog, horse, sheep
  • Vehicle: aeroplane, bicycle, boat, bus, car, motorbike, train
  • Indoor: bottle, chair, dining table, potted plant, sofa, tv/monitor

オブジェクト認識は大きくわけて、

  • classification / detection
  • segmentation
  • action classification
という3種類がある。

Data

http://host.robots.ox.ac.uk/pascal/VOC/voc2012/index.html#devkit

Classification/Detection Image Set

4種類の集合が用意されている。VOC2011と全く同じデータとのこと。

train Training data
val Validation data
trainval train と val の和集合
test Test data

Development Kit

VOCtrainval_11-May-2012.tar (1.9 GB)

    VOCdevkit/VOC2012/Annotations/2007_000027.xml   % annotation file
    VOCdevkit/VOC2012/Annotations/2007_000032.xml
             ...
    VOCdevkit/VOC2012/JPEGImagess/2007_000027.jpg    % image file
    VOCdevkit/VOC2012/JPEGImagess/2007_000032.jpg    % image file
    ...

たとえば 2007_000027.xmlの内容は次の通り

In [ ]:
# %load VOC2012/VOCdevkit/VOC2012/Annotations/2007_000423.xml
<annotation>
	<folder>VOC2012</folder>
	<filename>2007_000423.jpg</filename>
	<source>
		<database>The VOC2007 Database</database>
		<annotation>PASCAL VOC2007</annotation>
		<image>flickr</image>
	</source>
	<size>
		<width>500</width>
		<height>375</height>
		<depth>3</depth>
	</size>
	<segmented>0</segmented>
	<object>
		<name>person</name>
		<pose>Frontal</pose>
		<truncated>0</truncated>
		<difficult>0</difficult>
		<part>
			<name>head</name>
			<bndbox>
				<xmin>69</xmin>
				<ymin>82</ymin>
				<xmax>94</xmax>
				<ymax>112</ymax>
			</bndbox>
		</part>
		<part>
			<name>hand</name>
			<bndbox>
				<xmin>63</xmin>
				<ymin>172</ymin>
				<xmax>70</xmax>
				<ymax>186</ymax>
			</bndbox>
		</part>
		<part>
			<name>hand</name>
			<bndbox>
				<xmin>104</xmin>
				<ymin>170</ymin>
				<xmax>115</xmax>
				<ymax>184</ymax>
			</bndbox>
		</part>
		<part>
			<name>foot</name>
			<bndbox>
				<xmin>78</xmin>
				<ymin>249</ymin>
				<xmax>96</xmax>
				<ymax>263</ymax>
			</bndbox>
		</part>
		<part>
			<name>foot</name>
			<bndbox>
				<xmin>98</xmin>
				<ymin>250</ymin>
				<xmax>114</xmax>
				<ymax>263</ymax>
			</bndbox>
		</part>
		<bndbox>
			<xmin>51</xmin>
			<ymin>80</ymin>
			<xmax>122</xmax>
			<ymax>266</ymax>
		</bndbox>
	</object>
	<object>
		<name>person</name>
		<pose>Unspecified</pose>
		<truncated>1</truncated>
		<difficult>0</difficult>
		<part>
			<name>head</name>
			<bndbox>
				<xmin>374</xmin>
				<ymin>96</ymin>
				<xmax>413</xmax>
				<ymax>143</ymax>
			</bndbox>
		</part>
		<part>
			<name>foot</name>
			<bndbox>
				<xmin>424</xmin>
				<ymin>282</ymin>
				<xmax>443</xmax>
				<ymax>297</ymax>
			</bndbox>
		</part>
		<part>
			<name>hand</name>
			<bndbox>
				<xmin>382</xmin>
				<ymin>179</ymin>
				<xmax>401</xmax>
				<ymax>200</ymax>
			</bndbox>
		</part>
		<bndbox>
			<xmin>367</xmin>
			<ymin>92</ymin>
			<xmax>461</xmax>
			<ymax>315</ymax>
		</bndbox>
	</object>
</annotation>

keras-yolo3 用の annotation file を作成する

  1. github で公開されているKeras実装 https://github.com/qqwweee/keras-yolo3 をこのデータで training してみる。
  2. そのため、VOC2012 形式の annotation files から keras-yolo3 形式の annotation file を作成するプログラム anno.py を作成した。
  3. 基本的な動作は次の通り。
    • カラー画像だけを選ぶ
    • keras-yolo3入力に合わせるため画像の大きさを変更するが、認識オブジェクトのバウンディングボックスはそのときの座標系の値。
  4. anno.py を用いて keras-yolo3 形式のannotation file を作成する。
  5.   $ python anno.py --annotation_path VOCdevkit/VOC2012/Annotations --prefix resize --class_path voc_classes.txt --out_path train.txt
    
    • VOC の annotation は VOCdevkit/VOC2012/Annotations/*.xml
    • VOC の画像ファイルは resize フォルダの中
    • VOCで認識するオブジェクトクラスの名前が入ったファイルは voc_classes.txt
    • 生成する keras-yolo3用のannotation file は train.txt
  6. 次のような内容の train.txt が生成される。
  7. resize/2007_000027.jpg 148,84,298,292,14
    resize/2007_000063.jpg 102,127,315,305,11 62,1,356,416,8
    ...
    
    17125 行ある。
  8. jpeg ファイルの画像を 416x416 に変更して resize フォルダに入れる。
  9. 画像ファイルの大きさを変更するのに、今回は Windows 上で XMedia Recode を用いた。

  10. keras-yolo3 で VOC2012のデータを与えて training した。
  11. 手元のマシン( garellia QF980HG, Core i7-4810MQ 2.80GHz, GTX980m, 16GB) で学習させると 1 epoch に30分弱かかった。 50回数繰り返したのでほぼ丸1日かかった。 この話は別記事で。

    ちなみに、training中は7GBのメモリを確保しようとするので、 メモリ8GBのマシンではkeras-yolo3の train.py は動作しなかった。

In [ ]:
#!/usr/bin/env pytho
# 2018/10/31 by Yoshihisa Nitta
import os
import argparse
import codecs
import glob
import xml.etree.ElementTree as ET

class AnnotationData():
    def __init__(self):
        # image dic
        self.img_idx = 0
        self.img2idx = {}
        self.idx2img = {}
        self.idx2sz = {}
        self.imgIdx2objs = {}
        # objects dic
        self.obj_idx = 0
        self.objIdx2name = {}
        self.objIdx2bb = {}
        self.obj2imgIdx = {}
        # name dic
        self.name2objs = {}

    def readVOCxml(self, xmldata):
        root = ET.fromstring(xmldata)
        fname_el = root.find('filename')
        print(fname_el.tag, fname_el.text)
        img_file = fname_el.text
        self.img2idx[img_file] = self.img_idx
        self.idx2img[self.img_idx] = img_file

        sz_el = root.find('size')
        w = int(sz_el.find('width').text)
        h = int(sz_el.find('height').text)
        d = int(sz_el.find('depth').text)
        print('size = ',w,h,d)
        self.idx2sz[self.img_idx] = (w,h,d)

        self.imgIdx2objs[self.img_idx] = []
        
        obj_el_list = root.findall('object')
        for obj_el in obj_el_list:
            name = obj_el.find('name').text
            print('name = ',name)
            bb_el = obj_el.find('bndbox')
            xmin = int(float(bb_el.find('xmin').text))
            ymin = int(float(bb_el.find('ymin').text))
            xmax = int(float(bb_el.find('xmax').text))
            ymax = int(float(bb_el.find('ymax').text))
            self.objIdx2name[self.obj_idx] = name
            self.objIdx2bb[self.obj_idx] = (xmin,ymin,xmax,ymax)

            self.imgIdx2objs[self.img_idx].append(self.obj_idx);
            self.obj2imgIdx[self.obj_idx] = self.img_idx

            if not name in self.name2objs: self.name2objs[name] = []
            self.name2objs[name].append(self.obj_idx)

            self.obj_idx += 1
            
        self.img_idx += 1

    def readFile(self, file):
        with codecs.open(file, 'r', encoding='utf-8') as fin:
            lines = [line.strip() for line in fin]
        return ' '.join(lines)
        
    def readVOCFile(self, xmlfile):
        return self.readVOCxml(self.readFile(xmlfile))

    def yolo_bb_c(self, objIdx, class_dic, xscale=1.0, yscale=1.0):
        name = self.objIdx2name[objIdx]
        print('name = ', name)
        if not name in class_dic:
            return ""
        xmin, ymin, xmax, ymax = self.objIdx2bb[objIdx]
        xmin *= xscale
        ymin *= yscale
        xmax *= xscale
        ymax *= yscale
        return "{:d},{:d},{:d},{:d},{:d}".format(int(xmin),
                                                 int(ymin),
                                                 int(xmax),
                                                 int(ymax),
                                                 class_dic[name])

    def yolo_line(self, imgIdx, class_dic, xscale=1.0, yscale=1.0):
        objs = self.imgIdx2objs[imgIdx];
        ws = []
        for objIdx in objs:
            s = self.yolo_bb_c(objIdx, class_dic, xscale, yscale)
            if s != "":
                ws.append(s)
        return self.idx2img[imgIdx] + ' ' + ' '.join(ws)

    def yolo_lines(self, imgIndices, class_dic, tgt_w, tgt_h):
        ans = []
        for imgIdx in imgIndices:
            img_w, img_h, img_c = self.idx2sz[imgIdx]
            ans.append(self.yolo_line(imgIdx, class_dic, tgt_w/img_w, tgt_h/img_h))
        return ans

    
def do_job(t_w, t_h, t_d, class_path='', out_path='', prefix='', annotation_path='', annos=[]):
    annoData = AnnotationData()
    class_dic = {}
    with codecs.open(class_path, 'r', encoding='utf-8') as fin:
        lines = [line.strip() for line in fin]
    for i, c in enumerate(lines):
        class_dic[c] = i
    print('class_dic = ', class_dic)
    p = os.path.join(annotation_path, '*.xml')
    files = glob.glob(p)
    for anno in files:
        annoData.readVOCFile(anno)
    for anno in annos:
        annoData.readVOCFile(anno)
    imgIndices = [idx for idx in annoData.idx2img if annoData.idx2sz[idx][2]==t_d]
    print('imgIndices = ', imgIndices)
    lines = annoData.yolo_lines(imgIndices, class_dic, t_w, t_h)
    if out_path != '':
        with codecs.open(out_path,'w','utf-8') as fout:
            for line in lines:
                fout.write("{}/{}\n".format(prefix,line))
    else:
        for line in lines:
            print("{}/{}".format(prefix, line))
    return lines


if __name__ == '__main__':
    FLAGS = None
    parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
    parser.add_argument(
        '--image_width', type=int, default=416,
        help='image width of YOLO input'
    )
    parser.add_argument(
        '--image_height', type=int, default=416,
        help='image height of YOLO input'
    )
    parser.add_argument(
        '--image_depth', type=int, default=3,
        help='image depth of YOLO input'
    )
    parser.add_argument(
        '--prefix', type=str, default='',
        help='prefix of image path'
    )
    parser.add_argument(
        '--class_path', type=str, default='',
        help='class file'
    )
    parser.add_argument(
        '--out_path', type=str, default='',
        help='output for yolo annotation file'
    )
    parser.add_argument(
        '--annotation_path', type=str, default='',
        help='[optional] read all annotation files from the folder'
    )
    parser.add_argument(
        'annos', nargs='*', default=[],
        help='annotation files'
    )
    FLAGS = parser.parse_args()
    print(FLAGS)
    print(FLAGS.annos)
    do_job(FLAGS.image_width,
           FLAGS.image_height,
           FLAGS.image_depth,
           FLAGS.class_path,
           FLAGS.out_path,
           FLAGS.prefix,
           FLAGS.annotation_path,
           FLAGS.annos)