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

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

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

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

אזור כתיבה

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

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

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

לפני ההקשר

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

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

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

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