objc.io Issue 21: Camera and Photos · February 2015のAn Introduction to Core Imageの翻訳。
Core Image入門
By Warren Moore
この記事は、OS XやiOS向けの画像処理フレームワークのCore Imageについての入門記事だ。
この記事で挙げているコードに沿って読み進めたければ、GitHubのサンプルプロジェクトをダウンロードしてほしい。このサンプルのiOSアプリでは、システムで提供されているたくさんのフィルタを使えて、UIからエフェクトのかかり具合を見ながら調整できる。
サンプルコードはiOS向けのSwiftで書かれているけれど、考え方はObjective-CやOS Xにも応用できる。
基本的なコンセプト
Core Imageの話をする前に、基本的なコンセプトについて説明しておこう。
_フィルタ_とは、いくつかの入力値と出力値を持ち、変化させるオブジェクトを指す。例えば、ぼかしフィルタは入力画像とぼかし範囲に応じて、ぼかされた出力画像を生成する。
_フィルタグラフ_は、あるフィルタの出力値が別なフィルタの出力値となるように組み合わされるフィルタのネットワーク(有向非巡回グラフ)のことを指す。このやり方で、複雑なエフェクトを実現できる。この記事の後のほうで、フィルタを組み合わせて古びた感じのエフェクトを加える方法について見てみよう。
Core Image APIに慣れてみる
このコンセプトに基づいて、Core Imageを使った画像のフィルタリングのかけ方について調べてみよう。
Core Imageのアーキテクチャ
Core Imageはプラグインアーキテクチャを備えている。つまり、システムで提供されているフィルタとユーザーが自作したフィルタを組みあわせ、機能を拡張できるアーキテクチャだ。この記事では、Core Imageの拡張性については触れない。フレームワークのAPIがどう作用するかという点だけ、説明する。
Core Imageはほとんどのハードウェア上で動作するように書かれている。各フィルタは、GLSL(OpenGLのシェーダ言語)のサブセットで書かれた_カーネル_で実装されている。複数のフィルタを組み合わせてフィルタグラフを構成するとき、Core Imageはカーネルを数珠つなぎにして、GPU上で実行される一つのプログラムをビルドする。
Core Imageは、必要とされる時まで、できるだけ実行するタイミングを遅らせる。フィルタグラフ中の最後のフィルタが呼び出されるまで、メモリが確保されなかったり処理されなかったりする場合もよくある。
Core Imageを動作させるためには、_コンテキスト_と呼ばれるオブジェクトが必要になる。コンテキストはCore Imageのフレームワークで色々便利に使えるもので、必要なメモリを割り当てたり、画像処理を行うフィルタのカーネルをコンパイルして実行したりもする。コンテキストは生成コストがとても高いので、ひとつだけ生成して何回も使い回したくなるだろう。コンテキストを生成する方法については後述する。
フィルタを探してみる
Core Imageのフィルタは名前を指定して生成する。システムで提供されているフィルタの一覧を知りたければ、kCICategoryBuiltIn
のカテゴリを指定してフィルタの名前を確認してみよう。
let filterNames = CIFilter.filterNamesInCategory(kCICategoryBuiltIn) as [String]
OSXで使えるものの大部分が、iOSでも使える。OS Xでは169個のフィルタが提供されているが、iOSでは127個提供されている。
指定した名前のフィルタを生成してみる
フィルタのリストが手に入ったので、フィルタを生成して使ってみよう。例えば、Gausian Blurのフィルタを生成したければ、CIFilter
のイニシャライザにフィルタ名を引き渡してみよう。
let blurFilter = CIFilter(named:"CIGaussianBlur")
フィルタのパラメータを設定してみる
Core Imageはプラグインの仕組みでできているので、ほとんどのフィルタのプロパティには直接値を設定できない。その代わりに、キー値コーディング(KVC)を使う。例えば、ぼかす範囲を指定したい場合、KVCを使ってinputRadius
にこうして値を設定する。
blurFilter.setValue(10.0 forKey:"inputRadius")
このメソッドの引数はAnyObject?
(Objective-Cのid
型)なので、型安全ではない。なので、期待した通りの型のパラメータを引き渡されることが保証できるよう気をつけよう。
フィルタの属性を調べてみる
フィルタの入出力に使えるパラメータを知りたければ、inputKeys
とoutputKeys
を使って調べることができる。どちらの方法でもNSString
の配列を返す。
それぞれのパラメータについてもっと詳しく知りたければ、フィルタのattributes
ディクショナリから調べることもできる。入出力のパラメータ名は、どういった型のパラメータか、または最大値と最小値について紐付けて説明するようなディクショナリを持つ。例として、CIColorControls
フィルタのinputBrightness
パラメータに対応したディクショナリの例を見てみよう。
inputBrightness = {
CIAttributeClass = NSNumber;
CIAttributeDefault = 0;
CIAttributeIdentity = 0;
CIAttributeMin = -1;
CIAttributeSliderMax = 1;
CIAttributeSliderMin = -1;
CIAttributeType = CIAttributeTypeScalar;
};
数値のパラメータの場合、ディクショナリには入力値の範囲を示すkCIAttributeSliderMin
とkCIAttributeSliderMax
のキーが含まれている。ほとんどのパラメータには、パラメータのデフォルト値に紐付いたkCIAttributeDefault
キーも含まれている。
フィルタ処理を試してみよう
画像へのフィルタ処理は、フィルタグラフの構築と設定、入力画像をフィルタに送る、フィルタ処理済みの画像を取得する、の3つの工程からなる。この章では、詳細に説明していく。
フィルタグラフを構築する
フィルタグラフの構築は、目的に応じたフィルタを生成する、パラメータを設定する、そして画像データを流し込むようにフィルタを組み合わせる、ということから成っている。
この章では、19世紀のティンタイプ写真のような画像を生成するフィルタグラフを構築してみよう。このような効果を得るために、二つのエフェクトを組み合わせてみる: 画像の彩度を下げ、淡い色合いにするためにモノクロームフィルタをかけた後、画像の枠の部分をぼかすフィルタの2種類だ。
フィルタグラフのプロトタイプ作りには、Apple Developerのサイトからダウンロードできる、Quartz Composerが便利だ。下の例のように、Color MonochromeフィルタとVignetteフィルタを組み合わせて狙い通りの効果を得るフィルタを作成してみた。
フィルタの効果に満足できたなら、コード上でフィルタグラフを再現してみよう。
let sepiaColor = CIColor(red: 0.76, green: 0.65, blue: 0.54)
let monochromeFilter = CIFilter(name: "CIColorMonochrome",
withInputParameters: ["inputColor" : sepiaColor, "inputIntensity" : 1.0])
monochromeFilter.setValue(inputImage, forKey: "inputImage")
let vignetteFilter = CIFilter(name: "CIVignette",
withInputParameters: ["inputRadius" : 1.75, "inputIntensity" : 1.0])
vignetteFilter.setValue(monochromeFilter.outputImage, forKey: "inputImage")
let outputImage = vignetteFilter.outputImage
Color Monochromeフィルタから出力された画像がVignetteフィルタの入力画像になっていることに注意してほしい。こうすることで、モノクロームに加工した画像をぼかせるようになる。また、KVCを使わずにイニシャライザからパラメータを設定していることにも注意しておこう。
入力画像を生成する
Core Imageのフィルタには、CIImage
型の入力が画像が必要になる。UIImage
を使っているiOSのプログラマーにはあまり馴染みが無いかもしれないけれど、この違いがメリットになる。CIImage
は無限なサイズを扱えるので、UIImage
より広範に扱える。実際には、メモリ上に無限な大きさの画像を保持することはできない。けれど、概念的には2D空間中で指定した領域から画像データを取り出し、有意義な結果を得ることができる。
この記事の中では、UIImage
からCIImage
を生成しやすいように、どの画像も有限のサイズのものを使っている。実際に、たった1行で済ませられる。
let inputImage = CIImage(image: uiImage)
この他にも、画像のデータやファイルのURLから直接CIImage
を生成する便利なイニシャライザもある。
CIImage
を生成したあと、フィルタのinputImage
パラメータに設定すると、フィルタグラフの入力画像として設定することができる。
filter.setValue(inputImage, forKey:"inputImage")
フィルタした画像を取得する
フィルタには、outputImage
という名前のプロパティがある。予想通り、CIImage
型だ。CIImage
からUIImage
を生成するのとは逆の手順になるのかって?そう、ここまでフィルタグラフの作り方について学んできたけれど、ここからはCIContext
の力を生かして、画像にフィルタをかけてみよう。
CIContext
は、コンストラクタの引数にnilを渡すと簡単に生成できる。
let ciContext = CIContext(options: nil)
フィルタグラフから画像を得るには、CIContext
にCGImage
を生成するよう命令してみよう。その際、引数には入力画像のサイズ(bounds)を出力画像の範囲として渡す必要がある。
let cgImage = ciContext.createCGImage(filter.outputImage, fromRect: inputImage.extent())
出力画像の大きさはは入力画像と異なっている場合がよくあるから、入力画像のサイズを引数として使うことが多い。例えば、ぼかし画像は入力画像の端を超えて補間する場合があるから、端の部分に余分なピクセルが追加されてしまう場合がある。
これでやっとCGImage
からUIImage
を生成できる。
let uiImage = UIImage(CGImage: cgImage)
CIImage
からUIImage
を直接生成することもできるけど、このやり方をすると気になることも多い。例えば、UIImageView
の画像を表示しようとするような場合、contentMode
は無視されてしまう。CGImage
経由で生成するやり方は余分な処理を踏んでいるけど、こういう厄介なことが起こらない。
OpenGLを使ってパフォーマンスを向上してみる
UIKit
の描画に引き渡すためだけのために、CGImage
をCPUで描画させるのは時間がかかるし、無駄だ。Core Graphics
を経由せず、スクリーンに描画させられる方が良い。幸運にも、OpenGLとCore Imageには互換性があるので、そうすることができる。
OpenGLのcontextとCore Imageのcontextを共用させるには、ちょっと違う方法でCIContext
を生成する必要がある。
let eaglContext = EAGLContext(API: .OpenGLES2)
let ciContext = CIContext(EAGLContext: context)
この例では、EAGLContext
をOpenGL ES 2.0の機能セットに従って生成している。このGL contextはGLKView
のコンテキストかCAEAGLLayer
に描画するために使うことができる。効率的に画像を描画するため、サンプルではこの方法で生成している。
CIContext
をGL contextに紐付けたら、こう書くとフィルタした画像をOpenGLで描画することができる。
ciContext.drawImage(filter.outputImage, inRect: outputBounds, fromRect: inputBounds)
前にも書いたとおり、fromRect
パラメータはフィルタした画像の中から、描画する領域を指す。inRect
パラメータは、GL contextの座標系の中で画像を書き出す領域を示す。画像のアスペクト比に注意するのなら、inRect
の領域が適切なものになるよう計算する必要があるだろう。
CPUにフィルタ処理を実行させる
Core Imageは可能な限り、GPUでフィルタ処理を実行する。ただ、CPUにフォールバックさせることもできる。CPUで実行したフィルタ処理は、GPUよりもずっと正確だ。GPUは浮動小数点計算の正確さと引き換えに、スピードを優先させている。コンテキスト生成時にoptionsディクショナリのkCIContextUseSoftwareRenderer
キーにtrue
を指定してやると、Core ImageをCPUで実行させることができる。
Xcodeのスキーマ設定で、CI_PRINT_TREE
環境変数に1
を設定してやっても、CPUとGPUのレンダラのうちどちらを使うか指定することもできる。こうすることで、フィルタ処理を加えた画像が描画されるたび、デバッグに役立つ情報がCore Imageから出力される。この設定は、フィルタツリーの構成が正しいかどうか検査するのにも役に立つ。
サンプルアプリの紹介
この記事のサンプルで使っているコードはiPhone用のアプリで、iOS向けのCore Imageで使えるたくさんのフィルタを紹介している。
フィルタのパラメータからGUIを生成する
すべてのフィルタを紹介するため、サンプルアプリでは、Core imageの内省的な性質を利用し、対応しているフィルタのパラメータを操作できるGUIを生成しよう。
サンプルアプリでは、一枚の画像にだけフィルタ処理するように制限している。0枚や2枚以上の入力には対応していない。1枚だけに処理できるフィルタ以外にも、面白いフィルタがある。(合成フィルタとかトランジションフィルタとか)そういう制限はあるけれど、このサンプルアプリでCore Imageで使える機能の大枠はつかめるはずだ。
フィルタの入力パラメータはどれも、パラメータの最小値から最大値の間のスライダーで設定でき、デフォルト値に設定してある。スライダーの値が変わると、CIFilter
を参照しているUIImageView
のサブクラスにあるdelegateに対して、変わったことを通知する。
ビルトインされたフィルタを使ってみる
サンプルアプリではたくさんのビルトインフィルタに加え、iOS7で追加されたフィルタも試せるようにしている。これらのフィルタは調整できるパラメータがないけれど、iOSの写真アプリで使えるエフェクトをエミュレートできるというメリットがある。
まとめ
この記事では、高いパフォーマンスで画像処理できるフレームワークの、Core Imageを簡単に紹介してきた。この短い構成で、できるだけ実用的にこのフレームワークのたくさんの機能を紹介できるようにしてきた。Core Imageのフィルタの生成の仕方や組み合わせ方、フィルタグラフから画像を取り出す方法、狙い通りの結果を得るためにパラメータを調整する方法について、学べただろう。また、システムが提供しているフィルタの使い方や、iOSの写真アプリと同じようにふるまわせる方法についても学んできただろう。
もう、自分で画像編集できるアプリケーションを作れるくらい十分学んだはずだ。もう少し色々試してみると、MacやiPhoneの能力をフル活用して、未だ想像したこともないような効果のフィルタを自分で書くこともできるだろう。Go forth and filter!(何かの警句だと思うけど、はっきりわからない…)