33
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Core Image入門(objc.io Issue 21: Camera and Photos "An Introduction to Core Image"の翻訳)

Last updated at Posted at 2015-07-29

objc.io Issue 21: Camera and Photos · February 2015An 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型)なので、型安全ではない。なので、期待した通りの型のパラメータを引き渡されることが保証できるよう気をつけよう。

フィルタの属性を調べてみる

フィルタの入出力に使えるパラメータを知りたければ、inputKeysoutputKeysを使って調べることができる。どちらの方法でもNSStringの配列を返す。

それぞれのパラメータについてもっと詳しく知りたければ、フィルタのattributesディクショナリから調べることもできる。入出力のパラメータ名は、どういった型のパラメータか、または最大値と最小値について紐付けて説明するようなディクショナリを持つ。例として、CIColorControlsフィルタのinputBrightnessパラメータに対応したディクショナリの例を見てみよう。

inputBrightness = {
    CIAttributeClass = NSNumber;
    CIAttributeDefault = 0;
    CIAttributeIdentity = 0;
    CIAttributeMin = -1;
    CIAttributeSliderMax = 1;
    CIAttributeSliderMin = -1;
    CIAttributeType = CIAttributeTypeScalar;
};

数値のパラメータの場合、ディクショナリには入力値の範囲を示すkCIAttributeSliderMinkCIAttributeSliderMaxのキーが含まれている。ほとんどのパラメータには、パラメータのデフォルト値に紐付いた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)

フィルタグラフから画像を得るには、CIContextCGImageを生成するよう命令してみよう。その際、引数には入力画像のサイズ(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!(何かの警句だと思うけど、はっきりわからない…)

33
27
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
33
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?