Edited at

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

More than 1 year has passed since last update.

追記

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のヘルプに載っています。

リンク先はこれを書いた時のバージョンの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が変わっていなければですが😇