iPhoneのSFSpeechRecognizerとAVSpeechSynthesizerと発泡スチロールでボスっぽいなにかを作るの続き
概要
前回作ったボスっぽいなにかに、Google Cloud Vision APIによるラベリングを組み込んで、目の前にある物体について説明してくれるようにしたよ
前回は、「○○ってなんですか?」と聞くと「○○」についてWikipediaで調べてサマリーを話してくれるだけでしたが、今回はフロントカメラとGoogle Cloud Vision APIを使って、目の前にある物体について説明してくれるようにします。
プログラムの流れ
プログラムとしての流れは下記のような感じになります。
- SFSpeechRecognizerで、音声認識をする
- 「これなんですか?」的なフレーズが入力されたら、フロントカメラで撮影する
- Google Cloud Vision APIを使って、撮影した画像にラベリングをする(写真に写っている物体の名前を取得する)
- 検出された物体の名前をMicrosoft Translator APIを使って日本語に変換する
- 変換された単語をWikipediaで調べて、サマリーを取得する
- サマリーをボスっぽい口調に変換する
- 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でリクエストをするだけです。
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)
}
}
}
extension UIImage {
var base64String: String {
var imagedata = UIImagePNGRepresentation(self)
// 必要に応じて、リサイズする...
return imagedata!.base64EncodedString(options: .endLineWithCarriageReturn)
}
}
するとこんな感じでJSONでレスポンスが返ってきます。最大でmaxResults
で指定した数だけ、画像内に含まれる物体のラベルが返ってきます。
{
"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に登録をする必要があります。公式の手順にしたがって、サブスクリプションを追加します。
トークンの取得
- 翻訳のAPIを叩くためにはトークンが必要。
- トークンは、アプリごとに割り当てられるSubscription Keyを使って取得できる。(POSTのURLを叩くと、レスポンスでトークンが返ってくる)
- トークンは10分で失効してしまうので、10分以上間が空いてしまう場合は取得し直す必要がある。
トークンを使って翻訳
- 取得したトークンをヘッダーにつけてAPIを叩くと、翻訳結果が返ってくる(XMLで)
- XMLをパースして、翻訳結果を取得する。
実装
HTTP通信用にAlamofireを、レスポンスのXMLのパース用にSWXMLHashを使っています。
本当は、これに加えてトークン取得失敗時や失効時のハンドリングが必要になります。
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と発泡スチロールでボスっぽいなにかを作る を読んでね!
出来上がったもの
ラッキービーストをアップデートした。Google Cloud Vision APIを使って、見せたものについて説明できるようになった。まだ精度は低いけれども。 #けものフレンズ pic.twitter.com/RrNULicbON
— Sousai (@croquette0212) 2017年4月16日
まとめと今後の展望
物体認識を初めて使って見て、何かと勉強になりました。
- まず、Google Cloud Vision APIを使うと、導入が非常に簡単であること。
- 当然ながら、期待したようなワードで検出されるわけではないこと。
- 物体名は英語でラベリングされるため、日本語に翻訳する必要があるが、そこでさらに情報量が失われてしまうこと。
ボス(ラッキービースト)は、本来はパークガイドとして動物のことを説明するロボットですが、これを高い精度で実現するためには、最終的には「日本語で動物をラベリングできる」学習済みデータセットが必要になってくるな、ということが見えてきました。
ラベリングの処理自体は、TensorFlowを使ってiPhone上で行うのは難しくなさそうですが、学習データを作るのは流石に難易度が高すぎるので、どうしようか悩ましいところです。
まあ、まずは今回のプロトタイプを元に、色々調整をしてクオリティーを上げていきたいと思います。ガワもなんとかしたいし、やっぱり耳を動かしたり歩かせたりしたいなー。