~ HTTP ではないマイクロサービスのすゝめ ~
この記事では、IoT の世界で旬の Raspberry Pi3 上に、ZeroMQ と Python を使った分散システムを構築し、フォグ・コンピューティングを疑似体験してみたいと思います。
前編は、ZeroMQ の入門だけで終わってしまったので、今回は応用編です。
ラズパイ3を使った『画風変換ネットワーク・カメラ』を作ります。
3.画風変換とは
ニューラルネットワークで『画風』*1を学習し、一般画像をそれっぽく変換してしまうというものです。2015年夏頃に有名になったので、ご存じの方もいるのではと思います。
普通の風景写真が、ゴッホの絵のような風景写真になったり、キュービズムな風景写真になったりします。
PFN さんが、詳しい技術内容を記事にしてくれています。
本ブログ記事と同様の「画風変換カメラ」を、Android アプリ+サーバで作られた方もいるようです。その方は、速度に定評のある TensorFlow を使っていますね。また、動画配信プロトコルの RTMP を駆使して、サーバに映像を飛ばしています。結構、大掛かりです。
今回は、導入が簡単な ZeroMQ、Chainer、そして弊社提供の SCORER SDK を利用して、さくっと半日ぐらいで、同じ機能を作ってしまおうと思います。
4.全体構成
こちらを御覧ください。
ラズパイ3をネットワーク・カメラ化するのには、弊社の映像解析 IoT プラットフォームとして提供している SCORER SDK を利用します。SCORER には、OpenCV3.2 や ZeroMQ など、よく使うミドルウェアが最初からインストールされているので、高速開発に便利です。
画風の変換には、日本で人気の高いディープラーニング・フレームワークである Chainer を利用します。Chainer をラズパイ3上で動かすのは、さすがにメモリ不足なので、パソコンに処理をオフロードします。この時の通信に、ZeroMQ を活用します。
画風変換用のパソコンは、オフィスにあった普通の Windows 10 パソコンです。*2
5.ラズパイ3の準備
ラズパイ3に USB カメラを接続し、SCORER SDK のイメージを書き込んだ MicroSD を挿入して起動します。起動したら Wifi もしくは有線で LAN にも繋ぎましょう。*3
カメラ制御ページ*4である http://<ラズパイ3のIPアドレス>:20001/
にアクセスし、カメラの画角を確認しましょう。
CPU リソースを空けたいので、Detection Method は None に変更し、Apply Now を押下しておきます。
なお、横道にそれますが、画面右上リンク Go to RPi-Monitor をクリックすると、ラズパイの稼働グラフなども見ることができます。
5.1. Cloud9 で開発スタート
次に、開発用ページである http://<ラズパイ3のIPアドレス>:20002/
にアクセスし、Cloud9 を起動します。
利用を開始する前に、右上メニューの Cloud9 または Ctrl-,
で Preferences を表示し、Python Support の Python Version が Python3 になっていることを確認します。*5
また、Workspace にホームディレクトリも表示しておきましょう。そのためには歯車マークをクリックします。
5.2. カメラ画像の取得と ZeroMQ による送受信
SCORER SDK から、最新のカメラ映像を取得し、ZeroMQ で送信するのは簡単です。
今回は、ZeroMQ の Pub-Sub パターンを利用します。以下のファイルを Cloud9 上にドラッグ&ドロップして、ラズパイ3内に持ち込みましょう。
pub_images.py
import numpy as np import time import scorer import zmq import zmq_utils # Setup ZeroMQ image publisher & receiver context = zmq.Context() pub = context.socket(zmq.PUB) pub.set_hwm(1) pub.bind("tcp://*:5558") sub = context.socket(zmq.SUB) sub.setsockopt_string(zmq.SUBSCRIBE, "image") sub.set_hwm(1) sub.bind("tcp://*:5559") # Setup VideoCaputre Object cap = scorer.VideoCapture(0) last_tick_pub = last_tick_sub = time.time() duration_pub = duration_sub = 0 while True: frame = cap.read() if frame == None: continue # Publish the image image = frame.get_bgr() zmq_utils.send_image(pub, image) tick = time.time() duration_pub = tick - last_tick_pub last_tick_pub = tick # Receive the transformed image result = None try: result = zmq_utils.recv_image(sub, flags=zmq.NOBLOCK) except zmq.error.Again: pass if result is not None: tick = time.time() duration_sub = tick - last_tick_sub last_tick_sub = tick # Output scorer.imshow(1, result) print("Send {0:>7.3f} ({1:>7.3f} fps) Recv {2:>7.3f} ({3:>7.3f} fps)". format(duration_pub, 1.0/duration_pub if duration_pub>0 else 0.0, duration_sub, 1.0/duration_sub if duration_sub>0 else 0.0))
ポイントはこちら。
- カメラ画像を送信する PUB ソケットはポート5558、画風変換した画像を受け取る SUB ソケットはポート5559 で待受けます。
- 画風変換の間に合わなかった画像は、キューに溜めずにすぐ捨てます。そのため、
set_hwm(1)
でキューの最大長を1にしています。メモリ消費やレイテンシーが良好になります。 - SCORER SDK では OpenCV と同様のインタフェースでカメラ画像を取得できます。 cap オブジェクトがそのインタフェースです。
- 取得したカメラ画像は、直ちに
zmq_utils.send_image()
でブロードキャストします。そして、キャプチャのフレームレートを計算します。 - 画風変換された画像は、
zmq_utils.recv_image()
で受信します。変換結果がいつ来るか予測できないのでNOBLOCK
を指定し、データが到着したときだけ処理します。 - 変換結果を受信できた場合だけ
scorer.imshow()
で結果を Web に表示し、受信のフレームレートを計算します。
ZeroMQ で Numpy 画像を送受信する関数は、zmq_utils.py として、pub_images.py の隣に置いておきます。一種のライブラリ・ファイルですね。
zmq_utils.py
import numpy as np import json import zmq def send_image(socket, image, channel = b"image"): # Serialize a Numpy array dtype = str(image.dtype).encode('ascii') shape = json.dumps(image.shape).encode('ascii') data = image.tostring('C') socket.send_multipart([channel, dtype, shape, data]) def recv_image(socket, flags = 0): channel, dtype, shape, data = socket.recv_multipart(flags) # Deserialize a numpy array image = np.frombuffer(data, dtype=dtype.decode('ascii')) image.shape = json.loads(shape.decode('ascii')) return image
5.3. 画風変換のスタブ
画風変換のプログラムを動かす前に、スタブ・プログラムで処理の概要を確認しましょう。
mirror_images.py
import numpy as np import zmq import zmq_utils # Setup ZeroMQ image publisher & receiver context = zmq.Context() sub = context.socket(zmq.SUB) sub.setsockopt_string(zmq.SUBSCRIBE, "image") sub.set_hwm(1) sub.connect("tcp://localhost:5558") pub = context.socket(zmq.PUB) pub.set_hwm(1) pub.connect("tcp://localhost:5559") print("Starting...") while True: image = zmq_utils.recv_image(sub) zmq_utils.send_image(pub, image)
- PUB/SUBソケットは、pub_images.py とポート番号が逆になります。また、
bind()
ではなくconnect()
になります。 - 画像を
recv_image()
で受信したら、即、send_image()
で送信しています。
この送信と受信の間に、画風変換を入れていくことになります。
5.4. 単体で動かしてみる
Cloud9 の IDE 画面で、pub_images.py と mirror_images.py をそれぞれ ▶ Run すると、処理が動き始めます。
IDE 画面の中央下にあるコンソール・ペインに、ログが出力されると思います。
ZeroMQ のお陰で、どちらのプログラムを先に Run しても問題ありません。
実行中に SDK のページ http://<ラズパイ3のIPアドレス>:20002/
に行き、Web Show 1 を表示すると、pub_images.py で呼んでいる scorer.imshow()
の結果画像を確認することができます。
以上でラズパイ3側の作業は全てです。
プログラムを STOP し、画風変換パソコンの準備に取り掛かります。
6.画風変換用パソコンの準備
今回は、オフィスにあった Windows10 パソコンを利用しました。
Windows10 への Chainer 導入方法は、以下の記事を参考にしました。
- chainerインストールメモ( windows10 python3.5 cuda8.0 ) - Qiita
- 公式のpython on win10でPaintsChainer on CPUを動かす - Qiita
ありがとうございます!
6.1. インストール
Windows10 で Chainer を動かすのに必要なソフトウェアをインストールしていきます。
Python3.6 with CUDA
詳細は、上記の Qiita 記事を参照してください。
- Visual C++ Build Tools
- CUDA Toolkit 8.0
- cuDNN 5.1
- Zipファイルを展開後に内部の “bin” “include” “lib” を
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\
内に上書きコピー
- Zipファイルを展開後に内部の “bin” “include” “lib” を
- Anaconda 4.3.0 / Python 3.6 version 64bit *6
インストールが終わり、コマンドプロンプトから python -V
を実行すると、以下の表示が出るようになります。
Python 3.6.0 :: Anaconda 4.3.0 (64-bit)
※ もしGPUを利用せずCPUだけで使う場合、Anaconda のインストールのみでOKです。
pip パッケージ
Chainer 含め、Pythonのパッケージは pip コマンドで簡単に入ります。
コマンドプロンプトから、以下のコマンドを実行します。
pip install chainer pip install pillow pip install pyzmq
pyzmq は、ZeroMQ の Python バインディングだけでなく、ZeroMQ 本体もインストールしてくれます。
fast-neuralstyle をダウンロード
GitHub から、以下のレポジトリをダウンロードして、展開しておきます。
色々な画風のモデル(学習済みデータ)もあります。
GitHub から直接 Zip ファイルを取得・展開できます。
むしろ git clone したい方はこちら。
git clone https://github.com/yusuketomoto/chainer-fast-neuralstyle git clone https://github.com/gafr/chainer-fast-neuralstyle-models
6.2. 画風変換を試してみる
早速、画風変換してみましょう。
レポジトリ内の sample_images/tubingen.jpg を変換しますが、利用したパソコンのグラボは GTX 650 Ti で 1280×720 の解像度を処理するにはメモリ不足でした。事前に 640x480 まで画像を縮小し、tubigen_vga.jpg として保存しておきます。
コマンドプロンプトで Gitプロジェクトの展開先に chdir し、generate.py を叩きます。
cd chainer-fast-neuralstyle python generate.py --gpu 0 --model models\seurat.model --out ..\output.jpg --keep_colors sample_images\tubingen_vga.jpg 2.5774641036987305 sec
seurat.model という画風を tubingen.jpg に適用して、output.jpg が生成されました。かかった時間は 2.57 秒。
別途取得したレポジトリのモデルも使ってみます。
python generate.py --gpu 0 --model ..\chainer-fast-neuralstyle-models-master\models\cubist.model --out ..\output.jpg --keep_colors sample_images\tubingen_vga.jpg 2.7556166648864746 sec
CPU でも動作させてみます。--gpu -1
が CPU の指定です。
python generate.py --gpu -1 --model ..\chainer-fast-neuralstyle-models-master\models\cubist.model --out ..\output.jpg --keep_colors sample_images\tubingen_vga.jpg 5.451727628707886 sec
このパソコンのCPUは Core i7-3770 3.4GHz。GTX 650 Tiの2倍弱の時間がかかりました。
すべて順調に動作しているようです!
6.3. ZeroMQ と連結する
generate.py は、画像ファイルを読み込んで、変換して、ファイルに書き出すという構造になっています。画像ファイルの代わりに、ZeroMQ で受信した画像を変換し、結果も ZeroMQ で送り出すように改造します。
画像の扱いも、generate.py は Pillow ライブラリを使用しているので、PIL.Image オブジェクト(Pillow形式)と Numpy オブジェクト(OpenCV形式)の相互変換が必要となります。
以下のファイル transform_images.py を generate.py の隣にコピーしておきます。
transform_images.py
from __future__ import print_function import numpy as np import argparse from PIL import Image, ImageFilter import time import chainer from chainer import cuda, Variable, serializers from net import * import zmq import zmq_utils import time parser = argparse.ArgumentParser(description='Real-time style transfer image generator') parser.add_argument('--gpu', '-g', default=-1, type=int, help='GPU ID (negative value indicates CPU)') parser.add_argument('--model', '-m', default='models/style.model', type=str) parser.add_argument('--out', '-o', default='out.jpg', type=str) parser.add_argument('--median_filter', default=3, type=int) parser.add_argument('--padding', default=50, type=int) parser.add_argument('--keep_colors', action='store_true') parser.set_defaults(keep_colors=False) args = parser.parse_args() # from 6o6o's fork. https://github.com/6o6o/chainer-fast-neuralstyle/blob/master/generate.py def original_colors(original, stylized): h, s, v = original.convert('HSV').split() hs, ss, vs = stylized.convert('HSV').split() return Image.merge('HSV', (h, s, vs)).convert('RGB') model = FastStyleNet() serializers.load_npz(args.model, model) if args.gpu >= 0: cuda.get_device(args.gpu).use() model.to_gpu() xp = np if args.gpu < 0 else cuda.cupy print("The model has been read") # Setup ZeroMQ image publisher & receiver context = zmq.Context() sub = context.socket(zmq.SUB) sub.setsockopt_string(zmq.SUBSCRIBE, "image") sub.set_hwm(1) sub.connect("tcp://<ラズパイ3のIPアドレス>:5558") pub = context.socket(zmq.PUB) pub.set_hwm(1) pub.connect("tcp://<ラズパイ3のIPアドレス>:5559") print("Starting...") last = time.time() while True: # Convert cv::Mat to PIL.Image original = zmq_utils.recv_image(sub) original = Image.fromarray(original) b,g,r = original.split() original = Image.merge("RGB",(r,g,b)) image = np.asarray(original, dtype=np.float32).transpose(2, 0, 1) image = image.reshape((1,) + image.shape) if args.padding > 0: image = np.pad(image, [[0, 0], [0, 0], [args.padding, args.padding], [args.padding, args.padding]], 'symmetric') image = xp.asarray(image) x = Variable(image) y = model(x) result = cuda.to_cpu(y.data) print("result obtained") # Free the memory y.unchain_backward() if args.padding > 0: result = result[:, :, args.padding:-args.padding, args.padding:-args.padding] result = np.uint8(result[0].transpose((1, 2, 0))) med = Image.fromarray(result) if args.median_filter > 0: med = med.filter(ImageFilter.MedianFilter(args.median_filter)) if args.keep_colors: med = original_colors(original, med) tick = time.time() print(tick - last, 'sec') last = tick #med.save(args.out) # Convert PIL.Image back to cv::Mat r,g,b = med.split() med = Image.merge("RGB",(b,g,r)) med4pub = np.asarray(med) zmq_utils.send_image(pub, med4pub)
オリジナルの generate.py と比較すると、前述の mirror_images.py を組み込んでいるのがわかると思います。
PUBソケットとSUBソケットで connect()
する際の IP アドレスは、ラズパイ3のIPアドレス を書いておきます。
なお、このプログラムを動かすのには、隣に zmq_utils.py も必要ですので、忘れずに transform_images.py と同じフォルダにコピーしておきます。
7.全体で動かしてみる
ここまで出来てしまえば、全体を動かすのは簡単です。
ZeroMQのお陰でどちらが先でも良いのですが、ラズパイ3で pub_images.py を動作させ、次に画風変換パソコンで transform_images.py を動作させてみます。*7
7.1. ラズパイ3側
IDE から ▶ Run にて実行、もしくは IDE 画面下のコンソールから、以下のコマンドを実行します。
python3 ./pub_images.py
7.2. 画風変換パソコン側
python transform_images.py --gpu 0 --keep_colors --model ..\chainer-fast-neuralstyle-models-master\models\edtaonisl.model
そして Web show を見ると、キャプチャした画像が変換され表示されるはずです!
この時の画風変換パソコンのログを見るとこんな感じ。
VGA 画像1枚を 0.9 秒程でループできているようです。
コマンド引数の --gpu 0
を --gpu -1
にして、CPUを使った時のログはこちら。
画像1枚で6秒弱なので、複数枚の画像を連続で処理すると、GPU の恩恵が大きくなるようですね。
もう一つ、別の画風モデルで流した時の映像を貼っておきます。
その時のコマンドラインはこちら。
python transform_images.py --gpu -1 --keep_colors --model ..\chainer-fast-neuralstyle-models-master\models\cubist.model
画風変換おもしろいかも!!
中編まとめ(つづく)
今回、mattyaさん、yusuketomotoさんが公開している素晴らしいライブラリのお陰で、半日足らずで、画風変換カメラを作ることができました。
Chainer を利用した画像処理は、以下の記事にもあるとおり、他にも色々とあります。
本記事のノウハウを活用することで、このようなアルゴリズムを、カメラと簡単に組み合わせ、実環境に適用することができると思います。
それでも計算リソースが不足する場合には、複数のコンピュータを駆使して処理を分担するハイパフォーマンス・クラスタ的な使い方をしたくなるでしょう。
後編では、フォグ・コンピューティングの真骨頂というべき?、エッジ側でのハイパフォーマンス・クラスタを実現するロードバランサーについて解説したいと思います。
*1:画風は、英語で “Style” と言うようです。
*2:私のノートPCでも動作しますが、CPUでの処理はあまりに遅かったので、NVidia グラボ搭載のパソコンにしました。
*3:iOS App の SCORER Starter を利用すると、画面・キーボード不要のヘッドレス構成でネットワークの設定ができます。
*4:Scorer Camera Control - SCC と呼びます。
*5:ここが Python2 だと import scorer に失敗します。
*6:Anacondaは、Python の科学技術計算に最適化されたディストリビューションです。公式の Python 3.6 の代わりにインストールします。
*7:mirror_images.py はスタブなので、今回は動かしません。