Chromeで小さなJPEG画像が違うように見える理由

ウェブブラウザのChromeで、小さなJPEG画像がなぜか他のブラウザと異なって表示される現象に気づいたことがあるかもしれません。これは単なるバグではなく、Chromeが高度なJPEGデコード最適化処理を採用していることが原因です。アイコンのような小さくシャープな画像で、顕著に表示の違いが見られる場合があります。
Guillaume Técher
https://guillaumetech.github.io/posts/jpg-scaling-chrome/
以下の図は実際の画像とは異なりますが両者の表示の違いをわかりやすくしたイメージで、左がFirefox、右がChromeでの表示を示しています。目を細めたり一歩下がったりするとChromeの方が太く見えます。奇妙なことに画像をSVGに置き換えると表示は等しくなりました。

一般的にJPEG画像を小さく表示する際、画像をメモリ上で完全に展開してから目的のサイズに縮小するという手順が考えられます。しかし、例えば2000x2000ピクセル(約12MB)の画像を20x20ピクセル(約1.2KB)に縮小する場合を考えてみると、大量のメモリが消費される割には最終的に使用される情報はごくわずかであるため非効率的だといえます。
縮小プロセスで失われるのはランダムな情報ではなく主に高周波情報のディテール、つまり細かい模様や急激な色の変化です。一例としてたくさんの葉と粗い樹皮を持つ木の画像を考えてみると、葉や樹皮といった細かい部分はピクセルからピクセルへと急速に変化するため高周波情報として捉えることができます。次に木の画像を20×10ピクセルのような小さなものにスケールダウンすると、葉の上部にある緑色の塊や幹の下部にある茶色の棒だけが情報として残ります。つまり縮小バージョンでは細部の高周波情報が捨てられているということになります。

JPEG圧縮の仕組みについて少しだけ技術的な話をすると、JPEG圧縮では画像を8x8ピクセルのブロックに分割し、離散コサイン変換(DCT)によって周波数領域に変換されます。8×8ピクセルのブロックにおいて最も低い周波数は単色です。厳密に言えば単色は何の変化もないため周波数ですらなく定数成分といえます。その対極にある最も高い周波数はチェッカーボードのように見えるものであり、値が最大限に変化します。その間のすべてが残りの周波数領域を表しており、基底関数と呼ばれます。つまり、8×8のブロックを周波数領域に変換するということは、基本的には「このブロックには各パターンがどの程度含まれているか」を問うことと同義であり、「どの程度含まれているか」の割合を係数と呼びます。

次に画像を8分の1サイズに縮小する場合を考えると、元の8x8ピクセルのブロックは縮小後の画像では1ピクセルに相当します。そして高周波ディテールは縮小によって失われるため、画像には低周波成分の情報のみが必要となります。Chromeはこの特性を利用し、画像全体を完全に非圧縮するのではなく高周波成分の係数をスキップし、粗いバージョンの画像に必要な低周波成分の係数のみを使用してデコードを行います。よって元の画像を完全に展開することなく縮小された結果を得ることができ、デコードに必要なメモリ量と時間を大幅に削減できます。この技術は「部分IDCTスケーリング(partial IDCT scaling)」と呼ばれ、特にターゲットサイズが元の画像の8分の1・4分の1・8分の3といった、分母が8の分数倍である場合に効果的です。ChromeはグラフィックライブラリSkiaを通じてlibjpeg-turboを利用しており、libjpeg-turboが部分IDCTスケーリングを実装しています。
Chromeはまず部分IDCTスケーリングを用いて画像を分母が8の最も近い分数を算出してデコードし、次に一般的なスケーリングアルゴリズムで最終的なサイズまで縮小します。この二段階のプロセス、つまり部分IDCTスケーリングで低周波成分のみが残された画像に対してさらにスケーリングアルゴリズムが適用されることで、エッジのソフト化やグラデーションといった処理は一切適用されず、画像が太く見えたりディテールが失われたりする表示の違いが生じるわけです。
結論として、部分IDCTスケーリングは写真のような画像に対しては効果的な最適化ですが、アイコンのようなシャープなディテールが求められる画像には適していません。JPEGフォーマットは人間の視覚が写真のディテールをどのように認識するかに基づいて設計されているため、アイコンのような用途では意図しない表示の劣化を引き起こす可能性があります。したがって、アイコンなどの用途にはJPEGではなくSVGなどの他の画像フォーマットを使用すべきです。
