ここではVisionを使って顔を検出する方法を紹介します。VisionはiOS11から使える画像処理フレームワークです。ここではiOS12を使って説明します。
Visonフレームワークとは
Vision は画像処理用のフレームワークです。ただOpenCVなどのように汎用的な画像処理ではなく、顔検出や文字検出など機械学習に特化したフレームワークになっています。
Visionフレームワークを使うと、次のようなものを簡単に検出することができます。それぞれのものについては個別に説明します。
- 顔検出
- 顔ランドマーク検出
- 文字検出
- 矩形検出
- バーコード
- オブジェクトトラッキング
顔検出の流れ
Visionフレームワークを使って顔検出をするには「リクエスト」と「リクエストハンドラ」の2つを使います。
リクエストハンドラ(VNImageRequestHandler)に処理したい画像とリクエスト内容を渡すことで、指定されたオブジェクトの検出処理が行われます。検出結果はObservationクラスを使って取得することができます。
リクエストには次のような種類があります。ここでは顔検出をしたいのでVNDetectFaceRectanglesRequestを使用しています。
VNDetectFaceRectanglesRequest | 顔検出 |
VNDetectFaceLandmarksRequest | 顔ランドマーク検出 |
VNDetectRectanglesRequest | 矩形検出 |
VNDetectBarcodesRequest | バーコード検出 |
VNDetectTextRectanglesRequest | テキスト検出 |
それでは早速、写真から顔を検出するプログラムを作ってみましょう。
写真から顔を検出する
まずは読み込んだ写真から顔の位置を検出して、その周りに矩形を表示するプログラムを作ってみましょう。
Xcodeのプロジェクトを作成して、次のtest.jpgの画像をプロジェクトに追加します。追加できたらViewController.swiftに次のプログラムを入力してください。
import UIKit import Vision class ViewController: UIViewController { var image : UIImage! func drawRect(box:CGRect){ let xRate : CGFloat = self.view.bounds.width / self.image.size.width let yRate : CGFloat = self.view.bounds.height / self.image.size.height let faceRect = CGRect( x: (box.minX) * self.image.size.width * xRate, y: (1 - box.maxY) * self.image.size.height * yRate, width: box.width * self.image.size.width * xRate, height: box.height * self.image.size.height * yRate ) let faceTrackingView = UIView(frame: faceRect) faceTrackingView.backgroundColor = UIColor.clear faceTrackingView.layer.borderWidth = 1.0 faceTrackingView.layer.borderColor = UIColor.green.cgColor self.view.addSubview(faceTrackingView) self.view.bringSubviewToFront(faceTrackingView) } override func viewDidLoad() { super.viewDidLoad() // 画像を読み込む self.image = UIImage(named: "test.jpg") // 顔検出用のリクエストを生成 let request = VNDetectFaceRectanglesRequest { (request: VNRequest, error: Error?) in for observation in request.results as! [VNFaceObservation] { // 枠線を描画する self.drawRect(box:observation.boundingBox) } } // 顔検出開始 if let cgImage = image.cgImage { let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) try? handler.perform([request]) } } }
viewDidLoadメソッドの最初でVNDetectFaceRectanglesRequestで顔検出のリクエストを作成しています。
顔が見つかった場合はrequest.resultsにVNFaceObservationの配列として結果が格納されているので、ひとつずつ取り出してdrawRectメソッドで矩形を描画しています。
リクエストは作成しただけでは実行されません。最後にVNImageRequestHandlerを作って、performメソッドに先程の顔検出のリクエストを渡して実行しています。このタイミングで実際の顔検出が行われています。
動画から顔を検出する
一枚の写真から顔が検出できるようになったところで、次は動画から顔を検出してみます。といっても動画は時系列に画像が並んだものとみなせるので、顔検出の方法はほとんど同じです。ただiOSで動画を扱うにはAVCaptureSessionを使う必要があり、これが結構面倒です・・・
動画で顔検出する処理を追加したプログラムは、次のようになります。まずは全プログラムを載せておきます。
import UIKit import Vision import AVFoundation class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate { private var _captureSession = AVCaptureSession() private var _videoDevice = AVCaptureDevice.default(for: AVMediaType.video) private var _videoOutput = AVCaptureVideoDataOutput() private var _videoLayer : AVCaptureVideoPreviewLayer? = nil private var rectArray:[UIView] = [] var image : UIImage! func setupVideo( camPos:AVCaptureDevice.Position, orientaiton:AVCaptureVideoOrientation ){ // カメラ関連の設定 self._captureSession = AVCaptureSession() self._videoOutput = AVCaptureVideoDataOutput() self._videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: camPos) // Inputを作ってSessionに追加 do { let videoInput = try AVCaptureDeviceInput(device: self._videoDevice!) as AVCaptureDeviceInput self._captureSession.addInput(videoInput) } catch let error as NSError { print(error) } // Outputを作ってSessionに追加 self._videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable as! String : Int(kCVPixelFormatType_32BGRA)] self._videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) self._videoOutput.alwaysDiscardsLateVideoFrames = true self._captureSession.addOutput(self._videoOutput) for connection in self._videoOutput.connections { connection.videoOrientation = orientaiton } // 出力レイヤを作る self._videoLayer = AVCaptureVideoPreviewLayer(session: self._captureSession) self._videoLayer?.frame = UIScreen.main.bounds self._videoLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill self._videoLayer?.connection?.videoOrientation = orientaiton self.view.layer.addSublayer(self._videoLayer!) // 録画開始 self._captureSession.startRunning() } private func imageFromSampleBuffer(sampleBuffer: CMSampleBuffer) -> UIImage { let imageBuffer: CVImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)! CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) let context = CGContext(data: CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0), width: CVPixelBufferGetWidth(imageBuffer), height: CVPixelBufferGetHeight(imageBuffer), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer), space: colorSpace, bitmapInfo: bitmapInfo) let imageRef = context!.makeImage() CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) let resultImage: UIImage = UIImage(cgImage: imageRef!) return resultImage } func drawRect(box:CGRect){ let xRate : CGFloat = self.view.bounds.width / self.image.size.width let yRate : CGFloat = self.view.bounds.height / self.image.size.height let faceRect = CGRect( x: (1 - box.maxX) * self.image.size.width * xRate, y: (1 - box.maxY) * self.image.size.height * yRate, width: box.width * self.image.size.width * xRate, height: box.height * self.image.size.height * yRate ) let faceTrackingView = UIView(frame: faceRect) faceTrackingView.backgroundColor = UIColor.clear faceTrackingView.layer.borderWidth = 1.0 faceTrackingView.layer.borderColor = UIColor.green.cgColor self.view.addSubview(faceTrackingView) self.view.bringSubviewToFront(faceTrackingView) rectArray.append(faceTrackingView) } func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { DispatchQueue.main.async { self.image = self.imageFromSampleBuffer(sampleBuffer: sampleBuffer) // 顔検出用のリクエストを生成 let request = VNDetectFaceRectanglesRequest { (request: VNRequest, error: Error?) in self.rectArray.forEach{ $0.removeFromSuperview() } self.rectArray.removeAll() for observation in request.results as! [VNFaceObservation] { // 枠線を描画する self.drawRect(box:observation.boundingBox) } } // 顔検出開始 if let cgImage = self.image.cgImage { let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) try? handler.perform([request]) } } } override func viewDidLoad() { super.viewDidLoad() setupVideo(camPos: .front, orientaiton: .portrait) } }
AVCaptureSessionを使うために、次のようにプログラムを変更しています。
- import AVFoundationを追加
- AVCaptureVideoDataOutputSampleBufferDelegateを継承
- setupVideoメソッドの中でAVCaptureSessionの初期化
- captureOutputデリゲートを宣言して、その中で顔検出
AVCaptureSessionには入力ソースと出力形式を指定する必要があります。入力にはiPhoneデバイスのカメラを指定します。一方出力には「静止画」「動画」「音声」などを選ぶことができます。
ここでは入力にはフロントカメラを指定しました。また、出力には動画のフレームデータがほしいのでAVCaptureVideoDataOutput指定しています。また、画像フォーマットやdelegateメソッド、表示の向きなどもあわせて指定します。
AVCaptureSessionが正しく設定できると、captureOutputメソッドが1フレームごとに呼び出されるようになります。この中で顔検出の処理を行って矩形を描画しています(ここは写真から顔検出するのとほぼ同じです)
プログラムができたらinfo.plistにPrivacy - Camera Usage Descriptionを追加してから(忘れたらアプリがクラッシュします!)実行してみてください。↓のような感じで動くと思います。
iOSのVisionフレームワークで顔検出 pic.twitter.com/SV3GzMk1Vz
— 北村愛実 (@tasonco_company) 2019年7月25日