Android-App erstellen, mit der Objekte in Bildern erkannt werden

1. Hinweis

In diesem Codelab lernen Sie, wie Sie mit TensorFlow Serving mit REST und gRPC eine Inferenz einer Objekterkennung in einer Android-App ausführen.

Vorbereitung

  • Grundkenntnisse in der Android-Entwicklung mit Java
  • Grundkenntnisse in Machine Learning mit TensorFlow, z. B. für Training und Bereitstellung
  • Grundkenntnisse zu Terminals und Docker

Lerninhalte

  • Vortrainierte Objekterkennungsmodelle in TensorFlow Hub finden
  • Hier erfahren Sie, wie Sie mit TensorFlow Serving (REST und gRPC) eine einfache Android-App erstellen und Vorhersagen mit dem heruntergeladenen Modell zur Objekterkennung treffen.
  • Rendering des Erkennungsergebnisses in der UI

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 gt; ZIP herunterladen, um den gesamten Code für dieses Codelab herunterzuladen.

a72f2bb4caa9a96

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

Für dieses Codelab benötigen Sie nur die Dateien im Unterverzeichnis TFServing/ObjectDetectionAndroid des Repositorys mit zwei Ordnern:

  • Der Ordner starter enthält den Startcode, den Sie für dieses Codelab erstellen.
  • Der Ordner finished enthält den abgeschlossenen Code für die fertige 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 im Quellcode, den Sie zuvor heruntergeladen haben, den Ordner starter aus.

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ü die Option 541e90b497a7fef7 Projekt mit Gradle-Dateien synchronisieren aus.

4. Start-App ausführen

App ausführen und entdecken

Die App sollte auf Ihrem Android-Gerät gestartet werden. Die Benutzeroberfläche ist ziemlich einfach: Es gibt ein Katzenbild, in dem Sie Objekte erkennen möchten und der Nutzer auswählen kann, wie die Daten mit REST oder gRPC an das Back-End 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, mit der die Benutzeroberfläche noch einmal gerendert wird.

24eab579530e9645

Wenn Sie auf Inferenz ausführen klicken, passiert im Moment nichts. Das liegt daran, dass noch keine Kommunikation mit dem Back-End möglich ist.

5. Objekterkennungsmodell mit TensorFlow Serving bereitstellen

Die Objekterkennung ist eine sehr gängige ML-Aufgabe und besteht darin, Objekte in Bildern zu erkennen, um mögliche Kategorien der Objekte und Begrenzungsrahmen um sich herum vorherzusagen. Hier ein Beispiel für ein Erkennungsergebnis:

a68f9308fb2fc17b

Google hat eine Reihe vortrainierter Modelle auf TensorFlow Hub veröffentlicht. Die vollständige Liste finden Sie auf der Seite object_detection. Für dieses Codelab verwenden Sie das relativ einfache Modell SSD MobileNet V2 FPNLite 320x320, sodass Sie nicht unbedingt eine GPU dafür verwenden müssen.

So stellen Sie das Objekterkennungsmodell mit TensorFlow Serving bereit:

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

Du kannst den Ordner ssd_mobilenet_v2_2_320 als Ordner SavedModel verwenden. 123 ist eine Beispielversionsnummer. Wenn du möchtest, kannst du eine andere Nummer auswählen.

Die Ordnerstruktur sollte so aussehen:

42c8150a42033767

TensorFlow-Bereitstellung starten

  • Starten Sie in Ihrem Terminal TensorFlow Serving mit Docker, aber ersetzen Sie 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 automatisch das TensorFlow Serving-Image herunter. Das dauert eine Minute. Danach sollte die TensorFlow-Bereitstellung beginnen. Das Protokoll 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 mit TensorFlow über REST verbinden

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

  • REST
  • gRPC

Anfragen senden und Antworten über REST erhalten

Es sind drei einfache Schritte:

  • 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 UI.

Dies erreichen Sie in MainActivity.java.

REST-Anfrage erstellen

Im Moment gibt es eine leere createRESTRequest()-Funktion in der MainActivity.java-Datei. Diese Funktion implementieren Sie, um eine REST-Anfrage zu erstellen.

private Request createRESTRequest() {
}

Für TensorFlow Serving wird eine POST-Anfrage mit dem Bildtensor für das von Ihnen verwendete SSD MobileNet-Modell erwartet. Daher müssen Sie die RGB-Werte aus jedem Pixel des Bilds in einem Array extrahieren und das Array dann in einer JSON-Datei zusammenfassen (die Nutzlast der Anfrage).

  • Füge der Funktion createRESTRequest() diesen Code hinzu:
//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 Anwendung kann der Nutzer REST oder gRPC für die Kommunikation mit TensorFlow Serving auswählen. Es gibt also 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üge diesen Code dem REST-Branch des onClick(View view)-Listeners hinzu, um mithilfe von OkHttp die Anfrage an TensorFlow Serving zu senden:
// 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:

  • num_detections: Anzahl der Erkennungen
  • detection_scores: Erkennungswerte
  • detection_classes: der Index der Erkennungsklasse
  • detection_boxes: die Begrenzungsrahmen-Koordinaten

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

private void postprocessRESTResponse(Predict.PredictResponse response) {

}
  • Füge der Funktion postprocessRESTResponse() diesen Code hinzu:
// 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 prognostizierte Werte aus der Antwort, identifiziert die wahrscheinlichste Kategorie des Objekts und die Koordinaten der Eckpunkte des Begrenzungsrahmens und rendert schließlich den Begrenzungsrahmen zur Erkennung auf der Benutzeroberfläche.

Ausführen

  1. Klicken Sie im Navigationsmenü auf Ausführen.png Ausführen 'App' und warten Sie, bis die App geladen ist.
  2. Wählen Sie REST > RunInferenz aus.

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

5a1a32768dc516d6

7. Android-App mit TensorFlow über gRPC verbinden

Zusätzlich zur REST-Unterstützung unterstützt TensorFlow Serving auch BeyondCorp.

b6f4449c2c850b0e.png

gRPC ist ein modernes, leistungsstarkes Open-Source-RPC-Framework (Remote Procedure Call), das in jeder Umgebung ausgeführt werden kann. Dank der flexiblen Unterstützung für Load-Balancing, Tracing, Systemdiagnose und Authentifizierung lassen sich Dienste effizient in und zwischen Rechenzentren verbinden. Es wurde festgestellt, dass gRPC in der Praxis leistungsstärker ist als REST.

Anfragen mit gRPC senden und Antworten erhalten

Es sind vier einfache Schritte:

  • [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 UI.

Dies erreichen Sie in MainActivity.java.

Optional: gRPC-Client-Stub-Code generieren

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

a9d0e5cb543467b4.png

Die Bereitstellung von .proto-Dateien wird durch TensorFlow Serving und TensorFlow definiert. Ab TensorFlow und TensorFlow Serving 2.8 sind diese .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 die Stub-Datei 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üge diesen Code der Funktion createGRPCRequest() hinzu:
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

Sie können jetzt den Listener onClick(View view) abschließen.

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 in gRPC implementieren Sie die Funktion postprocessGRPCResponse(), um die Antwort zu verarbeiten.

private void postprocessGRPCResponse(Predict.PredictResponse response) {

}
  • Füge der Funktion postprocessGRPCResponse() diesen Code hinzu:
// 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);

Mit der Nachbearbeitungsfunktion können jetzt vorhergesagte Werte aus der Antwort extrahiert und der Erkennungs-Begrenzungsrahmen in der UI gerendert werden.

Ausführen

  1. Klicken Sie im Navigationsmenü auf Ausführen.png Ausführen 'App' und warten Sie, bis die App geladen ist.
  2. Wählen Sie CIDR > Ausführung ausführen aus.

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

8. Glückwunsch

Sie haben TensorFlow Serving verwendet, um Ihrer App Objekterkennungsfunktionen hinzuzufügen.

Weitere Informationen