2018/12/31

【14.実践7】TinyYoloでリアルタイム物体検出&AR出力

どうも、ディープなクラゲです。
ゼロから学ぶディープラーニング推論」シリーズの最終回です。
このシリーズでは、Neural Compute StickとRaspberryPiの使い方をゼロから徹底的に学び、成果としてディープラーニングの推論アプリケーションが作れるようになることを目指しています。

最終回は、TinyYoloでリアルタイム物体検出とAR出力を行います。

動画は、動画入力とAR出力のデモを行っている様子です。
今回も分かりやすいように、クラゲが段階的に説明します!

【 目次 】


ソースコード入手

実行するためのソースコードをダウンロードします。
Pythonソースコードは demo1.py, demo2.py, demo3.pyの3つです。それぞれ独立に実行するソースコードで、段階的に内容が追加されています。先程の動画で実行しているプログラムは最後のdemo3.pyです。
それに加えて、動画ファイル cars.mp4 の1つと画像ファイル aquarium.jpg と jellyfish.png の2つも使用します。
つまり、ダウンロードすべきファイルは6ファイルです
ターミナルにて、適当な場所に移動します

cd /home/pi/ダウンロード


以前にダウンロードしたフォルダ "movidius-ncs" がある場合は一旦削除して下さい(前回ダウンロードしたのがつい最近であればそのまま使えます)
git cloneコマンドを使ってダウンロードします

git clone https://github.com/electricbaka/movidius-ncs.git

movidius-ncsというフォルダが出来ていると思います。
ファイルマネージャーを2つ開きます

以下の6つのファイルを "/home/pi/workspace/ncappzoo/caffe/TinyYolo" へコピーすればOKです

アプリ実行

今回もスピーカーなど別途必要なものはありません。demo1のみUSBカメラを使用します。
まずは、ターミナルで実行ファイルの位置へ移動します

cd /home/pi/workspace/ncappzoo/caffe/TinyYolo

demo1(カメラ入力)

python3 demo1.py

demo1.pyを実行すると、カメラ映像のウィンドウが開き、ターミナル側にはその映像に対するリアルタイム推論結果(検出したオブジェクトのバウンディングボックス情報)が文字で次々と表示されると思います。カメラ映像側のウィンドウで何かキーを押せばプログラムは終了します。
なお、エラーになった場合は、もう一度実行してみて下さい。

物体は、前回紹介した20種類を検出できます。
例えば本物の車などをカメラで映すのが一番良いのですが、この例のようにクルマのおもちゃでも良いですし、スマホやタブレットにクルマ画像を表示させて、そこにカメラを向けてみても検出すると思います。
カメラの距離や映す場所によってはうまく検知できない場合がありますので、色々試してみて下さい。

demo2(動画入力&調整)

python3 demo2.py

demo2.pyを実行すると、カメラ映像の代わりにMP4ファイルの動画映像が表示されます。
動画ファイルは任意のものに変更可能です。ただし、Neural Compute Stickを使っているとは言え、ラズパイでのリアルタイム動画検出は処理が重たくなります。なので、今回の動画はフレームレートを10fpsに落としたものを使っています。

demo3(AR出力)

python3 demo3.py

demo3.pyは、水槽の中にクラゲが泳いでいるようなアニメーションのウィンドウが追加になっています。
入力映像はdemo2と同じですが、バウンディングボックスの位置に連動させてクラゲを描画しています。

ソースコード解説

demo1(カメラ入力)

それではソースコードの解説です。run.pyから手を加えた箇所を説明します。
コードはこちらからも閲覧可能です。
https://github.com/electricbaka/movidius-ncs/blob/master/TinyYolo/demo1.py

変更点はUSBカメラでリアルタイム入力している点のみです。
元のrun.pyの全体構成を復習しましょう
main関数とdisplay_objects_in_gui関数を抜き出し、それぞれの中の処理をざっくり記述しました

#ざっくりrun.py 
 
def display_objects_in_gui:
  ボックスを画像に追加
  画像表示
  while True:
    キー入力があればループ終了
 
def main:
  各種準備
  静止画像読み込み
  推論実行しボックス情報を得る
  ボックスの重複を削除(filter_object)
  display_objects_in_gui
  後片付け

demo1.pyでは、display_objects_in_gui関数にあった画像表示とwhileループを削除し、mainにおなじみのカメラ画像表示ループを追加しています。

#ざっくりdemo1.py 
 
def display_objects_in_gui:
  ボックスを画像に追加
 
def main:
  各種準備
  while True:
    カメラ画像読み込み
    キー入力があればループ終了
    推論実行しボックス情報を得る
    ボックスの重複を削除(filter_object)
    display_objects_in_gui
    画像表示
  後片付け


具体的な変更箇所を説明します
display_objects_in_gui関数にて、変数display_imageをsource_imageのコピーから実体へ変更しています。
変数source_imageは、main関数の引数display_imageからの参照渡しです。
source_imageの実体を変更することにより、main関数のdisplay_imageが直接変更されます。

display_image = source_image # not copy 



display_objects_in_gui関数にて、以下の箇所は丸ごと削除しています

window_name = 'TinyYolo (hit key to exit)'
cv2.imshow(window_name, display_image)
 
while (True):
    raw_key = cv2.waitKey(1)
 
    # check if the window is visible, this means the user hasn't closed 
    # the window via the X button (may only work with opencv 3.x 
    prop_val = cv2.getWindowProperty(window_name, cv2.WND_PROP_ASPECT_RATIO)
    if ((raw_key != -1) or (prop_val < 0.0)):
        # the user hit a key or closed the window (in that order) 
        break



main関数にて、VideoCaptureの追加とwhileループ処理を加えています

#------------------------------ 
# capture 
#------------------------------ 
cap = cv2.VideoCapture(0)
 
while True:
    ret, frame = cap.read()
 
    # Wait key 
    key = cv2.waitKey(1)
    if key != -1:
      break



input_image への入力を画像ファイル読み込みからカメラ映像のframeへ変更

#input_image = cv2.imread(input_image_file) 
input_image = frame



whileループの最後にウィンドウへの表示追加

# Display 
cv2.imshow("window", display_image)



最後にVideoCapture用の後片付け処理追加

# Clean up capture 
cap.release()
cv2.destroyAllWindows()

demo2(動画入力&調整)

続いてdemo2のソースコード解説です。demo1.pyから手を加えた箇所を説明します。
コードはこちらからも閲覧可能です。
https://github.com/electricbaka/movidius-ncs/blob/master/TinyYolo/demo2.py

demo1からの主な変更点は以下です

まず、カメラ映像から動画ファイルへの変更です

#------------------------------ 
# capture 
#------------------------------ 
cap = cv2.VideoCapture(0)
 
while True:
    ret, frame = cap.read()

上記がdemo1で下記がdemo2です
cv2.VideoCaptureの引数が変更になっていることと、if文が追加されていることが変更点です。
このように動画ファイルの読み込みも、ほぼカメラ映像の読み込みと同じように簡単に扱うことができます。
動画ファイルはwhile文の中で 1フレームずつ順番に readされます。最後のif文は、動画を最後まで読み込み終わったら自動的にwhileループを終了させるためです。動画読み込み時に最終フレームを超えると 変数retにFalseが入力されることを利用しています。

#------------------------------ 
# capture 
#------------------------------ 
cap = cv2.VideoCapture("cars.mp4")
 
while True:
    ret, frame = cap.read()
    if ret == False:
        break



次に、調整部分です。
このままでも動きますが、なるべくバウンディングボックスの誤検知を減らすための調整を3つ行いました。
調整値は実際に動作させながら、試行錯誤で数値を決めた値です。読込む動画によって数値は変わりますので注意してください。

1つ目は、filter_objects関数の閾値probability_thresholdの変更です
0.07から0.15に引き上げています。誤検知はBB確率が低い箇所に多いため、閾値を大きくすることにより減ります。一方で大きくし過ぎると正しい検知も消えてしまうので注意が必要です。

# only keep boxes with probabilities greater than this 
probability_threshold = 0.15 #0.07 > 0.15 



2つ目は、重複の閾値max_iouの調整です。get_duplicate_box_mask関数の最初にあります。
この数値を下げれば、2重に検知されてしまうような重複は減る方向ですが、小さくし過ぎると正しい検知も消えてしまうので注意が必要です。

# The intersection-over-union threshold to use when determining duplicates. 
# objects/boxes found that are over this threshold will be 
# considered the same object 
max_iou = 0.10 #0.35 > 0.10 



3つ目は、大きすぎるBBや小さすぎるBBの除去です。
何もない空間が誤検知されてしまう場合があります。さらにそのBB確率は先程の閾値では除去できない高い数値になっている場合の対策です。
例えば、以下の画像では何もない空間に0.19のBB確率でcarと検知されています。
またBB確率の高い小さなBBの中にも誤検知しているモノがあります。

そこで、filter_objects関数の中で、BB面積が 1000~15000以外のオブジェクトについては除去するようにしました。
コードの内容はduplicate_box_maskを応用したものです。

# delete large or small box 
box_size_list = np.multiply(boxes_above_threshold[:,2],boxes_above_threshold[:,3])
size_mask = np.array((box_size_list>=1000) & (box_size_list<=15000), dtype='bool')
boxes_above_threshold = boxes_above_threshold[size_mask]
classifications_for_boxes_above = classifications_for_boxes_above[size_mask]
probabilities_above_threshold = probabilities_above_threshold[size_mask ]

ちなみに、display_objects_in_gui関数にて対策を行う方法もあり、コード記述も以下のように簡単です。

#demo2にはない比較のためのコードです 
#for文の直下に書く 
        box_size = filtered_objects[obj_index][3] * filtered_objects[obj_index][4]
        if(box_size < 1000 or box_size > 15000):
            continue

しかし、先程のコードのように重複削除処理を行う前に処置した方が、より精度が高い結果となります
左の画像がdisplay_objects_in_gui関数での対策結果、右の画像がfilter_objects関数での対策結果です。
display_objects_in_gui関数での対策の場合、常にBBが消える訳ではないですが、左画像のようにときどき消える場合があります。

下流工程でパッチを当てるような対策を行うよりも、上流工程で早めに対策した方が良い精度になるという良い例です。

demo3(AR出力)

最後にdemo3について見てゆきましょう。ARというべきか、VRというべきか微妙なところですが、別ウィンドウにBB情報を元にしたクラゲのアニメーション表示を行っています。
コードはこちらからも閲覧可能です。
https://github.com/electricbaka/movidius-ncs/blob/master/TinyYolo/demo3.py

画像の表示はOpenCVでも可能ですが、背景画像の上に別の透過画像を重ね合わせるのはちょっと面倒です。
そこで、ラズパイに最初から入っているpygameというゲーム制作向けのPythonのライブラリを活用します

まずは簡単なpygameサンプルを作って動かしてみましょう
ファイルを新規作成します。

import pygame
 
#------------------------------ 
# pygame init 
#------------------------------ 
pygame.init()
screen = pygame.display.set_mode((640360))
pygame.display.set_caption("Window")
background = pygame.image.load("aquarium.jpg")
player = pygame.image.load("jellyfish.png").convert_alpha()
= -50
 
while True:
    #------------------------------ 
    # pygame draw 
    #------------------------------ 
    #draw background 
    screen.blit(background, (00))
 
    #draw player 
    screen.blit(player, (x, 200))
    x = x + 1
    if x > 700:
        x = -50
 
    #update display 
    pygame.display.flip()
 
    #wait 
    pygame.time.wait(10)

実行すると、クラゲが左から右へ流れるアニメーションが実行されます。
無限ループなので、Ctrl + C で終了してください

主にpygame init部とpygame draw部に分かれていて、init部は最初の1回だけ、draw部は無限ループの中で何度も実行します。
始めにpygameのモジュールを使用するためimportを行っています

まずはinit部の説明をダーっと行います

#------------------------------ 
# pygame init 
#------------------------------ 
pygame.init()
screen = pygame.display.set_mode((640360))
pygame.display.set_caption("Window")
background = pygame.image.load("aquarium.jpg")
player = pygame.image.load("jellyfish.png").convert_alpha()
= -50

init()でpygameの初期化
display.set_modeで640×360のウィンドウサイズ設定
display.set_captionでウィンドウの表示名設定
image.loadを使って、aquarium.jpgとjellyfish.pngの読み込み
jellyfish.pngは透過部があるため、convert_alpha()を付加
クラゲのX座標初期値を-50に設定



次にdraw部の説明です

#------------------------------ 
# pygame draw 
#------------------------------ 
#draw background 
screen.blit(background, (00))
 
#draw player 
screen.blit(player, (x, 200))
= x + 1
if x > 700:
    x = -50
 
#update display 
pygame.display.flip()
 
#wait 
pygame.time.wait(10)

screen.blitで背景(aquarium.jpg)を座標原点(0,0)に描画。aquarium.jpgは640×360なのでぴったり収まる
screen.blitでプレイヤー(jellyfish.png)を(x, 200)に描画。xの初期値は-50なので最初はクラゲが見えない状態
x = x + 1 で xをインクリメント(1つ増加)し、700を超えたら初期値に戻す
display.flip()でこれまでの描画を更新
time.wait(10)で10ms待つ
これをwhileループで無限に繰り返す

プレイヤーX座標の初期値を-50、最大値を700に設定しているのは、より自然な感じに背景に出入りしているように見せるためです。
これでpygameの概要は分かったと思います。
demo3ではこれを利用してAR表示を行っています。demo2からの変更点は3箇所です

import pygame

これはいいですね。pygameを使うためにimportしています。

#------------------------------ 
# pygame init 
#------------------------------ 
pygame.init()
screen = pygame.display.set_mode((640360))
pygame.display.set_caption("TinyYolo VR")
background = pygame.image.load("aquarium.jpg")
player = pygame.image.load("jellyfish.png").convert_alpha()
x_ratio = float(screen.get_width()) / NETWORK_IMAGE_WIDTH
y_ratio = float(screen.get_height()) / NETWORK_IMAGE_HEIGHT

main関数のVideoCapture後に追加しています。
先程のpygameサンプルとの違いはx_ratioとy_ratioの存在です。OpenCVのウィンドウサイズとpygameのウィンドウサイズは異なるため、それぞれの比率を取得しておきます。

#------------------------------ 
# pygame draw 
#------------------------------ 
#draw background 
screen.blit(background, (00))
 
#draw player 
for obj_index in range(len(filtered_objs)):
    center_x = int(filtered_objs[obj_index][1] * x_ratio)
    center_y = int(filtered_objs[obj_index][2] * y_ratio)
    screen.blit(player, (center_x - player.get_width()/2, center_y - player.get_height()/2))
 
#update display 
pygame.display.flip()

main関数のwhileループの最後に追加しています
先程のpygameサンプルからの違いはdraw playerの部分です。全てのBB情報を元にfor文を使ってクラゲを描画しています。
BB中心座標とクラゲの中心座標が一致するように計算で調整しています。
本プログラムでは推論で時間がかかり、それがtime.waitの代わりになるため、time.waitは削除しています。

今回は使いませんでしたが、filtered_objs[obj_index][3]とfiltered_objs[obj_index][4]を使うとBBの幅と高さを得ることができます。pygameでは"transform.smoothscale"を使って画像を拡大・縮小させることができますので、連動してみるのも面白いと思います。


「ゼロから学ぶディープラーニング推論」全て読んでいただいた皆様、お疲れさまでした!
ディープラーニングの「推論」そして画像認識「CNN」を中心に学びましたが、「ディープラーニング学習」や「他のネットワーク」を学ぶ際にも大きく役立つと思います。
今回のシリーズで学んだことを活用し、皆さんがオリジナルアプリをたくさん作ることをクラゲは期待しています!
以上、「TinyYoloでリアルタイム物体検出&AR出力」でした!!