זיהוי דיו דיגיטלי באמצעות 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:19.0.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));

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

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

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

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