画像の顔認識!Node-REDとVisual Recognitionでリア充判定をしてみた!

NodeRED WebAPI
この記事はteratailのBluemixアドベントカレンダー21日目の記事です。

冬、人肌恋しい季節になりました。もうすぐクリスマスですが、当日はリア充カップルで2ショットの写真がfacebookやTwitterで飛び交うと考えると身の毛もよだちますよね!ね?
そんな寂しさなのか恐怖なのか分からないやり場のない気持ちを打破するため、エンジニアは毎年技術と共に冬を越していきます。

人生初アドベントカレンダーとなる今回はBluemixVisual Recognitionを使って写真のリア充判定をやってみたいと思います。

リア充(りあじゅう)とは、「リアル(現実の生活)が充実している者」の略称である。身分では士に相当するため、無礼討ちならぬ無視の特権が認められている。
リア充 – アンサイクロペディア

かなしい。

スポンサーリンク

Bluemixとは?

IBM による最新のクラウド・オファリングです。Bluemix を使用すると、組織や開発者は迅速かつ簡単にクラウド上でアプリケーションを作成、デプロイ、管理することができます。Bluemix は、オープンソース PaaS (Platform as a Service) の Cloud Foundry をベースとする、IBM のオープン・クラウド・アーキテクチャーの実装です。
IBM Bluemixとは?

難しそうな言葉がぽつぽつ出てきますが、簡単に言うと以下のようなサービスです。

  • クラウドで簡単にアプリケーションを開発・デプロイ出来る
  • 開発時に使用する環境、ミドルウェアやAPIを一括管理・利用ができる

僕もはじめて触りましたが、HelloWorldレベルまでは30分かからずに実装できました。

Visual Recognitionで顔認識を実装する

今回はBluemixのWatsonAPIに含まれる、Visual RecognitionFace detectionを使用します。Visual Recognitionは顔認識を行うことができ、以下のような解析を行います。

  • おおよその年齢
  • 顔がある位置の座標
  • 性別
  • 著名人であればその人の名前

画像を使って色々面白そうなことができそうですね。APIに画像URLを指定してリクエストすると以下のようなJSONが返ってきます。

{
    "images": [
        {
            "faces": [
                {
                    "age": {
                        "max": 24, 
                        "min": 18, 
                        "score": 0.502411
                    }, 
                    "face_location": {
                        "height": 350, 
                        "left": 1964, 
                        "top": 601, 
                        "width": 350
                    }, 
                    "gender": {
                        "gender": "MALE", 
                        "score": 0.119203
                    }
                }, 
                {
                    "age": {
                        "max": 34, 
                        "min": 25, 
                        "score": 0.331244
                    }, 
                    "face_location": {
                        "height": 321, 
                        "left": 1403, 
                        "top": 901, 
                        "width": 274
                    }, 
                    "gender": {
                        "gender": "FEMALE", 
                        "score": 0.982014
                    }
                }
            ], 
            "resolved_url": "http://example.com/test.JPG", 
            "source_url": "http://example.com/test.JPG"
        }
    ], 
    "images_processed": 1
}

※画像URLはサンプルです。

Node-REDとVisual Recognitionで顔認識を実装してみる

リア充判定

リア充判定は以下のように行います。

  1. 写真に写っている人が2人きりである
  2. 2人は異性同士である
  3. 女性が好みの年齢であるか

なんだか非常に悔しいですね。
ここでは女性の年齢については、今回は明確に指定せず僕が年下好きであると”仮定”して「男性よりも女性が年下であった場合」とします。成人未満がどうとか難しい話はここでは考えません。

画像データの確保

顔認識をするには当然写真データが必要です。テスト用に男性✕男性と男性✕女性の画像が必要になります。ネット上のフリー素材を使うのも手でしたが、今回は弊社内で画像を集めました。
会議で全員が集まった後、同期の女性フロントエンドエンジニア(年下)とシンガーソングライターの社員に2ショット写真を依頼することに。念のため複数枚撮影を行いました。

フロントエンドエンジニア中村さんの場合

僕「折り入ってお願いがあるんですが」
中村「はい」
僕「僕と2ショットの写真を撮ってくれないですか」
中村「えっ」
とのことで撮影を快諾していただき、写真を何枚か撮影しました。

2ショット

2ショット

シンガーソングライター下側哲也さんの場合

僕「2ショット写真撮らせてください!!!!!!!」
下側さん「いいですよ!」
快諾していただきました。こちらも何枚か撮影しました。

2ショット

2ショット

Node-REDでの実装

普通はAPIを使って何かを作ろうとするとローカルの環境を整えたりサーバを用意したりと色々面倒です。ですが、今回はそんな面倒なものをすっ飛ばしてサクッと実装ができるNode-REDを使用して実装してみました。

Node-REDでリア充認識

Node-REDでリア充認識

Node-REDの細かい使い方などは本記事では割愛しますが、

  • フローチャートのような形で実装ができる
  • 様々な機能を持つノードが用意されており、それらのノードを使うことでコード(Node.js)をほぼ書かずに実装可能
  • 値の判定などやパラメータチェック、値の代入、APIへのリクエストなど様々なノードが用意されている
  • コードを含むフローチャート全体をJSONでエクスポートすることができ、ソースの共有が簡単
  • Deployボタンですぐにデプロイができるので、テストもすぐにできる

といった特徴があります。普通に実装すると意外に面倒くさい、APIへのリクエスト~データ取得まで簡単にできてしまうので、開発者は機能の実装に集中することができます。
全てブラウザ上で操作で実装できるのでChromebookユーザの僕もらくらく実装できました。

View側の実装

Node-REDではTemplateのノードでViewの部分を出力できます。テンプレートエンジンはmustacheが利用できました。初めて使用しましたが、mustacheはLogic less tenplateと書かれているように、複雑な処理が書けないようになっています。Viewでごりごり実装してしまうといったことがなくなるので、コードの見通しが良くなりそうです。
Viewは以下のように実装しました。

<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>リア充判定</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css" integrity="sha384-AysaV+vQoT3kOAXZkl02PThvDr8HYKPZhNT5h/CXfBThSRXQ6jW5DO2ekP5ViFdi" crossorigin="anonymous">
<style>
body {
    padding-top: 2rem;
    padding-bottom: 2rem;
}
#img-result {
    width: 75%;
    height: auto;
}
</style>

<div class="container">
  <div class="row">
    <div class="col-xs">
      <h1>リア充判定 | Node-RED</h1>
      <p>画像のURLを指定すると、リア充判定を行います。</p>
      <form action="{{req._parsedUrl.pathname}}" method="get">
        <div class="form-group row">
          <label for="imageUrl" class="col-xs-2 col-form-label">URL</label>
          <div class="col-xs-8">
            <input type="url" class="form-control" name="imageUrl" required>
          </div>
          <button class="btn btn-primary col-xs-2">送信</button>
        </div>
      </form>
    </div>
  </div>
  {{#payload.imageSrc}}
  <div class="row">
    <div class="col-xs-12">
      {{#payload.isRiazyu}}
      <p class="h2 text-danger">リア充爆発しろ</p>    
      {{/payload.isRiazyu}}
      {{^payload.isRiazyu}}
      <p class="h2 text-success">リア充ではありませんでした</p>    
      {{/payload.isRiazyu}}
      <img src="{{payload.imageSrc}}" id="img-result" class="img-responsive rounded">
    </div>
  </div>
  {{/payload.imageSrc}}
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha384-3ceskX3iaEnIogmQchP8opvBy3Mi7Ce34nWjpBIwVTHfGYWQS9jwHDVRnpKKHJg7" crossorigin="anonymous"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js" integrity="sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8" crossorigin="anonymous"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/js/bootstrap.min.js" integrity="sha384-BLiI7JTZm+JWlgKa0M0kGRpJbF2J8q+qreVrKBC47e3K6BW78kGLrCkeRX6I9RoK" crossorigin="anonymous"></script>

※省略可能なHTMLタグは全て省略してます。

リア充判定処理の実装

簡単な値チェックなどはNode-REDのノードである「switch」や「range」で実装可能です。複雑な処理は「function」ノードを使用してnode.jsでの実装を行います。今回はswitchで簡単にチェック処理を組み込み、リア充判定はゴリゴリjsで書きました。

const AGE = 26;
var isRiazyu = false;
var faces = [];
msg.payload = {};
faces = msg.result.images[0].faces;
// 2人ショットでかつ2人が異性
if(faces.length === 2 && faces[0].gender.gender !== faces[1].gender.gender) {
    for(var i in faces) {
        var face = faces[i];
        // 女性の場合
        if(face.gender.gender === 'FEMALE') {
            // minが必ずしも返ってくるとは限らないため、maxとminをそれぞれ判定
            isRiazyu = (face.age.min <= AGE || face.age.max <= AGE); 
            break;
        }
    }
}
msg.payload.faces = faces;
msg.payload.isRiazyu = isRiazyu;
msg.payload.imageSrc = msg.result.images[0].resolved_url;
return msg;

顔認識アプリケーション

Bootstrap4を使って簡単なフォームを作ってみました。

Node-RED 顔認識アプリ

Node-RED 顔認識アプリ


inputに画像のURLを投げるとその画像がリア充の画像であるかを判定します。

下川さんと僕の場合は2人きりで写っていますが、異性では無いため残念ながら非リア充と判定されました。

非リア充

非リア充

中村さんと僕の場合は2人きりで写っており、異性であり、中村さんが年下判定されたのでリア充と判定されています。やったね

どうみてもリア充

どうみてもリア充

全体のソースは以下のJSONからインポートできます。

[{"id":"38752643.22432a","type":"tab","label":"Flow 1"},{"id":"5badb731.a1aef8","type":"http in","z":"38752643.22432a","name":"","url":"/recognition","method":"get","swaggerDoc":"","x":141,"y":82,"wires":[["8cf0772c.de5e58","bdd5a0d5.0835f"]]},{"id":"3b507a78.c26356","type":"visual-recognition-v3","z":"38752643.22432a","name":"visual recognition","apikey":"__PWRD__","image-feature":"detectFaces","lang":"ja","x":561.8834228515625,"y":221.81185913085938,"wires":[["5d56a30d.5d40bc","4d8841b0.9c2bb"]]},{"id":"5d56a30d.5d40bc","type":"debug","z":"38752643.22432a","name":"","active":true,"console":"false","complete":"result","x":763.687744140625,"y":289.41363525390625,"wires":[]},{"id":"9029f1ad.b74c1","type":"function","z":"38752643.22432a","name":"リア充判定","func":"const AGE = 26;\nvar isRiazyu = false;\nvar faces = [];\nmsg.payload = {};\nfaces = msg.result.images[0].faces;\n// 2人ショットでかつ2人が異性\nif(faces.length === 2 && faces[0].gender.gender !== faces[1].gender.gender) {\n    for(var i in faces) {\n        var face = faces[i];\n        // 女性の場合\n        if(face.gender.gender === 'FEMALE') {\n            // minが必ずしも返ってくるとは限らないため、maxとminをそれぞれ判定\n            isRiazyu = (face.age.min <= AGE || face.age.max <= AGE); \n            break;\n        }\n    }\n}\nmsg.payload.faces = faces;\nmsg.payload.isRiazyu = isRiazyu;\nmsg.payload.imageSrc = msg.result.images[0].resolved_url;\nreturn msg;","outputs":1,"noerr":0,"x":971,"y":244,"wires":[["680d1812.25cd68","ff8f3027.68013"]]},{"id":"13249c3f.8717a4","type":"http response","z":"38752643.22432a","name":"","x":1262.2205810546875,"y":163.25299072265625,"wires":[]},{"id":"8cf0772c.de5e58","type":"debug","z":"38752643.22432a","name":"","active":true,"console":"false","complete":"false","x":359,"y":80,"wires":[]},{"id":"bafcc9ee.fa4d88","type":"change","z":"38752643.22432a","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.imageUrl","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":347,"y":222,"wires":[["3b507a78.c26356","4c3fbdd5.a8a684"]]},{"id":"680d1812.25cd68","type":"template","z":"38752643.22432a","name":"View","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n<meta http-equiv=\"x-ua-compatible\" content=\"ie=edge\">\n<title>リア充判定</title>\n<link rel=\"stylesheet\" href=\"//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css\" integrity=\"sha384-AysaV+vQoT3kOAXZkl02PThvDr8HYKPZhNT5h/CXfBThSRXQ6jW5DO2ekP5ViFdi\" crossorigin=\"anonymous\">\n<style>\nbody {\n    padding-top: 2rem;\n    padding-bottom: 2rem;\n}\n#img-result {\n    width: 75%;\n    height: auto;\n}\n</style>\n\n<div class=\"container\">\n  <div class=\"row\">\n    <div class=\"col-xs\">\n      <h1>リア充判定 | Node-RED</h1>\n      <p>画像のURLを指定すると、リア充判定を行います。</p>\n      <form action=\"{{req._parsedUrl.pathname}}\" method=\"get\">\n          \n        <div class=\"form-group row\">\n          <label for=\"imageUrl\" class=\"col-xs-2 col-form-label\">URL</label>\n          <div class=\"col-xs-8\">\n            <input type=\"url\" class=\"form-control\" name=\"imageUrl\" required>\n          </div>\n          <button class=\"btn btn-primary col-xs-2\">送信</button>\n        </div>\n      </form>\n    </div>\n  </div>\n  {{#payload.imageSrc}}\n  <div class=\"row\">\n    <div class=\"col-xs-12\">\n      {{#payload.isRiazyu}}\n      <p class=\"h2 text-danger\">リア充爆発しろ</p>    \n      {{/payload.isRiazyu}}\n      {{^payload.isRiazyu}}\n      <p class=\"h2 text-success\">リア充ではありませんでした</p>    \n      {{/payload.isRiazyu}}\n      <img src=\"{{payload.imageSrc}}\" id=\"img-result\" class=\"img-responsive rounded\">\n    </div>\n  </div>\n  {{/payload.imageSrc}}\n</div>\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js\" integrity=\"sha384-3ceskX3iaEnIogmQchP8opvBy3Mi7Ce34nWjpBIwVTHfGYWQS9jwHDVRnpKKHJg7\" crossorigin=\"anonymous\"></script>\n<script src=\"//cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js\" integrity=\"sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8\" crossorigin=\"anonymous\"></script>\n<script src=\"//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/js/bootstrap.min.js\" integrity=\"sha384-BLiI7JTZm+JWlgKa0M0kGRpJbF2J8q+qreVrKBC47e3K6BW78kGLrCkeRX6I9RoK\" crossorigin=\"anonymous\"></script>\n","x":1126,"y":163,"wires":[["13249c3f.8717a4"]]},{"id":"bdd5a0d5.0835f","type":"switch","z":"38752643.22432a","name":"画像URLチェック","property":"payload.imageUrl","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","outputs":2,"x":141,"y":172,"wires":[["680d1812.25cd68"],["bafcc9ee.fa4d88"]]},{"id":"4c3fbdd5.a8a684","type":"debug","z":"38752643.22432a","name":"","active":true,"console":"false","complete":"payload","x":560,"y":291,"wires":[]},{"id":"ff8f3027.68013","type":"debug","z":"38752643.22432a","name":"","active":true,"console":"false","complete":"payload","x":1150,"y":300,"wires":[]},{"id":"4d8841b0.9c2bb","type":"switch","z":"38752643.22432a","name":"レスポンスチェック","property":"result.images[0].faces.length","propertyType":"msg","rules":[{"t":"null"},{"t":"nnull"}],"checkall":"true","outputs":2,"x":770,"y":220,"wires":[["680d1812.25cd68"],["9029f1ad.b74c1"]]}]

今回ハマった所

BluemixやNode-REDは比較的簡単で扱いやすかったのですが、いくつかハマった点がありました。
念のため以下に残しておきます。

Bluemix 顔認識で調べるとAlchemyとVisual Recognitionの情報が2つ出てくる
顔認識はAlchemyAPIでできたようですが、現在はVisualRecognitionに統合されたようです。
Visual Recognitionのageレスポンスでmaxが返ってこない場合がある
必ずしも全てのパラメータが返ってくるわけではないようです。nullとかにしてほしかった。。
Node-REDのfunctionノードでデバッグログが出せない!
Node.log()でログが出せるかと思ったら出ませんでした。Node.warn()やNode.error()を使用しないとdebugタブに出てこないようです。

最後に

今回はBluemixのNode-REDVisualRecognitionを使用してみました。最初はBluemixのダッシュボードを眺めながら「難しそうだな…」と思っていましたが、思ったより簡単で、環境の用意で躓くことはありませんでした。
特にNode-REDでの実装も簡単で、最低限のコードと直感的な操作で今まで面倒だった処理が簡単に記述できてしまいます。VisualRecognitionとの繋ぎ込みもAPIキーを入力するだけで簡単に行うことができ、サクサク進めることができました。ただし、本格的に使用する場合はデザインパターンを学んだ上で実戦投入するのが良さそうです。(今回はゴリゴリ書いてしまったので僕も勉強します。。。)
この記事ではVisual Recognitionしか使用しませんでしたが、BluemixのWatsonAPIには他にも様々なAPIがあり、サービスの可能性を大きく広げることが出来ると思います。是非使ってみてください!

WebAPI
スポンサーリンク
この記事が気に入ったら
いいね!しよう
最新情報をお届けします。
この記事を書いた人

スパイスファクトリー株式会社 Webエンジニア。フロントエンドやWebサイトの高速化が得意です。インフラ・バックエンドも一通りやってます。
個人的なお仕事のご依頼や情報交換などはお問い合わせまたはTwitterにメンションをお願いします。

ShoheiTaiをフォローする
このエントリーが役に立ったらシェアをお願いします!
イケてないコード – Webエンジニアのブログ

コメント

タイトルとURLをコピーしました