【Google Cloud Vision API】Rails6.1で画像登録時に関連度を判定する

# はじめに
ポートフォリオの課題でお花屋さんの商品予約サイトを作成中のとき。
Google Cloud Vision APIを使って、お花と関連のない商品写真が登録された場合、管理者の承認が必要になるという設定を作ることにしました。

※前提として、いろんなお花屋さんが登録した商品の中から、ユーザが選ぶという形のサイトになっているので、「複数のお花屋さん」「複数のユーザー」「サイトの管理者」という3パターンのユーザが存在しています。

# 実装内容
Cloud Vision APIについては公式ドキュメントを確認してみてください。

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

このページ内の「デモ」の部分で画像をアップロードしてみると仕組みがイメージしやすいです。

## 準備
visionAPIの導入については学習課題を見ながら進めたため省きます。
導入について以下の記事が参考になりそうでした。

https://qiita.com/ysda/items/67c983510f4d7d1c6982

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

## lib/vision.rbファイルを作成して記述
正直、「lib/vision.rb」の内容は学習課題の内容をほぼコピペした状態で、導入時はどこでどういう指示が行われているのかいまいち理解しきれていませんでした。
なので、改めて調べたコードの説明を追記しておきます。

また、学習課題そのままの記述ではなく、一部設定を変えているので間違っている部分もあるかもしれません。ご注意ください。

```ruby:lib/vision.rb
require 'base64'
require 'json'
require 'net/https'

# Visionというモジュールを定義
module Vision
  class << self
    def get_image_data(image_file)
      # APIのURL作成（APIキーは.envファイルに記載して呼び出し）
      api_url = "https://vision.googleapis.com/v1/images:annotate?key=#{ENV['GOOGLE_API_KEY']}"

      # Base64.encode64(...)は、引数として与えられたバイナリデータをBase64形式にエンコード
      # File.read(...)は、ファイルの場所を示している
      base64_image = Base64.encode64(File.read(image_file.path))

      # APIリクエスト用のJSONに変換するパラメータ
      params = {
        requests: [{
          image: {
            content: base64_image
          },
          # ここで解析のタイプを指定
          features: [
            {
              type: 'LABEL_DETECTION'
            }
          ]
        }]
      }.to_json

      # APIのURL文字列（api_url）を解析し、uriオブジェクトを作成（ホスト名、ポート番号、パスなど、URLに関する情報を格納）
      uri = URI.parse(api_url)
      # 新しいHTTPオブジェクトを作成（host：APIのホスト名、port：使用するポート番号）
      https = Net::HTTP.new(uri.host, uri.port)
      # HTTPS（HTTP over SSL/TLS）を使用するように設定（trueで暗号化され、安全にデータを送受信）
      https.use_ssl = true
      # 新しいHTTP POSTリクエストを作成（request_uri：APIのパス）
      request = Net::HTTP::Post.new(uri.request_uri)
      # Content-Type を application/jsonに設定
      request['Content-Type'] = 'application/json'
      # https.request：作成したHTTP POSTリクエスト（request）を送信
      response = https.request(request, params)
      # JSON形式の文字列をRubyのハッシュや配列に変換し、response_body 変数に解析されたデータを格納
      response_body = JSON.parse(response.body)
      

      # データ送信チェック（想定したデータが送られてきているか確認したいときに使用）
      # Rails.logger.debug("Google Cloud Vision API response: #{response_body}")
      

      # APIレスポンス出力…レスポンスデータ中にエラーが含まれているかどうかをチェック
      if (error = response_body['responses'][0]['error']).present?
        raise error['message']
      else
        # APIから返されたデータの中で、ラベル（タグ）の注釈が含まれる部分を取得
        label_annotations = response_body['responses'][0]['labelAnnotations']
        label_annotations.map do |annotation|
          {
            # description：ラベルの説明
            description: annotation['description'],
            # topicality：画像に対するICA（Image Content Annotation）ラベルの関連度
            topicality: annotation['topicality']
          }
        end
      end
    end
  end
end
```

:::note info
<b>モジュール</b>
Rubyでコードを組織化し、再利用可能にするための構造の一つです。
クラスに似ていますが、インスタンスを作成できない点や多重継承をサポートする点でクラスとは異なります。
:::

:::note info
<b>class << self</b>
これを使うことで、特定のオブジェクトの特異クラスを開いて、その中にメソッドを定義することができます。

<b>特異クラス</b>
通常、メソッドはクラスに対して定義され、クラスのすべてのインスタンスで共有されますが、特異クラスを使うと、特定のオブジェクトだけにメソッドを追加することができます。
そのため、他のオブジェクトに影響を与えません。
:::

:::note info
<b>Base64</b>
バイナリデータ（画像や音声ファイルなど）をテキストデータに変換するためのエンコーディング方法。具体的には、バイナリデータを64種類の文字（A-Z, a-z, 0-9, +, /）を使って表現します。
:::

私の記述したVisionモジュールでは、最終的にlabelAnnotationsから<b>「description（ラベルの説明）」</b>と<b>「topicality（画像に対するラベルの関連度）」</b>のデータを呼び出しています。

この2つのデータを利用して、ラベルの<b>plant（植物）</b>と<b>flower（花）</b>への関連度の高い商品画像が登録された際には、商品の公開に管理者の許可が必要な設定にします。

これで必要なデータをCloud Vision APIから受け取る準備が整いました。

## 商品登録時の実装内容
今回作成する機能は、
商品（Item）の画像登録の際に、先ほど作成したVisionモジュールで取得した<b>商品の情報を取得</b>
↓
取得した内容を元に、登録された画像の内容が
<b>植物や花と関連しているならpermissionを「permit（承認）」</b>
<b>関連性が低いと判断されればpermissionを「confirm（確認中）」</b>に変更して、さらに商品を<b>非公開</b>にします。

そのために必要な商品判定用の（item_checkモデル）を作成。

![2024-06-24画像承認2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3762011/48a3634b-4d2b-3f43-325e-f60a7b9856e8.png)

label_check：判定結果（true/false）
permission：承認状況（permit/confirm/not_permit）

```ruby:モデル
# ItemCheckモデル
class ItemCheck < ApplicationRecord
  belongs_to :item

  enum permission: { permit: 0, confirm: 1, not_permit: 2 }
end

# Itemモデル
class Item < ApplicationRecord
:
  has_one :item_check, dependent: :destroy
  has_one_attached :item_image
  
  def get_item_image(width, height)
    item_image.variant(resize_to_fill: [width, height]).processed
  end

  def get_item_image_webp(width, height)
    item_image.variant(resize_to_fill: [width, height], format: :webp).processed if item_image.attached?
  end
:
end
```

## 【Shop側】商品登録用のコントローラ
商品を保存する際のコントローラの記述は以下の通りです。

```ruby:
  def new
    @item = Item.new
  end

  def create
    @item = Item.new(item_params)
    @item.shop_id = current_shop.id
    
    # 画像を圧縮してjpegで保存（今回のAPIには関係なし）
    if params[:item][:item_image].present?
      resized_images = resize_image_set_dpi(params[:item][:item_image])
      original_filename_base = File.basename(params[:item][:item_image].original_filename, ".*")
      @item.item_image.attach(
        io: resized_images,
        filename: "#{original_filename_base}.jpg",
        content_type: 'image/jpg'
        )
        
      # visionモジュールのデータを取得
      item_checks = Vision.get_image_data(item_params[:item_image])
    end

    if @item.save
      # visionで取得したデータを使って画像の関連度判定
      plant_found = false
      flower_found = false

      item_checks.each do |check|
        # ラベル名にPlantが含まれていて、尚且つ関連度が0.8以上ならplant_foundをtrueにする
        if check[:description].include?("Plant") && check[:topicality] >= 0.8
          plant_found = true
        elsif check[:description].include?("Flower") && check[:topicality] >= 0.8
          flower_found = true
        end
      end

      item_check = ItemCheck.new
      item_check.item_id = @item.id
      
      # 判定した結果をitem_checkに格納
      if flower_found || plant_found
        item_check.label_check = true
        item_check.permission = 0        
      else
        item_check.label_check = false
        item_check.permission = 1
        @item.update(is_active: false)
      end
      item_check.save
      redirect_to item_path(@item)
    else
      flash.now[:alert] = "入力内容に誤りがあります"
      render :new
    end
  end
  
  private

  # 画像のリサイズ（今回のAPIには関係なし）
  def resize_image_set_dpi(uploaded_file)
    image = MiniMagick::Image.read(uploaded_file.tempfile)
    image.resize 'x1350'
    image.density '96'

    tempfile_jpg = Tempfile.new('resized')
    image.write (tempfile_jpg.path)
    tempfile_jpg.rewind
    tempfile_jpg
  end

  def item_params
    params.require(:item).permit(:item_image, :name, :introduction, :size, :price, :stock, :deadline, :is_active, :first_is_active)
  end
```

ここでちょっと困るのが、データがきちんと取得されているか確認できないことです。
そこで、先ほどのVisionモジュールの記述の中にコメントアウトで記載してあった、

```diff_ruby:
# データ送信チェック（想定したデータが送られてきているか確認したいときに使用）
+ Rails.logger.debug("Google Cloud Vision API response: #{response_body}")
```
こちらを記述することで、送られてきたデータを確認します。

```
Google Cloud Vision API response: {"responses"=>[{"labelAnnotations"=>[{"mid"=>"/m/0c9ph5", "description"=>"Flower", "score"=>0.98177266, "topicality"=>0.98177266}, {"mid"=>"/m/05s2s", "description"=>"Plant", "score"=>0.96711993, "topicality"=>0.96711993}, {"mid"=>"/m/02s195", "description"=>"Vase", "score"=>0.9566836, "topicality"=>0.9566836}, …
```

そうするとSQL上でこんな内容が確認できます。
"description"=>"Flower"や"topicality"=>0.98177266というデータも見られ、想定通りのデータが送られてきているようです。

あとは、管理者側のビューで商品一覧にItem_checkの内容を表示すれば、登録時の画像判定は完了です。

ちなみに、商品画像が承認確認中になると、自動的に商品を非公開にする設定となっているので、Shop側からすると何故か非公開になってしまったという状況になりかねません。
なので、item_checkのpermissionがconfirm（確認中）かnot_permit（承認不可）なら、
「※登録された画像に問題のある可能性があります。画像を変更するか管理者チェックが完了するまでこの商品は公開できません。」
というメッセージを表示することにしています。


## 【Admin側】商品承認設定のコントローラ

今度は管理者側で「confirm（確認中）」を確認して、「permit（承認）」もしくは「not_permit（承認不可）」にするための機能を追加します。

ビューはこんな感じです。
（ビューのコードは省略します）

![2024-06-24画像承認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3762011/47c78c00-536e-6036-4d94-5fb9458d4462.png)

ちなみに、日本語表示のためにymlファイルを使って日本語化も行います。
```ruby:config/locales/ja.yml
:
:
      item_check:
        permission:
          permit: "承認"
          confirm: "確認中"
          not_permit: "承認不可"
```

```ruby:admin/item_checks_controller.rb
class Admin::ItemChecksController < ApplicationController
  before_action :authenticate_admin!

  def update
    item = Item.find(params[:item_id])
    item_check = ItemCheck.find_by(item_id: item.id)
    item_check.update(item_check_params)
    unless item_check.permission == 'permit'
    # 承認しないなら同時に商品を非公開にします
      item.update(is_active: false)
    end
    flash[:notice] = "「" + item_check.permission_i18n + "」に更新しました。"
    redirect_to request.referer
  end

  private

  def item_check_params
    params.require(:item_check).permit(:permission)
  end
end

```
これで管理者側の機能も追加できました。

# ちょっとした疑問と改善点
実装自体はスムーズに進んだのですが、「ラベル名にPlantかFlowerが含まれていて、尚且つその関連度が0.8以上なら承認」という設定が適切かどうかは少し疑問点が残ります。
今回はポートフォリオだったため良しとしましたが、もし実務として使う場合は、もっといろんな画像を使って、<b>ラベル名や数値の良い塩梅を探る</b>ことが必要かなという気がしました。

また、<b>API側の設定が変わってしまうとせっかく作成したコードも機能しなくなってしまう可能性があるのでは</b>という懸念点も…例えば、ラベル名に使われていたPlantが別の単語に代わるとか…。

メンターの方に実装チェックをしていただいた際にこちらについて確認してみると、今回私が使用したPlantやFlowerに関しては一般的な単語のため変更される可能性は低いが、確かにAPI側の設定が変わる可能性もありえるとのこと。

実際の開発でAPIを使用する際は、APIを管理している企業などに資料を請求して、今後機能として不具合が起こる可能性はないかを十分確認した上で取り入れるのだとか。納得です。

# おわりに
画像解析のAPIを少し見ただけでも、画像内のテキストを検出したり、画像の色や人の顔を検出したり、攻撃性の高い画像ではないかを判定したり…いろんな情報を受け取れることが分かりました。

これだけ情報があれば活用の仕方次第でいろんなツールに活かす方法が見つかりそうです。
まだまだほんのちょっと触れた程度ですが、AI技術の奥深さを感じました。
重要なのは情報の選定ですね。