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

# はじめに
Raspberry Pi上でImageNetの学習済みモデルを使い，カメラから入力した画像を認識しようとしましたが，**モデルが大きすぎてメモリに乗りませんでした。**
そこで，以下のようにラズパイからサーバーに画像を送信して認識結果を返すような仕組みを実装しました。
![概要](https://qiita-image-store.s3.amazonaws.com/0/259761/ae779b7d-7857-cfdb-3427-00d849ef58e7.png)

# 方法
ソケット通信でサーバーとやり取りします。
コードは[GitHubのリポジトリ](https://github.com/kzkadc/raspi_deep)にもアップロードしてあります。

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

```python: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`はカテゴリ番号からカテゴリ名を参照するために必要なので[こちら](https://github.com/leetenki/googlenet_chainer)からダウンロードして同じフォルダに配置してください。

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

## クライアント（ラズパイ）側
画像を読み込んで$224\times 224$に縮小した後，サーバー側に送信します。
OpenCVのインストールは[こちら](https://qiita.com/mt08/items/e8e8e728cf106ac83218)を参考にさせていただきました。

```python: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](https://qiita-image-store.s3.amazonaws.com/0/259761/c42cb48f-4c44-f93d-1f7a-4cfa9a311d74.jpeg)

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

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

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

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

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

# 参考
- [ラズパイ3にOpenCV3を簡単に導入](https://qiita.com/mt08/items/e8e8e728cf106ac83218)
- [leetenki/googlenet_chainer](https://github.com/leetenki/googlenet_chainer)
- [TCPでカメラからの映像を送信(Python)](https://qiita.com/DIODE/items/cb19d0f7d699c19cf4b2)
- [Python、マルチスレッドを使い簡易チャット](https://torina.top/detail/253/)