
昨年からちょこちょこコードを書いていて、たまに思い立ったように機能を追加していた画像最適化ツールのlightpngに1.0のタグを打ちました。だいたいやりたい機能は入ったかな、ということで。時間的にはこんな感じで進化してきました。
- 2012/5/17: ファーストバージョン。pngを読み込んで16ビットに減色機能
- 2012/6/14: クロスコンパイルでWindowsバイナリ生成
- 2012/6/16: jpeg読み込み。
- 2012/6/18: PVRTC/ATITCの生成。
- 2012/7/30: libpng/zlibの圧縮オプションをブルートフォースで最適化
- 2012/11/5: プレビューモード追加
- 2012/11/28: Mac OS 10.8上でも10.6から動くバイナリが作れるようにビルドオプション変更
- 2012/1/28: png最適化スキップ機能を追加
- 2012/3/18: pngnqの256色化コード+パレット最適化のインデックスカラー画像生成機能追加
- 2012/3/23: 16bitかつ256色コード生成機能を追加
- 2012/3/24: Zopfli圧縮オプションを追加
- 2012/3/25: 256色化をpngnqから生成後のサイズの小さいpngquantに変更
- 2012/3/28: ドキュメントを修正して1.0タグを打った
なぜこのコマンドを作ったのか?
ngCoreというゲームエンジンがありまして、16ビット画像を使って使用するテクスチャメモリ半減!みたいな機能があったり、ATC/PVRの圧縮テクスチャが使えたりします。16ビットはpngとかjpegファイルを読み込んで末尾のビットを削って1ピクセル4バイトの情報を2バイトに押し込めます。当然そのまま削るとグラデーションがガタガタになったりうれしくないので、ディザリングをかけて見た目がおかしくならないようにする必要があります。
ATCの圧縮テクスチャ生成も、Windows用のコマンドラインツールはあるんですが、Mac用が見当たらなかったというのもあります。PVRも、Macのtexturetoolと同じ形式(古いPVR v2形式)で画像を出力するコマンドラインツールがWindowsで見つからなかったり。Android/iOSの両対応のゲームを開発する&WindowsでもMacでも!という場合は、それぞれ足りないツールを補完する必要がありますよね。まだ試してませんが(たぶん動く)、Linuxでも動けば、Jenkinsで継続的インテグレーションで画像ファイル変換とかもできるようになるはず。
圧縮テクスチャはメモリサイズが1/4になったり1/8になったりパフォーマンスも向上したりといいことが多いのですが、圧縮のしすぎで細部のディティールが悪化したりします。特に圧縮率の高いPVRTC。PVRTCがVQと呼ばれていたDreamcastで使われていたPowerVR2時代でも、キャラクタの顔に使うのをデザイナーさんが嫌がったり、ということがあったとか。エッジがはっきりしたUI画像とか文字とかもPVRTCは厳しいです(特にアルファ値を使うケース)。そういう時には16ビットのpngは有力な選択肢になります。
このあたりは最初の1〜2ヶ月で完了し、後はファイルサイズ縮小の追求ですね。pngはマニア向けなフォーマットで、圧縮オプションしだいでファイルサイズが微妙に変わったりします。最適化してもzlibの内部の辞書情報の最適化なので展開速度は全然変わらないはず(展開時のメモリ使用料は制御可能)。クオリティが微妙に変わっても良ければさらに大胆にファイルサイズを削れます。
10%でもファイルサイズが減れば、ブラウザゲームのレスポンスは上がるし、apkとかappファイルにそれだけ多くのコンテンツを入れられるようになります。何かをしながら過ぎている10分よりも、何もしない10分の方が苦痛が数倍だったりすることを考えれば、Google PlayとかAppStoreからのダウンロードは他のことをしながらバックグラウンドでできるけど、ゲーム起動後の追加ダウンロードで待たせるのは、ユーザさんからすれば何もできない待ち時間なので苦痛が数倍です。このツールを使って、少しでもインターネットを通じて世界をより良くしよう、ズッ友だよ!ということです。
フィードバックもpull-requestも評判も反応もまったくないけど、地味で誰もやりたがらないことを地道にやるのが大事ですよね。
現状の結果
トップ画像のレナ・ソーダバーグ画像はロスレスで圧縮しなおした結果です。tifファイルをPreview.appで圧縮した時のサイズが525,521バイトで、このツールで圧縮しなおす(-o 2 -32
)と473,226バイトになります。10%の縮小。

こちらの画像は、16ビット化(-o 2 -16
)したものです。274,089となって、48%ぐらい減ります。

こちらの画像は、256色のインデックスカラー化して減色して、圧縮したもの(-o 2 -32i
)です。178,214バイトになって、66%削減です。

こちらが256色かつ16ビットモード(-o 2 -16i
)です。153,976バイトで71%減です。ネット上でこれを効率よく行うアルゴリズムとか見つからなかったので、メディアンカット法をやりつつ、16ビットカラーに適合するパレットを作ってFloyd-Steinberg法のディザリングを使って・・・という方法で作っています。上記の16ビット化を先に適用してからpngquantのクオンタイズというのも試してみたけどクオリティがいまいちだったので今はこの手法です。アルファ値がゼロのところが怪しかったり、クオリティの向上の余地はありそうです。
256色化ではpngqunatと同じエンジンを使ってますが、Zopfliの恩恵でこっちのツールの方が微妙に小さい結果になってます。また、pngnqよりも小さいです。速度はpngnqが最速で次がpngquantで、lightpngが一番遅いですが。他の結果も、pngcrushやoptipngよりも小さいサイズになります。Kflateはオープンソースではないので試してません&組み込んでません。
Zopfliの使い方
Googleが発表して、一瞬ネットニュースを駆け巡ったZopfliですが、その後トライしましたみたいな話題があまり出て来ませんでした。途中でAPIが変わったのは僕の中で大事件だったのですが・・・libpngへのZopfliの組み込みはoptipngへの組み込みを参考に、png_set_compressor_type
というAPIを追加して圧縮ライブラリを切り替えられるようにしたものをこちらにおいてたりします。
提供されているのはC言語のライブラリで、gitリポジトリからコード取得できますが、extern "C"
がなかったり、.gitignore
がなかったり、とっても男らしいコードです。でもAPIはとってもシンプル。出力先バッファもZopfli内部で確保してくれますので、バッファを外部で生成し、ループで終わるまで何度も実行、みたいなことをする必要はありません。
struct ZopfliOptions zopfli_options; /* オプション構造体作成 */ ZopfliInitOptions(&zopfli_options); /* オプション構造体初期化 */ unsigned char* out; /* 出力バッファ */ size_t out_size; /* 出力サイズ */ zopfli_options.numiterations = 15; /* 試行回数設定 */ /* 圧縮 */ ZopfliCompress(&zopfli_options, ZOPFLI_FORMAT_ZLIB, inbuffer, inbuffer_size, &out, &out_size); /* 使い終わった出力バッファを破棄 */ free(out);
Zopfli以前のlightpngでは、zlibの圧縮アルゴリズム4種(Z_DEFAULT_STRATEGY, Z_FILTERED, Z_RLE, Z_HUFFMAN_ONLY)と、libpngのフィルタオプション6種類(PNG_FILTER_NONE, PNG_FILTER_SUB, PNG_FILTER_UP, PNG_FILTER_PAETH, PNG_FILTER_AVG, PNG_ALL_FILTERS)の組み合わせの24通りで圧縮をして、最小のものを出力してました。圧縮アルゴリズムにはZ_FIXEDというのもあるのですが、圧縮率が低いし、他のpng最適化ツールもこのオプションは使っていないので除外してます。本来ならフィルタは組み合わせがあるので32通りx圧縮アルゴリズム5種 x 圧縮率9通りで膨大な量の組み合わせがありますが、そこまではやってません。
Zopfliを組み込んでみていろいろな画像に対して傾向を見ると、フィルタごとの圧縮結果は、Z_DEFAULT_STRATEGYの時の圧縮結果と正の相関がありそうでした。lightpngではこの結果を踏まえて、-o 2
圧縮オプションではzlibで各種フィルタごとの圧縮傾向を調査してみて(マルチスレッドで回すので1回分の時間)、その後、最小だったフィルタを使ってZopfliで圧縮して出力するようにしています。-o 3
だと、zlib 24パターン+ Zopfli 6パターンでブルートフォース方式で最小化オプションを探しますが、僕の見る限りでは今のところ-o 2
と-o 3
では結果は同じになってます。とはいえ、今まではスレッドプール方式で8スレッドで24パターンを処理していたのと比べると、最後のZopfliの部分が並列化できなくて処理時間がすごく伸びてしまっているので、そこはコマンド実行側で並列化する必要があります。あるいは複数画像を同時に処理できるように内部構造を変えるか・・・
今後修正するとしたら
Windowsで圧縮テクスチャを使えるように(今のクロスコンパイルではC++のマングリング規則がVC++と違ってリンクできないので、Windows環境でビルドできるように)するのが1つですね。あとは圧縮テクスチャの種類を増やしたいのですが、プレビュー用に圧縮テクスチャ→pngと2度変換するモードを用意しているので、圧縮テクスチャの逆圧縮が欲しいところです。ETC1とS3TCの逆変換は手にはいりそうですが、ETC2/EACは論文を見ながら実装しないとダメそうです。PowerVRのタブレットは手元にあるけど、テスト用に誰かTegra機(Nexus 7)とMali機(Nexus 10)と、Adreno機(Nexus 4)を誰かください。
後は16bit&256色インデックスカラーモード。どこかでプログラムをミスっているのか、16bit化したたときに同じカラーのインデックスが登場して256色使い切ってないケースがありそうな予感がしています。一度変換して、ピクセル数が多い(同じ色が連続して登場している)場所の色を分割して、256色を使いきってさらにきれいに見えるように、と2パス、3パス処理にすればいいのかなぁ、となんとなく思っています。その時に、肌色とかひと目で差が見つかりやすいところを集中的に改善するようにすればクオリティをもっと上げられそうな気がしています。