真・東方縦画面化ツールに搭載したフィルタの中で、(3次の)B-Splineフィルタがもっとも軽くなっています。
しかし、実際に3次のB-Splineとバイキュービックを書いたことがある人なら分かると思いますが、この2つは各項にかかる係数が違うだけで、それ以外は全く同じ式です。では、なぜB-Splineフィルタが軽いのか…というのが今回の話。
各フィルタの補間関数をグラフ化すると次のようになります。
Bi-Cubic1はシャープネス0、Bi-Cubic2はシャープネス1に設定したときのバイキュービックフィルタです。(シャープネスの値は一般的なものではなく、私が実用的と判断した範囲に設定した値です)
この補間関数は、フィルタのインパルス応答に相当し、入力信号が出力にどのように影響するかを表しています。例えばバイキュービックフィルタでは、入力画素値と同じ位置では1.0(元の値と全く同じ)で、そこから離れるにしたがって影響力が減っていき、ちょうど1画素分離れた位置では影響が0になります。更にそこから2画素分離れた位置まではマイナスの影響をします。
※ここでの説明は全て1次元ですが、今回使用しているフィルタは全て「分離型フィルタ」というもので、2次元のフィルタをX方向とY方向の1次元フィルタに分けることができます。このようなフィルタでは、X方向に1次元フィルタをかけた後に、Y方向に1次元フィルタをかけることで、2次元のフィルタと同じ結果を得ることができます。
ただし、この概念は入力信号中心の考え方ですが、実際のプログラムでは出力信号中心の考え方が必要になります。そのためには、「畳み込み」という計算を行います。
畳み込みは、フィルタ関数の定義域の全ての値を取得し、フィルタ関数を掛けて足し合わせるという処理になります。今回のフィルタは全て定義域が-2〜2で幅が4なので、入力画像から4つのピクセルをサンプリングすることになります。連続する4点をサンプリングし、補間関数のちょうど1ずつ離れた位置の値を掛けて足し合わせます。
さて、この4つの曲線のうち、B-Splineだけがマイナスの領域がないことが分かると思います。B-Splineの軽さの秘密は、ここにあります。
GPUには基本機能としてバイリニアフィルタが搭載されています。バイリニアフィルタはX方向とY方向にそれぞれ2点ずつ合計4つのピクセルを、距離に応じた比例配分で混合します。比例配分は、p1*a+p2*(1.0-a) のような計算で、必ず係数の合計が1.0になります。この処理はよく使うので、テクスチャユニットに組み込まれていて、非常に少ないコストで利用することができます。(ちなみにバイリニアフィルタも分離型フィルタ)
畳み込み処理で2つのピクセルに注目したとき、例えばそれぞれに掛ける係数が0.1と0.4だったとすると、次のように変形できます。
p1*0.1+p2*0.4
= (p1*0.2+p2*0.8)*0.5
= (p1*0.2+p2*(1.0-0.2))*0.5
= (p1*a+p2*(1.0-a))*0.5 [a=0.2]
これはバイリニアフィルタでサンプリングした結果に、係数の合計値を掛ければ畳み込み処理の2点分を処理できることを表します。
現在のGPUは、計算は速いがテクスチャの読み込みは遅いという性質があります。そのため、多少計算が複雑になっても、テクスチャ読み込み回数を減らした方が速くなります。B-Splineフィルタはこのテクニックを使って、16点フィルタを4回のテクスチャフェッチで処理しています。
ただし、バイリニアフィルタの係数は0.0〜1.0に限定されるので、負の係数が混ざっているとこの方法は使えません。
3次のB-SplineフィルタのX/Yフィルタの符号を表にすると次のようになります。
| + / + | + / + | + / + | + / + |
| + / + | + / + | + / + | + / + |
| + / + | + / + | + / + | + / + |
| + / + | + / + | + / + | + / + |
全ての領域で符号が同じなので、この手法が使えることが分かると思います。
一方、バイキュービックフィルタやLanczos2は次のようになります。
| - / - | + / - | + / - | - / - |
| - / + | + / + | + / + | - / + |
| - / + | + / + | + / + | - / + |
| - / - | + / - | + / - | - / - |
正負の符号が混ざっているために、この手法が使えません。
2点に掛ける係数がどちらも負の値の場合は、係数の符号を反転してバイリニアフィルタで読み込み、その結果を再び反転することで同じ結果が得られます。四隅を省略して12点補間とした簡易版バイキュービックフィルタではこの方法を使い、符号が揃っている中央の4点、上下左右の2点ずつをそれぞれバイリニアフィルタで読み込み、計5回のテクスチャフェッチで処理しています。
簡易版バイキュービックフィルタがこの方法で軽量化に成功してしまったために、軽いフィルタが作れると思って実装したB-Splineとの差がほとんど無くなってしまいました。
ちなみに、修正版バイキュービックフィルタではこのような最適化は行っておらず、16回テクスチャを読み込んでいます。
ところで、上のグラフでBi-Cubic1とLanzos2がほとんど同じ曲線ですよね。もう少しパラメータを変えるともっと近くなります。出力画像を見てこの2つのフィルタの違いがよく分からないと思っていたのですが、グラフを描いてみて納得しました。
Lanzosを使うなら2ではなく3以上を使わないと意味がなさそうです。Lanczos3になると36点補間となり、一気に倍以上の処理量になるので、私のツールで採用する気はありません。(費用対効果が悪過ぎる)