PNGの圧縮率調査

先日TIFFファイルの圧縮についての記事を書いた。これによって大量にTIFFファイルの圧縮を行い、テラバイトオーダーの領域を確保することができた。消費コスト*1を考慮しても、十分に経済合理性のある作業だった。

ところで、メジャーな画像フォーマットのうち、PNG(Portable Network Graphics)も可逆圧縮をサポートしている。私の認識では、その名の通りネットに流通させるのに適したファイルフォーマットで、Web画面のボタンや社名のロゴなど比較的小さい画像に使うことが多い。さらに、GIFと同様に透明も扱えるのが特徴。この程度の理解で居たので、高解像度画像の記録に適したフォーマットではなく、個人的にはマスクなどのアルファチャネルが必要な用途以外では制作環境ではほとんど使っていなかった。

先日のストレージ逼迫を契機として、圧縮済TIFFと比較して、PNGの圧縮率はどうなのだろうかという疑問が生じたので調べてみた。

PNGの圧縮の仕組み

A guide to PNG optimizationによると、

The PNG compression works in a pipeline manner.
In the first stage, the image pixels are passed through a lossless arithmetic transformation named delta filtering, or simply filtering, and sent further as a (filtered) byte sequence. Filtering does not compress or otherwise reduce the size of the data, but it makes the data more compressible.
In the second stage, the filtered byte sequence is passed through the Ziv-Lempel algorithm (LZ77), producing LZ77 codes that are further compressed by the Huffman algorithm in the third and final stage. The combination of the last two stages is referred to as the Deflate compression, a widely-used, patent-free algorithm for universal, lossless data compression. The maximum size of the LZ77 sliding window in Deflate is 32768 bytes, and the LZ77 matches can be between 3 and 258 bytes long.
A complete description of the PNG compression is beyond the scope of this guide. The PNG Specification describes the format completely, and provides a complete list of references to the underlying technologies.

とある。適当に意訳してみると、

PNGの圧縮処理は、パイプラインみたく段階がある。
始めにデルタフィルタリング(或いは単にフィルタリング)と呼ばれるロスレス変換演算を行う。これ自体はデータの圧縮や減量ではなく、圧縮しやすくするための処理である。
次にフィルタリングされたデータをLZ77アルゴリズムで処理する。
最後にLZ77で符号化されたデータをハフマンアルゴリズムで圧縮する。
完全な詳細はこのガイドの範囲外やから、知りたかったらPNG仕様書読むといい。

ざっくりと、フィルタリングで整形後、Deflate(LZ77→Huffman)で圧縮してると理解しておけばよいだろうか。
フィルタリングって何してるんだという疑問が生じるが、同じソースに以下の説明がある。

The role of filtering can be illustrated in the following example. Assume the sequence 2, 3, 4, 5, 6, 7, 8, 9. Although it has much redundancy, the sequence is not compressible by a Ziv-Lempel compressor, nor by a Huffman compressor. However, if one makes a simple and reversible transformation, replacing each value with the numerical difference between it and the value to its left, the sequence becomes 2, 1, 1, 1, 1, 1, 1, 1, which is highly compressible.
The PNG format employs five types of filters: None, Left, Up, Average, and Paeth. The first filter leaves the original data intact, and the other four are subtracting from each pixel a value that involves the neighbor pixels from the left, up, and/or the upper left.
A certain filter is assigned to each row, and is applied to all pixels from that row. Therefore, an image can be delta-filtered in a huge number of possible configurations (5 ^ height), and each configuration leads to a different compressed output. Two different filter configurations may make a difference in the compressed file size by a couple of factors, so a careful choice of filters is of paramount importance.
It is possible to apply a single filter to all rows, or to apply different filters to different rows. In the former case, the filtering process is fixed; in the latter, it is adaptive.

こちらも適当に意訳してみると、

フィルタリングの役割は以下の例で説明できる。
2,3,4,5,6,7,8,9というデータがあるとして、このままではLZ77やハフマンで圧縮できない。
が、左の値からの差分で表わせば、2,1,1,1,1,1,1,1となり、冗長性があるから超絶圧縮できる。しかも簡単に元のデータに逆変換できる。
PNGではNone,Left,Up,Average,Paethの5種類のフィルターが定義されてる。
Noneは何もしない、それ以外は近傍の画素からの差分を抽出する。近傍ってどこやねんというのを、左(Left)、上(Up)、その両方の平均(Average)、左・上・左上からの計算値(Paeth)で指定できる。
いくつかのフィルタは行毎に割り当てできて、その行以降の全ピクセルに対して適用可能。
故に、5^行数(フィルタの種類^画像の高さ)という膨大な数のフィルタパターンが取り得るけど、圧縮しやすい結果になるようにフィルタを選ぶことが大事やね。
単一のフィルタを全行に適用することも可能だし、行ごとに違うフィルタを適用することも可能で、前者のフィルタリング処理はfixed、後者はadaptiveやで。

なるほど。ベタ塗りはもちろん、差分を得るフィルタリングを行えば規則的なグラデーション的な画像も圧縮しやすいということだろう。PNGで高い圧縮率を実現するには、なるべく冗長的なデータを出力するフィルタの選定が肝だろう。
(フィルタを適用しなければDeflateによる圧縮だけなので、ZIP圧縮TIFFと似たような圧縮率になるのだろうか。)

ImageMagickによるPNGの圧縮パラメータ指定

パラメータを指定してPNGファイルを書き出すためには何らかのツールが必要であるが、ここではImageMagickを使う。
ImageMagickのリファレンス(Common Formats -- IM v6 Examples)には、以下の記述がある。

PNG compression
When used with PNG output, quality is regarded as two decimal figures. The first digit (tens) is the zlib compression level, 1-9. However if a setting of '0' is used you will get Huffman compression rather than 'zlib' compression, which is often better! Weird but true!
The second digit is the PNG data encoding filtering (before it is comressed) type: 0 is none, 1 is "sub", 2 is "up", 3 is "average", 4 is "Paeth", and 5 is "adaptive". So for images with solid sequences of color a "none" filter (-quality 00) is typically better. For images of natural landscapes an "adaptive" filtering (-quality 05) is generally better.

PNG圧縮
PNGで出力するときはqualityパラメータは2桁整数で指定する。
最初の桁(10の位): zlibの圧縮レベル(1~9)を指定する。0ならzlib圧縮じゃなくハフマン圧縮を指定する。妙な話だがハフマンだけの方が圧縮率がいいことが良くあるんだ!
次の桁(1の位): フィルタを指定(0:none, 1:sub, 2:up, 3:average, 4:Paeth, 5: adaptive)。
典型的にはカラーの塗り絵みたいな画像は、noneがベター。(-quality 00)
一般的には自然風景みたいな画像にはadaptiveがベター。(-quality 05)

2桁数字と見せかけて、有意コード混じりですって。そして、謎のハフマン推しである。

実測

ここではSigma Photo Proで出力したTIFFファイルをInputとして実際に検証してみる。

Inputデータ仕様
  • 無圧縮TIFF
  • マルチページ
    • 1ページ目:4704x3136f:id:kachine:20151024221835j:plain
    • 2ページ目:160x106(サムネイル相当。※必要無いので以降は1ページ目の画像についての処理結果である)
  • 微妙にTIFFの仕様を逸脱(本論とは特に関係ない。シグマさんには優先度最低で構わないので修正してほしいので書いた。)
    • "Invalid TIFF directory; tags are not sorted in ascending order."とImageMagickに警告される。
測定結果

TIFFの圧縮アルゴリズム(LZW/ZIP)、PNGの圧縮パラメータ(フィルタ及びzlibの圧縮レベル)をそれぞれ指定した場合の、ファイルサイズは以下の通りとなった。

Pattern Size CompressionRatio
Original(page#1) 88,539,024 -
TIFF-LZW 89,839,688 101.5%
TIFF-ZIP 66,946,402 75.6%
PNG-Huffman_Filter:None 74,514,525 84.2%
PNG-Huffman_Filter:Sub 74,876,550 84.6%
PNG-Huffman_Filter:Up 74,946,178 84.6%
PNG-Huffman_Filter:Average 74,718,667 84.4%
PNG-Huffman_Filter:Paeth 74,889,119 84.6%
PNG-Huffman_Filter:Adaptive 71,166,039 80.4%
PNG-zlibCompressionLevel:1_Filter:None 50,123,421 56.6%
PNG-zlibCompressionLevel:1_Filter:Sub 70,170,471 79.3%
PNG-zlibCompressionLevel:1_Filter:Up 70,233,842 79.3%
PNG-zlibCompressionLevel:1_Filter:Average 73,381,819 82.9%
PNG-zlibCompressionLevel:1_Filter:Paeth 73,695,459 83.2%
PNG-zlibCompressionLevel:1_Filter:Adaptive 50,123,421 56.6%
PNG-zlibCompressionLevel:2_Filter:None 48,121,881 54.4%
PNG-zlibCompressionLevel:2_Filter:Sub 69,617,041 78.6%
PNG-zlibCompressionLevel:2_Filter:Up 69,694,262 78.7%
PNG-zlibCompressionLevel:2_Filter:Average 73,186,607 82.7%
PNG-zlibCompressionLevel:2_Filter:Paeth 73,514,599 83.0%
PNG-zlibCompressionLevel:2_Filter:Adaptive 48,121,881 54.4%
PNG-zlibCompressionLevel:3_Filter:None 45,762,448 51.7%
PNG-zlibCompressionLevel:3_Filter:Sub 69,212,186 78.2%
PNG-zlibCompressionLevel:3_Filter:Up 69,302,919 78.3%
PNG-zlibCompressionLevel:3_Filter:Average 73,117,290 82.6%
PNG-zlibCompressionLevel:3_Filter:Paeth 73,435,376 82.9%
PNG-zlibCompressionLevel:3_Filter:Adaptive 45,762,448 51.7%
PNG-zlibCompressionLevel:4_Filter:None 46,391,838 52.4%
PNG-zlibCompressionLevel:4_Filter:Sub 74,083,886 83.7%
PNG-zlibCompressionLevel:4_Filter:Up 74,233,869 83.8%
PNG-zlibCompressionLevel:4_Filter:Average 74,659,692 84.3%
PNG-zlibCompressionLevel:4_Filter:Paeth 74,823,990 84.5%
PNG-zlibCompressionLevel:4_Filter:Adaptive 46,391,838 52.4%
PNG-zlibCompressionLevel:5_Filter:None 44,999,907 50.8%
PNG-zlibCompressionLevel:5_Filter:Sub 73,951,305 83.5%
PNG-zlibCompressionLevel:5_Filter:Up 74,107,287 83.7%
PNG-zlibCompressionLevel:5_Filter:Average 74,655,451 84.3%
PNG-zlibCompressionLevel:5_Filter:Paeth 74,817,224 84.5%
PNG-zlibCompressionLevel:5_Filter:Adaptive 74,531,163 84.2%
PNG-zlibCompressionLevel:6_Filter:None 44,036,396 49.7%
PNG-zlibCompressionLevel:6_Filter:Sub 73,860,189 83.4%
PNG-zlibCompressionLevel:6_Filter:Up 74,016,429 83.6%
PNG-zlibCompressionLevel:6_Filter:Average 74,651,823 84.3%
PNG-zlibCompressionLevel:6_Filter:Paeth 74,810,352 84.5%
PNG-zlibCompressionLevel:6_Filter:Adaptive 74,518,888 84.2%
PNG-zlibCompressionLevel:7_Filter:None 44,014,603 49.7%
PNG-zlibCompressionLevel:7_Filter:Sub 73,840,699 83.4%
PNG-zlibCompressionLevel:7_Filter:Up 73,996,650 83.6%
PNG-zlibCompressionLevel:7_Filter:Average 74,651,232 84.3%
PNG-zlibCompressionLevel:7_Filter:Paeth 74,808,491 84.5%
PNG-zlibCompressionLevel:7_Filter:Adaptive 74,514,525 84.2%
PNG-zlibCompressionLevel:8_Filter:None 44,007,922 49.7%
PNG-zlibCompressionLevel:8_Filter:Sub 73,816,440 83.4%
PNG-zlibCompressionLevel:8_Filter:Up 73,973,877 83.5%
PNG-zlibCompressionLevel:8_Filter:Average 74,650,272 84.3%
PNG-zlibCompressionLevel:8_Filter:Paeth 74,807,293 84.5%
PNG-zlibCompressionLevel:8_Filter:Adaptive 74,507,658 84.2%
PNG-zlibCompressionLevel:9_Filter:None 44,007,896 49.7%
PNG-zlibCompressionLevel:9_Filter:Sub 73,816,176 83.4%
PNG-zlibCompressionLevel:9_Filter:Up 73,973,585 83.5%
PNG-zlibCompressionLevel:9_Filter:Average 74,650,265 84.3%
PNG-zlibCompressionLevel:9_Filter:Paeth 74,807,268 84.5%
PNG-zlibCompressionLevel:9_Filter:Adaptive 74,507,678 84.2%

f:id:kachine:20151029154536p:plain
※所要時間は測定していないが、zlibの圧縮レベルが高いほど時間がかかった感覚がある。なお、激烈に遅いTIFFの圧縮にZIPを指定した場合よりはいずれも速かったと思う。

考察
  • フィルタ無しが最も高圧縮率であることが判る。
  • zlibの圧縮レベルが同一でもフィルタの選択次第で1.5倍程度も圧縮率が異なる。
  • フィルタ無しでzlibを使用して圧縮したPNGファイルは、ZIP圧縮を指定したTIFFファイルより、最大25%程度も高圧縮率。
  • zlibの圧縮レベルは高いほど圧縮率が高まるが、リニアではない。今回のInputではフィルタ無し圧縮レベル6~9で圧縮率0.1%の差しかない。
    • 所要時間を加味すれば無闇に高い圧縮レベルを指定する合理的理由は無い。
  • 圧縮レベルよりフィルタの選択がより重要。
    • 高速な低圧縮レベルで全フィルタパターンを試行し、最も効果的なフィルタパターンを特定した後、圧縮レベルを上げる運用が実用的と考えられる。

まとめ

  • ZIP圧縮を指定したTIFFより適切なオプションで圧縮されたPNGの方が高圧縮率
  • この結果だけを以てTIFFを捨てPNGに乗り換える決断をするのは拙速。
    • PNGにはEXIFが無く、ImageMagickで変換されたPNGには一部のEXIF情報がPNGメタデータとして記録されるが、欠落及び情報の変化*2が生じる。
    • EXIF情報が不要な用途においてはTIFFの代替としてのPNGは有力な選択肢。
      • 絵の場合は作品保存にPNGを利用して不都合はないが、写真の場合はEXIFの正確性をどこまで要求するかによる。
      • そもそも可逆圧縮に拘るような写真なら元のRAWデータを保持しているだろうから、正確なEXIFはRAWから抜けばいい。と、割り切れば写真保存にPNGを活用するのも有力な選択肢。
      • exiftoolで元ファイルのEXIF情報をPNGメタデータとして書き込む*3ことは一応できる。
  • 異なるInputデータの場合は全く異なる結果となる可能性があるが、ImageMagickのハフマン推しは何だったのか。
    • 今回のInputは空も地面も黒系の類似色の画素が大半を占めるので、そもそも検証用には不適切ではないか説。
    • 他の画像パターンでも追試してみる必要がある。

*1:ローカル環境で旧式の余剰マシンを使って行った作業なので事実上電気代だけ。

*2:ApertureのF2.8がF3になっていたりする

*3:exiftool -tagsfromfile source.tif target.png