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

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

רוצה לנסות?

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

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

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

יצירת אובייקט Ink

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

התבנית הכללית הזו מוצגת בקטע הקוד הבא. במדריך למתחילים של 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));

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

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

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

אזור הכתיבה

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

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

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

הקשר מקדים

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

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

ההקשר המקדים יכול גם לעזור למזהה לזהות את הרווחים בין המילים. אפשר להקליד רווח, אבל אי אפשר לצייר אותו. אז איך המערכת יודעת מתי מילה אחת נגמרת והמילה הבאה מתחילה? אם המשתמש כבר כתב "שלום" וממשיך עם המילה הכתובה "עולם", ללא הקשר קודם, המזהה מחזיר את המחרוזת "עולם". לעומת זאת, אם תציינו את ההקשר המקדים "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 מכיל מלבן ואליפסה זה לצד זה, יכול להיות שהמזהה יחזיר את אחד מהם (או משהו אחר לגמרי) כתוצאה, כי מועמד יחיד לזיהוי לא יכול לייצג שתי צורות.