中央値フィルタ


~ Median filter ~






■はじめに

GeForce FX 用の、オリジナルのネタを考えてみました。
上の図の、左側が元の絵で、右側が中央値フィルタを掛けた画像です。
中央値フィルタは、点々としたポイントノイズを除去する効果があります(完全にではありませんが)。

下のファイルがソースと実行ファイルです。

まぁ、いつものように適当にファイルが入っています。
今回は、天球を半球にしてみました。

fp.cgフラグメントプログラム。
main.cppメインループ。
CBitmap.hビットマップファイルの読み込み。
CBitmap.cppビットマップファイルの読み込み。
test.bmp

あと、実行ファイル及び、プロジェクトファイルが入っています。

■中央値フィルタとは

中央値フィルタとは、局所的な領域での濃淡の中央の値を持つ色を出力するフィルタです。

特徴としては、一番上の絵のようなごま塩状のノイズを取り除くことができます。 ただし、完璧に取り除けるのは白か黒のノイズの時のみで、ノイズの色が中間の色の時には、その色が残ります。 例えば、下の絵では一部のノイズが残ってしまいました。

また、比較的、元の形を残すフィルタです。

単純なサンプリングではなく、真中の値を決める計算が必要とのことで、 いままでのシェーダではできませんでした。

■ビットマップファイルの読み込み

今回は、ビットマップファイルを読み込んで、テクスチャとして張り込んでいます。
ビットマップファイルの読み込みをどうしようかとネットで探したら、 sonsonさんのサイトにそのままずばりのプログラムがあったので、そのプログラムを参考にしました。

読み込みの手順は次のようになります。 基本的には、ファイルをオープンしてヘッダなどを読み込んだ後に、フォーマット等をチェックして データ種別ごとに読み込みます。
私は基本的に「goto大好きっ娘」なので、エラーが起きた後は最後の行までジャンプして処理するようにしています。 別にgotoなど使わなくても書けるのですが、このほうがバグがでないと思いませんか?

CBitmap.cpp
0161: unsigned int CBitmap::Load(const char *filename)
0162: {
0163:     unsigned int tex_id = 0;
0164:     FILE *fp;
0165:     //ビットマップヘッダ
0166:     BitmapHeader header;
0167:     BitmapInfoHeader info;
0168:     //色数
0169:     unsigned int color_bit;
0170:     
0171:     // ファイルオープン
0172:     if( NULL==( fp = fopen(filename, "rb") ) ) return tex_id;
0173:     
0174:     //ヘッダ読み込み
0175:     if( _ReadHeader(&header, fp))   goto end;
0176:     if( _ReadInfoHeader(&info, fp)) goto end;
0177:     
0178:     //ヘッダチェック
0179:     if( !(color_bit = _CheckHeaders(&header, &info))) goto end;
0180:     
0181:     //ビットマップファイルサイズチェック
0182:     if( !_CheckSize(info.width*info.height) ) goto end;
0183:     
0184:     //カラービット数で処理を分ける
0185:     switch(color_bit){
0186:     case 8:
0187:         tex_id = _Analize8bit(&info, fp);
0188:         break;
0189:     case 24:
0190:         tex_id = _Analize24bit(&info, fp);
0191:         break;
0192:     }
0193:     
0194: end:
0195:     // ファイルクローズ
0196:     fclose(fp);
0197:     
0198:     return tex_id;
0199: }

さて、フォーマットのチェックで何をするかということですが、 「ビットマップファイルの先頭は「BM」で始まっている」等の規約がありますので、 それらをチェックします。
この辺は、仕様で決まっているので、さらに深く知りたい方は、フォーマットの仕様をチェックしましょう。

CBitmap.cpp
0006: // ---------------------------------------------------------------------------
0007: // Bitmapヘッダチェック
0008: // ---------------------------------------------------------------------------
0009: int CBitmap::_CheckHeaders (const BitmapHeader *pBH, const BitmapInfoHeader *pBIH)
0010: {
0011:     //ヘッダのBMの文字
0012:     if( pBH->distinct1!='B' || pBH->distinct2!='M' )return 0;
0013:     //レイヤー1
0014:     if( pBIH->plane !=1 )return 0;
0015:     //圧縮なし
0016:     if( pBIH->compression !=0 ) return 0;
0017:     //色数を返す
0018:     if( pBIH->bits ==24 || pBIH->bits ==8 ) return pBIH->bits;
0019:     
0020:     return 0;
0021: }

今回は、無圧縮256色か24bit色のファイルだけを取り扱います。

あと、OpenGLの仕様として、「テクスチャの幅xテクスチャの高さ=2^n」である必要があるので、 このチェックもします。
ということで、読み込めるテクスチャにはこの制限が付きますので注意してください。

CBitmap.cpp
0022: // ---------------------------------------------------------------------------
0023: // ビットマップファイルのサイズチェック
0024: // ---------------------------------------------------------------------------
0025: int CBitmap::_CheckSize(unsigned int pict_size)
0026: {
0027:     unsigned int i=2;
0028:     const unsigned int TEX_SIZE = 256;
0029:     unsigned int max_texture = TEX_SIZE*TEX_SIZE;
0030:     
0031:     //サイズチェック
0032:     for(i=2;i<=max_texture;i*=2){
0033:         if( i == pict_size){
0034:             return 1;
0035:         }
0036:     }
0037:     return 0;
0038: };

フォーマットが分かったら実際にデータを読み込んで、テクスチャとして設定します。
24bitsの時は中身がべたで入っているので、そのまま読み込んでイメージとしてからテクスチャにします。
但し、データの中身の順序が逆なので読み込むときに御尻から読み込む必要があります。

CBitmap.cpp
0124: // ---------------------------------------------------------------------------
0125: // 24bitsテクスチャの読み込み
0126: // ---------------------------------------------------------------------------
0127: unsigned int CBitmap::_Analize24bit( const BitmapInfoHeader *pBIH, FILE *fp )
0128: {
0129:     unsigned int tex_id = 0;
0130:     unsigned char *bitdata;
0131:     unsigned char red,green,blue;
0132:     int i, j;
0133:     
0134:     //メモリ確保
0135:     bitdata = (unsigned char*)malloc(3*pBIH->width*pBIH->height*sizeof(unsigned char));
0136:     //読み込み
0137:     for(j=(pBIH->height-1);j>=0;j--){
0138:         for(i=(pBIH->width-1);i>=0;i--){
0139:             fread( &red,   sizeof(unsigned char),1,fp);
0140:             fread( &green, sizeof(unsigned char),1,fp);
0141:             fread( &blue,  sizeof(unsigned char),1,fp);
0142:             *(bitdata+3*i+3*j*pBIH->width)  = blue;
0143:             *(bitdata+3*i+3*j*pBIH->width+1)= green; 
0144:             *(bitdata+3*i+3*j*pBIH->width+2)= red;
0145:         }
0146:     }
0147:     //テクスチャ作成
0148:     glGenTextures(1, &tex_id);
0149:     glBindTexture(GL_TEXTURE_2D, tex_id);
0150:     glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, pBIH->width, pBIH->height,
0151:                 0, GL_RGB, GL_UNSIGNED_BYTE, bitdata);
0152:     
0153:     //メモリ開放
0154:     free(bitdata);
0155:     
0156:     return tex_id;
0157: }

テクスチャを読み込むときは、ロードして、適当な属性を設定します。

main.cpp
0167:     //テクスチャの生成
0168:     tex = CBitmap::Load("test.bmp");
0169:     glBindTexture(GL_TEXTURE_2D, tex);
0170:     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
0171:     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
0172:     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
0173:     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
0174:     glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);

以上でテクスチャを読み込むことができます。
この辺は、D3DXのような標準のペルパーライブラリを作ってもらいたいものです。

■フラグメントプログラム

さて、肝心かなめのフラグメントプログラムです。
今回は頂点プログラムはありません。 入力された頂点が座標変換されて、フラグメントプログラムへデータが入力されます。

fp.cg
0001: // ---------------------------------------------------------------------------
0002: // 頂点プログラム出力データ
0003: // ---------------------------------------------------------------------------
0004: struct vert2frag
0005: {
0006:     float4 hpos     : HPOS; // 頂点座標
0007:     float4 tcoords  : TEX0; // テクスチャ座標
0008: };

今回入力されるデータはテクスチャ座標です。
ポリゴンにあわせてテクスチャが張られるように座標を調整します。

フラグメントプログラムでは、最初にテクセル中心とその周りの4テクセルの色をサンプリングします。
GeForce FX 世代では、1つのテクスチャから5回サンプリングするなんてできるので、 テクスチャ座標を少しずつずらしながら、読み込みます。
中心の座標はテクセル中心に合わせるために、(0.5, 0.5)にしています。

fp.cg
0009: // ---------------------------------------------------------------------------
0010: // Median filter フラグメントプログラム
0011: // ---------------------------------------------------------------------------
0012: fragout main(vert2frag I
0013:             , uniform texobj2D SrcMap : texunit0
0014:     )
0015: {
0016:     fragout O;
0017:     
0020:     float2 shift00 = { 0.5f/256.0f, 0.5f/256.0f};   // 上下左右中心5つからサンプリング
0021:     float2 shift01 = { 0.5f/256.0f, 1.5f/256.0f};   // 00  01  10  0N  N0 
0022:     float2 shift10 = { 1.5f/256.0f, 0.5f/256.0f};   //  x   x   x   o   x  
0023:     float2 shift0N = { 0.5f/256.0f,-0.5f/256.0f};   // xox xxx xxo xxx oxx 
0024:     float2 shiftN0 = {-0.5f/256.0f, 0.5f/256.0f};   //  x   o   x   x   x  
0025:     
0026:     // テクスチャから少しずらしてサンプリング
0027:     float3 col0 = f3tex2D( SrcMap, I.tcoords.xy + shift00 );
0028:     float3 col1 = f3tex2D( SrcMap, I.tcoords.xy + shift01 );
0029:     float3 col2 = f3tex2D( SrcMap, I.tcoords.xy + shift10 );
0030:     float3 col3 = f3tex2D( SrcMap, I.tcoords.xy + shift0N );
0031:     float3 col4 = f3tex2D( SrcMap, I.tcoords.xy + shiftN0 );

次に、テクセルの色の強さを求めます。
色の強さは、内積計算だけで求められます。

fp.cg
0018:     float3 rgb2lum = {0.299, 0.587, 0.114};         // RGB から、色の強さを求める定数
0032:     
0033:     // 色の強さを求める
0034:     float b0 = dot( col0, rgb2lum );
0035:     float b1 = dot( col1, rgb2lum );
0036:     float b2 = dot( col2, rgb2lum );
0037:     float b3 = dot( col3, rgb2lum );
0038:     float b4 = dot( col4, rgb2lum );

さて、今得られた色の強さの真ん中の値をピクセルの色にします。
この「真ん中」を得る方法が問題です。
フラグメントプログラムでは、さすがにソートは使えないので、 別の方法を考えなくてはなりません。
今回行う方法は、各テクセルの色の強さが上から数えて何番目かを数えて、 3番目に強い色をピクセルの色に採用します。
色の順番 flag* が2の時に3番目の色になりますが、 浮動小数点数計算をするので厳密に「2」の値を見れないので、 ±0.5の範囲を調べて「2」の値を持つテクセルがどれか調べます。
また、色が同じ時にも優先をつけるために、比較する時に等号をどちらか付けて「2」の値が2つ以上でないように気をつけました (左辺の番号が右辺の番号よりも大きいときに等号を付けました)。

fp.cg
0040:     // 色の順番を求める(「4-値」の並びの位置にいる)
0041:     float flag0 = ((b0< b1)?1.0:0.0) + ((b0< b2)?1.0:0.0) + ((b0< b3)?1.0:0.0) + ((b0< b4)?1.0:0.0);
0042:     float flag1 = ((b1<=b0)?1.0:0.0) + ((b1< b2)?1.0:0.0) + ((b1< b3)?1.0:0.0) + ((b1< b4)?1.0:0.0);
0043:     float flag2 = ((b2<=b0)?1.0:0.0) + ((b2<=b1)?1.0:0.0) + ((b2< b3)?1.0:0.0) + ((b2< b4)?1.0:0.0);
0044:     float flag3 = ((b3<=b0)?1.0:0.0) + ((b3<=b1)?1.0:0.0) + ((b3<=b2)?1.0:0.0) + ((b3< b4)?1.0:0.0);
0045: 
0046:     // 中心の色をメディアンとして採用
0047:     O.col.xyz =  ( 1.5f<flag0 && flag0<2.5f ) ? col0 : 
0048:                 (( 1.5f<flag1 && flag1<2.5f ) ? col1 :
0049:                 (( 1.5f<flag2 && flag2<2.5f ) ? col2 :
0050:                 (( 1.5f<flag3 && flag3<2.5f ) ? col3 : col4 )));
0051: 
0052:     return O;
0053: } 

suzunaさんとの飲み会のときにミキタさんから、 「Cg使い物になるん?」聞かれたので、ためしに今回のアセンブラを表示してみると下のようになります。
ざっと112命令あります。こんなのを手で書く気にはなれません。 これだけでも非常に有用であることがわかるかと思います。
欠点としては、リソースが豊富にあるので最適化をあまり考えなくなってしまうことですね。
今回も、比較処理の部分等でアルゴリズム的により軽い処理も考えれられるのですが、命令数が目では見えないので、どうもやる気がしなくなります。

!!FP1.0
# NV_fragment_program generated by NVIDIA Cg compiler
# cgc version 1.5.0001, build date Sep 12 2002  15:50:03
# command line args: fp.cg -o C:\cgt168A.tmp -l C:\cgt168B.tmp -q -profile fp30
#vendor NVIDIA Corporation
#version 1.0.1
#profile fp30
#program main
#semantic main.SrcMap : texunit0
#var float4 I.tcoords : $vin.TEX0 : TEX0 : 0 : 1
#var texobj2D SrcMap : texunit0 : texunit 0 : 1 : 1
#var half4 col : $vout.COL : COL : -1 : 1
#var float depth : $vout.DEPR : DEPR : -1 : 1
ADDR  R0.xy, f[TEX0].xyyy, {-0.001953125, 0.001953125}.xyyy;
TEX  R0.xyz, R0.xyyy, TEX0, 2D;
ADDR  R1.xy, f[TEX0].xyyy, {0.001953125, -0.001953125}.xyyy;
TEX  R9.xyz, R1.xyyy, TEX0, 2D;
MOVR  R11.x, {0}.x;
DP3R  R4.x, R0.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz;
DP3R  R5.x, R9.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz;
SLTR  H2.x, R5.x, R4.x;
MOVXC HC.x, H2.x;
MOVR  R11.x(GT.x), {1}.x;
MOVR  R12.x, {0}.x;
ADDR  R1.xy, f[TEX0].xyyy, {0.005859375, 0.001953125}.xyyy;
TEX  R8.xyz, R1.xyyy, TEX0, 2D;
DP3R  R2.x, R8.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz;
SLER  H2.x, R5.x, R2.x;
MOVXC HC.x, H2.x;
MOVR  R12.x(GT.x), {1}.x;
MOVR  R13.x, {0}.x;
ADDR  R1.xy, f[TEX0].xyyy, {0.001953125, 0.005859375}.xyyy;
TEX  R3.xyz, R1.xyyy, TEX0, 2D;
DP3R  R7.x, R3.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz;
SLER  H2.x, R5.x, R7.x;
MOVXC HC.x, H2.x;
MOVR  R13.x(GT.x), {1}.x;
MOVR  R10.x, {0}.x;
ADDR  R1.xy, f[TEX0].xyyy, {0.001953125, 0.001953125}.xyyy;
TEX  R1.xyz, R1.xyyy, TEX0, 2D;
DP3R  R6.x, R1.xyzz, {0.29899999, 0.58700001, 0.114}.xyzz;
SLER  H28.x, R5.x, R6.x;
MOVXC HC.x, H28.x;
MOVR  R10.x(GT.x), {1}.x;
ADDR  R10.x, R10.x, R13.x;
ADDR  R10.x, R10.x, R12.x;
ADDR  R11.x, R10.x, R11.x;
SLTR  H21.x, R11.x, {2.5}.x;
SLTR  H20.x, {1.5}.x, R11.x;
MULX  H20.x, H20.x, H21.x;
MOVXC HC.x, H20.x;
MOVR  R0.xyz(GT.xxxx), R9.xyzz;
MOVR  R10.x, {0}.x;
SLTR  H18.x, R2.x, R4.x;
MOVXC HC.x, H18.x;
MOVR  R10.x(GT.x), {1}.x;
MOVR  R11.x, {0}.x;
SLTR  H18.x, R2.x, R5.x;
MOVXC HC.x, H18.x;
MOVR  R11.x(GT.x), {1}.x;
MOVR  R12.x, {0}.x;
SLER  H18.x, R2.x, R7.x;
MOVXC HC.x, H18.x;
MOVR  R12.x(GT.x), {1}.x;
MOVR  R9.x, {0}.x;
SLER  H26.x, R2.x, R6.x;
MOVXC HC.x, H26.x;
MOVR  R9.x(GT.x), {1}.x;
ADDR  R9.x, R9.x, R12.x;
ADDR  R9.x, R9.x, R11.x;
ADDR  R10.x, R9.x, R10.x;
SLTR  H19.x, R10.x, {2.5}.x;
SLTR  H18.x, {1.5}.x, R10.x;
MULX  H18.x, H18.x, H19.x;
MOVXC HC.x, H18.x;
MOVR  R0.xyz(GT.xxxx), R8.xyzz;
MOVR  R9.x, {0}.x;
SLTR  H16.x, R7.x, R4.x;
MOVXC HC.x, H16.x;
MOVR  R9.x(GT.x), {1}.x;
MOVR  R10.x, {0}.x;
SLTR  H16.x, R7.x, R5.x;
MOVXC HC.x, H16.x;
MOVR  R10.x(GT.x), {1}.x;
MOVR  R11.x, {0}.x;
SLTR  H16.x, R7.x, R2.x;
MOVXC HC.x, H16.x;
MOVR  R11.x(GT.x), {1}.x;
MOVR  R8.x, {0}.x;
SLER  H24.x, R7.x, R6.x;
MOVXC HC.x, H24.x;
MOVR  R8.x(GT.x), {1}.x;
ADDR  R8.x, R8.x, R11.x;
ADDR  R8.x, R8.x, R10.x;
ADDR  R9.x, R8.x, R9.x;
SLTR  H17.x, R9.x, {2.5}.x;
SLTR  H16.x, {1.5}.x, R9.x;
MULX  H16.x, H16.x, H17.x;
MOVXC HC.x, H16.x;
MOVR  R0.xyz(GT.xxxx), R3.xyzz;
MOVR  R3.x, {0}.x;
SLTR  H8.x, R6.x, R4.x;
MOVXC HC.x, H8.x;
MOVR  R3.x(GT.x), {1}.x;
MOVR  R4.x, {0}.x;
SLTR  H10.x, R6.x, R5.x;
MOVXC HC.x, H10.x;
MOVR  R4.x(GT.x), {1}.x;
MOVR  R5.x, {0}.x;
SLTR  H4.x, R6.x, R2.x;
MOVXC HC.x, H4.x;
MOVR  R5.x(GT.x), {1}.x;
MOVR  R2.x, {0}.x;
SLTR  H12.x, R6.x, R7.x;
MOVXC HC.x, H12.x;
MOVR  R2.x(GT.x), {1}.x;
ADDR  R2.x, R2.x, R5.x;
ADDR  R2.x, R2.x, R4.x;
ADDR  R3.x, R2.x, R3.x;
SLTR  H5.x, R3.x, {2.5}.x;
SLTR  H4.x, {1.5}.x, R3.x;
MULX  H4.x, H4.x, H5.x;
MOVXC HC.x, H4.x;
MOVR  R0.xyz(GT.xxxx), R1.xyzz;
MOVH  o[COLH].xyz, R0.xyzz;
END

■最後に

メディアンフィルタは、ノイズリダクションフィルタなので、 フォトンマップの画像に使えるかなぁと考えていました。
ためしに、フォトンマップの画像にかけると、次のようになります(^c さん、勝手に使ってごめんなさい)。

同じく右がフィルタを掛けた画像です。
あまり綺麗にノイズが取れなかったので、ちょっと残念でした。
まぁ、それ以前に、Bee さんに「わざわざフォトントレースをしたのに、 フィルタをかけて誤魔化すのは本末転倒だ」といわれたことがあるので、 これ以上、このネタには言及するのは止めましょう。

今回は4近傍と中心のピクセルを取ったのですが、 8近傍を考えるとよりノイズを取る効果が出ると思います。 ところが、そのまま組むと、シェーダの命令数だかレジスタ数だかをオーバーしてしまうようで、 今回は断念しました。





もどる

imagire@gmail.com