Rails × Google Cloud Vision API を使って画像解析してみた

# はじめに
「RUNTEQ Advent Calendar 2023」の7日目を担当させていただきます🎄
今回は初めて個人開発のアプリを作成するにあたり、技術検証としていくつかのAPIを触ってみましたので、記事にしてみました。
その中で画像解析してくれる「Google Cloud Vision API」が面白かったので紹介します。

# 今回のゴール
画像データをAPIに送って色情報を解析してもらい、結果を画面に表示する。
（この画像にはどんな色がどれぐらいの割合で占めているのか〜的な）

# 前提
::: note
ローカルの開発環境にて技術検証用のお試しアプリ作って触った流れを紹介します🙏
まず動かすことを目的としたので、コードが鬼汚いです。
:::

使用技術
・Ruby on Rails：7.1.2
・gem "carrierwave"（アップローダー作成・カラム紐付けまで完了済み）
・gem "dotenv-rails"
・gem "google-cloud-vision"

# 目次
・Google Cloud Vision APIとは？
・準備
・実装
・画像を解析してみよう

# Google Cloud Vision APIとは？
Googleが提供する **「画像認識サービス」** です
要するに画像を送るだけで色々解析してくれる便利なAPIです。
[Google Cloud Vision API公式ドキュメント](https://cloud.google.com/vision/docs?hl=ja)
### 機能一覧
・画像プロパティ **👈今回はこれを使ってみました**
・テキスト検出
・ドキュメント検出
・ランドマーク検出
・ロゴ検出
・ラベル検出
・オブジェクトのローカライズ
・クロップヒント検出
・ウェブエンティティとページ
・顔検出

詳細説明は省きますが、とにかく機能がいっぱいです。

# 準備
### まずはAPIキーを取得します。
下記のガイドに従いAPIキーを取得・有効にしてください。
[Google Cloud Vision API 基本とクイックスタート](https://cloud.google.com/vision/docs/quickstarts?hl=ja)

今回、取得したAPIキーを環境変数で管理する方法とてして「gem "dotenv-rails"」を使ってみました。
また、「gem "google-cloud-vision"」もインストールします。
```ruby
# 環境変数管理
gem 'dotenv-rails'

# Google Cloud Vision API
gem "google-cloud-vision"
```
インストールしたら「.env」ファイルを作業フォルダに作成し、環境変数を定義。
```ruby:.env
# Google Cloud Vision APIのAPIkey
GOOGLE_API_KEY= "取得したAPIキー"
```
gitignoreファイルに「.env」ファイルを追記 **（忘れずに！）**。
```.gitignore
# 環境変数管理
/.env
```
これでAPIキーの設定は完了。


### 次に簡単な画像選択フォームを作成しておきます。
```ruby:app/views/apps/new.html.erb
<%= form_with model: app do |f| %>
  <%= f.label :'画像選択' %>
  <%= f.file_field :desk_image %>
  <%= f.hidden_field :desk_image_cache %>
  <%= f.submit '画像解析！' %>
<% end %>
```
[![Image from Gyazo](https://i.gyazo.com/b5d989bea253452ceed161e43c4182c9.png)](https://gyazo.com/b5d989bea253452ceed161e43c4182c9)

### コントローラーに必要な処理を書いていきます。
 ```ruby:/controllers/Apps_controller.rb
class AppsController < ApplicationController
  def show
    @app = App.find(params[:id])
  end

  def new
    @app = App.new
  end

  def create
    @app = App.new(app_params)

   # analyze_imageメソッド(画像解析)を実行。analyze_imageメソッドはモデルに定義。
   uploaded_image_path = params[:app][:app_image].tempfile.path
   @app.result = @app.analyze_image(uploaded_image_path)

   # データベースに保存
    if @app.save
      redirect_to app_path(@app)
    else
      render :new
    end
  end 

  private
    def app_params
      params.require(:app).permit(:app_image, :app_image_cache)
    end
end
 ```

 ### 画像解析メソッドを定義します。（サービスディレクトリに格納するべき。修正します）
 今回はモデルファイルにanalyze_imageメソッドとして定義します。
ここでAPIにリクエストを送り、解析結果のレスポンスを受け取ります。
 ```ruby:app/models/app.rb
class App < ApplicationRecord
  # アップローダーをカラムに紐付け
  mount_uploader :app_image, AppImageUploader 

  def analyze_image(uploaded_image_path)
    # APIキーを環境変数として渡す。
    api_key = ENV['GOOGLE_API_KEY']
    
    # Google Cloud Vision APIのエンドポイントを構築。
    uri = URI("https://vision.googleapis.com/v1/images:annotate?key=#{api_key}")

    # 渡された画像データをBase64にエンコードしてAPIに送れる形にする。
    image_data = Base64.strict_encode64(File.open(uploaded_image_path, 'rb').read)

     # 画像データと解析したい内容（今回は色情報が欲しいのでプロパティを選択）を指定
    body = {
      requests: [{
        image: {
          content: image_data
        },
        features: [{
          type: "IMAGE_PROPERTIES",
          maxResults: 10
        }]
      }]
    }.to_json

    # HTTPオブジェクトを使ってHTTPリクエストを作成。
    # POSTメソッド+ヘッダーに'Content-Type' => 'application/json'を指定。
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    request = Net::HTTP::Post.new(uri.request_uri, {'Content-Type' => 'application/json'})
    request.body = body
    
    # リクエストを送信→レスポンス受け取り。
    response = http.request(request)

    # レスポンスのHTTPステータスコードが200の場合、レスポンスボディから色情報を抽出。
    if response.code == '200'
      color_full_data = JSON.parse(response.body)['responses'][0]['imagePropertiesAnnotation']['dominantColors']['colors']
    else
      # エラーハンドリングの実装をここに記述
    end
  end
end

 ```
 
 今回はAPI動かすことがゴールなので、コード若干散らかってますがこのまま行きます🙏

 ### 結果を表示するページも準備します。
 ```:/apps/show.html.erb
<div>【解析した画像】</div>
  <% if @app.desk_image? %>
    <%= image_tag @app.desk_image.url %> 
  <% end %>

<>【画像解析結果のcolor情報】</h5> 
  <%= @app.result %>
 ```

これで最低限の準備は完了。

# 画像を解析してみよう
早速ちゃんと動くか確認してみましょう。
今回は実家の猫とお花のツーショット写真の色情報を解析していきます。
![スクリーンショット 2023-12-06 11.08.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478709/28f5e588-ecab-5d92-e3e4-5ddfb0744352.png)
まず写真を選択します。
（画像プレビュー表示したら親切ですね。今回は割愛してます、すみません）
![スクリーンショット 2023-12-06 11.11.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478709/9b10b02c-a249-64a1-2f1b-ac33e5719eb8.png)
画像解析ボタンを押します。
[![Image from Gyazo](https://i.gyazo.com/eb88a90fef426749ec4f9f2e014c66dd.gif)](https://gyazo.com/eb88a90fef426749ec4f9f2e014c66dd)
無事画像解析結果の色情報を取得できました。

下記の値が取得できました。
```ruby
# 【画像解析結果のcolor情報】
[{"color":{"red":221,"green":239,"blue":169},"score":0.081625775,"pixelFraction":0.008081085},{"color":{"red":229,"green":189,"blue":203},"score":0.042425383,"pixelFraction":0.008218052},{"color":{"red":99,"green":129,"blue":85},"score":0.035680704,"pixelFraction":0.008628955},{"color":{"red":242,"green":234,"blue":236},"score":0.034619678,"pixelFraction":0.039309684},{"color":{"red":100,"green":21,"blue":26},"score":0.033964556,"pixelFraction":0.009108341},{"color":{"red":35,"green":60,"blue":22},"score":0.02578642,"pixelFraction":0.0056156693},{"color":{"red":163,"green":40,"blue":40},"score":0.021904174,"pixelFraction":0.0040405425},{"color":{"red":179,"green":95,"blue":89},"score":0.020016026,"pixelFraction":0.00417751},{"color":{"red":49,"green":50,"blue":44},"score":0.018129067,"pixelFraction":0.09122038},{"color":{"red":223,"green":166,"blue":199},"score":0.0033444783,"pixelFraction":0.0002739351}]
```

| 取得した値  |  内容   |
|--------------|------------|
|RGB      | 赤(Red)、緑(Green)、青(Blue)の3色の強さを表す数値で、色を識別するために使われている。値は0から255の範囲で表現される。|
|score    |その色が画像全体の中でどれくらいの割合で存在するかの相対的な重要度を示す値。0から1の範囲で、大きいほど画像内でより支配的な色ということになる。|
|pixelFraction |その色が画像内のどれくらいの割合のピクセルを占めているかを示す値。0から1の範囲で、全ピクセル数に対するその色のピクセル数の比率を表す。|

色々とツッコミどころ満載ですが、ひとまずゴールできました。🎉
必要な情報だけ抽出することでいろんな機能が実装できそうです（実装したい🫠）。


 # まとめ
 　外部APIを試すのは今回が初めてでしたが、どんなAPIがあるのかを知ることでアイデアの幅も広がると実際に触りながら感じました。また、今回は基本的な機能を動かすことを目標としたので、実際はバリデーション・エラーハンドリング等についてもしっかり対策する必要があります。

　動かすまでに色々調べて試行錯誤しましたが、無事に実装できた時は嬉しかったです（自分の効率悪すぎて時間かけすぎた感はあります）。アプリ作成進めていく中で詰まるところが多くあると思いますが、無事に動いた時の感動を求めて取り組んでいきたいと思います。

 以上です🙏

# 参考
https://cloud.google.com/vision?hl=ja

https://zenn.dev/crinoid/articles/f41a4a09753a71

https://qiita.com/dam-san/items/775791b60f90b830c754