Dzięki funkcji rozpoznawania pisma cyfrowego w ML Kit możesz rozpoznawać tekst pisany na cyfrowej powierzchni w setkach języków oraz klasyfikować szkice.
Wypróbuj
- Aby zobaczyć przykład użycia tego interfejsu API, wypróbuj przykładową aplikację.
Zanim zaczniesz
Dodaj do pliku Podfile te biblioteki ML Kit:
pod 'GoogleMLKit/DigitalInkRecognition', '7.0.0'
Po zainstalowaniu lub zaktualizowaniu pakietów projektu otwórz projekt Xcode za pomocą
.xcworkspace
. ML Kit jest obsługiwany w Xcode w wersji 13.2.1 lub nowszej.
Możesz już rozpoznawać tekst w obiektach Ink
.
Tworzenie obiektu Ink
Głównym sposobem tworzenia obiektu Ink
jest narysowanie go na ekranie dotykowym. W iOS możesz użyć UIImageView oraz obsług zdarzeń dotyku, które rysują ścieżki na ekranie, a także przechowują punkty ścieżek, aby zbudować obiekt Ink
. Ten ogólny schemat przedstawia poniższy fragment kodu. Pełniejszy przykład, który oddziela obsługę zdarzeń dotykowych, rysowanie na ekranie i zarządzanie danymi pociągnięć, znajdziesz w aplikacji quickstart.
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]; }
Pamiętaj, że fragment kodu zawiera przykładową funkcję do rysowania obrysu w UIImageView, którą należy dostosować do potrzeb aplikacji. Zalecamy używanie zaokrąglonych czcionek podczas rysowania odcinków linii, aby odcinki o długości 0 były rysowane jako kropka (podobnie jak kropka na małej literze i). Funkcja doRecognition()
jest wywoływana po zapisaniu każdego skrótu i będzie 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ć go do pamięci RAM. Można to zrobić za pomocą tego fragmentu kodu, który ze względów upraszczających znajduje się w metodie viewDidLoad()
i korzysta z zakodowanego na stałe nazwy języka. Przykład wyświetlania użytkownikowi listy dostępnych języków i pobierania wybranego języka znajdziesz w aplikacji krótkiego wprowadzenia.
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 podręcznikiem szybkiego uruchamiania zawierają dodatkowy kod, który pokazuje, jak obsługiwać wiele pobrań jednocześnie i jak określić, które pobieranie zakończyło się sukcesem, obsługując powiadomienia o zakończeniu.
Rozpoznawanie obiektu Ink
Następnie przechodzimy do funkcji doRecognition()
, która dla uproszczenia jest wywoływana z poziomu touchesEnded()
. W innych zastosowaniach rozpoznawanie może być wywoływane tylko po upływie określonego czasu lub po naciśnięciu przez użytkownika przycisku uruchamiają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 pobieraniem modeli
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, 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ż rozpoznawanie pisma cyfrowego jest trenowane pod kątem obsługi wielu stylów pisania, wyniki mogą się różnić w zależności od użytkownika.
Oto kilka sposobów na poprawę dokładności rozpoznawania tekstu. Pamiętaj, że te techniki nie dotyczą klasyfikatorów rysunków dla 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 pisma, który go zawiera. Na przykład różnica między małą i wielką literą „o” lub „c” albo między przecinkiem a ukośnikiem.
Podanie rozpoznawalności szerokości i wysokości obszaru pisania może zwiększyć dokładność. Rozpoznawca zakłada jednak, że obszar pisania zawiera tylko jeden wiersz tekstu. Jeśli fizyczna powierzchnia pisania jest wystarczająco duża, aby umożliwić użytkownikowi zapisanie 2 lub więcej wierszy, możesz uzyskać lepsze wyniki, przekazując WritingArea z wysokość odpowiadającą Twojemu najlepszemu szacowaniu wysokości pojedynczego wiersza tekstu. Obiekt WritingArea przekazywany do rozpoznawacza nie musi dokładnie odpowiadać fizycznej obszarowi pisania na ekranie. Zmiana wysokości WritingArea w ten sposób działa lepiej w niektórych językach niż w innych.
Podczas określania obszaru pisania podaj jego szerokość i wysokość w tych samych jednostkach co współrzędne obrysu. Argumenty x i y nie wymagają podania jednostki miary – interfejs API normalizuje wszystkie jednostki, więc jedyne, co się liczy, to względna wielkość i położenie kresek. Możesz przesłać współrzędne w dowolnej skali, która ma sens w Twoim systemie.
Kontekst wstępny
Wstępny kontekst to tekst bezpośrednio poprzedzający znaki w Ink
, które chcesz rozpoznać. Możesz pomóc rozpoznawacza, podając mu wstępny kontekst.
Na przykład litery „n” i „u” pisane kursywą są często ze sobą mylone. Jeśli użytkownik wpisał już część słowa „arg”, może kontynuować pisanie za pomocą ruchów, które mogą zostać rozpoznane jako „ument” lub „nment”. Podanie wstępnego kontekstu „arg” rozwiązuje niejednoznaczność, ponieważ słowo „argument” jest bardziej prawdopodobne niż „argnment”.
Kontekst sytuacyjny może też pomóc rozpoznawacza w identyfikowaniu podziału wyrazów i spacji między wyrazami. Możesz wpisać spację, ale nie możesz jej narysować. Jak więc rozpoznawacz może określić, kiedy kończy się jedno słowo, a zaczyna następne? Jeśli użytkownik napisał już „hello” (cześć) i kontynuuje pisanie słowa „world” (świat), bez kontekstu rozpoznawanie zwraca ciąg znaków „world”. Jeśli jednak podasz kontekst wstępny „hello”, model zwróci ciąg znaków „world” z przecinem wiodącym, ponieważ „helloworld” ma więcej sensu niż „helloword”.
Podaj jak najdłuższy ciąg znaków przed kontekstem (maksymalnie 20 znaków, w tym spacje). Jeśli ciąg jest dłuższy, rozpoznawacz używa tylko ostatnich 20 znaków.
Przykładowy kod poniżej 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ść uderzeń
Dokładność rozpoznawania zależy od kolejności kresek. Rozpoznawacze oczekują, że pociągnięcia będą wykonywane w kolejności, w jakiej ludzie naturalnie piszą, np. w języku angielskim od lewej do prawej. Wszelkie odstępstwa od tego wzoru, np. pisanie zdania w języku angielskim, zaczynając od ostatniego słowa, daje mniej dokładne wyniki.
Innym przykładem jest usunięcie słowa w środku wyrażenia Ink
i zastąpienie go innym słowem. Poprawka znajduje się prawdopodobnie w środku zdania, ale jej linie są na końcu sekwencji linii.
W takim przypadku zalecamy wysłanie do interfejsu API nowo napisanego słowa osobno i zlanie wyniku z poprzednimi rozpoznaniami za pomocą własnej logiki.
Praca z niejednoznacznymi kształtami
Czasami znaczenie kształtu przekazanego do rozpoznawania jest niejednoznaczne. Na przykład prostokąt z bardzo zaokrąglonymi rogami może być postrzegany jako prostokąt lub elipsa.
W takich niejasnych przypadkach można użyć wyników rozpoznawania, jeśli są dostępne. Wyniki dostarczają tylko klasyfikatory kształtu. Jeśli model jest bardzo pewny siebie, wynik pierwszego wyniku będzie znacznie lepszy niż wynik drugiego najlepszego. Jeśli nie ma pewności, wyniki 2 najlepszych wyników będą zbliżone. Pamiętaj też, że klasyfikatory kształtów interpretują cały Ink
jako jeden kształt. Jeśli na przykład Ink
zawiera prostokąt i elipsę obok siebie, rozpoznawacz może zwrócić jeden z nich (lub coś zupełnie innego), ponieważ pojedynczy kandydat do rozpoznania nie może reprezentować 2 kształtów.