GaussianBlurFilter で学ぶ自作 CIFilter（Filter-Chain の場合）の作り方

どうもお久しぶりです、いろんなものに手を突っ込んで墓穴を掘ることを日課としている筆者です。最近は画像フィルターの製作に悩まされたりしているところです。

CoreImage は便利だよ。はい。通常の UIKit と比べると非常に資料が少なくて調べるのに大変だけど。

そして一時期 [GPUImage](https://github.com/BradLarson/GPUImage) と言うフレームワークも使ってみたけどまあベースは Objective-C だからね、Swift のプロジェクトでも使えるけどまあカスタマイズしようとすると大変ですね。そして最近これの Swift 版である [GPUImage2](https://github.com/BradLarson/GPUImage2) も同じ作者からリリースされたけど、これはこれでなんか微妙に命名規約がキモかったりするんだよね…

と言うわけで結局散々チャレンジしてみた結果おとなしく CoreImage 使うことにしました。

さて、本題に戻りますが、`CIFilter` ていうフィルタークラスが CoreImage に提供されているけど、これもなかなかのクセモノですね。`UIView` みたいにベースクラスを作っていろんなサブクラスを定義されたわけではなく、すべて `CIFilter(name: String)` で作られるから全部 `CIFilter` オブジェクトだったり、その `name` が提供された定数で決めてるわけではなくドキュメント読んで名前を引き出さねばならんかったり、さらにこれオプショナルイニシャライザーだから名前間違えると `nil` で返されたりする可能性もあったり、おまけに仮にちゃんと作られたとしてもそのフィルターはどんなプロパティーが設定できるかもわからなかったりと。まあクソ面倒ですね。

と言うわけで `CIFilter` をベースとした自作サブクラスを作ることにした。

[公式ドキュメント](https://developer.apple.com/library/prerelease/content/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_filer_recipes/ci_filter_recipes.html#//apple_ref/doc/uid/TP30001185-CH4-SW1)ではこう書いてあります：

> You can create custom effects by using the output of one image filter as the input of another, chaining as many filters together as you’d like. When you create an effect this way that you want to use multiple times, consider subclassing CIFilter to encapsulate the effect as a filter.

つまりフィルターチェインを作りたいなら自分のサブクラスを作れ、と。そして一応サンプルコードとして、Objective-C ですがまあ参考程度になるものがあるから自分でなんとかしろ、と

```objc:CIColorInvert.h
@interface CIColorInvert: CIFilter {
    CIImage *inputImage;
}
@property (retain, nonatomic) CIImage *inputImage;
@end
```

```objc:CIColorInvert.m
@implementation CIColorInvert
@synthesize inputImage;
- (CIImage *) outputImage
{
    CIFilter *filter = [CIFilter filterWithName:@"CIColorMatrix"
                            withInputParameters: @{
            kCIInputImageKey: inputImage,
        @"inputRVector": [CIVector vectorWithX:-1 Y:0 Z:0],
        @"inputGVector": [CIVector vectorWithX:0 Y:-1 Z:0],
        @"inputBVector": [CIVector vectorWithX:0 Y:0 Z:-1],
        @"inputBiasVector": [CIVector vectorWithX:1 Y:1 Z:1],
        }];
    return filter.outputImage;
}
```

まあ Objective-C が読めない人や苦手な人に、これの Swift 版を自分で書いてみました：

```swift:CIColorInvert.swift
class CIColorInvert: CIFilter {
	
	var inputImage: CIImage?
	override var outputImage: CIImage? {
		let filter = CIFilter(name: "CIColorMatrix", withInputParameters: [
			"inputRVector": CIVector(x: -1, y: 0, z: 0),
			"inputGVector": CIVector(x: 0, y: -1, z: 0),
			"inputBVector": CIVector(x: 0, y: 0, z: -1),
			"inputBiasVector": CIVector(x: 1, y: 1, z: 1),
		])
		filter?.setValue(inputImage, forKey: kCIInputImageKey)
		return filter?.outputImage
	}
	
}
```

と、まあぶっちゃけあんまり参考になるポイントがない（そもそもチェイニングしてねぇし）けど、ソースコードを読むことで察せるものもある、それは Swift 版を読めばわかりますが `outputImage` というやらはただの算出プロパティーであって、こいつこそがフィルターの実態だということ、つまり他のプロパティーとかはあくまでフィルターに必要な変数を格納しているだけであって `outputImage` が呼ばれるまでフィルターは何も動作しない、ということです。これでわかったことは一つ：どんな自作 CIFilter でも、`inputImage: CIImage?` と `outputImage: CIImage?` は必ず存在すること、しかもその中の `outputImage` は算出プロパティーだ、ということです。となると、`CIFilter` をサブクラス化するときに、どれもこれも `inputImage` と `outputImage` 書くのダルくね？実際のフィルタリングのプロセスだけ書きたいんだけど？

というわけで、`CIFilter` のサブクラス化用のためのサブクラス（）を作ろう。

```swift:CustomFilter
class CustomCIFilter: CIFilter {
	
	var inputImage: CIImage?
	
	override var outputImage: CIImage? {
		
		return self.inputImage
		
	}
	
}
```

と上記のように、最初から `inputImage: CIImage?` が入ってあるフィルタークラスを作ります。これで `outputImage` だけをオーバーライドすればいい。便利。

ではこのように、最初のサンプルである `CIColorInvert` をこちらでサブクラス化してみましょう。

```swift:CIColorInvert
class CIColorInvert: CustomCIFilter {
	
	override var outputImage: CIImage? {
		let filter = CIFilter(name: "CIColorMatrix", withInputParameters: [
			"inputRVector": CIVector(x: -1, y: 0, z: 0),
			"inputGVector": CIVector(x: 0, y: -1, z: 0),
			"inputBVector": CIVector(x: 0, y: 0, z: -1),
			"inputBiasVector": CIVector(x: 1, y: 1, z: 1),
			])
		filter?.setValue(self.inputImage, forKey: kCIInputImageKey)
		return filter?.outputImage
	}
	
}
```

まあなんか最初の例と比べてそんなに大して短くなってないような感は否めないが…でもまあ自分で `inputImage: CIImage?` を定義しなくて済むしね

しかしまあこれではすべて `CIImage` でやり取りしないといけないが、実際のプログラムでは `UIImage` に落とし込まないと話にならないですね。というわけで、`CIFilter` に直接 `UIImage` を取り扱う拡張もついでに一緒に作りましょう。

```swift:CIFilter.swift
extension CIFilter {
	
	func getFilteredImage(from originalImage: UIImage?, in context: CIContext? = nil) -> UIImage? {
		
		guard let originalImage = originalImage else {
			return nil
		}
		
		guard let originalCIImage = originalImage.ciImage ?? CIImage(image: originalImage) else {
			return originalImage
		}
		
		self.setValue(originalCIImage, forKey: kCIInputImageKey)
		
		guard let outputCIImage = self.outputImage else {
			return originalImage
		}
		
		if let context = context, let outputCGImage = context.createCGImage(outputCIImage, from: outputCIImage.extent) {
			return UIImage(cgImage: outputCGImage)
			
		} else {
			return UIImage(ciImage: outputCIImage)
			
		}
		
	}
	
}
```

ここで `in context: CIContext? = nil` の引数を作っているのは、`CIImage` から直接 `UIImage` を作ると、新しく `CIContext` をその場で生成しないといけない（はず、確か、確信はないが…間違ってたらツッコミお願いします）のだが、この `CIContext` の生成は非常に時間がかかるものなので、このようなフィルタリングが非常に頻繁にやりたい場合は自分で `CIContext` を作って保持しておく必要があるので、そのような場合は保持してある `CIContext` を直接渡してあげればオーバーヘッドが少なくなる、ということです。

と前振りとしてはだいぶ長くなってしまいましたが、ここからが本題です：GaussianBlurFilter の製作です。

いやまあ `CIFilter(name: "CIGaussianBlur")` で作ればいいんだろ？と思うあなた、甘いですね。甘々です。おなじみの[レナさん](https://ja.wikipedia.org/wiki/レナ_(画像データ))で見てみましょうか。試しに Playground に下記のようなコードを書いてみましょう。下記の比較画像のようなことになります。

```swift
let basicImage = UIImage(named: "lena.png")
let view = UIImageView(image: basicImage)
PlaygroundPage.current.liveView = view

let filter = CIFilter(name: "CIGaussianBlur")
let filteredImage = filter?.getFilteredImage(from: basicImage)

view.image = filteredImage
```

![preview.png](https://qiita-image-store.s3.amazonaws.com/0/63998/2cc24f5c-04b3-3579-a509-0976a564e484.png)

これはなぜかというと、画像をぼかした場合、ピクセルデータが周囲に拡散するから、画像解像度が高くなってしまうわけですよ。つまり `CIFilter` 内蔵の `CIGaussianFilter` をそのまま使っても、我々が望む結果にはならない、ということです。さてここからが本来のフィルターチェイニングの活躍だ。

やることは非常に簡単、要するにボカされた画像を元画像の解像度で一回切り取りすれば OK になるはずです。早速試してみましょう。

```swift
class GaussianBlurFilter: CustomCIFilter {
		
	override var outputImage: CIImage? {
		
		guard let inputImage = self.inputImage else {
			return nil
		}
		
		guard let blurFilter = CIFilter(name: "CIGaussianBlur") else {
			return inputImage
		}
		blurFilter.setValue(inputImage, forKey: kCIInputImageKey)
		guard let blurredImage = blurFilter.outputImage else {
			return inputImage
		}
		
		guard let cropFilter = CIFilter(name: "CICrop") else {
			return blurredImage
		}
		let originalRect = inputImage.extent
		cropFilter.setValue(blurredImage, forKey: kCIInputImageKey)
		cropFilter.setValue(originalRect, forKey: "inputRectangle")
		guard let croppedImage = cropFilter.outputImage else {
			return blurredImage
		}
		
		return croppedImage
		
	}
	
}

let basicImage = UIImage(named: "lena.png")
let view = UIImageView(image: basicImage)
PlaygroundPage.current.liveView = view

let filter = GaussianBlurFilter()
let filteredImage = filter.getFilteredImage(from: basicImage)

view.image = filteredImage
```

![preview.png](https://qiita-image-store.s3.amazonaws.com/0/63998/a6603452-8054-0553-9f76-7f4ed8d5db45.png)

ふむふむ、だいぶ良くなりましたね。ところがよく見てみるとやはり周りが微妙に黒い帯が付いています。

これを解決するにはどうすればいいかと色々ググってみたが、[ここ](http://stackoverflow.com/questions/12839729/correct-crop-of-cigaussianblur)によりますとどうやらぼかしを適用する前に一回 CIAffineClamp のフィルターを適用しなければならないそうです。早速改良してみよう。

```swift
class GaussianBlurFilter: CustomCIFilter {
	
	override var outputImage: CIImage? {
		
		guard let inputImage = self.inputImage else {
			return nil
		}
		
		guard let affineClampFilter = CIFilter(name: "CIAffineClamp") else {
			return inputImage
		}
		let transform = CGAffineTransform(scaleX: 1, y: 1)
		affineClampFilter.setValue(inputImage, forKey: kCIInputImageKey)
		affineClampFilter.setValue(transform, forKey: kCIInputTransformKey)
		guard let affineClampedImage = affineClampFilter.outputImage else {
			return inputImage
		}
		
		guard let blurFilter = CIFilter(name: "CIGaussianBlur") else {
			return affineClampedImage
		}
		blurFilter.setValue(affineClampedImage, forKey: kCIInputImageKey)
		guard let blurredImage = blurFilter.outputImage else {
			return affineClampedImage
		}
		
		guard let cropFilter = CIFilter(name: "CICrop") else {
			return blurredImage
		}
		let originalRect = inputImage.extent
		cropFilter.setValue(blurredImage, forKey: kCIInputImageKey)
		cropFilter.setValue(originalRect, forKey: "inputRectangle")
		guard let croppedImage = cropFilter.outputImage else {
			return blurredImage
		}
		
		return croppedImage
		
	}
	
}

let basicImage = UIImage(named: "lena.png")
let view = UIImageView(image: basicImage)
PlaygroundPage.current.liveView = view

let filter = GaussianBlurFilter()
let filteredImage = filter.getFilteredImage(from: basicImage)

view.image = filteredImage
```

![preview.png](https://qiita-image-store.s3.amazonaws.com/0/63998/382a44d1-2e05-5222-f016-7dba7ec6b450.png)

よし、これでパーフェクトなぼかし画像が出来上がりました。

ところがちょっと待った。これ、ガウスぼかしだよ。ガウスぼかしってぼかしの具合が調節できるんだよ。しかしこのガウスぼかしフィルターじゃあそのぼかし具合ってのが調節できないじゃん。

というわけで最後もう一手間。この自作 `GaussianBlurFilter` に `radius: CGFloat` のプロパティーも追加してあげましょう。

```swift
class GaussianBlurFilter: CustomCIFilter {
	
	var radius: CGFloat = 10
	
	override var outputImage: CIImage? {
		
		guard let inputImage = self.inputImage else {
			return nil
		}
		let radius = self.radius
		
		guard let affineClampFilter = CIFilter(name: "CIAffineClamp") else {
			return inputImage
		}
		let transform = CGAffineTransform(scaleX: 1, y: 1)
		affineClampFilter.setValue(inputImage, forKey: kCIInputImageKey)
		affineClampFilter.setValue(transform, forKey: kCIInputTransformKey)
		guard let affineClampedImage = affineClampFilter.outputImage else {
			return inputImage
		}
		
		guard let blurFilter = CIFilter(name: "CIGaussianBlur") else {
			return affineClampedImage
		}
		blurFilter.setValue(affineClampedImage, forKey: kCIInputImageKey)
		blurFilter.setValue(radius, forKey: kCIInputRadiusKey)
		guard let blurredImage = blurFilter.outputImage else {
			return affineClampedImage
		}
		
		guard let cropFilter = CIFilter(name: "CICrop") else {
			return blurredImage
		}
		let originalRect = inputImage.extent
		cropFilter.setValue(blurredImage, forKey: kCIInputImageKey)
		cropFilter.setValue(originalRect, forKey: "inputRectangle")
		guard let croppedImage = cropFilter.outputImage else {
			return blurredImage
		}
		
		return croppedImage
		
	}
	
}

let basicImage = UIImage(named: "lena.png")
let view = UIImageView(image: basicImage)
PlaygroundPage.current.liveView = view

let filter = GaussianBlurFilter()
filter.radius = 50
let filteredImage = filter.getFilteredImage(from: basicImage)

view.image = filteredImage
```

![preview.png](https://qiita-image-store.s3.amazonaws.com/0/63998/dbe352f3-732b-ddac-f322-5b569102e4f4.png)

これで、`filter.radius` を設定することで、ガウスぼかしのぼかし具合が簡単に設定できるようになります。そもそもどんなプロパティーが設定できるかもわかるようになる。便利。もう昔のように `filter.setValue(50, forKey: kCIInputRadiusKey)` とか見たいなわけわからんキーにバリューを設定しなくて済む。これで今すぐ使える `GaussianBlurFilter` が自作できました。

でも落ち着こう。もう一度よ〜く考えてみよう。そもそもこれらのフィルターってチェインに使うフィルターってのは最初から決まってあるわけですよ。だからこれらのフィルターを一回作って格納しておけばいいんじゃね？しかもアップルも最初から丁寧にフィルターのプロパティー初期化というかデフォルト値に戻すメソッドまで用意してあります。というわけで、サブクラス化用のクラスをこのように直します：

```swift:CustomCIFilter.swift
open class CustomCIFilter: CIFilter {
	
	public var inputImage: CIImage?
	
	open override func setDefaults() {
		self.inputImage = nil
	}
	
	override open var outputImage: CIImage? {
		
		return self.inputImage
		
	}
	
	
}
```

```swift:GaussianBlurFilter.swift
public class GaussianBlurFilter: CustomCIFilter {
	
	private let _affineClampFilter = CIFilter(name: "CIAffineClamp")
	private let _gaussianBlurFilter = CIFilter(name: "CIGaussianBlur")
	private let _cropFilter = CIFilter(name: "CICrop")
	
	public var radius: CGFloat = 10
	
	public override func setDefaults() {
		super.setDefaults()
		self._affineClampFilter?.setDefaults()
		self._gaussianBlurFilter?.setDefaults()
		self._cropFilter?.setDefaults()
	}
	
	public override var outputImage: CIImage? {
		
		guard let inputImage = self.inputImage else {
			return nil
		}
		
		guard let affineClampFilter = self._affineClampFilter else {
			return inputImage
		}
		let transform = CGAffineTransform(scaleX: 1, y: 1)
		affineClampFilter.setValue(inputImage, forKey: kCIInputImageKey)
		affineClampFilter.setValue(transform, forKey: kCIInputTransformKey)
		guard let affineClampedImage = affineClampFilter.outputImage else {
			return inputImage
		}
		
		guard let blurFilter = self._gaussianBlurFilter else {
			return affineClampedImage
		}
		let radius = self.radius
		blurFilter.setValue(affineClampedImage, forKey: kCIInputImageKey)
		blurFilter.setValue(radius, forKey: kCIInputRadiusKey)
		guard let blurredImage = blurFilter.outputImage else {
			return affineClampedImage
		}
		
		guard let cropFilter = self._cropFilter else {
			return blurredImage
		}
		let originalRect = inputImage.extent
		cropFilter.setValue(blurredImage, forKey: kCIInputImageKey)
		cropFilter.setValue(originalRect, forKey: "inputRectangle")
		guard let croppedImage = cropFilter.outputImage else {
			return blurredImage
		}
		
		return croppedImage
		
	}
	
}
```

これで、`GaussianBlurFilter` を一回インスタンス化したら、中の `CIAffineClamp`、`CIGaussianBlur` と `CICrop` のフィルターがそのまま保持され、もし初期化したかったら `setDefaults` を呼び出せばいい。

ちなみにこれのフレームワーク版である Usami も[ただいま GitHub で絶賛公開中](https://github.com/el-hoshino/Usami)。~~Usami の名前はもちろん[「この美術部には問題がある！」の宇佐美さん](http://www.tbs.co.jp/anime/konobi/chara/mizuki.html)から来ています~~