Go
Azure
golang
ComputerVision
TranslateAPI
Go3Day 25

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

はじめに

この記事はGo3 Advent Calendar 2017の25日の記事です。

Caption BotとはMicrosoftのAIがアップロード画像を解析し、画像に合ったキャプションをつけるサービスです。

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

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

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

Cognitive Servicesの利用申請

まず最初にMicrosoftアカウントを作成し、Azure コンソールにログインします。
+新規からAI + Cognitive Servicesを選択、Computer Vision APIを追加します。
image.png

Computer Visionは1カ月につき5,000トランザクションまで無料で使え、それ以降は機能ごとの従量課金となります。
価格の詳細

ダッシュボードに追加されたサービスをクリックし、Show access keysからアクセスキーを取得します。
image.png

クイックスタートに各種言語での実装方法が説明されていますが、現時点ではgo言語のサンプルがなかったため、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-TypeOcp-Apim-Subscription-Keyを指定し、取得したい項目をvisualFeaturesに指定、画像を--data-ascii に指定してエンドポイントにアクセスすると取得できることがわかります。
こちらを参考に go で実装します。

Computer Vision APIから結果を取得する

こちらの画像を使用して description を取得するプログラムを作ります。
寝袋で朝をまつ行列組

gyouretuIMGL6110_TP_V4.jpg

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の中のcaptionJSONオブジェクトに格納されています。

a group of people on a sidewalk

なので複数の人間が歩道にいる、という感じでしょうか。
confidence0.91571610732732367 なのでComputer Vision的にはかなり信用に足る値のようです。
(Computer Visionのcaptionは常に一定ではなく、経過と共にcaptionが変わることがあります。)

画像が性的なコンテンツかどうかを判定する

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

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

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

"adult":{"isAdultContent":false,"isRacyContent":false,"adultScore":0.007760913111269474,"racyScore":0.01193587388843298}

"isAdultContent":false"isRacyContent":false という判定結果でしたので、この画像は性的コンテンツではないことがわかりました。

カテゴリーの取得

Computer Visionはアップロードした画像を86種類のカテゴリーに分類します。
カテゴリー情報を取得するにはvisualFeaturesの値にcategoriesを指定します。

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

Translator Speechは100万文字までが無料で、それ以降の料金は契約体系により異なります。
価格の詳細

Computer Visionと同様に、アクセスキーを取得します。
image.png
エンドポイントがComputer Visionと異なるので、こちらも控えておきます。

Microsoft Translator Text APIからAPIの仕様を確認します。

トークンを取得する

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

下記のURLに対してOcp-Apim-Subscription-Keyカスタムヘッダを指定し、管理画面で取得したアクセスキーを指定してアクセスするとトークン情報が取得できます。

https://api.cognitive.microsoft.com/sts/v1.0/issueToken

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パラメータでappidtextfromenを付けてアクセスします。

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.

ボブ・マーリィの名言だそうです。意味は「自分の生きる人生を愛せ。自分の愛する人生を生きろ。」

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オブジェクトを作成します。

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

UPした画像を解析して自動で説明文を付与するCaptionbot 日本語版が出来ました。
ソースコードはこちらです。

終わりに

今回のソースコードを一部修正し、WEBサービスとして公開しました。
アップロードされた画像同士を戦わせることができるWEBサービスです。

http://画像アップロード.みんな

遊び方

アップロードボタンを押して
1.png

写真を撮影または任意の画素増を選択してアップロードすると
2.png

写真を判定します。
3.png

次に対戦する写真を選ぶと
4.png

戦いが始まります。

cap.gif

画像の強さはkagomeword2vecを使用して算出します。

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

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

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

サービス停止しました。