إنشاء تطبيق Android لرصد العناصر داخل الصور

1. قبل البدء

في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية تنفيذ استنتاج رصد العناصر من تطبيق Android باستخدام TensorFlow Serving مع REST وgRPC.

المتطلبات الأساسية

  • معرفة أساسية بتطوير تطبيقات Android باستخدام Java
  • معرفة أساسية بتعلُّم الآلة باستخدام TensorFlow، مثل التدريب والنشر
  • معرفة أساسية بالمحطات الطرفية وDocker

أهداف الدورة التعليمية

  • كيفية العثور على نماذج رصد الأجسام المُدرَّبة مسبقًا على TensorFlow Hub
  • كيفية إنشاء تطبيق Android بسيط وتقديم توقّعات باستخدام نموذج رصد العناصر الذي تم تنزيله من خلال TensorFlow Serving (REST وgRPC)
  • كيفية عرض نتيجة الرصد في واجهة المستخدم

المتطلبات

2. طريقة الإعداد

لتنزيل الرمز البرمجي لهذا الدرس التطبيقي حول الترميز، اتّبِع الخطوات التالية:

  1. انتقِل إلى مستودع GitHub الخاص بهذا الدرس العملي.
  2. انقر على الرمز > تنزيل ملف zip لتنزيل كل الرمز البرمجي لهذا الدرس التطبيقي حول الترميز.

a72f2bb4caa9a96.png

  1. فكّ ضغط ملف zip الذي تم تنزيله لفتح مجلد جذر codelabs يحتوي على جميع الموارد التي تحتاج إليها.

في هذا الدرس العملي، تحتاج فقط إلى الملفات في الدليل الفرعي TFServing/ObjectDetectionAndroid في المستودع، والذي يحتوي على مجلدَين:

  • يحتوي المجلد starter على الرمز الأولي الذي ستستند إليه في هذا الدرس العملي.
  • يحتوي المجلد finished على الرمز البرمجي المكتمل لتطبيق العيّنة النهائي.

3- إضافة التبعيات إلى المشروع

استيراد تطبيق البداية إلى Android Studio

  • في "استوديو Android"، انقر على ملف > جديد > استيراد مشروع، ثم اختَر مجلد 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 مزامنة المشروع مع ملفات Gradle من قائمة التنقّل.

4. تشغيل التطبيق النموذجي

تشغيل التطبيق واستكشافه

من المفترض أن يتم تشغيل التطبيق على جهاز Android. واجهة المستخدم بسيطة جدًا: هناك صورة قطة تريد رصد الكائنات فيها، ويمكن للمستخدم اختيار طريقة إرسال البيانات إلى الخلفية، باستخدام REST أو gRPC. ينفّذ الخلفية عملية رصد العناصر في الصورة ويعرض نتائج الرصد على تطبيق العميل، الذي يعيد عرض واجهة المستخدم.

24eab579530e9645.png

في الوقت الحالي، إذا نقرت على تشغيل الاستدلال، لن يحدث شيء. ويرجع ذلك إلى أنّه لا يمكنه التواصل مع الخلفية بعد.

5- نشر نموذج لرصد الأجسام باستخدام TensorFlow Serving

رصد الأجسام هو مهمة شائعة جدًا في تعلُّم الآلة، وتهدف إلى رصد الأجسام داخل الصور، أي توقّع الفئات المحتملة للأجسام والمربّعات المحيطة بها. في ما يلي مثال على نتيجة رصد:

a68f9308fb2fc17b.png

نشرت Google عددًا من النماذج المدرَّبة مسبقًا على TensorFlow Hub. للاطّلاع على القائمة الكاملة، انتقِل إلى صفحة object_detection. تستخدِم في هذا الدرس النموذجي نموذج 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 Serving

  • في نافذة الأوامر، ابدأ 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 أولاً، ويستغرق ذلك دقيقة واحدة. بعد ذلك، من المفترض أن يبدأ 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، وهو حمولة الطلب.

  • أضِف الرمز التالي إلى الدالة 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، لذا هناك فرعان في أداة الاستماع 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);

تعمل دالة ما بعد المعالجة الآن على استخراج القيم المتوقّعة من الرد، وتحديد الفئة الأكثر احتمالاً للعنصر وإحداثيات رؤوس المربّع المحيط، ثم عرض المربّع المحيط الخاص بعملية الرصد على واجهة المستخدم.

تشغيلها

  1. انقر على execute.png تشغيل "التطبيق" في قائمة التنقّل، ثمّ انتظِر إلى أن يتم تحميل التطبيق.
  2. اختَر REST > تشغيل الاستدلال.

يستغرق التطبيق بضع ثوانٍ قبل عرض المربّع المحيط بالقطة وعرض 17 كفئة الكائن، والتي تتطابق مع الكائن cat في مجموعة بيانات COCO.

5a1a32768dc516d6.png

7. ربط تطبيق Android بخدمة TensorFlow Serving من خلال gRPC

بالإضافة إلى REST، يتيح TensorFlow Serving أيضًا استخدام gRPC.

b6f4449c2c850b0e.png

‫gRPC هو إطار عمل حديث ومفتوح المصدر وعالي الأداء لاستدعاء الإجراءات عن بُعد (RPC) يمكن تشغيله في أي بيئة. يمكنه ربط الخدمات بكفاءة داخل مراكز البيانات وفي ما بينها مع إمكانية إضافة ميزات موازنة التحميل والتتبُّع والتحقّق من الصحة والمصادقة. وقد تبيّن أنّ gRPC يتفوّق على REST من حيث الأداء في الواقع العملي.

إرسال الطلبات وتلقّي الردود باستخدام gRPC

في ما يلي أربع خطوات بسيطة:

  • [اختياري] أنشئ رمزًا بديلًا لعميل gRPC.
  • أنشِئ طلب gRPC.
  • أرسِل طلب gRPC إلى TensorFlow Serving.
  • استخرِج النتيجة المتوقّعة من استجابة gRPC واعرض واجهة المستخدم.

ستحقّق هذه الأهداف في MainActivity.java.

اختياري: إنشاء رمز بديل لعميل gRPC

لاستخدام gRPC مع TensorFlow Serving، عليك اتّباع سير عمل gRPC. لمزيد من المعلومات عن التفاصيل، يُرجى الاطّلاع على مستندات gRPC.

a9d0e5cb543467b4.png

تحدّد منصة TensorFlow للعرض و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
  • لإنشاء رمز تجريبي، أضِف هذا الرمز إلى ملف 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);

يمكن الآن لدالة ما بعد المعالجة استخراج القيم المتوقّعة من الردّ وعرض مربّع الإحاطة الخاص بعملية الرصد في واجهة المستخدم.

تشغيلها

  1. انقر على execute.png تشغيل "التطبيق" في قائمة التنقّل، ثمّ انتظِر إلى أن يتم تحميل التطبيق.
  2. اختَر gRPC > تشغيل الاستنتاج.

يستغرق التطبيق بضع ثوانٍ قبل عرض المربّع المحيط بالقطة وعرض 17 كفئة الكائن، والتي تتطابق مع الفئة cat في مجموعة بيانات COCO.

8. تهانينا

لقد استخدمت TensorFlow Serving لإضافة إمكانات رصد العناصر إلى تطبيقك.

مزيد من المعلومات