Xcode
QRcode
playground
swift4

XcodeのPlaygroundでQRコードを読みたかった

滅多にないことですがごく稀にmacでQRコードを読みたいことがあります。
そういうときはiPhoneで読みとってAirDropするなどしていたのですが、何度目かになると流石に面倒になってきます。
そしてそんなのちゃんとしたアプリケーションじゃなくていいならPlaygroundでちょいちょいじゃないのと調べ始めた訳です。

Playground

おなじみPlaygroundですが詳しい使い方はXcodeのヘルプに載っています。
リンク先はこれを書いた時のバージョンの9.3なのでHelpメニューから最新を見るのがお勧めです。
Rich commentの書き方やAssistant EditorにLive Viewを追加する方法などはここに載っています。

QRコードリーダー

ちょっと調べて見るとAppleのサンプルコードにAVCamBarcode: Using AVFoundation to Detect Barcodes and Facesというのが見つかりました。
しかし残念ながらこれで使っているAVCaptureMetadataOutputはiOSでしか使えないということがわかります。
スマホ用のカメラモジュールにはこういう機能が内蔵されているのでしょう。

仕方がないので確かCoreImageにQRコードを検出するのがあったはずと見てみます。
するとImage Feature Detectionのところに これからはこの手のことはVisionでやることにしたぜ みたいなことが書いてあります。
こういう時は素直にVisionのリンクを開くのが平和にmacOSで開発するコツです。

OK OK、ちょうど新しいframeworkを触ってみたかったところさ とか言いながらVisionのドキュメントを開いて見るとなかなか簡単に扱えそうな雰囲気です。
ただサンプルは静止画の単純なものがWWDC2017のVision Frameworkの紹介にあるだけのようです。
しかしGoogle先生に尋ねると先達の知恵の1つ2つは簡単に見つかりました。

まとめ

これだけ揃えばあとはまとめるだけです。
AVCaptureVideoDataOutputの使い方については
AVCamPhotoFilter: Using AVFoundation to Capture photos with image processingを合わせて見ました。
このためにdelegate用のオブジェクトが必要になるのが少し残念ですね。
ここもクロージャーで済めばPlaygroundらしいダラっとしたコードになるのですが。

import Foundation
import Cocoa
import AVFoundation
import Vision
import PlaygroundSupport

// delegate object
class VideoDataOutputDelegate : NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {

    let requestHandler = VNSequenceRequestHandler()
    var resultHistory = Set<String>()

    public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // one frame
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            print("sampleBuffer convertion failed")
            return;
        }

        // QR code detect request
        let detectRequest = VNDetectBarcodesRequest { [weak self](request, error) in
            guard let unwrappedSelf = self else {
                return;
            }
            guard let results = request.results as? [VNBarcodeObservation] else {
                print("no result")
                return
            }

            for observation in results {
                if let payloadString = observation.payloadStringValue {
                    if !unwrappedSelf.resultHistory.contains(payloadString) {
                        print("new QRcode found: \(payloadString)")
                        unwrappedSelf.resultHistory.insert(payloadString)
                    }
                }
            }
        }
        detectRequest.symbologies = [VNBarcodeSymbology.QR]

        do {
            try requestHandler.perform([detectRequest], on: pixelBuffer)
        } catch {
            print(error.localizedDescription)
        }
    }

}

// main routine
// setup capture session
let session = AVCaptureSession()

session.beginConfiguration()
session.sessionPreset = .medium

let videoDataDelegate : VideoDataOutputDelegate?
do {
    // find device
    guard let videoDevice = AVCaptureDevice.default(for: .video) else {
        print("video device not found")
        throw NSError(domain: "device error", code: 1)
    }

    print("using device: \(videoDevice.localizedName)")

    // setup input
    let deviceInput = try AVCaptureDeviceInput(device: videoDevice)

    if session.canAddInput(deviceInput) {
        session.addInput(deviceInput)
    }

    // setup output
    let dataOutput = AVCaptureVideoDataOutput()
    // dataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32BGRA)]
    dataOutput.alwaysDiscardsLateVideoFrames = true

    videoDataDelegate = VideoDataOutputDelegate()
    dataOutput.setSampleBufferDelegate(videoDataDelegate, queue: .global())

    if session.canAddOutput(dataOutput) {
        session.addOutput(dataOutput)
    }

} catch {
    print(error.localizedDescription)
}

session.commitConfiguration()

// setup preview
let videoLayer = AVCaptureVideoPreviewLayer(session: session)
videoLayer.videoGravity = .resizeAspect

let view = NSView(frame: NSRect(x: 0, y: 0, width: 640, height: 480))
view.wantsLayer = true
view.layer = videoLayer

// start
session.startRunning()

// show preview
PlaygroundPage.current.liveView = view

videoのPixelFormatは特に指定しなくても良いようです。
またとりあえず検出できたものを1回printすれば良かったのでSetに突っ込んでいます。
これでまたmacでQRコードを読みたい時が来ても大丈夫。
その時にAPIが変わっていなければですが😇