Rozpoznawanie tuszów cyfrowych za pomocą ML Kit na iOS

Dzięki rozpoznawaniu pisma odręcznego w ML Kit możesz rozpoznawać tekst napisany ręcznie na powierzchni cyfrowej w setkach języków, a także klasyfikować szkice.

Wypróbuj

Zanim zaczniesz

  1. W pliku Podfile uwzględnij te biblioteki ML Kit:

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. Po zainstalowaniu lub zaktualizowaniu podów projektu otwórz projekt Xcode za pomocą pliku .xcworkspace. ML Kit jest obsługiwany w Xcode w wersji 13.2.1 lub nowszej.

Możesz już zacząć rozpoznawać tekst w obiektach Ink.

Tworzenie obiektu Ink

Głównym sposobem tworzenia obiektu Ink jest narysowanie go na ekranie dotykowym. W iOS, możesz używać UIImageView wraz z obsługą zdarzeń dotykowych , które rysują pociągnięcia na ekranie, a także przechowują punkty pociągnięć, aby utworzyć obiekt Ink. Ten ogólny wzorzec jest pokazany w tym fragmencie kodu. Bardziej kompletny przykład, który oddziela obsługę zdarzeń dotykowych, rysowanie na ekranie i zarządzanie danymi pociągnięć, znajdziesz w aplikacji z krótkim wprowadzeniem.

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];
}

Pamiętaj, że fragment kodu zawiera przykładową funkcję rysowania pociągnięcia w UIImageView, którą należy w razie potrzeby dostosować do swojej aplikacji. Podczas rysowania odcinków linii zalecamy używanie zaokrąglonych zakończeń, aby odcinki o zerowej długości były rysowane jako kropka (np. kropka nad małą literą „i”). Funkcja doRecognition() jest wywoływana po napisaniu każdego pociągnięcia i zostanie zdefiniowana poniżej.

Pobieranie instancji DigitalInkRecognizer

Aby przeprowadzić rozpoznawanie, musimy przekazać obiekt Ink do instancji DigitalInkRecognizer. Aby uzyskać instancję DigitalInkRecognizer, musimy najpierw pobrać model rozpoznawania dla wybranego języka i wczytać go do pamięci RAM. Można to zrobić za pomocą tego fragmentu kodu, który dla uproszczenia został umieszczony w metodzie viewDidLoad() i używa zakodowanej na stałe nazwy języka. Przykład tego, jak wyświetlić użytkownikowi listę dostępnych języków i pobrać wybrany język, znajdziesz w aplikacji z krótkim wprowadzeniem.

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];
}

Aplikacje z krótkim wprowadzeniem zawierają dodatkowy kod, który pokazuje, jak obsługiwać wiele pobrań jednocześnie i jak określić, które pobieranie się powiodło, obsługując powiadomienia o zakończeniu.

Rozpoznawanie obiektu Ink

Następnie przechodzimy do funkcji doRecognition(), która dla uproszczenia jest wywoływana z touchesEnded(). W innych aplikacjach można wywoływać rozpoznawanie dopiero po upływie limitu czasu lub gdy użytkownik naciśnie przycisk, aby je uruchomić.

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];
  }];
}

Zarządzanie pobieraniem modeli

Widzieliśmy już, jak pobrać model rozpoznawania. Te fragmenty kodu pokazują, jak sprawdzić, czy model został już pobrany, lub jak usunąć model, gdy nie jest już potrzebny, aby odzyskać miejsce na dane.

Sprawdzanie, czy model został już pobrany

Swift

let model : DigitalInkRecognitionModel = ...
let modelManager = ModelManager.modelManager()
modelManager.isModelDownloaded(model)

Objective-C

MLKDigitalInkRecognitionModel *model = ...;
MLKModelManager *modelManager = [MLKModelManager modelManager];
[modelManager isModelDownloaded:model];

Usuwanie pobranego modelu

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.");
                                }];
}

Wskazówki dotyczące poprawy dokładności rozpoznawania tekstu

Dokładność rozpoznawania tekstu może się różnić w zależności od języka. Dokładność zależy też od stylu pisania. Rozpoznawanie pisma odręcznego jest trenowane pod kątem obsługi wielu stylów pisania, ale wyniki mogą się różnić w zależności od użytkownika.

Oto kilka sposobów na zwiększenie dokładności rozpoznawania tekstu. Pamiętaj, że te techniki nie mają zastosowania do klasyfikatorów rysunków emoji, autodraw i kształtów.

Obszar pisania

Wiele aplikacji ma dobrze zdefiniowany obszar pisania, w którym użytkownik może wprowadzać dane. Znaczenie symbolu jest częściowo określane przez jego rozmiar w stosunku do rozmiaru obszaru pisania, w którym się znajduje. Na przykład różnica między małą i wielką literą „o” lub „c” a przecinkiem i ukośnikiem.

Podanie rozpoznawaniu szerokości i wysokości obszaru pisania może zwiększyć dokładność. Rozpoznawanie zakłada jednak, że obszar pisania zawiera tylko 1 wiersz tekstu. Jeśli fizyczny obszar pisania jest wystarczająco duży, aby użytkownik mógł napisać 2 lub więcej wierszy, możesz uzyskać lepsze wyniki, przekazując WritingArea o wysokości, która jest najlepszym oszacowaniem wysokości 1 wiersza tekstu. Obiekt WritingArea przekazywany do rozpoznawania nie musi dokładnie odpowiadać fizycznemu obszarowi pisania na ekranie. Zmiana wysokości WritingArea w ten sposób działa lepiej w niektórych językach niż w innych.

Gdy określasz obszar pisania, podaj jego szerokość i wysokość w tych samych jednostkach co współrzędne pociągnięć. Argumenty współrzędnych x i y nie mają wymagań dotyczących jednostek – interfejs API normalizuje wszystkie jednostki, więc liczy się tylko względny rozmiar i położenie pociągnięć. Możesz przekazywać współrzędne w dowolnej skali, która ma sens w Twoim systemie.

Kontekst poprzedzający

Kontekst poprzedzający to tekst, który bezpośrednio poprzedza pociągnięcia w obiekcie Ink, który próbujesz rozpoznać. Możesz pomóc rozpoznawaniu, informując je o kontekście poprzedzającym.

Na przykład odręczne litery „n” i „u” są często mylone. Jeśli użytkownik wpisał już częściowe słowo „arg”, może kontynuować pociągnięcia, które można rozpoznać jako „ument” lub „nment”. Określenie kontekstu poprzedzającego „arg” rozwiązuje niejednoznaczność, ponieważ słowo „argument” jest bardziej prawdopodobne niż „argnment”.

Kontekst poprzedzający może też pomóc rozpoznawaniu w identyfikowaniu podziałów wyrazów, czyli spacji między wyrazami. Możesz wpisać spację, ale nie możesz jej narysować. Jak więc rozpoznawanie może określić, kiedy kończy się jeden wyraz, a zaczyna następny? Jeśli użytkownik napisał już „hello” i kontynuuje, pisząc „world”, bez kontekstu poprzedzającego rozpoznawanie zwróci ciąg znaków „world”. Jeśli jednak określisz kontekst poprzedzający „hello”, model zwróci ciąg znaków „ world” ze spacją na początku, ponieważ „hello world” ma więcej sensu niż „helloword”.

Powinieneś podać jak najdłuższy ciąg znaków kontekstu poprzedzającego, maksymalnie 20 znaków, w tym spacje. Jeśli ciąg znaków jest dłuższy, rozpoznawanie używa tylko ostatnich 20 znaków.

Poniższy przykładowy kod pokazuje, jak zdefiniować obszar pisania i użyć obiektu RecognitionContext, aby określić kontekst poprzedzający.

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);
                         }];

Kolejność pociągnięć

Dokładność rozpoznawania zależy od kolejności pociągnięć. Rozpoznawanie oczekuje, że pociągnięcia będą wykonywane w kolejności, w jakiej ludzie naturalnie piszą, np. od lewej do prawej w przypadku języka angielskiego. Każdy przypadek, który odbiega od tego wzorca, np. napisanie zdania w języku angielskim zaczynając od ostatniego słowa, daje mniej dokładne wyniki.

Innym przykładem jest sytuacja, gdy słowo w środku obiektu Ink jest usuwane i zastępowane innym słowem. Poprawka znajduje się prawdopodobnie w środku zdania, ale pociągnięcia dotyczące poprawki są na końcu sekwencji pociągnięć. W takim przypadku zalecamy wysłanie nowo napisanego słowa osobno do interfejsu API i połączenie wyniku z poprzednimi rozpoznaniami za pomocą własnej logiki.

Radzenie sobie z niejednoznacznymi kształtami

W niektórych przypadkach znaczenie kształtu przekazanego do rozpoznawania jest niejednoznaczne. Na przykład prostokąt o bardzo zaokrąglonych krawędziach można uznać za prostokąt lub elipsę.

W takich niejasnych przypadkach można używać wyników rozpoznawania, gdy są dostępne. Wyniki są dostępne tylko w przypadku klasyfikatorów kształtów. Jeśli model jest bardzo pewny, wynik najlepszy będzie znacznie lepszy od drugiego najlepszego. Jeśli występuje niepewność, wyniki 2 najlepszych wyników będą zbliżone. Pamiętaj też, że klasyfikatory kształtów interpretują cały obiekt Ink jako pojedynczy kształt. Jeśli na przykład obiekt Ink zawiera prostokąt i elipsę obok siebie, rozpoznawanie może zwrócić jeden z nich (lub coś zupełnie innego), ponieważ pojedynczy kandydat rozpoznawania nie może reprezentować 2 kształtów.