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ęcznie napisany na platformie cyfrowej w setkach języków, a także klasyfikować szkice.

Wypróbuj

Zanim zaczniesz

  1. Dołącz do pliku Podfile te biblioteki ML Kit:

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. Po zainstalowaniu lub zaktualizowaniu podów w projekcie otwórz projekt Xcode, korzystając z .xcworkspace. ML Kit obsługuje 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 narysowanie go na ekranie dotykowym. W systemie iOS możesz użyć obiektu UIImageView z modułami obsługi zdarzeń dotknięcia, które rysują pociągnięcia na ekranie i zapisują je w celu utworzenia obiektu Ink. Ten ogólny wzorzec przedstawiamy w poniższym fragmencie kodu. Pełen przykład, w którym omówiono obsługę zdarzeń dotknięcia, rysowanie ekranu i zarządzanie danymi kresek, znajdziesz w krótkim opisie aplikacji.

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. Zalecamy, aby podczas rysowania segmentów liniowych używać liter zaokrąglonych, aby segmenty o zerowej długości były rysowane jako kropki (np. kropka na małej literze i). Funkcja 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 do instancji DigitalInkRecognizer. Aby uzyskać instancję DigitalInkRecognizer, najpierw musimy pobrać model rozpoznawania dla wybranego języka i załadować model do pamięci RAM. Możesz 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 krótkim opisie aplikacji.

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ótkiego wprowadzenia zawierają dodatkowy kod, który pokazuje, jak obsłużyć kilka pobrań w tym samym czasie i jak sprawdzić, które pobieranie się udało, na podstawie powiadomień o zakończeniu pobierania.

Rozpoznaj obiekt Ink

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

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 modelu

Omówiliśmy już, jak pobrać model rozpoznawania. Poniższe fragmenty kodu pokazują, jak sprawdzić, czy model został już pobrany, oraz jak go usunąć, gdy nie jest już potrzebny do odzyskania miejsca na dane.

Sprawdzanie, czy model nie 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ż od stylu pisania. Chociaż technologia ta jest wytrenowana pod kątem obsługi różnego rodzaju stylów pisania, wyniki mogą być różne w przypadku różnych użytkowników.

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

Obszar do pisania

Wiele aplikacji ma wyraźnie określony obszar do wprowadzania danych przez użytkownika. Znaczenie symbolu jest częściowo określane na podstawie jego rozmiaru w odniesieniu do obszaru, który się w nim znajduje. Na przykład różnica między małą lub wielką literą „o” lub „c” oraz przecinkiem i ukośnikiem.

Informacja o szerokości i wysokości obszaru pisania może zwiększyć dokładność rozpoznawania tekstu. Moduł rozpoznawania zakłada jednak, że obszar pisania zawiera tylko jeden wiersz tekstu. Jeśli fizyczny obszar pisania jest dostatecznie duży, by umożliwić użytkownikowi wpisanie dwóch lub więcej wierszy, można uzyskać lepsze wyniki, przesyłając pole WriteArea 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, podaj jego szerokość i wysokość w tych samych jednostkach co współrzędne linii. Argumenty współrzędnych x i y nie wymagają jednostek – interfejs API normalizuje wszystkie jednostki, więc liczą się tylko względy rozmiar i położenie pociągnięć. Możesz przemieszczać się we współrzędnych w skali odpowiedniej dla Twojego układu.

Przed kontekstem

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

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 można rozpoznać jako „ument” lub „nment”. Określenie „argument” jako wstępnego kontekstu rozwiewa tę wątpliwości, ponieważ słowo „argument” jest bardziej prawdopodobne niż „argnment”.

Podgląd wstępny może też ułatwić modułowi rozpoznawania rozpoznawanie przerw i odstępów między słowami. Znak spacji można wpisać, ale nie można tego narysować. Jak zatem moduł rozpoznawania może określić, kiedy kończy się jedno słowo, a zaczyna się następne? Jeśli użytkownik napisał „cześć” i dalej wpisuje słowo „world”, moduł rozpoznawania zwróci ciąg „world”. Jeśli jednak określisz wstępnie kontekst „hello”, model zwróci ciąg „world” z początkową spacją, ponieważ „helloworld” ma większy sens niż „helloword”.

Musisz podać najdłuższy możliwy ciąg znaków przed kontekstem (maksymalnie 20 znaków łącznie ze spacjami). Jeśli jest dłuższy, moduł rozpoznawania użyje tylko 20 ostatnich 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 uzależniona od kolejności kresek. Moduły rozpoznawania spodziewają się pociągnięć w kolejności, w jakiej ludzie napiszą tekst, np. od lewej do prawej w przypadku języka angielskiego. Każdy przypadek odbiegający od tego wzorca, np. wpisanie zdania po angielsku z ostatnim słowem, pozwala uzyskać mniej dokładne wyniki.

Innym przykładem jest usunięcie słowa w elemencie Ink i zastąpienie go innym słowem. Poprawka jest prawdopodobnie w środku zdania, ale kreski umożliwiające powtórzenie znajdują się na końcu sekwencji kresek. W takim przypadku zalecamy wysłanie nowo napisanego słowa osobno do interfejsu API i połączenie jego wyniku z wcześniejszymi rozpoznaniami 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. Na przykład prostokąt z bardzo zaokrąglonymi krawędziami może być widoczny jako prostokąt lub elipsa.

W takich niejasnych przypadkach można skorzystać z wyników rozpoznawania, gdy są dostępne. Wyniki dają tylko klasyfikatory kształtów. Jeśli model jest bardzo pewny, najlepszy wynik będzie znacznie 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 element Ink jako pojedynczy kształt. Jeśli na przykład Ink zawiera prostokątny i elipsę obok siebie, moduł rozpoznawania może zwrócić jeden albo drugi (lub coś zupełnie innego), ponieważ pojedynczy kandydat do rozpoznawania nie może reprezentować 2 kształtów.