使用 Android 上的 Firebase 身份驗證和功能,透過 Cloud Vision 安全地識別圖像中的文本

為了從您的應用程式呼叫 Google Cloud API,您需要建立一個中間 REST API,用於處理授權並保護 API 金鑰等秘密值。然後,您需要在行動應用程式中編寫程式碼以對此中間服務進行身份驗證並與其進行通訊。

建立此REST API 的一種方法是使用Firebase 驗證和函數,它為您提供了一個通往Google Cloud API 的託管無伺服器網關,該API 可以處理身份驗證,並可以使用預先建置的SDK 從您的移動應用進行調用。

本指南示範如何使用此技術從您的應用程式呼叫 Cloud Vision API。此方法將允許所有經過驗證的使用者透過您的雲端專案存取 Cloud Vision 計費服務,因此在繼續之前請考慮此身份驗證機制是否足以滿足您的用例。

在你開始之前

配置您的項目

  1. 如果您尚未將 Firebase 新增至您的 Android 專案中,請將其新增至您的 Android 專案中。
  2. 如果您尚未為您的專案啟用基於雲端的 API,請立即執行此操作:

    1. 開啟 Firebase 控制台的Firebase ML API 頁面
    2. 如果您尚未將項目升級到 Blaze 定價計劃,請按一下升​​級來執行此操作。 (只有當您的專案不在 Blaze 計劃中時,系統才會提示您升級。)

      只有 Blaze 等級的項目才能使用基於雲端的 API。

    3. 如果尚未啟用基於雲端的 API,請按一下啟用基於雲端的 API
  3. 配置現有 Firebase API 金鑰以禁止存取 Cloud Vision API:
    1. 開啟雲端控制台的憑證頁面。
    2. 對於清單中的每個 API 金鑰,開啟編輯視圖,然後在金鑰限制部分中,將除 Cloud Vision API之外的所有可用 API 新增至清單。

部署可呼叫函數

接下來,部署將用於橋接應用程式和 Cloud Vision API 的 Cloud Function。 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 新增到您的應用程式中。

為您的應用程式添加必要的依賴項

  • 將 Cloud Functions for Firebase(客戶端)和 gson Android 庫的依賴項新增至模組(應用程式)Gradle 檔案(通常<project>/<app-module>/build.gradle.kts<project>/<app-module>/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.呼叫callable函數識別文本

    若要識別圖像中的文本,請呼叫可呼叫函數,並傳遞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 。有關兩個用例之間的差異,請參閱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);
        }
    }