luggage baggage

Machine learning, data analysis, web technologies and things around me.

cv::Mat を protocol buffer に格納する (C++ で OpenCV & protobuf)

この記事では、C++ で OpenCV を使いつつ、cv::Mat に格納されたバイナリデータを protocol buffer に格納しシリアライズする方法を書いていきます。画像認識系のアルゴリズムを作る際、カメラデバイス上では C++ を使ってフレームを取得し、サーバ上の Python 機械学習アルゴリズムに対して gRPC 通信でデータを送る、みたいなシチュエーションで使った方法となります。

検証環境

  • docker on MacOS: Version 18.06.1-ce-mac73 (26764)
  • Ubuntu 16.04 image: Linux 056010041c82 4.9.93-linuxkit-aufs #1 SMP Wed Jun 6 16:55:56 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
  • gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10)

g++ のバージョンがやや古く C++17 等は使わない想定ですが、特に問題はありませんでした。

全体の流れ

次のような流れで作業をしていきます。本論は 2, 3, 4 です。
1. gRPC/protobuf と OpenCV のビルド
2. protocol buffer の定義(メッセージの定義)
3. OpenCV を介した画像の読み出しと protocol buffer に保存可能な形式への変換、データの格納
4. protocol buffer のシリアライズ
5. (例として)Python でのデシリアライズ

gRPC/protobuf と OpenCV のビルド

まず必要なライブラリを入れておきます。protocol buffer と OpenCV の公式に従って入れるだけなので特に問題はないかと思われますが、一応私が使ったセットアップスクリプトを GitHub に置いておいた*1ので、必要な方はご利用ください。ただし、このスクリプトでは gRPC/protobuf がグローバルにインストールされるので、環境を汚したくない方は適宜変更してください。

protocol buffer の定義(メッセージの定義)

protocol buffer は、Google が開発しているデータフォーマットで、データ構造を便利に構造化する仕組みが備わっており、C++, Python, Go といった言語を横断して使用できる点で利便性が高いものです。

今回は、「画像」を表現する protocol buffer を一つ用意し、これに C++ 側からデータを格納した上で、Python から読み出すということをやってみます。事例として使う protocol buffer は下記のとおり。

// image.proto

syntax = "proto3";

package image;

message Image {
    uint32 width = 1;
    uint32 height = 2;
    uint32 channel = 3;
    bytes data = 4;
}

大事なのは message の箇所で、これが今回の主役となるデータ構造です。属性を4つ持っており、特に bytes 型の data に画像のバイナリデータを入れていくことになります。

前述の通り、protocol buffer それ自体は言語非依存に定義できますが、実際には何らかの言語で書かれたプログラムから読み出す必要があり、その際には当然ながら言語依存の型を割り当ててデータを読み取ります*2。今回特に覚えておく必要があるのは、protocol buffer の bytes 型は C++ では string が対応する*3ということですね。この事実を後ほど使います。

OpenCV を介した画像の読み出しと protocol buffer に保存可能な形式への変換、データの格納

C++ で OpenCV を使う際に中心となるのは cv::Mat ですが、そのバイナリデータには cv::Mat.data でアクセスできます。C++ において上で定義した Image メッセージに画像データを格納する際には、このデータを string 型に変換しておかねばなりません。そこで、次のようにします。

cv::Mat img = cv::imread(argv[1], cv::IMREAD_UNCHANGED);
int width = img.cols;
int height = img.rows;
int channel = img.channels();
int size = width * height * channel;
std::string data_str(reinterpret_cast<char const*>(img.data), size);

reinterpret_cast の使用やむなし、という局面です。第二引数の size を指定しない場合、バイナリデータが必要十分に変換できない現象が見られたので、このように書いています。ここまでくれば、あとはデータを格納するだけです。

// Store into protobuf
Image image;
image.set_width(width);
image.set_height(height);
image.set_channel(channel);
image.set_data(data_str);

protocol buffer のシリアライズ

これは簡単で、メソッドを一つ呼ぶだけで完了です。

std::fstream outpb("out.pb", std::ios::out | std::ios::trunc | std::ios::binary);
if (!image.SerializeToOstream(&outpb)) {
  std::cerr << "Failed to serialize to .pb" << std::endl;
  return -1;
}

正常にシリアライズできた場合、out.pb というバイナリファイルが一つ作られているはずです。

Python でのデシリアライズ

シリアライズされた .pb ファイルは様々な言語から読み取ることができますが、ここでは例として Python から読んでみましょう。

まず必要なライブラリの準備として、次のコマンドを実行しておきます。

python3 -m pip install grpcio grpcio-tools googleapis-common-protos

次に、定義済みのメッセージを読み出せるように、image.proto を Python 用にコンパイルします。

protoc -I./protos --python_out=. image.proto

こうすると image_pb2.py ができていると思います。中を見ると

# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: image.proto

と書かれており、実際、人間向けには書かれていない自動生成コードが格納されています。

先程シリアライズした .pb ファイルを読むには、基本的には次のようにするだけですね。

import numpy as np
import image_pb2

with open("out.pb", "rb") as pb:
    image = image_pb2.Image()
    image.ParseFromString(pb.read())
w = image.width
h = image.height
c = image.channel
data = image.data
img_np = np.frombuffer(data, np.uint8)
img_np = img_np.reshape(h, w, c)

こうして作成した img_np が、Python 側で処理しやすい形式の画像データということになります。

まとめ

C++ にて OpenCV の主要なオブジェクトである cv::Mat からデータを読み出し、protocol buffer 形式でシリアライズする方法について書きました。

うちの会社では gRPC がモジュール間の通信規格として使われていることもあり、protocol buffer が基本的な言葉として使われています。今回の記事内容の発展形としては、protobuf に格納した画像データを gRPC の stub に入れて、リモートの Python 機械学習アルゴリズムに食わせた後に推論結果を json 形式で response protobuf に格納し、C++ サイドで読み出す、等がありえますね。

世の中的にはマイクロサービスが主流となってきているので、gRPC/protobuf は一つの選択肢になり得るかと思います。この記事が多少でも役に立てば嬉しいですね。

gRPC/protobuf, OpenCV ビルド用の Makefile 含む一連のコードは、blog/cv_mat_to_protobuf at master · dlnp2/blog · GitHub に置いておきました。