تشخیص دست‌نویسی دیجیتال با ML Kit در Android

با تشخیص جوهر دیجیتال ML Kit، می‌توانید نوشتار دست‌نویس روی سطح دیجیتال را به صدها زبان تشخیص دهید و همچنین طرح‌ها را طبقه‌بندی کنید.

امتحان کنید

  • با برنامه نمونه کار کنید تا نمونه‌ای از کاربرد این API را ببینید.

قبل‌از شروع

  1. در فایل build.gradle سطح پروژه، حتماً مخزن Maven متعلق به Google را در هر دو بخش buildscript و allprojects اضافه کنید.
  2. وابستگی‌های کتابخانه‌های Android «کیت یادگیری ماشین» را به فایل Gradle سطح برنامه واحدتان اضافه کنید که معمولاً app/build.gradle است:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:19.0.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()

جاوا

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())

جاوا

// 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")
    }

جاوا

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 تشخیص جوهر دیجیتال از صدها زبان پشتیبانی می‌کند، هر زبان به بارگیری برخی داده‌ها قبل‌از هرگونه تشخیص نیاز دارد. برای هر زبان حدود ۲۰ مگابایت فضای ذخیره‌سازی لازم است. این کار توسط شیء 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")
    }

جاوا

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)

جاوا

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»، و ویرگول درمقابل خط مورب.

با گفتن عرض و ارتفاع ناحیه نوشتاری به تشخیص‌دهنده می‌توانید دقت را بهبود ببخشید. بااین‌حال، تشخیص‌دهنده فرض می‌کند که ناحیه نوشتن فقط حاوی یک خط نوشتار است. اگر ناحیه نوشتن فیزیکی به‌اندازه کافی بزرگ باشد که کاربر بتواند دو یا چند خط بنویسد، ممکن است با ارسال WritingArea با ارتفاعی که بهترین تخمین شما از ارتفاع یک خط نوشتار است، نتایج بهتری دریافت کنید. شیء WritingArea که به تشخیص‌دهنده ارسال می‌کنید لازم نیست دقیقاً با ناحیه نوشتن فیزیکی روی صفحه‌نمایش مطابقت داشته باشد. تغییر ارتفاع WritingArea به این روش در برخی زبان‌ها بهتر از زبان‌های دیگر کار می‌کند.

وقتی ناحیه نوشتن را مشخص می‌کنید، عرض و ارتفاع آن را با همان واحدهای مختصات قلم‌ضربه مشخص کنید. آرگومان‌های مختصات x و y نیازی به واحد ندارند - API همه واحدها را نرمال‌سازی می‌کند، بنابراین تنها چیزی که اهمیت دارد اندازه و موقعیت نسبی خطوط است. می‌توانید مختصات را در هر مقیاسی که برای سیستم شما منطقی است وارد کنید.

پیش‌زمینه

پیش‌زمینه نوشتاری است که بلافاصله قبل‌از خطوط در Ink که می‌خواهید تشخیص دهید قرار دارد. می‌توانید با گفتن پیش‌زمینه به تشخیص‌دهنده کمک کنید.

برای مثال، حروف سرهم «n» و «u» اغلب با یکدیگر اشتباه گرفته می‌شوند. اگر کاربر قبلاً بخشی از کلمه «arg» را وارد کرده باشد، ممکن است با ضربه‌هایی ادامه دهد که می‌توانند به‌عنوان «ument» یا «nment» تشخیص داده شوند. مشخص کردن پیش‌زمینه «arg» ابهام را برطرف می‌کند، زیرا کلمه «argument» محتمل‌تر از «argnment» است.

پیش‌زمینه همچنین می‌تواند به تشخیص‌دهنده کمک کند تا وقفه‌های کلمه‌ای، یعنی فاصله‌های بین کلمات را شناسایی کند. می‌توانید نویسه فاصله را تایپ کنید اما نمی‌توانید آن را بکشید، پس تشخیص‌دهنده چگونه می‌تواند تعیین کند که یک کلمه کجا تمام می‌شود و کلمه بعدی کجا شروع می‌شود؟ اگر کاربر قبلاً «سلام» را نوشته باشد و با کلمه نوشتاری «دنیا» ادامه دهد، بدون پیش‌زمینه، تشخیص‌دهنده رشته «دنیا» را برمی‌گرداند. بااین‌حال، اگر پیش‌زمینه «hello» را مشخص کنید، مدل رشته « world» را با یک فاصله در ابتدا برمی‌گرداند، زیرا «hello world» منطقی‌تر از «helloword» است.

باید طولانی‌ترین رشته پیش‌زمینه ممکن را تا ۲۰ نویسه، ازجمله فاصله‌ها، ارائه دهید. اگر رشته طولانی‌تر باشد، تشخیص‌دهنده فقط از ۲۰ نویسه آخر استفاده می‌کند.

نمونه کد زیر نشان می‌دهد که چگونه یک ناحیه نوشتن را تعریف کنید و از یک شیء 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)

جاوا

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 حاوی یک مستطیل و یک بیضی در کنار یکدیگر باشد، تشخیص‌دهنده ممکن است یکی یا دیگری (یا چیزی کاملاً متفاوت) را به‌عنوان نتیجه برگرداند، زیرا یک نامزد تشخیص واحد نمی‌تواند دو شکل را نشان دهد.