non vorrei lavorare

2020年度からの小学校プログラミング教育の必修化を親として迎えるブロガーの書く、子供との日常

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年前の記事