이미지 내에 있는 객체를 감지하는 Android 앱 만들기

1. 시작하기 전에

이 Codelab에서는 REST 및 gRPC를 사용하여 TensorFlow Serving을 사용하여 Android 앱에서 객체 감지 추론을 실행하는 방법을 알아봅니다.

기본 요건

  • 자바를 사용한 Android 개발 관련 기본 지식
  • 학습 및 배포와 같은 TensorFlow를 활용한 머신러닝에 관한 기본 지식
  • 터미널 및 Docker에 관한 기본 지식

실습 내용

  • TensorFlow Hub에서 선행 학습된 객체 감지 모델을 찾는 방법
  • TensorFlow Serving (REST 및 gRPC)을 통해 간단한 Android 앱을 빌드하고 다운로드한 객체 감지 모델로 예측하는 방법
  • UI에서 감지 결과를 렌더링하는 방법

준비물

2 설정

이 Codelab의 코드를 다운로드하려면 다음 안내를 따르세요.

  1. 이 Codelab의 GitHub 저장소로 이동합니다.
  2. Code > 다운로드 zip을 클릭하여 이 Codelab의 모든 코드를 다운로드합니다.

A72f2bb4caa9a96.png

  1. 다운로드한 ZIP 파일의 압축을 풀고 필요한 모든 리소스로 codelabs 루트 폴더를 압축 해제합니다.

이 Codelab에서는 저장소의 TFServing/ObjectDetectionAndroid 하위 디렉터리에 있는 다음 두 폴더만 있는 파일이 필요합니다.

  • starter 폴더에는 이 Codelab을 위해 빌드하는 시작 코드가 포함되어 있습니다.
  • finished 폴더에는 완료된 샘플 앱의 완성된 코드가 포함되어 있습니다.

3. 프로젝트에 종속 항목 추가

Android 스튜디오로 시작 앱 가져오기

  • Android 스튜디오에서 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 파일과 동기화

  • 탐색 메뉴에서 541e90b497a7fef7.png 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. 7-Zip과 같은 압축 해제 도구로 다운로드한 .tar.gz 파일의 압축을 풉니다.
  3. ssd_mobilenet_v2_2_320 폴더를 만든 후 이 폴더에 123 하위 폴더를 만듭니다.
  4. 추출된 variables 폴더와 saved_model.pb 파일을 123 하위 폴더에 넣습니다.

ssd_mobilenet_v2_2_320 폴더를 SavedModel 폴더로 참조할 수 있습니다. 123는 버전 번호의 예입니다. 원하는 경우 다른 번호를 선택할 수 있습니다.

폴더 구조는 다음과 같습니다.

42c8150a42033767.png

TensorFlow Serving 시작

  • 터미널에서 Docker로 TensorFlow Serving을 시작하지만 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 이미지를 먼저 자동으로 다운로드합니다. 몇 분 정도 걸립니다. 그런 다음 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를 통해 TensorFlow Serving으로 Android 앱 연결하기

이제 백엔드가 준비되었으므로 클라이언트 요청을 TensorFlow Serving으로 보내 이미지 내의 객체를 감지할 수 있습니다. TensorFlow Serving에 요청을 보내는 방법에는 두 가지가 있습니다.

  • REST
  • gRPC

REST를 통한 요청 전송 및 응답 수신

다음의 세 가지 간단한 단계가 가능합니다.

  • 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 요청 보내기

이 앱은 사용자가 REST 또는 gRPC를 선택하여 TensorFlow Serving과 통신할 수 있도록 합니다. 따라서 onClick(View view) 리스너에는 두 개의 브랜치가 있습니다.

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 (Remote Procedure Call) 프레임워크입니다. 부하 분산, 추적, 상태 확인, 인증을 지원하며 데이터 센터 안팎에서 서비스를 효율적으로 연결할 수 있습니다. gRPC가 실제로 REST보다 성능이 더 좋은 것으로 관찰되었습니다.

gRPC로 요청 보내기 및 응답 수신

다음의 간단한 4단계가 있습니다.

  • [선택사항] gRPC 클라이언트 스텁 코드를 생성합니다.
  • gRPC 요청을 만듭니다.
  • TensorFlow Serving에 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 요청과 마찬가지로 createGRPCRequest() 함수에 gRPC 요청을 만듭니다.

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 > 추론 실행을 선택합니다.

앱이 고양이의 경계 상자를 렌더링하고 객체 카테고리로 17을 표시하는 데는 몇 초 정도 걸리며 COCO 데이터 세트cat 카테고리에 매핑됩니다.

8 축하합니다

TensorFlow Serving을 사용하여 앱에 객체 감지 기능을 추가했습니다.

자세히 알아보기