この記事はWebAssembly Advent Calendar 2018の21日目です。wasm-bindgen
を使用して何かしてみたいと思っていたので、今回は以前Rustで実装した画像の差分を取るツールをwasm-bindgenを使用してnode_modulesとして使用できるようにしてみたいと思います。
移植元
これはもともと、go-diff-image(https://github.com/murooka/go-diff-image)というgolang製のツールをRustへポーティングしたものになります。
同じピクセル同士を比較して差分を出力するのではなく、githubのdiffのような感じで画像の差分を可視化するツールです。 以下のような比較画像を生成します。
成果物
手順
さっそくミニマムなプロジェクトを作ってみます。
- cargo.toml
[package] name = "node-lcs-img-diff" version = "0.1.0" authors = ["bokuweb"] edition = "2018" [lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2"
wasm-bindgen-cliが入ってない場合はインストールします。
rustup target add wasm32-unknown-unknown --toolchain nightly cargo +nightly install wasm-bindgen-cli
まずは1を加算する関数で試してみます。
- src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add_one(n: usize) -> usize {
n + 1
}
次にMakefileを用意しておきます。 wasm-bindgenはデフォルトブラウザ向けのコードを吐きますが、今回はnodejs向けに--nodejsつけて実行するようにします。
build: cargo +nightly build --target wasm32-unknown-unknown --release mkdir -p dist wasm-bindgen ./target/wasm32-unknown-unknown/release/node_lcs_img_diff.wasm --out-dir ./dist --nodejs
以下でビルド。
$ make build
- node_lcs_img_diff_bg.d.ts
- node_lcs_img_diff_bg.js
- node_lcs_img_diff_bg.wasm
- node_lcs_img_diff.d.ts
- node_lcs_img_diff.js
が吐かれる
- node_lcs_img_diff_bg.d.ts
/* tslint:disable */ export const memory: WebAssembly.Memory; export function add_one(a: number): number;
内部で使用される定義
- nodde-lcs_img_diff.d.ts
/* tslint:disable */ export function add_one(arg0: number): number;
公開関数の定義
- node_lcs_img_diff_bg.js
const path = require('path').join(__dirname, 'node_lcs_img_diff_bg.wasm'); const bytes = require('fs').readFileSync(path); let imports = {}; const wasmModule = new WebAssembly.Module(bytes); const wasmInstance = new WebAssembly.Instance(wasmModule, imports); module.exports = wasmInstance.exports;
wasmの読み込みからinstanciateまで。
- node_lcs_img_diff.js
/* tslint:disable */ var wasm; /** * @param {number} arg0 * @returns {number} */ module.exports.add_one = function(arg0) { return wasm.add_one(arg0); }; wasm = require('./node_lcs_img_diff_bg');
使用方法は以下のように呼ぶだけ。
- index.ts
import { add_one } from './dist/node_lcs_img_diff'; add_one(1); // -> 2
良さそうです。 後はせっせと移植していきます。 注意点としてはVecは返り値として返せないので、そのような場合JSONにしStringを返すことになりそうです。
細かい部分は省略しますが、Rust側は以下のようになりました。去年以下の記事を書きましたがwasm-bindgenのおかげで受け取る値も返す値もシンプルになっています。以前はArrayBufferのオフセットやデータの長さを受け取り、自分でバッファに変換する必要がありましたが、そのあたりの処理をwasm-bindgenが受け持ってくれているからですね。
どういうことをやっているかざっくり言うと、画層データを2枚受け取ってデコード。差分が発生した領域を計算して、元画像に緑/赤色をブレンドしたあとpngにエンコードして返しています。細かい処理は省略していますが、mainとなるdiff関数は以下のような感じです。
- lib.rs
#[wasm_bindgen] pub fn diff(before: &[u8], after: &[u8]) -> String { let mut before = load_from_memory(before).expect("Unable to load image from memory"); let mut after = load_from_memory(after).expect("Unable to load image from memory"); let encoded_before = create_encoded_rows(&before.raw_pixels(), before.dimensions().0 as usize); let encoded_after = create_encoded_rows(&after.raw_pixels(), after.dimensions().0 as usize); let result = lcs_diff::diff(&encoded_before, &encoded_after); let mut added: Vec<usize> = Vec::new(); let mut removed: Vec<usize> = Vec::new(); for d in result.iter() { match d { &lcs_diff::DiffResult::Added(ref a) => added.push(a.new_index.unwrap()), &lcs_diff::DiffResult::Removed(ref r) => removed.push(r.old_index.unwrap()), _ => (), } } create_marked_image(&mut after, (99, 195, 99), RATE, &added); create_marked_image(&mut before, (255, 119, 119), RATE, &removed); serde_json::to_string(&Result { after: to_png(&after), before: to_png(&before) }).unwrap() }
typescriptの型まで吐いてくれるので以下のように使用できます。
- index.ts
import { diff } from './dist/node_lcs_img_diff'; const [before, after] = await Promise.all([readFile("YOUR_IMAGE"), readFile("YOUR_IMAGE")]); JSON.parse(diff(before, after));
実際にはcliや画像の読み書き処理を追加しています。詳しくは以下を参照してみてください。
あとはbuildしてnpn publish
すれば完了です。
速度
自分はよくJavaScriptとwasmの速度比較を行うのですが、今回はJavaScript実装がないのでRust版と速度比較をしてお茶を濁しときます。
- wasm(node v10.11.0)
Benchmark #1: node . test/images/before.png test/images/after.png --dist test/expected Time (mean ± σ): 720.2 ms ± 57.1 ms [User: 1.150 s, System: 0.145 s] Range (min … max): 687.9 ms … 821.9 ms 5 runs
- Rust 1.31
Benchmark #1: lcs-image-diff test/images/before.png test/images/after.png aaa.png Time (mean ± σ): 29.3 ms ± 0.8 ms [User: 26.2 ms, System: 5.0 ms] Range (min … max): 28.2 ms … 32.4 ms 89 runs
ベンチマークにはhyperfineを使用しました。(MacBook Air (11-inch, Early 2015), 1.6 GHz Intel Core i5, 8 GB 1600 MHz DDR3)です。 結構な差がでましたね。どうもwasmの方はwasmファイルのリードからinstansiateまでで200msくらい持ってかれてるようです。diff関数は185msくらいですね。