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