Firebase MLKitを用いて検出した顔の輪郭を描画する

# はじめに
この記事は[ユニークビジョン株式会社 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/uniquevision)の3日目の記事です。

みんな大好きFirebaseでも機械学習を利用できるMLKitが今年導入されました(ベータ版)。
機械学習に深い知見がなくても、簡単に機械学習を利用できる魅了的なサービスなので使ってみたいと思います。

# 概要
MLKitのAPIは端末上で動作するものと、クラウドを利用する2種類があります。
端末上では通信が発生しないのでリアルタイムでの処理が可能になりますが、精度が落ちるようです。
クラウドを利用したAPIは、GCPのCloud Vision APIが使われており[有料](https://cloud.google.com/vision/pricing)です。（ざっくりと月1000枚の画像までは無料です）
その分、非常に正確に検出が可能です。

利用可能な機能は以下の通りです。

|機能|端末  |クラウド  |
|:---|:---|:---|
|テキスト認識（OCR） 　　　　　|○  |○  |
|顔検出  　　　　　　　　　　　　　　　　　　　　　　　　　　　　|○  |○  |
|バーコード スキャン 　　　　　　　　　　 |○  |×  |
|画像のラベル付け  　　　　　　　　　　　　|×  |○  |
|ロゴ認識  　　　　　　　　　　　　　　　　　　　　　　　　　|×  |○  |
|ランドマーク認識  　　　　　　　　　　　　　　|×  |○  |
|不適切なコンテンツの検出  |×  |○  |
|類似画像の検索  　　　　　　　　　　　　　　|×  |○  |
|カスタムモデルの推論  　　　　　　|○  |×  |

詳細は[こちら](https://firebase.google.com/docs/ml-kit/)	

今回は端末で顔検出を利用してみようと思います。

# Firebaseを導入
[公式ドキュメント](https://firebase.google.com/docs/ios/setup)に沿ってプロジェクトを作成します

## 
``` :Podfile
pod 'Firebase/Core'
pod 'Firebase/MLVision'
pod 'Firebase/MLVisionFaceModel'
```
上記を**Podfile**に記述して`pod install`

# 実装
[チュートリアル](https://firebase.google.com/docs/ml-kit/ios/detect-faces)を参考にして進めます。
最後に[全コード](#全コード)を載せています。

## 顔を検出するオプションを設定
``` swift
import FirebaseMLVision

class Options: VisionFaceDetectorOptions {
    override init() {
        super.init()
        // 精度を重視
        self.performanceMode = .accurate
        // ランドマークを全て検出する
        self.landmarkMode = .all
        // 輪郭を全て検出する
        self.contourMode = .all
        // 分類を全て検出する
        self.classificationMode = .all
    }
}
```

## FaceDetectorを作成
``` swift
let vision = Vision.vision()
let options = Options()
lazy var faceDetector = vision.faceDetector(options: options)
```

## 顔を検出する
``` swift
faceDetector.process(image) { (visionFacesOrNil, errorOrNil) in
    if let error = errorOrNil {
        print("Error: \(error.localizedDescription)")
    }
    guard let visionFaces = visionFacesOrNil, !visionFaces.isEmpty else { return }
        visionFaces.forEach { self.analyze(face: $0) }
    }
```
**faceDetector.process(_: VisionImage, completion: VisionFaceDetectionCallback)**で顔の検出が実行されます。
実行結果はコールバックの第一引数に含まれているの解析していきます。

## 顔を解析する
``` swift
func analyze(face: VisionFace) {
    // オイラーY
    if face.hasHeadEulerAngleY {
        let rotY = face.headEulerAngleY
        print("rotY: \(rotY)")
    }
    // オイラーZ
    if face.hasHeadEulerAngleZ {
        let rotZ = face.headEulerAngleZ
        print("rotZ: \(rotZ)")
    }
    // 笑顔の度合い
    if face.hasSmilingProbability {
        let smileProb = face.smilingProbability
        print("smileProb: \(smileProb)")
    }
    // 右目の開き度合い
    if face.hasRightEyeOpenProbability {
        let rightEyeOpenProb = face.rightEyeOpenProbability
        print("rightEyeOpenProb: \(rightEyeOpenProb)")
    }
    // 左目の開き度合い
    if face.hasLeftEyeOpenProbability {
        let leftEyeOpenProb = face.leftEyeOpenProbability
        print("leftEyeOpenProb: \(leftEyeOpenProb)")
    }
}
```
オイラーY,Zは[ドキュメント](https://firebase.google.com/docs/ml-kit/face-detection-concepts)によると
> オイラー X: オイラー X の角度が正である顔は上向きです。
オイラー Y: オイラー Y の角度が正の場合、顔の右側がカメラに向いています。
オイラー Z: オイラー Z の角度が正である顔は、カメラを基準にして反時計回りに回転しています。

だそうです。
つまり顔の向きが取得できるということですね。

ちなみにオイラーXはサポートされていないそうです。

## 輪郭を取得する
``` swift
func analyzeFaceContourWithDraw(_ face: VisionFace) {
    // 左目
    if let leftEyeContour = face.contour(ofType: .leftEye) {
        let leftEyePoints = leftEyeContour.points
        print("leftEyePoints: \(leftEyePoints)")
    }
    // 右目
    if let rightEyeContour = face.contour(ofType: .rightEye) {
        let rightEyePoints = rightEyeContour.points
        print("leftEyePoints: \(rightEyePoints)")
    }
    // 上記のように全パーツ取得
}
```

**face.contour(ofType: FaceContourType)**から顔のランドマークが取得できます。
**FaceContourType**で検出可能なランドマークが定義されているので、全て取得していきます（オイラーYの角度によって検出できるパーツが異なります）

## 動かしてみる
こちらの画像で検証してみましょう。
ブログ等でよく見かけるフリー素材モデル河村友歌さんです

![IMARI20160806343807_TP_V.jpg](https://qiita-image-store.s3.amazonaws.com/0/224246/cfca048a-eda2-da8c-56bd-ea49240acaec.jpeg)


結果は以下の通りです。

```
rotY: -5.896273612976074
rotZ: -7.039237976074219
smileProb: 0.9863819479942322
rightEyeOpenProb: 0.9991688132286072
leftEyeOpenProb: 0.9965924024581909
leftEyePoints: [(689.487305, 437.164032), (692.902649, 435.063446), (698.461853, 432.188354), (708.168884, 428.382141), (724.492371, 427.688843), (740.619446, 431.622620), (755.102417, 440.750305), (764.392090, 451.884583), (769.080994, 457.946686), (766.180664, 458.290558), (758.214600, 457.760651), (744.420898, 457.433929), (729.664978, 455.905182), (714.161255, 452.206573), (703.468933, 447.821594), (695.964844, 443.272888)]
rightEyePoints: [(871.931641, 472.027740), (878.040710, 467.396118), (891.075439, 459.303223), (908.470276, 454.507172), (925.604736, 455.452362), (941.898315, 460.703308), (951.543640, 467.327423), (956.729675, 471.744232), (960.458557, 474.872192), (951.184082, 478.931152), (941.447632, 481.273956), (928.935791, 482.550262), (912.138367, 481.640594), (896.953003, 478.750977), (882.878235, 475.066528), (874.777954, 473.278748)]
```

> 分類は確実性の値として表され、顔の特徴が存在する確かさを示します。たとえば、笑顔の分類の値が 0.7 以上であれば、かなりの可能性で人が微笑んでいることを示します。

素晴らしい笑顔です。

## 画像に輪郭を描く
数字だけ見てもピンとこないので[こんな感じ](https://firebase.google.com/docs/ml-kit/images/examples/face_contours.svg)で輪郭を描いてみます。

ランドマークの各**Points**は画像内に点として表されますので、それらを繋いでいくと輪郭が描かれます。

まず輪郭を表す構造体を定義します。

``` swift
struct Contour {
    let type: FaceContourType
    let points: [VisionPoint]
    let color: UIColor
}
```

続いて`VisionPoint`で表される点を`CGPoint`に変換する関数を用意します。

``` swift
extension VisionPoint {
    var cgPoint: CGPoint {
        return CGPoint(x: CGFloat(truncating: self.x), y: CGFloat(truncating: self.y))
    }
}
```

先程の`analyzeFaceContourWithDraw(_: VisionFace)`に少し手を加えて`Contour`を作成してまわります。

``` swift
func analyzeFaceContourWithDraw(_ face: VisionFace) {
    var contours: [Contour] = []
    // 左目
    if let leftEyeContour = face.contour(ofType: .leftEye) {
        let leftEyePoints = leftEyeContour.points
        contours.append(Contour(type: .leftEye, points: leftEyePoints, color: .green))
        print("leftEyePoints: \(leftEyePoints)")
    }
    // 右目
    if let rightEyeContour = face.contour(ofType: .rightEye) {
        let rightEyePoints = rightEyeContour.points
        contours.append(Contour(type: .rightEye, points: rightEyePoints, color: .red))
        print("leftEyePoints: \(rightEyePoints)")
    }
    // 上記のように全パーツ取得
}
```


`Contour`から点を取得して`UIImage`を描画します。

``` swift
func draw(contours: [Contour]) -> UIImage? {
    let size = image.size
    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
    let context = UIGraphicsGetCurrentContext()
    image.draw(in: CGRect(origin: .zero, size: size))
    contours.forEach { contour in
        guard let firstPoint = contour.points.first else { return }
        context?.setLineWidth(1.0)
        context?.setStrokeColor(contour.color.cgColor)
        context?.move(to: firstPoint.cgPoint)
        contour.points.forEach { point in
            context?.addLine(to: point.cgPoint)
        }
        context?.strokePath()
    }
    let drawnImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return drawnImage
}
```

生成した`UIImage`を`View`に反映させます

``` swift
self.imageView.image = draw(contours: contours)
```

結果がこちらです。
<img width="375" alt="Simulator Screen Shot - iPhone XS - 2018-12-04 at 21.21.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/224246/0f893277-ac7f-5205-b9c8-012883db25a2.png">


色合いが気持ち悪いですが、各ランドマークの輪郭が描画されました。
かなり正確に顔を認識していることがわかります。

# まとめ
Firebaseを使用して簡単に顔を認識することができました。
今回は学習済みのモデルを使用しましたが、TensorFlow Liteを利用して自身で作成したモデルを使うこともできます。

MLKitはまだベータ版なので今後に期待できそうです。

明日は@Takayuki_Nakanoさんです。

# 参考
https://firebase.google.com/docs/ml-kit/ios/detect-faces
https://dev.classmethod.jp/smartphone/iphone/ios-11-vision/

# 全コード

``` swift

import UIKit
import FirebaseMLVision

final class ViewController: UIViewController {
    
    @IBOutlet private weak var imageView: UIImageView!
    @IBOutlet private weak var smileProbLabel: UILabel!
    @IBOutlet private weak var rightEyeOpenProbLabel: UILabel!
    @IBOutlet private weak var leftEyeOpenProbLabel: UILabel!
    @IBOutlet private weak var rotateYLabel: UILabel!
    @IBOutlet private weak var rotateZLabel: UILabel!
    
    private let vision = Vision.vision()
    private let options = Options()
    private lazy var faceDetector = vision.faceDetector(options: options)
    
    private let image = UIImage(named:"IMARI20160806343807_TP_V")!
    private lazy var drawer = Drawer(image: image)

    override func viewDidLoad() {
        super.viewDidLoad()
        self.detect(image: VisionImage(image: image))
    }
    
    private func detect(image: VisionImage) {
        faceDetector.process(image) { (visionFacesOrNil, errorOrNil) in
            if let error = errorOrNil {
                print("Error: \(error.localizedDescription)")
            }
            guard let visionFaces = visionFacesOrNil, !visionFaces.isEmpty else { return }
            visionFaces.forEach { self.analyze(face: $0) }
        }
    }

    private func analyze(face: VisionFace) {
        if face.hasHeadEulerAngleY {
            let rotY = face.headEulerAngleY
            self.rotateYLabel.text = rotY.description
            print("rotY: \(rotY)")
        }
        if face.hasHeadEulerAngleZ {
            let rotZ = face.headEulerAngleZ
            self.rotateZLabel.text = rotZ.description
            print("rotZ: \(rotZ)")
        }
        if face.hasSmilingProbability {
            let smileProb = face.smilingProbability
            self.smileProbLabel.text = smileProb.description
            print("smileProb: \(smileProb)")
        }
        if face.hasRightEyeOpenProbability {
            let rightEyeOpenProb = face.rightEyeOpenProbability
            self.rightEyeOpenProbLabel.text = rightEyeOpenProb.description
            print("rightEyeOpenProb: \(rightEyeOpenProb)")
        }
        if face.hasLeftEyeOpenProbability {
            let leftEyeOpenProb = face.leftEyeOpenProbability
            self.leftEyeOpenProbLabel.text = leftEyeOpenProb.description
            print("leftEyeOpenProb: \(leftEyeOpenProb)")
        }
        self.analyzeFaceContourWithDraw(face)
    }
    
    private func analyzeFaceContourWithDraw(_ face: VisionFace) {
        var contours: [Contour] = []
        // 左目
        if let leftEyeContour = face.contour(ofType: .leftEye) {
            let leftEyePoints = leftEyeContour.points
            contours.append(Contour(type: .leftEye, points: leftEyePoints, color: .green))
            print("leftEyePoints: \(leftEyePoints)")
        }
        // 左眉下
        if let leftEyebrowBottom = face.contour(ofType: .leftEyebrowBottom) {
            let leftEyebrowBottomPoints = leftEyebrowBottom.points
            contours.append(Contour(type: .leftEyebrowBottom, points: leftEyebrowBottomPoints, color: .green))
            print("leftEyebrowBottomPoints: \(leftEyebrowBottomPoints)")
        }
        // 左眉上
        if let leftEyebrowTop = face.contour(ofType: .leftEyebrowTop) {
            let leftEyebrowTopPoints = leftEyebrowTop.points
            contours.append(Contour(type: .leftEyebrowTop, points: leftEyebrowTopPoints, color: .green))
            print("leftEyebrowTopPoints: \(leftEyebrowTopPoints)")
        }
        // 右目
        if let rightEyeContour = face.contour(ofType: .rightEye) {
            let rightEyePoints = rightEyeContour.points
            contours.append(Contour(type: .rightEye, points: rightEyePoints, color: .red))
            print("rightEyePoints: \(rightEyePoints)")
        }
        // 右眉下
        if let rightEyebrowBottomContour = face.contour(ofType: .rightEyebrowBottom) {
            let rightEyebrowBottomPoints = rightEyebrowBottomContour.points
            contours.append(Contour(type: .rightEyebrowBottom, points: rightEyebrowBottomPoints, color: .red))
            print("rightEyebrowBottomPoints: \(rightEyebrowBottomPoints)")
        }
        // 右眉上
        if let rightEyebrowTopContour = face.contour(ofType: .rightEyebrowTop) {
            let rightEyebrowTopPoints = rightEyebrowTopContour.points
            contours.append(Contour(type: .rightEyebrowTop, points: rightEyebrowTopPoints, color: .red))
            print("rightEyebrowTopPoints: \(rightEyebrowTopPoints)")
        }
        // 鼻筋
        if let noseBridgeContour = face.contour(ofType: .noseBridge) {
            let noseBridgePoints = noseBridgeContour.points
            contours.append(Contour(type: .noseBridge, points: noseBridgePoints, color: .purple))
            print("noseBridgePoints: \(noseBridgePoints)")
        }
        // 鼻下
        if let rightEyebrowTopContour = face.contour(ofType: .noseBottom) {
            let rightEyebrowTopPoints = rightEyebrowTopContour.points
            contours.append(Contour(type: .rightEyebrowTop, points: rightEyebrowTopPoints, color: .purple))
            print("rightEyebrowTopPoints: \(rightEyebrowTopPoints)")
        }
        // 上唇上
        if let upperLipTopContour = face.contour(ofType: .upperLipTop) {
            let upperLipTopPoints = upperLipTopContour.points
            contours.append(Contour(type: .upperLipTop, points: upperLipTopPoints, color: .magenta))
            print("upperLipTopPoints: \(upperLipTopPoints)")
        }
        // 上唇下
        if let upperLipBottomContour = face.contour(ofType: .upperLipBottom) {
            let upperLipBottomPoints = upperLipBottomContour.points
            contours.append(Contour(type: .upperLipBottom, points: upperLipBottomPoints, color: .magenta))
            print("upperLipBottomPoints: \(upperLipBottomPoints)")
        }
        // 下唇上
        if let lowerLipTopContour = face.contour(ofType: .lowerLipTop) {
            let lowerLipTopPoints = lowerLipTopContour.points
            contours.append(Contour(type: .lowerLipTop, points: lowerLipTopPoints, color: .magenta))
            print("lowerLipTopPoints: \(lowerLipTopPoints)")
        }
        // 下唇下
        if let lowerLipBottomContour = face.contour(ofType: .lowerLipBottom) {
            let lowerLipBottomPoints = lowerLipBottomContour.points
            contours.append(Contour(type: .lowerLipBottom, points: lowerLipBottomPoints, color: .magenta))
            print("lowerLipBottomPoints: \(lowerLipBottomPoints)")
        }
        // 顔
        if let faceContour = face.contour(ofType: .face) {
            let facePoints = faceContour.points
            contours.append(Contour(type: .face, points: facePoints, color: .blue))
            print("facePoints: \(facePoints)")
        }
        self.imageView.image = drawer.draw(contours: contours)
    }
}

struct Drawer {
    private let image: UIImage
    
    init(image: UIImage) {
        self.image = image
    }
    
    func draw(contours: [Contour]) -> UIImage? {
        let size = image.size
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        let context = UIGraphicsGetCurrentContext()
        image.draw(in: CGRect(origin: .zero, size: size))
        contours.forEach { contour in
            guard let firstPoint = contour.points.first else { return }
            context?.setLineWidth(1.0)
            context?.setStrokeColor(contour.color.cgColor)
            context?.move(to: firstPoint.cgPoint)
            contour.points.forEach { point in
                context?.addLine(to: point.cgPoint)
            }
            context?.strokePath()
        }
        let drawnImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return drawnImage
    }
}

extension VisionPoint {
    var cgPoint: CGPoint {
        return CGPoint(x: CGFloat(truncating: self.x), y: CGFloat(truncating: self.y))
    }
}

struct Contour {
    let type: FaceContourType
    let points: [VisionPoint]
    let color: UIColor
}

class Options: VisionFaceDetectorOptions {
    override init() {
        super.init()
        // 精度を重視
        self.performanceMode = .accurate
        // ランドマークを全て検出する
        self.landmarkMode = .all
        // 輪郭を全て検出する
        self.contourMode = .all
        // 分類を全て検出する
        self.classificationMode = .all
    }
}

```