1. לפני שמתחילים
ב-codelab הזה תלמדו איך להריץ מסקנה של זיהוי אובייקטים מאפליקציית Android באמצעות TensorFlow Serving עם REST ו-gRPC.
דרישות מוקדמות
- ידע בסיסי בפיתוח ל-Android באמצעות Java
- ידע בסיסי בלמידת מכונה עם TensorFlow, כמו אימון ופריסה
- ידע בסיסי בטרמינלים וב-Docker
מה תלמדו
- איך למצוא מודלים של זיהוי אובייקטים שאומנו מראש ב-TensorFlow Hub.
- איך יוצרים אפליקציית Android פשוטה ומבצעים חיזויים באמצעות מודל לזיהוי אובייקטים שהורד דרך TensorFlow Serving (REST ו-gRPC).
- איך להציג את תוצאת הזיהוי בממשק המשתמש.
מה נדרש
- הגרסה האחרונה של Android Studio
- Docker
- Bash
2. להגדרה
כדי להוריד את הקוד של ה-Codelab הזה:
- עוברים אל מאגר GitHub של ה-codelab הזה.
- לוחצים על Code > Download zip (קוד > הורדת קובץ zip) כדי להוריד את כל הקוד של ה-codelab הזה.

- מבטלים את הדחיסה של קובץ ה-ZIP שהורדתם כדי לחלץ תיקיית בסיס
codelabsעם כל המשאבים שאתם צריכים.
ב-codelab הזה, צריך רק את הקבצים בספריית המשנה TFServing/ObjectDetectionAndroid במאגר, שמכילה שתי תיקיות:
- התיקייה
starterמכילה את קוד ההתחלה שעליו תבנו את הפתרון במעבדת הקוד הזו. - התיקייה
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
- בתפריט הניווט, בוחרים באפשרות
Sync Project with Gradle Files.
4. הפעלת האפליקציה למתחילים
- מפעילים את Android Emulator ולוחצים על
Run ‘app' (הפעלת האפליקציה) בתפריט הניווט.
הפעלת האפליקציה וסקירת התכונות שלה
האפליקציה אמורה להיפתח במכשיר Android. ממשק המשתמש די פשוט: יש תמונה של חתול שרוצים לזהות בה אובייקטים, והמשתמש יכול לבחור את הדרך לשליחת הנתונים אל ה-Backend, באמצעות REST או gRPC. הקצה העורפי מבצע זיהוי אובייקטים בתמונה ומחזיר את תוצאות הזיהוי לאפליקציית הלקוח, שמציגה מחדש את ממשק המשתמש.

בשלב הזה, אם לוחצים על Run inference (הפעלת הסקה), לא קורה כלום. הסיבה לכך היא שהאפליקציה עדיין לא יכולה לתקשר עם ה-backend.
5. פריסת מודל לזיהוי אובייקטים באמצעות TensorFlow Serving
זיהוי אובייקטים הוא משימה נפוצה מאוד ב-ML. המטרה שלה היא לזהות אובייקטים בתמונות, כלומר לחזות את הקטגוריות האפשריות של האובייקטים ואת תיבות התוחמות סביבם. דוגמה לתוצאת זיהוי:

Google פרסמה מספר מודלים שאומנו מראש ב-TensorFlow Hub. הרשימה המלאה זמינה בדף object_detection. ב-codelab הזה משתמשים במודל SSD MobileNet V2 FPNLite 320x320 הקל יחסית, כך שלא בהכרח צריך להשתמש ב-GPU כדי להריץ אותו.
כדי לפרוס את מודל זיהוי האובייקטים באמצעות TensorFlow Serving:
- מורידים את קובץ המודל.
- מבטלים את הדחיסה של קובץ
.tar.gzשהורדתם באמצעות כלי לביטול דחיסה, כמו 7-Zip. - יוצרים תיקייה בשם
ssd_mobilenet_v2_2_320ואז יוצרים בתוכה תיקיית משנה בשם123. - מעבירים את התיקייה
variablesואת הקובץsaved_model.pbשחולצו לתיקיית המשנה123.
אפשר להתייחס לתיקייה ssd_mobilenet_v2_2_320 כאל התיקייה SavedModel. 123 היא דוגמה למספר גרסה. אם רוצים, אפשר לבחור מספר אחר.
מבנה התיקיות צריך להיראות כמו בתמונה הזו:

הפעלת TensorFlow Serving
- בטרמינל, מפעילים את TensorFlow Serving עם Docker, אבל מחליפים את placeholder
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. חיבור אפליקציית Android ל-TensorFlow Serving באמצעות REST
הקצה העורפי מוכן עכשיו, כך שאפשר לשלוח בקשות לקוח ל-TensorFlow Serving כדי לזהות אובייקטים בתמונות. יש שתי דרכים לשלוח בקשות ל-TensorFlow Serving:
- REST
- gRPC
שליחת בקשות וקבלת תשובות באמצעות REST
יש שלושה שלבים פשוטים:
- יוצרים את בקשת ה-REST.
- שולחים את בקשת ה-REST אל TensorFlow Serving.
- מחולצים את התוצאה החזויה מתגובת ה-REST ומציגים את ממשק המשתמש.
תגיעו ליעדים האלה תוך MainActivity.java.
יצירת בקשת REST
כרגע, יש פונקציה ריקה createRESTRequest() בקובץ MainActivity.java. מטמיעים את הפונקציה הזו כדי ליצור בקשת REST.
private Request createRESTRequest() {
}
TensorFlow Serving מצפה לבקשת POST שמכילה את טנסור התמונה של מודל ה-SSD MobileNet שבו אתם משתמשים, ולכן אתם צריכים לחלץ את ערכי ה-RGB מכל פיקסל בתמונה למערך, ואז לעטוף את המערך ב-JSON, שהוא מטען הייעודי (payload) של הבקשה.
- מוסיפים את הקוד הזה לפונקציה
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;
שליחת בקשת REST אל TensorFlow Serving
האפליקציה מאפשרת למשתמש לבחור בין REST לבין gRPC כדי לתקשר עם TensorFlow Serving, ולכן יש שני ענפים ב-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 {
}
}
}
)
- מוסיפים את הקוד הזה לענף ה-REST של מאזין
onClick(View view)כדי להשתמש ב-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;
}
עיבוד התגובה של REST מ-TensorFlow Serving
מודל 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);
עכשיו פונקציית העיבוד שלאחר מכן מחלצת ערכים חזויים מהתשובה, מזהה את הקטגוריה הסבירה ביותר של האובייקט ואת הקואורדינטות של קודקודי תיבת התוחמת, ולבסוף מעבדת את תיבת התוחמת של הזיהוי בממשק המשתמש.
הפעלה
- בתפריט הניווט, לוחצים על
Run ‘app' (הפעלת האפליקציה) ומחכים שהאפליקציה תיטען. - בוחרים באפשרות REST > Run inference (מנוחה > הפעלת הסקה).
האפליקציה מציגה את תיבת התוחמת של החתול אחרי כמה שניות, ומציגה את 17 כקטגוריה של האובייקט, שמשויכת לאובייקט cat במערך הנתונים של COCO.

7. חיבור אפליקציית Android ל-TensorFlow Serving דרך gRPC
בנוסף ל-REST, TensorFlow Serving תומך גם ב-gRPC.

gRPC היא מסגרת מודרנית בקוד פתוח לקריאה לשירות מרוחק (RPC) עם ביצועים גבוהים, שיכולה לפעול בכל סביבה. הוא יכול לחבר ביעילות שירותים במרכזי נתונים שונים, עם תמיכה בחיבורים לטעינת איזון, מעקב, בדיקת תקינות ואימות. בפועל, נראה ש-gRPC מניב ביצועים טובים יותר מ-REST.
שליחת בקשות וקבלת תגובות באמצעות gRPC
יש ארבע פעולות פשוטות:
- [אופציונלי] יוצרים את קוד ה-stub של לקוח gRPC.
- יוצרים את בקשת ה-gRPC.
- שולחים את בקשת gRPC אל TensorFlow Serving.
- מחלקים את התוצאה החזויה מהתגובה של gRPC ומציגים את ממשק המשתמש.
תגיעו ליעדים האלה תוך MainActivity.java.
אופציונלי: יצירת קוד stub של לקוח gRPC
כדי להשתמש ב-gRPC עם TensorFlow Serving, צריך לפעול לפי תהליך העבודה של gRPC. פרטים נוספים זמינים במסמכי התיעוד של gRPC.

TensorFlow Serving ו-TensorFlow מגדירים את קובצי ה-.proto בשבילכם. החל מגרסה 2.8 של TensorFlow ו-TensorFlow Serving, אלה הקבצים שנדרשים:.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
- כדי ליצור את ה-stub, מוסיפים את הקוד הזה לקובץ
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();
שליחת בקשת gRPC אל TensorFlow Serving
עכשיו אפשר לסיים את ההגדרה של מאזין 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;
}
עיבוד התגובה של gRPC מ-TensorFlow Serving
בדומה ל-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);
עכשיו פונקציית העיבוד שלאחר מכן יכולה לחלץ ערכים חזויים מהתגובה ולהציג את תיבת התוחמת של הזיהוי בממשק המשתמש.
הפעלה
- בתפריט הניווט, לוחצים על
Run ‘app' (הפעלת האפליקציה) ומחכים שהאפליקציה תיטען. - בוחרים באפשרות gRPC > Run inference (gRPC > הפעלת הסקה).
האפליקציה מציגה את התיבה התוחמת של החתול אחרי כמה שניות, ומציגה את 17 כקטגוריה של האובייקט, שמשויכת לקטגוריה cat במערך הנתונים COCO.
8. מזל טוב
השתמשתם ב-TensorFlow Serving כדי להוסיף לאפליקציה שלכם יכולות של זיהוי אובייקטים.