AzureのFace APIを使って提供目を自動生成

本日の記事はAizu Advent Calendarの15日目の記事になっています。

adventar.org




記事を書いてほしい問題

このアドベントカレンダーを作ったのは僕なんですが

f:id:flying_hato_bus:20181212194915p:plain













みなさん













f:id:flying_hato_bus:20181212200140j:plain

記事書いてなさすぎじゃないですか!?!?!?!???













この問題割と深刻で、半数近くの人がきちんと書いてないです。













f:id:flying_hato_bus:20181212194915p:plain

ちゃんと













f:id:flying_hato_bus:20181212200140j:plain

書いて

本題

最初に飛ばしまくりまくりましたがどうもこんにちは、はとバスです。

f:id:flying_hato_bus:20181212194915p:plain

この画像見飽きましたよね、これ以上は使わないので勘弁してください。

こんにちは、はとバス(twitter: @flying_hato_bus)と申します。

普段僕は意味のわからないツイートをする傍ら、朝と昼と夜にご飯を食べたり、赤べこの首を振らせたりしています。 普段はGolangPythonを使ってサーバーの作成や、研究として機械学習をやったりしています。

赤べこの首を振らせる以外にも、ビニールハウス内の情報をセンサーで取得してAlexaスキルで取ってこれるようなもので新聞社から賞を頂いたりしています。



提供目について

みなさん、"提供目"ってご存知でしょうか?

dic.pixiv.net

提供目とはアニメやドラマなどがCMに入る際、アイキャッチと共に表示される『提 供』の文字が人物の目と被っている現象を指す。

提供目というのは、上にもあるように、色々なシチュエーションと出来事が重なり、目の部分に「提供」の文字が重なるということです。

例です

f:id:flying_hato_bus:20181210143413j:plain
アイカツ!での例

今回はこのように、目の部分にちょうど「提供」の文字が覆いかぶさるような画像を生成してくれるくんを作りました。

github.com

使い方

使い方ですが、必要なのはGolangの実行環境とAzureのFaceAPIキー。Golangのインストールは各自の課題だとして、Azure FaceAPIの有効化について説明します。

Azure FaceAPIの有効化

まず必要なのはAzureのアカウント登録、みなさんが持っているメールアドレスで登録できるので登録しちゃいましょう。ちなみに、今メールアドレスを登録すると20000円分のクレジットが付いてくるので、これを使って有料のプランも自分のおサイフを痛めないで使用することができます。

次にFaceAPIの有効化、アカウント登録した後にAzureのポータルで有効化できます。

AI + machinelearning をクリックして、Faceを選びます。

f:id:flying_hato_bus:20181212211633p:plain

必要な情報を色々入れればデプロイが開始されます。デプロイが終わると、FaceAPIが使えるようになります。

ここでサブスクリプションキーが生成されているので、FaceAPIのコンソールからKeyをクリックし、サブスクリプションキーを確認します。このキーが後々必要になります。

f:id:flying_hato_bus:20181212213212p:plain

githubからレポジトリを持ってくる

go getを使おう

コードの方はgithubで管理しているんですが、git cloneで持ってくるよりはgo getで取得してくる方を推奨しています。

理由なんですが、git cloneは、任意の場所にレポジトリを持ってくることができる一方でgo getではある程度決まった位置にレポジトリを持ってくるという性質からです。

go getをしてレポジトリを持ってきたときには、よっぽどで無い限り

GOPATH/src/github.com/hatobus/Teikyo

に僕のレポジトリがクローンされてきます。git cloneでは、任意の場所にクローンされるので、画像までのパスなどを解決する必要になります。go getを使えば一定の場所にインストールされるのでその心配はいらず、パスの設定などをせずにそのままで動かすことができます。

.envファイルの設定

クローンしてきたファイルの中には.env.sampleというファイルがあると思います。これはサブスクリプションキーなどを管理するためのファイルで、このファイルを.envというファイル名でコピーして、その中に先ほど取得したサブスクリプションキーを記載していきます。

URL=
KEY1=
KEY2=

ファイルの中身はこのようになっていますが、KEY1,2には、さきほど取得したサブスクリプションキーを入れます、URLはリソースを作成した場所で微妙に違いますが以下のようにすればいいでしょう。

URL=https://[location].api.cognitive.microsoft.com/face/v1.0/
KEY1=XXXXXXXXXXXXXXXXXXXX
KEY2=YYYYYYYYYYYYYYYYYYYY

実際に動かしてみる

.envファイルを書き終えればとりあえずできるはずです。

go run server.go

で動かしてみましょう。何もなければサーバーがlocalhostの8080番ポートで動いてくれるはずです。

もし8080が別プロセスなどで使用されているときには、server.goの r.Run(":8080")の部分を任意のポートに置き換えてください。

ポートが使われているかどうかを調べるには

lsof -i:[ポート番号]

で調べられます。

きちんと動いたことを確認してから、リクエストを投げます。リクエストの例です。

curl http://localhost:8080/detect -F "upload[]=@/path/to/picture1.jpg" -F "upload[]=@/path/to/picture2.jpg" -H "multipart/form-data"

このAPIでは複数の画像に対応するためにヘッダをmultipart/form-dataで処理をしています。(このためにちょっとしためんどくさいことになったりしたけど)また、jpeg画像でなければ弾かれてしまうので注意。

処理がきちんとされればpicture/output/output[n].pngに画像が生成されています。

f:id:flying_hato_bus:20181212203646p:plain f:id:flying_hato_bus:20181214192130j:plain

プログラムの解説

ここからは今回のプログラムを解説していきます。とは言っても本当に必要な部分のみになりますが。

画像フォーマットを取得する

画像のフォーマットはjpegのみを受け付けている、ファイルの終わりが.jpgになっているかなどで考えてもいいが、これだと.pngファイルの拡張子だけを.jpgに変更しただけのファイルなどの場合に死んだりする。ちゃんとやるときには、ファイルのバイナリを解析したりするのが良いみたいですが、それをよしなにしてくれるのが image.DecodeConfig

image.DecodeConfigは写真のカラータイプ、フォーマットを返すメソッド。これを使えば画像がjpegなのか、またはそれ以外のフォーマットなのかがわかります。

f, err := file.Open()
defer f.Close()

// 一回DecodeConfigでファイルをいじるとファイルが壊れるために
// 別のbufにコピーをして回避しておく
io.Copy(b, f)

_, format, err := image.DecodeConfig(b)
if err != nil {
    errch[file.Filename] = err.Error()
    b.Reset()
    break
} else if format != "jpeg" {
    errch[file.Filename] = "Filetype must be jpeg"
    b.Reset()
    break
}

画像のフォーマットを取得する部分はここ。ちなみになぜ別のbufにコピーをしているかと言うと、image.DecodeConfigの内部実装にファイルを読み込む部分があるため。一回ファイルが読み込まれているので、あとでファイルの中身を扱おうとした時にEOF errorで落ちてしまう。そのために一度バッファにコピーしておくことでそれを回避しています。

画像をデコードする

FaceAPIに画像を投げるときには、画像をデコードしないといけない、そのためにデコードする処理を挟む。

buf := new(bytes.Buffer)
// どうやらファイルの先頭までシークをしなければいけなかったっぽい
// https://stackoverflow.com/questions/32193395/golang-io-reader-issue-with-jpeg-decode-returning-eof
fstream.Seek(0, 0)

img, err := jpeg.Decode(fstream)
if err != nil {
    // シークしないとunexpected EOFで落ちる
    return buf.Bytes(), err
}

if err = jpeg.Encode(buf, img, nil); err != nil {
    return buf.Bytes(), err
}

ファイルのシーク操作ですが、画像のデータをバッファにコピーした時に、先頭の位置からずれるようです。 そのために、Seek関数を用いてファイルの先頭まで持ってきます。

提供をかぶせる部分の座標を作成

f:id:flying_hato_bus:20181212201214p:plain

Face APIのレスポンスには、顔のパーツの座標を返してきます。目、口、鼻のみならず、眉などの情報を持っています。 Golangは構造体を定義し、それを元にjsonをパースします。返ってくる情報は膨大な量があるのですが、本当に必要な情報だけに変換する処理を噛ませています。

func (fp FaceParts) ToLandmark() *Landmark {
    LM := &Landmark{}

    LM.EyeRight.TopX = fp.FaceLandmarks.EyebrowRightInner.X
    LM.EyeRight.TopY = fp.FaceLandmarks.EyebrowRightInner.Y

    LM.EyeRight.BottomX = fp.FaceLandmarks.EyebrowRightOuter.X
    LM.EyeRight.BottomY = fp.FaceLandmarks.EyeRightBottom.Y

    LM.EyeLeft.TopX = fp.FaceLandmarks.EyebrowLeftOuter.X
    LM.EyeLeft.TopY = fp.FaceLandmarks.EyebrowLeftOuter.Y

    LM.EyeLeft.BottomX = fp.FaceLandmarks.EyebrowLeftInner.X
    LM.EyeLeft.BottomY = fp.FaceLandmarks.EyeLeftBottom.Y

    return LM
}

見て分かると思うんですが、実は提供の字をかぶせているのは眉の情報を元にしています。 これから説明していくんですが、文字だけではわかりづらいと思うので

f:id:flying_hato_bus:20181214193017j:plain

千鳥のノブで説明します。

まず、やりたいこととしてはこういうことにしたい。 目の上にいい感じに乗せてあげたいです。

f:id:flying_hato_bus:20181214193628j:plain

しかし、Face APIは賢いんで、目に関してはこんな感じで情報を返してきます。

f:id:flying_hato_bus:20181214194058j:plain





賢すぎるんじゃあ!!!!!





このまま提供を重ねてしまうと、

f:id:flying_hato_bus:20181214195501j:plain

まあ、うん... 間違いとは言えないけどちょっと違うよね... もっとこう... 目の全体を覆うように...

どうしようかと思った時に、返ってくるのは目の情報だけではないことに気づきました。

そう、眉の情報も使えば良い

f:id:flying_hato_bus:20181214194955j:plain

こう考えれば良いんです。

そういうわけで

提の字の始まり(左上の点) = (左眉の外側の点, 左眉の一番低い点)
提の時の終わり(右下の点) = (左目の内側の点, 左目の一番下の点)

供の字の始まり(左上の点) = (右眉の内側の点, 右眉の一番低い点)
供の時の終わり(右下の点) = (右目の外側の点, 右目の一番下の点)

という4つの点で「提供」の字をかぶせています。

図で示すとこんな感じ

f:id:flying_hato_bus:20181214201623p:plain

さらにこれで画像を書き出すとこんな感じになります。

f:id:flying_hato_bus:20181214201146p:plain

アルゴリズム的にはこんな感じで目のところにかぶせているというものでした。

最後に

これを作ったのが今週の頭くらいで、「作ったよ〜」みたいなノリでtwitterにあげてみました。

ちょっとバズった。

中には僕のガバガバ英文がお気に召してくれた方がおり

みんなの心温めるコンテンツレーベルになれたかなと思います。

ちなみにこれのライセンスは SUSHI-WARE なので、何か使いたいときがあれば僕に寿司をおごってください。

github.com

参考にさせていただいたサイト

画像をリサイズする

[http://dempatow.hatenablog.com/entry/2016/11/17/画像をリサイズしてDBへ保存する/golang:embed:cite]

Seekしないと落ちる問題

stackoverflow.com

デコードした後にファイルがぶっ壊れる問題

suguru03.hatenablog.com

以上です、悪ふざけにお付き合い頂きありがとうございました。