【Swift】WatsonのAlchemyVision(画像解析)を使ってiOSアプリを作ってみた

More than 1 year has passed since last update.


追記: Dec-11. 2016

Alchemy Vision は、Visual Recognition に統合されました。


追記: Dec-26. 2016

本稿のアプリを、Visual RecognitionおよびSwift 3.0を使用して、作り直してみました。

http://qiita.com/y-some/items/45e2c99b91485638e05b


はじめに

AlchemyTest_6s.gif

動画はこちら

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"に相当するオプションを持っていない?

最終的にはこちらの記事を参考にさせていただきました。


ストーリーボード

スクリーンショット 2016-05-13 6.15.52.png


MainViewController

最初の画面です。


  • フォトライブラリで選択された画像を表示するためのUIImageView

  • フォトライブラリを起動するためのUIButton

  • 解析(API連携)を開始するためのUIButton

  • 解析待ち用UIActivityIndicatorView

  • Segue: Identifier="next"


SubViewController

解析結果画像を表示する画面です。


  • 解析結果画像を表示するためのUIImageView

  • 画像をズームするためのUIScrollView


コード

エラーハンドリングは甘いと思います。

アーキテクチャーデザイン(MV?)も、大した規模ではないのであえて無視しています。


AppDelegate.swift

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> = []

(以下略)



MainViewController.swift

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

}



SubViewController.swift

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



ソースファイル

https://github.com/y-some/AlchemyTest