Firebase Auth と Functions を使用して Cloud Vision で画像内のテキストを安全に認識する(Android)

アプリから Google Cloud API を呼び出すには、認可を処理し、API キーなどのシークレット値を保護するための中間 REST API を作成する必要があります。次に、モバイルアプリでこの中間サービスに対する認証と通信を行うためのコードを記述します。

この REST API を作成する方法の一つとして、Firebase Authentication と Functions を使用する方法があります。この方法では、Google Cloud API に対するサーバーレスのマネージド ゲートウェイが提供され、そこで認証が処理されます。このゲートウェイは、事前構築された SDK を使用してモバイルアプリから呼び出すことができます。

このガイドでは、この手法を使用してアプリから Cloud Vision API を呼び出す方法について説明します。この手法では、すべての認証済みユーザーが Cloud のプロジェクトを経由して Cloud Vision の課金サービスにアクセスできます。そのため、続行する前に、目的のユースケースにおいてこの認証メカニズムで十分かどうかを検討してください。

始める前に

プロジェクトを構成する

  1. まだ Firebase を Android プロジェクトに追加していない場合は追加します。
  2. プロジェクトで Cloud ベースの API をまだ有効にしていない場合は、ここで有効にします。

    1. Firebase コンソールの Firebase ML の [APIs] ページを開きます。
    2. まだプロジェクトを Blaze 料金プランにアップグレードしていない場合は、[アップグレード] をクリックしてアップグレードします(プロジェクトをアップグレードするよう求められるのは、プロジェクトが Blaze プランでない場合のみです)。

      Blaze レベルのプロジェクトだけが Cloud ベースの API を使用できます。

    3. Cloud ベースの API がまだ有効になっていない場合は、[Cloud ベースの API を有効化] をクリックします。
  3. 既存の Firebase API キーを構成して、Cloud Vision API へのアクセスを許可しないようにします。
    1. Cloud コンソールの [認証情報] ページを開きます。
    2. リスト内の各 API キーについて、編集ビューを開き、[キーの制限] セクションで Cloud Vision API を除く使用可能なすべての API をリストに追加します。

呼び出し可能関数をデプロイする

次に、アプリと Cloud Vision API の間のブリッジとして使用する Cloud Functions の関数をデプロイします。functions-samples リポジトリには、使用可能なサンプルが含まれています。

デフォルトでは、この関数を通じて Cloud Vision API にアクセスすると、アプリの認証済みユーザーのみが Cloud Vision API にアクセスできます。関数はさまざまな要件に応じて変更できます。

関数をデプロイするには:

  1. 関数サンプル リポジトリをダウンロードするか、そのクローンを作成して、Node-1st-gen/vision-annotate-image ディレクトリに移動します。
    git clone https://github.com/firebase/functions-samples
    cd Node-1st-gen/vision-annotate-image
    
  2. 依存関係をインストールします。
    cd functions
    npm install
    cd ..
    
  3. Firebase CLI がインストールされていない場合は、インストールします。
  4. vision-annotate-image ディレクトリ内で Firebase プロジェクトを初期化します。プロンプトが表示されたら、リストからプロジェクトを選択します。
    firebase init
  5. 関数をデプロイします。
    firebase deploy --only functions:annotateImage

アプリに Firebase Auth を追加する

上記でデプロイした呼び出し可能関数は、アプリ内で認証されていないユーザーからのリクエストを拒否します。アプリにまだ Firebase Auth を追加していない場合は、Firebase Auth を追加する必要があります。

必要な依存関係をアプリに追加する

  • Firebase Functions ライブラリと Gson Android ライブラリの依存関係を ** モジュール(アプリレベル)** の Gradle ファイル(通常は `//build.gradle.kts` または `//build.gradle`)に追加します。
        implementation("com.google.firebase:firebase-functions:20.4.0")
        implementation("com.google.code.gson:gson:2.8.6")
        
  • これで、画像内のテキストを認識する準備ができました。

    1. 入力画像を準備する

    Cloud Vision を呼び出すには、画像を base64 でエンコードされた文字列としてフォーマットする必要があります。保存されたファイルの URI から画像を処理する方法は次のとおりです。
    1. 画像を Bitmap オブジェクトとして取得します。

      Kotlin+KTX

      var bitmap: Bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
      

      Java

      Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
    2. 必要に応じて、帯域幅を節約するために画像を縮小します。Cloud Vision の推奨画像サイズをご覧ください。

      Kotlin+KTX

      private fun scaleBitmapDown(bitmap: Bitmap, maxDimension: Int): Bitmap {
          val originalWidth = bitmap.width
          val originalHeight = bitmap.height
          var resizedWidth = maxDimension
          var resizedHeight = maxDimension
          if (originalHeight > originalWidth) {
              resizedHeight = maxDimension
              resizedWidth =
                  (resizedHeight * originalWidth.toFloat() / originalHeight.toFloat()).toInt()
          } else if (originalWidth > originalHeight) {
              resizedWidth = maxDimension
              resizedHeight =
                  (resizedWidth * originalHeight.toFloat() / originalWidth.toFloat()).toInt()
          } else if (originalHeight == originalWidth) {
              resizedHeight = maxDimension
              resizedWidth = maxDimension
          }
          return Bitmap.createScaledBitmap(bitmap, resizedWidth, resizedHeight, false)
      }

      Java

      private Bitmap scaleBitmapDown(Bitmap bitmap, int maxDimension) {
          int originalWidth = bitmap.getWidth();
          int originalHeight = bitmap.getHeight();
          int resizedWidth = maxDimension;
          int resizedHeight = maxDimension;
      
          if (originalHeight > originalWidth) {
              resizedHeight = maxDimension;
              resizedWidth = (int) (resizedHeight * (float) originalWidth / (float) originalHeight);
          } else if (originalWidth > originalHeight) {
              resizedWidth = maxDimension;
              resizedHeight = (int) (resizedWidth * (float) originalHeight / (float) originalWidth);
          } else if (originalHeight == originalWidth) {
              resizedHeight = maxDimension;
              resizedWidth = maxDimension;
          }
          return Bitmap.createScaledBitmap(bitmap, resizedWidth, resizedHeight, false);
      }

      Kotlin+KTX

      // Scale down bitmap size
      bitmap = scaleBitmapDown(bitmap, 640)

      Java

      // Scale down bitmap size
      bitmap = scaleBitmapDown(bitmap, 640);
    3. ビットマップ オブジェクトを base64 でエンコードされた文字列に変換します。

      Kotlin+KTX

      // Convert bitmap to base64 encoded string
      val byteArrayOutputStream = ByteArrayOutputStream()
      bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
      val imageBytes: ByteArray = byteArrayOutputStream.toByteArray()
      val base64encoded = Base64.encodeToString(imageBytes, Base64.NO_WRAP)

      Java

      // Convert bitmap to base64 encoded string
      ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
      bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
      byte[] imageBytes = byteArrayOutputStream.toByteArray();
      String base64encoded = Base64.encodeToString(imageBytes, Base64.NO_WRAP);
    4. Bitmap オブジェクトによって表される画像は、さらに回転する必要がないよう、正しい向きになっていなければなりません。

    2. 呼び出し可能関数を呼び出し、テキストを認識する

    画像内のテキストを認識するには、呼び出し可能関数を呼び出し、JSON Cloud Vision リクエストを渡します。

    1. 最初に Cloud Functions のインスタンスを初期化します。

      Kotlin+KTX

      private lateinit var functions: FirebaseFunctions
      // ...
      functions = Firebase.functions
      

      Java

      private FirebaseFunctions mFunctions;
      // ...
      mFunctions = FirebaseFunctions.getInstance();
      
    2. 関数を呼び出すメソッドを定義します。

      Kotlin+KTX

      private fun annotateImage(requestJson: String): Task<JsonElement> {
          return functions
              .getHttpsCallable("annotateImage")
              .call(requestJson)
              .continueWith { task ->
                  // This continuation runs on either success or failure, but if the task
                  // has failed then result will throw an Exception which will be
                  // propagated down.
                  val result = task.result?.data
                  JsonParser.parseString(Gson().toJson(result))
              }
      }
      

      Java

      private Task<JsonElement> annotateImage(String requestJson) {
          return mFunctions
                  .getHttpsCallable("annotateImage")
                  .call(requestJson)
                  .continueWith(new Continuation<HttpsCallableResult, JsonElement>() {
                      @Override
                      public JsonElement then(@NonNull Task<HttpsCallableResult> task) {
                          // This continuation runs on either success or failure, but if the task
                          // has failed then getResult() will throw an Exception which will be
                          // propagated down.
                          return JsonParser.parseString(new Gson().toJson(task.getResult().getData()));
                      }
                  });
      }
      
    3. JSON リクエストを作成します。Cloud Vision API では、TEXT_DETECTIONDOCUMENT_TEXT_DETECTION の 2 つのタイプのテキスト検出がサポートされています。この 2 つのユースケースの違いについては、Cloud Vision OCR のドキュメントをご覧ください。

      Kotlin+KTX

      // Create json request to cloud vision
      val request = JsonObject()
      // Add image to request
      val image = JsonObject()
      image.add("content", JsonPrimitive(base64encoded))
      request.add("image", image)
      // Add features to the request
      val feature = JsonObject()
      feature.add("type", JsonPrimitive("TEXT_DETECTION"))
      // Alternatively, for DOCUMENT_TEXT_DETECTION:
      // feature.add("type", JsonPrimitive("DOCUMENT_TEXT_DETECTION"))
      val features = JsonArray()
      features.add(feature)
      request.add("features", features)
      

      Java

      // Create json request to cloud vision
      JsonObject request = new JsonObject();
      // Add image to request
      JsonObject image = new JsonObject();
      image.add("content", new JsonPrimitive(base64encoded));
      request.add("image", image);
      //Add features to the request
      JsonObject feature = new JsonObject();
      feature.add("type", new JsonPrimitive("TEXT_DETECTION"));
      // Alternatively, for DOCUMENT_TEXT_DETECTION:
      //feature.add("type", new JsonPrimitive("DOCUMENT_TEXT_DETECTION"));
      JsonArray features = new JsonArray();
      features.add(feature);
      request.add("features", features);
      

      必要に応じて、言語の検出に役立つ言語ヒントを提供します(サポートされている言語をご覧ください)。

      Kotlin+KTX

      val imageContext = JsonObject()
      val languageHints = JsonArray()
      languageHints.add("en")
      imageContext.add("languageHints", languageHints)
      request.add("imageContext", imageContext)
      

      Java

      JsonObject imageContext = new JsonObject();
      JsonArray languageHints = new JsonArray();
      languageHints.add("en");
      imageContext.add("languageHints", languageHints);
      request.add("imageContext", imageContext);
      
    4. 関数を呼び出します。

      Kotlin+KTX

      annotateImage(request.toString())
          .addOnCompleteListener { task ->
              if (!task.isSuccessful) {
                  // Task failed with an exception
                  // ...
              } else {
                  // Task completed successfully
                  // ...
              }
          }
      

      Java

      annotateImage(request.toString())
              .addOnCompleteListener(new OnCompleteListener<JsonElement>() {
                  @Override
                  public void onComplete(@NonNull Task<JsonElement> task) {
                      if (!task.isSuccessful()) {
                          // Task failed with an exception
                          // ...
                      } else {
                          // Task completed successfully
                          // ...
                      }
                  }
              });
      

    3. 認識したテキストのブロックからテキストを抽出する

    テキスト認識処理が成功すると、タスクの結果として BatchAnnotateImagesResponse の JSON レスポンスが返されます。テキスト アノテーションは fullTextAnnotation オブジェクトにあります。

    認識されたテキストは text フィールド内の文字列として取得できます。例:

    Kotlin+KTX

    val annotation = task.result!!.asJsonArray[0].asJsonObject["fullTextAnnotation"].asJsonObject
    System.out.format("%nComplete annotation:")
    System.out.format("%n%s", annotation["text"].asString)
    

    Java

    JsonObject annotation = task.getResult().getAsJsonArray().get(0).getAsJsonObject().get("fullTextAnnotation").getAsJsonObject();
    System.out.format("%nComplete annotation:%n");
    System.out.format("%s%n", annotation.get("text").getAsString());
    

    画像内の特定の領域に関する情報を取得することもできます。blockparagraphwordsymbol のそれぞれのオブジェクトについて、領域内で認識されたテキストと、領域の境界座標を取得できます。例:

    Kotlin+KTX

    for (page in annotation["pages"].asJsonArray) {
        var pageText = ""
        for (block in page.asJsonObject["blocks"].asJsonArray) {
            var blockText = ""
            for (para in block.asJsonObject["paragraphs"].asJsonArray) {
                var paraText = ""
                for (word in para.asJsonObject["words"].asJsonArray) {
                    var wordText = ""
                    for (symbol in word.asJsonObject["symbols"].asJsonArray) {
                        wordText += symbol.asJsonObject["text"].asString
                        System.out.format(
                            "Symbol text: %s (confidence: %f)%n",
                            symbol.asJsonObject["text"].asString,
                            symbol.asJsonObject["confidence"].asFloat,
                        )
                    }
                    System.out.format(
                        "Word text: %s (confidence: %f)%n%n",
                        wordText,
                        word.asJsonObject["confidence"].asFloat,
                    )
                    System.out.format("Word bounding box: %s%n", word.asJsonObject["boundingBox"])
                    paraText = String.format("%s%s ", paraText, wordText)
                }
                System.out.format("%nParagraph: %n%s%n", paraText)
                System.out.format("Paragraph bounding box: %s%n", para.asJsonObject["boundingBox"])
                System.out.format("Paragraph Confidence: %f%n", para.asJsonObject["confidence"].asFloat)
                blockText += paraText
            }
            pageText += blockText
        }
    }
    

    Java

    for (JsonElement page : annotation.get("pages").getAsJsonArray()) {
        StringBuilder pageText = new StringBuilder();
        for (JsonElement block : page.getAsJsonObject().get("blocks").getAsJsonArray()) {
            StringBuilder blockText = new StringBuilder();
            for (JsonElement para : block.getAsJsonObject().get("paragraphs").getAsJsonArray()) {
                StringBuilder paraText = new StringBuilder();
                for (JsonElement word : para.getAsJsonObject().get("words").getAsJsonArray()) {
                    StringBuilder wordText = new StringBuilder();
                    for (JsonElement symbol : word.getAsJsonObject().get("symbols").getAsJsonArray()) {
                        wordText.append(symbol.getAsJsonObject().get("text").getAsString());
                        System.out.format("Symbol text: %s (confidence: %f)%n", symbol.getAsJsonObject().get("text").getAsString(), symbol.getAsJsonObject().get("confidence").getAsFloat());
                    }
                    System.out.format("Word text: %s (confidence: %f)%n%n", wordText.toString(), word.getAsJsonObject().get("confidence").getAsFloat());
                    System.out.format("Word bounding box: %s%n", word.getAsJsonObject().get("boundingBox"));
                    paraText.append(wordText.toString()).append(" ");
                }
                System.out.format("%nParagraph:%n%s%n", paraText);
                System.out.format("Paragraph bounding box: %s%n", para.getAsJsonObject().get("boundingBox"));
                System.out.format("Paragraph Confidence: %f%n", para.getAsJsonObject().get("confidence").getAsFloat());
                blockText.append(paraText);
            }
            pageText.append(blockText);
        }
    }