GoogleのCloud Vision APIをrubyで実装してrakeタスクにしたった

前から気になっていた [Cloud Vision API](https://cloud.google.com/vision/?hl=ja) を触ってみたのでメモ。


## 事前準備

Cloud Vision APIを使うには、Google Cloud Platformへ登録が必要である。未登録の場合、[Cloud Vision API](https://cloud.google.com/vision/?hl=ja)の「無料トライアル」ボタンを押して登録しよう。

登録したら、APIキー(サーバーキー)の払い出しをして、APIを叩けるようにしよう。

[Cloud Vision APIの使い方まとめ (サンプルコード付き)](https://syncer.jp/cloud-vision-api)が参考になるので、困ったらこちらで。


## 実装

ローカルマシンにあるファイルで試してみる。やることは大きく３つ。

* 画像をbase64にエンコードする
* エンコード済の画像を含めて、APIリクエストパラメータを組み立てる
* Cloud Vision APIにリクエストを投げる

というわけでサンプルコードを晒しておこう。

```ruby
require 'base64'
require 'json'
require 'net/https'

IMAGE_FILE = './sample.jpg'

API_KEY = 'xxxxxxxx'
API_URL = "https://vision.googleapis.com/v1/images:annotate?key=#{API_KEY}"

# 画像をbase64にエンコード
base64_image = Base64.strict_encode64(File.new(IMAGE_FILE, 'rb').read)

# APIリクエスト用のJSONパラメータの組み立て
body = {
  requests: [{
    image: {
      content: base64_image
    },
    features: [
      {
        type: 'LABEL_DETECTION',
        maxResults: 5
      }
    ]
  }]
}.to_json

# Google Cloud Vision APIにリクエスト投げる
uri = URI.parse(API_URL)
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
request = Net::HTTP::Post.new(uri.request_uri)
request["Content-Type"] = "application/json"
response = https.request(request, body)

# APIレスポンス出力
puts response.body
```

gemを使わずに実装したので、リクエストを投げるトコロが若干雑然としているが、Cloud Vision API 自体は非常にシンプルに使うことができる。


## 検証

JSONパラメータを構築している箇所では```LABEL_DETECTION ```と指定しているが、ココを変更すると、物体認識以外も行える。主な機能は以下のとおり。

* 物体認識 : LABEL_DETECTION
* 顔認識 : FACE_DETECTION
* ロゴ認識 : LOGO_DETECTION
* ランドマーク認識 : LANDMARK_DETECTION
* テキスト認識 : TEXT_DETECTION
* エログロ認識 : SAFE_SEARCH_DETECTION

いくつか試してみよう。


### LABEL_DETECTION

とりあえず、平和にネコ画像で試してみる。レスポンスを見るとちゃんとネコって認識された！

#### リクエスト画像

![cat.jpg](https://qiita-image-store.s3.amazonaws.com/0/39241/6349bc8f-b4cf-4c1c-33a7-a5efc4a95c84.jpeg "cat.jpg")
https://www.pakutaso.com/20160344074post-7250.html

#### レスポンス

```json
{
  "responses": [
    {
      "labelAnnotations": [
        {
          "mid": "/m/01l7qd",
          "description": "whiskers",
          "score": 0.97826087
        },
        {
          "mid": "/m/0jbk",
          "description": "animal",
          "score": 0.93817031
        },
        {
          "mid": "/m/04rky",
          "description": "mammal",
          "score": 0.92028791
        },
        {
          "mid": "/m/01yrx",
          "description": "cat",
          "score": 0.88385665
        },
        {
          "mid": "/m/0hjzp",
          "description": "kitten",
          "score": 0.8793053
        }
      ]
    }
  ]
}
```


### FACE_DETECTION

なんだか怪しげな男性の画像を投げてみた。目や唇などのかなり細かい情報が返ってきている。

joyLikelihood が ```LIKELY``` になっていて、感情認識の結果は「楽しい」ということらしい。まぁ、楽しいってよりは怪しいだけど。

#### リクエスト画像

![face2.jpg](https://qiita-image-store.s3.amazonaws.com/0/39241/2886665b-efaa-1b7c-004d-7bd574bc913d.jpeg "face2.jpg")
https://www.pakutaso.com/201410082832-1.html

#### レスポンス

```json
{
  "responses": [
    {
      "faceAnnotations": [
        {
          "boundingPoly": {
            "vertices": [
              {
                "x": 148,
                "y": 29
              },
              {
                "x": 270,
                "y": 29
              },
              {
                "x": 270,
                "y": 171
              },
              {
                "x": 148,
                "y": 171
              }
            ]
          },
          "fdBoundingPoly": {
            "vertices": [
              {
                "x": 161,
                "y": 71
              },
              {
                "x": 247,
                "y": 71
              },
              {
                "x": 247,
                "y": 157
              },
              {
                "x": 161,
                "y": 157
              }
            ]
          },
          "landmarks": [
            {
              "type": "LEFT_EYE",
              "position": {
                "x": 181.09821,
                "y": 99.2435,
                "z": 0.0002638482
              }
            },
            {
              "type": "RIGHT_EYE",
              "position": {
                "x": 214.13016,
                "y": 97.099365,
                "z": -12.981007
              }
            },
            {
              "type": "LEFT_OF_LEFT_EYEBROW",
              "position": {
                "x": 167.40213,
                "y": 91.100555,
                "z": 7.2265382
              }
            },
            {
              "type": "RIGHT_OF_LEFT_EYEBROW",
              "position": {
                "x": 184.34164,
                "y": 91.6631,
                "z": -10.791
              }
            },
            {
              "type": "LEFT_OF_RIGHT_EYEBROW",
              "position": {
                "x": 201.12782,
                "y": 90.614441,
                "z": -17.404898
              }
            },
            {
              "type": "RIGHT_OF_RIGHT_EYEBROW",
              "position": {
                "x": 225.91609,
                "y": 88.156143,
                "z": -15.385879
              }
            },
            {
              "type": "MIDPOINT_BETWEEN_EYES",
              "position": {
                "x": 193.25211,
                "y": 98.665451,
                "z": -13.574019
              }
            },
            {
              "type": "NOSE_TIP",
              "position": {
                "x": 190.78255,
                "y": 123.63931,
                "z": -20.968615
              }
            },
            {
              "type": "UPPER_LIP",
              "position": {
                "x": 196.33002,
                "y": 135.32417,
                "z": -10.72403
              }
            },
            {
              "type": "LOWER_LIP",
              "position": {
                "x": 198.57047,
                "y": 145.97295,
                "z": -6.6631284
              }
            },
            {
              "type": "MOUTH_LEFT",
              "position": {
                "x": 186.68929,
                "y": 138.16307,
                "z": 4.5638247
              }
            },
            {
              "type": "MOUTH_RIGHT",
              "position": {
                "x": 216.51965,
                "y": 134.52066,
                "z": -5.9020534
              }
            },
            {
              "type": "MOUTH_CENTER",
              "position": {
                "x": 197.80579,
                "y": 139.89142,
                "z": -7.4092579
              }
            },
            {
              "type": "NOSE_BOTTOM_RIGHT",
              "position": {
                "x": 206.1855,
                "y": 124.34725,
                "z": -10.612206
              }
            },
            {
              "type": "NOSE_BOTTOM_LEFT",
              "position": {
                "x": 187.03342,
                "y": 124.05929,
                "z": -3.0776432
              }
            },
            {
              "type": "NOSE_BOTTOM_CENTER",
              "position": {
                "x": 195.05676,
                "y": 128.69412,
                "z": -11.879737
              }
            },
            {
              "type": "LEFT_EYE_TOP_BOUNDARY",
              "position": {
                "x": 177.71968,
                "y": 97.635468,
                "z": -1.9731181
              }
            },
            {
              "type": "LEFT_EYE_RIGHT_CORNER",
              "position": {
                "x": 186.35037,
                "y": 99.970093,
                "z": -1.8754189
              }
            },
            {
              "type": "LEFT_EYE_BOTTOM_BOUNDARY",
              "position": {
                "x": 179.83739,
                "y": 102.04105,
                "z": 0.64033604
              }
            },
            {
              "type": "LEFT_EYE_LEFT_CORNER",
              "position": {
                "x": 173.11966,
                "y": 99.7238,
                "z": 6.581109
              }
            },
            {
              "type": "LEFT_EYE_PUPIL",
              "position": {
                "x": 177.93286,
                "y": 100.0177,
                "z": 0.06591022
              }
            },
            {
              "type": "RIGHT_EYE_TOP_BOUNDARY",
              "position": {
                "x": 212.77274,
                "y": 95.898186,
                "z": -15.496446
              }
            },
            {
              "type": "RIGHT_EYE_RIGHT_CORNER",
              "position": {
                "x": 221.93607,
                "y": 97.035385,
                "z": -12.34076
              }
            },
            {
              "type": "RIGHT_EYE_BOTTOM_BOUNDARY",
              "position": {
                "x": 214.28592,
                "y": 99.752068,
                "z": -12.837732
              }
            },
            {
              "type": "RIGHT_EYE_LEFT_CORNER",
              "position": {
                "x": 206.82005,
                "y": 98.930138,
                "z": -9.9026051
              }
            },
            {
              "type": "RIGHT_EYE_PUPIL",
              "position": {
                "x": 213.9201,
                "y": 98.2251,
                "z": -13.893643
              }
            },
            {
              "type": "LEFT_EYEBROW_UPPER_MIDPOINT",
              "position": {
                "x": 174.36987,
                "y": 87.284866,
                "z": -4.6636252
              }
            },
            {
              "type": "RIGHT_EYEBROW_UPPER_MIDPOINT",
              "position": {
                "x": 212.43112,
                "y": 85.409851,
                "z": -19.328856
              }
            },
            {
              "type": "LEFT_EAR_TRAGION",
              "position": {
                "x": 174.67477,
                "y": 111.92154,
                "z": 54.401321
              }
            },
            {
              "type": "RIGHT_EAR_TRAGION",
              "position": {
                "x": 251.83156,
                "y": 108.63575,
                "z": 22.954367
              }
            },
            {
              "type": "FOREHEAD_GLABELLA",
              "position": {
                "x": 192.27304,
                "y": 91.03653,
                "z": -15.46795
              }
            },
            {
              "type": "CHIN_GNATHION",
              "position": {
                "x": 202.21428,
                "y": 161.46428,
                "z": 0.617989
              }
            },
            {
              "type": "CHIN_LEFT_GONION",
              "position": {
                "x": 175.51978,
                "y": 138.10352,
                "z": 41.916679
              }
            },
            {
              "type": "CHIN_RIGHT_GONION",
              "position": {
                "x": 247.22699,
                "y": 134.34093,
                "z": 14.262062
              }
            }
          ],
          "rollAngle": -5.8018222,
          "panAngle": -21.274296,
          "tiltAngle": -7.6548777,
          "detectionConfidence": 0.99945271,
          "landmarkingConfidence": 0.66470236,
          "joyLikelihood": "LIKELY",
          "sorrowLikelihood": "VERY_UNLIKELY",
          "angerLikelihood": "VERY_UNLIKELY",
          "surpriseLikelihood": "VERY_UNLIKELY",
          "underExposedLikelihood": "VERY_UNLIKELY",
          "blurredLikelihood": "VERY_UNLIKELY",
          "headwearLikelihood": "UNLIKELY"
        }
      ]
    }
  ]
}
```

### SAFE_SEARCH_DETECTION

画像は自粛するが、適当に拾ってきたエロ画像で試してみた。ちゃんと、adultが```VERY_LIKELY ```になっているｗ

#### レスポンス

```json
{
  "responses": [
    {
      "safeSearchAnnotation": {
        "adult": "VERY_LIKELY",
        "spoof": "UNLIKELY",
        "medical": "LIKELY",
        "violence": "VERY_UNLIKELY"
      }
    }
  ]
}
```


## rake タスク

さすがに、実装が雑すぎて検証しづらいのでrakeタスクにしてみた。楽に検証できるよう、ローカルにあるファイルだけじゃなく、Webにある画像を直接指定できるようにしている。

https://github.com/tmknom/ruby-cloud-vision


### インストール方法

```bash
git clone git@github.com:tmknom/ruby-cloud-vision.git && cd ruby-cloud-vision
bundle install --path=vendor/bundle
echo 'export GCP_API_KEY=[your_api_key]' >> .env
```

### 使い方

```bash
# Display all tasks.
$ bundle exec rake -T

# Run face detection.
$ bundle exec rake vision:face[<path/to/image>]

# Compute a set of properties about the image (such as the image's dominant colors).
$ bundle exec rake vision:image_properties[<path/to/image>]

# Run label detection.
$ bundle exec rake vision:label[<path/to/image>]

# Run landmark detection.
$ bundle exec rake vision:landmark[<path/to/image>]

# Run logo detection.
$ bundle exec rake vision:logo[<path/to/image>]

# Run various computer vision models to compute image safe-search properties.
$ bundle exec rake vision:safe_search[<path/to/image>]

# Run OCR.
$ bundle exec rake vision:text[<path/to/image>]

# Unspecified feature type.
$ bundle exec rake vision:unspecified[<path/to/image>]
```

### 実行例

ローカルにある画像

```bash
$ bundle exec rake vision:label[/Users/sample_user/sample.jpg]
```

Web上にある画像

```bash
$ bundle exec rake vision:label[http://example.com/sample.jpg]
```


## まとめ

というわけで、Cloud Vision APIを叩いて遊んでみた。

使い方次第で面白いサービスが手軽に作れそうだね。スゴイ時代になったもんだ。