LoginSignup
44
43

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-14

追記: 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
    }
}

ソースファイル

44
43
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
44
43