Phase 04 - Lesson 19

OCR e Compreensão de Documentos

OCR e um pipeline de tres estagios: detectar caixas de texto, reconhecer os caracteres e, em seguida, organizar seu layout. Todo sistema moderno de OCR reordena esses estagios ou os funde.

Type: Learn + Use Languages: Python Prerequisites: Fase 4 Licao 06 (Deteccao), Fase 7 Licao 02 (Auto-atencao) Time: ~45 minutos

Objetivos de Aprendizagem

  • Percorrer o pipeline classico de OCR (detectar -> reconhecer -> layout) e as alternativas modernas ponta a ponta (Donut, Qwen-VL-OCR)
  • Implementar a perda CTC (Connectionist Temporal Classification) para treinamento de OCR sequencia a sequencia
  • Usar PaddleOCR ou EasyOCR para analise de documentos em producao sem treinamento
  • Distinguir OCR, analise de layout e compreensao de documentos, e escolher a ferramenta certa para cada tarefa

O Problema

Imagens cheias de texto estao em toda parte: recibos, faturas, documentos de identidade, livros digitalizados, formularios, quadros brancos, placas, capturas de tela. Extrair dados estruturados delas, nao apenas os caracteres, mas "este e o valor total", e um dos problemas de visao aplicada de maior valor.

A area se divide em tres camadas de habilidade:

  1. OCR propriamente dito: transformar pixels em texto.
  2. Analise de layout: agrupar a saida do OCR em regioes (titulo, corpo, tabela, cabecalho).
  3. Compreensao de documentos: extrair campos estruturados ("invoice_total = $42.50") a partir do layout.

Cada camada tem abordagens classicas e modernas, e a distancia entre "quero o texto de uma imagem" e "preciso do valor total deste recibo" e maior do que a maioria das equipes imagina.

O Conceito

O pipeline classico

flowchart LR
    IMG["Imagem"] --> DET["Deteccao de texto<br/>(DB, EAST, CRAFT)"]
    DET --> BOX["Caixas delimitadoras<br/>de palavra/linha"]
    BOX --> CROP["Recortar cada regiao"]
    CROP --> REC["Reconhecimento<br/>(CRNN + CTC)"]
    REC --> TXT["Strings de texto"]
    TXT --> LAY["Ordenacao<br/>de layout"]
    LAY --> OUT["Texto em ordem de leitura"]

    style DET fill:#dbeafe,stroke:#2563eb
    style REC fill:#fef3c7,stroke:#d97706
    style OUT fill:#dcfce7,stroke:#16a34a
  • A deteccao de texto produz quadrilateros por linha ou por palavra.
  • O reconhecimento recorta cada regiao para uma altura fixa e executa uma CNN + BiLSTM + CTC para produzir uma sequencia de caracteres.
  • O layout reconstroi a ordem de leitura (de cima para baixo, da esquerda para a direita no alfabeto latino; diferente para arabe e japones).

CTC em um paragrafo

O reconhecimento de OCR produz uma sequencia de comprimento variavel a partir de um mapa de caracteristicas de comprimento fixo. O CTC (Graves et al., 2006) permite treinar isso sem alinhamento em nivel de caractere. O modelo gera uma distribuicao sobre (vocabulario + blank) em cada passo de tempo; a perda CTC marginaliza sobre todos os alinhamentos que se reduzem ao texto-alvo apos fundir repeticoes e remover os blanks.

raw output: "h h h _ _ e e l l _ l l o _ _"
after merge repeats and remove blanks: "hello"

O CTC e a razao pela qual a CRNN funcionou em 2015 e ainda treina a maioria dos modelos de OCR de producao em 2026.

Modelos modernos ponta a ponta

  • Donut (Kim et al., 2022): um codificador ViT + um decodificador de texto; le uma imagem e emite JSON diretamente. Sem detector de texto, sem modulo de layout.
  • TrOCR: ViT + decodificador transformer para OCR em nivel de linha.
  • Qwen-VL-OCR / InternVL: modelos de visao e linguagem completos ajustados para tarefas de OCR; melhor precisao em 2026 em documentos complexos.
  • PaddleOCR: pipeline classico DB + CRNN em um pacote de producao maduro; ainda o cavalo de batalha de codigo aberto.

Modelos ponta a ponta precisam de mais dados e computacao, mas evitam o acumulo de erros dos pipelines de varios estagios.

Analise de layout

Para documentos estruturados, execute um detector de layout (LayoutLMv3, DocLayNet) que rotula cada regiao: Titulo, Paragrafo, Figura, Tabela, Nota de rodape. A ordem de leitura entao se torna "iterar pelas regioes na ordem do layout e concatenar".

Para formularios, use modelos de extracao Chave-Valor (Donut para documentos visualmente ricos, LayoutLMv3 para digitalizacoes simples). Eles recebem imagem + texto detectado + posicoes e preveem pares chave-valor estruturados.

Metricas de avaliacao

  • Taxa de Erro de Caractere (CER): distancia de Levenshtein / comprimento da referencia. Quanto menor, melhor. Meta de producao: < 2% em digitalizacoes limpas.
  • Taxa de Erro de Palavra (WER): o mesmo em nivel de palavra.
  • F1 em campos estruturados: para tarefas chave-valor; mede se {invoice_total: 42.50} aparece corretamente.
  • Distancia de edicao em JSON: para analise de documentos ponta a ponta; o artigo do Donut introduziu a distancia de edicao de arvore normalizada.

Construa

Passo 1: perda CTC + decodificador guloso

import torch
import torch.nn as nn
import torch.nn.functional as F


def ctc_loss(log_probs, targets, input_lengths, target_lengths, blank=0):
    """
    log_probs:      (T, N, C) log-softmax over vocab including blank at index 0
    targets:        (N, S) int targets (no blanks)
    input_lengths:  (N,) per-sample time steps used
    target_lengths: (N,) per-sample target length
    """
    return F.ctc_loss(log_probs, targets, input_lengths, target_lengths,
                      blank=blank, reduction="mean", zero_infinity=True)


def greedy_ctc_decode(log_probs, blank=0):
    """
    log_probs: (T, N, C) log-softmax
    returns: list of index sequences (blanks removed, repeats merged)
    """
    preds = log_probs.argmax(dim=-1).transpose(0, 1).cpu().tolist()
    out = []
    for seq in preds:
        decoded = []
        prev = None
        for idx in seq:
            if idx != prev and idx != blank:
                decoded.append(idx)
            prev = idx
        out.append(decoded)
    return out

F.ctc_loss usa a implementacao eficiente do CuDNN quando disponivel. O decodificador guloso e mais simples do que uma busca em feixe (beam search) e geralmente fica a 1% de CER dela.

Passo 2: reconhecedor CRNN minusculo

CNN + BiLSTM minimo para OCR de linha.

class TinyCRNN(nn.Module):
    def __init__(self, vocab_size=40, hidden=128, feat=32):
        super().__init__()
        self.cnn = nn.Sequential(
            nn.Conv2d(1, feat, 3, 1, 1), nn.BatchNorm2d(feat), nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(feat, feat * 2, 3, 1, 1), nn.BatchNorm2d(feat * 2), nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
            nn.Conv2d(feat * 2, feat * 4, 3, 1, 1), nn.BatchNorm2d(feat * 4), nn.ReLU(inplace=True),
            nn.MaxPool2d((2, 1)),
            nn.Conv2d(feat * 4, feat * 4, 3, 1, 1), nn.BatchNorm2d(feat * 4), nn.ReLU(inplace=True),
            nn.MaxPool2d((2, 1)),
        )
        self.rnn = nn.LSTM(feat * 4, hidden, bidirectional=True, batch_first=True)
        self.head = nn.Linear(hidden * 2, vocab_size)

    def forward(self, x):
        # x: (N, 1, H, W)
        f = self.cnn(x)                # (N, C, H', W')
        f = f.mean(dim=2).transpose(1, 2)  # (N, W', C)
        h, _ = self.rnn(f)
        return F.log_softmax(self.head(h).transpose(0, 1), dim=-1)  # (W', N, vocab)

Entrada de altura fixa (a CNN faz max-pool da altura ate 1). A largura e a dimensao temporal para o CTC.

Passo 3: OCR sintetico

Gere strings de digitos pretos sobre branco para um teste rapido ponta a ponta.

import numpy as np

def synthetic_line(text, height=32, char_width=16):
    W = char_width * len(text)
    img = np.ones((height, W), dtype=np.float32)
    for i, c in enumerate(text):
        x = i * char_width
        shade = 0.0 if c.isalnum() else 0.5
        img[6:height - 6, x + 2:x + char_width - 2] = shade
    return img


def build_batch(strings, vocab):
    H = 32
    W = 16 * max(len(s) for s in strings)
    imgs = np.ones((len(strings), 1, H, W), dtype=np.float32)
    target_lengths = []
    targets = []
    for i, s in enumerate(strings):
        imgs[i, 0, :, :16 * len(s)] = synthetic_line(s)
        ids = [vocab.index(c) for c in s]
        targets.extend(ids)
        target_lengths.append(len(ids))
    return torch.from_numpy(imgs), torch.tensor(targets), torch.tensor(target_lengths)


vocab = ["_"] + list("0123456789abcdefghijklmnopqrstuvwxyz")
imgs, targets, lengths = build_batch(["hello", "world"], vocab)
print(f"images: {imgs.shape}   targets: {targets.shape}   lengths: {lengths.tolist()}")

Um conjunto de dados de OCR real adiciona fontes, ruido, rotacao, desfoque e cor. O pipeline acima e identico.

Passo 4: esboco de treinamento

model = TinyCRNN(vocab_size=len(vocab))
opt = torch.optim.Adam(model.parameters(), lr=1e-3)

for step in range(200):
    strings = ["abc" + str(step % 10)] * 4 + ["xyz" + str((step + 1) % 10)] * 4
    imgs, targets, target_lens = build_batch(strings, vocab)
    log_probs = model(imgs)  # (W', 8, vocab)
    input_lens = torch.full((8,), log_probs.size(0), dtype=torch.long)
    loss = ctc_loss(log_probs, targets, input_lens, target_lens, blank=0)
    opt.zero_grad(); loss.backward(); opt.step()

A perda deve cair de ~3 para ~0.2 ao longo de 200 passos nesses dados sinteticos triviais.

Use

Tres caminhos para producao:

  • PaddleOCR: maduro, rapido, multilingue. Uso em uma linha: paddleocr.PaddleOCR(lang="en").ocr(image_path).
  • EasyOCR: nativo em Python, multilingue, backbone em PyTorch.
  • Tesseract: classico; ainda util para documentos digitalizados antigos quando os modelos tem dificuldade.

Para analise de documentos ponta a ponta, use Donut ou um VLM:

from transformers import DonutProcessor, VisionEncoderDecoderModel

processor = DonutProcessor.from_pretrained("naver-clova-ix/donut-base-finetuned-cord-v2")
model = VisionEncoderDecoderModel.from_pretrained("naver-clova-ix/donut-base-finetuned-cord-v2")

Para recibos, faturas e formularios com estrutura repetivel, ajuste o Donut. Para documentos arbitrarios ou OCR com raciocinio, um VLM como o Qwen-VL-OCR e o padrao atual.

Entregue

Esta licao produz:

  • outputs/prompt-ocr-stack-picker.md: um prompt que escolhe Tesseract / PaddleOCR / Donut / VLM-OCR dado o tipo de documento, idioma e estrutura.
  • outputs/skill-ctc-decoder.md: uma skill que escreve decodificadores CTC guloso e por busca em feixe do zero, incluindo normalizacao por comprimento.

Exercicios

  1. (Facil) Treine a TinyCRNN em strings numericas aleatorias de 5 digitos por 500 passos. Reporte o CER em um conjunto de teste separado.
  2. (Medio) Substitua a decodificacao gulosa por busca em feixe (beam_width=5). Reporte a variacao de CER. Em quais entradas a busca em feixe vence?
  3. (Dificil) Use PaddleOCR em um conjunto de 20 recibos, extraia os itens de linha e calcule o F1 em relacao a uma verdade de referencia rotulada a mao para pares {item_name, price}.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
OCR "Texto a partir de pixels" Transformar regioes de imagem em sequencias de caracteres
CTC "Perda sem alinhamento" Perda que treina um modelo de sequencia sem rotulos por passo de tempo; marginaliza sobre os alinhamentos
CRNN "Modelo classico de OCR" Extrator de caracteristicas conv + BiLSTM + CTC; o baseline de 2015 ainda usado em producao
Donut "OCR ponta a ponta" Codificador ViT + decodificador de texto; emite JSON diretamente da imagem
Analise de layout "Encontrar regioes" Detectar e rotular regioes de Titulo/Tabela/Figura/Paragrafo em um documento
Ordem de leitura "Sequencia de texto" Ordenacao das regioes reconhecidas em uma frase; trivial para o latim, nao trivial para layouts mistos
CER / WER "Taxas de erro" Distancia de Levenshtein / comprimento da referencia em granularidade de caractere ou palavra
VLM-OCR "LLM que le" Um modelo de visao e linguagem treinado ou induzido para tarefas de OCR; o SOTA atual em documentos complexos

Leitura Complementar

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).