追記: Dec-11. 2016
Alchemy Vision は、Visual Recognition に統合されました。
追記: Dec-26. 2016
本稿のアプリを、Visual RecognitionおよびSwift 3.0を使用して、作り直してみました。
http://qiita.com/y-some/items/45e2c99b91485638e05b
はじめに

AlchemyVisionAPIは、AlchemyAPIの中の画像認識関連のAPIです。
AlchemyAPIには、その他にテキスト解読などの機能もあります。
元々はAlchemyAPI社が開発した技術で、2015年3月にIBMが買収しました。
現在はWatsonの機能の一つとしてBluemix上で提供されています。
AlchemyAPIの詳細とFree API key(無料トライアル)の取得方法はこちらの記事をご参照ください。
今回は、顔認識のAPI「ImageGetRankedImageFaceTags」を使って、iOSアプリを作ってみました。
顔が含まれる画像を解析し、以下のような情報を返してくれるAPIです。
- 検出された顔の座標
- 性別と確率
- 年齢と確率
- 名前、職業などのタグ
- 複数人の場合は配列となっている
有名人ではない場合、名前とタグを除く項目が返ってきます。
性別・年齢の解析結果は結構正確で、自分、両親、妻、子供などの写真で試したところ、ほとんど正解でした。
ただし、自分の顔を送ったら"Naoto Kan"(菅直人)と名前が返ってきました。
今まで似てると言われたことないです(笑)
顔解析以外にもいろいろな画像解析APIがあります。
飲食店の口コミサービスで、写真から料理名を解析してタグ付けしたりとか、そんな使い道があるようです。
AlchemyVisionAPIのリファレンス
Free API keyを使用する場合、1日あたり1000トランザクションという制限があります。
「リクエスト1000回」ではなくAPI内部のトランザクションです。
また商用利用のライセンスの扱いについては未調査です。
APIのリクエスト・レスポンス
リクエストパラメータはシンプルです。→AlchemyVisionAPIのリファレンスを参照
ただし、ボディに画像データをバイナリのままエンコードなしでセットする必要があるようですので注意が必要。
これは、curlコマンドの"--data-binary"オプションに相当します。
レスポンス(JSON)のサンプルは以下の通りです。
{
"usage" : "By accessing AlchemyAPI or using information generated by AlchemyAPI, you are agreeing to be bound by the AlchemyAPI Terms of Use: http:\/\/www.alchemyapi.com\/company\/terms.html",
"NOTICE" : "THIS API FUNCTIONALITY IS DEPRECATED AND HAS BEEN MIGRATED TO WATSON VISUAL RECOGNITION. THIS API WILL BE DISABLED ON MAY 19, 2017.",
"status" : "OK",
"totalTransactions" : "4",
"imageFaces" : [
{
"gender" : {
"score" : "0.99593",
"gender" : "MALE"
},
"height" : "175",
"positionX" : "200",
"age" : {
"score" : "0.43953",
"ageRange" : "55-64"
},
"width" : "175",
"positionY" : "65",
"identity" : {
"name" : "Barack Obama",
"score" : "0.970688",
"disambiguated" : {
"website" : "http:\/\/www.whitehouse.gov\/",
"yago" : "http:\/\/yago-knowledge.org\/resource\/Barack_Obama",
"subType" : [
"Person",
"Politician",
"President",
"Appointer",
"AwardWinner",
"Celebrity",
"PoliticalAppointer",
"U.S.Congressperson",
"USPresident",
"TVActor"
],
"dbpedia" : "http:\/\/dbpedia.org\/resource\/Barack_Obama",
"name" : "Barack Obama",
"freebase" : "http:\/\/rdf.freebase.com\/ns\/m.02mjmr"
}
}
}
]
}
開発環境
Xcode7.3
使用ライブラリ
JSONを扱い易くしてくれるライブラリ「SwiftyJson」を使いました。
ライブラリ管理ツールとして「Carthage」を使いました。
APIのリクエストについて「Alamofire」を使ってみたのですが、上手くいかず諦めました。
POSTは正常にできているようですが、あれこれ試しても解析結果がnullとなってしまいました。
#Alamofireはcurlコマンドの"--data-binary"に相当するオプションを持っていない?
最終的にはこちらの記事を参考にさせていただきました。
ストーリーボード

MainViewController
最初の画面です。
- フォトライブラリで選択された画像を表示するためのUIImageView
- フォトライブラリを起動するためのUIButton
- 解析(API連携)を開始するためのUIButton
- 解析待ち用UIActivityIndicatorView
- Segue: Identifier="next"
SubViewController
解析結果画像を表示する画面です。
- 解析結果画像を表示するためのUIImageView
- 画像をズームするためのUIScrollView
コード
エラーハンドリングは甘いと思います。
アーキテクチャーデザイン(MV?)も、大した規模ではないのであえて無視しています。
import UIKit
// 解析結果を格納するクラス
class AnalyzedFace {
var height: String?
var width: String?
var positionX: String?
var positionY: String?
var gender: String?
var genderScore: String?
var ageRange: String?
var ageScore: String?
var name: String?
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
//ViewController間でデータを受け渡しするための変数
var analyzedImage: UIImage?
var analyzedFaces: Array<AnalyzedFace> = []
(以下略)
import UIKit
import SwiftyJSON
import Photos
class MainViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
// 選択された画像
@IBOutlet weak var selectedImageView: UIImageView!
// 解析中のインジケータ
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
// MARK: Event
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: Action
/// 画像選択ボタンTap
@IBAction func SelectPicButtonTapped(sender: AnyObject) {
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.PhotoLibrary) {
let controller = UIImagePickerController()
controller.delegate = self
controller.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
self.presentViewController(controller, animated: true, completion: nil)
}
}
/// 解析開始ボタンTap
@IBAction func goButtonTaped(sender: UIButton) {
if self.selectedImageView.image == nil {
return
}
callAlchemyAPI(self.selectedImageView.image!)
}
// MARK: Delegate
/// UIImagePickerControllerDelegate:画像選択時
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
picker.dismissViewControllerAnimated(true, completion: nil)
guard let image = info[UIImagePickerControllerOriginalImage] else {
return
}
self.selectedImageView.image = image as? UIImage
}
// MARK: Method
/// AlchemyAPI連携
/// - parameter image: 解析対象画像イメージ
func callAlchemyAPI(image: UIImage) {
let APIKey = "(AlchemyVisionのAPIKey)"
let url = "https://gateway-a.watsonplatform.net/calls/image/ImageGetRankedImageFaceTags?imagePostMode=raw&outputMode=json&apikey=" + APIKey
let destURL = NSURL(string: url)!
// API仕様の画像サイズ(1MB)を超えないようにする
let maxSize:Double = 1024 * 768
var ratio: CGFloat = 1
if Double(image.size.width * image.size.height) > maxSize {
ratio = CGFloat(maxSize / Double(image.size.width * image.size.height))
}
let imageData = UIImageJPEGRepresentation(image, ratio)
let request = NSMutableURLRequest(URL: destURL)
request.HTTPMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.HTTPBody = imageData
self.activityIndicator.startAnimating()
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if error == nil {
let json = JSON(data: data!)
print(json)
// 解析結果はAppDelegateの変数を経由してSubViewに渡す
let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.analyzedFaces = []
// レスポンスのimageFaces要素は配列となっている(複数人が映った画像の解析が可能)
let facesJson = json["imageFaces"].arrayValue
for faceJson in facesJson {
let face = AnalyzedFace()
face.height = faceJson["height"].string
face.width = faceJson["width"].string
face.positionX = faceJson["positionX"].string
face.positionY = faceJson["positionY"].string
face.gender = faceJson["gender"]["gender"].string
face.genderScore = faceJson["gender"]["score"].string
face.ageRange = faceJson["age"]["ageRange"].string
face.ageScore = faceJson["age"]["score"].string
if faceJson["identity"]["name"].string != nil {
face.name = faceJson["identity"]["name"].string
} else {
face.name = ""
}
appDelegate.analyzedFaces.append(face)
}
appDelegate.analyzedImage = UIImage.init(data: imageData!)
// リクエストは非同期のため画面遷移をmainQueueで行わないとエラーになる
NSOperationQueue.mainQueue().addOperationWithBlock(
{
self.activityIndicator.stopAnimating()
if appDelegate.analyzedFaces.count > 0 {
self.performSegueWithIdentifier("next", sender: self)
} else {
let actionSheet = UIAlertController(title:"エラー", message: "顔検出されませんでした", preferredStyle: UIAlertControllerStyle.Alert)
let actionCancel = UIAlertAction(title: "キャンセル", style: UIAlertActionStyle.Cancel, handler: {action in
})
actionSheet.addAction(actionCancel)
self.presentViewController(actionSheet, animated: true, completion: nil)
}
}
)
}
}
task.resume()
}
}
import UIKit
class SubViewController: UIViewController, UIScrollViewDelegate {
// 解析結果ImageViewの親であるScrollView
@IBOutlet weak var resultScrollView: UIScrollView!
// 解析結果画像のImageView
@IBOutlet weak var resultImageView: UIImageView!
// MARK: Event
override func viewDidLoad() {
super.viewDidLoad()
self.resultScrollView.delegate = self
self.resultScrollView.maximumZoomScale = 4.0
self.resultScrollView.minimumZoomScale = 0.4
self.setResult()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: Delegate
/// UIScrollViewDelegate:解析結果ImageViewの親であるScrollViewのZoom時
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return self.resultImageView
}
// MARK: Method
/// 解析対象画像と解析結果(テキストと顔の矩形)を合成する
func setResult() {
// 解析結果はAppDelegateの変数を経由して受け取る
let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let drawImage = appDelegate.analyzedImage!
let imageWidth = appDelegate.analyzedImage!.size.width
let imageHeight = appDelegate.analyzedImage!.size.height
let rect = CGRectMake(0, 0, imageWidth, imageHeight)
UIGraphicsBeginImageContext(appDelegate.analyzedImage!.size)
drawImage.drawInRect(rect)
let analyzedFaces = appDelegate.analyzedFaces
var outputText: String
let font = UIFont.boldSystemFontOfSize(30)
let textStyle = NSMutableParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [
NSFontAttributeName: font,
NSForegroundColorAttributeName: UIColor.orangeColor(),
NSParagraphStyleAttributeName: textStyle
]
for i in 0...analyzedFaces.count - 1 {
// 顔の矩形描画
let roundRect = UIBezierPath(
roundedRect: CGRectMake(
CGFloat(Double(analyzedFaces[i].positionX!)!),
CGFloat(Double(analyzedFaces[i].positionY!)!),
CGFloat(Double(analyzedFaces[i].width!)!),
CGFloat(Double(analyzedFaces[i].height!)!)),
cornerRadius: 10)
UIColor.orangeColor().setStroke()
roundRect.lineWidth = 6
roundRect.stroke()
// テキストの描画
outputText = ""
if let gender = analyzedFaces[i].gender {
if gender == "MALE" {
outputText += "男性 "
} else {
outputText += "女性 "
}
}
if let genderScore = analyzedFaces[i].genderScore {
let outputGenderScore: Double = floor(Double(genderScore)! * 1000) / 10
outputText += "\(outputGenderScore)" + "%\n"
}
if let ageRange = analyzedFaces[i].ageRange {
outputText += "\(ageRange)" + "才 "
}
if let ageScore = analyzedFaces[i].ageScore {
let outputAgeScore: Double = floor(Double(ageScore)! * 1000) / 10
outputText += "\(outputAgeScore)" + "%\n"
}
if let name = analyzedFaces[i].name {
outputText += "\(name)"
}
let margin: Double = 10 //矩形とテキストのマージン
let textRect = CGRectMake(
CGFloat(Double(analyzedFaces[i].positionX!)!),
CGFloat(Double(analyzedFaces[i].positionY!)! + Double(analyzedFaces[i].height!)! + margin),
250,
250)
outputText.drawInRect(textRect, withAttributes: textFontAttributes)
}
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.resultImageView.image = newImage
}
}
ソースファイル