すでに投稿してある画像処理のデファクトスタンダードライブラリ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;
}
);