透過 Android 上的 Firebase 驗證和函式,透過 Cloud Vision 安全地辨識圖片中的文字

如要從應用程式呼叫 Google Cloud API,您必須建立中繼 處理授權並保護密鑰值 (例如 API 金鑰) 的 REST API。接著您需要 在行動應用程式中編寫程式碼,以便驗證這個中繼服務並與其通訊。

建立此 REST API 的其中一種方法是使用 Firebase 驗證和函式。這類閘道會提供一個代管的無伺服器閘道,讓您 處理驗證作業的 Google Cloud API,可透過行動應用程式呼叫 預先建構的 SDK

本指南示範如何使用這項技術從應用程式呼叫 Cloud Vision API。 這個方法將允許所有通過驗證的使用者透過您的 Cloud 專案存取 Cloud Vision 計費服務。 請先思考這項驗證機制是否足以滿足您的用途需求,再繼續操作。

,瞭解如何調查及移除這項存取權。

事前準備

設定專案

  1. 如果還沒試過 將 Firebase 新增至您的 Android 專案
  2. 如果您尚未為專案啟用雲端式 API,請先啟用 現在:

    1. 開啟 Firebase ML Firebase 控制台的 API 頁面
    2. 如果您尚未將專案升級至 Blaze 定價方案,請按一下 如要這麼做,請升級。(只有在您的 專案並未採用 Blaze 方案)。

      只有 Blaze 層級的專案可以使用以雲端為基礎的 API。

    3. 如果尚未啟用雲端式 API,請按一下「Enable Cloud-based API」(啟用雲端式 API) API
    ,瞭解如何調查及移除這項存取權。
  3. 設定現有的 Firebase API 金鑰來禁止存取 Cloud Vision API:
    1. 開啟 Cloud 控制台的「Credentials」(憑證) 頁面。
    2. 針對清單中的每個 API 金鑰,開啟編輯檢視畫面,然後 限制專區,請新增所有可用的 API (「Cloud Vision」除外) 新增至清單

部署可呼叫函式

接下來,請部署您將用來連結應用程式和 Cloud Vision API。functions-samples 存放區包含範例 。

根據預設,透過這個函式存取 Cloud Vision API 將允許 只有通過驗證的應用程式使用者才能存取 Cloud Vision API。你可以 並根據不同的需求修改函式

如何部署函式:

  1. 複製或下載 functions-samples 存放區 然後變更為 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 驗證新增至應用程式

上方部署的可呼叫函式會拒絕所有未經驗證的要求 對於應用程式使用者而言如果尚未新增 Firebase 作業,請先完成這項操作。 向應用程式進行驗證。

為應用程式新增必要的依附元件

  • 為 Cloud Functions for Firebase (用戶端) 和 Gson Android 程式庫新增依附元件 新增至模組 (應用程式層級) Gradle 檔案 (通常為 <project>/<app-module>/build.gradle.kts<project>/<app-module>/build.gradle):
    implementation("com.google.firebase:firebase-functions:21.0.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。 請參閱 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. 從已辨識的文字區塊擷取文字

    如果文字辨識作業成功,系統會傳回 批次註解圖片回應 會在工作的結果中傳回文字註解位於 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);
        }
    }