分かる!画像補間 ~基本から応用まで~
2014/6/23
開発部の高山です。
1年が過ぎるのは早いもので、もう2014年もほぼ半分終わったと考えると恐ろしくなりますね。
今回のテーマは画像補間です。基本的な説明から独自で考えた手法までゆるく広く扱っていきます。
画像補間って何ぞや
画像の拡大・縮小や回転といった編集作業は日常皆さんがよく行っていらっしゃることだと思います。
その中で例えば拡大率の値は1.5倍とか2.3倍とかといった小数値でもきちんと処理してくれますよね。
でも、ちょっと待ってください。
画像の色情報は基本的にはピクセルごとにしか保存されていません。そして、そのピクセルの座標はもちろん整数値です。
では、求めたい点の座標が小数値で与えられた場合はどうやって色を計算するのでしょうか。これを解決するのが画像補間です。
画像補間では周囲のピクセルが持つ色情報から求めたい点の色を計算します。
このときに求めたい点と周囲のピクセルの距離に応じて重みをかけることで、
近いピクセルの影響が大きく、遠いピクセルの影響を少なくなるようにしています。
周囲のピクセルをどこまで見るのか、重みの式自体は手法ごとに異なりますが、それ以外の流れはどの手法でもほぼ同じです。
それでは、具体的にそれぞれの手法を見ていきましょう。
手法ごとの説明
・ニアレストネイバー(Nearest Neighbor)法
もっとも分かりやすい手法がこれ。最近傍補間とか最近隣補間ともいわれます。
原理は単純で、補間した点に最も近いピクセルの値をそのまま使います。
高速でかつ色数が変化しない所が利点ですが、その分クオリティは低めでギザギザが目立ちます。
特に文字を含む素材や実写の素材ではあまり使わない方がいいでしょう。
重みの式(w(x))は以下のようになっています。これに関しては式にすると逆に分かりにくいかもしれませんが。
|x|はxの絶対値を表しています。
・バイリニア(BiLinear)法
単純で、かつそれなりの結果が出せる手法がこれ。いわゆる線形補間です。
高クオリティを求めない場合は基本的にこれで十分です。
ただ、全体的にぼやけたようになってしまうので、輪郭を強調させたいアニメ調の絵などは苦手な傾向にあります。
・バイキュービック(BiCubic)法
よく用いられている手法の中で特に高クオリティな結果が出せるのがこれ。
ニアレストネイバーやバイリニアに比べると遠くまで見る分速度は落ちますが、
ぼやけもギザギザも少なく、色々な状況に対応できます。
重みの式は以下のようになります。
aの値はシャープさに影響しており、自由に決めることができますが、
実際にはa=-0.5(以下の式)やa=-1などを代入して用いることが多いようです。
グラフは以下のように。見てもらうと分かりますが、一部で負の値が出てきます。ごくまれですが結果がマイナスになる場合も。
・ランツォシュ(Lanczos)法
バイキュービック法と同等以上の高クオリティを期待できる手法がこれ。
ただし他の3手法と比べると実装されていないソフトも多めで、
さらに遠くのピクセルまで参照するため速度は明らかに遅いです。
重みの式は次の通り。nの数値によってどこまで見るのかが変わってきます。
一般的にはn = 2か3を使うケースがほとんどで、それぞれLanczos-2やLanczos-3とよばれます。
独自の式を考えてみる
大体の画像編集ソフトで実装されている手法は上で説明した4つのうちのいずれかです。
他にもガウス関数を使う手法やB-splineを使う手法などがあるらしいですが、あまり一般的ではありません。
さらにいえば特定の条件さえ満たせば(w(0)=1とか)重みの式は何でもいいはずなんですよね。
何でこれらの手法ばかり使われているのでしょうか。謎です。
ということで今回はバイキュービック法を下敷きに独自の重みの式を考えてみました。
・バイキュービック法改
上で説明したとおり、バイキュービック法はクオリティが高めの手法です。
一般的なバイキュービック法では4×4のピクセル情報を使って求めたい点の色を計算しています。
これを2×2とか6×6にするとどうなるのでしょうか。以下のような効果が期待できます。あくまで期待です。
2×2にする→一般的なバイキュービック法よりも高速で、ニアレストネイバーやバイリニアよりも高クオリティな結果に!
6×6にする→一般的なバイキュービック法よりも高クオリティな結果に!
ということで期待に胸を膨らましつつ、2×2バージョンと6×6バージョンの重みの式を導出してみました。
まず、2×2バージョンの式は以下のようになります。
6×6バージョンの式は次の通りです。
6×6バージョンの方は本来は重みの式が一意には決まりませんが、適当な値を代入しています。
(というか6×6バージョンの方は調べてみたらバイキュービック法の元論文(リンク)に式が載ってた…)
ではこれらも使って結果を比較してみましょう。
結果比較
今回使ったのは↓のDFのロゴです。これをそれぞれの手法で5倍に拡大してみます。
結果がこちら。上の画像は全体画像(クリックで原寸大)、下の画像は一部を抽出したものです。
Nearest Neighbor ニアレストネイバー法 |
BiLinear バイリニア法 |
BiCubic バイキュービック法 |
Lanczos-3 ランツォシュ-3法 |
ニアレストネイバ法ーだとギザギザに、バイリニア法だとぼやけがちになります。
では、バイキュービック法とバイキュービック法改の結果を比較してみましょう。
バイキュービック法改(2×2) | バイキュービック法(4×4) | バイキュービック法改(6×6) |
2×2の方はニアレストネイバーとバイリニアの間のような結果になっていますね。
もとのバイキュービック法より早く、場合によっては使うのもありかもしれません。
6×6の方はランツォシュ-3と似たような結果になっているでしょうか。
ランツォシュ-3よりも多少ですが早いのでこちらも場合によって使えるかも。
まとめ
結局用途によって手法を使い分けるのが一番、というありきたりな結論にいたるわけですが、
中味がどういう仕組みになっているのか知っておくと、状況に応じて最適な手法を選ぶことができるでしょう。
バイキュービック法改は汎用的ではないものの、場合によっては使うのもありといった程度でしょうか。
まあ全く使えないはずではないと思うので、試しに使ってみてください。
では、また~。
おまけ
今回の結果画像出力に使ったプログラムを一部載せておきます。
ただ、ランツォシュ手法で重みの合計で割る処理など省いている部分など完全に一致していない所があるので、
一般的なソフトと全く同じ結果になるとは限りません。
int METHOD_WIDTH[] = {2, 2, 2, 4, 6, 6}; float (*interpolation_func[])(float) = {NearestNeighbor, BiLinear, BiCubic1, BiCubic, BiCubic3, Lanczos3}; float clamp(float x, float min, float max){ if(x < min) x = min; if(x > max) x = max; return x; } int mirror(int x, int min, int max){ while(x < min || x >= max){ if(x < min) x = min + (min - x - 1); if(x >= max) x = max + (max - x - 1); } return x; } Color Pixel_Interpolation(Color *data, float x0, float y0, int width, int height, int method){ float wx[8], wy[8]; int px[8], py[8]; int mpx, mpy; float r, g, b; int MAX = METHOD_WIDTH[method]; for(int t=0;t<MAX;t++){ px[t] = (int)x0 + t - MAX / 2 + 1; py[t] = (int)y0 + t - MAX / 2 + 1; wx[t] = interpolation_func[method](fabs(x0 - px[t])); wy[t] = interpolation_func[method](fabs(y0 - py[t])); } r = 0.0f; g = 0.0f; b = 0.0f; for(int y=0;y<MAX;y++){ mpy = mirror(py[y], 0, height); for(int x=0;x<MAX;x++){ mpx = mirror(px[x], 0, width); r += data[mpx + mpy * width].R * wx[x] * wy[y]; g += data[mpx + mpy * width].G * wx[x] * wy[y]; b += data[mpx + mpy * width].B * wx[x] * wy[y]; } } return Color::FromArgb((int)clamp(r, 0.0f, 255.0f), (int)clamp(g, 0.0f, 255.0f), (int)clamp(b, 0.0f, 255.0f)); } float NearestNeighbor(float x){ if(x < 0.5f) return 1.0f; return 0.0f; } float BiLinear(float x){ if(x < 1.0f) return 1.0f - x; return 0.0f; } float BiCubic(float x){ if(x < 1.0f){ return 1.5f * x * x * x - 2.5f * x * x + 1.0f; } else if(x < 2.0f){ return -0.5f * x * x * x + 2.5f * x * x - 4.0f * x + 2.0f; } return 0.0f; } float Lanczos3(float x){ if(x == 0.0f){ return 1.0f; } else if(x < 3.0f){ return (float)(sin(PI * x) / (PI * x) * sin(PI * x / 3.0f) / (PI * x / 3.0f)); } return 0.0f; } float BiCubic1(float x){ if(x < 1.0f) return 2.0f * x * x * x - 3.0f * x * x + 1.0f; return 0.0f; } float BiCubic3(float x){ if(x < 1.0f){ return 4.0f / 3.0f * x * x * x - 7.0f / 3.0f * x * x + 1.0f; } else if(x < 2.0f){ return -7.0f / 12.0f * x * x * x + 3.0f * x * x - 59.0f / 12.0f * x + 5.0f / 2.0f; } else if(x < 3.0f){ return 1.0f / 12.0f * x * x * x - 2.0f / 3.0f * x * x + 7.0f / 4.0f * x - 3.0f / 2.0f; } return 0.0f; }
※免責事項※
本記事内で公開している全ての手法の有用性、安全性について、当方は一切の保証を与えるものではありません。
これらの手法を使用したことによって引き起こる直接的、間接的な損害に対し、当方は一切責任を負うものではありません。
自己責任でご活用ください
コメント
コメントフォーム