MetalでCIIMageのカスタムフィルタを作る環境整備

はじめに

最近画像処理で遊んでいたのですが、UIImageにピクセル単位で書き込んでいくとだいぶ時間がかかってストレスなので何か良い方法はないかと探していたところ、CIImageのフィルタをMetalで記述することができるのをしったので試してみました

前準備

前準備としてプロジェクトの設定でCIFilterでMetalを読み込めるようにする必要があります。
罠なのが、古い情報が多く公式ドキュメントMetal Shading Language for Core Image Kernels を見てもカスタムフィルタを使用する場合は Xcode の Build Settings で Metal Compiler Build Options の Other Metal Compiler Flags に -fcikernel オプションを設定しろと言う説明がされています。
ですが、Build SettingsでMetalで検索をかけると1件も表示されませんでした。

だいぶハマっていたのですがMetalで書かれたカスタムフィルタをXcode11.6でビルドするの記事に救われました、AppleもBuild Metal-based Core Image kernels with Xcodeの動画で解説が出されているようです。

Build Rulesへの記述の追加

以下の画像のように2つの設定をBuild Rulesへ記述することでMetalファイルを参照できるようになります。

*.ci.airxcrun metallib -cikernel “${INPUT_FILE_PATH}” -o “${SCRIPT_OUTPUT_FILE_0}”$(METAL_LIBRARY_OUTPUT_DIR)/$(INPUT_FILE_BASE).metallib
*.ci.metalxcrun metal -c $MTL_HEADER_SEARCH_PATHS -fcikernel “${INPUT_FILE_PATH}” -o “${SCRIPT_OUTPUT_FILE_0}”$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).air

Metalファイルの作成

Build Rulesに記載した内容に従って*.ci.metalという命名になるようにMetalファイルを作成する必要があります。

今回はKarnel.ci.metalというファイルを作成しました、作成したフィルターは渡された画像をそのままの状態で返却するシンプルなtestFilterというメソッドを作成しています。

#include <metal_stdlib>
using namespace metal;

#include <CoreImage/CoreImage.h>

constant float3 kRec709Luma  = float3(0.2126, 0.7152, 0.0722);

extern "C" { namespace coreimage {
    
    float4 testFilter(sample_t image)
    {
        return image;
    }

}}

tips

記述の内容に関してはMetal Shading Language for CoreImage Kernelsのドキュメント参照

Metalで使えるメソッドをMetal組み込み関数のページで日本語化してくれているのでこっちの方が見やすいかも

呼び出し側

final class TestFilter: CIFilter {
    var inputImage: CIImage?
    
    static var kernel: CIColorKernel = { () -> CIColorKernel in
        let url = Bundle.main.url(forResource: "Kernel", withExtension: "ci.metallib")!
        let data = try! Data(contentsOf: url)
        return try! CIColorKernel(functionName: "testFilter", fromMetalLibraryData: data)
    }()
    
    override var outputImage: CIImage? {
        get {
            guard let input = inputImage else { return nil }
            return TestFilter.kernel.apply(extent: input.extent, arguments: [input])
        }
    }
}

final class GrayscaleConversionViewController: UIViewController {
    @IBOutlet private weak var imageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let filter = TestFilter()
        filter.inputImage = CIImage(image: UIImage(named: "フィルターをかけたいUIImage")!)
        
        if let output = filter.outputImage {
            let bwUIImage = UIImage(ciImage: output)
            imageView.image = bwUIImage
        }
    }
}