Crea una app para Android a fin de detectar objetos dentro de las imágenes

1. Antes de comenzar

En este codelab, aprenderás a ejecutar una inferencia de detección de objetos desde una app para Android con TensorFlow Serving con REST y gRPC.

Requisitos previos

  • Conocimientos básicos sobre el desarrollo para Android con Java
  • Conocimientos básicos del aprendizaje automático con TensorFlow, como la implementación y el entrenamiento
  • Conocimientos básicos de las terminales y Docker

Qué aprenderás

  • Cómo encontrar modelos de detección de objetos previamente entrenados en TensorFlow Hub
  • Cómo compilar una app para Android simple y hacer predicciones con el modelo de detección de objetos descargado a través de TensorFlow Serving (REST y gRPC).
  • Cómo renderizar el resultado de la detección en la IU

Requisitos

2. Prepárate

Para descargar el código de este codelab, haz lo siguiente:

  1. Navega al repositorio de GitHub de este codelab.
  2. Haz clic en Código > Descargar ZIP para descargar todo el código de este codelab.

a72f2bb4caa9a96.png

  1. Descomprime el archivo ZIP descargado para descomprimir una carpeta raíz codelabs con todos los recursos que necesitas.

Para este codelab, solo necesitas los archivos del subdirectorio TFServing/ObjectDetectionAndroid en el repositorio, que contiene dos carpetas:

  • La carpeta starter contiene el código de inicio en el que se basa este codelab.
  • La carpeta finished contiene el código completo de la app de muestra finalizada.

3. Cómo agregar las dependencias al proyecto

Cómo importar la app de inicio a Android Studio

  • En Android Studio, haz clic en File > New > Import project y elige la carpeta starter en el código fuente que descargaste antes.

Agrega las dependencias para OkHttp y gRPC

  • En el archivo app/build.gradle de tu proyecto, confirma la presencia de las dependencias.
dependencies {
  // ...
    implementation 'com.squareup.okhttp3:okhttp:4.9.0'
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
    implementation 'io.grpc:grpc-okhttp:1.29.0'
    implementation 'io.grpc:grpc-protobuf-lite:1.29.0'
    implementation 'io.grpc:grpc-stub:1.29.0'
}

Cómo sincronizar tu proyecto con archivos de Gradle

  • Selecciona 541e90b497a7fef7.png Sync Project with Gradle Files en el menú de navegación.

4. Ejecuta la app de inicio

Ejecuta y explora la app

La app debe iniciarse en el dispositivo Android. La IU es bastante simple: hay una imagen de gato en la que quieres detectar objetos y el usuario puede elegir la forma de enviar los datos al backend con REST o gRPC. El backend realiza la detección de objetos en la imagen y muestra los resultados en la app cliente, que vuelve a procesar la IU.

24eab579530e9645.png

En este momento, si haces clic en Run inferencia, no sucede nada. Esto se debe a que aún no puede comunicarse con el backend.

5. Implementa un modelo de detección de objetos con TensorFlow Serving

La detección de objetos es una tarea de AA muy común y su objetivo es detectar objetos dentro de las imágenes, es decir, predecir categorías posibles de los objetos y cuadros de límite a su alrededor. Este es un ejemplo de un resultado de detección:

a68f9308fb2fc17b.png

Google publicó una serie de modelos previamente entrenados en TensorFlow Hub. Para ver la lista completa, visita la página object_detection. Para este codelab, usarás el modelo SSD MobileNet V2 FPNLite 320x320 relativamente liviano, de modo que no necesariamente necesites usar una GPU para ejecutarlo.

Para implementar el modelo de detección de objetos con TensorFlow Serving, sigue estos pasos:

  1. Descarga el archivo del modelo.
  2. Descomprime el archivo .tar.gz descargado con una herramienta de descompresión, como 7-Zip.
  3. Crea una carpeta ssd_mobilenet_v2_2_320 y, luego, una subcarpeta 123 dentro de ella.
  4. Coloca la carpeta variables extraída y el archivo saved_model.pb en la subcarpeta 123.

Puedes hacer referencia a la carpeta ssd_mobilenet_v2_2_320 como la carpeta SavedModel. 123 es un número de versión de ejemplo. Si quieres, puedes elegir otro número.

La estructura de carpetas debe verse de la siguiente manera:

42c8150a42033767.png

Inicia TensorFlow Serving

  • En tu terminal, inicia TensorFlow Serving con Docker, pero reemplaza el marcador de posición PATH/TO/SAVEDMODEL por la ruta de acceso absoluta de la carpeta ssd_mobilenet_v2_2_320 en tu computadora.
docker pull tensorflow/serving

docker run -it --rm -p 8500:8500 -p 8501:8501 -v "PATH/TO/SAVEDMODEL:/models/ssd_mobilenet_v2_2" -e MODEL_NAME=ssd_mobilenet_v2_2 tensorflow/serving

Docker descarga automáticamente la imagen de TensorFlow Serving primero, lo cual tarda un minuto. Luego, TensorFlow Serving debería comenzar. El registro debería verse como este fragmento de código:

2022-02-25 06:01:12.513231: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:206] Restoring SavedModel bundle.
2022-02-25 06:01:12.585012: I external/org_tensorflow/tensorflow/core/platform/profile_utils/cpu_utils.cc:114] CPU Frequency: 3000000000 Hz
2022-02-25 06:01:13.395083: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:190] Running initialization op on SavedModel bundle at path: /models/ssd_mobilenet_v2_2/123
2022-02-25 06:01:13.837562: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:277] SavedModel load for tags { serve }; Status: success: OK. Took 1928700 microseconds.
2022-02-25 06:01:13.877848: I tensorflow_serving/servables/tensorflow/saved_model_warmup_util.cc:59] No warmup data file found at /models/ssd_mobilenet_v2_2/123/assets.extra/tf_serving_warmup_requests
2022-02-25 06:01:13.929844: I tensorflow_serving/core/loader_harness.cc:87] Successfully loaded servable version {name: ssd_mobilenet_v2_2 version: 123}
2022-02-25 06:01:13.985848: I tensorflow_serving/model_servers/server_core.cc:486] Finished adding/updating models
2022-02-25 06:01:13.985987: I tensorflow_serving/model_servers/server.cc:367] Profiler service is enabled
2022-02-25 06:01:13.988994: I tensorflow_serving/model_servers/server.cc:393] Running gRPC ModelServer at 0.0.0.0:8500 ...
[warn] getaddrinfo: address family for nodename not supported
2022-02-25 06:01:14.033872: I tensorflow_serving/model_servers/server.cc:414] Exporting HTTP/REST API at:localhost:8501 ...
[evhttp_server.cc : 245] NET_LOG: Entering the event loop ...

6. Conecta la app para Android con TensorFlow Serving a través de REST

El backend está listo, por lo que puedes enviar solicitudes de clientes a TensorFlow Serving para detectar objetos en imágenes. Hay dos maneras de enviar solicitudes a TensorFlow Serving:

  • REST
  • gRPC

Envía solicitudes y recibe respuestas a través de REST

Existen tres pasos sencillos:

  • Crea la solicitud REST.
  • Envía la solicitud de REST a TensorFlow Serving.
  • Extrae el resultado previsto de la respuesta de REST y procesa la IU.

Los lograrás en MainActivity.java.

Crea la solicitud de REST

En este momento, hay una función createRESTRequest() vacía en el archivo MainActivity.java. Esta función se implementa para crear una solicitud REST.

private Request createRESTRequest() {
}

TensorFlow Serving espera una solicitud POST que contiene el tensor de imagen para el modelo SSD MobileNet que usas, por lo que debes extraer los valores RGB de cada píxel de la imagen en un array y luego unirlo en un JSON, que es la carga útil de la solicitud.

  • Agrega este código a la función createRESTRequest():
//Create the REST request.
int[] inputImg = new int[INPUT_IMG_HEIGHT * INPUT_IMG_WIDTH];
int[][][][] inputImgRGB = new int[1][INPUT_IMG_HEIGHT][INPUT_IMG_WIDTH][3];
inputImgBitmap.getPixels(inputImg, 0, INPUT_IMG_WIDTH, 0, 0, INPUT_IMG_WIDTH, INPUT_IMG_HEIGHT);
int pixel;
for (int i = 0; i < INPUT_IMG_HEIGHT; i++) {
    for (int j = 0; j < INPUT_IMG_WIDTH; j++) {
    // Extract RBG values from each pixel; alpha is ignored
    pixel = inputImg[i * INPUT_IMG_WIDTH + j];
    inputImgRGB[0][i][j][0] = ((pixel >> 16) & 0xff);
    inputImgRGB[0][i][j][1] = ((pixel >> 8) & 0xff);
    inputImgRGB[0][i][j][2] = ((pixel) & 0xff);
    }
}

RequestBody requestBody =
    RequestBody.create("{\"instances\": " + Arrays.deepToString(inputImgRGB) + "}", JSON);

Request request =
    new Request.Builder()
        .url("http://" + SERVER + ":" + REST_PORT + "/v1/models/" + MODEL_NAME + ":predict")
        .post(requestBody)
        .build();

return request;

Envía la solicitud de REST a TensorFlow Serving

La app le permite al usuario elegir REST o gRPC para comunicarse con TensorFlow Serving, por lo que hay dos ramas en el objeto de escucha onClick(View view).

predictButton.setOnClickListener(
    new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (requestRadioGroup.getCheckedRadioButtonId() == R.id.rest) {
                // TODO: REST request
            }
            else {

            }
        }
    }
)
  • Agrega este código a la rama de REST del objeto de escucha onClick(View view) para usar OkHttp a fin de enviar la solicitud a TensorFlow Serving:
// Send the REST request.
Request request = createRESTRequest();
try {
    client =
        new OkHttpClient.Builder()
            .connectTimeout(20, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .callTimeout(20, TimeUnit.SECONDS)
            .build();
    Response response = client.newCall(request).execute();
    JSONObject responseObject = new JSONObject(response.body().string());
    postprocessRESTResponse(responseObject);
} catch (IOException | JSONException e) {
    Log.e(TAG, e.getMessage());
    responseTextView.setText(e.getMessage());
    return;
}

Cómo procesar la respuesta de REST de TensorFlow Serving

El modelo Mobile Mobile SSD muestra varios resultados, entre los que se incluyen los siguientes:

  • num_detections: Es la cantidad de detecciones.
  • detection_scores: puntuaciones de detección
  • detection_classes: Es el índice de la clase de detección.
  • detection_boxes: las coordenadas del cuadro de límite

Implementarás la función postprocessRESTResponse() para controlar la respuesta.

private void postprocessRESTResponse(Predict.PredictResponse response) {

}
  • Agrega este código a la función postprocessRESTResponse():
// Process the REST response.
JSONArray predictionsArray = responseObject.getJSONArray("predictions");
//You only send one image, so you directly extract the first element.
JSONObject predictions = predictionsArray.getJSONObject(0);
// Argmax
int maxIndex = 0;
JSONArray detectionScores = predictions.getJSONArray("detection_scores");
for (int j = 0; j < predictions.getInt("num_detections"); j++) {
    maxIndex =
        detectionScores.getDouble(j) > detectionScores.getDouble(maxIndex + 1) ? j : maxIndex;
}
int detectionClass = predictions.getJSONArray("detection_classes").getInt(maxIndex);
JSONArray boundingBox = predictions.getJSONArray("detection_boxes").getJSONArray(maxIndex);
double ymin = boundingBox.getDouble(0);
double xmin = boundingBox.getDouble(1);
double ymax = boundingBox.getDouble(2);
double xmax = boundingBox.getDouble(3);
displayResult(detectionClass, (float) ymin, (float) xmin, (float) ymax, (float) xmax);

Ahora, la función de procesamiento posterior extrae los valores previstos de la respuesta, identifica la categoría más probable del objeto y las coordenadas de los vértices del cuadro de límite, y, por último, renderiza el cuadro de límite de detección en la IU.

Ejecuta la app

  1. Haz clic en execute.png Run 'app' en el menú de navegación y espera a que se cargue la app.
  2. Selecciona REST > Ejecutar inferencia.

La app tarda unos segundos en renderizar el cuadro de límite del gato y muestra 17 como la categoría del objeto, que se asigna al objeto cat en el conjunto de datos COCO.

5a1a32768dc516d6.png

7. Conecta la app para Android con TensorFlow Serving a través de gRPC

Además de REST, TensorFlow Serving también es compatible con gRPC.

b6f4449c2c850b0e.png

gRPC es un framework moderno de código abierto, de alto rendimiento y llamada de procedimiento remoto (RPC), que se puede ejecutar en cualquier entorno. Puede conectar servicios en centros de datos, y entre ellos, con compatibilidad conectable para balanceo de cargas, seguimiento, verificación de estado y autenticación. Se observó que gRPC tiene un mejor rendimiento que REST en la práctica.

Envía solicitudes y recibe respuestas con gRPC

Hay cuatro pasos sencillos:

  • Genera el código de stub del cliente de gRPC (opcional).
  • Crea la solicitud de gRPC.
  • Envía la solicitud gRPC a TensorFlow Serving.
  • Extrae el resultado previsto de la respuesta de gRPC y procesa la IU.

Los lograrás en MainActivity.java.

Genera el código de stub del cliente de gRPC (opcional)

Para usar gRPC con TensorFlow Serving, debes seguir el flujo de trabajo de gRPC. Para obtener más información, consulta la documentación de gRPC.

a9d0e5cb543467b4.png

TensorFlow Serving y TensorFlow definen los archivos .proto por ti. A partir de TensorFlow y TensorFlow Serving 2.8, estos archivos .proto son los necesarios:

tensorflow/core/example/example.proto
tensorflow/core/example/feature.proto
tensorflow/core/protobuf/struct.proto
tensorflow/core/protobuf/saved_object_graph.proto
tensorflow/core/protobuf/saver.proto
tensorflow/core/protobuf/trackable_object_graph.proto
tensorflow/core/protobuf/meta_graph.proto
tensorflow/core/framework/node_def.proto
tensorflow/core/framework/attr_value.proto
tensorflow/core/framework/function.proto
tensorflow/core/framework/types.proto
tensorflow/core/framework/tensor_shape.proto
tensorflow/core/framework/full_type.proto
tensorflow/core/framework/versions.proto
tensorflow/core/framework/op_def.proto
tensorflow/core/framework/graph.proto
tensorflow/core/framework/tensor.proto
tensorflow/core/framework/resource_handle.proto
tensorflow/core/framework/variable.proto

tensorflow_serving/apis/inference.proto
tensorflow_serving/apis/classification.proto
tensorflow_serving/apis/predict.proto
tensorflow_serving/apis/regression.proto
tensorflow_serving/apis/get_model_metadata.proto
tensorflow_serving/apis/input.proto
tensorflow_serving/apis/prediction_service.proto
tensorflow_serving/apis/model.proto
  • Para generar el stub, agrega este código al archivo app/build.gradle.
apply plugin: 'com.google.protobuf'

protobuf {
    protoc { artifact = 'com.google.protobuf:protoc:3.11.0' }
    plugins {
        grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.29.0'
        }
    }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java { option 'lite' }
            }
            task.plugins {
                grpc { option 'lite' }
            }
        }
    }
}

Crea la solicitud de gRPC

Al igual que con la solicitud de REST, debes crear la solicitud de gRPC en la función createGRPCRequest().

private Request createGRPCRequest() {

}
  • Agrega este código a la función createGRPCRequest():
if (stub == null) {
  channel = ManagedChannelBuilder.forAddress(SERVER, GRPC_PORT).usePlaintext().build();
  stub = PredictionServiceGrpc.newBlockingStub(channel);
}

Model.ModelSpec.Builder modelSpecBuilder = Model.ModelSpec.newBuilder();
modelSpecBuilder.setName(MODEL_NAME);
modelSpecBuilder.setVersion(Int64Value.of(MODEL_VERSION));
modelSpecBuilder.setSignatureName(SIGNATURE_NAME);

Predict.PredictRequest.Builder builder = Predict.PredictRequest.newBuilder();
builder.setModelSpec(modelSpecBuilder);

TensorProto.Builder tensorProtoBuilder = TensorProto.newBuilder();
tensorProtoBuilder.setDtype(DataType.DT_UINT8);
TensorShapeProto.Builder tensorShapeBuilder = TensorShapeProto.newBuilder();
tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(1));
tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(INPUT_IMG_HEIGHT));
tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(INPUT_IMG_WIDTH));
tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(3));
tensorProtoBuilder.setTensorShape(tensorShapeBuilder.build());
int[] inputImg = new int[INPUT_IMG_HEIGHT * INPUT_IMG_WIDTH];
inputImgBitmap.getPixels(inputImg, 0, INPUT_IMG_WIDTH, 0, 0, INPUT_IMG_WIDTH, INPUT_IMG_HEIGHT);
int pixel;
for (int i = 0; i < INPUT_IMG_HEIGHT; i++) {
    for (int j = 0; j < INPUT_IMG_WIDTH; j++) {
    // Extract RBG values from each pixel; alpha is ignored.
    pixel = inputImg[i * INPUT_IMG_WIDTH + j];
    tensorProtoBuilder.addIntVal((pixel >> 16) & 0xff);
    tensorProtoBuilder.addIntVal((pixel >> 8) & 0xff);
    tensorProtoBuilder.addIntVal((pixel) & 0xff);
    }
}
TensorProto tensorProto = tensorProtoBuilder.build();

builder.putInputs("input_tensor", tensorProto);

builder.addOutputFilter("num_detections");
builder.addOutputFilter("detection_boxes");
builder.addOutputFilter("detection_classes");
builder.addOutputFilter("detection_scores");

return builder.build();

Envía la solicitud de gRPC a TensorFlow Serving

Ahora puedes finalizar el objeto de escucha onClick(View view).

predictButton.setOnClickListener(
    new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (requestRadioGroup.getCheckedRadioButtonId() == R.id.rest) {

            }
            else {
                // TODO: gRPC request
            }
        }
    }
)
  • Agrega este código a la rama de gRPC:
try {
    Predict.PredictRequest request = createGRPCRequest();
    Predict.PredictResponse response = stub.predict(request);
    postprocessGRPCResponse(response);
} catch (Exception e) {
    Log.e(TAG, e.getMessage());
    responseTextView.setText(e.getMessage());
    return;
}

Cómo procesar la respuesta de gRPC de TensorFlow Serving

Al igual que gRPC, implementa la función postprocessGRPCResponse() para controlar la respuesta.

private void postprocessGRPCResponse(Predict.PredictResponse response) {

}
  • Agrega este código a la función postprocessGRPCResponse():
// Process the response.
float numDetections = response.getOutputsMap().get("num_detections").getFloatValList().get(0);
List<Float> detectionScores =    response.getOutputsMap().get("detection_scores").getFloatValList();
int maxIndex = 0;
for (int j = 0; j < numDetections; j++) {
    maxIndex = detectionScores.get(j) > detectionScores.get(maxIndex + 1) ? j : maxIndex;
}
Float detectionClass =    response.getOutputsMap().get("detection_classes").getFloatValList().get(maxIndex);
List<Float> boundingBoxValues =    response.getOutputsMap().get("detection_boxes").getFloatValList();
float ymin = boundingBoxValues.get(maxIndex * 4);
float xmin = boundingBoxValues.get(maxIndex * 4 + 1);
float ymax = boundingBoxValues.get(maxIndex * 4 + 2);
float xmax = boundingBoxValues.get(maxIndex * 4 + 3);
displayResult(detectionClass.intValue(), ymin, xmin, ymax, xmax);

Ahora, la función de procesamiento posterior puede extraer los valores previstos de la respuesta y renderizar el cuadro de límite de detección en la IU.

Ejecuta la app

  1. Haz clic en execute.png Run 'app' en el menú de navegación y espera a que se cargue la app.
  2. Selecciona gRPC > Ejecutar inferencia.

La app tarda unos segundos en renderizar el cuadro de límite del gato y muestra 17 como la categoría del objeto, que se asigna a la categoría cat en el conjunto de datos COCO.

8. Felicitaciones

Usaste TensorFlow Serving para agregar capacidades de detección de objetos a tu app.

Más información