物体認識用データセット自動作成プログラム

#目的
YOLOやSSDで自作のデータセットを用いて物体認識を行う際、大量な画像とラベリング処理が必要となる。
今回は、ラベリング処理にかかる時間を短縮するためのプログラムを作成した。

**※クラスを新たに追加して検出する際は、100枚程度を手作業でラベリングすることと、voc_annotation.pyの変更が必要。**

#使用したもの
###物体認識アルゴリズム
[YOLO]https://github.com/qqwweee/keras-yolo3
[SSD]https://github.com/rykov8/ssd_keras
###ラベリング用ソフト
https://github.com/tzutalin/labelImg

#処理の流れ
1. 学習(python train.py) 
2. yolo_labeling.py の model_path を1で作成したものに変更
3. 検出(python yolo_img.py) 
4. "Input filepath" のあとに検出画像があるディレクトリを入力
5. 検出結果からAnnotationファイル作成

なお、デフォルトのクラス内に存在するものであれば1.2は省略可。
プログラムの詳細は最後に示す。

#結果
未学習画像に対して、自動的にラベリングができる。
これを使用することで学習データを増やす際のラベリング処理にかかる時間を短縮することができる。



![Screenshot from 2019-11-28 17-48-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538304/429a85af-01c5-1130-7c2c-391aa716a355.png)
![Screenshot from 2019-11-28 17-48-30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538304/d3033467-319a-670e-33ed-3c4da019bd7d.png)

#結論
デフォルトのクラスにない新たなものを認識する場合は大量のデータセットが必要となる。実際YOLOでは、単純なものであれば100枚程度で認識することはできたが、似たものが存在する場合は認識できなかった。また、バウンディングボックスの位置にもずれがあった。
このプログラムを使用することで、100枚程度は手作業でラベリングする必要があるがその後の何百枚を短時間でラベリングすることができる。

今回はラベリングソフトに合わせた形式で出力を行った。そのため、このプログラム実行後にvoc_annotation.pyを実行して、train.txtを作成する必要がある。今後はこの処理も短縮し、train.txtに自動的に追加する機能を持たせたい。
また、新しいものを物体認識するためには、voc_annotation.py の変更などを行う必要がある。今後はこれについて説明する記事も書きたい。

#プログラム
プログラムはgithub上に公開済　https://github.com/ptxyasu/keras-yolo3
ここでは、keras-yolo3に追加したプログラムの説明を行う。

デフォルトのyolo.pyをラベリング作成用に修正したものがyolo_labeling.pyである。
ラベリング処理用に　make_xml, make_xml_object を追加した。
make_xmlは主なタグの作成とファイル名やファイルパス、画像サイズをセットする。
make_xml_objectは検出されたオブジェクトごとにクラス名と位置をセットする。
**※Annotationファイルの形式は参考にあるラベリングソフトに合わせてあることに注意**

```python:yolo_labeling.py
　　def make_xml(self,image,img_name,img_path):
        #parent
      　annotation = ET.Element('annotation')

        #child
        folder = ET.SubElement(annotation, 'folder')
        filename = ET.SubElement(annotation, 'filename')
        path = ET.SubElement(annotation, 'path')
        source = ET.SubElement(annotation, 'source')
        size = ET.SubElement(annotation, 'size')
        segmented = ET.SubElement(annotation, 'segmented')
        database = ET.SubElement(source, 'database')
        width = ET.SubElement(size,'width')
        height = ET.SubElement(size,'height')
        depth = ET.SubElement(size,'depth')

        #set
        folder.text = "JPEGImages"
        filename.text = img_name
        path.text = os.getcwd() +"/"+ img_path
        database.text = "Unknown"

        imgw,imgh = image.size

        width.text = str(imgw)
        height.text = str(imgh)
        depth.text = "3"
        segmented.text = "0"

        return annotation


    def make_xml_object(self,annotation,class_name,x1,y1,x2,y2):
        object = ET.SubElement(annotation, 'object')
        name = ET.SubElement(object,'name')
        pose = ET.SubElement(object,'pose')
        truncated = ET.SubElement(object,'truncated')
        difficult = ET.SubElement(object,'difficult')
        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')

        name.text = class_name
        pose.text = "Unspecified"
        truncated.text = "0"
        difficult.text = "0"

        xmin.text = str(x1)
        ymin.text = str(y1)
        xmax.text = str(x2)
        ymax.text = str(y2)

        return annotation
```

また、上の二つのをプログラムに実装するためにdetect_image の変更を行った。
make_xmlは写真１枚につき１回、make_xml_objectは検出されたオブジェクトの数だけ実行される。



```python:yolo_labiling.py
    def detect_image(self,image,img_name,img_path,annotation_path):
        start = timer()

        if self.model_image_size != (None, None):
            assert self.model_image_size[0]%32 == 0, 'Multiples of 32 required'
            assert self.model_image_size[1]%32 == 0, 'Multiples of 32 required'
            boxed_image = letterbox_image(image, tuple(reversed(self.model_image_size)))
        else:
            new_image_size = (image.width - (image.width % 32),
                              image.height - (image.height % 32))
            boxed_image = letterbox_image(image, new_image_size)
        image_data = np.array(boxed_image, dtype='float32')

        print(image_data.shape)
        image_data /= 255.
        image_data = np.expand_dims(image_data, 0)  # Add batch dimension.

        out_boxes, out_scores, out_classes = self.sess.run(
            [self.boxes, self.scores, self.classes],
            feed_dict={
                self.yolo_model.input: image_data,
                self.input_image_shape: [image.size[1], image.size[0]],
                K.learning_phase(): 0
            })

        print('Found {} boxes for {}'.format(len(out_boxes), 'img'))

        font = ImageFont.truetype(font='font/FiraMono-Medium.otf',
                    size=np.floor(8e-3 * image.size[1] + 0.5).astype('int32'))
        thickness = (image.size[0] + image.size[1]) // 500


        ann = self.make_xml(image,img_name,img_path)
        for i, c in reversed(list(enumerate(out_classes))):
            predicted_class = self.class_names[c]
            box = out_boxes[i]
            score = out_scores[i]

            label = '{} {:.2f}'.format(predicted_class, score)
            draw = ImageDraw.Draw(image)
            label_size = draw.textsize(label, font)

            top, left, bottom, right = box
            top = max(0, np.floor(top + 0.5).astype('int32'))
            left = max(0, np.floor(left + 0.5).astype('int32'))
            bottom = min(image.size[1], np.floor(bottom + 0.5).astype('int32'))
            right = min(image.size[0], np.floor(right + 0.5).astype('int32'))
            print(label, (left, top), (right, bottom))

            ann = self.make_xml_object(ann,predicted_class,left,bottom,right,top)
            if top - label_size[1] >= 0:
                text_origin = np.array([left, top - label_size[1]])
            else:
                text_origin = np.array([left, top + 1])

            # My kingdom for a good redistributable image drawing library.
            for i in range(thickness):
                draw.rectangle(
                    [left + i, top + i, right - i, bottom - i],
                    outline=self.colors[c])
            draw.rectangle(
                [tuple(text_origin), tuple(text_origin + label_size)],
                fill=self.colors[c])
            draw.text(text_origin, label, fill=(0, 0, 0), font=font)
            del draw

        end = timer()
        print(end - start)


        tree = ET.ElementTree(ann)
        annotation_name,ext = img_name.split(".")
        annotation_name = "/" + annotation_name + ".xml"
        annotation = annotation_path + annotation_name
        tree.write(annotation,"utf-8",True)

        return image
```

また、デフォルトではyolo_video.py にオプションimageをつけて実行することで、画像からの物体認識が行えるが、画像名を毎回手入力しなければならない。複数画像を一気に行うためにyolo_video.pyを変更したものがyolo_img.pyである。
”Input filepath”と表示されたら、対象画像があるディレクトリを入力する。そのディレクトリ内にある画像全てに物体認識が行われ、検出結果を表す画像とAnnotationファイルが出力される。

```python:yolo_img.py
import sys
import argparse
import os
#from yolo import YOLO, detect_video
from yolo_labeling import YOLO, detect_video
from PIL import Image

def detect_img(yolo):
    while True:
        file_path = input('Input filepath:')
        img_list = os.listdir(file_path)
        if file_path[-1] != "/":
            file_path += "/"

        if os.path.exists(file_path+"result") != True:
           os.mkdir(file_path+"result")
        if os.path.exists(file_path+"Annotations") != True:
           annotation_path = file_path+"Annotations"
           os.mkdir(annotation_path)

        for i in range(len(img_list)):
            img_name = img_list[i]
            print(img_name)

            img_path = file_path + img_name

            try:
                image = Image.open(img_path)
            except:
                print('Open Error! Try again!')
                continue

            else:
                #r_image = yolo.detect_image(image)
                #r_image.show()
                r_image = yolo.detect_image(image,img_name,img_path,annotation_path)
                r_image.save(file_path+"result/"+img_name)

    yolo.close_session()

FLAGS = None

if __name__ == '__main__':
    # class YOLO defines the default value, so suppress any default here
    parser = argparse.ArgumentParser(argument_default=argparse.SUPPRESS)
    '''
    Command line options
    '''
    parser.add_argument(
        '--model', type=str,
        help='path to model weight file, default ' + YOLO.get_defaults("model_path")
    )

    parser.add_argument(
        '--anchors', type=str,
        help='path to anchor definitions, default ' + YOLO.get_defaults("anchors_path")
    )

    parser.add_argument(
        '--classes', type=str,
        help='path to class definitions, default ' + YOLO.get_defaults("classes_path")
    )

    parser.add_argument(
        '--gpu_num', type=int,
        help='Number of GPU to use, default ' + str(YOLO.get_defaults("gpu_num"))
    )


    FLAGS = parser.parse_args()

    print("Image detection mode")
    detect_img(YOLO(**vars(FLAGS)))
```