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

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

הקשר מראש

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

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

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