はじめに
この記事はGo3 Advent Calendar 2017の25日の記事です。
Caption Botとは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 コンソールにログインします。
+新規
からAI + Cognitive Services
を選択、Computer Vision API
を追加します。
Computer Vision
は1カ月につき5,000トランザクションまで無料で使え、それ以降は機能ごとの従量課金となります。
価格の詳細
ダッシュボードに追加されたサービスをクリックし、Show access keys
からアクセスキーを取得します。
クイックスタートに各種言語での実装方法が説明されていますが、現時点では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-Type
とOcp-Apim-Subscription-Key
を指定し、取得したい項目をvisualFeatures
に指定、画像を--data-ascii
に指定してエンドポイントにアクセスすると取得できることがわかります。
こちらを参考に go
で実装します。
Computer Vision APIから結果を取得する
こちらの画像を使用して description
を取得するプログラムを作ります。
寝袋で朝をまつ行列組
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
にこちらを追加して画像が性的コンテンツかどうかを判定してもらいます。
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種類のカテゴリーに分類します。
カテゴリー情報を取得するにはvisualFeatures
の値にcategories
を指定します。
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
を追加します。
Translator Speech
は100万文字までが無料で、それ以降の料金は契約体系により異なります。
価格の詳細
Computer Vision
と同様に、アクセスキーを取得します。
エンドポイントが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
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.
ボブ・マーリィの名言だそうです。意味は「自分の生きる人生を愛せ。自分の愛する人生を生きろ。」
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オブジェクトを作成します。
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
結果の確認
画像をアップロードして、キャプションがついていることを確認します。
UPした画像を解析して自動で説明文を付与するCaptionbot 日本語版が出来ました。
ソースコードはこちらです。
終わりに
今回のソースコードを一部修正し、WEBサービスとして公開しました。
アップロードされた画像同士を戦わせることができるWEBサービスです。
遊び方
戦いが始まります。
画像の強さはkagomeとword2vecを使用して算出します。
Computer Vision
により解析された結果をTranslate API
に渡し、そこから得た翻訳結果をkagome
で形態素解析、単語ごとにword2vec
をかませ、関連語とそのスコアの量を元に画像の強さを決定する仕組みです。
word2vec
はgoの移植版を使わせていただき、学習データは日本語版Wikipediaを使用しました。
https://qiita.com/KojiOhki/items/350a02ea83fe6677bfd1
こちらのサービスはお小遣いの範囲でEC2,S3,DynamoDBで運用しており、奥さんに怒られた時点で終了いたしますので予めご了承ください。
※サービス終了時にはUPいただいた画像は削除させていただきます。
サービス停止しました。