重心抽出


~ Extraction of the center of mass ~






■はじめに

SIG-GT2の講演で高橋 誠史さんが動画像処理へのGPUの応用方法という講演を行っていたのですが、 その中で、「GPUに向かない画像処理」として、「サーフェス色の任意色の座標値の所得」をあげられていました。
個人的には、特定の色の中心を求めるのはGPUがやっても差し支えないと思います。
ということで、テストプログラムを書いてみました。

で、いつものようにプログラムです。

左ドラッグで白い線を描きます。その中心を赤いターゲットが指します。

ソースには、いつものように適当にファイルが入っています。 大事なファイルは次のものです。

main.hアプリケーションのヘッダ
main.cppアプリケーションのソース
hlsl.fxシェーダプログラム

■何をやっているの?

さて、重心を求めます。重心とは有効な画素iの位置xiの平均

    Σ xi
X = ----
    Σ 1

になります。
この式を一般化すると、画素が有効なときには1、無効なときには0となるような重みwiを定義することによって、全画素の合計を取った式

    Σ wixi
X = ------
     Σ wi

に書き直すことができます。 なお、wiは、0と1だけではなく、その中間でもよく、その場合には、画素が有効度合いのような意味が付くでしょう。

さて、この式は、wixi と wi に関して全画素の合計をとり、その結果を割るといった処理になっています。
といっても、実際には合計をとる処理に関して一様なスケーリングをしても結果が変わらないので、「合計」をとるのでも、「平均」をとるのでもその結果はかわらないということになります。
全画素の平均を取るというのは、いわゆるブラーを最後までとることです。 これはミップマップとしてCGでよく知られている方法で処理できます。
ということで、各画素に関してwixi と wiを計算し、それらの平均をミップマップをすることによってもとめ、最後にその結果を割ると欲しい重心が求まります。

今回のプログラムでは、r及びg成分にwixiを、a成分にwiを格納して計算します。 なお、wiは、重心を取りたい元画像の白黒の濃度に他なりません。

■元画像を使った各テクセルのxy座標の埋め込み

最初のステップは、基画像に座標値を掛けて、ピクセルごとに合成する基の数値を導出することです。
座標値は、左上が(0,0)、右下が(1,1)になるように、射影空間の座標値をスケーリングします。

hlsl.fx
0039: struct FRAGMENT0
0040: {
0041:     float4 Pos          : POSITION;
0042:     float2 Tex          : TEXCOORD0;
0043:     float4 XY           : TEXCOORD1;
0044: };
0045: // ------------------------------------------------------------
0046: FRAGMENT0 VS0 (
0047:       float4 Pos    : POSITION           // (モデルの頂点)
0048:     , float2 Tex    : TEXCOORD0          // (テクスチャ座標)
0049:       )
0050: {
0051:     FRAGMENT0 Out = (FRAGMENT0)0;
0052:     
0053:     // Position(位置座標)
0054:     Out.Pos = Pos;
0055:     
0056:     // [0,1]にした位置
0057:     Out.XY = 1;
0058:     Out.XY.x = (  0.5 * Pos.x + 0.5);
0059:     Out.XY.y = (- 0.5 * Pos.y + 0.5);
0060:     
0061:     // テクスチャ座標
0062:     Out.Tex = Tex;
0063:     
0064:     return Out;
0065: }
0066: // ------------------------------------------------------------
0067: float4 PS0 (FRAGMENT0 In) : COLOR
0068: {
0069:     float4 Out;
0070:     
0071:     Out = tex2D( LinearSamp, In.Tex ).x * In.XY;
0072:     
0073:     return Out;
0074: }

アプリケーションプログラム側では、基画像のテクスチャを読み込みテクスチャとして、ポリゴンを全画面で描画します。
ちなみに、バイリニア補間を効かせるために、1テクセルだけ座標値をずらして描画します。

main.cpp
0464:             m_pEffect->SetTechnique( m_hTechnique );
0465:             m_pEffect->Begin( NULL, 0 );
0466: 
0467:             // 0 へ
0468:             m_pEffect->Pass( 0 );
0469:             m_pd3dDevice->SetRenderTarget( 0, m_pSurf[0] );
0470:             m_pd3dDevice->SetFVF( D3DFVF_XYZ | D3DFVF_TEX1 );
0471:             TVERTEX Vertex[4] = {
0472:                 // x   y  z      tu            tv
0473:                 { -1,  1, 0,  0+1.0/512.0, 0+1.0/512.0,},
0474:                 {  1,  1, 0,  1+1.0/512.0, 0+1.0/512.0,},
0475:                 {  1, -1, 0,  1+1.0/512.0, 1+1.0/512.0,},
0476:                 { -1, -1, 0,  0+1.0/512.0, 1+1.0/512.0,},
0477:             };
0478:             m_pEffect->SetTexture( "LinearTex",  m_pBackBufferTex );
0479:             m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, Vertex, sizeof( TVERTEX ) );

■ブラー

ミップマップの計算は、16ボックスサンプリングで行いました。
精度の問題で、縮小バッファに浮動小数点バッファを使いました。 RADEON9600では浮動小数点バッファにバイリニア補間が使えないので1回のサンプリングで16テクセルの情報しか拾えません。GeForce 6800 では、16ボックスサンプリングで64テクセルの情報を拾うこともできるでしょう。

hlsl.fx
0081: struct FRAGMENT1
0082: {
0083:     float4 Pos          : POSITION;
0084:     float2 Tex0         : TEXCOORD0;
0085:     float2 Tex1         : TEXCOORD1;
0086:     float2 Tex2         : TEXCOORD2;
0087:     float2 Tex3         : TEXCOORD3;
0088:     float2 Tex4         : TEXCOORD4;
0089:     float2 Tex5         : TEXCOORD5;
0090:     float2 Tex6         : TEXCOORD6;
0091:     float2 Tex7         : TEXCOORD7;
0092: };
0093: // ------------------------------------------------------------
0094: FRAGMENT1 VS1 (
0095:       float4 Pos    : POSITION           // (モデルの頂点)
0096:     , float2 Tex    : TEXCOORD0          // (テクスチャ座標)
0097:       )
0098: {
0099:     FRAGMENT1 Out = (FRAGMENT1)0;
0100:     
0101:     // Position(位置座標)
0102:     Out.Pos = Pos;
0103:     
0104:     // テクスチャ座標
0105:     Out.Tex0 = Tex + float2( 0.5*inv_size, 0.5*inv_size );
0106:     Out.Tex1 = Tex + float2( 1.5*inv_size, 0.5*inv_size );
0107:     Out.Tex2 = Tex + float2( 2.5*inv_size, 0.5*inv_size );
0108:     Out.Tex3 = Tex + float2( 3.5*inv_size, 0.5*inv_size );
0109:     Out.Tex4 = Tex + float2( 0.5*inv_size, 1.5*inv_size );
0110:     Out.Tex5 = Tex + float2( 1.5*inv_size, 1.5*inv_size );
0111:     Out.Tex6 = Tex + float2( 2.5*inv_size, 1.5*inv_size );
0112:     Out.Tex7 = Tex + float2( 3.5*inv_size, 1.5*inv_size );
0113:     
0114:     return Out;
0115: }
0116: // ------------------------------------------------------------
0117: float4 PS1 (FRAGMENT1 In) : COLOR
0118: {
0119:     float4 Out;
0120:     
0121:     Out = tex2D( PointSamp, In.Tex0 )
0122:         + tex2D( PointSamp, In.Tex1 )
0123:         + tex2D( PointSamp, In.Tex2 )
0124:         + tex2D( PointSamp, In.Tex3 )
0125:         + tex2D( PointSamp, In.Tex4 )
0126:         + tex2D( PointSamp, In.Tex5 )
0127:         + tex2D( PointSamp, In.Tex6 )
0128:         + tex2D( PointSamp, In.Tex7 )
0129:         + tex2D( PointSamp, In.Tex0 + float2( 0.0, 2.0*inv_size ) )
0130:         + tex2D( PointSamp, In.Tex1 + float2( 0.0, 2.0*inv_size ) )
0131:         + tex2D( PointSamp, In.Tex2 + float2( 0.0, 2.0*inv_size ) )
0132:         + tex2D( PointSamp, In.Tex3 + float2( 0.0, 2.0*inv_size ) )
0133:         + tex2D( PointSamp, In.Tex4 + float2( 0.0, 2.0*inv_size ) )
0134:         + tex2D( PointSamp, In.Tex5 + float2( 0.0, 2.0*inv_size ) )
0135:         + tex2D( PointSamp, In.Tex6 + float2( 0.0, 2.0*inv_size ) )
0136:         + tex2D( PointSamp, In.Tex7 + float2( 0.0, 2.0*inv_size ) );
0137:     
0138:     return Out/16;
0139: }

アプリケーションプログラム側では、レンダリングターゲットを変えてポリゴンを全画面に表示します。

main.cpp
0482:             m_pEffect->Pass( 1 );
0483:             float size = 256.0;
0484:             for( i = 1; i < 5; i++ )
0485:             {
0486:                 m_pEffect->SetFloat("inv_size", 1.0/size);
0487:                 m_pd3dDevice->SetRenderTarget( 0, m_pSurf[i] );
0488:                 TVERTEX Vertex[4] = {
0489:                     // x   y  z   tu tv
0490:                     { -1,  1, 0,  0, 0,},
0491:                     {  1,  1, 0,  1, 0,},
0492:                     {  1, -1, 0,  1, 1,},
0493:                     { -1, -1, 0,  0, 1,},
0494:                 };
0495:                 m_pEffect->SetTexture( "PointTex",  m_pTex[i-1] );
0496:                 m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, Vertex, sizeof( TVERTEX ) );
0497: 
0498:                 size /= 4.0;
0499:             }

■ターゲットスコープの表示

最後に、1テクセルまで落とした情報から平均の座標値を求めて、w成分(有効なテクセル数)で割ることによって、重心を計算します。
ターゲットを動かすのに、表示するターゲットのテクスチャ座標をずらす方法を採用しました。この方法では、ターゲットを表示するために、全画面のポリゴンを表示する必要があります。ただ、他の方法を採用するぐらいなら、この方法のほうが速いと感じたので、この方法を採用しました。
今回は、ターゲットの大きさが32ピクセルで、画面の大きさは512ピクセルにしました。この場合には、テクスチャ座表が512/32=16のときにターゲットは画面の端に行きます。位置座標に512/32倍しているのは、0~1の範囲で動くターゲットを画面の大きさに調整するスケーリングのためです。

hlsl.fx
0142: // ------------------------------------------------------------
0143: // ターゲットを動かす
0144: // ------------------------------------------------------------
0145: struct FRAGMENT2
0146: {
0147:     float4 Pos          : POSITION;
0148:     float2 Tex          : TEXCOORD0;
0149: };
0150: // ------------------------------------------------------------
0151: float4 PS2 (FRAGMENT2 In) : COLOR
0152: {
0153:     float4 Out;
0154:     
0155:     float4 pos =  tex2D( PointSamp, float2(0.5,0.5) );
0156:     pos /= pos.w;
0157:     
0158:     pos *= 512/32;
0159: 
0160:     Out =  tex2D( LinearSamp, In.Tex - pos.xy );
0161: 
0164:     return Out;
0165: }

アプリケーション側では、ターゲットの中心が原点(左上)になるように調整して、全画面にポリゴンを表示します。

main.cpp
0534:         if( m_pEffect != NULL ) 
0535:         {
0536:             m_pEffect->SetTechnique( m_hTechnique );
0537:             m_pEffect->Begin( NULL, 0 );
0538:             m_pEffect->Pass( 2 );
0539: 
0540:             m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE );
0541:             m_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );
0542:             m_pd3dDevice->SetRenderState( D3DRS_SRCBLEND,  D3DBLEND_SRCALPHA );
0543: 
0544:             m_pd3dDevice->SetFVF( D3DFVF_XYZRHW | D3DFVF_TEX1 );
0545:             TVERTEX Vertex[4] = {
0546:             //                  x                                  y          z rhw tu tv
0547:             {                             0,                               0, 0, 1, 0.5,        0.5,},
0548:             {(FLOAT)m_d3dsdBackBuffer.Width,                               0, 0, 1, 0.5+512/32, 0.5,},
0549:             {(FLOAT)m_d3dsdBackBuffer.Width, (FLOAT)m_d3dsdBackBuffer.Height, 0, 1, 0.5+512/32, 0.5+512/32,},
0550:             {                             0, (FLOAT)m_d3dsdBackBuffer.Height, 0, 1, 0.5,        0.5+512/32,},
0551:             };
0552:             m_pEffect->SetTexture( "PointTex",  m_pTex[4] );
0553:             m_pEffect->SetTexture( "LinearTex", m_pTargetTex );
0554:             m_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, Vertex, sizeof( TVERTEX ) );
0555: 
0556:             m_pEffect->End();
0557:             m_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE );
0558:         }

■CPUで重心を使うときには?

さて、今回の結果をCPUで使いたいときですが、1x1テクセルまで落としたテクスチャをロックして読み込めばよいということになります。
当然、その方法にはペナルティが付くので、1x1のテクスチャは複数用意してバッファリングすべきでしょう。
リアルタイムで結果を反映させなければならないところは、今回のターゲットの表示部分のように処理すれば問題ないですね。

ということで、平均操作ぐらいは、もはやGPUで処理してもいいのではないかと思います。

■最後に

今回のネタは、ShaderX2の焼き直しだし、新しいことはなにもないね。





もどる

imagire@gmail.com