[Swift] PhotoshopのACVファイルを使って画像フィルターを作る

## 始めに

写真にフィルターをかけるために、iOSでは標準のSDKで用意されているCIFilterを使うことで、色々加工することができます。
しかし、iOSだけではなくAndroidでも同じ実装をする必要があるのはよくあることです。各OSで共通して同じ効果を得るために、画像編集ソフトなどで作成できるトーンカーブを使うと、色々な効果を再現することができます。

トーンカーブを扱うにはLUTテクスチャ画像を使用する方法や、Adobe Photoshop のACVファイルを使う方法があります。今回はSwiftでACVファイルを使って実装したので、その内容をまとめておきたいと思います。

ちなみにiOSにはGPUImageという有名なライブラリがあり、その中でもACVファイルはサポートされていて、GPUImage 3 で OpenGL -> Metal への移植が進められています。（この記事を書き始めて調べ直している間に 3 の存在を知りました…）
https://github.com/BradLarson/GPUImage3/

## ACVからCIFilterにかけるまで
以下の手順を追って反映させていきます

1. ACVファイルからトーンカーブのポイントデータを抽出する
2. ポイントデータからColorCubeデータを生成する
3. ColorCubeデータを使ってCIFilterで画像を変換する

## 1. ACVファイルからトーンカーブのポイントデータを抽出する

ACVファイルには red,  green, blue, composit の4要素があり、それぞれ赤、緑、青、輝度を設定できます
[ImageFilter/ACVParser.swift at master · Matzo/ImageFilter · GitHub](https://github.com/Matzo/ImageFilter/blob/master/ImageFilter/ACVParser.swift)

ポイントデータは2要素ずつ x,y のペアが [x1,y1,x2,y2,...xn,yn]として以下のようになっています。

``` swift
composit: [0.0, 0.12549019607843137, 0.16470588235294117, 0.14901960784313725, 0.34901960784313724, 0.2784313725490196, 0.5725490196078431, 0.5176470588235293, 1.0, 1.0]

red: [0.0, 0.0, 0.27058823529411763, 0.2901960784313725, 0.5490196078431373, 0.596078431372549, 1.0, 1.0]

green: [0.06274509803921569, 0.0, 0.2784313725490196, 0.21568627450980393, 0.5725490196078431, 0.5294117647058824, 0.8, 0.7843137254901961, 1.0, 1.0]

blue: [0.0, 0.0, 0.2549019607843137, 0.17254901960784313, 0.596078431372549, 0.5058823529411764, 0.8117647058823529, 0.788235294117647, 1.0, 1.0]
```


## 2. ポイントデータからColorCubeデータを生成する
こちらを参考にさせていただきました
[GitHub - churabou/ToneCurve](https://github.com/churabou/ToneCurve) 

その際、出力時の画像が暗くなっていたので、 [Does Your App Need Color Management?](https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_performance/ci_performance.html#//apple_ref/doc/uid/TP30001185-CH10-SW7) こちらを参考に補正を適用しました。
ドキュメント内のコードが何やらバグってますが、正しくは以下のようです。

``` swift
# Conversions from and to sRGB add to the filter complexity
rgb = mix(rgb*0.0774, pow(rgb*0.9479 + 0.05213, 2.4), step(0.04045,rgb))
rgb = mix(rgb*12.92, pow(rgb,0.4167) * 1.055 - 0.055, step(0.00313,rgb)) 
```

参考) http://docs.huihoo.com/apple/wwdc/2012/session_511__core_image_techniques.pdf の 46ページ

また、composit が反映されていなかったので、反映されるように修正しています。

``` swift
    public var cubeData: Data {

        let compositeCurve = self.getPreparedSplineCurve(point: rgbCompositeCurvePoints)
        let redCurve = self.getPreparedSplineCurve(point: redCurvePoints)
        let greenCurve = self.getPreparedSplineCurve(point: greenCurvePoints)
        let blueCurve = self.getPreparedSplineCurve(point: blueCurvePoints)

        let indexs: [Int] = [  0,   8,  16,  25,  33,  41,  49,  58,
                               66,  74,  82,  90,  99, 107, 115, 123,
                               132, 140, 148, 156, 165, 173, 181, 189,
                               197, 206, 214, 222, 230, 239, 247, 255]

        let size = cubeDimension
        var colorCubeData: [Float] = []//[Float](repeatElement(0, count: size*size*size*4))

        for b in 0..<size {
            for g in 0..<size {
                for r in 0..<size {

                    // bgra for upload to texture
                    var currentCurveIndex = indexs[b]
                    let bb = fmin(fmax(Float(currentCurveIndex) + blueCurve![currentCurveIndex] + compositeCurve![currentCurveIndex], 0), 255)

                    currentCurveIndex = indexs[g]
                    let gg = fmin(fmax(Float(currentCurveIndex) + greenCurve![currentCurveIndex] + compositeCurve![currentCurveIndex], 0), 255)

                    currentCurveIndex = indexs[r]
                    let rr = fmin(fmax(Float(currentCurveIndex) + redCurve![currentCurveIndex] + compositeCurve![currentCurveIndex], 0), 255)

                    let div: Float = 1.0 / 255.0
                    colorCubeData.append(sRGB_to_lightLiner(rr*div))
                    colorCubeData.append(sRGB_to_lightLiner(gg*div))
                    colorCubeData.append(sRGB_to_lightLiner(bb*div))
                    colorCubeData.append(1)

                }
            }
        }

        return NSData(bytes: colorCubeData, length: size*size*size*4*MemoryLayout<Float>.size) as Data
    }
```


サンプルプロジェクト全体がこちら
https://github.com/Matzo/ImageFilter/tree/master/ImageFilter

## 3. ColorCubeデータを使ってCIFilterで画像を変換する
最後に、生成した ColorCubeデータを使って CIImage を変換するのがこちらです

``` swift
func applyingFilter(to sourceImage: CIImage) -> CIImage {

    guard let parser = ACVParser(with: "foo.acv") else { return sourceImage }

    return sourceImage
        .applyingFilter("CIColorCubeWithColorSpace", parameters: [
            "inputCubeDimension": parser.cubeDimension,
            "inputCubeData": parser.cubeData,
            "inputColorSpace": parser.colorSpace
            ])
}
```

## おわりに
ACVファイルの中身が思ったよりもシンプルで、ポイントデータからスプラインカーブを生成してColorCubeに変換するのが意外と力技なんだなと知りました。内部の計算で何をしているのか雰囲気しか分かってません。

LUTテクスチャを使用する方法もまたいずれまとめたいと思います。

最後に、 ACVファイルを使用したフィルターのキャプチャをサンプルとして載せておきます。

![image_filter_sample.jpg](https://qiita-image-store.s3.amazonaws.com/0/25206/b8db32ba-41af-9cd4-14fb-2471db65d8b0.jpeg)
左: original、 右: aurora