Reconhecer pontos de referência com segurança usando o Cloud Vision com o Firebase Auth e Functions no Android

Para chamar uma API do Google Cloud usando seu app, crie uma API REST intermediária que gerencie a autorização e proteja valores secretos, como chaves de API. Em seguida, escreva o código no seu app para dispositivos móveis a fim de autenticar e se comunicar com esse serviço intermediário.

Uma maneira de criar essa API REST é usar o Firebase Authentication e o Functions, que oferecem um gateway gerenciado sem servidor para as APIs do Google Cloud, que processa a autenticação e pode ser chamado no seu app para dispositivos móveis com SDKs pré-criados.

Neste guia, você verá como usar essa técnica para chamar a API Cloud Vision do seu app. Esse método permitirá que todos os usuários autenticados acessem os serviços faturados do Cloud Vision no projeto do Cloud. Sendo assim, considere se esse mecanismo de autenticação é suficiente para seu caso de uso antes de continuar.

Antes de começar

Configurar seu projeto

  1. Adicione o Firebase ao seu projeto para Android, caso ainda não tenha feito isso.
  2. Se você ainda não ativou APIs baseadas em nuvem para seu projeto, siga estas etapas:

    1. Abra a página de APIs do Firebase ML do console do Firebase.
    2. Se você ainda não fez o upgrade do seu projeto para o plano de preços Blaze, clique em Fazer upgrade. Você só vai receber uma mensagem para fazer upgrade se o projeto não estiver no plano Blaze.

      Apenas projetos no nível Blaze podem usar APIs baseadas na nuvem.

    3. Caso as APIs baseadas na nuvem ainda não estejam ativadas, clique em Ativar APIs baseadas na nuvem.
  3. Configure as chaves de API do Firebase para proibir o acesso à API Cloud Vision:
    1. Abra a página Credenciais do Console do Cloud.
    2. Para cada chave de API na lista, abra a visualização de edição e, na seção "Restrições de chave", adicione à lista todas as APIs disponíveis, exceto a API Cloud Vision.

Como implantar a função chamável

Agora, você implantará a função do Cloud que será usada para conectar seu app e a API Cloud Vision. O repositório functions-samples contém um exemplo que pode ser usado.

Por padrão, essa função permitirá que apenas usuários autenticados do seu app acessem a API Cloud Vision. É possível modificar a função para requisitos diferentes.

Para implantar a função, siga estas etapas:

  1. Clone ou faça o download do repositório functions-samples e mude para o diretório Node-1st-gen/vision-annotate-image:
    git clone https://github.com/firebase/functions-samples
    cd Node-1st-gen/vision-annotate-image
    
  2. Instale as dependências:
    cd functions
    npm install
    cd ..
  3. Se você não tiver a CLI do Firebase, faça a instalação.
  4. Inicialize um novo projeto do Firebase no diretório vision-annotate-image. Quando solicitado, selecione o projeto na lista.
    firebase init
  5. Implante a função:
    firebase deploy --only functions:annotateImage

Adicionar o Firebase Auth ao seu app

A função chamável implementada acima rejeitará qualquer solicitação de usuários não autenticados do seu app. Adicione o Firebase Authentication ao app, caso ainda não tenha feito isso.

Adicionar as dependências necessárias ao app

  • Adicione as dependências das bibliotecas do Cloud Functions para Firebase (cliente) e do gson para Android ao seu arquivo Gradle do módulo (nível do app) (geralmente <project>/<app-module>/build.gradle.kts ou <project>/<app-module>/build.gradle):
    implementation("com.google.firebase:firebase-functions:21.1.0")
    implementation("com.google.code.gson:gson:2.8.6")
  • 1. Preparar a imagem de entrada

    Para chamar o Cloud Vision, a imagem precisa ser formatada como uma string codificada em base64. Para processar uma imagem usando um URI de arquivo salvo:
    1. Consiga a imagem como um objeto Bitmap:

      Kotlin+KTX

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

      Java

      Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
    2. Opcionalmente, diminua a imagem para economizar largura de banda. Confira os tamanhos de imagem recomendados do 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. Converta o objeto bitmap em uma string codificada em 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. A imagem representada pelo objeto Bitmap precisa estar na posição vertical, sem necessidade de girá-la novamente.

    2. Invocar a função chamável para reconhecer pontos de referência

    Para reconhecer pontos de referência em uma imagem, invoque a função chamável transmitindo uma solicitação JSON do Cloud Vision.

    1. Inicialize uma instância do Cloud Functions:

      Kotlin+KTX

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

      Java

      private FirebaseFunctions mFunctions;
      // ...
      mFunctions = FirebaseFunctions.getInstance();
      
    2. Defina um método para invocar a função:

      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. Crie uma solicitação JSON com Type LANDMARK_DETECTION:

      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("maxResults", JsonPrimitive(5))
      feature.add("type", JsonPrimitive("LANDMARK_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("maxResults", new JsonPrimitive(5));
      feature.add("type", new JsonPrimitive("LANDMARK_DETECTION"));
      JsonArray features = new JsonArray();
      features.add(feature);
      request.add("features", features);
      
    4. Por fim, invoque a função:

      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. Ver informações sobre os pontos de referência reconhecidos

    Se a operação de reconhecimento de ponto de referência for bem-sucedida, uma resposta JSON de BatchAnnotateImagesResponse será retornada no resultado da tarefa. Cada objeto na matriz landmarkAnnotations representa um ponto de referência reconhecido na imagem. Para cada ponto de referência, é possível receber as coordenadas delimitadoras na imagem de entrada, o nome do ponto de referência, a latitude, a longitude, o ID da entidade no Mapa de informações (se disponível) e a pontuação de confiança da correspondência. Exemplo:

    Kotlin+KTX

    for (label in task.result!!.asJsonArray[0].asJsonObject["landmarkAnnotations"].asJsonArray) {
        val labelObj = label.asJsonObject
        val landmarkName = labelObj["description"]
        val entityId = labelObj["mid"]
        val score = labelObj["score"]
        val bounds = labelObj["boundingPoly"]
        // Multiple locations are possible, e.g., the location of the depicted
        // landmark and the location the picture was taken.
        for (loc in labelObj["locations"].asJsonArray) {
            val latitude = loc.asJsonObject["latLng"].asJsonObject["latitude"]
            val longitude = loc.asJsonObject["latLng"].asJsonObject["longitude"]
        }
    }
    

    Java

    for (JsonElement label : task.getResult().getAsJsonArray().get(0).getAsJsonObject().get("landmarkAnnotations").getAsJsonArray()) {
        JsonObject labelObj = label.getAsJsonObject();
        String landmarkName = labelObj.get("description").getAsString();
        String entityId = labelObj.get("mid").getAsString();
        float score = labelObj.get("score").getAsFloat();
        JsonObject bounds = labelObj.get("boundingPoly").getAsJsonObject();
        // Multiple locations are possible, e.g., the location of the depicted
        // landmark and the location the picture was taken.
        for (JsonElement loc : labelObj.get("locations").getAsJsonArray()) {
            JsonObject latLng = loc.getAsJsonObject().get("latLng").getAsJsonObject();
            double latitude = latLng.get("latitude").getAsDouble();
            double longitude = latLng.get("longitude").getAsDouble();
        }
    }