Subscribed unsubscribe Subscribe Subscribe

豪鬼メモ

一瞬千撃

ImageMagickのビット深度と画質劣化

写真のレタッチでよく言われるのが、加工すればするほど画質が劣化するという話である。画像処理の際の画質の劣化は量子化誤差に起因する。多くのレタッチソフトは各チャンネルの情報を16ビットで持つため、何か処理をするたびに、各ピクセルの各チャンネルの計算結果を0から65535までの16ビット値に丸めることになる。そういった操作を何度もやるとその誤差が蓄積されるというわけだ。例えば、20%グレーの値は13107だが、それにガンマ補正0.84をかけるとすると、(13107 / 65535)^(1/0.84) * 65535で 、9646.42になる。これは実際には9646に丸められて保持されるので、-0.42の誤差が発生する。65535分の-0.42なんて誤差は小さいようにも見えるが、この誤差は以降の演算で増幅されていく。

同じ問題がファイル保存形式のビット深度でも発生する。例えばJPEGは8ビット固定なので、画像処理の結果をJPEGに書き出すと、JPEG圧縮による劣化を無視したとしても、8ビット量子化の誤差が画質を大きく劣化させてしまう。しかし、ファイル保存形式の量子化誤差は、RAWから最終出力までの画像処理を同一ソフトウェアで一気にやってしまえば回避できる。ファイルに書き出すにしてもTIFFPNGなどのビッド深度を指定できる形式にして16ビットやそれ以上のビット深度を選べばよい。


さて、みんな大好きImageMagickの内部表現も、デフォルトでは16ビット深度である。したがって、多くの画像処理を繰り返すと画像の劣化が蓄積するはずだ。そこで量子化誤差の影響が実際どの程度なのかをここで把握しておきたい。ImageMagickのビルドオプションで内部表現のビット深度を8ビット、16ビット、32ビット、64ビット、HDRIと変えられるので、それらの比較をしてみよう。HDRIは、内部表現を浮動小数点数で持つ機能である。

このような実験を行う。黒から白までのグラデーションをまず生成し、それをガンマ補正で暗くしてから、逆のガンマ補正でまた明るくして元の画像に復元する。ガンマ値を変えながら、どの程度の補正まで耐えられるか見てみる。階段の如く断続的に色が変わるトーンジャンプやそれに伴って同一の色が並ぶバンディングが視認されると劣化が始まっているということが明確になる。例えばガンマ補正2.0ならこんなコマンドで確認できる。

convert \
  -size 48x1024 -colorspace RGB \
  'gradient:#000000-#ffffff' -rotate 90 \
  -gamma 0.5 -gamma 2.0 result.jpg

8ビットの結果。ガンマ2.0で既にバンディングを視認できる。
f:id:fridaynight:20160212220228j:plain

16ビット。ガンマ4.0からバンディングを視認できる。
f:id:fridaynight:20160212220239j:plain

32ビット。なぜか16ビットと全く同じ結果。
f:id:fridaynight:20160212220252j:plain

64ビット。ガンマ10でも全くバンディングが見られない。
f:id:fridaynight:20160212220302j:plain

HDRI。ガンマ10でも全くバンディングが見られない。
f:id:fridaynight:20160212220313j:plain

ガンマ補正2.0くらいかけて明るくするってのは普通にあり得るので、8ビットはちょっと使い物にならないかな。JPEGファイルを加工することの罪深さがよくわかる。一方でImageMagickのデフォルトである16ビットだと、ガンマ4.0程度までは耐えられるので、実用上問題ないと思う。ただし、HDR合成とか強烈なことをやりだすと16ビットでは不足があると思う。32ビットが16ビットと同じ結果なのは不思議で、ビルドミスかと思って確認したけど、ミスではない。使ったバージョン6.9.3-3はだが、この挙動はバグなのか仕様なのか。64ビットとHDRIに関しては、どんなに激しい操作を行っても量子化誤差による視認できる劣化は起こらないと言えそうだ。

ちなみにガンマ補正2.0や逆の0.5ってのはどんな補正がというと、こんな感じだ。ガンマ2.0、元画像、ガンマ0.5の順に並べる。
f:id:fridaynight:20160212220329j:plain
f:id:fridaynight:20160212220339j:plain
f:id:fridaynight:20160212220354j:plain

画質の観点で言えば明らかに64ビットやHDRIを使うべきなのだが、もちろん欠点もある。ビット深度に比例した作業メモリが必要になることと、各種画像処理が遅くなることだ。作業メモリに関しては自明なので、ここでは処理速度の計測をしてみる。

convert \
  -size 3000x2000 -colorspace RGB 'gradient:#000000-#ffffff' \
  -gamma 0.3 -gamma 3.0 result.jpg

上の処理を10回試行して最短の経過時間を測ったところ、以下の結果になった。

  • 8ビット: 1.526秒
  • 16ビット: 1.758秒
  • 32ビット: 1.798秒
  • 64ビット: 4.275秒
  • HDRI: 3.220秒

なお、実行環境は、諸事情により、さくらVPSの2GBプランである。3GHzの手元のマシンでやったら16ビットで0.464秒だった。

以上の結果により、HDRIがかなり有望だということがわかる。作業メモリのサイズからしておそらくfloatでデータを保持する実装になっているっぽい。6000x4000のRGBデータを保持すると作業メモリは6000*4000*3*4で288MBくらいになるはずで、JPEG変換時には入力と出力のバッファが必要だからその2倍を食うはずだ。実際にプロセスが使用するメモリは750MBくらいだったので、まあそんなもんだろう。24Mピクセルのデジカメの写真を750MBのメモリで処理できるのなら、メモリ使用量を気にすることはほとんどないはずだ。処理時間は2倍かかるが、バッチ処理で回す分には問題にならないことが多いだろう。

実際の写真を使ってガンマ補正0.25してからガンマ補正4.0する例を見てみよう。これは現像したまま元画像。
f:id:fridaynight:20160213000504j:plain

8ビット。絵が破綻しているのが一目でわかる。
f:id:fridaynight:20160213000513j:plain

16ビット。一見、劣化していないように見えるかもしれないが、子供の影の部分などのシャドー部は破綻している。
f:id:fridaynight:20160213000531j:plain

HDRI。これは元画像と完全に区別がつかない。
f:id:fridaynight:20160213000545j:plain

とはいえ、さすがにガンマ4.0もの補正が必要な失敗写真を救いたくなることはない。実際は2.0くらいだろう。その際に16ビット版とHDRI版の区別がつくかというと、お恥ずかしながら自分はほとんど区別つかない。しかし、劣化を気にしなくてすむという精神衛生上の利点と、弁別できない微妙な階調の差も無意識化では実は立体感の醸成に影響しているかもしれないので、HDRIを使いたくなってしまう今日このごろ。ImageMagick 7からはHDRIがデフォルトになるらしいし。