Google Cloud Visionを使ってなんか作ってみた話

# Google Cloud Visionとは
2016年2月18日に「Google Cloud Vision API」のパブリックβ版が公開されました。
普段Googleフォトを使っているのですが、保存した画像を自動でカテゴリ毎にグルーピングしてくれる機能があって、結構精度が良くて驚いていたんですね。
恐らくここに使われているであろうAPIがベータ版でありながら公開されたということで、これは何か作ってみたいと思ったわけです。

Google Cloud Visionって何なのかって話ですが、以下引用。
> Google Cloud Vision API は、強力な機械学習モデルの能力を活用することで、画像の内容を理解できるアプリケーションの開発を可能にします。Cloud Vision API は、画像を数千のカテゴリ（たとえば、「ヨット」「ライオン」「エッフェル塔」など）にすばやく分類する機能や、画像に映る個々の物体や人物の顔を検知する機能、画像に含まれる活字体の文字を認識して読み取る機能などを提供します。 この Cloud Vision API により、アプリケーションで扱う多数の画像のタグ付け、不適切な画像の検出、画像の意味の分析に基づく新しいマーケティング手法への応用が可能です。画像はリクエストの中に含めてアップロードすることで分析します。将来のリリースでは、Google Cloud Storage 上の画像ストレージとの連携も可能になる予定です。
出典：[Google Cloud Platform](https://cloud.google.com/vision/?hl=ja)

結局何ができるのかというと…
## Cloud Visionでできること（Feature Type一覧）
* FACE_DETECTION（顔検出）
* LANDMARK_DETECTION（観光名所などの名前）
* LOGO_DETECTION（ロゴの検出）
* LABEL_DETECTION（カテゴリの検出）
* TEXT_DETECTION（文字の検出）
* SAFE_SEARCH_DETECTION（セーフサーチ）
* IMAGE_PROPERTIES（色データ検出）

# ちょっと試してみた
## まずはCloud Visionを使えるようにする
[Cloud Vision APIの使い方まとめ (サンプルコード付き)](https://syncer.jp/cloud-vision-api)
こちらを参考にさせていただきました。
使えるようにするところまではここを参考にすれば簡単に設定できました。

## デモ用スクリプトを作る
```
<?php
# cloudVisionDemo.php

// APIキー
$api_key = "<your APIkey>" ;

// 画像へのパス
$image_path = $argv[1];

// Feature Type
$feature = $argv[2];

// パラメータ設定
$param = array("requests" => array());
$item["image"] = array("content" => base64_encode(file_get_contents($image_path)));
$item["features"] = array(array("type" => $feature, "maxResults" => 3));
$param["requests"][] = $item;

// リクエスト用のJSONを作成
$json = json_encode($param);

// リクエストを実行
$curl = curl_init() ;
curl_setopt($curl, CURLOPT_URL, "https://vision.googleapis.com/v1/images:annotate?key=" . $api_key);
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-Type: application/json"));
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, 15);
curl_setopt($curl, CURLOPT_POSTFIELDS, $json);
$res1 = curl_exec($curl);
$res2 = curl_getinfo($curl);
curl_close($curl);

// 取得したデータ
$json = substr($res1, $res2["header_size"]);
$array = json_decode($json, true);

// 出力
var_dump($array);
```
こんな感じでコマンドを入力します。

```
php cloudVisionDemo.php <URL> <Feature Type>
```

## リクエストしてみる
### LABEL_DETECTION
■ 画像
![sea.jpg](https://qiita-image-store.s3.amazonaws.com/0/105205/b1282d63-42ee-c870-69f1-575e222cd06c.jpeg)
■ コマンド実行

```
$ php cloudVisionDemo.php 'https://qiita-image-store.s3.amazonaws.com/0/105205/b1282d63-42ee-c870-69f1-575e222cd06c.jpeg' LABEL_DETECTION
```

■ 実行結果

```
array(1) {
  ["responses"]=>
  array(1) {
    [0]=>
    array(1) {
      ["labelAnnotations"]=>
      array(3) {
        [0]=>
        array(3) {
          ["mid"]=>
          string(8) "/m/06npx"
          ["description"]=>
          string(3) "sea"
          ["score"]=>
          float(0.98956168)
        }
        [1]=>
        array(3) {
          ["mid"]=>
          string(9) "/m/01814z"
          ["description"]=>
          string(12) "bodyboarding"
          ["score"]=>
          float(0.80245268)
        }
        [2]=>
        array(3) {
          ["mid"]=>
          string(9) "/m/02jwqh"
          ["description"]=>
          string(8) "vacation"
          ["score"]=>
          float(0.77475709)
        }
      }
    }

```
ちゃんと海を認識しています。ボディーボードはしてないけどバケーションっちゃバケーションですね。

### LANDMARK_DETECTION
■ 画像
![426375_389655517718422_1097042322_n.jpg](https://qiita-image-store.s3.amazonaws.com/0/105205/d1e74545-b680-7753-422c-72fd2500c02a.jpeg)

■ コマンド実行

```
$ php cloudVisionDemo.php 'https://qiita-image-store.s3.amazonaws.com/0/105205/d1e74545-b680-7753-422c-72fd2500c02a.jpeg' LANDMARK_DETECTION
```

■ 実行結果

```
array(1) {
  ["responses"]=>
  array(1) {
    [0]=>
    array(1) {
      ["landmarkAnnotations"]=>
      array(3) {
        [0]=>
        array(5) {
          ["mid"]=>
          string(10) "/m/06423sw"
          ["description"]=>
          string(7) "Merlion"
          ["score"]=>
          float(0.80336565)
          ["boundingPoly"]=>
          array(1) {
            ["vertices"]=>
            array(4) {
              [0]=>
              array(2) {
                ["x"]=>
                int(140)
                ["y"]=>
                int(16)
              }
              [1]=>
              array(2) {
                ["x"]=>
                int(458)
                ["y"]=>
                int(16)
              }
              [2]=>
              array(2) {
                ["x"]=>
                int(458)
                ["y"]=>
                int(289)
              }
              [3]=>
              array(2) {
                ["x"]=>
                int(140)
                ["y"]=>
                int(289)
              }
            }
          }
          ["locations"]=>
          array(1) {
            [0]=>
            array(1) {
              ["latLng"]=>
              array(2) {
                ["latitude"]=>
                float(1.286783)
                ["longitude"]=>
                float(103.85441958904)
              }
            }
          }
        }
...
```
ちゃんとマーライオンを認識してる！緯度経度情報もある！ぱなぃ

### LOGO_DETECTION
■ 画像
![Yahooのロゴ](http://k.yimg.jp/images/top/sp2/cmn/logo-ns-131205.png)

■ コマンド実行

```
$ php cloudVisionDemo.php 'http://k.yimg.jp/images/top/sp2/cmn/logo-ns-131205.png' LOGO_DETECTION
```

■ 実行結果

```
array(1) {
  ["responses"]=>
  array(1) {
    [0]=>
    array(1) {
      ["logoAnnotations"]=>
      array(1) {
        [0]=>
        array(3) {
          ["description"]=>
          string(8) "Yahoo BB"
          ["score"]=>
          float(0.46835244)
          ["boundingPoly"]=>
          array(1) {
            ["vertices"]=>
            array(4) {
              [0]=>
              array(2) {
                ["x"]=>
                int(16)
                ["y"]=>
                int(9)
              }
              [1]=>
              array(2) {
                ["x"]=>
                int(207)
                ["y"]=>
                int(9)
              }
              [2]=>
              array(2) {
                ["x"]=>
                int(207)
                ["y"]=>
                int(40)
              }
              [3]=>
              array(2) {
                ["x"]=>
                int(16)
                ["y"]=>
                int(40)
              }
            }
          }
        }
      }
    }
  }
}
```
Yahoo BBかぁ…惜しい。

### TEXT_DETECTION
■ 画像
![401412_389664421050865_725039968_n.jpg](https://qiita-image-store.s3.amazonaws.com/0/105205/5019c94b-1193-730b-5b22-039d6ac8eb9c.jpeg)
■ コマンド実行

```
$ php cloudVisionDemo.php 'https://qiita-image-store.s3.amazonaws.com/0/105205/5019c94b-1193-730b-5b22-039d6ac8eb9c.jpeg' TEXT_DETECTION
```

■ 実行結果

```
array(1) {
  ["responses"]=>
  array(1) {
    [0]=>
    array(1) {
      ["textAnnotations"]=>
      array(1) {
        [0]=>
        array(3) {
          ["locale"]=>
          string(2) "lb"
          ["description"]=>
          string(14) "LOUIS VUITTON
"
          ["boundingPoly"]=>
          array(1) {
            ["vertices"]=>
            array(4) {
              [0]=>
              array(2) {
                ["x"]=>
                int(176)
                ["y"]=>
                int(225)
              }
              [1]=>
              array(2) {
                ["x"]=>
                int(466)
                ["y"]=>
                int(225)
              }
              [2]=>
              array(2) {
                ["x"]=>
                int(466)
                ["y"]=>
                int(272)
              }
              [3]=>
              array(2) {
                ["x"]=>
                int(176)
                ["y"]=>
                int(272)
              }
            }
          }
        }
      }
    }
  }
}
```

完璧。

### SAFE_SEARCH_DETECTION
■ 画像
![409299_292482390851424_58100342_n.jpg](https://qiita-image-store.s3.amazonaws.com/0/105205/487201ee-3f76-1bd1-9bc2-beaa94083b1c.jpeg)

■ コマンド実行

```
$ php cloudVisionDemo.php 'https://qiita-image-store.s3.amazonaws.com/0/105205/487201ee-3f76-1bd1-9bc2-beaa94083b1c.jpeg' SAFE_SEARCH_DETECTION
```

■ 実行結果

```
array(1) {
  ["responses"]=>
  array(1) {
    [0]=>
    array(1) {
      ["safeSearchAnnotation"]=>
      array(4) {
        ["adult"]=>
        string(13) "VERY_UNLIKELY"
        ["spoof"]=>
        string(13) "VERY_UNLIKELY"
        ["medical"]=>
        string(13) "VERY_UNLIKELY"
        ["violence"]=>
        string(13) "VERY_UNLIKELY"
      }
    }
  }
}
```

当然エロくもグロくもないです。


# で、これを使ってどんなサービスを作ろうか
## Cloud Visionを使った事例
[GolangとGoogle Cloud Vision APIを使って牛の画像認識をする](http://developersnote.jp/google-apps/google-cloud-vision-api.html)

[Node.jsとGoogle Cloud Vision API使って色々な画像認識試してみた。](https://indie-inc.github.io/2015/12/24/cloud-vision-api/)

[Cloud Vision APIの凄さを伝えるべくRasPi botとビデオを作った話](http://qiita.com/kazunori279/items/768c7fdf96cdf45a9d16)

まだ試してみた系が多い。サービスっぽいものを作ってる人はいない…！

## 無限の猿定理
以下引用です。
> 無限の猿定理（むげんのさるていり、英語: infinite monkey theorem）はランダムに文字列を作り続ければどんな文字列もいつかはできあがるという定理である。比喩的に「猿がタイプライターの鍵盤をいつまでもランダムに叩きつづければ、ウィリアム・シェイクスピアの作品を打ち出す」などと表現されるため、この名がある。
出典：[Wiki](https://ja.wikipedia.org/wiki/%E7%84%A1%E9%99%90%E3%81%AE%E7%8C%BF%E5%AE%9A%E7%90%86)

これと同じように、
「コンピュータがランダムに画像を生成し続ければ、いつかはモナリザを打ち出す」
という発想でモノを作ってみます。


具体的には、１ピクセルずつがランダムな256色で構成される縦横比１：１の画像をImageMagickで生成して、Cloud Visionに食わせた結果をTwitterに投稿します。
これを20分に1回実行させます。
いつかはモナリザ（じゃなくてもランダムに生成された画像がたまたま何かに似たモノ）になることを期待してみよう、というものです。

# 実際に作ってみる
## ImageMagickで画像を生成（nodejs）

```
'use strict';
var async = require('async');
var gm = require('gm').subClass({ imageMagick: true });
var dot = 1;
var width = 150;
var height = width;
var imgSize = dot * width;
var x1 = new Array();
var y1 = new Array();

var rand = function () {
	var array = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] ;
	return array[ Math.floor( Math.random() * array.length ) ];
}

function colorGen(){
    return '#' + rand() + rand() + rand();
}

var resultbuff = null;
async.waterfall([
    function makeRandomColor(callback) {
        console.log('make random color');
        var colorArray = new Array();
        for (var i = 0; i < height * width; i++) {
            colorArray[i] = colorGen();
        }
        callback(null, colorArray);
    },
    function makeFirstImage(data, callback) {
        console.log('make first image');
        gm(imgSize, imgSize, '#FFF').toBuffer('PNG', function(err, buffer) {
            if (err) {
                console.log('error');
            } else {
                resultbuff = buffer;
                callback(err, data);
            }
        });
    },
    function calc(data, callback) {
        var id = 0;
        async.eachSeries(data, function(c, callback) {
            console.log('id : ' + id);
            var id_x = id % width;
            var id_y = Math.floor(id / width);
            x1[id] = id_x * dot;
            y1[id] = id_y * dot;
            id++;
            callback(null);
        }, function(err) {
            callback(err, data);
        });
    },
    function makeImage(data, callback) {
        var id = 0;
        console.log('hoge!');
        async.eachSeries(data, function(c, callback) {
            gm(resultbuff).fill(c).drawLine(x1[id], y1[id], x1[id], y1[id]).toBuffer('PNG', function(err, buffer) {
            	console.log('x1: ' + x1[id]);
            	console.log('y1: ' + y1[id]);
                resultbuff = buffer;
            	id++;
                callback(null);
            });
        }, function(err) {
            callback(err);
        });
    },
    function writeImage(callback) {
        gm(resultbuff).write('<path>' + process.argv[2] + '.png', function(err) {
            if (err) {
                console.log('error');
            } else {
                callback(null);
            }
        });
    }
], function (error, result) {
    if (error) {
        console.log('error');
    }
});
```
これで150x150のpng画像ができあがります。
こんな感じ。
![生成された画像](https://pbs.twimg.com/media/CdKUvN3UEAANf-p.jpg)

## 生成した画像をCloudVisionに投げて結果を表示(PHP)

```
<?php
// APIキー
$api_key = "<your APIkey>" ;

// 画像へのパス
$image_path = "<path>" . $argv[1];

// リクエスト用のJSONを作成
$json = json_encode(array(
    "requests" => array(
        array(
            "image" => array(
                "content" => base64_encode(file_get_contents($image_path)),
            ),
            "features" => array(
                array(
                    "type" => "LOGO_DETECTION",
                    "maxResults" => 3,
                ) ,
                array(
                    "type" => "LABEL_DETECTION",
                    "maxResults" => 3,
                ),
            ),
        ),
    ),
));

// リクエストを実行
$curl = curl_init() ;
curl_setopt($curl, CURLOPT_URL, "https://vision.googleapis.com/v1/images:annotate?key=" . $api_key);
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-Type: application/json"));
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, 15);
curl_setopt($curl, CURLOPT_POSTFIELDS, $json);
$res1 = curl_exec($curl);
$res2 = curl_getinfo($curl);
curl_close($curl);

// 取得したデータ
$json = substr($res1, $res2["header_size"]);             // 取得したJSON
$array = json_decode($json, true);
$header = substr($res1, 0, $res2["header_size"]);        // レスポンスヘッダー

// 出力
echo $array['responses'][0]['labelAnnotations'][0]['description'] . "(" . $array['responses'][0]['labelAnnotations'][0]['score'] . "点)";
```

これの実行結果は

```
flower(0.70933831点) 
```
です。花と認識されたようです。


## Twitterへ投稿(nodejs)

```
var Twitter = require('twitter');
var async = require('async');
var fs = require('fs');

async.waterfall([
    function getImage(callback) {
        var file = fs.readFile('<path>', function(err, data) {
            callback(err, new Buffer(data));
        });
    },
    function uploadImage(data, callback) {
        var client = new Twitter({
            consumer_key: '',
            consumer_secret: '',
            access_token_key: '',
            access_token_secret: ''
        });
        var params = {media: data};
        client.post('media/upload', params, function(error, result, response){
            if (!error) {
                console.log(result);
                // Lets tweet it
                var status = {
                    status: process.argv[2],
                    media_ids: result.media_id_string
                }
                client.post('statuses/update', status, function(error, tweet, response){
                    if (!error) {
                        console.log(tweet);
                        callback(error, tweet.id_str);
                    } else {
                        callback(error, result);
                    }
                });
            } else {
                console.log(error);
                callback(error);
            }
        });
    }
], function (error, result) {
    if (error) {
        console.log('fault');
    } else {
    }
});
```
投稿結果がこれ。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">flower(0.70933831点) <a href="https://t.co/CQrGKRfdm1">pic.twitter.com/CQrGKRfdm1</a></p>&mdash; owari (@owariowarif) <a href="https://twitter.com/owariowarif/status/707791634226872321">2016, 3月 10</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

# 課題と今後の展開
* Twitterのアカウントが「フカセ暗号ジェネレーター」使い回しなのを直す。
* プロジェクトページ作って、このプロジェクトのコンセプトの説明とか、どの画像がどのように解釈されたのかを一覧表示できるようにしたい。
* 画像をもっと大きくしたいけど生成処理に時間が掛かってしまう(150x150で20分くらい)
* 本物のモナリザの画像をLABEL_DETECTIONで食わせてもモナリザが結果に出ない←致命的
  * LOGO_DETECTIONだったらモナリザとして認識するのですが、LOGO_DETECTIONはロゴのみを返すので結果として返ってこない場合のほうが多い。LOGO_DETECTIONして結果なしだったらLABEL_DETECTIONの結果を入れるとかするしか。
* Cloud Vision使ってもっと面白いもの作れそう。なんか無いですかね（アイディア募集中）