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

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

التجربة الآن

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

قبل البدء

  1. تضمين مكتبات ML Kit التالية في Podfile:

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. بعد تثبيت لوحات مشروعك أو تحديثها، افتح مشروعك في Xcode باستخدام .xcworkspace. يتوافق تعلّم الآلة مع الإصدار 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];
}

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

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

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

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

ما قبل السياق

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

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

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

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