何番煎じか分かりませんが,今回はよくある笑い男マークを顔の部分に出現させるやつを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::JsValue
をjs_sys::Error
から作成して渡すことでエラー内容をJavascript側に伝播させることができます.reqwestと同じインターフェースをwasmで提供するreqwest_wasmクレートでgetリクエストのレスポンスをbytes::Byte
型で取得します.rustface::readmodel
はstd::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_id
とnavigator
は以下のように定義しています.
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
として取得します.
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
を返す関数で以下で定義しています.
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対応すればもっと高速になると思います.