Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事はユニークビジョン株式会社 Advent Calendar 2018の23日目の記事です。

はじめに

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

Metal入門・・・の前の基礎知識を一読するとこの記事は読みやすいと思います。

Metalとは

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

Metalを使う準備をする

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

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に設定しています

mtkView.preferredFramesPerSecond = 60

CIFilterでやってみる

CIFilterは優秀な子です。こちらによると201種類ものフィルターが公式で用意されていて、普通に画像処理をしたいだけなら十分だと思われます。
今回はブラーをかけてぼかしたいのでCIGaussianBlurを使ってみます。

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

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

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

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は時間経過で増えていくように設定してあります

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

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

mtkView.framebufferOnly = false

結果

ezgif.com-video-to-gif (1).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に最適化され簡単かつ効率的に優れたパフォーマンスを発揮するそうです。
画像フィルター、ニューラルネットワーク、行列とベクトル、光線追跡といった機能があります。詳しくはこちら

MPS対応デバイス

MPSに対応しているデバイスはドキュメントによるとGPUファミリー2以降(iPhone6以降)をサポートしています。
GPUファミリーってなんぞやなのですがこちらの記事がわかりやすいです。

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

MPSでやってみる

画像フィルターの中からMPSImageGaussianBlurを使います。

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

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でレンダリングしていた箇所を以下に書き換えます

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

まとめ

今回はMetalことはじめということで簡単な画像処理をやってみました。
本格的にやるのであればシェーダを書く必要があるのでC++もといMetalShadingLanguageを使いこなす必要があります。

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

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

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

参考

Metal入門・・・の前の基礎知識
Metal入門 ←とてもわかりやすいです
MetalKitでGPUを使いこなす

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした