2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-04-28

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?