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

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

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

  1. אם עדיין לא עשיתם זאת, מוסיפים את Firebase לפרויקט Android.
  2. הוספת יחסי התלות של ספריות ML Kit ל-Android למודול (ברמת האפליקציה) קובץ Gradle (בדרך כלל app/build.gradle):
    apply plugin: 'com.android.application'
    apply plugin: 'com.google.gms.google-services'
    
    dependencies {
      // ...
    
      implementation 'com.google.firebase:firebase-ml-vision:24.0.3'
      implementation 'com.google.firebase:firebase-ml-vision-barcode-model:16.0.1'
    }

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

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

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

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

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

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

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

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

1. הגדרת הגלאי של ברקודים

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

לדוגמה, כדי לזהות רק קודי Aztec וקודי QR, יוצרים אובייקט FirebaseVisionBarcodeDetectorOptions כמו בדוגמה הבאה:

Java

FirebaseVisionBarcodeDetectorOptions options =
        new FirebaseVisionBarcodeDetectorOptions.Builder()
        .setBarcodeFormats(
                FirebaseVisionBarcode.FORMAT_QR_CODE,
                FirebaseVisionBarcode.FORMAT_AZTEC)
        .build();

Kotlin+KTX

val options = FirebaseVisionBarcodeDetectorOptions.Builder()
        .setBarcodeFormats(
                FirebaseVisionBarcode.FORMAT_QR_CODE,
                FirebaseVisionBarcode.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. הרצת הגלאי של הברקוד

כדי לזהות ברקודים בתמונה, צריך ליצור אובייקט FirebaseVisionImage מ-Bitmap, media.Image, ByteBuffer, ממערך בייטים או מקובץ במכשיר. לאחר מכן מעבירים את האובייקט FirebaseVisionImage אל השיטה detectInImage של FirebaseVisionBarcodeDetector.

  1. יוצרים אובייקט FirebaseVisionImage מהתמונה.

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

      אם משתמשים ספריית CameraX, OnImageCapturedListener ImageAnalysis.Analyzer מחלקות מחשבים את ערך הסבב בשבילך, צריך רק להמיר את הסבב ROTATION_ קבועים לפני הקריאה FirebaseVisionImage.fromMediaImage():

      Java

      private class YourAnalyzer implements ImageAnalysis.Analyzer {
      
          private int degreesToFirebaseRotation(int degrees) {
              switch (degrees) {
                  case 0:
                      return FirebaseVisionImageMetadata.ROTATION_0;
                  case 90:
                      return FirebaseVisionImageMetadata.ROTATION_90;
                  case 180:
                      return FirebaseVisionImageMetadata.ROTATION_180;
                  case 270:
                      return FirebaseVisionImageMetadata.ROTATION_270;
                  default:
                      throw new IllegalArgumentException(
                              "Rotation must be 0, 90, 180, or 270.");
              }
          }
      
          @Override
          public void analyze(ImageProxy imageProxy, int degrees) {
              if (imageProxy == null || imageProxy.getImage() == null) {
                  return;
              }
              Image mediaImage = imageProxy.getImage();
              int rotation = degreesToFirebaseRotation(degrees);
              FirebaseVisionImage image =
                      FirebaseVisionImage.fromMediaImage(mediaImage, rotation);
              // Pass image to an ML Kit Vision API
              // ...
          }
      }

      Kotlin+KTX

      private class YourImageAnalyzer : ImageAnalysis.Analyzer {
          private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
              0 -> FirebaseVisionImageMetadata.ROTATION_0
              90 -> FirebaseVisionImageMetadata.ROTATION_90
              180 -> FirebaseVisionImageMetadata.ROTATION_180
              270 -> FirebaseVisionImageMetadata.ROTATION_270
              else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
          }
      
          override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
              val mediaImage = imageProxy?.image
              val imageRotation = degreesToFirebaseRotation(degrees)
              if (mediaImage != null) {
                  val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
                  // Pass image to an ML Kit Vision API
                  // ...
              }
          }
      }

      אם לא משתמשים בספריית מצלמה שמאפשרת סיבוב תמונה, הוא יכול לחשב אותו על סמך סיבוב המכשיר וכיוון המצלמה החיישן במכשיר:

      Java

      private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
      static {
          ORIENTATIONS.append(Surface.ROTATION_0, 90);
          ORIENTATIONS.append(Surface.ROTATION_90, 0);
          ORIENTATIONS.append(Surface.ROTATION_180, 270);
          ORIENTATIONS.append(Surface.ROTATION_270, 180);
      }
      
      /**
       * 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, Context context)
              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);
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          CameraManager cameraManager = (CameraManager) context.getSystemService(CAMERA_SERVICE);
          int sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION);
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360;
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          int result;
          switch (rotationCompensation) {
              case 0:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  break;
              case 90:
                  result = FirebaseVisionImageMetadata.ROTATION_90;
                  break;
              case 180:
                  result = FirebaseVisionImageMetadata.ROTATION_180;
                  break;
              case 270:
                  result = FirebaseVisionImageMetadata.ROTATION_270;
                  break;
              default:
                  result = FirebaseVisionImageMetadata.ROTATION_0;
                  Log.e(TAG, "Bad rotation value: " + rotationCompensation);
          }
          return result;
      }

      Kotlin+KTX

      private val ORIENTATIONS = SparseIntArray()
      
      init {
          ORIENTATIONS.append(Surface.ROTATION_0, 90)
          ORIENTATIONS.append(Surface.ROTATION_90, 0)
          ORIENTATIONS.append(Surface.ROTATION_180, 270)
          ORIENTATIONS.append(Surface.ROTATION_270, 180)
      }
      /**
       * 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, context: Context): 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)
      
          // On most devices, the sensor orientation is 90 degrees, but for some
          // devices it is 270 degrees. For devices with a sensor orientation of
          // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
          val cameraManager = context.getSystemService(CAMERA_SERVICE) as CameraManager
          val sensorOrientation = cameraManager
                  .getCameraCharacteristics(cameraId)
                  .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
          rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
      
          // Return the corresponding FirebaseVisionImageMetadata rotation value.
          val result: Int
          when (rotationCompensation) {
              0 -> result = FirebaseVisionImageMetadata.ROTATION_0
              90 -> result = FirebaseVisionImageMetadata.ROTATION_90
              180 -> result = FirebaseVisionImageMetadata.ROTATION_180
              270 -> result = FirebaseVisionImageMetadata.ROTATION_270
              else -> {
                  result = FirebaseVisionImageMetadata.ROTATION_0
                  Log.e(TAG, "Bad rotation value: $rotationCompensation")
              }
          }
          return result
      }

      לאחר מכן, מעבירים את האובייקט media.Image את ערך הסיבוב ל-FirebaseVisionImage.fromMediaImage():

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation);

      Kotlin+KTX

      val image = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)
    • כדי ליצור אובייקט FirebaseVisionImage מכתובת URI של קובץ, מעבירים את הקשר של האפליקציה ואת כתובת ה-URI של הקובץ ל-FirebaseVisionImage.fromFilePath(). זה שימושי כאשר משתמשים ב-Intent ACTION_GET_CONTENT כדי לבקש מהמשתמש לבחור תמונה מאפליקציית הגלריה.

      Java

      FirebaseVisionImage image;
      try {
          image = FirebaseVisionImage.fromFilePath(context, uri);
      } catch (IOException e) {
          e.printStackTrace();
      }

      Kotlin+KTX

      val image: FirebaseVisionImage
      try {
          image = FirebaseVisionImage.fromFilePath(context, uri)
      } catch (e: IOException) {
          e.printStackTrace()
      }
    • כדי ליצור אובייקט FirebaseVisionImage מתוך ByteBuffer או מערך בייטים, מחשבים קודם את התמונה של סיבוב המסך כפי שמתואר למעלה עבור קלט media.Image.

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

      Java

      FirebaseVisionImageMetadata metadata = new FirebaseVisionImageMetadata.Builder()
              .setWidth(480)   // 480x360 is typically sufficient for
              .setHeight(360)  // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build();

      Kotlin+KTX

      val metadata = FirebaseVisionImageMetadata.Builder()
              .setWidth(480) // 480x360 is typically sufficient for
              .setHeight(360) // image recognition
              .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
              .setRotation(rotation)
              .build()

      משתמשים במאגר הנתונים הזמני או במערך ובאובייקט המטא-נתונים כדי ליצור אובייקט FirebaseVisionImage:

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromByteBuffer(buffer, metadata);
      // Or: FirebaseVisionImage image = FirebaseVisionImage.fromByteArray(byteArray, metadata);

      Kotlin+KTX

      val image = FirebaseVisionImage.fromByteBuffer(buffer, metadata)
      // Or: val image = FirebaseVisionImage.fromByteArray(byteArray, metadata)
    • כדי ליצור אובייקט FirebaseVisionImage מתוך אובייקט Bitmap:

      Java

      FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(bitmap);

      Kotlin+KTX

      val image = FirebaseVisionImage.fromBitmap(bitmap)
      התמונה שמיוצגת על ידי האובייקט Bitmap חייבת להיות זקופה, ללא צורך בסיבוב נוסף.

  2. אחזור מופע של FirebaseVisionBarcodeDetector:

    Java

    FirebaseVisionBarcodeDetector detector = FirebaseVision.getInstance()
            .getVisionBarcodeDetector();
    // Or, to specify the formats to recognize:
    // FirebaseVisionBarcodeDetector detector = FirebaseVision.getInstance()
    //        .getVisionBarcodeDetector(options);

    Kotlin+KTX

    val detector = FirebaseVision.getInstance()
            .visionBarcodeDetector
    // Or, to specify the formats to recognize:
    // val detector = FirebaseVision.getInstance()
    //        .getVisionBarcodeDetector(options)
  3. לבסוף, מעבירים את התמונה לשיטה detectInImage:

    Java

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

    Kotlin+KTX

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

3. קבלת מידע מברקודים

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

לדוגמה:

Java

for (FirebaseVisionBarcode 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 FirebaseVisionBarcode.TYPE_WIFI:
            String ssid = barcode.getWifi().getSsid();
            String password = barcode.getWifi().getPassword();
            int type = barcode.getWifi().getEncryptionType();
            break;
        case FirebaseVisionBarcode.TYPE_URL:
            String title = barcode.getUrl().getTitle();
            String url = barcode.getUrl().getUrl();
            break;
    }
}

Kotlin+KTX

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) {
        FirebaseVisionBarcode.TYPE_WIFI -> {
            val ssid = barcode.wifi!!.ssid
            val password = barcode.wifi!!.password
            val type = barcode.wifi!!.encryptionType
        }
        FirebaseVisionBarcode.TYPE_URL -> {
            val title = barcode.url!!.title
            val url = barcode.url!!.url
        }
    }
}

טיפים לשיפור הביצועים בזמן אמת

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

  • לא לצלם קלט ברזולוציה המקורית של המצלמה. במכשירים מסוימים, לכידת קלט ברזולוציה המקורית מפיקה גדול מאוד (10+ כך שזמן האחזור נמוך מאוד ואין תועלת מדויקות. צריך לבקש מהמצלמה רק את הגודל הנדרש לזיהוי ברקוד: בדרך כלל רזולוציה של עד 2 מגה-פיקסלים.

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

  • צמצום מספר הקריאות למזהה. אם פריים חדש של וידאו זמין בזמן שהגלאי פועל, צריך להסיר את הפריים.
  • אם אתם משתמשים בפלט של הגלאי כדי להוסיף שכבת-על של גרפיקה לתמונה הקלט, קודם צריך לקבל את התוצאה מ-ML Kit, ואז לבצע עיבוד (רנדור) של התמונה ולהוסיף את שכבת-העל בשלב אחד. כך תוכלו להציג את משטח המסך פעם אחת בלבד לכל מסגרת קלט.
  • אם משתמשים ב- Camera2 API, מצלמים תמונות ב פורמט של ImageFormat.YUV_420_888.

    אם משתמשים בגרסה הישנה של ממשק ה-API של המצלמה, מצלמים תמונות ב פורמט של ImageFormat.NV21.