画像リサイズ処理の個性についてのうんちく話です。(2019年06月04日 投稿)
はじめに
ビットマップ画像は表示する状況に応じてさりげなくリサイズ処理するシーンが多々あります。
そのリサイズ結果をよく眺めると、同じ画像を同じサイズ(width x height)指定でリサイズしても、アプリの種類やその時々の状況で異なる画像が生成される事に気づくでしょう。
その差異になり得る要素を紹介します。
- アスペクト比と内接外接
- 補間フィルタ
- iDCT scaling
- コーナー合わせ
- パレット色維持
- ガンマ補正とリニアRGB
- 内部ビット深度
- USM(アンシャープマスク)
- 超解像
項目が多すぎるので詳細を書かずに、各々の観点のさわりだけ紹介します。
アスペクト比と内接外接
四角形の横と縦のサイズ比をアスペクト比と呼びます。
任意のサイズの画像を固定サイズの表示枠に当てはめるといったケースでアスペクト比が変わる事があり、元画像よりも横長又は縦長になるのは都合が悪いでしょう。
今時の SNS プロフィール画像のように画像アップロード時に画像編集画面を出して、手動で決まったアスペクト比にして貰うのも手ですが、そういった融通の効かないシーンもあります。
この対処方として、主に内接と外接の2つが利用されます。
内接
指定したサイズの内側に画像を治める方式です。
横長になる場合と、縦長になる場合、各々3通り考えるとして、大雑把に6通りのケースに分けられます。
あと、実際の画像に応じて微調整もあり得ます。
画像の無い隙間領域にどんな画素を埋めるかについても、いくつかパターンがあります。
透明に出来れば話が早いですが、JPEG のように透明度が扱えない画像形式もありますし、PNG だとしても透明度を入れるとデータ量が増えるので、何かしら色を入れたくなります。
透明色 | 背景色(黒とか) | エッジリピート | タイリング | ミラー(リフレクト) |
---|---|---|---|---|
状況に応じて使い分けましょう。
外接
画像を枠でクロップ(切り取り)する方式です。
横長になる場合と縦長になる場合で、中央に寄せたり、左右/上下どちらかに寄せます。
画像が注視されそうな点(プロフィール画像なら顔の場所)を推定して、そこが残るように切り取る賢いシステムもあります。
Content aware image cropping |
---|
https://github.com/jwagner/smartcrop.js |
この issue での説明が分かりやすく、 (1)彩度が高い (2) 輝度エッジ(sobelフィルタ)が多い (3) 肌色(変更可能)に近い。の3つのスコアが高いところを中心に残すようです。
補間フィルタ
画像の拡大/縮小は、各ピクセルの仕切り直しと考える事もできます。
src | resample | dst | |
---|---|---|---|
拡大 | |||
縮小 |
拡大時にこの白い隙間に色をどう埋めるか、縮小では周囲のピクセルをどう混ぜるかによって、リサイズ処理の結果が変わってきます。
色を埋める処理を補間フィルタ、その具体的なアルゴリズムを補間メソッドと呼びます。
概念的にこの補間メソッドは2次元の特定サイズ領域(カーネル)の畳み込みですが、補間メソッドの多くはセパラブルフィルタで、1次元の畳み込み処理で済む事が期待できます。
セパラブルフィルタ
リサイズの補間フィルタは本来2次元ですが、横と縦で別々にリサイズしても見た目がほぼ変わらないので、1次元で2回リサイズする事が殆どです。
src | horizontal | vertical | |
---|---|---|---|
拡大 | |||
縮小 |
このようにフィルタを別フェーズに分けられるのをセパラブルと表現します。例えば、blur(ぼかす効果)を実現するガウシアンフィルタも横と縦を別に処理ができるのでセパラブルフィルタです。
縦と横どっちを先にするか
計算誤差があるので、縦横どちらを先に処理するかでわずかに結果は変わります。
例えば有名なリング画像(1000x1000)を、横=>縦で縮小した画像と、縦=>横で縮小した画像はこんな感じ(1,2枚目)、2Dconv した画像から差分をとったらこうなる(3,4枚目)
— \助けよや/ (@yoya) 2018年8月22日
ただ強調しないと目に見えない程度の差異ですので、メモリアクセスの効率が良い方を選んだ方が良いでしょう。
具体的な実装としては、以下のような流派が見られます。
- 横を先に処理する。次に縦。(Python-Pillow)
- より縮小率の高い方を先に処理する (ImageMagick 5,6)
- 縦が広がる場合は横を先にする。(ImageMagick 3,4)
補間カーネル
拡大でも縮小でも周囲のピクセルを適度な割合で混ぜて補間を行います。混ぜる割合の重み付け付け数列は2次元の行列として表現できて、一般に補間カーネルと呼びます。
大抵はセパラブルフィルタなので、その補間カーネルは1次元でも表現できます。参考までに代表的なメソッドを並べます。
Nearest-Neighbor | Bi-Linear |
---|---|
Bi-Cubic (b:1/3,c:1/3, mitchell) | Lanczos (lobe:4, lanczos4) |
これら以外にも沢山の補間メソッドが提唱されていて、選んだメソッドによってリサイズ結果がだいぶ変わります。リサイズ結果が期待したのと違う大抵の場合は意図しないメソッド選択が原因です。
補間フィルタの詳細はページに分けたので、こちらを参考にどうぞ。
- 画像リサイズのうんちく (補間フィルタ)
補間フィルタ粒度
補間メソッドの Nearest, Bi-Linear はともかく、Bi-Cubic, Lanczos 等は計算が重たいので、あらかじめ、カーネルのサポート範囲分の数列を計算してテーブルに保存し、それをルックアップして使います。
例えば、ImageMagick の場合は 0.01 刻み(=100分割)のテーブルを作ります。
% convert -define filter:verbose=1 rose: -resize 1x1 null:
# Resampling Filter (for graphing)
#
# filter = SincFast
# window = SincFast
# support = 3
# window-support = 3
# scale-blur = 1
# practical-support = 3
0.00 1
0.01 0.999817
0.02 0.999269
0.03 0.998356
<略>
2.99 1.11837e-05
3.00 4.996e-16
3.00 0
このように ImageMagick は 0.01 単位の固定した LUT 粒度を持ちます。
一方、OpenCV や Python-Pillow、あと FFmpeg(swscale) では画像のサイズに応じて LUT の粒度を変えます。
特に、OpenCV(opencv-4.1.0/modules/imgproc/src/resize.cpp) と Python-Pillow(Pillow-6.0.0/src/libImaging/Resample.c) はサイズ後の横幅や縦幅分の LUT 粒度になるので、画像サイズが大きいとちょっと勿体無い気もしますし、画像毎に LUT を作り直す必要があるので高速化の余地がありそうです。
FFmpeg の swscale は実装コード(FFmpeg/libswscale/utils.c)を読む限り恐らく dstW/srcW(+キリが良いよう調整)のようです。
当然、この粒度によって計算結果が微妙に変わります。
端っこのピクセル
Bi-Linear を含む大抵のフィルタは、端っこのピクセルを混ぜる時に範囲(いわゆるフィルタ窓)が画像の外にはみ出ます。
代表的な2つを紹介します。
画像の枠で窓をクリップする
畳み込む対象を画像内に絞る方式です。
エッジの色が劣化しがちです。
ImageMagick や PIL がこれです。
画像の外をエッジリピートしてるものとして扱う
画像の外はそのエッジの色がそのまま続いてると仮定する方式です。
元画像 | エッジリピート |
---|---|
4ピクセルの画像だとこんなイメージです。
画像 | エッジリピート |
---|---|
先程の 109 から 127 に増えて、ほんの少し明るいです。
つまり、エッジの色が残る傾向があります。OpenCV がこれのようです。
ただし、エッジリピートでない画像、例えばタイル上に並べるつもりの画像だと違和感が生じます。
その他
画像の外をタイリングする。ミラーする。画像の外がどうなっているのか仮定の置き方次第で、色々な方法があります。
元画像 | エッジリピート | タイリング | ミラー(リフレクト) |
---|---|---|---|
これらのシチュエーションに応じた処理をするべきですが。
実際のところ難しいので、先に紹介した2つの方式のどちらかに決め打ちする事が殆どです。
iDCT scaling
JPEG のように、画像のピクセルデータを周波数成分で記録する画像フォーマットの場合、デコードと同時に拡大や縮小が可能です。
詳しくはこちらで。
- JPEG の size hinting について
(c) https://www.cl.cam.ac.uk/teaching/1011/R08/jpeg/acs10-jpeg.pdf |
---|
なお、実装アルゴリズムの都合と、縮小時に必要な LPF 閾値の都合で、2の冪乗スケーリングに対して機能します。キリの悪いリサイズをする場合は、2の冪乗である程度縮小/拡大して、そこから普通(Bi-Cubic とか)のリサイズを行う事になるでしょう。
コーナー合わせ
画像を2倍に拡大するだけの単純な話でも、具体的なピクセルの広げ方に幾つかのパターンが考えられます。
例えば左上のピクセルを基準に素直にピクセルを2倍の距離で広げると、右下が余ります。
src | resample | extend | dst |
---|---|---|---|
この偏りをなくすのに、ピクセルのオフセットを半分ずらし真ん中をベースにするおと、4隅のピクセルを合わせる方法があります。
4隅のピクセルを合わせる場合は画像の縦横サイズの拡大率と、ピクセルの広がり方が違ってくるので注意が必要です。
以下の「4隅を合わせる」例では縦横サイズは2倍で、ピクセルの広がりは 2.5倍となります。
src | 左上ベース | 真ん中ベース | 4隅を合わせる |
---|---|---|---|
2x2サンプル |
---|
世間にある実装としては、PIL(Pillow)の古いバージョンが左上合わせ、最近の PIL(Pillow)は真ん中ベース、ImageMagick も真ん中ベースを採用しています。TensorFlow は align_corners フラグで左上ベースか、四隅を合わせるかのスイッチが出来ます。PyTorch の nn.Upsample も align_corners フラグがありますが、真ん中ベースと、四隅を合わせるかのスイッチで False の挙動が TensorFlow とは異なります。
内部ビット深度
大抵のビットマップ画像はビット深度 8ビット x 3(RGB)or4(RGBA) で保存します。
この 8 ビットのままで計算しても問題ない場合もありますが、何かしら追加の処理があると階調が潰れてしまう事があります。
例えば、ImageMagick はデフォルトで処理中画像が 16ビットです。ビルド時の指定で 8bit にも出来ますが、その場合、リニアRGBを使うのは危険になります。
ガンマ補正とリニアRGB
Webで一番普及している RGB の規格は sRGB で、この R,G,B 輝度は物理的な輝度とリニアな関係にありません。およそ 2.2 相当のガンマ補正がかかっています。
(c) https://ja.wikipedia.org/wiki/ガンマ値 |
この影響で、Bi-Linear や Bi-Cubic といった四則演算でR,G,B値を混ぜる補間メソッドを使う場合、ガンマ補正のかかったまま計算すると本来の輝度より暗くなります。
- Resizing with Colorspace Correction
sRGB のままリサイズ | gamma:1 にしてリサイズ |
---|---|
一度、ガンマ補正を解除して、リサイズを行い、改めてまたガンマ補正をかけ直すと、良い結果が得られます。
ガンマ補正の詳しくはこちらをどうぞ。
注意点として、ビット深度 8bit のままガンマ補正を解除してリニアRGBに戻すと、低輝度の階調が潰れてしまうので、ビット深度を12や16に増やすか浮動小数点で計算する等の対応が必要です。
USM (アンシャープマスク)
Nearest-Neighbor 以外の補間メソッド、例えば Bi-Linear や Bi-Cubic, Lanczos 等でリサイズ処理すると、拡大でも縮小でも元の画像に比べて大抵ぼやけて見えます。その対策として USM(アンシャープマスク)フィルタがよく使われます。ImageMagick の -unsharp オプションでは以下のような感じです。
original | -resize 50% | -unsharp 10x5+0.5+0 |
---|---|---|
なお、シャープ(sharpness)にするのに unsharp という用語を使うのは、アナログ写真の時代からある unsharp-masking (USM) に由来します。ぼかした上で反転した画像のマスクを作り、それを合成する事でボケを相殺します。
https://en.wikipedia.org/wiki/Unsharp_masking |
---|
パレット色維持
Nearest-Neighbor 以外の補間メソッドでリサイズ処理すると、リサイズ前の色と違う色がリサイズ後の画像に表れます。
例えば、GIF や PNG8(パレット形式PNG)で、パレットが変わると困る事があります。
Nearest-Neighbor
GIF や PNG8 等、パレット形式の画像をリサイズする時は、大抵 Nearest-Neighbor がデフォルトだと思います。
これらの画像で色が変わるのはとても面倒ですので。
元のパレット色に戻す
リサイズ前のパレットを抽出しておいて、リサイズ後の色を元のパレット色に変える。という方法もありえます。(実際にはあまり見かけませんが)
超解像
エッジベース
先に説明した補間メソッドは、周囲の全方向のピクセルを同様に混ぜるので、エッジがぼやけがちです。
エッジの方向を推定して、その方向に補間する事でぼやけないように出来ます。
ピクセルアートに相性が良いのと、実際ゲーム画面の拡大で使われる事から、Pixel Art Scaling と呼ぶ事も多いです。
ImageMagick では、特に単純な Scale2X algorithm を magnify オプションで実行できます。
- Scale2x
original | scale2x | scale4 |
---|---|---|
% convert -size 8x8 pattern:CrossHatch30 -virtual-pixel tile \
-magnify -magnify -magnify magnify_crosshatch.gif
pattern:CrossHatch30 | magnify_crosshatch.gif |
---|---|
更にもう少し高度な FCBI アルゴリズムについて、以前解説を書いた事があるので、参考にどうぞ
- エッジ判定型超解像アルゴリズム FCBI (Fast curvature based interpolation)
original | Nearest-Neighbor | Bi-Linear | FCBI(tm:12) |
---|---|---|---|
(c) https://twitter.com/myuton0407/status/693361955000549376 |
機械学習ベース
機械学習ベースの超解像については日進月歩の世界で、色んなところで解説されてるので、そちらに任せます。
元々、waifu2x (SRCNN), EnhanceNet-PAT, RAISR あたりから世間から注目を浴び始めた印象で、その後も ESPCN, SRGAN, ESRGAN, DRCN, DCSCN, etc.. 新しいモデルが次々と提唱されている終わりの見えない戦いが繰り広げられています。
DeNA さんのこちらの資料が読み応えがあったので、参考にどうぞ。
参考
- ImageMagick リサイズ補間アルゴリズム
- Pillow(PIL) の Image resize NEAREST の動きがバージョンで異なる
- 【図解】CSSだけで画像の縦横比を維持したサムネイルを表示する