StackPractices
beginner Por Mathias Paulenko

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

EnfoqueMétodoAspect RatioUsar Cuando
resize()Tamaño exactoDistorsionadoDimensiones fijas requeridas
thumbnail()Caber en cajaPreservadoThumbnails de galería
Función ratio customMax ancho/altoPreservadoLayouts flexibles
Conversión WebPCambio de formatoPreservadoOptimización web

Pautas

  • Usa LANCZOS para 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=85 para 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. Usa thumbnail() 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 NEAREST resampling para fotos. Se ve pixelado. Siempre usa LANCZOS o BICUBIC.
  • No activar optimize=True para 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

  1. 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)
  1. Usa Image.MAX_IMAGE_PIXELS para 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
  1. 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

  1. 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)
  1. Usar resize() con expectativas de thumbnail(). 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
  1. 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.