باستخدام ميزة "التعرّف على الكتابة بالحبر الرقمي" في حزمة تعلّم الآلة، يمكنك التعرّف على النص المكتوب بخط اليد على سطح رقمي بمئات اللغات، بالإضافة إلى تصنيف الرسومات.
للتجربة:
- يمكنك تجربة نموذج التطبيق للاطّلاع على مثال على استخدام واجهة برمجة التطبيقات هذه.
قبل البدء
أدرِج مكتبات "حزمة تعلّم الآلة" التالية في ملف Podfile:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'بعد تثبيت 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."); }]; }
نصائح لتحسين دقة التعرّف على النص
قد تختلف دقة التعرّف على النص باختلاف اللغات. تعتمد الدقة أيضًا على أسلوب الكتابة. على الرغم من أنّ واجهة Digital Ink Recognition مدرَّبة على التعامل مع العديد من أنواع أساليب الكتابة، يمكن أن تختلف النتائج من مستخدم لآخر.
في ما يلي بعض الطرق لتحسين دقة أداة التعرّف على النص. يُرجى العِلم أنّ هذه الأساليب لا تنطبق على مصنّفات الرسم الخاصة بالإيموجي والرسم التلقائي والأشكال.
مساحة الكتابة
تحتوي العديد من التطبيقات على منطقة كتابة محدّدة جيدًا لإدخال المستخدم. يتم تحديد معنى الرمز جزئيًا من خلال حجمه بالنسبة إلى حجم مساحة الكتابة التي تحتوي عليه. على سبيل المثال، الفرق بين الحرف الصغير أو الكبير "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 يحتوي على مستطيل وشكل بيضاوي بجانب بعضهما البعض، فقد يعرض أداة التعرّف أحدهما أو الآخر (أو شيئًا مختلفًا تمامًا) نتيجة لذلك، نظرًا لأن مرشح التعرف الفردي لا يمكنه تمثيل شكلين.