Android アプリを作成して画像内のオブジェクトを検出する

1. 始める前に

この Codelab では、REST と gRPC で TensorFlow Serving を使用して、Android アプリからオブジェクト検出の推論を実行する方法を学びます。

前提条件

  • Java を使用した Android 開発の基本的な知識
  • トレーニングやデプロイなど、TensorFlow による機械学習に関する基本的な知識
  • ターミナルと Docker に関する基本的な知識

学習内容

  • TensorFlow Hub で事前トレーニング済みオブジェクト検出モデルを見つける方法
  • 簡単な Android アプリを作成し、ダウンロードしたオブジェクト検出モデルで TensorFlow Serving(REST と gRPC)を使用して予測を行う方法。
  • 検出結果を UI でレンダリングする方法。

必要なもの

2. 設定する

この Codelab のコードをダウンロードするには:

  1. この Codelab の GitHub リポジトリに移動します。
  2. [Code] > [Download zip] をクリックして、この Codelab のすべてのコードをダウンロードします。

a72f2bb4caa9a96.png

  1. ダウンロードした zip ファイルを解凍して、必要なリソースがすべて揃った codelabs ルートフォルダを展開します。

この Codelab では、リポジトリの TFServing/ObjectDetectionAndroid サブディレクトリ内のファイルのみが必要です。このサブディレクトリには 2 つのフォルダが含まれています。

  • starter フォルダには、この Codelab で構築するスターター コードが含まれています。
  • finished フォルダには、完成したサンプルアプリの完成したコードが含まれています。

3. 依存関係をプロジェクトに追加する

Android Studio にスターター アプリをインポートする

  • Android Studio で、[File] > [New] > [Import project] をクリックし、先ほどダウンロードしたソースコードから starter フォルダを選択します。

OkHttp と gRPC の依存関係を追加する

  • プロジェクトの app/build.gradle ファイルで、依存関係が存在することを確認します。
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'
}

プロジェクトと Gradle ファイルを同期する

  • ナビゲーション メニューから [2016-0105 Sync Project with Gradle Files] を選択します。

4.スターター アプリを実行する

アプリを実行して操作する

アプリが Android デバイスで起動します。UI は非常にシンプルで、オブジェクトを検出する猫の画像があります。ユーザーは、REST または gRPC を使用して、バックエンドにデータを送信する方法を選択できます。バックエンドは画像に対してオブジェクト検出を行い、その検出をクライアント アプリに返します。これにより、UI がレンダリングされます。

24EAB579530E9645.PNG

[推論を実行] をクリックしても何も起こりません。これは、まだバックエンドと通信できないためです。

5. TensorFlow Serving によるオブジェクト検出モデルのデプロイ

オブジェクト検出は非常に一般的な ML タスクで、画像内のオブジェクトを検出することを目的としています。つまり、オブジェクトの可能なカテゴリと周囲の境界ボックスを予測します。検出結果の例を次に示します。

a68f9308fb2fc17b.png

Google では TensorFlow Hub に多数の事前トレーニング済みモデルを公開しています。完全なリストについては、object_detection ページをご覧ください。この Codelab には比較的軽量な SSD MobileNet V2 FPNLite 320x320 モデルを使用しているため、必ずしも GPU を使用して実行する必要はありません。

TensorFlow Serving を使用してオブジェクト検出モデルをデプロイするには:

  1. モデルファイルをダウンロードします。
  2. ダウンロードした .tar.gz ファイルを 7-Zip などの解凍ツールを使用して解凍します。
  3. ssd_mobilenet_v2_2_320 フォルダを作成し、その中に 123 サブフォルダを作成します。
  4. 抽出した variables フォルダと saved_model.pb ファイルを 123 サブフォルダに配置します。

ssd_mobilenet_v2_2_320 フォルダは、SavedModel フォルダとして参照できます。123 はバージョン番号の例です。別の数字を選択することもできます。

フォルダ構造は次の画像のようになります。

42C8150a42033767.png

TensorFlow サービスの提供を開始

  • ターミナルで、TensorFlow Serving を Docker で起動しますが、PATH/TO/SAVEDMODEL プレースホルダを、パソコンの ssd_mobilenet_v2_2_320 フォルダの絶対パスに置き換えます。
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 では、最初に TensorFlow Serving の画像が自動的にダウンロードされます。これには 1 分ほどかかります。その後、TensorFlow Serving が起動します。ログは次のコード スニペットのようになります。

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. REST を介して Android アプリを TensorFlow Serving に接続する

これでバックエンドの準備が整い、クライアント リクエストを TensorFlow Serving に送信して、画像内のオブジェクトを検出できるようになりました。TensorFlow Serving にリクエストを送信する方法は 2 つあります。

  • REST
  • gRPC

REST 経由でのリクエストの送信とレスポンスの受信

手順は次の 3 つです。

  • REST リクエストを作成します。
  • TensorFlow Serving に REST リクエストを送信します。
  • REST レスポンスから予測結果を抽出し、UI をレンダリングします。

達成は MainActivity.java. で行われます

REST リクエストを作成する

現時点では、MainActivity.java ファイルに空の createRESTRequest() 関数があります。この関数を実装して REST リクエストを作成します。

private Request createRESTRequest() {
}

TensorFlow Serving は、使用する SSD MobileNet モデルの画像テンソルを含む POST リクエストを想定しています。そのため、画像の各ピクセルから RGB 値を配列に抽出し、その配列をペイロードである JSON にラップする必要があります。リクエストします。

  • 次のコードを 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;

TensorFlow Serving に REST リクエストを送信する

このアプリを使用すると、ユーザーは TensorFlow Serving と通信するために REST または gRPC を選択できるため、onClick(View view) リスナーに 2 つのブランチがあります。

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

            }
        }
    }
)
  • 次のコードを onClick(View view) リスナーの REST ブランチに追加して、OkHttp を使用してリクエストを 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;
}

TensorFlow Serving から REST レスポンスを処理する

SSD MobileNet モデルからは次のような結果が返されます。

  • num_detections: 検出の数
  • detection_scores: 検出スコア
  • detection_classes: 検出クラス インデックス
  • detection_boxes: 境界ボックスの座標

postprocessRESTResponse() 関数を実装してレスポンスを処理します。

private void postprocessRESTResponse(Predict.PredictResponse response) {

}
  • 次のコードを 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);

これで、後処理関数からレスポンスから予測値が抽出され、オブジェクトの最も可能性の高いカテゴリと境界ボックスの頂点の座標が特定され、最後に検出境界ボックスが UI にレンダリングされます。

実行

  1. ナビゲーション メニューの execute.png [Run 'app'] をクリックし、アプリが読み込まれるのを待ちます。
  2. [REST] > [Run 推論] を選択します。

アプリが猫の境界ボックスをレンダリングし、オブジェクトのカテゴリとして 17 が表示されるまでに数秒かかります。カテゴリは COCO データセットcat オブジェクトにマッピングされます。

5a1a32768dc516d6.png

7. gRPC を介して Android アプリを TensorFlow Serving に接続する

TensorFlow Serving は REST に加えて、gRPC もサポートします。

b6f4449c2c850b0e.png

gRPC は、最新のオープンソースで高性能なリモート プロシージャ コール(RPC)フレームワークです。あらゆる環境で実行できます。負荷分散やトレース、ヘルスチェック、認証に対応するプラグイン可能なサポートにより、データセンター内やデータセンター間でサービスを効率的に接続できます。実際には、gRPC は REST よりもパフォーマンスが高いことがわかっています。

gRPC でリクエストを送信してレスポンスを受信する

次の 4 つの簡単な手順に沿ってください。

  • (省略可)gRPC クライアント スタブコードを生成します。
  • gRPC リクエストを作成する。
  • gRPC サービングに gRPC リクエストを送信します。
  • gRPC レスポンスから予測結果を抽出し、UI をレンダリングします。

達成にはMainActivity.java.が必要です

省略可: gRPC クライアント スタブコードを生成する

TensorFlow Serving で gRPC を使用するには、gRPC ワークフローに従う必要があります。詳細については、gRPC のドキュメントをご覧ください。

a9d0e5cb543467b4.png

TensorFlow Serving と TensorFlow は .proto ファイルを定義します。TensorFlow と TensorFlow Serving 2.8 では、次の .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
  • スタブを生成するには、次のコードを 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' }
            }
        }
    }
}

gRPC リクエストを作成する

REST リクエストと同じように、gRPC リクエストは createGRPCRequest() 関数で作成します。

private Request createGRPCRequest() {

}
  • 次のコードを 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();

TensorFlow Serving に gRPC リクエストを送信する

これで onClick(View view) リスナーを終了できます。

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

            }
            else {
                // TODO: gRPC request
            }
        }
    }
)
  • 次のコードを 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;
}

TensorFlow Serving から gRPC レスポンスを処理する

gRPC と同様に、postprocessGRPCResponse() 関数を実装してレスポンスを処理します。

private void postprocessGRPCResponse(Predict.PredictResponse response) {

}
  • 次のコードを 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);

これで、後処理関数でレスポンスから予測値を抽出し、検出境界ボックスを UI にレンダリングできるようになりました。

実行

  1. ナビゲーション メニューの execute.png [Run 'app'] をクリックし、アプリが読み込まれるのを待ちます。
  2. [gRPC] > [Run 推論] を選択します。

アプリが猫の境界ボックスをレンダリングし、オブジェクトのカテゴリとして 17 が表示されるまでに数秒かかります。これは、COCO データセットcat カテゴリにマッピングされます。

8. 完了

TensorFlow Serving を使用して、オブジェクト検出機能をアプリに追加しました。

詳細