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

يتم توضيح هذا النمط العام في مقتطف الرمز التالي. راجِع نموذج البدء السريع لاستخدام حزمة تعلّم الآلة للاطّلاع على مثال أكثر اكتمالاً.

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" والفاصلة مقابل الشرطة المائلة للأمام.

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

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

ما قبل السياق

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

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

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

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

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

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