【つくばチャレンジ2016】DeepLearningを使ってリアルタイム看板検知をしてみた

つくばチャレンジとは

「つくばチャレンジ」は、つくば市内の遊歩道等の実環境を、移動ロボットに自律走行させる技術チャレンジであり、地域と研究者が協力して行う、人間とロボットが共存する社会の実現のための先端的技術への挑戦です。(下記HPから引用)

つくばチャレンジ

大雑把に言うと、遊歩道2kmをロボットに自律走行させるコンテスト。今回、土浦プロジェクトというチーム名でつくばチャレンジに参加し、DeepLearning(CNN)を用いたリアルタイム看板検知を実装したので、その概要を記す。

本ページでは今回開発したシステムの概要を記す。
その内、それぞれの詳細・まとめたソースコードもアップしたいところ・・・。

成果

動画中から特定の物体を検出するシステムを担当し、実装した。
DeepLearningのフレームワークはChainerを使用し、64*64ビットの画像から、対象物の有無・属性を判別するCNNモデルを作成した。画面内の対象物がありうる場所に検知窓をスライドさせ、逐次CNNをかけることで対象を検出している。モデルは試行錯誤しながら最適なモデルを自作した。
以下に検知動画を示す。

人(看板)検知

つくばチャレンジにて本番使用し、4人中2人の人発見に成功。
本番はTK1を使用したため検知速度に限界が有り、2人検知漏れしてしまったが、GTX9xx,GTX10xx等の高速で処理を行えるGPUを使用すれば、ロバストに検知が可能だと考えている。
(少なくとも、これまでのログデータを見る限りは、ほぼ完璧に検知できている)

動画は試験走行のもの(カメラはリコーTHETA)
youtu.be

信号検知

手元確認のみで本番は使用せず。
ある程度はロバスト性がありそうだが、未検証。

動画は試験走行のもの(カメラはwebカメラ
www.youtube.com

コアとなる知見

CNNにてモデルを構築する上で最も問題となるのが、学習データをどう集めるかということ。
基本的には多く集めれば集めるだけ良いが、真面目にやるには、撮影する時間もラベル付をする時間も必要になる。

そんな時間はないので、今回はコーンの全周囲から撮影した10画像を元に、学習データを自動生成するスクリプトを記載し、学習データの大幅な水増しを行った。今回のつくばチャレンジ用には、10枚の元画像から3万枚のコーンの画像を作成して学習をさせている。
このようにしてランダムの背景画像と組み合わせることで、学習データの作成時間削減の他、以下の利点があり、非常に有効だと考えている。

・背景に対する過学習を抑える
・モデルの位置、サイズ等の変化に対するロバスト性の向上
・誤検知した画像を背景に加え、再学習させていくことでモデルの誤検知率を調整可能
・一度スクリプトを組んでしまえば、別の検知対象のモデルが簡単に作れる

特に最後の利点はとても大きく、3時間で看板検知プログラム→信号検知プログラムへの変更が出来た。忙しい現代人にはピッタリ。

と、ここまで書いたが、元となるアイディアは技ラボさんの以下ページです。
認識のさせ方、合成したデータの扱いについてはオリジナルのつもり。
Deep Learningで用いるデータを「生成」してみた | 技ラボ

学習データの作成

看板のみを切り出した透過画像の作成

まずは看板のみを切り出した透過画像を作成する。
以下のサンプルの通り、そこまで真面目に切り出さなくともOK。

f:id:t_nkb:20161107183054p:plain:w100 f:id:t_nkb:20161107183055p:plain:w60f:id:t_nkb:20161107183056p:plain:w70f:id:t_nkb:20161107183114p:plain:w80

学習画像の作成

上記で切り出した透過画像のサイズ・縦横比をランダムに変更させ、ランダムな背景画像に追加する。
ここで大きさ、位置をバラけさせることで、動画内から対象を探す時に、検出窓を重ねなくても済むようになる(=計算コストが非常に下がる。重要!!)
f:id:t_nkb:20161107183222j:plain:w100f:id:t_nkb:20161107183227j:plain:w100f:id:t_nkb:20161107183238j:plain:w100f:id:t_nkb:20161107183210j:plain:w100
f:id:t_nkb:20161107213220j:plain:w100f:id:t_nkb:20161107213207j:plain:w100f:id:t_nkb:20161107212849j:plain:w100f:id:t_nkb:20161107212813j:plain:w100

データオーグメンテーション

作成した学習画像を元に、反転・ガンマ変換・コントラスト変換を行い、画像を水増しする。
また、上記画像を上下左右に数ピクセルずつランダムにずらす、ランダムにノイズを入れる等の小細工も行っている。

1.元画像
f:id:t_nkb:20161107183538j:plain:w300

2.左右反転
定番。
f:id:t_nkb:20161107183632p:plain:w300

3.ガンマ変換
明るさの調整。
明度を変化させたものを学習させることで、モデルのロバスト性を増すことができる。この画像を学習させることで、太陽光に対するロバスト性が大きく増加した。

f:id:t_nkb:20161107183717p:plain:w300f:id:t_nkb:20161107183727p:plain:w300f:id:t_nkb:20161107183732p:plain:w300f:id:t_nkb:20161107183739p:plain:w300

4.コントラスト変換
効果は実感できなかったが、有効だと言われているのでとりあえず入れてみた。
f:id:t_nkb:20161107183752p:plain:w300f:id:t_nkb:20161107183801p:plain:w300

学習

上記で作成した画像を元に、検知対象の有り・無しの2つのラベルの認識を行うモデルを学習させた。
以下にchainerのモデル部分のみを記載する。
大雑把に言うと、畳み込み4層、全結合層3層のCNNモデルとなる。
上記のモデルを元にGTX1070で学習を行うと、およそ1時間程度でtestデータの正答率98%、4時間ほどで99%以上の検知性能のモデルを作成することができた。

このモデルを使って、THETAの全方位画像のうち、看板の有りそうな高さの画像を20分割してそれぞれを64*64にリサイズし、看板が領域内にあるかどうかを識別することでどの方位に看板があるかを検出する。
冒頭の動画で、人の確率90%以上の部分を青枠で囲っているが、これを見ると、探索対象を3人共検知できていることがわかる。(対象に近づき過ぎると検知できなくなるが・・・)

モデル作成時のノウハウ等も公開したかったが、データをまとめるのが大変なのでおいおい・・・。

(少しまとめてみた)
t-nkb.hatenablog.com

if args.lname == "none":
    model = chainer.FunctionSet(conv1=F.Convolution2D(3, pic_size, 5, pad=1),
                                bn1 = F.BatchNormalization(pic_size),
                                conv2=F.Convolution2D(pic_size, pic_size_conv, 5, pad=1),
                                bn2 = F.BatchNormalization(pic_size_conv),
                                conv3=F.Convolution2D(pic_size_conv, pic_size_conv, 7, pad=1),
                                bn3 = F.BatchNormalization(pic_size_conv),
                                conv4=F.Convolution2D(pic_size_conv, pic_size_conv, 7, pad=1),
                                bn4 = F.BatchNormalization(pic_size_conv),
                                
                                l1=F.Linear(1344, 256),
                                l2=F.Linear(256, 64),
                                l4=F.Linear(64, dataset.get_n_types_target()))
else:
    model = pickle.load(open(args.lname,'rb'))


def forward(x_data, y_data, train):
    dropout_ratio = args.drop
    x, t = chainer.Variable(x_data), chainer.Variable(y_data)
    h = F.dropout(F.relu(model.bn1(model.conv1(x),test = not train)),ratio=dropout_ratio,train=train)
    h = F.dropout(F.max_pooling_2d(F.relu(model.bn2(model.conv2(h),test = not train)), 2),ratio=dropout_ratio,train=train)
    h = F.dropout(F.relu(model.bn3(model.conv3(h),test = not train)),ratio=dropout_ratio,train=train)
    h = F.dropout(F.max_pooling_2d(F.relu(model.bn4(model.conv4(h),test = not train)), 2),ratio=dropout_ratio,train=train)
    h = F.dropout(F.spatial_pyramid_pooling_2d(h, 3, F.MaxPooling2D),ratio=dropout_ratio,train=train)
F.dropout(F.relu(model.l1(h)),ratio=dropout_ratio,train=train)
    h = F.dropout(F.relu(model.l2(h)),ratio=dropout_ratio,train=train)
    y = model.l4(h)
    return F.softmax_cross_entropy(y, t), F.accuracy(y, t)