Reconhecimento de tinta digital com o Kit de ML no iOS

Com o reconhecimento de tinta digital do Kit de ML, é possível reconhecer texto escrito à mão em uma superfície digital em centenas de idiomas, além de classificar esboços.

Faça um teste

Antes de começar

  1. Inclua as seguintes bibliotecas do ML Kit no seu Podfile:

    pod 'GoogleMLKit/DigitalInkRecognition', '8.0.0'
    
    
  2. Depois de instalar ou atualizar os pods do projeto, abra o projeto do Xcode usando o .xcworkspace. O Kit de ML é compatível com a versão 13.2.1 ou mais recente do Xcode.

Agora você já pode reconhecer texto em objetos Ink.

Crie um objeto Ink.

A principal maneira de criar um objeto Ink é desenhá-lo em uma tela sensível ao toque. No iOS, é possível usar uma UIImageView com processadores de eventos de toque, que desenham os traços na tela e armazenam os pontos deles para criar o objeto Ink. Esse padrão geral é demonstrado no snippet de código a seguir. Consulte o app de início rápido para ver um exemplo mais completo, que separa o processamento de eventos de toque, o desenho da tela e o gerenciamento de dados de traços.

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

O snippet de código inclui uma função de amostra para desenhar o traço na UIImageView, que precisa ser adaptada conforme necessário para seu aplicativo. Recomendamos usar roundcaps ao desenhar os segmentos de linha para que segmentos de comprimento zero sejam desenhados como um ponto (pense no ponto em uma letra i minúscula). A função doRecognition() é chamada depois que cada traço é escrito e será definida abaixo.

Receba uma instância de DigitalInkRecognizer

Para realizar o reconhecimento, precisamos transmitir o objeto Ink para uma instância de DigitalInkRecognizer. Para receber a instância DigitalInkRecognizer, primeiro é necessário fazer o download do modelo de reconhecimento para o idioma desejado e carregar o modelo na RAM. Isso pode ser feito usando o seguinte snippet de código, que, para simplificar, é colocado no método viewDidLoad() e usa um nome de idioma codificado. Consulte o app de início rápido para ver um exemplo de como mostrar a lista de idiomas disponíveis ao usuário e baixar o idioma selecionado.

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

Os apps de início rápido incluem código adicional que mostra como processar vários downloads ao mesmo tempo e como determinar qual download foi concluído processando as notificações de conclusão.

Reconhecer um objeto Ink

Em seguida, temos a função doRecognition(), que, para simplificar, é chamada de touchesEnded(). Em outros aplicativos, talvez seja necessário invocar o reconhecimento somente após um tempo limite ou quando o usuário pressionar um botão para acionar o reconhecimento.

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

Como gerenciar downloads de modelos

Já vimos como baixar um modelo de reconhecimento. Os snippets de código a seguir ilustram como verificar se um modelo já foi baixado ou como excluir um modelo quando ele não é mais necessário para recuperar o espaço de armazenamento.

Verificar se um modelo já foi baixado

Swift

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

Objective-C

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

Excluir um modelo baixado

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

Dicas para melhorar a precisão do reconhecimento de texto

A precisão do reconhecimento de texto pode variar em diferentes idiomas. A acurácia também depende do estilo de escrita. Embora o reconhecimento de tinta digital seja treinado para lidar com muitos tipos de estilos de escrita, os resultados podem variar de usuário para usuário.

Confira algumas maneiras de melhorar a precisão de um reconhecedor de texto. Essas técnicas não se aplicam aos classificadores de desenhos para emojis, desenho automático e formas.

Área de escrita

Muitos aplicativos têm uma área de escrita bem definida para entrada do usuário. O significado de um símbolo é parcialmente determinado pelo tamanho dele em relação ao tamanho da área de escrita que o contém. Por exemplo, a diferença entre uma letra "o" ou "c" maiúscula ou minúscula e uma vírgula em vez de uma barra.

Informar ao reconhecedor a largura e a altura da área de escrita pode melhorar a precisão. No entanto, o reconhecedor pressupõe que a área de escrita contém apenas uma linha de texto. Se a área de escrita física for grande o suficiente para permitir que o usuário escreva duas ou mais linhas, você poderá ter resultados melhores transmitindo um WritingArea com uma altura que seja sua melhor estimativa da altura de uma única linha de texto. O objeto WritingArea transmitido ao reconhecedor não precisa corresponder exatamente à área de escrita física na tela. Mudar a altura do WritingArea dessa forma funciona melhor em alguns idiomas do que em outros.

Ao especificar a área de escrita, informe a largura e a altura nas mesmas unidades das coordenadas do traço. Os argumentos de coordenadas x e y não têm requisito de unidade. A API normaliza todas as unidades. Portanto, a única coisa que importa é o tamanho e a posição relativos dos traços. Você pode transmitir coordenadas em qualquer escala que faça sentido para seu sistema.

Pré-contexto

O pré-contexto é o texto que precede imediatamente os traços no Ink que você está tentando reconhecer. Você pode ajudar o reconhecedor informando sobre o pré-contexto.

Por exemplo, as letras cursivas "n" e "u" são frequentemente confundidas. Se o usuário já tiver inserido a palavra parcial "arg", ele poderá continuar com traços que podem ser reconhecidos como "ument" ou "nment". Especificar o pré-contexto "arg" resolve a ambiguidade, já que a palavra "argument" é mais provável do que "argnment".

O pré-contexto também pode ajudar o reconhecedor a identificar quebras de palavras, os espaços entre elas. Você pode digitar um espaço, mas não desenhar um. Então, como um reconhecedor determina quando uma palavra termina e a próxima começa? Se o usuário já tiver escrito "hello" e continuar com a palavra escrita "world", sem pré-contexto, o reconhecedor vai retornar a string "world". No entanto, se você especificar o pré-contexto "hello", o modelo vai retornar a string " world", com um espaço à esquerda, já que "hello world" faz mais sentido do que "helloword".

Forneça a string de pré-contexto mais longa possível, com até 20 caracteres, incluindo espaços. Se a string for mais longa, o reconhecedor usará apenas os últimos 20 caracteres.

O exemplo de código abaixo mostra como definir uma área de gravação e usar um objeto RecognitionContext para especificar o pré-contexto.

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

Ordem de traços

A precisão do reconhecimento depende da ordem dos traços. Os reconhecedores esperam que os traços ocorram na ordem em que as pessoas escrevem naturalmente, por exemplo, da esquerda para a direita em inglês. Qualquer caso que se desvie desse padrão, como escrever uma frase em inglês começando com a última palavra, gera resultados menos precisos.

Outro exemplo é quando uma palavra no meio de um Ink é removida e substituída por outra palavra. A revisão provavelmente está no meio de uma frase, mas os traços dela estão no fim da sequência. Nesse caso, recomendamos enviar a palavra recém-escrita separadamente para a API e mesclar o resultado com os reconhecimentos anteriores usando sua própria lógica.

Como lidar com formas ambíguas

Há casos em que o significado da forma fornecida ao reconhecedor é ambíguo. Por exemplo, um retângulo com bordas muito arredondadas pode ser visto como um retângulo ou uma elipse.

Esses casos podem ser tratados usando pontuações de reconhecimento quando disponíveis. Somente os classificadores de forma fornecem pontuações. Se o modelo estiver muito confiante, a pontuação do melhor resultado será muito melhor do que a do segundo melhor. Se houver incerteza, as pontuações dos dois principais resultados serão próximas. Além disso, os classificadores de forma interpretam todo o Ink como uma única forma. Por exemplo, se o Ink contiver um retângulo e uma elipse um ao lado do outro, o reconhecedor poderá retornar um ou outro (ou algo completamente diferente) como resultado, já que um único candidato de reconhecimento não pode representar duas formas.