Google Cloud Vision APIとMicrosoft Translator APIを使って、ラッキービーストに物体認識させるようにしたよ

[iPhoneのSFSpeechRecognizerとAVSpeechSynthesizerと発泡スチロールでボスっぽいなにかを作る](http://qiita.com/croquette0212/items/bf0e41ca1b65c6d320b4)の続き

# 概要

前回作ったボスっぽいなにかに、Google Cloud Vision APIによるラベリングを組み込んで、目の前にある物体について説明してくれるようにしたよ

![system2.png](https://qiita-image-store.s3.amazonaws.com/0/14097/2067ff8c-ad9c-8577-3be3-a896967d5bff.png)

前回は、「○○ってなんですか?」と聞くと「○○」についてWikipediaで調べてサマリーを話してくれるだけでしたが、今回はフロントカメラとGoogle Cloud Vision APIを使って、目の前にある物体について説明してくれるようにします。

## プログラムの流れ

プログラムとしての流れは下記のような感じになります。

1. SFSpeechRecognizerで、音声認識をする
2. 「これなんですか?」的なフレーズが入力されたら、フロントカメラで撮影する
3. Google Cloud Vision APIを使って、撮影した画像にラベリングをする(写真に写っている物体の名前を取得する)
4. 検出された物体の名前をMicrosoft Translator APIを使って日本語に変換する
5. 変換された単語をWikipediaで調べて、サマリーを取得する
6. サマリーをボスっぽい口調に変換する
7. AVSpeechSynthesizerで音声合成して喋らせる

プログラム全体で見ると、初期化や設定、エラーハンドリング、状態管理などの分量が増え複雑になってきていますが、各ステップの処理自体は極めてシンプルなので、ここでは各ステップの処理の部分だけ切り出して説明します。

プログラム全体は、週末あたりにリファクタリングしてからGitHubで公開しようと思ってます。

# Google Cloud Vision APIによるラベリング
フロントカメラに写った物体を認識する部分については、Google Cloud Vision APIを使います。レイテンシーや値段の問題はありますが、プロトタイプを作る上ではTensorFlowなどを自前で組み込むより圧倒的に楽です。

## 画像を送ってラベリングさせる
Google Cloud Vision APIの使い方は至って簡単です。

* APIキーが含まれるURL( https://vision.googleapis.com/v1/images:annotate?key= )に
* Base64エンコードした画像データと
* 何をするか(ここではラベリングをしたいので`LABEL_DETECTION`を指定)

を指定して、POSTでリクエストをするだけです。

```Swift
func detectObjects(in image: UIImage) {
    let request: Parameters = [
        "requests": [
            "image": [
                "content": image.base64String
            ],
            "features": [
                [
                    "type": "LABEL_DETECTION",
                    "maxResults": 10
                ]
            ]
        ]
    ]
        
    let httpHeader: HTTPHeaders = [
        "Content-Type": "application/json",
        "X-Ios-Bundle-Identifier": Bundle.main.bundleIdentifier ?? ""
    ]

    Alamofire.request("https://vision.googleapis.com/v1/images:annotate?key=\(googleAPIKey)", method: .post, parameters: request, encoding: JSONEncoding.default, headers: httpHeader).validate(statusCode: 200..<300).responseJSON { response in
        switch response.result {
        case .success(let json):
            if let dictionary = json as? [AnyHashable: Any], let response0 = (dictionary["responses"] as?[[AnyHashable: Any]])?.first, let labelAnnotations = response0["labelAnnotations"] as? [[AnyHashable: Any]], let firstDescription = labelAnnotations[0]["description"] as? String {
                debugPrint(labelAnnotations)
            } else {
                debugPrint("Error. No respo")
            }
        case .failure(let error):
            debugPrint(error)
        }
    }
}
```

```Swift
extension UIImage {
    var base64String: String {
        var imagedata = UIImagePNGRepresentation(self)
        // 必要に応じて、リサイズする...
        return imagedata!.base64EncodedString(options: .endLineWithCarriageReturn)
    }
}
```

するとこんな感じでJSONでレスポンスが返ってきます。最大で`maxResults`で指定した数だけ、画像内に含まれる物体のラベルが返ってきます。

```JSON
{
  "responses": [
    {
      "labelAnnotations": [
        {
          "mid": "/m/0bt9lr",
          "description": "dog",
          "score": 0.97346616
        },
        {
          "mid": "/m/09686",
          "description": "vertebrate",
          "score": 0.85700572
        },
        {
          "mid": "/m/01pm38",
          "description": "clumber spaniel",
          "score": 0.84881884
        },
        {
          "mid": "/m/04rky",
          "description": "mammal",
          "score": 0.847575
        },
        {
          "mid": "/m/02wbgd",
          "description": "english cocker spaniel",
          "score": 0.75829375
        }
      ]
    }
  ]
}
```

問題は、返ってきた複数のラベルのうち、どれを使うかということなのですが、今回は最初のプロトタイプなので一番最初の要素(一番確度の高い要素)を使っています。

ただ、そうするとなかなか狙った通りの物体が選ばれないので、汎用的なワードをフィルタリングしたり、画像の中央からの距離などで重み付けをするなど、「どのラベルを選ぶか」のチューニングが今後一つ肝になりそうです。

### 撮影のタイミング
なお、前回作った`SFSpeechRecognizer`による音声認識で「これ〜なに？」みたいなパターンを検出した場合に、撮影＋API呼び出しが行われるようにしています。

# Microsoft Translator APIで翻訳する
Google Cloud Vision APIでラベリングされた結果は英語ですが、ボスは日本語で喋らせたいので、日本語に変換する必要があります。今回は一定量まで無料で使えるMicrosoft Translator APIを使いました。

## Microsoft Translator APIの概要と流れ
* Microsoft Translator APIを使うためには、Azureに登録をする必要があります。[公式](https://www.microsoft.com/ja-jp/translator/getstarted.aspx)の手順にしたがって、サブスクリプションを追加します。

### トークンの取得
* 翻訳のAPIを叩くためにはトークンが必要。
* トークンは、アプリごとに割り当てられるSubscription Keyを使って取得できる。(POSTのURLを叩くと、レスポンスでトークンが返ってくる)
* トークンは10分で失効してしまうので、10分以上間が空いてしまう場合は取得し直す必要がある。

### トークンを使って翻訳
* 取得したトークンをヘッダーにつけてAPIを叩くと、翻訳結果が返ってくる(XMLで)
* XMLをパースして、翻訳結果を取得する。

## 実装

HTTP通信用にAlamofireを、レスポンスのXMLのパース用にSWXMLHashを使っています。
本当は、これに加えてトークン取得失敗時や失効時のハンドリングが必要になります。

```Swift
import Alamofire
import SWXMLHash

let text = inputTextField.text!
let headers = ["Ocp-Apim-Subscription-Key": "YOUR APP KEY"]

Alamofire.request("https://api.cognitive.microsoft.com/sts/v1.0/issueToken", method: .post, headers: headers).responseString { (response) in
    switch response.result {
    case .success(let str):
        Alamofire.request("https://api.microsofttranslator.com/v2/Http.svc/Translate", method: .get, parameters: ["text": text, "to": "ja"], headers: ["Authorization": "Bearer \(str)"]).responseString(completionHandler: { (response) in
            switch response.result {
            case .success(let str):
                let xml = SWXMLHash.parse(str)
                self.outputLabel.text = xml["string"].element?.text
            case .failure(let error):
                debugPrint(error)
            }
        })
    case .failure(let error):
        debugPrint(error)
    }
}
```

# Wikipediaで調べて口調変換して、発声させる
ここから先は前回の実装と同じなので、省略します。[iPhoneのSFSpeechRecognizerとAVSpeechSynthesizerと発泡スチロールでボスっぽいなにかを作る](http://qiita.com/croquette0212/items/bf0e41ca1b65c6d320b4) を読んでね!

# 出来上がったもの

<blockquote class="twitter-video" data-lang="ja"><p lang="ja" dir="ltr">ラッキービーストをアップデートした。Google Cloud Vision APIを使って、見せたものについて説明できるようになった。まだ精度は低いけれども。 <a href="https://twitter.com/hashtag/%E3%81%91%E3%82%82%E3%81%AE%E3%83%95%E3%83%AC%E3%83%B3%E3%82%BA?src=hash">#けものフレンズ</a> <a href="https://t.co/RrNULicbON">pic.twitter.com/RrNULicbON</a></p>&mdash; Sousai (@croquette0212) <a href="https://twitter.com/croquette0212/status/853465013024980992">2017年4月16日</a></blockquote> <script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>


# まとめと今後の展望
物体認識を初めて使って見て、何かと勉強になりました。

* まず、Google Cloud Vision APIを使うと、導入が非常に簡単であること。
* 当然ながら、期待したようなワードで検出されるわけではないこと。
* 物体名は英語でラベリングされるため、日本語に翻訳する必要があるが、そこでさらに情報量が失われてしまうこと。

ボス(ラッキービースト)は、本来はパークガイドとして動物のことを説明するロボットですが、これを高い精度で実現するためには、最終的には「日本語で動物をラベリングできる」学習済みデータセットが必要になってくるな、ということが見えてきました。

ラベリングの処理自体は、TensorFlowを使ってiPhone上で行うのは難しくなさそうですが、学習データを作るのは流石に難易度が高すぎるので、どうしようか悩ましいところです。

まあ、まずは今回のプロトタイプを元に、色々調整をしてクオリティーを上げていきたいと思います。ガワもなんとかしたいし、やっぱり耳を動かしたり歩かせたりしたいなー。

# 参考資料
* [Swift3.0でCloud APIを使ってOCRをしてみる](http://gtech.hatenablog.com/entry/2017/01/20/173934)