זיהוי דיו דיגיטלי באמצעות 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 כדי לקבל דוגמה מלאה יותר.

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, ופסיק לעומת קו נטוי.

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

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

לפני ההקשר

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

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

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