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

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

جرّبه الآن

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

قبل البدء

  1. أدرِج مكتبات ML Kit التالية في ملف Podfile:

    pod 'GoogleMLKit/DigitalInkRecognition', '7.0.0'
    
    
  2. بعد تثبيت حِزم Pods في مشروعك أو تحديثها، افتح مشروع Xcode باستخدام .xcworkspace. تتوفّر حزمة ML Kit في الإصدار 13.2.1 من Xcode أو الإصدارات الأحدث.

أنت الآن جاهز لبدء التعرّف على النصوص في Ink.

إنشاء عنصر Ink

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

Swift

@IBOutlet weak var mainImageView: UIImageView!
var kMillisecondsPerTimeInterval = 1000.0
var lastPoint = CGPoint.zero
private var strokes: [Stroke] = []
private var points: [StrokePoint] = []

func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
  UIGraphicsBeginImageContext(view.frame.size)
  guard let context = UIGraphicsGetCurrentContext() else {
    return
  }
  mainImageView.image?.draw(in: view.bounds)
  context.move(to: fromPoint)
  context.addLine(to: toPoint)
  context.setLineCap(.round)
  context.setBlendMode(.normal)
  context.setLineWidth(10.0)
  context.setStrokeColor(UIColor.white.cgColor)
  context.strokePath()
  mainImageView.image = UIGraphicsGetImageFromCurrentImageContext()
  mainImageView.alpha = 1.0
  UIGraphicsEndImageContext()
}

override func touchesBegan(_ touches: Set, with event: UIEvent?) {
  guard let touch = touches.first else {
    return
  }
  lastPoint = touch.location(in: mainImageView)
  let t = touch.timestamp
  points = [StrokePoint.init(x: Float(lastPoint.x),
                             y: Float(lastPoint.y),
                             t: Int(t * kMillisecondsPerTimeInterval))]
  drawLine(from:lastPoint, to:lastPoint)
}

override func touchesMoved(_ touches: Set, with event: UIEvent?) {
  guard let touch = touches.first else {
    return
  }
  let currentPoint = touch.location(in: mainImageView)
  let t = touch.timestamp
  points.append(StrokePoint.init(x: Float(currentPoint.x),
                                 y: Float(currentPoint.y),
                                 t: Int(t * kMillisecondsPerTimeInterval)))
  drawLine(from: lastPoint, to: currentPoint)
  lastPoint = currentPoint
}

override func touchesEnded(_ touches: Set, with event: UIEvent?) {
  guard let touch = touches.first else {
    return
  }
  let currentPoint = touch.location(in: mainImageView)
  let t = touch.timestamp
  points.append(StrokePoint.init(x: Float(currentPoint.x),
                                 y: Float(currentPoint.y),
                                 t: Int(t * kMillisecondsPerTimeInterval)))
  drawLine(from: lastPoint, to: currentPoint)
  lastPoint = currentPoint
  strokes.append(Stroke.init(points: points))
  self.points = []
  doRecognition()
}

Objective-C

// Interface
@property (weak, nonatomic) IBOutlet UIImageView *mainImageView;
@property(nonatomic) CGPoint lastPoint;
@property(nonatomic) NSMutableArray *strokes;
@property(nonatomic) NSMutableArray *points;

// Implementations
static const double kMillisecondsPerTimeInterval = 1000.0;

- (void)drawLineFrom:(CGPoint)fromPoint to:(CGPoint)toPoint {
  UIGraphicsBeginImageContext(self.mainImageView.frame.size);
  [self.mainImageView.image drawInRect:CGRectMake(0, 0, self.mainImageView.frame.size.width,
                                                  self.mainImageView.frame.size.height)];
  CGContextMoveToPoint(UIGraphicsGetCurrentContext(), fromPoint.x, fromPoint.y);
  CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), toPoint.x, toPoint.y);
  CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
  CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 10.0);
  CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 1, 1, 1, 1);
  CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeNormal);
  CGContextStrokePath(UIGraphicsGetCurrentContext());
  CGContextFlush(UIGraphicsGetCurrentContext());
  self.mainImageView.image = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();
}

- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event {
  UITouch *touch = [touches anyObject];
  self.lastPoint = [touch locationInView:self.mainImageView];
  NSTimeInterval time = [touch timestamp];
  self.points = [NSMutableArray array];
  [self.points addObject:[[MLKStrokePoint alloc] initWithX:self.lastPoint.x
                                                         y:self.lastPoint.y
                                                         t:time * kMillisecondsPerTimeInterval]];
  [self drawLineFrom:self.lastPoint to:self.lastPoint];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event {
  UITouch *touch = [touches anyObject];
  CGPoint currentPoint = [touch locationInView:self.mainImageView];
  NSTimeInterval time = [touch timestamp];
  [self.points addObject:[[MLKStrokePoint alloc] initWithX:currentPoint.x
                                                         y:currentPoint.y
                                                         t:time * kMillisecondsPerTimeInterval]];
  [self drawLineFrom:self.lastPoint to:currentPoint];
  self.lastPoint = currentPoint;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event {
  UITouch *touch = [touches anyObject];
  CGPoint currentPoint = [touch locationInView:self.mainImageView];
  NSTimeInterval time = [touch timestamp];
  [self.points addObject:[[MLKStrokePoint alloc] initWithX:currentPoint.x
                                                         y:currentPoint.y
                                                         t:time * kMillisecondsPerTimeInterval]];
  [self drawLineFrom:self.lastPoint to:currentPoint];
  self.lastPoint = currentPoint;
  if (self.strokes == nil) {
    self.strokes = [NSMutableArray array];
  }
  [self.strokes addObject:[[MLKStroke alloc] initWithPoints:self.points]];
  self.points = nil;
  [self doRecognition];
}

يُرجى العِلم أنّ مقتطف الرمز البرمجي يتضمّن نموذجًا لدالة لرسم الخطوط في UIImageView، والتي يجب تعديلها حسب الحاجة لتطبيقك. ننصحك باستخدام القوس المستدير عند رسم أجزاء الخطّ لكي يتم رسم الأجزاء التي لا طول لها على شكل نقطة (تذكَّر النقطة على الحرف الصغير i). يتم استدعاء الدالة doRecognition() بعد كتابة كل ضربة، وسيتم تعريفها أدناه.

الحصول على مثيل من DigitalInkRecognizer

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

Swift

override func viewDidLoad() {
  super.viewDidLoad()
  let languageTag = "en-US"
  let identifier = DigitalInkRecognitionModelIdentifier(forLanguageTag: languageTag)
  if identifier == nil {
    // no model was found or the language tag couldn't be parsed, handle error.
  }
  let model = DigitalInkRecognitionModel.init(modelIdentifier: identifier!)
  let modelManager = ModelManager.modelManager()
  let conditions = ModelDownloadConditions.init(allowsCellularAccess: true,
                                         allowsBackgroundDownloading: true)
  modelManager.download(model, conditions: conditions)
  // Get a recognizer for the language
  let options: DigitalInkRecognizerOptions = DigitalInkRecognizerOptions.init(model: model)
  recognizer = DigitalInkRecognizer.digitalInkRecognizer(options: options)
}

Objective-C

- (void)viewDidLoad {
  [super viewDidLoad];
  NSString *languagetag = @"en-US";
  MLKDigitalInkRecognitionModelIdentifier *identifier =
      [MLKDigitalInkRecognitionModelIdentifier modelIdentifierForLanguageTag:languagetag];
  if (identifier == nil) {
    // no model was found or the language tag couldn't be parsed, handle error.
  }
  MLKDigitalInkRecognitionModel *model = [[MLKDigitalInkRecognitionModel alloc]
                                          initWithModelIdentifier:identifier];
  MLKModelManager *modelManager = [MLKModelManager modelManager];
  [modelManager downloadModel:model conditions:[[MLKModelDownloadConditions alloc]
                                                initWithAllowsCellularAccess:YES
                                                allowsBackgroundDownloading:YES]];
  MLKDigitalInkRecognizerOptions *options =
      [[MLKDigitalInkRecognizerOptions alloc] initWithModel:model];
  self.recognizer = [MLKDigitalInkRecognizer digitalInkRecognizerWithOptions:options];
}

تتضمّن تطبيقات "البدء السريع" رمزًا إضافيًا يوضّح كيفية معالجة عمليات تنزيل متعددة في الوقت نفسه، وكيفية تحديد عملية التنزيل التي تمّت بنجاح من خلال معالجة إشعارات اكتمال التنزيل.

التعرّف على عنصر Ink

بعد ذلك، ننتقل إلى دالة doRecognition() التي يتم استدعاؤها من touchesEnded() لتسهيل الأمر. في التطبيقات الأخرى، قد تحتاج إلى بدء عملية التعرّف فقط بعد انتهاء مهلة زمنية أو عندما يضغط المستخدم على زر لبدء عملية التعرّف.

Swift

func doRecognition() {
  let ink = Ink.init(strokes: strokes)
  recognizer.recognize(
    ink: ink,
    completion: {
      [unowned self]
      (result: DigitalInkRecognitionResult?, error: Error?) in
      var alertTitle = ""
      var alertText = ""
      if let result = result, let candidate = result.candidates.first {
        alertTitle = "I recognized this:"
        alertText = candidate.text
      } else {
        alertTitle = "I hit an error:"
        alertText = error!.localizedDescription
      }
      let alert = UIAlertController(title: alertTitle,
                                  message: alertText,
                           preferredStyle: UIAlertController.Style.alert)
      alert.addAction(UIAlertAction(title: "OK",
                                    style: UIAlertAction.Style.default,
                                  handler: nil))
      self.present(alert, animated: true, completion: nil)
    }
  )
}

Objective-C

- (void)doRecognition {
  MLKInk *ink = [[MLKInk alloc] initWithStrokes:self.strokes];
  __weak typeof(self) weakSelf = self;
  [self.recognizer
      recognizeInk:ink
        completion:^(MLKDigitalInkRecognitionResult *_Nullable result,
                     NSError *_Nullable error) {
    typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf == nil) {
      return;
    }
    NSString *alertTitle = nil;
    NSString *alertText = nil;
    if (result.candidates.count > 0) {
      alertTitle = @"I recognized this:";
      alertText = result.candidates[0].text;
    } else {
      alertTitle = @"I hit an error:";
      alertText = [error localizedDescription];
    }
    UIAlertController *alert =
        [UIAlertController alertControllerWithTitle:alertTitle
                                            message:alertText
                                     preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"OK"
                                              style:UIAlertActionStyleDefault
                                            handler:nil]];
    [strongSelf presentViewController:alert animated:YES completion:nil];
  }];
}

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

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

التحقّق مما إذا سبق تنزيل نموذج

Swift

let model : DigitalInkRecognitionModel = ...
let modelManager = ModelManager.modelManager()
modelManager.isModelDownloaded(model)

Objective-C

MLKDigitalInkRecognitionModel *model = ...;
MLKModelManager *modelManager = [MLKModelManager modelManager];
[modelManager isModelDownloaded:model];

حذف نموذج تم تنزيله

Swift

let model : DigitalInkRecognitionModel = ...
let modelManager = ModelManager.modelManager()

if modelManager.isModelDownloaded(model) {
  modelManager.deleteDownloadedModel(
    model!,
    completion: {
      error in
      if error != nil {
        // Handle error
        return
      }
      NSLog(@"Model deleted.");
    })
}

Objective-C

MLKDigitalInkRecognitionModel *model = ...;
MLKModelManager *modelManager = [MLKModelManager modelManager];

if ([self.modelManager isModelDownloaded:model]) {
  [self.modelManager deleteDownloadedModel:model
                                completion:^(NSError *_Nullable error) {
                                  if (error) {
                                    // Handle error.
                                    return;
                                  }
                                  NSLog(@"Model deleted.");
                                }];
}

نصائح لتحسين دقة التعرّف على النصوص

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

في ما يلي بعض الطرق لتحسين دقة ميزة التعرّف على النصوص. يُرجى العلم أنّ هذه الأساليب لا تنطبق على أدوات تصنيف الرسومات لرموز التعبيرات والأشكال والرسومات التلقائية.

منطقة الكتابة

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

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

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

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

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

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

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

يجب تقديم أطول سلسلة ممكنة للسياق السابق، بما يصل إلى 20 حرفًا، بما في ذلك المسافات. إذا كانت السلسلة أطول، يستخدم المعرّف آخر 20 حرفًا فقط.

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

Swift

let ink: Ink = ...;
let recognizer: DigitalInkRecognizer =  ...;
let preContext: String = ...;
let writingArea = WritingArea.init(width: ..., height: ...);

let context: DigitalInkRecognitionContext.init(
    preContext: preContext,
    writingArea: writingArea);

recognizer.recognizeHandwriting(
  from: ink,
  context: context,
  completion: {
    (result: DigitalInkRecognitionResult?, error: Error?) in
    if let result = result, let candidate = result.candidates.first {
      NSLog("Recognized \(candidate.text)")
    } else {
      NSLog("Recognition error \(error)")
    }
  })

Objective-C

MLKInk *ink = ...;
MLKDigitalInkRecognizer *recognizer = ...;
NSString *preContext = ...;
MLKWritingArea *writingArea = [MLKWritingArea initWithWidth:...
                                              height:...];

MLKDigitalInkRecognitionContext *context = [MLKDigitalInkRecognitionContext
       initWithPreContext:preContext
       writingArea:writingArea];

[recognizer recognizeHandwritingFromInk:ink
            context:context
            completion:^(MLKDigitalInkRecognitionResult
                         *_Nullable result, NSError *_Nullable error) {
                               NSLog(@"Recognition result %@",
                                     result.candidates[0].text);
                         }];

ترتيب السكتات

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

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

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

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

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