סריקת ברקודים באמצעות ML Kit ב-Android

אפשר להשתמש ב-ML Kit כדי לזהות ברקודים ולפענח אותם.

תכונהלא חלק מהחבילהבחבילה
הטמעההורדת המודל מתבצעת באופן דינמי דרך Google Play Services.המודל מקושר באופן סטטי לאפליקציה בזמן ה-build.
גודל האפליקציההגדלה של כ-200KB.הגדלה של כ-2.4MB.
זמן האתחוליכול להיות שתצטרכו להמתין להורדת המודל לפני השימוש הראשון.המודל זמין באופן מיידי.

אני רוצה לנסות

לפני שמתחילים

  1. בקובץ build.gradle ברמת הפרויקט, חשוב לכלול את מאגר Maven של Google בקטע buildscript וגם בקטע allprojects.

  2. מוסיפים את יחסי התלות של ספריות ML Kit Android לקובץ GRid ברמת האפליקציה של המודול, בדרך כלל app/build.gradle. תוכלו לבחור באחד מיחסי התלות הבאים בהתאם לצרכים שלכם:

    כדי לקבץ את המודל עם האפליקציה:

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

    כדי להשתמש במודל ב-Google Play Services:

    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.0'
    }
    
  3. אם תבחרו להשתמש במודל ב-Google Play Services, תוכלו להגדיר שהאפליקציה תוריד את המודל באופן אוטומטי למכשיר אחרי התקנת האפליקציה מחנות 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 Services.

    אם לא מפעילים הורדות של מודלים בזמן ההתקנה או מבקשים הורדה מפורשת, תתבצע הורדה של המודל בפעם הראשונה שתפעילו את הסורק. בקשות שמבצעים לפני סיום ההורדה לא מובילות לתוצאות.

הנחיות להוספת תמונה

  • כדי ש-ML Kit יוכל לקרוא ברקודים באופן מדויק, תמונות הקלט צריכות להכיל ברקודים שמיוצגים על ידי נתוני פיקסלים מספיקים.

    הדרישות הספציפיות לנתוני פיקסלים תלויות בסוג הברקוד ובכמות הנתונים שמקודדים בו, כי ברקודים רבים תומכים במטען ייעודי (payload) בגודל משתנה. באופן כללי, היחידה המשמעותית הקטנה ביותר של הברקוד צריכה להיות ברוחב של 2 פיקסלים לפחות, ועבור קודים דו-ממדיים, היא צריכה להיות בגובה של 2 פיקסלים.

    לדוגמה, ברקודים מסוג EAN-13 מורכבים מעמודות ומרווחים ברוחב 1, 2, 3 או 4 יחידות. לכן, תמונת ברקוד מסוג EAN-13 מיועדת לכלול עמודות ורווחים ברוחב 2, 4, 6 ו-8 פיקסלים לפחות. מכיוון שברקוד EAN-13 הוא ברוחב של 95 יחידות בסך הכול, הוא צריך להיות ברוחב של 190 פיקסלים לפחות.

    בפורמטים צפופים יותר, כמו PDF417, נדרשים מידות פיקסלים גבוהות יותר כדי ש-ML Kit יוכל לקרוא אותם בצורה אמינה. לדוגמה, קוד PDF417 יכול לכלול עד 34 "מילים" ברוחב של 17 יחידות בשורה אחת, וברוחב של עד 1,156 פיקסלים.

  • מיקוד לא טוב של תמונה עלול להשפיע על רמת הדיוק של הסריקה. אם האפליקציה לא מקבלת תוצאות מקובלות, צריך לבקש מהמשתמש לצלם מחדש את התמונה.

  • באפליקציות טיפוסיות, מומלץ לספק תמונה ברזולוציה גבוהה יותר, כמו 1280x720 או 1,920x1,080, כך שאפשר לסרוק את הברקודים ממרחק גדול יותר מהמצלמה.

    עם זאת, באפליקציות שבהן זמן האחזור קריטי אפשר לשפר את הביצועים על ידי צילום תמונות ברזולוציה נמוכה יותר. לשם כך, צריך שהברקוד יהווה את רוב תמונת הקלט. כדאי גם לקרוא את המאמר טיפים לשיפור הביצועים בזמן אמת.

‫1. הגדרת סורק הברקוד

אם יודעים אילו פורמטים של ברקוד מצפים לקרוא, אפשר להגדיר את גלאי הברקוד כך שיזהה רק את הפורמטים האלה, וכך להגביר את המהירות שלו.

לדוגמה, כדי לזהות רק קוד אצטקי וקודי QR, יוצרים אובייקט 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)
  • Codabar (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)
  • קוד QR (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(). האפשרות הזו שימושית כשמשתמשים ב-Intent של 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. עיבוד התמונה

מעבירים את התמונה ל-method 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 מגה-פיקסל.

    אם מהירות הסריקה חשובה, אפשר להוריד עוד יותר את רזולוציית הצילום של התמונה. עם זאת, חשוב לזכור את הדרישות המינימליות לגודל הברקוד שמפורטות למעלה.

    אם מנסים לזהות ברקודים מרצף של פריימים של וידאו בסטרימינג, המזהה עשוי להפיק תוצאות שונות מפריים לפריים. כדאי להמתין עד שמתקבלת סדרה רציפה של אותו ערך כדי להיות בטוחים שהתקבלה תוצאה טובה.

    אין תמיכה בספרת ביקורת (checksum) ב-ITF וב-CODE-39.

  • אם משתמשים ב-API של Camera או camera2, צריך לווסת את הקריאות למזהה. אם פריים חדש בווידאו הופך לזמין בזמן שהגלאי פועל, משחררים את הפריים. לדוגמה, תוכלו לעיין בשיעור VisionProcessorBase באפליקציה לדוגמה של המדריך למתחילים.
  • אם משתמשים ב-API של CameraX, צריך לוודא שאסטרטגיית לחיצה לאחור מוגדרת לערך ברירת המחדל שלה ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST. כך אפשר להבטיח שרק תמונה אחת תוצג לניתוח בכל פעם. אם יופקו עוד תמונות כשהכלי לניתוח נתונים עמוס, הן יוסרו באופן אוטומטי ולא ימתינו לתור למשלוח. לאחר סגירת התמונה שמנתחת על ידי קריאה ל-ImageProxy.close(), מתקבלת התמונה העדכנית ביותר.
  • אם משתמשים בפלט של הגלאי כדי ליצור שכבת-על של גרפיקה בתמונת הקלט, מקבלים קודם את התוצאה מ-ML Kit ואז מעבדים את התמונה ושכבת-העל בפעולה אחת. הרינדור של משטח המסך מתבצע פעם אחת בלבד לכל מסגרת קלט. לדוגמה, אפשר לעיין בכיתות CameraSourcePreview ו- GraphicOverlay באפליקציה לדוגמה של המדריך למתחילים.
  • אם משתמשים ב- Camera2 API, צריך לצלם תמונות בפורמט ImageFormat.YUV_420_888. אם משתמשים בגרסה הקודמת של Camera API, צריך לצלם תמונות בפורמט ImageFormat.NV21.