Digitale Tinte mit ML Kit für iOS erkennen

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

Ausprobieren

  • Probieren Sie die Beispiel-App aus, 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', '3.2.0'
    
    
  2. Nachdem Sie die Pods Ihres Projekts installiert oder aktualisiert haben, öffnen Sie Ihr Xcode-Projekt mit seiner .xcworkspace. ML Kit wird ab Xcode-Version 13.2.1 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 einen Touchscreen zu zeichnen. Unter iOS können Sie UIImageView zusammen mit Touch-Event-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 Schnellstart-App finden Sie ein umfassenderes Beispiel, in dem die Verarbeitung von Touch-Ereignissen, die Bildschirmzeichnung und die Verwaltung von Strichdaten getrennt wird.

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 für Ihre Anwendung entsprechend angepasst werden sollte. Wir empfehlen die Verwendung von runden Kapiteln beim Zeichnen der Liniensegmente, damit Segmente mit der Länge null als Punkt gezeichnet werden (also wie bei dem Punkt auf dem Kleinbuchstaben i). Die Funktion doRecognition() wird aufgerufen, nachdem jeder Strich geschrieben wurde, und wird unten definiert.

Instanz von DigitalInkRecognizer abrufen

Für die Erkennung muss das Ink-Objekt an eine DigitalInkRecognizer-Instanz übergeben werden. Um die Instanz DigitalInkRecognizer zu erhalten, müssen Sie zuerst das Erkennungsmodell für die gewünschte Sprache herunterladen und das Modell in den RAM laden. Verwenden Sie dazu das folgende Code-Snippet, das der Einfachheit halber in der Methode viewDidLoad() platziert wird und einen hartcodierten Sprachnamen verwendet. In der Schnellstart-App finden Sie ein Beispiel dafür, wie Sie dem Nutzer die Liste der verfügbaren Sprachen anzeigen lassen und die ausgewählte Sprache herunterladen können.

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 Apps aus der Kurzanleitung enthalten zusätzlichen Code, der zeigt, wie mehrere Downloads gleichzeitig verarbeitet werden und wie mithilfe von Abschlussbenachrichtigungen ermittelt werden kann, welcher Download erfolgreich war.

Ink-Objekt erkennen

Als Nächstes kommen wir zur Funktion doRecognition(), die der Einfachheit halber aus touchesEnded() aufgerufen wird. In anderen Anwendungen kann es sinnvoll sein, die Erkennung erst nach einer Zeitüberschreitung oder erst dann auszulösen, wenn der Nutzer auf eine Schaltfläche drückt.

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 werden kann. Die folgenden Code-Snippets veranschaulichen, wie Sie prüfen können, ob ein Modell bereits heruntergeladen wurde, oder ein Modell löschen, wenn es zur Wiederherstellung des Speicherplatzes nicht mehr benötigt wird.

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. Genauigkeit hängt auch vom Schreibstil ab. Die digitale Tintenerkennung wurde für die Verarbeitung vieler Arten von Schreibstilen trainiert, die Ergebnisse können jedoch von Nutzer zu Nutzer variieren.

Im Folgenden finden Sie einige Möglichkeiten, die Genauigkeit der Texterkennung zu verbessern. Diese Verfahren gelten nicht für Zeichenklassifikatoren für Emojis, das Autozeichnen und die Formen.

Schreibbereich

Viele Anwendungen haben einen genau definierten Schreibbereich für Nutzereingaben. Die Bedeutung eines Symbols wird zum Teil durch seine Größe im Verhältnis zur Größe des Schreibbereichs bestimmt, in dem es enthalten ist. Zum Beispiel der Unterschied zwischen einem Klein- oder Großbuchstaben „o“ oder „c“ sowie einem Komma und einem Schrägstrich.

Wenn der Erkennung die Breite und Höhe des Schreibbereichs mitgeteilt wird, kann dies die Genauigkeit verbessern. Bei der Erkennung wird jedoch davon ausgegangen, dass der Schreibbereich nur eine einzige Textzeile enthält. Wenn der physische Schreibbereich groß genug ist, um dem Nutzer das Schreiben von zwei oder mehr Zeilen zu ermöglichen, erhalten 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 die Erkennung übergeben, muss nicht genau mit dem physischen Schreibbereich auf dem Bildschirm übereinstimmen. Diese Änderung der WritingArea-Höhe funktioniert in einigen Sprachen besser als in anderen.

Breite und Höhe des Schreibbereichs müssen dabei in denselben Einheiten wie für die Strichkoordinaten verwendet werden. Für die x- und y-Koordinatenargumente ist keine Einheit erforderlich – die API normalisiert alle Einheiten, sodass nur die relative Größe und Position der Striche wichtig sind. Sie können Koordinaten in jedem für Ihr System sinnvollen Maßstab übergeben.

Vorkontext

„Pre-Kontext“ ist der Text, der den Strichen in der Ink, die Sie erkennen möchten, unmittelbar vorausgeht. Sie können der Erkennung helfen, indem Sie sie über den Vorkontext informieren.

Zum Beispiel werden die Kursivbuchstaben „n“ und „u“ oft verwechselt. Wenn der Nutzer den Teilwort "arg" bereits eingegeben hat, kann er mit Strichen fortfahren, die als "ument" oder "nment" erkannt werden. Durch Angabe des vorkontextbezogenen „arg“-Elements wird die Mehrdeutigkeit beseitigt, da das Wort „Argument“ wahrscheinlicher als „argnment“ ist.

Vorkontext kann der Erkennung auch dabei helfen, Wortumbrüche zu erkennen, also Leerzeichen zwischen Wörtern. Sie können ein Leerzeichen eingeben, aber kein Zeichen zeichnen. Wie kann eine Erkennung also ermitteln, wann ein Wort endet und das nächste beginnt? Wenn der Nutzer bereits "hello" geschrieben hat und mit dem geschriebenen Wort "world" fortfährt, gibt das Erkennungssystem ohne Vorkontext den String "world" zurück. Wenn Sie jedoch „hello“ im Vorkontext 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 präkontextbasierten String mit bis zu 20 Zeichen an, einschließlich Leerzeichen. Wenn der String länger ist, verwendet die Erkennung nur die letzten 20 Zeichen.

Das folgende Codebeispiel zeigt, wie Sie einen Schreibbereich definieren und ein RecognitionContext-Objekt verwenden, um einen Kontext festzulegen.

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

Strichanordnung

Die Erkennungsgenauigkeit hängt von der Reihenfolge der Striche ab. Das Erkennungssystem erwartet, dass die Striche in der Reihenfolge auftreten, in der Menschen normalerweise schreiben, z. B. von links nach rechts für Englisch. In allen Fällen, die von diesem Muster abweichen, z. B. wenn ein Satz auf Englisch mit dem letzten Wort beginnt, erhalten Sie weniger genaue Ergebnisse.

Ein anderes Beispiel wäre, wenn ein Wort mitten in einem Ink entfernt und durch ein anderes Wort ersetzt wird. Die Überarbeitung befindet sich wahrscheinlich mitten in einem Satz, aber die Striche für die Überarbeitung befinden sich am Ende der Strichsequenz. In diesem Fall empfehlen wir, das neu geschriebene Wort separat an die API zu senden und das Ergebnis mit der vorherigen Erkennung unter Verwendung Ihrer eigenen Logik zusammenzuführen.

Umgang mit zweideutigen Formen

Es gibt Fälle, in denen die Bedeutung der Form, die der Erkennung bereitgestellt wird, nicht eindeutig ist. Ein Rechteck mit stark abgerundeten Kanten kann beispielsweise als Rechteck oder Ellipse betrachtet werden.

Diese unklaren Fälle können mithilfe von Erkennungswerten behoben werden, sofern diese verfügbar sind. Bewertungen werden nur von Formklassifikatoren bereitgestellt. Wenn das Modell sehr zuversichtlich ist, ist die Punktzahl des besten Ergebnisses viel besser als das zweitbeste Ergebnis. Bei Unsicherheit liegen die Werte für die beiden besten Ergebnisse nahe. Beachten Sie außerdem, dass die Formklassifikatoren das gesamte Ink-Element als eine einzelne Form interpretieren. Wenn die Ink beispielsweise ein Rechteck und eine Ellipse nebeneinander enthält, gibt die Erkennung möglicherweise das eine oder das andere oder etwas völlig anderes zurück, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.