LoginSignup
15
15

More than 5 years have passed since last update.

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

Last updated at Posted at 2013-10-22

すでに投稿してある画像処理のデファクトスタンダードライブラリGPUImageの基本と応用のプログラマブルシェーダについてを分けて投稿する。

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

GPUImage3x3ConvolutionFilterクラスを読む

関連するソースコード

カーネルの設定について

GPUImage3x3ConvolutionFilter.hには、3x3のカーネルを保持するプロパティがあり、これに3x3のカネールを渡すことで画像をフィルタリングすることが出来る。

@property(readwrite, nonatomic) GPUMatrix3x3 convolutionKernel;

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

[(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構造体の中にGPUVector3構造体が3つあり、データを設定しやすいようにしている。

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という変数としてシェーダに渡される。

- (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カーネルを渡したフラグメントシェーダ

フラグメントシェーダ側にはObjective-Cのコードからグローバルな定数として渡される。そのためuniformとなる。

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()
 {
     //まず近傍画像3行目の色を取得
     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;
     //近傍画像1行目の色を取得
     mediump vec3 topColor = texture2D(inputImageTexture, topTextureCoordinate).rgb;
     mediump vec3 topRightColor = texture2D(inputImageTexture, topRightTextureCoordinate).rgb;
     mediump vec3 topLeftColor = texture2D(inputImageTexture, topLeftTextureCoordinate).rgb;

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

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

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

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

頂点シェーダはGPUImage3x3ConvolutionFilterが継承するGPUImage3x3TextureSamplingFilterに実装されている

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

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

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

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

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

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;
 }
);
15
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
15