• トップ
  • ブログ一覧
  • 【iOS】Vision Frameworkを使ってリアルタイム顔検出アプリを作ってみた
  • 【iOS】Vision Frameworkを使ってリアルタイム顔検出アプリを作ってみた

    広告メディア事業部広告メディア事業部
    2020.11.17

    IT技術

    Vision Framework を使ってみた!

    小林先生

    (株)ライトコードの小林です!

    皆さんは、iOS の標準フレームワーク「Vision Framework」をご存知でしょうか?

    「顔検出」や「テキスト検出」など、高度な画像解析処理ができるフレームワークです。

    今回は、その Vision Framework を使って、「リアルタイム顔検出アプリ」を作っていきます!

    開発環境

    開発環境は、下記のとおりです。

    MacOS10.15.4
    Swift5.3
    Xcode12.0
    CocoaPods1.8.4
    RxSwift, RxCocoa5.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/

    具体的には何が出来るの?

    このフレームワークには、以下のような画像解析に関する様々な機能が備わっています。

    1. 顔検出
    2. テキスト検出
    3. バーコード認識

    今回は、「顔検出機能」に焦点を当てて、実装してみたいと思います。

    でも、ただ顔を検出するだけでは物足りないので、検出された顔に好きな画像を重ねるようにしてみましょう!

    リアルタイムで顔検出を行うアプリを作る

    すべて ViewController 内に処理を書いてもいいのですが、FatViewController になるのを防ぐため、RxSwift を使って MVVM で実装していきます。

    ViewModel の実装

    まずは、今回の肝となる「顔検出処理」と「検出した部分に画像を重ねる処理」を実装していきましょう。

    Input と Output を定義

    ViewModel のファイルを作成し、Input と Output を定義します。

    Inputカメラの出力内容
    Output顔検出結果画像
    1import Foundation
    2import AVFoundation
    3import RxSwift
    4import RxCocoa
    5import Vision
    6
    7protocol FaceDetectionViewModelInput {
    8    // カメラの出力
    9    var captureOutputTrigger: PublishSubject<CMSampleBuffer> { get }
    10}
    11
    12protocol FaceDetectionViewModelOutput {
    13    // 顔検出結果画像
    14    var detectionResultImage: PublishSubject<UIImage?> { get }
    15}
    16
    17protocol FaceDetectionViewModelType {
    18    var input: FaceDetectionViewModelInput { get }
    19    var output: FaceDetectionViewModelOutput { get }
    20}
    21
    22final class FaceDetectionViewModel: FaceDetectionViewModelType, FaceDetectionViewModelInput, FaceDetectionViewModelOutput {
    23    var input: FaceDetectionViewModelInput { return self }
    24    var output: FaceDetectionViewModelOutput { return self }
    25
    26    // MARK: - input
    27    var captureOutputTrigger = PublishSubject<CMSampleBuffer>()
    28
    29    // MARK: - output
    30    var detectionResultImage = PublishSubject<UIImage?>()
    31
    32    // カメラから取得した画像のデータ
    33    private var sampleBuffer: CMSampleBuffer?
    34
    35    private let disposeBag = DisposeBag()
    36
    37    init() {}
    38}

    顔検出処理を実装

    顔検出を行うには、下記の Vision Framework クラスを使います。

    VNDetectFaceRectanglesRequest画像内から顔を検出する処理をリクエスト
    VNImageRequestHandler画像に対してリクエストされた処理を実行
    VNFaceObservation画像解析処理リクエストによって検出された顔に関する情報

    今回は、受け取った画像に対して顔検出処理を要求し、処理が完了したら Observable に解析結果を流すように実装しています。

    1    // 顔検出処理
    2    private func detectFace() -> Observable<[VNFaceObservation]> {
    3        return Observable<[VNFaceObservation]>.create({ [weak self] observer in
    4            if let sampleBuffer = self?.sampleBuffer,
    5                let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
    6                // 顔検出処理を要求
    7                let request = VNDetectFaceRectanglesRequest { (request, error) in
    8                    guard let results = request.results as? [VNFaceObservation] else {
    9                        // 顔が検出されなかったら空列を流す
    10                        observer.onNext([])
    11                        return
    12                    }
    13                    // 顔が検出されたら検出結果を流す
    14                    observer.onNext(results)
    15                }
    16                // 顔検出処理を実行
    17                let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
    18                try? handler.perform([request])
    19            }
    20            return Disposables.create()
    21        })
    22    }

    検出した顔に画像を重ね、出来た画像を返す処理を実装

    今回は、猫のイラストを顔に重ねたいので、イラスト画像を用意して、同プロジェクト内に置いておきます。

    簡単に説明すると、カメラから出力された画像データをもとに、画像を再生成します。

    その過程の中で、顔検出で取得した顔の位置情報をもとに、画像を重ねる処理をするという形になっています。

    1    // 猫の顔の画像
    2    private var catFaceCgImage: CGImage?
    3
    4    init() {
    5        // 猫の顔の画像を設定
    6        guard let imagePath = Bundle.main.path(forResource: "CatFace", ofType: "png") else { return }
    7        guard let image = UIImage(contentsOfFile: imagePath) else { return }
    8        catFaceCgImage = image.cgImage
    9    }
    10
    11    (省略)
    12
    13    // 画像の再生成
    14    private func regenerationImage(_ faceObservations: [VNFaceObservation]) -> UIImage? {
    15        guard let sampleBuffer = sampleBuffer else { return nil }
    16        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }
    17
    18        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
    19
    20        guard let pixelBufferBaseAddres = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) else {
    21            CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
    22            return nil
    23        }
    24
    25        let width = CVPixelBufferGetWidth(imageBuffer)
    26        let height = CVPixelBufferGetHeight(imageBuffer)
    27        let bitmapInfo = CGBitmapInfo(rawValue: (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue))
    28
    29        let context = CGContext(
    30            data: pixelBufferBaseAddres,
    31            width: width,
    32            height: height,
    33            bitsPerComponent: 8,
    34            bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer),
    35            space: CGColorSpaceCreateDeviceRGB(),
    36            bitmapInfo: bitmapInfo.rawValue
    37        )
    38
    39        // 検出結果を元に顔の表示位置に対して処理を実行
    40        faceObservations
    41            // 表示位置情報(CGRect)に変換(※1)
    42            .compactMap { $0.boundingBox.converted(to: CGSize(width: width, height: height)) }
    43            // 検出した顔ごとに猫の顔の画像を描画
    44            .forEach {
    45                guard let catFaceCgImage = catFaceCgImage else { return }
    46                context?.draw(catFaceCgImage, in: $0)
    47            }
    48
    49        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
    50
    51        guard let imageRef = context?.makeImage() else { return nil }
    52
    53        return UIImage(cgImage: imageRef, scale: 1.0, orientation: UIImage.Orientation.up)
    54    }

    ※1 Extension

    ※1では、下記のような Extension を作り、CGRect を取得しています。

    1import AVFoundation
    2
    3extension CGRect {
    4    func converted(to size: CGSize) -> CGRect {
    5        return CGRect(
    6            x: self.minX * size.width,
    7            y: self.minY * size.height,
    8            width: self.width * size.width,
    9            height: self.height * size.height
    10        )
    11    }
    12}

    バインディング

    最後に、これまで実装した処理を使い、初期化処理の中で Input と Output のバインディングをします。

    これで、ViewModel の実装は完了です!

    1    init() {
    2        (省略)
    3
    4        captureOutputTrigger
    5            // 画像データを設定
    6            .map { [weak self] in self?.sampleBuffer = $0 }
    7            // 顔検出処理
    8            .flatMapLatest { return self.detectFace() }
    9            // 画像の再生成
    10            .map { [weak self] in self?.regenerationImage($0) }
    11            // 検出結果画像にバインディング
    12            .bind(to: detectionResultImage)
    13            .disposed(by: disposeBag)
    14    }

    ViewController の実装

    続いて、ViewController の実装を行っていきます。

    初期化処理を実装

    まずは、ViewModel を ViewController に注入するための初期化処理を実装します。

    1import UIKit
    2import AVFoundation
    3import RxSwift
    4import RxCocoa
    5
    6class FaceDetectionViewController: UIViewController {
    7
    8    private var viewModel: FaceDetectionViewModelType
    9
    10    required init(with dependency: FaceDetectionViewModel) {
    11        viewModel = dependency
    12        super.init(nibName: nil, bundle: nil)
    13    }
    14
    15    required init?(coder: NSCoder) {
    16        fatalError("init(coder:) has not been implemented")
    17    }
    18
    19    override func viewDidLoad() {
    20        super.viewDidLoad()
    21    }
    22}

    カメラの設定

    フロントカメラから取得した画像データを Stream で流すようにしています。

    また、カメラのプライバシーアクセスの許可を取得するために、Info.plistPrivacy - Camera Usage Description を設定します。

    (この設定を忘れるとクラッシュします!)

    1class FaceDetectionViewController: UIViewController {
    2
    3    (省略)
    4
    5    @IBOutlet weak var detectionResultImageView: UIImageView!
    6
    7    private var avCaptureSession = AVCaptureSession()
    8    private var videoDevice: AVCaptureDevice?
    9
    10    // カメラの出力結果を流すためのStream
    11    private let capturedOutputStream = PublishSubject<CMSampleBuffer>()
    12
    13    (省略)
    14
    15    override func viewDidLoad() {
    16        super.viewDidLoad()
    17
    18        setupVideoProcessing()
    19    }
    20
    21    private func onViewDidDisappear() {
    22        avCaptureSession.stopRunning()
    23    }
    24
    25    private func setupVideoProcessing() {
    26        avCaptureSession.sessionPreset = .photo
    27
    28        // AVCaptureSession#addInput
    29        videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
    30        guard let videoDevice = videoDevice else { return }
    31        guard let deviceInput = try? AVCaptureDeviceInput(device: videoDevice) else { return }
    32        avCaptureSession.addInput(deviceInput)
    33
    34        // AVCaptureSession#addOutput
    35        let videoDataOutput = AVCaptureVideoDataOutput()
    36        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32BGRA)]
    37        videoDataOutput.setSampleBufferDelegate(self, queue: .global())
    38        avCaptureSession.addOutput(videoDataOutput)
    39
    40        avCaptureSession.startRunning()
    41    }
    42}
    43
    44extension FaceDetectionViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    45
    46    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    47        // カメラの出力結果を流す
    48        capturedOutputStream.onNext(sampleBuffer)
    49        connection.videoOrientation = .portrait
    50    }
    51}

    バインディング

    次に、ViewModel とのバインディングを行います。

    1    private let disposeBag = DisposeBag()
    2
    3    (省略)
    4
    5    override func viewDidLoad() {
    6        super.viewDidLoad()
    7
    8        bind()
    9        setupVideoProcessing()
    10    }
    11
    12    private func bind() {
    13        // input
    14        capturedOutputStream
    15            .bind(to: viewModel.input.captureOutputTrigger)
    16            .disposed(by: disposeBag)
    17
    18        // output
    19        viewModel.output.detectionResultImage
    20            .bind(to: detectionResultImageView.rx.image)
    21            .disposed(by: disposeBag)
    22    }

    ViewModel を注入し、ViewController を設定

    最後に、SceneDelegate 内で ViewController に ViewModel を注入し、rootViewController に先程作った ViewController を設定します。

    お疲れさまでした、これで実装完了です!

    1import UIKit
    2
    3class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    4
    5    var window: UIWindow?
    6
    7    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    8
    9        guard let windowScene = scene as? UIWindowScene else { return }
    10        let viewModel = FaceDetectionViewModel()
    11        let vc = FaceDetectionViewController(with: viewModel)
    12
    13        let window = UIWindow(windowScene: windowScene)
    14        window.rootViewController = vc
    15        window.makeKeyAndVisible()
    16        self.window = window
    17    }
    18}

    動作確認

    さっそく動作確認してみましょう!

    ビルドして、実機を起動してみた結果…

    顔検出結果gif

    無事、リアルタイムで顔を検出し、検出された顔に猫のイラストを重ねることができました!

    さいごに

    今回は、Vision Framework を使って「リアルタイム顔検出アプリ」を作ってみました。

    同じ要領で、顔部分にモザイクをかけたりなんかもできそうですね!

    AWS 等のクラウドサービスでも様々な顔認識サービスを提供していますが、Vision はネットワークを経由せず、ローカル内で完結するため、場所や時間を問わず使えるのが大きなメリットです。

    気になった方は、ぜひ試してみてください!

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
    featureImg2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...
    featureImg2020.08.14スマホ技術 特集Android開発Android開発をJavaからKotlinへ変えていくためのお勉強DelegatedPropert...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background