التعرّف على الحبر الرقمي باستخدام أدوات تعلّم الآلة على نظام التشغيل Android

باستخدام ميزة التعرّف على الحبر الرقمي في أدوات تعلّم الآلة، يمكنك التعرّف على النص المكتوب بخط اليد على سطح رقمي بمئات اللغات، بالإضافة إلى تصنيف الرسومات.

التجربة الآن

  • جرّب نموذج التطبيق للاطّلاع على مثال لاستخدام واجهة برمجة التطبيقات هذه.

قبل البدء

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

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

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

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

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

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

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