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

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

للتجربة:

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

قبل البدء

  1. أدرِج مكتبات حزمة تعلّم الآلة التالية في ملف Podfile:

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. بعد تثبيت أو تعديل ملفات Pods في مشروعك، افتح مشروع 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، علينا أولاً تنزيل نموذج التعرّف للغة المطلوبة وتحميل النموذج في ذاكرة الوصول العشوائي (RAM). يمكن إجراء ذلك باستخدام مقتطف الرمز البرمجي التالي، الذي يتم وضعه في طريقة 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".

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