ケルパンカ

あらゆる画像をドット絵に変換するハイパーめちゃすごツールを作った

この記事は『クソアプリ Advent Calendar 2025』シリーズ 4の17日目の記事です。

こんにちは

タイトルの通り、どんな画像でも瞬時にドット絵に変換するツールを作成しました!めちゃくちゃすごい!

https://dot.syobon.net/

実際に使ってみましょう!こんな画像を用意しました。

これをこのツールに入れると……

ほら!1x1のドット絵になりました!!!!!!!!

……はい。以上、出オチ記事でした。以下、ちょっとだけ真面目な話です。

画像を1x1に変換するには

さて、画像を1x1に変換する方法を考えてみます。「画像を」変換すると言っているので、画像の全てのピクセルが出力に影響を及ぼしてほしいわけですが、一般的な補間アルゴリズム(ニアレストネイバー、バイリニア、バイキュービック)では画像のピクセルのごく一部しか出力に影響を与えません。

一般的に、画像を縮小する際のアルゴリズムとしては面積平均法が優れているとされます。これは出力1ピクセルに対応する入力ピクセル群の平均を取る方法なのですが、出力が1x1のとき、これはつまり画像全体の色を平均するのと同じことです。 これなら、画像の全てのピクセルが出力に影響を及ぼしてくれるので、よさそうですね。

画像の平均色を取るには

⚠️注意: この節の記述には不正確な内容が含まれます。特に、色モデルと色空間を区別せずに記述しています。予めご了承ください。だって正確に書くのめんどくさいんだもん

というわけで画像の全てのピクセルの色の平均を取りたいわけですが、コンピュータ上では色情報だって数値で管理されているので、全部足してピクセル数で割ればいいですね。
つまり、各ピクセルのRGB値をそれぞれ合計してピクセル数で割れば平均色完成!おしまい!
……というわけにはいかないのが、「色」の難しいポイントです。

まず、一般的にRGBが保持する値はガンマ補正をかけた後のものであり、物理的な明るさに比例しません。この弊害としてよく言われる例は#FF0000#00FF00を平均する場合で、#7F7F00という想像よりもずっと暗い、くすんだような結果になってしまいます。

では明るさを情報として保持するHSLやHSVを使えばよいかと言うと、やはりこれも微妙です。確かにHSLやHSVは明るさの情報を持ちますが、その値は人間が感じる明るさとは必ずしも一致しません。例を挙げると、hsl(120 100% 50%)hsl(240 100% 50%)では明らかに前者の方が明るく感じますが、HSL上での明るさの値はどちらも0.5です。
また、色相の変化も人間の知覚と一致しません。例えば、HSL上ではhsl(120 100% 50%)hsl(180 100% 50%)の平均はhsl(150 100% 50%)hsl(120 100% 50%)hsl(60 100% 50%)の平均はhsl(90 100% 50%)となりますが、どちらも緑に寄りすぎているように見えます。

というわけで登場するのが均等色空間です。均等色空間というのは、色空間上の距離が、知覚的な色の距離とできるだけ同じになるように設計された色空間のことです。
この均等色空間にも、Hunter LabだのCIELABだのCIELUVだのいろいろあるようなのですが、なにやらいい感じ™らしいOklabという色空間を採用してみます。

先ほど挙げた例たちをOklab上で計算してみると、

  • #FF0000#00FF00の平均はoklab(0.74719745 -0.004512295 0.1526724)
  • hsl(120 100% 50%)hsl(180 100% 50%)の平均はoklab(0.8859194 -0.1916658 0.07005021)
  • hsl(120 100% 50%)hsl(60 100% 50%)の平均はoklab(0.91721106 -0.15262823 0.1890341)

と、いずれもより直感に近い結果となっていることがわかります。

アルファ値の処理

実際に作る前にもう一つ、アルファ値のことも考えなくてはなりません。

ただ、単純にアルファ値まで平均化してしまうと、ものすごく中途半端に半透明な画像になってしまい面白くなさそうです。

というわけで、不透明度が1.0のピクセルは1ピクセル、0.5のピクセルは0.5ピクセル、0.0のピクセルは0ピクセル……というようにカウントすることにします。

作る

方針が立ったので、あとは作るだけです。Rustで作ることにしました。

まず、画像のエンコードとデコード用にimageクレートを使用します。

imageクレートは色をRGBで表現するので、これをOklabと相互変換するためにoklabというそのまんまな名前のクレートを使います。

あとは全ピクセルのイテレートを並列化するためにrayonを使用していますが、WASMでは動作しないのでただの飾りです(wasm-bindgen-rayonを使用すればWASMでも動作するようですが、なぜか導入後の方が遅くなったので見送りました)。

あとは、

  1. DynamicImageを受け取り、
  2. .to_rgba8()でRGBA画像として扱い、
  3. .pixels().par_bridge()でピクセルを並列にイテレートし、
  4. Oklab::from()でピクセルのRGB値をsRGB扱いでOklabに変換し、
  5. .fold().reduce()で各ピクセルのl, a, bの値それぞれを合計しながらアルファ値を考慮したピクセル数を数え、
  6. l, a, bの値それぞれを数え上げたピクセル数で割り、
  7. 最後にOklabをsRGBに変換

することで平均色を取得することができます。簡単ですね。

見て面白いようなものではないですが、ソースコードはこちらに置いてあります: https://gitlab.com/syobon/one_pixelate

FAQ

Q. Oklabなんか使わなくても、Linear RGBでいいんじゃないの?

A. なんでそんなこというの

おわりに

というわけで、どんな画像でもドット絵(?)に変換できちゃうツールを作った話でした。
みなさんもいろんな画像をドット絵(?)にしてみてくださいね。