GPUImageという、GPUで画像に簡単にフィルタをかけられるライブラリがあります。
今回はこれを使ってツールを作ろうと思っているので、使い方なんかをメモしておこうと思います。
ライブラリとしてプロジェクトに追加する
これは単純に自分のiOS開発の経験不足ですが、ライブラリとして追加するにあたって、調べて出てくる情報がXcodeのバージョン違いなどでなかなか解決できなかったのでメモです。
ちなみに、.xcodeproj
ファイルをプロジェクトにDrag & Dropする方法での追加です。
ドラッグアンドドロップで .xcodeproj
を追加
GitHubのGPUImageリポジトリからファイルを落としてきたら、その中の「framework」内にある GPUImage.xcodeproj
ファイルを、自分のプロジェクトにドラッグアンドドロップで追加します。
依存関係を解決する
ただこのままだとまだ依存関係が解決されておらず、ビルドできません。
いくつか調べていたらターゲットに追加する、みたいなことを見かけたんですが、プロジェクトファイルを選択すると出てくるターゲットではなく、 Scheme設定 > Build > Targets のほうっぽいです。
こちらに追加したらビルドができました。
余談
余談ですが、実行ボタンをAltキーを押しながらクリックするとすぐSchemeの設定ウィンドウを表示できるので便利です。
Link Binary With LibrariesにGPUImage.frameworkを追加する
Build Phasesにある Link Binary With Libraries にも GPUImage.framework を追加します。
こうしてGPUImageのプロジェクトを追加することができたら、いよいよGPUImageを使っていきます。
GPUImageでHello Worldしてみる
GPUImageにはいくつかのインターフェースが準備されていて、画像だけでなく、動画などカメラの入力に対してフィルタをかける、というようなこともできるようです。
が、今回は普通の静止画に対してフィルタをかける方法です。
GPUImagePictureを使う
UIImage *image = [UIImage imageNamed:@"logo.jpg"];
GPUImagePicture *imageSource = [[GPUImagePicture alloc] initWithImage:image];
GPUImageSepiaFilter *imageFilter = [[GPUImageSepiaFiler alloc] init];
[imageSource addTarget:imageFilter];
[imageFilter useNextFrameForImageCapture];
[imageSource processImage];
UIImage *filteredImage = [imageFilter imageFromCurrentFramebuffer];
UIImageView *imageView = [[UIImageView alloc] initWithImage:filteredImage];
[aView addSubview:imageView];
上記の例は、読み込んだ画像に対してセピアフィルタをかけているところです。
GPUImagePicture
で画像を準備し、GPUImageFilter
を追加して出力しています。
GPUImageではこんなふうに、フィルタを追加していく形で実装するようです。
なので、いくつかのフィルタを重ねてかけることもできるようになっています。
複数フィルタを重ねがけしてみる
最初、フィルタを複数作って addTarget
を繰り返していけばいいのかな、と思っていたんですが、どうやらもう少し複雑なようです。
具体的には以下のように指定します。まずはおおまかな流れを示します。
複数フィルタを追加するフロー
-
GPUImagePicture
インスタンスを画像を指定して生成 - 必要な数のフィルタを生成する
-
GPUImageFilterGroup
インスタンスを生成 - フィルタグループに、必要なだけフィルタを追加(
addFilter:
)する - 初期化用のフィルタを配列で指定する(
setInitialFilters:
) - 終端(最後)のフィルタを指定する(
setTerminalFilter:
) - フィルタをつなげる(フィルタをかける順番を指示する)→ これには数珠つなぎになるように、前のフィルタの
addTarget:
メソッドに、次のフィルタを引数として渡してやります。 - (1)で生成した
GPUImagePicture
インスタンスに、セットアップしたフィルタグループをaddTarget:
する。 -
useNextFrameForImageCapture
メソッドを実行する - フィルタを実行する(
processImage
)
コード
さて、なにはともあれコードを見たほうが早いと思うので例を。
// 画像を読み込み
UIImage *image = [UIImage imageNamed:@"logo.jpg"];
// 静止画用のオブジェクトを作る
GPUImagePicture *stillImageSource = [[GPUImagePicture alloc] initWithImage:image];
// フィルタ生成
GPUImageSobelEdgeDetectionFilter *sobelFilter = [[GPUImageSobelEdgeDetectionFilter alloc] init];
GPUImageSepiaFilter *sepiaImageFilter = [[GPUImageSepiaFilter alloc] init];
// フィルタグループを作成
GPUImageFilterGroup *filterGroup = [[GPUImageFilterGroup alloc] init];
// ソーベルフィルタを追加
[filterGroup addFilter:sobelFilter];
// セピアフィルタを追加
[filterGroup addFilter:sepiaImageFilter];
// 初期化用フィルタを指定
[filterGroup setInitialFilters:@[sepiaImageFilter]];
// 最後のフィルタを指定
[filterGroup setTerminalFilter:sobelFilter];
// フィルタはフィルタ同士追加する
[sepiaImageFilter addTarget:sobelFilter];
// GPUImagePictureにフィルタグループ追加
[stillImageSource addTarget:filterGroup];
// フィルタ実行
[filterGroup useNextFrameForImageCapture];
[stillImageSource processImage];
UIImage *filterdImage = [filterGroup imageFromCurrentFramebuffer];
UIImageView *imageView = [[UIImageView alloc] initWithImage:filterdImage];
[self.view addSubview:imageView];
基本的な処理は、ひとつのフィルタを追加するのとあまり代わりません。
違いは、単体のフィルタに対して行っていた処理を GPUImageFilterGroup
に行う点でしょう。
(グループもひとつのフィルタと考えると分かりやすいと思います)
気をつける点
GPUImageFilterGroup
の addFilter:
を実行するだけではなく、生成したフィルタ同士を連結させないとならない点に注意が必要です。
(addFilter:
した順番での処理ではなさそう)
後述しますが、画面にフィルタ結果を出すだけなら GPUImageView
を使いますが、UIImage
として書き出したい場合、useNextFrameForImageCapture
メソッドを実行しておかないと出力されません。
想像ですが、フィルタの連結と関係あるのかなと。(出力用バッファに接続するイメージ?)
設定が済んだら、あとは processImage
でフィルタを実行し、imageFromCurrentFramebuffer
で画像を取得します。
画像は UIImage
なので、あとは普通に画像を使う要領で使用することができます。
GPUImageViewを使って画面にフィルタ結果を表示する
ちなみに画面に表示するだけであれば、GPUImageView
を使うのが簡単です。
これはおそらく、内部的にバッファへの接続などをよしなにやってくれるものだと思います。
使い方は以下の通り。
コード例
GPUImageView *imageView = [[GPUImageView alloc] initWithFrame:self.view.bounds];
imageView.fillMode = kGPUImageFillModePreserveAspectRatio;
[self.view addSubview:imageView];
[filterGroup forceProcessingAtSize:imageView.sizeInPixels];
[filterGroup addTarget:imageView];
ちなみに上記の場合は useNextFrameForImageCapture
の実行は必要ありません。
オリジナルのフィルタを追加する
GPUImageでは、オリジナルのフィルタを作成する方法も準備されています。
オリジナルフィルタを生成するには フラグメントシェーダ を自分で書く必要があります。
ただ、それ以外のセットアップはGPUImage側で行ってくれ、さらに必要最小限の記述でフィルタを作成することが可能になっています。
GLSLを記述する
まずはGLSLでフラグメントシェーダを書きます。
/**
* 赤と緑を入れ替えるフィルタ
*/
uniform sampler2D inputImageTexture;
varying highp vec2 textureCoordinate;
void main() {
vec4 textureColor = texture2D(inputImageTextre, textureCoordinate);
vec4 outputColor;
outputColor.r = textureColor.g;
outputColor.g = textureColor.r;
outputColor.b = textureColor.b;
outputColor.a = 1.0;
}
GPUImageでフィルタ登録する
次に、今書いたGLSLをGPUImageのフィルタとして機能させます。
// 前略(フィルタ生成部分以外は他と同じ)
GPUImageFilter *customFilter = [[GPUImageFilter alloc] initWithFragmentShaderFromFile:@"ColorSwapFilter"];
[imageSource addTarget:customFilter];
[customFilter useNextFrameForImageCapture];
[imageSource processImage];
// 後略(フィルタから画像を生成する手順は同じ)
以上の手順で、簡単にカスタムフィルタを使うことができます。
テキストからGLSLを生成する
initWithFragmentShaderFromFile:
は、内部では initWithFragmentShaderFromString:
を呼んでおり、テキスト情報を使ってGLSLを生成することも可能です。
そしてGPUImageには便利なマクロが準備されています。
#define STRINGIZE(x) #x
#define STRINGIZE2(x) STRINGIZE(x)
#define SHADER_STRING(text) @ STRINGIZE2(text)
SHADER_STRING
マクロを使うと、煩わしダブルクォーテーションなどの記述が不要になり、以下のようにGLSLをそのまま書けるようになります。
// GPUImageのFilterから一部抜粋
NSString *const kGPUImage3x3ConvolutionFragmentShaderString = SHADER_STRING
(
uniform sampler2D inputeImageTexture;
// ... 略
);
ちょっとしたGLSLなどは文字列で保持しておいたほうが分かりやすいケースもあるかと思います。
そんなときに便利なマクロですね。
FiltersでGLSLを書こう!
ちなみにFiltersとはiOSアプリとWebサイト上にあるエディタと接続することで、PCで リアルタイム にカメラのフィルタを作成できるサービスです。(もちろんそれを保存してあとで使うこともできます)
ここで色々試してみたり、あるいはここで公開されているものを、GPUImage向けに変更して使うと面白いフィルタがすぐに使えると思います。