هناك طريقتان لدمج ميزة تصنيف الصور مع النماذج المخصّصة: من خلال تجميع مسار التعلّم كجزء من تطبيقك، أو من خلال استخدام مسار تعلّم غير مجمَّع يعتمد على "خدمات Google Play". إذا اخترت مسار التعلّم غير المجمَّع، سيكون حجم تطبيقك أصغر. راجِع الجدول التالي للحصول على التفاصيل.
| مُجمَّعة | غير مجمّعة | |
|---|---|---|
| اسم المكتبة | com.google.mlkit:image-labeling-custom | com.google.android.gms:play-services-mlkit-image-labeling-custom |
التنفيذ | يتم ربط خط الأنابيب بشكل ثابت بتطبيقك في وقت الإنشاء. | يتم تنزيل مسار التعلّم بشكل ديناميكي باستخدام "خدمات Google Play". |
| حجم التطبيق | زيادة في الحجم تبلغ حوالي 3.8 ميغابايت | زيادة في الحجم تبلغ حوالي 200 كيلوبايت |
| وقت التهيئة | تتوفّر ميزة "مسار المعالجة" على الفور. | قد تحتاج إلى الانتظار إلى حين تنزيل مسار التعلّم قبل الاستخدام الأول. |
| مرحلة دورة حياة واجهة برمجة التطبيقات | مرحلة التوفّر للجمهور العام (GA) | إصدار تجريبي |
هناك طريقتان لدمج نموذج مخصّص: تجميع النموذج من خلال وضعه داخل مجلد مواد العرض في تطبيقك، أو تنزيله ديناميكيًا من Firebase. يقارن الجدول التالي بين هذين الخيارَين.
| النموذج المجمَّع | النموذج المستضاف |
|---|---|
| النموذج هو جزء من حِزمة APK الخاصة بتطبيقك، ما يؤدي إلى زيادة حجمها. | ولا يشكّل النموذج جزءًا من حزمة APK، بل تتم استضافته من خلال تحميله إلى Cloud Storage. وننصحك باستخدام Cloud Storage for Firebase. |
| يتوفّر النموذج على الفور، حتى عندما يكون جهاز Android غير متصل بالإنترنت. | يجب أن يتضمّن تطبيقك رمزًا برمجيًا لتنزيل النموذج عند الطلب |
| لا حاجة إلى مشروع Firebase | يتطلّب مشروع Firebase (في حال استخدام "مساحة تخزين سحابية لـ Firebase"). |
| يجب إعادة نشر تطبيقك لتحديث النموذج. | إرسال تحديثات النموذج بدون إعادة نشر تطبيقك |
| ما مِن ميزة مدمجة لاختبار A/B | إجراء اختبار A/B باستخدام ميزة الإعداد عن بُعد عبر Firebase |
للتجربة:
- يمكنك الاطّلاع على تطبيق البدء السريع الخاص بميزة "الرؤية" للحصول على مثال على استخدام النموذج المجمّع، وتطبيق البدء السريع الخاص بميزة AutoML للحصول على مثال على استخدام النموذج المستضاف.
قبل البدء
في ملف
build.gradle.ktsعلى مستوى المشروع، تأكَّد من تضمين مستودع Maven من Google في كل من القسمَينbuildscriptوallprojects.أضِف العناصر التابعة لحزمة تعلّم الآلة على Android إلى ملف Gradle على مستوى التطبيق في الوحدة، والذي يكون عادةً
app/build.gradle.kts. اختَر إحدى التبعيات التالية بناءً على احتياجاتك:لتضمين مسار العرض في تطبيقك، اتّبِع الخطوات التالية:
dependencies { // ... // Use this dependency to bundle the pipeline with your app implementation("com.google.mlkit:image-labeling-custom:17.0.3") }لاستخدام مسار العرض في "خدمات Google Play"، يجب استيفاء ما يلي:
dependencies { // ... // Use this dependency to use the dynamically downloaded pipeline in Google Play services implementation("com.google.android.gms:play-services-mlkit-image-labeling-custom:16.0.0-beta5") }في حال اختيار استخدام مسار العرض في "خدمات Google Play"، يمكنك ضبط تطبيقك لتنزيل مسار العرض تلقائيًا على الجهاز بعد تثبيت تطبيقك من "متجر Play". ولإجراء ذلك، أضِف البيان التالي إلى ملف
AndroidManifest.xmlالخاص بتطبيقك:<application ...> ... <meta-data android:name="com.google.mlkit.vision.DEPENDENCIES" android:value="custom_ica" /> <!-- To use multiple downloads: android:value="custom_ica,download2,download3" --> </application>يمكنك أيضًا التحقّق بشكل صريح من توفّر مسار المعالجة وطلب التنزيل من خلال ModuleInstallClient API في "خدمات Google Play".
إذا لم تفعِّل عمليات تنزيل مسار البيانات في وقت التثبيت أو لم تطلب تنزيلًا صريحًا، سيتم تنزيل مسار البيانات عند تشغيل أداة وضع التصنيفات للمرة الأولى. لن يتم عرض أي نتائج للطلبات التي تُجريها قبل اكتمال عملية التنزيل.
إذا أردت تنزيل نموذج باستخدام مساحة تخزين سحابية لـ Firebase، تأكَّد من إضافة Firebase إلى مشروع Android، إذا لم يسبق لك إجراء ذلك. لا يكون ذلك مطلوبًا عند تجميع النموذج.
1. تحميل النموذج
يمكنك تحميل النموذج من مصدر مجمّع محليًا أو مصدر مستضاف عن بُعد.
ضبط مصدر نموذج محلي
لتضمين النموذج في تطبيقك، اتّبِع الخطوات التالية:
انسخ ملف النموذج (الذي ينتهي عادةً بـ
.tfliteأو.lite) إلى مجلدassets/في تطبيقك. (قد تحتاج إلى إنشاء المجلد أولاً من خلال النقر بزر الماوس الأيمن على مجلدapp/، ثم النقر على جديد > مجلد > مجلد مواد العرض).أنشئ الكائن
LocalModel، مع تحديد المسار إلى ملف النموذج:Kotlin
val localModel = LocalModel.Builder() .setAssetFilePath("model.tflite") // or .setAbsoluteFilePath(absolute path to model file) // or .setUri(URI to model file) .build()
جافا
LocalModel localModel = new LocalModel.Builder() .setAssetFilePath("model.tflite") // or .setAbsoluteFilePath(absolute path to model file) // or .setUri(URI to model file) .build();
ضبط مصدر نموذج مستضاف عن بُعد
لاستخدام النموذج المستضاف عن بُعد، عليك تنزيل ملف النموذج إلى وحدة التخزين المحلية على الجهاز باستخدام منطق التطبيق الخاص بك، ثم تحميله كنموذج محلي. ننصحك باستخدام مساحة تخزين سحابية لـ Firebase لاستضافة نموذج. وللحصول على تفاصيل التنفيذ، راجِع دليل نقل البيانات من Firebase ML إلى Cloud Storage.
ضبط أداة تصنيف الصور
بعد ضبط إعدادات مصادر النموذج، أنشئ عنصر ImageLabeler من أحدها.
تتوفّر الخيارات التالية:
| الخيارات | |
|---|---|
confidenceThreshold
|
الحدّ الأدنى لدرجة الثقة في التصنيفات التي تم رصدها في حال عدم ضبطها، سيتم استخدام أي حد مصنّف تحدّده البيانات الوصفية للنموذج. إذا كان النموذج لا يحتوي على أي بيانات وصفية أو إذا كانت البيانات الوصفية لا تحدّد حدًا أدنى للمصنّف، سيتم استخدام حد أدنى تلقائي يبلغ 0.0. |
maxResultCount
|
الحد الأقصى لعدد التصنيفات المطلوب عرضها. في حال عدم ضبطها، سيتم استخدام القيمة التلقائية 10. |
إذا كان لديك نموذج مجمّع محليًا فقط، ما عليك سوى إنشاء أداة تصنيف من عنصر
LocalModel:
Kotlin
val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel) .setConfidenceThreshold(0.5f) .setMaxResultCount(5) .build() val labeler = ImageLabeling.getClient(customImageLabelerOptions)
جافا
CustomImageLabelerOptions customImageLabelerOptions = new CustomImageLabelerOptions.Builder(localModel) .setConfidenceThreshold(0.5f) .setMaxResultCount(5) .build(); ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);
إذا كان لديك نموذج مستضاف عن بُعد، عليك التأكّد من تنزيله قبل تشغيله.
على الرغم من أنّه عليك تأكيد ذلك قبل تشغيل أداة تصنيف الصور فقط، إذا كان لديك نموذج مستضاف عن بُعد ونموذج مجمّع محليًا، قد يكون من المنطقي إجراء هذا التحقّق عند إنشاء مثيل لأداة تصنيف الصور: إنشاء أداة تصنيف من النموذج المستضاف عن بُعد إذا تم تنزيله، ومن النموذج المحلي في الحالات الأخرى.
Kotlin
val modelFile = File(context.cacheDir, "my_downloaded_model.tflite") val model = if (modelFile.exists()) { // Use the downloaded model if available LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build() } else { // Fall back to the bundled model LocalModel.Builder().setAssetFilePath("model.tflite").build() } val options = CustomImageLabelerOptions.Builder(model) .setConfidenceThreshold(0.5f) .setMaxResultCount(5) .build() val labeler = ImageLabeling.getClient(options)
جافا
File modelFile = new File(context.getCacheDir(), "my_downloaded_model.tflite"); LocalModel model; if (modelFile.exists()) { // Use the downloaded model if available model = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build(); } else { // Fall back to the bundled model model = new LocalModel.Builder().setAssetFilePath("model.tflite").build(); } CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(model) .setConfidenceThreshold(0.5f) .setMaxResultCount(5) .build(); ImageLabeler labeler = ImageLabeling.getClient(options);
إذا كان لديك نموذج مستضاف عن بُعد فقط، عليك إيقاف الوظائف ذات الصلة بالنموذج، مثل إخفاء جزء من واجهة المستخدم أو عرضه باللون الرمادي، إلى أن تتأكّد من تنزيل النموذج.
Kotlin
val localFile = File(context.cacheDir, "my_remote_model.tflite") if (localFile.exists()) { initializeLabeler(localFile) } else { showLoadingUI() val storage = Firebase.storage val modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite") modelRef.getFile(localFile) .addOnSuccessListener { hideLoadingUI() initializeLabeler(localFile) } .addOnFailureListener { showErrorUI() } } private fun initializeLabeler(modelFile: File) { val localModel = LocalModel.Builder().setAbsoluteFilePath(modelFile.absolutePath).build() val options = CustomImageLabelerOptions.Builder(localModel).build() val labeler = ImageLabeling.getClient(options) enableMLFeatures(labeler) }
جافا
File localFile = new File(context.getCacheDir(), "my_remote_model.tflite"); if (localFile.exists()) { initializeLabeler(localFile); } else { showLoadingUI(); FirebaseStorage storage = FirebaseStorage.getInstance(); StorageReference modelRef = storage.getReferenceFromUrl("gs://YOUR_BUCKET/path/to/model.tflite"); modelRef.getFile(localFile) .addOnSuccessListener(new OnSuccessListener<FileDownloadTask.TaskSnapshot>() { @Override public void onSuccess(FileDownloadTask.TaskSnapshot taskSnapshot) { hideLoadingUI(); initializeLabeler(localFile); } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception exception) { showErrorUI(); } }); } private void initializeLabeler(File modelFile) { LocalModel localModel = new LocalModel.Builder().setAbsoluteFilePath(modelFile.getAbsolutePath()).build(); CustomImageLabelerOptions options = new CustomImageLabelerOptions.Builder(localModel).build(); ImageLabeler labeler = ImageLabeling.getClient(options); enableMLFeatures(labeler); }
2. تجهيز الصورة المصدر
بعد ذلك، أنشئInputImage
كائنًا من صورتك لكل صورة تريد تصنيفها. يعمل مصنّف الصور بأسرع ما يمكن عند استخدام Bitmap
أو YUV_420_888 media.Image إذا كنت تستخدم Camera2 API، وهما
الخياران اللذان يُنصح بهما عند الإمكان.
يمكنك إنشاء عنصر InputImage من مصادر مختلفة، ويتم توضيح كل مصدر أدناه.
استخدام media.Image
لإنشاء عنصر InputImage من عنصر media.Image، مثلاً عند التقاط صورة من كاميرا جهاز، مرِّر العنصر media.Image ودوران الصورة إلى InputImage.fromMediaImage().
إذا كنت تستخدم مكتبة
CameraX، سيحسب لك الفئتان OnImageCapturedListener وImageAnalysis.Analyzer قيمة التدوير.
Kotlin
private class YourImageAnalyzer : ImageAnalysis.Analyzer { override fun analyze(imageProxy: ImageProxy) { val mediaImage = imageProxy.image if (mediaImage != null) { val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) // Pass image to an ML Kit Vision API // ... } } }
جافا
private class YourAnalyzer implements ImageAnalysis.Analyzer { @Override public void analyze(ImageProxy imageProxy) { Image mediaImage = imageProxy.getImage(); if (mediaImage != null) { InputImage image = InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees()); // Pass image to an ML Kit Vision API // ... } } }
إذا كنت لا تستخدم مكتبة كاميرا تمنحك درجة دوران الصورة، يمكنك احتسابها من درجة دوران الجهاز واتجاه مستشعر الكاميرا في الجهاز:
Kotlin
private val ORIENTATIONS = SparseIntArray() init { ORIENTATIONS.append(Surface.ROTATION_0, 0) ORIENTATIONS.append(Surface.ROTATION_90, 90) ORIENTATIONS.append(Surface.ROTATION_180, 180) ORIENTATIONS.append(Surface.ROTATION_270, 270) } /** * Get the angle by which an image must be rotated given the device's current * orientation. */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Throws(CameraAccessException::class) private fun getRotationCompensation(cameraId: String, activity: Activity, isFrontFacing: Boolean): Int { // Get the device's current rotation relative to its "native" orientation. // Then, from the ORIENTATIONS table, look up the angle the image must be // rotated to compensate for the device's rotation. val deviceRotation = activity.windowManager.defaultDisplay.rotation var rotationCompensation = ORIENTATIONS.get(deviceRotation) // Get the device's sensor orientation. val cameraManager = activity.getSystemService(CAMERA_SERVICE) as CameraManager val sensorOrientation = cameraManager .getCameraCharacteristics(cameraId) .get(CameraCharacteristics.SENSOR_ORIENTATION)!! if (isFrontFacing) { rotationCompensation = (sensorOrientation + rotationCompensation) % 360 } else { // back-facing rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360 } return rotationCompensation }
جافا
private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); static { ORIENTATIONS.append(Surface.ROTATION_0, 0); ORIENTATIONS.append(Surface.ROTATION_90, 90); ORIENTATIONS.append(Surface.ROTATION_180, 180); ORIENTATIONS.append(Surface.ROTATION_270, 270); } /** * Get the angle by which an image must be rotated given the device's current * orientation. */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private int getRotationCompensation(String cameraId, Activity activity, boolean isFrontFacing) throws CameraAccessException { // Get the device's current rotation relative to its "native" orientation. // Then, from the ORIENTATIONS table, look up the angle the image must be // rotated to compensate for the device's rotation. int deviceRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); int rotationCompensation = ORIENTATIONS.get(deviceRotation); // Get the device's sensor orientation. CameraManager cameraManager = (CameraManager) activity.getSystemService(CAMERA_SERVICE); int sensorOrientation = cameraManager .getCameraCharacteristics(cameraId) .get(CameraCharacteristics.SENSOR_ORIENTATION); if (isFrontFacing) { rotationCompensation = (sensorOrientation + rotationCompensation) % 360; } else { // back-facing rotationCompensation = (sensorOrientation - rotationCompensation + 360) % 360; } return rotationCompensation; }
بعد ذلك، مرِّر العنصر media.Image وقيمة درجة التدوير إلى InputImage.fromMediaImage():
Kotlin
val image = InputImage.fromMediaImage(mediaImage, rotation)
Java
InputImage image = InputImage.fromMediaImage(mediaImage, rotation);
استخدام عنوان URI للملف
لإنشاء عنصر InputImage من معرّف موارد منتظم (URI) لملف، مرِّر سياق التطبيق ومعرّف الموارد المنتظم (URI) للملف إلى InputImage.fromFilePath(). يكون ذلك مفيدًا عند استخدام غرض ACTION_GET_CONTENT لطلب أن يختار المستخدم صورة من تطبيق معرض الصور.
Kotlin
val image: InputImage try { image = InputImage.fromFilePath(context, uri) } catch (e: IOException) { e.printStackTrace() }
Java
InputImage image; try { image = InputImage.fromFilePath(context, uri); } catch (IOException e) { e.printStackTrace(); }
استخدام ByteBuffer أو ByteArray
لإنشاء عنصر InputImage من ByteBuffer أو ByteArray، عليك أولاً حساب درجة دوران الصورة كما سبق أن شرحنا في ما يخص إدخال media.Image.
بعد ذلك، أنشئ عنصر InputImage باستخدام المخزن المؤقت أو المصفوفة، بالإضافة إلى ارتفاع الصورة وعرضها وتنسيق ترميز الألوان ودرجة التدوير:
Kotlin
val image = InputImage.fromByteBuffer( byteBuffer, /* image width */ 480, /* image height */ 360, rotationDegrees, InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12 ) // Or: val image = InputImage.fromByteArray( byteArray, /* image width */ 480, /* image height */ 360, rotationDegrees, InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12 )
جافا
InputImage image = InputImage.fromByteBuffer(byteBuffer, /* image width */ 480, /* image height */ 360, rotationDegrees, InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12 ); // Or: InputImage image = InputImage.fromByteArray( byteArray, /* image width */480, /* image height */360, rotation, InputImage.IMAGE_FORMAT_NV21 // or IMAGE_FORMAT_YV12 );
استخدام Bitmap
لإنشاء كائن InputImage
من كائن Bitmap، عليك إجراء التصريح التالي:
Kotlin
val image = InputImage.fromBitmap(bitmap, 0)
Java
InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);
يتم تمثيل الصورة باستخدام عنصر Bitmap مع درجات التدوير.
3- تشغيل أداة تصنيف الصور
لتصنيف العناصر في صورة، مرِّر العنصر image إلى طريقة ImageLabelerprocess().
Kotlin
labeler.process(image) .addOnSuccessListener { labels -> // Task completed successfully // ... } .addOnFailureListener { e -> // Task failed with an exception // ... }
جافا
labeler.process(image) .addOnSuccessListener(new OnSuccessListener<List<ImageLabel>>() { @Override public void onSuccess(List<ImageLabel> labels) { // Task completed successfully // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });
4. الحصول على معلومات عن الكيانات المصنَّفة
في حال نجاح عملية تصنيف الصور، يتم تمرير قائمةImageLabel
بالكائنات إلى أداة معالجة النجاح. يمثّل كل عنصر ImageLabel شيئًا تم تصنيفه في الصورة. يمكنك الحصول على وصف نصي لكل تصنيف (إذا كان متاحًا في البيانات الوصفية لملف نموذج LiteRT) ودرجة الثقة والفهرس. على سبيل المثال:
Kotlin
for (label in labels) { val text = label.text val confidence = label.confidence val index = label.index }
جافا
for (ImageLabel label : labels) { String text = label.getText(); float confidence = label.getConfidence(); int index = label.getIndex(); }
نصائح لتحسين الأداء في الوقت الفعلي
إذا كنت تريد تصنيف الصور في تطبيق يعمل في الوقت الفعلي، اتّبِع الإرشادات التالية لتحقيق أفضل معدلات عرض اللقطات:
- إذا كنت تستخدم واجهة برمجة التطبيقات
Cameraأوcamera2، عليك تقليل عدد الطلبات إلى أداة تصنيف الصور. وإذا توفّر إطار فيديو جديد أثناء تشغيل أداة تصنيف الصور، عليك تجاهل الإطار. يمكنك الاطّلاع على الفئةVisionProcessorBaseفي تطبيق العيّنة السريع البدء للحصول على مثال. - إذا كنت تستخدم واجهة برمجة التطبيقات
CameraX، تأكَّد من ضبط استراتيجية الضغط الخلفي على القيمة التلقائيةImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. يضمن ذلك تسليم صورة واحدة فقط لتحليلها في كل مرة. إذا تم إنتاج المزيد من الصور عندما يكون المحلّل مشغولاً، سيتم إسقاطها تلقائيًا ولن يتم وضعها في قائمة انتظار التسليم. بعد إغلاق الصورة التي يتم تحليلها من خلال استدعاء ImageProxy.close()، سيتم عرض أحدث صورة تالية. - إذا كنت تستخدم ناتج أداة تصنيف الصور لتراكب الرسومات على صورة الإدخال، احصل أولاً على النتيجة من حزمة تعلّم الآلة، ثم اعرض الصورة والتراكب في خطوة واحدة. يتم عرض هذا الإطار على مساحة العرض مرة واحدة فقط لكل إطار إدخال. يمكنك الاطّلاع على الفئتين
CameraSourcePreviewوGraphicOverlayفي نموذج تطبيق البدء السريع للحصول على مثال. - إذا كنت تستخدم Camera2 API، التقط الصور بتنسيق
ImageFormat.YUV_420_888. إذا كنت تستخدم Camera API القديمة، التقط الصور بتنسيقImageFormat.NV21.