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

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

רוצה לנסות?

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

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

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. אחרי שמתקינים או מעדכנים את קבוצות ה-Pod של הפרויקט, פותחים את פרויקט Xcode באמצעות .xcworkspace שלו. יש תמיכה ב-ML Kit בגרסת Xcode 13.2.1 ומעלה.

עכשיו אפשר להתחיל לזהות טקסט באובייקטים Ink.

פיתוח אובייקט Ink

הדרך העיקרית לבנות אובייקט Ink היא לשרטט אותו על מסך מגע. ב-iOS, אפשר להשתמש ב-UIImageView אירוע מגע רכיבי handler שמשרטטים את הקווים על המסך וגם מאחסנים את הקווים נקודות לפיתוח את האובייקט 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, ופסיק לעומת קו נטוי.

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

כשמציינים את אזור הכתיבה, מציינים את הרוחב והגובה שלו באותן יחידות של הקו של הקואורדינטות. לארגומנטים של קואורדינטות x,y אין דרישת יחידה - ה-API מנרמל את הכול ולכן הדבר היחיד שחשוב הוא הגודל והמיקום היחסי של הקווים. מותר לך להעביר את הקואורדינטות בכל קנה מידה שמתאים למערכת שלכם.

לפני ההקשר

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

לדוגמה, האותיות "n" ו-"u" בדרך כלל עלולים לחשוב שמדובר באחר. אם למשתמש יש כבר הזנת את המילה החלקית "arg", ייתכן שהם ימשיכו עם קווים שניתן לזהות אותם בתור ument או 'nment'. ציון ה'ארגומנט' לפני ההקשר פותרים את חוסר הבהירות, כי המילה 'ארגומנט' יש סבירות גבוהה יותר מ'ארגומנט'.

הוספת ההקשר יכולה גם לעזור לזיהוי של מעברי מילים, שהם רווחים בין המילים. אפשר מקלידים תו רווח אבל אי אפשר לשרטט תו, אז איך המזהה יכול לקבוע מתי מילה אחת מסתיימת והסרטון הבא מתחיל? אם המשתמש כבר כתב "hello" וממשיך במילה הכתובה 'world', ללא הקשר מקדים, המזהה מחזיר את המחרוזת 'world'. אבל אם מציינים לפני ההקשר "hello", המודל יחזיר את המחרוזת " בעולם, עם חלל מוביל, כי "שלום עולם" יותר הגיוני מ"מילת מפתח".

צריך לספק מחרוזת ארוכה ככל האפשר לפני ההקשר, עד 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 מכיל מלבן ואליפסה לצד כל אחד מהם אחר, המזהה עשוי להחזיר אחד או את השני (או משהו שונה לחלוטין) התוצאה, מכיוון שמועמד להכרה יחיד לא יכול לייצג שתי צורות.