Digitale Tinte mit ML Kit für iOS erkennen

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

Jetzt ausprobieren

Hinweis

  1. Fügen Sie Ihrer Podfile-Datei die folgenden ML Kit-Bibliotheken hinzu:

    pod 'GoogleMLKit/DigitalInkRecognition', '7.0.0'
    
    
  2. Nachdem Sie die Pods Ihres Projekts installiert oder aktualisiert haben, öffnen Sie Ihr Xcode-Projekt mit der .xcworkspace. ML Kit wird in Xcode-Version 13.2.1 oder höher unterstützt.

Sie können jetzt mit dem Erkennen von Text 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-Ereignishandlern verwenden, die die Striche auf dem Bildschirm zeichnen und die Punkte der Striche speichern, um das Ink-Objekt zu erstellen. Dieses allgemeine Muster wird im folgenden Code-Snippet veranschaulicht. In der Kurzanleitungs-App finden Sie ein vollständigeres Beispiel, in dem die Touch-Ereignisbehandlung, die Bildschirmzeichnung und die Verwaltung von Strichdaten getrennt werden.

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 der UIImageView, die bei Bedarf an Ihre Anwendung angepasst werden sollte. Wir empfehlen, beim Zeichnen der Liniensegmente abgerundete Enden zu verwenden, damit Segmente mit einer Länge von null als Punkt dargestellt werden (denken Sie an den Punkt auf einem Kleinbuchstaben „i“). Die Funktion doRecognition() wird nach jedem Strich aufgerufen und wird 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 abzurufen, müssen wir zuerst das Erkennungsmodell für die gewünschte Sprache herunterladen und in den RAM laden. Das ist mit dem folgenden Code-Snippet möglich, das zur Vereinfachung in die viewDidLoad()-Methode eingefügt wird und einen hartcodierten Sprachnamen verwendet. In der Kurzanleitungs-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 Quickstart-Apps enthalten zusätzlichen Code, der zeigt, wie mehrere Downloads gleichzeitig verarbeitet werden und wie Sie anhand der Benachrichtigungen zur Fertigstellung feststellen können, welcher Download erfolgreich war.

Ink-Objekt erkennen

Als Nächstes geht es um die Funktion doRecognition(), die aus Gründen der Einfachheit von touchesEnded() aufgerufen wird. In anderen Anwendungen kann die Erkennung beispielsweise erst nach einem Zeitlimit oder wenn der Nutzer eine Schaltfläche gedrückt hat, aufgerufen werden.

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];
  }];
}

Modelldownloads 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 Erkennung von digitaler Tinte ist zwar für viele Arten von Schreibstilen trainiert, die Ergebnisse können jedoch von Nutzer zu Nutzer variieren.

Hier sind einige Möglichkeiten, die Genauigkeit eines Texterkennungstools zu verbessern. Diese Techniken gelten nicht für die Zeichenklassifikatoren für Emojis, AutoDraw und Formen.

Schreibfläche

Viele Anwendungen haben einen klar definierten Eingabebereich für die Nutzereingabe. Die Bedeutung eines Symbols wird teilweise durch seine Größe im Verhältnis zur Größe des Schreibbereichs bestimmt, in dem es enthalten ist. Beispielsweise der Unterschied zwischen einem Kleinbuchstaben oder Großbuchstaben „o“ oder „c“ und einem Komma oder einem Schrägstrich.

Wenn Sie der Erkennung die Breite und Höhe des Schreibbereichs mitteilen, kann die Genauigkeit verbessert werden. Der Erkenner geht jedoch davon aus, dass der Schreibbereich nur eine einzige Textzeile enthält. Wenn der physische Schreibbereich groß genug ist, dass der Nutzer zwei oder mehr Zeilen schreiben kann, erzielen Sie möglicherweise bessere Ergebnisse, wenn Sie einen Schreibbereich mit einer Höhe übergeben, die Ihrer besten Schätzung der Höhe einer einzelnen Textzeile entspricht. Das WritingArea-Objekt, das Sie an den Erkenner übergeben, muss nicht genau mit dem physischen Schreibbereich auf dem Bildschirm übereinstimmen. Die Höhe des Schreibbereichs auf diese Weise zu ändern, funktioniert in einigen Sprachen besser als in anderen.

Geben Sie Breite und Höhe des Schreibbereichs in denselben Einheiten wie die Koordinaten der Striche an. Für die Argumente der X‑ und Y‑Koordinaten sind keine Einheiten erforderlich. Die API normalisiert alle Einheiten. Daher sind nur die relative Größe und Position der Striche wichtig. Sie können Koordinaten in einem beliebigen Maßstab für Ihr System eingeben.

Vorheriger Kontext

Der Vorkontext ist der Text, der unmittelbar vor den Strichen in der Ink steht, die Sie erkennen möchten. Sie können dem Erkennungstool helfen, indem Sie ihm den vorherigen Kontext mitteilen.

So werden beispielsweise die kursiven Buchstaben „n“ und „u“ häufig miteinander verwechselt. Wenn der Nutzer bereits das Teilwort „arg“ eingegeben hat, kann er mit Strichen fortfahren, die als „ment“ oder „nment“ erkannt werden. Durch die Angabe des vorherigen Kontexts „arg“ wird die Mehrdeutigkeit beseitigt, da das Wort „Argument“ wahrscheinlicher ist als „Argnment“.

Der Vorkontext kann dem Erkennungsmodul auch dabei helfen, Worttrennungen und Leerzeichen zwischen Wörtern zu erkennen. Sie können ein Leerzeichen eingeben, aber nicht zeichnen. Wie kann ein Recognizer 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 der Recognizer ohne vorherigen Kontext den String „Welt“ zurück. Wenn Sie jedoch den vorherigen Kontext „hallo“ angeben, gibt das Modell den String „welt“ mit einem vorangestellten Leerzeichen zurück, da „hallowelt“ mehr Sinn macht als „hallowort“.

Sie sollten den längsten möglichen String vor dem Kontext angeben, bis zu 20 Zeichen einschließlich Leerzeichen. Ist der String länger, werden nur die letzten 20 Zeichen verwendet.

Im folgenden Codebeispiel wird gezeigt, wie Sie einen Schreibbereich definieren und mit einem RecognitionContext-Objekt einen vorherigen Kontext angeben.

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);
                         }];

Stroke-Reihenfolge

Die Erkennungsgenauigkeit ist von der Reihenfolge der Striche abhängig. Die Erkennungsprogramme erwarten, dass die Striche in der Reihenfolge ausgeführt werden, in der Menschen normalerweise schreiben, z. B. von links nach rechts für Englisch. Abweichungen von diesem Muster, z. B. wenn ein englischer Satz mit dem letzten Wort beginnt, führen zu weniger genauen Ergebnissen.

Ein weiteres Beispiel ist, wenn ein Wort in der Mitte eines Ink entfernt und durch ein anderes Wort ersetzt wird. Die Korrektur befindet sich wahrscheinlich in der Mitte eines Satzes, die Striche für die Korrektur sind aber am Ende der Strichsequenz. In diesem Fall empfehlen wir, das neu geschriebene Wort separat an die API zu senden und das Ergebnis mithilfe Ihrer eigenen Logik mit den vorherigen Erkennungen zusammenzuführen.

Mehrdeutige Formen

Es gibt Fälle, in denen die Bedeutung der Form, die dem Erkennungstool zur Verfügung gestellt wird, nicht eindeutig ist. Ein Rechteck mit sehr abgerundeten Ecken kann beispielsweise als Rechteck oder als Ellipse erkannt werden.

Diese unklaren Fälle können mithilfe von Erkennungswerten bearbeitet werden, sofern diese verfügbar sind. Nur Klassifikatoren für die Form liefern Bewertungen. Wenn das Modell sehr sicher ist, ist der Wert des Top-Ergebnisses viel besser als der des zweitbesten. Bei Unsicherheit sind die Bewertungen der beiden besten Ergebnisse nah beieinander. Denken Sie auch daran, dass die Formklassifikatoren die gesamte Ink als eine einzelne Form interpretieren. Wenn der Ink beispielsweise ein Rechteck und eine Ellipse nebeneinander enthält, gibt der Recognizer möglicherweise einen oder den anderen (oder etwas ganz anderes) als Ergebnis zurück, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.