2018年4月4日追記
新しい記事を書きましたので、以下を参照してください。
[iOS] Watson Visual Recognitionを使って顔解析アプリを作ってみた(Swift 4 Codable対応版)
はじめに
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 |
使用ライブラリ
- SwiftyJSON
- パッケージマネージャとしてCarthageを利用
ストーリーボード

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
コード
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> = []
//(以下略)
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)
}
}
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
}
}
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
}
}