はじめに
iOS11が登場してしばらくが経ちました。
そんな中、今回は遅ればせながら ARKit の影に隠れがちな(?) Vision.Framework にある オブジェクトトラッキング について紹介をしていきたいと思います。
Vision.Framework について
まず Vision.Framework について簡単に説明をします。
Vision.Framework は画像解析を行うフレームワークで、画像、動画のどちらも対象とすることができます。
解析の推論に特化した内容となっており、独自に用意した学習モデルを元に推論による画像解析を行うこともできますが、 学習モデルを用意せずとも気軽に高性能な画像解析を行なう機能も備わっています。
Vision.Framework による画像解析はデバイスのみで完結できるので、 オフラインの状況でも画像解析を利用したいというような場面で活躍すると思います。
Vision.Framework で解析できる内容は以下になります。
- 顔検出と認識
- 機械学習画像解析
- バーコード検出
- 画像整列解析
- テキスト検出
- 水平線の検出
- オブジェクトの検出と追跡
今回はこの中の「オブジェクトの検出と追跡」を試してみることにします。
試してみる
カメラから入力される動画の内容から対象をタップで選択し、選択した内容を追跡し続けるアプリを作ります。 プロジェクトは Single View APP で作成するものとして進めていきます。
プロジェクトの設定
まず Vision.Framework は iOS11以上が対象ですので、 Deployment Target を11.0としましょう。
また今回のサンプルではカメラを使いますので、 info.plist の内容に Privacy - Camera Usage Description
を追加しておきましょう。
フレームワークの import
Single View APP の選択で作成されている ViewCotroller.swift
にコードを書いていきます。
import する内容としては Vision と AVFoundation を設定します。
import Vision import AVFoundation
AVFoundation による動画表示の実装
まず AVCaptureVideoDataOutputSampleBufferDelegate
を ViewController
に追加します。
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
入出力を管理する AVCaptureSession
を ViewController
のプロパティに用意します。
private lazy var captureSession: AVCaptureSession = { let session = AVCaptureSession() guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back), let input = try? AVCaptureDeviceInput(device: backCamera) else { return session } session.addInput(input) return session }()
ビデオを表示するレイヤーとなる AVCaptureVideoPreviewLayer
を設定します。
private lazy var previewLayer: AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
AVCaptureSession
の設定を viewDidLoad
で行っていきます。
AVCaptureVideoDataOutput
を生成し AVCaptureSession
に出力内容として設定します。
また、表示用のレイヤーである AVCaptureVideoPreviewLayer
を ViewController
の View
に追加します。
override func viewDidLoad() { super.viewDidLoad() let videoOutput = AVCaptureVideoDataOutput() videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "Queue")) captureSession.addOutput(videoOutput) captureSession.startRunning() view.layer.addSublayer(previewLayer) }
AVCaptureVideoDataOutput
の delegate
のメソッドを用意します。
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } }
また ViewController
の View のレイアウトが確定した段階で AVCaptureVideoPreviewLayer
のサイズを変更しておきます。
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() previewLayer.frame = self.view.bounds previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill }
選択範囲を表す UIView の実装
まず、選択範囲を表す UIView を ViewController のプロパティに用意します。
今回は背景色を透過とし、枠線に白を設定したものとします。
private lazy var highlightView: UIView = { let view = UIView() view.layer.borderColor = UIColor.white.cgColor view.layer.borderWidth = 4 view.backgroundColor = .clear return view }()
画面タッチの判定は touchesBegan
と touchesEnded
で組みわせて行なっていくことにします。
選択範囲を表す UIView の frame が zero となるようにします。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { highlightView.frame = .zero }
選択範囲を表す UIView を、タッチされた位置に表示されるようにします。
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch: UITouch = touches.first else { return } highlightView.frame.size = CGSize(width: 120, height: 120) highlightView.center = touch.location(in: view) }
ここまででカメラの内容が表示され、タッチされた位置に矩形の枠線が表示されるようになりました。
オブジェクトの検出と追跡の実装
ここから Vision.Framework を使用していきます。
まず Vision の画像認識では簡単にですが以下の役割を持ったクラスが出てきます。
- Request
- 画像解析要求
- RequestHandler
- 画像解析要求を処理するオブジェクト
- Observation
- 画像解析結果
今回のケースではタッチの位置でまず Observation
を作成し、その後は前の Observation
を元に新たに Observation
を生成を繰り返していきます。
Observation
の情報を UIView で表示することで追跡が行われていることを視覚的に確認していきます。
RequestHandler
としては複数の画像に関連する要求を処理する VNSequenceRequestHandler
を使用し、Observation
としては検出された範囲を持つ VNDetectedObjectObservation
を使用します。
まず、ViewController
のプロパティとして VNSequenceRequestHandler
と VNDetectedObjectObservation
を設定します。
また VNDetectedObjectObservation
は追跡の対象を変更する上で一度 nil としたいので、オプショナルとしておきます。
private var requestHandler: VNSequenceRequestHandler = VNSequenceRequestHandler() private var lastObservation: VNDetectedObjectObservation?
また、切り替える上でタッチが開始されてから離されるまでは追跡を行わない作りとします。
そのため現在タッチ中かどうかのフラグを用意します。
private var isTouched: Bool = false
ここまでで ViewController で用意するプロパティは出揃いました。
それではまず先ほど用意した touchesBegan
に対して VNDetectedObjectObservation
を nil とし、 isTouched を true とします。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { highlightView.frame = .zero lastObservation = nil isTouched = true }
また、 touchesEnded
で追跡を追跡対象となる Observation
を設定し、 isTouched は false とします。
AVCaptureVideoPreviewLayer
における範囲(0〜1で表される)を取得した上で VNDetectedObjectObservation
に渡しますが、
VNDetectedObjectObservation
では y の座標が逆となるため、1から引いた値とすることで対応します。
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch: UITouch = touches.first else { return } highlightView.frame.size = CGSize(width: 120, height: 120) highlightView.center = touch.location(in: view) isTouched = false var convertedRect = previewLayer.metadataOutputRectConverted(fromLayerRect: highlightView.frame) convertedRect.origin.y = 1 - convertedRect.origin.y lastObservation = VNDetectedObjectObservation(boundingBox: convertedRect) }
AVCaptureVideoDataOutput
の更新部分に手を入れていきます。
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), let lastObservation = self.lastObservation else { requestHandler = VNSequenceRequestHandler() return } if self.isTouched { return } let request = VNTrackObjectRequest(detectedObjectObservation: lastObservation, completionHandler: update) request.trackingLevel = .accurate do { try requestHandler.perform([request], on: pixelBuffer) } catch { print("Throws: \(error)") } }
追跡対象を数回繰り返した際に問題とならないように追跡対象が変更される(nil となっている)タイミングで VNSequenceRequestHandler
を作り直す処理を行っています。
VNTrackObjectRequest
には前回の結果となる detectedObjectObservation
と処理完了時に呼ぶ completionHandler
を引数として渡します。
今回は completionHandler
には別のメソッドとして update を用意する形にしました。
VNTrackObjectRequest
の品質は精度を重視する accurate
を指定しています。
VNSequenceRequestHandler
の perform
で request
と動画の内容を渡し解析を開始しています。
画像認識の結果に対する処理を行っていきます。
private func update(_ request: VNRequest, error: Error?) { DispatchQueue.main.async { guard let newObservation = request.results?.first as? VNDetectedObjectObservation else { return } self.lastObservation = newObservation guard newObservation.confidence >= 0.3 else { self.highlightView.frame = .zero return } var transformedRect = newObservation.boundingBox transformedRect.origin.y = 1 - transformedRect.origin.y let convertedRect = self.previewLayer.layerRectConverted(fromMetadataOutputRect: transformedRect) self.highlightView.frame = convertedRect } }
Observation
として結果を取得できていたとしても VNDetectedObjectObservation
の confidence
で信頼度を確認し、一定の値以下であれば表示は行わないとしています。
最後に VNDetectedObjectObservation
から得られた範囲を元に、選択範囲を表す UIView の frame を算出しています。
まとめ
今回は Vision.Framework のオブジェクトの検出と追跡といった機能を試してみました。
画像処理の専門的な知識がなくとも、少ない記述量で簡単に実装できてしまいした。
普段開発するようなアプリでも Vision.Framework の他の機能や ARKit などを組み合わせることで、体験としてより価値の高いものを少ない労力で実現できるようになったのではないでしょうか?
公式
https://developer.apple.com/documentation/vision