Phase 04 - Lesson 19
OCR y Comprension de Documentos
El OCR es un pipeline de tres etapas: detectar cuadros de texto, reconocer los caracteres y luego organizar su disposicion. Todo sistema moderno de OCR reordena estas etapas o las fusiona.
Type: Learn + Use Languages: Python Prerequisites: Fase 4 Leccion 06 (Deteccion), Fase 7 Leccion 02 (Autoatencion) Time: ~45 minutos
Objetivos de Aprendizaje
- Recorrer el pipeline clasico de OCR (detectar -> reconocer -> disposicion) y las alternativas modernas de extremo a extremo (Donut, Qwen-VL-OCR)
- Implementar la perdida CTC (Connectionist Temporal Classification) para el entrenamiento de OCR de secuencia a secuencia
- Usar PaddleOCR o EasyOCR para el analisis de documentos en produccion sin entrenamiento
- Distinguir OCR, analisis de disposicion y comprension de documentos, y elegir la herramienta adecuada para cada tarea
El Problema
Las imagenes llenas de texto estan en todas partes: recibos, facturas, documentos de identidad, libros escaneados, formularios, pizarras, letreros, capturas de pantalla. Extraer datos estructurados de ellas, no solo los caracteres, sino "este es el monto total", es uno de los problemas de vision aplicada de mayor valor.
El campo se divide en tres capas de habilidad:
- OCR propiamente dicho: convertir pixeles en texto.
- Analisis de disposicion: agrupar la salida del OCR en regiones (titulo, cuerpo, tabla, encabezado).
- Comprension de documentos: extraer campos estructurados ("invoice_total = $42.50") a partir de la disposicion.
Cada capa tiene enfoques clasicos y modernos, y la brecha entre "quiero el texto de una imagen" y "necesito el monto total de este recibo" es mas grande de lo que la mayoria de los equipos cree.
El Concepto
El pipeline clasico
flowchart LR
IMG["Imagen"] --> DET["Deteccion de texto<br/>(DB, EAST, CRAFT)"]
DET --> BOX["Cuadros delimitadores<br/>de palabra/linea"]
BOX --> CROP["Recortar cada region"]
CROP --> REC["Reconocimiento<br/>(CRNN + CTC)"]
REC --> TXT["Cadenas de texto"]
TXT --> LAY["Ordenamiento<br/>de disposicion"]
LAY --> OUT["Texto en orden de lectura"]
style DET fill:#dbeafe,stroke:#2563eb
style REC fill:#fef3c7,stroke:#d97706
style OUT fill:#dcfce7,stroke:#16a34a
- La deteccion de texto produce cuadrilateros por linea o por palabra.
- El reconocimiento recorta cada region a una altura fija y ejecuta una CNN + BiLSTM + CTC para producir una secuencia de caracteres.
- La disposicion reconstruye el orden de lectura (de arriba hacia abajo, de izquierda a derecha para el alfabeto latino; distinto para el arabe y el japones).
CTC en un parrafo
El reconocimiento de OCR produce una secuencia de longitud variable a partir de un mapa de caracteristicas de longitud fija. El CTC (Graves et al., 2006) permite entrenar esto sin alineacion a nivel de caracter. El modelo genera una distribucion sobre (vocabulario + blank) en cada paso de tiempo; la perdida CTC marginaliza sobre todas las alineaciones que se reducen al texto objetivo despues de fusionar repeticiones y eliminar los blanks.
raw output: "h h h _ _ e e l l _ l l o _ _"
after merge repeats and remove blanks: "hello"
El CTC es la razon por la cual la CRNN funciono en 2015 y aun entrena la mayoria de los modelos de OCR de produccion en 2026.
Modelos modernos de extremo a extremo
- Donut (Kim et al., 2022): un codificador ViT + un decodificador de texto; lee una imagen y emite JSON directamente. Sin detector de texto, sin modulo de disposicion.
- TrOCR: ViT + decodificador transformer para OCR a nivel de linea.
- Qwen-VL-OCR / InternVL: modelos completos de vision y lenguaje afinados para tareas de OCR; la mejor precision en 2026 en documentos complejos.
- PaddleOCR: pipeline clasico DB + CRNN en un paquete de produccion maduro; aun el caballo de batalla de codigo abierto.
Los modelos de extremo a extremo necesitan mas datos y computo, pero evitan la acumulacion de errores de los pipelines de varias etapas.
Analisis de disposicion
Para documentos estructurados, ejecute un detector de disposicion (LayoutLMv3, DocLayNet) que etiquete cada region: Titulo, Parrafo, Figura, Tabla, Nota al pie. El orden de lectura se convierte entonces en "iterar por las regiones en el orden de la disposicion y concatenar".
Para formularios, use modelos de extraccion Clave-Valor (Donut para documentos visualmente ricos, LayoutLMv3 para escaneos simples). Toman imagen + texto detectado + posiciones y predicen pares clave-valor estructurados.
Metricas de evaluacion
- Tasa de Error de Caracter (CER): distancia de Levenshtein / longitud de la referencia. Cuanto menor, mejor. Objetivo de produccion: < 2% en escaneos limpios.
- Tasa de Error de Palabra (WER): lo mismo a nivel de palabra.
- F1 en campos estructurados: para tareas clave-valor; mide si
{invoice_total: 42.50}aparece correctamente. - Distancia de edicion en JSON: para el analisis de documentos de extremo a extremo; el articulo de Donut introdujo la distancia de edicion de arbol normalizada.
Construyelo
Paso 1: perdida CTC + decodificador voraz
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 la implementacion eficiente de CuDNN cuando esta disponible. El decodificador voraz es mas simple que una busqueda en haz (beam search) y normalmente queda dentro del 1% de CER respecto a ella.
Paso 2: reconocedor CRNN diminuto
CNN + BiLSTM minimo para OCR de linea.
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 fija (la CNN hace max-pool de la altura hasta 1). El ancho es la dimension temporal para el CTC.
Paso 3: OCR sintetico
Genere cadenas de digitos negros sobre blanco para una prueba rapida de extremo a extremo.
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()}")
Un conjunto de datos de OCR real agrega fuentes, ruido, rotacion, desenfoque y color. El pipeline anterior es identico.
Paso 4: esquema de entrenamiento
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()
La perdida deberia caer de ~3 a ~0.2 a lo largo de 200 pasos con estos datos sinteticos triviales.
Usalo
Tres caminos hacia produccion:
- PaddleOCR: maduro, rapido, multilingue. Uso en una linea:
paddleocr.PaddleOCR(lang="en").ocr(image_path). - EasyOCR: nativo en Python, multilingue, backbone en PyTorch.
- Tesseract: clasico; aun util para documentos escaneados antiguos cuando los modelos tienen dificultades.
Para el analisis de documentos de extremo a extremo, use Donut o un 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, facturas y formularios con estructura repetible, afine Donut. Para documentos arbitrarios u OCR con razonamiento, un VLM como Qwen-VL-OCR es el estandar actual.
Entregalo
Esta leccion produce:
outputs/prompt-ocr-stack-picker.md: un prompt que elige Tesseract / PaddleOCR / Donut / VLM-OCR segun el tipo de documento, el idioma y la estructura.outputs/skill-ctc-decoder.md: una skill que escribe decodificadores CTC voraz y por busqueda en haz desde cero, incluyendo la normalizacion por longitud.
Ejercicios
- (Facil) Entrene la TinyCRNN con cadenas numericas aleatorias de 5 digitos durante 500 pasos. Reporte el CER en un conjunto de prueba separado.
- (Medio) Reemplace la decodificacion voraz por busqueda en haz (beam_width=5). Reporte la variacion de CER. En cuales entradas gana la busqueda en haz?
- (Dificil) Use PaddleOCR en un conjunto de 20 recibos, extraiga los items de linea y calcule el F1 frente a una verdad de referencia etiquetada a mano para pares {item_name, price}.
Terminos Clave
| Termino | Lo que dice la gente | Lo que realmente significa |
|---|---|---|
| OCR | "Texto a partir de pixeles" | Convertir regiones de imagen en secuencias de caracteres |
| CTC | "Perdida sin alineacion" | Perdida que entrena un modelo de secuencia sin etiquetas por paso de tiempo; marginaliza sobre las alineaciones |
| CRNN | "Modelo clasico de OCR" | Extractor de caracteristicas conv + BiLSTM + CTC; el baseline de 2015 aun usado en produccion |
| Donut | "OCR de extremo a extremo" | Codificador ViT + decodificador de texto; emite JSON directamente desde la imagen |
| Analisis de disposicion | "Encontrar regiones" | Detectar y etiquetar regiones de Titulo/Tabla/Figura/Parrafo en un documento |
| Orden de lectura | "Secuencia de texto" | Ordenamiento de las regiones reconocidas en una oracion; trivial para el latin, no trivial para disposiciones mixtas |
| CER / WER | "Tasas de error" | Distancia de Levenshtein / longitud de la referencia con granularidad de caracter o palabra |
| VLM-OCR | "LLM que lee" | Un modelo de vision y lenguaje entrenado o inducido para tareas de OCR; el SOTA actual en documentos complejos |
Lectura Adicional
- CRNN (Shi et al., 2015): la arquitectura original CNN+RNN+CTC
- CTC (Graves et al., 2006): el articulo original de CTC; densamente repleto de las ideas algoritmicas
- Donut (Kim et al., 2022): transformer de comprension de documentos sin OCR
- PaddleOCR: la stack de OCR de produccion de codigo abierto