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

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

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

جرّبه الآن

قبل البدء

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

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

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

    dependencies {
      // ...
      // Use this dependency to bundle the model with your app
      implementation 'com.google.mlkit:barcode-scanning:17.3.0'
    }
    

    لاستخدام النموذج في "خدمات 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.3.1'
    }
    
  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>
    

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

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

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

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

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

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

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

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

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

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

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)-A (FORMAT_UPC_A)
  • الرمز العالمي للمنتج (UPC)-E (FORMAT_UPC_E)
  • رمز الاستجابة السريعة (FORMAT_QR_CODE)
  • PDF417 (FORMAT_PDF417)
  • أزتيك (FORMAT_AZTEC)
  • مصفوفة البيانات (FORMAT_DATA_MATRIX)

بدءًا من النموذج المجمّع 17.1.0 والنموذج غير المجمّع 18.2.0، يمكنك أيضًا الاتصال enableAllPotentialBarcodes() لعرض كل الرموز الشريطية المحتمَلة حتى في حال كانت . يمكن استخدام ذلك لتسهيل رصد المزيد من الأنشطة، مثلاً من خلال تكبير الصورة للحصول على صورة أوضح لأي رمز شريطي في الصورة المعروضة المربع المحيط.

Kotlin

val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(...)
        .enableAllPotentialBarcodes() // Optional
        .build()

Java

BarcodeScannerOptions options =
        new BarcodeScannerOptions.Builder()
        .setBarcodeFormats(...)
        .enableAllPotentialBarcodes() // Optional
        .build();

Further on, starting from bundled library 17.2.0 and unbundled library 18.3.0, a new feature called auto-zoom has been introduced to further enhance the barcode scanning experience. With this feature enabled, the app is notified when all barcodes within the view are too distant for decoding. As a result, the app can effortlessly adjust the camera's zoom ratio to the recommended setting provided by the library, ensuring optimal focus and readability. This feature will significantly enhance the accuracy and success rate of barcode scanning, making it easier for apps to capture information precisely.

To enable auto-zooming and customize the experience, you can utilize the setZoomSuggestionOptions() method along with your own ZoomCallback handler and desired maximum zoom ratio, as demonstrated in the code below.

Kotlin

val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(...)
        .setZoomSuggestionOptions(
            new ZoomSuggestionOptions.Builder(zoomCallback)
                .setMaxSupportedZoomRatio(maxSupportedZoomRatio)
                .build()) // Optional
        .build()

Java

BarcodeScannerOptions options =
        new BarcodeScannerOptions.Builder()
        .setBarcodeFormats(...)
        .setZoomSuggestionOptions(
            new ZoomSuggestionOptions.Builder(zoomCallback)
                .setMaxSupportedZoomRatio(maxSupportedZoomRatio)
                .build()) // Optional
        .build();

zoomCallback is required to be provided to handle whenever the library suggests a zoom should be performed and this callback will always be called on the main thread.

The following code snippet shows an example of defining a simple callback.

Kotlin

fun setZoom(ZoomRatio: Float): Boolean {
    if (camera.isClosed()) return false
    camera.getCameraControl().setZoomRatio(zoomRatio)
    return true
}

Java

boolean setZoom(float zoomRatio) {
    if (camera.isClosed()) {
        return false;
    }
    camera.getCameraControl().setZoomRatio(zoomRatio);
    return true;
}

maxSupportedZoomRatio is related to the camera hardware, and different camera libraries have different ways to fetch it (see the javadoc of the setter method). In case this is not provided, an unbounded zoom ratio might be produced by the library which might not be supported. Refer to the setMaxSupportedZoomRatio() method introduction to see how to get the max supported zoom ratio with different Camera libraries.

When auto-zooming is enabled and no barcodes are successfully decoded within the view, BarcodeScanner triggers your zoomCallback with the requested zoomRatio. If the callback correctly adjusts the camera to this zoomRatio, it is highly probable that the most centered potential barcode will be decoded and returned.

A barcode may remain undecodable even after a successful zoom-in. In such cases, BarcodeScanner may either invoke the callback for another round of zoom-in until the maxSupportedZoomRatio is reached, or provide an empty list (or a list containing potential barcodes that were not decoded, if enableAllPotentialBarcodes() was called) to the OnSuccessListener (which will be defined in step 4. Process the image).

2. Prepare the input image

To recognize barcodes in an image, create an InputImage object from either a Bitmap, media.Image, ByteBuffer, byte array, or a file on the device. Then, pass the InputImage object to the BarcodeScanner's process method.

You can create an InputImage object from different sources, each is explained below.

Using a media.Image

To create an InputImage object from a media.Image object, such as when you capture an image from a device's camera, pass the media.Image object and the image's rotation to InputImage.fromMediaImage().

If you use the CameraX library, the OnImageCapturedListener and ImageAnalysis.Analyzer classes calculate the rotation value for you.

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) في نموذج تطبيق Quickstart كمثال.
  • في حال استخدام CameraX API: تأكَّد من ضبط استراتيجية الضغط العكسي على قيمتها التلقائية ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST وهذا يضمن تسليم صورة واحدة فقط للتحليل في كل مرة. إذا كانت المزيد من الصور يتم إنتاجها عندما يكون المحلل مشغولاً، فسيتم إسقاطها تلقائيًا ولن يتم وضعها في قائمة الانتظار التسليم. بمجرد إغلاق الصورة التي يتم تحليلها عن طريق استدعاء ImageProxy.Close()، سيتم تسليم الصورة التالية الأحدث.
  • إذا استخدمت مخرجات أداة الكشف لتراكب الرسومات على الصورة المدخلة، والحصول أولاً على النتيجة من ML Kit، ثم عرض الصورة وتراكبها في خطوة واحدة. يتم عرض هذا المحتوى على سطح الشاشة. مرة واحدة فقط لكل إطار إدخال يمكنك الاطّلاع على CameraSourcePreview و GraphicOverlay صفًا في نموذج تطبيق Quickstart كمثال.
  • في حال استخدام واجهة برمجة التطبيقات Camera2 API، يمكنك التقاط الصور في تنسيق ImageFormat.YUV_420_888 إذا كنت تستخدم واجهة برمجة التطبيقات للكاميرا القديمة، يمكنك التقاط الصور في تنسيق ImageFormat.NV21