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

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

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

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

אזור הכתיבה

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

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

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

לפני ההקשר

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

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

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