クリスマス、忘年会のシーズンが近づいてきて、
部の飲み、同僚との飲み、旧友との飲み、などが増えてくるかと思います。
世間一般的には。
しかしプログラマ、データ分析官は、
繁華街のネオンや、イルミネーションの眩しさよりも、
スクリーンの眩しさが目にしみる実装の日々かと思います。
とはいえ、それでもなお、
飲みに行く機会が生じたらどうするか。
そして自分がお店をセッティングしなければならないとしたら。
我々の手元にあるのは何でしょうか。
経験に基づく良いお店のリストは無し。
良いお店を教えてくれる人脈は無し。
カテゴリで探そうにも、食べたいものに対するこだわりも無し。
猜疑心のため口コミに対する信頼も無し。
一方で、近年、スマホの発展とともに、
あらゆる人にとって写真撮影が一般的・日常的になってきました。※[ref]※1:FUJI FILMの調査では、写真アプリ利用者は世界で10億人、撮影数は、写真フィルムの20倍。[/ref]
また、スマートフォンで撮影する対象は、
友人など身近な人や、料理の写真が多いかと思います。※[ref]※2:MMDの調査ではスマートフォン撮影対象のTOP3は「友達・家族・恋人」、「自然」、「料理」 (MMD研究所,2016)[/ref]
そこで、
ぐるなびの「レストラン検索API」とMicrosoftの「computer vision api」を用いて
一緒に飲みに行きたい人や食べたい料理、
行きたい雰囲気のお店の写真を用いてレストラン検索できるウェブアプリ、
「gurunavision」を開発しました。

| (1) イメージ動画
gurunavisionの概要がわかる1分動画となります。
※概要動画があるのなら、上記の無駄に鬱な前フリは不要なのでは、ということはさておき。
| (2) システム構成
実装はjavascriptとphp、サーバはaws ec2となります。
処理フローは下記となります。
1). 画像を送信
2). 画像をmicrosoft computer vision apiに送信
3). 解析結果の一部を用いてデータ可視化
4). 「この写真を用いて検索」をクリックされると
画像情報と現在位置(あるいは選択エリア)をサーバに送信
5). サーバで、受け取った画像情報を前処理。
写真内の人物や風景、写っている食べ物や、
色味などから総合的に判断し、
ぐるなびapiに投げるためのURLを自動生成
6). ぐるなびapiに上記URLを送信
7). 検索結果を表示
| (3) 詳細解説:画像解析フェーズ(Microsoft computer vision api)
gurunavisionは、「画像解析」と、「レストラン検索」の、2つのフェーズからなります。
まずはMicrosoft computer vision apiを用いた画像解析について解説します。
画像解析apiは
GoogleのCloud vision api、
IBMのWatson api(Visiaul Recognition)、
など様々な企業から提供されております。
それぞれ、画像のタグや性年齢推計、表情(感情)判定など機能自体には大きな差がないのですが、
今回、Microsoft computer vision apiを採用した理由は、
・タグ付けの精度(タグおよび確信度)が正確
・性年齢推計精度が正確
・写真についての正確な説明文(description)を出力可能
といった観点からとなります。
とはいっても上記はあくまで個人的な見解ですが、
下記公式サイトにてcomputer visionのトライアルが可能です。
Microsoftの画像解析系のapiは大きく下記の5つあります。
| Computer Vision | 画像を解析しタグ付け |
| Content Moderator | 画像中に不適切なコンテンツが含まれていないかのチェック |
| Emotion | 画像中の人物の表情からの感情推計 |
| Face | 画像中の人物の顔および目や鼻などの各パーツ位置把握 |
| Video | 動画中の人物のトラッキングや動きの判定、タイムラインにそったタグ付け |
上記の中で、今回利用した、Microsoft Computer Vision apiで取得できる項目は主に下記のようなものとなります。(一部)
| Description | 文章での画像の説明とその確信度 |
| Tags | 画像の特徴(タグ)およびその確信度 |
| Image Format | 画像ファイル形式 |
| Image Dimentions | 画像サイズ |
| Categories | 画像のカテゴリおよびその確信度 |
| Faces | 画像中の人物(いた場合)の位置、および推計の性年齢 |
| Dominant Colors | 画像中の主な色味 |
今回のgurunavisionでのような
写真から、tagと推計の性年齢などを配列に格納するためのコードは下記となります。
※正確には、このあとphp経由で次の処理に移るため、文字列として保存し、サーバに投げて、サーバ側でparseして配列に格納。
※事前にAPI利用のためのアクセスキーを取得する必要があります。
(下記コード中の”key”に取得したアクセスキーを入力)
※要は下記コード中、parse_data内に各種データが入っているので、
parse_data.description、とかparse_data.facesとかparse_data.tagsとかすればよい。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
function send_key_for_msvision(key){ $.ajax({ url: apiUrl, beforeSend: function (xhrObj) { xhrObj.setRequestHeader("Content-Type", "application/octet-stream"); xhrObj.setRequestHeader("Ocp-Apim-Subscription-Key", key); }, type: "POST", data: file, processData: false }) .done(function (response) { ProcessResult(response); }) .fail(function (error) { $("#response").text(error.getAllResponseHeaders()); }); } function ProcessResult(response){ var data = JSON.stringify(response,null , "\t"); parse_data = JSON.parse(data); document.getElementById("image_description").innerHTML = "<span id='desc_span'>「" + parse_data.description.captions[0].text + "」</span>な写真"; document.getElementById("image_tags").innerHTML = "(画像から" + parse_data.description.tags.length + "個の特徴を抽出しました。)"; img_tags=""; img_color=""; img_people=""; img_tags += parse_data.description.tags; for(var i=0;i<parse_data.tags.length;i++){ img_tags += parse_data.tags[i].name; if(i < parse_data.tags.length-1){ img_tags += ","; } } img_color += parse_data.color.dominantColors; for(var i=0;i<parse_data.faces.length;i++){ if(parse_data.faces[i].gender =="Male"){ img_people += "m" + parse_data.faces[i].age; }else{ img_people += "f" + parse_data.faces[i].age; } if(i < parse_data.faces.length-1){ img_people += ","; } } } |
上記で得られたタグ情報などを、次のフェーズであるレストラン検索に用います。
| (4) 詳細解説:レストラン検索フェーズ(ぐるなびレストラン検索api)
ぐるなびレストラン検索apiは、指定した任意の条件でレストラン検索ができるapiとなります。
条件は、場所(緯度・経度あるいはエリア)、業態、のほか、
ランチ営業、禁煙席、カード利用、飲み放題、
個室、深夜営業、駐車場、電源、プロジェクタ…etcの有無など多岐に渡ります。
公式サイトのリクエストパラメータページにて一覧を確認できます。
また上記ページではapiを利用した、javascript、php、python、javaでのサンプルプログラムが紹介されています。
ぐるなびapiも、apiテストツールが提供されておりますので、
テストページにて、各種条件でのレストラン検索のトライアルが可能です。
まずはjavascriptから受け取ったパラメータを用いて、前処理し、
ぐるなびapiに投げるためのURLを自動生成します。
これは単純にタグをそのままフリーワード条件としてぐるなびapiに投げるにしても、
複数あるタグの中からどのタグを(レストラン検索として)優先するか、
タグ以外の性年齢推計なども、条件としてうまく加味するためとなります。
画像からのタグや、もし人が写っている画像の場合、
性年齢や人数をどのように検索条件に加味するか、のルールはあらかじめ用意しておき、
さらにセレンディピティを出すために、意外なタグも混ぜる、ということが、
裏側の処理となります。
例えば、基本的なルールの1つである「デート」用途の検索ロジックは、
「画像に、男性、女性が1人づつ、かつその年齢が45歳以下の場合」としています。
|
1 2 3 4 5 6 7 |
if($male_num == 1 and $female_num == 1 and $ave_male_age < 45 and $ave_female_age < 45){ if($area_flag == 0){ $url = sprintf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s", $uri, "?keyid=", $acckey, "&format=", $format, "&latitude=", $lati, "&longitude=", $longi, "&range=", $range_int, "&hit_per_page=", $hit_per_page, "&freeword=デート"); }else{ $url = sprintf("%s%s%s%s%s%s%s%s%s%s", $uri, "?keyid=", $acckey, "&format=", $format, "&areacode_l=", $area_code, "&hit_per_page=",$hit_per_page, "&freeword=デート"); } } |
ペット入店可なお店の検索の場合は、
「画像に、”animal”、”mammal”、”dog”、”cat”いずれかのタグが含まれている場合」としています。
|
1 2 3 4 5 6 7 |
if($img_tag_i == "animal" or $img_tag_i == "mammal" or $img_tag_i == "dog" or $img_tag_i == "cat"){ if($area_flag == 0){ $url = sprintf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s", $uri, "?keyid=", $acckey, "&format=", $format, "&latitude=", $lati, "&longitude=", $longi, "&range=", $range_int, "&hit_per_page=", $hit_per_page, "&freeword=ペット"); }else{ $url = sprintf("%s%s%s%s%s%s%s%s%s%s", $uri, "?keyid=", $acckey, "&format=", $format, "&areacode_l=", $area_code, "&hit_per_page=",$hit_per_page, "&freeword=ペット"); } } |
上記のようなベースルールを、100種類以上、事前に用意しておき、
あとは必要に応じてタグからのアドリブ(tagをそのままフリーワードパラメータに投げる)で対応しています。
自動生成されたurlを用いてレストラン検索apiになげた結果から、
クライアント側に表示するhtmlを生成します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
foreach((array)$obj as $key => $val){ if(strcmp($key, "rest") == 0){ echo "rest_count:".count($val); foreach($val as $restArray){ if($device_flag == 1){ $rest_html = $rest_html."$<div class='restaurant_data'><a href='"; if(checkString($restArray->{'url'})) $rest_html = $rest_html.$restArray->{'url'}."' target='_blank'>"; if(checkString($restArray->{'name'})) $rest_html = $rest_html."<div class='rest_name'>".$restArray->{'name'}; if(checkString($restArray->{'category'})) $rest_html = $rest_html." <span class='rest_category'>".$restArray->{'category'}."</span></div>"; if(checkString($restArray->{'image_url'}->{'shop_image1'})) $rest_html = $rest_html."<div class='rest_photo'><img src='".$restArray->{'image_url'}->{'shop_image1'}."' width=100 height=100>提供:ぐるなび</div>"; $rest_html = $rest_html."<div class='rest_info'><table>"; if(checkString($restArray->{'access'}->{'line'})) $rest_html = $rest_html."<tr><td class='rest_head'>access</td><td>".(string)$restArray->{'access'}->{'line'}." "; if(checkString($restArray->{'access'}->{'station'})) $rest_html = $rest_html.(string)$restArray->{'access'}->{'station'}." "; if(checkString($restArray->{'access'}->{'walk'})) $rest_html = $rest_html.(string)$restArray->{'access'}->{'walk'}."分</td></tr>"; if(checkString($restArray->{'budget'})) $rest_html = $rest_html."<tr><td class='rest_head'>budget</td><td>平均".(string)$restArray->{'budget'}."円</td>"; if(checkString($restArray->{'opentime'})) $rest_html = $rest_html."</tr><tr><td class='rest_head'>time</td><td>".(string)$restArray->{'opentime'}."</td></tr>"; if(checkString($restArray->{'pr'}->{'pr_short'})) $rest_html = $rest_html."<tr><td class='rest_head'>profile</td><td>".(string)$restArray->{'pr'}->{'pr_short'}."</td></tr>"; $rest_html = $rest_html."</table></div></a><div class='clear'></div></div>"; }else{ $rest_html = $rest_html.",<div class='restaurant_data'><a href='"; if(checkString($restArray->{'url'})) $rest_html = $rest_html.$restArray->{'url'}."' target='_blank'>"; if(checkString($restArray->{'name'})) $rest_html = $rest_html."<div class='rest_name'>".$restArray->{'name'}; if(checkString($restArray->{'category'})) $rest_html = $rest_html." <span class='rest_category'>".$restArray->{'category'}."</span></div>"; if(checkString($restArray->{'image_url'}->{'shop_image1'})) $rest_html = $rest_html."<div class='rest_photo'><img src='".$restArray->{'image_url'}->{'shop_image1'}."' width=60 height=60>提供:ぐるなび</div>"; $rest_html = $rest_html."</a><div class='clear'></div><div class='rest_info'><table>"; if(checkString($restArray->{'access'}->{'line'})) $rest_html = $rest_html."<tr><td class='rest_head'>access</td><td>".(string)$restArray->{'access'}->{'line'}." "; if(checkString($restArray->{'access'}->{'station'})) $rest_html = $rest_html.(string)$restArray->{'access'}->{'station'}." "; if(checkString($restArray->{'access'}->{'walk'})) $rest_html = $rest_html.(string)$restArray->{'access'}->{'walk'}."分</td></tr>"; if(checkString($restArray->{'budget'})) $rest_html = $rest_html."<tr><td class='rest_head'>budget</td><td>平均".(string)$restArray->{'budget'}."円</td>"; if(checkString($restArray->{'opentime'})) $rest_html = $rest_html."</tr><tr><td class='rest_head'>time</td><td>".(string)$restArray->{'opentime'}."</td></tr>"; if(checkString($restArray->{'pr'}->{'pr_short'})) $rest_html = $rest_html."<tr><td class='rest_head'>profile</td><td>".(string)$restArray->{'pr'}->{'pr_short'}."</td></tr>"; $rest_html = $rest_html."</table></div></div>"; } } } echo $rest_html; } |
以上が、一通りの動作となります。
| (5) 補足:現在位置取得
GPSによる現在位置の取得についてですが、
ユーザのプライバシー保護の観点から、2016年4月以降の最新のChromeにおいて
ユーザの現在位置の取得は暗号化されたセキュアな通信(https)のみに限定されました。
なお、今後、Safariなど他のブラウザにおいても現在位置の取得はhttps接続のみに
限定されることが予想されております。
対策は、httpsサイトにしましょう、ということに尽きます。
もはや、Googleはすべての通信をhttps化推奨としており、
検索順位においてhttps化サイトを優先することを公言しております。
なお、awsでは、AWS Certification Managerで無料で証明書を発行しhttps化するフローが整備されており、
今回のgurunavisionでもawsでhttps化フローを行いました。
以上、gurunavisionのご紹介となりました。
これでレストラン探しも怖くないですね。
あとは、肝心の、そもそもの飲みにいく機会、飲みにいく相手、を待ちましょう。

















