
【iOS】Vision Frameworkを使ってリアルタイム顔検出アプリを作ってみた
2021.12.20
Vision Framework を使ってみた!

(株)ライトコードの小林です!
皆さんは、iOS の標準フレームワーク「Vision Framework」をご存知でしょうか?
「顔検出」や「テキスト検出」など、高度な画像解析処理ができるフレームワークです。
今回は、その Vision Framework を使って、「リアルタイム顔検出アプリ」を作っていきます!
開発環境
開発環境は、下記のとおりです。
MacOS | 10.15.4 |
Swift | 5.3 |
Xcode | 12.0 |
CocoaPods | 1.8.4 |
RxSwift, RxCocoa | 5.1.1 |
Vision Framework とは?
「Vision Framework」とは、Apple が標準で提供している、高度な画像解析処理を行うフレームワークです。
【公式ドキュメント】
https://developer.apple.com/documentation/vision
iOS 11の SDK から追加されたものですが、WWDC 2020 では、「体や手のポーズを検出してジェスチャーで命令できる機能」や、「空中に文字を描くことができる機能」が発表されたりと、まだまだ進化を続けています。
参考:【Detect Body and Hand Pose with Vision】
https://developer.apple.com/videos/play/wwdc2020/10653/
具体的には何が出来るの?
このフレームワークには、以下のような画像解析に関する様々な機能が備わっています。
- 顔検出
- テキスト検出
- バーコード認識
今回は、「顔検出機能」に焦点を当てて、実装してみたいと思います。
でも、ただ顔を検出するだけでは物足りないので、検出された顔に好きな画像を重ねるようにしてみましょう!
リアルタイムで顔検出を行うアプリを作る
すべて ViewController 内に処理を書いてもいいのですが、FatViewController になるのを防ぐため、RxSwift を使って MVVM で実装していきます。
ViewModel の実装
まずは、今回の肝となる「顔検出処理」と「検出した部分に画像を重ねる処理」を実装していきましょう。
Input と Output を定義
ViewModel のファイルを作成し、Input と Output を定義します。
Input | カメラの出力内容 |
Output | 顔検出結果画像 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import Foundation import AVFoundation import RxSwift import RxCocoa import Vision protocol FaceDetectionViewModelInput { // カメラの出力 var captureOutputTrigger: PublishSubject<CMSampleBuffer> { get } } protocol FaceDetectionViewModelOutput { // 顔検出結果画像 var detectionResultImage: PublishSubject<UIImage?> { get } } protocol FaceDetectionViewModelType { var input: FaceDetectionViewModelInput { get } var output: FaceDetectionViewModelOutput { get } } final class FaceDetectionViewModel: FaceDetectionViewModelType, FaceDetectionViewModelInput, FaceDetectionViewModelOutput { var input: FaceDetectionViewModelInput { return self } var output: FaceDetectionViewModelOutput { return self } // MARK: - input var captureOutputTrigger = PublishSubject<CMSampleBuffer>() // MARK: - output var detectionResultImage = PublishSubject<UIImage?>() // カメラから取得した画像のデータ private var sampleBuffer: CMSampleBuffer? private let disposeBag = DisposeBag() init() {} } |
顔検出処理を実装
顔検出を行うには、下記の Vision Framework クラスを使います。
VNDetectFaceRectanglesRequest | 画像内から顔を検出する処理をリクエスト |
VNImageRequestHandler | 画像に対してリクエストされた処理を実行 |
VNFaceObservation | 画像解析処理リクエストによって検出された顔に関する情報 |
今回は、受け取った画像に対して顔検出処理を要求し、処理が完了したら Observable に解析結果を流すように実装しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 顔検出処理 private func detectFace() -> Observable<[VNFaceObservation]> { return Observable<[VNFaceObservation]>.create({ [weak self] observer in if let sampleBuffer = self?.sampleBuffer, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { // 顔検出処理を要求 let request = VNDetectFaceRectanglesRequest { (request, error) in guard let results = request.results as? [VNFaceObservation] else { // 顔が検出されなかったら空列を流す observer.onNext([]) return } // 顔が検出されたら検出結果を流す observer.onNext(results) } // 顔検出処理を実行 let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]) try? handler.perform([request]) } return Disposables.create() }) } |
検出した顔に画像を重ね、出来た画像を返す処理を実装
今回は、猫のイラストを顔に重ねたいので、イラスト画像を用意して、同プロジェクト内に置いておきます。
簡単に説明すると、カメラから出力された画像データをもとに、画像を再生成します。
その過程の中で、顔検出で取得した顔の位置情報をもとに、画像を重ねる処理をするという形になっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | // 猫の顔の画像 private var catFaceCgImage: CGImage? init() { // 猫の顔の画像を設定 guard let imagePath = Bundle.main.path(forResource: "CatFace", ofType: "png") else { return } guard let image = UIImage(contentsOfFile: imagePath) else { return } catFaceCgImage = image.cgImage } (省略) // 画像の再生成 private func regenerationImage(_ faceObservations: [VNFaceObservation]) -> UIImage? { guard let sampleBuffer = sampleBuffer else { return nil } guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil } CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) guard let pixelBufferBaseAddres = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) else { CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) return nil } let width = CVPixelBufferGetWidth(imageBuffer) let height = CVPixelBufferGetHeight(imageBuffer) let bitmapInfo = CGBitmapInfo(rawValue: (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)) let context = CGContext( data: pixelBufferBaseAddres, width: width, height: height, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo.rawValue ) // 検出結果を元に顔の表示位置に対して処理を実行 faceObservations // 表示位置情報(CGRect)に変換(※1) .compactMap { $0.boundingBox.converted(to: CGSize(width: width, height: height)) } // 検出した顔ごとに猫の顔の画像を描画 .forEach { guard let catFaceCgImage = catFaceCgImage else { return } context?.draw(catFaceCgImage, in: $0) } CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) guard let imageRef = context?.makeImage() else { return nil } return UIImage(cgImage: imageRef, scale: 1.0, orientation: UIImage.Orientation.up) } |
※1 Extension
※1では、下記のような Extension を作り、CGRect を取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 | import AVFoundation extension CGRect { func converted(to size: CGSize) -> CGRect { return CGRect( x: self.minX * size.width, y: self.minY * size.height, width: self.width * size.width, height: self.height * size.height ) } } |
バインディング
最後に、これまで実装した処理を使い、初期化処理の中で Input と Output のバインディングをします。
これで、ViewModel の実装は完了です!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | init() { (省略) captureOutputTrigger // 画像データを設定 .map { [weak self] in self?.sampleBuffer = $0 } // 顔検出処理 .flatMapLatest { return self.detectFace() } // 画像の再生成 .map { [weak self] in self?.regenerationImage($0) } // 検出結果画像にバインディング .bind(to: detectionResultImage) .disposed(by: disposeBag) } |
ViewController の実装
続いて、ViewController の実装を行っていきます。
初期化処理を実装
まずは、ViewModel を ViewController に注入するための初期化処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import UIKit import AVFoundation import RxSwift import RxCocoa class FaceDetectionViewController: UIViewController { private var viewModel: FaceDetectionViewModelType required init(with dependency: FaceDetectionViewModel) { viewModel = dependency super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() } } |
カメラの設定
フロントカメラから取得した画像データを Stream で流すようにしています。
また、カメラのプライバシーアクセスの許可を取得するために、 Info.plist に Privacy - Camera Usage Description を設定します。
(この設定を忘れるとクラッシュします!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | class FaceDetectionViewController: UIViewController { (省略) @IBOutlet weak var detectionResultImageView: UIImageView! private var avCaptureSession = AVCaptureSession() private var videoDevice: AVCaptureDevice? // カメラの出力結果を流すためのStream private let capturedOutputStream = PublishSubject<CMSampleBuffer>() (省略) override func viewDidLoad() { super.viewDidLoad() setupVideoProcessing() } private func onViewDidDisappear() { avCaptureSession.stopRunning() } private func setupVideoProcessing() { avCaptureSession.sessionPreset = .photo // AVCaptureSession#addInput videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) guard let videoDevice = videoDevice else { return } guard let deviceInput = try? AVCaptureDeviceInput(device: videoDevice) else { return } avCaptureSession.addInput(deviceInput) // AVCaptureSession#addOutput let videoDataOutput = AVCaptureVideoDataOutput() videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32BGRA)] videoDataOutput.setSampleBufferDelegate(self, queue: .global()) avCaptureSession.addOutput(videoDataOutput) avCaptureSession.startRunning() } } extension FaceDetectionViewController: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // カメラの出力結果を流す capturedOutputStream.onNext(sampleBuffer) connection.videoOrientation = .portrait } } |
バインディング
次に、ViewModel とのバインディングを行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | private let disposeBag = DisposeBag() (省略) override func viewDidLoad() { super.viewDidLoad() bind() setupVideoProcessing() } private func bind() { // input capturedOutputStream .bind(to: viewModel.input.captureOutputTrigger) .disposed(by: disposeBag) // output viewModel.output.detectionResultImage .bind(to: detectionResultImageView.rx.image) .disposed(by: disposeBag) } |
ViewModel を注入し、ViewController を設定
最後に、 SceneDelegate 内で ViewController に ViewModel を注入し、rootViewController に先程作った ViewController を設定します。
お疲れさまでした、これで実装完了です!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = scene as? UIWindowScene else { return } let viewModel = FaceDetectionViewModel() let vc = FaceDetectionViewController(with: viewModel) let window = UIWindow(windowScene: windowScene) window.rootViewController = vc window.makeKeyAndVisible() self.window = window } } |
動作確認
さっそく動作確認してみましょう!
ビルドして、実機を起動してみた結果…

無事、リアルタイムで顔を検出し、検出された顔に猫のイラストを重ねることができました!
さいごに
今回は、Vision Framework を使って「リアルタイム顔検出アプリ」を作ってみました。
同じ要領で、顔部分にモザイクをかけたりなんかもできそうですね!
AWS 等のクラウドサービスでも様々な顔認識サービスを提供していますが、Vision はネットワークを経由せず、ローカル内で完結するため、場所や時間を問わず使えるのが大きなメリットです。
気になった方は、ぜひ試してみてください!
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン