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

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

جرّبه الآن

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

قبل البدء

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

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

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

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