Mit der digitalen Tinte von ML Kit können Sie handschriftlichen Text auf einer digitalen Oberfläche in Hunderten von Sprachen erkennen und Skizzen klassifizieren.
Jetzt ausprobieren
- Probieren Sie die Beispiel-App aus, um ein Anwendungsbeispiel für diese API zu sehen.
Hinweis
Fügen Sie Ihrer Podfile die folgenden ML Kit-Bibliotheken hinzu:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'Öffnen Sie nach der Installation oder Aktualisierung der Pods Ihres Projekts Ihr Xcode-Projekt mit der Datei
.xcworkspace. ML Kit wird in Xcode Version 13.2.1 oder höher unterstützt.
Sie können jetzt mit der Texterkennung in Ink-Objekten beginnen.
Ink-Objekt erstellen
Die Hauptmethode zum Erstellen eines Ink-Objekts besteht darin, es auf einem Touchscreen zu zeichnen. Unter iOS,
können Sie eine UIImageView zusammen mit
Touch-Ereignis
Handlern
verwenden, die die Striche auf dem Bildschirm zeichnen und auch die Punkte der Striche speichern, um
das Ink Objekt zu erstellen. Dieses allgemeine Muster wird im folgenden Code-Snippet veranschaulicht. In der Kurzanleitung
finden Sie ein
vollständigeres Beispiel, in dem die Verarbeitung von Touch-Ereignissen, das Zeichnen auf dem Bildschirm
und die Verwaltung von Strichdaten getrennt sind.
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]; }
Das Code-Snippet enthält eine Beispielfunktion zum Zeichnen des Strichs in
die UIImageView,
die nach Bedarf für Ihre Anwendung angepasst werden sollte. Wir empfehlen, beim Zeichnen der Liniensegmente runde Enden zu verwenden, damit Segmente mit der Länge 0 als Punkt gezeichnet werden (z. B. der Punkt auf einem Kleinbuchstaben „i“). Die Funktion doRecognition() wird nach jedem geschriebenen Strich aufgerufen und unten definiert.
Instanz von DigitalInkRecognizer abrufen
Für die Erkennung müssen wir das Ink Objekt an eine
DigitalInkRecognizer Instanz übergeben. Um die DigitalInkRecognizer-Instanz zu erhalten, müssen wir zuerst das Erkennungsmodell für die gewünschte Sprache herunterladen und in den RAM laden. Dies kann mit dem folgenden Code-Snippet erreicht werden, das zur Vereinfachung in der Methode viewDidLoad() platziert ist und einen fest codierten Sprachnamen verwendet. In der Kurzanleitung
finden Sie ein
Beispiel dafür, wie Sie dem Nutzer die Liste der verfügbaren Sprachen anzeigen und die ausgewählte Sprache herunterladen.
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]; }
Die Kurzanleitungs-Apps enthalten zusätzlichen Code, der zeigt, wie mehrere Downloads gleichzeitig verarbeitet werden und wie Sie anhand der Benachrichtigungen über den Abschluss eines Downloads feststellen können, welcher Download erfolgreich war.
Ink-Objekt erkennen
Als Nächstes kommt die Funktion doRecognition(), die zur Vereinfachung von touchesEnded() aufgerufen wird. In anderen Anwendungen kann es sinnvoll sein, die Erkennung erst nach einem Timeout oder wenn der Nutzer auf eine Schaltfläche geklickt hat, um die Erkennung auszulösen, aufzurufen.
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]; }]; }
Modell-Downloads verwalten
Wir haben bereits gesehen, wie ein Erkennungsmodell heruntergeladen wird. Die folgenden Code-Snippets veranschaulichen, wie Sie prüfen, ob ein Modell bereits heruntergeladen wurde, oder ein Modell löschen, wenn es nicht mehr benötigt wird, um Speicherplatz freizugeben.
Prüfen, ob ein Modell bereits heruntergeladen wurde
Swift
let model : DigitalInkRecognitionModel = ... let modelManager = ModelManager.modelManager() modelManager.isModelDownloaded(model)
Objective-C
MLKDigitalInkRecognitionModel *model = ...; MLKModelManager *modelManager = [MLKModelManager modelManager]; [modelManager isModelDownloaded:model];
Heruntergeladenes Modell löschen
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."); }]; }
Tipps zur Verbesserung der Genauigkeit der Texterkennung
Die Genauigkeit der Texterkennung kann je nach Sprache variieren. Die Genauigkeit hängt auch vom Schreibstil ab. Die digitale Tinte ist zwar darauf trainiert, viele Arten von Schreibstilen zu verarbeiten, die Ergebnisse können jedoch je nach Nutzer variieren.
Hier sind einige Möglichkeiten, die Genauigkeit eines Texterkenners zu verbessern. Diese Techniken gelten nicht für die Zeichenklassifizierer für Emojis, Autodraw und Formen.
Schreibfläche
Viele Anwendungen haben eine klar definierte Schreibfläche für die Nutzereingabe. Die Bedeutung eines Symbols wird teilweise durch seine Größe im Verhältnis zur Größe der Schreibfläche bestimmt, die es enthält. Beispiele: der Unterschied zwischen einem Klein- oder Großbuchstaben „o“ oder „c“ und einem Komma im Vergleich zu einem Schrägstrich.
Wenn Sie dem Erkennungstool die Breite und Höhe der Schreibfläche mitteilen, kann die Genauigkeit verbessert werden. Das Erkennungstool geht jedoch davon aus, dass die Schreibfläche nur eine einzelne Textzeile enthält. Wenn die physische Schreibfläche groß genug ist, damit der Nutzer zwei oder mehr Zeilen schreiben kann, erzielen Sie möglicherweise bessere Ergebnisse, wenn Sie eine Schreibfläche mit einer Höhe übergeben, die Ihrer besten Schätzung der Höhe einer einzelnen Textzeile entspricht. Das Objekt „Schreibfläche“, das Sie an das Erkennungstool übergeben, muss nicht genau der physischen Schreibfläche auf dem Bildschirm entsprechen. Die Änderung der Höhe der Schreibfläche auf diese Weise funktioniert in einigen Sprachen besser als in anderen.
Wenn Sie die Schreibfläche angeben, geben Sie ihre Breite und Höhe in denselben Einheiten wie die Strichkoordinaten an. Für die Argumente für die x‑ und y‑Koordinaten sind keine Einheiten erforderlich. Die API normalisiert alle Einheiten. Es kommt also nur auf die relative Größe und Position der Striche an. Sie können Koordinaten in einer beliebigen Skala übergeben, die für Ihr System sinnvoll ist.
Vorkontext
Der Vorkontext ist der Text, der den Strichen in der Ink unmittelbar vorangeht, die Sie erkennen möchten. Sie können dem Erkennungstool helfen, indem Sie ihm den Vorkontext mitteilen.
Beispielsweise werden die kursiven Buchstaben „n“ und „u“ oft verwechselt. Wenn der Nutzer bereits das Teilwort „arg“ eingegeben hat, kann er mit Strichen fortfahren, die als „ument“ oder „nment“ erkannt werden können. Wenn Sie den Vorkontext „arg“ angeben, wird die Mehrdeutigkeit aufgelöst, da das Wort „argument“ wahrscheinlicher ist als „argnment“.
Der Vorkontext kann dem Erkennungstool auch helfen, Worttrennungen zu erkennen, also die Leerzeichen zwischen Wörtern. Sie können ein Leerzeichen eingeben, aber nicht zeichnen. Wie kann ein Erkennungstool also feststellen, wann ein Wort endet und das nächste beginnt? Wenn der Nutzer bereits „hallo“ geschrieben hat und mit dem geschriebenen Wort „Welt“ fortfährt, gibt das Erkennungstool ohne Vorkontext den String „Welt“ zurück. Wenn Sie jedoch den Vorkontext „hallo“ angeben, gibt das Modell den String „ Welt“ mit einem vorangestellten Leerzeichen zurück, da „hallo Welt“ sinnvoller ist als „halloWelt“.
Sie sollten den längstmöglichen Vorkontext-String angeben, bis zu 20 Zeichen, einschließlich Leerzeichen. Wenn der String länger ist, verwendet das Erkennungstool nur die letzten 20 Zeichen.
Das folgende Codebeispiel zeigt, wie Sie eine Schreibfläche definieren und ein RecognitionContext-Objekt verwenden, um den Vorkontext anzugeben.
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); }];
Strichreihenfolge
Die Erkennungsgenauigkeit hängt von der Reihenfolge der Striche ab. Die Erkennungstools erwarten, dass die Striche in der Reihenfolge auftreten, in der Menschen normalerweise schreiben, z. B. von links nach rechts für Deutsch. In allen Fällen, die von diesem Muster abweichen, z. B. wenn Sie einen deutschen Satz schreiben, der mit dem letzten Wort beginnt, sind die Ergebnisse weniger genau.
Ein weiteres Beispiel: Ein Wort in der Mitte einer Ink wird entfernt und durch ein anderes Wort ersetzt. Die Überarbeitung befindet sich wahrscheinlich in der Mitte eines Satzes, die Striche für die Überarbeitung befinden sich jedoch am Ende der Strichfolge.
In diesem Fall empfehlen wir, das neu geschriebene Wort separat an die API zu senden und das Ergebnis mit der vorherigen Erkennung mithilfe Ihrer eigenen Logik zusammenzuführen.
Umgang mit mehrdeutigen Formen
Es gibt Fälle, in denen die Bedeutung der Form, die dem Erkennungstool zur Verfügung gestellt wird, mehrdeutig ist. Ein Rechteck mit sehr abgerundeten Ecken kann beispielsweise entweder als Rechteck oder als Ellipse angesehen werden.
Diese unklaren Fälle können mithilfe von Erkennungswerten verarbeitet werden, wenn sie verfügbar sind. Nur Formenklassifizierer liefern Werte. Wenn das Modell sehr zuversichtlich ist, ist der Wert des besten Ergebnisses viel besser als der des zweitbesten. Bei Unsicherheit sind die Werte für die beiden besten Ergebnisse ähnlich. Beachten Sie außerdem, dass die Formenklassifizierer die gesamte Ink als eine einzelne Form interpretieren. Wenn die Ink beispielsweise ein Rechteck und eine Ellipse nebeneinander enthält, kann das Erkennungstool das eine oder das andere (oder etwas ganz anderes) als Ergebnis zurückgeben, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.