סריקת ברקודים באמצעות למידת מכונה ב-Android

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

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

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

רוצה לנסות?

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

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

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

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

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

    כדי להשתמש במודל ב-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.1.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 תקרא ברקודים בצורה מדויקת, תמונות הקלט צריכות להכיל ברקודים שמיוצגים על ידי מספיק נתוני פיקסלים.

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

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

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

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

  • באפליקציות אופייניות, מומלץ לספק תמונה ברזולוציה גבוהה יותר, כמו למשל 1280x720 או 1920x1080, המאפשרת סריקה של ברקודים ממרחק גדול יותר מהמצלמה.

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

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)

2. מכינים את תמונת הקלט

כדי לזהות ברקודים בתמונה, צריך ליצור אובייקט InputImage ממערך (Bitmap), media.Image, ByteBuffer, מערך בייט או קובץ במכשיר. לאחר מכן, צריך להעביר את האובייקט InputImage למתודה process של BarcodeScanner.

אפשר ליצור אובייקט InputImage ממקורות שונים, כמו שמוסבר בהמשך.

באמצעות media.Image

כדי ליצור אובייקט InputImage מאובייקט media.Image, למשל כשמצלמים תמונה ממצלמה של מכשיר, צריך להעביר את האובייקט media.Image ואת סיבוב התמונה ל-InputImage.fromMediaImage().

אם משתמשים בספריית CameraX, המחלקות 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, תחילה יש לחשב את מידת סיבוב התמונה כפי שתואר קודם. לאחר מכן, יוצרים את האובייקט 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 מגה פיקסל.

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

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

    הספרה Checksum אינה נתמכת עבור ITF וקוד CODE-39.

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