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

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

Wypróbuj

Zanim zaczniesz

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

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. Po zainstalowaniu lub zaktualizowaniu podów projektu otwórz projekt Xcode za pomocą .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

Podstawowym sposobem utworzenia obiektu Ink jest rysowanie na ekranie dotykowym. W systemie iOS można użyć obiektu UIImageView razem z modułami obsługi zdarzeń dotknięcia, które rysują i zapisują pociągnięcia na ekranie w celu utworzenia obiektu Ink. Ten ogólny wzorzec ilustruje ten fragment kodu. Dokładniejszy przykład znajdziesz w aplikacji szybkiego startu, w którym podzielisz obsługę zdarzeń dotyku, rysowanie ekranu i zarządzanie danymi kresek.

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

Zwróć uwagę, że fragment kodu zawiera przykładową funkcję rysowania linii w UIImageView, którą musisz odpowiednio dostosować do swojej aplikacji. Przy rysowaniu segmentów liniowych zalecamy używanie zaokrąglonych liter, aby segmenty o zerowej długości były przedstawiane jako kropka (kropka na małej litery i). Funkcja doRecognition() jest wywoływana po zapisaniu każdego kreski i jest definiowana poniżej.

Pobieranie instancji DigitalInkRecognizer

Aby przeprowadzić rozpoznawanie, musimy przekazać obiekt Ink do instancji DigitalInkRecognizer. Aby uzyskać instancję DigitalInkRecognizer, najpierw musimy pobrać model rozpoznawania dla wybranego języka i załadować model w pamięci RAM. Można to zrobić za pomocą poniższego fragmentu kodu, który dla uproszczenia został umieszczony w metodzie viewDidLoad() i wykorzystuje zakodowaną na stałe nazwę języka. Przykład tego, jak wyświetlić użytkownikowi listę dostępnych języków i pobrać wybrany język, znajdziesz w opisie aplikacji szybkiego startu.

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łużyć wiele pobrań w tym samym czasie i jak sprawdzić, które pobieranie się udało, na podstawie powiadomień o zakończeniu pobierania.

Rozpoznawanie obiektu Ink

Następnie dochodzimy do funkcji doRecognition(), która dla uproszczenia nazywa się touchesEnded(). W innych aplikacjach można wywoływać rozpoznawanie dopiero po upływie limitu czasu lub po naciśnięciu przez użytkownika przycisku aktywującego rozpoznawanie.

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. Poniższe fragmenty kodu pokazują, jak sprawdzić, czy model został już pobrany, lub usunąć model, gdy nie jest już potrzebny do zwolnienia 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 poprawy dokładności rozpoznawania tekstu

Dokładność rozpoznawania tekstu może być różna w zależności od języka. Dokładność zależy też od stylu pisania. Chociaż technologia ta jest trenowana pod kątem obsługi różnego rodzaju stylów pisania, wyniki mogą być różne w zależności od użytkownika.

Oto kilka sposobów na zwiększenie dokładności modułu rozpoznawania tekstu. Pamiętaj, że te techniki nie dotyczą klasyfikatorów emotikonów, automatycznego rysowania i kształtów.

Obszar do pisania

Wiele aplikacji ma dobrze zdefiniowany obszar do wprowadzania danych przez użytkownika. Znaczenie symbolu jest częściowo określane przez jego rozmiar w odniesieniu do powierzchni, w której się on znajduje. Na przykład różnica między małą lub wielką literą „o” lub „c” oraz przecinkiem i ukośnikiem.

Informowanie modułu rozpoznawania o szerokości i wysokości obszaru pisania może zwiększyć dokładność. Moduł rozpoznawania zakłada jednak, że obszar pisania zawiera tylko jeden wiersz tekstu. Jeśli fizyczny obszar pisania jest wystarczająco duży, aby użytkownik mógł napisać co najmniej 2 wiersze, można uzyskać lepsze wyniki, przekazując obszar Pisanie o wysokości, która najlepiej określa wysokość pojedynczego wiersza tekstu. Obiekt WriteArea, który przekazujesz do modułu rozpoznawania, nie musi dokładnie odpowiadać fizycznemu obszarowi do pisania na ekranie. Ta zmiana wysokości obszaru pisania działa lepiej w niektórych językach niż w innych.

Określając obszar do pisania, określ jego szerokość i wysokość w tych samych jednostkach co współrzędne kreski. Argumenty współrzędnych x,y nie mają wymagań jednostki – interfejs API normalizuje wszystkie jednostki, więc liczą się tylko względy rozmiar i położenie kresek. Możesz wprowadzać współrzędne w dowolnej skali.

Przed kontekstem

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

Na przykład litery „n” i „u” są często mylone ze sobą. Jeśli użytkownik wpisał już część słowa „argument”, może kontynuować rysowanie, które zostanie rozpoznane jako „ument” lub „nment”. Określenie argumentu „argment” pozwala rozwiązać ten problem, ponieważ słowo „argument” jest bardziej prawdopodobne niż „argnment”.

Wstępny kontekst może również pomóc modułowi rozpoznawania w identyfikowaniu podziałów słów, czyli spacji między nimi. Można wpisywać spację, ale nie można jej narysować. Jak zatem moduł rozpoznawania może określić, kiedy kończy się jedno słowo, a zaczyna następne? Jeśli użytkownik napisał już „cześć”, a ciągnie dalej z tekstem „world”, moduł rozpoznawania zwróci ciąg „world” (bez kontekstu). Jeśli jednak podasz w kontekście ciąg „hello”, model zwróci ciąg „world” z początkową spacją, ponieważ słowo „helloworld” ma większą sens niż „helloword”.

Musisz podać jak najdłuższy ciąg znaków dostępny przed kontekstem (maksymalnie 20 znaków łącznie ze spacjami). Jeśli jest dłuższy, moduł rozpoznawania użyje tylko ostatnich 20 znaków.

Przykładowy kod poniżej pokazuje, jak określić obszar do pisania i użyć obiektu RecognitionContext do określenia wstępnego kontekstu.

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 jest zależna od kolejności kresek. Moduły rozpoznawania oczekują, że kreski będą występować w takiej kolejności, w jakiej ludzie piszą w sposób naturalny, np. od lewej do prawej w przypadku języka angielskiego. Strony, które odstają od tego wzorca, na przykład pisanie angielskie zdania rozpoczynającego się od ostatniego słowa, dają mniej dokładne wyniki.

Innym przykładem jest usunięcie słowa w elemencie Ink i zastąpienie go innym słowem. Wersja prawdopodobnie znajduje się w środku zdania, ale zmiany kreski znajdują się na końcu sekwencji kresek. W takim przypadku zalecamy wysłanie nowo napisanego słowa oddzielnie do interfejsu API i scalenie wyniku z wcześniejszymi rozpoznaniami za pomocą własnej logiki.

Radzenie sobie z niejednoznacznymi kształtami

W niektórych przypadkach znaczenie kształtu przekazanego modułowi rozpoznawania jest niejednoznaczne. Na przykład prostokąt z bardzo zaokrąglonymi krawędziami może być prezentowany jako prostokąt lub elipsa.

W przypadku takich niejasnych przypadków można zastosować oceny rozpoznawania, jeśli są dostępne. Wyniki podają tylko klasyfikatory kształtów. Jeśli model jest bardzo pewny, wynik z najwyższego wyniku będzie dużo wyższy niż drugi najlepszy. W przypadku niepewności wyniki dwóch pierwszych wyników będą zbliżone. Pamiętaj też, że klasyfikatory kształtów interpretują cały Ink jako pojedynczy kształt. Jeśli na przykład element Ink zawiera obok siebie prostokątny i elipsę, moduł rozpoznawania może w wyniku wyświetlić jeden albo drugi element (lub coś zupełnie innego), ponieważ pojedynczy kandydat do rozpoznawania nie może reprezentować 2 kształtów.