Tworzenie aplikacji na Androida do wykrywania obiektów na obrazach

1. Zanim zaczniesz

Z tego przewodnika dowiesz się, jak uruchomić wnioskowanie dotyczące wykrywania obiektów z poziomu aplikacji na Androida za pomocą TensorFlow Serving z interfejsami REST i gRPC.

Wymagania wstępne

  • Podstawowa wiedza na temat tworzenia aplikacji na Androida w języku Java
  • Podstawowa wiedza o uczeniu maszynowym z TensorFlow, np. trenowanie i wdrażanie
  • Podstawowa znajomość terminali i Dockera

Czego się nauczysz

  • Jak znaleźć wstępnie wytrenowane modele wykrywania obiektów w TensorFlow Hub.
  • Jak utworzyć prostą aplikację na Androida i dokonywać prognoz za pomocą pobranego modelu wykrywania obiektów za pomocą TensorFlow Serving (REST i gRPC).
  • Jak renderować wynik wykrywania w interfejsie.

Czego potrzebujesz

2. Konfiguracja

Aby pobrać kod do tego ćwiczenia:

  1. Otwórz repozytorium GitHub dla tego laboratorium.
  2. Aby pobrać cały kod do tych ćwiczeń z programowania, kliknij Code > Download zip (Kod > Pobierz plik ZIP).

a72f2bb4caa9a96.png

  1. Rozpakuj pobrany plik ZIP, aby wyodrębnić folder główny codelabs ze wszystkimi potrzebnymi zasobami.

W tym ćwiczeniu potrzebne są tylko pliki z podkatalogu TFServing/ObjectDetectionAndroid w repozytorium, który zawiera 2 foldery:

  • Folder starter zawiera kod początkowy, na którym będziesz pracować w tym laboratorium.
  • Folder finished zawiera gotowy kod ukończonej przykładowej aplikacji.

3. Dodawanie zależności do projektu

Importowanie aplikacji początkowej do Android Studio

  • W Android Studio kliknij Plik > Nowy > Importuj projekt, a potem wybierz folder starter z pobranego wcześniej kodu źródłowego.

Dodaj zależności dla OkHttp i gRPC

  • W pliku app/build.gradle projektu sprawdź, czy występują zależności.
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'
}

Synchronizowanie projektu z plikami Gradle

  • W menu nawigacyjnym wybierz 541e90b497a7fef7.png Synchronizuj projekt z plikami Gradle.

4. Uruchamianie aplikacji wyjściowej

Uruchamianie i poznawanie aplikacji

Aplikacja powinna się uruchomić na urządzeniu z Androidem. Interfejs jest dość prosty: jest to obraz kota, na którym chcesz wykryć obiekty, a użytkownik może wybrać sposób wysyłania danych do backendu – za pomocą REST lub gRPC. Backend przeprowadza wykrywanie obiektów na obrazie i zwraca wyniki wykrywania do aplikacji klienta, która ponownie renderuje interfejs.

24eab579530e9645.png

Jeśli teraz klikniesz Uruchom wnioskowanie, nic się nie stanie. Dzieje się tak, ponieważ nie może jeszcze komunikować się z backendem.

5. Wdrażanie modelu wykrywania obiektów za pomocą TensorFlow Serving

Wykrywanie obiektów to bardzo popularne zadanie uczenia maszynowego. Jego celem jest wykrywanie obiektów na obrazach, czyli przewidywanie możliwych kategorii obiektów i ramek ograniczających wokół nich. Oto przykład wyniku wykrywania:

a68f9308fb2fc17b.png

Google opublikowało wiele wstępnie wytrenowanych modeli w TensorFlow Hub. Pełną listę znajdziesz na stronie object_detection. W tym module używasz stosunkowo lekkiego modelu SSD MobileNet V2 FPNLite 320x320, więc nie musisz używać GPU do jego uruchomienia.

Aby wdrożyć model wykrywania obiektów za pomocą TensorFlow Serving:

  1. Pobierz plik modelu.
  2. Rozpakuj pobrany plik .tar.gz za pomocą narzędzia do dekompresji, np. 7-Zip.
  3. Utwórz folder ssd_mobilenet_v2_2_320, a następnie utwórz w nim podfolder 123.
  4. Umieść wyodrębniony variables folder i saved_model.pb plik w podfolderze 123.

Folder ssd_mobilenet_v2_2_320 możesz określać jako folder SavedModel. 123 to przykładowy numer wersji. Jeśli chcesz, możesz wybrać inny numer.

Struktura folderów powinna wyglądać jak na tym obrazie:

42c8150a42033767.png

Uruchamianie TensorFlow Serving

  • W terminalu uruchom TensorFlow Serving za pomocą Dockera, ale zastąp obiekt zastępczy PATH/TO/SAVEDMODEL bezwzględną ścieżką do folderu ssd_mobilenet_v2_2_320 na komputerze.
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 najpierw automatycznie pobierze obraz TensorFlow Serving, co zajmie minutę. Następnie powinna się uruchomić usługa TensorFlow Serving. Dziennik powinien wyglądać jak ten fragment kodu:

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. Łączenie aplikacji na Androida z TensorFlow Serving za pomocą interfejsu REST

Backend jest już gotowy, więc możesz wysyłać żądania klienta do TensorFlow Serving, aby wykrywać obiekty na obrazach. Żądania do TensorFlow Serving można wysyłać na 2 sposoby:

  • REST
  • gRPC

Wysyłanie żądań i otrzymywanie odpowiedzi za pomocą interfejsu REST

Wystarczą 3 proste kroki:

  • Utwórz żądanie REST.
  • Wyślij żądanie REST do TensorFlow Serving.
  • Wyodrębnij przewidywany wynik z odpowiedzi REST i wyświetl interfejs.

Osiągniesz je za MainActivity.java.

Tworzenie żądania REST

Obecnie w pliku MainActivity.java znajduje się pusta funkcja createRESTRequest(). Ta funkcja służy do tworzenia żądania REST.

private Request createRESTRequest() {
}

TensorFlow Serving oczekuje żądania POST, które zawiera tensor obrazu dla używanego modelu SSD MobileNet. Musisz więc wyodrębnić wartości RGB z każdego piksela obrazu do tablicy, a następnie umieścić tablicę w pliku JSON, który jest ładunkiem żądania.

  • Dodaj ten kod do funkcji 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;    

Wysyłanie żądania REST do TensorFlow Serving

Aplikacja umożliwia użytkownikowi wybór REST lub gRPC do komunikacji z TensorFlow Serving, więc w onClick(View view) listenerze są 2 gałęzie.

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

            }
        }
    }
)
  • Dodaj ten kod do gałęzi REST onClick(View view) odbiornika, aby używać OkHttp do wysyłania żądań do 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;
}

Przetwarzanie odpowiedzi REST z TensorFlow Serving

Model SSD MobileNet zwraca kilka wyników, w tym:

  • num_detections: liczba wykrytych zagrożeń.
  • detection_scores: wyniki wykrywania
  • detection_classes: indeks klasy wykrywania.
  • detection_boxes: współrzędne ramki ograniczającej,

Aby obsłużyć odpowiedź, zaimplementuj funkcję postprocessRESTResponse().

private void postprocessRESTResponse(Predict.PredictResponse response) {

}
  • Dodaj ten kod do funkcji 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);

Funkcja przetwarzania końcowego wyodrębnia teraz z odpowiedzi przewidywane wartości, identyfikuje najbardziej prawdopodobną kategorię obiektu i współrzędne wierzchołków ramki ograniczającej, a na koniec renderuje ramkę ograniczającą wykrywanie w interfejsie.

Uruchom

  1. W menu nawigacyjnym kliknij execute.png Uruchom „aplikację”, a następnie poczekaj, aż aplikacja się wczyta.
  2. Wybierz REST > Uruchom wnioskowanie.

Aplikacja potrzebuje kilku sekund, aby wyrenderować ramkę ograniczającą kota i wyświetlić 17 jako kategorię obiektu, która jest powiązana z obiektem catzbiorze danych COCO.

5a1a32768dc516d6.png

7. Łączenie aplikacji na Androida z TensorFlow Serving za pomocą gRPC

Oprócz REST TensorFlow Serving obsługuje też gRPC.

b6f4449c2c850b0e.png

gRPC to nowoczesna, otwarta i wydajna platforma zdalnego wywoływania procedur (RPC), która może działać w dowolnym środowisku. Umożliwia wydajne łączenie usług w centrach danych i między nimi dzięki wtyczkom do równoważenia obciążenia, śledzenia, sprawdzania stanu i uwierzytelniania. Zaobserwowano, że w praktyce gRPC jest wydajniejszy niż REST.

Wysyłanie żądań i otrzymywanie odpowiedzi za pomocą gRPC

Wystarczą 4 proste kroki:

  • [Opcjonalnie] Wygeneruj kod namiestnika klienta gRPC.
  • Utwórz żądanie gRPC.
  • Wysyłanie żądania gRPC do TensorFlow Serving.
  • Wyodrębnij prognozowany wynik z odpowiedzi gRPC i wyświetl interfejs.

Osiągniesz je za MainActivity.java.

Opcjonalnie: generowanie kodu namiestnika klienta gRPC

Aby używać gRPC z TensorFlow Serving, musisz postępować zgodnie z procesem gRPC. Więcej informacji znajdziesz w dokumentacji gRPC.

a9d0e5cb543467b4.png

TensorFlow Serving i TensorFlow definiują za Ciebie pliki .proto. W TensorFlow i TensorFlow Serving w wersji 2.8 wymagane są te .proto pliki:

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
  • Aby wygenerować stub, dodaj ten kod do pliku 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' }
            }
        }
    }
}

Tworzenie żądania gRPC

Podobnie jak w przypadku żądania REST, żądanie gRPC tworzysz w funkcji createGRPCRequest().

private Request createGRPCRequest() {

}
  • Dodaj ten kod do funkcji 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();

Wysyłanie żądania gRPC do TensorFlow Serving

Teraz możesz zakończyć onClick(View view).

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

            }
            else {
                // TODO: gRPC request
            }
        }
    }
)
  • Dodaj ten kod do gałęzi 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;
}

Przetwarzanie odpowiedzi gRPC z TensorFlow Serving

Podobnie jak w przypadku gRPC, implementujesz funkcję postprocessGRPCResponse() do obsługi odpowiedzi.

private void postprocessGRPCResponse(Predict.PredictResponse response) {

}
  • Dodaj ten kod do funkcji 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);

Funkcja przetwarzania końcowego może teraz wyodrębniać z odpowiedzi przewidywane wartości i renderować w interfejsie ramkę wykrywania.

Uruchom

  1. W menu nawigacyjnym kliknij execute.png Uruchom „aplikację”, a następnie poczekaj, aż aplikacja się wczyta.
  2. Kliknij gRPC > Uruchom wnioskowanie.

Aplikacja potrzebuje kilku sekund, aby wyrenderować ramkę ograniczającą kota i wyświetlić 17 jako kategorię obiektu, która odpowiada kategorii catzbiorze danych COCO.

8. Gratulacje

Za pomocą TensorFlow Serving udało Ci się dodać do aplikacji funkcje wykrywania obiektów.

Więcej informacji