Spaces:
Sleeping
Sleeping
| """ | |
| ================================================================ | |
| 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() | |