Con el reconocimiento de tinta digital del ML Kit, puedes reconocer texto escrito a mano en una superficie digital en cientos de idiomas, así como clasificar bocetos.
Probar
- Prueba la app de ejemplo para ver un ejemplo de uso de esta API.
Antes de comenzar
Incluye las siguientes bibliotecas de ML Kit en tu Podfile:
pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'Después de instalar o actualizar los Pods de tu proyecto, abre el proyecto de Xcode con su
.xcworkspace. El ML Kit es compatible con Xcode 13.2.1 o versiones posteriores.
Ya estás listo para comenzar a reconocer texto en objetos Ink.
Compila un objeto Ink
La forma principal de compilar un objeto Ink es dibujarlo en una pantalla táctil. En iOS,
puedes usar un UIImageView junto con
controladores de eventos
táctiles
que dibujen los trazos en la pantalla y también almacenen los puntos de los trazos para compilar
el objeto Ink. Este patrón general se demuestra en el siguiente fragmento de código. Consulta la app de guía de inicio rápido para ver un ejemplo completado, que separa el control de eventos táctiles, el dibujo de pantalla y la administración de datos de trazos.
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]; }
Ten en cuenta que el fragmento de código incluye una función de ejemplo para dibujar el trazo en la UIImageView, que se debe adaptar según sea necesario para tu aplicación. Te recomendamos que uses
extremos redondeados cuando dibujes los segmentos de línea para que los segmentos de longitud cero se
dibujen como un punto (piensa en el punto de la letra i minúscula). La función doRecognition() se llama después de que se escribe cada trazo y se definirá a continuación.
Obtén una instancia de DigitalInkRecognizer
Para realizar el reconocimiento, debemos pasar el objeto Ink a una
instancia de DigitalInkRecognizer. Para obtener la instancia DigitalInkRecognizer,
primero debemos descargar el modelo de reconocedor para el idioma deseado y
cargar el modelo en la RAM. Esto se puede lograr con el siguiente fragmento de código, que, para simplificar, se coloca en el método viewDidLoad() y usa un nombre de idioma codificado. Consulta la app de inicio rápido para ver un ejemplo de cómo mostrar la lista de idiomas disponibles al usuario y descargar el idioma seleccionado.
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]; }
Las apps de la guía de inicio rápido incluyen código adicional que muestra cómo controlar varias descargas al mismo tiempo y cómo determinar qué descarga se realizó correctamente mediante el control de las notificaciones de finalización.
Reconoce un objeto Ink
A continuación, llegamos a la función doRecognition(), a la que, para simplificar, se llama
desde touchesEnded(). En otras aplicaciones, es posible que se quiera invocar
el reconocimiento solo después de un tiempo de espera o cuando el usuario presione un botón para activar
el reconocimiento.
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]; }]; }
Administra las descargas de modelos
Ya vimos cómo descargar un modelo de reconocimiento. En los siguientes fragmentos de código, se ilustra cómo verificar si ya se descargó un modelo o cómo borrarlo cuando ya no sea necesario para recuperar el espacio de almacenamiento.
Verifica si ya se descargó un modelo
Swift
let model : DigitalInkRecognitionModel = ... let modelManager = ModelManager.modelManager() modelManager.isModelDownloaded(model)
Objective-C
MLKDigitalInkRecognitionModel *model = ...; MLKModelManager *modelManager = [MLKModelManager modelManager]; [modelManager isModelDownloaded:model];
Borra un modelo descargado
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."); }]; }
Sugerencias para mejorar la precisión del reconocimiento de texto
La precisión del reconocimiento de texto puede variar según el idioma. La precisión también depende del estilo de escritura. Si bien el Reconocimiento de tinta digital está entrenado para manejar muchos tipos de estilos de escritura, los resultados pueden variar de un usuario a otro.
Estas son algunas formas de mejorar la precisión de un reconocedor de texto. Ten en cuenta que estas técnicas no se aplican a los clasificadores de dibujos para emojis, dibujo automático y formas.
Área de escritura
Muchas aplicaciones tienen un área de escritura bien definida para la entrada del usuario. El significado de un símbolo se determina, en parte, por su tamaño en relación con el tamaño del área de escritura que lo contiene. Por ejemplo, la diferencia entre una letra "o" o "c" en mayúscula o minúscula, y una coma en comparación con una barra diagonal.
Indicarle al reconocedor el ancho y la altura del área de escritura puede mejorar la precisión. Sin embargo, el reconocedor supone que el área de escritura solo contiene una línea de texto. Si el área de escritura física es lo suficientemente grande como para permitir que el usuario escriba dos o más líneas, puedes obtener mejores resultados si pasas un WritingArea con una altura que sea tu mejor estimación de la altura de una sola línea de texto. No es necesario que el objeto WritingArea que pasas al reconocedor corresponda exactamente con el área de escritura física en la pantalla. Cambiar la altura de WritingArea de esta manera funciona mejor en algunos idiomas que en otros.
Cuando especifiques el área de escritura, indica su ancho y alto en las mismas unidades que las coordenadas de los trazos. Los argumentos de coordenadas x,y no tienen requisitos de unidad: la API normaliza todas las unidades, por lo que lo único que importa es el tamaño y la posición relativos de los trazos. Puedes pasar coordenadas en cualquier escala que tenga sentido para tu sistema.
Contexto previo
El precontexto es el texto que precede de inmediato a los trazos en el Ink que intentas
reconocer. Puedes ayudar al reconocedor proporcionándole información sobre el contexto previo.
Por ejemplo, las letras cursivas "n" y "u" suelen confundirse entre sí. Si el usuario ya ingresó la palabra parcial "arg", puede continuar con trazos que se puedan reconocer como "ument" o "nment". Especificar el contexto previo "arg" resuelve la ambigüedad, ya que la palabra "argument" es más probable que "argnment".
El contexto previo también puede ayudar al reconocedor a identificar las pausas entre palabras, es decir, los espacios entre ellas. Puedes escribir un carácter de espacio, pero no puedes dibujar uno, por lo que, ¿cómo puede un reconocedor determinar cuándo termina una palabra y comienza la siguiente? Si el usuario ya escribió "hola" y continúa con la palabra escrita "mundo", sin contexto previo, el reconocedor devuelve la cadena "mundo". Sin embargo, si especificas el contexto previo "hello", el modelo devolverá la cadena " world", con un espacio inicial, ya que "hello world" tiene más sentido que "helloword".
Debes proporcionar la cadena de contexto previo más larga posible, de hasta 20 caracteres, incluidos los espacios. Si la cadena es más larga, el reconocedor solo usa los últimos 20 caracteres.
En la siguiente muestra de código, se indica cómo definir un área de escritura y usar un objeto RecognitionContext para especificar el contexto previo.
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); }];
Orden de los trazos
La precisión del reconocimiento es sensible al orden de los trazos. Los reconocedores esperan que los trazos se realicen en el orden en que las personas escribirían de forma natural; por ejemplo, de izquierda a derecha para el inglés. Cualquier caso que se aparte de este patrón, como escribir una oración en inglés que comience con la última palabra, da resultados menos precisos.
Otro ejemplo es cuando se quita una palabra en medio de un Ink y se reemplaza por
otra. Es probable que la revisión esté en medio de una oración, pero los trazos de la revisión
están al final de la secuencia de trazos.
En este caso, recomendamos enviar la palabra recién escrita por separado a la API y combinar el
resultado con los reconocimientos anteriores usando tu propia lógica.
Cómo lidiar con formas ambiguas
Hay casos en los que el significado de la forma proporcionada al reconocedor es ambiguo. Por ejemplo, un rectángulo con bordes muy redondeados podría verse como un rectángulo o una elipse.
Estos casos poco claros se pueden controlar con las puntuaciones de reconocimiento cuando estén disponibles. Solo los clasificadores de formas proporcionan puntuaciones. Si el modelo tiene mucha confianza, la puntuación del resultado principal será
mucho mejor que la del segundo mejor. Si hay incertidumbre, las puntuaciones de los dos resultados principales serán
cercanas. Además, ten en cuenta que los clasificadores de formas interpretan todo el Ink como una sola forma. Por ejemplo, si el Ink contiene un rectángulo y una elipse uno al lado del otro, el reconocedor puede devolver uno o el otro (o algo completamente diferente) como resultado, ya que un solo candidato de reconocimiento no puede representar dos formas.