Digitale Tinte mit ML Kit unter iOS erkennen

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

  • 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. Öffnen Sie nach der Installation oder Aktualisierung der Pods Ihres Projekts das Xcode-Projekt mit dem zugehörigen .xcworkspace. ML Kit wird ab Xcode-Version 13.2.1 unterstützt.

Sie können jetzt Text in Ink-Objekten erkennen.

Ink-Objekt erstellen

Zum Erstellen eines Ink-Objekts kannst du es hauptsächlich auf einem Touchscreen zeichnen. Unter iOS können Sie eine UIImageView zusammen mit Touch-Ereignis-Handlern verwenden, die die Striche auf dem Bildschirm zeichnen und außerdem die Striche speichern, um das Ink-Objekt zu erstellen. Dieses allgemeine Muster wird im folgenden Code-Snippet veranschaulicht. Ein ausführlicheres Beispiel finden Sie in der Kurzanleitungs-App, in der die Verarbeitung von Touch-Ereignissen, Bildschirmzeichnungen und 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 UIImageView, der bei Bedarf für Ihre Anwendung angepasst werden muss. Wir empfehlen, beim Zeichnen der Liniensegmente Roundcaps zu verwenden, damit Segmente mit der Länge 0 Punkte als Punkt dargestellt werden. Die Funktion doRecognition() wird aufgerufen, nachdem jeder Strich geschrieben wurde, 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 Instanz DigitalInkRecognizer zu erhalten, müssen Sie zuerst das Erkennungsmodell für die gewünschte Sprache herunterladen und das Modell in RAM laden. Dies kann mit dem folgenden Code-Snippet erreicht werden, das der Einfachheit halber in der Methode viewDidLoad() platziert wird und einen hartcodierten Sprachnamen verwendet. In der Kurzanleitungs-App finden Sie ein Beispiel dafür, wie Sie dem Nutzer eine Liste der verfügbaren Sprachen anzeigen 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 Kurzanleitungs-Apps enthalten zusätzlichen Code, der zeigt, wie mehrere Downloads gleichzeitig verarbeitet werden können und wie durch Download-Benachrichtigungen bestimmt wird, welcher Download erfolgreich war.

Ink-Objekt erkennen

Als Nächstes kommen wir zur Funktion doRecognition(), die der Einfachheit halber von touchesEnded() aus aufgerufen wird. In anderen Anwendungen kann es sinnvoll sein, die Erkennung erst nach einer Zeitüberschreitung aufzurufen oder wenn der Nutzer auf eine Schaltfläche klickt, um die Erkennung 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];
  }];
}

Modelldownloads verwalten

Wir haben bereits gesehen, wie ein Erkennungsmodell heruntergeladen wird. Die folgenden Code-Snippets zeigen, wie Sie feststellen können, ob ein Modell bereits heruntergeladen wurde. Außerdem erfahren Sie, wie Sie ein Modell löschen, wenn es nicht mehr zur Wiederherstellung des Speicherplatzes 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. Die Genauigkeit hängt auch vom Schreibstil ab. Obwohl die digitale Tintenerkennung auf die Verarbeitung vieler Arten von Schreibstilen trainiert wurde, können die Ergebnisse von Nutzer zu Nutzer variieren.

Im Folgenden finden Sie einige Möglichkeiten, die Genauigkeit einer Texterkennung zu verbessern. Diese Techniken gelten nicht für Zeichenklassifikatoren für Emojis, AutoDraw und Formen.

Schreibbereich

Viele Anwendungen haben einen klar definierten Schreibbereich für die Nutzereingabe. Die Bedeutung eines Symbols wird teilweise durch seine Größe relativ zur Größe des Schreibbereichs bestimmt, in dem es enthalten ist. Zum Beispiel der Unterschied zwischen einem Kleinbuchstaben (&) und „&“ (C) und einem Komma im Vergleich zu einem Schrägstrich.

Wenn die Breite und Höhe des Schreibbereichs angegeben werden, kann die Genauigkeit verbessert werden. Bei der Erkennung wird jedoch davon ausgegangen, dass der Schreibbereich nur eine Textzeile enthält. Wenn der physische Schreibbereich groß genug ist, um dem Nutzer das Schreiben von zwei oder mehr Zeilen zu ermöglichen, können Sie bessere Ergebnisse erzielen, wenn Sie einen WriteArea-Wert mit einer Höhe übergeben, die der bestmöglichen Schätzung der Höhe einer einzelnen Textzeile entspricht. Das Objekt „WriteArea“, das Sie an das Erkennungsmodul übergeben, muss nicht genau mit dem physischen Schreibbereich auf dem Bildschirm übereinstimmen. Das Ändern der WriterArea-Höhe auf diese Weise funktioniert in einigen Sprachen besser als in anderen.

Wenn Sie den Schreibbereich angeben, geben Sie die Breite und Höhe in denselben Einheiten wie die Strichkoordinaten an. Für die x- und y-Koordinaten-Argumente ist keine Einheitsanforderung erforderlich. Die API normalisiert alle Einheiten, daher ist nur die relative Größe und Position der Striche wichtig. Sie können Koordinaten in jeder beliebigen Skala Ihres Systems übergeben.

Vor dem Kontext

Der Pre-Kontext ist der Text, der den Strichen im Ink, die du erkennen möchtest, unmittelbar vorangeht. Sie können dem Erkennungsmodul helfen, indem Sie es über den Kontext informieren.

So werden beispielsweise die Schreibbuchstaben "&" und "u" häufig irrtümlich für sich gehalten. Wenn der Nutzer das unvollständige Wort „&arg“ bereits eingegeben hat, kann er mit Strichen fortfahren, die als „&“ oder „n“" erkannt werden. Durch Angabe des Vorkontexts "arg" werden die Unklarheiten beseitigt, da das Wort "argument" wahrscheinlicher ist als "argnment".

Der Kontext kann auch dazu beitragen, dass das Erkennungstool Wortumbrüche erkennt, also die Leerzeichen zwischen den Wörtern. Sie können ein Leerzeichen eingeben, aber nicht zeichnen. Wie kann nun eine Erkennung bestimmen, wann ein Wort endet und das nächste beginnt? Wenn der Nutzer bereits "hello" geschrieben hat und mit dem geschriebenen Wort "world" fortfährt, ohne Vorkontext, gibt die Erkennung den String "world" zurück. Wenn Sie jedoch den Kontext „"hello"“ angeben, gibt das Modell den String „world“ mit einem führenden Leerzeichen zurück, da „Hello World“ sinnvoller ist als „&hellot;helloword"“.

Geben Sie den längsten möglichen Pre-Kontext-String mit maximal 20 Zeichen an, einschließlich Leerzeichen. Ist der String länger, werden nur die letzten 20 Zeichen verwendet.

Das Codebeispiel unten zeigt, wie Sie einen Schreibbereich definieren und ein RecognitionContext-Objekt verwenden, um einen Pre-Kontext 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);
                         }];

Strichanordnung

Die Erkennungsgenauigkeit hängt von der Reihenfolge der Striche ab. Die Erkennungssysteme erwarten, dass die Striche in der Reihenfolge vorkommen, in der Nutzer sie schreiben, z. B. von links nach rechts. Jeder Fall, der von diesem Muster abweicht, z. B. das Schreiben eines englischen Satzes, der mit dem letzten Wort beginnt, führt zu weniger genauen Ergebnissen.

Ein weiteres Beispiel: Ein Wort in der Mitte eines Ink wird entfernt und durch ein anderes Wort ersetzt. Die Überarbeitung befindet sich wahrscheinlich in der Mitte eines Satzes, aber die Striche für die Überarbeitung befinden sich 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 Erkennungsvorgängen mit 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, nicht eindeutig ist. Ein Rechteck mit sehr abgerundeten Kanten kann beispielsweise als Rechteck oder als Ellipse angesehen werden.

Diese unklaren Fälle können mithilfe von Erkennungswerten behoben werden, sobald sie verfügbar sind. Nur Formklassifikatoren bieten Bewertungen. Wenn das Modell sehr sicher ist, ist das Top-Ergebnis viel besser als das zweitbeste Ergebnis. Bei Unsicherheit liegen die Werte der ersten beiden Ergebnisse sehr dicht. Denken Sie auch daran, dass die Formklassifikatoren die gesamte Ink als eine Form interpretieren. Wenn beispielsweise Ink ein Rechteck und zwei Ellipsen nebeneinander enthält, kann die Erkennung eine der anderen Ergebnisformen (oder etwas völlig anderes) zurückgeben, da ein einzelner Erkennungskandidat nicht zwei Formen darstellen kann.