Безопасное распознавание текста на изображениях с помощью Cloud Vision с использованием аутентификации Firebase и функций на Android

Для вызова API Google Cloud из вашего приложения необходимо создать промежуточный REST API, который обрабатывает авторизацию и защищает секретные значения, такие как ключи API. Затем вам нужно написать код в вашем мобильном приложении для аутентификации и взаимодействия с этим промежуточным сервисом.

Один из способов создания такого REST API — использование Firebase Authentication and Functions, который предоставляет управляемый бессерверный шлюз к API Google Cloud, обрабатывающий аутентификацию и доступный для вызова из мобильного приложения с помощью предварительно настроенных SDK.

В этом руководстве показано, как использовать этот метод для вызова API Cloud Vision из вашего приложения. Этот метод позволит всем авторизованным пользователям получать доступ к платным услугам Cloud Vision через ваш облачный проект, поэтому, прежде чем продолжить, подумайте, достаточно ли этого механизма аутентификации для вашего случая.

Прежде чем начать

Настройте свой проект

  1. Если вы еще этого не сделали, добавьте Firebase в свой Android-проект .
  2. Если вы еще не включили облачные API для своего проекта, сделайте это сейчас:

    1. Откройте страницу API Firebase ML в консоли Firebase .
    2. Если вы еще не перевели свой проект на тарифный план Blaze с оплатой по факту использования , нажмите «Обновить» , чтобы сделать это. (Вам будет предложено обновить тарифный план только в том случае, если ваш проект не подключен к тарифному плану Blaze.)

      Использовать облачные API могут только проекты, использующие тарифный план Blaze.

    3. Если облачные API еще не включены, нажмите «Включить облачные API» .
  3. Настройте существующие ключи API Firebase, чтобы запретить доступ к API Cloud Vision:
    1. Откройте страницу «Учетные данные» в консоли Cloud.
    2. Для каждого ключа API в списке откройте окно редактирования и в разделе «Ограничения по ключу» добавьте в список все доступные API, кроме Cloud Vision API.

Разверните вызываемую функцию

Далее разверните облачную функцию, которую вы будете использовать для связи вашего приложения с API Cloud Vision. В репозитории functions-samples содержится пример, который вы можете использовать.

По умолчанию доступ к API Cloud Vision через эту функцию будет разрешен только авторизованным пользователям вашего приложения. Вы можете изменить функцию в соответствии с другими требованиями.

Для развертывания функции:

  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. Создайте проект Firebase в каталоге vision-annotate-image . При появлении запроса выберите свой проект в списке.
    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:22.1.0")
    implementation("com.google.code.gson:gson:2.8.6")
  • Теперь вы готовы начать распознавать текст на изображениях.

    1. Подготовьте входное изображение.

    Для вызова Cloud Vision изображение должно быть отформатировано как строка, закодированная в base64. Для обработки изображения из сохраненного файла по URI:
    1. Получите изображение в виде объекта Bitmap :

      Kotlin

      var bitmap: Bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)

      Java

      Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
    2. При желании можно уменьшить масштаб изображения для экономии трафика. См. рекомендуемые размеры изображений в Cloud Vision.

      Kotlin

      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

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

      Java

      // Scale down bitmap size
      bitmap = scaleBitmapDown(bitmap, 640);
    3. Преобразуйте объект растрового изображения в строку, закодированную в формате Base64:

      Kotlin

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

      private lateinit var functions: FirebaseFunctions
      // ...
      functions = Firebase.functions
      

      Java

      private FirebaseFunctions mFunctions;
      // ...
      mFunctions = FirebaseFunctions.getInstance();
      
    2. Определите метод для вызова функции:

      Kotlin

      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-запрос. API Cloud Vision поддерживает два типа распознавания текста: TEXT_DETECTION и DOCUMENT_TEXT_DETECTION . Различия между этими двумя вариантами использования см. в документации Cloud Vision OCR.

      Kotlin

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

      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

      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. Извлечение текста из блоков распознанного текста.

    Если операция распознавания текста пройдет успешно, в результате выполнения задачи будет возвращен JSON-ответ типа BatchAnnotateImagesResponse . Текстовые аннотации можно найти в объекте fullTextAnnotation .

    Распознанный текст можно получить в виде строки в text поле. Например:

    Kotlin

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

    Вы также можете получить информацию, относящуюся к конкретным областям изображения. Для каждого block , paragraph , word и symbol вы можете получить текст, распознанный в этой области, и ограничивающие координаты этой области. Например:

    Kotlin

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