PixelShaderを使った画像補間 (3) Lanczosフィルタによる補間

というわけで本丸(?)です。前回の続きです。

ちなみに、こういうネタを書いているのは、どちらかというとこういうシステムはできる限りグラフィックでの拡大処理をするルーチンを実装してほしいな~という思いもあります。

Lanczosフィルタの定義式

そもそも、Lanczosというのは補間方法ではなく、窓関数として機能するフィルタを補間方式として当てはめたものです。一種のLPFとして動作します。

また、Lanczosにはパラメータnが存在し、その値によってLanczos-nという呼び方をします。重みw(t)は

w(t)=¥begin{cases} sinc(t) ¥cdot sinc¥left( ¥frac{t}{n} ¥right) & (|t| ¥le n) ¥¥ 0 & (|t| > n) ¥end{cases}

ただし、

sinc(x) = ¥frac{¥sin(¥pi x)}{¥pi x}

とする。

という、sinc関数を使った補間を行います。画像の拡大や縮小で使用されるのはLanczos-2またはLanczos-3になり、今回はLanczos-3を使います。

Lanczos-3での補間で使われるのは6×6の範囲ですので、¥mathbf{P}は6次の正方行列になります。中心点は行列の要素では(2,2)ですね。

PixelShader(Lanczos-3)

解説もいらないと思いますのであっさりと全部貼り付けます。

struct VS_OUTPUT{
	float4 Pos : POSITION;
	float2 Tex0 : TEXCOORD0; //(0,-2)
	float2 Tex1 : TEXCOORD1; //(0,-1)
	float2 Tex2 : TEXCOORD2; //(0, 0)
	float2 Tex3 : TEXCOORD3; //(0, 1)
	float2 Tex4 : TEXCOORD4; //(0, 2)
	float2 Tex5 : TEXCOORD5; //(0, 3)
};
Texture Tex;
sampler Samp = sampler_state
{
	Texture = <Tex>;
	MinFilter = POINT;
	MagFilter = POINT;
	MipFilter = NONE;
	AddressU = Clamp;
	AddressV = Clamp;
};
//定数宣言
const float2 TexSize; //テクスチャサイズ
const float2 TexDiffU; //U方向の差分
//定数係数宣言
static float M_PI = 3.14159265358979324; //円周率
static float2 TexDiff = { 1.0, 1.0 }; //差分値
//PixelShader
float4 PS(VS_OUTPUT In) : COLOR
{
	float4 clr[6]; float2 w[6]; float2 p[6]; float2 t0,t1; int i;
	p[2] = -frac(In.Tex2 * TexSize); p[3] = p[2] + TexDiff;
	p[1] = p[2] - TexDiff; p[4] = p[3] + TexDiff;
	p[0] = p[1] - TexDiff; p[5] = p[4] + TexDiff;
	for(i = 0;i < 6;i++){
		t0 = p[i] * M_PI; t1 = t0 / 3.0;
		w[i].x = (t0.x != 0.0 ? ((sin(t0.x) / t0.x) * (sin(t1.x) / t1.x)) : 1.0);
		w[i].y = (t0.y != 0.0 ? ((sin(t0.y) / t0.y) * (sin(t1.y) / t1.y)) : 1.0);
		//w[i] = (t0 != 0.0 ? ((sin(t0) / t0) * (sin(t1) / t1)) : 1.0);
		p[i] = TexDiffU * ((float)i - 2.0);
	}
	clr[0] = tex2D(Samp,In.Tex0) * w[2].x;
	clr[1] = tex2D(Samp,In.Tex1) * w[2].x;
	clr[2] = tex2D(Samp,In.Tex2) * w[2].x;
	clr[3] = tex2D(Samp,In.Tex3) * w[2].x;
	clr[4] = tex2D(Samp,In.Tex4) * w[2].x;
	clr[5] = tex2D(Samp,In.Tex5) * w[2].x;
	for(i = 0;i < 6;i++){
		if(i == 2) continue;
		clr[0] += tex2D(Samp,In.Tex0 + p[i]) * w[i].x;
		clr[1] += tex2D(Samp,In.Tex1 + p[i]) * w[i].x;
		clr[2] += tex2D(Samp,In.Tex2 + p[i]) * w[i].x;
		clr[3] += tex2D(Samp,In.Tex3 + p[i]) * w[i].x;
		clr[4] += tex2D(Samp,In.Tex4 + p[i]) * w[i].x;
		clr[5] += tex2D(Samp,In.Tex5 + p[i]) * w[i].x;
	}
	clr[0] *= w[0].y;
	for(i = 1;i < 6;i++){
		clr[0] += clr[i] * w[i].y;
	}
	return saturate(clr[0]);
}

wは重み係数配列、pは重み係数配列を計算するまでは重み座標、計算後はテクスチャ座標のそれぞれの位置での差分値となるようにしています。

今回に限り、配列やforループなど制御構造を派手に使っています。

コンパイルすると配列やforループはできる限り展開して分岐処理はすべて削除されるので安心してください。

また、さすがにLanczos-3となると、テクスチャのサンプリング制限と命令スロット数の関係でPixelShader3.0が必要です。実際、このルーチンもかなり命令スロットを消費しています。

テクスチャの頂点数は6点になっています。対応はTex2を中心点にして縦方向の座標列です。

もしかするともう少し最適な頂点の取り方があるかもしれませんが、とりあえず動いたので問題なし、ということで・・・。

ちょっと面倒なのが、sinc関数を計算するときです。もちろんPixelShaderにsinc関数を計算する命令などないのでsin関数を使うわけですが、

sinc(0)において、sinc(0) = ¥frac{¥sin(0)}{0}というように0除算が出てきます。もちろん、sin関数の微分に必要な式から、

sinc(0) = ¥lim_{x ¥to 0}¥frac{¥sin(x)}{x} = 1と定義されているわけですが、その部分だけはif文で分岐を行い、不定形演算を防ぐ必要があります。

また、コメントで別方式での方法を示していますが、そちらの構文が正しいか自分としては自信が持てないので各成分ごとに演算を行う方式にしてあります。

コメント形式の場合、この記事に示してあるとおり、sincosを使う方式ではなく近似式を使う方式になります。

使用するDirectX側のコードは前回とほとんど同じ(TexDiffUは2.0*duではなくdu)ですので、前回の記事を参考にしてください。

・・・一応ちゃんと補間されているのを確認したのでこれであっているはずですが・・・。

3回分の記事も完了

ということで、3回に分けたPixelShaderを使った画像補間でした。

ちなみに、一応拡大だけではなく縮小でも同じ要領で補間処理を行うことはできますが、縮小の場合は線形補間で十分だと思います。

画素数が少なくなる方の補間にそれほど重い処理を突っ込む意味はないと思いますので。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この記事のトラックバック用URL