באמצעות ערכת למידת מכונה, אפשר לזהות ברקודים ולפענח אותם.
תכונה | לא מקובצות | בחבילה |
---|---|---|
יישום | הורדת המודל מתבצעת באופן דינמי באמצעות Google Play Services. | המודל מקושר באופן סטטי לאפליקציה שלך בזמן הבנייה. |
גודל האפליקציה | גידול של כ-200KB. | תוספת של כ-2.4MB. |
זמן אתחול | ייתכן שתצטרכו להמתין עד להורדת המודל לפני השימוש הראשון. | המודל זמין באופן מיידי. |
רוצה לנסות?
- כדאי לשחק עם האפליקציה לדוגמה כדי לראות שימוש לדוגמה ב-API הזה.
- כדי לבצע הטמעה מקצה לקצה של ה-API הזה, אפשר להיעזר באפליקציה של Material Design.
לפני שמתחילים
בקובץ
build.gradle
ברמת הפרויקט, חשוב לכלול את מאגר Maven ב-Google בקטעיםbuildscript
וגםallprojects
.מוסיפים את יחסי התלות של ספריות ה-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' }
אם בוחרים להשתמש במודל ב-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
.