בעזרת זיהוי דיו דיגיטלי של למידת מכונה, תוכלו לזהות טקסט בכתב יד על משטח דיגיטלי במאות שפות, וגם לסווג רישומים.
רוצה לנסות?
- כדאי לשחק עם האפליקציה לדוגמה כדי לראות שימוש לדוגמה ב-API הזה.
לפני שמתחילים
יש לכלול את הספריות הבאות של ערכת ה-ML ב-Podfile:
pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
אחרי ההתקנה או העדכון של ה-Pods של הפרויקט, פותחים את פרויקט Xcode באמצעות
.xcworkspace
שלו. קיימת תמיכה ב-ML Kit בגרסה Xcode מגרסה 13.2.1 ומעלה.
עכשיו אפשר להתחיל לזהות טקסט באובייקטים של Ink
.
יצירת אובייקט מסוג Ink
הדרך העיקרית לבנות אובייקט Ink
היא לשרטט אותו במסך מגע. ב-iOS תוכלו להשתמש ב-UIImageView וגם ב-Touchhandlers של אירועי מגע, כדי לשרטט את התנועות על המסך וגם לאחסן את הנקודות של החתימות כדי ליצור את האובייקט 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."); }]; }
טיפים לשיפור הדיוק של זיהוי הטקסט
הדיוק של זיהוי טקסט עשוי להשתנות בשפות שונות. מידת הדיוק תלויה גם בסגנון הכתיבה. בעוד שהזיהוי של דיו דיגיטלי מאומן לטיפול בסוגים רבים של סגנונות כתיבה, התוצאות יכולות להשתנות ממשתמש למשתמש.
לפניכם כמה דרכים לשיפור הדיוק של מזהה טקסט. שימו לב: הטכניקות האלה לא רלוונטיות למסווגי שרטוטים עבור אמוג'ים, שרטוטים אוטומטיים וצורות.
תחום כתיבה
באפליקציות רבות יש אזור כתיבה מוגדר היטב לקלט של משתמשים. המשמעות של סמל נקבעת באופן חלקי בהתאם לגודלו של אזור הכתיבה שמכיל אותו. לדוגמה, ההבדל בין אותיות רישיות וקטנות "o" או "c", לבין פסיק לעומת קו נטוי קדימה.
אם אומרים למזהה את הרוחב והגובה של אזור הכתיבה, אפשר לשפר את הדיוק. עם זאת, המזהה מניח שאזור הכתיבה מכיל רק שורת טקסט אחת. אם תחום הכתיבה הפיזית גדול מספיק כדי לאפשר למשתמש לכתוב שתי שורות או יותר, ייתכן שתשיגו תוצאות טובות יותר אם תכתבו בתוך Writeing את הגובה שהוא האומדן הטוב ביותר של גובה שורת טקסט יחידה. האובייקט של WriteArea שאתם מעבירים למזהה לא חייב להיות זהה לחלוטין לאזור הכתיבה הפיזי שמופיע על המסך. שינוי של גובה הכתיבה כך פועל טוב יותר בשפות מסוימות.
כשאתם מציינים את אזור הכתיבה, אתם יכולים לציין את הרוחב והגובה של אותן יחידות בקואורדינטות של החתירה. לארגומנטים x,y קואורדינטות אין דרישה עבור יחידה כלשהי - ממשק ה-API מנרמל את כל היחידות, כך שהדבר החשוב ביותר הוא הגודל היחסי והמיקום של הקווים. אתם יכולים לבחון את הנתונים של קואורדינטות בכל היקף שמתאים למערכת שלכם.
הקשר מראש
הקשר מקדים הוא הטקסט שמופיע מיד לפני התנועות ב-Ink
שרוצים לזהות. תוכלו לעזור ליוצר לספר על כך באמצעות הקשר מראש.
לדוגמה, האותיות 's' ו-'u' המשמשות לעיתים קרובות כ-Google טועות זו לזו. אם המשתמש כבר הזין את המילה החלקית "arg", הוא עשוי להמשיך עם קווי התנועה שניתן לזהות אותם כ-"ment" או כ-"ment". כשאתם מציינים את ההקשר מראש "arg", המשמעות היא דו-משמעית, מכיוון שהמילה "ארגומנט" סבירה יותר מאשר "ארגומנט".
גם הקשר מראש יכול לעזור למזהה לזהות מעברי מילים, המרווחים בין המילים. אתם יכולים להקליד תו של רווח, אבל אי אפשר לצייר תו, אז איך מזהה יכול לזהות מתי מילה אחת מסתיימת והמילה הבאה מתחילה? אם המשתמש כבר כתב "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
או מחליפים אותה במילה אחרת. הגרסה הקודמת נמצאת כנראה באמצע משפט, אבל הקווים של הגרסה הקודמת נמצאים בסוף רצף הקווים.
במקרה כזה, מומלץ לשלוח בנפרד את המילה הכתובה החדשה ל-API ולמזג את התוצאה עם ההגנות הקודמות באמצעות לוגיקה משלכם.
התמודדות עם צורות לא ברורות
יש מקרים שבהם המשמעות של הצורה שסופקה למזהה היא רב משמעית. לדוגמה, ניתן לראות מלבן עם קצוות מעוגלים מאוד בתור מלבן או שלוש נקודות.
לא ניתן לטפל במקרים הלא ברורים האלה באמצעות ציוני הכרה, אם הם זמינים. רק המסווגים מעוצבים נותנים ציונים. אם המודל בטוח מאוד, הציון של התוצאה העליונה יהיה גבוה בהרבה מהציון השני. אם לא בטוחים, התוצאות של שתי התוצאות המובילות יהיו קרובות. כמו כן, חשוב לזכור שהמסווגים של הצורות מפרשים את כל הInk
כצורה אחת. לדוגמה, אם השדה Ink
מכיל מלבן ושלוש נקודות יחד, המזהה עשוי להחזיר אחד את השני (או משהו אחר לחלוטין) מכיוון שמועמד יחיד לזיהוי אינו יכול לייצג שתי צורות.