Para qué sirve extraer thumbnails de vídeo
Extraer fotogramas de vídeo tiene aplicaciones en múltiples ámbitos:
- Plataformas de vídeo: generar miniaturas automáticas para YouTube, Vimeo, portales internos
- Análisis de contenido: machine learning sobre frames para detección de objetos, escenas, emociones
- Edición: obtener el frame exacto para usarlo como carátula o en presentaciones
- Vigilancia: extraer momentos clave de grabaciones de seguridad
- Catálogos: generar mosaicos de previsualización para grandes bibliotecas de vídeo
- Storyboards: crear paneles de navegación visual para documentales o cursos
Instalación
pip install opencv-python-headless Pillow ffmpeg-python
# FFmpeg en el sistema:
# Ubuntu: sudo apt install ffmpeg
# macOS: brew install ffmpeg
# Windows: descargar desde ffmpeg.org y añadir al PATH
Método 1: FFmpeg directo (más rápido y flexible)
import subprocess
import shutil
from pathlib import Path
def thumbnail_en_tiempo(video, tiempo, salida, ancho=None, alto=None, calidad=95):
"""
Extrae un fotograma en el tiempo especificado.
Args:
tiempo: puede ser segundos (float), "HH:MM:SS", "00:01:30.500"
ancho: None = tamaño original, int = redimensionar manteniendo aspecto
calidad: 1-95 para JPEG (95 = casi sin pérdidas)
"""
if not shutil.which('ffmpeg'):
raise RuntimeError("FFmpeg no está instalado")
cmd = ['ffmpeg', '-y', '-ss', str(tiempo), '-i', str(video)]
# Filtro de escala
if ancho and alto:
vf = f'scale={ancho}:{alto}'
elif ancho:
vf = f'scale={ancho}:-1'
elif alto:
vf = f'scale=-1:{alto}'
else:
vf = None
if vf:
cmd += ['-vf', vf]
cmd += [
'-vframes', '1', # solo 1 fotograma
'-q:v', str(int(2 + (100 - calidad) / 10)), # calidad JPEG (2=mejor, 31=peor)
str(salida)
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
tamaño = Path(salida).stat().st_size / 1024
print(f"Thumbnail: {salida} ({tamaño:.1f} KB)")
return str(salida)
else:
raise RuntimeError(f"FFmpeg error:\n{result.stderr}")
def multiples_thumbnails(video, tiempos, carpeta_salida, ancho=1280):
"""
Extrae thumbnails en múltiples instantes del vídeo.
tiempos: lista de segundos o strings "HH:MM:SS"
"""
carpeta = Path(carpeta_salida)
carpeta.mkdir(parents=True, exist_ok=True)
stem = Path(video).stem
generados = []
for i, t in enumerate(tiempos, 1):
salida = carpeta / f"{stem}_thumb_{i:03d}.jpg"
try:
thumbnail_en_tiempo(video, t, str(salida), ancho=ancho)
generados.append(str(salida))
except Exception as e:
print(f" ✗ Error en t={t}: {e}")
print(f"Extraídos {len(generados)}/{len(tiempos)} thumbnails")
return generados
# Ejemplos
thumbnail_en_tiempo('pelicula.mp4', 30, 'thumb_30s.jpg', ancho=1280)
thumbnail_en_tiempo('tutorial.mp4', '00:02:15', 'thumb_2m15s.jpg', ancho=960)
multiples_thumbnails('webinar.mp4',
[0, 60, 120, 180, 300, 600],
'thumbnails/', ancho=854)
Thumbnail del mejor fotograma automático
def thumbnail_automatico(video, salida, ancho=1280, metodo='medio'):
"""
Genera un thumbnail automático eligiendo un buen fotograma.
metodo:
'medio' — fotograma al 50% de duración (buena heurística general)
'mejor' — usa el filtro thumbnail de FFmpeg (analiza nítidez/color)
'iframes' — primer I-frame significativo (muy rápido)
"""
if metodo == 'mejor':
# FFmpeg thumbnail filter: analiza 100 frames y elige el más representativo
cmd = [
'ffmpeg', '-y', '-i', str(video),
'-vf', f'thumbnail=n=100,scale={ancho}:-1',
'-vframes', '1',
'-q:v', '2',
str(salida)
]
elif metodo == 'medio':
# Obtener duración primero
cmd_dur = [
'ffprobe', '-v', 'quiet',
'-show_entries', 'format=duration',
'-of', 'csv=p=0',
str(video)
]
resultado = subprocess.run(cmd_dur, capture_output=True, text=True)
try:
duracion = float(resultado.stdout.strip())
tiempo_medio = duracion / 2
except ValueError:
tiempo_medio = 30 # fallback
return thumbnail_en_tiempo(video, tiempo_medio, salida, ancho=ancho)
else: # iframes
cmd = [
'ffmpeg', '-y', '-i', str(video),
'-vf', f'select=eq(pict_type\\,I),scale={ancho}:-1',
'-vframes', '1',
'-q:v', '2',
'-vsync', 'vfr',
str(salida)
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"Thumbnail automático ({metodo}): {salida}")
return str(salida)
else:
raise RuntimeError(f"FFmpeg error: {result.stderr}")
thumbnail_automatico('documental.mp4', 'thumb_auto.jpg', metodo='mejor')
Generar mosaico de thumbnails (contact sheet)
def mosaico_thumbnails(video, salida, columnas=4, filas=3, ancho=1920):
"""
Genera una imagen de mosaico con thumbnails de todo el vídeo.
Muy útil para vista previa de archivos en una biblioteca.
"""
total_frames = columnas * filas
# Filtro FFmpeg para mosaico
# fps=1/duración_segmento captura uniformemente; tile hace el mosaico
vf = (
f'fps=1/{max(1, int(30 / total_frames))},' # selección de frames
f'scale={ancho // columnas}:-1,' # redimensionar cada frame
f'tile={columnas}x{filas}' # crear mosaico
)
cmd = [
'ffmpeg', '-y', '-i', str(video),
'-vf', vf,
'-vframes', '1',
'-q:v', '4',
str(salida)
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
tamaño = Path(salida).stat().st_size / 1024
print(f"Mosaico {columnas}×{filas}: {salida} ({tamaño:.1f} KB)")
return str(salida)
else:
raise RuntimeError(f"FFmpeg error: {result.stderr}")
mosaico_thumbnails('pelicula.mp4', 'mosaico_4x3.jpg', columnas=4, filas=3)
mosaico_thumbnails('serie_ep01.mkv', 'preview_6x4.jpg', columnas=6, filas=4)
Método 2: OpenCV (para análisis frame por frame)
import cv2
from PIL import Image
import numpy as np
def extraer_frame_opencv(video, tiempo_segundos, salida, ancho=None):
"""Extrae un frame exacto en segundos con OpenCV."""
cap = cv2.VideoCapture(str(video))
fps = cap.get(cv2.CAP_PROP_FPS)
frame_n = int(tiempo_segundos * fps)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_n)
ret, frame = cap.read()
cap.release()
if not ret:
raise RuntimeError(f"No se pudo leer el frame en t={tiempo_segundos}s")
# OpenCV usa BGR, convertir a RGB
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = Image.fromarray(frame_rgb)
if ancho:
aspecto = img.width / img.height
nuevo_alto = int(ancho / aspecto)
img = img.resize((ancho, nuevo_alto), Image.LANCZOS)
img.save(salida, quality=95)
print(f"Frame OpenCV: {salida}")
return img
def analizar_nitidez_frames(video, n_muestras=20):
"""
Analiza la nitidez de frames distribuidos en el vídeo.
Útil para encontrar el frame más nítido automáticamente.
"""
cap = cv2.VideoCapture(str(video))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
paso = max(1, total_frames // n_muestras)
resultados = []
for i in range(0, total_frames, paso):
cap.set(cv2.CAP_PROP_POS_FRAMES, i)
ret, frame = cap.read()
if not ret:
continue
gris = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Varianza del Laplaciano: mide la nitidez (más alto = más nítido)
nitidez = cv2.Laplacian(gris, cv2.CV_64F).var()
resultados.append({
'frame': i,
'tiempo': i / fps,
'nitidez': nitidez,
})
cap.release()
if resultados:
mejor = max(resultados, key=lambda x: x['nitidez'])
print(f"Frame más nítido: t={mejor['tiempo']:.2f}s "
f"(nitidez={mejor['nitidez']:.1f})")
return mejor
return None
def detectar_cambios_escena(video, umbral=30):
"""
Detecta cambios de escena en el vídeo comparando frames consecutivos.
Devuelve los timestamps de cada cambio de escena detectado.
"""
cap = cv2.VideoCapture(str(video))
fps = cap.get(cv2.CAP_PROP_FPS)
escenas = []
ret, frame_ant = cap.read()
if not ret:
return escenas
frame_num = 0
gris_ant = cv2.cvtColor(frame_ant, cv2.COLOR_BGR2GRAY)
while True:
ret, frame = cap.read()
if not ret:
break
frame_num += 1
gris = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Diferencia media entre frames consecutivos
diff = np.mean(np.abs(gris.astype(float) - gris_ant.astype(float)))
if diff > umbral:
tiempo = frame_num / fps
escenas.append({
'frame': frame_num,
'tiempo': tiempo,
'diff': diff,
})
print(f" Cambio de escena: t={tiempo:.2f}s (diff={diff:.1f})")
gris_ant = gris
cap.release()
print(f"Total: {len(escenas)} cambios de escena detectados")
return escenas
Procesar una biblioteca de vídeos en lote
def lote_thumbnails(directorio, carpeta_salida, ancho=1280,
patron='*.mp4', metodo='mejor'):
"""
Genera thumbnails automáticos para todos los vídeos de una carpeta.
"""
carpeta = Path(directorio)
dest = Path(carpeta_salida)
dest.mkdir(parents=True, exist_ok=True)
videos = []
for ext in ['*.mp4', '*.mkv', '*.avi', '*.mov', '*.webm']:
videos.extend(carpeta.glob(ext))
videos = sorted(set(videos))
resultados = {'ok': 0, 'error': 0}
print(f"Generando thumbnails para {len(videos)} vídeos...")
for video in videos:
salida = dest / f"{video.stem}_thumb.jpg"
try:
thumbnail_automatico(str(video), str(salida), ancho=ancho, metodo=metodo)
resultados['ok'] += 1
except Exception as e:
print(f" ✗ {video.name}: {e}")
resultados['error'] += 1
print(f"\nResultados: {resultados['ok']} generados, "
f"{resultados['error']} errores")
return resultados
# Procesar biblioteca completa
lote_thumbnails('videos/', 'thumbnails/', ancho=1280, metodo='mejor')
Generar thumbnails con anotaciones
from PIL import Image, ImageDraw, ImageFont
import time
def thumbnail_con_timestamp(video, tiempo, salida, ancho=1280):
"""Extrae thumbnail y añade el timestamp visible en la imagen."""
# Extraer frame
thumb_tmp = salida.replace('.jpg', '_tmp.jpg')
thumbnail_en_tiempo(video, tiempo, thumb_tmp, ancho=ancho)
# Añadir timestamp
img = Image.open(thumb_tmp)
d = ImageDraw.Draw(img)
# Formatear tiempo
t_seg = int(tiempo)
ts = f"{t_seg // 3600:02d}:{(t_seg % 3600) // 60:02d}:{t_seg % 60:02d}"
# Texto con sombra
pos = (20, img.height - 60)
d.text((pos[0]+2, pos[1]+2), ts, fill='black', font_size=32)
d.text(pos, ts, fill='white', font_size=32)
img.save(salida, quality=92)
Path(thumb_tmp).unlink(missing_ok=True)
print(f"Thumbnail con timestamp: {salida}")
return str(salida)
Conclusión
Para extraer thumbnails de vídeo en Python, FFmpeg es la herramienta más potente: permite elegir el frame más representativo automáticamente (filtro thumbnail), generar mosaicos completos y procesar formatos de vídeo sin importar el codec. OpenCV es superior cuando necesitas análisis frame por frame: detectar nitidez, cambios de escena o procesar frames con algoritmos de visión artificial. Combina ambas: usa FFmpeg para generación masiva y OpenCV para análisis de contenido.
Conversiones relacionadas
Conversiones de vídeo que encajan con esta guía: