Catch up on everything we announced at this year's Firebase Summit. Learn more

在 Android 上使用 Firebase 身份驗證和函數通過 Cloud Vision 安全識別圖像中的文本

為了從您的應用程序調用 Google Cloud API,您需要創建一個中間 REST API 來處理授權並保護 API 密鑰等秘密值。然後,您需要在您的移動應用程序中編寫代碼來驗證此中間服務並與之通信。

創建此 REST API 的一種方法是使用 Firebase 身份驗證和函數,它為您提供了一個託管的、無服務器的 Google Cloud API 網關,該網關處理身份驗證並可從您的移動應用程序中使用預構建的 SDK 進行調用。

本指南演示瞭如何使用此技術從您的應用程序調用 Cloud Vision API。此方法將允許所有經過身份驗證的用戶通過您的 Cloud 項目訪問 Cloud Vision 計費服務,因此在繼續之前請考慮此身份驗證機制是否足以滿足您的用例。

在你開始之前

配置您的項目

  1. 如果你還沒有,添加火力地堡到您的Android項目
  2. 如果您尚未為您的項目啟用基於雲的 API,請立即啟用:

    1. 打開火力地堡ML API頁面的火力地堡控制台。
    2. 如果您尚未升級您的項目以大火定價計劃,單擊升級到這樣做。 (只有當您的項目不在 Blaze 計劃中時,才會提示您升級。)

      只有 Blaze 級別的項目可以使用基於雲的 API。

    3. 如果基於雲的API尚未啟用,單擊啟用基於雲的API。
  3. 配置您現有的 Firebase API 密鑰以禁止訪問 Cloud Vision API:
    1. 打開證書雲控制台的頁面。
    2. 對於列表中的每個API密鑰,打開編輯視圖,並在重點限制部分,加入所有可用的API,除了雲願景API到列表中。

部署可調用函數

接下來,部署您將用於橋接應用程序和 Cloud Vision API 的 Cloud Functions。該functions-samples庫包含你可以用一個例子。

默認情況下,通過此函數訪問 Cloud Vision API 將僅允許您的應用程序的經過身份驗證的用戶訪問 Cloud Vision API。您可以根據不同的要求修改該功能。

部署函數:

  1. 克隆或下載的功能樣本回購並切換到vision-annotate-image目錄:
    git clone https://github.com/firebase/functions-samples
    cd vision-annotate-image
    
  2. 安裝依賴:
    cd functions
    npm install
    cd ..
    
  3. 如果你不具備火力地堡CLI,安裝它
  4. 初始化在一個火力地堡項目vision-annotate-image目錄。出現提示時,在列表中選擇您的項目。
    firebase init
  5. 部署功能:
    firebase deploy --only functions:annotateImage

將 Firebase 身份驗證添加到您的應用

上面部署的可調用函數將拒絕來自您的應用程序的未經身份驗證的用戶的任何請求。如果你還沒有這樣做的話,你將需要火力地堡驗證添加到您的應用程序。

向您的應用程序添加必要的依賴項

  • 添加依賴的火力地堡的功能和Android的GSON庫,你的模塊(應用程序級)搖籃文件(通常是應用程序/的build.gradle):
    implementation 'com.google.firebase:firebase-functions:20.0.1'
    implementation 'com.google.code.gson:gson:2.8.6'
    
  • 現在您已準備好開始識別圖像中的文本。

    1.準備輸入圖像

    為了調用 Cloud Vision,圖像必須格式化為 base64 編碼的字符串。要處理來自已保存文件 URI 的圖像:
    1. 獲取圖像作為Bitmap對象:

      爪哇

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

      科特林+KTX

      var bitmap: Bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
    2. 或者,縮小圖像以節省帶寬。見雲願景推薦的圖像尺寸。

      爪哇

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

      科特林+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)
      }

      爪哇

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

      科特林+KTX

      // Scale down bitmap size
      bitmap = scaleBitmapDown(bitmap, 640)
    3. 將位圖對象轉換為 base64 編碼的字符串:

      爪哇

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

      科特林+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雲觀測要求

    1. 首先,初始化 Cloud Functions 的一個實例:

      爪哇

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

      科特林+KTX

      private lateinit var functions: FirebaseFunctions
      // ...
      functions = Firebase.functions
      
    2. 定義調用函數的方法:

      爪哇

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

      科特林+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 請求。雲願景API支持兩種類型的文字檢測: TEXT_DETECTIONDOCUMENT_TEXT_DETECTION 。見雲願景OCR文檔的兩個用例之間的差異。

      爪哇

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

      科特林+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)
      

      可選,提供語言提示,以幫助語言檢測(見支持的語言):

      爪哇

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

      科特林+KTX

      val imageContext = JsonObject()
      val languageHints = JsonArray()
      languageHints.add("en")
      imageContext.add("languageHints", languageHints)
      request.add("imageContext", imageContext)
      
    4. 最後,調用函數:

      爪哇

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

      科特林+KTX

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

    3.從識別的文本塊中提取文本

    如果文本識別操作成功,一個JSON響應BatchAnnotateImagesResponse將在任務的結果返回。文本註釋可以在找到fullTextAnnotation對象。

    你可以得到識別的文本作為一個字符串text字段。例如:

    爪哇

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

    科特林+KTX

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

    您還可以獲得特定於圖像區域的信息。對於每一個blockparagraphwordsymbol ,你可以在該地區公認的文字和該地區的邊界坐標。例如:

    爪哇

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

    科特林+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
        }
    }