Rozpoznawanie cyfrowych atramentów z ML Kit na iOS

Zadbaj o dobrą organizację dzięki kolekcji Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.

Dzięki cyfrowemu rozpoznawaniu tuszu w ML Kit możesz rozpoznawać tekst pisany odręcznie na cyfrowej powierzchni w setkach języków i klasyfikować szkice.

Wypróbuj

Zanim zaczniesz

  1. W pliku Podfile umieść te biblioteki ML Kit:

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. Gdy zainstalujesz lub zaktualizujesz pody projektu, otwórz projekt Xcode, używając jego .xcworkspace. ML Kit jest obsługiwany w Xcode w wersji 13.2.1 lub nowszej.

Teraz możesz zacząć rozpoznawać tekst w obiektach Ink.

Tworzenie obiektu Ink

Głównym sposobem na zbudowanie obiektu Ink jest narysowanie go na ekranie dotykowym. W systemie iOS możesz użyć UIImageView wraz z modułami obsługi zdarzeń dotykowych, które rysują kreski na ekranie, a także przechowują punkty kreski, aby utworzyć obiekt Ink. Ogólny wzorzec jest przedstawiony w poniższym fragmencie kodu. Zobacz aplikację Szybki start, gdzie znajdziesz kompletny przykład, który przedstawia obsługę zdarzeń dotykowych, rysowania ekranu i zarządzania kreskami.

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 funkcję przykładową do rysowania kreski w funkcji UIImageView, którą należy w razie potrzeby dostosować do aplikacji. Zalecamy, aby przy rysowaniu segmentów liniowych używać wielkich liter, tak aby segmenty o zerowej długości nie były rysowane jako kropki (czyli kropki w małych literach i). Funkcja doRecognition() jest wywoływana po napisaniu każdego pociągnięcia i zostanie zdefiniowana poniżej.

Pobieranie instancji DigitalInkRecognizer

Aby umożliwić rozpoznawanie, musimy przekazać obiekt Ink do instancji DigitalInkRecognizer. Aby uzyskać instancję DigitalInkRecognizer, musisz najpierw pobrać model modułu rozpoznawania dla wybranego języka i wczytać go do pamięci RAM. Możesz to zrobić, korzystając z poniższego fragmentu kodu, który dla uproszczenia jest umieszczany w metodzie viewDidLoad() i zawiera zakodowaną na stałe nazwę języka. Zobacz aplikację Szybki start, aby zobaczyć, jak wyświetlić użytkownikowi listę dostępnych języków i pobrać wybrany język.

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ótkimi instrukcjami zawierają dodatkowy kod, który pokazuje, jak jednocześnie pobierać wiele plików oraz w jaki sposób zarządzać powiadomieniami o ich ukończeniu.

Rozpoznawanie obiektu Ink

Następnie przechodzimy do funkcji doRecognition(), która dla uproszczenia nosi nazwę touchesEnded(). W innych aplikacjach można wywoływać rozpoznawanie dopiero po przekroczeniu limitu czasu lub gdy użytkownik naciśnie przycisk, aby je włączyć.

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 pobranymi modelami

Wiemy już, jak pobrać model rozpoznawania twarzy. Poniższe fragmenty kodu pokazują, jak sprawdzić, czy model został już pobrany, lub usunąć go, jeśli nie jest już potrzebny do odzyskania miejsca 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 zwiększania 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. Chociaż model cyfrowych atramentów jest trenowany do obsługi różnych rodzajów pisma, wyniki mogą być różne w zależności od użytkownika.

Oto kilka sposobów na zwiększenie dokładności mechanizmu rozpoznawania tekstu. Te metody nie mają zastosowania w przypadku klasyfikatorów rysunków emotikonów, automatycznych rysunków i kształtów.

Obszar pisania

Wiele aplikacji ma dobrze sprecyzowany obszar wpisywania tekstu przez użytkownika. Znaczenie symbolu jest częściowo określane przez jego wielkość w stosunku do rozmiaru obszaru roboczego, w którym się on znajduje. Na przykład różnicę między małymi a dużymi literami „o” lub „c” oraz przecinek zamiast ukośnika.

Podanie modułu rozpoznającego szerokość i wysokość obszaru pisania może zwiększyć dokładność. Moduł rozpoznawania zakłada jednak, że obszar zapisu zawiera tylko jeden wiersz tekstu. Jeśli fizyczny obszar do pisania jest wystarczająco duży, aby użytkownik mógł napisać co najmniej 2 wiersze, możesz uzyskać lepsze wyniki, przekazując obszar do pisania z wysokością, która jest szacowaną wysokością pojedynczego wiersza tekstu. Obiekt WriteArea przekazywany do modułu rozpoznawania nie musi dokładnie odpowiadać fizycznemu obszarowi pisania na ekranie. W ten sposób zmiana wysokości obszaru pisania działa lepiej w niektórych językach niż w innych.

Podczas określania obszaru pisania podaj szerokość i wysokość w tych samych jednostkach, co współrzędne kreski. Argumenty współrzędnej x,y nie mają wymagań dotyczących jednostki – interfejs API normalizuje wszystkie jednostki, więc najważniejsza jest względna wielkość i położenie kreski. Możesz wykorzystać współrzędne w dowolnej skali.

Wstęp do kontekstu

Wstępny kontekst to tekst, który bezpośrednio poprzedza kreski Ink, które próbujesz rozpoznać. Możesz pomóc modułowi rozpoznawania, informując go o kontekście.

Na przykład kursywę „n” i „u” często myli się ze sobą nawzajem. Jeśli użytkownik ma już wpisane częściowe słowo „argument”, może kontynuować rysowanie, które można rozpoznać jako „ument” lub „mentor”. Określenie kontekstu w poddaniu „argument” rozwiązuje wątpliwości, ponieważ słowo „argument” jest bardziej prawdopodobne niż argument „argument”.

Kontekst może też pomóc w rozpoznawaniu podziałów słów oraz spacji między słowami. Można wpisać spację, ale nie rysować znaku. Jak moduł rozpoznający może określić, kiedy kończy się jedno słowo i zaczyna się drugie? Jeśli użytkownik wpisał już „Cześć” i chce wpisać ciąg „world”, bez kontekstu, moduł rozpoznawania zwróci ciąg znaków „world”. Jeśli jednak określisz kontekst „hello”, model zwróci ciąg znaków „world” ze spacją na początku, ponieważ „hello world” ma sens więcej niż „helloword”.

Musisz wpisać najdłuższy ciąg tekstowy z kontekstu (maksymalnie 20 znaków razem ze spacjami). Jeśli ciąg jest dłuższy, moduł rozpoznawania wykorzysta tylko 20 ostatnich znaków.

Przykładowy kod poniżej pokazuje, jak zdefiniować obszar pisania i użyć obiektu RecognitionContext, aby określić kontekst wstępny.

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ść kresek

Dokładność rozpoznawania zależy od kolejności pociągnięć. Moduły rozpoznawania spodziewają się, że udarem wystąpią w kolejności, w jakiej naturalnie brzmią, np. od lewej do prawej w przypadku języka angielskiego. Każdy przypadek, który różni się od danego wzorca, na przykład angielskie zdanie zaczynające się od ostatniego słowa, daje mniej dokładne wyniki.

Inny przykład to usunięcie słowa w środku elementu Ink i zastąpienie go innym słowem. Wersja prawdopodobnie znajduje się w połowie zdania, ale kreski są na końcu sekwencji kreski. W takim przypadku zalecamy wysłanie nowego słowa osobno do interfejsu API i scalenie wyniku z wcześniejszymi rozpoznaniami przy użyciu własnej logiki.

Radzenie sobie z niejednoznacznymi kształtami

Zdarza się, że znaczenie modułu rozpoznawania jest niejednoznaczne. Na przykład prostokąt z bardzo zaokrąglonymi krawędziami może być prostokątem lub wielokropkiem.

Niejasne przypadki można rozwiązać, korzystając z wyników rozpoznawania, gdy są dostępne. Wyniki zapewniają tylko klasyfikatory kształtów. Jeśli model ma dużą pewność, wyniki pierwszego wyniku będą znacznie lepsze od drugiego wyniku. W razie niepewności wyniki dotyczące 2 pierwszych wyników będą zamknięte. Pamiętaj też, że klasyfikatory kształtu interpretują całą Ink jako pojedynczy kształt. Jeśli na przykład Ink zawiera obok siebie prostokąt i kropek obok siebie, moduł rozpoznawania może zwrócić wynik (lub coś zupełnie innego), ponieważ pojedynczy kandydat do rozpoznawania nie może reprezentować dwóch kształtów.