获取我们在 Firebase 峰会上发布的所有信息,了解 Firebase 可如何帮助您加快应用开发速度并满怀信心地运行应用。了解详情

在 Android 上使用 Firebase 身份验证和函数通过 Cloud Vision 安全地识别图像中的文本

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

为了从您的应用调用 Google Cloud API,您需要创建一个中间 REST API 来处理授权并保护 API 密钥等秘密值。然后,您需要在您的移动应用程序中编写代码来验证此中间服务并与之通信。

创建此 REST API 的一种方法是使用 Firebase 身份验证和函数,它为您提供了一个托管的、无服务器的 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. 配置您现有的 Firebase API 密钥以禁止访问 Cloud Vision API:
    1. 打开云控制台的凭据页面。
    2. 对于列表中的每个 API 密钥,打开编辑视图,然后在密钥限制部分中,将Cloud Vision API 之外的所有可用 API 添加到列表中。

部署可调用函数

接下来,部署您将用于桥接您的应用程序和 Cloud Vision API 的 Cloud Functions 函数。 functions-samples存储库包含一个您可以使用的示例。

默认情况下,通过此函数访问 Cloud Vision API 将只允许您应用的经过身份验证的用户访问 Cloud Vision API。您可以根据不同的要求修改功能。

部署功能:

  1. 克隆或下载functions-samples repo并更改到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 身份验证添加到您的应用

上面部署的可调用函数将拒绝来自您应用程序的未经身份验证的用户的任何请求。如果您尚未这样做,则需要将 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.调用可调用函数识别文本

    要识别图像中的文本,请调用可调用函数,传递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 。请参阅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)
    

    您还可以获得特定于图像区域的信息。对于每个blockparagraphwordsymbol ,您可以获得区域内识别的文本和区域的边界坐标。例如:

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