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

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

امتحان کنید

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

قبل‌از شروع

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

جاوا

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 که به تشخیص‌دهنده ارسال می‌کنید لازم نیست دقیقاً با ناحیه نوشتن فیزیکی روی صفحه‌نمایش مطابقت داشته باشد. تغییر ارتفاع «فضای نوشتن» به این روش در برخی‌از زبان‌ها بهتر از زبان‌های دیگر کار می‌کند.

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

پیش‌زمینه

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

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

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

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

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