non vorrei lavorare

昔はおもにプログラミングやガジェット系、今は?

Surface Goのカメラ映像をRustでコマンドプロンプトに表示した

この記事は、Rustその2 Advent Calendar 201820日目の記事と@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でOpenCVC++ 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を利用している。 今回OpenCVC++ APIをラップするC++のコードをこのcc crateを使うことでRustのプロジェクトの 一部としてシームレスに扱えるようになる。

これにより、cargo buildcargo runC++のコードもビルド出来るようになる。

github.com

具体的には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も作成してれる。

参考資料

qiita.com

akitsu-sanae.hatenablog.com

OpenCVC++ 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の時はOpenCVAPIで使われている型に対応する型を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;
}

成果物

www.youtube.com

github.com

あとがき

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

マイクロソフト Surface Go (128GB/8GB) MCZ-00014

関連記事

9年前の記事

4年前の記事

1年前の記事