FutureStandard MAKERS

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

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のプログラムを連携させたい場合が生じた場合などは、本エントリで例示した方法が役立てば幸いです。