ドット絵が好きなので、あらゆるものをドット絵っぽくしたいな〜と思ったので、カメラの映像をリアルタイムにドット絵風に変換する Web アプリを作りました。これで Zoom などのウェブ会議もドット絵エフェクトで参加できますね。
- デモ: https://pixelated-video.zaru.dev ( 表示に時間がかかります / スマホだと重いです )
- ソースコード Web: https://github.com/zaru/pixelated-video
- ソースコード WASM: https://github.com/zaru/go-wasm-pixelate
こんな感じになります。
技術的には、Web カメラを <video>
に流して、それを <canvas>
に渡して Go WebAssembly でドット絵風のエフェクト処理をかけている感じになります。 今回の記事で紹介するのは画像をドット絵風に変換する処理の部分です。カメラをブラウザで扱う方法はQR コードを連続で読み取れる Web アプリを作ったをみてください。
画像をどうやってドット絵風にするか
そもそも画像をどうやってドット絵風にするかです。とても単純な仕組みです。
ドット絵にしたいピクセルの大き単位で画像を分割します。
分割されました。
それをさらに 1px に分割します。
塗りつぶすために、色の平均値を計算します。
平均値をとったら、そのセルを塗り潰します。
それをひたすら繰り返すだけです。
これが画像を変換してみた例です。なんかドット絵というより解像度が低い画像ですね。
ドット絵というと色数が少ないのが特徴なので、似たような色は一つの色にまとめちゃいましょう。
この色の平均を取るために今回は K means でクラスタリングします。
こんな感じになりました。ちょっと色味が地味に変換されているので RGB での計算じゃなくて Lab の色空間で計算してみます。
なんかそれっぽくなりましたね。
というわけで、こういった処理を Go WebAssembly で書くときに詰まったポイントを紹介します。
Go で K means クラスタリング
探してたら便利なライブラリ muesli/kmeans があったので、それを利用しています。ありがたい。使い方も簡単で、クラスタわけしたい値を float64 で渡してあげて、何個のクラスタにしたいかを選択すれば良いです。今回の場合は、クラスタ数 = 色数になります。それなりに処理が重いのでリアルタイムに計算する場合は 4-8 色くらいが良いと思います。
var d clusters.Observations
for x := 0; x < 1024; x++ {
d = append(d, clusters.Coordinates{
rand.Float64(),
rand.Float64(),
})
}
km := kmeans.New()
clusters, err := km.Partition(d, 16)
Go で RGB から Lab 変換
RGB から Lab に色変換する式はないので自前で実装しようか悩んだんですが、ちょっとセンスがなくて雑な色味になったのでこれも lucasb-eyer/go-colorful という良い感じのライブラリがあったので、それを使っています。
cc := colorful.Color{r, g, b}
cc.Lab()
これで RGB から Lab に変換ができました。
Go から JavaScript の Uint8Array にアクセスする
canvas の色情報を取得する JavaScript は以下のような感じです。
ctx.getImageData(0, 0, 100, 100).data
この data
プロパティに 1pixel あたりの RGBA 4つの情報が入っています。これを Go から読み取りたいのですが、そのまま読み取ると非常にパフォーマンスが悪かったです。
cell := ctx.Call("getImageData", 0, 0, 100, 100)
data := cell.Get("data")
data[0] // 遅い
jsValue
にそのままアクセスするのではなく CopyBytesToGo
を使って Uint8Array をバイトコードに変換してからアクセスをすると高速に処理できました。
cell := ctx.Call("getImageData", 0, 0, 100, 100)
data := cell.Get("data")
uint8Arr := js.Global().Get("Uint8Array").New(data)
received := make([]byte, data.Get("length").Int())
_ = js.CopyBytesToGo(received, uint8Arr)
received[0] // 速い
以上で、リアルタイムにドット絵風な処理をしたカメラ Web アプリが完成します。やりましたね!