Pipeline modular de análisis visual para imágenes de retail. Le metes una foto de una estantería o de un producto y te devuelve texto extraído, precios detectados, marcas identificadas y una imagen anotada con los resultados.
No entrena modelos. Integra, orquesta y extrae información estructurada usando YOLOv8 + PaddleOCR sobre imágenes reales.
- Preprocesa la imagen — Sharpen, CLAHE, corrección gamma y escalado inteligente para maximizar la calidad del OCR.
- Detecta bandas de estantería — Canny edges + HoughLines para segmentar filas y procesar cada zona por separado.
- Extrae texto con PaddleOCR — OCR multi-escala (1.0x + 1.5x) con fallback automático GPU → CPU si el entorno no tiene CUDA disponible.
- Fusiona y limpia tokens — Agrupa palabras de la misma línea por proximidad, elimina duplicados y aplica fuzzy matching para corregir marcas.
- Extrae precios — Regex + heurísticas para normalizar formatos como
$12,95,1295,€2.99a valores float validados. - Genera resultados estructurados — JSON con todos los tokens, precios detectados, metadatos de tiempo y una imagen anotada.
| Componente | Herramienta |
|---|---|
| API REST | FastAPI ≥ 0.115 |
| Detección de objetos | YOLOv8 ≥ 8.3 (modelo sku110k.pt) |
| OCR | PaddleOCR ≥ 2.9 |
| Procesado de imagen | OpenCV ≥ 4.10 + Pillow ≥ 10.4 |
| Interfaz web | Streamlit ≥ 1.39 |
| Fuzzy matching | RapidFuzz (corrección de marcas) |
VisuCheck/
├── src/
│ ├── app/ # Servidor FastAPI (main.py, schemas.py)
│ ├── core/
│ │ ├── detectors/ # Wrapper YoloDetector
│ │ ├── ocr/ # PaddleOCR + Tesseract + preprocesado
│ │ ├── rules/ # Parsers de precios, pesos, variantes
│ │ └── utils/ # Carga de configuración YAML
│ ├── services/
│ │ ├── pipeline.py # Pipeline principal (analyze_image) ← núcleo del sistema
│ │ └── annotate.py # Anotación visual de resultados
│ ├── lang/ # LLM/reasoning (estructura preparada, no activo)
│ └── ui/app.py # Frontend Streamlit
│
├── scripts/
│ ├── analyze_samples.py # Procesa imágenes en batch
│ ├── analyze_shelf.py # Análisis con segmentación por estanterías
│ ├── export_results.py # Exporta JSONs a JSONL
│ ├── import_to_sqlite.py # Importa resultados a SQLite
│ └── visualize_results.py # Visualiza anotaciones
│
├── dashboard/app.py # Dashboard para revisar resultados generados
│
├── configs/
│ ├── datasets.yaml # Paths de datos y modelos
│ ├── detector.yaml # Configuración YOLO
│ ├── ocr.yaml # Configuración PaddleOCR
│ └── rules/ # Reglas por dominio (precios, pesos, variantes)
│
├── data/
│ ├── samples/ # Imágenes de ejemplo (versionadas)
│ └── results/ # Salida generada (ignorada por git)
│
└── pyproject.toml
# 1. Clona el repo
git clone https://github.com/Izanvz/VisuCheck.git
cd VisuCheck
# 2. Crea el entorno virtual (Python 3.10+)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3. Instala dependencias
pip install -e .Si tienes GPU NVIDIA y quieres acelerar el OCR, asegúrate de tener CUDA instalado. PaddleOCR intenta usar GPU automáticamente y hace fallback a CPU si no puede.
Requisito del sistema: FFmpeg no es necesario aquí, pero sí tener los drivers de GPU correctos si quieres aprovechar CUDA.
python -m scripts.analyze_samples --glob "data/samples/*.*" --warmup --lang en --outdir data/results --txtParámetros:
| Flag | Descripción | Default |
|---|---|---|
--glob |
Patrón de imágenes a procesar | data/samples/*.* |
--lang |
Idioma para PaddleOCR | en |
--outdir |
Carpeta de salida | data/results |
--txt |
Guardar también .txt además del .json |
false |
--warmup |
Precarga el modelo OCR antes del primer análisis | false |
Salida por cada imagen: {nombre}.json + {nombre}_viz.jpg (anotada)
python -m scripts.analyze_shelf \
--glob "data/samples/*.jpg,data/samples/*.png" \
--lang en \
--outdir data/results \
--font "C:\Windows\Fonts\arial.ttf"Detecta las bandas horizontales de la estantería y procesa cada fila por separado. Mejora los resultados en imágenes con mucho texto a distintas alturas.
uvicorn src.app.main:app --reload --port 8000- API: http://localhost:8000
- Swagger docs: http://localhost:8000/docs
streamlit run dashboard/app.pyVisualiza los JSONs generados en data/results/. Muestra la imagen original junto a la imagen anotada y el JSON completo.
streamlit run src/ui/app.pyInterfaz para subir imágenes directamente al servidor FastAPI y ver la respuesta.
Analiza una imagen y devuelve los resultados estructurados.
Request: multipart/form-data
| Campo | Tipo | Descripción |
|---|---|---|
image |
File | Imagen JPG o PNG |
ruleset |
string | Conjunto de reglas a aplicar (ej: producto_simple_v1) |
Response:
{
"status": "valid",
"failures": [],
"evidence": {
"objects": [
{ "label": "producto", "conf": 0.87, "bbox": { "x": 100, "y": 50, "w": 200, "h": 150 } }
],
"ocr": [
{ "text": "FOLGERS", "bbox": { "x": 110, "y": 60, "w": 80, "h": 20 }, "conf": 0.95 }
],
"scores": {}
},
"explanation": "",
"artifacts": {}
}status puede ser valid, invalid o uncertain.
{
"meta": {
"image": "test.jpg",
"init_ms": 234,
"ocr_ms": 456,
"total_ms": 690,
"device": "GPU",
"lang": "en",
"raw_boxes": 45,
"merged_lines": 28,
"shelf_bands": [[0, 150], [150, 300]],
"scale": 1.5
},
"items": [
{ "text": "FOLGERS", "conf": 0.95, "boxes": [[[x1,y1],[x2,y2],[x3,y3],[x4,y4]]] }
],
"texts": ["FOLGERS", "COFFEE", "12,95"],
"prices": [["$12,95", 12.95], ["€1,99", 1.99]]
}Todo el comportamiento del sistema está parametrizado en configs/. Cambias el YAML y cambias el comportamiento sin tocar el código.
configs/detector.yaml — YOLO
model: "artifacts/yolo/sku110k.pt"
fallback_model: "yolov8n.pt"
confidence_threshold: 0.25
iou_threshold: 0.45
device: "cuda"configs/ocr.yaml — PaddleOCR
lang: "en"
use_gpu: true
use_angle_cls: trueconfigs/rules/price.yaml — Patrones de precios
price_patterns:
- '(?:\$|€|£)\s?([0-9]+[\.,][0-9]{2})'
- '([0-9]+[\.,][0-9]{2})\s?(USD|EUR|GBP)'
currency_symbols: ['$', '€', '£']# Exportar resultados a JSONL (un token por línea, listo para ML)
python -m scripts.export_results --glob "data/results/*.json" --out data/exports/tokens.jsonl
# Importar a SQLite
python -m scripts.import_to_sqlite --glob "data/results/*.json" --db data/visucheck.db
# Limpiar y normalizar precios detectados
python -m scripts.clean_prices --glob "data/results/*.json"
# Visualizar anotaciones
python -m scripts.visualize_results --glob "data/results/*.json"El pipeline en services/pipeline.py aplica este stack antes de lanzar el OCR:
- Escalado — Si el lado largo es menor de 2600px, escala la imagen proporcionalmente
- Sharpen —
img * 1.4 - blur * 0.4para aumentar nitidez de bordes - CLAHE — Equalización de histograma adaptativa en canal L del espacio LAB (
clipLimit=3.0) - Gamma correction — γ=1.20 para recuperar detalle en zonas oscuras
- OCR multi-escala — Corre PaddleOCR a 1.0x y 1.5x, combina resultados y remapea coordenadas
Después del OCR, fusiona tokens de la misma fila (mismo eje Y ± 60% de la altura del token, distancia horizontal < 260px) y aplica fuzzy matching para corregir marcas conocidas.
El repo incluye fallback a yolov8n.pt (descarga automática con ultralytics), pero para retail el modelo recomendado es SKU110K:
- Descarga
sku110k.ptdesde Hugging Face / repositorios de la comunidad - Ponlo en
artifacts/yolo/sku110k.pt
Si no existe ese archivo, el sistema carga yolov8n.pt automáticamente.
Pausado — La arquitectura está completa y el pipeline funciona sobre las imágenes de muestra. No está orientado a producción. Lo pausé cuando alcancé el objetivo principal: diseñar un sistema modular donde cambiar el OCR, el detector o las reglas no rompe el resto.
Lo que queda pendiente si se retoma:
- Activar diarización real con pyannote (separar fuentes en imágenes con texto superpuesto)
- Conectar la capa
lang/(LangGraph + LangChain) para razonamiento sobre los resultados - Añadir base de datos real (SQLAlchemy + aiosqlite ya están como dependencia)
- Soporte multiidioma configurable por imagen
- Entrenamiento fine-tuning sobre dataset propio de retail español
Izan Villarejo Adames — Backend Developer & AI Engineer
- Portfolio: portfolio-izanv.vercel.app
- LinkedIn: linkedin.com/in/izan-villarejo-ai
- GitHub: github.com/Izanvz
"La IA no es el producto. El sistema que la integra correctamente, sí."