تصنيف الصور بنموذج مدرّب تلقائيًا على Android

بعد تدريب النموذج الخاص بك باستخدام AutoML Vision Edge ، يمكنك استخدامه في تطبيقك لتسمية الصور.

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

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

قبل ان تبدأ

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

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

    dependencies {
      // ...
      // Image labeling feature with bundled automl model
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
    }
    

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

    dependencies {
      // ...
      // Image labeling feature with automl model downloaded
      // from firebase
      implementation 'com.google.mlkit:image-labeling-custom:16.3.1'
      implementation 'com.google.mlkit:linkfirebase:16.1.0'
    }
    
  2. إذا كنت تريد تنزيل نموذج ، فتأكد من إضافة Firebase إلى مشروع Android الخاص بك ، إذا لم تكن قد قمت بذلك بالفعل. هذا غير مطلوب عند تجميع النموذج.

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

تكوين مصدر نموذج محلي

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

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

  2. قم بتضمين النموذج الخاص بك وملفات البيانات التعريفية الخاصة به في حزمة تطبيقك:

    1. إذا لم يكن لديك مجلد أصول في مشروعك، فقم بإنشاء واحد عن طريق النقر بزر الماوس الأيمن فوق app/ المجلد، ثم النقر فوق جديد > مجلد > مجلد الأصول .
    2. قم بإنشاء مجلد فرعي ضمن مجلد الأصول ليحتوي على ملفات النموذج.
    3. انسخ الملفات model.tflite و dict.txt و manifest.json إلى المجلد الفرعي (يجب أن تكون الملفات الثلاثة جميعها في نفس المجلد).
  3. أضف ما يلي إلى ملف build.gradle الخاص بالتطبيق الخاص بك للتأكد من أن Gradle لا يقوم بضغط ملف النموذج عند إنشاء التطبيق:

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

    سيتم تضمين ملف النموذج في حزمة التطبيق وسيكون متاحًا لـ ML Kit كأصل أولي.

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

    جافا

    AutoMLImageLabelerLocalModel localModel =
        new AutoMLImageLabelerLocalModel.Builder()
            .setAssetFilePath("manifest.json")
            // or .setAbsoluteFilePath(absolute file path to manifest file)
            .build();
    

    كوتلين

    val localModel = LocalModel.Builder()
        .setAssetManifestFilePath("manifest.json")
        // or .setAbsoluteManifestFilePath(absolute file path to manifest file)
        .build()
    

قم بتكوين مصدر نموذج مستضاف على Firebase

لاستخدام النموذج المستضاف عن بعد، قم بإنشاء كائن CustomRemoteModel ، مع تحديد الاسم الذي قمت بتعيينه للنموذج عند نشره:

جافا

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

كوتلين

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

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

جافا

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

كوتلين

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

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

قم بإنشاء ملصق صورة من النموذج الخاص بك

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

إذا كان لديك نموذج مُجمَّع محليًا فقط، فما عليك سوى إنشاء مُلصق من كائن CustomImageLabelerOptions الخاص بك وتكوين حد درجة الثقة الذي تريد طلبه (راجع تقييم النموذج الخاص بك ):

جافا

CustomImageLabelerOptions customImageLabelerOptions = new CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build();
ImageLabeler labeler = ImageLabeling.getClient(customImageLabelerOptions);

كوتلين

val customImageLabelerOptions = CustomImageLabelerOptions.Builder(localModel)
    .setConfidenceThreshold(0.0f)  // Evaluate your model in the Cloud console
                                   // to determine an appropriate value.
    .build()
val labeler = ImageLabeling.getClient(customImageLabelerOptions)

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

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

جافا

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
        .addOnSuccessListener(new OnSuccessListener<Boolean>() {
            @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.0f)  // Evaluate your model in the Cloud console
                                                       // to determine an appropriate threshold.
                        .build();

                ImageLabeler labeler = ImageLabeling.getClient(options);
            }
        });

كوتلين

RemoteModelManager.getInstance().isModelDownloaded(remoteModel)
    .addOnSuccessListener { isDownloaded ->
        val optionsBuilder =
            if (isDownloaded) {
                CustomImageLabelerOptions.Builder(remoteModel)
            } else {
                CustomImageLabelerOptions.Builder(localModel)
            }
        // Evaluate your model in the Cloud console to determine an appropriate threshold.
        val options = optionsBuilder.setConfidenceThreshold(0.0f).build()
        val labeler = ImageLabeling.getClient(options)
}

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

جافا

RemoteModelManager.getInstance().download(remoteModel, conditions)
        .addOnSuccessListener(new OnSuccessListener<Void>() {
            @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.
            }
        });

كوتلين

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.
    }

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

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

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

باستخدام media.Image

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

إذا كنت تستخدم مكتبة CameraX ، فستقوم فئتا OnImageCapturedListener و ImageAnalysis.Analyzer بحساب قيمة التدوير نيابةً عنك.

Kotlin+KTX

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) {
        if (imageProxy == null || imageProxy.getImage() == null) {
            return;
        }
        Image mediaImage = imageProxy.getImage();
        InputImage image =
                InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees);
        // Pass image to an ML Kit Vision API
        // ...
    }
}

إذا كنت لا تستخدم مكتبة الكاميرا التي تعطيك درجة دوران الصورة، يمكنك حسابها من درجة دوران الجهاز واتجاه مستشعر الكاميرا في الجهاز:

Kotlin+KTX

private val ORIENTATIONS = SparseIntArray()

init {
    ORIENTATIONS.append(Surface.ROTATION_0, 90)
    ORIENTATIONS.append(Surface.ROTATION_90, 0)
    ORIENTATIONS.append(Surface.ROTATION_180, 270)
    ORIENTATIONS.append(Surface.ROTATION_270, 180)
}
/**
 * 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, context: Context): 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)

    // On most devices, the sensor orientation is 90 degrees, but for some
    // devices it is 270 degrees. For devices with a sensor orientation of
    // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
    val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
    val sensorOrientation = cameraManager
        .getCameraCharacteristics(cameraId)
        .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
    rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360

    // Return the corresponding FirebaseVisionImageMetadata rotation value.
    val result: Int
    when (rotationCompensation) {
        0 -> result = FirebaseVisionImageMetadata.ROTATION_0
        90 -> result = FirebaseVisionImageMetadata.ROTATION_90
        180 -> result = FirebaseVisionImageMetadata.ROTATION_180
        270 -> result = FirebaseVisionImageMetadata.ROTATION_270
        else -> {
            result = FirebaseVisionImageMetadata.ROTATION_0
            Log.e(TAG, "Bad rotation value: $rotationCompensation")
        }
    }
    return result
}

Java

private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
    ORIENTATIONS.append(Surface.ROTATION_0, 90);
    ORIENTATIONS.append(Surface.ROTATION_90, 0);
    ORIENTATIONS.append(Surface.ROTATION_180, 270);
    ORIENTATIONS.append(Surface.ROTATION_270, 180);
}

/**
 * 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, Context context)
        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);

    // On most devices, the sensor orientation is 90 degrees, but for some
    // devices it is 270 degrees. For devices with a sensor orientation of
    // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
    CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
    int sensorOrientation = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION);
    rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;

    // Return the corresponding FirebaseVisionImageMetadata rotation value.
    int result;
    switch (rotationCompensation) {
        case 0:
            result = FirebaseVisionImageMetadata.ROTATION_0;
            break;
        case 90:
            result = FirebaseVisionImageMetadata.ROTATION_90;
            break;
        case 180:
            result = FirebaseVisionImageMetadata.ROTATION_180;
            break;
        case 270:
            result = FirebaseVisionImageMetadata.ROTATION_270;
            break;
        default:
            result = FirebaseVisionImageMetadata.ROTATION_0;
            Log.e(TAG, "Bad rotation value: " + rotationCompensation);
    }
    return result;
}

بعد ذلك، قم بتمرير كائن media.Image وقيمة درجة التدوير إلى InputImage.fromMediaImage() :

Kotlin+KTX

val image = InputImage.fromMediaImage(mediaImage, rotation)

Java

InputImage image = InputImage.fromMediaImage(mediaImage, rotation);

باستخدام ملف URI

لإنشاء كائن InputImage من ملف URI، قم بتمرير سياق التطبيق وملف URI إلى InputImage.fromFilePath() . يعد هذا مفيدًا عند استخدام هدف ACTION_GET_CONTENT لمطالبة المستخدم بتحديد صورة من تطبيق المعرض الخاص به.

Kotlin+KTX

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+KTX

val image = InputImage.fromByteBuffer(
        byteBuffer,
        /* 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
);

باستخدام Bitmap

لإنشاء كائن InputImage من كائن Bitmap ، قم بالإعلان التالي:

Kotlin+KTX

val image = InputImage.fromBitmap(bitmap, 0)

Java

InputImage image = InputImage.fromBitmap(bitmap, rotationDegree);

يتم تمثيل الصورة بواسطة كائن Bitmap مع درجات التدوير.

3. قم بتشغيل أداة تسمية الصورة

لتسمية كائنات في صورة ما، قم بتمرير كائن image إلى طريقة process() الخاصة بـ ImageLabeler .

جافا

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
                // ...
            }
        });

كوتلين

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

4. احصل على معلومات حول الكائنات ذات العلامات

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

جافا

for (ImageLabel label : labels) {
    String text = label.getText();
    float confidence = label.getConfidence();
    int index = label.getIndex();
}

كوتلين

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

نصائح لتحسين الأداء في الوقت الحقيقي

إذا كنت تريد تصنيف الصور في تطبيق في الوقت الفعلي، فاتبع هذه الإرشادات لتحقيق أفضل معدلات الإطارات:

  • خنق المكالمات إلى ملصق الصورة. إذا أصبح إطار فيديو جديد متاحًا أثناء تشغيل أداة تسمية الصورة، فقم بإسقاط الإطار. راجع فئة VisionProcessorBase في نموذج تطبيق التشغيل السريع للحصول على مثال.
  • إذا كنت تستخدم مخرجات أداة تسمية الصور لتراكب الرسومات على الصورة المدخلة، فاحصل أولاً على النتيجة، ثم اعرض الصورة والتراكب في خطوة واحدة. من خلال القيام بذلك، يمكنك العرض على سطح العرض مرة واحدة فقط لكل إطار إدخال. راجع فئتي CameraSourcePreview و GraphicOverlay في نموذج التطبيق السريع للحصول على مثال.
  • إذا كنت تستخدم Camera2 API، فالتقط الصور بتنسيق ImageFormat.YUV_420_888 .

    إذا كنت تستخدم واجهة برمجة تطبيقات الكاميرا الأقدم، فالتقط الصور بتنسيق ImageFormat.NV21 .