deepdream / deepdream_engine.py
gaxoblanco's picture
project
f7c780b verified
Raw
History Blame Contribute Delete
12.5 kB
"""
================================================================
DEEPDREAM_ENGINE.PY — Motor de DeepDream (Lógica Pura)
================================================================
Implementacion fiel al notebook deepdream_tutorial_universal.ipynb.
Diferencias clave respecto a implementaciones comunes:
- La imagen se mantiene en rango [0, 1] SIN normalizacion ImageNet.
El notebook usa solo ToTensor() — normalizar con mean/std distorsiona
los gradientes y produce resultados incorrectos.
- La piramide usa tensores y F.interpolate (no PIL resize en numpy).
- El blur se aplica cada 4 iteraciones dentro del loop (no post-octava).
- El detalle entre octavas se acumula en tensor y se suma a img_base.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision.transforms import GaussianBlur
import torchvision.models as models
import torchvision.transforms as transforms
import numpy as np
from PIL import Image, ImageOps
# ----------------------------------------------------------------
# CONSTANTES
# ----------------------------------------------------------------
# Minimo de pixeles en la piramide (evita RuntimeError de kernel)
MIN_SIZE = 75
TAMAÑOS_MODELO = {
"inception": (299, 299),
"alexnet": (512, 512),
}
CAPAS_POR_MODELO = {
"inception": {
"Mixed_5b": "Bordes, colores y formas geometricas simples",
"Mixed_5c": "Texturas y patrones repetitivos",
"Mixed_5d": "Formas basicas y contornos",
"Mixed_6a": "Estructuras intermedias y gradientes",
"Mixed_6b": "Figuras complejas en formacion",
"Mixed_6c": "Patrones tipo ojo y espiral",
"Mixed_6d": "Estructuras tipo animal o planta",
"Mixed_6e": "Objetos reconocibles (recomendada)",
"Mixed_7a": "Faces, animales, conceptos abstractos",
"Mixed_7b": "Alta abstraccion — muy dreamlike",
"Mixed_7c": "Maxima abstraccion de InceptionV3",
},
"alexnet": {
"features.0": "Conv1 — Bordes y colores crudos",
"features.3": "Conv2 — Texturas simples",
"features.6": "Conv3 — Patrones y mosaicos",
"features.8": "Conv4 — Formas complejas",
"features.10": "Conv5 — Objetos de alto nivel (recomendada)",
},
}
# Parametros del notebook original.
# lr por modelo: Inception=0.04, AlexNet=0.03
# Las iteraciones determinan la intensidad del efecto.
INTENSIDADES = {
"Micro": {"iterations": 3}, # apenas visible — patrones sutiles
"Suave": {"iterations": 10},
"Normal": {"iterations": 20},
"Intenso": {"iterations": 30},
"Extremo": {"iterations": 50},
}
# ================================================================
# CLASE: CapturaActivaciones
# ================================================================
class CapturaActivaciones:
"""
Hook de PyTorch para capturar activaciones de una capa.
Implementacion identica al notebook: busca la capa por nombre
usando modelo.named_modules() y registra el hook directamente.
Uso:
capturador = CapturaActivaciones(modelo, "Mixed_6e")
modelo(img)
acts = capturador.obtener_activaciones()
capturador.remover_hook()
"""
def __init__(self, modelo: nn.Module, nombre_capa: str):
self.activaciones = None
capa_encontrada = False
for nombre, modulo in modelo.named_modules():
if nombre == nombre_capa:
self.hook = modulo.register_forward_hook(self._hook_fn)
capa_encontrada = True
break
if not capa_encontrada:
raise ValueError(
f"Capa '{nombre_capa}' no encontrada en el modelo")
def _hook_fn(self, modulo, input, output):
self.activaciones = output
def obtener_activaciones(self):
return self.activaciones
def remover_hook(self):
self.hook.remove()
# ================================================================
# FUNCIONES: modelo y preprocesamiento
# ================================================================
def cargar_modelo_interno(nombre: str) -> nn.Module:
"""
Carga modelo pre-entrenado en eval() con parametros congelados.
aux_logits=False: desactiva la cabeza auxiliar de InceptionV3.
En DeepDream no queremos esa salida adicional — genera gradientes
que interfieren con el efecto.
"""
if nombre == "inception":
modelo = models.inception_v3(
weights=models.Inception_V3_Weights.DEFAULT)
modelo.aux_logits = False
elif nombre == "alexnet":
modelo = models.alexnet(weights=models.AlexNet_Weights.DEFAULT)
else:
raise ValueError(
f"Modelo '{nombre}' no soportado. Usar 'inception' o 'alexnet'.")
modelo.eval()
for param in modelo.parameters():
param.requires_grad_(False)
return modelo
def preprocesar_imagen(pil_img: Image.Image, tamanio: tuple) -> torch.Tensor:
"""
PIL Image -> tensor [1, 3, H, W] en rango [0, 1].
IMPORTANTE: NO aplicamos normalizacion ImageNet (sin mean/std).
El notebook usa solo ToTensor() que convierte [0,255] -> [0,1].
Normalizar con ImageNet distorsiona los gradientes en DeepDream
porque el modelo espera esa distribucion en inferencia normal,
pero en DeepDream estamos optimizando la imagen directamente.
Aplica correccion EXIF para fotos de iPhone/Android.
"""
pil_img = ImageOps.exif_transpose(pil_img)
pil_img = pil_img.convert("RGB")
# Respetar proporción original — solo limitar el lado más largo
max_lado = max(tamanio)
w, h = pil_img.size
if w >= h:
nuevo_w = max_lado
nuevo_h = int(h * max_lado / w)
else:
nuevo_h = max_lado
nuevo_w = int(w * max_lado / h)
pil_img = pil_img.resize((nuevo_w, nuevo_h), Image.LANCZOS)
transform = transforms.Compose([transforms.ToTensor()])
return transform(pil_img).unsqueeze(0)
def numpy_a_pil(img_numpy: np.ndarray) -> Image.Image:
"""numpy float32 [H,W,3] [0,1] -> PIL Image RGB."""
img_uint8 = np.clip(img_numpy * 255, 0, 255).astype(np.uint8)
return Image.fromarray(img_uint8)
def tensor_a_pil(img_tensor: torch.Tensor) -> Image.Image:
"""tensor [1,3,H,W] o [3,H,W] en [0,1] -> PIL Image RGB."""
if img_tensor.ndim == 4:
img_tensor = img_tensor.squeeze(0)
img_np = img_tensor.detach().cpu().clamp(0, 1).numpy()
img_np = np.transpose(img_np, (1, 2, 0))
return Image.fromarray((img_np * 255).astype(np.uint8))
# ================================================================
# FUNCION PRINCIPAL: deepdream_universal
# ================================================================
def deepdream_universal(
img_tensor: torch.Tensor,
modelo: nn.Module,
nombre_capa: str,
config: dict,
device: torch.device,
callback=None,
) -> Image.Image:
"""
DeepDream con piramide de octavas — implementacion fiel al notebook.
ALGORITMO (igual que el notebook):
1. Calcular lista de tamanios decrecientes (piramide).
Empezar desde el tamanio original y dividir por scale_factor.
Invertir la lista: procesar de pequeno a grande.
2. Por cada octava (tamanio):
a. Escalar img_tensor al tamanio de esta octava (F.interpolate)
b. Si hay detalle acumulado de la octava anterior, sumarlo
c. Loop de gradient ascent:
- Jitter aleatorio (torch.roll)
- Forward pass -> capturar activaciones via hook
- loss = norma de activaciones
- Backward -> gradiente respecto a la imagen
- Normalizar gradiente por std
- img = img + lr * grad (ASCENSO, no descenso)
- Deshacer jitter
- Cada 4 iteraciones: blur gaussiano suave
- Clamp a [0, 1]
d. detail = img - img_base (lo que agrego DeepDream)
3. Retornar la imagen final como PIL.
Args:
img_tensor: tensor [1, 3, H, W] en [0,1] — salida de preprocesar_imagen
modelo: modelo en eval() con parametros congelados
nombre_capa: nombre de la capa a maximizar
config: dict con iterations, num_octavas, scale_factor, lr
device: torch.device
callback: funcion opcional callback(octava_actual, total, pil_img)
Returns:
PIL Image con DeepDream aplicado
"""
iterations = config.get("iterations", 20)
num_octavas = config.get("num_octavas", 4)
scale_factor = config.get("scale_factor", 1.3)
lr = config.get("lr", 0.04)
modelo = modelo.to(device)
img_tensor = img_tensor.to(device)
# Hook para capturar activaciones de la capa objetivo
capturador = CapturaActivaciones(modelo, nombre_capa)
# Blur suave para regularizacion (aplicado cada 4 iteraciones)
blur = GaussianBlur(kernel_size=3, sigma=0.5)
try:
# ----------------------------------------------------------
# PIRAMIDE: tamanios decrecientes desde original
# ----------------------------------------------------------
# Igual que el notebook: partir del tamanio original,
# dividir por scale_factor, cortar si alguna dimension < MIN_SIZE.
# Luego invertir para procesar de pequeno a grande.
base_h, base_w = img_tensor.shape[-2:]
tamanios = []
h, w = base_h, base_w
for _ in range(num_octavas):
tamanios.append((h, w))
h = int(h / scale_factor)
w = int(w / scale_factor)
if h < MIN_SIZE or w < MIN_SIZE:
break
tamanios.reverse() # pequeno -> grande
num_octavas_real = len(tamanios)
# ----------------------------------------------------------
# GRADIENT ASCENT POR OCTAVA
# ----------------------------------------------------------
detail = None # acumula el "sueno" entre octavas
for octave_idx, (h, w) in enumerate(tamanios):
# Escalar la imagen original a este tamanio
img_base = F.interpolate(
img_tensor,
size=(h, w),
mode='bilinear',
align_corners=False
)
# Sumar detalle de la octava anterior (si existe)
# El detalle es lo que DeepDream agrego — lo transferimos
# a la siguiente escala para que los patrones sean coherentes
if detail is not None:
detail_resized = F.interpolate(
detail,
size=(h, w),
mode='bilinear',
align_corners=False
)
img_base = img_base + detail_resized
img = img_base.clone()
# Loop de gradient ascent
for i in range(iterations):
# Jitter: desplazamiento aleatorio para reducir artefactos
ox = np.random.randint(-24, 25)
oy = np.random.randint(-24, 25)
img = torch.roll(img, shifts=(ox, oy), dims=(2, 3))
# Requiere gradiente respecto a la imagen
img = img.detach().requires_grad_(True)
# Forward pass: el hook captura activaciones
output = modelo(img)
if isinstance(output, tuple):
output = output[0]
activaciones = capturador.obtener_activaciones()
# Maximizar norma de activaciones = la red "ve mas" en la imagen
loss = activaciones.norm()
loss.backward()
grad = img.grad.data
# Normalizar por std -> learning rate efectivo estable
grad = grad / (grad.std() + 1e-8)
# ASCENSO: sumar gradiente (en entrenamiento se resta)
img = img.data + lr * grad
# Deshacer jitter
img = torch.roll(img, shifts=(-ox, -oy), dims=(2, 3))
# Blur suave cada 4 iteraciones (regularizacion)
if (i + 1) % 4 == 0:
img = blur(img)
img = torch.clamp(img, 0, 1)
# Guardar el detalle que agrego DeepDream en esta octava
detail = img.detach() - img_base.detach()
# Callback para mostrar progreso en la UI
if callback is not None:
callback(octave_idx + 1, num_octavas_real, tensor_a_pil(img))
return tensor_a_pil(img)
finally:
capturador.remover_hook()