画像処理のデファクトスタンダードライブラリGPUImageの基本と応用

  • 254
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

iOSアプリ開発で高速に写真や動画にエフェクトを加える事が出来るGPUImageの基本的なパターンと応用方法を理解するためのメモ。

GPUImageライブラリはOpenGL ES2.0をベースとしているため、大抵の場合は処理にCPUを使った場合よりも高速な動作が特徴。その上、複雑な概念を理解する必要があるOpenGLについて知る必要はなく、さらにカメラのためのAVFoundationのフレームワークを触る必要もない。

これはもう画像処理やカメラアプリに使わない理由がないんだけど案外日本語の説明がなかったので調べて行った過程とフィルタの作成方法についてを書いてみた。構成は下記の通り。

  1. GPUImageの基本パターン
    1. 用意した静止画像へのフィルタ適用で基本パターンを解説。
    2. カメラでのリアルタイムフィルタリング利用のための基本パターン解説
  2. GPUImageの応用
    1. 複数フィルタを重ねがけする
    2. オリジナルのフィルタを作るための方法
    3. オリジナルフィルタをプログラマブルに調整するための解説

GPUImageの基本パターン

静止画像とカメラ動画へのエフェクトの適用(フィルタリング)の基本パターンをGPUImageのリポジトリにあるiOSのデモのコードをベースに解説する。

GPUImageの基本パターン: 静止画像にフィルタリング

GitHubにあるデモプロジェクトSimpleImageFilterでは、プロジェクト内に用意された画像ファイル"WID-small.jpg"に対してフィルタリングしている。

WID-small.jpgはこんな感じの写真

WID-small.jpeg

デモプロジェクトではフィルタリングにSobelフィルタを使っていて、輪郭抽出を行っている。

スクリーンショット 2013-10-13 19.26.35.png

重要なこと

GPUImageの静止画像フィルタリングで抑えておくべきなのは次の点のみ。

  • GPUImagePictureクラスが元画像でそこに処理するフィルタを追加する
    • addTarget:メソッドでフィルタを指定
  • GPUImageFilterクラスがフィルタになる
  • GPUImagePictureクラスを使い画像のフィルタリングを行う
    • processImageメソッドでフィルタリング処理を実行
  • 表示のためのViewはGPUImageViewクラスのインスタンスをつかう

静止画フィルタリングデモのソースコードは次のようになっている

重要な事をおさえるとソースコードでやっていることが明確になるので見てみる

- (void)setupDisplayFiltering;
{
    //元画像を用意
    UIImage *inputImage = [UIImage imageNamed:@"WID-small.jpg"];     
    sourcePicture = [[GPUImagePicture alloc] initWithImage:inputImage smoothlyScaleOutput:YES];

    //フィルタを用意
   //GPUImageFilterクラスを継承したGPUImageSobelEdgeDetectionFilterで
   //Sobelフィルタをインスタンス化している。
    sobelFilter = [[GPUImageSobelEdgeDetectionFilter alloc] init];

    //表示するためのviewを用意。
    GPUImageView *imageView = (GPUImageView *)self.view;
    [sobelFilter forceProcessingAtSize:imageView.sizeInPixels];

    //GPUImagePictureでフィルタを追加
    [sourcePicture addTarget: sobelFilter];
    //フィルタに表示すためのビューを指定
    [sobelFilter addTarget:imageView];

    //フィルタリング処理を実行
    [sourcePicture processImage];
}

GPUImageの基本パターン: 動画フィルタリング

GitHubにあるSimpleVideoFilterを使い、次はリアルタイムにカメラで撮影した動画にエフェクトを加えるコードを解説する。

重要なこと

動画でのフィルタリングは静止画像とほぼ同じ。

  • GPUImageVideoCameraクラスが元動画で処理するフィルタを追加する
    • addTarget:メソッドでフィルタを指定
  • GPUImageFilterクラスがフィルタになる
  • GPUImageVideoCameraクラスを使い撮影とフィルタリングを行う
    • startCameraCaptureメソッドで撮影とフィルタリング処理を開始
  • 表示のためのViewはGPUImageViewクラスのインスタンスをつかう

動画フィルタリングデモのコードは次のようになっている

重要な事を抑えた上でソースコードを見る。動画撮影では静止画像と比較してカメラ設定が細かいのが分かる。

- (void)viewDidLoad
{
    [super viewDidLoad];
    //カメラの設定
    videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
    videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
    videoCamera.horizontallyMirrorFrontFacingCamera = NO;
    videoCamera.horizontallyMirrorRearFacingCamera = NO;

    //フィルタのカメラへの追加
    filter = [[GPUImageSepiaFilter alloc] init];
    [videoCamera addTarget:filter];
    //表示用ビューの追加
    GPUImageView *filterView = (GPUImageView *)self.view;
    [filter addTarget:filterView];
    //撮影とフィルタリングの開始
    [videoCamera startCameraCapture];
}

基本パターンおさらい

基本パターンのおさらいをすると、静止画ならGPUImagePictureを使い、カメラならGPUImageVideoCameraを使うだけの違いになっている。

  • GPUImagePicture/GPUImageVideoCameraクラスが元画像でそこに処理するフィルタを追加する
    • addTarget:メソッドでフィルタを指定
  • GPUImageFilterクラスがフィルタになる
  • GPUImagePicture/GPUImageVideoCameraクラスを使い画像のフィルタリングを行う
    • processImage/startCameraCaptureメソッドでフィルタリング処理を実行
  • 表示のためのViewはGPUImageViewクラスのインスタンスをつかう

とても使いやすいライブラリってことがわかる。

GPUImageの応用

GPUImageの応用: 複数フィルタを重ねがけする

複数フィルタを重ねがけすることでより複雑なフィルタを作成することが出来る。例として最初に用いたエッジ検出された静止画像にセピア化させるフィルタで説明する。

スクリーンショット 2013-10-13 21.48.31.png

重要なこと

  • 複数フィルタはGPUImageFilterGroupクラスに追加
  • 複数フィルタはフィルタリングする順序でフィルタ同士ターゲット追加
    • エッジ検出したあとでセピア色にするならエッジにセピアを追加
- (void)viewDidLoad
{
    [super viewDidLoad];
    //元画像を用意
    UIImage *inputImage = [UIImage imageNamed:@"WID-small.jpg"]; 
    sourcePicture = [[GPUImagePicture alloc] initWithImage:inputImage smoothlyScaleOutput:YES];

    //フィルタ用意
    GPUImageSobelEdgeDetectionFilter *sobelFilter = [[GPUImageSobelEdgeDetectionFilter alloc] init];
    GPUImageSepiaFilter *sepiaImageFilter = [[GPUImageSepiaFilter alloc] init];

    //フィルタグループを作ってそこに追加していく
    GPUImageFilterGroup *filterGroup = [[GPUImageFilterGroup alloc] init];
    [filterGroup addFilter:sobelFilter];
    [filterGroup addFilter:sepiaImageFilter];
    [filterGroup setInitialFilters:@[sobelFilter]];
    [filterGroup setTerminalFilter:sepiaImageFilter];

    //フィルタはフィルタ同士追加していく
    [sobelFilter addTarget:sepiaImageFilter];

    //表示するためのviewを用意
    GPUImageView *imageView = (GPUImageView *)self.view;
    [filterGroup forceProcessingAtSize:imageView.sizeInPixels]

    //GPUImagePictureでフィルタグループ追加
    [sourcePicture addTarget:filterGroup];
    [filterGroup addTarget:imageView];

    [sourcePicture processImage];
}

フィルタグループにフィルタを順番に追加することでフィルタの一元管理をしてもらいたいものだけど、元のフィルタに次のフィルタを追加する必要があるらしい。

これはおそらくさらに多くのフィルタがあった場合や同じフィルタかけ合わせる場合などを考慮しているためで、上記2つのフィルタだけでは冗長に見えてしまうのかもしれない。ここらへんの理由を知っている人がいたら教えて欲しい。

GPUImageの応用: オリジナルフィルタを作成する

GPUImageには用意されたいくつかのフィルタがあるが、自前でフィルタを作ることも出来る。このオリジナルフィルタはOpenGLの頂点シェーダとフラグメントシェーダを用いて作成する。

これらシェーダについて大雑把に説明すると、頂点シェーダはテクスチャ位置を計算していて、フラグメントシェーダは、フラグメント(ピクセル)ごとに色を決めるための技術。2つのシェーダは"シェーディング言語 (GLSL: OpenGL Shading Language)"を使うことによって実現できる。

シェーダに関しては下記のサイトがとっかかりとしては最適かもしれない

『フラグメントシェーダー事始め』で勉強したメモ
http://d.hatena.ne.jp/shu223/20130114/1358664861

iOSアプリ開発の場合、シェーダはOpenGL ES2.0で使うことになるが、GPUImageは内部でそれをやってくれるので、GLSLを記述するだけで良い。

フラグメントシェーダによる赤と緑の置換フィルタ

赤の要素が緑で、緑の要素が赤のフィルタを作る。これは色を置換しているだけなのでフラグメントシェーダだけで簡単にできる。

スクリーンショット 2013-10-13 23.21.36.png

実際に記述していくにはまず、シェーディング言語を記述するファイルをプロジェクトに追加する。まずは赤と緑をピクセル上で入れ替えるためのフィルターを「ColorSwapFilter.fsh」として作成しプロジェクトに追加する。

このファイルはGPUImageが[NSBundle mainBundle]で読み込むため、Xcodeのターゲットから「Copy Bundle Resources」でリソースとして設定する必要がある(設定しないと読み込めず例外になる)。

実際のコードは次のようになる。テクスチャユニット番号を保持するinputImageTextureと座標値を保持するグローバル変数textureCoordinateが登場しているが、これらはGPUImageでオリジナルフィルタを使う上でのお約束だと思おう。これらの変数を用いてtexture2D関数でピクセルごとの色を取得し、赤と緑を置き換えている。

//グローバル変数
/*
  inputImageTextureという変数名はGPUImageで指定されている。
  GPUImage(OpenGL)側で初期化されテクスチャ情報がサンプリングされている。
  uniformはシステムから渡される定数であることを示す。
  sampler2Dは2Dのテクスチャをサンプリングするデータ型。uniformである必要がある。 
*/
uniform sampler2D inputImageTexture;

/*
  textureCoordinateという変数名はGPUImageで指定されている。
  GPUImage(OpenGL)側で頂点シェーダにより座標値が書き込まれている。
  varyingは頂点シェーダで書き込まれていることを示す。
  highpは精度が高いことを示す。
  vec2は2Dベクトルを示す型。
*/
varying highp vec2 textureCoordinate;

//エントリポイントmainはC言語とは違い戻り値は整数を返さない
void main()
{
    //texture2Dを使い、指定のテクスチャユニットに対してピクセル単位で色を取得する
    //引数の1つ目でテクスチャユニットを指定
    //引数の2つ目で頂点シェーダにより指定された座標値を取得
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
    //出力するピクセルの色を設定する変数
    lowp vec4 outputColor;
    outputColor.r = textureColor.g;  //赤に緑
    outputColor.g = textureColor.r;  //緑に赤
    outputColor.b = textureColor.b;  //青は青
    outputColor.a = 1.0;             //アルファ1.0で不透明

    //グローバルなgl_FragColorが出力するピクセルの色になる
    gl_FragColor = outputColor;
}

次に、作成したフラグメントシェーダを読み込むコードを書く。フィルタとして何度も登場しているGPUImageFilterのinitWithFragmentShaderFromFile:メソッドでフラグメントシェーダのファイル名を指定するのみ。

- (void)filterFromShaderFile
{
    //元画像を用意
    UIImage *inputImage = [UIImage imageNamed:@"WID-small.jpg"]; 
    sourcePicture = [[GPUImagePicture alloc] initWithImage:inputImage smoothlyScaleOutput:YES];

    //フィルタ用意。ColorSwapFilter.fshを読み込ませる。拡張子はいらない
    GPUImageFilter *filter = [[GPUImageFilter alloc] initWithFragmentShaderFromFile:@"ColorSwapFilter"];

    //表示するためのviewを用意
    GPUImageView *imageView = (GPUImageView *)self.view;
    [filter forceProcessingAtSize:imageView.sizeInPixels]; 

    //フィルタを対象として追加 
    [sourcePicture addTarget:filter];
    [filter addTarget:imageView];

    //フィルタリング処理を実行
    [sourcePicture processImage];
}

これで実行できるはずだし、なんとなくフラグメントシェーダが分かってきたと思う。
理解を深めるために、フラグメントシェーダを読み込むGPUImageFilterのinitWithFragmentShaderFromFile:メソッドを見てみると、記述しなかった頂点シェーダがデフォルトで記述されていることがわかる。

NSString *const kGPUImageVertexShaderString = SHADER_STRING
(
 attribute vec4 position;
 attribute vec4 inputTextureCoordinate;

 varying vec2 textureCoordinate;  //フラグメントシェーダに渡すxy頂点座標

 void main()
 {
     gl_Position = position; //モデルビュー射影変換後の座標
     textureCoordinate = inputTextureCoordinate.xy;
 }
 );

まず、SHADER_STRING()はObjective-CのなかでGLSLを記述するマクロ。
textureCoordinateはフラグメントシェーダで利用した座標値だった。デフォルトの頂点シェーダでは単に座標値をグローバル変数にセットしてフラグメントシェーダに受け渡しをしているだけだ。

GPUImageの応用: GPUImageでシェーダと値をやりとりする

シェーダの記述方法は分かったが、シェーダを自在に操れないのでは変化のないフィルタとなってしまう。条件によってシェーダを微調整したいこともあるはずだろう。Objective-Cの変数とシェーディング言語上の変数(厳密には定数)で値をやりとりする場合はどのようにすればよいかは、既存のGPUImageBrightnessFilterを読むと分かる。

GPUImageBrightnessFilterを読む

GPUImageBrightnessFilterは明るさを調整するフィルタだ。明るさはGPUImageBrightnessFilterクラスのプロパティCGFloat brightnessを変更することで調整できるようになっている。

ヘッダーは次のようになっている。

#import "GPUImageFilter.h"

@interface GPUImageBrightnessFilter : GPUImageFilter
{
    GLint brightnessUniform;
}

// Brightness ranges from -1.0 to 1.0, with 0.0 as the normal level
@property(readwrite, nonatomic) CGFloat brightness; 

@end

実装ファイルの初期化メソッドではメンバ変数brightnessUniformに対して、シェーディング言語で使う定数"brightness"のindexを渡している。indexはシェーディング言語中の変数番号であり、アクセスするためのindexになる。

- (id)init;
{
    if (!(self = [super initWithFragmentShaderFromString:kGPUImageBrightnessFragmentShaderString]))
    {
        return nil;
    }
    //メンバ変数に
    brightnessUniform = [filterProgram uniformIndex:@"brightness"];
    self.brightness = 0.0;

    return self;
}

次に、brightnessプロパティのsetterを見るとシェーディング言語中の定数brightnessに対して、セットする値を代入している。

- (void)setBrightness:(CGFloat)newValue;
{
    _brightness = newValue;

    [self setFloat:_brightness forUniform:brightnessUniform program:filterProgram];
}

同ファイルにはフラグメントシェーダも記述してありbrightnessを使って明度を調整していることが分かるだろう。

NSString *const kGPUImageBrightnessFragmentShaderString = SHADER_STRING
(
 varying highp vec2 textureCoordinate;

 uniform sampler2D inputImageTexture;
 uniform lowp float brightness;

 void main()
 {
     lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);

     gl_FragColor = vec4((textureColor.rgb + vec3(brightness)), textureColor.w);
 }
);

ここまでくるとuniformは定数で、シェーディング言語に渡された定数であるという説明もわかってくるのではないかと思う。

より良いフィルタを作るため

より良いオリジナルフィルタを作るため、OpenGLについて知りたい場合は、古い本だけれど「OpenGLプログラミングガイド」が基本的なことについて抑えてある。



Amazonへのリンク: OpenGLプログラミングガイド 原著第5版

Amazonのレビューでは最近の主流であるOpenGL4.0では古すぎて役に立たないと書かれているが、今のところGPUImageで利用するのはOpenGL ES2.0なので微妙に役に立つのではないかと思う。800ページ以上もあるので枕になるし。

最近の本だと「OpenGL+GLSLによる画像処理プログラミング」は3Dではなく画像処理に徹していてGPUImageを使って画像処理を行うには最適な一冊。



OpenGL+GLSLによる画像処理プログラミング―「OpenGL」と「シェーダ言語」で「レタッチ・ソフト」の仕組みを知る! (I・O BOOKS)

工学社I/O Booksの本は、本自体が小さいにも関わらず文字サイズが大きく、ソースコードを載せる技術書としてかなり読みづらい。また行間の調整もイマイチで図表とテキストの行間が狭く印刷物としての余白の美しさみたいなものがないので敬遠してたんだけど内容は良かった。

なぜGLSLは高速に処理を行えるのか

フラグメントシェーダや頂点シェーダはGPUを用い、画像の全体あるいは一部の領域の画素に対し画素値の計算を同時に実行する。GPUはSIMD演算ユニットで構成され、1回の命令で複数のデータに対する処理を同時に行う。

SIMD演算の概要
http://cell.fixstars.com/ps3linux/index.php/2.1_SIMD%E6%BC%94%E7%AE%97%E3%81%AE%E6%A6%82%E8%A6%81

if文の処理

GPUは並列に命令を処理する性質上if elseの命令両方をセットしなくてはならず、命令を実質的に実行するかどうかで処理が変わる。セットするだけでもコストかかるのでelse側の命令を少なくすべき

Kepler GPUアーキテクチャとプログラム最適化
http://news.mynavi.jp/series/kepler_gpu/002/

iPhoneのGPUについて

iPhoneのGPUはプロセッサ内にあり世代別に違いがある。

iPhone GPU 備考
iPhone5s iPowerVR Series 6 クアッドコア G6340
iPhone5 iPowerVR Series 5XT トリプルコア SGX543MP3
Phone4S iPowerVR Series 5XT デュアルコア SGX543MP2
Phone4 iPowerVR Series 5 SGX535

PowerVR - Wikipedia
http://en.wikipedia.org/wiki/PowerVR