画像処理ライブラリGPUImageを使った3x3のフィルタリングで理解するプログラマブルシェーダ言語


すでに投稿してある[画像処理のデファクトスタンダードライブラリGPUImageの基本と応用](http://qiita.com/yimajo/items/e38e8bafc17144b7af79)のプログラマブルシェーダについてを分けて投稿する。

[GPUImageライブラリ](https://github.com/BradLarson/GPUImage)では3x3のカーネル（オペレータ、重み）を変更することで画像処理を行うためのクラスGPUImage3x3ConvolutionFilterが用意されている。このクラスの説明と、コードを読むことで頂点シェーダとフラグメントシェーダの理解を深めるためのメモ。画像処理プログラミングの基礎として、カーネルを用いた積和演算（畳み込み）についての知識がないと理解できないと思う。

## GPUImage3x3ConvolutionFilterクラスを読む

### 関連するソースコード

### カーネルの設定について

[GPUImage3x3ConvolutionFilter.h](https://github.com/BradLarson/GPUImage/blob/master/framework/Source/GPUImage3x3ConvolutionFilter.h)には、3x3のカーネルを保持するプロパティがあり、これに3x3のカネールを渡すことで画像をフィルタリングすることが出来る。

```objectivec:
@property(readwrite, nonatomic) GPUMatrix3x3 convolutionKernel;
```

GPUMatrix3x3は構造体になっており次のように設定できる。

```objectivec:
[(GPUImage3x3ConvolutionFilter *)filter setConvolutionKernel:(GPUMatrix3x3){
    { 0.11f,  0.11f, 0.11f},
    { 0.11f,  0.11f, 0.11f},
    { 0.11f,  0.11f, 0.11f}
}];
```

設定している0.11fは1/9（3x3=9の値の平均値を取り出すための設定値）であり移動平均の値となる（移動平均について: http://imagingsolution.blog107.fc2.com/blog-entry-88.html ）。

この書式は[GPUMatrix3x3](https://github.com/BradLarson/GPUImage/blob/master/framework/Source/GPUImageFilter.h)構造体の中にGPUVector3構造体が3つあり、データを設定しやすいようにしている。

```objectivec:
struct GPUMatrix3x3 {
    GPUVector3 one;
    GPUVector3 two;
    GPUVector3 three;
};
typedef struct GPUMatrix3x3 GPUMatrix3x3;

struct GPUVector3 {
    GLfloat one;
    GLfloat two;
    GLfloat three;
};
typedef struct GPUVector3 GPUVector3;
```


設定するカーネルの値はconvolutionMatrixという変数としてシェーダに渡される。

```objectivec:
- (id)initWithFragmentShaderFromString:(NSString *)fragmentShaderString;
{
    if (!(self = [super initWithFragmentShaderFromString:fragmentShaderString]))
    {
        return nil;
    }
    //シェーダに渡すindexを保持
    convolutionMatrixUniform = [filterProgram uniformIndex:@"convolutionMatrix"];
    
    return self;
}

#pragma mark -
#pragma mark Accessors

- (void)setConvolutionKernel:(GPUMatrix3x3)newValue;
{
    _convolutionKernel = newValue;
    //setterをオーバーライドしシェーダに渡す設定を行う
    [self setMatrix3f:_convolutionKernel forUniform:convolutionMatrixUniform program:filterProgram];
}
```
### 3x3カーネルを渡したフラグメントシェーダ

[フラグメントシェーダ側](https://github.com/BradLarson/GPUImage/blob/master/framework/Source/GPUImage3x3ConvolutionFilter.m)にはObjective-Cのコードからグローバルな定数として渡される。そのためuniformとなる。

```objectivec:
NSString *const kGPUImage3x3ConvolutionFragmentShaderString = SHADER_STRING
(
 precision highp float;

 uniform sampler2D inputImageTexture;
 //uniformはシステムから渡されることを示す定数。カーネルを定数として渡している。
 uniform mediump mat3 convolutionMatrix;
 
 //varyingは頂点シェーダから渡される値なのでここでは気にしない
 varying vec2 textureCoordinate;
 varying vec2 leftTextureCoordinate;
 varying vec2 rightTextureCoordinate;
 
 varying vec2 topTextureCoordinate;
 varying vec2 topLeftTextureCoordinate;
 varying vec2 topRightTextureCoordinate;
 
 varying vec2 bottomTextureCoordinate;
 varying vec2 bottomLeftTextureCoordinate;
 varying vec2 bottomRightTextureCoordinate;
 
 void main()
 {
     //まず近傍画像３行目の色を取得
     mediump vec3 bottomColor = texture2D(inputImageTexture, bottomTextureCoordinate).rgb;
     mediump vec3 bottomLeftColor = texture2D(inputImageTexture, bottomLeftTextureCoordinate).rgb;
     mediump vec3 bottomRightColor = texture2D(inputImageTexture, bottomRightTextureCoordinate).rgb;
     //センターの色を取得
     mediump vec4 centerColor = texture2D(inputImageTexture, textureCoordinate);
     //センター左のピクセルの色を取得
     mediump vec3 leftColor = texture2D(inputImageTexture, leftTextureCoordinate).rgb;
     //センター左のピクセルの色を取得      
     mediump vec3 rightColor = texture2D(inputImageTexture, rightTextureCoordinate).rgb;
     //近傍画像１行目の色を取得
     mediump vec3 topColor = texture2D(inputImageTexture, topTextureCoordinate).rgb;
     mediump vec3 topRightColor = texture2D(inputImageTexture, topRightTextureCoordinate).rgb;
     mediump vec3 topLeftColor = texture2D(inputImageTexture, topLeftTextureCoordinate).rgb;

     //まず１行目をカーネルと畳み込む
     mediump vec3 resultColor = topLeftColor * convolutionMatrix[0][0] + topColor * convolutionMatrix[0][1] + topRightColor * convolutionMatrix[0][2];
     //２行目をカーネルと畳み込む
     resultColor += leftColor * convolutionMatrix[1][0] + centerColor.rgb * convolutionMatrix[1][1] + rightColor * convolutionMatrix[1][2];
     //３行目をカーネルと畳み込む
     resultColor += bottomLeftColor * convolutionMatrix[2][0] + bottomColor * convolutionMatrix[2][1] + bottomRightColor * convolutionMatrix[2][2];

     gl_FragColor = vec4(resultColor, centerColor.a);
 }
);
```

フラグメントシェーダは3x3のフィルタリングのため畳み込み（積和演算）を行っているだけなのが分かる。varyingなグローバル変数が近傍画像の座標値となっており、これは頂点シェーダで設定された値となる。

### 3x3フィルタのための頂点シェーダ

頂点シェーダはGPUImage3x3ConvolutionFilterが継承する[GPUImage3x3TextureSamplingFilterに実装されている](https://github.com/BradLarson/GPUImage/blob/master/framework/Source/GPUImage3x3TextureSamplingFilter.m)。

頂点シェーダのコードを読む前に、知って置かなければいけない前提知識は

* attribute変数は頂点ごとに設定できる変数（頂点シェーダのみ使用可）
* 画像の縦横サイズは0〜1に正規化される

attribute変数はuniformと同じようにGPUImageが渡している変数で、大抵の場合GPUImageが渡しているのは頂点の座標値となる。

画像の縦横サイズはOpenGLでは0から1までの座標値として扱われる。そのため、頂点シェーダでは1/サイズとして計算される。より具体的には例えば320pxの横画像であれば、横10pxの位置を示す場合1/320 * 10 = 0.3125となる。

頂点シェーダではフラグメントシェーダで処理する3x3の近傍領域の値が欲しいので、この縦横サイズから1px分ずらした座標値を取得しグローバルなvarying変数に保持する。

```objectivec:
NSString *const kGPUImageNearbyTexelSamplingVertexShaderString = SHADER_STRING
(
 //GPUImageの内部で設定されている座標値
 attribute vec4 position;
 attribute vec4 inputTextureCoordinate;
 
 //画像の横と縦サイズ。これはGPUImage3x3TextureSamplingFilterで設定される
 uniform float texelWidth;
 uniform float texelHeight; 
 
 //フラグメントシェーダと共用する座標
 varying vec2 textureCoordinate;
 varying vec2 leftTextureCoordinate;
 varying vec2 rightTextureCoordinate;
 
 varying vec2 topTextureCoordinate;
 varying vec2 topLeftTextureCoordinate;
 varying vec2 topRightTextureCoordinate;
 
 varying vec2 bottomTextureCoordinate;
 varying vec2 bottomLeftTextureCoordinate;
 varying vec2 bottomRightTextureCoordinate;
 
 void main()
 {
     //頂点シェーダはお約束のようにgl_Positionにpositionを渡す
     gl_Position = position;

     //近傍領域3x3の計算のためにあらかじめ変数化しておく
     //横1px分だけ（縦は0）の値
     vec2 widthStep = vec2(texelWidth, 0.0);
     //縦1px分だけ（横は0）の値
     vec2 heightStep = vec2(0.0, texelHeight);
     //縦横1pxの値
     vec2 widthHeightStep = vec2(texelWidth, texelHeight);
     //縦1px横-1pxの値
     vec2 widthNegativeHeightStep = vec2(texelWidth, -texelHeight);

     //フラグメントシェーダの中央になる
     textureCoordinate = inputTextureCoordinate.xy;
     //左の点になる
     leftTextureCoordinate = inputTextureCoordinate.xy - widthStep;
     //右の点
     rightTextureCoordinate = inputTextureCoordinate.xy + widthStep;
     //上の点
     topTextureCoordinate = inputTextureCoordinate.xy - heightStep;
     //左上の点
     topLeftTextureCoordinate = inputTextureCoordinate.xy - widthHeightStep;
     //右上の点
     topRightTextureCoordinate = inputTextureCoordinate.xy + widthNegativeHeightStep;
     //下の点
     bottomTextureCoordinate = inputTextureCoordinate.xy + heightStep;
     //左下の点
     bottomLeftTextureCoordinate = inputTextureCoordinate.xy - widthNegativeHeightStep;
     //右下の点
     bottomRightTextureCoordinate = inputTextureCoordinate.xy + widthHeightStep;
 }
);
```