Dzięki rozpoznawaniu cyfrowego atramentu w ML Kit możesz rozpoznawać tekst odręczny na powierzchni cyfrowej w setkach języków, a także klasyfikować szkice.
Wypróbuj
- Wypróbuj przykładową aplikację, aby zobaczyć, jak można używać tego interfejsu API.
Zanim zaczniesz
Uwzględnij w pliku Podfile te biblioteki ML Kit:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'Po zainstalowaniu lub zaktualizowaniu podów projektu otwórz projekt Xcode za pomocą pliku
.xcworkspace. Zestaw ML Kit jest obsługiwany w Xcode w wersji 13.2.1 lub nowszej.
Możesz teraz rozpocząć rozpoznawanie tekstu w obiektach Ink.
Tworzenie obiektu Ink
Głównym sposobem na zbudowanie obiektu Ink jest narysowanie go na ekranie dotykowym. W iOS możesz użyć UIImageView wraz z modułami obsługi zdarzeń dotknięcia, które rysują pociągnięcia na ekranie i zapisują ich punkty, aby utworzyć obiekt Ink. Ten ogólny wzorzec został przedstawiony w tym fragmencie kodu. Więcej informacji znajdziesz w aplikacji do szybkiego startu, która zawiera bardziej kompletny przykład, w którym oddzielono obsługę zdarzeń dotknięcia, rysowanie ekranu i zarządzanie danymi pociągnięć.
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ą należy dostosować do potrzeb aplikacji. Zalecamy używanie zaokrąglonych zakończeń podczas rysowania segmentów linii, aby segmenty o zerowej długości były rysowane jako kropki (jak kropka nad małą literą i). Funkcja doRecognition() jest wywoływana po każdym pociągnięciu i zostanie zdefiniowana poniżej.
Uzyskiwanie 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 wczytać go do pamięci RAM. Można to zrobić za pomocą tego fragmentu kodu, który dla uproszczenia jest umieszczony w metodzie viewDidLoad() i używa zakodowanej na stałe nazwy języka. Zobacz aplikację z krótkiego wprowadzenia, aby dowiedzieć się, 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 przewodnika Szybki start zawierają dodatkowy kod, który pokazuje, jak obsługiwać wiele pobrań jednocześnie i jak określić, które pobieranie się powiodło, przez obsługę powiadomień o zakończeniu.
Rozpoznawanie obiektu Ink
Następnie przechodzimy do funkcji doRecognition(), która dla uproszczenia jest wywoływana z funkcji touchesEnded(). W innych aplikacjach może być konieczne wywołanie rozpoznawania dopiero po upływie limitu czasu lub po naciśnięciu przez użytkownika przycisku, który uruchamia 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 pobieraniem modeli
Wiemy już, jak pobrać model rozpoznawania. Poniższe fragmenty kodu pokazują, jak sprawdzić, czy model został już pobrany, lub jak usunąć model, gdy nie jest już potrzebny, aby odzyskać miejsce 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 się różnić w zależności od języka. Dokładność zależy też od stylu pisania. Chociaż funkcja rozpoznawania pisma cyfrowego jest przeszkolona w zakresie obsługi wielu różnych stylów pisania, wyniki mogą się różnić 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 dotyczą klasyfikatorów rysunków w przypadku emotikonów, automatycznego rysowania i kształtów.
Obszar pisania
Wiele aplikacji ma dobrze zdefiniowany obszar pisania, w którym użytkownik może wprowadzać tekst. Znaczenie symbolu jest częściowo określane przez jego rozmiar w stosunku do rozmiaru obszaru pisania, który go zawiera. Na przykład różnicę między małą i wielką literą „o” lub „c” albo między przecinkiem a ukośnikiem.
Podanie rozpoznającemu szerokości i wysokości obszaru pisania może poprawić dokładność. Jednak rozpoznawanie zakłada, że obszar pisania zawiera tylko 1 wiersz tekstu. Jeśli fizyczny obszar pisania jest wystarczająco duży, aby użytkownik mógł napisać co najmniej 2 wiersze, lepsze wyniki możesz uzyskać, przekazując obiekt WritingArea o wysokości, która jest Twoim najlepszym oszacowaniem wysokości pojedynczego wiersza tekstu. Obiekt WritingArea przekazywany do modułu rozpoznawania nie musi dokładnie odpowiadać fizycznemu obszarowi pisania na ekranie. Zmiana wysokości obszaru pisania w ten sposób działa lepiej w niektórych językach.
Podczas określania obszaru pisania podaj jego szerokość i wysokość w tych samych jednostkach co współrzędne pociągnięcia. Argumenty współrzędnych x,y nie wymagają jednostek – interfejs API normalizuje wszystkie jednostki, więc liczy się tylko względny rozmiar i pozycja pociągnięć. Możesz przekazywać współrzędne w dowolnej skali, która jest odpowiednia dla Twojego systemu.
Kontekst poprzedzający
Kontekst wstępny to tekst, który bezpośrednio poprzedza pociągnięcia w obszarze Ink, który próbujesz rozpoznać. Możesz pomóc rozpoznawaniu, podając kontekst wstępny.
Na przykład litery „n” i „u” pisane kursywą są często mylone. Jeśli użytkownik wpisał już częściowe słowo „arg”, może kontynuować pociągnięcia, które można rozpoznać jako „ument” lub „nment”. Określenie kontekstu wstępnego „arg” rozwiązuje niejednoznaczność, ponieważ słowo „argument” jest bardziej prawdopodobne niż „argnment”.
Kontekst wstępny może też pomóc rozpoznawaniu w identyfikowaniu przerw między słowami, czyli spacji. Możesz wpisać spację, ale nie możesz jej narysować, więc jak rozpoznawanie pisma odręcznego może określić, gdzie kończy się jedno słowo, a zaczyna drugie? Jeśli użytkownik napisał już „hello” i kontynuuje, pisząc słowo „world”, bez kontekstu wstępnego moduł rozpoznawania zwraca ciąg „world”. Jeśli jednak określisz kontekst wstępny „hello”, model zwróci ciąg „ world” ze spacją na początku, ponieważ „hello world” ma więcej sensu niż „helloword”.
Należy podać jak najdłuższy ciąg znaków przed kontekstem, 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 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ść pociągnięć
Dokładność rozpoznawania zależy od kolejności pociągnięć. Rozpoznawanie oczekuje, że pociągnięcia będą występować w kolejności, w jakiej ludzie naturalnie piszą, na przykład od lewej do prawej w przypadku języka angielskiego. Każdy przypadek, który odbiega od tego wzorca, np. napisanie angielskiego zdania zaczynającego się od ostatniego słowa, daje mniej dokładne wyniki.
Innym przykładem jest usunięcie słowa w środku Ink i zastąpienie go innym słowem. Korekta prawdopodobnie znajduje się w środku zdania, ale pociągnięcia związane z korektą znajdują się na końcu sekwencji pociągnięć.
W takim przypadku zalecamy wysłanie nowo napisanego 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
W niektórych przypadkach znaczenie kształtu podanego w rozpoznawaniu jest niejednoznaczne. Na przykład prostokąt z bardzo zaokrąglonymi krawędziami może być postrzegany jako prostokąt lub elipsa.
Takie niejednoznaczne przypadki można rozwiązać, korzystając z wyników rozpoznawania, jeśli są dostępne. Tylko klasyfikatory kształtów podają wyniki. Jeśli model jest bardzo pewny, wynik pierwszego wyniku będzie znacznie lepszy niż drugiego. Jeśli występuje niepewność, wyniki dla 2 najlepszych 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ąt i elipsę obok siebie, rozpoznawanie może zwrócić jeden z tych kształtów (lub coś zupełnie innego), ponieważ pojedynczy kandydat do rozpoznania nie może reprezentować 2 kształtów.