■ 2014年07月26日 [OpenGL][GLSL] GLSL で画像処理 (2) 簡単なフィルタ
暑い
暑いです.めっちゃ暑いです.意味わからんくらい暑いです.しかしデマンド超過防止のために部屋の空調切ってたので,たまらずに冷却材を投入しました.売店のガリガリ君専用冷凍庫,半分が梨味で埋まってたように見えたんですけど,問題ないんでしょうか.そんなこと気にしてたら体温が上がりますね.梨味おいしいです.一本食べたら飽きるけど.
フィルタを書いてみる
昨日作ったサンプルプログラム
のフラグメントシェーダはこんな具合になってました.
#version 330 uniform sampler2DRect image; layout (location = 0) out vec4 fc; void main(void) { fc = texture(image, gl_FragCoord.xy); }
これで,こういう絵が出たとします.
これはテクスチャからサンプリングした色を,そのままカラーバッファの画素値として出力しています.そこで,サンプリングした色から,その右隣の画素の色を引いてみます.引き算の値が負だと黒と区別がつかないので,GLSL の組み込み関数 abs() で絶対値を取ります.
fc = abs(texture(image, gl_FragCoord.xy) - textureOffset(image, gl_FragCoord.xy, ivec2(1, 0)));
これだけで横方向に微分した画像が得られます.GLSL の組み込み関数 textureOffset() は,テクスチャ座標から画素単位にオフセットした位置の色をサンプリングします.
これに,上の画素との差を加えてみます.
fc = abs(texture(image, gl_FragCoord.xy) - textureOffset(image, gl_FragCoord.xy, ivec2(1, 0))) + abs(texture(image, gl_FragCoord.xy) - textureOffset(image, gl_FragCoord.xy, ivec2(0, 1)));
一応,エッジが検出できます.
平滑フィルタ
ということで,いろいろフィルタを書いてみます.たとえば平滑フィルタなら,textureOffset() を並べて足すだけです.
#version 330 // 3x3 移動平均フィルタ uniform sampler2DRect image; layout (location = 0) out vec4 fc; // 平均を求める void main(void) { fc = texture(image, gl_FragCoord.xy); fc += textureOffset(image, gl_FragCoord.xy, ivec2(-1, -1)); fc += textureOffset(image, gl_FragCoord.xy, ivec2( 0, -1)); fc += textureOffset(image, gl_FragCoord.xy, ivec2( 1, -1)); fc += textureOffset(image, gl_FragCoord.xy, ivec2(-1, 0)); fc += textureOffset(image, gl_FragCoord.xy, ivec2( 1, 0)); fc += textureOffset(image, gl_FragCoord.xy, ivec2(-1, 1)); fc += textureOffset(image, gl_FragCoord.xy, ivec2( 0, 1)); fc += textureOffset(image, gl_FragCoord.xy, ivec2( 1, 1)); fc *= 0.11111111; }
でも,なんかすっきりしません.こういうのは,やっぱり,
fc = vec4(0.0); for (int i = -1; i <= 1; ++i) for (int j = -1; j <= 1; ++j) fc += textureOffset(image, gl_FragCoord.xy, ivec2(i, j));
って書きたいところです.でも,textureOffset() のマニュアルには「offset は定数でないといけない」って書いてあって,実際コンパイル時にエラーになります.であればと,
const ivec2 offset[] = ivec2[] ( ivec2(-1, -1), ivec2( 0, -1), ivec2( 1, -1), ivec2(-1, 0), ivec2( 1, 0), ivec2(-1, 1), ivec2( 0, 1), ivec2( 1, 1) ); fc = texture(image, gl_FragCoord.xy); for (int i = 0; i < offset.length(); ++i) fc += textureOffset(image, gl_FragCoord.xy, offset[i]);
ということをやってみました.しかし,ここでまた頭を抱えてしまいます.今日 (2014 年 7 月 26 日) の時点で Windows 7 の Quadro K4000 (ドライバのバージョン 335.23) だとこれで一応動いた気がします.でも,Windows 8.1 の GeForce 840M (337.88) だとなんだかサンプル点がずれてるように見えて,まともに動いているようには思えません.また Mac OS X (marvericks) の MacBook Air だと,やっぱり「offset は定数でないといけない」っていうエラーになります.
たぶん,NVIDIA の GLSL コンパイラの最適化能力が非常に高くて*1,これくらいのループはアンローリングされてインラインに展開されてしまうのだと思います.だから「定数の配列」でも OK なのかもしれません.でも,仕様上それが正しいのかどうかわからなかったので,結局上のように「手でアンローリング」することにしました.もっとエレガントな書き方ないですか.
いろいろ書いてみた
今まで作ったフィルタをサンプルプログラム (1)に追加しました.このフラグメントシェーダを以下のもので置き換えれば,たぶん動くと思います.
- 3x3 の平均
- 5x5 の平均
- 7x7 の平均
- 3x3 の最大値と最小値を除いた平均
- 5x5 の最大値と最小値を除いた平均
- 7x7 の最大値と最小値を除いた平均
- 3x3 の閾値未満を除いた平均
- 5x5 の閾値未満を除いた平均
- 7x7 の閾値未満を除いた平均
- 5x5 のガウスフィルタ
- 5x5 のバイラテラルフィルタ
- 3x3 の中間値
- 3x3 のソーベルフィルタ
- 3x3 のラプラシアンフィルタ
- 5x5 のガウスフィルタの微分
ちなみに,中間値フィルタは RGBA それぞれ独立に中間値を求めているので,個々の画素のチャンネルはシャッフルされています.あと,ソーティングで使われるような繰り返し回数が定まらないループをシェーダで使うと「ありえんくらい遅くなる」ので,これも力技でインラインで書いています*2.これ元ネタあるんですけど,ソースがわかりません.また探します.