تصنيف الصور باستخدام نموذج مخصص على Android

يمكنك استخدام حزمة تعلّم الآلة للتعرّف على الكيانات في صورة وتصنيفها. تتوافق واجهة برمجة التطبيقات هذه مع مجموعة كبيرة من نماذج تصنيف الصور المخصّصة. راجِع النماذج المخصّصة باستخدام حزمة تعلّم الآلة للحصول على إرشادات حول متطلبات توافق النماذج ومكان العثور على النماذج المدرَّبة مسبقًا وكيفية تدريب نماذجك الخاصة.

هناك طريقتان لدمج ميزة تصنيف الصور مع النماذج المخصّصة: من خلال تجميع مسار التعلّم كجزء من تطبيقك، أو من خلال استخدام مسار تعلّم غير مجمَّع يعتمد على "خدمات Google Play". إذا اخترت مسار التعلّم غير المجمَّع، سيكون حجم تطبيقك أصغر. راجِع الجدول التالي للحصول على التفاصيل.

مُجمَّعةغير مجمّعة
اسم المكتبةcom.google.mlkit:image-labeling-customcom.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

للتجربة:

قبل البدء

  1. في ملف build.gradle.kts على مستوى المشروع، تأكَّد من تضمين مستودع Maven من Google في كل من القسمَين buildscript وallprojects.

  2. أضِف العناصر التابعة لحزمة تعلّم الآلة على 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")
    }
    
  3. في حال اختيار استخدام مسار العرض في "خدمات 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".

    إذا لم تفعِّل عمليات تنزيل مسار البيانات في وقت التثبيت أو لم تطلب تنزيلًا صريحًا، سيتم تنزيل مسار البيانات عند تشغيل أداة وضع التصنيفات للمرة الأولى. لن يتم عرض أي نتائج للطلبات التي تُجريها قبل اكتمال عملية التنزيل.

  4. إذا أردت تنزيل نموذج باستخدام مساحة تخزين سحابية لـ Firebase، تأكَّد من إضافة Firebase إلى مشروع Android، إذا لم يسبق لك إجراء ذلك. لا يكون ذلك مطلوبًا عند تجميع النموذج.

1. تحميل النموذج

يمكنك تحميل النموذج من مصدر مجمّع محليًا أو مصدر مستضاف عن بُعد.

ضبط مصدر نموذج محلي

لتضمين النموذج في تطبيقك، اتّبِع الخطوات التالية:

  1. انسخ ملف النموذج (الذي ينتهي عادةً بـ .tflite أو .lite) إلى مجلد assets/ في تطبيقك. (قد تحتاج إلى إنشاء المجلد أولاً من خلال النقر بزر الماوس الأيمن على مجلد app/، ثم النقر على جديد > مجلد > مجلد مواد العرض).

  2. أنشئ الكائن 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.