1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Rust] WebAssemblyで笑い男

Last updated at Posted at 2021-10-29

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

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

この記事では顔検出モデルの読み込み,カメラからの画像取得と顔の検出について説明します.DOM操作やJavascriptへのエラーの伝播にwasm_bindgenを利用しています.

rustfaceモデルをwasmで利用する

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

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::JsValuejs_sys::Errorから作成して渡すことでエラー内容をJavascript側に伝播させることができます.reqwestと同じインターフェースをwasmで提供するreqwest_wasmクレートでgetリクエストのレスポンスをbytes::Byte型で取得します.rustface::readmodelstd::io::Readを実装している型を引数にとるので,readerメソッドでbytes::buf::Readerに変換して渡します.

webカメラから動画を取得し画像に変換,顔を検出する

webカメラから動画を取得してHtmlVideoElementのsrcにするのはJavascriptからWeb APIを利用するのとほとんど同じです.

# [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_idnavigatorは以下のように定義しています.

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::Promisewasm_bindgen_futures::JsFutureに変換してからawaitできるようになることです.

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

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ではImageDatadataUint8ClampedArrayを返しますが,wasm_bindgenではそこからVec<u8>が取得できます.
ここでdetect_facesは顔情報のVecを返す関数で以下で定義しています.

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::FaceInfoVecが取得できます.

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

感想

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?