Detección de Landmarks con MediaPipe
Localización de puntos clave en rostros, manos y cuerpo humano con MediaPipe: detección de landmarks en tiempo real.
Detección de Landmarks con MediaPipe
Localización de puntos clave en rostros, manos y cuerpo humano
Presentación
La detección de landmarks (también llamada detección de puntos clave o keypoint detection) es una de las tareas más importantes de la visión por computador moderna. Consiste en localizar un conjunto predefinido de puntos anatómicos o geométricamente significativos sobre una imagen. A diferencia de la detección de objetos (que devuelve bounding boxes rectangulares) o la segmentación (que clasifica cada píxel), los landmarks proporcionan una representación estructural compacta y rica que codifica la geometría, la pose y la articulación del sujeto.
Objetivos de este notebook
- Comprender las bases teóricas de la detección de landmarks: formulación matemática, arquitecturas cascada (detector + regresor), enfoques basados en heatmaps vs. regresión directa, y funciones de pérdida específicas.
- Explorar tres modelos de MediaPipe sobre imágenes reales descargadas de internet:
- Face Mesh — 478 landmarks 3D para reconstrucción facial completa.
- Hand Landmarks — 21 puntos por mano con topología articular definida.
- Pose Estimation — 33 landmarks corporales con scores de visibilidad.
- Analizar propiedades geométricas de los landmarks detectados: distribución de profundidad Z, visibilidad por región corporal y cálculo de ángulos articulares.
- Demostrar aplicaciones prácticas: segmentación multi-persona, análisis biomecánico y tabla comparativa de los modelos.
Bases teóricas
Formulación del problema
Un modelo de landmarks recibe una imagen $I \in \mathbb{R}^{H \times W \times 3}$ y predice un conjunto ordenado de $K$ puntos clave:
$$ f_\theta(I) = {(x_k, y_k, z_k, v_k)}_{k=1}^{K} $$
donde $(x_k, y_k) \in [0, 1]^2$ son las coordenadas 2D normalizadas, $z_k$ es la profundidad relativa (en modelos 3D) y $v_k \in [0, 1]$ es la confianza o visibilidad del punto.
Arquitectura en cascada: detector + regresor
Los sistemas modernos de landmarks (BlazeFace/BlazePose de MediaPipe, OpenPose, HRNet) suelen emplear un pipeline de dos etapas:
- Detector de ROI: una red ligera (SSD, BlazeFace) que localiza la región de interés (rostro, palma, persona) con una bounding box.
- Regresor de landmarks: una CNN que recibe el recorte (crop) de la ROI y predice directamente las $K$ coordenadas.
Esta separación permite que el regresor trabaje siempre a resolución fija (ej. 192×192 para Face Mesh), independientemente del tamaño de la imagen original.
Enfoques: heatmaps vs. regresión directa
| Enfoque | Idea | Ventajas | Modelos representativos |
|---|---|---|---|
| Heatmap | Predice un mapa $H_k \in \mathbb{R}^{h \times w}$ por landmark, con un pico gaussiano centrado en la posición | Mayor precisión subpíxel, gradientes más estables | Hourglass, HRNet, SimpleBaseline |
| Regresión directa | La red predice directamente $(x_k, y_k, z_k)$ como valores escalares | Más eficiente, menos memoria, ideal para móvil | MediaPipe, MoveNet, PIPNet |
Función de pérdida
La pérdida más común es el error cuadrático medio ponderado por visibilidad:
$$ \mathcal{L} = \frac{1}{K} \sum_{k=1}^{K} v_k \left[ (\hat{x}_k - x_k)^2 + (\hat{y}_k - y_k)^2 + (\hat{z}_k - z_k)^2 \right] $$
donde $v_k$ pesa más los landmarks claramente visibles y reduce el impacto de los ocluidos.
MediaPipe: ML on-device de Google
MediaPipe es un framework de machine learning on-device desarrollado por Google que proporciona modelos optimizados para inferencia en tiempo real en CPU, GPU móvil y navegadores web. En este notebook usaremos tres soluciones:
| Solución | Landmarks | Detector | Resolución | Descripción |
|---|---|---|---|---|
| Face Mesh | 478 puntos 3D | BlazeFace (~0.2 ms) | 192×192 | Malla facial completa con iris refinado |
| Hands | 21 puntos 3D × mano | Palm Detector | 256×256 | Articulaciones y puntas de cada dedo |
| Pose | 33 puntos 3D | Person Detector | 256×256 | Esqueleto corporal completo con visibilidad |
Imágenes utilizadas
Usaremos imágenes descargadas de Unsplash (licencia libre) que contienen rostros, manos y cuerpos en diferentes poses. No se necesita ningún dataset estructurado: los modelos de MediaPipe están preentrenados y funcionan directamente sobre cualquier imagen.
# --- Parche de compatibilidad: mediapipe 0.10.x con protobuf >= 7 ---
# MediaPipe 0.10.14 requiere protobuf<5, pero coexiste con TensorFlow (protobuf>=6.31).
# Parcheamos las incompatibilidades antes de importar mediapipe.
import sys, types
# 1) Mock tensorflow.tools.docs.doc_controls para evitar que mediapipe
# intente importar TF completo (solo se usa para generar docs, no para inferencia)
for _mn in ['tensorflow', 'tensorflow.tools', 'tensorflow.tools.docs',
'tensorflow.tools.docs.doc_controls']:
if _mn not in sys.modules:
_m = types.ModuleType(_mn)
if _mn.endswith('doc_controls'):
_m.do_not_generate_docs = lambda x: x
sys.modules[_mn] = _m
import mediapipe
# Restaurar tensorflow real (para que otros imports no se vean afectados)
for _k in list(sys.modules.keys()):
if _k == 'tensorflow' or _k.startswith('tensorflow.'):
mod = sys.modules[_k]
if isinstance(mod, types.ModuleType) and getattr(mod, '__file__', None) is None:
if not hasattr(mod, '__path__'):
del sys.modules[_k]
# 2) Parche GetPrototype → GetSymbol (eliminado en protobuf 7.x)
from google.protobuf import symbol_database as _sdb
if not hasattr(_sdb.Default(), 'GetPrototype'):
_sdb.SymbolDatabase.GetPrototype = lambda self, desc: self.GetSymbol(desc.full_name)
# 3) Reemplazar _modify_calculator_options para usar is_repeated
# en lugar de FieldDescriptor.label (eliminado en protobuf 7.x)
from mediapipe.python.solution_base import SolutionBase as _SB
from collections.abc import Iterable as _Iter
from google.protobuf import symbol_database as _sdb2
def _patched_modify(self, graph_config, calc_params):
nested = {}
for cname, val in calc_params.items():
parts = cname.rsplit('.', 1)
if len(parts) != 2:
raise ValueError(f'Invalid key "{cname}"')
nested.setdefault(parts[0], []).append((parts[1], val))
def _set_fields(opts, flist):
for fn, fv in flist:
if fv is None:
opts.ClearField(fn)
elif opts.DESCRIPTOR.fields_by_name[fn].is_repeated:
if not isinstance(fv, _Iter):
raise ValueError(f'{fn} is repeated but value is not iterable')
opts.ClearField(fn)
for e in fv:
getattr(opts, fn).append(e)
else:
setattr(opts, fn, fv)
for node in graph_config.node:
nn = node.name or node.calculator
if nn not in nested:
continue
fl = nested[nn]
done = False
if node.node_options:
for elem in node.node_options:
tn = elem.type_url.split('/')[-1]
try:
db = _sdb2.Default()
ot = db.GetSymbol(db.pool.FindMessageTypeByName(tn).full_name)
except KeyError:
continue
if tn == ot.DESCRIPTOR.full_name:
co = ot.FromString(elem.value)
_set_fields(co, fl)
elem.value = co.SerializeToString()
done = True
if not done and node.HasField('options'):
for ext in node.options.Extensions:
_set_fields(node.options.Extensions[ext], fl)
_SB._modify_calculator_options = _patched_modify
print('✓ Parche protobuf/mediapipe aplicado')
✓ Parche protobuf/mediapipe aplicado
# Librerías y configuración
import cv2
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import urllib.request
import os
import mediapipe as mp
plt.rcParams['figure.figsize'] = (12, 7)
print(f'MediaPipe version: {mp.__version__}')
print(f'OpenCV version: {cv2.__version__}')
MediaPipe version: 0.10.14 OpenCV version: 4.11.0
1) Carga de imágenes de ejemplo
Descargamos imágenes de ejemplo desde URLs públicas. Usaremos imágenes con rostros visibles, manos y cuerpo completo para probar los tres modelos de landmarks.
# Utilidad para descargar y cargar imágenes desde URL
def load_image_from_url(url, filename=None):
"""Descarga una imagen desde URL y la devuelve en formato RGB."""
if filename is None:
filename = url.split('/')[-1].split('?')[0]
cache_dir = '/tmp/landmarks_images'
os.makedirs(cache_dir, exist_ok=True)
filepath = os.path.join(cache_dir, filename)
if not os.path.exists(filepath):
print(f'Descargando {filename}...')
urllib.request.urlretrieve(url, filepath)
img = cv2.imread(filepath)
if img is None:
raise ValueError(f'No se pudo cargar la imagen: {filepath}')
return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Imágenes de ejemplo (dominio público / licencia libre)
# Usamos imágenes de Unsplash y Pexels (free to use)
FACE_URL = 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=640'
HANDS_URL = 'https://images.unsplash.com/photo-1586348943529-beaae6c28db9?w=640'
POSE_URL = 'https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=640'
GROUP_URL = 'https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=640'
img_face = load_image_from_url(FACE_URL, 'face_portrait.jpg')
img_hands = load_image_from_url(HANDS_URL, 'hands_close.jpg')
img_pose = load_image_from_url(POSE_URL, 'pose_exercise.jpg')
img_group = load_image_from_url(GROUP_URL, 'group_people.jpg')
# Visualización
fig, axes = plt.subplots(1, 4, figsize=(20, 5))
titles = ['Rostro', 'Manos', 'Pose corporal', 'Grupo']
for ax, img, title in zip(axes, [img_face, img_hands, img_pose, img_group], titles):
ax.imshow(img)
ax.set_title(title, fontsize=13)
ax.axis('off')
plt.suptitle('Imágenes de ejemplo para detección de landmarks', fontsize=15)
plt.tight_layout()
plt.show()
2) Face Mesh: malla facial con 478 landmarks
El modelo Face Mesh de MediaPipe detecta 478 puntos 3D en cada rostro. Estos puntos forman una malla triangular que cubre toda la superficie facial, incluyendo contornos de ojos, cejas, nariz, labios y mandíbula.
Arquitectura interna
Face Mesh usa un pipeline de dos etapas:
- BlazeFace: detector ultraligero (~0.2 ms en móvil) que localiza la bounding box del rostro.
- Face Landmark Model: red de regresión que recibe el recorte facial (192×192) y predice 478 landmarks 3D.
El modelo es lo suficientemente eficiente para funcionar en tiempo real en dispositivos móviles, lo que lo hace ideal para aplicaciones de realidad aumentada.
Grupos de landmarks relevantes
Los 478 puntos se organizan en regiones anatómicas:
- Contorno facial: mandíbula, frente
- Ojos: párpados, iris (puntos refinados)
- Cejas: arco superior
- Nariz: puente, punta, aletas
- Labios: contorno exterior e interior
# Inicializamos Face Mesh
mp_face_mesh = mp.solutions.face_mesh
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
with mp_face_mesh.FaceMesh(
static_image_mode=True,
max_num_faces=1,
refine_landmarks=True, # Incluye landmarks del iris
min_detection_confidence=0.5
) as face_mesh:
results_face = face_mesh.process(img_face)
if results_face.multi_face_landmarks:
face_landmarks = results_face.multi_face_landmarks[0]
n_landmarks = len(face_landmarks.landmark)
print(f'Rostro detectado con {n_landmarks} landmarks')
# Mostramos algunos landmarks clave
key_points = {
'Punta de la nariz': 1,
'Mentón': 152,
'Ojo izquierdo (centro)': 468,
'Ojo derecho (centro)': 473,
'Comisura labio izq': 61,
'Comisura labio der': 291,
}
h, w = img_face.shape[:2]
print(f'\nLandmarks clave (coordenadas en píxeles):')
for name, idx in key_points.items():
lm = face_landmarks.landmark[idx]
print(f' {name} (#{idx}): x={lm.x*w:.0f}, y={lm.y*h:.0f}, z={lm.z:.4f}')
else:
print('No se detectó ningún rostro')
Rostro detectado con 478 landmarks Landmarks clave (coordenadas en píxeles): Punta de la nariz (#1): x=300, y=429, z=-0.1000 Mentón (#152): x=305, y=563, z=0.0690 Ojo izquierdo (centro) (#468): x=239, y=329, z=-0.0102 Ojo derecho (centro) (#473): x=357, y=325, z=-0.0098 Comisura labio izq (#61): x=245, y=460, z=0.0421 Comisura labio der (#291): x=363, y=455, z=0.0423
I0000 00:00:1773746515.957774 3340639 gl_context_egl.cc:85] Successfully initialized EGL. Major : 1 Minor: 5 I0000 00:00:1773746515.986043 3340902 gl_context.cc:357] GL version: 3.2 (OpenGL ES 3.2 NVIDIA 580.126.09), renderer: NVIDIA GeForce RTX 4090/PCIe/SSE2 W0000 00:00:1773746515.987380 3340873 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors. W0000 00:00:1773746515.994815 3340870 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors.
Visualización de la malla facial
Dibujamos la malla triangular completa sobre la imagen original. MediaPipe proporciona utilidades de dibujo que conectan los landmarks según la topología facial definida (tesselation).
# Dibujamos la malla facial completa
fig, axes = plt.subplots(1, 3, figsize=(20, 7))
# 1) Imagen original
axes[0].imshow(img_face)
axes[0].set_title('Imagen original', fontsize=13)
axes[0].axis('off')
# 2) Solo landmarks (puntos)
img_points = img_face.copy()
if results_face.multi_face_landmarks:
h, w = img_points.shape[:2]
for lm in face_landmarks.landmark:
cx, cy = int(lm.x * w), int(lm.y * h)
cv2.circle(img_points, (cx, cy), 1, (0, 255, 0), -1)
axes[1].imshow(img_points)
axes[1].set_title(f'478 landmarks (puntos)', fontsize=13)
axes[1].axis('off')
# 3) Malla completa (tesselation)
img_mesh = img_face.copy()
if results_face.multi_face_landmarks:
mp_drawing.draw_landmarks(
image=img_mesh,
landmark_list=face_landmarks,
connections=mp_face_mesh.FACEMESH_TESSELATION,
landmark_drawing_spec=None,
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_tesselation_style()
)
# Añadimos contornos de ojos y labios
mp_drawing.draw_landmarks(
image=img_mesh,
landmark_list=face_landmarks,
connections=mp_face_mesh.FACEMESH_CONTOURS,
landmark_drawing_spec=None,
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_contours_style()
)
axes[2].imshow(img_mesh)
axes[2].set_title('Malla facial + contornos', fontsize=13)
axes[2].axis('off')
plt.suptitle('Face Mesh: 478 landmarks 3D', fontsize=15)
plt.tight_layout()
plt.show()
Análisis de la distribución 3D de landmarks
Una característica interesante de Face Mesh es que predice coordenadas 3D. La componente $z$ representa la profundidad relativa: valores negativos indican puntos más cercanos a la cámara (como la punta de la nariz) y valores positivos indican puntos más lejanos (como las orejas).
# Extraemos coordenadas 3D de todos los landmarks faciales
if results_face.multi_face_landmarks:
coords = np.array([(lm.x, lm.y, lm.z) for lm in face_landmarks.landmark])
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
# Scatter 2D coloreado por profundidad Z
scatter = axes[0].scatter(
coords[:, 0], 1 - coords[:, 1], # Invertimos Y para que coincida con la imagen
c=coords[:, 2], cmap='coolwarm', s=3, alpha=0.8
)
axes[0].set_title('Landmarks 2D coloreados por profundidad Z', fontsize=12)
axes[0].set_xlabel('X normalizado')
axes[0].set_ylabel('Y normalizado')
axes[0].set_aspect('equal')
plt.colorbar(scatter, ax=axes[0], label='Profundidad Z')
# Histograma de valores Z
axes[1].hist(coords[:, 2], bins=40, color='steelblue', edgecolor='white', alpha=0.8)
axes[1].axvline(x=0, color='red', linestyle='--', alpha=0.7, label='Z=0 (plano de referencia)')
axes[1].set_title('Distribución de profundidad Z', fontsize=12)
axes[1].set_xlabel('Valor Z')
axes[1].set_ylabel('Número de landmarks')
axes[1].legend()
plt.tight_layout()
plt.show()
print(f'Rango Z: [{coords[:, 2].min():.4f}, {coords[:, 2].max():.4f}]')
print(f'Nariz (idx 1) Z = {coords[1, 2]:.4f} (punto más cercano esperado)')
Rango Z: [-0.1098, 0.1989] Nariz (idx 1) Z = -0.1000 (punto más cercano esperado)
3) Hand Landmarks: 21 puntos por mano
El modelo de manos de MediaPipe detecta 21 landmarks por cada mano visible. Estos puntos corresponden a las articulaciones de cada dedo más la muñeca.
Topología de los 21 landmarks
8 12 16 20 (puntas de los dedos)
| | | |
7 11 15 19
| | | |
6 10 14 18
| | | |
4 5 9 13 17 (nudillos / base)
| |
3 |
| |
2 ------+
|
1 (base del pulgar)
|
0 (muñeca)
Cada landmark tiene índices bien definidos:
- 0: Muñeca
- 1–4: Pulgar (CMC, MCP, IP, punta)
- 5–8: Índice (MCP, PIP, DIP, punta)
- 9–12: Medio
- 13–16: Anular
- 17–20: Meñique
Pipeline de detección
Similar a Face Mesh, usa dos etapas:
- Palm Detector: localiza la palma (más fácil de detectar que dedos individuales).
- Hand Landmark Model: predice 21 puntos 3D desde el recorte de la palma.
# Inicializamos Hand Landmarks
mp_hands = mp.solutions.hands
with mp_hands.Hands(
static_image_mode=True,
max_num_hands=2,
min_detection_confidence=0.5
) as hands:
results_hands = hands.process(img_hands)
if results_hands.multi_hand_landmarks:
n_hands = len(results_hands.multi_hand_landmarks)
print(f'Manos detectadas: {n_hands}')
for i, (hand_lms, handedness) in enumerate(
zip(results_hands.multi_hand_landmarks, results_hands.multi_handedness)
):
label = handedness.classification[0].label
score = handedness.classification[0].score
print(f'\nMano {i+1}: {label} (confianza: {score:.2%})')
# Puntas de los dedos
finger_tips = {
'Pulgar': 4, 'Índice': 8, 'Medio': 12, 'Anular': 16, 'Meñique': 20
}
h, w = img_hands.shape[:2]
for name, idx in finger_tips.items():
lm = hand_lms.landmark[idx]
print(f' Punta {name} (#{idx}): x={lm.x*w:.0f}, y={lm.y*h:.0f}')
else:
print('No se detectaron manos. Prueba con otra imagen.')
No se detectaron manos. Prueba con otra imagen.
I0000 00:00:1773746516.351244 3340639 gl_context_egl.cc:85] Successfully initialized EGL. Major : 1 Minor: 5 I0000 00:00:1773746516.377709 3340935 gl_context.cc:357] GL version: 3.2 (OpenGL ES 3.2 NVIDIA 580.126.09), renderer: NVIDIA GeForce RTX 4090/PCIe/SSE2 W0000 00:00:1773746516.383802 3340905 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors. W0000 00:00:1773746516.394173 3340933 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors.
# Visualización de landmarks de manos
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
axes[0].imshow(img_hands)
axes[0].set_title('Imagen original', fontsize=13)
axes[0].axis('off')
img_hands_draw = img_hands.copy()
if results_hands.multi_hand_landmarks:
for hand_lms in results_hands.multi_hand_landmarks:
mp_drawing.draw_landmarks(
img_hands_draw,
hand_lms,
mp_hands.HAND_CONNECTIONS,
mp_drawing_styles.get_default_hand_landmarks_style(),
mp_drawing_styles.get_default_hand_connections_style()
)
axes[1].imshow(img_hands_draw)
axes[1].set_title('Hand Landmarks (21 puntos por mano)', fontsize=13)
axes[1].axis('off')
plt.tight_layout()
plt.show()
4) Pose Estimation: 33 landmarks corporales
El modelo de Pose de MediaPipe detecta 33 landmarks que cubren el esqueleto corporal completo: cabeza, hombros, codos, muñecas, caderas, rodillas, tobillos y pies.
Topología del esqueleto
Los 33 puntos se distribuyen así:
- 0–10: Rostro (nariz, ojos, orejas, boca)
- 11–12: Hombros (izquierdo, derecho)
- 13–14: Codos
- 15–16: Muñecas
- 17–22: Manos (meñique, índice, pulgar de cada mano)
- 23–24: Caderas
- 25–26: Rodillas
- 27–28: Tobillos
- 29–32: Pies (talón, punta)
Utilidad de la visibilidad
Cada landmark incluye un score de visibilidad $v_k \in [0, 1]$. Esto es crucial en aplicaciones prácticas: si una persona está de perfil, los landmarks del lado oculto tendrán baja visibilidad. El modelo es capaz de inferir posiciones de puntos ocluidos (predicción, no observación directa), pero con menor confianza.
# Inicializamos Pose
mp_pose = mp.solutions.pose
with mp_pose.Pose(
static_image_mode=True,
model_complexity=2, # 0=lite, 1=full, 2=heavy (más preciso)
min_detection_confidence=0.5
) as pose:
results_pose = pose.process(img_pose)
if results_pose.pose_landmarks:
pose_landmarks = results_pose.pose_landmarks
print(f'Pose detectada con {len(pose_landmarks.landmark)} landmarks')
# Landmarks corporales principales
body_parts = {
'Nariz': 0, 'Hombro izq': 11, 'Hombro der': 12,
'Codo izq': 13, 'Codo der': 14,
'Muñeca izq': 15, 'Muñeca der': 16,
'Cadera izq': 23, 'Cadera der': 24,
'Rodilla izq': 25, 'Rodilla der': 26,
'Tobillo izq': 27, 'Tobillo der': 28,
}
h, w = img_pose.shape[:2]
print(f'\nLandmarks principales:')
for name, idx in body_parts.items():
lm = pose_landmarks.landmark[idx]
vis = '✓' if lm.visibility > 0.5 else '✗'
print(f' {name} (#{idx}): x={lm.x*w:.0f}, y={lm.y*h:.0f}, vis={lm.visibility:.2f} {vis}')
else:
print('No se detectó pose corporal')
Downloading model to /home/nuberu/xuan/naux/.venv/lib/python3.10/site-packages/mediapipe/modules/pose_landmark/pose_landmark_heavy.tflite Pose detectada con 33 landmarks Landmarks principales: Nariz (#0): x=252, y=95, vis=1.00 ✓ Hombro izq (#11): x=265, y=127, vis=1.00 ✓ Hombro der (#12): x=189, y=128, vis=1.00 ✓ Codo izq (#13): x=303, y=185, vis=0.30 ✗ Codo der (#14): x=182, y=203, vis=1.00 ✓ Muñeca izq (#15): x=345, y=214, vis=0.21 ✗ Muñeca der (#16): x=234, y=223, vis=1.00 ✓ Cadera izq (#23): x=239, y=235, vis=1.00 ✓ Cadera der (#24): x=199, y=243, vis=1.00 ✓ Rodilla izq (#25): x=288, y=245, vis=0.95 ✓ Rodilla der (#26): x=222, y=352, vis=0.99 ✓ Tobillo izq (#27): x=305, y=328, vis=0.94 ✓ Tobillo der (#28): x=155, y=334, vis=0.76 ✓
I0000 00:00:1773746518.397536 3340639 gl_context_egl.cc:85] Successfully initialized EGL. Major : 1 Minor: 5 I0000 00:00:1773746518.423568 3340968 gl_context.cc:357] GL version: 3.2 (OpenGL ES 3.2 NVIDIA 580.126.09), renderer: NVIDIA GeForce RTX 4090/PCIe/SSE2 W0000 00:00:1773746518.457279 3340939 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors. W0000 00:00:1773746518.508638 3340959 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors.
# Visualización de pose
fig, axes = plt.subplots(1, 2, figsize=(16, 8))
axes[0].imshow(img_pose)
axes[0].set_title('Imagen original', fontsize=13)
axes[0].axis('off')
img_pose_draw = img_pose.copy()
if results_pose.pose_landmarks:
mp_drawing.draw_landmarks(
img_pose_draw,
pose_landmarks,
mp_pose.POSE_CONNECTIONS,
landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style()
)
axes[1].imshow(img_pose_draw)
axes[1].set_title('Pose Landmarks (33 puntos)', fontsize=13)
axes[1].axis('off')
plt.tight_layout()
plt.show()
Análisis de visibilidad por región corporal
Veamos qué partes del cuerpo detecta el modelo con mayor confianza. Los puntos con alta visibilidad están claramente expuestos en la imagen, mientras que los de baja visibilidad podrían estar ocluidos o en un ángulo desfavorable.
# Análisis de visibilidad
if results_pose.pose_landmarks:
landmark_names = [
'nariz', 'ojo_int_izq', 'ojo_izq', 'ojo_ext_izq', 'ojo_int_der',
'ojo_der', 'ojo_ext_der', 'oreja_izq', 'oreja_der', 'boca_izq',
'boca_der', 'hombro_izq', 'hombro_der', 'codo_izq', 'codo_der',
'muñeca_izq', 'muñeca_der', 'meñique_izq', 'meñique_der',
'índice_izq', 'índice_der', 'pulgar_izq', 'pulgar_der',
'cadera_izq', 'cadera_der', 'rodilla_izq', 'rodilla_der',
'tobillo_izq', 'tobillo_der', 'talón_izq', 'talón_der',
'pie_izq', 'pie_der'
]
visibilities = [lm.visibility for lm in pose_landmarks.landmark]
# Coloreamos por visibilidad
colors = ['forestgreen' if v > 0.7 else 'orange' if v > 0.3 else 'crimson' for v in visibilities]
plt.figure(figsize=(14, 6))
plt.barh(range(33), visibilities, color=colors, edgecolor='white', height=0.7)
plt.yticks(range(33), landmark_names, fontsize=8)
plt.xlabel('Visibilidad')
plt.title('Visibilidad de cada landmark de pose')
plt.axvline(x=0.5, color='gray', linestyle='--', alpha=0.5, label='Umbral 0.5')
# Leyenda
patches = [
mpatches.Patch(color='forestgreen', label='Alta (>0.7)'),
mpatches.Patch(color='orange', label='Media (0.3–0.7)'),
mpatches.Patch(color='crimson', label='Baja (<0.3)')
]
plt.legend(handles=patches, loc='lower right')
plt.tight_layout()
plt.show()
5) Aplicación combinada: múltiples personas
Una aplicación realista implica procesar imágenes con múltiples personas. Face Mesh y Hands permiten detectar múltiples instancias, mientras que Pose detecta una persona a la vez (se puede iterar con un detector de personas previo).
Probamos Face Mesh en la imagen de grupo para ver cómo maneja múltiples rostros.
# Face Mesh en imagen de grupo
with mp_face_mesh.FaceMesh(
static_image_mode=True,
max_num_faces=10,
refine_landmarks=True,
min_detection_confidence=0.5
) as face_mesh:
results_group = face_mesh.process(img_group)
img_group_draw = img_group.copy()
n_faces = 0
if results_group.multi_face_landmarks:
n_faces = len(results_group.multi_face_landmarks)
for face_lms in results_group.multi_face_landmarks:
mp_drawing.draw_landmarks(
image=img_group_draw,
landmark_list=face_lms,
connections=mp_face_mesh.FACEMESH_CONTOURS,
landmark_drawing_spec=None,
connection_drawing_spec=mp_drawing_styles.get_default_face_mesh_contours_style()
)
fig, axes = plt.subplots(1, 2, figsize=(18, 7))
axes[0].imshow(img_group)
axes[0].set_title('Imagen original', fontsize=13)
axes[0].axis('off')
axes[1].imshow(img_group_draw)
axes[1].set_title(f'Face Mesh: {n_faces} rostros detectados', fontsize=13)
axes[1].axis('off')
plt.tight_layout()
plt.show()
print(f'Rostros detectados: {n_faces}')
I0000 00:00:1773746518.888538 3340639 gl_context_egl.cc:85] Successfully initialized EGL. Major : 1 Minor: 5 I0000 00:00:1773746518.915730 3341001 gl_context.cc:357] GL version: 3.2 (OpenGL ES 3.2 NVIDIA 580.126.09), renderer: NVIDIA GeForce RTX 4090/PCIe/SSE2 W0000 00:00:1773746518.916983 3340969 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors. W0000 00:00:1773746518.923359 3340997 inference_feedback_manager.cc:114] Feedback manager requires a model with a single signature inference. Disabling support for feedback tensors.
Rostros detectados: 0
6) Cálculo de ángulos articulares a partir de landmarks
Una aplicación práctica importante de los landmarks es el cálculo de ángulos articulares. Por ejemplo, en fisioterapia o análisis deportivo se mide el ángulo del codo, rodilla u hombro.
Dado tres landmarks $A$, $B$, $C$ donde $B$ es la articulación, el ángulo se calcula como:
$$ \theta = \arccos\left(\frac{\vec{BA} \cdot \vec{BC}}{|\vec{BA}| , |\vec{BC}|}\right) $$
donde $\vec{BA} = A - B$ y $\vec{BC} = C - B$.
def calculate_angle(a, b, c):
"""Calcula el ángulo en grados en el punto B, formado por A-B-C."""
a = np.array(a)
b = np.array(b)
c = np.array(c)
ba = a - b
bc = c - b
cosine = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc) + 1e-8)
angle = np.degrees(np.arccos(np.clip(cosine, -1, 1)))
return angle
# Calculamos ángulos articulares si tenemos pose
if results_pose.pose_landmarks:
lms = results_pose.pose_landmarks.landmark
def get_coords(idx):
return [lms[idx].x, lms[idx].y]
# Ángulos articulares clave
angles = {
'Codo izquierdo': calculate_angle(get_coords(11), get_coords(13), get_coords(15)),
'Codo derecho': calculate_angle(get_coords(12), get_coords(14), get_coords(16)),
'Hombro izquierdo': calculate_angle(get_coords(13), get_coords(11), get_coords(23)),
'Hombro derecho': calculate_angle(get_coords(14), get_coords(12), get_coords(24)),
'Rodilla izquierda': calculate_angle(get_coords(23), get_coords(25), get_coords(27)),
'Rodilla derecha': calculate_angle(get_coords(24), get_coords(26), get_coords(28)),
'Cadera izquierda': calculate_angle(get_coords(11), get_coords(23), get_coords(25)),
'Cadera derecha': calculate_angle(get_coords(12), get_coords(24), get_coords(26)),
}
print('Ángulos articulares estimados:')
for name, angle in angles.items():
print(f' {name}: {angle:.1f}°')
# Visualización de ángulos
plt.figure(figsize=(10, 5))
names = list(angles.keys())
values = list(angles.values())
colors = ['#3498db' if 'izq' in n else '#e74c3c' for n in names]
plt.barh(names, values, color=colors, edgecolor='white')
plt.xlabel('Ángulo (grados)')
plt.title('Ángulos articulares estimados a partir de landmarks de pose')
plt.axvline(x=90, color='gray', linestyle='--', alpha=0.5, label='90°')
plt.axvline(x=180, color='gray', linestyle=':', alpha=0.5, label='180°')
plt.legend()
plt.tight_layout()
plt.show()
else:
print('No hay datos de pose disponibles para calcular ángulos')
Ángulos articulares estimados: Codo izquierdo: 159.9° Codo derecho: 116.7° Hombro izquierdo: 32.8° Hombro derecho: 6.7° Rodilla izquierda: 114.3° Rodilla derecha: 59.7° Cadera izquierda: 97.5° Cadera derecha: 175.4°
7) Comparativa de los tres modelos
Resumimos las características de los tres modelos de landmarks que hemos explorado.
import pandas as pd
comparison = pd.DataFrame({
'Modelo': ['Face Mesh', 'Hand Landmarks', 'Pose Estimation'],
'Landmarks': [478, 21, 33],
'Dimensiones': ['3D (x, y, z)', '3D (x, y, z)', '3D (x, y, z) + visibilidad'],
'Multi-instancia': ['Sí (max_num_faces)', 'Sí (max_num_hands=2)', 'No (1 persona)'],
'Detector previo': ['BlazeFace', 'Palm Detector', 'Person Detector'],
'Aplicaciones típicas': [
'AR, expresiones, identidad',
'Gestos, lenguaje signos, control',
'Deportes, fisioterapia, animación'
]
})
display(comparison)
| Modelo | Landmarks | Dimensiones | Multi-instancia | Detector previo | Aplicaciones típicas | |
|---|---|---|---|---|---|---|
| 0 | Face Mesh | 478 | 3D (x, y, z) | Sí (max_num_faces) | BlazeFace | AR, expresiones, identidad |
| 1 | Hand Landmarks | 21 | 3D (x, y, z) | Sí (max_num_hands=2) | Palm Detector | Gestos, lenguaje signos, control |
| 2 | Pose Estimation | 33 | 3D (x, y, z) + visibilidad | No (1 persona) | Person Detector | Deportes, fisioterapia, animación |
Conclusiones y siguientes pasos
Conclusiones
- Los landmarks proporcionan representaciones estructurales ricas que van mucho más allá de las bounding boxes de la detección de objetos. Permiten entender la geometría y articulación del sujeto.
- MediaPipe ofrece modelos optimizados que funcionan en tiempo real incluso en CPU, gracias a su arquitectura ligera de cascada detector + regresor.
- Face Mesh (478 puntos) permite aplicaciones sofisticadas de realidad aumentada y análisis facial. La componente Z añade información 3D valiosa.
- Hand Landmarks (21 puntos) con su topología bien definida facilita el reconocimiento de gestos y la estimación de la configuración de la mano.
- Pose Estimation (33 puntos) proporciona un esqueleto corporal completo con scores de visibilidad, esencial para análisis de movimiento y biomecánica.
- Los ángulos articulares calculados a partir de landmarks tienen aplicación directa en deportes, rehabilitación y ergonomía.
Qué podrías explorar después
- Procesar vídeo en tiempo real con la webcam usando estos mismos modelos.
- Implementar un clasificador de gestos de mano usando los landmarks como features.
- Comparar con otros modelos de landmarks: OpenPose, HRNet, MMPose.
- Usar landmarks faciales para transferencia de expresiones entre caras.
- Entrenar un modelo de action recognition a partir de secuencias temporales de landmarks de pose.
Idea final: los landmarks son el puente entre la percepción visual (qué hay en la imagen) y la comprensión estructural (cómo está organizado). Son fundamentales en aplicaciones que requieren interacción humano-computador.