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

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

التجربة الآن

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

قبل البدء

  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];
}

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

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

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

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

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

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

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

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