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

Funkcja cyfrowego rozpoznawania atramentu w ML Kit pozwala rozpoznawać tekst odręczny cyfrowe 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. Po zainstalowaniu lub zaktualizowaniu podów swojego projektu otwórz projekt Xcode za pomocą: .xcworkspace. ML Kit jest obsługiwany w wersji Xcode 13.2.1 lub nowsza.

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

Tworzenie obiektu Ink

Głównym sposobem utworzenia obiektu Ink jest rysowanie go na ekranie dotykowym. W iOS: możesz użyć obiektu UIImageView razem z zdarzenie dotknięcia moduły obsługi które rysują ruchy na ekranie i zapisują ruchy punkty do zdobycia obiekt Ink. Ten ogólny schemat ilustruje poniższy kod . Zapoznaj się z krótkim wprowadzeniem bardziej kompletny przykład z podziałem na obsługę zdarzeń dotyku, rysowanie ekranu i i zarządzanie danymi o udarach.

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ę do rysowania kreski. widok UIImageView, które należy dostosować odpowiednio do potrzeb aplikacji. Zalecamy użycie okrągłe litery przy rysowaniu segmentów linii, tak aby odcinki o zerowej długości narysowany jako kropka (np. kropka na literze i). doRecognition() jest wywoływana po zapisaniu każdego pociągnięcia i zostanie zdefiniowana poniżej.

Pobieranie instancji DigitalInkRecognizer

Aby przeprowadzić rozpoznawanie, musimy przekazać obiekt Ink DigitalInkRecognizer instancję. Aby uzyskać instancję DigitalInkRecognizer, najpierw musimy pobrać model rozpoznawania dla wybranego języka. i załadować model do pamięci RAM. Można to zrobić za pomocą poniższego kodu który dla uproszczenia jest umieszczany w metodzie viewDidLoad() i używa zakodowaną na stałe nazwę języka. Zapoznaj się z krótkim wprowadzeniem Przykład pokazujący, jak wyświetlić użytkownikowi listę dostępnych języków i pobrać w wybranym języku.

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 pokazujący, jak obsługiwać pobierania plików w tym samym czasie i jak określić, które z nich zakończyły się powodzeniem, powiadomienia o fakcie ukończenia.

Rozpoznawanie obiektu Ink

Następnie dochodzimy do funkcji doRecognition(), która dla uproszczenia nazywa się od touchesEnded(). W innych aplikacjach warto wywołać może to nastąpić dopiero po upływie limitu czasu lub po naciśnięciu przez użytkownika przycisku i rozpoznawanie obiektów.

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 można pobrać model rozpoznawania. Następujący kod: ilustrują, jak sprawdzić, czy model został już pobrany; aby usunąć model, gdy nie jest już potrzebny do odzyskiwania 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 być różna w zależności od języka. Dokładność zależy też na styl pisania. Podczas gdy cyfrowe rozpoznawanie atramentu jest trenowane tak, by radzić sobie z wieloma stylami pisania, wyniki mogą być różne 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 są stosowane do klasyfikatorów rysunków dla emotikonów, automatycznego rysowania i kształtów.

Miejsce do pisania

Wiele aplikacji ma dobrze zdefiniowany obszar do pisania przez użytkownika. Znaczenie symbolu to zależy częściowo od rozmiaru obszaru pisania, w którym się znajduje. na przykład różnicę między małą lub wielką literą „o”; lub „c” i przecinek zamiast a. ukośnik prawy.

Określanie szerokości i wysokości obszaru pisania może zwiększyć dokładność rozpoznawania. Pamiętaj jednak: moduł rozpoznawania zakłada, że obszar do pisania zawiera tylko jeden wiersz tekstu. Jeśli obszar pisania jest wystarczająco duży, aby użytkownik mógł napisać dwa lub więcej wierszy, przez podanie obszaru do pisania o wysokości, która jest najdokładniejszym oszacowaniem wysokości jednego wiersza. Obiekt WriteArea przekazywany do modułu rozpoznawania nie musi odpowiadać z użyciem fizycznego obszaru do pisania na ekranie. Zmienianie wysokości obszaru do pisania w ten sposób w niektórych językach sprawdza się lepiej niż w innych.

Określasz obszar pisania, podając jego szerokość i wysokość w tych samych jednostkach co kreska. . Argumenty współrzędnych x,y nie mają wymagań dotyczących jednostek – interfejs API normalizuje wszystkie jednostek, więc zwracają uwagę tylko na ich względny rozmiar i pozycję pociągnięć. Masz prawo i w dowolnej skali.

Przed kontekstem

Wstępny kontekst to tekst, który bezpośrednio poprzedza kreski w Ink, które nie są w stanie rozpoznać. Możesz pomóc modułowi rozpoznawania, podając mu informacje o wstępnym kontekście.

na przykład pisane kursywą litery „n” i „u” są często mylone. Jeśli użytkownik ma już wpisało się częściowe słowo „arg”, mogą kontynuować ciągi znaków, które będą rozpoznawane jako „ument” czy „nment”. Określanie elementu „arg” powiązanego ze wstępnie kontekstem rozwiązuje niejednoznaczność, „argument” jest bardziej prawdopodobne niż „argnment”.

Kontekst wstępny może też pomóc systemowi rozpoznawania w wykrywaniu podziałów słów, czyli spacji między słowami. Dostępne opcje wpisz spację, ale nie możesz narysować znaku, więc jak moduł rozpoznawania może określić, kiedy kończy się jeden wyraz? i zaczyna następna? Jeśli użytkownik napisał już „Cześć” i kontynuuje od słowa pisanego „world” (bez wstępnego kontekstu) moduł rozpoznawania zwraca ciąg „world” (świat). Jeśli jednak określisz parametr „hello”, model zwróci ciąg znaków „ na świecie”, z wiodącym spacją, ponieważ „witaj” świecie”. ma sens niż „helloword”.

Musisz podać najdłuższy możliwy ciąg znaków poprzedzający kontekst, do 20 znaków, w tym spacje. Jeśli ciąg jest dłuższy, moduł rozpoznawania używa tylko ostatnich 20 znaków.

Poniższy przykładowy kod pokazuje, jak zdefiniować obszar do pisania i używać RecognitionContext obiekt 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ść ruchów

Dokładność rozpoznawania zależy od kolejności ruchów. Moduły rozpoznawania oczekują, że udar mózgu będzie występują w takiej kolejności, w jakiej ludzie naturalnie napisali; np. od lewej do prawej w przypadku języka angielskiego. Dowolne wychodząc z tego wzorca, np. wpisując angielskie zdanie zaczynające się od ostatniego słowa, daje mniej dokładne wyniki.

Innym przykładem jest usunięcie słowa w środku ciągu Ink i zastąpienie go lub inne słowo. Rewizja znajduje się prawdopodobnie w środku zdania, ale znajdziesz w niej kreski znajdują się na końcu sekwencji pociągnięć. W takim przypadku zalecamy wysłanie nowo utworzonego słowa oddzielnie do interfejsu API i scalanie z wcześniejszymi rozpoznawaniami przy użyciu własnej logiki.

Radzenie sobie z niejednoznacznymi kształtami

W niektórych przypadkach znaczenie kształtu przekazanego modułowi rozpoznawania jest niejednoznaczne. Dla: na przykład prostokąt z mocno zaokrąglonymi krawędziami może być prostokątny albo elipsa.

W takich niejasnych przypadkach można użyć wyników rozpoznawania, jeśli są dostępne. Tylko wyniki są podawane przez klasyfikatory kształtów. Jeśli model jest bardzo pewny, wynik najlepszego to dużo lepsze niż drugi najlepszy. Jeśli brak pewności co do dokładności, wyniki dwóch pierwszych pozycji będą być blisko. Pamiętaj też, że klasyfikatory kształtów interpretują cały element Ink jako jednego kształtu. Jeśli na przykład Ink zawiera prostokąt i elipsę obok każdego z nich moduł rozpoznawania może zwrócić jeden lub drugi element (albo coś zupełnie innego) jako ponieważ jeden kandydat do rozpoznawania nie może reprezentować dwóch kształtów.