koturnの日記

転職したい社会人2年生の技術系日記.ブログ上のコードはコピペ自由です.

ZopfliPNGによるPNGの可逆再圧縮とVRMの容量削減について

この記事はIQ1 Advent Calendar 2020の7日目の記事です.

TL;DR

zopflipng.dllをいい感じに使うツールを作りました.

  • 並列実行可能
  • zipアーカイブファイル内のファイルを一時的に書き出すことなく処理可能
  • 更新日時タイムスタンプはそのままで再縮率実施

また,最新のバイナリはここに置いてあります.

背景

Zopfli(ツオップリ)とはGoogleが開発したDeflate互換の圧縮アルゴリズムで下記の特徴があります.

  • Deflate互換で高い圧縮率
  • 当然,可逆圧縮
  • 圧縮にはかなりの時間が必要(100倍ぐらいの時間)

そもそも,Deflate圧縮とはzipやgzip形式のファイルに使用されている圧縮形式であり,zopfliを使用すれば単純に高圧縮率のzipファイルやgzipファイルを作成することができるわけですね. つまり,既存のツール(unzipやgunzipなど)でも圧縮結果を伸長することができるのがセールスポイントですね.

そして,PNGファイルに使用されている圧縮方式もDeflateであるため,その部分にもZopfliを適用することができるわけです.

PNGの保存容量を削減するなら,そのPNGファイルのディレクトリを7zip等でLZMA2とかの圧縮アルゴリズムを用いればよくない?と思うかもしれませんが,PNG自体がDeflate圧縮を用いているため,PNGファイル自体に対して何かしらの圧縮アルゴリズムを用いたとしてもほとんど容量の削減が出来ないわけです. なので,PNGファイルの中のDeflate圧縮部分を改善することには大きな意味があります.

GoogleによるZopfliのリファレンス実装のリポジトリでは,gzip形式の圧縮を行なうためのzopfliコマンドおよびPNGの再圧縮を行なうためのzopflipngをビルドすることが可能です.

コマンドだけではなく,.soや.dllといったライブラリファイルとしてビルドし,他の言語から呼び出すことも可能です.

Googleのリファレンス実装のZopfliPNGをデフォルトのオプション指定で利用した場合,20~25%程度の容量削減になることが確認できました. これだけの容量を削減しながら,全く画像の劣化が伴わないのは驚愕ですね.

ただし,1920x1080(FullHDの解像度)のPNG画像だと1枚あたりの処理時間が60~120秒といった具合で,大量の画像を処理するにはなかなかしんどいなといった具合です.

開発動機

何となくZopfliを使用したツールを作ろ~と思ってたら,できあがっていきました(簡単なツールを書いてプログラミング欲を満たしたかっただけ). バイナリはGoogleドライブに置いています.

が必要なはずなので,ご使用の際は上記2つがインストールされていることを確認し,インストールされていなければお使いの環境に合わせたものをインストールしてください.

後付けの動機としては,今年の5月ぐらいからVRChatやclusterといったVR・バーチャルSNSにハマり,写真撮影をしまくるために,PNG画像が大量に貯まってきたので,それをいい感じに圧縮したかったというところです. (ローカルに20000枚ぐらいのPNGファイルがあります...!!)

clusterの写真は撮影後にcluster側に保存され,Webからダウンロードできるのですが,まとめてダウンロードする場合はzipファイルにまとめられています. clusterのPNG画像ファイルはGUID名になっており,タイムスタンプのみが保存日時を知る術となっているので,再圧縮するにしてもタイムスタンプをそのままにしたかったというのと,いちいちディスクに一時ファイルを書き出して再度zipファイルに格納するのは明らかに冗長だと思いました.

また,前述の通り,処理にはものすごく時間がかかるので,1枚の画像の処理に対して1スレッドを割り当てて並列処理を行えば,多少はマシになるかなと思った次第です.

普通はGoogleのリファレンス実装のzopflipngコマンドをそのままシェルスクリプトやバッチファイル等で回せばいいと思うものですが,上記の要件

  • 並列実行可能
  • zipアーカイブファイル内のファイルを一時的に書き出すことなく処理可能
  • 更新日時タイムスタンプはそのままで再縮率実施

を満たすのはしんどいので,とりあえずC# で書きました.

ツールの使用方法

残念ながら(?),私はGUIのツールがあまり好きではないので(自動化しにくいのと自分にとって不要であるのと作るのがめんどくさいので),現状はコマンドラインツールとして開発しています.

基本的に対象がzipファイルの場合,

> ReCompressPng.exe [対象のzipファイル]

ディレクトリが対象の場合,

> ReCompressPng.exe [対象のディレクトリ]

と指定すればOKです. 同時実行スレッド数はデフォルトでは無制限なので,他の作業を並行して行なうために制限したい場合は -n オプションで指定することが可能です. 下記は同時実行スレッド数を2に制限する例です.

> ReCompressPng.exe -n 2 [対象のzipファイル]

オプション表

オプション オプション引数 説明
-c, --count-only 対象zip・ディレクトリ内のPNGファイルとそのサイズ,および合計サイズを表示します.
-h, --help ツールの使用方法を表示し,プログラムを終了します.
-i, --num-iteration 繰り返し回数 Zopfliの繰り返し回数を指定します.
-I, --num-iteration-large 繰り返し回数 大きな画像データに対するZopfliの繰り返し回数を指定します.
大きな画像の閾値は身長後の画像データ長が200000 Bytes以上かどうかです.
-n, --num-thread スレッド数 同時実行スレッド数を指定します.
-r, --replace-force ZopfliPNGによる再圧縮後のPNGファイルのサイズがオリジナルのPNGのサイズより大きくても置きかえを行ないます.
-s, --strategies カンマ区切りの数字 Zopfliのストラテジをカンマ区切りで指定します.例:-s 0,1,2,3,4,6,7 .詳細はZopfliPNGのソースコード等を参照してください.
-v, --verbose カンマ区切りのチャンク名 zopflipng.dllからの標準出力を有効にします.
--keep-chunks 指定したチャンクは除去しないようにします.
--lossy-transparent (※非可逆圧縮)アルファ値0の画素のRGB設定値を0にします.
--lossy-8bit (※非可逆圧縮)16bit画像を8bit画像にします.
--no-overwrite 対象ファイルの上書きを行なわず,別のファイルとして再圧縮結果のPNGファイルを出力します.
--no-auto-filter-strategy ストラテジの自動選択を行なわないようにします.
--no-use-zopfli Zopfliを利用しないようにします.
--no-verify-image 圧縮前後のPNG画像データの比較を行なわないようにします.

注意点

確かにZopfliPNGによる再圧縮では高圧縮率のPNGファイルを得ることができました. しかし,他のペイントツール等で読み込み,そのツールで保存を行なった場合には,通常のPNGのDeflate圧縮が行なわれてしまうため,ファイルサイズが元に戻ってしまいます(可逆圧縮なので当然ですが). ですので,この場合は再度ZopfliPNGに通さなければPNGファイルのサイズが元に戻ったままとなります.

興味本位の調査

VRSNSで使用するアバターは当然テクスチャ画像が含まれているわけですが,このテクスチャ画像のうち,PNGのものをZopfliPNGで再圧縮するとアバターが軽量化できるのではないかと思って,やってみました. アバターの軽量化はネットワーク負荷を軽減させることになるため,他の人のためにもできる限りアバター容量を小さくしたいという思いがあります. (特にclusterでは)

VRM

普段僕がclusterで使用しているアバターは,VRoid Studioで作成し,VRMConverterでUnityに取り込み,UniVRMで出力したものです.

f:id:koturn:20201207193236p:plain

これに対し,UnityプロジェクトのAssetsディレクトリ内のPNGファイル全てをZopfliPNGで圧縮し,全く同じ出力設定で比較を行いました.

結果としては...

f:id:koturn:20201207193541p:plain

なんと,約9MBから6.8MBに容量が削減できました!

VRM形式は全然詳しくはありませんが,結果を見る限りではおそらく,テクスチャ等のPNGファイルをそのまま格納しているのだろうと予想します.

ただ,VRMファイルが軽くなったからといって,ネットワーク負荷が軽くなるかどうかはわからないところです. 例えば,バーチャルSNS:clusterでサーバ側にあるVRMファイルをそのままクライアントに送りつけて,クライアントで展開するという実装になっているのであれば,VRMファイル自体の容量が小さいことに意味はあります. しかし,サーバ側でVRMをクライアントにとって扱いやすい形式に変換し,送りつけているのであれば,画像は一度伸長されていると思われるので,ZopfliPNGによる圧縮はネットワーク負荷の軽減には全く寄与しないわけですね.

とはいえ,VRM自体のファイル容量は削減できているので,アップロードの25MB制限にひっかかったときにZopfliPNGが使えるかもしれないですね(アバターに依りますが25%の容量削減が出来たのはかなり大きいかと!). まぁ,ポリゴン数とかボーン数に引っかかる人がほとんどで,25MB制限に引っかかる人は稀な気がしますが....

VRChat

実験にはBoothで販売されているアバター響狐リクちゃんを使用しました. とても可愛いですね.

これもUnityプロジェクトのAssetsディレクトリ内のPNGファイル全てをZopfliPNGで圧縮してみました. その結果は...

f:id:koturn:20201207192640p:plain f:id:koturn:20201207192644p:plain

なんと容量に変化はありませんでした! 一応,テストビルドのVRChatのアバターファイルを確認したところ,1KB程度の容量の削減は確認できましたが,雀の涙程度でしかないですね....

おそらく画像ファイルは一度展開し,まとめるとか,独自の形式にしているとかやっているのではないかと思われます.

今後の展望

GUIを付ける

一般人ウケを考えると必須な作業ですね. しかしやる気がないので,多分やらないです.

VRMファイル内のPNG画像の再圧縮

VRMファイルの形式には詳しくないですが,PNGの再圧縮によりファイルサイズを減少させることができたので,おそらくPNGファイルはそのままVRMファイル内に格納されているのだと予想できます. となれば,VRMファイル内のPNGファイルを再圧縮し,再度VRMファイルを出力するツールを作ってみたくなりますね.

keep_colortype

Googleの提供しているZopfliPNGのC APIだと,keep_colortype を指定する術がありません

keep_colortype が指定できない場合,現在の実装では keep_colortype = false となるため,例えば,32bit ARGBのPNG画像かつアルファ値が全画素で255(透過しない)となっていると24bit RGB形式に変換してしまうわけです(無駄なアルファチャンネルの削除をするわけですね). この操作では当然視覚的な変化は全く無いし,再度全画素にアルファ値255を加えれば元に戻せるわけですが,厳密には可逆ではない圧縮ということになりますね.

keep_colortype をC++ APIの方からは指定する術はあるのですが,C# 側からこの関数の呼び出しを行うのは,マングルされた関数名を指定してやる必要があるため,しんどいわけです.

となれば,別のDLLにC++ APIを呼び出すだけのC APIの関数を用意するのが,手元で可能なワークアラウンドとなりますが,そのためにDLL 1個を増やすのはなぁ...といったところです.

keep_colortype が追加されたのは 2020-05-24であるため,単にC APIの方への追加忘れではないかと思います. これはプルリクチャンスですかね...誰かやってもらえると助かります.

(配布しているバイナリではzopfli本体のソースコードを修正し,zopflipng.dllを再ビルドした上で,RecompressPng自体も対応するようにしているので,--keep-color-type というオプションを使うことができます.)

余談

ちなみに,zopfliのリポジトリには,gccを使ったビルド方法が簡単に書かれているだけで,MSVCでのビルド方法が書かれていないですが,CMakeLists.txt があるので,それを利用してビルドを行なうのが簡単です.

ただし,普通にビルドしたのでは単純にコマンドラインツールのみが生成されるので,DLLを生成するために, ZOPFLI_BUILD_SHAREDON にする必要があります.

下記はビルド手順の一例です. 最初に,zopfliのリポジトリのトップディレクトリにいるものとします.

> mkdir build
> cd build
> cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DZOPFLI_BUILD_SHARED=ON
> cmake --build .

あくまでリファレンス実装であるため,ソースコードがドキュメントだ!と言わんばかりの姿勢がいいですね.

まとめ

とにかくZopfli/ZopfliPNGはすごいよということが言いたかっただけですね. 副産物としてVRMのファイルサイズ削減できることを発見できたのは大きな収穫でした.

みんなもZopfliPNGで色々なPNG画像を圧縮しましょう!

僕の配布しているバイナリが怪しいという意識をお持ちの方は,逆コンパイラ等で中身見て安全であることを確認するなり,GitHubソースコードを落としてきてビルドするなり,自前でプログラム書くなり,オリジナルのZopfliを使うなりするとよいと思います.

一応,Visual Studioやcmakeを用意するのが面倒な人のために,オリジナルのZopfliをビルドしたものも置いてあります

参考