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

تنظيم صفحاتك في مجموعات يمكنك حفظ المحتوى وتصنيفه حسب إعداداتك المفضّلة.

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

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

مجمعةغير مجمعة
اسم المكتبةcom.google.mlkit:image-labeling-customcom.google.android.gms:play-services-mlkit-image-labeling-custom
التنفيذيرتبط Pipeline بشكل ثابت بتطبيقك في وقت الإنشاء.يتم تنزيل Pipeline ديناميكيًا من خلال "خدمات Google Play".
حجم التطبيقزيادة في الحجم بمقدار 3.8 ميغابايت تقريبًا.زيادة حجم الصورة حوالي 200 كيلوبايت.
وقت الإعداديتوفّر مسار الإجراءات فورًا.قد يكون عليك انتظار تنزيل المسار قبل الاستخدام لأول مرة.
مرحلة مراحل نشاط واجهة برمجة التطبيقاتمدى التوفّر للجمهور العام (GA)إصدار تجريبي

هناك طريقتان لدمج نموذج مخصص: يمكنك تجميع النموذج من خلال وضعه داخل مجلد مواد العرض لتطبيقك، أو تنزيله ديناميكيًا من Firebase. يقارن الجدول التالي بين هذين الخيارين.

نموذج مجمّع نموذج مستضاف
النموذج هو جزء من حِزمة APK لتطبيقك، ما يؤدي إلى زيادة حجمه. النموذج ليس جزءًا من ملف APK. تتم استضافتها من خلال تحميل المحتوى إلى Firebase Machine Learning.
يتوفّر النموذج على الفور حتى إذا كان جهاز Android غير متصل بالإنترنت. يتم تنزيل النموذج عند الطلب
لا حاجة لمشروع Firebase مطلوب مشروع Firebase
يجب إعادة نشر تطبيقك لتحديث النموذج نشر تحديثات النموذج بدون إعادة نشر تطبيقك
لا يوجد اختبار A/B مدمج إجراء اختبار أ/ب بسهولة باستخدام ميزة الإعداد عن بُعد في Firebase

تجربة السمات والبيانات

قبل البدء

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

  2. أضِف التبعيات لمكتبات ML Kit في Android إلى ملف الدرجات على مستوى التطبيق في وحدتك، والذي عادة ما يكون app/build.gradle. اختر إحدى التبعيات التالية وفقًا لاحتياجاتك:

    لربط عملية المعالجة بتطبيقك:

    dependencies {
      // ...
      // Use this dependency to bundle the pipeline with your app
      implementation 'com.google.mlkit:image-labeling-custom:17.0.1'
    }
    

    لاستخدام مسار التعلّم في "خدمات 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-beta4'
    }
    
  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 في خدمات Google Play.

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

  4. أضف التبعية linkFirebase إذا كنت تريد تنزيل نموذج من Firebase ديناميكيًا:

    لتنزيل نموذج من Firebase ديناميكيًا، يمكنك إضافة ملحق linkFirebase:

    dependencies {
      // ...
      // Image labeling feature with model downloaded from Firebase
      implementation 'com.google.mlkit:image-labeling-custom:17.0.1'
      // Or use the dynamically downloaded pipeline in Google Play Services
      // implementation 'com.google.android.gms:play-services-mlkit-image-labeling-custom:16.0.0-beta4'
      implementation 'com.google.mlkit:linkfirebase:17.0.0'
    }
    
  5. إذا كنت ترغب في تنزيل نموذج، فتأكد من إضافة Firebase إلى مشروع Android، إذا لم تكن قد فعلت ذلك، فهذا غير مطلوب عند تجميع النموذج.

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

تهيئة مصدر نموذج محلي

لتجميع النموذج مع تطبيقك:

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

  2. بعد ذلك، أضِف ما يلي إلى ملف build.gradle لتطبيقك لضمان عدم ضغط Gradle ملف النموذج عند إنشاء التطبيق:

    android {
        // ...
        aaptOptions {
            noCompress "tflite"
            // or noCompress "lite"
        }
    }
    

    سيتم تضمين ملف النموذج في حزمة التطبيق وسيتوفر لـ ML Kit باعتباره مادة عرض أولية.

  3. إنشاء كائن LocalModel، مع تحديد المسار إلى ملف النموذج:

    Kotlin

    val localModel = LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute file path to model file)
            // or .setUri(URI to model file)
            .build()

    لغة Java

    LocalModel localModel =
        new LocalModel.Builder()
            .setAssetFilePath("model.tflite")
            // or .setAbsoluteFilePath(absolute file path to model file)
            // or .setUri(URI to model file)
            .build();

إعداد مصدر نموذج مستضاف على Firebase

لاستخدام النموذج المستضاف عن بُعد، أنشئ كائن RemoteModel قبل FirebaseModelSource، مع تحديد الاسم الذي عينته للنموذج عند نشره:

Kotlin

// Specify the name you assigned in the Firebase console.
val remoteModel =
    CustomRemoteModel
        .Builder(FirebaseModelSource.Builder("your_model_name").build())
        .build()

لغة Java

// Specify the name you assigned in the Firebase console.
CustomRemoteModel remoteModel =
    new CustomRemoteModel
        .Builder(new FirebaseModelSource.Builder("your_model_name").build())
        .build();

بعد ذلك، ابدأ مهمة تنزيل النموذج، مع تحديد الشروط التي تريد السماح بالتنزيل بموجبها. إذا لم يكن النموذج على الجهاز، أو إذا كان إصدار أحدث من النموذج متاحًا، فسيتم تنزيل النموذج من Firebase بشكل غير متزامن:

Kotlin

val downloadConditions = DownloadConditions.Builder()
    .requireWifi()
    .build()
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
    .addOnSuccessListener {
        // Success.
    }

لغة Java

DownloadConditions downloadConditions = new DownloadConditions.Builder()
        .requireWifi()
        .build();
RemoteModelManager.getInstance().download(remoteModel, downloadConditions)
        .addOnSuccessListener(new OnSuccessListener() {
            @Override
            public void onSuccess(@NonNull Task task) {
                // Success.
            }
        });

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

تهيئة تصنيف الصورة

بعد ضبط مصادر النموذج، يمكنك إنشاء كائن ImageLabeler من أحد هذه المصادر.

تتوفّر الخيارات التالية:

الخيارات
confidenceThreshold

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

maxResultCount

الحد الأقصى لعدد التصنيفات المطلوب عرضها. وفي حال تم ترك هذه السياسة بدون ضبط، سيتم استخدام القيمة التلقائية وهي 10.

إذا كان لديك نموذج مجمّع محليًا فقط، ما عليك سوى إنشاء تصنيف من عنصر LocalModel:

Kotlin

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.5f)
    .setMaxResultCount(5)
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

لغة Java

CustomImageLabelerOptions customImageLabelerOptions =
        new CustomImageLabelerOptions.Builder(localModel)
            .setConfidenceThreshold(0.5f)
            .setMaxResultCount(5)
            .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

إذا كان لديك نموذج تتم استضافته عن بُعد، يجب عليك التحقق من أنه قد تم تنزيله قبل تشغيله. يمكنك التحقق من حالة مهمة تنزيل النموذج باستخدام طريقة مدير النموذج isModelDownloaded().

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

Kotlin

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
    val optionsBuilder =
        if (isDownloaded) {
            CustomImageLabelerOptions.Builder(remoteModel)
        } else {
            CustomImageLabelerOptions.Builder(localModel)
        }
    val options = optionsBuilder
                  .setConfidenceThreshold(0.5f)
                  .setMaxResultCount(5)
                  .build()
    val labeler = ImageLabeling.getClient(options)
}

لغة Java

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
        .addOnSuccessListener(new OnSuccessListener() {
            @Override
            public void onSuccess(Boolean isDownloaded) {
                CustomImageLabelerOptions.Builder optionsBuilder;
                if (isDownloaded) {
                    optionsBuilder = new CustomImageLabelerOptions.Builder(remoteModel);
                } else {
                    optionsBuilder = new CustomImageLabelerOptions.Builder(localModel);
                }
                CustomImageLabelerOptions options = optionsBuilder
                    .setConfidenceThreshold(0.5f)
                    .setMaxResultCount(5)
                    .build();
                ImageLabeler labeler = ImageLabeling.getClient(options);
            }
        });

إذا لم يكن لديك سوى نموذج تتم استضافته عن بُعد، يجب تعطيل الوظائف ذات الصلة بالنموذج، على سبيل المثال، الرمادي أو إخفاء جزء من واجهة المستخدم، إلى أن تؤكد أنه تم تنزيل النموذج. يمكنك تنفيذ ذلك من خلال إرفاق أداة معالجة بأسلوب download() في مدير النموذج:

Kotlin

RemoteModelManager.getInstance().download(remoteModel, conditions)
    .addOnSuccessListener {
        // Download complete. Depending on your app, you could enable the ML
        // feature, or switch from the local model to the remote model, etc.
    }

لغة Java

RemoteModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener() {
            @Override
            public void onSuccess(Void v) {
              // Download complete. Depending on your app, you could enable
              // the ML feature, or switch from the local model to the remote
              // model, etc.
            }
        });

2. إعداد صورة الإدخال

بعد ذلك، أنشِئ عنصرًا من الصورة في كل صورة تريد InputImage تصنيفها. يتم تشغيل تصنيف الصور بشكل أسرع عند استخدام Bitmap أو، إذا كنت تستخدم واجهة برمجة تطبيقات الكاميرا 2، فإن YUV_420_888 media.Image، والتي يوصى بها عند الإمكان.

يمكنك إنشاء كائن 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
            // ...
        }
    }
}

لغة Java

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
}

لغة Java

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
)

لغة Java

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 إلى طريقة ImageLabeler process().

Kotlin

labeler.process(image)
        .addOnSuccessListener { labels ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener { e ->
            // Task failed with an exception
            // ...
        }

لغة Java

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 ما تم تصنيفه في الصورة. يمكنك الحصول على وصف نص كل تصنيف (إذا كان متاحًا في البيانات الوصفية لملف نموذج TensorFlow Lite) ونتيجة الثقة والمؤشر. مثلاً:

Kotlin

for (label in labels) {
    val text = label.text
    val confidence = label.confidence
    val index = label.index
}

لغة Java

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()، سيتم تسليم الصورة التالية التالية.
  • إذا كنت تستخدم إخراج تصنيف الصور لتركيب الرسومات على الصورة المدخلة، عليك الحصول على النتيجة من ML Kit أولاً، ثم عرض الصورة والتراكب في خطوة واحدة. يتم عرض هذا المحتوى على سطح العرض مرة واحدة فقط لكل إطار إدخال. للاطّلاع على مثال، يمكنك الاطّلاع على الصفَّين CameraSourcePreview و GraphicOverlay في نموذج التطبيق السريع للبدء.
  • إذا كنت تستخدم Camera2 API، يمكنك التقاط الصور بتنسيق ImageFormat.YUV_420_888. إذا كنت تستخدم إصدارًا قديمًا من واجهة برمجة تطبيقات الكاميرا، التقِط الصور بتنسيق ImageFormat.NV21.