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
Em um 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 perfeitamente e, ainda assim, produzir um registro de abastecimento errado.
Adicionei a leitura de recibos ao OdoKeep, meu diário de bordo em React Native e Expo. O reconhecimento é executado no celular: com o Vision da Apple no iOS e os reconhecedores de texto do Google ML Kit incluídos no app Android. Esse recurso 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âmera ou de um modelo de celular.
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 revisado
-> 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 usuário pode corrigir e salvar 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 em uma 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, porcentagens, 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 que 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 ponto flutuante. Em uma moeda com duas casas decimais, a tolerância de base é meio centavo. Não usa uma porcentagem 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 fechar. Quantidade e preço unitário podem continuar sendo úteis quando o montante final inclui um desconto.
O OdoKeep calcula o custo salvo multiplicando a quantidade pelo preço unitário e subtraindo um desconto opcional revisado pelo usuário. O total impresso não é copiado para o custo salvo. Também não permite obter por divisão uma quantidade ou um preço que não foram reconhecidos.
Se só foi reconhecido o total, o usuário 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, o 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 usuário os corrigir.
Unidades e moedas exigem escolhas explícitas
As unidades geram outra família de erros plausí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 do app. Não são intercambiáveis.
Um recibo que imprime apenas gal não determina qual dos galões foi usado. O app pergunta ao usuário. 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
Isso 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 usuário pode mantê-la. Se escolher a moeda configurada no 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 salvo em outra moeda é uma responsabilidade separada da conversão do app baseada na data do registro.
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 padrão.
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 se 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, o app inclui os reconhecedores dos sistemas de escrita latino, chinês, devanágari, japonês e coreano descritos na documentação do ML Kit. Isso aumenta o tamanho do app e disponibiliza esses reconhecedores offline. A captura pelo scanner de documentos tem requisitos próprios dos Google Play services e pode precisar de uma download inicial. O seletor de imagens e o OCR incluído no app oferecem uma alternativa. Isso 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 pixels, e as caixas do OCR precisam 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 girar a imagem.
As cópias temporárias pertencem a uma sessão de leitura. Uma sessão cancelada não pode salvar 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. Voltar ao formulário não deve substituir as correções do usuário 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 apagada 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 registro 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 registra abastecimentos ou carregamentos pode experimentar o percurso no app iOS: fotografar o recibo, rever o que foi recuperado e terminar no formulário habitual.