Crea una app para Android que detecte objetos en 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 a través de REST y gRPC.

Requisitos previos

  • Conocimientos básicos sobre el desarrollo de Android con Java
  • Conocimientos básicos del aprendizaje automático con TensorFlow, como el entrenamiento y la implementación
  • 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 crear una app sencilla para Android y realizar 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 Code > Download ZIP para descargar todo el código de este codelab.

a72f2bb4caa9a96.png

  1. Descomprime el archivo ZIP descargado para desempaquetar 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 completado de la app de ejemplo finalizada.

3. Agrega las dependencias al proyecto

Cómo importar la app de partida a Android Studio

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

Agrega las dependencias de 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'
}

Sincroniza 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 partida

Ejecuta y explora la app

La app debería iniciarse en tu dispositivo Android. La IU es bastante sencilla: hay una imagen de un gato en la que deseas 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 devuelve los resultados de la detección a la app cliente, que vuelve a renderizar la IU.

24eab579530e9645.png

En este momento, si haces clic en Run inference, no sucederá 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 cuyo objetivo es detectar objetos en imágenes, es decir, predecir posibles categorías 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ó varios modelos previamente entrenados en TensorFlow Hub. Para ver la lista completa, visita la página object_detection. En este codelab, usarás el modelo SSD MobileNet V2 FPNLite 320x320, que es relativamente ligero, por lo que no es necesario que uses una GPU para ejecutarlo.

Para implementar el modelo de detección de objetos con TensorFlow Serving, haz lo siguiente:

  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, crea una subcarpeta 123 dentro de ella.
  4. Coloca la carpeta variables extraída y el archivo saved_model.pb en la subcarpeta 123.

Puedes consultar la carpeta ssd_mobilenet_v2_2_320 como la carpeta SavedModel. 123 es un ejemplo de número de versión. Si lo deseas, puedes elegir otro número.

La estructura de carpetas debería verse como en la siguiente imagen:

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 iniciarse. 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

Ahora el backend está preparado, por lo que puedes enviar solicitudes de clientes a TensorFlow Serving para detectar objetos en imágenes. Existen dos métodos de enviar solicitudes a TensorFlow Serving:

  • REST
  • gRPC

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

Sigue estos tres pasos simples:

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

Lograrás estos objetivos en MainActivity.java.

Crea la solicitud de REST

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

private Request createRESTRequest() {
}

TensorFlow Serving espera una solicitud POST que contenga 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, incluir el array 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 permite que el usuario elija 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 y 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;
}

Procesa la respuesta de REST de TensorFlow Serving

El modelo SSD MobileNet devuelve una cantidad de resultados, que incluyen lo siguiente:

  • 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 delimitador

Implementas 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 predichos de la respuesta, identifica la categoría más probable del objeto y las coordenadas de los vértices del cuadro delimitador y, por último, renderiza el cuadro delimitador de detección en la IU.

Ejecución

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

La app tarda unos segundos en renderizar el cuadro de límite del gato y mostrar 17 como la categoría del objeto, que se asigna al objeto cat en el conjunto de datos de 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 admite gRPC.

b6f4449c2c850b0e.png

gRPC es un framework de llamada de procedimiento remoto (RPC) moderno de código abierto y alto rendimiento, que se puede ejecutar en cualquier entorno. Puede conectar de forma eficaz servicios en centros de datos (y entre ellos) y ofrece compatibilidad conectable con 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:

  • [Opcional] Genera el código de stub del cliente de gRPC.
  • Crea la solicitud de gRPC.
  • Envía la solicitud de gRPC a TensorFlow Serving.
  • Extrae el resultado previsto de la respuesta de gRPC y renderiza la IU.

Lograrás estos objetivos 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. Si quieres 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, se necesitan estos archivos .proto:

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

Procesa la respuesta de gRPC de TensorFlow Serving

Al igual que con gRPC, implementas 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 delimitador de detección en la IU.

Ejecución

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

La app tarda unos segundos en renderizar el cuadro delimitador del gato y mostrar 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