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
- Przyjrzyj się przykładowej aplikacji, aby zobaczyć przykładowe użycie tego interfejsu API.
Zanim zaczniesz
Dołącz do pliku Podfile te biblioteki ML Kit:
pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
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.