Metalで書かれたカスタムフィルタをXcode11.6でビルドする

iOS 13 で OCR を使ってみた。OCR自体は簡単にできるのだけど汎用性や精度を考えるとその前段階、画像の準備の部分で色々とやらなければならないことに気付いた。

せっかくなのでCIFilterでできないかな?と探すも閾値を指定して白黒2値にしてくれるフィルタが見つからない。ググってみると見つかったのはMetalで書かれたカスタムの CIFilter。

と言うわけで今回はこれを使ってアプリをビルドするまでの手順をメモ。

Xcode 11.6ではMetal Compiler Build Optionsが見つからない

Appleの Metal Shading Language for Core Image Kernels の最後の部分やググって出てくる情報では、Metal で書かれたカスタムフィルタを使用する場合は Xcode の Build Settings で Metal Compiler Build Options の Other Metal Compiler Flags に -fcikernel オプションを設定しろと言う説明がなされている。
が、しかし Xcode 11.6で Single View App プロジェクトを作ると Build Settings に Metal Compiler Build Options が見つからない。Xcode の検索窓に Metal と入れても何も出てこない!

結構な時間ググってみたのだけどそれらしい話は見つからない。自分の環境だけ出てこないのかと心配になって新しいプロジェクトを AR App の設定で作ってみた。すると Metal Compiler Build Options が見つかったのだけど、これじゃ解決じゃない…

WWDC20 で新しい?方法が説明されている

と言うわけで、最新の方法が何か別にあるのか?と思い WWDC20 をあれこれ探していると見つかったのがこれ

まさに探していた内容そのまんまのビデオ!
ここでは Build Settings でなく Build Rules にスクリプトを書く方法が説明されている。

具体的には .ci.metal と .ci.air それぞれに対するスクリプトを作成するだけ。
.ci.metal は

xcrun metal -c $MTL_HEADER_SEARCH_PATHS -fcikernel "${INPUT_FILE_PATH}" -o "${SCRIPT_OUTPUT_FILE_0}"

を書いて Output Files に

$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).air

を設定。

.ci.air は

xcrun metallib -cikernel "${INPUT_FILE_PATH}" -o "${SCRIPT_OUTPUT_FILE_0}"

を書いて Output Files に

$(METAL_LIBRARY_OUTPUT_DIR)/$(INPUT_FILE_BASE).metallib

を設定。

こんな感じになる。

これで Build Rules の準備は終了

コード部分

コードの部分は基本的に同じ。

metal のコードは .ci.metal ファイルになっていれば OK。
WWDC20 の例ではこんなコード。

// MyKernels.ci.metal
#include <CoreImage/CoreImage.h> // includes CIKernelMetalLib.h
using namespace metal;

extern "C" float4 HDRZebra (coreimage::sample_t s, float time, coreimage::destination dest) 
{
	float diagLine = dest.coord().x + dest.coord().y;
	float zebra = fract(diagLine/20.0 + time*2.0);
	if ((zebra > 0.5) && (s.r > 1 || s.g > 1 || s.b > 1))
		return float4(2.0, 0.0, 0.0, 1.0);
	return s;
}

カスタムの CIFilter からの Metal コードの呼び出しも特別なことはしていない。
WWDC20 の例ではこんなコード。

class HDRZebraFilter: CIFilter {
    var inputImage: CIImage?
	var inputTime: Float = 0.0

    static var kernel: CIColorKernel = { () -> CIColorKernel in 
	    let url = Bundle.main.url(forResource: "MyKernels", 
                                withExtension: "ci.metallib")!
		let data = try! Data(contentsOf: url)
		return try! CIColorKernel(functionName: "HDRzebra", 
                          fromMetalLibraryData: data)
	}()

  	override var outputImage : CIImage? {
		get {
			guard let input = inputImage else {return nil}
			return HDRZebraFilter.kernel.apply(extent: input.extent, 
											 arguments: [input, inputTime])
		}
	}
}

これで HDRZebraFilter の outputImage にアクセスすればフィルタのかかった画像がゲットできる!

サンプル

今回は白黒2値にするフィルタが欲しかったのだけど、見つかったのが下記のカスタムフィルタ。これを使うために難儀した…

で、これを使って画像を閾値で白黒にするサンプルを作ったので github に。

こんな感じでスライダを動かして閾値をリアルタイムに変更可能。

実際に必要だったのは、コインのアイコンが数字に誤認識されることが多かったので文字の白を抜き出してコインを消すことでした。閾値をおおよそ 0.85 以上にするとコインは暗い側になって黒くなるので、文字だけ抜き出すことができました!

消せないアラート

とりあえずビルドもできて動作も問題ないのだけどリンク時にどうしても消えないアラートがある。

気持ち悪いので調べてみると Apple Developer Forums で質問がされていた

  • https://developer.apple.com/forums/thread/122729

しかし「僕も!」と言う返信があるだけで今のところ解決策は見つかっていない。

最後に

今回やりたかった白黒2値への閾値での変換、リアルタイムでやらなければならないことではなかったので画像データのビット毎のRGB値を調べて新しい画像を作るのでも良かった。と言うか、それなら何も悩まずに済んだのに…
だけど、せっかく CIFilter のことを調べてみたのでカスタムでコードがあるなら使ってみようとしてハマった話です。

OCR、綺麗な状態での精度は凄く高いのだけど、OCR にかけるまでの下準備が色々と大変でノウハウが必要みたい。なかなか「事前にこうすれば良い」と言う情報はネット上に出てこない感じかな。

Leave a Reply

Your email address will not be published. Required fields are marked *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)