Metal 入門したので画像をぼかしてみる

この記事は[ユニークビジョン株式会社 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/uniquevision)の23日目の記事です。

# はじめに
最近Metalの勉強をはじめました。
Metalを使っていろいろとやろうと思うことはじめです。
Metalって響きがなんかかっこいい。。。


[Metal入門・・・の前の基礎知識](https://qiita.com/shu223/items/b95e97d50c738ba6c39a)を一読するとこの記事は読みやすいと思います。

# Metalとは
MetalはグラフィックスAPIのことでGPUにアクセスできるAPIです。
有名なものだとOpenGL、OpenCLなどがありますがそれらは汎用的に設計されているため、Apple製品本来のパフォーマンスが出せないってことでMetalがつくられました。
グラフィックスAPIと聞くと、ゲーム作るわけでもないから関係ないとか思う人も多いと思います(私はそうでした)。ですがCPUが不得手なこと、つまり並列演算をGPUに任せられるんですね。Metalは機械学習（CNN）向けのAPIもありますしメインはそこになってくると思います。(CoreMLも内部ではMetalを使用しています)


# Metalを使う準備をする
まずはMetalを使うためにいろいろ初期化します。

``` swift
import MetalKit
import UIKit

var mtkView: MTKView!
let device = MTLCreateSystemDefaultDevice()!
var commandQueue: MTLCommandQueue!

func setupMetal() {
    commandQueue = device.makeCommandQueue()
    mtkView.device = self.device
    mtkView.delegate = self
}

extension ViewController: MTKViewDelegate {
    func draw(in view: MTKView) {}
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
}
```

`draw(in view: MTKView)`は毎フレームごと呼ばれます。
今回は60fpsに設定しています

``` swift
mtkView.preferredFramesPerSecond = 60
```

# CIFilterでやってみる
[CIFilter](https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW29)は優秀な子です。[こちら](https://qiita.com/shtnkgm/items/fb1425ef88d31fdad1d4)によると201種類ものフィルターが公式で用意されていて、普通に画像処理をしたいだけなら十分だと思われます。
今回はブラーをかけてぼかしたいので`CIGaussianBlur`を使ってみます。

``` swift
let filter = CIFilter(name: "CIGaussianBlur")!
filter.setValue(ciImage, forKey: kCIInputImageKey)

func processImage(_ image: CIImage, scale: Double) {
    filter.setValue(scale, forKey: kCIInputScaleKey)
}
```

ブラーをかける準備が整いました。
これをMetalで描画します。

``` swift
let context = CIContext()
let colorSpace = CGColorSpaceCreateDeviceRGB()

extension ViewController: MTKViewDelegate {
	func draw(in view: MTKView) {
	    guard let drawable = view.currentDrawable else { return }
        guard let commandBuffer = commandQueue.makeCommandBuffer() else { return }
        guard let outputImage = self.filter.outputImage else { return }
        defer {
            commandBuffer.present(drawable)
            commandBuffer.commit()
            commandBuffer.waitUntilCompleted()
        }
        self.processImage(image, scale: scale)
        self.context.render(
            outputImage,
            to: drawable.texture,
            commandBuffer: commandBuffer,
            bounds: outputImage.extent,
            colorSpace: self.colorSpace
        )
      }
}
```

scaleは時間経過で増えていくように設定してあります

``` swift
let startTime = Date().timeIntervalSinceReferenceDate
var scale: Double {
    return Date().timeIntervalSinceReferenceDate - self.startTime
}
```

これだと実行時に
*failed assertion `frameBufferOnly texture not supported for compute.'*
とかいわれて死んだので以下を設定しておきます。(readonlyなのに何書き込もうとしてるんだよ的な理由だと思います)

``` swift
mtkView.framebufferOnly = false
```

## 結果
![ezgif.com-video-to-gif (1).gif](https://qiita-image-store.s3.amazonaws.com/0/224246/4cbfcde0-6241-286a-1c26-da7b2d60105a.gif)

ピクセルが散らばるので縁が黒くなっていきます。
トリミングすれば問題にはなりませんが手間がかかります。
(右下にずれていくのはリサイズ処理が甘いせいです...)

# MPSでもっと簡単にできる
はい。MPSというとても便利なものがありました。
**MetalPerformanceShaders**の略です。

> Optimize graphics and compute performance with kernels that are fine-tuned for the unique characteristics of each Metal GPU family.

とApple様はおっしゃっていて、各GPUに最適化され簡単かつ効率的に優れたパフォーマンスを発揮するそうです。
画像フィルター、ニューラルネットワーク、行列とベクトル、光線追跡といった機能があります。詳しくは[こちら](https://developer.apple.com/documentation/metalperformanceshaders)


## MPS対応デバイス
MPSに対応しているデバイスは[ドキュメント](https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf)によるとGPUファミリー2以降（iPhone6以降）をサポートしています。
GPUファミリーってなんぞやなのですが[こちらの記事](https://qiita.com/shu223/items/8027201e50c68288f0be#metal-%E3%81%AE-device-capability)がわかりやすいです。

`MPSSupportsMTLDevice(_ device: MTLDevice?) -> Bool`という関数も用意されているので実行時に判定することもできます。

## MPSでやってみる
[画像フィルター](https://developer.apple.com/documentation/metalperformanceshaders/image_filters)の中から`MPSImageGaussianBlur`を使います。

まずは画像を読み込んでtextureを作成します。

``` swift
func loadImage() {
    let textureLoader = MTKTextureLoader(device: self.device)
    self.texture = try! textureLoader.newTexture(
        name: "imageName",
        scaleFactor: view.contentScaleFactor,
        bundle: nil
    )
    mtkView.colorPixelFormat = texture.pixelFormat
}
```

CIFilterでレンダリングしていた箇所を以下に書き換えます

``` swift
import MetalPerformanceShaders

extension ImageProcessViewController: MTKViewDelegate {
    func draw(in view: MTKView) {
        // 中略
        let blur = MPSImageGaussianBlur(device: self.device, sigma: Float(scale))
        blur.encode(
            commandBuffer: commandBuffer,
            sourceTexture: self.texture,
            destinationTexture: drawable.texture
        )
    }
}
```

これだけでOKです。

## 結果
とてもいい感じです（gifなので荒く見えてしまってます...）
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.amazonaws.com/0/224246/c1af9991-db9f-2cff-7694-54c1936dadf4.gif)


# まとめ
今回はMetalことはじめということで簡単な画像処理をやってみました。
本格的にやるのであればシェーダを書く必要があるのでC++もとい[MetalShadingLanguage](https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf)を使いこなす必要があります。

日本語の記事はあまり多くないのですが、ドキュメントはけっこう豊富なので基本の動きを抑えておけば理解はしやすいです。

アプリにうまくMetalを組み込めたらもっと幅が広がりそうですね。

次は計算処理をやってみようと思います。

# 参考
[Metal入門・・・の前の基礎知識](https://qiita.com/shu223/items/b95e97d50c738ba6c39a)
[Metal入門](https://booth.pm/ja/items/826318) ←とてもわかりやすいです
[MetalKitでGPUを使いこなす](https://www.gaprot.jp/pickup/tips/metalkit)