Raspberry Pi 3+NCS×３本+Webカメラによる超リアルタイム複数動体検知

`I wrote it in English in the comment section.`
#◆はじめに
Intel公式ForumにてIntelのアーキテクトから当記事を取り上げて頂いたようでViewの伸びが凄まじいが、取り上げてもらえるほどクレバーな記事ではない気が…

さておき、前回、NCS(Neural Compute Stick) + Raspberry Pi 3 Model B + WebカメラでDeepLearning複数動体検知環境を構築したが、かなり動作スピードが改善したとはいえ、まだまだモッサリ感があってイライラしたため、マルチスティック(Multi Stick)環境をPythonで構築してみた。
NCSは１本￥9,900 もする高価なデバイスのため、良い子のみんなは絶対にマネしないように。
![DSC_0046.jpg](https://qiita-image-store.s3.amazonaws.com/0/194769/ffeafe01-e058-b5e7-e213-bd7c13cb23c5.jpeg)

★【前回記事】環境構築
https://qiita.com/PINTO/items/b084fe3dc716c42e2867

★こちらを参考にさせていただいた、謝辞
https://qiita.com/PonDad/items/1a500c05a607a18f6384

#◆前提
１．前回記事を参考にRaspberry Pi 3 + Stretch + Yolo + Python 環境を構築済みであること
２．セルフパワーのUSBハブ必須 (4 Port推奨)
３．NCS(Intel Movidius Neural Compute Stick)×３本 

#◆結果
まずは下記GIF画像をご覧いただきたい。
NCSでの推論の遅さよりも、openCVによるRaspberry Pi画面へのレンダリングがボトルネックになり、多少のモッサリ感は残るが、かなりのサクサク感を体験できる。
あきらかに速い。。。
~~が、最終的に５本まで増設して試したが、３本、４本、５本と増やすたびに３倍速→４倍速→５倍速、とはならない。~~
Stick１本あたり１２コアを持つが分散処理の仕組みを持たないため、２本にすれば２４コア、という単純計算では認識されない。
あくまで１２コアの別デバイスがｎ本、という計算になるため、１本あたり１２コアの処理性能をｎ本分巡回しながら順次活用するメージとなる。
ロジックの組み方次第だとは思うが、複数のStickの推論結果をキューにため込み、取り出す順番はキューに排他を掛けて無理やり直列化しているため、画像１枚レンダリングあたり０．３秒弱のラグが発生してしまう。
むしろ、体感的にはStick１本のほうがスムーズに描写される。
効率的なロジックの組み方が分かる方は是非ご教示いただきたい。

サンプルのモデルデータがショボく、かなりの頻度で間違えた結果を表示する。
今回の検証では、かなり低確率でも検出されるように Threshold は 20% のままとしている。
大したことはしていないが、Intelのサンプルソースを改変して実現。
openCVのカメラ画像インプット および RaspberryPi へのレンダリング処理、NCSによる推論をマルチスレッド化して処理を非同期化し、即応性を極限まで高める工夫をしている。
前回のロジックに比べ、stick １本でもこちらの実装のほうが応答性が遥かに優れている。
下記の画像は等倍速だが、MP4→GIF変換時にフレームレートが落ちて画質も劣化してしまっている。

![ezgif.com-crop .gif](https://qiita-image-store.s3.amazonaws.com/0/194769/2df5f024-7007-ae83-ffee-a5ade91e553b.gif)

★未編集の動画はこちら(Youtube)
https://youtu.be/GIpwdDnqHX4

ちなみに、Intelが公開している下記のYoutube動画は静止画をコマ送りしているところが味噌で、２本にすれば２倍速になっているように見える。
Intelの見せ方に若干の悪意を感じるが、注意しなければならないのは、静止画の紙芝居表示は動画とは違って画像処理の前後関係（時系列）を意識する必要が無く、非同期で順不問に処理されていても表面上はスムーズに順処理されているように見えてしまうことだ。
動画前半の談話時に後ろに見えている動画でのディテクションはしっかり見るとかなり遅い。
~~繰り返しになるが、処理能力が動画再生スピードに負け続けるため、かなり画期的なロジックを開発するか、性能が１０倍以上にでもスペックアップしない限り、**現状Stick仕様と動画の組み合わせでMultiStick構成により倍速以上のパフォーマンスを出すことは有り得ない。**~~
◆IntelのNCS紹介動画
https://www.youtube.com/watch?v=jMcrbhIa9EA

2018/02/25追記
ロジック見直しでの限界を感じたため、Google Cloud Platform で NVIDIA Tesla K80 ２本搭載 の環境を手に入れ、なんとかして10FPS以上の性能が出せないか、学習データへのパフォーマンスチューニングを試行中。
併せて Tesla K80 4本への増設もGoogle Supportへリクエスト中。

**2018/03/03追記**
**入力解像度を落としたり、学習データをスモールにしたり、OpenGLに頼ったりして、次回記事の実装では精度は若干落ちるがStick１本で２倍近いパフォーマンスが出せた。**
https://qiita.com/PINTO/items/db3ab44a3e2bcd87f2d8

**2018/03/26追記**
**こちらが現時点の自分の力量で最速・最高精度の実装。**
https://qiita.com/PINTO/items/b97b3334ed452cb555e2

#◆実装
１．下記ソースを全コピペ（バックスラッシュは半角￥に読み替える）

```MultiStick.py
import sys
graph_folder="./"
if sys.version_info.major < 3 or sys.version_info.minor < 4:
    print("Please using python3.4 or greater!")
    exit(1)

if len(sys.argv) > 1:
    graph_folder = sys.argv[1]

from mvnc import mvncapi as mvnc
import numpy as np
import cv2
from os import system
import io, time
from os.path import isfile, join
from queue import Queue
from threading import Thread, Event, Lock
import re
from time import sleep
from Visualize import *
from libpydetector import YoloDetector
from skimage.transform import resize

mvnc.SetGlobalOption(mvnc.GlobalOption.LOG_LEVEL, 2)

devices = mvnc.EnumerateDevices()
if len(devices) == 0:
    print("No devices found")
    quit()
print(len(devices))

devHandle   = []
graphHandle = []

with open(join(graph_folder, "graph"), mode="rb") as f:
    graph = f.read()

for devnum in range(len(devices)):
    devHandle.append(mvnc.Device(devices[devnum]))
    devHandle[devnum].OpenDevice()

    #opt = devHandle[devnum].GetDeviceOption(mvnc.DeviceOption.OPTIMISATION_LIST)

    graphHandle.append(devHandle[devnum].AllocateGraph(graph))
    graphHandle[devnum].SetGraphOption(mvnc.GraphOption.ITERATIONS, 1)
    iterations = graphHandle[devnum].GetGraphOption(mvnc.GraphOption.ITERATIONS)

    dim = (416,416)
    #dim = (320,320)
    blockwd = 12
    #blockwd = 9
    wh = blockwd*blockwd
    targetBlockwd = 12
    #targetBlockwd = 9
    classes = 20
    threshold = 0.2
    nms = 0.4

print("\nLoaded Graphs!!!")

cam = cv2.VideoCapture(0)
#cam = cv2.VideoCapture('/home/pi/YoloV2NCS/detectionExample/xxxx.mp4')

if cam.isOpened() != True:
    print("Camera/Movie Open Error!!!")
    quit()

cam.set(cv2.CAP_PROP_FRAME_WIDTH, 416)
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 234)
#cam.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
#cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)

#(WEBカメラのみ)
#捨てるフレームが増えてカクつきが増える代わりに実像とプレディクション枠のズレを軽減
#cam.set(cv2.CAP_PROP_FPS, 100)

lock = Lock()
frameBuffer = []
results = Queue()
detector = YoloDetector(1)

def camThread(cam, lock, buff, resQ):
    lastresults = None
    print("press 'q' to quit!\n")
    append = buff.append
    get = resQ.get

    while 1:
        s, img = cam.read()
        if not s:
            print("Could not get frame")
            continue
        lock.acquire()
        if len(buff)>10:
            for i in range(10):
                buff.pop()
        append(img)
        lock.release()
        results = None

        try:
            results = get(False)
        except:
            pass

        if results == None:
            if lastresults == None:
                pass
            else:
                imdraw = Visualize(img, lastresults)
                cv2.imshow('DEMO', imdraw)
        else:
            imdraw = Visualize(img, results)
            cv2.imshow('DEMO', imdraw)
            lastresults = results

        key = cv2.waitKey(1) & 0xFF

        if key == ord("q"):
            break

    cv2.destroyAllWindows()

    lock.acquire()
    while len(buff) > 0:
        del buff[0]
    lock.release()

def inferencer(results, lock, frameBuffer, handle):
    failure = 0
    sleep(1)
    while failure < 100:

        lock.acquire()
        if len(frameBuffer) == 0:
            lock.release()
            failure += 1
            continue

        img = frameBuffer[-1].copy()
        del frameBuffer[-1]
        failure = 0
        lock.release()

        imgw = img.shape[1]
        imgh = img.shape[0]
        im,offx,offy = PrepareImage(img, dim)
        handle.LoadTensor(im.astype(np.float16), 'user object')
        out, userobj = handle.GetResult()
        out = Reshape(out, dim)
        internalresults = detector.Detect(out.astype(np.float32), int(out.shape[0]/wh), blockwd, blockwd, classes, imgw, imgh, threshold, nms, targetBlockwd)
        pyresults = [BBox(x) for x in internalresults]
        results.put(pyresults)

def PrepareImage(img, dim):
    imgw = img.shape[1]
    imgh = img.shape[0]
    imgb = np.empty((dim[0], dim[1], 3))
    imgb.fill(0.5)

    if imgh/imgw > dim[1]/dim[0]:
        neww = int(imgw * dim[1] / imgh)
        newh = dim[1]
    else:
        newh = int(imgh * dim[0] / imgw)
        neww = dim[0]
    offx = int((dim[0] - neww)/2)
    offy = int((dim[1] - newh)/2)

    imgb[offy:offy+newh,offx:offx+neww,:] = resize(img.copy()/255.0,(newh,neww),1)
    im = imgb[:,:,(2,1,0)]
    return im,offx,offy

def Reshape(out, dim):
    shape = out.shape
    out = np.transpose(out.reshape(wh, int(shape[0]/wh)))  
    out = out.reshape(shape)
    return out

class BBox(object):
    def __init__(self, bbox):
        self.left = bbox.left
        self.top = bbox.top
        self.right = bbox.right
        self.bottom = bbox.bottom
        self.confidence = bbox.confidence
        self.objType = bbox.objType
        self.name = bbox.name

threads = []

camT = Thread(target=camThread, args=(cam, lock, frameBuffer, results))
camT.start()
threads.append(camT)

for devnum in range(len(devices)):
  t = Thread(target=inferencer, args=(results, lock, frameBuffer, graphHandle[devnum]))
  t.start()
  threads.append(t)

for t in threads:
  t.join()

for devnum in range(len(devices)):
  graphHandle[devnum].DeallocateGraph()
  devHandle[devnum].CloseDevice()

print("\n\nFinished\n\n")
```

２．/home/pi/YoloV2NCS/detectionExample 直下に上記で作成した「MultiStick.py」を配置。
３．Webカメラ(UVC対応) を Raspberry Pi 本体のUSBポートへ接続
４．NCS(Neural Compute Stick)×３本をセルフパワーUSBハブへ接続、USBハブをRaspberryPiのUSBポートへ接続する
５．下記コマンドをRaspberryPiのコンソール上で実行

```
$ cd ~/YoloV2NCS
$ python3 ./detectionExample/MultiStick.py
```

#◆注意点
バスパワーのUSBハブを意気揚々と購入して環境を仕立てたところ、見事に電圧不足となる洗礼を受けた。
セルフパワーのUSBハブが無いとまともに動かない。
ちなみに、Stick 2本でも電圧は不足し、実行時に正常にstickが認識されない。
記事内の画像にも映っているが、自分は下記を追加調達した。

★エレコム USBハブ 3.0 2.0対応 4ポート ACアダプタ付 ブラック U3H-A408SBK セルフパワー対応
https://www.amazon.co.jp/%E3%82%A8%E3%83%AC%E3%82%B3%E3%83%A0-USB%E3%83%8F%E3%83%96-2-0%E5%AF%BE%E5%BF%9C-AC%E3%82%A2%E3%83%80%E3%83%97%E3%82%BF%E4%BB%98-U3H-A408SBK/dp/B00KKJJCXC

#◆おまけ
WEBカメラが手元に無い、などの理由で動画ファイルでお手軽に試したいときは、

```
#cam = cv2.VideoCapture(0)
cam = cv2.VideoCapture('/home/pi/YoloV2NCS/detectionExample/xxxx.mp4')
```

のようにコメント部を入れ替えたうえで、mp4ファイル名を宜しく変更いただければ動く。
その後試行を重ねることで、推論より画像のリサイズ加工のコストが極めて高いことが分かった。
あらかじめ動画の縦横サイズを416px以下の小ささへ加工しておくと、ラズパイにしてはかなりの爆速感を楽しめる。
もちろん、mp4ファイルを該当のパスへあらかじめコピーしておくことも忘れずに。