Reconocimiento de tinta digital con ML Kit en iOS

Con el reconocimiento de tinta digital de ML Kit, puedes reconocer texto escrito a mano en una en cientos de idiomas, además de clasificar bocetos.

Probar

Antes de comenzar

  1. Incluye las siguientes bibliotecas de ML Kit en tu Podfile:

    pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
    
    
  2. Después de instalar o actualizar los Pods de tu proyecto, abre tu proyecto de Xcode. con su .xcworkspace. El Kit de AA es compatible con la versión de Xcode 13.2.1 o superior.

Ya puedes 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 una UIImageView junto con evento táctil controladores que dibujan los trazos en la pantalla y también almacenan los trazos puntos para crear el objeto Ink. Este patrón general se demuestra en el siguiente código . Consulta la guía de inicio rápido app para un ejemplo más completo, que separa el manejo de eventos táctiles, el dibujo de pantalla, y la administración de datos de accidentes cerebrovasculares.

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

Observa que el fragmento de código incluye una función de ejemplo para dibujar el trazo en la UIImageView, que debe adaptarse según sea necesario para tu aplicación. Recomendamos usar los vértices redondeados al dibujar los segmentos de línea, de modo que los segmentos de longitud cero queden dibujada como un punto (piensa en el punto sobre una letra i minúscula). El doRecognition() se llama a la función después de escribir cada trazo y se definirá a continuación.

Obtén una instancia de DigitalInkRecognizer.

Para realizar el reconocimiento, debemos pasar el objeto Ink a un Instancia DigitalInkRecognizer. Para obtener la instancia DigitalInkRecognizer, haz lo siguiente: primero debemos descargar el modelo de reconocimiento para el idioma deseado. cargarás el modelo en la RAM. Esto se puede lograr con el siguiente código: , que, por cuestiones de simplicidad, se coloca en el método viewDidLoad() y usa un nombre de idioma codificado. Consulta la guía de inicio rápido de la aplicación para 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 inicio rápido incluyen código adicional que muestra cómo manejar varias al mismo tiempo y cómo determinar cuál de ellas se realizó correctamente mediante la administración las notificaciones de finalización.

Reconoce un objeto Ink

Luego, llegamos a la función doRecognition(), que por simplicidad se llama desde touchesEnded(). En otras aplicaciones, es posible invocar el reconocimiento de imágenes solo después de un tiempo de espera, o cuando el usuario presiona un botón para activar el reconocimiento de imágenes.

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. El siguiente código Los fragmentos muestran cómo comprobar si un modelo ya se descargó. para borrar un modelo cuando ya no sea necesario para recuperar 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 en el 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 dibujo para emojis, autodibujo y formas.

Área de escritura

Muchas aplicaciones tienen un área de escritura bien definida para las entradas del usuario. El significado de un símbolo es determinado parcialmente por su tamaño en relación con el tamaño del área de escritura que lo contiene. Por ejemplo, la diferencia entre una "o" minúscula o mayúscula o "c", y una coma versus una tiene una barra diagonal.

Decirle 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 los componentes físicos El área de escritura es lo suficientemente grande como para permitir que el usuario escriba dos o más líneas, puedes mejorar resultados pasando un WritingArea con una altura que sea tu mejor estimación de la altura de una una sola línea de texto. El objeto WritingArea que pasas al reconocedor no tiene que corresponder exactamente con el área física de escritura en la pantalla. Se cambia la altura de WritingArea de esta manera funciona mejor en algunos lenguajes que en otros.

Cuando especifiques el área de escritura, especifica su ancho y alto en las mismas unidades que el trazo coordenadas. Los argumentos de las coordenadas x,y no tienen requisitos de unidades; la API normaliza todos por lo que lo único que importa es el tamaño relativo y la posición de los trazos. Puedes pasar coordenadas en la escala que tenga sentido para tu sistema.

Contexto previo

El contexto previo es el texto que precede inmediatamente a los trazos en la Ink que intentan reconocer. Puedes ayudar al reconocedor si le informas sobre el contexto previo.

Por ejemplo, las letras cursivas "n" y "u" a menudo se confunden unos con otros. Si el usuario tiene ingresaron la palabra parcial "arg", podrían continuar con trazos que se puedan reconocer como "ument" o "nment". Cómo especificar el “argumento” previo al contexto resuelve la ambigüedad, ya que la palabra "argumento" es más probable que “argumento”.

El contexto previo también puede ayudar al reconocedor a identificar saltos de palabras, los espacios entre palabras. Puedes Escribe un carácter de espacio, pero no puedes dibujarlo, ¿cómo puede un reconocedor determinar cuándo termina una palabra? y empieza la siguiente? Si el usuario ya escribió "hello" y continúa con la palabra escrita “world”, sin el contexto previo, el reconocedor muestra la string “world”. Sin embargo, si especificas pre-context “hello”, el modelo mostrará la cadena “ mundo", con un espacio líder, ya que “hola en todo el mundo" tiene más sentido que “helloword”.

Debes proporcionar la cadena previa al contexto más larga posible, con un máximo de 20 caracteres, incluida la y 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 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 trazo

La precisión del reconocimiento depende del orden de los trazos. Los reconocedores esperan que los trazos ocurrir en el orden en que la gente naturalmente escribiría; por ejemplo, de izquierda a derecha para inglés. Cualquier caso que se aparta de este patrón, como escribir una oración en inglés empezando con la última palabra, da resultados menos precisos.

Otro ejemplo es cuando se quita una palabra del medio de un Ink y se reemplaza por otra palabra. La revisión probablemente esté en la mitad de una oración, pero los trazos para la revisión están al final de la secuencia de trazo. En este caso, recomendamos enviar la palabra nueva por separado a la API y combinar la resultado con los reconocimientos previos usando tu propia lógica.

Cómo abordar las formas ambiguas

Hay casos en los que el significado de la forma proporcionada al reconocedor es ambiguo. Para ejemplo, un rectángulo con bordes muy redondeados podría verse como un rectángulo o una elipse.

Estos casos poco claros se pueden manejar con puntuaciones de reconocimiento cuando están disponibles. Solo los clasificadores de formas proporcionan puntuaciones. Si el modelo es muy seguro, la puntuación del resultado principal será mucho mejor que la segunda mejor. Si hay incertidumbre, las puntuaciones de los dos primeros resultados y estar cerca. Además, ten en cuenta que los clasificadores de formas interpretan el Ink completo como un elemento forma única. Por ejemplo, si Ink contiene un rectángulo y una elipse junto a cada uno el reconocedor puede mostrar uno u otro (o algo completamente diferente) como dado que un solo candidato de reconocimiento no puede representar dos formas.