זיהוי טקסט בתמונות באמצעות ML Kit ב-Android

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

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

ממשק API לזיהוי טקסט
תיאורזיהוי טקסט לטיני בתמונות או בסרטונים.
שם הספרייהcom.google.android.gms:play-services-mlkit-text-recognition
יישוםהספרייה ניתנת להורדה באופן דינמי באמצעות Google Play Services.
ההשפעה על גודל האפליקציה260KB
זמן אתחוליכול להיות שתצטרכו להמתין להורדת הספרייה לפני השימוש הראשון.
ביצועיםזמן אמת ברוב המכשירים.

רוצה לנסות?

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

  1. בקובץ build.gradle ברמת הפרויקט, חשוב לכלול את מאגר ה-Maven של Google גם בקטע buildscript וגם בקטע allprojects.
  2. מוסיפים את יחסי התלות של ספריות ה-Android של ה-ML Kit לקובץ ה-gradle ברמת האפליקציה, שהוא בדרך כלל app/build.gradle:
    dependencies {
      // ...
    
      implementation 'com.google.android.gms:play-services-mlkit-text-recognition:18.0.2'
    }
    
  3. אופציונלי אבל מומלץ: אפשר להגדיר שהאפליקציה תוריד באופן אוטומטי את מודל ה-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 הזה.