【Rails】Google Cloud Vision APIで不適切な画像をバリデーションしてみた

# はじめに
おつかれさまです。
おおくまです。

今回はRailsで実装した個人開発のアプリにGoogle Cloud Vision APIを導入し、不適切な画像をバリデーションしてみました。
というのも、先日、そういったことが起こったので、対策として実装してみました。笑

# 注意
私は今年の4月からプログラミング学習を始めました。
内容に誤りがある場合がございます。
あらかじめご了承ください。
また、コメント等で教えていただけると幸甚です🙇

# Cloud Vision APIとは
Cloud Vision APIとは、Google Cloud Platformで提供されている画像処理サービスです。
このAPIを利用することで、画像を分析し、テキスト検出、物体検出、顔認識、ラベル付けなどのさまざまなことができます。
[こちらのリンク](https://cloud.google.com/vision?hl=ja)から試すことができます。

# Google APIキーの取得
まずは[API ライブラリ – API とサービス](https://console.cloud.google.com/apis/library?hl=ja)にアクセスして、Cloud Vision APIを探して、クリックします。![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f333334383434342f37666630303538342d363565352d616262362d383038612d6239656461333161616366382e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348444/3fe57b54-b107-3098-aef9-30342e72e341.png)

そして、「有効にする」をクリックします。
すると、Googleアカウントでログインしていない方は、ログインが求められますので、ログインします。

このような画面になるかと思いますので、「プロジェクトを作成」をクリックします。
![スクリーンショット 2023-09-11 11.21.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348444/9ab26644-a471-6279-fd94-4de19cc9a821.png)
プロジェクト名を入力し、「作成」をクリックします。
Cloud Vision APIの画面に戻ると思いますので、再度「有効にする」をクリックします。

このような画面になるかと思いますので、「認証情報を作成」をクリックします。
![スクリーンショット 2023-09-11 11.27.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348444/bdae1020-671b-0fd4-f863-d33c64874227.png)
「ユーザーデータ」にチェック　→　「次へ」をクリック　→　「アプリ名」、「ユーザーサポートメール」、「デベロッパーの連絡先情報 メールアドレス」を入力　→　「保存して次へ」をクリック　→　スコープは省略可なので、「保存して次へ」をクリック → アプリケーションの種類「ウェブアプリケーション」を選択し、名前にアプリ名を入力　→　「作成」をクリック　→　「完了」をクリック

![スクリーンショット 2023-09-11 11.38.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348444/e57a8496-9363-db37-21ae-f2dc56f296e6.png)
次に、左のサイドバーの「認証情報」をクリック　→　上の方にある「認証情報を作成」をクリック　→　「APIキー」をクリック
するとAPIキーが作成されますので、これを控えておきます。

# Railsに実装する
今回はCloud Vision AIの「SAFE_SEARCH_DETECTION」という機能を使用します。
この機能は、Cloud Vision AIの機能の1つで、与えられた画像に含まれる可能性のある不適切なコンテンツを検出するための機能です。
SAFE_SEARCH_DETECTIONは与えられた画像について5つの観点から6段階で評価してくれます。
詳しくはこれらのリンクを参考してください。
[不適切なコンテンツを検出する（セーフサーチ）](https://cloud.google.com/vision/docs/detecting-safe-search?hl=ja)
[SafeSearchAnnotation](https://cloud.google.com/vision/docs/reference/rpc/google.cloud.vision.v1#safesearchannotation)

まず、先ほど取得したAPIキーを.envファイルに記載します。
```ruby:.env
GOOGLE_API_KEY="先ほど取得したAPIキー"
```

次に``lib``フォルダの直下に``vision.rb``を作成します。
```ruby:vision.rb
require "base64"
require "json"
require "net/https"

module Vision
  class << self
    def image_analysis(image_file)
      api_url = "https://vision.googleapis.com/v1/images:annotate?key=#{ENV['GOOGLE_API_KEY']}"
      base64_image = Base64.encode64(image_file.tempfile.read)
      params = {
        requests: [{
          image: {
            content: base64_image
          },
          features: [
            {
              type: "SAFE_SEARCH_DETECTION"
            }
          ]
        }]
      }.to_json
      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, params)
      result = JSON.parse(response.body)
      if (error = result["responses"][0]["error"]).present?
        raise error["message"]
      else
        result_arr = result["responses"].flatten.map do |parsed_image|
          parsed_image["safeSearchAnnotation"].values
        end.flatten
        if result_arr.include?("LIKELY") || result_arr.include?("VERY_LIKELY")
          false
        else
          true
        end
      end
    end
  end
end
```
コードの内容としては、先ほどの5つの観点のうち1つでも"LIKELY"か"VERY_LIKELY"の判定を受けるとfalseを返すコードになっています。

次に``lib/vision.rb``を読み込むために``config/application.rb``に以下の記述を追記します。
```ruby:config/application.rb
module ZooMania
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.0
    config.i18n.load_path += Dir[Rails.root.join('config/locales/**/*.{rb,yml}').to_s]
    config.i18n.default_locale = :ja
    + config.paths.add "lib", eager_load: true #この1行を追記
    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")
  end
end
```

私のアプリではPostテーブルにimageというカラムがあり、それが画像投稿機能と結びついています。
なので、今回はPostコントローラのcreateアクションとupdateアクションを編集していきます。
```ruby:posts_controller.rb
  def new
    @post = Post.new
  end

  def create
    @post = current_user.posts.build(post_params)
    if post_params[:image].present?
      result = Vision.image_analysis(post_params[:image])
      if result
        if @post.save
          redirect_to post_path(@post), notice: t('.success_create_post')
        else
          flash.now['danger'] = t('.fail_create_post')
          render :new, status: :unprocessable_entity
        end
      else
        flash.now['danger'] = t('defaults.inappropriate_image')
        render :new, status: :unprocessable_entity
      end
    elsif @post.save
    else
      flash.now['danger'] = t('.fail_create_post')
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    @post = current_user.posts.find(params[:id])
  end

  def update
    @post = current_user.posts.find(params[:id])
    if post_params[:image].present?
      result = Vision.image_analysis(post_params[:image])
      if result
        if @post.update(post_params)
          redirect_to post_path(@post), notice: t('.success_update_post')
        else
          flash.now['danger'] = t('.fail_update_post')
          render :edit, status: :unprocessable_entity
        end
      else
        flash.now['danger'] = t('defaults.inappropriate_image')
        render :new, status: :unprocessable_entity
      end
    elsif @post.update(post_params)
      redirect_to post_path(@post), notice: t('.success_update_post')
    else
      flash.now['danger'] = t('.fail_update_post')
      render :new, status: :unprocessable_entity
    end
  end
```
~~クソコードなので、これからリファクタリング頑張ります。~~
先ほど``lib/vision.rb``で定義したメソッドをcreateアクションとupdateアクションで呼び出しており、falseの場合は不適切な画像として投稿できない仕組みになっています。

# 動作
![スクリーンショット 2023-09-11 12.29.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3348444/f6ac977f-c14b-1b70-6c1c-febfe9bf89f2.png)
無事に機能するようになりました。

# まとめ
今回、初めて使ったAPIでしたが、無事に実装できてよかったです。
こういったハプニングや予期せぬことを繰り返さないために機能を追加していくことも、アプリケーションを運用する醍醐味かと思いますので、今後も機能追加を頑張りたいと思います。
最後まで読んでいただき、ありがとうございました。