Dzięki rozpoznawaniu pisma odręcznego w ML Kit możesz rozpoznawać tekst napisany odręcznie na powierzchni cyfrowej w setkach języków, a także klasyfikować szkice.
Wypróbuj
- Wypróbuj przykładową aplikację, aby zobaczyć przykład użycia tego interfejsu API.
Zanim zaczniesz
W pliku Podfile uwzględnij te biblioteki ML Kit:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
Po zainstalowaniu lub zaktualizowaniu Pods w projekcie otwórz projekt Xcode za pomocą pliku
.xcworkspace
. 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 tworzenia obiektu Ink
jest narysowanie go na ekranie dotykowym. W iOS możesz użyć UIImageView wraz z procedurami obsługi zdarzeń dotknięcia, które rysują pociągnięcia na ekranie, a także przechowują punkty pociągnięć, aby utworzyć obiekt Ink
. Ten ogólny wzorzec jest widoczny w tym fragmencie kodu. Bardziej kompletny przykład, który rozdziela obsługę zdarzeń dotykowych, rysowanie ekranu i zarządzanie danymi o pociągnięciach, znajdziesz w aplikacji na początek.
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ę rysowania linii w UIImageView, którą w razie potrzeby należy dostosować do swojej aplikacji. Podczas rysowania odcinków linii zalecamy używanie zaokrąglonych zakończeń, aby odcinki o długości zerowej były rysowane jako kropka (np. kropka nad małą literą „i”). Funkcja doRecognition()
jest wywoływana po napisaniu każdego pociągnięcia 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 załadować go do pamięci RAM. Możesz to zrobić za pomocą tego fragmentu kodu, który dla uproszczenia został umieszczony w metodzie viewDidLoad()
i używa nazwy języka zakodowanej na stałe. W aplikacji znajdziesz przykład tego, 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 do szybkiego rozpoczęcia pracy zawierają dodatkowy kod, który pokazuje, jak obsługiwać wiele pobrań jednocześnie i jak określać, które pobieranie się powiodło, poprzez 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 rozpoznawanie może być wywoływane dopiero po upływie limitu czasu 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
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. Funkcja rozpoznawania pisma odręcznego jest trenowana pod kątem obsługi wielu stylów pisania, ale 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 mają zastosowania do klasyfikatorów rysunków w przypadku emoji, automatycznego rysowania i kształtów.
Obszar pisania
Wiele aplikacji ma dobrze zdefiniowany obszar pisania, w którym użytkownik może wprowadzać dane. Znaczenie symbolu jest częściowo określone przez jego rozmiar w stosunku do rozmiaru obszaru pisania, w którym się znajduje. Na przykład różnica między małą i wielką literą „o” lub „c” oraz między przecinkiem a ukośnikiem.
Podanie rozpoznawaniu szerokości i wysokości obszaru pisania może zwiększyć dokładność. Jednak rozpoznawanie zakłada, że obszar pisania zawiera tylko jeden wiersz tekstu. Jeśli fizyczny obszar pisania jest wystarczająco duży, aby użytkownik mógł napisać 2 lub więcej wierszy, możesz uzyskać lepsze wyniki, przekazując wartość WritingArea o wysokości, która jest Twoim najlepszym oszacowaniem wysokości pojedynczego wiersza tekstu. Obiekt WritingArea przekazywany do rozpoznawania nie musi dokładnie odpowiadać fizycznemu obszarowi pisania na ekranie. Zmiana wysokości obszaru pisania w ten sposób sprawdza się 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 pociągnięcia. Argumenty współrzędnych x i y nie wymagają jednostek – interfejs API normalizuje wszystkie jednostki, więc liczy się tylko względny rozmiar i położenie pociągnięć. Możesz przekazywać współrzędne w dowolnej skali, która jest odpowiednia dla Twojego systemu.
Kontekst przed
Kontekst poprzedzający to tekst, który bezpośrednio poprzedza znaki w Ink
, które próbujesz rozpoznać. Możesz pomóc rozpoznawaniu, podając mu kontekst.
Na przykład litery pisane „n” i „u” są często mylone. Jeśli użytkownik wpisał już część słowa „arg”, może kontynuować wpisywanie znaków, które można rozpoznać jako „ument” lub „nment”. Określenie kontekstu poprzedzającego „arg” rozwiązuje niejednoznaczność, ponieważ słowo „argument” jest bardziej prawdopodobne niż „argnment”.
Kontekst przed słowem może też pomóc rozpoznawaniu w określaniu przerw między słowami, czyli spacji. Możesz wpisać spację, ale nie możesz jej narysować. Jak więc system rozpoznawania może określić, kiedy kończy się jedno słowo, a zaczyna drugie? Jeśli użytkownik napisał już „hello” i kontynuuje pisanie słowa „world”, bez kontekstu wstępnego rozpoznawanie zwróci ciąg znaków „world”. Jeśli jednak określisz kontekst wstępny „hello”, model zwróci ciąg znaków „ world” ze spacją na początku, ponieważ „hello world” ma większy sens niż „helloword”.
Podaj jak najdłuższy ciąg znaków przed kontekstem (maksymalnie 20 znaków, w tym spacji). Jeśli ciąg jest dłuższy, rozpoznawanie obejmuje tylko ostatnie 20 znaków.
Poniższy przykładowy kod pokazuje, jak zdefiniować obszar pisania i użyć obiektu RecognitionContext
do określenia kontekstu wstępnego.
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 kreślenia. Rozpoznawanie pisma odręcznego oczekuje, że pociągnięcia będą wykonywane w kolejności, w jakiej ludzie naturalnie piszą, np. od lewej do prawej w przypadku języka angielskiego. Każdy przypadek, który odbiega od tego wzorca, np. napisanie zdania w języku angielskim zaczynającego się od ostatniego słowa, daje mniej dokładne wyniki.
Inny przykład to usunięcie słowa ze środka Ink
i zastąpienie go innym słowem. Poprawka prawdopodobnie znajduje się w środku zdania, ale pociągnięcia związane z poprawką są na końcu sekwencji pociągnięć.
W takim przypadku zalecamy wysłanie nowo napisanego słowa osobno do interfejsu API i połączenie wyniku z poprzednimi rozpoznaniami za pomocą własnej logiki.
Radzenie sobie z niejednoznacznymi kształtami
Czasami znaczenie kształtu przekazanego do rozpoznawania jest niejednoznaczne. Na przykład prostokąt o bardzo zaokrąglonych krawędziach może być postrzegany jako prostokąt lub elipsa.
W takich niejasnych przypadkach można używać wyników rozpoznawania, jeśli są dostępne. Tylko klasyfikatory kształtów podają wyniki. Jeśli model jest bardzo pewny, wynik najlepszego rezultatu będzie znacznie lepszy niż drugiego w kolejności. Jeśli istnieje niepewność, wyniki dla 2 najlepszych rezultatów będą zbliżone. Pamiętaj też, że klasyfikatory kształtów interpretują cały znak Ink
jako jeden kształt. Jeśli na przykład Ink
zawiera prostokąt i elipsę obok siebie, rozpoznawanie może zwrócić jeden z nich (lub coś zupełnie innego), ponieważ pojedynczy kandydat rozpoznawania nie może reprezentować dwóch kształtów.