FutureStandard MAKERS

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

【SOT-ES100】無線による光伝送、使ってみた

 

 

1. はじめに

みなさん、こんにちは。

FutureStandardでエンジニアをしています、田本です。

会社のブログを書くのは初めてですが、よろしくお願いします。

 

今回は、弊社の監視カメラのある案件で使用した面白い通信機器を紹介したいと思います。

東洋電機さん(開発: 太陽誘電さん)の空間光伝送装置「SOT-ES100」です。

「光による無線伝送」という大変面白い製品ですので、その時の評価レポートをまとめました。

 

 

2. SOT-ES100とは?

f:id:FutureStandard:20170227184754j:plain

LANケーブルの信号を光に変換して無線で送受信するメディアコンバーターです。
他の通信方法と比較すると、下記のような利点があります。

・ケーブルを這わせたくないような場所からも、ネットワークに接続できる。

・初期設定不要で使用できる。

・電波の混雑などでWi-Fiが使えない場所でも通信できる。

Wi-Fiなどと比べ、通信の傍受がされづらい。

・電波法認証不要で使用できる。*1

・屋外でも使用できる。*2

・最大で200mまで通信可能。*3

現状の最大伝送速度は10Mbpsですが、将来的に100Mbpsに対応されるとのことです。

 

通信方法別比較表

 

有線LAN

無線LAN

(無指向性)

無線LAN

(指向性)

LTE

光無線通信

SOT-ES

シリーズ

最大通信速度

100Gpbs

1Gbps

30Mbps

100Mbps

10Mbps

最大通信距離

100m

100m

10km

-

200m

配線の有無

あり

なし

なし

なし

なし

電波の干渉

-

弱い

弱い

弱い

強い

通信の傍受

強い

弱い

弱い

弱い

強い

初期設定

不要

必要

必要

必要

不要

www.toyo-elec.co.jp

 

外観 

f:id:FutureStandard:20170304205739j:plain 

上面: LEDで通信強度がわかります。 

 

f:id:FutureStandard:20170403200716j:plain

 前面: 送光・受光部です。

 

f:id:FutureStandard:20170403200955j:plain

後面: LANケーブルの挿し込み口がついています。

 

 

3. 製品評価の概要

性能の実測や、光ならではの性質を利用した実験を行いました。

本体にLANケーブルが挿せるようになっていて、AとBの間でデータを送受信できます。

今回はAの方にNTTのモデムを、Bの方にパソコンをつないで実験してみました。

 

 

3. 1. スピードテストやってみた

まずはスピードテスト

 

スピードテストの構成図

f:id:FutureStandard:20170421115611p:plain

今回はUSENスピードテストを使用しました。
(http://www.usen.com/speedtest02/

 

f:id:FutureStandard:20170227185136p:plain

最初は機器の間隔を20cmに設定してスピードテストを行いました。*4
最大伝送速度が10Mbpsなので、約90%のスピードが出ていますね。*5

 

その後、装置の間隔を10m程度に変更して10回スピードテストを行いました。

試行回数

スピード[Mbps]

1

9.189

2

9.214

3

9.156

4

9.214

5

9.166

6

9.166

7

9.167

8

9.167

9

9.163

10

9.188

平均値

9.179

装置の間隔によるスピードの違いは、誤差の範囲内と言って問題ないと思います。

 

 

3. 2. 光を遮ってみた

光による無線通信ということで、装置間に障害物があると通信が途切れてしまうことがあります。

試しに養生テープや緩衝材の薄いスポンジで遮ってみましたが、
若干通信強度が落ちることがあっても、通信には問題ありませんでした。

f:id:FutureStandard:20170421125704j:plain

f:id:FutureStandard:20170421130720j:plain

製品試験では豪雨や濃霧などの悪条件でのテストもされているそうなので、予想以上に切れづらいです。

 

当たり前かもしれませんが、手で完全に覆うと切断してしまいます。

f:id:FutureStandard:20170421131816j:plain

通信強度が0になってしまいました。(赤丸で囲った部分)

 

  

3. 3. 鏡で反射させてみた

最後に鏡で反射させても通信できるのか実験してみました。 

f:id:FutureStandard:20170227185759j:plain

角度の調整がシビアでしたが、 問題なくインターネットに接続できました。

 

f:id:FutureStandard:20170403195545j:plain

わかりづらいですが、受信強度を示すLEDが光っていますね。(赤丸で囲った部分)

 

4. まとめ

今回の実験では最大伝送速度の約90%のスピードが出ていたので、今後100Mbpsに対応したときが非常に楽しみです。

天井など、障害物が少なく人が光を遮りづらい場所に設置すると、通信が切断されることなく使えると思います。

弊社では監視カメラシステムに、この光無線装置を活用しております。
興味がございましたら下記のリンクをご覧ください。

SCORER Surveillance

 

*1:いわゆる技適のこと

*2:防水ケースが必要

*3:今回使用した「SOT-ES100」は100mまで

*4:製品仕様に記載されている伝送距離の最短が20cmであるため

*5:装置を介さない場合は約67Mbpsだったので、回線がボトルネックになっているわけではありません

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

OpenCVデータをC++とPython間で交換する方法

1.はじめに

弊社では社長を含めてメンバーの中で一番年上の林です。新卒で業務系ソフトウエア開発会社にエンジニアとして入社したのち、海外での寄り道を経由して、ビジネス番組を衛星放送およびインターネットに配信している会社にて番組企画・プロデューサーとして働いた後、現在は縁あってFuture Standardにて主にRaspberry Piを利用したシステムの開発を行ってます。エンジニア歴としては、UNIXを利用したC言語システム開発の経験が長い一方、Webのフロント開発は苦手です。

先日のエントリにありましたとおり、弊社ではRaspberry Pi、USBカメラ、Pythonの組み合わせで、簡単に映像解析(顔検知、バーコード読み取り、AI物体認識など)を行うことができるSCORER SDKをリリースしました。そこで、本エントリではSCORER SDKで使われている技術の一部を説明します。

SCORER SDKの内部では、Gstreamer+OpenCVを使ってUSBカメラからの画像取得を行っています。このプログラムはC++で書かれています。

一方、SCORER SDKは使用言語がPythonとなります。そこでSCORER SDK内部では、C++OpenCV形式で保持されている画像データを、Python形式に交換しています。またSDKで操作された画像データをクラウドにアップロードするプログラムもC++で作られている事から、PythonからC++へのデータ交換も実施しています。この交換方法が本エントリーのテーマです。

2.C++OpenCVにおける画像データ

例えばある画像の高さ(rows)、幅(cols)、ピクセルデータ(data)がわかっている場合、C++OpenCVにおいては、下記の手法で画像データ(img)を作成する事ができます。 なお、今回は簡便化の為にC++側、Python側共に画素データの形式はBGR形式を前提とします。

static const int GRAY = 1;
cv::Mat create_image(int type, int rows, int cols, void* data){
    cv:: Mat img;
    if( type == GRAY){
        //グレイ画像
        img = cv::Mat(rows, cols, CV_8UC1 , data);   //CV_8UC1 はOpenCVの定数
    }else{
        //カラー画像
        img = cv::Mat(rows, cols, CV_8UC3 , data);   //CV_8UC3はOpenCVの定数
    }

    return img;
}

3.PythonOpenCVにおける画像データ

PythonOpenCVにてある画像の高さ(rows)、幅(cols)、ピクセルデータ(data)があらかじめ分かっている場合、画像データ(img)の生成方法を考えます。

PythonOpenCVでは、Numpy形式でデータを保持します。そこでピクセルデータをNumpy形式に交換する必要があります。SCORER SDKではピクセルデータをNumpy形式に交換する際、高速化のためNumpyが提供する機能であるfrombufferを利用しています。USBカメラからの画像取得など、連続したデータを扱う場合はfrombufferを利用する事を前提に考えた方が良いでしょう。

GRAY=1
def create_image(type, rows, cols, data):
    if type == GRAY:
        # グレイ画像
        # 画素データをnumpy形式に交換
        img=numpy.frombuffer(data, dtype=np.uint8).reshape((rows, cols))
        # グレイデータに交換
        gray = cv2.cvtColor(self.image, cv2.COLOR_BGR2GRAY)
    else:
        # カラー画像
        # 画素データをnumpy形式に交換f。このimgをそのまま利用可能    
        img=numpy.frombuffer(data, dtype=np.uint8).reshape((rows, cols,3))

    return img

frombufferに関する参考記事:http://stackoverflow.com/questions/5674960/efficient-python-array-to-numpy-array-conversion

4.ZeroMQを利用したプロセス間通信

C++版、Python版それぞれのOpenCVにおいて画像データの作成方法が整理出来ましたので、次はC++のプログラムとPythonプログラムの間の通信方法を考えます。先日のエントリにも説明がありますが、SCORER SDKではこのようなプログラム間の通信には、軽量なメッセージング・ミドルウエアであるZeroMQを利用しています。ZeroMQでは利用する接続文字列を変える事で、プログラムを修正する必要なく同一マシン間でのプロセス間通信と、マシンもしくはネットワークを跨いだ通信が可能になります。そこで本エントリーでもZeroMQを利用します。

5.C++からPythonへの交換

それではC++プログラムから読み込んだBMP画像を、Pythonプログラムに送信して、ファイル書き出しするプログラムを見てみます。

5.1: 送信側C++プログラム

送信プログラムでは、引数で与えられた画像ファイルをOpenCV形式に読み込んだのちに、画像の情報をZeroMQを利用して送信します。

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "zmq.hpp"
#include <stdlib.h>
#include <stdio.h>

void my_free(void *data, void *hint)
{
    free(data);
}

main( int argc, char *argv[]){
    cv::Mat image;
    int i;

    // File Open
    if( !strcmp(argv[2], "color") ){
        printf("color\n");
        image = cv::imread(argv[1], CV_LOAD_IMAGE_COLOR);
    }else{
        printf("gray\n");
        image = cv::imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
    }

    // Image Info
    int32_t  info[3];
    info[0] = (int32_t)image.rows;
    info[1] = (int32_t)image.cols;
    info[2] = (int32_t)image.type();

    // Open ZMQ Connection
    zmq::context_t context (1);
    zmq::socket_t socket (context, ZMQ_REQ);
    socket.connect ("tcp://localhost:5555");

    // Send Rows, Cols, Type
    for(i=0; i<3; i++ ){
        zmq::message_t msg ( (void*)&info[i], sizeof(int32_t), NULL  );
        socket.send(msg, ZMQ_SNDMORE);
    }

    // Pixel data
    void* data = malloc(image.total() * image.elemSize());
    memcpy(data, image.data, image.total() * image.elemSize());

    // Send Pixel data
    zmq::message_t msg2(data, image.total() * image.elemSize(), my_free, NULL);
    socket.send(msg2);

    return 0;
}

5.2: 受信側Pythonプログラム

受信側では、送信プログラムから送られてきた画像情報を、PythonにおけるOpenCV形式に交換したのちに、ファイルに出力します。ZeroMQでは受信されたデータはbyte列となっています。Pythonプログラムにおいて、変数がbyte列のままでは画像の高さ、横幅といった数字を正しく扱えません。そこでこのプログラムは受信したバイト列をPythonで扱える数字に交換するために、struct.upackメソッドを利用してバイト列の交換を行っています。

import zmq
import cv2
import struct
import numpy as np

# Connection String
conn_str      = "tcp://*:5555"

# Open ZMQ Connection
ctx = zmq.Context()
sock = ctx.socket(zmq.REP)
sock.bind(conn_str)

# Receve Data from C++ Program
byte_rows, byte_cols, byte_mat_type, data=  sock.recv_multipart()

# Convert byte to integer
rows = struct.unpack('i', byte_rows)
cols = struct.unpack('i', byte_cols)
mat_type = struct.unpack('i', byte_mat_type)

if mat_type[0] == 0:
    # Gray Scale
    image = np.frombuffer(data, dtype=np.uint8).reshape((rows[0],cols[0]));
else:
    # BGR Color
    image = np.frombuffer(data, dtype=np.uint8).reshape((rows[0],cols[0],3));

# Write BMP Image
cv2.imwrite("recv.bmp", image);

6.PythonからC++への交換

それでは今度はPythonプログラムにて読み込んだBMP画像を、C++プログラムへ送信するプログラムの実例をみてみます。

6.1: 送信Pythonプログラム

5.1と同様に引数で与えられた画像ファイルをOpenCV形式に読み込んだのち、画像の情報をZeroMQを利用して送信します。

import zmq
import cv2
import sys
import numpy as np

conn_str="tcp://localhost:5556"

args = sys.argv

ctx = zmq.Context()
sock = ctx.socket(zmq.REQ)
sock.connect(conn_str)

if( args[2] == "color"):
    # Color
    img = cv2.imread(args[1], cv2.IMREAD_COLOR);
else:
    # Gray
    img = cv2.imread(args[1], cv2.IMREAD_GRAYSCALE);

height, width = img.shape[:2]
ndim = img.ndim

data = [ np.array( [height] ), np.array( [width] ), np.array( [ndim] ), img.data ]
sock.send_multipart(data)

6.2: 受信側C++プログラム

こちらの受信プログラムも5.2と同様にZeroMQ経由で送信されてきた画像情報をOpenCV形式に交換した後にファイルに出力しています。

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "zmq.hpp"
#include <stdlib.h>
#include <stdio.h>

main( int argc, char *argv[]){
    int cnt=0;
    int rows, cols, type;
    cv::Mat img;
    void *data;

    // Open ZMQ Connection
    zmq::context_t context (1);
    zmq::socket_t socket (context, ZMQ_REP);
    socket.bind("tcp://*:5556");

    while(1){
        zmq::message_t rcv_msg;
        socket.recv(&rcv_msg, 0);

        // Receive Data from ZMQ
        switch(cnt){
         case 0:
            rows = *(int*)rcv_msg.data();
            break;
         case 1:
            cols = *(int*)rcv_msg.data();
            break;
         case 2:
            type = *(int*)rcv_msg.data();
            break;
         case 3:
            data = (void*)rcv_msg.data();
            printf("rows=%d, cols=%d type=%d\n", rows, cols, type);

            if (type == 2) {
                 img = cv::Mat(rows, cols, CV_8UC1, data);
            }else{
                 img = cv::Mat(rows, cols, CV_8UC3, data);
            }
            cv::imwrite("recv.bmp", img);
            break;
        }

        if( !rcv_msg.more() ){
            // No massage any more
            break;
        }

        cnt++;
    }

    return 0;
}

まとめ

本エントリでは、C++ <-> Python間におけるOpenCV形式の画像データの交換方法を解説してみました。OpenCV自体はC++で開発されているので、C++でプログラムを書いた方が最新の機能などを利用できるかもしれません。しかし、PythonOpenCVはNumpy形式でデータを扱うため、Numpyが提供する各種機能を活用する事でプログラムをシンプルに書ける場合もあるでしょう。

そこでOpenCVを活用したC++Pythonのプログラムを連携させたい場合が生じた場合などは、本エントリで例示した方法が役立てば幸いです。