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