catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

カメラアプリ開発: tauriとopencvの組み合わせ

tauriでwebカメラの映像を加工する

少しずつ目的の形に近づいてくると楽しいですね。かたりぃなです。 さて、今回はtauriでちょっと凝ったことをしてみます。

webカメラから映像を取得し、それを加工してみます。

方法は色々あると思いますが、jsとrust以下のような役割分担にして作ってみます

  • フロント(js)
    • Webカメラの映像をプレビュー表示する。
    • ボタンが押されたら、キャプチャして静止画を取得して表示する
  • バック(Rust)
    • フロントから渡された静止画を加工して返す

いわゆるカメラアプリですね。完成形はこうなりました。

フロントエンドの実装

基本的に https://developer.mozilla.org/ja/docs/Web/API/Media_Capture_and_Streams_API/Taking_still_photos

のコードをそのままコピペすれば動きます。

画像を表示する前にrustに画像データを受け渡して加工し、それを表示するように変更したので、そこだけ説明します。

  async function takepicture() {
    const context = canvas.getContext("2d");
    if (width && height) {
      canvas.width = width;
      canvas.height = height;
      context.drawImage(video, 0, 0, width, height);
  
      const data = canvas.toDataURL("image/png");
      const b64 = data.replace(/^.*,/, ''); //mimeタイプとかを取り除く
      const b64_mod = await invoke("img_receive", { "name":"test", "img":b64 });

      // rust側で画像処理した結果をセットする
      const mod_data = "data:image/png;base64," + b64_mod;
      photo.setAttribute("src", mod_data);
    } else {
      clearphoto();
    }
  }

作業用のキャンバスの画像をpng形式で取得します。 そのままだとmimeタイプとかついてるので、それを除去してからrustに渡しています。 具体的にはこういうもの( data:image/png;base64, ) が付与されているので除去します。

(生成されたテンプレートコードからの改造なので、invokeの第一引数に余計なものがついていますが、そこはご愛敬。)

      const data = canvas.toDataURL("image/png");
      const b64 = data.replace(/^.*,/, ''); //mimeタイプとかを取り除く
      const b64_mod = await invoke("img_receive", { "name":"test", "img":b64 });

rust側で加工して返してくれたデータにmimeタイプを付与しなおしてから、セットすることで、画像が加工されていること以外は元通りです。

png形式で返ってくる前提になっていますが、一旦良しとしましょう。

      const mod_data = "data:image/png;base64," + b64_mod;
      photo.setAttribute("src", mod_data);

rustの実装

簡単なコードなので、全体を示します。

use simple_base64::{Engine as _, alphabet, engine::{self, general_purpose}};
use opencv::*;
use opencv::core::*;
use opencv::imgproc::*;
use opencv::types::VectorOfu8;

fn img_receive(name: &str, img: &str) -> String {
    // imgをbase64デコード,pngデコードして、ピクセルデータ列(cv::mat)をつくる。
    let bytes = general_purpose::STANDARD.decode(img).unwrap();
    let original = imgcodecs::imdecode(&VectorOfu8::from_iter(bytes), imgcodecs::IMREAD_COLOR).unwrap();

    // ここで色々処理できるので、適当に処理する
    let mut edge_img = original.clone();
    let result_find_edge = canny(&original, &mut edge_img, 100.0, 100.0, 3, false);

    // 呼び出し元へ返すために、png形式でエンコード,base64エンコードする
    let mut result_png = Vector::<u8>::new();
    let _ = imgcodecs::imencode(".png", &edge_img, &mut result_png, &Vector::new());
    general_purpose::STANDARD.encode(&result_png);
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, img_receive])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

追加したライブラリはこんな感じです。

opencv = "0.88.8"
simple-base64 = "*"

rustコードの説明

まず、jsから渡されてくる画像データはbase64エンコードされたpngデータとします。 なので、base64デコードしてpngデコードすれば生ピクセルデータが得られます。

同様にして、呼び出し元で表示可能な画像形式に戻すにはこの逆を行えばいいわけです。 デコードとエンコードそれぞれ対になっています。

base64エンコードでやるの冗長な気がするのですが、tauriでのjsとrust間でのデータ交換というかjsでのバイナリデータの取り扱いののベストプラクティスがわからないので、一旦こんな感じです。

デコード

    let bytes = general_purpose::STANDARD.decode(img).unwrap();
    let original = imgcodecs::imdecode(&VectorOfu8::from_iter(bytes), imgcodecs::IMREAD_COLOR).unwrap();

エンコード

    let mut result_png = Vector::<u8>::new();
    let _ = imgcodecs::imencode(".png", &edge_img, &mut result_png, &Vector::new());
    general_purpose::STANDARD.encode(&result_png);

で、生のピクセルデータ列が得られればこっちのものなので、適当に画像加工をしてみます。 エッジ画像にするとかが変化がわかりやすくていいでしょう。

    // ここで色々処理できるので、適当に処理する
    let mut edge_img = original.clone();
    let result_find_edge = canny(&original, &mut edge_img, 100.0, 100.0, 3, false);

cv::Mat形式にできたならばデバッグも捗ります。

Matの中身見るにはこうします。

    println!("original imgsize = {:?}", original );

結果はこんな感じになります

original imgsize = Mat { type: "CV_8UC3", flags: 1124024336, channels: 3, depth: "CV_8U", dims: 2, size: Size_ { width: 640, height: 480 }, rows: 480, cols: 640, elem_size: 3, elem_size1: 1, total: 307200, is_continuous: true, is_submatrix: false }

ファイルに保存するのも簡単です。

imgcodecs::imwrite("../test.png", &original, &Vector::new() );

opencvすごいですね。

それでは今回はこれくらいで。