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

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

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

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

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

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

النموذج المجمّع النموذج المستضاف
هذا النموذج هو جزء من حزمة APK لتطبيقك، ما يزيد من حجمه. الطراز ليس جزءًا من حزمة APK. وتتم استضافتها من خلال تحميل المحتوى إلى تعلُّم الآلة في Firebase.
يتوفّر الطراز على الفور، حتى عندما يكون جهاز Android غير متصل بالإنترنت. يتم تنزيل النموذج عند الطلب.
لا حاجة إلى مشروع على Firebase يتطلب مشروع Firebase
يجب إعادة نشر تطبيقك لتعديل النموذج. إرسال تحديثات النماذج بدون إعادة نشر تطبيقك
لا يوجد اختبار A/B المدمج اختبار 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>
    

    يمكنك أيضًا التحقّق صراحةً من مدى توفّر مسار التعلّم وطلب تنزيل التطبيقات من خلال UnitInstallClient API في خدمات 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/، ثم النقر على New > Folder >Asset Folder.)

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

    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().

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

3- تشغيل تصنيف الصورة

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

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