Créer une application Android pour détecter des objets dans des images

1. Avant de commencer

Dans cet atelier de programmation, vous allez apprendre à exécuter une inférence de détection d'objets à partir d'une application Android à l'aide de TensorFlow Serving avec REST et gRPC.

Prérequis

  • Connaissances de base sur le développement Android avec Java
  • Connaissances de base du machine learning avec TensorFlow, telles que l'entraînement et le déploiement
  • Connaissances de base des terminaux et de Docker

Points abordés

  • Découvrez comment trouver des modèles de détection d'objets pré-entraînés sur TensorFlow Hub.
  • Créer une application Android simple et effectuer des prédictions avec le modèle de détection d'objets téléchargé via TensorFlow Serving (REST et gRPC)
  • Afficher le résultat de la détection dans l'UI

Prérequis

2. Configuration

Pour télécharger le code de cet atelier de programmation, procédez comme suit :

  1. Accédez au dépôt GitHub pour cet atelier de programmation.
  2. Cliquez sur Code > Download ZIP (Code > Télécharger le fichier ZIP) afin de télécharger l'ensemble du code pour cet atelier de programmation.

a72f2bb4caa9a96.png

  1. Décompressez le fichier ZIP téléchargé pour accéder au dossier racine codelabs contenant toutes les ressources nécessaires.

Pour cet atelier de programmation, vous n'avez besoin que des fichiers du sous-répertoire TFServing/ObjectDetectionAndroid dans le dépôt, qui contient deux dossiers :

  • Le dossier starter contient le code de démarrage sur lequel s'appuie cet atelier de programmation.
  • Le dossier finished contient le code final de l'application exemple.

3. Ajouter les dépendances au projet

Importer l'application de départ dans Android Studio

  • Dans Android Studio, cliquez sur File > New > Import project (Fichier > Nouveau > Importer un projet), puis sélectionnez le dossier starter dans le code source que vous avez téléchargé précédemment.

Ajouter les dépendances pour OkHttp et gRPC

  • Dans le fichier app/build.gradle de votre projet, vérifiez la présence des dépendances.
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'
}

Synchroniser votre projet avec les fichiers Gradle

  • Sélectionnez 541e90b497a7fef7.png Sync Project with Gradle Files (Synchroniser le projet avec les fichiers Gradle) dans le menu de navigation.

4. Exécuter l'application de démarrage

Exécuter et explorer l'application

L'application doit se lancer sur votre appareil Android. L'UI est assez simple : elle contient une image de chat dans laquelle vous souhaitez détecter des objets. L'utilisateur peut choisir d'envoyer les données au backend avec REST ou gRPC. Le backend effectue la détection d'objets sur l'image et renvoie les résultats de la détection à l'application cliente, qui affiche à nouveau l'UI.

24eab579530e9645.png

Pour le moment, si vous cliquez sur Run inference (Exécuter l'inférence), rien ne se passe. En effet, la communication avec le backend n'est pas encore établie.

5. Déployer un modèle de détection d'objets avec TensorFlow Serving

La détection d'objets est une tâche de ML très courante. Elle consiste à détecter des objets dans des images, c'est-à-dire à prédire les catégories possibles des objets et les cadres de délimitation autour d'eux. Voici un exemple de résultat de détection :

a68f9308fb2fc17b.png

Google a publié un certain nombre de modèles pré-entraînés sur TensorFlow Hub. Pour obtenir la liste complète, consultez la page object_detection. Pour cet atelier de programmation, vous utilisez le modèle SSD MobileNet V2 FPNLite 320x320 relativement léger, ce qui vous permet de l'exécuter sans forcément avoir besoin d'un GPU.

Pour déployer le modèle de détection d'objets avec TensorFlow Serving :

  1. Téléchargez le fichier de modèle.
  2. Décompressez le fichier .tar.gz téléchargé à l'aide d'un outil de décompression tel que 7-Zip.
  3. Créez un dossier ssd_mobilenet_v2_2_320, puis créez-y un sous-dossier 123.
  4. Placez le dossier variables et le fichier saved_model.pb extraits dans le sous-dossier 123.

Vous pouvez faire référence au dossier ssd_mobilenet_v2_2_320 en tant que dossier SavedModel. 123 est un exemple de numéro de version. Si vous le souhaitez, vous pouvez choisir un autre nombre.

La structure des dossiers doit ressembler à celle de cette image :

42c8150a42033767.png

Démarrer TensorFlow Serving

  • Dans votre terminal, démarrez TensorFlow Serving avec Docker, en remplaçant l'espace réservé PATH/TO/SAVEDMODEL par le chemin absolu du dossier ssd_mobilenet_v2_2_320 sur votre ordinateur.
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 commence par télécharger automatiquement l'image TensorFlow Serving, ce qui prend une minute. Le service TensorFlow Serving devrait alors démarrer. Le journal doit se présenter comme cet extrait de code :

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. Connecter l'application Android à TensorFlow Serving via REST

Le backend est prêt pour que vous puissiez envoyer des requêtes client à TensorFlow Serving afin de détecter des objets dans des images. Il existe deux façons d'envoyer des requêtes à TensorFlow Serving :

  • REST
  • gRPC

Envoyer des requêtes et recevoir des réponses via REST

Il vous suffit de suivre trois étapes simples :

  • Créez la requête REST.
  • Envoyez la requête REST à TensorFlow Serving.
  • Extrayez le résultat prédit de la réponse REST et affichez l'UI.

Vous les atteindrez dans MainActivity.java.

Créer la requête REST

Pour l'instant, le fichier MainActivity.java contient une fonction createRESTRequest() vide. Vous implémentez cette fonction pour créer une requête REST.

private Request createRESTRequest() {
}

TensorFlow Serving s'attend à recevoir une requête POST contenant le Tensor d'image pour le modèle SSD MobileNet que vous utilisez. Vous devez donc extraire les valeurs RVB de chaque pixel de l'image dans un tableau, puis encapsuler le tableau dans un JSON, qui constitue la charge utile de la requête.

  • Ajoutez ce code à la fonction 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;    

Envoyer la requête REST à TensorFlow Serving

L'application permet à l'utilisateur de choisir REST ou gRPC pour communiquer avec TensorFlow Serving. Il existe donc deux branches dans l'écouteur onClick(View view).

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

            }
        }
    }
)
  • Ajoutez ce code à la branche REST du listener onClick(View view) pour utiliser OkHttp afin d'envoyer la requête à 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;
}

Traiter la réponse REST à partir de TensorFlow Serving

Le modèle SSD MobileNet renvoie un certain nombre de résultats, dont les suivants :

  • num_detections : nombre de détections
  • detection_scores : scores de détection
  • detection_classes : index de la classe de détection
  • detection_boxes : coordonnées du cadre de délimitation

Vous implémentez la fonction postprocessRESTResponse() pour gérer la réponse.

private void postprocessRESTResponse(Predict.PredictResponse response) {

}
  • Ajoutez ce code à la fonction 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);

La fonction de post-traitement extrait désormais les valeurs prédites de la réponse, identifie la catégorie la plus probable de l'objet et les coordonnées des sommets du cadre de sélection, puis affiche le cadre de sélection de la détection dans l'UI.

Exécuter le code

  1. Dans le menu de navigation, cliquez sur execute.png Run 'app' (Exécuter l'application), puis attendez que l'application se charge.
  2. Sélectionnez REST > Run inference (REST > Exécuter l'inférence).

Il faut quelques secondes avant que l'application n'affiche le cadre de délimitation du chat et indique 17 comme catégorie de l'objet, qui correspond à l'objet cat dans l'ensemble de données COCO.

5a1a32768dc516d6.png

7. Connecter l'application Android à TensorFlow Serving via gRPC

TensorFlow Serving est non seulement compatible avec REST, mais aussi avec gRPC.

b6f4449c2c850b0e.png

gRPC est un framework d'appel de procédure à distance (RPC) moderne, Open Source et haute performance qui peut être exécuté dans n'importe quel environnement. Il permet de connecter efficacement les services dans et entre les centres de données avec compatibilité par plug-in pour l'équilibrage de charge, le traçage, la vérification de l'état et l'authentification. Selon les observations, gRPC s'avère plus performant que REST dans la pratique.

Envoyer des requêtes et recevoir des réponses avec gRPC

Voici quatre étapes simples :

  • [Facultatif] Générez le code du bouchon pour le client gRPC.
  • Créez la requête gRPC.
  • Envoyez la requête gRPC à TensorFlow Serving.
  • Extrayez le résultat prédit de la réponse gRPC, puis affichez l'UI.

Vous les atteindrez dans MainActivity.java.

(Facultatif) Générer le code du bouchon pour le client gRPC

Pour utiliser gRPC avec TensorFlow Serving, vous devez suivre le workflow gRPC. Pour en savoir plus, consultez la documentation gRPC.

a9d0e5cb543467b4.png

TensorFlow Serving et TensorFlow définissent les fichiers .proto automatiquement. Pour TensorFlow et TensorFlow Serving 2.8, vous avez besoin des fichiers .proto suivants :

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
  • Pour générer le stub, ajoutez ce code au fichier 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' }
            }
        }
    }
}

Créer la requête gRPC

Comme pour la requête REST, vous devez créer la requête gRPC dans la fonction createGRPCRequest().

private Request createGRPCRequest() {

}
  • Ajoutez ce code à la fonction 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();

Envoyer la requête gRPC à TensorFlow Serving

Vous pouvez maintenant terminer l'écouteur onClick(View view).

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

            }
            else {
                // TODO: gRPC request
            }
        }
    }
)
  • Ajoutez ce code à la branche 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;
}

Traiter la réponse gRPC de TensorFlow Serving

Comme pour gRPC, vous implémentez la fonction postprocessGRPCResponse() pour gérer la réponse.

private void postprocessGRPCResponse(Predict.PredictResponse response) {

}
  • Ajoutez ce code à la fonction 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);

La fonction de post-traitement peut désormais extraire les valeurs prédites de la réponse et afficher le cadre de sélection de la détection dans l'UI.

Exécuter le code

  1. Dans le menu de navigation, cliquez sur execute.png Run 'app' (Exécuter l'application), puis attendez que l'application se charge.
  2. Sélectionnez gRPC > Run inference (gRPC > Exécuter l'inférence).

Il faut quelques secondes avant que l'application n'affiche le cadre de sélection du chat et indique 17 comme catégorie de l'objet, ce qui correspond à la catégorie cat dans l'ensemble de données COCO.

8. Félicitations

Vous avez utilisé TensorFlow Serving pour ajouter des fonctionnalités de détection d'objets à votre application.

En savoir plus