Firebase Summit で発表されたすべての情報をご覧ください。Firebase を使用してアプリ開発を加速し、自信を持ってアプリを実行する方法を紹介しています。詳細

AndroidのFirebaseAuthとFunctionsを使用して、CloudVisionで画像内のテキストを安全に認識します

アプリから Google Cloud API を呼び出すには、承認を処理し、API キーなどの秘密の値を保護する中間 REST API を作成する必要があります。次に、モバイル アプリでコードを記述して、この中間サービスを認証し、通信する必要があります。

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

このガイドでは、この手法を使用してアプリから Cloud Vision API を呼び出す方法を示します。この方法では、すべての認証済みユーザーが Cloud プロジェクトを介して Cloud Vision の有料サービスにアクセスできるようになるため、先に進む前に、この認証メカニズムがユースケースに十分かどうかを検討してください。

あなたが始める前に

プロジェクトを構成する

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

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

      Blaze レベルのプロジェクトのみがクラウドベースの API を使用できます。

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

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

次に、アプリと Cloud Vision API をブリッジするために使用する Cloud Function をデプロイします。 functions-samplesリポジトリには、使用できる例が含まれています。

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

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

  1. functions-samples リポジトリを複製またはダウンロードし、 vision-annotate-imageディレクトリに変更します:
    git clone https://github.com/firebase/functions-samples
    cd 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 関数と gson Android ライブラリの依存関係をモジュール (アプリレベル) の Gradle ファイル (通常は app/build.gradle) に追加します:
    implementation 'com.google.firebase:firebase-functions:20.2.1'
    implementation 'com.google.code.gson:gson:2.8.6'
    
  • これで、画像内のテキストの認識を開始する準備が整いました。

    1. 入力画像を準備する

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

      Java

      Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);

      Kotlin+KTX

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

      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

      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

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

      Kotlin+KTX

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

      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);

      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)
    4. Bitmapオブジェクトによって表されるイメージは、追加の回転は必要なく、直立している必要があります。

    2. callable 関数を呼び出してテキストを認識する

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

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

      Java

      private FirebaseFunctions mFunctions;
      // ...
      mFunctions = FirebaseFunctions.getInstance();
      

      Kotlin+KTX

      private lateinit var functions: FirebaseFunctions
      // ...
      functions = Firebase.functions
      
    2. 関数を呼び出すメソッドを定義します。

      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()));
                      }
                  });
      }
      

      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))
                  }
      }
      
    3. JSON 要求を作成します。 Cloud Vision API は、 TEXT_DETECTIONDOCUMENT_TEXT_DETECTIONの 2種類のテキスト検出をサポートしています。 2 つのユース ケースの違いについては、 Cloud Vision OCR ドキュメントを参照してください。

      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

      // 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

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

      Kotlin+KTX

      val imageContext = JsonObject()
      val languageHints = JsonArray()
      languageHints.add("en")
      imageContext.add("languageHints", languageHints)
      request.add("imageContext", imageContext)
      
    4. 最後に、関数を呼び出します。

      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
                          // ...
                      }
                  }
              });
      

      Kotlin+KTX

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

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

    テキスト認識操作が成功すると、タスクの結果でBatchAnnotateImagesResponseの JSON 応答が返されます。テキスト注釈はfullTextAnnotationオブジェクトにあります。

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

    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());
    

    Kotlin+KTX

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

    画像の領域に固有の情報を取得することもできます。 blockparagraphword 、およびsymbolごとに、領域で認識されたテキストと領域の境界座標を取得できます。例えば:

    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);
        }
    }
    

    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
        }
    }