Detección de objetos con YOLOv7 desplegado en AWS

Detección de objetos con YOLOv7 desplegado en AWS

Resumen del proyecto

Este proyecto consiste en desplegar en la nube un modelo de detección de objetos YOLOv7 ya entrenado, usando AWS Elastic Beanstalk. La aplicación permite subir una imagen y recibir una respuesta en JSON con los objetos detectados y su nivel de confianza. El objetivo era construir un pipeline completo, de principio a fin: desde la conversión del modelo hasta el despliegue en producción, dejando la solución escalable y accesible mediante una API REST. Esto incluye convertir el modelo YOLOv7 de PyTorch a ONNX, validar su desempeño y comparar sus predicciones para asegurar que todo siga siendo consistente. Una pieza clave del proyecto es el transfer learning: en lugar de entrenar un modelo desde cero, se aprovecha la robustez y precisión de YOLOv7 —un modelo de detección de objetos de última generación— para enfocarse en integrarlo bien en un entorno de producción.

Tecnologías utilizadas

Estas son las tecnologías usadas para desarrollar el proyecto:

  • Backend: Python, Flask
  • Inferencia del modelo: ONNX Runtime
  • Frontend: HTML, CSS, JavaScript, Bootstrap
  • Testing: Pytest (pruebas unitarias), Selenium (automatización), Insomnia (pruebas de API)
  • CI/CD: GitHub Actions
  • Infraestructura en la nube: AWS S3 (almacenamiento del modelo), AWS Elastic Beanstalk (despliegue)

Convirtiendo el modelo YOLOv7 de PyTorch a ONNX

Para preparar el modelo YOLOv7 y desplegarlo en la nube, el primer paso fue convertirlo de su formato original en PyTorch al formato ONNX (Open Neural Network Exchange). Este paso fue clave para optimizar el modelo de cara a producción y asegurar una inferencia eficiente.

¿Por qué convertir a ONNX?

Cuando llevas modelos de Machine Learning a producción —sobre todo en entornos con recursos limitados, como una API en la nube— minimizar el uso de memoria y la latencia es fundamental. ONNX ofrece varias ventajas que lo hicieron ideal para este proyecto:

  • Runtime ligero: a diferencia de PyTorch, que necesita un runtime y dependencias más pesadas, los modelos ONNX pueden correr con el liviano onnxruntime, reduciendo el consumo de memoria y acelerando el arranque del contenedor.
  • Mejor velocidad de inferencia: ONNX puede ofrecer tiempos de inferencia más rápidos, algo crítico para tareas de detección de objetos en tiempo real.
  • Interoperabilidad entre frameworks: ONNX actúa como puente entre frameworks, permitiendo que modelos entrenados en PyTorch se desplieguen en sistemas pensados originalmente para TensorFlow, Caffe2 u otras plataformas.
  • Listo para producción: al convertir el modelo a ONNX, se facilita su integración en distintos pipelines de producción, haciendo el despliegue y mantenimiento más consistentes y escalables.
Si quieres más información, visita el sitio oficial de ONNX .



Detalles de la implementación

La conversión se hizo en Google Colab, aprovechando su soporte de GPU y flexibilidad. Después de convertir el modelo, se probó y comparó contra la versión original en PyTorch para asegurar que los resultados de inferencia se mantuvieran consistentes.

Créditos: el script de conversión original se adaptó de una versión compartida por Wong Kin Yiu, el autor de YOLOv7. El código se modificó para ajustarse a las necesidades específicas de este pipeline de despliegue.



El siguiente fragmento de código muestra los pasos para convertir el modelo YOLOv7 al formato ONNX.

Paso 1: instalar las dependencias necesarias

Primero instalamos las librerías necesarias para trabajar con ONNX y PyTorch. Esto incluye onnx, onnxruntime y onnx-simplifier, entre otras. También nos aseguramos de tener las versiones correctas de protobuf.


!pip install --upgrade setuptools pip --user
!pip install onnx
!pip install onnxruntime
#!pip install --ignore-installed PyYAML
#!pip install Pillow

!pip install protobuf<4.21.3
!pip install onnxruntime-gpu
!pip install onnx>=1.9.0
!pip install onnx-simplifier>=0.3.6 --user

Paso 2: clonar el repositorio de YOLOv7

Después clonamos el repositorio oficial de YOLOv7 de Wong Kin Yiu, que trae los pesos de entrenamiento y los scripts de exportación necesarios para la conversión del modelo.


!git clone https://github.com/WongKinYiu/yolov7
%cd yolov7
!ls

Paso 3: descargar los pesos preentrenados

Descargamos los pesos preentrenados usados para la detección de objetos. Estos pesos se entrenaron con un dataset estándar y más adelante se usarán para la inferencia y la exportación.


!wget https://github.com/WongKinYiu/yolov7/releases/download/v0.1/yolov7_training.pt

Paso 4: correr una inferencia de prueba (opcional)

Antes de exportar el modelo, conviene validar que funcione como se espera en PyTorch, corriendo una inferencia sobre una imagen de ejemplo.


!python detect.py --weights yolov7_training.pt --source ./inference/images/bus.jpg
from PIL import Image

Image.open('/content/yolov7/yolov7/runs/detect/exp3/bus.jpg')

Prueba de inferencia de YOLOv7 en PyTorch sobre bus.jpg

Paso 5: instalar Graph Surgeon (opcional, para optimización avanzada)

Para manipular y optimizar el grafo antes de la conversión, instalamos onnx_graphsurgeon desde el registro NGC de NVIDIA.


!pip install onnx_graphsurgeon --index-url https://pypi.ngc.nvidia.com

Paso 6: exportar el modelo a formato ONNX

Por último, convertimos el modelo de PyTorch a ONNX usando el script export.py, aplicando simplificaciones y configuraciones óptimas para la inferencia en AWS:


%cd /content/yolov7/yolov7/
!python export.py --weights ./yolov7_training.pt \
        --grid --end2end --simplify \
        --topk-all 100 --iou-thres 0.45 --conf-thres 0.25 \
        --img-size 640 640 --max-wh 640

Esto genera un archivo de modelo .onnx, que se valida y luego se sube a AWS S3, listo para que la API de inferencia basada en Flask lo consuma.

Validando el modelo ONNX con una inferencia

Después de convertir el modelo a formato ONNX, hicimos una inferencia manual para validar que funcionara bien y que los resultados fueran coherentes con el modelo original en PyTorch. Con onnxruntime cargamos el modelo .onnx exportado y corrimos la inferencia sobre la misma imagen usada en la prueba de PyTorch. La imagen se preprocesó siguiendo los requisitos de entrada de YOLOv7: se redimensionó y rellenó con la función letterbox, se normalizó y se convirtió al formato que espera el runtime de ONNX.


import cv2
import time
import requests
import random
import numpy as np
import onnxruntime as ort
from PIL import Image
from pathlib import Path
from collections import OrderedDict,namedtuple

## CONFIGURACION
cuda = False
w = "/content/yolov7/yolov7/yolov7_training.onnx"
# img = cv2.imread('/content/yolov7/yolov7/runs/detect/exp2/bus.jpg')
img_path = Path('/content/yolov7/yolov7/runs/detect/exp3/bus.jpg').as_posix()
img = cv2.imread(img_path)
names = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
         'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
         'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
         'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
         'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
         'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
         'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
         'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
         'hair drier', 'toothbrush']
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] if cuda else ['CPUExecutionProvider']
session = ort.InferenceSession(w, providers=providers)

def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleup=True, stride=32):
    # Resize and pad image while meeting stride-multiple constraints
    shape = im.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup:  # only scale down, do not scale up (for better val mAP)
        r = min(r, 1.0)

    # Compute padding
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding

    if auto:  # minimum rectangle
        dw, dh = np.mod(dw, stride), np.mod(dh, stride)  # wh padding

    dw /= 2  # divide padding into 2 sides
    dh /= 2

    if shape[::-1] != new_unpad:  # resize
        im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border
    return im, r, (dw, dh)

## Pre-processing
colors = {name:[random.randint(0, 255) for _ in range(3)] for i,name in enumerate(names)}
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
image = img.copy()
image, ratio, dwdh = letterbox(image, auto=False)
image = image.transpose((2, 0, 1))
image = np.expand_dims(image, 0)
image = np.ascontiguousarray(image)
im = image.astype(np.float32)
im /= 255
im.shape
outname = [i.name for i in session.get_outputs()]
inname = [i.name for i in session.get_inputs()]
inp = {inname[0]:im}

# ONNX inference
outputs = session.run(outname, inp)[0]
outputs

Comparación visual: resultado de PyTorch vs ONNX

Para verificar que el modelo ONNX rinde igual que su contraparte en PyTorch, renderizamos los resultados de ambos pipelines de inferencia sobre la misma imagen y los comparamos visualmente. Se aplicaron los siguientes pasos de post-procesamiento a las salidas de ONNX:

  • Las coordenadas de las cajas delimitadoras se reescalaron para coincidir con las dimensiones de la imagen original.
  • Cada detección se dibujó sobre la imagen con un color específico según su clase.
  • Las etiquetas se anotaron con el nombre de la clase y el puntaje de confianza.


ori_images = [img.copy()]

for i,(batch_id,x0,y0,x1,y1,cls_id,score) in enumerate(outputs):
    image = ori_images[int(batch_id)]
    box = np.array([x0,y0,x1,y1])
    box -= np.array(dwdh*2)
    box /= ratio
    box = box.round().astype(np.int32).tolist()
    cls_id = int(cls_id)
    score = round(float(score),3)
    name = names[cls_id]
    color = colors[name]
    name += ' '+str(score)
    cv2.rectangle(image,box[:2],box[2:],color,2)
    cv2.putText(image,name,(box[0], box[1] - 2),cv2.FONT_HERSHEY_SIMPLEX,0.75,[225, 255, 255],thickness=2)

imgpytorch = Image.open('/content/yolov7/yolov7/runs/detect/exp3/bus.jpg')
# /content/yolov7/yolov7/runs/detect/exp3/bus.jpg
imgonnx = Image.fromarray(ori_images[0])
plt.figure(figsize=(14,10))
plt.subplot(121)
plt.imshow(imgpytorch)
plt.subplot(122)
plt.imshow(imgonnx)
plt.show()
ONNX vs PyTorch

Ambos resultados muestran un desempeño consistente en la detección de objetos, identificando las mismas instancias (personas y un bus) con cajas delimitadoras y puntajes de confianza similares. Esta paridad visual confirma que el modelo ONNX mantiene la integridad predictiva de la versión original en PyTorch, validando el éxito del proceso de conversión.


Configurando la carpeta del proyecto

Para que la aplicación fuera mantenible y estuviera lista para producción, el proyecto se organizó siguiendo las mejores prácticas de desarrollo web en Python y despliegue de modelos. Esto incluyó crear una estructura de carpetas clara, configurar un entorno virtual y especificar las dependencias del proyecto en un archivo requirements.txt.



Estructura del proyecto

Aquí tienes un vistazo a la estructura de carpetas principal del proyecto:


AWS-Project/
│
├── app/                    # Core application logic
│   ├── application.py      # Initializes and configures the Flask app
│   ├── yolocounterv1.py    # Handles object counting logic
│   ├── yolomodel.py        # Loads and runs ONNX model inference
│   ├── static/             # CSS and JS 
│   └── templates/          # HTML template (Flask view)
│
├── tests/                  # Automated tests
│   ├── conftest.py
│   ├── pytest.ini
│   ├── run_tests.py
│   ├── test_app.py         # Unit tests for Flask endpoints
│   ├── test_integration.py # Integration tests
│   └── test_ui.py          # Selenium UI tests
│
├── .github/                # GitHub Actions workflows for CI
├── .ebextensions/          # Elastic Beanstalk config files
├── .platform/              # Platform-specific settings
├── .env                    # Environment variables
├── .gitignore              # Git exclusions
├── venv/                   # Python virtual environment
├── app.py                  # Entry point to run the application locally
├── yolov7_training.onnx    # Converted ONNX model
└── requirements.txt        # List of dependencies

Punto de entrada: app.py

El archivo app.py en la raíz del proyecto es el punto de entrada principal para desarrollo y pruebas locales. Simplemente importa y ejecuta la app de Flask definida en app/application.py:


from app.application import application

if __name__ == '__main__':
    application.run(debug=True)

Entorno virtual

Para manejar las dependencias de forma aislada y evitar conflictos, se creó un entorno virtual de Python (venv/). Esto garantiza que el entorno sea reproducible y evita ensuciar la instalación de Python del sistema.


python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate


Archivo de requerimientos

Todos los paquetes necesarios se listaron en el archivo requirements.txt para que el entorno fuera fácilmente reproducible, sobre todo pensando en el despliegue en la nube:


onnxruntime==1.16.2
opencv_python==4.8.1.78
Pillow==10.0.1
requests==2.31.0 
flask==3.0.0
numpy==1.26.2 
python-dotenv
selenium
pytest 
pytest-flask
webdriver-manager
pytest-cov 
psutil 
pillow

Entre las dependencias están:

  • Flask: para construir la API REST.
  • ONNX Runtime, OpenCV y NumPy: para la inferencia del modelo y el manejo de imágenes.
  • Selenium y Pytest: para las pruebas automatizadas.
  • Python-Dotenv: para cargar variables de entorno de forma segura.
  • psutil: para monitorear recursos en algunos casos de prueba o ajustes de rendimiento.


Cómo correr la aplicación localmente

Para correr la aplicación localmente, primero activa tu entorno virtual y ejecuta el comando: python application.py. Una vez arrancada, abre tu navegador y ve a la URL local (normalmente http://127.0.0.1:5000) que aparece en la consola. Ahí verás la interfaz web, lista para interactuar con el modelo desplegado.



Aplicación en funcionamiento

Pruebas locales

Antes de llevar la aplicación a producción, es esencial verificar que todos los componentes se comporten bien en un entorno controlado. Este proyecto usa tres herramientas principales para testing:



Insomnia – pruebas manuales de la API

Usamos Insomnia para verificar manualmente el comportamiento de la ruta /detect durante el desarrollo. Al enviar una petición POST con un archivo de imagen, la API devuelve dos bloques estructurados:

  • countings: un resumen de los tipos de objetos y cuántos se encontraron de cada uno.
  • detections: una lista de detecciones individuales, cada una con:
    • class: el nombre de la clase del objeto detectado.
    • confidence: el puntaje de confianza de la detección (como texto).
    • bounding_box: las coordenadas [x1, y1, x2, y2] de la caja delimitadora alrededor del objeto detectado.


    Aquí se envía la imagen a la API y la respuesta se muestra en formato JSON:



    Imagen de ejemplo enviada a la API de detección de objetos


    Respuesta de ejemplo de la API:

    
    {
      "countings": {
        "backpack": 2,
        "bus": 1,
        "car": 11,
        "motorcycle": 1,
        "person": 4,
        "truck": 2
      },
      "detections": [
        [[379, 248, 959, 632], 2, "0.9502241", "car"],
        [[133, 176, 293, 520], 0, "0.9456609", "person"],
        [[0, 290, 255, 631], 2, "0.9187772", "car"],
        ...
        [[271, 107, 454, 276], 5, "0.2698659", "bus"]
      ]
    }
    

    Casos de uso cubiertos:

    • Verificar el esquema y las claves de la respuesta.
    • Confirmar la detección correcta de tipos de objetos como carro, persona, camión, bus, etc.
    • Inspeccionar manualmente los puntajes de confianza para control de calidad.
    • Validar la inferencia de YOLO tras la conversión a ONNX.

Pytest – pruebas unitarias y de integración

Pytest es un framework de testing muy usado en Python. En este proyecto se usó para validar:

  • La lógica de la aplicación (por ejemplo, el enrutamiento y el manejo de imágenes).
  • La estructura y contenido de las respuestas.
  • Casos límite (por ejemplo, entradas inválidas o datos faltantes).
  • Métricas de rendimiento (por ejemplo, el tiempo de respuesta).
  • Distintos formatos y tamaños de imagen.
Caso de uso: permite hacer pruebas automatizadas y repetibles de la lógica del backend y los flujos de integración.

Resultado total de correr las pruebas:


============================= test session starts =============================
platform win32 -- Python 3.11.4, pytest-8.4.1
rootdir: /tests
collected 26 items

tests/test_app.py .....................                                  [ 80%]
tests/test_integration.py ...                                            [ 92%]
tests/test_ui.py ..                                                      [100%]

============================= 26 passed in 46.70s =============================

✅ All tests passed successfully.


Algunos ejemplos de pruebas superadas:

  • Acceso a las rutas index y detect.
  • Validación de los tipos de respuesta y la estructura del contenido.
  • Pruebas con archivos inválidos o faltantes.
  • Simulación de casos límite en la inferencia de YOLO.
  • Evaluación de varios formatos de imagen (JPEG, PNG, BMP).
  • Medición del tiempo de respuesta para asegurar el rendimiento.


GitHub Actions – integración continua

GitHub Actions cumple un papel clave automatizando el proceso de testing y validación del proyecto. Al integrar CI (Integración Continua), nos aseguramos de que cada nuevo commit o pull request hacia la rama main se verifique automáticamente con linting y pruebas. Esto ayuda a detectar errores a tiempo, mantener la calidad del código y un comportamiento consistente entre entornos.


Estructura común del workflow

GitHub Actions usa archivos de workflow en formato .yml, ubicados normalmente en la carpeta .github/workflows/ del repositorio. En este proyecto usamos un archivo llamado python-app.yml, que dispara el workflow cuando:

  • Hay un push a la rama main
  • Hay un pull request dirigido a la rama main

Desglose del workflow

Así funciona el pipeline de CI, paso a paso:


name: Python application
on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

permissions:
  contents: read

  • El workflow se llama "Python application".
  • Se dispara con pushes y pull requests a la rama main.
  • 
    jobs:
      build:
        runs-on: ubuntu-latest
    
  • El job de build corre en un entorno limpio de Ubuntu, garantizando consistencia entre builds.
  • 
    steps:
    - uses: actions/checkout@v4
    
  • Hace checkout del repositorio para que el workflow pueda acceder a los archivos del proyecto.
  • 
    - name: Set up Python 3.11
    uses: actions/setup-python@v3
    with:
        python-version: "3.11"
    
  • Configura Python 3.11, la versión usada en el desarrollo.
  • 
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 pytest
        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
    
  • Instala las dependencias del proyecto, incluyendo flake8 para linting y pytest para testing.
  • 
    - name: Lint with flake8
      run: |
        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
    
  • Corre un análisis estático de código con flake8, revisando errores de sintaxis y complejidad del código.
  • La segunda línea (exit-zero) evita que los problemas no críticos rompan el build.
  • 
    - name: Set environment variables
      run: |
        echo "S3_BUCKET_URL=https://modelv0.s3.us-east-1.amazonaws.com/" >> $GITHUB_ENV
    
  • Define una variable de entorno necesaria para que la aplicación descargue el modelo ONNX desde S3
  • 
    - name: Test with pytest
      run: |
        pytest
    
Por qué importa
  • Garantía automatizada: cada cambio de código se valida sin intervención manual.
  • Previene regresiones: los fallos en pruebas o sintaxis alertan de inmediato a quienes contribuyen.
  • Consistencia: el entorno de CI imita las condiciones de producción.
  • Escalabilidad: se pueden añadir nuevas pruebas y validaciones sin fricción.

Despliegue en AWS

La aplicación se desplegó usando AWS Elastic Beanstalk, un servicio que simplifica el proceso de desplegar y escalar aplicaciones web. Como este proceso involucra varios pasos de configuración específicos —como preparar el entorno, asignar roles de servicio y elegir el tipo de instancia adecuado— se creó una presentación dedicada que explica cada paso de forma visual y clara.


En términos generales, el despliegue consiste en comprimir la aplicación en un archivo .zip que incluye los archivos clave:

  • application.py
  • requirements.txt
  • carpeta app/
  • carpeta .ebextensions/
  • carpeta .platform/
Ese archivo se sube a Elastic Beanstalk, donde se lanza una instancia EC2 para correr la aplicación.


El siguiente video muestra una demostración de la aplicación funcionando correctamente en la nube de AWS.