Riconoscimento dell'inchiostro digitale con ML Kit su iOS

Con il riconoscimento dell'inchiostro digitale di ML Kit, puoi riconoscere il testo scritto a mano su una superficie digitale in centinaia di lingue, nonché classificare gli schizzi.

Prova

  • Prova l'app di esempio per vedere un esempio di utilizzo di questa API.

Prima di iniziare

  1. Includi le seguenti librerie ML Kit nel tuo Podfile:

    pod 'GoogleMLKit/DigitalInkRecognition', '7.0.0'
    
    
  2. Dopo aver installato o aggiornato i pod del progetto, apri il progetto Xcode utilizzando il relativo .xcworkspace. ML Kit è supportato nella versione 13.2.1 o successive di Xcode.

Ora puoi iniziare a riconoscere il testo negli oggetti Ink.

Creare un oggetto Ink

Il modo principale per creare un oggetto Ink è disegnarlo su un touchscreen. Su iOS, puoi utilizzare un UIImageView insieme a gestori di eventi di tocco che disegnano i tratti sullo schermo e memorizzano anche i punti dei tratti per creare l'oggetto Ink. Questo pattern generale è dimostrato nel seguente snippet di codice. Consulta l'app walkthrough per un esempio più completo, che separa la gestione degli eventi tocco, il disegno sullo schermo e la gestione dei dati degli alberi.

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

Tieni presente che lo snippet di codice include una funzione di esempio per disegnare il tratto nell'UIImageView, che deve essere adattata in base alle esigenze della tua applicazione. Ti consigliamo di utilizzare i tratti arrotondati quando disegni i segmenti di linea in modo che i segmenti di lunghezza zero vengano disegnati come un punto (come il punto sulla lettera i minuscola). La funzione doRecognition() viene chiamata dopo la scrittura di ogni tratto e verrà definita di seguito.

Recupera un'istanza di DigitalInkRecognizer

Per eseguire il riconoscimento, dobbiamo passare l'oggetto Ink a un'istanza DigitalInkRecognizer. Per ottenere l'istanza DigitalInkRecognizer, dobbiamo prima scaricare il modello di riconoscimento per la lingua desiderata e caricarlo nella RAM. Questo può essere ottenuto utilizzando il seguente snippet di codice, che per semplicità è inserito nel metodo viewDidLoad() e utilizza un nome della lingua hardcoded. Consulta l'app Guida rapida per un esempio di come mostrare all'utente l'elenco delle lingue disponibili e scaricare la lingua selezionata.

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

Le app di avvio rapido includono codice aggiuntivo che mostra come gestire più download contemporaneamente e come determinare quale download è andato a buon fine gestendo le notifiche di completamento.

Riconoscere un oggetto Ink

Passiamo ora alla funzione doRecognition(), che per semplicità viene chiamata da touchesEnded(). In altre applicazioni potrebbe essere opportuno invocare il riconoscimento solo dopo un timeout o quando l'utente ha premuto un pulsante per attivarlo.

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

Gestione dei download dei modelli

Abbiamo già visto come scaricare un modello di riconoscimento. I seguenti snippet di codice mostrano come verificare se un modello è già stato scaricato o come eliminarlo quando non è più necessario per recuperare lo spazio di archiviazione.

Controllare se un modello è già stato scaricato

Swift

let model : DigitalInkRecognitionModel = ...
let modelManager = ModelManager.modelManager()
modelManager.isModelDownloaded(model)

Objective-C

MLKDigitalInkRecognitionModel *model = ...;
MLKModelManager *modelManager = [MLKModelManager modelManager];
[modelManager isModelDownloaded:model];

Eliminare un modello scaricato

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

Suggerimenti per migliorare l'accuratezza del riconoscimento del testo

La precisione del riconoscimento del testo può variare in base alle lingue. L'accuratezza dipende anche dallo stile di scrittura. Sebbene il riconoscimento della scrittura digitale sia addestrato a gestire molti tipi di stili di scrittura, i risultati possono variare da utente a utente.

Ecco alcuni modi per migliorare la precisione di un riconoscitore di testo. Tieni presente che queste tecniche non si applicano ai classificatori dei disegni per emoji, disegni automatici e forme.

Area di scrittura

Molte applicazioni hanno un'area di scrittura ben definita per l'input dell'utente. Il significato di un simbolo è parzialmente determinato dalle sue dimensioni rispetto alle dimensioni dell'area di scrittura che lo contiene. Ad esempio, la differenza tra una lettera minuscola o maiuscola "o" o "c" e una virgola rispetto a una barra.

Indicare al riconoscitore la larghezza e l'altezza dell'area di scrittura può migliorare la precisione. Tuttavia, il riconoscimento presuppone che l'area di scrittura contenga una sola riga di testo. Se l'area di scrittura fisica è abbastanza grande da consentire all'utente di scrivere due o più righe, puoi ottenere risultati migliori passando un'area di scrittura con un'altezza pari alla tua stima migliore dell'altezza di una singola riga di testo. L'oggetto WritingArea che passi al riconoscitore non deve corrispondere esattamente all'area di scrittura fisica sullo schermo. La modifica dell'altezza di WritingArea in questo modo funziona meglio in alcune lingue rispetto ad altre.

Quando specifichi l'area di scrittura, specifica la larghezza e l'altezza nelle stesse unità di misura delle coordinate del tratto. Gli argomenti delle coordinate x,y non hanno requisiti relativi alle unità. L'API normalizza tutte le unità, quindi l'unica cosa che conta è la dimensione e la posizione relative dei tratti. Puoi scegliere la scala più adatta al tuo sistema per inserire le coordinate.

Pre-contesto

Il contesto precedente è il testo che precede immediatamente i tratti della Ink che stai tentando di riconoscere. Puoi aiutare il sistema di riconoscimento fornendogli il contesto precedente.

Ad esempio, le lettere corsive "n" e "u" vengono spesso scambiate l'una per l'altra. Se l'utente ha già inserito la parola parziale "arg", può continuare con tratti che possono essere riconosciuti come "ument" o "nment". La specifica del precontesto "arg" risolve l'ambiguità, poiché la parola "argomento" è più probabile di "argnment".

Il contesto precedente può anche aiutare il riconoscitore a identificare gli a capo, gli spazi tra le parole. Puoi digitare un carattere di spazio, ma non puoi disegnarlo, quindi come può un sistema di riconoscimento determinare quando termina una parola e inizia la successiva? Se l'utente ha già scritto "ciao" e continua con la parola scritta "mondo", senza pre-contesto il riconoscitore restituisce la stringa "mondo". Tuttavia, se specifichi il precontesto "ciao", il modello restituirà la stringa " mondo", con uno spazio iniziale, poiché "ciao mondo" ha più senso di "ciaomondo".

Devi fornire la stringa di precontesto più lunga possibile, fino a 20 caratteri, spazi inclusi. Se la stringa è più lunga, il riconoscitore utilizza solo gli ultimi 20 caratteri.

L'esempio di codice seguente mostra come definire un'area di scrittura e utilizzare un oggetto RecognitionContext per specificare il pre-contesto.

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

Ordinamento tratti

L'accuratezza del riconoscimento è sensibile all'ordine dei tratti. I riconoscitori si aspettano che i tratti si verifichino nell'ordine in cui le persone scriverebbero naturalmente, ad esempio da sinistra a destra per l'inglese. Qualsiasi caso che si discosti da questo schema, ad esempio scrivere una frase in inglese che inizia con l'ultima parola, fornisce risultati meno accurati.

Un altro esempio è quando una parola al centro di un Ink viene rimossa e sostituita con un'altra parola. La revisione è probabilmente nel mezzo di una frase, ma i tratti per la revisione si trovano alla fine della sequenza di tratti. In questo caso, ti consigliamo di inviare la parola appena scritta separatamente all'API e di unire il risultato con i riconoscimenti precedenti utilizzando la tua logica.

Gestire le forme ambigue

In alcuni casi il significato della forma fornita al riconoscitore è ambiguo. Ad esempio, un rettangolo con bordi molto arrotondati può essere visto come un rettangolo o un'ellisse.

Questi casi poco chiari possono essere gestiti utilizzando i punteggi di riconoscimento, se disponibili. Solo i classificatori di forma forniscono punteggi. Se il modello è molto affidabile, il punteggio del risultato migliore sarà molto migliore del secondo migliore. In caso di incertezza, i punteggi dei due risultati principali saranno simili. Inoltre, tieni presente che i classificatori delle forme interpretano l'intero Ink come una singola forma. Ad esempio, se Ink contiene un rettangolo e un'ellisse uno accanto all'altro, il sistema di riconoscimento potrebbe restituire uno o l'altro (o qualcosa di completamente diverso) come risultato, poiché un singolo candidato al riconoscimento non può rappresentare due forme.