Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

この記事はユニークビジョン株式会社 Advent Calendar 2018の3日目の記事です。

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

概要

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

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

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

詳細はこちら

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

Firebaseを導入

公式ドキュメントに沿ってプロジェクトを作成します

Podfile
pod 'Firebase/Core'
pod 'Firebase/MLVision'
pod 'Firebase/MLVisionFaceModel'

上記をPodfileに記述してpod install

実装

チュートリアルを参考にして進めます。
最後に全コードを載せています。

顔を検出するオプションを設定

import FirebaseMLVision

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

FaceDetectorを作成

let vision = Vision.vision()
let options = Options()
lazy var faceDetector = vision.faceDetector(options: options)

顔を検出する

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)で顔の検出が実行されます。
実行結果はコールバックの第一引数に含まれているの解析していきます。

顔を解析する

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はドキュメントによると

オイラー X: オイラー X の角度が正である顔は上向きです。
オイラー Y: オイラー Y の角度が正の場合、顔の右側がカメラに向いています。
オイラー Z: オイラー Z の角度が正である顔は、カメラを基準にして反時計回りに回転しています。

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

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

輪郭を取得する

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

結果は以下の通りです。

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 以上であれば、かなりの可能性で人が微笑んでいることを示します。

素晴らしい笑顔です。

画像に輪郭を描く

数字だけ見てもピンとこないのでこんな感じで輪郭を描いてみます。

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

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

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

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

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

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

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を描画します。

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
}

生成したUIImageViewに反映させます

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

結果がこちらです。
Simulator Screen Shot - iPhone XS - 2018-12-04 at 21.21.12.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/

全コード

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
    }
}

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away