Sobre este codelab
1. Antes de começar
Neste codelab, você aprenderá a executar uma inferência de detecção de objetos em um app Android usando o TensorFlow Serving com REST e gRPC.
Prerequisites
- Conhecimento básico de desenvolvimento para Android com Java
- Conhecimento básico de machine learning com o TensorFlow, como treinamento e implantação
- Conhecimento básico de terminais e Docker
O que você aprenderá
- Como encontrar modelos de detecção de objetos pré-treinados no TensorFlow Hub.
- Como criar um app Android simples e fazer previsões com o modelo de detecção de objetos transferido por download usando o TensorFlow Serving (REST e gRPC).
- Como renderizar o resultado da detecção na IU.
Pré-requisitos
- A versão mais recente do Android Studio.
- Docker
- Bash
2. Começar a configuração
Para fazer o download do código para este codelab, faça o seguinte:
- Navegue até o repositório do GitHub deste codelab.
- Clique em Code > Download zip para fazer o download de todo o código para este codelab.
- Descompacte o arquivo ZIP transferido por download para descompactar uma pasta raiz do
codelabs
com todos os recursos que você precisa.
Neste codelab, você só precisará dos arquivos no subdiretório TFServing/ObjectDetectionAndroid
do repositório, que contém duas pastas:
- A pasta
starter
contém o código inicial que você usará como base para este codelab. - A pasta
finished
contém o código concluído do app de exemplo finalizado.
3. Adicionar as dependências ao projeto
Importar o app inicial para o Android Studio
- No Android Studio, clique em File > New > Import project e escolha a pasta
starter
no código-fonte que você transferiu por download anteriormente.
Adicionar as dependências para OkHttp e gRPC
- No arquivo
app/build.gradle
do projeto, confirme a presença das dependências.
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'
}
Sincronizar seu projeto com arquivos do Gradle
- Selecione
Sync Project with Gradle Files no menu de navegação.
4. Executar o app inicial
- Inicie o Android Emulator e clique em
Executar "app" no menu de navegação.
Executar e explorar o app
O app será aberto no dispositivo Android. A IU é bem direta: há uma imagem de gato na qual você quer detectar objetos e o usuário pode escolher a forma de enviar os dados para o back-end, com REST ou gRPC. O back-end realiza a detecção de objetos na imagem e retorna os resultados da detecção para o app cliente, que renderiza a IU novamente.
No momento, nada acontece quando você clica em Executar inferência. Isso ocorre porque ele ainda não pode se comunicar com o back-end.
5. Implantar um modelo de detecção de objetos com o TensorFlow Serving
A detecção de objetos é uma tarefa de ML muito comum, e o objetivo dela é detectar objetos em imagens, ou seja, de prever possíveis categorias de objetos e caixas delimitadoras ao redor deles. Veja um exemplo de resultado de detecção:
O Google publicou vários modelos pré-treinados no TensorFlow Hub. Para ver a lista completa, visite a página object_detection. Para este codelab, você usará o modelo SSD MobileNet V2 FPNLite 320x320 relativamente leve para não precisar usar uma GPU para executá-lo.
Para implantar o modelo de detecção de objetos com o TensorFlow Serving:
- Faça o download do arquivo de modelo.
- Descompacte o arquivo
.tar.gz
salvo com uma ferramenta de descompactação, como o 7-Zip. - Crie uma pasta
ssd_mobilenet_v2_2_320
e uma subpasta123
dentro dela. - Coloque a pasta
variables
extraída e o arquivosaved_model.pb
na subpasta123
.
Você pode se referir à pasta ssd_mobilenet_v2_2_320
como a pasta SavedModel
. 123
é um exemplo de número da versão. Se quiser, você pode escolher outro número.
A estrutura de pasta será semelhante a esta imagem:
Iniciar o TensorFlow Serving
- No seu terminal, inicie o TensorFlow Serving com o Docker, mas substitua o marcador de posição
PATH/TO/SAVEDMODEL
pelo caminho absoluto da pastassd_mobilenet_v2_2_320
no seu computador.
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
Primeiro, o Docker faz o download automático da imagem do TensorFlow Serving em um minuto. Depois disso, o TensorFlow Serving será iniciado. O registro será semelhante a este snippet de código:
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. Conectar o app para Android com o TensorFlow Serving por meio do REST
O back-end está pronto, então você pode enviar solicitações de cliente ao TensorFlow Serving para detectar objetos em imagens. Há duas maneiras de enviar solicitações ao TensorFlow Serving:
- REST
- gRPC
Enviar solicitações e receber respostas por REST
Há três etapas simples:
- Crie a solicitação REST.
- Envie a solicitação REST para o TensorFlow Serving.
- Extraia o resultado previsto da resposta REST e renderize a IU.
Você conseguirá em MainActivity.java.
Criar a solicitação REST
No momento, há uma função createRESTRequest()
vazia no arquivo MainActivity.java
. Implemente essa função para criar uma solicitação REST.
private Request createRESTRequest() {
}
O TensorFlow Serving espera uma solicitação POST que contenha o tensor de imagem para o modelo SSD MobileNet usado, então você precisa extrair os valores RGB de cada pixel da imagem em uma matriz e depois unir a matriz em um JSON, que é o payload da solicitação.
- Adicione este código à função
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;
Enviar a solicitação REST para o TensorFlow Serving
O app permite que o usuário escolha REST ou gRPC para se comunicar com o TensorFlow Serving, portanto há duas ramificações no listener onClick(View view)
.
predictButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (requestRadioGroup.getCheckedRadioButtonId() == R.id.rest) {
// TODO: REST request
}
else {
}
}
}
)
- Adicione este código à ramificação REST do listener
onClick(View view)
para usar OkHttp e enviar a solicitação ao 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;
}
Processar a resposta REST do TensorFlow Serving
O modelo SSD MobileNet retorna vários resultados, que incluem:
num_detections
: o número de detecçõesdetection_scores
: pontuações de detecçãodetection_classes
: o índice da classe de detecçãodetection_boxes
: as coordenadas da caixa delimitadora
Implemente a função postprocessRESTResponse()
para processar a resposta.
private void postprocessRESTResponse(Predict.PredictResponse response) {
}
- Adicione este código à função
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);
Agora, a função de pós-processamento extrai os valores previstos da resposta, identifica a categoria mais provável do objeto e as coordenadas dos vértices da caixa delimitadora e, por último, renderiza a caixa delimitadora de detecção na IU.
Executar
- Clique em
Run 'app' no menu de navegação e aguarde o carregamento do app.
- Selecione REST > Executar inferência.
Leva alguns segundos para o app renderizar a caixa delimitadora do gato e mostrar 17
como a categoria do objeto, que é mapeada para o objeto cat
no conjunto de dados COCO.
7. Conectar o app Android com o TensorFlow Serving por meio do gRPC
Além do REST, o TensorFlow Serving também é compatível com o gRPC.
O gRPC é um framework de chamada de procedimento remoto (RPC) moderno, de código aberto e de alta performance que pode ser executado em qualquer ambiente. Ele pode conectar serviços de forma eficiente em data centers e com compatibilidade conectável para balanceamento de carga, rastreamento, verificação de integridade e autenticação. Observamos que o gRPC tem uma performance melhor do que o REST na prática.
Enviar solicitações e receber respostas com o gRPC
Há quatro etapas simples:
- [Opcional] Gere o código de stub do cliente gRPC.
- Criar a solicitação gRPC
- Enviar a solicitação gRPC para o TensorFlow Serving
- Extrair o resultado previsto da resposta gRPC e renderizar a IU
Você conseguirá em MainActivity.java.
Opcional: gerar o código stub do cliente gRPC
Para usar o gRPC com o TensorFlow Serving, é necessário seguir o fluxo de trabalho do gRPC. Caso queira saber mais, consulte a documentação do gRPC.
O TensorFlow Serving e o TensorFlow definem os arquivos .proto
automaticamente. No TensorFlow e no TensorFlow Serving 2.8, os seguintes arquivos .proto
são necessários:
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
- Para gerar o stub, adicione esse código ao arquivo
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' }
}
}
}
}
Criar a solicitação gRPC
Semelhante à solicitação REST, você cria a solicitação gRPC na função createGRPCRequest()
.
private Request createGRPCRequest() {
}
- Adicione este código à função
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();
Enviar a solicitação gRPC para o TensorFlow Serving
Agora você pode terminar o listener onClick(View view)
.
predictButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (requestRadioGroup.getCheckedRadioButtonId() == R.id.rest) {
}
else {
// TODO: gRPC request
}
}
}
)
- Adicione este código à ramificação 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;
}
Processar a resposta gRPC do TensorFlow Serving
Assim como no gRPC, você implementa a função postprocessGRPCResponse()
para processar a resposta.
private void postprocessGRPCResponse(Predict.PredictResponse response) {
}
- Adicione este código à função
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);
Agora, a função de pós-processamento pode extrair valores previstos da resposta e renderizar a caixa delimitadora de detecção na IU.
Executar
- Clique em
Run 'app' no menu de navegação e aguarde o carregamento do app.
- Selecione gRPC > Executar inferência.
Leva alguns segundos para o app renderizar a caixa delimitadora do gato e mostrar 17
como a categoria do objeto, que é mapeada para a categoria cat
no conjunto de dados COCO.
8. Parabéns
Você usou o TensorFlow Serving para adicionar recursos de detecção de objetos ao seu app.