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

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

السياق السابق

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

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

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

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