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

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

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

  1. יש לכלול את הספריות הבאות של למידת המכונה ב-Podfile:

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

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

בניית אובייקט Ink

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

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

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

הקשר מראש

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

אם המשתמש כבר הזין את המילה החלקית '"arg", הוא עשוי להמשיך עם מעברים שניתנים לזיהוי כ-"ument" או "nment". ציון ה-&context;arg&&;, פותר את העמימות, מאחר שהמילה "argument" היא בעלת סיכוי גבוה יותר מה-"argnment".

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