זיהוי דיו דיגיטלי באמצעות ערכת ML ב-Android

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

לפני שמתחילים

  1. בקובץ build.gradle ברמת הפרויקט, חשוב להקפיד לכלול את מאגר Maven ב-Google בקטעים buildscript ו-allprojects.
  2. מוסיפים את יחסי התלות של הספריות ל-Android Kit ב-ML Kit, ברמת הקובץ של האפליקציה, שהיא בדרך כלל app/build.gradle:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:18.0.0'
}

עכשיו אפשר להתחיל לזהות טקסט באובייקטים של Ink.

בניית אובייקט Ink

הדרך העיקרית ליצור אובייקט Ink היא לשרטט אותו במסך מגע. ב-Android אפשר להשתמש בCanvas למטרה הזו. ה-handlers של אירועי המגע צריכים לקרוא לשיטה 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" לבין פסיק לעומת קו נטוי קדמי.

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

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

הקשר מראש

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

אם המשתמש כבר הזין את המילה החלקית '"arg", הוא עשוי להמשיך עם מעברים שניתנים לזיהוי כ-"ument" או "nment". ציון ה-&context;arg&&;, פותר את העמימות, מאחר שהמילה "argument" היא בעלת סיכוי גבוה יותר מה-"argnment".

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