Redimensionar Imágenes en Lote con Python
Cómo redimensionar y optimizar imágenes en lote usando Pillow y Python para entrega web.
Visión General
Redimensionar imágenes en lote es una tarea común para optimización web, generación de thumbnails y pipelines de contenido. Python con Pillow (fork de PIL) hace esto sencillo. Esta recipe cubre redimensionado batch, conversión de formato, optimización de calidad y generación de thumbnails.
Cuándo Usar
- Necesitas generar thumbnails para una galería o catálogo de productos
- Estás optimizando imágenes para entrega web (reducir tamaño de archivo)
- Necesitas convertir imágenes entre formatos (PNG a WebP, JPEG a PNG)
- Estás procesando uploads de usuarios a un tamaño consistente
Solución
Redimensionado básico
from PIL import Image
img = Image.open("photo.jpg")
resized = img.resize((800, 600))
resized.save("photo_small.jpg")
Redimensionar todas las imágenes de un directorio
from PIL import Image
from pathlib import Path
input_dir = Path("images")
output_dir = Path("images_resized")
output_dir.mkdir(exist_ok=True)
target_size = (800, 600)
for img_path in input_dir.glob("*"):
if img_path.suffix.lower() not in (".jpg", ".jpeg", ".png", ".webp"):
continue
img = Image.open(img_path)
resized = img.resize(target_size, Image.Resampling.LANCZOS)
resized.save(output_dir / img_path.name)
print(f"Resized: {img_path.name}")
Redimensionar manteniendo aspect ratio
from PIL import Image
def resize_maintain_ratio(img, max_width, max_height):
width, height = img.size
ratio = min(max_width / width, max_height / height)
new_size = (int(width * ratio), int(height * ratio))
return img.resize(new_size, Image.Resampling.LANCZOS)
img = Image.open("photo.jpg")
resized = resize_maintain_ratio(img, 800, 600)
resized.save("photo_ratio.jpg")
Generar thumbnails
from PIL import Image
from pathlib import Path
img = Image.open("photo.jpg")
img.thumbnail((200, 200), Image.Resampling.LANCZOS)
img.save("thumb_photo.jpg")
Convertir formato y optimizar para web
from PIL import Image
from pathlib import Path
input_dir = Path("images")
output_dir = Path("images_webp")
output_dir.mkdir(exist_ok=True)
for img_path in input_dir.glob("*.png"):
img = Image.open(img_path)
img = img.convert("RGB") # WebP necesita RGB
output_path = output_dir / (img_path.stem + ".webp")
img.save(output_path, "webp", quality=85, method=6)
print(f"Converted: {img_path.name} -> {output_path.name}")
Procesamiento batch con seguimiento de progreso
from PIL import Image
from pathlib import Path
import time
input_dir = Path("photos")
output_dir = Path("photos_optimized")
output_dir.mkdir(exist_ok=True)
files = list(input_dir.glob("*.[jp][pn]g"))
total = len(files)
for i, img_path in enumerate(files, 1):
img = Image.open(img_path)
img.thumbnail((1200, 1200), Image.Resampling.LANCZOS)
output_path = output_dir / (img_path.stem + ".jpg")
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
img.save(output_path, "JPEG", quality=85, optimize=True)
print(f"[{i}/{total}] {img_path.name} -> {output_path.name}")
Explicación
Pillow proporciona dos métodos de redimensionado: resize() cambia las dimensiones a valores exactos (puede distorsionar), y thumbnail() redimensiona para caber dentro de una caja preservando aspect ratio (modificación in-place).
Image.Resampling.LANCZOS da la mejor calidad para downscaling. Para upscaling, BICUBIC suele ser mejor. El default NEAREST es rápido pero produce resultados pixelados.
Para optimización web, convierte a formato WebP con quality=85. WebP produce archivos 25-35% más pequeños que JPEG a calidad equivalente. Usa method=6 para máxima compresión (más lento pero archivos más pequeños).
Variantes
| Enfoque | Método | Aspect Ratio | Usar Cuando |
|---|---|---|---|
| resize() | Tamaño exacto | Distorsionado | Dimensiones fijas requeridas |
| thumbnail() | Caber en caja | Preservado | Thumbnails de galería |
| Función ratio custom | Max ancho/alto | Preservado | Layouts flexibles |
| Conversión WebP | Cambio de formato | Preservado | Optimización web |
Pautas
- Usa
LANCZOSpara downscaling de fotos. Produce los resultados más nítidos. - Convierte imágenes RGBA/P a RGB antes de guardar como JPEG. JPEG no soporta transparencia.
- Usa
quality=85para JPEG. Bajo 80, los artefactos son visibles; sobre 90, el tamaño crece con ganancia mínima. - Usa WebP para entrega web. Es soportado por todos los navegadores modernos y produce archivos más pequeños.
- Procesa imágenes en un directorio de output separado. Nunca sobrescribas los originales.
Errores Comunes
- Usar
resize()cuando quieres preservar aspect ratio. Usathumbnail()o una función de ratio custom. - Olvidar convertir RGBA a RGB antes de guardar como JPEG. Esto lanza error o produce colores incorrectos.
- Usar
NEARESTresampling para fotos. Se ve pixelado. Siempre usaLANCZOSoBICUBIC. - No activar
optimize=Truepara JPEG. Reduce el tamaño sin pérdida de calidad. - Sobrescribir archivos originales. Siempre escribe a un directorio separado para evitar pérdida de datos.
Mejores Prácticas Adicionales
- For a deeper guide, see Compress and Decompress Files.
- Usa
ImageOps.exif_transpose()antes de redimensionar. Las fotos de teléfonos suelen tener tags de orientación EXIF. Sin auto-orientación, la imagen redimensionada puede aparecer rotada:
from PIL import Image, ImageOps
img = Image.open("phone_photo.jpg")
img = ImageOps.exif_transpose(img) # Aplicar orientación EXIF
img.thumbnail((1200, 1200), Image.Resampling.LANCZOS)
img.save("photo_correct.jpg", "JPEG", quality=85)
- Usa
Image.MAX_IMAGE_PIXELSpara prevenir bombas de descompresión. Pillow tiene un límite integrado para evitar que imágenes maliciosas agoten la memoria. Ajústalo si procesas imágenes grandes legítimas:
from PIL import Image
# El límite por defecto es ~89 millones de píxeles. Súbelo para panoramas/tiled.
Image.MAX_IMAGE_PIXELS = 200_000_000 # 200 megapíxeles
- Guarda JPEGs progresivos para carga web más rápida. Los JPEGs progresivos cargan en pasadas de grueso a fino, dando al usuario una vista previa antes de que la imagen completa cargue:
from PIL import Image
img = Image.open("photo.jpg")
img.save("photo_progressive.jpg", "JPEG", quality=85, optimize=True, progressive=True)
Errores Comunes Adicionales
- No manejar el modo de color CMYK. Algunos JPEGs de flujos de impresión usan modo CMYK. Guardar como WebP o JPEG RGB sin conversión produce colores incorrectos:
from PIL import Image
img = Image.open("print_photo.jpg")
if img.mode == "CMYK":
img = img.convert("RGB") # Convertir CMYK a RGB para web
img.save("web_photo.jpg", "JPEG", quality=85)
- Usar
resize()con expectativas dethumbnail().thumbnail()modifica la imagen in-place y nunca escala hacia arriba.resize()crea una imagen nueva y puede distorsionar. Conoce la diferencia:
from PIL import Image
img = Image.open("photo.jpg")
# thumbnail() preserva aspect ratio, modifica in-place, sin upscaling
img.thumbnail((800, 800))
# img ahora es <= 800x800 con aspect ratio original
img2 = Image.open("photo.jpg")
# resize() crea imagen nueva, puede distorsionar, puede escalar
resized = img2.resize((800, 800)) # Exactamente 800x800, puede distorsionar
- Ignorar perfiles de color ICC. Las imágenes con perfiles ICC embebidos pueden verse diferentes después del procesamiento. Preserva o convierte el perfil:
from PIL import Image, ImageCms
img = Image.open("photo_with_icc.jpg")
icc = img.info.get("icc_profile")
# Convertir a sRGB para visualización web consistente
if icc:
img = ImageCms.profileToProfile(
img,
ImageCms.ImageCmsProfile(ImageCms.getOpenProfileFromString(icc)),
ImageCms.ImageCmsProfile("sRGB"),
outputMode="RGB",
)
img.save("photo_srgb.jpg", "JPEG", quality=85) Preguntas frecuentes
¿Cómo redimensiono imágenes sin Pillow?
Puedes usar opencv-python (cv2.resize()) o imageio con skimage.transform.resize. Pillow es la opción más común porque es ligero y está bien documentado.
¿Cómo proceso imágenes en lote en paralelo?
Usa concurrent.futures.ProcessPoolExecutor para procesar imágenes entre núcleos de CPU:
from concurrent.futures import ProcessPoolExecutor
def process_one(img_path):
img = Image.open(img_path)
img.thumbnail((800, 800), Image.Resampling.LANCZOS)
img.save(f"out/{img_path.name}")
with ProcessPoolExecutor() as pool:
pool.map(process_one, list(Path("images").glob("*.jpg")))
¿Cómo elimino datos EXIF por privacidad?
Pasa exif=b"" al guardar, o usa img.info.pop("exif", None) antes de guardar. Esto remueve coordenadas GPS y metadatos de cámara.
¿Cuál es la diferencia entre quality y method en WebP?
quality controla la fidelidad visual (0-100). method controla el esfuerzo de compresión (0-6). method más alto significa encoding más lento pero archivos más pequeños. Usa quality=85, method=6 para mejores resultados.
Recursos Relacionados
Comprimir y Descomprimir Archivos
Cómo manejar archivos ZIP, GZIP y TAR programáticamente.
RecipeCopiar y Mover Archivos
Cómo copiar y mover archivos de forma segura y eficiente entre plataformas.
RecipeGenerar Archivos Temporales
Cómo crear archivos y directorios temporales de forma segura con limpieza automática en Python, Node.js, Java y Bash.
RecipeOptimización de Imágenes
Cómo redimensionar, comprimir y optimizar imágenes para rendimiento web.
RecipeLeer Archivos Grandes
Cómo leer archivos grandes de forma eficiente sin agotar la memoria.