זיהוי דיו דיגיטלי באמצעות ML Kit ב-Android

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

רוצה לנסות?

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

  1. בקובץ build.gradle ברמת הפרויקט, חשוב לכלול את מאגר Maven של Google גם בקטעים buildscript וגם בקטעים allprojects.
  2. מוסיפים את יחסי התלות של ספריות ML Kit ל-Android לקובץ Gradle ברמת האפליקציה של המודול, בדרך כלל app/build.gradle:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.1.0'
}

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

יצירה של אובייקט Ink

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

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

Kotlin

var inkBuilder = Ink.builder()
lateinit var strokeBuilder: Ink.Stroke.Builder

// Call this each time there is a new event.
fun addNewTouchEvent(event: MotionEvent) {
  val action = event.actionMasked
  val x = event.x
  val y = event.y
  var t = System.currentTimeMillis()

  // If your setup does not provide timing information, you can omit the
  // third paramater (t) in the calls to Ink.Point.create
  when (action) {
    MotionEvent.ACTION_DOWN -> {
      strokeBuilder = Ink.Stroke.builder()
      strokeBuilder.addPoint(Ink.Point.create(x, y, t))
    }
    MotionEvent.ACTION_MOVE -> strokeBuilder!!.addPoint(Ink.Point.create(x, y, t))
    MotionEvent.ACTION_UP -> {
      strokeBuilder.addPoint(Ink.Point.create(x, y, t))
      inkBuilder.addStroke(strokeBuilder.build())
    }
    else -> {
      // Action not relevant for ink construction
    }
  }
}

...

// This is what to send to the recognizer.
val ink = inkBuilder.build()

Java

Ink.Builder inkBuilder = Ink.builder();
Ink.Stroke.Builder strokeBuilder;

// Call this each time there is a new event.
public void addNewTouchEvent(MotionEvent event) {
  float x = event.getX();
  float y = event.getY();
  long t = System.currentTimeMillis();

  // If your setup does not provide timing information, you can omit the
  // third paramater (t) in the calls to Ink.Point.create
  int action = event.getActionMasked();
  switch (action) {
    case MotionEvent.ACTION_DOWN:
      strokeBuilder = Ink.Stroke.builder();
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      break;
    case MotionEvent.ACTION_MOVE:
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      break;
    case MotionEvent.ACTION_UP:
      strokeBuilder.addPoint(Ink.Point.create(x, y, t));
      inkBuilder.addStroke(strokeBuilder.build());
      strokeBuilder = null;
      break;
  }
}

...

// This is what to send to the recognizer.
Ink ink = inkBuilder.build();

אחזור מכונה של DigitalInkRecognizer

כדי לבצע זיהוי, שולחים את המופע Ink לאובייקט DigitalInkRecognizer. הקוד הבא מראה איך ליצור מכונה של מזהה כזה מתג BCP-47.

Kotlin

// Specify the recognition model for a language
var modelIdentifier: DigitalInkRecognitionModelIdentifier
try {
  modelIdentifier = DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-US")
} catch (e: MlKitException) {
  // language tag failed to parse, handle error.
}
if (modelIdentifier == null) {
  // no model was found, handle error.
}
var model: DigitalInkRecognitionModel =
    DigitalInkRecognitionModel.builder(modelIdentifier).build()


// Get a recognizer for the language
var recognizer: DigitalInkRecognizer =
    DigitalInkRecognition.getClient(
        DigitalInkRecognizerOptions.builder(model).build())

Java

// Specify the recognition model for a language
DigitalInkRecognitionModelIdentifier modelIdentifier;
try {
  modelIdentifier =
    DigitalInkRecognitionModelIdentifier.fromLanguageTag("en-US");
} catch (MlKitException e) {
  // language tag failed to parse, handle error.
}
if (modelIdentifier == null) {
  // no model was found, handle error.
}

DigitalInkRecognitionModel model =
    DigitalInkRecognitionModel.builder(modelIdentifier).build();

// Get a recognizer for the language
DigitalInkRecognizer recognizer =
    DigitalInkRecognition.getClient(
        DigitalInkRecognizerOptions.builder(model).build());

עיבוד אובייקט Ink

Kotlin

recognizer.recognize(ink)
    .addOnSuccessListener { result: RecognitionResult ->
      // `result` contains the recognizer's answers as a RecognitionResult.
      // Logs the text from the top candidate.
      Log.i(TAG, result.candidates[0].text)
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error during recognition: $e")
    }

Java

recognizer.recognize(ink)
    .addOnSuccessListener(
        // `result` contains the recognizer's answers as a RecognitionResult.
        // Logs the text from the top candidate.
        result -> Log.i(TAG, result.getCandidates().get(0).getText()))
    .addOnFailureListener(
        e -> Log.e(TAG, "Error during recognition: " + e));

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

ניהול ההורדות של מודלים

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

הורדת מודל חדש

Kotlin

import com.google.mlkit.common.model.DownloadConditions
import com.google.mlkit.common.model.RemoteModelManager

var model: DigitalInkRecognitionModel =  ...
val remoteModelManager = RemoteModelManager.getInstance()

remoteModelManager.download(model, DownloadConditions.Builder().build())
    .addOnSuccessListener {
      Log.i(TAG, "Model downloaded")
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error while downloading a model: $e")
    }

Java

import com.google.mlkit.common.model.DownloadConditions;
import com.google.mlkit.common.model.RemoteModelManager;

DigitalInkRecognitionModel model = ...;
RemoteModelManager remoteModelManager = RemoteModelManager.getInstance();

remoteModelManager
    .download(model, new DownloadConditions.Builder().build())
    .addOnSuccessListener(aVoid -> Log.i(TAG, "Model downloaded"))
    .addOnFailureListener(
        e -> Log.e(TAG, "Error while downloading a model: " + e));

איך בודקים אם מודל כבר הועלה

Kotlin

var model: DigitalInkRecognitionModel =  ...
remoteModelManager.isModelDownloaded(model)

Java

DigitalInkRecognitionModel model = ...;
remoteModelManager.isModelDownloaded(model);

מחיקת מודל שכבר הורדת

הסרת מודל מהאחסון במכשיר מפנה מקום.

Kotlin

var model: DigitalInkRecognitionModel =  ...
remoteModelManager.deleteDownloadedModel(model)
    .addOnSuccessListener {
      Log.i(TAG, "Model successfully deleted")
    }
    .addOnFailureListener { e: Exception ->
      Log.e(TAG, "Error while deleting a model: $e")
    }

Java

DigitalInkRecognitionModel model = ...;
remoteModelManager.deleteDownloadedModel(model)
                  .addOnSuccessListener(
                      aVoid -> Log.i(TAG, "Model successfully deleted"))
                  .addOnFailureListener(
                      e -> Log.e(TAG, "Error while deleting a model: " + e));

טיפים לשיפור הדיוק של זיהוי הטקסט

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

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

אזור הכתיבה

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

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

כשמציינים את אזור הכתיבה, צריך לציין את הרוחב והגובה שלו באותן יחידות שבהן מצוינות קואורדינטות הקו. אין דרישה ליחידות בארגומנטים של הקואורדינטות x,y – ה-API מבצע נורמליזציה של כל היחידות, כך שהדבר היחיד שחשוב הוא המיקום והגודל היחסי של הקווים. אתם יכולים להעביר קואורדינטות בכל קנה מידה שמתאים למערכת שלכם.

הקשר מקדים

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

לדוגמה, האותיות 'n' ו-'u' בכתב יד מעורבב נוטות להתבלבל זו בזו. אם המשתמש כבר הזין את המילה החלקית 'arg', הוא עשוי להמשיך עם קווים שאפשר לזהות כ'ument' או כ'nment'. ציון ההקשר הקודם "arg" פותר את הספק, כי סביר יותר שהמילה "argument" תופיע מאשר "argnment".

ההקשר המקדים יכול גם לעזור למזהה לזהות את הפסקות המילים ואת הרווחים בין המילים. אפשר להקליד תו רווח אבל אי אפשר לצייר אותו, אז איך המזהה יכול לקבוע מתי מילה אחת מסתיימת ומתי מתחילה המילה הבאה? אם המשתמש כבר כתב "hello" והמשיך עם המילה הכתובה "world", ללא הקשר מראש, המזהה מחזיר את המחרוזת "world". עם זאת, אם מציינים את ההקשר המקדים 'hello', המודל יחזיר את המחרוזת ' world', עם רווח בהתחלה, כי 'hello world' הגיוני יותר מאשר 'helloword'.

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

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

Kotlin

var preContext : String = ...;
var width : Float = ...;
var height : Float = ...;
val recognitionContext : RecognitionContext =
    RecognitionContext.builder()
        .setPreContext(preContext)
        .setWritingArea(WritingArea(width, height))
        .build()

recognizer.recognize(ink, recognitionContext)

Java

String preContext = ...;
float width = ...;
float height = ...;
RecognitionContext recognitionContext =
    RecognitionContext.builder()
                      .setPreContext(preContext)
                      .setWritingArea(new WritingArea(width, height))
                      .build();

recognizer.recognize(ink, recognitionContext);

סדר התנועות

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

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

טיפול בצורות לא ברורות

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

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