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