こんにちは、iOSエンジニアのしだです。
急に寒くなってきて秋をすっ飛ばしていきなり冬になってしまった感じがします。
iOS11 で追加された Vision.framework を使ってQRコードを読み込みしたいと思います。(n番煎じ感あります。)
QR コード
iOSで QRコード を読む場合2種類あります。
- AVFoundation.framework (AVCaptureMetadataOutputObjectsDelegate)
- Vision.framework
AVCaptureMetadataOutputObjectsDelegate
は AVFoundation 内でキャプチャ中に検出するのに対して、Vision.framework ではキャプチャした画像に対して検出する違いがあります。
あと、AVCamBarcode サンプルコードを動かした感じ一度に4つのQRコードが検出されましたが、Vision.framework の方は時間はかかりますが 4つ以上は検出できました。
できたもの
用意したQRコードは、1から16の文字列をQRコードにしたものです。そして検出されたQRコードに緑枠を表示するシンプルなものです。
iPad mini 2 で試しましたが、16個検出するのに1分以上かかりました。
Vision.framework 使い方
Requests
を作って、RequestHandler
で実行して、Observations
で結果を受け取るようになっています。
Requests
- VNDetectFaceRectanglesRequest: 顔検出
- VNDetectBarcodesRequest: QRコードなどのバーコード検出
- VNDetectTextRectanglesRequest: テキスト検出
- VNTrackObjectRequest: オブジェクト検出
- など...
これらの Requests を作成して、 RequestHandler で実行します。そうすると Requests と対になる Observation が返ってきます。
例えば、 VNDetectFaceRectanglesRequest
の場合 VNFaceObservation
が結果として受け取れます。
バーコード検出
AVCaptureVideoDataOutput
から受け取った CVPixelBuffer
からQRコードを検出してます。検出したQRコードに緑枠を描画するために結果を AVCaptureVideoPreviewLayer
のビューに渡しています。
extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } let handler = VNSequenceRequestHandler() let barcodesDetectionRequest = VNDetectBarcodesRequest { [weak self] (request, error) in if let error = error { NSLog("%@, %@", #function, error.localizedDescription) } guard let results = request.results else { return } ... DispatchQueue.main.async { let barcodes: [VNBarcodeObservation] = results.flatMap { $0 as? VNBarcodeObservation } self?.previewView.barcodes = barcodes } } try? handler.perform([barcodesDetectionRequest], on: pixelBuffer) } }
検出したQRコードに緑枠を付ける
VNBarcodeObservation には、検出した文字列(payloadStringValue
)や CIBarcodeDescriptor
を持っています。
また矩形の座標をもっているので描画します。
class PreviewView: UIView { let targetLayerName = "box" /// バーコードを設定したら画面更新 var barcodes: [VNBarcodeObservation] = [] { didSet { self.setNeedsDisplay() } } ... override func draw(_ rect: CGRect) { super.draw(rect) /// 緑枠クリア self.layer.sublayers? .filter { $0.name == targetLayerName } .forEach { $0.removeFromSuperlayer() } guard let previewLayer = self.layer as? AVCaptureVideoPreviewLayer else { return } for barcode in barcodes { /// VNRectangleObservation が持っている座標がY軸反転しているのでアフィン変換する let t = CGAffineTransform(translationX: 0, y: 1) .scaledBy(x: 1, y: -1) let tl = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.topLeft.applying(t)) let tr = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.topRight.applying(t)) let bl = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.bottomLeft.applying(t)) let br = previewLayer.layerPointConverted(fromCaptureDevicePoint: barcode.bottomRight.applying(t)) /// 緑枠を描画 let l = rectangle(UIColor.green, topLeft: tl, topRight: tr, bottomLeft: bl, bottomRight: br) self.layer.addSublayer(l) } } func rectangle(_ color: UIColor, topLeft: CGPoint, topRight: CGPoint, bottomLeft: CGPoint, bottomRight: CGPoint) -> CALayer { let lineWidth: CGFloat = 2 let path: CGMutablePath = CGMutablePath() path.move(to: topLeft) path.addLine(to: topRight) path.addLine(to: bottomRight) path.addLine(to: bottomLeft) path.addLine(to: topLeft) let layer = CAShapeLayer() layer.name = targetLayerName layer.fillColor = UIColor.clear.cgColor layer.strokeColor = color.cgColor layer.lineWidth = lineWidth layer.lineJoin = kCALineJoinMiter layer.path = path return layer } }
おわりに
Vision.framework であれば複数のQRコードを検出できるのですが、検出数によって処理の時間が変わってきます。
4つ検出するだけであれば1秒くらいで検出できましたが、9つQRコード検出には10秒以上かかっていました。
同時にQRコードを検出することはないと思いますがなにかあれば参考にしたいと思います。
コードはこちらにあります。