画像の背景を削除する GrabCutter を作りました。
名前の通り GrabCut アルゴリズムを使っています (安直)。
前景と後景をアノテーションできるようにしておいたので、
削除と復元の微調整しやすいのが利点です。
こんな感じの背景透過画像がサクッと作れます。
割と大きめの画像でも 初回実行は 1秒以内、微調整は一瞬という感じです。
とはいえ巨大な画像はさすがに重いので、ROI を作って部分適用したほうが良いかも知れません。
他にも共有メモリで分散処理はどんなアルゴリズムでも検討したいところですが、今回はそこまで作ってないです。
AI を使わない時にどれくらい精度が出るのかの勉強用で作りましたが、
AI なしでも割と良い精度です。せいぜい 2-3回の微調整で十分な結果が得られるので、
これはこれでアリじゃないかなあ。
グラフカットの試行回数がパラメータで設定できるのですが、まったく必要ない気がします。
試行回数が増えても遅くなるだけなので、1回で固定して微調整で修正したほうが良いです。
alphamat をさらに考慮する方法などもあるっぽいことに後から気付きましたが、
ものすごい時間が掛かるみたいなので、現状はこれでいいかなと。
marmooo's blog
2025年3月19日水曜日
2025年2月11日火曜日
MIDI 再生ライブラリ Midy を作った
MIDI 再生ライブラリを前々から欲しかったので、Midy というライブラリを作りました。
ひとまず GM1 の再生に必要な機能は実装しているつもりです。
このライブラリを作る前は FluidSynth を wasm にするのが無難ではあったと思いますが、
wasm サイズが大き過ぎたり、確認が甘いだけかも知れませんが動作に納得がいかないところがありました。
うまく使いこなせなかったので、再生負荷が低く、ライブラリのサイズが小さく、拡張性の高い実装が欲しかったです。
Web での利用を想定して SF3 形式に対応していることは大前提です。
実際にこの構成で基本的な部分はあっさり完成しました。 しかしメインスレッドで実行すると再生負荷が高い問題にすぐ直面しました。 何も考えずにスケジューリングするだけではすぐにラグが発生します。 再生負荷が高いのは短時間にイベント処理を大量に行うからと予想できたので、 setInterval() を使って対象となる音符を探しながら、 AudioWorklet 上でサンプリング単位で処理を行うことで、負荷を低減する実装をしました。 まあまあ動くようになったところで他のライブラリを改めて見てみると、 AudioWorklet を使う実装はほとんどないことを知りました。 AudioWorklet を使う方法もアリとは思いますが、 たいていは AudioBufferSourceNode で十分のようです。
AudioBufferSourceNode を使った MIDI 再生は以下が参考にはなるのですが、 GUI の実装と基盤ライブラリの実装が絡み合っていたり、バグがあったり、中身がよくわからないところが多いので、やはり自作することにしました。 あとゼロから実装していると「その実装じゃ動かないんだわ」にたくさん気付かされたので、やはり採用はできなかったです。 でもまあ私の実装もまだまだ間違いがたくさんあると思うので、じょじょに直していきます。 私が欲しいのはきちんとした再生ができるライブラリです。 他にも色々な実装や fork がありますが、中身がよくわからないところが多いので参考にしてないです。 ちなみに再生負荷を下げるだけなら js-synthesizer が安定しています。 自作はより良いものを作るためです。 他には FluidSynth や Timidity のコードは当然ながら参考にはなるのでしょうが、苦手過ぎてあまり読めていません。 やっぱ JavaScript のコードってめちゃくちゃ読みやすいし、メンテしやすいんだよなあ。
サウンドフォントで一番謎だったのは音源のサンプリングデータの扱いです。 SoundFont 2.04 の 24bit とかどうやって扱うんでしょうかねこれは…。 そんな型はないので 32bit にするのか、専用の型を用意するのか。 サンプリングデータは通常は 16bit で扱うんだと思いますが、 SF3 サポートや 24bit 対応を考えると Uint8Array が正解かな。 ただこれだとサウンドフォントの仕様を知ってないと扱えなくなる欠点はあります。 とはいえどうせ以下のサウンドフォントの仕様をよく読んで作らないと動かないので、あまり差はない気もします。 しかし読んでもわからないことのほうが多いのがまた問題ですが…。
タイマー周りは検証が面倒臭かったですが、実装面で苦労したのは Promse 周りです。 Promise は本当に難しくて、たとえば以下の 1行だけでバグります。
何日か格闘して再生処理をフルスクラッチで書いたら、JavaScript でも十分早いことがわかりました。 途中で気付きましたが、音声処理は大部分がブラウザ上でネイティブ動作するので、 wasm にしたからといって速度が上がる訳ではないと思います。 速度が上がるのはせいぜいバッファー生成だけですが、MIDI では事前生成がほとんどです。 再生はネイティブのタイマーで処理できるので、 JavaScript 側では負荷の小さいスケジューリングで渡せば十分と思います。 より高速な実装を目指す場合、AudioWorklet + wasm でバッファー処理すれば、 リアルタイム性では勝てる可能性があるかも知れませんが、wasm よりネイティブのほうが早いので期待はできないと思います。
ちなみに Tone.js のコードもかなり眺めたのですが、私の実装と基本的には一緒だと思います。 ただ核心部分では演奏位置のメモリ管理、タイムラインの検索処理、 タイムラインのループ処理あたりの実装が非効率です。 エフェクトとかもチェインを細かく設定しないといけないので、 なんやかんや自作が最強な気がしています。
現状の課題としてはリバーブが重いことを認識しています。 複雑な実装はしていないので、単純にリアルタイム処理が重いということだと思います。 MIDI はリアルタイム処理すると、構想段階の実装でもノート再生でさえ遅いことがわかっているので、いかにリアルタイム処理をなくすかが肝となりました。 リバーブの重さも、回避するにはなるべく様々な処理をオフラインでレンダリングしておいて、リアルタイム処理を減らす必要があると思います。 いかにリアルタイム処理をなくすかが肝だからこそ、AudioWorklet を使うのはあまり良い手段ではなさそう。 MIDI はノート・チャネル・マスターと 3層に分かれて音声処理をしていているので、層ごとに最適化が必要そうです。 GM2 だと初期状態でリバーブが掛かっていることを想定しているように見える のですが、これは現状だとちょっと厳しい。 今のところ GM1 対応しかしてないので、リバーブはオフにしています。 リバーブがなくても大量に音符があると稀に重いので、最適化はまだまだ必要です。
あとエフェクトが掛かったときの細かなサウンドフォントの扱いなどもまだまだ完全には実装していません。 やることが山ほどあり過ぎるので、ひとまず基本部分が再生できるようになったところでリリースしたという状態です。 完全に動くまで待つとエターなりそうだし。 まずは GM2 をフルサポートし、将来的には GS/XG の一部にも対応しようとは思っています。 実は GM2 をフルサポートしているライブラリってないんじゃないかな。 FluidSynth とかも対応してないです (SoftPedal を実装しているときに気付いた)。 GS/XG に対応し始めると 1流シンセサイザーという感じで、特にエフェクトのチェインが難しいです。 まずはそこを作る前にバグを潰していく必要があります。
Midy をリリースするためは、大量のライブラリに手を入れる必要があって、割と時間が掛かりました。 まだ色々と実装が足りないところがあると思いますが、ボチボチ直していきます。 だいたい動くものとして first commit をするまでが、検証が多くて一番大変。 ここからは平凡に実装とバグ潰しです。 現状はデモで動くものを頑張って探すレベルなので、もっと実装の精度を高めていく必要があります。
構想段階の話
上記の条件を満たすライブラリの開発は前々から検討していて、 最初はサウンドフォントのパーサを実装してサウンドフォントを読み込み、 @tonejs/midi で MIDI をパースし、Tone.js で再生処理を実装することを考えていました。 Tone.js を使うとコントロールチェンジの実装が簡単かなと最初は思っていました。実際にこの構成で基本的な部分はあっさり完成しました。 しかしメインスレッドで実行すると再生負荷が高い問題にすぐ直面しました。 何も考えずにスケジューリングするだけではすぐにラグが発生します。 再生負荷が高いのは短時間にイベント処理を大量に行うからと予想できたので、 setInterval() を使って対象となる音符を探しながら、 AudioWorklet 上でサンプリング単位で処理を行うことで、負荷を低減する実装をしました。 まあまあ動くようになったところで他のライブラリを改めて見てみると、 AudioWorklet を使う実装はほとんどないことを知りました。 AudioWorklet を使う方法もアリとは思いますが、 たいていは AudioBufferSourceNode で十分のようです。
AudioBufferSourceNode を使った MIDI 再生は以下が参考にはなるのですが、 GUI の実装と基盤ライブラリの実装が絡み合っていたり、バグがあったり、中身がよくわからないところが多いので、やはり自作することにしました。 あとゼロから実装していると「その実装じゃ動かないんだわ」にたくさん気付かされたので、やはり採用はできなかったです。 でもまあ私の実装もまだまだ間違いがたくさんあると思うので、じょじょに直していきます。 私が欲しいのはきちんとした再生ができるライブラリです。 他にも色々な実装や fork がありますが、中身がよくわからないところが多いので参考にしてないです。 ちなみに再生負荷を下げるだけなら js-synthesizer が安定しています。 自作はより良いものを作るためです。 他には FluidSynth や Timidity のコードは当然ながら参考にはなるのでしょうが、苦手過ぎてあまり読めていません。 やっぱ JavaScript のコードってめちゃくちゃ読みやすいし、メンテしやすいんだよなあ。
前準備 (@marmooo/soundfont-parser を作った)
きちんとした再生ライブラリを作るためには色々前準備も必要でした。 まず @tonejs/midi だと SysEx 命令に対応できないことに気付いたので midi-file を使って MIDI ファイルをパースするようにしました。 サウンドフォントのパーサは ryohey/sf2synth.js のコードが綺麗だったのでベースとしながら、SF3 サポートを加えました。 名前はせっかくなので @marmooo/soundfont-parser にしました。 SF3 サポート以外にも、実装途中で気付いた問題として、パーカッション対応や、軽微な処理速度の向上、規格準拠率の向上、strict mode への対応などもしています。サウンドフォントで一番謎だったのは音源のサンプリングデータの扱いです。 SoundFont 2.04 の 24bit とかどうやって扱うんでしょうかねこれは…。 そんな型はないので 32bit にするのか、専用の型を用意するのか。 サンプリングデータは通常は 16bit で扱うんだと思いますが、 SF3 サポートや 24bit 対応を考えると Uint8Array が正解かな。 ただこれだとサウンドフォントの仕様を知ってないと扱えなくなる欠点はあります。 とはいえどうせ以下のサウンドフォントの仕様をよく読んで作らないと動かないので、あまり差はない気もします。 しかし読んでもわからないことのほうが多いのがまた問題ですが…。
再生処理の実装
SF3 形式が読み込めるようになった後は、再生処理を作りました。 最初は setInterval() で作っていたのですが、処理が被ると酷いことになることがわかったので、setTimeout() にしました。 しかし setTimeout() もバックグラウンド再生の課題があるとわかったので AudioBufferSourceNode を使ったタイマーを作っています。 このへんの話は、JavaScript で使えるタイマーのベンチマークを作った が関連します。タイマー周りは検証が面倒臭かったですが、実装面で苦労したのは Promse 周りです。 Promise は本当に難しくて、たとえば以下の 1行だけでバグります。
const promise = new Promise(...);
return promise;
これは以下のように書かないといけません。
return new Promise(...);
音声処理みたいにタイミングがシビアだと、非同期でない部分をすべて Promise で囲わないとバグります。
1ms くらいのタイムラグの話ですが、音声処理だとこれが致命的な問題になります。
他のアプリで山のように間違ってそうだから直していかないとなあ。
何日か格闘して再生処理をフルスクラッチで書いたら、JavaScript でも十分早いことがわかりました。 途中で気付きましたが、音声処理は大部分がブラウザ上でネイティブ動作するので、 wasm にしたからといって速度が上がる訳ではないと思います。 速度が上がるのはせいぜいバッファー生成だけですが、MIDI では事前生成がほとんどです。 再生はネイティブのタイマーで処理できるので、 JavaScript 側では負荷の小さいスケジューリングで渡せば十分と思います。 より高速な実装を目指す場合、AudioWorklet + wasm でバッファー処理すれば、 リアルタイム性では勝てる可能性があるかも知れませんが、wasm よりネイティブのほうが早いので期待はできないと思います。
ちなみに Tone.js のコードもかなり眺めたのですが、私の実装と基本的には一緒だと思います。 ただ核心部分では演奏位置のメモリ管理、タイムラインの検索処理、 タイムラインのループ処理あたりの実装が非効率です。 エフェクトとかもチェインを細かく設定しないといけないので、 なんやかんや自作が最強な気がしています。
MIDI Event の実装
根幹部分が完成したらあとはひたすら細かな実装です。 コントロールチェンジや SysEx 命令などの MIDI イベントを地道に実装します。 実装しなければいけないことを、以下で確認しながら作りました。 ただ非常にドキュメントが長く真面目に読んでいるといつまで経っても実装が終わらないため、正直あまりきちんとは読んでない気がします…。- General MIDI - Wikipedia (ja)
- General MIDI - Wikipedia (en)
- General MIDI Level 2 - Wikipedia (en)
- MIDI 1.0 Control Change Messages (Data Bytes) – MIDI.org
- GM2(GENERAL MIDI Level 2) | DTM Solutions
- MIDI:コントロール番号一覧 | DTM Solutions
- ソフトウェアプログラマーのためのMIDI CCリスト - Fwmiの日記 (良い)
- MIDI Registered Parameter Number (RPN) | RecordingBlogs
- RPN/NRPN 一覧表
- MIDI:RPNとNRPN | DTM Solutions
- サクラの為のMIDI規格 -System Exclusive Message
- MIDI 1.0 Universal System Exclusive Messages – MIDI.org
- Global Parameter Control – MIDI.org
- RP/CA (すべての規格書)
- MIDI 1.0 規格書 (300ページあって熟読は無理→152ページが重要)
- General MIDI Lite
- GENERAL MIDI Level 2 Recommended Practice (RP024)
TODO
先に述べたエフェクトは GM2 の話で、既に結構実装はできているのですが、完璧ではない状態です。 GM2 をフルサポートをしようとすると、Global Parameter Control でリバーブやコーラスの推奨要件が細かく規定されているので、実装の精度が上がっていくと思います。 たぶん一番よくわからなかったコーラス周りの仕様が安定するはずです。現状の課題としてはリバーブが重いことを認識しています。 複雑な実装はしていないので、単純にリアルタイム処理が重いということだと思います。 MIDI はリアルタイム処理すると、構想段階の実装でもノート再生でさえ遅いことがわかっているので、いかにリアルタイム処理をなくすかが肝となりました。 リバーブの重さも、回避するにはなるべく様々な処理をオフラインでレンダリングしておいて、リアルタイム処理を減らす必要があると思います。 いかにリアルタイム処理をなくすかが肝だからこそ、AudioWorklet を使うのはあまり良い手段ではなさそう。 MIDI はノート・チャネル・マスターと 3層に分かれて音声処理をしていているので、層ごとに最適化が必要そうです。 GM2 だと初期状態でリバーブが掛かっていることを想定しているように見える のですが、これは現状だとちょっと厳しい。 今のところ GM1 対応しかしてないので、リバーブはオフにしています。 リバーブがなくても大量に音符があると稀に重いので、最適化はまだまだ必要です。
あとエフェクトが掛かったときの細かなサウンドフォントの扱いなどもまだまだ完全には実装していません。 やることが山ほどあり過ぎるので、ひとまず基本部分が再生できるようになったところでリリースしたという状態です。 完全に動くまで待つとエターなりそうだし。 まずは GM2 をフルサポートし、将来的には GS/XG の一部にも対応しようとは思っています。 実は GM2 をフルサポートしているライブラリってないんじゃないかな。 FluidSynth とかも対応してないです (SoftPedal を実装しているときに気付いた)。 GS/XG に対応し始めると 1流シンセサイザーという感じで、特にエフェクトのチェインが難しいです。 まずはそこを作る前にバグを潰していく必要があります。
GUI
本格的な GUI に関してはこれから作りたい…と言いたいのは山々ですが、現実的にはそうも言ってられません。 なぜかというと WebAudio は現状ブラウザ上でしか確認できないので、テストをするためには GUI が必要だからです。 仕方ないので簡易的な再生用 UI を作ってデバッグすることにしました。 どうせそのうち作ることになる MIDI 再生 GUI を簡単に作れる @marmooo/midi-player を作りました。 @marmooo/midi-player を作る時に基本的な操作で必要な API の確認やテストをしているので、 そんなにバグはないんじゃないかと思います。 やはり GUI 化すると細かな API のズレが出がちですが、自分で作っているからこそバグが起きないようにできたのが大きいです。 基本的な再生部分が完成すれば、もう Promise などでハマる可能性がないので、あとはひたすら実装するだけです。 @marmooo/midi-player はもっと高機能にしていくつもりですが、 あまり高機能にし過ぎるよりはライブラリを分けたほうが良いかも知れません。Midy をリリースするためは、大量のライブラリに手を入れる必要があって、割と時間が掛かりました。 まだ色々と実装が足りないところがあると思いますが、ボチボチ直していきます。 だいたい動くものとして first commit をするまでが、検証が多くて一番大変。 ここからは平凡に実装とバグ潰しです。 現状はデモで動くものを頑張って探すレベルなので、もっと実装の精度を高めていく必要があります。
2025年1月29日水曜日
JavaScript で使えるタイマーのベンチマークを作った
JavaScript で使えるタイマーのベンチマーク js-timer-benchmark を作りました。
タイマー関数として setTimeout, setInterval がよく知られていて、
画像処理などをやっている人はこれに加えて requestAnimationFrame も知っていることでしょう。
ただ音声処理の実装をしていると、これらではうまくいかないことに気付いたので、
あまり知られていない他のタイマー機能を紹介します。
それは AudioBufferSourceNode, OscillatorNode, ConstantSourceNode を使う方法です。 これらは音声データや正弦波、定数値を再生するためのノードですが、 これらには s秒後に再生を開始し、t秒後に再生を止める機能が備わっています。
上記を用いてタイマー機能を作成すると、方法によって様々な精度の違いが生まれます。 精度を検証するためのアプリ js-timer-benchmark はこちら。 Audio 関連のコードは Node/Deno では動かないのでブラウザ上で確認するしかありません。 まずはフォアグラウンドで初回起動時の結果がこちら。
フォアグラウンドで2回目以降時の結果がこちら。
バッググラウンドで初回起動時の結果がこちら。
バックグラウンドで2回目以降時の結果がこちら。
まとめると、requestAnimationFrame はバックグラウンドでは停止しているようなもので使いものになりません。 setTimeout, setInterval はバックグラウンドでは非常に遅くなります。 AudioBufferSourceNode, OscillatorNode, ConstantSourceNode は同じ速度で実行できます。 音楽などはバックグラウンドで再生速度が変わったら困るので、それを考慮しているのだと思います。 AudioBufferSourceNode は初回起動時になぜか遅いですが、それ以降は高速です。 つまり、音声処理のタイマーや、バックグラウンドでミリ秒単位の高精度タイマーが必要なら、 AudioBufferSourceNode を使いましょうという結論になりそうです。 とはいえ setInterval より精度が低いのは意外でした。
ちなみにウィンドウが非アクティブのときにどのように動作するかは、Window: setTimeout() メソッド - Web API | MDN にまとまっています。 setTimeout/setInterval はベンチマークの結果通り、1秒単位で処理するようです。 記述を読む限り AudioContext を使った手法はおそらく処理速度が落ちたりはしないですが、特にそれが定まっている訳でもないみたいです。
それは AudioBufferSourceNode, OscillatorNode, ConstantSourceNode を使う方法です。 これらは音声データや正弦波、定数値を再生するためのノードですが、 これらには s秒後に再生を開始し、t秒後に再生を止める機能が備わっています。
上記を用いてタイマー機能を作成すると、方法によって様々な精度の違いが生まれます。 精度を検証するためのアプリ js-timer-benchmark はこちら。 Audio 関連のコードは Node/Deno では動かないのでブラウザ上で確認するしかありません。 まずはフォアグラウンドで初回起動時の結果がこちら。
method | 1sec error | 2sec error |
---|---|---|
setTimeout | 0.9ms | 0.6ms |
setInterval (10) | 0.8ms | 0.4ms |
setInterval (100) | 0.6ms | 0.4ms |
AudioBufferSourceNode | 106.1ms | 2.2ms |
OscillatorNode | 6.2ms | 14.3ms |
ConstantSourceNode | 6.6ms | 20.2ms |
requestAnimationFrame | 9.7ms | 8.8ms |
フォアグラウンドで2回目以降時の結果がこちら。
method | 1sec error | 2sec error |
---|---|---|
setTimeout | 0.7ms | 0.4ms |
setInterval (10) | 0.6ms | 0.4ms |
setInterval (100) | 0.4ms | 0.5ms |
AudioBufferSourceNode | 8.6ms | 5.8ms |
OscillatorNode | 9.3ms | 19.1ms |
ConstantSourceNode | 10.1ms | 20.4ms |
requestAnimationFrame | 1.0ms | 0.1ms |
バッググラウンドで初回起動時の結果がこちら。
method | 1sec error | 2sec error |
---|---|---|
setTimeout | 998.9ms | 900.6ms |
setInterval (10) | 0.5ms | 999.7ms |
setInterval (100) | 998.8ms | 999.5ms |
AudioBufferSourceNode | 89.9ms | 5.8ms |
OscillatorNode | 10.7ms | 19.9ms |
ConstantSourceNode | 3.9ms | 20.2ms |
requestAnimationFrame | 34985.8ms | 30565.4ms |
バックグラウンドで2回目以降時の結果がこちら。
method | 1sec error | 2sec error |
---|---|---|
setTimeout | 998.4ms | 390.6ms |
setInterval (10) | 1000.2ms | 0.4ms |
setInterval (100) | 999.5ms | 999.1ms |
AudioBufferSourceNode | 8.0ms | 1.3ms |
OscillatorNode | 9.7ms | 19.6ms |
ConstantSourceNode | 10.1ms | 19.5ms |
requestAnimationFrame | 14580.2ms | 2245.5ms |
まとめると、requestAnimationFrame はバックグラウンドでは停止しているようなもので使いものになりません。 setTimeout, setInterval はバックグラウンドでは非常に遅くなります。 AudioBufferSourceNode, OscillatorNode, ConstantSourceNode は同じ速度で実行できます。 音楽などはバックグラウンドで再生速度が変わったら困るので、それを考慮しているのだと思います。 AudioBufferSourceNode は初回起動時になぜか遅いですが、それ以降は高速です。 つまり、音声処理のタイマーや、バックグラウンドでミリ秒単位の高精度タイマーが必要なら、 AudioBufferSourceNode を使いましょうという結論になりそうです。 とはいえ setInterval より精度が低いのは意外でした。
ちなみにウィンドウが非アクティブのときにどのように動作するかは、Window: setTimeout() メソッド - Web API | MDN にまとまっています。 setTimeout/setInterval はベンチマークの結果通り、1秒単位で処理するようです。 記述を読む限り AudioContext を使った手法はおそらく処理速度が落ちたりはしないですが、特にそれが定まっている訳でもないみたいです。
2024年12月8日日曜日
動画や画像の同時視聴アプリ「ぺたぺた」を作った
動画や画像を画面内にぺたぺた貼ることで、たくさんのデータを同時に視聴するためのアプリを作りました。
画面全体に等間隔で並べたり、サムネイル形式で表示したり、自由配置で表示したり、様々な表示形式に対応しています。
ときどき動画や画像ファイルを一度にたくさん開いて同時に見るのですが、良いツールがありません。 一般的な動画プレイヤーで開くとプロセスが大量に立ち上がって管理しにくいです。 一般的な画像ビューワーで開くと画像を一覧にして見にくかったりする時があります。 そんな訳で作りました。
たとえばこんな感じの UI で動画や画像を見れます。
最近はほとんどのツールを Web アプリ化しているので、動画プレイヤーや画像ビューワーの Web アプリ化はちょうど良い機会になりました。 作ってみて気付いたのは、Web アプリのほうが動画や画像のロードが早いことです。 プロセスをたくさん立ち上げないで良いし、Web ブラウザの実装が高速だからと思います。
割とサクッと作れましたが、なかなかいい感じです。 D & D での貼り付け、Ctrl + V での貼り付けに対応しており、スムーズに動画や画像を追加できます。 動画サイトの同時視聴などもできると嬉しい人がいるかなと思って、 HTML タグの貼り付けにも対応しておきました。 HTML タグが貼り付けられるので SVG や数式、メモの貼り付けなどもできます。 ホワイトボードアプリや付箋アプリとしても使えるかも知れません。 ちなみに YouTube だったら共有用の embed タグをコピーして貼り付ければ良いだけです。 ただ YouTube などの動画サイトはウィンドウサイズによって色々挙動を変えているので、 コツを掴まないとリサイズ処理がスムーズにいかないのが欠点です。たぶんどうしようもない。
Sortable、Resizable、Draggable などのクラスをたくさん実装したのですが、 なんかもうこのへんの機能はライブラリを使わないほうが良い気がしています。 ライブラリを使うと古くてあまり効率的な実装になっていないことが多く、 古い実装に影響されすぎて逆につらい気がするのが昨今です。
ときどき動画や画像ファイルを一度にたくさん開いて同時に見るのですが、良いツールがありません。 一般的な動画プレイヤーで開くとプロセスが大量に立ち上がって管理しにくいです。 一般的な画像ビューワーで開くと画像を一覧にして見にくかったりする時があります。 そんな訳で作りました。
たとえばこんな感じの UI で動画や画像を見れます。
最近はほとんどのツールを Web アプリ化しているので、動画プレイヤーや画像ビューワーの Web アプリ化はちょうど良い機会になりました。 作ってみて気付いたのは、Web アプリのほうが動画や画像のロードが早いことです。 プロセスをたくさん立ち上げないで良いし、Web ブラウザの実装が高速だからと思います。
割とサクッと作れましたが、なかなかいい感じです。 D & D での貼り付け、Ctrl + V での貼り付けに対応しており、スムーズに動画や画像を追加できます。 動画サイトの同時視聴などもできると嬉しい人がいるかなと思って、 HTML タグの貼り付けにも対応しておきました。 HTML タグが貼り付けられるので SVG や数式、メモの貼り付けなどもできます。 ホワイトボードアプリや付箋アプリとしても使えるかも知れません。 ちなみに YouTube だったら共有用の embed タグをコピーして貼り付ければ良いだけです。 ただ YouTube などの動画サイトはウィンドウサイズによって色々挙動を変えているので、 コツを掴まないとリサイズ処理がスムーズにいかないのが欠点です。たぶんどうしようもない。
Sortable、Resizable、Draggable などのクラスをたくさん実装したのですが、 なんかもうこのへんの機能はライブラリを使わないほうが良い気がしています。 ライブラリを使うと古くてあまり効率的な実装になっていないことが多く、 古い実装に影響されすぎて逆につらい気がするのが昨今です。
2024年11月1日金曜日
画像を SVG に変換する image2svg を作った
以前作った @marmooo/imagetracer のフロントエンドアプリとして image2svg を作りました。
image2svg の名前で利用したかったので分離しただけです。
たとえばこんな感じの変換ができます。
生成されるまでどうせ待たないといけないので Worker を使わず実装していますが、使ったほうが良いか微妙なところです。 だいたいのデータで 1秒以内に結果が得られます。 1秒以内に実行できれば他のアプリよりはかなり早いのですが、気持ちよく利用するにはまだまだ高速化が必要です。 とはいえシンプルな高速化はだいたいやりきったので、今後は Wasm 化やマルチスレッド処理を考えたほうが良さそうです。
デフォルトで使いやすいようにオプションは設定していますが、画像サイズに合わせて多少の調整は必要な気がします。 画像サイズが小さくなると lineTolerance/splineTolerance が大きくなったときに穴ができてしまうので、閾値を小さくすると良いです。 といって小さくしすぎると圧縮率が下がるので strokeWidth で調整すると良いというのがだいたいのイメージです。 しかし改めてオプションを弄ってみても、ほとんど出力が変わらないケースが多いです。 それだけ洗練されたとも言えるのですが、なかなか難しい。
たとえばこんな感じの変換ができます。
生成されるまでどうせ待たないといけないので Worker を使わず実装していますが、使ったほうが良いか微妙なところです。 だいたいのデータで 1秒以内に結果が得られます。 1秒以内に実行できれば他のアプリよりはかなり早いのですが、気持ちよく利用するにはまだまだ高速化が必要です。 とはいえシンプルな高速化はだいたいやりきったので、今後は Wasm 化やマルチスレッド処理を考えたほうが良さそうです。
デフォルトで使いやすいようにオプションは設定していますが、画像サイズに合わせて多少の調整は必要な気がします。 画像サイズが小さくなると lineTolerance/splineTolerance が大きくなったときに穴ができてしまうので、閾値を小さくすると良いです。 といって小さくしすぎると圧縮率が下がるので strokeWidth で調整すると良いというのがだいたいのイメージです。 しかし改めてオプションを弄ってみても、ほとんど出力が変わらないケースが多いです。 それだけ洗練されたとも言えるのですが、なかなか難しい。
2024年10月29日火曜日
画像を SVG に変換する @marmooo/imagetracer を作った
画像を SVG に変換する @marmooo/imagetracer を作りました。
他の有名なツールとしては vtracer, potrace, SVGcode, imagetracerjs があります。
これは imagetracerjs の改良・高速化版です。
次に dilate で詳細な輪郭を抽出して 2値化し、RETR_TREE の findContours して、 輪郭ごとに平均色を算出する方法を考えました。 しかしこれは処理時間の大半を平均色の算出に持っていかれて、ボツとなりました。 findContours の処理で生成される内部配列を使えれば、 平均色の算出は理論上はかなり早くできるのですが、現実では使えないので駄目でした。 輪郭数は数万程度には抑えられる利点はありましたが、自作しないと早いものは厳しそうでした。 詰まるところ、findContours の実装などに色々と課題があるのですよね。 OpenCV はデファクトでしょうが、こういう問題は他にも結構あると思っています。
改良余地はかなりあるので、今後もたぶん色々改良すると思います。 たくさんテストとベンチマークを書いたので思ったより作るのに時間が掛かりました。 blur は削除してしまいましたが、このへんの前処理はそもそも他のライブラリを使ったほうが良いです。
ただ non-holed path の場合、例えば前景と背景の輪郭部分では 1ドットだけ色が違うような領域が大量に発生し、 その 1ドットをどうやって補間すれば良いかの問題が起きます。 雑に考えると周囲 8マスを見てもっとも勢力の大きな色を借りれば良いと想像はでき、 これは issue #15 などでも報告されていました。 ただ non-holed path はエッジ同士が結びついて、大きさが大きくなったり最初の座標が変わる可能性があります。 オリジナルの実装はこれが原因で穴が空きます。 さらに言えば 短い non-holed path が隣り合っていたりすると、周囲から代替となる path を抽出するのが困難になります。 下手に取り除くとやはり穴になります。 この問題があるので、処理の起点は holed path で、その中にあるものを取り除くことに徹するほうが楽です。 他にも色々問題があって、色をマージする方式だと同じレイヤ内でもエッジが結びつく可能性もあります。 これはちょっとやそっとの処理では解決できないので、たぶんエッジ検出をやり直さないといけないでしょう。 結びついても別のエッジにするなら隣接ドットの置換で済みそうですが、実装は面倒臭そうです。
そこでまずは holed path に絞って filterHoles を作成することにしました。 まずは点の少ない holed path をリストアップします。 次に対応する non-holed path のレイヤ番号を知るために、path の points を 0/1 で bounding box の領域に書き込み、 bounding box の外側の点を起点として floodfill します。 その後、内側をインデックスカラー画像と照合することで対応するレイヤがわかります。 対応するレイヤがわかったら holed path より長さが短く、bounding box に含まれる path を探して削除します。 ちなみに 3x3 のサイズで □ の形ができるまでは holed path が存在せずレイヤ ID が一意に定まることを利用すると、 path length < 12 までは flooodfill をしないで済むので高速化ができます。 結果、見た目をほとんど変えずにファイルサイズを削減することに成功しましたが、意外と圧縮効果は低い (10% くらい) ともわかりました。 filterHoles オプションを作ったことで設定項目はかなり洗練されたと思います。
Node.js でも動きますが、SVG を画像に変換して確認するテストが動かなかったです。 Deno なら動くのに test: false してリリースせざるを得ないのは負けた気分…。 制作物とはあまり関係のない resvg/sharp の細かなバグが原因なのかと思っていますが、JavaScript ランタイムの保証もなかなか難しいなと感じています。
OpenCV の限界
実のところ同じようなものを OpenCV を使って作ろうとしていました。 最初は 24bit color の画像に findContours アルゴリズムを適用し、 求めた輪郭から path を描写する方法を考えました。 これなら OpenCV の実装が流用できます。 ちなみに現実的には画像の情報量は多すぎるので、 cv.LUT() を使って簡易的な減色を行って情報削減することは必要です。 しかし作ってみたところ、24bit color で動く findContours は RETR_CCOMP (RETR_FLOODFILL) しかないので、 階層情報をきちんと取得できず画像を復元できないことあるとがわかりました。 また思いっきり減色しても輪郭数が数十万も残ってしまう問題が見つかりました。 findContours を自作するか、包含関係の推定を自作しないと厳しそうでした。次に dilate で詳細な輪郭を抽出して 2値化し、RETR_TREE の findContours して、 輪郭ごとに平均色を算出する方法を考えました。 しかしこれは処理時間の大半を平均色の算出に持っていかれて、ボツとなりました。 findContours の処理で生成される内部配列を使えれば、 平均色の算出は理論上はかなり早くできるのですが、現実では使えないので駄目でした。 輪郭数は数万程度には抑えられる利点はありましたが、自作しないと早いものは厳しそうでした。 詰まるところ、findContours の実装などに色々と課題があるのですよね。 OpenCV はデファクトでしょうが、こういう問題は他にも結構あると思っています。
imagetracerjs の改良
さてどうしたものかと思って GitHub を探していて imagetracerjs を見つけました。 ラスター画像をベクター画像に変換することを bitmap tracing ということを、いまさら知りました。普通は image2svg だと思うじゃん…。 先に述べたライブラリもその後見つかったのですが、imagetracerjs は他より高速・高精度に動作するように見えます。 ただいかんせん実装が古くて使いにくいことが気になりました。そこで、 (1) 一部の不要な実装を削除して API を簡素化し、 (2) 減色と blur を外部化して汎用性を持たせ、 (3) Deno で使いやすいように ESM 化し、 (4) テストをたくさん書いて完璧な移植をし、 (5) ベンチマークをたくさん書いて高速化し、 (6) 減色処理をライブラリ化して複数選択可能にし、 (7) 生成 SVG を minify して出力するようにしたものを、 @marmooo/imagetracer として公開しました。改良余地はかなりあるので、今後もたぶん色々改良すると思います。 たくさんテストとベンチマークを書いたので思ったより作るのに時間が掛かりました。 blur は削除してしまいましたが、このへんの前処理はそもそも他のライブラリを使ったほうが良いです。
オプションの改善
Wasm 化以外の速度改善は難しいと思うので、JavaScript 実装は細かな改善が今後の課題です。 いまはオリジナルの実装に付属していたオプションの見直しをしています。 例えばこんなことをやっています。layering 廃止
オリジナルの実装ではエッジ検出の手法が 2つ用意されていて、layering オプションで切り替えます。 オリジナルの配列を使った実装ではあまり差がないので妥当ですが、TypedArray を使った実装に変更すると話が変わります。 色ごとに処理するよりまとめて処理するほうが圧倒的に早いのでオプションは廃止しました。 エッジ検出は最大 50倍くらい早くなっています。mergePaths 追加
オリジナルの実装だとレイヤ (色) ごとに内部の path を別々にレンダリングするのですが、これはレンダリング速度が遅くなります。 そこで色ごとに path を 1つに merge する mergePaths を付けました。 SVG 生成速度は変わりませんが、ブラウザでのレンダリング速度は大幅に高速化できます。 手元の検証では 6倍速くらいになりました。pathomit/linefilter 廃止 → filterHoles 追加
短い path をスキップする pathomit/linefilter オプションは高速化に役立つのですが、 ほとんど同じ処理をしているので 2つもいらない気がしますし、オリジナルの実装だと白抜きの穴ができてしまう問題があります。 細かな線を消すときには、その穴を補完する手段を用意しておかないといけません。 わかりやすいのは holed path の場合で、穴をなかったことにして親ノードの色で塗りつぶすことです。 これはおそらくオリジナルの TODO にも書いてあった課題です。ただ non-holed path の場合、例えば前景と背景の輪郭部分では 1ドットだけ色が違うような領域が大量に発生し、 その 1ドットをどうやって補間すれば良いかの問題が起きます。 雑に考えると周囲 8マスを見てもっとも勢力の大きな色を借りれば良いと想像はでき、 これは issue #15 などでも報告されていました。 ただ non-holed path はエッジ同士が結びついて、大きさが大きくなったり最初の座標が変わる可能性があります。 オリジナルの実装はこれが原因で穴が空きます。 さらに言えば 短い non-holed path が隣り合っていたりすると、周囲から代替となる path を抽出するのが困難になります。 下手に取り除くとやはり穴になります。 この問題があるので、処理の起点は holed path で、その中にあるものを取り除くことに徹するほうが楽です。 他にも色々問題があって、色をマージする方式だと同じレイヤ内でもエッジが結びつく可能性もあります。 これはちょっとやそっとの処理では解決できないので、たぶんエッジ検出をやり直さないといけないでしょう。 結びついても別のエッジにするなら隣接ドットの置換で済みそうですが、実装は面倒臭そうです。
そこでまずは holed path に絞って filterHoles を作成することにしました。 まずは点の少ない holed path をリストアップします。 次に対応する non-holed path のレイヤ番号を知るために、path の points を 0/1 で bounding box の領域に書き込み、 bounding box の外側の点を起点として floodfill します。 その後、内側をインデックスカラー画像と照合することで対応するレイヤがわかります。 対応するレイヤがわかったら holed path より長さが短く、bounding box に含まれる path を探して削除します。 ちなみに 3x3 のサイズで □ の形ができるまでは holed path が存在せずレイヤ ID が一意に定まることを利用すると、 path length < 12 までは flooodfill をしないで済むので高速化ができます。 結果、見た目をほとんど変えずにファイルサイズを削減することに成功しましたが、意外と圧縮効果は低い (10% くらい) ともわかりました。 filterHoles オプションを作ったことで設定項目はかなり洗練されたと思います。
TODO
個人的には現状でも類似アプリの中では一番使いやすいかなあと思っているのですが、 あとは出力ファイルサイズがやや大きいところをなんとかしたいところです。 包含関係を持たず隣接しているだけのノードを良い感じにマージすればかなりサイズは小さくなりそうですが、結構難しそうです。 周囲が 8割くらい囲われていたら面積の多い方に色を寄せて path をマージしたりするのが良いかな?Node.js でも動きますが、SVG を画像に変換して確認するテストが動かなかったです。 Deno なら動くのに test: false してリリースせざるを得ないのは負けた気分…。 制作物とはあまり関係のない resvg/sharp の細かなバグが原因なのかと思っていますが、JavaScript ランタイムの保証もなかなか難しいなと感じています。
2024年10月8日火曜日
様々な言語で作った Wasm をベンチマークした (2)
以前作ったお手軽ベンチマークを高度化してベンチマークの種類を増やしました。
減色処理では色のカウントアップの後に色のリストアップを行うのですが、
そのリストアップ処理までのベンチマークです。
前回 (countColors) は色のカウントアップ、
そのリストアップをして返す getColors、
リストアップした結果は返却せず内部に保持する initColors の 3種類にしました。
getColors は動的配列は使わなくても実装できるのですが、面倒なのでアルゴリズム上の変更はしません。
クラスや構造体や動的配列に対応していない言語、明らかに遅いとわかっている言語は実装しませんでした。
実装は @marmooo/wasm-bench にあります。
C++ は計算後に JavaScript オブジェクトへ変換するのが一番早かったです。 ちまちま JavaScript オブジェクトを触りながら作るとものすごく遅くなります。 この結果を見ると、Wasm では動的配列を JavaScript オブジェクトに変換すること自体が間違いでしょう。 Rust と AssemblyScript はメモリリークで結構ハマりました。 おそらく JavaScript オブジェクトを内部で使うと 解放のタイミングがわからなくなるのでメモリリークします。 AssemblyScript の対策はわかりやすくて、 内部オブジェクトで export しないように気を付ければいいだけです。 Rust もたぶん一緒なのですが、wasm-bindgen が自動変換してくるので見落としがちで、 きちんと手動で JavaScript オブジェクトに変換しないと怖い印象を受けました。
Rust はさらに serializer を使わないと異常に遅くなる問題に直面しました。 Rust はなんでこれが遅いの? が結構ある気がします。 C++ や AssemblyScript は serializer を使うともっと早くなるのかも。 最終的には AssemblyScript が C++ より早くて少し驚きました。 他にも 2^20 くらいのループまでは Wasm のほうが JavaScript より早かったりするのもよくわからないところです。 メモリの再割当てが原因かな。
結論としては C++ が鬼のように早いことがわかりました。Rust も結構早い。 opencv.js を見ていると Wasm は JavaScript より 4倍くらい早そうと思っていたのですが、 データ転送もオブジェクト変換もなければ、もっと早いかもなあ。
, Box<[u32]> などを返り値として処理すると、
おそらく内部で wasm-bindgen が js_sys を使って自動型変換をしているのですが、遅くなります。
C++ の embind だと自動型変換をしても爆速なので課題を感じます。
他にもJavaScript 側で import の仕方を間違えた場合なども遅くなったりします。
またベンチマークを取ると GC がうまく動いていないことがわかります。 Wasm は GC をセルフ実装することになるのでベンチマークを取ってみることは大切そうです。 他にも little endian がデフォルトの JavaScript と、 big endian がデフォルトの Go では整合性を取るのが大変ということがわかりました。 Go に慣れていればこのへん楽かも知れませんが、慣れていないのでね。 さらに syscall/js の Uint32Array から Index(i) して取得したデータをInt() でしか取得できないので、 uint32 で欲しい画像処理の場合、取得した時点で範囲外の数値が壊れます。 byte で取得できないと処理がかなり大変で、関数が不足しているように感じます。 範囲を指定してデータ取得もできないので、色々と限界はあります。 メモリコピーするしか解決策がわかってないのですが、これだとまあ遅くなります。
他にも GC=leaking ならテストが通るのに、その他だとテストが通らないような問題も発生しました。 さらに関数を JavaScript 化すると現状は非常に遅い問題などもあります。 issuses #32591, #46473 にも上がっていましたが、 報告からすでに 5年間も経っているので解決は時間が掛かりそうです。 使えない実装も残してはいるのでこのへんの問題が確認はできるようにしてあります。 実用面はまだまだこれからかなあ。
現状は C++ が一番楽だし高速そうですが、Rust も実用レベルと思います。 今回のコードでは適用できなかったですが、どちらも autovectorizer が組み込まれているので自動 SIMD 化も期待できます。 サクッと Wasm にしたいだけなら AssemblyScript も良い言語だと思います。 他の言語はまだ厳しいかなあ。でもどの言語もまだ Wasm 対応は始まったばかりなので、 今後に期待が良いと思います。次は Kotlin/Dart あたりが面白そうかな?
getColors ベンチマークの追加
getColors ベンチマークでは、すべて JavaScript の性能を下回りました。 結果は以下。 CPU | Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
Runtime | Deno 1.46.3 (x86_64-unknown-linux-gnu)
benchmark time/iter (avg) iter/s (min … max) p75 p99 p995
------------------------------------------- ----------------------------- --------------------- --------------------------
JavaScript, Deno 1.46.3 192.0 ms 5.2 (189.7 ms … 201.7 ms) 192.0 ms 201.7 ms 201.7 ms
AssemblyScript 0.27.30 (Number) 286.1 ms 3.5 (233.6 ms … 322.7 ms) 307.5 ms 322.7 ms 322.7 ms
AssemblyScript 0.27.30 (Class) 319.3 ms 3.1 (279.6 ms … 370.7 ms) 334.2 ms 370.7 ms 370.7 ms
Rust 1.81.0, wasm-bindgen 0.2.93 (Simple) 2.2 s 0.5 ( 2.2 s … 2.2 s) 2.2 s 2.2 s 2.2 s
Rust 1.81.0, wasm-bindgen 0.2.93 (Serde) 337.3 ms 3.0 (316.3 ms … 355.4 ms) 351.8 ms 355.4 ms 355.4 ms
C++, emscripten 3.1.68 390.2 ms 2.6 (368.9 ms … 401.8 ms) 398.0 ms 401.8 ms 401.8 ms
わかってはいましたが、オブジェクトの転送コストが大きいとものすごく遅くなります。
オブジェクトを転送しなければ早いので、とにかくオブジェクトを転送してはいけないことがわかります。
メモリで転送するのはプリミティブと TypedArray に留めたほうが良さそうです。
C++ は計算後に JavaScript オブジェクトへ変換するのが一番早かったです。 ちまちま JavaScript オブジェクトを触りながら作るとものすごく遅くなります。 この結果を見ると、Wasm では動的配列を JavaScript オブジェクトに変換すること自体が間違いでしょう。 Rust と AssemblyScript はメモリリークで結構ハマりました。 おそらく JavaScript オブジェクトを内部で使うと 解放のタイミングがわからなくなるのでメモリリークします。 AssemblyScript の対策はわかりやすくて、 内部オブジェクトで export しないように気を付ければいいだけです。 Rust もたぶん一緒なのですが、wasm-bindgen が自動変換してくるので見落としがちで、 きちんと手動で JavaScript オブジェクトに変換しないと怖い印象を受けました。
Rust はさらに serializer を使わないと異常に遅くなる問題に直面しました。 Rust はなんでこれが遅いの? が結構ある気がします。 C++ や AssemblyScript は serializer を使うともっと早くなるのかも。 最終的には AssemblyScript が C++ より早くて少し驚きました。 他にも 2^20 くらいのループまでは Wasm のほうが JavaScript より早かったりするのもよくわからないところです。 メモリの再割当てが原因かな。
initColors ベンチマークの追加
getColors のようにオブジェクトの変換コストが大きいと Wasm 化する利点がないので、 変換せず内部に保持するようにした initColors ベンチマークでは、Wasm が圧倒的に高速で、最初の想像通りの結果になりました。 結果は以下。 CPU | Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
Runtime | Deno 1.46.3 (x86_64-unknown-linux-gnu)
benchmark time/iter (avg) iter/s (min … max) p75 p99 p995
---------------------------------- ----------------------------- --------------------- --------------------------
JavaScript, Deno 1.46.3 188.9 ms 5.3 (173.5 ms … 213.0 ms) 194.7 ms 213.0 ms 213.0 ms
AssemblyScript 0.27.30 148.8 ms 6.7 (123.7 ms … 187.2 ms) 160.3 ms 187.2 ms 187.2 ms
Rust 1.81.0, wasm-bindgen 0.2.93 70.3 ms 14.2 ( 63.0 ms … 104.4 ms) 74.9 ms 104.4 ms 104.4 ms
C++, emscripten 3.1.68 43.7 ms 22.9 ( 42.4 ms … 48.8 ms) 44.0 ms 48.8 ms 48.8 ms
AssemblyScript はクラスをグルーコードとして生成できない課題があるので、
constructor と使いたいメソッドだけをクラスの外部に関数として定義するのが良いです。
面倒ではありますが、メモリリークの可能性が減るので悪くはない気がしました。
Rust は記法に慣れていないことによって時間掛かりましたが、そんなに悩みはしなかった気がします。
C++ は Array だと大きなメモリ確保ができなくて落ちることだけハマりました。
この 3つの言語は書くのも楽で、あまり迷うところがないです。
どれも uint8 を使わないほうが型変換が減るぶんだけわずかに早い気もしますが、
省メモリ動作も Wasm の利点なので真面目にしました。
結論としては C++ が鬼のように早いことがわかりました。Rust も結構早い。 opencv.js を見ていると Wasm は JavaScript より 4倍くらい早そうと思っていたのですが、 データ転送もオブジェクト変換もなければ、もっと早いかもなあ。
countColors ベンチマークの改善
前回作ったベンチマークも多言語サポートを進めました。 といっても試してみた結果、まだ実用段階にないことを確認することがほとんどでしたが。Rust
Rust は速度が気になったので色々な方式で実装してみました。 まずはポインタを返り値として処理するときは C/C++ と同じ速度が出ることがわかりました。 しかし Vecunsafe { Uint32Array::view(&color_count) }
の書き方を見つけてからは、これが一番楽そうかなと思っています。
Go
Go はポインタなら C/C++ に近い速度で動作します。 しかしオブジェクトを透過的に扱おうとすると、export ディレクティブが使えず、 JavaScript 化した関数をグローバルにエクスポートする処理を書く必要が出てきます。 サンプルなどを見ると globalThis にエクスポートしているのですが、 これは非常に邪魔なので ESM のように自由な名前でエクスポートしたいところです。 設計面での変更が必要そうです。またベンチマークを取ると GC がうまく動いていないことがわかります。 Wasm は GC をセルフ実装することになるのでベンチマークを取ってみることは大切そうです。 他にも little endian がデフォルトの JavaScript と、 big endian がデフォルトの Go では整合性を取るのが大変ということがわかりました。 Go に慣れていればこのへん楽かも知れませんが、慣れていないのでね。 さらに syscall/js の Uint32Array から Index(i) して取得したデータをInt() でしか取得できないので、 uint32 で欲しい画像処理の場合、取得した時点で範囲外の数値が壊れます。 byte で取得できないと処理がかなり大変で、関数が不足しているように感じます。 範囲を指定してデータ取得もできないので、色々と限界はあります。 メモリコピーするしか解決策がわかってないのですが、これだとまあ遅くなります。
他にも GC=leaking ならテストが通るのに、その他だとテストが通らないような問題も発生しました。 さらに関数を JavaScript 化すると現状は非常に遅い問題などもあります。 issuses #32591, #46473 にも上がっていましたが、 報告からすでに 5年間も経っているので解決は時間が掛かりそうです。 使えない実装も残してはいるのでこのへんの問題が確認はできるようにしてあります。 実用面はまだまだこれからかなあ。
Zig
Zig は色々試してみましたが、文法から難しくてまだ理解できていないです。 理解の範疇でば Wasm を作りにくい気もしていて、その理由は関数の返り値です。 配列を JavaScript と共有するにはヒープに明示する必要があるようなのですが、 alloc の成否をチェックする必要があって error union ポインタが返り値になります。 しかし C 互換で export する時はこの機能を使えません。といってキャストもよくわからなかったです。 オプショナルポインタで表現するのも駄目なので、どうすれば良いのかわからなかったです。 C/C++ のポインタは実質 NULL を取り得るオプショナルポインタだと思うのですが、わからん。 グルーコードも生成してくれないので実行までのハードルも高いです。 zig-js とかあるし、できないことはないはずなんだけどなあ。Java
Java はサンプルを見たら、割と簡単に変換できることがわかりました。 やはり設定ファイルがわかりやすいと、初心者にはわかりやすいです。 ただ Java には参照渡しがない問題があるので、どうするのかと思っていました。 案の定、issue #907 を見ていても、 メモリに領域を確保しようとしたときアドレスを渡す手段がない話をしていました。 アドレスを使わないと、バイナリデータの処理は、 値渡しで画像を渡して、処理して、値渡しで返す必要があるので遅いです。 あと uint32 がないので画像はバイト単位でシフト演算するしかないでしょうが、 わずかに遅くなることも予想できます。 つまり countColors と似たなにかは実装できても、同じものは実装できない認識です。 Java で配列などを Wasm を通じて透過的に処理するためには、 事実上変化がなくても変更可能性のある一般的なデータ型の内部位置をコンパイラ側で保証し、 マッピングする必要があるのでしょうかね。 人気言語の中では Wasm 利用に課題が多いかも知れません。Kotlin
Kotlin は文法が簡単な一方で、設定ファイルが難しいのが欠点です。 サンプルで main 文を動かす方法はわかりましたが、関数のエクスポートがわかりません。 現状は Wasm のドキュメントが皆無なのでカスタマイズができないです。 まだ main 文以外はサポートしていないかな? Java 派生ですが、Java と異なり UintArray があるのでマッピングは楽かも知れません。Scala
Scala + Scala.js は サンプルプロジェクト を眺めてみたら、 独自の sbt 設定ファイルの内容が難しすぎて、書ける気がしなかったです。 それとも見たサンプルがいけなかったか。Scala も uint32 がない問題があります。Dart
Dart は文法も簡単だし、簡単に Wasm を生成できるので良い感じです。 ただすぐに使える dart compile wasm コマンドでは main 文の export しか現状できないので、今後に期待です。 dart2wasm という非公開の公式ビルドツールを使うと wasm:export も使えるようなのですが、 ビルドツールを用意するのは大変そうなので諦めました。 wasm:export できるようになれば一気に使いやすくなると思いますし、 公式サポートされたらベンチマークもすぐ追加できそう。 Dart にもポインタがないですが、Uint32List があるのでマッピングは簡単そうです。MoonBit
MoonBit は簡単なので良い感じですが、グルーコードは生成できなそうに見えます。 クラスや構造体を定義するのはあまりにも面倒なので、進化に期待したいところです。まとめ
Wasm ビルドのコードをたくさん書いてみた結果、言語自体の話は置いておくとして、 ビルド環境自体の使いやすさは以下で決まるとわかりました。- 簡単にビルドできる (Scala は課題あり)
- グルーコードを自動生成できる (MoonBit と Zig は課題あり)
- 関数が export できる (Dart と Kotlin は課題あり)
- 高速安定動作する (AssemblyScript と Go は課題あり)
- JS オブジェクトと自動型変換できる (現状 C++/Rust のみ)
現状は C++ が一番楽だし高速そうですが、Rust も実用レベルと思います。 今回のコードでは適用できなかったですが、どちらも autovectorizer が組み込まれているので自動 SIMD 化も期待できます。 サクッと Wasm にしたいだけなら AssemblyScript も良い言語だと思います。 他の言語はまだ厳しいかなあ。でもどの言語もまだ Wasm 対応は始まったばかりなので、 今後に期待が良いと思います。次は Kotlin/Dart あたりが面白そうかな?
登録:
投稿 (Atom)