iOS で firebase-MLKit の顔検出を使う

# はじめに
[firebase](https://firebase.google.com/?gclid=CjwKCAjw98rpBRAuEiwALmo-yjWdeQD-87_4GvuOV48UYdAyvOCRDg0-eS5c8_fVBOL6LqeuG5cRixoCnuEQAvD_BwE)は数年前にGoogleが買収したBaaS (Backend as a Service) プロダクトです。バックエンド構築に関しAWS等に比べ柔軟性は劣るものの、

- 圧倒的に作業量を減らすことができる
- ユーザー認証システムが簡単に作れる
- gmailなどでも使われているNon-SQLデータベースが簡単に使える
- ログ取得してgoogle analyticsと密な連携
- Googleの機械学習リソースが簡単に活用できる

などの恩恵が得られるため、個人開発においては欠かせないサービスの一つと言って良いのではと思います。

#### 目次
この記事は以下のような構成です。

- MLKitって？
- iOSにfirebaseを導入
- 顔認識の実行


# MLKitについて
MLKitはfirebaseの機能の一部であり、以下のようなことが可能になります。

◆ firebaseで用意された学習済モデルの実行
- 顔認識やバーコード認識、ORMなど基本的な機能は揃っている。
- 端末にインストールもしくはクラウドで実行のいずれも可能。前者は基本無料。

◆ 自作のtensorflow liteモデルの管理
- ユーザー端末に導入するモデルをクラウドで簡単に管理できる。基本無料。
- モデルの更新があればユーザー端末が自動で (端末がwifiに繋がった際にバックグラウンドで) ダウンロード、更新をしてくれる。
- 自作の場合はクラウド実行はできず、サイズは1モデルあたり40MBまで。


### MLKit導入の前に
firebaseの学習済モデルはGoogleが関わっていることもあり高精度です。自前のモデルを構築する前にまずはfirebase提供のモデル ([リンク](https://firebase.google.com/docs/ml-kit?authuser=0)) で解決できないか検討するのが良いと思います。


### 自作モデルホスト時の注意点
tensorflowのバージョンに注意です！😵

- MLKitが対応しているtensorflowのバージョンは必ずしも最新ではない。 
↓
例えばTFは1.13がメジャーリリースされていてもfirebaseが対応していない可能性あり。古いバージョンにおいて未対応のOpsを使ったモデルは実行できません。

- バージョンは後述のpod installで確認するのが手取り早そう。 
↓ 
podfileのtensorflowのバージョンを所望のもの (例えば 1.12.0 から 1.13.1)に変えてコマンドラインでpod installをしてみて、firebaseのインストールの方にエラーが出ればバージョンが対応してないということです。

- GPUのサポートは十分でない 
↓ 
tensorflow liteはまだ (2019/7月時点) iOSへのGPU対応が十分でなく([リンク](https://www.tensorflow.org/lite/performance/gpu#ios_with_xcode))、どうしても高速化が必要な場合はCoreMLなどを選んだ方が良いかもしれません。


# Firebaseをインストール
まずはユーザー登録しましょう。

料金プランは[ここ](https://firebase.google.com/pricing/?hl=ja)で見れます。
無料プランはSparkプランになります。十分すぎるくらい無料枠が充実してます。

### アプリをfirebaseと紐付
一連の流れは[公式](https://firebase.google.com/docs/ios/setup?hl=ja)に書かれています。以下はざっくりとした流れだけ紹介です。 
なお[CocoaPod](https://guides.cocoapods.org/using/getting-started.html#getting-started)がない場合はインストールしておきましょう。

- [コンソール](https://console.firebase.google.com/u/0/)にて「iOS アプリにfirebaseを追加」を選ぶ。
- コンソールにアプリのBundle IDを入力する。
- GoogleService-Info.plistをアプリに追加 
↓ 
「Firebase iOS 構成ファイル」における「GoogleService-Info.plist」をコンソールからダウンロードし、XCodeのアプリのルートフォルダにドラッグ&ドロップ。このとき、構成ファイルを全てのターゲットに追加する。

- PodFileがまだ無い場合はコマンドラインでアプリルートフォルダに移動して`pod init`
- PodFileに以下を記入。

```pod
platform :ios, '11.0'

target '(Your App)' do

  # Pods for Your App
  pod 'Firebase/Core'
  pod 'Firebase/MLVision'
  pod 'Firebase/MLVisionFaceModel'

  # TensorFlowの自作モデル使用の場合は以下も記述。
  # pod 'Firebase/MLModelInterpreter'
  # pod 'TensorFlowLite', '1.12.0'

  # TensorFlowのバージョンが対応しているか確認したい場合は
  # 上記 '1.12.0' を '1.13.1' などに変えて pod install してみて、
  # エラーが出ればそのバージョンのTFは使えないということ。

end
```

- コマンドラインで`pod install`  
↓  
podでライブラリ、すなわち自分とは別のプロジェクトを導入した後は複数プロジェクトを扱うことになるため、単体プロジェクト専用の .xcproject は有効ではありません。かわりに .xcworkspace ファイルをXCodeで開いて編集するようにしましょう。


### アプリ内のコード
基本は AppDelegate.swift に以下のように、たった２行を入れるだけです。

```swift
import UIKit
import Firebase // インポート

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
        -> Bool {
            FirebaseApp.configure() // これを記述。FirebaseコンソールのAnalyticsにログが上がるようになります！
            return true
    }
}
```

### MLKitの既存画像処理モデルを使う
MLKitの顔検出モデルを使ってみたいと思います。 
コンセプトは[こちら](https://firebase.google.com/docs/ml-kit/face-detection-concepts?hl=ja)、パラメータ等は[こちら](https://firebase.google.com/docs/ml-kit/ios/detect-faces?hl=ja)をご覧ください。

以下がコードの例です。

```swift
import AVFoundation
import FirebaseMLVision

class FacialDetector {
    // Firebase 用の Vision クラスを活用する。
    // iOS にも Vision クラスが有りますが、別物(?)だと思います。
    private lazy var vision = Vision.vision()

    // 以下ではAVCaptureDeviceでカメラ画像を使用することを想定。
    // 画像はCVPixelBufferを用いることにします。MLKitでは
    // CMSampleBufferが必要なため、下部に変換コードを書いています。
    func detectFaces(pixelBuffer: CVPixelBuffer, devicePosition: AVCaptureDevice.Position, deviceOrientation: UIInterfaceOrientation) -> Array<VisionFace>? {
        var processingSampleBuffer: CMSampleBuffer
        processingSampleBuffer = retrieveSampleBuffer(pixelBuffer: pixelBuffer)
        let visionImage = VisionImage(buffer: processingSampleBuffer)
        let metadata = VisionImageMetadata()

        // ここの switch 文は状況によって書き換えていただく形かと思います。
        // 要点は meta.orientation に正しく画像の方向を指定することです。
        switch deviceOrientation {
        case .portrait:
            metadata.orientation = devicePosition == .front ? .leftTop : .rightTop
        case .landscapeLeft:
            metadata.orientation = devicePosition == .front ? .topRight : .bottomRight
        case .portraitUpsideDown:
            metadata.orientation = devicePosition == .front ? .rightBottom : .leftBottom
        case .landscapeRight:
            metadata.orientation = devicePosition == .front ? .bottomLeft : .topLeft
        case .unknown:
            metadata.orientation = .leftTop
        @unknown default:
            metadata.orientation = .leftTop
        }
        
        visionImage.metadata = metadata
        let options = VisionFaceDetectorOptions()
        
        // 以下のパラメータは上記のリンクに書かれています。
        options.performanceMode = .accurate
        options.landmarkMode = .all
        options.contourMode = .none
        options.classificationMode = .none

        let faceDetector = self.vision.faceDetector(options: options)
        var detectedFaces: [VisionFace]? = nil
        do {
            detectedFaces = try faceDetector.results(in: visionImage)
        } catch let error {
            print("Failed to detect faces with error: \(error.localizedDescription).")
        }
        
        guard let faces = detectedFaces, !faces.isEmpty else {
            return nil
        }
        return faces
    }

    // CMSampleBuffer を CVPixelBuffer から作る。
    func retrieveSampleBuffer(pixelBuffer: CVImageBuffer) -> CMSampleBuffer {
        var sampleBuffer: CMSampleBuffer?
        var formatDescription: CMFormatDescription?
        var timingInfo = CMSampleTimingInfo()
        
        CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &formatDescription)
        
        CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: formatDescription!, sampleTiming: &timingInfo, sampleBufferOut: &sampleBuffer)
        
        guard let sBuffer = sampleBuffer else{
            abort()
        }
        return sBuffer
    }
```

顔検出後、例えば目の位置を取得したい場合は以下です。

```swift
// VisionPoint クラスを拡張して CGPoint を取得できるようにします。
extension VisionPoint {
    var cgPoint: CGPoint {
        return CGPoint(x: CGFloat(truncating: self.x), y: CGFloat(truncating: self.y))
    }
}

// ここでは [左目, 右目] を、顔検出された人数分Arrayで出力したいと思います。
func getFaceEyes(faces: Array<VisionFace>) -> Array<Array<CGPoint?>> {
    var faceEyes: Array<Array<CGPoint?>> = []
    
    var leftEyePoint: VisionPoint
    var rightEyePoint: VisionPoint
    
    for face in self.selectedFaces {
        var eyesArray: Array<CGPoint?> = []
        
        // Left Eyes
        if let leftEye = face.landmark(ofType: .leftEye) {
            leftEyePoint = leftEye.position
            let leftEyeCGPoint = leftEyePoint.cgPoint
            eyesArray.append(leftEyeCGPoint)
        } else {
            eyesArray.append(nil)
        }
        
        // Right Eyes
        if let rightEye = face.landmark(ofType: .rightEye) {
            rightEyePoint = rightEye.position
            let rightEyeCGPoint = rightEyePoint.cgPoint
            eyesArray.append(rightEyeCGPoint)
        } else {
            eyesArray.append(nil)
        }
        
        faceEyes.append(eyesArray)
    }
    return faceEyes
}
```

#### 備考
自作モデルを使う方法については今回は割愛しますが、ご要望があれば記事にしたいと思っています。 
詳しくは公式の[こちら](https://firebase.google.com/docs/ml-kit/ios/use-custom-models?hl=ja)をご参照ください。


# おわりに
いかがでしたでしょうか？

ご参考になれば幸いです！
改善方法やご意見などあれば、どしどしコメント下さい！