تصنيف الصور باستخدام حزمة تعلّم الآلة على نظام التشغيل Android

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

يمكنك استخدام حزمة تعلّم الآلة لتصنيف العناصر التي تم التعرّف عليها في صورة. ويتوافق النموذج التلقائي المتوفّر مع حزمة تعلّم الآلة على أكثر من 400 تصنيف مختلف.

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

الميزةغير مجمعةمجمعة
التنفيذيتم تنزيل النموذج بشكل ديناميكي من خلال خدمات Google Play.ويكون النموذج مرتبطًا بشكل ثابت في وقت الإنشاء.
حجم التطبيقحوالي 700 كيلوبايت زيادة في الحجم.زيادة حجم الملف عن 7.3 ميغابايت تقريبًا
وقت الإعدادوقد نضطر إلى الانتظار حتى يتم تنزيل النموذج قبل الاستخدام الأول.يتوفّر الطراز على الفور.
  • يمكنك استخدام نموذج التطبيق للاطّلاع على مثال عن استخدام واجهة برمجة التطبيقات هذه.

قبل البدء

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

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

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

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

    لاستخدام النموذج في "خدمات Google Play"، يُرجى اتّباع الخطوات التالية:

    dependencies {
      // ...
      // Use this dependency to use the dynamically downloaded model in Google Play Services
      implementation 'com.google.android.gms:play-services-mlkit-image-labeling:16.0.8'
    }
    
  3. إذا اخترت استخدام النموذج في "خدمات Google Play"، يمكنك ضبط تطبيقك لتنزيل النموذج تلقائيًا على الجهاز بعد تثبيت تطبيقك من "متجر Play". ولإجراء ذلك، أضِف البيان التالي إلى ملف التطبيق AndroidManifest.xml الخاص بتطبيقك:

    <application ...>
          ...
          <meta-data
              android:name="com.google.mlkit.vision.DEPENDENCIES"
              android:value="ica" >
          <!-- To use multiple models: android:value="ica,model2,model3" -->
    </application>
    

    يمكنك أيضًا التحقّق صراحةً من مدى توفّر النموذج وطلب التنزيل من خلال خدمات Google Play UnitInstallClient API.

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

يمكنك الآن تصنيف الصور.

1- إعداد صورة الإدخال

أنشِئ عنصر InputImage من صورتك. يعمل تصنيف الصورة بشكل أسرع عند استخدام Bitmap، أو إذا كنت تستخدم camera2 API، يُعد YUV_420_888 media.Image، ويُنصح باستخدامه متى أمكن.

يمكنك إنشاء كائن InputImage من مصادر مختلفة، وسيتم توضيح كل منها في ما يلي.

باستخدام media.Image

لإنشاء عنصر InputImage من كائن media.Image، مثلاً عند التقاط صورة من كاميرا الجهاز، مرِّر الكائن media.Image وتدوير الصورة إلى InputImage.fromMediaImage().

إذا كنت تستخدم مكتبة XX، تحسب الفئتان 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 جنبًا إلى جنب مع درجات التدوير.

2. ضبط تصنيف الصورة وتشغيله

لتصنيف العناصر في صورة، ما عليك سوى تمرير الكائن InputImage إلى طريقة process ImageLabeler's.

  1. أولاً، احصل على مثال من ImageLabeler.

    إذا كنت تريد استخدام تصنيف الصور على الجهاز فقط، عليك الإعلان التالي:

Kotlin

// To use default options:
val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)

// Or, to set the minimum confidence required:
// val options = ImageLabelerOptions.Builder()
//     .setConfidenceThreshold(0.7f)
//     .build()
// val labeler = ImageLabeling.getClient(options)

Java

// To use default options:
ImageLabeler labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS);

// Or, to set the minimum confidence required:
// ImageLabelerOptions options =
//     new ImageLabelerOptions.Builder()
//         .setConfidenceThreshold(0.7f)
//         .build();
// ImageLabeler labeler = ImageLabeling.getClient(options);
  1. بعد ذلك، مرِّر الصورة إلى طريقة 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
                // ...
            }
        });

3- الحصول على معلومات حول العناصر التي تم تصنيفها

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

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