sobel フィルタ

実行結果

今回のサンプルの実行結果

エッジ検出

前回はフレームバッファを活用したセピア調への色変換を解説しました。一度、フレームバッファにシーンをレンダリングすることによって、シーン全体に色変換の処理を行うことができたのでしたね。

今回も、前回のセピア調変換や、前々回に行なったグレイスケール変換と同じような処理として、フレームバッファへのシーンのレンダリングを活用したフィルタ処理を行なってみます。今回のフィルタは sobel フィルタです。

sobel はソーベル、またはゾーベルと読むようです。恐らく由来は人名でしょうか。

sobel フィルタでは、色の諧調が極端に変化しているところ、つまりエッジの検出を行うことができます。エッジ検出には様々な手法がありますが、sobel フィルタは比較的エッジが強く検出されます。今回のサンプルでは、トーラスをレンダリングした結果に対して sobel フィルタを適用したものと、画像を読み込んでフィルタを適用したものとを表示するようにプログラムしてみます。

sobel フィルタの概要とシェーダ

sobel フィルタは一次微分を計算することで色の階調差を検出します。一次微分などと言われるとなにやら難しい数学の式が登場するのではと臆してしまいそうですが、実際にはそれほど難しくありません。

まず、これから処理しようとしているピクセルを中心として、八方向それぞれの色情報を取り出します。その後、取り出した各ピクセルの色情報に対し、以下のようなテーブル(これをカーネルと呼びます)に沿って値を補正します。

sobel フィルタの横方向カーネル

sobel フィルタ 横方向カーネル

実はこれは、sobel フィルタの横方向カーネルです。これと同様に、縦方向についても以下のカーネルに沿って色を補正します。

sobel フィルタの縦方向カーネル

sobel フィルタ 縦方向カーネル

これを見るとわかるとおり、本来処理しようとしている中心のピクセルには、0 が掛けられることになり、その代わり横方向であれば左右それぞれ 1 ピクセルずつずれた座標、縦方向であれば上下に 1 ピクセルずつずれた座標に、プラスとマイナスの係数が掛けられます。

この 3 x 3 のカーネルを適用する範囲 9 ピクセルが全て同じ色であれば、カーネルに沿って係数を掛けたとしても、結果的に全てのカーネルを合算したときに結果は 0 になりますよね。これを上下左右の方向で行うことで、結果的に階調の差が大きいピクセルを発見できるというわけです。

今回は、フラグメントシェーダ内でカーネルに沿った色の補正を行います。ただし、カーネルは uniform 変数の配列としてシェーダにプッシュするようにしますので、そのことを念頭に置いてシェーダのソースを見てみましょう。

sobel フィルタのフラグメントシェーダ

precision mediump float;

uniform sampler2D texture;
uniform bool      sobel;
uniform bool      sobelGray;
uniform float     hCoef[9];
uniform float     vCoef[9];
varying vec2      vTexCoord;

const float redScale   = 0.298912;
const float greenScale = 0.586611;
const float blueScale  = 0.114478;
const vec3  monochromeScale = vec3(redScale, greenScale, blueScale);

void main(void){
	vec2 offset[9];
	offset[0] = vec2(-1.0, -1.0);
	offset[1] = vec2( 0.0, -1.0);
	offset[2] = vec2( 1.0, -1.0);
	offset[3] = vec2(-1.0,  0.0);
	offset[4] = vec2( 0.0,  0.0);
	offset[5] = vec2( 1.0,  0.0);
	offset[6] = vec2(-1.0,  1.0);
	offset[7] = vec2( 0.0,  1.0);
	offset[8] = vec2( 1.0,  1.0);
	float tFrag = 1.0 / 512.0;
	vec2  fc = vec2(gl_FragCoord.s, 512.0 - gl_FragCoord.t);
	vec3  horizonColor = vec3(0.0);
	vec3  verticalColor = vec3(0.0);
	vec4  destColor = vec4(0.0);
	
	horizonColor  += texture2D(texture, (fc + offset[0]) * tFrag).rgb * hCoef[0];
	horizonColor  += texture2D(texture, (fc + offset[1]) * tFrag).rgb * hCoef[1];
	horizonColor  += texture2D(texture, (fc + offset[2]) * tFrag).rgb * hCoef[2];
	horizonColor  += texture2D(texture, (fc + offset[3]) * tFrag).rgb * hCoef[3];
	horizonColor  += texture2D(texture, (fc + offset[4]) * tFrag).rgb * hCoef[4];
	horizonColor  += texture2D(texture, (fc + offset[5]) * tFrag).rgb * hCoef[5];
	horizonColor  += texture2D(texture, (fc + offset[6]) * tFrag).rgb * hCoef[6];
	horizonColor  += texture2D(texture, (fc + offset[7]) * tFrag).rgb * hCoef[7];
	horizonColor  += texture2D(texture, (fc + offset[8]) * tFrag).rgb * hCoef[8];
	
	verticalColor += texture2D(texture, (fc + offset[0]) * tFrag).rgb * vCoef[0];
	verticalColor += texture2D(texture, (fc + offset[1]) * tFrag).rgb * vCoef[1];
	verticalColor += texture2D(texture, (fc + offset[2]) * tFrag).rgb * vCoef[2];
	verticalColor += texture2D(texture, (fc + offset[3]) * tFrag).rgb * vCoef[3];
	verticalColor += texture2D(texture, (fc + offset[4]) * tFrag).rgb * vCoef[4];
	verticalColor += texture2D(texture, (fc + offset[5]) * tFrag).rgb * vCoef[5];
	verticalColor += texture2D(texture, (fc + offset[6]) * tFrag).rgb * vCoef[6];
	verticalColor += texture2D(texture, (fc + offset[7]) * tFrag).rgb * vCoef[7];
	verticalColor += texture2D(texture, (fc + offset[8]) * tFrag).rgb * vCoef[8];
	
	if(sobel){
		destColor = vec4(vec3(sqrt(horizonColor * horizonColor + verticalColor * verticalColor)), 1.0);
	}else{
		destColor = texture2D(texture, vTexCoord);
	}
	if(sobelGray){
		float grayColor = dot(destColor.rgb, monochromeScale);
		destColor = vec4(vec3(grayColor), 1.0);
	}
	gl_FragColor = destColor;
}

今回のフラグメントシェーダはだいぶボリュームがありますね。

ただ、文字数が多いだけでそれほど難しいことはやっていません。落ち着いて見ていきましょう。

まず、今回のシェーダでは uniform 変数を配列として受け取っています。これは今までに登場したことのないテクニックですね。シェーダ内の変数定義を行なっている部分だけを抜粋してみます。

変数定義箇所を抜粋

uniform sampler2D texture;
uniform bool      sobel;
uniform bool      sobelGray;
uniform float     hCoef[9];
uniform float     vCoef[9];
varying vec2      vTexCoord;

さて、これを見るとわかるかと思いますが float 型で宣言されているふたつの uniform 変数が配列になっています。これは、横方向と縦方向のふたつのカーネルを受け取るために使います。

メインプログラムである javascript 側で、一次元の配列としてカーネルを定義しておき、それをシェーダにプッシュします。シェーダ内では、この送られてきたカーネルの情報を参照しつつ、各ピクセルの色に対して係数を掛けていきます。

以前のテキストでも解説したことがありますが、テクスチャの各テクセルを参照するためには GLSL の組み込み変数 gl_FragCoord を使います。この変数の s メンバには対象となるテクスチャの横幅がピクセル単位で、同様に t メンバには縦幅がピクセル単位で入っています。

あらかじめ vec2 型の配列(offset[])に 3 x 3 の範囲のテクセルにアクセスするためのオフセット値を入れておき、それとカーネルの値とを使って横方向と縦方向それぞれの階調差を算出しておきます。

最終的に縦横の色を正規化して出力すれば、これで sobel フィルタが完成です。今回は、そもそもフィルタリングを行なわないようにする処理と、フィルタを掛けたあとでグレイスケール化する処理とを uniform 変数として入ってきた bool 型の変数によって分岐させるようにしてあります。

ちなみに、このフラグメントシェーダと対になる頂点シェーダは以下のようになります。

attribute vec3 position;
attribute vec2 texCoord;
uniform   mat4 mvpMatrix;
varying   vec2 vTexCoord;

void main(void){
	vTexCoord   = texCoord;
	gl_Position = mvpMatrix * vec4(position, 1.0);
}

こちらは非常に簡易ですね。特別解説することも無いのですが、要はここの部分は前回までとまったく同じです。わからない場合には以前のテキストを適宜読み返してみてください。

javascript プログラム

さて、先ほどシェーダの解説を行なったところでも触れましたが、今回はシェーダに対して配列を送ってやる必要があります。ここは今までに登場したことのない部分ですので、注意深く見てみます。

まず、今回のプログラムの肝となるカーネルを定義している部分。

カーネルを一次元配列として変数に代入

// sobelフィルタのカーネル
var hCoef = [
	 1.0,  0.0, -1.0,
	 2.0,  0.0, -2.0,
	 1.0,  0.0, -1.0
];
var vCoef = [
	 1.0,  2.0,  1.0,
	 0.0,  0.0,  0.0,
	-1.0, -2.0, -1.0
];

いずれも、九つの要素を持つ純粋な一次元の配列です。これを最終的にシェーダに送ることになるわけですが、その部分も抜粋して見てみましょう。

配列をシェーダにプッシュする

gl.uniform1fv(oUniLocation[4], hCoef);
gl.uniform1fv(oUniLocation[5], vCoef);

ここで注目すべきは、たくさんの種類がある uniform 系メソッドのうち、なにが使われているのかという事実です。

たとえば行列をシェーダに送る際には uniformMatrix4fv を利用しますよね。三つの要素を持つベクトルを送るのであれば uniform3fv を使います。これらの uniform 系メソッドは、その名称につけられる数字と、f や i 、あるいは v などの文字によって用途を判別できます。

f は float の頭文字です。i は同様に int を表します。整数なのか、浮動小数点なのかによって f と i を使い分けるわけですね。さらに、データを配列として送る場合には v の添え字をつけます。

今回は float 型の値を配列に格納してシェーダに送りたいわけです。そのことから、利用すべき uniform 系メソッドは、f と v が付き、ひとつの要素を送ることから 1 が付く、と考えることができます。つまり uniform1fv を使ってシェーダにデータを送ればいいことがわかりますね。

こうして無事にカーネルの情報をシェーダに送ることができるようになります。それ以外は、あまり前回までのサンプルと変わりません。HTML 内の input 要素から値を取得し、フィルタリングを行なうかどうかを判断する部分など、細かな違いはあれどやっていることは前回までと同じです。

まとめ

今回のサンプルでは、トーラスをレンダリングし、そのレンダリングしたシーンに対して sobel フィルタを適用するバージョンのほか、あらかじめ読み込んでおいた画像をテクスチャとして、フィルタリングを適用した結果を表示させることができるようになっています。

ちなみにテクスチャとして適用する画像は以下のものを使用しました。

サンプル画像 1

サンプル画像1

サンプル画像 2

サンプル画像2

また、今回のサンプルではカラーでのフィルタ結果と、グレイスケール化したフィルタ結果とを任意に表示切替することが可能です。実際に動作するサンプルを見てフィルタリング結果を確認してみてください。

サンプルへのリンクはいつものように以下に。

entry

PR

press Z key