תוכלו להשתמש בערכת למידת מכונה כדי לזהות טקסט בתמונות או בסרטונים, כמו טקסט של שלט. המאפיינים העיקריים של תכונה זו הם:
ממשק API לזיהוי טקסט | |
---|---|
תיאור | זיהוי טקסט לטיני בתמונות או בסרטונים. |
שם הספרייה | com.google.android.gms:play-services-mlkit-text-recognition |
יישום | הספרייה ניתנת להורדה באופן דינמי באמצעות Google Play Services. |
ההשפעה על גודל האפליקציה | 260KB |
זמן אתחול | יכול להיות שתצטרכו להמתין להורדת הספרייה לפני השימוש הראשון. |
ביצועים | זמן אמת ברוב המכשירים. |
רוצה לנסות?
- כדאי לשחק עם האפליקציה לדוגמה כדי לראות שימוש לדוגמה ב-API הזה.
- אפשר לנסות את הקוד בעצמך באמצעות ה-Codelab.
לפני שמתחילים
- בקובץ
build.gradle
ברמת הפרויקט, חשוב לכלול את מאגר ה-Maven של Google גם בקטעbuildscript
וגם בקטעallprojects
. - מוסיפים את יחסי התלות של ספריות ה-Android של ה-ML Kit לקובץ ה-gradle ברמת האפליקציה, שהוא בדרך כלל
app/build.gradle
:dependencies { // ... implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.2' }
-
אופציונלי אבל מומלץ: אפשר להגדיר שהאפליקציה תוריד באופן אוטומטי את מודל ה-ML למכשיר אחרי שהאפליקציה תותקן מחנות Play. כדי לעשות זאת, עליך להוסיף את ההצהרה הבאה לקובץ
AndroidManifest.xml
של האפליקציה שלך:<application ...> ... <meta-data android:name="com.google.mlkit.vision.DEPENDENCIES" android:value="ocr" /> <!-- To use multiple models: android:value="ocr,model2,model3" --> </application>
ניתן גם לבדוק באופן מפורש את זמינות הדגם ולבקש הורדה באמצעות ModuleInstallClient API של שירותי Google Play.
אם לא תפעיל הורדות של מודל בזמן ההתקנה, ההורדה של המודל תתבצע בפעם הראשונה שתפעיל את המזהה במכשיר. בקשות שיישלחו לפני השלמת ההורדה לא יניבו תוצאות.
1. יצירת מופע של TextRecognizer
יצירת מכונה של TextRecognizer
:
Kotlin
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
Java
TextRecognizer recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);
2. מכינים את תמונת הקלט
כדי לזהות טקסט בתמונה, יוצרים אובייקט InputImage
מ-Bitmap
, media.Image
, ByteBuffer
, ממערך בייט או מקובץ במכשיר. לאחר מכן, צריך להעביר את האובייקט InputImage
לשיטת processImage
של TextRecognizer
.
אפשר ליצור אובייקט 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. עיבוד התמונה
יש להעביר את התמונה לשיטה process
:
Kotlin
val result = recognizer.process(image) .addOnSuccessListener { visionText -> // Task completed successfully // ... } .addOnFailureListener { e -> // Task failed with an exception // ... }
Java
Task<Text> result = recognizer.process(image) .addOnSuccessListener(new OnSuccessListener<Text>() { @Override public void onSuccess(Text visionText) { // Task completed successfully // ... } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Task failed with an exception // ... } });
4. חילוץ טקסט מבלוקים של טקסט מזוהה
אם פעולת זיהוי הטקסט מצליחה, אובייקט Text
מועבר למאזין ההצלחה. אובייקט Text
מכיל את הטקסט המלא המזוהה בתמונה ואפס אובייקטים TextBlock
לפחות.
כל TextBlock
מייצג בלוק מלבני של טקסט, שמכיל אפס או יותר אובייקטים של Line
. כל אובייקט Line
מייצג שורת טקסט שמכיל אובייקט Element
או יותר. כל אובייקט Element
מייצג מילה או ישות שמזכירה מילה, ומכילה אפס או יותר אובייקטים מסוג Symbol
. כל אובייקט Symbol
מייצג דמות, ספרה או ישות שמזכירה מילה.
עבור כל אובייקט TextBlock
, Line
, Element
ו-Symbol
, אתם יכולים לזהות את הטקסט המוכר באזור, את הקואורדינטות התוחמות של האזור ועוד מאפיינים רבים, כמו מידע על סבבים, תוצאות אמינות וכו'.
לדוגמה:
Kotlin
val resultText = result.text for (block in result.textBlocks) { val blockText = block.text val blockCornerPoints = block.cornerPoints val blockFrame = block.boundingBox for (line in block.lines) { val lineText = line.text val lineCornerPoints = line.cornerPoints val lineFrame = line.boundingBox for (element in line.elements) { val elementText = element.text val elementCornerPoints = element.cornerPoints val elementFrame = element.boundingBox } } }
Java
String resultText = result.getText(); for (Text.TextBlock block : result.getTextBlocks()) { String blockText = block.getText(); Point[] blockCornerPoints = block.getCornerPoints(); Rect blockFrame = block.getBoundingBox(); for (Text.Line line : block.getLines()) { String lineText = line.getText(); Point[] lineCornerPoints = line.getCornerPoints(); Rect lineFrame = line.getBoundingBox(); for (Text.Element element : line.getElements()) { String elementText = element.getText(); Point[] elementCornerPoints = element.getCornerPoints(); Rect elementFrame = element.getBoundingBox(); for (Text.Symbol symbol : element.getSymbols()) { String symbolText = symbol.getText(); Point[] symbolCornerPoints = symbol.getCornerPoints(); Rect symbolFrame = symbol.getBoundingBox(); } } } }
הנחיות להזנת תמונה
-
כדי ש-ML Kit יזהה טקסט בצורה מדויקת, הוא חייב לכלול טקסט שמיוצג על ידי מספיק נתוני פיקסלים. במצב אידיאלי, כל תו צריך להיות בגודל של 16x16 פיקסלים לפחות. באופן כללי, אין יתרון לדיוק של תווים שיכול להיות גדול מ-24x24 פיקסלים.
לדוגמה, תמונה בגודל 640x480 עשויה להתאים לסריקת כרטיס ביקור ברוחב מלא של התמונה. כדי לסרוק מסמך שמודפס על דף בגודל אות, ייתכן שיהיה צורך בתמונה בגודל 720x1280 פיקסלים.
-
התמקדות גרועה בתמונה יכולה להשפיע על הדיוק של זיהוי הטקסט. אם לא מתקבלות תוצאות קבילות, אפשר לנסות לבקש מהמשתמש לצלם שוב את התמונה.
-
אם מזהים טקסט בזמן אמת, צריך להביא בחשבון את המידות הכלליות של תמונות הקלט. אפשר לעבד תמונות קטנות יותר מהר יותר. כדי לצמצם את זמן האחזור, צריך לתפוס את גודל הטקסט כמה שיותר גדול ולצלם תמונות ברזולוציות נמוכות יותר (חשוב לזכור את דרישות הדיוק שצוינו למעלה). מידע נוסף זמין במאמר טיפים לשיפור הביצועים.
טיפים לשיפור הביצועים
- אם משתמשים ב-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
. - כדאי לצלם תמונות ברזולוציה נמוכה יותר. עם זאת, חשוב לזכור גם את הדרישות בנוגע למידות התמונה ב-API הזה.