Com o reconhecimento 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.
Testar
- Explore o app de amostra para ver um exemplo de uso dessa API.
Antes de começar
Inclua as seguintes bibliotecas do kit de ML no seu Podfile:
pod 'GoogleMLKit/DigitalInkRecognition', '3.2.0'
Depois de instalar ou atualizar os pods do projeto, abra o projeto do Xcode usando o
.xcworkspace
. O Kit de ML é compatível com o Xcode versão 13.2.1 ou mais recente.
Agora você está pronto para começar a reconhecer texto em objetos Ink
.
Criar um objeto Ink
A principal maneira de criar um objeto Ink
é desenhá-lo em uma tela touchscreen. No iOS,
é possível usar uma UIImageView com
gerenciadores de eventos
de toque
que desenham os traços na tela e também 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 gerenciamento de eventos de toque, o desenho da tela e o gerenciamento de dados de traço.
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 em UIImageView, que precisa ser adaptada conforme necessário para seu aplicativo. Recomendamos o uso de letras maiúsculas ao desenhar os segmentos de linha para que os segmentos com tamanho zero sejam desenhados como um ponto. Pense no ponto em uma letra minúscula i. A função doRecognition()
é chamada depois que cada traço é gravado e será definida abaixo.
Receber uma instância de DigitalInkRecognizer
Para realizar o reconhecimento, precisamos transmitir o objeto Ink
para uma
instância DigitalInkRecognizer
. Para conseguir a instância DigitalInkRecognizer
, primeiro é necessário fazer o download do modelo de reconhecimento da linguagem desejada e carregá-lo na RAM. Isso pode ser feito usando o snippet de código a seguir, que, para simplificar, é colocado no método viewDidLoad()
e usa um nome de linguagem codificada. 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 fazer o download do 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 aplicativos 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 processado com êxito por meio das notificações de conclusão.
Reconhecer um objeto Ink
Em seguida, chegamos à função doRecognition()
, que, para simplificar, é chamada em touchesEnded()
. Em outros aplicativos, talvez você queira 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 fazer o download de um modelo de reconhecimento. Os snippets de código a seguir ilustram como verificar se um modelo já foi transferido por download ou como excluir um modelo quando não for mais necessário para recuperar o espaço de armazenamento.
Verificar se um modelo já foi transferido por download
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 transferido por download
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 de acordo com o idioma. A precisão 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.
Veja algumas maneiras de melhorar a precisão de um reconhecedor de texto. Essas técnicas não se aplicam aos classificadores de desenho para emojis, renderização automática e formas.
Área de escrita
Muitos aplicativos têm uma área de escrita bem definida para a 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 maiúscula ou minúscula "o" ou "c" e uma vírgula em relação a uma barra.
Informar ao reconhecedor a largura e a altura da área de escrita pode melhorar a precisão. No entanto, o reconhecedor presume que a área de escrita contém apenas uma única 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 uma área de gravação com uma altura que seja sua melhor estimativa da altura de uma única linha de texto. O objeto WritingArea que você transmite ao reconhecedor não precisa corresponder exatamente à área de escrita física na tela. Alterar a altura da WritingArea dessa maneira funciona melhor em alguns idiomas do que em outros.
Ao especificar a área de escrita, especifique a largura e a altura nas mesmas unidades que as coordenadas do traço. Os argumentos de coordenada x,y não têm requisito de unidade. A API normaliza todas as unidades. Portanto, o único fator importante é o tamanho relativo e a posição dos traços. Você pode passar coordenadas em qualquer escala que fizer sentido para seu sistema.
Pré-contexto
Pré-contexto é o texto que precede imediatamente os traços no Ink
que você está tentando reconhecer. Você pode ajudar o reconhecedor informando o pré-contexto.
Por exemplo, as letras cursivas "n" e "u" costumam ser confundidas umas com as outras. Se o usuário já inseriu a palavra parcial "arg", ele pode continuar com traços que podem ser reconhecidos como "ument" ou "nment". Especificar o "arg" pré-contexto resolve a ambiguidade, já que a palavra "argumento" é mais provável do que "argumento".
O pré-contexto também pode ajudar o reconhecedor a identificar quebras de palavras, os espaços entre as palavras. Você pode digitar um caractere de espaço, mas não pode desenhá-lo. Então, como um reconhecedor pode determinar 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 o contexto, o reconhecedor retornará a string "world". No entanto, se você especificar o pré-contexto "hello", o modelo retornará a string "world", com um espaço inicial, 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á somente os últimos 20 caracteres.
O exemplo de código abaixo mostra como definir uma área de escrita 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 dos traços
A precisão do reconhecimento é sensível à ordem dos traços. Os reconhecedores esperam que os traços ocorram na ordem em que as pessoas escreveriam naturalmente, por exemplo, da esquerda para a direita no inglês. Qualquer caso que parte desse padrão, como escrever uma frase em inglês que começa com a última palavra, fornece resultados menos precisos.
Outro exemplo é quando uma palavra no meio de uma Ink
é removida e substituída por outra palavra. A revisão provavelmente está no meio de uma frase, mas os traços da revisão estão no final.
Nesse caso, recomendamos enviar a palavra recém-gravada 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 pouco claros podem ser tratados com o uso de pontuações de reconhecimento, quando disponíveis. Somente classificadores de forma fornecem pontuações. Se o modelo for muito confiante, a pontuação do primeiro resultado será muito melhor do que a segunda melhor. Se houver incerteza, as pontuações dos dois primeiros resultados serão próximas. Além disso, lembre-se de que os classificadores de forma interpretam Ink
inteira 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 a reconhecimento não pode representar duas formas.