From 07a7ba8c01dee860b899466b58bdf9ca0d1b08ea Mon Sep 17 00:00:00 2001
From: Sergio Jimenez Jimenez
Se implementó una clase Python para cargar pares imagen-texto:
-class ImageTextDataset:
- def __init__(self, root):
- # Carga pares (imagen, texto) de carpetas pareadas
-
- def __getitem__(self, idx):
- # Retorna (PIL.Image, str)
Se implementó una clase Python para cargar pares imagen-texto que retorna tuplas (PIL.Image, str) desde carpetas pareadas. La implementación completa está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).
Tabla 14. Modelos OCR evaluados en el benchmark inicial.
@@ -4996,14 +4990,7 @@ concretos y metodología de trabajoFuente: Elaboración propia.Se utilizó la biblioteca jiwer para calcular:
-from jiwer import wer, cer
-
-def evaluate_text(reference, prediction):
- return {
- 'WER': wer(reference, prediction),
- 'CER': cer(reference, prediction)
- }
Se utilizó la biblioteca jiwer para calcular CER y WER comparando el texto de referencia con la predicción del modelo OCR. La implementación está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).
Tabla 15. Hiperparámetros seleccionados para optimización.
@@ -5011,45 +4998,25 @@ def evaluate_text(reference, prediction):Fuente: Elaboración propia.
from ray import tune
-from ray.tune.search.optuna import OptunaSearch
-
-search_space = {
- "use_doc_orientation_classify": tune.choice([True, False]),
- "use_doc_unwarping": tune.choice([True, False]),
- "textline_orientation": tune.choice([True, False]),
- "text_det_thresh": tune.uniform(0.0, 0.7),
- "text_det_box_thresh": tune.uniform(0.0, 0.7),
- "text_det_unclip_ratio": tune.choice([0.0]),
- "text_rec_score_thresh": tune.uniform(0.0, 0.7),
-}
-
-tuner = tune.Tuner(
- trainable_paddle_ocr,
- tune_config=tune.TuneConfig(
- metric="CER",
- mode="min",
- search_alg=OptunaSearch(),
- num_samples=64,
- max_concurrent_trials=2
- )
-)
El espacio de búsqueda se definió utilizando tune.choice() para parámetros booleanos y tune.uniform() para parámetros continuos, con OptunaSearch como algoritmo de optimización configurado para minimizar CER en 64 trials. La implementación completa está disponible en src/raytune/raytune_ocr.py (ver Anexo A).
Debido a incompatibilidades entre Ray y PaddleOCR en el mismo proceso, se implementó una arquitectura basada en subprocesos:
-Figura 5. Arquitectura de ejecución con subprocesos
-
Se implementó una arquitectura basada en contenedores Docker para aislar los servicios OCR y facilitar la reproducibilidad:
+Figura 5. Arquitectura de ejecución con Docker Compose
+
Fuente: Elaboración propia.
El script recibe hiperparámetros por línea de comandos:
-python paddle_ocr_tuning.py \
- --pdf-folder ./dataset \
- --textline-orientation True \
- --text-det-box-thresh 0.5 \
- --text-det-thresh 0.4 \
- --text-rec-score-thresh 0.6
Y retorna métricas en formato JSON:
+Los servicios se orquestan mediante Docker Compose (src/docker-compose.tuning.*.yml):
+# Iniciar servicio OCR
+docker compose -f docker-compose.tuning.doctr.yml up -d doctr-gpu
+
+# Ejecutar optimización (64 trials)
+docker compose -f docker-compose.tuning.doctr.yml run raytune --service doctr --samples 64
+
+# Detener servicios
+docker compose -f docker-compose.tuning.doctr.yml down
El servicio OCR expone una API REST que retorna métricas en formato JSON:
{
"CER": 0.0125,
"WER": 0.1040,
@@ -5142,91 +5109,16 @@ color:#0098CD;mso-font-kerning:16.0pt;mso-bidi-font-weight:bold'>
La conversión del PDF a imágenes se realizó mediante PyMuPDF (fitz):
-import fitz # PyMuPDF
-
-def pdf_to_images(pdf_path, output_dir, dpi=300):
- doc = fitz.open(pdf_path)
- for page_num, page in enumerate(doc):
- # Matriz de transformación para 300 DPI
- mat = fitz.Matrix(dpi/72, dpi/72)
- pix = page.get_pixmap(matrix=mat)
- pix.save(f"{output_dir}/page_{page_num:04d}.png")
La resolución de 300 DPI fue seleccionada como estándar para OCR de documentos, proporcionando suficiente detalle para caracteres pequeños sin generar archivos excesivamente grandes.
+La conversión del PDF a imágenes se realizó mediante PyMuPDF (fitz) a 300 DPI, resolución estándar para OCR que proporciona suficiente detalle para caracteres pequeños sin generar archivos excesivamente grandes. La implementación está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).
El texto de referencia se extrajo directamente del PDF mediante PyMuPDF:
-def extract_text(pdf_path):
- doc = fitz.open(pdf_path)
- text = ""
- for page in doc:
- blocks = page.get_text("dict")["blocks"]
- for block in blocks:
- if "lines" in block:
- for line in block["lines"]:
- for span in line["spans"]:
- text += span["text"]
- text += "\n"
- return text
Esta aproximación preserva la estructura de líneas del documento original, aunque puede introducir errores en layouts muy complejos (tablas anidadas, texto en columnas).
+El texto de referencia se extrajo directamente del PDF mediante PyMuPDF, preservando la estructura de líneas del documento original. Esta aproximación puede introducir errores en layouts muy complejos (tablas anidadas, texto en columnas). La implementación está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).
Según el código en ocr_benchmark_notebook.ipynb:
-EasyOCR:
-import easyocr
-
-easyocr_reader = easyocr.Reader(['es', 'en']) # Spanish and English
-results = easyocr_reader.readtext(image_path)
-text = ' '.join([r[1] for r in results])
La configuración incluye soporte para español e inglés, permitiendo reconocer palabras en ambos idiomas que puedan aparecer en documentos académicos (referencias, términos técnicos).
-PaddleOCR (PP-OCRv5):
-from paddleocr import PaddleOCR
-
-paddleocr_model = PaddleOCR(
- text_detection_model_name="PP-OCRv5_server_det",
- text_recognition_model_name="PP-OCRv5_server_rec",
- use_doc_orientation_classify=False,
- use_doc_unwarping=False,
- use_textline_orientation=True,
-)
-
-result = paddleocr_model.predict(image_path)
-text = '\n'.join([line['rec_texts'][0] for line in result[0]['rec_res']])
Se utilizaron los modelos "server" que ofrecen mayor precisión a costa de mayor tiempo de inferencia. La versión utilizada fue PaddleOCR 3.2.0.
-DocTR:
-from doctr.models import ocr_predictor
-
-doctr_model = ocr_predictor(
- det_arch="db_resnet50",
- reco_arch="sar_resnet31",
- pretrained=True
-)
-
-result = doctr_model([image])
-text = result.render()
Se seleccionaron las arquitecturas db_resnet50 para detección y sar_resnet31 para reconocimiento, representando una configuración de alta precisión.
+La configuración de cada modelo se detalla en src/ocr_benchmark_notebook.ipynb (ver Anexo A):
+· EasyOCR: Configurado con soporte para español e inglés, permitiendo reconocer palabras en ambos idiomas que puedan aparecer en documentos académicos (referencias, términos técnicos).
+· PaddleOCR (PP-OCRv5): Se utilizaron los modelos "server" (PP-OCRv5_server_det y PP-OCRv5_server_rec) que ofrecen mayor precisión a costa de mayor tiempo de inferencia. La versión utilizada fue PaddleOCR 3.2.0.
+· DocTR: Se seleccionaron las arquitecturas db_resnet50 para detección y sar_resnet31 para reconocimiento, representando una configuración de alta precisión.
Se utilizó la biblioteca jiwer para calcular CER y WER de manera estandarizada:
-from jiwer import wer, cer
-
-def evaluate_text(reference, prediction):
- """
- Calcula métricas de error entre texto de referencia y predicción.
-
- Args:
- reference: Texto ground truth
- prediction: Texto predicho por el OCR
-
- Returns:
- dict con WER y CER
- """
- # Normalización básica
- ref_clean = reference.lower().strip()
- pred_clean = prediction.lower().strip()
-
- return {
- 'WER': wer(ref_clean, pred_clean),
- 'CER': cer(ref_clean, pred_clean)
- }
La normalización a minúsculas y eliminación de espacios extremos asegura una comparación justa que no penaliza diferencias de capitalización.
+Se utilizó la biblioteca jiwer para calcular CER y WER de manera estandarizada. La normalización a minúsculas y eliminación de espacios extremos asegura una comparación justa que no penaliza diferencias de capitalización. La implementación está disponible en src/ocr_benchmark_notebook.ipynb (ver Anexo A).
Durante el benchmark inicial se evaluó PaddleOCR con configuración por defecto en un subconjunto del dataset. Los resultados preliminares mostraron variabilidad significativa entre páginas, con CER entre 1.54% y 6.40% dependiendo de la complejidad del layout.
@@ -5319,23 +5211,29 @@ def evaluate_text(reference, prediction):Fuente: Elaboración propia.
Debido a incompatibilidades entre Ray y PaddleOCR cuando se ejecutan en el mismo proceso, se implementó una arquitectura basada en subprocesos:
-Figura 6. Arquitectura de ejecución con subprocesos
-
La arquitectura basada en contenedores Docker es fundamental para este proyecto debido a los conflictos de dependencias inherentes entre los diferentes componentes:
+· Conflictos entre motores OCR: PaddleOCR, DocTR y EasyOCR tienen dependencias mutuamente incompatibles (diferentes versiones de PyTorch/PaddlePaddle, OpenCV, etc.)
+· Incompatibilidades CUDA/cuDNN: Cada motor OCR requiere versiones específicas de CUDA y cuDNN que no pueden coexistir en un mismo entorno virtual
+· Aislamiento de Ray Tune: Ray Tune tiene sus propias dependencias que pueden entrar en conflicto con las librerías de inferencia OCR
+Esta arquitectura containerizada permite ejecutar cada componente en su entorno aislado óptimo, comunicándose via API REST:
+Figura 6. Arquitectura de ejecución con Docker Compose
+
Fuente: Elaboración propia.
El script src/paddle_ocr_tuning.py actúa como wrapper que:
-1. Recibe hiperparámetros por línea de comandos
-2. Inicializa PaddleOCR con la configuración especificada
-3. Evalúa sobre el dataset
-4. Retorna métricas en formato JSON
-python paddle_ocr_tuning.py \
- --pdf-folder ./dataset \
- --textline-orientation True \
- --text-det-box-thresh 0.5 \
- --text-det-thresh 0.4 \
- --text-rec-score-thresh 0.6
Salida:
+La arquitectura containerizada (src/docker-compose.tuning.*.yml) ofrece:
+1. Aislamiento de dependencias entre Ray Tune y los motores OCR
+2. Health checks automáticos para asegurar disponibilidad del servicio
+3. Comunicación via API REST (endpoints /health y /evaluate)
+4. Soporte para GPU mediante nvidia-docker
+# Iniciar servicio OCR con GPU
+docker compose -f docker-compose.tuning.doctr.yml up -d doctr-gpu
+
+# Ejecutar optimización (64 trials)
+docker compose -f docker-compose.tuning.doctr.yml run raytune --service doctr --samples 64
+
+# Detener servicios
+docker compose -f docker-compose.tuning.doctr.yml down
Respuesta del servicio OCR:
Característica Valor Páginas totales 24 Páginas por trial 5 (páginas 5-10) Estructura Carpetas img/ y txt/ pareadas Resolución 300 DPI Formato imagen PNG Fuente: Elaboración propia. La clase ImageTextDataset en src/dataset_manager.py gestiona la carga de pares imagen-texto: La clase ImageTextDataset gestiona la carga de pares imagen-texto desde la estructura de carpetas pareadas. La implementación está disponible en el repositorio (ver Anexo A). El espacio de búsqueda se definió considerando los hiperparámetros más relevantes identificados en la documentación de PaddleOCR: El espacio de búsqueda se definió considerando los hiperparámetros más relevantes identificados en la documentación de PaddleOCR, utilizando tune.choice() para parámetros booleanos y tune.uniform() para umbrales continuos. La implementación está disponible en src/raytune/raytune_ocr.py (ver Anexo A). Tabla 25. Descripción detallada del espacio de búsqueda. Parámetro Tipo Rango Descripción use_doc_orientation_classify Booleano {True, False} Clasificación de orientación del documento completo use_doc_unwarping Booleano {True, False} Corrección de deformación/curvatura textline_orientation Booleano {True, False} Clasificación de orientación por línea de texto text_det_thresh Continuo [0.0, 0.7] Umbral de probabilidad para píxeles de texto text_det_box_thresh Continuo [0.0, 0.7] Umbral de confianza para cajas detectadas text_det_unclip_ratio Fijo 0.0 Coeficiente de expansión (no explorado) text_rec_score_thresh Continuo [0.0, 0.7] Umbral de confianza de reconocimiento Fuente: Elaboración propia. 1. text_det_unclip_ratio fijo: Por decisión de diseño inicial, este parámetro se mantuvo constante para reducir la dimensionalidad del espacio de búsqueda. 1. Parámetros booleanos completos: Los tres parámetros de preprocesamiento se exploran completamente para identificar cuáles son necesarios para documentos digitales. Se configuró Ray Tune con OptunaSearch como algoritmo de búsqueda, optimizando CER en 64 trials con 2 ejecuciones concurrentes. La implementación está disponible en src/raytune/raytune_ocr.py (ver Anexo A). Tabla 26. Parámetros de configuración de Ray Tune. Parámetro Valor Justificación Métrica objetivo CER Métrica estándar para OCR Modo min Minimizar tasa de error Algoritmo OptunaSearch (TPE) Eficiente para espacios mixtos Número de trials 64 Balance entre exploración y tiempo Trials concurrentes 2 Limitado por memoria disponible Fuente: Elaboración propia. Recomendación: Evitar text_det_thresh < 0.1 en cualquier configuración. La configuración óptima identificada se evaluó sobre el dataset completo de 24 páginas, comparando con la configuración baseline: Configuración Baseline: Configuración Optimizada: La configuración óptima identificada se evaluó sobre el dataset completo de 24 páginas, comparando con la configuración baseline (valores por defecto de PaddleOCR). Los parámetros optimizados más relevantes fueron: textline_orientation=True, text_det_thresh=0.4690, text_det_box_thresh=0.5412, y text_rec_score_thresh=0.6350. Tabla 35. Comparación baseline vs optimizado (24 páginas). Modelo CER Precisión Caracteres WER Precisión Palabras PaddleOCR (Baseline) 7.78% 92.22% 14.94% 85.06% PaddleOCR-HyperAdjust 1.49% 98.51% 7.62% 92.38% Fuente: Elaboración propia. 3. Con GPU, los tiempos serían 10-50× menores según benchmarks de PaddleOCR. Esta sección ha presentado: 1. Configuración del experimento: Arquitectura de subprocesos, dataset extendido, espacio de búsqueda de 7 dimensiones 1. Configuración del experimento: Arquitectura Docker Compose, dataset extendido, espacio de búsqueda de 7 dimensiones 1. Resultados estadísticos: - CER medio: 5.25% (std: 11.03%) - CER mínimo: 1.15% - 67.2% de trials con CER < 2% 1. Hallazgos clave: Para documentos académicos en español similares a los evaluados: Configuración recomendada: Tabla 46. Configuración recomendada para PaddleOCR. Parámetro Valor Prioridad Justificación textline_orientation True Obligatorio Reduce CER en 69.7% text_det_thresh 0.45 (rango: 0.4-0.5) Recomendado Correlación fuerte con CER text_rec_score_thresh 0.6 (rango: 0.5-0.7) Recomendado Filtra reconocimientos poco confiables text_det_box_thresh 0.55 (rango: 0.5-0.6) Opcional Impacto moderado use_doc_orientation_classify False No recomendado Innecesario para PDFs digitales use_doc_unwarping False No recomendado Innecesario para PDFs digitales Fuente: Elaboración propia. La optimización de hiperparámetros es recomendable cuando: 1. Sin GPU disponible: El fine-tuning requiere GPU; la optimización de hiperparámetros no. Para evaluar la viabilidad práctica del enfoque optimizado en escenarios de producción, se realizó una validación adicional utilizando aceleración GPU. Esta fase complementa los experimentos en CPU presentados anteriormente y demuestra la aplicabilidad del método cuando se dispone de hardware con capacidad de procesamiento paralelo. Tabla 46. Especificaciones del entorno de validación GPU. Tabla 47. Especificaciones del entorno de validación GPU. Componente Especificación GPU NVIDIA GeForce RTX 3060 Laptop VRAM 5.66 GB CUDA 12.4 Sistema Operativo Ubuntu 24.04.3 LTS Kernel 6.14.0-37-generic Fuente: Elaboración propia. El entorno de validación representa hardware de consumo típico para desarrollo de aplicaciones de machine learning, permitiendo evaluar el rendimiento en condiciones realistas de despliegue. Se evaluó el tiempo de procesamiento utilizando la configuración optimizada identificada en la fase anterior, comparando el rendimiento entre CPU y GPU. Tabla 47. Rendimiento comparativo CPU vs GPU. Tabla 48. Rendimiento comparativo CPU vs GPU. Métrica CPU GPU (RTX 3060) Factor de Aceleración Tiempo/Página 69.4s 0.55s 126x Dataset completo (45 páginas) ~52 min ~25 seg 126x Fuente: Elaboración propia. La aceleración de 126x obtenida con GPU transforma la aplicabilidad práctica del sistema. Mientras que el procesamiento en CPU limita el uso a escenarios de procesamiento por lotes sin restricciones de tiempo, la velocidad con GPU habilita casos de uso interactivos y de tiempo real. PaddleOCR ofrece dos variantes de modelos: Mobile (optimizados para dispositivos con recursos limitados) y Server (mayor precisión a costa de mayor consumo de memoria). Se evaluó la viabilidad de ambas variantes en el hardware disponible. Tabla 48. Comparación de modelos Mobile vs Server en RTX 3060. Tabla 49. Comparación de modelos Mobile vs Server en RTX 3060. Modelo VRAM Requerida Resultado Recomendación PP-OCRv5 Mobile 0.06 GB Funciona correctamente ✓ Recomendado PP-OCRv5 Server 5.3 GB OOM en página 2 ✗ Requiere >8 GB VRAM Fuente: Elaboración propia. {
"CER": 0.0125,
"WER": 0.1040,
@@ -5349,46 +5247,9 @@ def evaluate_text(reference, prediction):
class ImageTextDataset:
- def __init__(self, root):
- """
- Carga pares (imagen, texto) de carpetas pareadas.
-
- Estructura esperada:
- root/
- 0/
- img/
- page_0001.png
- txt/
- page_0001.txt
- """
- self.pairs = []
- for doc_folder in sorted(os.listdir(root)):
- img_folder = os.path.join(root, doc_folder, 'img')
- txt_folder = os.path.join(root, doc_folder, 'txt')
- # Cargar pares...
-
- def __getitem__(self, idx):
- img_path, txt_path = self.pairs[idx]
- return PIL.Image.open(img_path), open(txt_path).read()
Espacio de Búsqueda
-from ray import tune
-from ray.tune.search.optuna import OptunaSearch
-
-search_space = {
- # Parámetros booleanos
- "use_doc_orientation_classify": tune.choice([True, False]),
- "use_doc_unwarping": tune.choice([True, False]),
- "textline_orientation": tune.choice([True, False]),
-
- # Parámetros continuos (umbrales)
- "text_det_thresh": tune.uniform(0.0, 0.7),
- "text_det_box_thresh": tune.uniform(0.0, 0.7),
- "text_det_unclip_ratio": tune.choice([0.0]), # Fijado
- "text_rec_score_thresh": tune.uniform(0.0, 0.7),
-}Configuración de Ray Tune
-tuner = tune.Tuner(
- trainable_paddle_ocr,
- tune_config=tune.TuneConfig(
- metric="CER",
- mode="min",
- search_alg=OptunaSearch(),
- num_samples=64,
- max_concurrent_trials=2
- ),
- run_config=air.RunConfig(
- verbose=2,
- log_to_file=False
- ),
- param_space=search_space
-)
Comparación Baseline vs Optimizado
Evaluación sobre Dataset Completo
-baseline_config = {
- "textline_orientation": False, # Valor por defecto
- "use_doc_orientation_classify": False,
- "use_doc_unwarping": False,
- "text_det_thresh": 0.3, # Valor por defecto
- "text_det_box_thresh": 0.6, # Valor por defecto
- "text_det_unclip_ratio": 1.5, # Valor por defecto
- "text_rec_score_thresh": 0.5, # Valor por defecto
-}optimized_config = {
- "textline_orientation": True,
- "use_doc_orientation_classify": False,
- "use_doc_unwarping": False,
- "text_det_thresh": 0.4690,
- "text_det_box_thresh": 0.5412,
- "text_det_unclip_ratio": 0.0,
- "text_rec_score_thresh": 0.6350,
-}Resumen de la Sección
Implicaciones Prácticas
Guía de Configuración Recomendada
config_recomendada = {
- # OBLIGATORIO
- "textline_orientation": True,
-
- # RECOMENDADO
- "text_det_thresh": 0.45, # Rango: 0.4-0.5
- "text_rec_score_thresh": 0.6, # Rango: 0.5-0.7
-
- # OPCIONAL
- "text_det_box_thresh": 0.55, # Rango: 0.5-0.6
-
- # NO RECOMENDADO para PDFs digitales
- "use_doc_orientation_classify": False,
- "use_doc_unwarping": False,
-}
+Cuándo Aplicar Esta Metodología
Validación con Aceleración GPU
Configuración del Entorno GPU
-Comparación CPU vs GPU
Comparación de Modelos PaddleOCR
Este capít
Este Trabajo Fin de Máster ha demostrado que es posible mejorar significativamente el rendimiento de sistemas OCR preentrenados mediante optimización sistemática de hiperparámetros, sin requerir fine-tuning ni recursos GPU dedicados. El objetivo principal del trabajo era alcanzar un CER inferior al 2% en documentos académicos en español. Los resultados obtenidos confirman el cumplimiento de este objetivo: Tabla 49. Cumplimiento del objetivo de CER. Tabla 50. Cumplimiento del objetivo de CER. Métrica Objetivo Resultado CER < 2% 1.49% Fuente: Elaboración propia. Conclusiones Generales
Este capít
Respecto a OE4 (Optimización con Ray Tune): · Se ejecutaron 64 trials con el algoritmo OptunaSearch · El tiempo total del experimento fue aproximadamente 6 horas (en CPU) · La arquitectura basada en subprocesos permitió superar incompatibilidades entre Ray y PaddleOCR · La arquitectura basada en contenedores Docker permitió superar incompatibilidades entre Ray y los motores OCR, facilitando además la portabilidad y reproducibilidad Respecto a OE5 (Validación de la configuración): · Se validó la configuración óptima sobre el dataset completo de 24 páginas · La mejora obtenida fue del 80.9% en reducción del CER (7.78% → 1.49%)
Tabla 50. Especificaciones del sistema de desarrollo.
+Tabla 51. Especificaciones del sistema de desarrollo.
Componente | Especificación |
Sistema Operativo | Ubuntu 24.04.3 LTS |
CPU | AMD Ryzen 7 5800H |
RAM | 16 GB DDR4 |
GPU | NVIDIA RTX 3060 Laptop (5.66 GB VRAM) |
CUDA | 12.4 |
Fuente: Elaboración propia.
Tabla 51. Dependencias del proyecto.
+Tabla 52. Dependencias del proyecto.
Componente | Versión |
Python | 3.12.3 |
Docker | 29.1.5 |
NVIDIA Container Toolkit | Requerido para GPU |
Ray | 2.52.1 |
Optuna | 4.7.0 |
Fuente: Elaboración propia.
Tabla 52. Servicios Docker y puertos.
+Tabla 53. Servicios Docker y puertos.
Servicio | Puerto | Script de Ajuste |
PaddleOCR | 8002 | paddle_ocr_payload |
DocTR | 8003 | doctr_payload |
EasyOCR | 8002 | easyocr_payload |
Fuente: Elaboración propia.
· DocTR - Más rápido (0.50s/página)
· EasyOCR - Balance intermedio
Tabla 53. Resumen de resultados del benchmark por servicio.
+Tabla 54. Resumen de resultados del benchmark por servicio.
Servicio | CER Base | CER Ajustado | Mejora |
PaddleOCR | 8.85% | 7.72% | 12.8% |
DocTR | 12.06% | 12.07% | 0% |
EasyOCR | 11.23% | 11.14% | 0.8% |
Fuente: Elaboración propia.