«GLSL で画像処理 (1) 画像を取り込む 最新 GLSL で画像処理 (3) ワーピング»

床井研究室

※このブログは遅くとも 2027 年 3 月に管理者の定年退職により閉鎖します (移転先は管理者本人共々模索中)

■ 2014年07月26日 [OpenGL][GLSL] GLSL で画像処理 (2) 簡単なフィルタ

2014年07月31日 18:43更新

暑い

暑いです.めっちゃ暑いです.意味わからんくらい暑いです.しかしデマンド超過防止のために部屋の空調切ってたので,たまらずに冷却材を投入しました.売店のガリガリ君専用冷凍庫,半分が梨味で埋まってたように見えたんですけど,問題ないんでしょうか.そんなこと気にしてたら体温が上がりますね.梨味おいしいです.一本食べたら飽きるけど.

画像の説明

フィルタを書いてみる

昨日作ったサンプルプログラム

のフラグメントシェーダはこんな具合になってました.

#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)に追加しました.このフラグメントシェーダを以下のもので置き換えれば,たぶん動くと思います.

ちなみに,中間値フィルタは RGBA それぞれ独立に中間値を求めているので,個々の画素のチャンネルはシャッフルされています.あと,ソーティングで使われるような繰り返し回数が定まらないループをシェーダで使うと「ありえんくらい遅くなる」ので,これも力技でインラインで書いています*2.これ元ネタあるんですけど,ソースがわかりません.また探します.

*1 使ってない uniform をコンパイル時に消されて,変数定義してるのに glGetUniformLocation() に -1 を返されてうろたえたってことがありました.

*2 GLSL の関数はインライン展開されるのでしょうか.


編集 «GLSL で画像処理 (1) 画像を取り込む 最新 GLSL で画像処理 (3) ワーピング»