Postprocesamiento de imágenes generadas por IA: recorte y corrección de color con Python para retratos Hanfu

Cada vez que genero retratos Hanfu con IA, como el personaje "Shuang’er", lo más frustrante no suele ser la generación en sí. Modelos como Z-Turbo producen imágenes con expresiones faciales y detalles de vestimenta que ya teinen mucho encanto, pero al usarlas directamente, algo falta. El fondo puede ser demasiado caótico, los tonos demasiado oscuros o la composición no resalta al sujeto, y en redes sociales el resultaod pierde impacto.

Ahí es donde el postprocesamiento se convierte en el paso clave para pasar de "aceptable" a "excelente". Hacerlo manualmente con software de retoque, imagen por imagen, es ineficiente. Hoy comparto un flujo automatizado con Python basado en mi experiencia procesando grandes volúmenes de imágenes generadas por IA. Este método permite recortar inteligentemente al sujeto, corregir colores, agregar elementos de estilo antiguo chino e incluso crear collages por lotes, transformando cada retrato Hanfu de "Shuang’er" en una obra de alta calidad.

1. ¿Por qué las imágenes Hanfu generadas por IA necesitan postprocesamiento?

Quizás pienses: si la IA ya las genera, ¿por qué molestarse en procesarlas? La razón está en cómo funciona el modelo. Su objetivo es porducir una imagen visualmente coherente que coincida con la descripción textual, pero no entiende tu "uso final".

Por ejemplo, para una portada de Xiaohongshu o TikTok necesitas un recorte vertical con el sujeto destacado y colores vivos. Pero la salida original puede ser un cuadrado con fondo complejo donde la persona ocupa solo una parte. Además, un lote de imágenes generadas puede tener tonos inconsistentes, lo que se nota al hacer un collage. Estos problemas de "última milla" son los que resuelve el postprocesamiento.

Con scripts simples en Python podemos automatizar estas optimizaciones por lotes, logrando que el contenido generado por IA tenga calidad lista para publicar. A continuación, empezamos con la preparación del entorno.

2. Configuración rápida del entorno de procesamiento de imágenes

Para el procesamiento usaremos dos bibliotecas principales: PIL/Pillow y OpenCV. La primera es más ligera y amigable para recortes, ajustes de color y composiciones básicas; la segunda es potente para detección inteligente (como reconocimiento facial). Recomiendo instalar ambas y usarlas según el caso.

2.1 Instalación de bibliotecas necesarias

Abre tu terminal o símbolo del sistema y ejecuta:

pip install Pillow opencv-python numpy

  • Pillow: rama amigable de PIL (Python Imaging Library), nuestro caballo de batalla para procesamiento de imágenes.
  • opencv-python: biblioteca de visión artificial, la usaremos para detectar personas inteligentemente.
  • numpy: base para cálculos científicos, OpenCV depende de ella para manejar datos de imagen.

Verifica la instalación importando en Python:

from PIL import Image
import cv2
import numpy as np
print("¡Todas las bibliotecas listas!")

2.2 Prepara tus imágenes originales de "Shuang’er"

Coloca las imágenes generadas por Z-Turbo en una carpeta separada, por ejemplo ./raw_images/. El objetivo es leer todas las imágenes de esa carpeta, procesarlas y guardarlas en otra, por ejemplo ./processed_images/.

3. Técnicas centrales de postprocesamiento detalladas

A continuación, desglosamos cada paso en funciones independientes que puedes combinar como bloques de construcción.

3.1 Recorte inteligente: haz que "Shuang’er" sea la protagonista absoluta

Las imágenes originales pueden tener fondos distractores. El recorte inteligente busca automáticamente al sujeto y produce una composición atractiva, especialmente en formato vertical para móviles.

Usamos un método simple pero efectivo: detectar rostros con el clasificador preentrenado de OpenCV y recortar alrededor de la región detectada.

import cv2
import numpy as np
from PIL import Image

def recorte_retrato_inteligente(ruta_entrada, ruta_salida, relacion_objetivo=9/16):
    """
    Recorta inteligentemente un retrato en orientación vertical (ej. 9:16).
    :param ruta_entrada: ruta de la imagen de entrada
    :param ruta_salida: ruta de la imagen de salida
    :param relacion_objetivo: relación alto/ancho objetivo (alto/ancho)
    """
    img_cv = cv2.imread(ruta_entrada)
    if img_cv is None:
        print(f"No se pudo leer la imagen: {ruta_entrada}")
        return
    
    alto, ancho = img_cv.shape[:2]
    
    # Clasificador Haar para detección de rostros (frontal)
    clasificador_rostros = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
    gris = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
    rostros = clasificador_rostros.detectMultiScale(gris, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))
    
    if len(rostros) > 0:
        # Tomar el rostro más grande
        rostro_mayor = max(rostros, key=lambda rect: rect[2] * rect[3])
        x, y, w, h = rostro_mayor
        
        centro_x, centro_y = x + w//2, y + h//2
        
        # Altura deseada del recorte según la relación objetivo
        alto_recorte = int(ancho * relacion_objetivo)
        if alto_recorte > alto:
            alto_recorte = alto
            ancho_recorte = int(alto / relacion_objetivo)
        else:
            ancho_recorte = ancho
        
        inicio_x = max(0, centro_x - ancho_recorte // 2)
        inicio_y = max(0, centro_y - alto_recorte // 2)
        
        # Ajustar para no salir de los bordes
        if inicio_x + ancho_recorte > ancho:
            inicio_x = ancho - ancho_recorte
        if inicio_y + alto_recorte > alto:
            inicio_y = alto - alto_recorte
        inicio_x, inicio_y = max(0, inicio_x), max(0, inicio_y)
        
        img_recortada = img_cv[inicio_y:inicio_y+alto_recorte, inicio_x:inicio_x+ancho_recorte]
    else:
        print(f"No se detectó rostro en {ruta_entrada}, usando recorte central conservador")
        if ancho / alto > relacion_objetivo:  # imagen más ancha
            nuevo_ancho = int(alto * relacion_objetivo)
            inicio_x = (ancho - nuevo_ancho) // 2
            img_recortada = img_cv[:, inicio_x:inicio_x+nuevo_ancho]
        else:
            nuevo_alto = int(ancho / relacion_objetivo)
            inicio_y = (alto - nuevo_alto) // 2
            img_recortada = img_cv[inicio_y:inicio_y+nuevo_alto, :]
    
    cv2.imwrite(ruta_salida, img_recortada)
    print(f"Recorte guardado en: {ruta_salida}")

# Ejemplo de uso
recorte_retrato_inteligente("./raw_images/shuanger_01.png", "./processed_images/shuanger_01_recortado.png")

Esta función intenta centrar el rostro de "Shuang’er" y producir una composición vertical adecuada para redes sociales.

3.2 Corrección automática de color: unifica y realza la atmósfera visual

Las imágenes generadas por IA a veces tienen tonos grises o contraste insuficiente. Ajustamos brillo, contraste y saturación para que los colores del Hanfu sean más vivos y la piel más luminosa. Usamos una estrategia robusta.

from PIL import Image, ImageEnhance
import numpy as np

def correccion_color_automatica(ruta_entrada, ruta_salida, factor_brillo=1.05, factor_contraste=1.1, factor_saturacion=1.15):
    """
    Corrección automática de color: ajusta brillo, contraste y saturación.
    Factor >1 aumenta, <1 disminuye.
    """
    img = Image.open(ruta_entrada).convert('RGB')
    
    # 1. Brillo
    mejorador = ImageEnhance.Brightness(img)
    img = mejorador.enhance(factor_brillo)
    
    # 2. Contraste
    mejorador = ImageEnhance.Contrast(img)
    img = mejorador.enhance(factor_contraste)
    
    # 3. Saturación
    mejorador = ImageEnhance.Color(img)
    img = mejorador.enhance(factor_saturacion)
    
    # (Opcional) Nitidez ligera para detalles de vestimenta
    mejorador = ImageEnhance.Sharpness(img)
    img = mejorador.enhance(1.1)
    
    img.save(ruta_salida)
    print(f"Corrección de color guardada en: {ruta_salida}")

# Ejemplo de uso
correccion_color_automatica("./processed_images/shuanger_01_recortado.png", "./processed_images/shuanger_01_color.png")

Puedes ajustar los factores según el estilo deseado. Para un toque más suave y antiguo, reduce contraste y saturación.

3.3 Agregar marco y marca de agua de estilo antiguo: realza la integridad de la obra

Añadir un marco simple de color pergamino y una marca de agua discreta eleva la calidad percibida.

from PIL import Image, ImageDraw, ImageFont

def agregar_marco_marca_agua(ruta_entrada, ruta_salida, color_marco=(220, 208, 192), ancho_marco=20, texto_marca="@Shuang'er"):
    """
    Añade un marco de color pergamino y una marca de agua de texto simple.
    """
    img = Image.open(ruta_entrada).convert('RGB')
    ancho, alto = img.size
    
    # 1. Crear un lienzo más grande para el marco
    nuevo_ancho = ancho + 2*ancho_marco
    nuevo_alto = alto + 2*ancho_marco
    img_con_marco = Image.new('RGB', (nuevo_ancho, nuevo_alto), color=color_marco)
    img_con_marco.paste(img, (ancho_marco, ancho_marco))
    
    # 2. Agregar marca de agua (texto simple)
    dibujar = ImageDraw.Draw(img_con_marco)
    try:
        fuente = ImageFont.truetype("simhei.ttf", 30)  # Fuente china
    except IOError:
        fuente = ImageFont.load_default()
    
    # Posición en la esquina inferior derecha
    caja_texto = dibujar.textbbox((0,0), texto_marca, font=fuente)
    ancho_texto = caja_texto[2] - caja_texto[0]
    alto_texto = caja_texto[3] - caja_texto[1]
    pos_x = nuevo_ancho - ancho_texto - 20
    pos_y = nuevo_alto - alto_texto - 20
    dibujar.text((pos_x, pos_y), texto_marca, fill=(150, 150, 150, 128), font=fuente)
    
    img_con_marco.save(ruta_salida)
    print(f"Imagen con marco y marca guardada en: {ruta_salida}")

# Ejemplo de uso
agregar_marco_marca_agua("./processed_images/shuanger_01_color.png", "./processed_images/shuanger_01_final.png")

El marco imita el tono claro del pergamino de pinturas antiguas, y la marca de agua se coloca discretamente en la esquina.

3.4 Procesamiento por lotes y creación de collage

Con las funciones por imagen listas, el procesamiento por lotes es sencillo. También podemos combinar varias imágenes finales en un collage tipo póster.

import os
from PIL import Image

def procesar_lote(carpeta_entrada="./raw_images", carpeta_salida="./processed_images"):
    """Procesa todas las imágenes de una carpeta aplicando la secuencia completa."""
    if not os.path.exists(carpeta_salida):
        os.makedirs(carpeta_salida)
    
    formatos_soportados = ('.png', '.jpg', '.jpeg', '.bmp')
    
    for nombre_archivo in os.listdir(carpeta_entrada):
        if nombre_archivo.lower().endswith(formatos_soportados):
            base = os.path.splitext(nombre_archivo)[0]
            ruta_original = os.path.join(carpeta_entrada, nombre_archivo)
            
            ruta_recorte = os.path.join(carpeta_salida, f"{base}_recortado.png")
            ruta_color = os.path.join(carpeta_salida, f"{base}_color.png")
            ruta_final = os.path.join(carpeta_salida, f"{base}_final.png")
            
            recorte_retrato_inteligente(ruta_original, ruta_recorte)
            correccion_color_automatica(ruta_recorte, ruta_color)
            agregar_marco_marca_agua(ruta_color, ruta_final)
            
            # Opcional: eliminar archivos intermedios
            # os.remove(ruta_recorte)
            # os.remove(ruta_color)
    
    print("¡Procesamiento por lotes completado!")

def crear_collage(lista_rutas, ruta_salida, tipo="vertical", imagenes_por_fila=2):
    """
    Combina varias imágenes en un póster.
    :param tipo: 'vertical' (tira vertical) o 'grid' (cuadrícula)
    """
    imagenes = [Image.open(r).convert('RGB') for r in lista_rutas]
    if not imagenes:
        return
    
    if tipo == "vertical":
        anchos, altos = zip(*(i.size for i in imagenes))
        alto_total = sum(altos)
        ancho_max = max(anchos)
        
        collage = Image.new('RGB', (ancho_max, alto_total))
        desplazamiento_y = 0
        for img in imagenes:
            collage.paste(img, ((ancho_max - img.size[0]) // 2, desplazamiento_y))
            desplazamiento_y += img.size[1]
    else:  # grid
        num = len(imagenes)
        filas = (num + imagenes_por_fila - 1) // imagenes_por_fila
        
        anchos, altos = zip(*(i.size for i in imagenes))
        ancho_celda = max(anchos)
        alto_celda = max(altos)
        
        collage = Image.new('RGB', (ancho_celda * imagenes_por_fila, alto_celda * filas))
        
        for indice, img in enumerate(imagenes):
            x = (indice % imagenes_por_fila) * ancho_celda + (ancho_celda - img.size[0]) // 2
            y = (indice // imagenes_por_fila) * alto_celda + (alto_celda - img.size[1]) // 2
            collage.paste(img, (x, y))
    
    collage.save(ruta_salida)
    print(f"Collage guardado en: {ruta_salida}")

# Ejemplos de uso
# 1. Procesar lote
procesar_lote()

# 2. Crear collage vertical con las primeras tres imágenes finales
archivos_procesados = ["./processed_images/shuanger_01_final.png",
                       "./processed_images/shuanger_02_final.png",
                       "./processed_images/shuanger_03_final.png"]
crear_collage(archivos_procesados, "./processed_images/collage_vertical.png", tipo="vertical")

4. Conclusión

Al seguir este flujo, las imágenes originales generadas por IA se transforman en obras de alta calidad listas para publicación, gracias al recorte inteligente, corrección de color y decoración. Todo el proceso está automatizado con scripts Python que puedes reutilizar para todas tus futuras imágenes de "Shuang’er" u otros retratos Hanfu.

El postprocesamiento cierra la brecha entre la "generación" de la IA y la "aplicación" humana. No requiere algoritmos avanzados, sino una reflexión sobre el resultado final y la combinación de técnicas prácticas. Combina libremente las funciones presentadas: solo recorte y color, o solo collage por lotes. Experimenta y encontrarás los parámetros ideales para tu estilo.

Espero que este conjunto de técnicas te ayude a realzar las bellezas Hanfu generadas por Z-Turbo y otras herramientas.

Etiquetas: Python PIL Pillow opencv procesamiento de imágenes

Publicado el 7-5 21:02