Railsで画像プレビュー時にGoogle Cloud Vision APIに連携し、データラベルを取得する

# はじめに
Ruby on RailsでWebアプリのポートフォリオを作成している。Rails初学者です。
今回は、Google Cloud Vision APIを使って投稿前の画像プレビュー時点で、簡単な画像解析をする作業を実装しましたので、その覚書として残しておきます。

## 概要
今回実装した内容は以下のような機能です。
1. 投稿画面で、画像をアップロードすると、画像プレビューと同時に「空の画像か」を判別し、空の画像ではない場合は「空の画像ではありません」を出力する
2. 空の画像と判定された場合、解析結果の上位４つのラベルをタグとして自動的に出力する

出来上がりはこんな感じです。
![gitアニメver2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176783/8e994665-4595-bfc9-04c9-9b6923230722.gif)


## やろうと思ったきっかけ
自分が撮った空をシェアするSNS「is your sky」というwebサービスを作成しています。
https://isyoursky-web.herokuapp.com/

あくまでも空が主役のサービスなので、**空が写っている画像**をユーザには投稿してもらいたい！
過激な画像などを投稿出来ないようにする機能はよく見るのですが、それとは少し趣旨が異なります。今回は、あくまでも方針として、ユーザに**「空の画像を投稿するように促す」**必要がありました。
なので、一旦投稿画面で画像がプレビュー時に空の画像ではない場合、アラートを出してユーザ側に自主的に気づいてもらおうと考えました。それが１の機能です。

さらに、投稿画面にはタグ機能も備わっているのですが、写真に合ったタグが自動的に付与されると便利だな！と思い２の機能も作成しました。
__※ただし、この機能、実装後にわかったのですがラベルの文章があまりタグとして機能しません。
　割とSkyとかredとか単語が出てくるので、タグとしてあんまり意味をなさないんですよね・・
　一応、備忘録として残しておきますが私のwebアプリでは採用しない方針です。__


## Google Cloud Vision APIって？

Googleが提供している機械学習を活用した画像解析サービスVision AIのサービスの内の一つです。
>Google Cloud の Vision API は REST API や RPC API を介して強力な事前トレーニング済み機械学習モデルを提供します。画像にラベルを割り当てることで、事前定義済みの数百万のカテゴリに画像を高速に分類できます。オブジェクトや顔を検出し、印刷テキストや手書き文字を読み取り、有用なメタデータを画像カタログに作成します
ー[公式サイトより引用](https://cloud.google.com/vision?hl=ja)


Google以外にもIBMや、AWS、Windowなどがこのような画像解析APIを提供していますが、
オブジェクトの抽出、ラベリング機能の精度が高いGoogleが
今回実現したいことに一番マッチしているかな、と思い採用しました。
もともとGoogle Cloud Platformを使っていたというのも大きな要因です。


参考）
[画像解析API各サービスの比較](https://ad-ron.jp/?p=12364)
[Googleの画像認識APIは最強！！画像認識API徹底比較結果](https://syuuai.hatenablog.com/entry/2019/09/20/233214)

## 前提
googleアカウントで、Google Cloud Platformにログインしプロジェクトを作成、
サービスアカウントキーのjsonファイルをゲット済みであること。
この辺については、各種参考サイト様をご参照ください。
参考）
[Google Cloud Vision APIで画像分析をしてみよう【入門】](https://www.asobou.co.jp/blog/web/vision-api)
[RailsにGoogle Cloud Vision APIを導入し、簡単に過激な画像を検知する](https://qiita.com/ko30005/items/7e2366e9488be2c8584e)

# 環境
普段は、Docker for Macでコンテナを作成し、Railsを開発しています。

```
ruby 2.4.5
Ruby on rails 5.0.7.2
```

# 準備
google cloud vision APIを使うためにgemをインストールします

```:Gemfile
gem 'google-cloud-vision'
```

ひとまずbundle install

```:terminal
bundle install
```
※2020年10月現在、インストールされたverは以下の通りです。
今回はこの1.0.0における書き方で書いていきます。

```:Gemfile.lock
google-cloud-vision (1.0.0)
```

※google-cloud-visionは1.0.0リリースで大幅なアップデートがされているようです。
 以前のバージョンと書き方も変わっている部分もあるので、ご参考までに。
 Docに詳細が載っているので、詳しくは[こちら](https://googleapis.dev/ruby/google-cloud-vision/latest/file.MIGRATING.html)に・・・


# 実装の仕方
今回の場合、画像プレビューのタイミングで発火するjQueryのコードの中に書いていきます。
ユーザが画像を選択後、以下のような流れになります。

1. jQueryが発火
2. ユーザがアップロードした画像を非同期通信(Ajax)でcontrollerに送る
3. コントローラ内でVision APIへ連携、各種情報をインスタンス変数にセット
4. jBuilderでインスタンス変数を配列にしてjQuery側に返す
5. jQuery側で処理を行い画面に表示する

文字にすると若干ややこしいのですが、主に処理を行うのは、jsファイルとControllerなので、
その辺の処理を中心に見ていきます。

## コード

### View
まずはViewのコードです。
一般的なフォームに、画像プレビュー用のコードを含んだものです。
`image-preview`クラスの一番下に`image_isnot_sky`というidを持つspanクラスを追加しました。空の画像でないと判定された場合、この部分をhiddenを消去し、画面に文字として表示します。

タグ機能については、tag-itを使用しています。
参考）[tag-it](https://github.com/aehlke/tag-it)

```ruby:_form.html.slim
= form_with model: post, local: true do |f|
:
(中略)
  .form-group
    = f.label :image, "画像"
    = f.file_field :image, class:"form-control", id: 'post_img'
  .image-preview
    img[id="image_img_prev" src="#" class='hidden']
    - if post.persisted? && post.image?
      = image_tag post.image.url, class: 'image_present_img'
      label
        = f.check_box :remove_image
        | 画像を削除
    - else
      = f.hidden_field :avatar_cache
    span[id="image_isnot_sky" class="hidden"]
      | 空の画像ではない可能性があります。空の写真を投稿ください


  #post-tags-field
    = f.label "タグ"
    ul[id="post-tags"]
    = hidden_field_tag :tag_hidden, tag_list
:
```

### javascript(jQuery)

先ほど見た、Viewのfile_fieldにfileが読み込まれた時点でjQueryが発火します。
`$.ajax`の部分で、非同期通信を行って、成功すれば`done`に、失敗であれば`fail`に入ります。
urlには`controllerとメソッド名`、typeは`POST`、dataには、今回通信したいデータ(ここではformData)をおきます。

`processData:false`と`contentType:false`の部分については、わたしも理解が追いついていないのですが、これをはずしてしまうと、dataが加工されて送られてしまうためうまく読み込めなくなってしまいます。
（ここを書いていなくてしばらくエラーになってました・・・）

```javascript:post.js
:
  function readURL(input) {
    if (input.files && input.files[0]) {
      var reader = new FileReader();

      reader.onload = function (e) {
        //formDataでアップロードされたファイルを開く
        var formData = new FormData();
        formData.append("image_cache", input.files[0]);
        //ajax通信処理
        $.ajax({
          url: "/posts/check_cache_image",
          type: "POST",
          data: formData,
          dataType: "json",
          processData: false,
          contentType: false,
        }).done(function(data){
           var tag_list = data.data.tag_list
           var image_flag = data.data.image_flag

           if(image_flag == true){
             //アラートを表示する
             $('#image_isnot_sky').removeClass('hidden');
             //タグを削除する
             $("#post-tags").tagit("removeAll");
           }else{
             //アラートがすでに表示済みの場合、隠す
             $('#image_isnot_sky').addClass('hidden');
             //タグを削除する
             $("#post-tags").tagit("removeAll");

             //ラベルを元にタグを作成する
             $.each(tag_list, function(index, tag){
               $('#post-tags').tagit('createTag', tag);
             })
           }

        }).fail(function(){
          //エラーの場合、アラートを出す。
          alert('画像の読み込みに失敗しました');
        })
        //画像プレビューを行う
        $('#image_img_prev').attr('src', e.target.result);
      }
      reader.readAsDataURL(input.files[0]);
    }
  }
:

```

あと非同期通信でもroutesは必須です。
追記しておきます。

```ruby:route/.rb
Rails.application.routes.draw do
:
  post "posts/check_cache_image"
:
end
```

### JSON（jbuilder使用）

`done`の中で使用されている変数`data`は、jBuilderを使ってcontrollerから返されたデータがjson形式で格納されています。
jBuilderは、viewの同じフォルダ内に、`コントローラー名.json.jbuilder`を作成することで使用可能になります。
詳しくは以下を参照ください
参考）https://pikawaka.com/rails/jbuilder


```:check_cache_image.json.jbuilder

json.data do |data|
  json.image_flag @data[:image_flag]
  json.tag_list @data[:tag_list]
end
```



### Controller

ここからは少しずつ細かくみていきます。
まずは、Vision APIとの連携部分です。今回はAPIの内、[**「ラベル検出」**](https://cloud.google.com/vision/docs/labels?hl=ja)という機能を使います。

環境変数には、冒頭で話したサービスアカウントのjsonファイルのファイルパスを指定します。
※configの書き方は1.0.0より以下のように変更になっています。

```ruby:post_controller.rb
:
  def check_cache_image
    require "google/cloud/vision"

    #Vision APIの設定
    image_annotator  = Google::Cloud::Vision.image_annotator do | config |
      config.credentials = ENV["GOOGLE_APPLICATION_CREDENTIALS"]
    end
:
````

formDataで持ってきたimage_cacheを取得し、Vision APIへと渡します。
ここのコードは[公式のコード](https://cloud.google.com/vision/docs/libraries)をほぼそのまま参考にしました。
`label.description`でラベル名を取得しています。

```ruby:post_controller.rb
:
    #キャッシュ情報を取得する
    image = params[:image_cache].path
    #キャッシュをVision APIにレスポンスとして渡す。
    response = image_annotator.label_detection image: image

    #labelを配列に入れる
    label_list = []
    response.responses.each do |res|
      res.label_annotations.each do |label|
        label_list.push(label.description)
      end
    end
:
```

以降は検出されたラベルをもとに各種必要な情報を取得します。
1.ラベル内に"Sky"があれば、「空の写真」であると判断します
2.ラベルの上位４項目を取得し、タグとして表示します

```ruby:post_controller.rb
:
    #1.空の写真かを判定する
    if label_list.include?("Sky")
      @image_flag = false
    else
      @image_flag = true
    end

    #2.labelの先頭4つをタグとして持ってくる
    if label_list.length >= 4
      @tag_list = label_list[0..3]
    else
      @tag_list = label_list
    end
:
``` 

最後はJSONの処理です。

```ruby:post_controller.rb
:
    #jBuilderに送る準備
    @data = { tag_list: @tag_list, image_flag: @image_flag }
    
    respond_to do |format|
      format.html
      format.json
    end
  end
:
```

以上で、画像プレビューのタイミングで、「空の画像かを判別」し「タグを自動的に付与」することができます。
かなり無理くりかもしれませんが、ひとまず実装できたので・・・なにかミス等ございましたらご指摘いただけますと幸いです。


# 終わりに
Google Cloud Vision APIを使って、簡単な画像解析を実装できました。
何枚かテストで無作為に写真を送っているのですが、空が少しでも入っていればきちんと「Sky」がラベリングされます。す、凄すぎる・・・！これを無料で使えるのはデカイですね。（ある一定数いけば課金対象にはなりますが・・・）
思ったよりもカンタンに実装できたので、他の機能も使ってみたいと思います :)

## 参考になったサイト
ありがとうございました！
[Google Cloud Vision APIを使って、食品パッケージ画像からテキストを非同期で読み取りフォームに記載してみる](https://qiita.com/3dexperience/items/5e36fe6b1eecfff645fb)
[Rails：jqueryでコントローラのactionを叩き、jqueryに変数を戻す方法](https://qiita.com/Kohei_Kishimoto0214/items/d32c59784a995798ba02)
[RailsにGoogle Cloud Vision APIを導入し、簡単に過激な画像を検知する](https://qiita.com/ko30005/items/7e2366e9488be2c8584e)