はじめに
この記事はユニークビジョン株式会社 Advent Calendar 2018の3日目の記事です。
みんな大好きFirebaseでも機械学習を利用できるMLKitが今年導入されました(ベータ版)。
機械学習に深い知見がなくても、簡単に機械学習を利用できる魅了的なサービスなので使ってみたいと思います。
概要
MLKitのAPIは端末上で動作するものと、クラウドを利用する2種類があります。
端末上では通信が発生しないのでリアルタイムでの処理が可能になりますが、精度が落ちるようです。
クラウドを利用したAPIは、GCPのCloud Vision APIが使われており有料です。(ざっくりと月1000枚の画像までは無料です)
その分、非常に正確に検出が可能です。
利用可能な機能は以下の通りです。
機能 | 端末 | クラウド |
---|---|---|
テキスト認識(OCR) | ○ | ○ |
顔検出 | ○ | ○ |
バーコード スキャン | ○ | × |
画像のラベル付け | × | ○ |
ロゴ認識 | × | ○ |
ランドマーク認識 | × | ○ |
不適切なコンテンツの検出 | × | ○ |
類似画像の検索 | × | ○ |
カスタムモデルの推論 | ○ | × |
詳細はこちら
今回は端末で顔検出を利用してみようと思います。
Firebaseを導入
公式ドキュメントに沿ってプロジェクトを作成します
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の角度によって検出できるパーツが異なります)
動かしてみる
こちらの画像で検証してみましょう。
ブログ等でよく見かけるフリー素材モデル河村友歌さんです
結果は以下の通りです。
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
}
生成したUIImage
をView
に反映させます
self.imageView.image = draw(contours: contours)
色合いが気持ち悪いですが、各ランドマークの輪郭が描画されました。
かなり正確に顔を認識していることがわかります。
まとめ
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
}
}