オブジェクト検出 YOLO

An example of object detection

You Only Look Once: Unified, Real-Time Object Detection
Joseph Redmon, Santosh Divvala, Ross Girshick, Ali Farhadi
YOLO: Real-Time Object Detection

YOLOとは?

YOLOはリアルタイムオブジェクト検出アルゴリズムです。YOLO(You Look Only Onse)の名前通り、このアルゴリズムでは検出窓をスライドさせるような仕組みを用いず、画像を一度CNNに通すことで、オブジェクトを検出することができます.

まず、このセクションではYOLOについて簡単に説明します。YOLOアルゴリズムで使用する教師データは少々複雑であるため、アルゴリズムのイメージを把握することは教師データの構造を理解することにも役立ちます.

YOLOは2つのプロセスを同時に行います。オブジェクトの検出とクラス分類です。

YOLOでは、画像をグリッドセルとして扱います。Fig1は元のイメージで、Fig2はグリッドセルを画像に当てはめたものです。この例では縦方向と横方向にそれぞれ7つのセルをおいています。

Fig.1 Fig.2
Fig.1 Fig.2

YOLOではそれぞれのセルがバウンディングボックスをオブジェクトに当てはめる役割を担っています。例えば、Fig3内の赤色で示されたセルはFig4にかかれているように、セル周辺の2つのバウンディングボックスを設置する役割があります。一つのセルが設置するバウンディングボックスの数はユーザが指定することが出来ます。

Fig.3 Fig.4
Fig.3 Fig.4

セルはバウンディングボックスを設置するのと同時に、クラス確率も**P(Car|Object)**も出力します。これはオブジェクトが存在していた場合どのクラスに属するかを示す事後確率です。Fig5はそれぞれのセルが示すクラス確率の最大値を取るクラスで色付けをしたものです。

ここでFig3、Fig4、Fig5を統合して考えます。Fig3で赤く塗られたセルは、Fig4のようなバウンディングボックスを予測し同時にFig5のようなクラス確率を持ちます。結果として、バウンディングボックスはオレンジ色のクラスのオブジェクトのバウンディングボックスとなります.

Fig.5

Fig.5

同様に、すべてのセルが設置した残りのバウンディングボックスについてもクラス毎に色をつけた図がFig5隣ります。最後の処理として、NMS(Non-Maximum Suppression)という処理を施します。

NMSはバウンディングボックスを結合する処理を担います。以上の処理により、Fig7のようなバウンディングボックスを得ることが出来ます.

Fig.2 Fig.2
Fig.6 Fig.7

Required Libaries

  • matplotlib 2.0.2
  • numpy 1.12.1
  • pillow 4.2.1
  • tqdm 4.19.4

このNotebookでは、2.3.0以降のReNomが必要となります.

In [1]:
import os
import sys
from xml.etree import ElementTree
from itertools import product
import urllib.request as request

import numpy as np
from tqdm import tqdm
from PIL import Image
from PIL import ImageDraw
import colorsys
import matplotlib.pyplot as plt

# ReNom version >= 2.3.0
import renom as rm
from renom.cuda import set_cuda_active
from renom.utility.trainer import Trainer
from renom.algorithm.image.detection.yolo import build_truth, Yolo, apply_nms, box_iou
from renom.utility.distributor import ImageDetectionDistributor
from renom.utility.image import *

set_cuda_active(True)

データの準備

このNotebookでは PASCAL VOC datasetを使用します。
上記のリンク先へ移動し、下記のようなダウンロードリンクからデータセットを取得することが出来ます。
link
In [2]:
dataset_path = "VOCdevkit/VOC2012/Annotations/"

train_file_list = [path for path in sorted(os.listdir(dataset_path)) if not "2012_" in path]
test_file_list = [path for path in os.listdir(dataset_path) if "2012_" in path]

tree = ElementTree.parse(os.path.join(dataset_path, train_file_list[-1]))

データセットの確認

最初にデータセットの内容を確認します。フォルダ構造は以下の用になっています。今回は"Annotations" と "JPEG Images"フォルダに入っているデータを使用します。

"Annotations"フォルダ内にはバウンディングボックスの情報がxml形式で提供されています。それらの情報をパースするため、"ElementTree"モジュールを使用します.

いkなお出力はパースされたxmlファイルの内容です。ファイル名、クラス、バウンディングボックスの情報を確認することが出来ます.

In [3]:
def parse(node, indent=1):
    print("{}{} {}".format('    ' * indent, node.tag, node.text.strip()))
    for child in node:
        parse(child, indent + 1)

print("/// Contents of a XML file ///")
parse(tree.getroot())
/// Contents of a XML file ///
    annotation
        filename 2011_007214.jpg
        folder VOC2011
        object
            name person
            actions
                jumping 0
                other 1
                phoning 0
                playinginstrument 0
                reading 0
                ridingbike 0
                ridinghorse 0
                running 0
                takingphoto 0
                usingcomputer 0
                walking 0
            bndbox
                xmax 274
                xmin 77
                ymax 375
                ymin 67
            difficult 0
            pose Unspecified
            point
                x 154
                y 151
        object
            name person
            actions
                jumping 0
                other 1
                phoning 0
                playinginstrument 0
                reading 0
                ridingbike 0
                ridinghorse 0
                running 0
                takingphoto 0
                usingcomputer 0
                walking 0
            bndbox
                xmax 500
                xmin 182
                ymax 375
                ymin 1
            difficult 0
            pose Unspecified
            point
                x 411
                y 146
        segmented 0
        size
            depth 3
            height 375
            width 500
        source
            annotation PASCAL VOC2011
            database The VOC2011 Database
            image flickr

バウンディングボックスの情報を取り出す.

In [4]:
label_dict = {}
img_size = (224*2, 224*2)
cells = 7

def get_obj_coordinate(obj):
    global label_dict
    class_name = obj.find("name").text.strip()
    if label_dict.get(class_name, None) is None:
        label_dict[class_name] = len(label_dict)
    class_id = label_dict[class_name]
    bbox = obj.find("bndbox")
    xmax = float(bbox.find("xmax").text.strip())
    xmin = float(bbox.find("xmin").text.strip())
    ymax = float(bbox.find("ymax").text.strip())
    ymin = float(bbox.find("ymin").text.strip())
    w = xmax - xmin
    h = ymax - ymin
    x = xmin + w/2
    y = ymin + h/2
    return class_id, x, y, w, h

def get_img_info(filename):
    tree = ElementTree.parse(filename)
    node = tree.getroot()
    file_name = node.find("filename").text.strip()
    img_h = float(node.find("size").find("height").text.strip())
    img_w = float(node.find("size").find("width").text.strip())
    obj_list = node.findall("object")
    objects = []
    for obj in obj_list:
        objects.append(get_obj_coordinate(obj))
    return file_name, img_w, img_h, objects
In [5]:
train_data_set = []
test_data_set = []

for o in train_file_list:
    train_data_set.append(get_img_info(os.path.join(dataset_path, o)))

for o in test_file_list:
    test_data_set.append(get_img_info(os.path.join(dataset_path, o)))
In [6]:
# Example and class labels.
print("{}".format(train_data_set[-1]))
print()
print("%-12s: number"%("class name"))
print("----------------------")
for k, v in sorted(label_dict.items(), key=lambda x:x[1]):
    print("%-12s: %d"%(k, v))
('2011_007214.jpg', 500.0, 375.0, [(0, 175.5, 221.0, 197.0, 308.0), (0, 341.0, 188.0, 318.0, 374.0)])

class name  : number
----------------------
person      : 0
aeroplane   : 1
tvmonitor   : 2
train       : 3
boat        : 4
dog         : 5
chair       : 6
bird        : 7
bicycle     : 8
bottle      : 9
sheep       : 10
diningtable : 11
horse       : 12
motorbike   : 13
sofa        : 14
cow         : 15
car         : 16
cat         : 17
bus         : 18
pottedplant : 19

教師データの作成

YOLOで扱う教師データは少々複雑な構造となっています.

以下の図が教師データのフォーマットです.

教師データとなる行列のサイズは(N, cell^2 * (bbox * 5 + class))となります.ここで"N"はバッチサイズ、"cell"は画像の縦、横方向に設置するセルの数、"bbox"はそれぞれのセルが設置するバウンディングボックスの数、"class"は、それぞれのセルに対応するonehot化されたクラスを表します.

In [7]:
label_length = len(label_dict)
last_layer_size = cells*cells*(5*2+label_length)

def one_hot(label):
    oh = [0]*label_length
    oh[label] = 1
    return oh

def create_detection_distributor(train_set=True):
    label_data = []
    img_path_list = []
    label_list = []
    if train_set:
        file_list = train_file_list
        data_set = train_data_set
        # Augumentation Settngs
        augmentatiion = DataAugmentation(
            [
                Flip(1),
                Rotate(90),
                # Resize(size=img_size),
                Shift((20, 20)),
                # ColorJitter(v=(1.0, 1.5)),
                # Zoom(zoom_rate=(1.0, 1.1)),
                Rescale(option=[-1, 1])
            ],
            random=True
        )
    else:
        file_list = test_file_list
        data_set = test_data_set
        augmentatiion = DataAugmentation(
            [Rescale(option=[-1, 1])],
        )
    for i in range(len(file_list)):
        img_path = os.path.join("VOCdevkit/VOC2012/JPEGImages/", data_set[i][0])

        # obj[1]:X, obj[2]:Y, obj[3]:Width, obj[4]:Height, obj[0]:Class
        objects = []
        for obj in data_set[i][3]:
            detect_label = {"bndbox":[obj[1], obj[2], obj[3], obj[4]],
                            "name":one_hot(obj[0])}
            objects.append(detect_label)
        img_path_list.append(img_path)
        label_list.append(objects)
    class_list = [c for c, v in sorted(label_dict.items(), key=lambda x:x[1])]
    return ImageDetectionDistributor(img_path_list,
                                     label_list,
                                     class_list,
                                     imsize = img_size,
                                     augmentation=augmentatiion)

def transform_to_yolo_format(label):
    yolo_format = []
    for l in label:
        yolo_format.append(build_truth(l.reshape(1, -1), img_size[0], img_size[1], cells, label_length).flatten())
    return np.array(yolo_format)

def draw_rect(draw_obj, rect):
    cor = (rect[0][0], rect[0][1], rect[1][0], rect[1][1])
    line_width = 3
    for i in range(line_width):
        draw_obj.rectangle(cor, outline="red")
        cor = (cor[0]+1,cor[1]+1, cor[2]+1,cor[3]+1)

train_detect_dist = create_detection_distributor(True)
test_detect_dist = create_detection_distributor(False)

教師データの確認

Note : 以下のコードはラベルデータの確認用コードなので、以下のコードを書かずともこのNotebookを実行することが出来ます.

In [8]:
sample, sample_label = train_detect_dist.batch(3, shuffle=True).__next__()

for Mth_img in range(len(sample)):
    example_img = Image.fromarray(((sample[Mth_img]+1)*255/2).transpose(1, 2, 0).astype(np.uint8))
    dr = ImageDraw.Draw(example_img)

    print("///Objects")
    for i in range(0, len(sample_label[Mth_img]), 4+label_length):
        class_label = np.argmax(sample_label[Mth_img][i+4:i+4+label_length])
        x, y, w, h = sample_label[Mth_img][i:i+4]
        if x==y==h==w==0:
            break
        draw_rect(dr, ((x-w/2, y-h/2), (x+w/2, y+h/2)))
        print("obj:%d"%(i+1),
              "class:{:7s}".format([k for k, v in label_dict.items() if v==class_label][0]),
              "x:%3d, y:%3d width:%3d height:%3d"%(x, y, w, h))

    plt.figure(figsize=(4, 4))
    plt.imshow(example_img)
    plt.show()

///Objects
obj:1 class:motorbike x:192, y:257 width:384 height:297
obj:25 class:car     x:386, y:187 width: 86 height: 38
../../../_images/notebooks_image_processing_yolo_notebook_15_1.png
///Objects
obj:1 class:motorbike x:165, y:195 width: 68 height:117
obj:25 class:car     x: 24, y:190 width: 48 height:124
obj:49 class:car     x:259, y:189 width:155 height:110
obj:73 class:car     x:356, y:197 width:143 height: 91
obj:97 class:car     x:119, y:174 width: 48 height: 47
obj:121 class:car     x:420, y:183 width: 37 height: 26
obj:145 class:person  x:287, y:282 width:134 height:213
../../../_images/notebooks_image_processing_yolo_notebook_15_3.png
///Objects
obj:1 class:chair   x:343, y:225 width:207 height:377
obj:25 class:chair   x: 90, y:181 width:151 height:329
obj:49 class:diningtable x:188, y:250 width:236 height:393
../../../_images/notebooks_image_processing_yolo_notebook_15_5.png

モデル定義

ここでは畳み込みニューラルネットワークとYOLOの誤差関数を定義します.

In [9]:
# Convolutional neural network
model = rm.Sequential([
    # 1st Block
    rm.Conv2d(channel=64, filter=7, stride=2, padding=3),
    rm.LeakyRelu(slope=0.1),
    rm.MaxPool2d(stride=2, filter=2),

    # 2nd Block
    rm.Conv2d(channel=192, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.MaxPool2d(stride=2, filter=2),

    # 3rd Block
    rm.Conv2d(channel=128, filter=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=256, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=256, filter=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=512, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.MaxPool2d(stride=2, filter=2),

    # 4th Block
    rm.Conv2d(channel=256, filter=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=512, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=256, filter=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=512, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=256, filter=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=512, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=256, filter=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=512, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=512, filter=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=1024, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.MaxPool2d(stride=2, filter=2),

    # 5th Block
    rm.Conv2d(channel=512, filter=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=1024, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=512, filter=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=1024, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=1024, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=1024, filter=3, stride=2, padding=1),
    rm.LeakyRelu(slope=0.1),

    # 6th Block
    rm.Conv2d(channel=1024, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),
    rm.Conv2d(channel=1024, filter=3, padding=1),
    rm.LeakyRelu(slope=0.1),

    # 7th Block
    rm.Flatten(),
    rm.Dense(512),
    rm.LeakyRelu(slope=0.1),
    rm.Dense(4096),
    rm.LeakyRelu(slope=0.1),
    rm.Dropout(0.5),

    # 8th Block
    rm.Dense(last_layer_size),
])

# Loss function.
yolo_detector = Yolo(cells=cells, classes=label_length)

YOLOモデルの学習

このNotebookでは、全結合レイヤ部のみを再学習します.

In [10]:
N = len(train_data_set)
batch = 64
batch_loop = int(np.ceil(N/batch))

# Download the learned model weights.
if not os.path.exists("yolo.h5"):
    print("Weight parameters will be downloaded.")
    url = "http://docs.renom.jp/downloads/weights/yolo.h5"
    request.urlretrieve(url, "yolo.h5")

model.load("yolo.h5")
In [11]:
model_upper = rm.Sequential(model[:-7])
model_detector = rm.Sequential(model[-7:])

# Define weight decay
def weight_decay():
    wd = 0
    for m in model_detector:
        if hasattr(m, "params"):
            w = m.params.get("w", None)
            if w is not None:
                wd += rm.sum(w**2)
    return wd
In [12]:
# Reset params of detector model for redoing the learning.
LEARN = False # True if relearning the model.
if LEARN:
    for layer in model_detector:
        if hasattr(layer, "params"):
            layer.params = {}
In [13]:
opt = rm.Sgd(momentum=0.9)

# We use different learning rate in each epoch.
lrarning_rates = []# [0.001] + [0.01]*60

for epoch in range(len(lrarning_rates) * LEARN):
    loss = 0
    test_loss = 0
    bar = tqdm(range(batch_loop))
    opt._lr = lrarning_rates[epoch]

    model_detector.set_models(inference=False)
    for j, (img, label) in enumerate(train_detect_dist.batch(batch, True)):
        if epoch==0:
            # Rise the learning rate slowly at first epoch.
            opt._lr = (0.01 - 0.001)/(batch_loop)*j + 0.001

        yolo_format_label = transform_to_yolo_format(label)
        h = model_upper(img).as_ndarray()

        with model_detector.train():
            z = model_detector(h)
            l = yolo_detector(z, yolo_format_label) + 0.0005*weight_decay()

        l.grad().update(opt)
        loss += l.as_ndarray()

        # Set descriptions to tqdm.
        bar.set_description("epoch {:03d} train loss:{:6.4f}".format(epoch, l.as_ndarray()[0]))
        bar.update(1)

    # Test
    model_detector.set_models(inference=True)
    for k, (img, label) in enumerate(test_detect_dist.batch(batch, True)):
        yolo_format_label = transform_to_yolo_format(label)
        h = model_upper(img).as_ndarray()
        z = model_detector(h)
        test_loss += yolo_detector(z, yolo_format_label) + 0.0005*weight_decay()
    test_loss = test_loss.as_ndarray()/(k+1)

    msg = "epoch {:03d} avg loss:{:6.4f} test loss:{:6.4f}".format(epoch, float(loss/(j+1)), float(test_loss))
    bar.set_description(msg)
    bar.update(0)
    bar.refresh()
    bar.close()

オブジェクト検出テスト

学習済みモデルに対し、テストデータを入力し結果を表示します.

In [14]:
sample_img, sample_label = test_detect_dist.batch(3, shuffle=True).__next__()
obj_list = []

model.set_models(inference=True)
for i in range(len(sample_img)):
    p = model(np.expand_dims(sample_img[i], axis=0)).as_ndarray().reshape(cells, cells, 5*2+label_length)
    objs = apply_nms(p, cells, 2, label_length, image_size=img_size, thresh=0.2)
    obj_list.append(objs)

for num in range(3):
    im = Image.fromarray(((sample_img[num] + 1)/2*255).transpose(1, 2, 0).astype(np.uint8))
    obj = obj_list[num]
    dr = ImageDraw.Draw(im)
    print("///Objects")
    for i in range(len(obj)):
        class_label = obj[i]["class"]
        w = obj[i]["box"][2]*448
        h = obj[i]["box"][3]*448
        x = obj[i]["box"][0]*448
        y = obj[i]["box"][1]*448
        x1 = x - w/2
        y1 = y - h/2
        x2 = x + w/2
        y2 = y + h/2
        print("obj:%d"%(i+1),
          "class:{:7s}".format([k for k, v in label_dict.items() if v==class_label][0]),
          "x:%3d, y:%3d width:%3d height:%3d"%(x, y, w, h))
        draw_rect(dr, ((x1, y1), (x2, y2)))
    plt.imshow(im)
    plt.show()
///Objects
obj:1 class:person  x:209, y:233 width:237 height:296
../../../_images/notebooks_image_processing_yolo_notebook_24_1.png
///Objects
obj:1 class:person  x:297, y:231 width:120 height:191
../../../_images/notebooks_image_processing_yolo_notebook_24_3.png
///Objects
obj:1 class:person  x:270, y:278 width:240 height:366
../../../_images/notebooks_image_processing_yolo_notebook_24_5.png