端末に保存されているムービーでオブジェクトトラッキング(Vision.Framework)を行う方法
この記事を書いている現在(2018.6.5)WWDC 2018が絶賛開催中ですが、去年のWWDCで発表となったVision.Frameworkのオブジェクトトラッキング機能についてごにょごにょしてみたので備忘録として残しておきます。
以下を参考にしました。
Vision.Framework のオブジェクトトラッキングを使ってみよう! | ギャップロ
AVCaptureSession(端末のカメラを使用したライブの映像)を使ったオブジェクトトラッキングなら上記方法で全く問題ないありません。
Webにも上記の例であれば結構たくさんあがっています。
しかしながら今回私は端末に保存されているムービーでオブジェクトトラッキングを実行したかったのです。
そうなるとなかなか「使える」情報が転がってませんでした。
端末に保存されているムービーでオブジェクトトラッキング 実現するにはどうすれば良いのか?
端末に保存されているムービーでオブジェクトトラッキングを実行するには何をしてあげればいいのでしょうか。
リンク参考に作成したプロジェクトを眺めてコードの思いを理解しようと努めましたところ、pixelBuffer(CVPixelBuffer型)の変数が怪しそうだと感じました。カメラからキャプチャした映像から取得したバッファpixelBufferをオブジェクトトラッキングを実行するメソッドに与え検出と追跡を行なっているように見えたのです。
とすればですよ。端末に保存されたムービーを読み出し再生するときにバッファpixelBufferを取得しそれでムービー再生できればオブジェクトトラッキングを実行できるのでは?と推測しました。
その為に行なったのが二つ前の記事 AVPlayerItemVideoOutputを使ってムービーファイルから取得したPixelBufferからムービーの再生させてみた です。
PixelBufferでのムービー再生とオブジェクトトラッキングの合体 そしてトラップ
pixelBufferでのムービー再生とオブジェクトトラッキングの合体しようとしました。
しかしまたここでもトラップがありました。
オブジェクトトラッキングの参考にしたコードでは、追跡箇所の座標(highlightView用)を画像解析用座標(lastObservation用)に変換するメソッドとして metadataOutputRectConverted を使用しています。また画像解析用座標から、通常の座標軸への変換するメソッドとして layerRectConverted を使用しています。
これらを今回のケースでも同じように使いたいと考えましたが、これらは二つとも AVCaptureVideoPreviewLayer のメソッドです。
AVCaptureVideoPreviewLayer は名前からわかる通り端末のカメラを使用したい時に使用するクラスですので、今回は使用できません。
結構調べたのですが、今回のケースで使用できる似たようなメソッドを見つけることはできませんでした。
PixelBufferでのムービー再生とオブジェクトトラッキングの合体 解決法
解決の糸口を見つける為に、色々なAVCaptureSessionでのオブジェクトトラッキングの例を見ましたが(ほとんどが同じやり方でしたが)、あるページを見た時、「ん?これはちょっと他のとは違うな…」と感じました。
引っかかったのはこのページ。
iOS ARKit 教程:不触摸屏幕,用空气中的手势作画 - CocoaChina_让移动开发更简单
中国語の為、何が書いてあるかはサーッパリ分からないですがコードは読めます。
気になった部分はタップした箇所の座標を画像解析用座標に変換して追跡対象として登録する tapAction メソッド。
その他のページの例では先で述べたように metadataOutputRectConverted メソッドを使って変換作業をしていますが、このページではmetadataOutputRectConverted を使用せずに変換を行なっているではありませんか!
ここから以下の工程で画像解析用座標に変換できることがわかりました。
1. タップした座標位置(CGRect型)のwidthとheightそれぞれについて逆数になるように変換する 2. Y座標を1から引いた値にする (VNDetectedObjectObservation では y の座標が逆となるため)
このページでは displayTransform というメソッドを使った処理が挟まれていますが、これはライブラリから読み出したファイルの場合には必要ありません。
displayTransform にしても metadataOutputRectConverted にしても端末のカメラを使った映像となっている為、解像度なんかがまだ未決で、その為に特別な変換処理が追加されたメソッドを準備しているのではないか、と推測しております。
オブジェクトトラッキングを追加したSampleBufferDisplayLayerViewクラスのコード
AVPlayerItemVideoOutputを使ってムービーファイルから取得したPixelBufferからムービーの再生させてみた
編集 からの変更になります。
PixelBufferでのムービー再生とオブジェクトトラッキングの合体しているという部分を意識すればそこまで複雑な流れではないかと思います。(長いですが…)
import UIKit import AVFoundation import Vision class SampleBufferDisplayLayerView: UIView, AVPlayerItemOutputPullDelegate { private var requestHandler: VNSequenceRequestHandler = VNSequenceRequestHandler() private var lastObservation: VNDetectedObjectObservation? private lazy var highlightView: UIView = { let view = UIView() view.layer.borderColor = UIColor.white.cgColor view.layer.borderWidth = 4 view.backgroundColor = .clear return view }() private var isTouched: Bool = false override public class var layerClass: Swift.AnyClass { get { return AVSampleBufferDisplayLayer.self } } let player = AVPlayer() required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) player.addObserver(self, forKeyPath: "currentItem", options: [.new], context: nil) playerItemVideoOutput.setDelegate(self, queue: queue) playerItemVideoOutput.requestNotificationOfMediaDataChange(withAdvanceInterval: advancedInterval) displayLink = CADisplayLink(target: self, selector: #selector(displayLinkCallback(_:))) displayLink.preferredFramesPerSecond = 1 / 30 displayLink.isPaused = true displayLink.add(to: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode) //タイマーを開始 } // KVO override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { switch keyPath { case "currentItem"?: if let item = change![NSKeyValueChangeKey.newKey] as? AVPlayerItem { item.add(playerItemVideoOutput) videoLayer.controlTimebase = item.timebase } default: //super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) break } } // MARK: AVPlayerItemOutputPullDelegate func outputMediaDataWillChange(_ sender: AVPlayerItemOutput) { print("outputMediaDataWillChange") displayLink.isPaused = false } func outputSequenceWasFlushed(_ output: AVPlayerItemOutput) { videoLayer.controlTimebase = player.currentItem?.timebase videoLayer.flush() } //private private let playerItemVideoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_32ARGB]) // ピクセルフォーマット(32bit BGRA) private let queue = DispatchQueue.main private let advancedInterval: TimeInterval = 0.1 private var displayLink: CADisplayLink! private var lastTimestamp: CFTimeInterval = 0 private var videoInfo: CMVideoFormatDescription? private var videoLayer: AVSampleBufferDisplayLayer { return self.layer as! AVSampleBufferDisplayLayer } /** setCADiplayLinkSettingに呼び出されるselector。 */ @objc private func displayLinkCallback(_ displayLink: CADisplayLink) { let nextOutputHostTime = displayLink.timestamp + displayLink.duration * CFTimeInterval(displayLink.preferredFramesPerSecond) let nextOutputItemTime = playerItemVideoOutput.itemTime(forHostTime: nextOutputHostTime) if playerItemVideoOutput.hasNewPixelBuffer(forItemTime: nextOutputItemTime) { lastTimestamp = displayLink.timestamp var presentationItemTime = kCMTimeZero let pixelBuffer = playerItemVideoOutput.copyPixelBuffer(forItemTime: nextOutputItemTime, itemTimeForDisplay: &presentationItemTime) displayPixelBuffer(pixelBuffer: pixelBuffer!, atTime: presentationItemTime) } else { if displayLink.timestamp - lastTimestamp > 0.5 { displayLink.isPaused = true playerItemVideoOutput.requestNotificationOfMediaDataChange(withAdvanceInterval: advancedInterval) } } } @objc private func displayPixelBuffer(pixelBuffer: CVPixelBuffer, atTime outputTime: CMTime) { var err: OSStatus = noErr let ciImage:CIImage = CIImage(cvPixelBuffer: pixelBuffer) let orientation:CGImagePropertyOrientation = CGImagePropertyOrientation.right let targetCIImage = ciImage.oriented(orientation) //回転させたい時はこっち // let targetCIImage = ciImage //回転させたくない時はこっち let targetPixelBuffer:CVPixelBuffer = convertFromCIImageToCVPixelBuffer(ciImage: targetCIImage)! if videoInfo == nil { err = CMVideoFormatDescriptionCreateForImageBuffer(nil, targetPixelBuffer, &videoInfo) if (err != noErr) { print("Error at CMVideoFormatDescriptionCreateForImageBuffer \(err)") } } detectObject(pixelBuffer: targetPixelBuffer) var sampleTimingInfo = CMSampleTimingInfo(duration: kCMTimeInvalid, presentationTimeStamp: outputTime, decodeTimeStamp: kCMTimeInvalid) var sampleBuffer: CMSampleBuffer? err = CMSampleBufferCreateForImageBuffer(nil, targetPixelBuffer, true, nil, nil, videoInfo!, &sampleTimingInfo, &sampleBuffer) if (err != noErr) { NSLog("Error at CMSampleBufferCreateForImageBuffer \(err)") } if videoLayer.isReadyForMoreMediaData { videoLayer.enqueue(sampleBuffer!) } sampleBuffer = nil } private func convertFromCIImageToCVPixelBuffer (ciImage:CIImage) -> CVPixelBuffer? { let size:CGSize = ciImage.extent.size var pixelBuffer:CVPixelBuffer? let options = [ kCVPixelBufferCGImageCompatibilityKey as String: true, kCVPixelBufferCGBitmapContextCompatibilityKey as String: true, kCVPixelBufferIOSurfacePropertiesKey as String: [:] ] as [String : Any] let status:CVReturn = CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width), Int(size.height), kCVPixelFormatType_32BGRA, options as CFDictionary, &pixelBuffer) CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) let ciContext = CIContext() if (status == kCVReturnSuccess && pixelBuffer != nil) { ciContext.render(ciImage, to: pixelBuffer!) } return pixelBuffer } private func detectObject (pixelBuffer: CVPixelBuffer) { guard 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)") } } 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 t = CGAffineTransform(scaleX: self.frame.size.width, y: self.frame.size.height) let convertedRect = transformedRect.applying(t) self.highlightView.frame = convertedRect } } //ViewControllerのtouchesBeganの中の処理でSampleBufferDisplayLayerView.touchBeganを呼び出してあげる override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { highlightView.frame = .zero lastObservation = nil isTouched = true } //ViewControllerのtouchesEndedの中の処理でSampleBufferDisplayLayerView.touchesEndedを呼び出してあげる override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch: UITouch = touches.first else { return } highlightView.frame.size = CGSize(width: 90, height: 90) highlightView.center = touch.location(in: self) isTouched = false let t = CGAffineTransform(scaleX: 1.0 / self.frame.size.width, y: 1.0 / self.frame.size.height) var normalizedTrackImageBoundingBox = highlightView.frame.applying(t) normalizedTrackImageBoundingBox.origin.y = 1 - normalizedTrackImageBoundingBox.origin.y lastObservation = VNDetectedObjectObservation(boundingBox: normalizedTrackImageBoundingBox) self.addSubview(highlightView) } }
Github
今回作成したプロジェクトは以下にあげてます。
ViewControllerで受け取ったタップ座標をSampleBufferDisplayLayerViewに伝える箇所は少し気をつけて見てください。