Python
RaspberryPi
socket
DeepLearning
Chainer
0

Raspberry Piとディープラーニングで画像認識やろうとしたら重かったのでサーバーに計算させた

はじめに

Raspberry Pi上でImageNetの学習済みモデルを使い,カメラから入力した画像を認識しようとしましたが,モデルが大きすぎてメモリに乗りませんでした。
そこで,以下のようにラズパイからサーバーに画像を送信して認識結果を返すような仕組みを実装しました。
概要

方法

ソケット通信でサーバーとやり取りします。

サーバー側

画像認識の部分はChainerで行っています。
モデルはVGGを利用しました。

vgg_server.py
# coding: utf-8
import socket, threading
import chainer
import chainer.links as L
import numpy as np

class ImageNetPredictor:
    def __init__(self):
        self.model = L.VGG16Layers()    # 初回実行時はモデルをダウンロードするので時間がかかる
        self.categories = np.loadtxt("synset_words.txt", str, delimiter="\n").tolist()

    def __call__(self, x):
        x = x[:,:,::-1] # BGR -> RGB
        h = self.model.extract([x], layers=["fc8"])["fc8"]
        h = h.array.argmax()    # 出力は1000次元で各カテゴリのスコアを表すので最大値のインデックスを求める

        return self.categories[h]

class Handler:
    def __init__(self, model):
        self.model = model

    def __call__(self, clientsock, client_address):
        data = b""
        while True:
            r = clientsock.recv(2048)   # 分割して受信
            data += r

            if len(data) >= 224*224*3:
                # 画像サイズ224*224*3のバイト数だけ受信したらループを抜ける
                break

        data = np.fromstring(data, dtype=np.uint8)
        data = data.reshape((224,224,3))    # データが1次元配列になってしまっているので整形する

        category = self.model(data) # 画像認識
        print(" ", client_address, category)
        clientsock.sendall(category.encode("utf-8"))    # 認識結果(カテゴリ名の文字列)をクライアントに返す

        clientsock.close()  # ソケットを閉じる

def main():
    HOST, PORT = "", 55555

    serversock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serversock.bind((HOST, PORT))
    serversock.listen(20)

    model = ImageNetPredictor() # モデル
    print("OK")

    while True:
        clientsock, client_address = serversock.accept() #接続されればデータを格納
        print("conected to "+client_address[0])

        # 接続されたら新規にスレッドを立てて処理する
        handle_thread = threading.Thread(target=Handler(model), args=(clientsock, client_address), daemon=True)
        handle_thread.start()

if __name__ == "__main__":
    main()

Chainerとnumpyはsudo pip install numpy chainer pillowで入ります。
PillowもChainer内部で使われるので一緒に入れておきましょう。

また,synset_words.txtはカテゴリ番号からカテゴリ名を参照するために必要なのでこちらからダウンロードして同じフォルダに配置してください。

上記のvgg_server.pyを起動したらモデルのロードが終わるまで少し待ちます。
初回実行時はモデルをダウンロードするので時間がかかります。
「OK」と表示されたら準備完了です。

クライアント(ラズパイ)側

画像を読み込んで224×224に縮小した後,サーバー側に送信します。
OpenCVのインストールはこちらを参考にさせていただきました。

vgg_client.py
# coding: utf-8
import socket, time
import numpy as np
import cv2

# サーバーのIPアドレス(適宜変える),ポート番号
HOST, PORT = "192.168.31.150", 55555

def image_recog(img):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as clientsock:
        clientsock.connect((HOST, PORT))
        clientsock.send(img.tostring())
        data = clientsock.recv(2048)

    return data.decode("utf-8")

img = cv2.imread("pizza.jpg")
img = cv2.resize(img, (224,224))

print("send image")
start_time = time.time()    # 認識結果が出るまでの時間を計測
category = image_recog(img)
elapsed_time = time.time() - start_time
print(category, elapsed_time)

上記の例ではカメラは使わず,以下の画像をpizza.jpgとして読み込んでいます。
pizza.jpg

結果

サーバー側でvgg_server.pyを起動した状態で,ラズパイ側でvgg_cilent.pyを実行します。

$ python vgg_client.py
send image
n07873807 pizza, pizza pie 0.6916546821594238

正しく認識できました!
画像を送信してからおよそ0.7秒で結果が返ってきています。時間的にもまあまあです。

おわりに

ラズパイでディープなモデルを扱おうとするとリソース不足なことがままあります。
今回紹介したようにサーバーに投げてしまえば,画風変換などいろいろなことができそうです。

実はモデルをGoogLeNetに変えたところ,ラズパイ上で動かすことはできたのですが画像認識に数秒かかりました。
ネットワークや計算機の環境にもよりますが,通信にかかる時間を差し引いてもサーバーを使うのは有効だと思います。

参考