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 に置いておきました。