« カメラ付きのワインセラー”Plum wine system” | トップページ | Lenovo G580が我が家にやってきた »

2018年1月13日 (土)

TensorFlow+KerasでSSDを独自データで使えるようにしてみた

物体検出コードといえば以前「ディープラーニングで一般物体検出する手法”YOLO”のTensorFlow版で独自データセットを使えるようにしてみた: EeePCの軌跡」という記事で紹介したYOLOv1という手法を使えるようにしたのですが、このYOLOv1、精度はいまいち。

ということで、最新のSSD or YOLOv2を使えるようにしたいなぁと考え、何とかこのSSD(Single Shot MultiBox Detector)を使えるようにしました。

コードの入手先はこちら。

GitHub - ryokov8/ssd_keras

参考サイトはこちら。

物体検出アルゴリズム(SSD : Single Shot MultiBox Detector)を学習させてみる - Qiita

準備

まず、このコードは”Keras”というのを使います。

実はこいつが曲者で、最新のKeras(バージョンが2.0以降)を入れると動きません。バージョンは”1.2.2”を入れる必要があります。

> pip install --upgrade keras==1.2.2

とすれば、Windows版Anacondaでもインストールできました。

TensorFlowは1.0以降であればOKのようです。我が家は1.2ですが、動きました。

ただ、環境によってはバックエンドがTensorFlowではない場合があります。その場合は以下を参考にバックエンドをTensorFlowに変更しておいてください。

Keras バックエンドの変更 - Qiita

先のGitHubリンクからコードをゲットします。

Windowsの場合は「GitHub - ryokov8/ssd_keras」をクリックして、その中の「Clone or Download」をクリック、「Download ZIP」をクリックしてZIP形式のファイルを落します。

解凍したのち、そのフォルダ内に「checkpoints」という空フォルダを作っておいてください。

Linuxだと、下記のコマンドで一発ですね。

> git clone https://github.com/rykov8/ssd_keras

さて、とりあえず動かすために、Pascal VOCのデータをダウンロードします。

私はVOC2007を使いました。入手の仕方は「The PASCAL Visual Object Classes Challenge 2007 (VOC2007)」のサイトの中ほどにある「Development Kit」の「 training/validation data 」をクリックします。

これを解凍すると、「VOCdevkit」というフォルダができます。これを、上のSSDのコードのあるところ(ssd_keras-master)にそのまま入れます。

他には学習済みデータを入手する必要があります。

https://mega.nz/#F!7RowVLCL!q3cEVRK9jyOSB9el3SssIA」から「weights_SSD300.hdf5」をダウンロードしてください。右クリックして「ダウンロード」-「標準ダウンロード」で入手できます。これもSSDコードのフォルダ(ssd_keras-master)に入れます。

他に、以下のコードを作っておいてください。参考サイトからほぼそのままいただきました。ファイル名は「train_ssd_keras.py」とでもしておいてください。

import cv2
import keras
from keras.applications.imagenet_utils import preprocess_input
from keras.backend.tensorflow_backend import set_session
from keras.models import Model
from keras.preprocessing import image
import matplotlib.pyplot as plt
import numpy as np
import pickle
from random import shuffle
from scipy.misc import imread
from scipy.misc import imresize
import tensorflow as tf

from ssd import SSD300
from ssd_training import MultiboxLoss
from ssd_utils import BBoxUtility

plt.rcParams['figure.figsize'] = (8, 8)
plt.rcParams['image.interpolation'] = 'nearest'

np.set_printoptions(suppress=True)

# 21
NUM_CLASSES = 21 #4
input_shape = (300, 300, 3)

priors = pickle.load(open('prior_boxes_ssd300.pkl', 'rb'))
bbox_util = BBoxUtility(NUM_CLASSES, priors)

# gt = pickle.load(open('gt_pascal.pkl', 'rb'))
gt = pickle.load(open('VOC2007.pkl', 'rb'))
keys = sorted(gt.keys())
num_train = int(round(0.8 * len(keys)))
train_keys = keys[:num_train]
val_keys = keys[num_train:]
num_val = len(val_keys)

class Generator(object):
    def __init__(self, gt, bbox_util,
                 batch_size, path_prefix,
                 train_keys, val_keys, image_size,
                 saturation_var=0.5,
                 brightness_var=0.5,
                 contrast_var=0.5,
                 lighting_std=0.5,
                 hflip_prob=0.5,
                 vflip_prob=0.5,
                 do_crop=True,
                 crop_area_range=[0.75, 1.0],
                 aspect_ratio_range=[3./4., 4./3.]):
        self.gt = gt
        self.bbox_util = bbox_util
        self.batch_size = batch_size
        self.path_prefix = path_prefix
        self.train_keys = train_keys
        self.val_keys = val_keys
        self.train_batches = len(train_keys)
        self.val_batches = len(val_keys)
        self.image_size = image_size
        self.color_jitter = []
        if saturation_var:
            self.saturation_var = saturation_var
            self.color_jitter.append(self.saturation)
        if brightness_var:
            self.brightness_var = brightness_var
            self.color_jitter.append(self.brightness)
        if contrast_var:
            self.contrast_var = contrast_var
            self.color_jitter.append(self.contrast)
        self.lighting_std = lighting_std
        self.hflip_prob = hflip_prob
        self.vflip_prob = vflip_prob
        self.do_crop = do_crop
        self.crop_area_range = crop_area_range
        self.aspect_ratio_range = aspect_ratio_range

    def grayscale(self, rgb):
        return rgb.dot([0.299, 0.587, 0.114])

    def saturation(self, rgb):
        gs = self.grayscale(rgb)
        alpha = 2 * np.random.random() * self.saturation_var
        alpha += 1 - self.saturation_var
        rgb = rgb * alpha + (1 - alpha) * gs[:, :, None]
        return np.clip(rgb, 0, 255)

    def brightness(self, rgb):
        alpha = 2 * np.random.random() * self.brightness_var
        alpha += 1 - self.saturation_var
        rgb = rgb * alpha
        return np.clip(rgb, 0, 255)

    def contrast(self, rgb):
        gs = self.grayscale(rgb).mean() * np.ones_like(rgb)
        alpha = 2 * np.random.random() * self.contrast_var
        alpha += 1 - self.contrast_var
        rgb = rgb * alpha + (1 - alpha) * gs
        return np.clip(rgb, 0, 255)

    def lighting(self, img):
        cov = np.cov(img.reshape(-1, 3) / 255.0, rowvar=False)
        eigval, eigvec = np.linalg.eigh(cov)
        noise = np.random.randn(3) * self.lighting_std
        noise = eigvec.dot(eigval * noise) * 255
        img += noise
        return np.clip(img, 0, 255)

    def horizontal_flip(self, img, y):
        if np.random.random() < self.hflip_prob:
            img = img[:, ::-1]
            y[:, [0, 2]] = 1 - y[:, [2, 0]]
        return img, y

    def vertical_flip(self, img, y):
        if np.random.random() < self.vflip_prob:
            img = img[::-1]
            y[:, [1, 3]] = 1 - y[:, [3, 1]]
        return img, y

    def random_sized_crop(self, img, targets):
        img_w = img.shape[1]
        img_h = img.shape[0]
        img_area = img_w * img_h
        random_scale = np.random.random()
        random_scale *= (self.crop_area_range[1] -
                         self.crop_area_range[0])
        random_scale += self.crop_area_range[0]
        target_area = random_scale * img_area
        random_ratio = np.random.random()
        random_ratio *= (self.aspect_ratio_range[1] -
                         self.aspect_ratio_range[0])
        random_ratio += self.aspect_ratio_range[0]
        w = np.round(np.sqrt(target_area * random_ratio))
          h = np.round(np.sqrt(target_area / random_ratio))
        if np.random.random() < 0.5:
            w, h = h, w
        w = min(w, img_w)
        w_rel = w / img_w
        w = int(w)
        h = min(h, img_h)
        h_rel = h / img_h
        h = int(h)
        x = np.random.random() * (img_w - w)
        x_rel = x / img_w
        x = int(x)
        y = np.random.random() * (img_h - h)
        y_rel = y / img_h
        y = int(y)
        img = img[y:y+h, x:x+w]
        new_targets = []
        for box in targets:
            cx = 0.5 * (box[0] + box[2])
            cy = 0.5 * (box[1] + box[3])
            if (x_rel < cx < x_rel + w_rel and
                y_rel < cy < y_rel + h_rel):
                xmin = (box[0] - x_rel) / w_rel
                ymin = (box[1] - y_rel) / h_rel
                xmax = (box[2] - x_rel) / w_rel
                ymax = (box[3] - y_rel) / h_rel
                xmin = max(0, xmin)
                ymin = max(0, ymin)
                xmax = min(1, xmax)
                ymax = min(1, ymax)
                box[:4] = [xmin, ymin, xmax, ymax]
                new_targets.append(box)
        new_targets = np.asarray(new_targets).reshape(-1, targets.shape[1])
        return img, new_targets

    def generate(self, train=True):
        while True:
            if train:
                shuffle(self.train_keys)
                keys = self.train_keys
            else:
                shuffle(self.val_keys)
                keys = self.val_keys
            inputs = []
            targets = []
            for key in keys:
                img_path = self.path_prefix + key
                img = imread(img_path).astype('float32')
                y = self.gt[key].copy()
                if train and self.do_crop:
                    img, y = self.random_sized_crop(img, y)
                img = imresize(img, self.image_size).astype('float32')

                if train:
                    shuffle(self.color_jitter)
                    for jitter in self.color_jitter:
                        img = jitter(img)
                    if self.lighting_std:
                        img = self.lighting(img)
                    if self.hflip_prob > 0:
                        img, y = self.horizontal_flip(img, y)
                    if self.vflip_prob > 0:
                        img, y = self.vertical_flip(img, y)
                #
                #print(y)
                y = self.bbox_util.assign_boxes(y)
                #print(y)
                inputs.append(img)
                        targets.append(y)
                if len(targets) == self.batch_size:
                    tmp_inp = np.array(inputs)
                    tmp_targets = np.array(targets)
                    inputs = []
                    targets = []
                    yield preprocess_input(tmp_inp), tmp_targets

path_prefix = './VOCdevkit/VOC2007/JPEGImages/'
gen = Generator(gt, bbox_util, 4, path_prefix,
                train_keys, val_keys,
                (input_shape[0], input_shape[1]), do_crop=False)

model = SSD300(input_shape, num_classes=NUM_CLASSES)
model.load_weights('weights_SSD300.hdf5', by_name=True)

freeze = ['input_1', 'conv1_1', 'conv1_2', 'pool1',
          'conv2_1', 'conv2_2', 'pool2',
          'conv3_1', 'conv3_2', 'conv3_3', 'pool3']#,
#           'conv4_1', 'conv4_2', 'conv4_3', 'pool4']

for L in model.layers:
    if L.name in freeze:
        L.trainable = False

def schedule(epoch, decay=0.9):
    return base_lr * decay**(epoch)

callbacks = [keras.callbacks.ModelCheckpoint('./checkpoints/weights.{epoch:02d}-{val_loss:.2f}.hdf5',
                                             verbose=1,
                                             save_weights_only=True),
             keras.callbacks.LearningRateScheduler(schedule)]

base_lr = 3e-4
optim = keras.optimizers.Adam(lr=base_lr)
model.compile(optimizer=optim,
              loss=MultiboxLoss(NUM_CLASSES, neg_pos_ratio=2.0).compute_loss)

nb_epoch = 100
history = model.fit_generator(gen.generate(True), gen.train_batches,
                              nb_epoch, verbose=1,
                              callbacks=callbacks,
                              validation_data=gen.generate(False),
                              nb_val_samples=gen.val_batches,
                              nb_worker=1)

inputs = []
images = []
img_path = path_prefix + sorted(val_keys)[0]
img = image.load_img(img_path, target_size=(300, 300))
img = image.img_to_array(img)
images.append(imread(img_path))
inputs.append(img.copy())
inputs = preprocess_input(np.array(inputs))

preds = model.predict(inputs, batch_size=1, verbose=1)
results = bbox_util.detection_out(preds)

for i, img in enumerate(images):
    # Parse the outputs.
    det_label = results[i][:, 0]
    det_conf = results[i][:, 1]
    det_xmin = results[i][:, 2]
    det_ymin = results[i][:, 3]
    det_xmax = results[i][:, 4]
    det_ymax = results[i][:, 5]

    # Get detections with confidence higher than 0.6.
    top_indices = [i for i, conf in enumerate(det_conf) if conf >= 0.6]

    top_conf = det_conf[top_indices]
    top_label_indices = det_label[top_indices].tolist()
    top_xmin = det_xmin[top_indices]
    top_ymin = det_ymin[top_indices]
    top_xmax = det_xmax[top_indices]
    top_ymax = det_ymax[top_indices]

    colors = plt.cm.hsv(np.linspace(0, 1, NUM_CLASSES)).tolist()

    plt.imshow(img / 255.)
    currentAxis = plt.gca()

    for i in range(top_conf.shape[0]):
        xmin = int(round(top_xmin[i] * img.shape[1]))
        ymin = int(round(top_ymin[i] * img.shape[0]))
        xmax = int(round(top_xmax[i] * img.shape[1]))
        ymax = int(round(top_ymax[i] * img.shape[0]))
        score = top_conf[i]
        label = int(top_label_indices[i])
        # label_name = voc_classes[label - 1]
        display_txt = '{:0.2f}, {}'.format(score, label)
        coords = (xmin, ymin), xmax-xmin+1, ymax-ymin+1
        color = colors[label]
        currentAxis.add_patch(plt.Rectangle(*coords, fill=False, edgecolor=color, linewidth=2))
        currentAxis.text(xmin, ymin, display_txt, bbox={'facecolor':color, 'alpha':0.5})

    plt.show()

コードのあるフォルダ(ssd_keras-master)の中にある「testing_utils」の中の「videotest.py」というコードは、一部修正が必要です。

87、88行目にある以下のコードを

        vidw = vid.get(cv2.cv.CV_CAP_PROP_FRAME_WIDTH)
        vidh = vid.get(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT)

以下のように変えます。

        vidw = vid.get(cv2.CAP_PROP_FRAME_WIDTH)
        vidh = vid.get(cv2.CAP_PROP_FRAME_HEIGHT)

これで、準備完了です。

学習

まず最初に「PASCAL_VOC」というフォルダに「get_data_from_XML.py」というコードがあるはずです。これを一つ上のフォルダ(ssd_keras-master)に移動し、実行します。

> python get_data_from_XML.py

すると「VOC2007.pkl」というファイルができるはずです。

これができたら、いよいよ実行です。

> python train_ssd_keras.py

これで「VOC2007」のデータを使って、学習がはじまるはずです。

100 epochs実行するのに、うちの環境(Corei3 3220 3.3GHz + GeForce 1050Ti 4GB)では約12時間かかりました。

終わると、「checkpoints」というフォルダに学習済みデータが入っているはずです(10 epochs毎)。

これを使って、いよいよ動画の”物体検出”を実行します。

検出(動画)

ここでは、動画を使ったリアルタイム検出を行います。

まず「testing_utils」というフォルダに、物体検出させたい動画を入れます(ホームビデオなど)。

続いて、「videotest_example.py」を開きます。18行目の「model.load_weights('~')」の' '内に学習済みデータのファイル名(ex. ../checkpoints/weights.96-2.83.hdf5)を入れて、24行目の「vid_test.run('~')」の' '内には動画ファイル名を入れます。

以下のような画像が出てくるはずです。

Ssdtest01

うちの環境では、7FPS程度しか出ません。

不思議と元データ「weights_SSD300.hdf5」を使うと、13FPS出るんですけどね。学習データによって倍もフレームレートが変わるという理由がよくわかりませんね。

ともかく、精度はまあまあありそうです。

独自データでの学習

さて、これまVOC2007のデータを使った話。独自データを使うにはどうするのか?

まずは、独自データとなる画像を集めます。これは必須。

続いて、「AnnotationTool.exe」をダウンロードします。

私がとりあえず作った、Windows上で動かすアノテーションデータ作成ソフトです。

使い方は以下。

-----------------------------------------------------------------------

Annotation00

① まずアノテーションファイル(*.xmlと”trainval.txt”)を保存するフォルダ(作業フォルダ)を作りドラッグ&ドロップ

② アノテーションを作成したい画像ファイルをドラッグ&ドロップ

③ 画像ファイルが出てくるので、物体をマウスで囲みます

④ ”ラベル名”のテキストボックスに物体の名前を書きます(半角英数) 

⑤ ”ラベル作成”をクリックすると登録

同一画像内で認識させたい物体の数だけ③~⑤を繰り返します。

⑥ 一通り終わったら「Annotation追加」をクリック

次の画像ファイルを読み込むため、再び②で別の画像を読み込み、⑥までを繰り返します。

すべての画像ファイルのアノテーション登録が終わったら

⑦ ”終了”をクリック

-----------------------------------------------------------------------

これを実行すると、画像ファイル分の「~.xml」ファイルと、「trainval.txt」ができているはずです。

これを、以下のように「VOCdevkit/VOC2007」に反映。

・ 「Annotations」フォルダには作成されたxmlファイルを全部入れます

・ 「ImageSets/Main」にある「trainval.txt」に、上で作られた「trainval.txt」の中身を追記します。

・ 「JPEGImages」フォルダに、独自データの画像ファイルをすべて入れます

そのあと、「get_data_from_XML.py」の中身を書き換えます。

42行目「if name == 'aeroplane':」以降に並んだ20個のラベルを、独自データ分のラベルと置き換えます。

例えば「イノシシ」と「鹿」と「馬」の画像ファイルを加えて、それぞれラベルを「inoshishi」、「shika」、「uma」としたいとします。その場合、42行目以降のコードは

        if name == 'inoshishi':
            one_hot_vector[0] = 1
        elif name == 'shika':
            one_hot_vector[1] = 1
        elif name == 'uma':
            one_hot_vector[2] = 1
         elif name == 'boat':
            one_hot_vector[3] = 1
        elif name == 'bottle':
         ・
         ・
         ・

となります(赤の部分が変更部位)。 ('uma'は'horse'じゃないのか等のツッコミはなしでお願いします)

この状態で

> python get_data_from_XML.py

を実行。

独自データ版の「VOC2007.pkl」ができるはずです。

この状態で、上の「学習」手順に基づき、学習コード(train_ssd_keras.py)を実行。

動画から物体検出させる場合は「testing_utils」の「videotest_example.py」の12行目を、上と同じラベルに書き換えます。あとは「検出(動画)」手順そのまま。

これで、独自データの検出が可能になるはず・・・です。

実は私自身、これを書いている時点ではまだ実行していないのですが、VOC2007のデータを使った学習ができているので、多分可能なはずです。

こちらのサイトでは、実際に動作に成功しているようです。

物体検出用SSD_Kerasで使える学習モデルの作成方法 | AI coordinator

とりあえず、これでSSDを実装することができました。

あとはネットワークを深層化したりしてやれれば万々歳なのですが、まだそこまでは試しておりません。

あと、どういうわけかこのコード、会社では同じようにやっても動かないんですよね。Keras 1.2.2にダウングレードしたのに、Keras 2.0.6で動かしたときのエラーが出ます。理由は不明。

Kerasというのはコードが書きやすくなる半面、学習データがブラックボックス化されるというか、どういう形式で書きこんでいるのかがわからなくなりますね。さらに、1.xから2.0に変えた時の変化点が大きすぎです。おかげで、Kerasのことはあまり好きになれません。

とはいえ、手軽に使える便利なコードなので、このSSD_Keras、おすすめです。

初めてのTensorFlow 数式なしのディープラーニング

« カメラ付きのワインセラー”Plum wine system” | トップページ | Lenovo G580が我が家にやってきた »

数値解析系」カテゴリの記事

コメント

過去記事への投稿失礼します。

非常にためになる記事をありがとうございます。
一通り動いたのですが、気づいたことがあるのでご連絡させていただきます。

独自データでのget_data_from_XML.py実行時にエラーになったので調べてみたところ、
AnnotationTool.exeを実行してできる .xml ファイルの3行目がとなっていますが、
の誤りではないでしょうか?(全て置換したところうまく動きました)
ご確認いただけたらと思いますし、可能であれば修正版をリリースいただけると、
非常に助かるので、ご検討いただければ幸いです。

こんにちは、機会学習勉強中さん。

ご指摘ありがとうございます。ただ、今ちょうど出先でして、なんとか今夜中に対応したいと思います。それまでお待ちいただけますか?

ご連絡いただきありがとうございます。
また、ご対応いただけるとのことで、大変ありがとうございます!
昨日のコメント内で肝心な部分が消えていたので、今更ですが念のため改めて書かせていただきます。
3行目のspirceとなっている箇所ですが、正しくはsourceかと思いますので、ご確認いただければ幸いです。

こんにちは、機械学習勉強中 さん。

たった今、アップいたしました。おっしゃる通り、”source”が”spirce”になってますね。ソースを直し、修正できていることを確認いたしました。

世の中にあるAnnotationツールに比べると、ずいぶんと雑な造りで申し訳ありません。他にも、ラベル無しでAnnotation書き込みを行うと例外エラーを起こしたりと、いろいろ不具合はあります。また、ラベルを付けた部分の枠線が画像中に残るようにできなかったり、不具合だらけですね。Linux環境であれば、もっといいツールがあるんですけどね・・・だれか、Windows版で作ってくれませんかねぇ。

お礼が遅くなってしまって申し訳ありません。
返信したつもりでしたが、投稿が反映されていませんでした。
この度はご対応いただきありがとうございました!
早速使わせていただいております。
短いですがお礼まで。

こんにちは、機械学習勉強中さん。

いえいえ、ご迷惑をおかけしました。会社で作ったコードを印刷して打ち込み、動いたのでそのままにしちゃいました。ちゃんとチェックしなきゃいけませんね。

ところで、このコードはkeras 1.2.2でないと動きません。最新のkeras(今は2.2.0)で使いたいときは、下記のサイトが参考になります。

http://d.hatena.ne.jp/natsutan/touch/20170318/1489851945

tensorflowも最新(1.8.0)にすればちゃんと動きました。本当は本文に書かないといけないのですが、この場を借りて紹介しておきます。ご参考まで。

コメントを書く

(ウェブ上には掲載しません)

トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/510034/66219107

この記事へのトラックバック一覧です: TensorFlow+KerasでSSDを独自データで使えるようにしてみた:

« カメラ付きのワインセラー”Plum wine system” | トップページ | Lenovo G580が我が家にやってきた »

当ブログ内検索

  • カスタム検索

スポンサード リンク

ブログ村

無料ブログはココログ