[Rust] WebAssemblyで笑い男

何番煎じか分かりませんが，今回はよくある笑い男マークを顔の部分に出現させるやつをWebAssemblyで実装していきます．rustで顔検出ができる[rustface](https://github.com/atomashpolskiy/rustface)というクレートを用いて，カメラによる画像の取得，顔検出，描画をwasmが担当します．以下がデモページです(カメラを利用しますが，サーバーは利用してないのでデータが送信されることはありません)．

[笑い男デモ(https://wasm-laughing-man-demo.vercel.app/)](https://wasm-laughing-man-demo.vercel.app/)リポジトリは[こちら](https://github.com/deepgreenAN/wasm_laughing_man_demo)

この記事では顔検出モデルの読み込み，カメラからの画像取得と顔の検出について説明します．DOM操作やJavascriptへのエラーの伝播に[wasm_bindgen](https://rustwasm.github.io/wasm-bindgen/)を利用しています．

## rustfaceモデルをwasmで利用する
rustfaceのexamplesにあるモデル読み込みの関数`create_detector`はファイルパスを引数にとるため，wasmでは利用できません．そこでクラウドに保存したファイルをhttp通信で読み込むことにします．モデルの読み込み部分の関数が以下となります．

```rust
pub async fn get_detecor(url: String) -> Result<Box<dyn rustface::Detector>, JsValue>{
    let res = reqwest_wasm::get(&url).await
        .map_err(|_|{JsValue::from(Error::new("detector model request error"))})?;
    let res_bytes = res.bytes().await
        .map_err(|_|{JsValue::from(Error::new("model requests cannot get bytes error"))})?;

    let model = rustface::read_model(res_bytes.reader())
        .map_err(|_|{JsValue::from(Error::new("cannot read model from bytes from request"))})?;
    Ok(rustface::create_detector_with_model(model))
}
```

関数の戻り値はエラーとして`wasm_bindgen::JsValue`を`js_sys::Error`から作成して渡すことでエラー内容をJavascript側に伝播させることができます．reqwestと同じインターフェースをwasmで提供する[reqwest_wasm](https://docs.rs/reqwest-wasm/0.11.0/reqwest_wasm/)クレートでgetリクエストのレスポンスを`bytes::Byte`型で取得します．`rustface::readmodel`は`std::io::Read`を実装している型を引数にとるので，`reader`メソッドで`bytes::buf::Reader`に変換して渡します．


## webカメラから動画を取得し画像に変換，顔を検出する
webカメラから動画を取得してHtmlVideoElementのsrcにするのはJavascriptからWeb APIを利用するのとほとんど同じです．

```rust
#[wasm_bindgen]
pub async fn init_video(app_opt: IAppOption) -> Result<(), JsValue>{
    let app_opt = AppOption::new(app_opt)?;
    let video: HtmlVideoElement = get_element_by_id::<HtmlVideoElement>(&app_opt.video_id)?;

    let mut media_constraints = web_sys::MediaStreamConstraints::new();
    media_constraints.audio(&JsValue::FALSE);
    media_constraints.video(&JsValue::TRUE);

    let stream_promise = navigator()?
        .media_devices()?
        .get_user_media_with_constraints(&media_constraints)?;
    
    let stream = JsFuture::from(stream_promise).await?
        .dyn_into::<web_sys::MediaStream>()?;

    video.set_src_object(Some(&stream));
    JsFuture::from(video.play()?).await?;
    Ok(())
}
```

ここで`get_element_by_id`と`navigator`は以下のように定義しています．

```rust
pub fn get_element_by_id<T: JsCast>(id: &str) -> Result<T, JsValue> {
    document()?
        .get_element_by_id(id)
        .ok_or(JsValue::from(Error::new("not found")))?
        .dyn_into::<T>().map_err(|_|{Error::new("convert error(dyn into)").into()})
}

pub fn navigator() -> Result<web_sys::Navigator, JsValue> {
    let navi = window()?
        .navigator();
    Ok(navi)
}
```

注意しなければならないのは，`js_sys::Promise`は`wasm_bindgen_futures::JsFuture`に変換してから`await`できるようになることです．

HtmlVideoElementから画像を取得するのもJavascriptと同じです．以下はvideo要素からグレースケール画像を作成しrustfaceのモデルで顔検出をしている部分です．
Web APIのcontextの`drawImage`の引数にHtmlVideoElementを指定し，contextの`getImageData`で`ImageData`として取得します．

```rust
self.make_image_context.draw_image_with_html_video_element_and_dw_and_dh(
    &self.stream_video,
    0.0,
    0.0,
    self.image_width as f64,
    self.image_height as f64
)?;

let image_vec = self.make_image_context.get_image_data(
    0.0,
    0.0,
    self.image_width as f64,
    self.image_height as f64
)?
.data()
.0;

let gray_vec = convert_rgba_to_luma_v2(image_vec, self.image_width, self.image_height);
let faces = detect_faces(&mut *self.detector, &gray_vec, self.image_width, self.image_height);
```

Web APIでは`ImageData`の`data`は`Uint8ClampedArray`を返しますが，wasm_bindgenではそこから`Vec<u8>`が取得できます．
ここで`detect_faces`は顔情報の`Vec`を返す関数で以下で定義しています．


```rust
use rustface::{Detector, FaceInfo, ImageData};

pub fn detect_faces(detector: &mut dyn Detector, gray_vec:&Vec<u8>, width: u32, height: u32) -> Vec<FaceInfo> {
    let mut image = ImageData::new(gray_vec, width, height);
    let faces = detector.detect(&mut image);
    faces
}
```

上の`ImageData`はWeb APIのものでなく，`rustface::ImageData`であることに注意してください．以上で，rustfaceのexamplesにあるような`rustface::FaceInfo`の`Vec`が取得できます．

デモページでは取得した矩形情報を簡単にトラッキングしimg要素の位置を変更して顔にかぶせています．


## 感想
ローカルの環境とwasmで同じインターフェースで機械学習モデルを利用できるのは分かりやすくていいですね．wasm_bindgenでDOM操作をするとどうしてもwasmファイルのサイズが大きくなってしまいますが，Web APIを直にwasmから呼べるようになることも計画されているようなので，今後はもっと使いやすくなると期待できますね．今回作成したデモは自分の環境では10～20fps程度でしたが，元のSeetaFaceがローカル環境では55fpsでるようなので，wasmで並列処理ができるようになったりrustfaceがSIMD対応すればもっと高速になると思います．