Android-App zum Erkennen von Objekten in Bildern erstellen

1. Hinweis

In diesem Codelab erfahren Sie, wie Sie mit TensorFlow Serving mit REST und gRPC eine Inferenz zur Objekterkennung über eine Android-App ausführen.

Vorbereitung

  • Grundkenntnisse der Android-Entwicklung mit Java
  • Grundkenntnisse im Bereich maschinelles Lernen mit TensorFlow, z. B. Training und Bereitstellung
  • Grundkenntnisse zu Terminals und Docker

Lerninhalte

  • So finden Sie vortrainierte Objekterkennungsmodelle in TensorFlow Hub.
  • Hier erfahren Sie, wie Sie eine einfache Android-App erstellen und mit dem heruntergeladenen Objekterkennungsmodell über TensorFlow Serving (REST und gRPC) Vorhersagen treffen.
  • So rendern Sie das Erkennungsergebnis in der Benutzeroberfläche.

Voraussetzungen

2. Einrichten

So laden Sie den Code für dieses Codelab herunter:

  1. Rufen Sie das GitHub-Repository für dieses Codelab auf.
  2. Klicken Sie auf Code > Download zip, um den gesamten Code für dieses Codelab herunterzuladen.

a72f2bb4caa9a96.png

  1. Entpacken Sie die heruntergeladene ZIP-Datei, um einen codelabs-Stammordner mit allen benötigten Ressourcen zu entpacken.

Für dieses Codelab benötigen Sie nur die Dateien im Unterverzeichnis TFServing/ObjectDetectionAndroid im Repository, das zwei Ordner enthält:

  • Der Ordner starter enthält den Startcode, auf dem Sie in diesem Codelab aufbauen.
  • Der Ordner finished enthält den vollständigen Code der fertigen Beispiel-App.

3. Abhängigkeiten zum Projekt hinzufügen

Starter-App in Android Studio importieren

  • Klicken Sie in Android Studio auf File > New > Import Project und wählen Sie dann den Ordner starter aus dem Quellcode aus, den Sie zuvor heruntergeladen haben.

Abhängigkeiten für OkHttp und gRPC hinzufügen

  • Prüfen Sie in der Datei app/build.gradle Ihres Projekts, ob die Abhängigkeiten vorhanden sind.
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'
}

Projekt mit Gradle-Dateien synchronisieren

  • Wählen Sie im Navigationsmenü 541e90b497a7fef7.png Sync Project with Gradle Files (Projekt mit Gradle-Dateien synchronisieren) aus.

4. Start-App ausführen

App ausführen und ausprobieren

Die App sollte auf Ihrem Android-Gerät gestartet werden. Die Benutzeroberfläche ist recht einfach: Es gibt ein Katzenbild, auf dem Sie Objekte erkennen möchten, und der Nutzer kann auswählen, wie die Daten über REST oder gRPC an das Backend gesendet werden sollen. Das Back-End führt die Objekterkennung für das Bild aus und gibt die Erkennungsergebnisse an die Client-App zurück, die die Benutzeroberfläche noch einmal rendert.

24eab579530e9645.png

Wenn Sie jetzt auf Run inference (Inferenz ausführen) klicken, passiert nichts. Das liegt daran, dass noch keine Kommunikation mit dem Backend möglich ist.

5. Objekterkennungsmodell mit TensorFlow Serving bereitstellen

Die Objekterkennung ist eine sehr häufige ML-Aufgabe. Ziel ist es, Objekte in Bildern zu erkennen, d. h. mögliche Kategorien der Objekte und Begrenzungsrahmen um sie herum vorherzusagen. Hier ein Beispiel für ein Erkennungsergebnis:

a68f9308fb2fc17b.png

Google hat eine Reihe vortrainierter Modelle auf TensorFlow Hub veröffentlicht. Eine vollständige Liste finden Sie auf der Seite object_detection. In diesem Codelab verwenden Sie das relativ einfache Modell SSD MobileNet V2 FPNLite 320 × 320, sodass Sie nicht unbedingt eine GPU benötigen, um es auszuführen.

So stellen Sie das Objekterkennungsmodell mit TensorFlow Serving bereit:

  1. Laden Sie die Modelldatei herunter.
  2. Entpacken Sie die heruntergeladene Datei .tar.gz mit einem Dekomprimierungstool wie 7‑Zip.
  3. Erstellen Sie einen ssd_mobilenet_v2_2_320-Ordner und darin einen 123-Unterordner.
  4. Verschieben Sie den extrahierten Ordner variables und die Datei saved_model.pb in den Unterordner 123.

Sie können auf den Ordner ssd_mobilenet_v2_2_320 als Ordner SavedModel verweisen. 123 ist eine Beispielversionsnummer. Sie können auch eine andere Nummer auswählen.

Die Ordnerstruktur sollte so aussehen:

42c8150a42033767.png

TensorFlow Serving starten

  • Starten Sie TensorFlow Serving mit Docker in Ihrem Terminal, ersetzen Sie aber den Platzhalter PATH/TO/SAVEDMODEL durch den absoluten Pfad des Ordners ssd_mobilenet_v2_2_320 auf Ihrem Computer.
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 lädt zuerst automatisch das TensorFlow Serving-Image herunter. Das dauert etwa eine Minute. Danach sollte TensorFlow Serving gestartet werden. Das Log sollte so aussehen:

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. Android-App über REST mit TensorFlow Serving verbinden

Das Backend ist jetzt bereit. Sie können Clientanfragen an TensorFlow Serving senden, um Objekte in Bildern zu erkennen. Es gibt zwei Möglichkeiten, Anfragen an TensorFlow Serving zu senden:

  • REST
  • gRPC

Anfragen senden und Antworten über REST empfangen

So gehts:

  • Erstellen Sie die REST-Anfrage.
  • Senden Sie die REST-Anfrage an TensorFlow Serving.
  • Extrahieren Sie das vorhergesagte Ergebnis aus der REST-Antwort und rendern Sie die Benutzeroberfläche.

Sie erreichen diese Ziele in MainActivity.java..

REST-Anfrage erstellen

Derzeit ist in der Datei MainActivity.java eine leere createRESTRequest()-Funktion vorhanden. Sie implementieren diese Funktion, um eine REST-Anfrage zu erstellen.

private Request createRESTRequest() {
}

TensorFlow Serving erwartet eine POST-Anfrage, die den Bildtensor für das verwendete SSD MobileNet-Modell enthält. Sie müssen also die RGB-Werte aus jedem Pixel des Bildes in ein Array extrahieren und das Array dann in ein JSON-Objekt einfügen, das die Nutzlast der Anfrage ist.

  • Fügen Sie diesen Code in die Funktion createRESTRequest() ein:
//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;    

REST-Anfrage an TensorFlow Serving senden

In der App kann der Nutzer REST oder gRPC für die Kommunikation mit TensorFlow Serving auswählen. Daher gibt es zwei Zweige im onClick(View view)-Listener.

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

            }
        }
    }
)
  • Fügen Sie diesen Code dem REST-Zweig des onClick(View view)-Listeners hinzu, um OkHttp zum Senden der Anfrage an TensorFlow Serving zu verwenden:
// 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;
}

REST-Antwort von TensorFlow Serving verarbeiten

Das SSD MobileNet-Modell gibt eine Reihe von Ergebnissen zurück, darunter:

  • num_detections: die Anzahl der erkannten Ereignisse
  • detection_scores: Erkennungsergebnisse
  • detection_classes: Der Index der Erkennungsklasse
  • detection_boxes: die Koordinaten des Begrenzungsrahmens

Sie implementieren die Funktion postprocessRESTResponse(), um die Antwort zu verarbeiten.

private void postprocessRESTResponse(Predict.PredictResponse response) {

}
  • Fügen Sie diesen Code in die Funktion postprocessRESTResponse() ein:
// 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);

Die Nachbearbeitungsfunktion extrahiert nun die vorhergesagten Werte aus der Antwort, ermittelt die wahrscheinlichste Kategorie des Objekts und die Koordinaten der Eckpunkte des umgebenden Rechtecks und rendert schließlich das umgebende Rechteck für die Erkennung in der Benutzeroberfläche.

Ausführen

  1. Klicken Sie im Navigationsmenü auf execute.png App ausführen und warten Sie, bis die App geladen ist.
  2. Wählen Sie REST > Run inference (REST > Inferenz ausführen) aus.

Es dauert einige Sekunden, bis die App den Begrenzungsrahmen der Katze rendert und 17 als Kategorie des Objekts anzeigt. Dies entspricht dem cat-Objekt im COCO-Dataset.

5a1a32768dc516d6.png

7. Android-App über gRPC mit TensorFlow Serving verbinden

Neben REST unterstützt TensorFlow Serving auch gRPC.

b6f4449c2c850b0e.png

gRPC ist ein modernes Open-Source-Framework für hochleistungsfähige Remote-Prozeduraufrufe (RPCs), das in jeder Umgebung ausgeführt werden kann. Damit lassen sich Dienste in und zwischen Rechenzentren effizient verbinden. Außerdem bietet es Plug-in-Unterstützung für Load-Balancing, Tracing, Systemdiagnosen und Authentifizierung. In der Praxis hat sich gezeigt, dass gRPC leistungsfähiger als REST ist.

Anfragen senden und Antworten mit gRPC empfangen

Dazu sind nur vier einfache Schritte erforderlich:

  • Optional: Generieren Sie den gRPC-Client-Stub-Code.
  • Erstellen Sie die gRPC-Anfrage.
  • Senden Sie die gRPC-Anfrage an TensorFlow Serving.
  • Extrahieren Sie das vorhergesagte Ergebnis aus der gRPC-Antwort und rendern Sie die Benutzeroberfläche.

Sie erreichen diese Ziele in MainActivity.java..

Optional: gRPC-Client-Stubcode generieren

Wenn Sie gRPC mit TensorFlow Serving verwenden möchten, müssen Sie dem gRPC-Workflow folgen. Weitere Informationen finden Sie in der gRPC-Dokumentation.

a9d0e5cb543467b4.png

TensorFlow Serving und TensorFlow definieren die .proto-Dateien für Sie. Ab TensorFlow und TensorFlow Serving 2.8 sind die folgenden .proto-Dateien erforderlich:

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
  • Fügen Sie der Datei app/build.gradle diesen Code hinzu, um den Stub zu generieren.
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' }
            }
        }
    }
}

gRPC-Anfrage erstellen

Ähnlich wie bei der REST-Anfrage erstellen Sie die gRPC-Anfrage in der Funktion createGRPCRequest().

private Request createGRPCRequest() {

}
  • Fügen Sie diesen Code in die Funktion createGRPCRequest() ein:
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();

gRPC-Anfrage an TensorFlow Serving senden

Jetzt können Sie den onClick(View view)-Listener fertigstellen.

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

            }
            else {
                // TODO: gRPC request
            }
        }
    }
)
  • Fügen Sie diesen Code dem gRPC-Branch hinzu:
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;
}

gRPC-Antwort von TensorFlow Serving verarbeiten

Ähnlich wie bei gRPC implementieren Sie die Funktion postprocessGRPCResponse(), um die Antwort zu verarbeiten.

private void postprocessGRPCResponse(Predict.PredictResponse response) {

}
  • Fügen Sie diesen Code in die Funktion postprocessGRPCResponse() ein:
// 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);

Die Nachbearbeitungsfunktion kann jetzt Vorhersagewerte aus der Antwort extrahieren und das Begrenzungsrechteck für die Erkennung in der Benutzeroberfläche rendern.

Ausführen

  1. Klicken Sie im Navigationsmenü auf execute.png App ausführen und warten Sie, bis die App geladen ist.
  2. Wählen Sie gRPC > Run inference (gRPC > Inferenz ausführen) aus.

Es dauert einige Sekunden, bis die App den Begrenzungsrahmen der Katze rendert und 17 als Kategorie des Objekts anzeigt. Diese Kategorie wird im COCO-Dataset der Kategorie cat zugeordnet.

8. Glückwunsch

Sie haben TensorFlow Serving verwendet, um Ihrer App Funktionen zur Objekterkennung hinzuzufügen.

Weitere Informationen