この記事は、Rustその2 Advent Calendar 2018の20日目の記事と@kjunichiの2018年パーソナルアドベントカレンダーの20日目の記事を兼ねています。
背景
Surface Goをセットアップして開発環境を整備がほぼ終わり、 以前Rustで作成したWebカメラの映像をコマンドプロンプトに表示するコマンドを OpenCV 3.4.2を利用するように修正して試したら、
[ WARN:0] cvCreateFileCaptureWithPreference: backend MSMF doesn't support legacy API anymore. [ WARN:0] cvCreateFileCaptureWithPreference: backend DSHOW doesn't support legacy API anymore.
とエラー。
これはOpenCVのC APIのサポートがどうやらほんとうに切れて、C++のAPIを使わないとビデオキャプチャー が行えなくなってしまったということを訴えているエラーメッセージの模様。
RustからOpenCVを使うのに、C++のAPIを使わないとならない。
RustでOpenCVのC++ APIを使う
RustでCのAPIを扱うには、FFIが用意されているので、Rust側でCに対応する型を使って 呼び出したいAPIの関数を宣言する程度で容易に扱える。
しかし、C++にはマングリングの問題や、クラスを扱っているのでこれらに対応する必要があり、 Cと同様にFFIすることはかなり難しい。
RustでC++を扱うには
C++で対象のライブラリをラップしてCのAPIで叩ける口を用意し、これをRustからFFIで利用するというのが一般的な手法の模様。
まずは、RustでC++を扱うには、cc crateというcrateがあり、WindowsではC/C++コンパイラとしてcl.exeを利用している。 今回OpenCVのC++ APIをラップするC++のコードをこのcc crateを使うことでRustのプロジェクトの 一部としてシームレスに扱えるようになる。
これにより、cargo build
やcargo run
でC++のコードもビルド出来るようになる。
具体的にはcc crateを利用することでbuild.rsを以下のように直感的に記述できるようになる。
extern crate cc; fn main() { cc::Build::new() .cpp(true) .warnings(true) .file("src\\webcam\\cpp\\src\\webcam.cpp") .include("src\\webcam\\cpp\\include") .include("C:\\tools\\opencv\\build\\include") .compile("libwebcam.a"); }
libwebcam.aなる指定があるが、cl.exeな環境でもこれで動くし、 内部的にwebcam.libも作成してれる。
参考資料
OpenCVのC++ APIの雑なラップ
CではRustからFFIで直接アクセスできたので、 OpenCVで使われている一部の構造体はRustでもstructで 定義して内部構造にアクセスできるようにしていたが、 今回C++なので、Rust側ではすべて、Enum宣言することで 対応できた。
C++ APIの関数
以下のC++ APIで用意されている関数を適当にextern "C"
で包んで
RustのFFI経由で利用できるようにした。
- cv::imwrite
- cv::imshow
- cv::imencode
- cv::namedWindow
- cv::waitKey
- cv::destroyAllWindows
クラス/構造体へアクセス関数
Cの時はOpenCVのAPIで使われている型に対応する型をRust側に用意していたが、 今回はC++なので、そのまま対応する型をRustには用意できないので、Rust <-> C 間での やり取りする型を別に用意する必要がった。
cv::VideoCapture, cv::Matの扱い
以下ようなCvVideoCapture, Cv2Mat(CvMatはOpenCVのC APIで使用済み)なる構造体を用意して、 これらの構造体を介して、Rust<->C<->C++のやり取りを行った。
typedef struct CvVideoCapture_ { void *raw_ptr; } cvVideoCapture; CvVideoCapture *cv_video_capture(int camnum) { VideoCapture *cap; cap = new VideoCapture(); cap->open(camnum); CvVideoCapture *ccap; ccap = (CvVideoCapture*)malloc(sizeof(CvVideoCapture)); ccap->raw_ptr = cap; return ccap; } typedef struct Cv2Mat_ { void *raw_ptr; } Cv2Mat; Cv2Mat *cv_create_mat() { Cv2Mat *mat; mat = (Cv2Mat*)malloc(sizeof(Cv2Mat)); mat->raw_ptr = new Mat(); return mat; }
画像データの扱い
C++ならばcv::Mat型とvector型の配列で画像データを扱えるが、Cを使うので、 画像データを扱える独自の構造体を用意して対処した。
C++のvector型がCにあれば良いが、無いので、Webカメラからの画像を以下のようにC側で先頭アドレスとサイズを保持するメンバ をもつ構造体を宣言して、これを使うようにした。
typedef struct ImgBuffer_ { void *ptr; int size; void *raw; } ImgBuffer; ImgBuffer *cv_imencode(const char *ext, Cv2Mat *img, int *params) { vector<uchar> *buf; buf = new vector<uchar>; imencode(ext, *((Mat*)(img->raw_ptr)), *buf, vector<int>()); ImgBuffer *dst = new ImgBuffer(); dst->ptr = buf->data(); dst->size = buf->size(); dst->raw = buf; return dst; }
成果物
あとがき
Rustとタイトルにあるが、コマンドプロンプトへの画像データの表示はGoで別途自作したライブラリを使っていたりする。
画像をオンメモリーで扱う際にC++のVector型で取得できるが、これをRust側に渡して、Rust側で 不要になったら領域を解放したかったが、ストレートにこれを行う方法が分からなかったので、 一旦C++側でクラッシックな配列を確保して、これをRust側に戻し、この配列をRust側で不要になった タイミングで開放するように実装した。
Rustは当初はWindowsではMinGWがメインストリームだったが、MSVCに移り、今回使ったcc crateもcl.exeを シームレスに使えてWindowsプラットフォームでもC/C++との連携も問題なく扱えることが分かった。
マイクロソフト Surface Go (128GB/8GB) MCZ-00014
- 出版社/メーカー: マイクロソフト
- 発売日: 2018/08/28
- メディア: エレクトロニクス
- この商品を含むブログを見る
関連記事
- RustでWebカメラの映像をコマンドプロンプトに出すコマンドを作った
- Rustでコマンドラインアプリでキーが押されたかの判定しつつ、別の処理も進めるには
- Go言語でコマンドプロンプトに画像を表示するコマンドを作って、Vimの:terminalで表示させた
- RustでGoのライブラリを使うときのCargoのbuild.rsの書き方
- Surface Goが届いた