laplacian フィルタ
今回のサンプルの実行結果
二次微分のエッジ検出
前回は sobel フィルタを用いたエッジの検出を行ないました。
カーネルと呼ばれる各ピクセルへの補正値のテーブルを適用することで、結果的に階調差の大きな部分を検出することができるのでしたね。サンプルでは sobel フィルタを適用した結果に対して、さらにグレイスケール変換を行なうこともできるようにしました。
前回のテキストでも触れたように、sobel フィルタは一次微分によるエッジ検出です。今回は、二次微分を用いたエッジ検出の手法である laplacian フィルタを実装してみます。
この laplacian (ラプラシアン)という言葉は、ラプラスという数学者の名前にあやかった呼称です。※ラプラスは物理学者や科学者でもありましたが……
先述の通り、laplacian フィルタは二次微分を用います。しかしこれも、sobel フィルタのときと同様、そんなに難しいものではありません。sobel フィルタは二つの方向で色差をあらかじめ求めておき、最終的に双方の結果を参照しながらフィルタリングを行ないました。laplacian フィルタはその性質上カーネルは一つで済みます。
laplacian カーネル
早速、laplacian フィルタのカーネルを見てみましょう。
laplacian フィルタのカーネル
ご覧のとおり、今回のカーネルには 0 の部分はありません。まず中心となるピクセルには大きなマイナスの補正が掛かることが見てわかると思います。そして、周囲 8 方向にはそれぞれ 1 が適用されます。
sobel フィルタのカーネルを思い出してもらえばわかると思いますが、sobel フィルタの場合はカーネルに設定した値によって、階調の差を検出する方向を一意に決めることができました。
たとえば、横方向の sobel フィルタのカーネルを例にとって考えると、左側と右側のどちらに負の係数を入れるかで方向を決められます。縦方向でもこれは同様で、上下のどちらに正負を設定するかで方向を定めることができるわけです。
一方、laplacian フィルタの場合は、先ほどのカーネルを見れば一目瞭然、処理対象となるピクセルの周囲を満遍なく 8 方向均等に参照します。これにより、sobel フィルタのように特定の方向に対しての階調差を求めるような特殊なエッジ検出はできません。
その代わりと言ってはなんですが、laplacian フィルタは sobel フィルタに比べ繊細で細い線によるエッジの検出ができます。
コードを見てみる
さて、それでは laplacian フィルタのシェーダプログラムを見てみましょう。
laplacian フィルタのフラグメントシェーダ
precision mediump float;
uniform sampler2D texture;
uniform bool laplacian;
uniform bool laplacianGray;
uniform float coef[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 destColor = vec3(0.0);
destColor += texture2D(texture, (fc + offset[0]) * tFrag).rgb * coef[0];
destColor += texture2D(texture, (fc + offset[1]) * tFrag).rgb * coef[1];
destColor += texture2D(texture, (fc + offset[2]) * tFrag).rgb * coef[2];
destColor += texture2D(texture, (fc + offset[3]) * tFrag).rgb * coef[3];
destColor += texture2D(texture, (fc + offset[4]) * tFrag).rgb * coef[4];
destColor += texture2D(texture, (fc + offset[5]) * tFrag).rgb * coef[5];
destColor += texture2D(texture, (fc + offset[6]) * tFrag).rgb * coef[6];
destColor += texture2D(texture, (fc + offset[7]) * tFrag).rgb * coef[7];
destColor += texture2D(texture, (fc + offset[8]) * tFrag).rgb * coef[8];
if(laplacian){
destColor = max(destColor, 0.0);
}else{
destColor = texture2D(texture, vTexCoord).rgb;
}
if(laplacianGray){
float grayColor = dot(destColor, monochromeScale);
destColor = vec3(grayColor);
}
gl_FragColor = vec4(destColor, 1.0);
}
ご覧のとおり、sobel フィルタと似た構成になっています。というより、カーネルが二つから一つになったことで、多少コードの量が少なくなっている感じです。
やっていることはほとんど変わっておらず、uniform 変数としてカーネルを float
型の配列で受け取り、 gl_FragCoord
を使って対象ピクセルの周囲 8 ピクセルを参照しています。
今回のサンプルでも、グレイスケール化ができるようになっており、uniform 変数として入ってきた bool
型の値に応じて処理が分岐するようになっています。
javascript サイドでは、カーネルを以下のようにして定義します。
javascript 側でのカーネルの定義
// laplacianフィルタのカーネル
var coef = [
1.0, 1.0, 1.0,
1.0, -8.0, 1.0,
1.0, 1.0, 1.0
];
単純な一次元の配列として、カーネルを定義しているのがわかりますね。これも、前回のサンプルと同様に uniform1fv
メソッドを使ってシェーダにプッシュします。
ほとんどが前回のコードそのままなので、もしわからない部分があれば過去のテキストを適宜参照してみてください。
まとめ
今回はちょっとしたフラグメントシェーダ等の変更だけでしたので、少々拍子抜けした感じがするかもしれません。
しかしこれを逆説的に考えれば、フラグメントシェーダに加える少々の変更で本当に多彩な表現を行うことが可能なのだとも考えることができます。フラグメントシェーダを用いたフィルタ系の処理は、その内容によっては非常に高負荷になってしまうこともありますが、本当に可能性は無限大だなぁと感じます。
先述の通り、sobel フィルタと比較するとエッジの検出は細い線で控えめな感じになる laplacian フィルタ。サンプルへのリンクはいつものように下記にありますので、実際に動作するサンプルを見てみてください。