MOVA Soccer Field Keypoints — HRNet-W18 v1
57 keypoints de la cancha de fútbol broadcast para calibración homográfica y mapeo pixel ↔ coordenadas reales del campo.
Entrenado por Orbital Lab como parte del stack MOVA (análisis táctico de fútbol con CV).
TL;DR
from huggingface_hub import snapshot_download
import sys, cv2
repo = snapshot_download("OrbitalLab/mova-hrnet-keypoints-v1")
sys.path.insert(0, repo)
from keypoints import load_model, preprocess, heatmaps_to_keypoints
model = load_model(f"{repo}/hrnet_w18_keypoints_v1.pth", f"{repo}/config.yaml")
frame = cv2.imread("partido.jpg")
tensor, orig_size = preprocess(frame)
heatmaps = model(tensor.to(next(model.parameters()).device))[0]
kps = heatmaps_to_keypoints(heatmaps, orig_size, conf_threshold=0.1)
for name, kp in kps.items():
print(f"{name}: ({kp['x']:.0f}, {kp['y']:.0f}) conf={kp['confidence']:.3f}")
→ Devuelve un dict {keypoint_name: {x, y, confidence}} con los keypoints detectados sobre las coordenadas del frame original.
Modelo
| Campo | Valor |
|---|---|
| Arquitectura | HRNet-W18 (heatmap regression head) |
| Backbone params | ~9.6 M (W18 lightweight) |
| Output channels | 58 (57 keypoints + 1 background) |
| Input resolution | 960 × 540 px (BGR → RGB normalizado ImageNet) |
| Output heatmap stride | 2 (upscale interno = 2) |
| Head | Conv → BN → ReLU → Conv(58 ch) → LogSoftmax |
| Tamaño checkpoint | 113 MB (.pth) |
| Framework | PyTorch ≥ 2.0 |
Métricas (validación)
Validación interna sobre split propio de Liga Betplay + SoccerNet broadcast.
| Métrica | Valor |
|---|---|
val_loss (KL divergence) |
8.21 × 10⁻⁵ |
val_kp_l2_dist (px en input 960×540) |
30.4 |
| PCK @ 5 px (conf ≥ 0.1) | 54.6 % |
| PCK @ 10 px (conf ≥ 0.1) | 62.1 % |
| Epochs entrenados | 15 |
PCK = porcentaje de keypoints predichos que caen dentro del umbral en pixels respecto al ground truth. Calculado solo sobre keypoints visibles + con confianza ≥ 0.1.
Lectura honesta: este es un baseline funcional. Es suficiente para calibración inicial + RANSAC, pero un detector más profundo (HRNet-W32/W48 + más data) cerraría el gap del 38 % de keypoints fuera de tolerancia a 10 px. Pull requests bienvenidos.
Mapa de keypoints (57)
Numerados 0-56 siguiendo el estándar SoccerNet / BOG. El canal 57 del modelo es background (no es un keypoint).
Arco izquierdo (0-7)
| ID | Nombre | Descripción |
|---|---|---|
| 0 | L_GOAL_TL_POST |
Esquina superior izquierda del poste izquierdo del arco izquierdo |
| 1 | L_GOAL_TR_POST |
Esquina superior derecha del poste izquierdo |
| 2 | L_GOAL_BL_POST |
Esquina inferior izquierda |
| 3 | L_GOAL_BR_POST |
Esquina inferior derecha |
| 4-7 | L_GOAL_AREA_*_CORNER |
4 esquinas del área chica izquierda |
Área grande izquierda + esquinas de cancha (8-15)
| ID | Nombre |
|---|---|
| 8-11 | L_PENALTY_AREA_*_CORNER |
| 12 | BL_PITCH_CORNER (esquina inferior izquierda de la cancha) |
| 13 | TL_PITCH_CORNER |
| 14 | B_TOUCH_AND_HALFWAY_LINES_INTERSECTION |
| 15 | T_TOUCH_AND_HALFWAY_LINES_INTERSECTION |
Lado derecho (16-29)
Espejo del izquierdo: área grande, área chica, postes, esquinas de cancha.
Círculo central (30-42)
| ID | Nombre |
|---|---|
| 30-33 | Tangentes del círculo central (TR, TL, BR, BL) |
| 34-37 | Cuartos del círculo central |
| 38-39 | Tangentes laterales (R, L) |
| 40 | Intersección línea de medio campo (Top) con círculo central |
| 41 | Intersección línea de medio campo (Bottom) con círculo central |
| 42 | CENTER_MARK (punto central) |
Arcos de penal (43-56)
| ID | Nombre |
|---|---|
| 43, 50 | Centros de arco de penal izquierdo / derecho |
| 44-47 | Intersecciones arco de penal izquierdo con línea de 16m + tangentes |
| 48-49 | Marca de penal izquierda + punto medio del área izquierda |
| 51-54 | Intersecciones arco de penal derecho + tangentes |
| 55-56 | Marca de penal derecha + punto medio del área derecha |
Para el mapa completo: ver keypoints.py (constante KEYPOINT_INDEX_TO_NAME).
Uso
Opción A — vía snapshot_download (plug-and-play)
Funciona desde cualquier proyecto sin clonar nada manualmente:
from huggingface_hub import snapshot_download
import sys, cv2
repo = snapshot_download("OrbitalLab/mova-hrnet-keypoints-v1")
sys.path.insert(0, repo)
from keypoints import load_model, preprocess, heatmaps_to_keypoints
model = load_model(f"{repo}/hrnet_w18_keypoints_v1.pth", f"{repo}/config.yaml")
frame = cv2.imread("partido.jpg")
tensor, orig_size = preprocess(frame)
with __import__("torch").no_grad():
heatmaps = model(tensor.to(next(model.parameters()).device))[0]
kps = heatmaps_to_keypoints(heatmaps, orig_size, conf_threshold=0.1)
print(f"Detectados {len(kps)} keypoints")
Opción B — clonando el repo
git clone https://huggingface.co/OrbitalLab/mova-hrnet-keypoints-v1
cd mova-hrnet-keypoints-v1
pip install torch numpy opencv-python PyYAML huggingface_hub
python -c "
from keypoints import load_model, preprocess, heatmaps_to_keypoints
import cv2, torch
m = load_model('hrnet_w18_keypoints_v1.pth', 'config.yaml')
frame = cv2.imread('YOUR_IMAGE.jpg')
t, s = preprocess(frame)
with torch.no_grad():
hm = m(t.to(next(m.parameters()).device))[0]
print(heatmaps_to_keypoints(hm, s))
"
Opción C — dentro del stack MOVA
Si trabajas en OrbitalLabBOG/mova-futbol, el adapter ya está integrado en mova.adapters.hrnet_adapter.HRNetAdapter. Solo apuntá weights_path al checkpoint descargado de este repo.
Homografía (uso típico downstream)
Los 57 keypoints corresponden a puntos conocidos en el sistema de coordenadas de una cancha estándar FIFA (105 × 68 m). Para obtener la matriz de homografía cámara → cancha:
import cv2
import numpy as np
# Coordenadas reales (metros) de cada keypoint en cancha FIFA estándar
# Tu propio diccionario {keypoint_name: (x_meters, y_meters)}
PITCH_TEMPLATE = {...} # ver SoccerNet-Calibration o sn-gamestate
# Filtrar keypoints detectados que tengan correspondencia conocida
img_pts = np.array([(kps[n]["x"], kps[n]["y"]) for n in kps if n in PITCH_TEMPLATE])
world_pts = np.array([PITCH_TEMPLATE[n] for n in kps if n in PITCH_TEMPLATE])
# Mínimo 4 puntos. Recomendado: 8+ con RANSAC para robustez
H, mask = cv2.findHomography(img_pts, world_pts, cv2.RANSAC, ransacReprojThreshold=5.0)
Para los PITCH_TEMPLATE (coordenadas en metros), referir a SoccerNet-Calibration o al módulo mova.core.homography del repo MOVA.
Dependencias mínimas
torch>=2.0
numpy>=1.24
opencv-python>=4.8
PyYAML>=6.0
huggingface_hub>=0.24
CUDA opcional pero recomendada (CPU ≈ 1-2 s/frame; RTX 3060 ≈ 30 ms/frame).
Limitaciones conocidas
- Cobertura desigual: los keypoints del círculo central y tangentes tienen mayor error que los de las áreas (más texturadas en broadcast)
- Cámaras tácticas / vista cenital: el modelo fue entrenado con broadcast TV (cámara lateral elevada). En vistas cenitales o cámaras tácticas el rendimiento cae fuerte
- Cuasi-confluencia visual: en planos cerrados donde solo se ve una porción de la cancha, muchos keypoints quedan fuera de frame y la confianza es baja — usar RANSAC con
ransacReprojThresholdgeneroso - Iluminación pobre / partidos nocturnos sin TV broadcast: menor precisión
Citación
Si usas este modelo en un trabajo académico o derivado, por favor cita:
@misc{orbitallab-mova-hrnet-keypoints-2026,
author = {Orbital Lab},
title = {MOVA Soccer Field Keypoints — HRNet-W18 v1},
year = {2026},
publisher = {HuggingFace},
howpublished = {\url{https://huggingface.co/OrbitalLab/mova-hrnet-keypoints-v1}}
}
Licencia
Apache 2.0 — uso libre comercial y académico. La arquitectura HRNet original es propiedad de Microsoft Research Asia (MIT). Esta implementación es una reescritura propia.
Repos relacionados
- 🎯
OrbitalLab/mova-rfdetr-soccernet-v1— Detector de jugadores/balón (RF-DETR-L) - 🏗️
OrbitalLabBOG/mova-futbol— Stack MOVA completo (privado, IP Orbital) - 🎓
uexternado-cv/mova-futbol— Versión educativa del Semillero CV Externado
Maintainer: Orbital Lab · orbitallabai@gmail.com · orbitallab.ai
Última actualización: 2026-05-14
- Downloads last month
- 4