【iOS】Vision Frameworkを使ってリアルタイム顔検出アプリを作ってみた
IT技術
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 | 顔検出結果画像 |
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.plist にPrivacy - 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}
動作確認
さっそく動作確認してみましょう!
ビルドして、実機を起動してみた結果…
無事、リアルタイムで顔を検出し、検出された顔に猫のイラストを重ねることができました!
さいごに
今回は、Vision Framework を使って「リアルタイム顔検出アプリ」を作ってみました。
同じ要領で、顔部分にモザイクをかけたりなんかもできそうですね!
AWS 等のクラウドサービスでも様々な顔認識サービスを提供していますが、Vision はネットワークを経由せず、ローカル内で完結するため、場所や時間を問わず使えるのが大きなメリットです。
気になった方は、ぜひ試してみてください!
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...
2020.08.14スマホ技術 特集Android開発Android開発をJavaからKotlinへ変えていくためのお勉強DelegatedPropert...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit