Microsoft Cognitive ServicesのAPIを使ってGoでCaptionBotを作る

# はじめに
この記事は[Go3 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/go3)の25日の記事です。

[Caption Bot](https://www.captionbot.ai/)とはMicrosoftのAIがアップロード画像を解析し、画像に合ったキャプションをつけるサービスです。

このサービスはおそらく`Microsoft Cognitive Services`のうちの`Computer Vision`というAPIを使って実装されていると思われ、`Computer Vision`は`Azure Cognitive Services`に登録さえすれば誰でも自由に使用することができます。

`Computer Vision`のAPIはRESTful URLなのでどの言語でも利用でき、もちろんGo言語でも利用できます。

このエントリーではGo言語から`Computre VIsion API`を呼び出し、`Captionbot`ライクなWEBサービスを実装することを目的とします。

# Cognitive Servicesの利用申請
まず最初にMicrosoftアカウントを作成し、[Azure コンソール](https://azure.microsoft.com/ja-jp/account/)にログインします。
`＋新規`から`AI + Cognitive Services`を選択、`Computer Vision API`を追加します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/32430/78e48e5c-348b-bfb1-cf04-35e4b0708c4d.png)


`Computer Vision`は1カ月につき5,000トランザクションまで無料で使え、それ以降は機能ごとの従量課金となります。
[価格の詳細](https://azure.microsoft.com/ja-jp/pricing/details/cognitive-services/computer-vision/)

ダッシュボードに追加されたサービスをクリックし、`Show access keys`からアクセスキーを取得します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/32430/8a782ffa-3534-2b85-0514-7a1c394326d1.png)


[クイックスタート](https://docs.microsoft.com/ja-jp/azure/cognitive-services/computer-vision/)に各種言語での実装方法が説明されていますが、現時点ではgo言語のサンプルがなかったため、[cURL](https://docs.microsoft.com/ja-jp/azure/cognitive-services/computer-vision/quickstarts/curl)を参考に実装を進めます。

```:サンプルから抜粋
curl -v -X POST "https://westcentralus.api.cognitive.microsoft.com/vision/v1.0/analyze?visualFeatures=Categories&details={string}&language=en"
-H "Content-Type: application/json"
-H "Ocp-Apim-Subscription-Key: {subscription key}"

--d
ata-ascii "{body}" 
```

このサンプルによれば、ヘッダに`Content-Type`と`Ocp-Apim-Subscription-Key`を指定し、取得したい項目を`visualFeatures`に指定、画像を`--data-ascii` に指定してエンドポイントにアクセスすると取得できることがわかります。
こちらを参考に `go` で実装します。

# Computer Vision APIから結果を取得する
こちらの画像を使用して `description` を取得するプログラムを作ります。
[寝袋で朝をまつ行列組](https://www.pakutaso.com/20171128307post-13928.html)

![gyouretuIMGL6110_TP_V4.jpg](https://qiita-image-store.s3.amazonaws.com/0/32430/73c6e390-007e-4eb1-9247-c65d048ad3b1.jpeg)

```go:sample.go
package main

import (
    "log"
    "bytes"
    "io/ioutil"
    "net/http"
    "net/url"
)


const(
    VisionUrl = "https://southeastasia.api.cognitive.microsoft.com/vision/v1.0/analyze?"
    VisionAccessKey = "ダッシュボードから取得したアクセスキー"
)

func main() {
    data, err := ioutil.ReadFile("/path/to/gyouretuIMGL6110_TP_V.jpg")
	if err != nil {
		log.Println(err)
		return
	}
    values := url.Values{}
    values.Add("visualFeatures","description")
    req, _ := http.NewRequest("POST", VisionUrl+values.Encode(),bytes.NewReader(data))
    req.Header.Set("Content-Type", "application/octet-stream")
    req.Header.Set("Ocp-Apim-Subscription-Key", VisionAccessKey)
    client := new(http.Client)
    response,err := client.Do(req)
    if err != nil {
        log.Println("error")
    }
    defer response.Body.Close()
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Println("error")
        return
    }
	log.Println(string(body))

}
```

### 実行結果

```
$ go run sample.go
2017/11/28 05:08:41 {"description":{"tags":["outdoor","building","sidewalk","sitting","street","man","walking","woman","young","people","bench","girl","city","little","table","standing","holding","park","group",
"large","laying","luggage"],"captions":[{"text":"a group of people on a sidewalk","confidence":0.91571610732732367}]},"requestId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","metadata":{"width":1600,"height":993,"for
mat":"Jpeg"}}
```
正しく取得できました。

画像のキャプションは`description`の中の`caption`JSONオブジェクトに格納されています。

```
a group of people on a sidewalk
```
なので複数の人間が歩道にいる、という感じでしょうか。
`confidence` が `0.91571610732732367` なのでComputer Vision的にはかなり信用に足る値のようです。
（Computer Visionのcaptionは常に一定ではなく、経過と共にcaptionが変わることがあります。）

### 画像が性的なコンテンツかどうかを判定する
Computer Visionは送った画像が性的なコンテンツを含むかどうかを`visualFeatures`の値に`adult`を指定することで判定できますので、`visualFeatures`にこちらを追加して画像が性的コンテンツかどうかを判定してもらいます。

```go:sample.go
    values.Add("visualFeatures","description,adult")
```

### 実行結果

```
$ go run sample.go
2017/11/29 00:50:12 {"adult":{"isAdultContent":false,"isRacyContent":false,"adultScore":0.007760913111269474,"racyScore":0.01193587388843298},"description":{"tags":["outdoor","building","sidewalk","sitting","str
eet","man","walking","woman","young","people","bench","girl","city","little","table","standing","holding","park","group","large","laying","luggage"],"captions":[{"text":"a group of people on a sidewalk","confide
nce":0.91571610732732367}]},"requestId":"xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","metadata":{"width":1600,"height":993,"format":"Jpeg"}}
```

性的なコンテンツかどうかは`adlut`JSONオブジェクトから確認できます。

```
"adult":{"isAdultContent":false,"isRacyContent":false,"adultScore":0.007760913111269474,"racyScore":0.01193587388843298}
```
`"isAdultContent":false`、 `"isRacyContent":false`　という判定結果でしたので、この画像は性的コンテンツではないことがわかりました。

### カテゴリーの取得
Computer Visionはアップロードした画像を[86種類のカテゴリー](https://docs.microsoft.com/ja-jp/azure/cognitive-services/computer-vision/category-taxonomy)に分類します。
カテゴリー情報を取得するには`visualFeatures`の値に`categories`を指定します。

```go:sample.go
    values.Add("visualFeatures","description,adult,categories")
```

### 実行結果

```
$ go run sample.go
2017/11/29 04:43:55 {"categories":[{"name":"abstract_","score":0.00390625},{"name":"others_","score":0.00390625},{"name":"outdoor_","score":0.0234375},{"name":"outdoor_street","score":0.16015625}],"adult":{"isAd
ultContent":false,"isRacyContent":false,"adultScore":0.007760913111269474,"racyScore":0.01193587388843298},"description":{"tags":["outdoor","building","sidewalk","sitting","street","man","walking","woman","young
","people","bench","girl","city","little","table","standing","holding","park","group","large","laying","luggage"],"captions":[{"text":"a group of people on a sidewalk","confidence":0.91571610732732367}]},"reques
tId":"xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","metadata":{"width":1600,"height":993,"format":"Jpeg"}}
```

結果は`categories`オブジェクトに入ります。

```
"categories":[{"name":"abstract_","score":0.00390625},{"name":"others_","score":0.00390625},{"name":"outdoor_","score":0.0234375},{"name":"outdoor_street","score":0.16015625}]
```


# Translate APIを使って日本語に翻訳する
Azureには翻訳のための`Translator Speech API`というのがあります。
せっかくなので、これを使い`Computer Vision`の結果を日本語に翻訳したいと思います。

Azureコンソールから`Translator Speech API`を追加します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/32430/1fb1d724-fdde-cd9e-7c1a-6473f2cc1f31.png)

`Translator Speech`は100万文字までが無料で、それ以降の料金は契約体系により異なります。
[価格の詳細](https://azure.microsoft.com/ja-jp/pricing/details/cognitive-services/translator-text-api/)

`Computer Vision`と同様に、アクセスキーを取得します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/32430/2f572f3b-c662-4afa-4c0c-0241845aeacc.png)
エンドポイントがComputer Visionと異なるので、こちらも控えておきます。

[Microsoft Translator Text API](http://docs.microsofttranslator.com/text-translate.html#!/default/get_Translate)からAPIの仕様を確認します。

### トークンを取得する
`Translator Text API`を実行する前に`Authorization API`にアクセスしてトークンを取得する必要があるため、`http`パッケージを使用してトークンを取得します。

下記のURLに対して`Ocp-Apim-Subscription-Key`カスタムヘッダを指定し、管理画面で取得したアクセスキーを指定してアクセスするとトークン情報が取得できます。
```
https://api.cognitive.microsoft.com/sts/v1.0/issueToken
```

```go:trans.go
package main

import (
    "log"
    "io/ioutil"
    "net/http"
)


const(
	TokenUrl = "https://api.cognitive.microsoft.com/sts/v1.0/issueToken"
	TranslateAccessKey ="ダッシュボードから取得したアクセスキー"
)

func main(){
    req, err := http.NewRequest("POST", TokenUrl,nil)
    req.Header.Set("Ocp-Apim-Subscription-Key", TranslateAccessKey)
	if err != nil{
		log.Println(err)
	}

    client := new(http.Client)
    response, _ := client.Do(req)
	defer response.Body.Close()
	body,err := ioutil.ReadAll(response.Body)
	
	if err != nil{
		log.Println(err)
	}
	log.Println(string(body))
}

```

### 実行結果

```
$ go run trans.go
2017/11/30 04:57:44 XXXXXXXXXXXXXXXXXXXXXXXXXXXXX(成功するとトークン文字列が表示されます)
```

### 翻訳する
`Translate API`で翻訳するには以下のURLにGETパラメータで`appid`、`text`、`from`、`en`を付けてアクセスします。

```
https://api.microsofttranslator.com/V2/Http.svc/Translate.
```
| 変数名 | 値 |
|:-:|:-:|
|appid| "Bearer " + Authorization APIで返ったトークン
|text|翻訳したいテキスト|
|from|元の言語|
|to|翻訳したい言語|

試しにこちらを日本語に翻訳してみます

```
Love the life you live. Live the life you love.
```
ボブ・マーリィの名言だそうです。意味は「自分の生きる人生を愛せ。自分の愛する人生を生きろ。」

```go:trans.go
package main

import (
    "log"
    "io/ioutil"
    "net/http"
	"net/url"
)


const(
	TranslateUrl = "https://api.microsofttranslator.com/v2/http.svc/Translate?"
	TokenUrl = "https://api.cognitive.microsoft.com/sts/v1.0/issueToken"
	TranslateAccessKey ="ダッシュボードから取得したアクセスキー"
)

func main(){
    req, err := http.NewRequest("POST", TokenUrl,nil)
    req.Header.Set("Ocp-Apim-Subscription-Key", TranslateAccessKey)
	if err != nil{
		log.Println(err)
	}

    client := new(http.Client)
    response, _ := client.Do(req)
	defer response.Body.Close()
	body,err := ioutil.ReadAll(response.Body)
	
	if err != nil{
		log.Println(err)
	}

	appid := url.QueryEscape("Bearer "+string(body))
	text := url.QueryEscape("Love the life you live. Live the life you love.")
    req, _ = http.NewRequest("GET", TranslateUrl+"from=en&to=ja&text="+text+"&appid="+appid,nil)

    response, _ = client.Do(req)
	defer response.Body.Close()
	body,_ = ioutil.ReadAll(response.Body)

	log.Println(string(body))
}
```

### 翻訳結果

```
$ go run trans.go
2017/12/01 01:06:17 <string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">あなたの生活が大好きです。あなたの愛する人生を生きる。</string>
```

微妙ですが、意味としては大筋合ってそうです。

# 画像アップローダー作成
ここまでできたので、あとは画像をアップロードする機能をつければ完成です。

 `*Request.FormFile`でブラウザからアップロードされたファイルを取得し、 `ioutil.ReadAll` でファイルを読み込みます。

結果と一緒にファイルを表示するため、アップロードされたファイルの内容を`io.Copy`でファイルにコピーし、コピーされたファイルを読み込んで`Copmuter Vision` のAPIに渡します。また`Computer Vision` で得た結果を`Translate API`に渡すためのJSONオブジェクトを作成します。

```go:sample.go
package main

import (
	"bytes"
	"encoding/json"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"text/template"
)

type Category struct {
	Name  string  `json:"name"`
	Score float64 `json:"score"`
}

type Categories []Category

type Description struct {
	Tags     []string `json:"tags"`
	Captions Captions `json:"captions"`
}

type Caption struct {
	Text       string  `json:"text"`
	Confidence float64 `json:"confidence"`
}

type Captions []Caption

type Metadata struct {
	Width  int    `json:"width"`
	Height int    `json:"height"`
	Format string `json:"format"`
}

type Adult struct {
	IsAdultContent bool    `json:"isAdultContent"`
	IsRacyContent  bool    `json:"isRacyContent"`
	AdultScore     float64 `json:"adultScore"`
	RacyScore      float64 `json:"racyScore"`
}

type AnalyzeResult struct {
	Categories  Categories  `json:"categories"`
	Description Description `json:"description"`
	RequestId   string      `json:"requestId"`
	Metadata    Metadata    `json:"metadata"`
	Faces       []string    `json:"faces"`
	Adult       Adult       `json:"adult"`
}

const (
	VisionUrl          = "https://southeastasia.api.cognitive.microsoft.com/vision/v1.0/analyze?"
	VisionAccessKey    = "ダッシュボードから取得したComputer Visionのアクセスキー"
	TranslateUrl       = "https://api.microsofttranslator.com/v2/http.svc/Translate?"
	TokenUrl           = "https://api.cognitive.microsoft.com/sts/v1.0/issueToken"
	TranslateAccessKey = "ダッシュボードから取得したTranslate APIのアクセスキー"
	ResultFile         = "/path/to/static/result.jpg"
)

func main() {
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
	http.HandleFunc("/", index)
	http.HandleFunc("/analyze", analyze)
	http.ListenAndServe(":8888", nil)
}

func index(w http.ResponseWriter, r *http.Request) {
	tpl, _ := template.ParseFiles("views/index.html")
	tpl.Execute(w, nil)
}

func analyze(w http.ResponseWriter, r *http.Request) {
	file, _, err := r.FormFile("file")
	defer file.Close()
	if err != nil {
		log.Println(err)
		return
	}

	updFile, err := os.Create(ResultFile)
	defer updFile.Close()
	if err != nil {
		log.Println(err)
		return
	}

	if _, err = io.Copy(updFile, file); err != nil {
		log.Println(err)
		return
	}

	readFile, err := os.Open(ResultFile)
	if err != nil {
		log.Println(err)
		return
	}

	data, err := ioutil.ReadAll(readFile)
	if err != nil {
		log.Println(err)
		return
	}

	analyzeResult, err := vision(bytes.NewReader(data))
	if err != nil {
		log.Println("error")
		return
	}
	translated, err := translate(analyzeResult.Description.Captions[0].Text)
	if err != nil {
		log.Println("error")
		return
	}

	tpl, _ := template.ParseFiles("views/analyze.html")
	tpl.Execute(w, map[string]string{"Translated": translated})
}

func vision(data *bytes.Reader) (analyzeResult *AnalyzeResult, err error) {
	values := url.Values{}
	values.Add("visualFeatures", "description,adult,categories")
	req, _ := http.NewRequest("POST", VisionUrl+values.Encode(), data)
	req.Header.Set("Content-Type", "application/octet-stream")
	req.Header.Set("Ocp-Apim-Subscription-Key", VisionAccessKey)
	client := new(http.Client)
	response, err := client.Do(req)
	if err != nil {
		log.Println("error")
	}
	defer response.Body.Close()
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Println("error")
		return
	}
	analyzeResult = new(AnalyzeResult)
	err = json.Unmarshal(body, analyzeResult)
	return
}

func translate(translateText string) (translated string, err error) {
	req, err := http.NewRequest("POST", TokenUrl, nil)
	req.Header.Set("Ocp-Apim-Subscription-Key", TranslateAccessKey)
	if err != nil {
		return
	}

	client := new(http.Client)
	response, _ := client.Do(req)
	defer response.Body.Close()
	body, err := ioutil.ReadAll(response.Body)

	if err != nil {
		return
	}

	appid := url.QueryEscape("Bearer " + string(body))
	text := url.QueryEscape(translateText)
	req, _ = http.NewRequest("GET", TranslateUrl+"from=en&to=ja&text="+text+"&appid="+appid, nil)

	response, _ = client.Do(req)
	defer response.Body.Close()
	body, _ = ioutil.ReadAll(response.Body)
	rep := regexp.MustCompile(`<("[^"]*"|'[^']*'|[^'">])*>`)
	translated = rep.ReplaceAllString(string(body), "")
	return
}
```

### 実行
WEBサーバを起動します。

```
$ go run sample.go
```

### 結果の確認
画像をアップロードして、キャプションがついていることを確認します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/32430/c75ec39e-9356-39ae-6b31-aec9fe0ef7ec.png)

UPした画像を解析して自動で説明文を付与するCaptionbot 日本語版が出来ました。
[ソースコード](https://github.com/masahikoofjoytoy/cvision)はこちらです。

# 終わりに
~~今回のソースコードを一部修正し、WEBサービスとして[公開](http://画像アップロード.みんな)しました。
アップロードされた画像同士を戦わせることができるWEBサービスです。~~

~~[http://画像アップロード.みんな](http://画像アップロード.みんな)~~

## 遊び方
アップロードボタンを押して
![1.png](https://qiita-image-store.s3.amazonaws.com/0/32430/55c70f2d-59ee-cf49-1615-a031ff01f274.png)

写真を撮影または任意の画素増を選択してアップロードすると
![2.png](https://qiita-image-store.s3.amazonaws.com/0/32430/d13a4269-ed11-ee43-a97c-4a376556876c.png)

写真を判定します。
![3.png](https://qiita-image-store.s3.amazonaws.com/0/32430/bb6075d1-6172-a836-e7c5-dbcc1ee0bf71.png)

次に対戦する写真を選ぶと
![4.png](https://qiita-image-store.s3.amazonaws.com/0/32430/3d9c9f47-2365-ded4-dddb-ab9bd5ecd38f.png)

戦いが始まります。

|![cap.gif](https://qiita-image-store.s3.amazonaws.com/0/32430/a304cd83-673f-2d90-1d3d-49847a4b7ced.gif)|
|:-:|


画像の強さは[kagome](https://github.com/ikawaha/kagome)と[word2vec](https://code.google.com/archive/p/word2vec/)を使用して算出します。

`Computer Vision`により解析された結果を`Translate API`に渡し、そこから得た翻訳結果を`kagome`で形態素解析、単語ごとに`word2vec`をかませ、関連語とそのスコアの量を元に画像の強さを決定する仕組みです。

`word2vec`はgoの移植版を使わせていただき、学習データは[日本語版Wikipedia](https://dumps.wikimedia.org/jawiki/latest/)を使用しました。
[https://qiita.com/KojiOhki/items/350a02ea83fe6677bfd1](https://qiita.com/KojiOhki/items/350a02ea83fe6677bfd1)

~~こちらのサービスはお小遣いの範囲でEC2,S3,DynamoDBで運用しており、奥さんに怒られた時点で終了いたしますので予めご了承ください。
※サービス終了時にはUPいただいた画像は削除させていただきます。~~

サービス停止しました。