mdiapp さんの「人生やりなおし機が欲しい」 で、
「バイラテラルフィルターって何?」ってのがあって、見てみると速攻で実装できそうだったので作ってみました。
画像処理実験室の記事を参考にさせていただいています。
今回のプログラムは、次のものです。
まぁ、いつものように適当にファイルが入っています。 大事な部分だけを抜粋すると、次のファイルだけに注目すればいいことになります。
main.cpp | メイン関数 |
main.fx | シェーダプログラム |
他にも、実行ファイル、リソースファイル、プロジェクトファイルが入っています。
Bilateral Filter は1988 年にTomashi とManduchi により提案されたエッジ保持平滑化フィルタで、 画像をスムーズにしつつもエッジ部分はぼかすことのない効果を持っているらしいです。
ガウスぼかしをする際に、画素間の距離で重みを決めるのではなく、 輝度の差も見て、変化が大きいところは重みを小さくすることによってエッジを残します。
式にすると、一般の場合には、ある画素iのフィルタの後の色fiは、次のように 位置に移動する重みwxと、輝度に依存する重みwdで合成する感じになります。
ぼかしの重みにガウス分布を用いると。それぞれの重みは次のようになります。
まぁ、exp の前の係数は、分母で割ることを考えるといらないけどね。
分散(の平方根)であるσxやσdは、ぼかす幅を決定するパラメータで、 距離や輝度差がσxやσdの時に、重みは exp(-0.5) = 0.60653066 と、元の値の半分ぐらいになるので、 重みの大体の幅を決めるものと考えることができます。
フィルタの幅をあらわす分散が小さいほど狭い範囲だけフィルタが係り、 大きくなると全体的にぼけたように合成されます。
x軸(横方向)にテクセルの差、y軸(奥方向)に輝度の差をとって、z軸(上方向)を合成の重みとします。
重みが大きいほうが合成結果に強く影響を及ぼします。
ガウスぼかしの時は、輝度によらず重みを合成するので、下のような感じで、輝度方向は変化しない重みになります。
バイラテラルフィルタでは、これが輝度によっても変わるので、重みのグラフは次のようになります。
エッジを強く残したいときは、輝度の差による重みの変化を大きくすれば(分散を小さくすれば)よく、次のようなグラフになります。
輝度の差の分散を大きくすれば、ガウスフィルタに近づいていきます
(または、フィルタのテクセルの幅を小さくして少しだけぼかすとき)。
たとえば、σx=1, σd=2の時は下のようなグラフになります。
元画像がこんなだったとします。
バイラテラルフィルタで距離の分散を大きく(σx=5)して、輝度の分散を小さく(σd=0.2)すると次のようになります。
ぼやけるところはぼやけていますが、境界部分などは比較的はっきりしています。
ちなみに全体的にぼかしたとき(σd=1, σx=5)は、下のようになり、
距離の分散を小さく(σx=0.01)すると全体的にはっきりします(σd=1)。
まぁ、でもこのサンプル画像はよくないかもね。
2パスで17x17テクセルのガウスぼかしのプログラムを変更することによって実装をしてみました。
中心以外のテクセルは、隣接したテクセルの平均値を使って2テクセル同時に計算しています。
1パス目で横方向にぼかします。
重みの和をアルファ成分に格納して、ぼかしの2パス目の出力時にアルファで割ることによって
重みの正規化をしています(だから浮動小数点バッファが必要です)。
したがって、1パス目の後の画像は全体的な輝度がおかしくなっています。
2パス目の縦方向にぼかした結果が次のものです。
ガウスぼかしと違うのは、輝度の差で重みを調整しているということです。
今回は、輝度の差の2乗を使うということで、RGBの各成分の色の差を内積命令で合成して使っています。
1パス目のシェーダプログラムは次のようなもんです。
この前のもと素材の生成時には、アルファ成分に1を書き込むようなレンダリングをしています。
main.fx 141: float4 RenderGaussX( in float2 OriginalUV : TEXCOORD0 ) : COLOR 142: { 143: float4 Color; 144: 145: half4 t00 = tex2D( MeshTextureSampler, OriginalUV ); 146: half4 t10 = tex2D( MeshTextureSampler, OriginalUV + half2(vBias.x, 0)); 147: half4 t11 = tex2D( MeshTextureSampler, OriginalUV - half2(vBias.x, 0)); 148: half4 t20 = tex2D( MeshTextureSampler, OriginalUV + half2(vBias.y, 0)); 149: half4 t21 = tex2D( MeshTextureSampler, OriginalUV - half2(vBias.y, 0)); 150: half4 t30 = tex2D( MeshTextureSampler, OriginalUV + half2(vBias.z, 0)); 151: half4 t31 = tex2D( MeshTextureSampler, OriginalUV - half2(vBias.z, 0)); 152: half4 t40 = tex2D( MeshTextureSampler, OriginalUV + half2(vBias.w, 0)); 153: half4 t41 = tex2D( MeshTextureSampler, OriginalUV - half2(vBias.w, 0)); 154: 155: // 中心との色の差 156: half3 d10 = t10.rgb - t00.rgb; 157: half3 d20 = t20.rgb - t00.rgb; 158: half3 d30 = t30.rgb - t00.rgb; 159: half3 d40 = t40.rgb - t00.rgb; 160: half3 d11 = t11.rgb - t00.rgb; 161: half3 d21 = t21.rgb - t00.rgb; 162: half3 d31 = t31.rgb - t00.rgb; 163: half3 d41 = t41.rgb - t00.rgb; 164: 165: // 中心との色の強さの差の2乗 166: half4 l0, l1; 167: l0.x = dot(d10, d10); 168: l0.y = dot(d20, d20); 169: l0.z = dot(d30, d30); 170: l0.w = dot(d40, d40); 171: l1.x = dot(d11, d11); 172: l1.y = dot(d21, d21); 173: l1.z = dot(d31, d31); 174: l1.w = dot(d41, d41); 175: 176: l0 = weight * exp(coeff_l * l0); 177: l1 = weight * exp(coeff_l * l1); 178: 179: Color = t00; 180: Color += l0.x * t10 + l1.x * t11; 181: Color += l0.y * t20 + l1.y * t21; 182: Color += l0.z * t30 + l1.z * t31; 183: Color += l0.w * t40 + l1.w * t41; 184: 185: return Color / 4;// 全部足すと精度が足りないようなので、適当に割っとく 186: }
最初、適当に作ったら、ps 2.0に収まらなかったので、係数をベクトルにするなどの最適化をしているので、見づらくなっています。すまん(まぁ、テクスチャ座標の計算を最適化しろとの声もありますが…)。
2パス目のシェーダは次です。
main.fx 199: float4 RenderGaussY( in float2 OriginalUV : TEXCOORD0 ) : COLOR 200: { 201: float4 Color; 202: 203: half4 t00 = tex2D( MeshTextureSampler, OriginalUV ); 204: half4 t10 = tex2D( MeshTextureSampler, OriginalUV + half2(0, hBias.x)); 205: half4 t11 = tex2D( MeshTextureSampler, OriginalUV - half2(0, hBias.x)); 206: half4 t20 = tex2D( MeshTextureSampler, OriginalUV + half2(0, hBias.y)); 207: half4 t21 = tex2D( MeshTextureSampler, OriginalUV - half2(0, hBias.y)); 208: half4 t30 = tex2D( MeshTextureSampler, OriginalUV + half2(0, hBias.z)); 209: half4 t31 = tex2D( MeshTextureSampler, OriginalUV - half2(0, hBias.z)); 210: half4 t40 = tex2D( MeshTextureSampler, OriginalUV + half2(0, hBias.w)); 211: half4 t41 = tex2D( MeshTextureSampler, OriginalUV - half2(0, hBias.w)); 212: 213: // 中心との色の差 214: half3 d10 = t10.rgb - t00.rgb; この間は1パス目と同じ 241: Color += l0.w * t40 + l1.w * t41; 242: 243: return Color/Color.a; 244: }
まぁ、exp が重いと思う人は、テーブル化するなり(区分)線形化すると良いのではないでしょうか。
輝度の差に関する係数 coeff_l は、あらかじめ符号などに関して前処理をします。
main.cpp 153: static void SetGaussRadius() 154: { 155: const float range = 1.0; 156: SetGaussWeight(g_fRadiusX); 157: 158: D3DXVECTOR4 width(1.5f*range/SCREEN_WIDTH, 3.5f*range/SCREEN_WIDTH, 5.5f*range/SCREEN_WIDTH, 7.5f*range/SCREEN_WIDTH); 159: D3DXVECTOR4 height(1.5f*range/SCREEN_HEIGHT, 3.5f*range/SCREEN_HEIGHT, 5.5f*range/SCREEN_HEIGHT, 7.5f*range/SCREEN_HEIGHT); 160: 161: if(g_pEffect) 162: { 163: g_pEffect->SetVector("vBias", &width); 164: g_pEffect->SetVector("hBias", &height); 165: g_pEffect->SetFloat("coeff_l", -0.5f/(g_fRadiusL*g_fRadiusL)); 166: } 167: }
また、テクセル間の距離に関する重みは、前もって計算しておきます。
main.cpp 135: static void SetGaussWeight(float radius=1.0) 136: { 139: float dispersion = radius * radius; 140: const unsigned int WEIGHT_MUN = 4; 141: float tbl[WEIGHT_MUN]; 142: DWORD i; 143: 144: for( i=0; i<WEIGHT_MUN; i++ ){ 145: // 2テクセルまとめて処理するので2倍 146: tbl[i] = 2.0f * expf(-0.5f*((2.0f*(FLOAT)i+1.5f)*(2.0f*(FLOAT)i+1.5f))/dispersion); 147: } 148: 149: if(g_pEffect) g_pEffect->SetVector("weight", (const D3DXVECTOR4 *)tbl); 150: 151: }
はやりもののフィルタらしいですねぇ。
何度もかけるとトゥーンシェーディングになるようです。
これ自体で、云々はないのかもしれないのですが、画像処理的には使えそうな感じがします。