dFdxとdFdyでわずか4行のお手軽エッジ検出!

dFdxとdFdyとは

GLSL1.1以降からdFdx/dFdyという組み込み関数が使えるのですが、そこまで使う機会がないため、使い方を知っている方は少ないかもしれません。

dFdx/dFdyはスクリーンのX方向・Y方向の偏微分を計算するフラグメントシェーダの関数です。

偏微分というと、難しそうな印象を受けるかもしれませんが、単なるスクリーン上の画素値(本当は任意の値)の差分です。

この記事では、dFdx/dFdyの使い方をエッジ検出の実例を踏まえて解説します。

なお、WebGLでdFdx/dFdyを使用するためには、gl.getExtension("OES_standard_derivatives")と拡張を有効にする必要があります。

dFdxとdFdyでお手軽エッジ検出!

dFdx/dFdyに関しては、難しいことを考えずに実例を見たほうが分かりやすいです。

今回はShadertoyで動作する動画のエッジ検出のシェーダを書きました。

完成版はコチラです。

160729-0007.png

次の節ではこのエッジ検出をステップごとに順を追って解説していきます。

1. 動画テクスチャのフェッチ

動画テクスチャも普通のテクスチャを同じようにtexture2Dでフェッチできます。

動画テクスチャのフェッチ
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    fragColor =  texture2D(iChannel0, uv);
}

160729-0002.png

2. グレースケースに変換

続いてグレースケールへ変換します。

length関数をつかって、3チャンネルのcolor.rgbを1チャンネルのgrayに変換します。

グレースケースに変換
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(gray), 1.0);
}

160729-0003.png

3. dFdyでY方向の偏微分をもとめる

ようやく本題のdFdx/dFdyです!

dFdy(gray)とすると、スクリーンのY方向に対して、grayを偏微分した値を求めることができます。

float result = dFdy(gray)とするだけで、スクリーンと同じサイズの別バッファにgrayの値を格納して、同時にresultgrayをY方向に偏微分した値が返ってくるイメージです。

dFdyでY方向の偏微分をもとめる
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(dFdy(gray)) * 5.0, 1.0);
}

160729-0004.png

4. XとYの両方向の偏微分をもとめる

Y方向のみだとX方向のエッジが検出できないので、X方向の偏微分も求めて、それぞれの結果を合成します。

X方向とY方向の両方の偏微分をもとめる
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(length(vec2(dFdx(gray), dFdy(gray)))), 1.0);
}

160729-0005.png

5. step関数で2値化

最後にstepという組み込み関数で結果を2値化します。

step(edge, x)は2引数の関数で、edgeが閾値、xが入力値です。要するに、step(edge, x)x < edge ? 0.0 : 1.0を返す関数です。

シェーダで分岐をするとパフォーマンスが落ちるので、stepなどの組み込み関数は積極的に使っていきましょう!

step関数で2値化
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(step(0.06, length(vec2(dFdx(gray), dFdy(gray))))), 1.0);
}

160729-0007.png

わずか4行のコードでエッジ検出ができました!とてもお手軽ですね。

番外編: dFdx/dFdyのその他の活用事例

エッジ検出以外のdFdx/dFdyの利用方法を軽く触れます。

頂点座標をスクリーン上でXY方向に偏微分を計算して、それらの外積を求めると法線となります。次の2つはどちらもdFdxとdFdyで法線を計算した実例です。

テクスチャーサンプリングの詳細レベルの計算(ミップマップのレベルの推定)や、異方性フィルタリングにおける異方性の軸に沿ったサンプリングのレベルの推定にも使用されます。ワールド座標をスクリーン上でdFdxとdFdyで偏微分することで、スクリーン上でのテクスチャのスケールを求めることができます。

アンチエイリアシングの実装において「変化の激しいピクセル」を抽出する際にも利用されます。エッジ検出と同じ要領でdFdxとdFdyを用いて変化の激しいピクセルを抽出した後、ピクセルを平均化するぼかし処理をすることでアンチエイリアシングが実現できます。

番外編: fwidthを使ったエッジ検出

上のエッジ検出のコードはfwidthによって、もっと短くできます!

fwidth(p)は、abs(dFdx(p)) + abs(dFdy(p))を返す関数です。

fwidthを使ったエッジ検出
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec4 color =  texture2D(iChannel0, uv);
    float gray = length(color.rgb);
    fragColor = vec4(vec3(step(0.07, fwidth(gray))), 1.0);
}

結果もほぼ同じですね。
160729-0008.png

まとめ

偏微分というと、とっつきにくい印象があるかもしれませんが、要するにピクセル単位の差分です。dFdx/dFdyがどういうものか、なんとなく分かっていただけたでしょうか?

フラグメントシェーダでは隣のピクセルの値を参照できないので、通常は偏微分を計算するためには2パス必要となってしまうのですが、dFdx/dFdyを使うと1パスで偏微分を計算できます!これは地味にすごいと思います1

みなさんもdFdx/dFdyを使いこなしていきましょう!


  1. 個人的にはdFdx/dFdyがどういう実装なのか気になるのですが、調べても良く分からなかったので、どなたか教えて欲しいです。結果をよく見ると2x2のブロックとなって、解像度が半分になっていますね。2x2のブロック単位でフラグメントシェーダがまとめて実行されて、dFdx/dFdyの箇所で同期待ちをしているのではないかと思っています。2x2のブロック内は同じ値が返ってくるので、ブロックノイズのようになるのでしょう。