مسح الرموز الشريطية ضوئيًا باستخدام حزمة تعلّم الآلة على نظام التشغيل Android

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

يمكنك استخدام حزمة تعلّم الآلة للتعرّف على الرموز الشريطية وفك ترميزها.

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

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

قبل البدء

  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:barcode-scanning:17.0.2'
    }
    

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

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

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

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

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

إرشادات الصور المدخلة

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

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

    على سبيل المثال، تتكوّن الرموز الشريطية EAN-13 من أشرطة ومساحات يبلغ حجمها 1 أو 2 أو 3 أو 4 وحدات، لذا يجب أن تتضمّن الصورة الشريطية EAN-13 أشرطة ومساحات لا يزيد عرضها عن 2 و4 و6 و8 بكسل. بما أنّ الرمز الشريطي EAN-13 هو إجمالي عرض 95 وحدة، يجب ألا يقلّ عرض الرمز الشريطي عن 190 بكسل.

    تحتاج تنسيقات كثافة أكبر، مثل PDF417، إلى أبعاد بكسل أكبر لحزمة تعلّم الآلة للقراءة بشكل موثوق. على سبيل المثال، يمكن أن يحتوي رمز PDF417 على ما يصل إلى 34 وحدة مقاس 17 وحدة "word" في صف واحد، ومن المفضّل أن يكون عرضه 1156 بكسل على الأقل.

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

  • بالنسبة إلى التطبيقات النموذجية، يُنصح بتوفير صورة أعلى دقة، مثل 1280×720 أو 1920×1080، ما يجعل الرموز الشريطية قابلة للمسح من مسافة بعيدة عن الكاميرا.

    ويمكنك تحسين الأداء في التطبيقات من خلال التقاط الصور بدقة أقل، ولكن مع طلب رمز شريطي يمثّل معظم الصورة المستخدَمة. اطّلِع أيضًا على نصائح لتحسين الأداء في الوقت الفعلي.

1- إعداد الماسح الضوئي للرموز الشريطية

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

على سبيل المثال، لاكتشاف رمز Aztec ورموز الاستجابة السريعة فقط، يمكنك إنشاء كائن BarcodeScannerOptions كما في المثال التالي:

Kotlin

val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(
                Barcode.FORMAT_QR_CODE,
                Barcode.FORMAT_AZTEC)
        .build()

Java

BarcodeScannerOptions options =
        new BarcodeScannerOptions.Builder()
        .setBarcodeFormats(
                Barcode.FORMAT_QR_CODE,
                Barcode.FORMAT_AZTEC)
        .build();

تتوفّر التنسيقات التالية:

  • الرمز 128 (FORMAT_CODE_128)
  • الرمز 39 (FORMAT_CODE_39)
  • الرمز 93 (FORMAT_CODE_93)
  • كودابار (FORMAT_CODABAR)
  • رقم EAN-13 (FORMAT_EAN_13)
  • رقم EAN-8 (FORMAT_EAN_8)
  • ITF (FORMAT_ITF)
  • الرمز العالمي للمنتج (UPC) (FORMAT_UPC_A)
  • الرمز العالمي للمنتج (UPC) (FORMAT_UPC_E)
  • رمز الاستجابة السريعة (FORMAT_QR_CODE)
  • PDF417 (FORMAT_PDF417)
  • الأزتيك (FORMAT_AZTEC)
  • مصفوفة البيانات (FORMAT_DATA_MATRIX)

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

للتعرّف على الرموز الشريطية في صورة، يمكنك إنشاء كائن InputImage إما من Bitmap أو media.Image أو ByteBuffer أو مصفوفة بايت أو ملف على الجهاز. بعد ذلك، يمكنك تمرير الكائن InputImage إلى طريقة process BarcodeScanner's.

يمكنك إنشاء كائن 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- الحصول على نسخة افتراضية من BarcodeScanner

Kotlin

val scanner = BarcodeScanning.getClient()
// Or, to specify the formats to recognize:
// val scanner = BarcodeScanning.getClient(options)

Java

BarcodeScanner scanner = BarcodeScanning.getClient();
// Or, to specify the formats to recognize:
// BarcodeScanner scanner = BarcodeScanning.getClient(options);

4. معالجة الصورة

تمرير الصورة إلى الطريقة process:

Kotlin

val result = scanner.process(image)
        .addOnSuccessListener { barcodes ->
            // Task completed successfully
            // ...
        }
        .addOnFailureListener {
            // Task failed with an exception
            // ...
        }

Java

Task<List<Barcode>> result = scanner.process(image)
        .addOnSuccessListener(new OnSuccessListener<List<Barcode>>() {
            @Override
            public void onSuccess(List<Barcode> barcodes) {
                // Task completed successfully
                // ...
            }
        })
        .addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // Task failed with an exception
                // ...
            }
        });

5. الحصول على معلومات من الرموز الشريطية

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

مثلاً:

Kotlin

for (barcode in barcodes) {
    val bounds = barcode.boundingBox
    val corners = barcode.cornerPoints

    val rawValue = barcode.rawValue

    val valueType = barcode.valueType
    // See API reference for complete list of supported types
    when (valueType) {
        Barcode.TYPE_WIFI -> {
            val ssid = barcode.wifi!!.ssid
            val password = barcode.wifi!!.password
            val type = barcode.wifi!!.encryptionType
        }
        Barcode.TYPE_URL -> {
            val title = barcode.url!!.title
            val url = barcode.url!!.url
        }
    }
}

Java

for (Barcode barcode: barcodes) {
    Rect bounds = barcode.getBoundingBox();
    Point[] corners = barcode.getCornerPoints();

    String rawValue = barcode.getRawValue();

    int valueType = barcode.getValueType();
    // See API reference for complete list of supported types
    switch (valueType) {
        case Barcode.TYPE_WIFI:
            String ssid = barcode.getWifi().getSsid();
            String password = barcode.getWifi().getPassword();
            int type = barcode.getWifi().getEncryptionType();
            break;
        case Barcode.TYPE_URL:
            String title = barcode.getUrl().getTitle();
            String url = barcode.getUrl().getUrl();
            break;
    }
}

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

إذا أردت مسح الرموز الشريطية ضوئيًا في الوقت الفعلي، اتّبِع الإرشادات التالية للحصول على أفضل معدّلات عرض الإطارات:

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

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

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

    رقم المجموع الاختباري غير متوافق مع ITF وCODE-39.

  • إذا كنت تستخدم Camera أو واجهة برمجة التطبيقات camera2، يمكنك تقييد الاتصالات إلى أداة الرصد. وفي حال توفّر إطار فيديو جديد أثناء تشغيل أداة الرصد، أفلِت الإطار. يمكنك الاطّلاع على السمة VisionProcessorBase في نموذج البدء السريع للحصول على مثال.
  • إذا كنت تستخدم واجهة برمجة تطبيقات CameraX، تأكد من ضبط استراتيجية الضغط على القيمة التلقائية ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. وهذا يضمن تسليم صورة واحدة فقط للتحليل في كل مرة. وإذا تم إنشاء المزيد من الصور عندما تكون أداة التحليل مشغولة، سيتم إدراجها تلقائيًا ولن يتم وضعها في قائمة الانتظار للتسليم. بعد أن يتم إغلاق الصورة التي يتم تحليلها من خلال استدعاء ImageProxy.close()، سيتم تسليم الصورة التالية التالية.
  • إذا كنت تستخدم ناتج أداة الكشف لتراكب الرسومات على الصورة المُدخلة، عليك أولاً الحصول على النتيجة من حزمة تعلّم الآلة، ثم عرض الصورة والتراكب على خطوة واحدة. ويتم العرض على مساحة العرض مرة واحدة فقط لكل إطار إدخال. يمكنك الاطّلاع على السمتَين CameraSourcePreview و GraphicOverlay في نموذج التطبيق السريع للبدء كمثال.
  • إذا كنت تستخدم واجهة برمجة التطبيقات للكاميرا 2، التقِط صورًا بتنسيق ImageFormat.YUV_420_888. إذا كنت تستخدم واجهة برمجة تطبيقات الكاميرا القديمة، التقِط صورًا بتنسيق ImageFormat.NV21.