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

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

للتجربة:

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

قبل البدء

  1. في ملف build.gradle على مستوى المشروع، تأكَّد من تضمين مستودع Google Maven في كلٍّ من قسمَي buildscript وallprojects.
  2. أضِف التبعيات لمكتبات حزمة تعلّم الآلة على Android إلى ملف Gradle على مستوى التطبيق في وحدتك، والذي يكون عادةً app/build.gradle:
dependencies {
  // ...
  implementation 'com.google.mlkit:digital-ink-recognition:19.0.0'
}

أنت الآن مستعد لبدء التعرّف على النص في كائنات Ink.

إنشاء كائن Ink

الطريقة الرئيسية لإنشاء كائن Ink هي رسمه على شاشة تعمل باللمس. على Android، يمكنك استخدام عنصر Canvas لهذا الغرض. يجب أن تستدعي معالجات أحداث اللمس الطريقة 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()

جافا

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

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

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

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

جافا

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

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

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

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

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

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

جافا

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