AzureのFace APIを使って提供目を自動生成
本日の記事はAizu Advent Calendarの15日目の記事になっています。
- 14日目 うじまるくん
- 15日目 たくぼくくん
記事を書いてほしい問題
このアドベントカレンダーを作ったのは僕なんですが
みなさん
記事書いてなさすぎじゃないですか!?!?!?!???
この問題割と深刻で、半数近くの人がきちんと書いてないです。
ちゃんと
書いて
本題
最初に飛ばしまくりまくりましたがどうもこんにちは、はとバスです。
この画像見飽きましたよね、これ以上は使わないので勘弁してください。
こんにちは、はとバス(twitter: @flying_hato_bus)と申します。
普段僕は意味のわからないツイートをする傍ら、朝と昼と夜にご飯を食べたり、赤べこの首を振らせたりしています。 普段はGolangやPythonを使ってサーバーの作成や、研究として機械学習をやったりしています。
赤べこの首を振らせる以外にも、ビニールハウス内の情報をセンサーで取得してAlexaスキルで取ってこれるようなもので新聞社から賞を頂いたりしています。
会津大で出会ったヘドバン赤べこ pic.twitter.com/SkwdDcDu5b
— 池澤あやか / いけあや (@ikeay) 2017年12月17日
提供目について
みなさん、"提供目"ってご存知でしょうか?
提供目とはアニメやドラマなどがCMに入る際、アイキャッチと共に表示される『提 供』の文字が人物の目と被っている現象を指す。
提供目というのは、上にもあるように、色々なシチュエーションと出来事が重なり、目の部分に「提供」の文字が重なるということです。
例です
今回はこのように、目の部分にちょうど「提供」の文字が覆いかぶさるような画像を生成してくれるくんを作りました。
使い方
使い方ですが、必要なのはGolangの実行環境とAzureのFaceAPIキー。Golangのインストールは各自の課題だとして、Azure FaceAPIの有効化について説明します。
Azure FaceAPIの有効化
まず必要なのはAzureのアカウント登録、みなさんが持っているメールアドレスで登録できるので登録しちゃいましょう。ちなみに、今メールアドレスを登録すると20000円分のクレジットが付いてくるので、これを使って有料のプランも自分のおサイフを痛めないで使用することができます。
次にFaceAPIの有効化、アカウント登録した後にAzureのポータルで有効化できます。
AI + machinelearning
をクリックして、Face
を選びます。
必要な情報を色々入れればデプロイが開始されます。デプロイが終わると、FaceAPIが使えるようになります。
ここでサブスクリプションキーが生成されているので、FaceAPIのコンソールからKey
をクリックし、サブスクリプションキーを確認します。このキーが後々必要になります。
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
に画像が生成されています。
プログラムの解説
ここからは今回のプログラムを解説していきます。とは言っても本当に必要な部分のみになりますが。
画像フォーマットを取得する
画像のフォーマットは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関数を用いてファイルの先頭まで持ってきます。
提供をかぶせる部分の座標を作成
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 }
見て分かると思うんですが、実は提供の字をかぶせているのは眉の情報を元にしています。 これから説明していくんですが、文字だけではわかりづらいと思うので
千鳥のノブで説明します。
まず、やりたいこととしてはこういうことにしたい。 目の上にいい感じに乗せてあげたいです。
しかし、Face APIは賢いんで、目に関してはこんな感じで情報を返してきます。
賢すぎるんじゃあ!!!!!
このまま提供を重ねてしまうと、
まあ、うん... 間違いとは言えないけどちょっと違うよね... もっとこう... 目の全体を覆うように...
どうしようかと思った時に、返ってくるのは目の情報だけではないことに気づきました。
そう、眉の情報も使えば良い
こう考えれば良いんです。
そういうわけで
提の字の始まり(左上の点) = (左眉の外側の点, 左眉の一番低い点) 提の時の終わり(右下の点) = (左目の内側の点, 左目の一番下の点) 供の字の始まり(左上の点) = (右眉の内側の点, 右眉の一番低い点) 供の時の終わり(右下の点) = (右目の外側の点, 右目の一番下の点)
という4つの点で「提供」の字をかぶせています。
図で示すとこんな感じ
さらにこれで画像を書き出すとこんな感じになります。
アルゴリズム的にはこんな感じで目のところにかぶせているというものでした。
最後に
これを作ったのが今週の頭くらいで、「作ったよ〜」みたいなノリでtwitterにあげてみました。
提供目を自動生成するツールを作りました。https://t.co/DUljb7HVIP pic.twitter.com/fRIJpKeGcg
— 飛ばすはとバス (@flying_hato_bus) 2018年12月10日
ちょっとバズった。
中には僕のガバガバ英文がお気に召してくれた方がおり
ウケる。“"提供目"(Teikyo-me) is japanese traditional funny picture for TV captured things.” / “GitHub - hatobus/Teikyo” https://t.co/oWx1DdVLrk
— uenot (@braitom) 2018年12月12日
みんなの心温めるコンテンツレーベルになれたかなと思います。
ちなみにこれのライセンスは SUSHI-WARE なので、何か使いたいときがあれば僕に寿司をおごってください。
参考にさせていただいたサイト
画像をリサイズする
[http://dempatow.hatenablog.com/entry/2016/11/17/画像をリサイズしてDBへ保存する/golang:embed:cite]
Seekしないと落ちる問題
デコードした後にファイルがぶっ壊れる問題
以上です、悪ふざけにお付き合い頂きありがとうございました。