تصنيف الصور بنموذج مدرّب تلقائيًا على 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 API ، فستعمل media.Image YUV_420_888.

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

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