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 un esempio di utilizzo di questa API.
Prima di iniziare
Includi le seguenti librerie di ML Kit nel tuo podfile:
pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
Dopo aver installato o aggiornato i pod del progetto, apri il progetto Xcode utilizzando
.xcworkspace
. ML Kit è supportato in Xcode versione 13.2.1 o successiva.
Ora puoi iniziare a riconoscere il testo in oggetti Ink
.
Crea un oggetto Ink
Il modo principale per creare un oggetto Ink
è tracciarlo su un touchscreen. Su iOS, puoi utilizzare una UIImageView insieme agli
gestori di eventi touch
che tracciano i tratti sullo schermo e memorizzano anche i punti dei tratti per creare
l'oggetto Ink
. Questo pattern generale è mostrato nel seguente snippet di codice. Per un esempio più completo, consulta la app della guida rapida, che separa la gestione degli eventi di tocco, il disegno dello schermo e la gestione dei dati sui tratti.
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 attirare l'elemento nella UIImageView, che deve essere adattata a seconda dell'applicazione. Consigliamo di utilizzare i limiti rotondi quando disegni i segmenti della linea, in modo che i segmenti di lunghezza pari a zero vengano disegnati come punti (pensa al punto su una lettera minuscola i). La funzione doRecognition()
viene chiamata dopo la scrittura di ogni tratto e viene definita di seguito.
Ottieni un'istanza di DigitalInkRecognizer
Per eseguire il riconoscimento, dobbiamo passare l'oggetto Ink
a un'istanza DigitalInkRecognizer
. Per ottenere l'istanza DigitalInkRecognizer
,
devi prima scaricare il modello di riconoscimento per la lingua desiderata e
caricare il modello nella RAM. Ciò può essere ottenuto utilizzando il seguente snippet di codice, che per semplicità viene inserito nel metodo viewDidLoad()
e utilizza un nome lingua hardcoded. Consulta l'app di 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 della guida rapida includono codice aggiuntivo che mostra come gestire più download contemporaneamente e come determinare quale download è riuscito gestendo le notifiche di completamento.
Riconosci un oggetto Ink
Ora passiamo alla funzione doRecognition()
, che per semplicità viene chiamata
da touchesEnded()
. In altre applicazioni, è possibile richiamare 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 del modello
Abbiamo già visto come scaricare un modello di riconoscimento. I seguenti snippet di codice indicano come verificare se un modello è già stato scaricato o come eliminare un modello quando non è più necessario 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 alla lingua. L'accuratezza dipende anche dallo stile di scrittura. Sebbene il riconoscimento dell'inchiostro digitale sia addestrato per gestire molti tipi di stili di scrittura, i risultati possono variare da utente a utente.
Ecco alcuni modi per migliorare la precisione di un riconoscimento di testo. Tieni presente che queste tecniche non si applicano ai classificatori di disegno per emoji, disegno automatico e forme.
Area di scrittura
Molte applicazioni hanno un'area di scrittura ben definita per l'input dell'utente. Il significato di un simbolo è determinato in parte dalle sue dimensioni rispetto all'area di scrittura che lo contiene. Ad esempio, la differenza tra una lettera minuscola o iniziale "o" o "c" e una virgola rispetto a una barra.
Indicare al riconoscimento della larghezza e dell'altezza dell'area di scrittura può migliorare la precisione. Tuttavia, il riconoscimento presuppone che l'area di scrittura contenga solo una singola riga di testo. Se l'area di scrittura fisica è abbastanza grande da consentire all'utente di scrivere due o più righe, potresti ottenere risultati migliori passando in un'area di scrittura con un'altezza che è la tua stima migliore dell'altezza di una singola riga di testo. L'oggetto WritingArea che trasmetti al riconoscimento non deve corrispondere esattamente all'area di scrittura fisica sullo schermo. La modifica dell'altezza Writing Area in questo modo funziona meglio in alcune lingue.
Quando specifichi l'area di scrittura, specifica la larghezza e l'altezza nelle stesse unità delle coordinate del tratto. Gli argomenti coordinati x,y non hanno requisiti per le unità: l'API normalizza tutte le unità, pertanto l'unica cosa importante è la dimensione e la posizione relative dei tratti. Puoi inviare le coordinate in qualsiasi scala adatta al tuo sistema.
Pre-contesto
Il pre-contesto è il testo che precede immediatamente i tratti del Ink
che stai
cercando di riconoscere. Puoi aiutare il riconoscimento comunicandogli il pre-contesto.
Ad esempio, le lettere corsive "n" e "u" sono spesso scambiate tra loro. Se l'utente ha già inserito la parola parziale "arg," potrebbe continuare con tratti che possono essere riconosciuti come "mento" o "nment". La specifica del pre-contesto "arg" risolve l'ambiguità, poiché la parola "argomento" è più probabile che "argnment".
Il pre-contesto può anche aiutare il riconoscimento a identificare le interruzioni di parola, ovvero gli spazi tra le parole. Puoi digitare uno spazio, ma non ne puoi disegnare uno, quindi in che modo un riconoscimento può determinare quando una parola termina e inizia la successiva? Se l'utente ha già scritto "hello" e continua con la parola scritta "world", senza pre-contesto, il riconoscimento restituisce la stringa "world". Tuttavia, se specifichi il pre-contesto "hello", il modello restituirà la stringa "world", con uno spazio iniziale, poiché "hello world" ha più senso di "helloword".
Devi fornire la stringa di contesto contestuale più lunga possibile, fino a 20 caratteri, spazi inclusi. Se la stringa è più lunga, il riconoscimento 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 bracciate
La precisione del riconoscimento è sensibile all'ordine dei tratti. I riconoscitori si aspettano che i tratti avvengano nell'ordine in cui le persone scriverebbero naturalmente, ad esempio da sinistra a destra per l'inglese. Qualsiasi caso che si discosti da questo pattern, ad esempio la scrittura di una frase inglese che inizia con l'ultima parola, fornisce risultati meno precisi.
Un altro esempio è quando una parola al centro di un elemento Ink
viene rimossa e sostituita con un'altra parola. Probabilmente la revisione si trova a metà di una frase, ma i tratti della revisione sono alla fine della sequenza di tratti.
In questo caso consigliamo di inviare separatamente la parola appena scritta all'API e di unire il risultato con i riconoscimenti precedenti utilizzando la tua logica.
Gestire forme ambigue
Ci sono casi in cui il significato della forma fornita al riconoscente è ambiguo. Ad esempio, un rettangolo con bordi molto arrotondati può essere considerato un rettangolo o un'ellisse.
Queste richieste non chiare possono essere gestite utilizzando i punteggi di riconoscimento, se disponibili. Solo i classificatori di forma forniscono punteggi. Se il modello è molto sicuro, il punteggio del risultato migliore sarà di gran lunga migliore rispetto al secondo. In caso di incertezza, i punteggi dei primi due risultati saranno vicini. Ricorda inoltre che i classificatori di forma interpretano l'intero Ink
come una singola forma. Ad esempio, se Ink
contiene un rettangolo e un'ellissi uno accanto all'altro, il riconoscimento può restituire l'uno o l'altro (o qualcosa di completamente diverso) come risultato, perché un singolo candidato di riconoscimento non può rappresentare due forme.