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


追記
Xcode 10 と macOS 10.14 Mojave では下記のコードは Playground で動作しないようです。
とりあえずわかっている原因の1つはXcodeのinfo.plistに`Privacy - Camera Usage Description(NSCameraUsageDescription)`が定義されていないことで、`AVCaptureDeviceInput`を作ることができません。
勝手にXcodeのinfo.plistに追記するとインプットを作ることはできますが、どれかのオブジェクトが解放されてしまうような感じでpreviewが出ずにセッションが停止してしまいます。
今のところ下のコードを参考にしてCocoaアプリケーションとして作ってしまうのが簡単な解決策だと思います。
2018/10/9

---

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

##Playground
おなじみPlaygroundですが詳しい使い方は[Xcodeのヘルプ](https://help.apple.com/xcode/mac/9.3/index.html#/dev188e45167)に載っています。
リンク先はこれを書いた時のバージョンの9.3なのでHelpメニューから最新を見るのがお勧めです。
Rich commentの書き方やAssistant EditorにLive Viewを追加する方法などはここに載っています。

##QRコードリーダー
ちょっと調べて見るとAppleのサンプルコードに[AVCamBarcode: Using AVFoundation to Detect Barcodes and Faces](https://developer.apple.com/library/content/samplecode/AVCamBarcode/Introduction/Intro.html#//apple_ref/doc/uid/TP40017312)というのが見つかりました。
しかし残念ながらこれで使っている[AVCaptureMetadataOutput](https://developer.apple.com/documentation/avfoundation/avcapturemetadataoutput)はiOSでしか使えないということがわかります。
スマホ用のカメラモジュールにはこういう機能が内蔵されているのでしょう。

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

*OK OK、ちょうど新しいframeworkを触ってみたかったところさ* とか言いながら[Vision](https://developer.apple.com/documentation/vision)のドキュメントを開いて見るとなかなか簡単に扱えそうな雰囲気です。
ただサンプルは静止画の単純なものが[WWDC2017のVision Frameworkの紹介](https://developer.apple.com/videos/play/wwdc2017/506/)にあるだけのようです。
しかしGoogle先生に尋ねると先達の知恵の[1つ](https://dev.classmethod.jp/smartphone/iphone/ios-11-qrcode-detection/)や[2つ](http://tech.unifa-e.com/entry/2017/11/01/114923)は簡単に見つかりました。

##まとめ
これだけ揃えばあとはまとめるだけです。
AVCaptureVideoDataOutputの使い方については
[AVCamPhotoFilter: Using AVFoundation to Capture photos with image processing](https://developer.apple.com/library/content/samplecode/AVCamPhotoFilter/Introduction/Intro.html#//apple_ref/doc/uid/TP40017556)を合わせて見ました。
このためにdelegate用のオブジェクトが必要になるのが少し残念ですね。
ここもクロージャーで済めばPlaygroundらしいダラっとしたコードになるのですが。

```swift
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が変わっていなければですが😇