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.
Estas son las tecnologías usadas para desarrollar el proyecto:
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.
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:
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.
!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
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
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
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')
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
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.
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
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:
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()
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.
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.
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
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)
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
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:
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.
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:
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:
Aquí se envía la imagen a la API y la respuesta se muestra en formato JSON:
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:
Pytest es un framework de testing muy usado en Python. En este proyecto se usó para validar:
============================= 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:
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.
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:
Así funciona el pipeline de CI, paso a paso:
name: Python application
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v3
with:
python-version: "3.11"
- 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
- 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
- name: Set environment variables
run: |
echo "S3_BUCKET_URL=https://modelv0.s3.us-east-1.amazonaws.com/" >> $GITHUB_ENV
- name: Test with pytest
run: |
pytest
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.pyrequirements.txtapp/.ebextensions/.platform/El siguiente video muestra una demostración de la aplicación funcionando correctamente en la nube de AWS.
Puedes explorar el código fuente completo en GitHub .