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

Funkcja rozpoznawania atramentu cyfrowego w ML Kit pozwala rozpoznawać tekst odręczny na cyfrowej powierzchni w setkach języków oraz 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ą zasobu .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 utworzenia obiektu Ink jest rysowanie go na ekranie dotykowym. W iOS można użyć obiektu UIImageView razem z modułami obsługi zdarzeń dotknięcia, które rysują ruchy na ekranie i zapisują ich punkty w celu utworzenia obiektu Ink. Ogólny wzorzec pokazuje podany niżej fragment kodu. Bardziej szczegółowy przykład, który rozdziela obsługę zdarzeń dotyku, rysowanie na ekranie i zarządzanie danymi ruchów, znajdziesz w aplikacji Krótkie wprowadzenie.

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 do obiektu UIImageView, którą należy dostosować w razie potrzeby do aplikacji. Zalecamy stosowanie wielkich liter podczas rysowania segmentów linii, by segmenty o zerowej długości były narysowane w postaci kropki (np. kropka na małej literze i). Funkcja doRecognition() jest wywoływana po zapisaniu każdego pociągnięcia i zostanie definiowana 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 załadować go do pamięci RAM. Możesz to zrobić za pomocą podanego niżej fragmentu kodu. Dla uproszczenia został on umieszczony w metodzie viewDidLoad() i używa zakodowanej na stałe nazwy języka. Zobacz aplikację z krótkim wprowadzeniem, aby się dowiedzieć, 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ótkim wprowadzeniem zawierają dodatkowy kod, który pokazuje, jak obsługiwać wiele pobrań jednocześnie i jak określić, które pobieranie zakończyło się powodzeniem przez obsługę powiadomień o ukończeniu.

Rozpoznawanie obiektu Ink

Teraz przejdźmy do funkcji doRecognition(), która dla uproszczenia jest wywoływana z metody touchesEnded(). W innych aplikacjach rozpoznawanie może być wywoływane dopiero po upływie czasu oczekiwania lub po naciśnięciu przez użytkownika przycisku.

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. Fragmenty kodu poniżej pokazują, jak sprawdzić, czy model został już pobrany, czy 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ż od stylu pisania. Chociaż cyfrowe rozpoznawanie atramentu jest trenowane tak, aby obsługiwać wiele rodzajów stylów pisania, wyniki mogą być różne dla każdego 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 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 zależy częściowo od jego rozmiaru w zależności od wielkości obszaru do pisania, w którym się on znajduje. Może to być na przykład różnica między małą lub wielką literą „o” lub „c” i przecinkiem a ukośnikiem.

Określanie szerokości i wysokości obszaru pisania może zwiększyć dokładność rozpoznawania. Moduł rozpoznawania zakłada jednak, że obszar do pisania zawiera tylko jeden wiersz tekstu. Jeśli obszar do pisania jest na tyle duży, że użytkownik może napisać 2 lub więcej wierszy, lepsze wyniki uzyskasz, przesyłając obszar do pisania o wysokości, która najlepiej szacuje wysokość 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 do pisania działa lepiej w niektórych językach niż w innych.

Określasz obszar do pisania, określając jego szerokość i wysokość w tych samych jednostkach co współrzędne kreski. Argumenty współrzędnych x,y nie mają wymagań dotyczących jednostek – interfejs API normalizuje wszystkie jednostki, a jedynie istotną rzeczą jest względny rozmiar i położenie kresek. Możesz podawać 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, podając mu informacje 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ęściowe słowo „argument”, może kontynuować kreski, które rozpoznają jako „ument” lub „nment”. Określenie wstępnego kontekstu „argument” rozwiązało niejednoznaczność, ponieważ słowo „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. Możesz wpisać spację, ale nie możesz narysować jakiegoś. Jak więc moduł rozpoznawania może określić, kiedy jedno słowo się kończy, a zaczyna następne? Jeśli użytkownik napisze już „Cześć” i przejdzie do słowa „world” (świat), bez wstępnego kontekstu moduł rozpoznawania zwróci ciąg „world” (świat). Jeśli jednak określisz prefiks „hello”, model zwróci ciąg znaków „world” ze spacją na początku, ponieważ „hello world” ma sens niż „helloword”.

Należy podać najdłuższy możliwy ciąg znaków poprzedzający kontekst (do 20 znaków łącznie ze spacjami). 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ż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ść ruchów

Dokładność rozpoznawania zależy od kolejności ruchów. Moduły rozpoznawania oczekują, że ruchy będą się odbywać w takiej kolejności, w jakiej piszą normalnie, np. od lewej do prawej w przypadku języka angielskiego. Każdy przypadek odchodzący od tego wzorca, na przykład napisanie w języku angielskim zdania zaczynającego się od ostatniego słowa, dają mniej dokładne wyniki.

Inny przykład to usunięcie słowa w środku ciągu Ink i zastąpienie go innym słowem. Rewizja znajduje się prawdopodobnie w środku zdania, ale pociągnięcia w wersji znajdują się na końcu. W takim przypadku zalecamy wysłanie nowo utworzonego słowa osobno do interfejsu API i połączenie 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ć prostokątny lub elipsa.

W takich niejasnych przypadkach można użyć wyników rozpoznawania, jeśli są dostępne. Wyniki są podawane tylko przez klasyfikatory kształtów. Jeśli model ma dużą pewność, wynik najlepszego wyniku będzie znacznie lepszy niż drugi najlepszy. W przypadku wątpliwości wyniki 2 pierwszych 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 Ink zawiera obok siebie prostokąt i elipsę, moduł rozpoznawania może zwrócić jeden lub drugi (albo coś zupełnie innego) jako wynik, ponieważ pojedynczy kandydat do rozpoznawania nie może reprezentować dwóch kształtów.