באמצעות זיהוי דיו דיגיטלי של ML Kit, אתם יכולים לזהות טקסט בכתב יד על משטח דיגיטלי במאות שפות, וגם לסווג שרטוטים.
רוצה לנסות?
- כדי לראות דוגמה לשימוש ב-API הזה, אתם יכולים להתנסות באפליקציה לדוגמה.
לפני שמתחילים
מוסיפים את הספריות הבאות של ML Kit ל-Podfile:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'אחרי שמתקינים או מעדכנים את ה-Pods של הפרויקט, פותחים את פרויקט Xcode באמצעות
.xcworkspace. ML Kit נתמך ב-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, קודם צריך להוריד את מודל הזיהוי לשפה הרצויה ולטעון את המודל ל-RAM. כדי לעשות את זה, אפשר להשתמש בקטע הקוד הבא, שלשם הפשטות ממוקם ב-method 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 אומן לזהות סגנונות כתיבה רבים, התוצאות יכולות להשתנות ממשתמש למשתמש.
ריכזנו כאן כמה דרכים לשפר את הדיוק של זיהוי הטקסט. חשוב לזכור שהשיטות האלה לא רלוונטיות לסיווגים של שרטוטים לאמוג'י, ל-AutoDraw ולצורות.
אזור הכתיבה
בהרבה אפליקציות יש אזור כתיבה מוגדר היטב לקלט של משתמשים. המשמעות של סמל נקבעת באופן חלקי לפי הגודל שלו ביחס לגודל של אזור הכתיבה שבו הוא נמצא. לדוגמה, ההבדל בין האותיות הקטנות או הגדולות "o" או "c", ובין פסיק ללוכסן.
כדי לשפר את הדיוק, אפשר להגדיר למזהה את הרוחב והגובה של אזור הכתיבה. עם זאת, המזהה מניח שאזור הכתיבה מכיל רק שורת טקסט אחת. אם אזור הכתיבה הפיזי גדול מספיק כדי לאפשר למשתמש לכתוב שתי שורות או יותר, יכול להיות שתקבלו תוצאות טובות יותר אם תעבירו WritingArea עם גובה שהוא ההערכה הטובה ביותר שלכם לגובה של שורת טקסט אחת. האובייקט WritingArea שאתם מעבירים למזהה לא חייב להתאים בדיוק לאזור הכתיבה הפיזי במסך. שינוי הגובה של WritingArea בדרך הזו עובד טוב יותר בשפות מסוימות מאשר באחרות.
כשמציינים את אזור הכתיבה, צריך לציין את הרוחב והגובה שלו באותן יחידות כמו הקואורדינטות של הקו. לארגומנטים של הקואורדינטות x,y אין דרישה ליחידה – ה-API מנרמל את כל היחידות, כך שהדבר היחיד שחשוב הוא הגודל והמיקום היחסיים של הקווים. אתם יכולים להעביר קואורדינטות בכל קנה מידה שמתאים למערכת שלכם.
הקשר מקדים
הקשר מקדים הוא הטקסט שמופיע מיד לפני הקווים ב-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 מוסרת ומוחלפת במילה אחרת. התיקון כנראה נמצא באמצע משפט, אבל קווי התיקון נמצאים בסוף רצף הקווים.
במקרה כזה, מומלץ לשלוח את המילה החדשה שנכתבה בנפרד ל-API ולמזג את התוצאה עם הזיהויים הקודמים באמצעות לוגיקה משלכם.
התמודדות עם צורות לא ברורות
יש מקרים שבהם המשמעות של הצורה שסופקה למזהה לא ברורה. לדוגמה, מלבן עם קצוות מעוגלים מאוד יכול להיראות כמו מלבן או כמו אליפסה.
כדי לטפל במקרים הלא ברורים האלה, אפשר להשתמש בציוני זיהוי כשהם זמינים. רק מסווגי צורות מספקים ציונים. אם המודל בטוח מאוד, הציון של התוצאה המובילה יהיה הרבה יותר טוב מהתוצאה השנייה הכי טובה. אם יש חוסר ודאות, הציונים של שתי התוצאות המובילות יהיו קרובים. חשוב גם לזכור שהמסווגים של הצורות מפרשים את כל ה-Ink כצורה אחת. לדוגמה, אם ה-Ink מכיל מלבן ואליפסה זה לצד זה, יכול להיות שהמזהה יחזיר את אחד מהם (או משהו אחר לגמרי) כתוצאה, כי מועמד יחיד לזיהוי לא יכול לייצג שתי צורות.