同僚にid:yashigani_w(ヤシガニ)とid:yigarashi(ワイイガラシ)がいる。いかにも似ているが、あるとき一緒に仕事をすることになって、紛らわしさが限界を超えた。
yashigani yigarashi
そうとわかって見たら、なるほど「y」で始まって「i」で終わるのが似ているね、くらいに思うかもしれない。しかし不意にSlackやGitHubで見かけると、一瞬どちらかわからない。
この紛らわしさをより俯瞰してみるため、画像にしてみました。
どうだ紛らわしいだろう。全体的な形がなんだか似ている。
ところでこの画像はPhotoshopで作った。テキストにガウシアンぼかしをかけて、その後から二値化した。なかなかかっこいいから、もっといろいろ試してみたい。
前置きが長くなったが、テキストにガウシアンぼかしをかけて、その後から二値化 するのを、macOSやiOSの仕組みでやってみるのが、本題である。
Core Textでテキストの画像を作る
まずはテキストを画像化しないと始まらない。テキストの画像を作るのは、Core Text(とCore Graphics)で素朴に実装できる。
import CoreGraphics import CoreText import Foundation struct Padding { let top: CGFloat let left: CGFloat let bottom: CGFloat let right: CGFloat static var zero: Padding { return Padding(top: 0, left: 0, bottom: 0, right: 0) } } func render(text: NSAttributedString, padding: Padding = .zero) -> CGImage { let framesetter = CTFramesetterCreateWithAttributedString(text) let frameSize = CTFramesetterSuggestFrameSizeWithConstraints( framesetter, CFRange(), nil, CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), nil) let width = Int(ceil(frameSize.width + padding.left + padding.right)) let height = Int(ceil(frameSize.height + padding.top + padding.bottom)) let ctx = CGContext( data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue) guard let context = ctx else { fatalError() } context.saveGState() context.setFillColor(.white) context.fill(CGRect(origin: CGPoint.zero, size: CGSize(width: width, height: height))) context.restoreGState() let path = CGPath( rect: CGRect( origin: CGPoint( x: (CGFloat(width) - frameSize.width) / 2, y: (CGFloat(height) - frameSize.height) / 2), size: frameSize), transform: nil) let frame = CTFramesetterCreateFrame(framesetter, CFRange(), path, nil) CTFrameDraw(frame, context) guard let image = context.makeImage() else { fatalError() } return image }
NSAttributedString
からCore TextのAPIを使ってCGImage
を作る。このときpaddingを設定できるようにすると、あとで都合がいい。
Core Imageで画像を加工する
画像の加工ではいつものように、Core Imageを使うことを検討する。以前CoreImage.CIFilterBuiltinsで試したように、CIFilter.gaussianBlur()
を使えば、ガウシアンぼかしは簡単だ。あとは二値化するだけだが、ここで手が止まる。意外なことに、組み込みのフィルターには二値化が存在しない。
Metal Shading Languageによるカスタムカーネル
二値化は簡単そうなので手作りする。Core ImageのカーネルをMetal Shading Languageで書く。ThresholdBinary.metal
を以下の内容で作った。またXcodeのBuild Settingsで、Other Metal Compiler Flags
に-fcikernel
を、User-Defined Settingsに追加したMTLLINKER_FLAGS
キーに-cikernel
を、それぞれ設定する必要がある。
#include <metal_stdlib> #include <CoreImage/CoreImage.h> using namespace metal; extern "C" { namespace coreimage { float4 threshold_binary(sample_t source, float threshold) { float4 image = premultiply(source); float y = 0.299 * image.r + 0.587 * image.g + 0.114 * image.b; // BT.601 float binary = y < threshold ? 0.0 : 1.0; return float4(binary, binary, binary, 1.0); } } }
RGB値から輝度成分を取り出すために、YUV色空間のYを計算する。これが閾値を超えていれば1、そうでなければ0とすることで、二値化できる。
このカーネルは以下のように読み込める。
import CoreImage let thresholdBinaryKernel: CIColorKernel = { guard let url = Bundle.main.url(forResource: "default", withExtension: "metallib"), let data = try? Data(contentsOf: url) else { fatalError("Unable to get metallib") } guard let kernel = try? CIColorKernel( functionName: "threshold_binary", fromMetalLibraryData: data) else { fatalError("Unable to create CIKernel from threshold_binary") } return kernel }()
Core Imageによる画像の加工
二値化のカーネルが書けたので、あとは実際に画像を加工するだけだ。
import CoreImage import CoreImage.CIFilterBuiltins let ciContext: CIContext = CIContext() func process(image: CGImage, sigma: Float, threshold: Float) -> CGImage? { let ciImage = CIImage(cgImage: image) let gaussianBlur = CIFilter.gaussianBlur() gaussianBlur.inputImage = ciImage.clampedToExtent() gaussianBlur.radius = sigma guard let blurredImage = gaussianBlur.outputImage else { return nil } guard let processedImage = thresholdBinaryKernel.apply( extent: ciImage.extent, arguments: [blurredImage, threshold] ) else { return nil } guard let cgImage = ciContext.createCGImage( processedImage, from: processedImage.extent) else { fatalError() } return cgImage }
CIImage.clampedToExtent()
を使うのがコツで、ガウシアンぼかしをかけるときに画像の端が変な色になってしまうのを防げる。
どうですか
ここまでで、テキストにガウシアンぼかしをかけて、その後から二値化 ができた。iOSでもmacOSでも同じように動くはずだ。
なんだかちょっとかっこよくないですか。
参考文献
Metalは初めて使うので、ちょっとだけ勉強しました。