Reconhecimento de recibos
OCR de recibos offline em Expo: o trabalho começa depois de reconhecer o texto
Interpretar recibos no dispositivo com Expo, Vision e ML Kit: leituras candidatas, verificação aritmética, unidades explícitas e revisão dos dados.
Atualizado em
Neste artigo
Num recibo de combustível, 1,899 pode ser o preço unitário. 95 pode identificar o combustível. 04 pode ser o número da bomba. O motor de OCR pode ler os três valores na perfeição e, ainda assim, produzir um registo de abastecimento errado.
Adicionei a leitura de recibos ao OdoKeep, o meu diário de bordo em React Native e Expo. O reconhecimento corre no telemóvel: com o Vision da Apple no iOS e os reconhecedores de texto do Google ML Kit incluídos na app Android. Esta funcionalidade não envia a fotografia nem o texto reconhecido para um servidor.
O módulo nativo trata da captura, preparação da imagem, reconhecimento e fotografia temporária. A interpretação vive em TypeScript, em lib/receipt-scan. Esta separação permite testar o parser sem depender de uma câmara ou de um modelo de telemóvel.
Manter o texto ligado às suas provas
O resultado nativo é mais rico do que uma string. Inclui linhas, caixas de origem, informação de confiança disponível e, quando o reconhecedor as disponibiliza, transcrições alternativas. O parser usa essas posições para ligar cada campo ao local de onde veio na fotografia.
O percurso é aproximadamente este:
Fotografia
-> linhas e caixas do OCR nativo
-> tokens numéricos com unidades e posições de origem
-> candidatos ordenados por campo
-> confirmação aritmética
-> rascunho revisto
-> formulário original de abastecimento ou carregamento
Não há escrita entre o reconhecimento e o formulário original. A leitura produz um preenchimento inicial que o utilizador pode corrigir e guardar pelo mesmo percurso da introdução manual.
Ordenar as leituras antes de escolher um número
A normalização numérica é o primeiro ponto em que o texto deixa de ser simples. Os recibos podem usar vírgula ou ponto decimal, espaços de agrupamento, apóstrofos suíços ou outros sistemas de algarismos. A impressão térmica também pode perder um separador ou transformar um algarismo numa letra.
O tokenizer pode conservar várias interpretações plausíveis de um número. Um token como 1,899 não precisa de ficar decidido antes de o parser analisar a legenda, a unidade e os valores próximos. Uma correção possível recebe menos peso do que uma leitura normal, e mantém a sua posição de origem.
A classificação começa pelas provas impressas. Uma unidade explícita de quantidade é um indício forte de quantidade. Uma indicação como EUR/L é um indício forte de preço unitário. Uma legenda de total aponta para o valor de referência.
Datas, percentagens, leituras de distância e referências administrativas são excluídas dos campos de montantes. Um número sem legenda não passa a ser a quantidade em falta só porque facilita a conta. A pesquisa conserva no máximo vinte candidatos por campo, limitando as combinações a avaliar.
A multiplicação confirma uma hipótese
A aritmética parece ser a parte fácil:
quantity * unitPrice = printedTotal
É útil, mas permite uma conclusão mais limitada do que «o recibo está correto». Um produto coincidente reforça uma interpretação candidata. Não prova que a imagem foi lida corretamente nem que os três campos representam a transação que se pressupõe.
O parser compara o produto com metade da unidade monetária mínima, acrescida de uma pequena margem para vírgula flutuante. Numa moeda com duas casas decimais, a tolerância de base é meio cêntimo. Não usa uma percentagem da conta, que faria aceitar erros maiores em recibos de maior valor.
Este exemplo faz parte dos testes:
31.93 L x 1.899 EUR/L
TOTAL 60.84 EUR
O produto é aproximadamente 60,635. Uma tolerância percentual generosa poderia aceitar 60,84. A comparação baseada na moeda rejeita essa correspondência aritmética.
Outro teste tem quantidade e preço perfeitamente legíveis, um subtotal igual ao produto e um valor final pago inferior:
40 L x 1.80 EUR/L
Subtotal 72 EUR
Discount 4 EUR
Total 68 EUR
O parser conserva 68 como total impresso de referência. Não promove 72 a total final só porque faz a multiplicação bater certo. Quantidade e preço unitário podem continuar a ser úteis quando o montante final inclui um desconto.
O OdoKeep calcula o custo guardado multiplicando a quantidade pelo preço unitário e subtraindo um desconto opcional revisto pelo utilizador. O total impresso não é copiado para o custo guardado. Também não permite calcular, por divisão, uma quantidade ou um preço em falta.
Se só foi reconhecido o total, o utilizador ainda precisa de preencher os valores. Se há quantidade, mas não há preço, o preço fica vazio. Um resultado parcial pode continuar parcial.
Quando a leitura não passa a verificação aritmética, a app pode pedir uma nova leitura com maior contraste. As duas passagens são comparadas primeiro pela confirmação aritmética e depois pelos campos de montantes recuperados. Em caso de empate, prevalece a leitura original. Data, hora, posto e outros campos não monetários podem preencher lacunas a partir da passagem preterida, porque ambas se referem à mesma fotografia.
Mesmo a leitura melhorada passa por revisão. Mais provas alteram a classificação dos candidatos; não eliminam a oportunidade de o utilizador os corrigir.
Unidades e moedas exigem escolhas explícitas
As unidades geram outra família de erros credíveis. Litros, galões americanos, galões imperiais, quilogramas, metros cúbicos e quilowatt-hora são válidos no contexto de abastecimento e carregamento da app. Não são intercambiáveis.
Um recibo que imprime apenas gal não determina qual dos galões foi usado. A app pergunta ao utilizador. Ao converter entre unidades de volume compatíveis, multiplica a quantidade pelo fator de conversão e divide o preço unitário pelo mesmo fator:
convertedQuantity = printedQuantity * factor
convertedUnitPrice = printedUnitPrice / factor
Isto preserva o produto antes do arredondamento dos campos. Os valores convertidos são arredondados para, no máximo, três casas decimais. Por isso, o custo recalculado no formulário pode diferir ligeiramente da referência impressa. Manter a unidade original evita esse arredondamento de conversão.
Conflitos entre grandezas físicas exigem mais prudência. Uma quantidade em litros e um preço explicitamente por quilowatt-hora não podem formar um custo de combustível. O parser conserva a quantidade e a unidade, deixando o preço vazio. Não reinterpreta uma grandeza como se fosse outra.
A moeda também é explícita. Se o recibo indicar outra moeda, o utilizador pode mantê-la. Se escolher a moeda configurada na app, o importador conserva a quantidade e limpa o preço unitário. Essa escolha não pressupõe uma taxa de câmbio. Mostrar posteriormente um custo guardado noutra moeda é uma responsabilidade separada da conversão da app baseada na data do registo.
O contexto regional vem da região do dispositivo, independentemente do idioma da interface. Quem usa a interface em inglês em Portugal não deve ter todos os recibos interpretados como americanos. Códigos monetários e unidades explícitos têm mais peso do que os valores regionais predefinidos.
Reconhecimento e ciclo de vida da fotografia
No iOS, o Vision usa reconhecimento preciso com a correção linguística desativada. A escolha é intencional: quantidades e nomes abreviados de produtos não beneficiam de todas as suposições que ajudam um modelo linguístico a corrigir prosa. As sugestões de idioma são verificadas contra os idiomas suportados pelo pedido Vision configurado antes de serem usadas.
No Android, a app inclui os reconhecedores dos sistemas de escrita latino, chinês, devanágari, japonês e coreano descritos na documentação do ML Kit. Isto aumenta o tamanho da app e disponibiliza esses reconhecedores offline. A captura pelo digitalizador de documentos tem requisitos próprios dos Google Play services e pode precisar de uma transferência inicial. O seletor de imagens e o OCR incluído na app oferecem uma alternativa. Isto não demonstra suporte para todos os sistemas de escrita.
O tratamento da imagem importa tanto como a classificação dos campos. A orientação é normalizada nos píxeis, e as caixas do OCR têm de coincidir com a fotografia apresentada. As duas implementações nativas podem experimentar rotações quando a leitura inicial é escassa, mas uns poucos fragmentos adicionais não chegam para justificar rodar a imagem.
As cópias temporárias pertencem a uma sessão de leitura. Uma sessão cancelada não pode guardar uma fotografia atrasada depois da limpeza, e uma revisão não pode apagar a imagem de outra. O rascunho pertence ainda ao formulário que o pediu e só pode ser consumido uma vez. Regressar ao formulário não deve substituir as correções do utilizador pela leitura antiga.
Testar a interpretação separadamente do reconhecimento
Os testes do parser incluem duas transcrições de recibos reais e casos sintéticos para formatos regionais, unidades ambíguas, descontos, datas inválidas, leituras alternativas e transferência parcial de campos. Verificam o que o parser faz com texto OCR. Não medem a taxa de reconhecimento de um conjunto de imagens. Pouca luz, impressão esbatida e cobertura de sistemas de escrita continuam a exigir avaliação em dispositivos.
Esta separação facilita a investigação das falhas. Se os caracteres estão errados, examino a captura e o reconhecimento. Se os caracteres estão certos, mas o registo está errado, examino as provas usadas pelo parser e a passagem para o formulário. Um objeto JSON aparentemente completo esconderia a diferença.
A leitura de recibos faz parte do OdoKeep. Quem regista abastecimentos ou carregamentos pode experimentar o percurso na app iOS: fotografar o recibo, rever o que foi recuperado e terminar no formulário habitual.