FutureStandard MAKERS

東京にある動画解析プラットフォーム「SCORER」の開発をしているスタートアップのブログです

Raspberry Pi3 と ZeroMQ でフォグ・コンピューティングの雰囲気を体験(中編)

~ HTTP ではないマイクロサービスのすゝめ ~

この記事では、IoT の世界で旬の Raspberry Pi3 上に、ZeroMQPython を使った分散システムを構築し、フォグ・コンピューティングを疑似体験してみたいと思います。

前編は、ZeroMQ の入門だけで終わってしまったので、今回は応用編です。

ラズパイ3を使った『画風変換ネットワーク・カメラ』を作ります。



3.画風変換とは

ニューラルネットワークで『画風*1を学習し、一般画像をそれっぽく変換してしまうというものです。2015年夏頃に有名になったので、ご存じの方もいるのではと思います。

普通の風景写真が、ゴッホの絵のような風景写真になったり、キュービズムな風景写真になったりします。

PFN さんが、詳しい技術内容を記事にしてくれています。

本ブログ記事と同様の「画風変換カメラ」を、Android アプリ+サーバで作られた方もいるようです。その方は、速度に定評のある TensorFlow を使っていますね。また、動画配信プロトコルRTMP を駆使して、サーバに映像を飛ばしています。結構、大掛かりです。

developers.linecorp.com

今回は、導入が簡単な ZeroMQ、Chainer、そして弊社提供の SCORER SDK を利用して、さくっと半日ぐらいで、同じ機能を作ってしまおうと思います。

4.全体構成

こちらを御覧ください。

f:id:FutureStandard:20170302192410j:plain

ラズパイ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/ にアクセスし、カメラの画角を確認しましょう。

f:id:FutureStandard:20170302201355j:plain

CPU リソースを空けたいので、Detection MethodNone に変更し、Apply Now を押下しておきます。

なお、横道にそれますが、画面右上リンク Go to RPi-Monitor をクリックすると、ラズパイの稼働グラフなども見ることができます。

f:id:FutureStandard:20170302201435j:plain

5.1. Cloud9 で開発スタート

次に、開発用ページである http://<ラズパイ3のIPアドレス>:20002/ にアクセスし、Cloud9 を起動します。

利用を開始する前に、右上メニューの Cloud9 または Ctrl-, で Preferences を表示し、Python Support の Python Version が Python3 になっていることを確認します。*5

また、Workspace にホームディレクトリも表示しておきましょう。そのためには歯車マークをクリックします。

f:id:FutureStandard:20170302204845j:plain

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_images.py@gist

ポイントはこちら。

  • カメラ画像を送信する 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

zmq_utils.py@gist

シリアライズの詳細は、本シリーズの前編を参照してください。

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)

mirror_images.py

  • PUB/SUBソケットは、pub_images.py とポート番号が逆になります。また、bind()ではなくconnect() になります。
  • 画像を recv_image() で受信したら、即、send_image()送信しています。

この送信と受信の間に、画風変換を入れていくことになります。

5.4. 単体で動かしてみる

Cloud9 の IDE 画面で、pub_images.py と mirror_images.py をそれぞれ ▶ Run すると、処理が動き始めます。

IDE 画面の中央下にあるコンソール・ペインに、ログが出力されると思います。

f:id:FutureStandard:20170302214832j:plain

ZeroMQ のお陰で、どちらのプログラムを先に Run しても問題ありません。

実行中に SDK のページ http://<ラズパイ3のIPアドレス>:20002/ に行き、Web Show 1 を表示すると、pub_images.py で呼んでいる scorer.imshow() の結果画像を確認することができます。

f:id:FutureStandard:20170302215243j:plain

以上でラズパイ3側の作業は全てです。

プログラムを STOP し、画風変換パソコンの準備に取り掛かります。

6.画風変換用パソコンの準備

今回は、オフィスにあった Windows10 パソコンを利用しました。

Windows10 への Chainer 導入方法は、以下の記事を参考にしました。

ありがとうございます!

6.1. インストール

Windows10 で Chainer を動かすのに必要なソフトウェアをインストールしていきます。

Python3.6 with CUDA

詳細は、上記の Qiita 記事を参照してください。

  1. Visual C++ Build Tools
    • C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin環境変数「PATH」に追加
    • C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt環境変数「INCLUDE」に追加
  2. CUDA Toolkit 8.0
  3. cuDNN 5.1
    • Zipファイルを展開後に内部の “bin” “include” “lib” を C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\ 内に上書きコピー
  4. 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.com

色々な画風のモデル(学習済みデータ)もあります。

github.com

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)

transform_images.py@gist

オリジナルの 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 を見ると、キャプチャした画像が変換され表示されるはずです!

f:id:FutureStandard:20170303233114j:plain

この時の画風変換パソコンのログを見るとこんな感じ。

f:id:FutureStandard:20170303233335p:plain

VGA 画像1枚を 0.9 秒程でループできているようです。

コマンド引数の --gpu 0--gpu -1 にして、CPUを使った時のログはこちら。

f:id:FutureStandard:20170303233438p:plain

画像1枚で6秒弱なので、複数枚の画像を連続で処理すると、GPU の恩恵が大きくなるようですね。

もう一つ、別の画風モデルで流した時の映像を貼っておきます。

f:id:FutureStandard:20170302231057j:plain

その時のコマンドラインはこちら。

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 はスタブなので、今回は動かしません。