LoginSignup
13
18

More than 5 years have passed since last update.

[Swift] [iOS] Watson Visual Recognitionを使って顔解析アプリを作ってみた

Last updated at Posted at 2016-12-25

2018年4月4日追記

新しい記事を書きましたので、以下を参照してください。
[iOS] Watson Visual Recognitionを使って顔解析アプリを作ってみた(Swift 4 Codable対応版)

WatsonVR.gif

はじめに

Watsonの顔認識APIを使って、iOSアプリを作ってみました。
「Visual Recognition」(以下VR)という画像解析機能の「Detect faces」というAPIです。
顔が含まれる画像を解析し、以下のような情報を返してくれます。

  • 検出された顔の座標
  • 性別と確率
  • 年齢と確率
  • 有名人の場合は名前およびタグ情報

このAPIはトレーニング済みの状態で提供されています。
VRには他に、利用者がトレーニング画像を集めてWatsonに学習させるタイプのAPIもあります。

私はこのAPIが「Alchemy Vision」という機能で提供されていた頃に顔解析のiOSアプリを作ってみたのですが、

  • Alchemy VisionがVRに統合された
  • その時はSwift 2だった

ということで、VRとSwift 3で作り直しました。

VRを利用する手順

事前に、IBM BluemixにVRのサービスを作成します。
手順はこちらの記事に詳細に記載されています。
BluemixでWatson API のVisual Recognition を使う by curl

サービス作成後に発行されるAPI KeyをiOSアプリからのリクエストで使用します。(後述)

VRには無料プランがあります。

Detect faces APIの仕様

リクエストパラメータ

POSTメソッドで画像を送ります。
Content-Typeは"application/x-www-form-urlencoded"です。
クエリパラメータに上記「VRを利用する手順」で控えておいたAPI Keyをセットし、Bodyに画像データをエンコードせずセットします。

詳細はDetect facesのAPI Referenceを参照。
サンプルは下記ソースコードを参照。

レスポンスデータのサンプル

JSONで返ってきます。
以下の通り、解析された顔が配列として格納されています。

{
  "images_processed" : 1,
  "images" : [
    {
      "faces" : [
        {
          "age" : {
            "max" : 54,
            "min" : 45,
            "score" : 0.405922
          },
          "face_location" : {
            "top" : 401,
            "height" : 105,
            "left" : 1117,
            "width" : 96
          },
          "gender" : {
            "score" : 0.9933070000000001,
            "gender" : "MALE"
          },
          "identity" : {
            "name" : "Barack Obama",
            "score" : 0.989013,
            "type_hierarchy" : "\/people\/politicians\/democrats\/barack obama"
          }
        },
        {
          "age" : {
            "max" : 64,
            "min" : 55,
            "score" : 0.617719
          },
          "face_location" : {
            "top" : 366,
            "height" : 125,
            "left" : 607,
            "width" : 81
          },
          "gender" : {
            "score" : 0.9933070000000001,
            "gender" : "MALE"
          },
          "identity" : {
            "name" : "Shinzō Abe",
            "score" : 0.731059
          }
        },
        {
          "age" : {
            "min" : 65,
            "score" : 0.416757
          },
          "face_location" : {
            "top" : 285,
            "height" : 107,
            "left" : 1339,
            "width" : 99
          },
          "gender" : {
            "score" : 0.0474259,
            "gender" : "FEMALE"
          }
        },
        {
          "age" : {
            "max" : 44,
            "min" : 35,
            "score" : 0.403753
          },
          "face_location" : {
            "top" : 311,
            "height" : 121,
            "left" : 228,
            "width" : 142
          },
          "gender" : {
            "score" : 0.970688,
            "gender" : "MALE"
          }
        }
      ]
    }
  ]
}

"age": "max"はセットされていない場合があるようですね。
"identity"はオバマさんと安倍さんだけセットされています。

開発環境

Items Version
Xcode 8.2
Swift 3.0.2

使用ライブラリ

ストーリーボード

スクリーンショット 2016-12-20 22.03.11.png

UINavigationController

初期画面と解析結果画面を行き来するために、UINavigationControllerを組み込みます。

MainViewController: UIViewController

初期画面です。
以下のUIオブジェクトを配置しています。

  • 操作ガイドのUILabel
  • 選択された画像を表示するためのUIImageView
  • カメラを起動するためのUIButton
  • フォトライブラリを起動するためのUIButton
  • 解析(API連携)を開始するためのUIButton
  • 解析待ち用UIActivityIndicatorView
  • Segue: Identifier="ShowResult"

SubTableViewController: UITableViewController

解析結果を一覧表示するためのUITableViewです。

ResultTableViewCell: UITableViewCell

解析結果用のセルです。
以下のUIオブジェクトを配置しています。

  • 画像を表示するためのUIImageView
  • 性別、性別確信度、年齢(Min-Max)、年齢確信度、名前を表示するためのUILabel

コード

AppDelegate.swift
import UIKit

// 解析結果を格納するクラス
class AnalyzedFace {
    var image: UIImage?
    var gender: String?
    var genderScore: String?
    var ageMin: String?
    var ageMax: String?
    var ageScore: String?
    var identity: String?
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    //ViewController間でデータを受け渡しするための変数
    var analyzedFaces: Array<AnalyzedFace> = []

//(以下略)
MainViewController.swift
import UIKit
import SwiftyJSON
import Photos

class MainViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    // ガイド文言Label
    @IBOutlet weak var guideLabel: UILabel!
    // 選択された画像
    @IBOutlet weak var selectedImageView: UIImageView!
    // 解析中のインジケータ
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    // MARK: UIViewController - Event

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    // MARK: IBAction

    /**
     カメラ起動ボタンTap
     */
    @IBAction func launchCameraButtonTapped(_ sender: Any) {
        if UIImagePickerController.isSourceTypeAvailable(.camera) {
            self.launchImagePicker(type: .camera)
        }
    }

    /**
     写真選択ボタンTap
     */
    @IBAction func launchPhotoButtonTapped(_ sender: Any) {
        if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
            self.launchImagePicker(type: .photoLibrary)
        }
    }

    /**
     解析開始ボタンTap
     */
    @IBAction func analyzeButtonTapped(_ sender: Any) {
        guard let selectedImage = self.selectedImageView.image else {
            return
        }
        // API仕様の画像サイズを超えないようにリサイズしてからAPIコールする
        guard let resizedImage = self.resizeJpeg(image: selectedImage) else {
            return
        }
        self.callApi(image: resizedImage)
    }

    // MARK: Delegate

    /**
     UIImagePickerControllerDelegate:画像選択時
     */
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        picker.dismiss(animated: true, completion: nil)
        guard let image = info[UIImagePickerControllerOriginalImage] else {
            return
        }
        // 選択画像をImageViewに表示
        self.selectedImageView.image = image as? UIImage
        // ガイドLabelを非表示に
        self.guideLabel.isHidden = true
    }

    // MARK: Method

    /**
     カメラ/フォトライブラリの起動
     - parameter type: カメラ/フォトライブラリ
     */
    func launchImagePicker(type: UIImagePickerControllerSourceType) {
        let controller = UIImagePickerController()
        controller.delegate = self
        controller.sourceType = type
        self.present(controller, animated: true, completion: nil)
    }

    /**
     画像圧縮処理
     - WatsonVR detect_faces APIの仕様により画像サイズは最大2MG(2016年12月現在)
     - サイズが収まるまで再帰的にサイズを縮小する
     - parameter image: ソース画像
     - returns: UIImage
     */
    func resizeJpeg(image: UIImage) -> UIImage? {
        let maxSize: Int = 1480000  // このぐらいのピクセル数だと2MBを超えないようだ(実験値)
        if Int(image.size.width * image.size.height) <= maxSize {
            return image
        }
        // 圧縮
        let size: CGSize = CGSize(width: (image.size.width * 0.8), height: (image.size.height * 0.8))
        UIGraphicsBeginImageContext(size)
        image.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
        guard let resizeImage = UIGraphicsGetImageFromCurrentImageContext() else {
            return nil
        }
        UIGraphicsEndImageContext()
        // 再帰処理
        return self.resizeJpeg(image: resizeImage)
    }

    /**
     API連携
     - parameter image: 解析対象画像イメージ
     */
    func callApi(image: UIImage) {
        // 解析結果はAppDelegateの変数を経由してSubViewに渡す
        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate

        // API呼び出し準備
        let APIKey = "(API Key)" // APIKeyを取得してここに記述
        let url = "https://gateway-a.watsonplatform.net/visual-recognition/api/v3/detect_faces?api_key=" + APIKey + "&version=2016-05-20"
        guard let destURL = URL(string: url) else {
            print ("url is NG: " + url) // debug
            return
        }
        var request = URLRequest(url: destURL)
        request.httpMethod = "POST"
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpBody = UIImageJPEGRepresentation(image, 1)

        // activityIndicator始動
        self.activityIndicator.startAnimating()

        // WatsonAPIコール
        let task = URLSession.shared.dataTask(with: request) {
            data, response, error in

            if error == nil {
                // APIレスポンス:正常
                let json = JSON(data: data!)
                print(json) // debug

                appDelegate.analyzedFaces = self.interpretJson(image: image, json: json)

                // リクエストは非同期のため画面遷移をmainQueueで行わないとエラーになる
                OperationQueue.main.addOperation(
                    {
                        // activityIndicator停止
                        self.activityIndicator.stopAnimating()
                        if appDelegate.analyzedFaces.count > 0 {
                            // 顔解析結果あり
                            self.performSegue(withIdentifier: "ShowResult", sender: self)
                        } else {
                            // 顔解析結果なし
                            let actionSheet = UIAlertController(title:"エラー", message: "顔検出されませんでした", preferredStyle: .alert)
                            let actionCancel = UIAlertAction(title: "キャンセル", style: .cancel, handler: {action in
                            })
                            actionSheet.addAction(actionCancel)
                            self.present(actionSheet, animated: true, completion: nil)
                        }
                    }
                )
            } else {
                // APIレスポンス:エラー
                print(error.debugDescription)   // debug
            }
            // activityIndicator停止
            if self.activityIndicator.isAnimating {
                self.activityIndicator.stopAnimating()
            }
        }

        task.resume()
    }

    /**
     解析結果のJSONを解釈してAnalyzedFace型の配列で返す
     - parameter image: 元画像
     - parameter json: JSONデータ
     - returns: AnalyzedFace型の配列
     */
    func interpretJson(image: UIImage, json: JSON) -> Array<AnalyzedFace> {
        var analyzedFaces: Array<AnalyzedFace> = []
        let facesJson = json["images"][0]["faces"].arrayValue
        // レスポンスのimageFaces要素は配列となっている(複数人が映った画像の解析が可能)
        for faceJson in facesJson {
            let face = AnalyzedFace()
            // 性別およびスコア
            guard let gender = faceJson["gender"]["gender"].string else {
                continue
            }
            if gender == "MALE" {
                face.gender = "男性"
            } else {
                face.gender = "女性"
            }
            guard let genderScore = faceJson["gender"]["score"].double else {
                continue
            }
            face.genderScore = String(floor(genderScore * 1000) / 10)
            // 年齢およびスコア
            if let ageMin = faceJson["age"]["min"].int {
                face.ageMin = String(ageMin)
            }
            if let ageMax = faceJson["age"]["max"].int {
                face.ageMax = String(ageMax)
            }
            guard let ageScore = faceJson["age"]["score"].double else {
                continue
            }
            face.ageScore = String(floor(ageScore * 1000) / 10)
            // Identity
            if let identity = faceJson["identity"]["name"].string {
                face.identity = identity
            }
            // 検出された顔の矩形
            guard let left = faceJson["face_location"]["left"].int else {
                continue
            }
            guard let top = faceJson["face_location"]["top"].int else {
                continue
            }
            guard let width = faceJson["face_location"]["width"].int else {
                continue
            }
            guard let height = faceJson["face_location"]["height"].int else {
                continue
            }
            // 元画像から切り抜いて変数にセット
            face.image = self.cropping(image: image, left: CGFloat(left), top: CGFloat(top), width: CGFloat(width), height: CGFloat(height))
            // 抽出完了
            analyzedFaces.append(face)
        }
        return analyzedFaces
    }

    /**
     元画像から矩形を切り抜く
     - parameter image: 元画像
     - parameter left: x座標
     - parameter top: y座標
     - parameter width: 幅
     - parameter height: 高さ
     - returns: UIImage
     */
    func cropping(image: UIImage, left: CGFloat, top: CGFloat, width: CGFloat, height: CGFloat) -> UIImage? {
        let imgRef = image.cgImage?.cropping(to: CGRect(x: left, y: top, width: width, height: height))
        return UIImage(cgImage: imgRef!, scale: image.scale, orientation: image.imageOrientation)
    }

}
SubTableViewController.swift
import UIKit

class SubTableViewController: UITableViewController {

    // 解析結果はAppDelegateの変数に入っている
    private let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    // MARK: - Table view data source

    /**
     セルの数を返す
     */
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.appDelegate.analyzedFaces.count
    }

    /**
     セルの項目をセット
     */
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let result = self.appDelegate.analyzedFaces[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "ResultCell", for: indexPath) as! ResultTableViewCell
        cell.setData(data: result)
        return cell
    }
}
ResultTableViewCell
import UIKit

class ResultTableViewCell: UITableViewCell {

    @IBOutlet weak var faceImageView: UIImageView!
    @IBOutlet weak var genderNameLabel: UILabel!
    @IBOutlet weak var genderScoreLabel: UILabel!
    @IBOutlet weak var ageRangeLabel: UILabel!
    @IBOutlet weak var ageScoreLabel: UILabel!
    @IBOutlet weak var identityLabel: UILabel!

    /**
     セルの内容をセット
     - parameter data: 解析結果
     */
    func setData(data: AnalyzedFace) {
        self.faceImageView.image = data.image
        if let gender = data.gender {
            self.genderNameLabel.text = "性別:" + gender
        }
        if let genderScore = data.genderScore {
            self.genderScoreLabel.text = "   確信度:" + genderScore + "%"
        }
        var ageRangeText = "年齢:"
        if let ageMin = data.ageMin {
            ageRangeText += ageMin + "才以上 "
        }
        if let ageMax = data.ageMax {
            ageRangeText += ageMax + "才以下"
        }
        self.ageRangeLabel.text = ageRangeText
        if let ageScore = data.ageScore {
            self.ageScoreLabel.text = "   確信度:" + ageScore + "%"
        }
        self.identityLabel.text = data.identity
    }

}
13
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
18