التعرّف على الحبر الرقمي باستخدام أدوات تعلّم الآلة على نظام التشغيل 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 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));

يفترض الرمز النموذجي أعلاه أن نموذج التعرف قد تم بالفعل تنزيل التطبيق، كما هو موضح في القسم التالي.

إدارة عمليات تنزيل النماذج

على الرغم من أنّ واجهة برمجة التطبيقات لميزة التعرّف على الحبر الرقمي تتيح استخدام مئات اللغات، تتطلّب كل لغة تنزيل بعض البيانات قبل التعرّف عليها. يجب توفير مساحة تخزين تبلغ نحو 20 ميغابايت لكل لغة. يتم التعامل مع ذلك من خلال معالجة الكائن 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" باللغة الإنجليزية بصورته الصغيرة أو الكبيرة، والفرق بين الفاصلة والشرطة المائلة للأمام.

يمكن أن يؤدي إخبار ميزة التعرّف على الكتابة بخط اليد بعرض منطقة الكتابة وارتفاعها إلى تحسين الدقة. ومع ذلك، يفترِض المعرِّف أنّ منطقة الكتابة تحتوي على سطر واحد من النص فقط. إذا كانت منطقة الكتابة المادية كبيرة بما يكفي للسماح للمستخدم بكتابة سطرَين أو أكثر، قد تحصل على نتائج أفضل من خلال إدخال WritingArea بارتفاع يمثّل أفضل تقدير لك لارتفاع سطر واحد من النص. ليس من الضروري أن يتطابق كائن WriteArea الذي تنقله إلى أداة التعرُّف بالضبط مع منطقة الكتابة الفعلية على الشاشة. تغيير ارتفاع WriteArea بهذه الطريقة يعمل بشكل أفضل في بعض اللغات أكثر من غيرها.

عند تحديد منطقة الكتابة، حدِّد عرضها وارتفاعها في الوحدات نفسها المستخدَمة في الحد الخارجي الإحداثيات. لا تتطلّب وسيطات الإحداثيات x وy استخدام وحدة معيّنة، لأنّ واجهة برمجة التطبيقات تسوي جميع الوحدات، لذا فإنّ الشيء الوحيد المهم هو الحجم النسبي للخطوط وموقعها. لك مطلق الحرية في ومرر الإحداثيات بأي مقياس يناسب نظامك.

ما قبل السياق

ما قبل السياق هو النص الذي يسبق الضغطات مباشرةً في Ink الذي يحاولون التعرف عليها. يمكنك مساعدة أداة التعرّف من خلال إخبارها بالسياق السابق.

على سبيل المثال، الأحرف التدوينية "n" و"u" وغالبًا ما يتم الخلط بينها وبين بعضها البعض. إذا سبق للمستخدم إدخال القسم "arg" من الكلمة، قد يواصل الكتابة بخطوط يمكن التعرّف عليها على أنّها "ument" أو "nment". تحديد "الوسيطة" التي تسبق السياق وتحل الغموض، نظرًا لأن كلمة "argument" أكثر احتمالاً من "التأثر".

يمكن أن يساعد السياق السابق أيضًا معرّف النصوص في تحديد فواصل الكلمات والمسافات بينها. يمكنك كتابة مسافة، ولكن لا يمكنك رسمه، فكيف يمكن لأداة التعرّف تحديد وقت انتهاء الكلمة وتبدأ المرحلة التالية؟ إذا كتب المستخدم "مرحبًا" بالفعل ويستمر بالكلمة المكتوبة "world"، بدون سياق ما قبل السياق، تعرض أداة التعرّف على السلسلة "world". ومع ذلك، إذا حدّدت السياق السابق "مرحبًا"، سيعرض النموذج السلسلة "عالم" مع مسافة بادئة، لأنّ "مرحبًا عالم" أكثر منطقية من "مرحبًاكلمة".

يجب تقديم أطول سلسلة ممكنة للسياق السابق، بما يصل إلى 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 واستبدالها بكلمة أخرى. من المحتمل أن تكون المراجعة في منتصف الجملة، لكن الضغطات الخاصة بالمراجعة تكون في نهاية تسلسل الخط. في هذه الحالة، ننصحك بإرسال الكلمة المكتوبة حديثًا بشكل منفصل إلى واجهة برمجة التطبيقات ودمج النتيجة مع عمليات التعرّف السابقة باستخدام منطقك الخاص.

التعامل مع الأشكال الغامضة

هناك حالات يكون فيها معنى الشكل المقدَّم إلى المعرّف غامضًا. على سبيل المثال، يمكن اعتبار المستطيل الذي يضمّ حوافًا مستديرة جدًا مستطيلاً أو بيضاويًا.

يمكن التعامل مع هذه الحالات غير الواضحة باستخدام نتائج التعرّف عندما تكون متاحة. لا تقدّم سوى مصنّفات الأشكال تقييمات. إذا كان النموذج واثقًا جدًا، ستكون نتيجة النتيجة الأولى أفضل بكثير من النتيجة الثانية. إذا كان هناك عدم يقين، فإن نتائج أعلى نتيجتين أن تكون قريبًا. علاوةً على ذلك، ضع في اعتبارك أن أدوات تصنيف الأشكال تفسّر Ink بالكامل على أنه شكل واحد. على سبيل المثال، إذا كان Ink يحتوي على مستطيل وقطع ناقص بجانب كل منهما. الآخر، فإن أداة التعرف قد تعرض أحدهما أو الآخر (أو شيئًا مختلفًا تمامًا) نظرًا لأن مرشحًا واحدًا للاعتراف لا يمكن أن يمثل شكلين.