前書き
少し前ですがFirebaseのハッカソンに参加してきました。
Firebaseは普段からごりごり業務で使っていて、使うことにそこまでは抵抗はない人って多いとは思いますが、、、
たくさん機能がある中でも、実際にサービスで使う機能って特定のものに限られますよね、、?
(Analytics, Crashlytics, RemoteConfig, ABTesting, Messaging, Predictions ...etc)
、、、と思っていて、せっかくハッカソンだし普段使わない機能を使いたい!というtryをしてみた結果、、
Firebaseの**MLKitを使ってなんかしたい!**にたどり着きました。笑
今回はその過程で学んだものをアウトプットしたいと思います。
実現したこと
内容
①MLKitを使って顔認識させ、UI上に結果を反映
具体的には、顔に連動して左上のいらすとやも変わるようになっています!
1日(8時間)というハッカソンの限られた時間だったので、3パターンまでしか用意できませんでしたが、、
**「通常」 「寝ている」 「笑っている」**の3パターンを検知しています。
たまに感度が悪い時もありますが、、、
かなり速い速度で顔を認識してくれます。
②認識させたデータをグラフにして可視化
MLKitで取得したデータをアプリで加工し、Web上でグラフとして可視化しています。
今回は**「笑っている度合い」「右目の空き具合」「左目の空き具合」**の指数をデータとして送っています。
データをわかりやすくするために1~100になるように丸め込んでいます。
横は時間軸になっており、縦はその時の数値になります。
数値の意味は以下になります。
- 笑顔の指数:100に近ければ笑っており、0に近ければ笑ってない
- 目の指数:100に近ければ開いており、0に近ければ閉じている
仕組み
行っていることは至ってシンプルです。
①カメラを通してMLKitで顔認識
②認識したデータをRealtime Databaseに送る
③Web上でグラフ化する
実装
準備
①Firebaseのセットアップ
MLKitを行うため用に、firebaseのコンソールからプロジェクトの作成
作成後、設定(歯車アイコン)からダウンロードできる**「GoogleService-Info.plist」**をアプリに入れる
雑になりましたが、詳しくはこの辺のセットアップ解説を参考にしてください
②Firebaseをアプリにインポート
MLkitとRealtime Databaseを使用するために、以下の対応するFirebaseライブラリをインストールしてください。
(Podじゃなくてもよいですが、自分でバイナリを用意するのは面倒かと思うので)
pod 'Firebase'
pod 'Firebase/Core'
pod 'Firebase/MLVision'
pod 'Firebase/MLVisionFaceModel'
pod 'Firebase/Database'
その後、AppDelegate
にFirebaseApp.configure()
を追加するのを忘れないでください。
参考として、公式のリファレンスをみてください。
実装
今回は実装に必要なものを洗い出した結果、3つ必要だったので管理クラスもそれぞれ分けて実装しました。
互いの責務が別れるようになっています。
①動画の読み取り
ここが調査に手間がかかりました、、。
「MLKitで顔認識やってみた」系の記事は、画像の認識しかやっている人がおらず、、
結論から言うと、動画データを細切れの画像データとして取得して、それを顔認識させます。
では、さっそく動画のデータを取得できるようにします。
今回はCaptureVideoManager
というシングルトンクラスで管理できるように切り出しました。
1.結果の分岐実装
動画を読み込んだ際の結果をenumで定義しておきます。
enum CaptureVideoResult {
case success(_ manager: CaptureVideoManager)
case failure
}
2.管理クラスの作成
import AVKit
final class CaptureVideoManager: NSObject {
static let shared = CaptureVideoManager()
private override init() {}
weak var delegate: CaptureVideoManagerDelegate?
lazy var videoLayer: AVCaptureVideoPreviewLayer = {
return AVCaptureVideoPreviewLayer(session: captureSession)
}()
private let captureSession = AVCaptureSession()
private let videoDevice = AVCaptureDevice.default(for: .video)
private lazy var videoOutput: AVCaptureVideoDataOutput = {
let output = AVCaptureVideoDataOutput()
let queue = DispatchQueue(label: "videoOutput", attributes: .concurrent)
output.setSampleBufferDelegate(self, queue: queue)
output.alwaysDiscardsLateVideoFrames = true
return output
}()
}
動画を画面上に表示するために必要なプロパティを定義しました。
今回は動画の映像を分析するため、オーディオ設定はしていないので、音を拾いたい場合は別途実装が必要です。
AVCaptureVideoPreviewLayer
は、スマホの画面上に撮影している動画を表示するために使用します。
他に必要なメソッドを追加していきます。
extension CaptureVideoManager {
// カメラを使用するための許諾
func requestPermission(completion: @escaping (CaptureVideoResult) -> Void) {
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
guard let self = self else { return }
completion((granted ? .success(self) : .failure))
}
}
// カメラ映像を接続できるようにセットアップ
func initialSession() {
guard let vDevice = videoDevice, let vInput = try? AVCaptureDeviceInput(device: vDevice) else {
debugPrint("error: non videoDevice")
return
}
captureSession.addInput(vInput)
captureSession.addOutput(videoOutput)
}
// カメラ映像の接続開始
func startRecording() {
captureSession.startRunning()
}
// カメラ映像の接続停止
func stopRecording() {
captureSession.stopRunning()
}
}
今回は最低限の実装しかしていませんが、フレームレートの設定など動画の設定は、initialSession
内で行なっているプロパティ操作部分で行うことができます。
3.動画データの受け取り
受け取った動画データを呼び出し元に返すためのDelegateを作成します。
protocol CaptureVideoManagerDelegate: class {
func captureOutput(didOutput buffer: CMSampleBuffer)
}
CMSampleBuffer
というのがMLKitで分析するために必要な型になります。
AVCaptureVideoDataOutputSampleBufferDelegate
を使用して動画のデータを受け取ります。
extension CaptureVideoManager: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
delegate?.captureOutput(didOutput: sampleBuffer)
}
}
先ほど作成したDelegateを呼んであげることで、呼びだし元にデータを渡すことができます。
AVCaptureOutput
とAVCaptureConnection
は今回は必要ないので一旦渡していません。
②顔認識させる
MLKitを使って顔を認識させます。
では、コードを記載していきます。
こちらもシングルトンのコードで管理クラスとして独立したものを作っていきます。
import AVFoundation
import FirebaseMLVision
final class FacialDetector {
static let shared = FacialDetector()
private init() {}
private lazy var vision = Vision.vision()
private let detectorOptions: VisionFaceDetectorOptions = {
/// 検出できるデータの設定を行う
let options = VisionFaceDetectorOptions()
options.performanceMode = .accurate
options.landmarkMode = .all
options.contourMode = .none
options.classificationMode = .all
options.isTrackingEnabled = true
return options
}()
}
MLKitのVisionを使用していることに注意してください。
(元からあるVision.frameworkのAPIではないということ)
必要なメソッドを追加していきます。
extension FacialDetector {
// 顔を認識させる
func detectFaces(buffer: CMSampleBuffer,
orientation: UIDeviceOrientation,
position: AVCaptureDevice.Position) -> [VisionFace]? {
let metadata = VisionImageMetadata()
metadata.orientation = imageOrientation(orientation: orientation, position: position)
let visionImage = VisionImage(buffer: buffer)
visionImage.metadata = metadata
let faceDetector = vision.faceDetector(options: detectorOptions)
guard let faces = try? faceDetector.results(in: visionImage), !faces.isEmpty else {
debugPrint("Failed to detect faces")
return nil
}
return faces
}
// 端末の向きから画像の向きを返す
func imageOrientation(orientation: UIDeviceOrientation,
position: AVCaptureDevice.Position) -> VisionDetectorImageOrientation {
switch orientation {
case .portrait:
return position == .front ? .leftTop : .rightTop
case .landscapeLeft:
return position == .front ? .bottomLeft : .topLeft
case .portraitUpsideDown:
return position == .front ? .rightBottom : .leftBottom
case .landscapeRight:
return position == .front ? .topRight : .bottomRight
case .faceDown, .faceUp, .unknown:
return .leftTop
}
}
// 顔のデータから必要な情報を取り出して配列として返す
func analysis(from faces: [VisionFace]) -> [String: Any] {
var params = [String: Int]()
for face in faces {
if face.hasSmilingProbability {
let smileProb = face.smilingProbability * 100
params["smileProb"] = Int(face.smilingProbability * 100)
}
if face.hasLeftEyeOpenProbability {
let leftEyeOpenProb = face.leftEyeOpenProbability * 100
params["leftEyeOpenProb"] = Int(leftEyeOpenProb)
}
if face.hasRightEyeOpenProbability {
let rightEyeOpenProb = face.rightEyeOpenProbability * 100
params["rightEyeOpenProb"] = Int(rightEyeOpenProb)
}
if face.hasTrackingID {
let trackingId = face.trackingID
params["trackingId"] = trackingId
}
}
return params
}
}
先ほども出てきたCMSampleBuffer
という聞いたこともない型に最初は戸惑っていましたが、、
MLKitを使用する際に、このCMSampleBuffer
が対応していた点は幸運でした。
そのおかげで、画像や動画のコンバートコードをかかずにすみました。
端末の向きから画像の向きを返すimageOrientation
ですが、こちらドキュメントのコードをそのまま使用すると横にした時にうまく動かず、、
ハッカソンで時間がなかったのでこちらの中身は編集していません。現状の上記のコードだと縦の画像(縦のカメラ)しかうまく認識しないので注意してください、
③Realtime Databaseへデータを書き込む
Realtime Databaseへのアクセスを一元管理するため、シングルトンのマネージャークラスを作成しています。
また、拡張性を持たせるためにKeyをenumで管理するようにしています。
Key(DBの子要素の名前)を指定することで、Realtime Database上の操作するデータを指定できます。
import Firebase
final class ConnectionManager {
enum Keys: String, CaseIterable {
case faces
}
static let shared = ConnectionManager()
private init() {}
private let reference = Database.database().reference()
/// Realtime Database へ書き込み
func write(by key: Keys, params: [String: Any]) {
let userReference = reference.child(key.rawValue)
userReference.childByAutoId().setValue(params)
}
/// Realtime Database の中身をリセット
func remove(by key: Keys) {
let userReference = reference.child(key.rawValue)
userReference.removeValue()
}
/// Realtime Database を監視する
func connect(by key: Keys) {
let userReference = reference.child(key.rawValue)
userReference.observe(.childAdded) { snap in
// do something
}
}
/// Realtime Database の監視を解除
func disconnect(by key: Keys) {
let userReference = reference.child(key.rawValue)
userReference.removeAllObservers()
}
}
データが書き込まれることによって、それをWEBが検知してグラフ化しています。
今回はRealtime Databaseの監視まわりは必要ないですが、必要に応じて作りたい物がある人は拡張してください。
RealtimeDatabaseの中身
配下は、実際に送られている物になります。
先のコードにもありましたが、faces
をkeyとして指定していたので、その下にデータが作られる様になっています。
faces配下の謎の文字列キーは、firebase側が勝手につけているユニークなものです。
グラフ化するにあたり、timestampとcounterはこちらで付与しています。
動画を細切れにした画像で送っているため、順番をつけないとグラフの横軸を作れないためです。
(コード上には、timestampとcounterを付与する部分は省いています。)
timestampはDateFormatterのextensionを作成して現在の時間を取得しています。
extension DateFormatter {
var timestamp: String {
let timeInterval = NSDate().timeIntervalSince1970
let myTimeInterval = TimeInterval(timeInterval)
let time = NSDate(timeIntervalSince1970: TimeInterval(myTimeInterval))
dateFormat = "yyyyMMddHHmmss"
return self.string(from: time as Date)
}
}
MLKitではtrakingIDというIDによって、顔の識別に使用しています。
複数の顔を読み込み可能です。
実際複数の顔を読みこめましたが、画面から顔が出て再び入ると新しいIDになってしまうなどの問題があります、、。
④動作
ここまで
① 動画データの読み込み(CaptureVideoManager)
② 顔を認識させる(FacialDetector)
③ RealtimeDatabaseへの書き込み(ConnectionManager)
の3つを作成してきました。
最後に呼び出し元のコントローラを記載しておきます。
import UIKit
import AVKit
final class MonitorViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
initialView()
requestPermission()
}
}
extension MonitorViewController: CaptureVideoManagerDelegate {
/// 動画データを受け取り、顔を認識させる処理に渡す
func captureOutput(didOutput buffer: CMSampleBuffer) {
detectFaces(from: buffer)
}
}
private extension MonitorViewController {
/// 動画の画面をiOS上に描画
func initialView() {
let videoLayer = CaptureVideoManager.shared.videoLayer
videoLayer.frame = self.view.bounds
videoLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(videoLayer)
}
/// 動画の許諾を確認。成功ならdelegateをセットして動画接続の開始。
func requestPermission() {
CaptureVideoManager.shared.requestPermission { result in
switch result {
case .success(let manager):
manager.delegate = self
manager.initialSession()
manager.startRecording()
case .failure:
// do some error handling
break
}
}
}
/// MLKitで顔認識とRealtimeDatabaseへの書き込み
func detectFaces(from buffer: CMSampleBuffer) {
let result = FacialDetector.shared.detectFaces(
buffer: buffer,
orientation: UIDevice.current.orientation,
position: AVCaptureDevice.Position.back
)
guard let faces = result else {
debugPrint("----- error detectFaces ----")
return
}
let params = FacialDetector.shared.analysis(from: faces)
guard !params.isEmpty else {
debugPrint("----- error params is empty ----")
return
}
ConnectionManager.shared.write(by: .faces, params: params)
}
}
責務を切り出しているので、コントローラはだいぶスッキリしていると思います。
一部コードを省略しているので、動かない場合や不具合があった場合は下記にあるリポジトリから動作するコードを見ていただけると幸いです。
後書き
MLKit、、正直感動しましたね笑
特に顔認識ソフトは何千万とかで販売されてたりするので、本来なら機械学習が必要な部分を無償提供してくれていると思うと、本当にすごいことだと思います。
その他
リポジトリ
こちらに実際に動くものを設置しておきます。
注意点として、自身の「GoogleService-Info.plist」をアプリに入れるのを忘れないでください。
じゃないと動作しません。
参考文献
- 公式リファレンス
- iOS で firebase-MLKit の顔検出を使う
- Firebase MLKitを用いて検出した顔の輪郭を描画する
- [iOS] AVFoundation(AVCaptureVideoDataOutput)で連写カメラを作ってみた
- Swift4で動画を撮影・保存する
- swift3/swift4/swift5でリアルタイム顔認識をする方法