Digitale Tinte mit ML Kit für iOS erkennen

Mit der Handschrifterkennung von ML Kit können Sie handgeschriebenen Text auf einer digitalen Oberfläche in Hunderten von Sprachen erkennen und Skizzen klassifizieren.

Jetzt ausprobieren

  • Beispiel-App ausprobieren, um ein Beispiel für die Verwendung dieser API zu sehen.

Hinweis

  1. Fügen Sie die folgenden ML Kit-Bibliotheken in Ihre Podfile-Datei ein:

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. Nachdem Sie die Pods Ihres Projekts installiert oder aktualisiert haben, öffnen Sie 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 wichtigste Methode zum Erstellen eines Ink-Objekts ist das Zeichnen auf einem Touchscreen. Unter iOS können Sie eine UIImageView zusammen mit Touch-Ereignis-Handlern verwenden, um die Striche auf dem Bildschirm zu zeichnen und die Punkte der Striche zu speichern, um das Ink-Objekt zu erstellen. Dieses allgemeine Muster wird im folgenden Code-Snippet veranschaulicht. Ein vollständigeres Beispiel, in dem die Verarbeitung von Touch-Ereignissen, das Zeichnen auf dem Bildschirm und die Verwaltung von Strichdaten getrennt sind, finden Sie in der Kurzanleitung zur App.

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. Diese Funktion muss bei Bedarf für Ihre Anwendung angepasst werden. Wir empfehlen, beim Zeichnen der Liniensegmente runde Enden zu verwenden, damit Segmente mit der Länge null als Punkt dargestellt werden (wie der Punkt auf einem Kleinbuchstaben i). Die Funktion doRecognition() wird nach jedem Strich aufgerufen und wird unten definiert.

DigitalInkRecognizer-Instanz abrufen

Für die Erkennung muss das Ink-Objekt an eine DigitalInkRecognizer-Instanz übergeben werden. Um die DigitalInkRecognizer-Instanz zu erhalten, müssen wir zuerst das Erkennungsmodell für die gewünschte Sprache herunterladen und in den RAM laden. Dazu können Sie das folgende Code-Snippet verwenden, das zur Vereinfachung in der viewDidLoad()-Methode platziert ist und einen fest codierten Sprachnamen verwendet. In der Kurzanleitung zur App 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 Schnellstart-Apps enthalten zusätzlichen Code, der zeigt, wie mehrere Downloads gleichzeitig verarbeitet werden und wie anhand der Benachrichtigungen zum Abschluss ermittelt wird, welcher Download erfolgreich war.

Ink-Objekt erkennen

Als Nächstes kommt die Funktion doRecognition(), die zur Vereinfachung von touchesEnded() aufgerufen wird. In anderen Anwendungen soll die Spracherkennung möglicherweise erst nach einem Zeitlimit oder dann aufgerufen werden, wenn der Nutzer eine Taste drückt, um die Spracherkennung auszulösen.

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 Handschrifterkennung ist zwar darauf ausgelegt, viele Arten von Schreibstilen zu verarbeiten, die Ergebnisse können jedoch von Nutzer zu Nutzer variieren.

Hier sind einige Möglichkeiten, die Genauigkeit eines Texterkenners zu verbessern. Diese Techniken werden nicht auf die Zeichenklassifizierer für Emojis, AutoDraw und Formen angewendet.

Schreibbereich

Viele Anwendungen haben einen klar definierten Schreibbereich für Nutzereingaben. Die Bedeutung eines Symbols wird teilweise durch seine Größe im Verhältnis zur Größe des Schreibbereichs bestimmt, in dem es enthalten ist. Beispiel: 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 des Schreibbereichs mitteilen, kann die Genauigkeit verbessert werden. Der Erkennungsdienst geht jedoch davon aus, dass der Schreibbereich nur eine Textzeile enthält. Wenn der physische Schreibbereich groß genug ist, damit der Nutzer zwei oder mehr Zeilen schreiben kann, erhalten Sie möglicherweise bessere Ergebnisse, wenn Sie einen „WritingArea“-Parameter mit einer Höhe übergeben, die Ihrer besten Schätzung der Höhe einer einzelnen Textzeile entspricht. Das WritingArea-Objekt, das Sie an die Erkennung übergeben, muss nicht genau mit dem physischen Schreibbereich auf dem Bildschirm übereinstimmen. Das Ändern der Höhe des Eingabebereichs auf diese Weise funktioniert in einigen Sprachen besser als in anderen.

Wenn Sie den Schreibbereich angeben, müssen Sie seine Breite und Höhe in denselben Einheiten wie die Strichkoordinaten angeben. Für die x,y-Koordinatenargumente ist keine Einheit 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 jeder Skala übergeben, die für Ihr System sinnvoll ist.

Vorkontext

Der Pre-Context ist der Text, der den Strichen in Ink unmittelbar vorangeht und den Sie erkennen möchten. Sie können die Spracherkennung unterstützen, indem Sie ihr den Kontext vor dem eigentlichen Befehl mitteilen.

So werden beispielsweise 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. Durch die Angabe des Pre-Context „arg“ wird die Mehrdeutigkeit behoben, da das Wort „argument“ wahrscheinlicher ist als „argnment“.

Der Vorabkontext kann dem Erkennungsmodul auch dabei helfen, Worttrennungen, also die Leerzeichen zwischen Wörtern, zu erkennen. Sie können ein Leerzeichen eingeben, aber nicht zeichnen. Wie kann ein Erkennungsprogramm also feststellen, wann ein Wort endet und das nächste beginnt? Wenn der Nutzer bereits „Hallo“ geschrieben hat und mit dem Wort „Welt“ fortfährt, gibt die Spracherkennung ohne vorherigen Kontext den String „Welt“ zurück. Wenn Sie jedoch den Pre-Context „hello“ angeben, gibt das Modell den String „ world“ mit einem vorangestellten Leerzeichen zurück, da „hello world“ sinnvoller ist als „helloword“.

Geben Sie den längstmöglichen Pre-Context-String an, der bis zu 20 Zeichen lang sein darf, einschließlich Leerzeichen. Wenn der String länger ist, verwendet der Erkennungsdienst nur die letzten 20 Zeichen.

Im folgenden Codebeispiel wird gezeigt, wie Sie einen Schreibbereich 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);
                         }];

Reihenfolge der Striche

Die Erkennungsgenauigkeit hängt von der Reihenfolge der Striche ab. Die Erkennungsfunktionen erwarten, dass Striche in der Reihenfolge ausgeführt werden, in der Menschen normalerweise schreiben, z. B. von links nach rechts für Englisch. Bei Abweichungen von diesem Muster, z. B. wenn Sie einen englischen Satz schreiben, der mit dem letzten Wort beginnt, sind die Ergebnisse weniger genau.

Ein weiteres Beispiel ist, wenn ein Wort in der Mitte eines Ink entfernt und durch ein anderes Wort ersetzt wird. 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 den vorherigen Erkennungen 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 interpretiert werden.

Diese unklaren Fälle können mithilfe von Erkennungsergebnissen behandelt werden, sofern diese verfügbar sind. Nur Formklassifizierer 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. Außerdem interpretieren die Formklassifizierer das gesamte Ink als eine einzelne Form. Wenn das Ink beispielsweise ein Rechteck und eine Ellipse nebeneinander enthält, kann die Erkennung entweder das eine oder das andere (oder etwas ganz anderes) als Ergebnis zurückgeben, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.