זיהוי דיו דיגיטלי באמצעות ערכת ML ב-iOS

קל לארגן דפים בעזרת אוספים אפשר לשמור ולסווג תוכן על סמך ההעדפות שלך.

בעזרת זיהוי דיו דיגיטלי של למידת מכונה, תוכלו לזהות טקסט בכתב יד על משטח דיגיטלי במאות שפות, וגם לסווג רישומים.

רוצה לנסות?

לפני שמתחילים

  1. יש לכלול את הספריות הבאות של ערכת ה-ML ב-Podfile:

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. אחרי ההתקנה או העדכון של ה-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 מכיל מלבן ושלוש נקודות יחד, המזהה עשוי להחזיר אחד את השני (או משהו אחר לחלוטין) מכיוון שמועמד יחיד לזיהוי אינו יכול לייצג שתי צורות.