ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Vision.framework を使って QR コードを読む

こんにちは、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分以上かかりました。

f:id:unifa_tech:20171031140022j:plain

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コードを検出することはないと思いますがなにかあれば参考にしたいと思います。

コードはこちらにあります。

bitbucket.org

参考