Spaces:
Sleeping
Sleeping
| """ | |
| 🌤️ Sky Replacer | |
| Substitui o céu em imagens com opções profissionais | |
| Autor: Daniel251 | |
| Versão: 2.0.0 | |
| Data: 2026 | |
| """ | |
| import gradio as gr | |
| from PIL import Image, ImageDraw, ImageFilter, ImageEnhance | |
| import numpy as np | |
| import cv2 | |
| import tempfile | |
| import time | |
| import os | |
| import sys | |
| import math | |
| import random | |
| import colorsys | |
| from io import BytesIO | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| print("=" * 60) | |
| print("🚀 INICIANDO APP 10: SKY REPLACER") | |
| print(f"Python version: {sys.version}") | |
| print(f"Pillow version: {Image.__version__}") | |
| print("=" * 60) | |
| # ========== CONFIGURAÇÃO ========== | |
| MAX_IMAGE_SIZE = 1024 | |
| SKY_LIBRARY = { | |
| "sunny_day": "☀️ Dia Ensolarado", | |
| "blue_sky": "🔵 Céu Azul Limpo", | |
| "cloudy": "☁️ Céu Nublado", | |
| "stormy": "⛈️ Tempestade", | |
| "sunset": "🌅 Pôr do Sol", | |
| "sunrise": "🌇 Nascer do Sol", | |
| "golden_hour": "✨ Hora Dourada", | |
| "night_sky": "🌙 Noite Estrelada", | |
| "northern_lights": "🌈 Aurora Boreal", | |
| "space": "🚀 Espaço Sideral", | |
| "foggy": "🌫️ Nebuloso", | |
| "rainbow": "🌈 Arco-Íris", | |
| "dramatic": "🎭 Dramático", | |
| "pastel": "🎨 Pastel", | |
| "gradient": "🔄 Gradiente" | |
| } | |
| print(f"📏 Tamanho máximo: {MAX_IMAGE_SIZE}px") | |
| print(f"🌤️ Céus disponíveis: {len(SKY_LIBRARY)}") | |
| # ========== FUNÇÕES AUXILIARES ========== | |
| def log_message(message): | |
| """Log para debug""" | |
| timestamp = time.strftime("%H:%M:%S") | |
| print(f"[{timestamp}] {message}") | |
| def validate_image(image): | |
| """Valida e prepara imagem""" | |
| try: | |
| if image is None: | |
| return None, "❌ Nenhuma imagem fornecida" | |
| # Converter para PIL Image | |
| if isinstance(image, np.ndarray): | |
| img = Image.fromarray(image) | |
| elif isinstance(image, Image.Image): | |
| img = image | |
| else: | |
| return None, "❌ Tipo de imagem desconhecido" | |
| # Verificar tamanho | |
| if max(img.size) > 4000: | |
| img = resize_image(img, 4000) | |
| log_message(f"Imagem redimensionada para {img.size}") | |
| if min(img.size) < 50: | |
| return None, "❌ Imagem muito pequena (<50px)." | |
| log_message(f"✅ Imagem válida: {img.size}px, {img.mode}") | |
| return img, "ok" | |
| except Exception as e: | |
| error_msg = f"❌ Erro na validação: {str(e)}" | |
| log_message(error_msg) | |
| return None, error_msg | |
| def resize_image(image, max_size): | |
| """Redimensiona mantendo aspect ratio""" | |
| if max(image.size) <= max_size: | |
| return image | |
| ratio = max_size / max(image.size) | |
| new_width = int(image.width * ratio) | |
| new_height = int(image.height * ratio) | |
| log_message(f"Redimensionando: {image.size} -> ({new_width}, {new_height})") | |
| return image.resize((new_width, new_height), Image.Resampling.LANCZOS) | |
| def detect_sky_region(image): | |
| """Detecta automaticamente a região do céu na imagem""" | |
| try: | |
| # Converter para array numpy | |
| img_array = np.array(image.convert('RGB')) | |
| height, width = img_array.shape[:2] | |
| # Método 1: Baseado em cores (azul/branco) | |
| hsv = cv2.cvtColor(img_array, cv2.COLOR_RGB2HSV) | |
| # Definição de cores de céu | |
| lower_blue1 = np.array([90, 50, 50]) | |
| upper_blue1 = np.array([130, 255, 255]) | |
| lower_blue2 = np.array([100, 40, 40]) | |
| upper_blue2 = np.array([140, 255, 255]) | |
| # Céu nublado (branco/cinza) | |
| lower_white = np.array([0, 0, 180]) | |
| upper_white = np.array([180, 50, 255]) | |
| # Criar máscaras | |
| mask_blue1 = cv2.inRange(hsv, lower_blue1, upper_blue1) | |
| mask_blue2 = cv2.inRange(hsv, lower_blue2, upper_blue2) | |
| mask_white = cv2.inRange(hsv, lower_white, upper_white) | |
| # Combinar máscaras | |
| sky_mask = cv2.bitwise_or(mask_blue1, mask_blue2) | |
| sky_mask = cv2.bitwise_or(sky_mask, mask_white) | |
| # Método 2: Baseado em posição (céu geralmente está no topo) | |
| position_mask = np.zeros((height, width), dtype=np.uint8) | |
| for y in range(height): | |
| weight = max(0, 1.0 - (y / height) * 1.5) | |
| position_mask[y, :] = int(255 * weight) | |
| # Método 3: Detecção de bordas para encontrar horizonte | |
| gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY) | |
| edges = cv2.Canny(gray, 50, 150) | |
| lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=50, | |
| minLineLength=width//4, maxLineGap=10) | |
| horizon_y = height * 0.3 | |
| if lines is not None: | |
| horizontal_lines = [] | |
| for line in lines: | |
| x1, y1, x2, y2 = line[0] | |
| angle = abs(np.arctan2(y2-y1, x2-x1) * 180 / np.pi) | |
| if abs(angle) < 10 or abs(angle - 180) < 10: | |
| horizontal_lines.append(min(y1, y2)) | |
| if horizontal_lines: | |
| horizon_y = np.median(horizontal_lines) | |
| # Criar máscara final | |
| combined_mask = cv2.addWeighted(sky_mask, 0.6, position_mask, 0.4, 0) | |
| combined_mask[int(horizon_y):, :] = 0 | |
| # Operações morfológicas para limpar | |
| kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5)) | |
| combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, kernel) | |
| combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_OPEN, kernel) | |
| # Preencher buracos e suavizar | |
| combined_mask = cv2.morphologyEx(combined_mask, cv2.MORPH_CLOSE, | |
| cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15,15))) | |
| combined_mask = cv2.GaussianBlur(combined_mask, (11, 11), 0) | |
| # Se a máscara for muito pequena, usar padrão | |
| if np.sum(combined_mask > 127) < 100: | |
| log_message("⚠️ Céu não detectado automaticamente, usando método padrão") | |
| combined_mask = np.zeros((height, width), dtype=np.uint8) | |
| combined_mask[:int(height*0.4), :] = 255 | |
| sky_percentage = np.sum(combined_mask > 127) / (width * height) * 100 | |
| log_message(f"✅ Céu detectado: {sky_percentage:.1f}% da imagem") | |
| return combined_mask | |
| except Exception as e: | |
| log_message(f"❌ Erro na detecção de céu: {str(e)}") | |
| height, width = image.size[1], image.size[0] | |
| mask = np.zeros((height, width), dtype=np.uint8) | |
| mask[:int(height*0.4), :] = 255 | |
| return mask | |
| # ========== GERADORES DE CÉU ========== | |
| def create_sunny_sky(size): | |
| """Cria céu de dia ensolarado""" | |
| width, height = size | |
| img = Image.new('RGB', size, (135, 206, 235)) | |
| draw = ImageDraw.Draw(img) | |
| # Gradiente de azul | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(135 * (1 - ratio) + 255 * ratio) | |
| g = int(206 * (1 - ratio) + 250 * ratio) | |
| b = int(235 * (1 - ratio) + 255 * ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| # Adicionar sol | |
| sun_size = min(width, height) // 10 | |
| sun_x = width * 0.8 | |
| sun_y = height * 0.2 | |
| # Brilho do sol | |
| for i in range(5, 0, -1): | |
| radius = sun_size * (1 + i * 0.3) | |
| temp_img = Image.new('RGBA', size, (0, 0, 0, 0)) | |
| temp_draw = ImageDraw.Draw(temp_img) | |
| alpha = int(50 / i) | |
| temp_draw.ellipse([sun_x-radius, sun_y-radius, sun_x+radius, sun_y+radius], | |
| fill=(255, 255, 200, alpha)) | |
| img = Image.alpha_composite(img.convert('RGBA'), temp_img).convert('RGB') | |
| # Sol principal | |
| sun_img = Image.new('RGBA', size, (0, 0, 0, 0)) | |
| sun_draw = ImageDraw.Draw(sun_img) | |
| sun_draw.ellipse([sun_x-sun_size, sun_y-sun_size, sun_x+sun_size, sun_y+sun_size], | |
| fill=(255, 255, 100), outline=(255, 200, 50), width=3) | |
| img = Image.alpha_composite(img.convert('RGBA'), sun_img).convert('RGB') | |
| # Adicionar nuvens simples | |
| img = add_simple_clouds(img, count=5) | |
| return img | |
| def create_blue_sky(size): | |
| """Cria céu azul limpo""" | |
| width, height = size | |
| img = Image.new('RGB', size, (0, 0, 0)) | |
| # Gradiente azul profundo | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(30 * ratio) | |
| g = int(144 * (0.7 + 0.3 * ratio)) | |
| b = int(255 * (0.8 + 0.2 * ratio)) | |
| for x in range(width): | |
| variation = math.sin(x * 0.01) * 0.05 + math.cos(y * 0.005) * 0.03 | |
| vr = int(r * (1 + variation)) | |
| vg = int(g * (1 + variation)) | |
| vb = int(b * (1 + variation)) | |
| img.putpixel((x, y), (vr, vg, vb)) | |
| return img | |
| def create_cloudy_sky(size): | |
| """Cria céu nublado""" | |
| width, height = size | |
| img = Image.new('RGB', size, (200, 220, 240)) | |
| # Base de nuvens | |
| for y in range(height): | |
| ratio = y / height | |
| base_r = 200 + int(30 * ratio) | |
| base_g = 220 + int(20 * ratio) | |
| base_b = 240 + int(10 * ratio) | |
| for x in range(width): | |
| noise = (math.sin(x * 0.01) + math.sin(y * 0.007) + | |
| math.sin((x + y) * 0.005)) / 3 | |
| r = max(100, min(255, base_r + int(noise * 40))) | |
| g = max(120, min(255, base_g + int(noise * 30))) | |
| b = max(140, min(255, base_b + int(noise * 20))) | |
| img.putpixel((x, y), (r, g, b)) | |
| # Adicionar mais nuvens | |
| img = add_simple_clouds(img, count=8, cloud_type="cumulus") | |
| return img | |
| def create_stormy_sky(size): | |
| """Cria céu de tempestade""" | |
| width, height = size | |
| img = Image.new('RGB', size, (50, 50, 70)) | |
| draw = ImageDraw.Draw(img) | |
| # Gradiente dramático | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(30 + 70 * ratio) | |
| g = int(30 + 60 * ratio) | |
| b = int(50 + 80 * ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| # Nuvens escuras | |
| img_array = np.array(img) | |
| for i in range(5): | |
| cloud_x = random.randint(0, width) | |
| cloud_y = random.randint(0, height // 2) | |
| cloud_size = random.randint(width // 4, width // 2) | |
| # Escurecer área da nuvem | |
| for dy in range(-cloud_size//2, cloud_size//2): | |
| for dx in range(-cloud_size//2, cloud_size//2): | |
| x = cloud_x + dx | |
| y = cloud_y + dy | |
| if 0 <= x < width and 0 <= y < height: | |
| dist = math.sqrt(dx*dx + dy*dy) | |
| if dist < cloud_size // 2: | |
| darkness = 1.0 - (dist / (cloud_size // 2)) * 0.7 | |
| img_array[y, x] = (img_array[y, x] * darkness).astype(np.uint8) | |
| img = Image.fromarray(img_array) | |
| return img | |
| def create_sunset_sky(size): | |
| """Cria céu de pôr do sol""" | |
| width, height = size | |
| img = Image.new('RGB', size, (0, 0, 0)) | |
| # Gradiente de pôr do sol | |
| for y in range(height): | |
| ratio = y / height | |
| if ratio < 0.3: | |
| r = 255 | |
| g = int(100 + 100 * (ratio / 0.3)) | |
| b = int(50 * (ratio / 0.3)) | |
| elif ratio < 0.6: | |
| sub_ratio = (ratio - 0.3) / 0.3 | |
| r = int(255 - 100 * sub_ratio) | |
| g = int(200 - 80 * sub_ratio) | |
| b = int(100 + 80 * sub_ratio) | |
| else: | |
| sub_ratio = (ratio - 0.6) / 0.4 | |
| r = int(155 - 100 * sub_ratio) | |
| g = int(120 - 70 * sub_ratio) | |
| b = int(180 + 75 * sub_ratio) | |
| for x in range(width): | |
| h_variation = math.sin(x * 0.005 + y * 0.002) * 0.1 | |
| vr = max(0, min(255, int(r * (1 + h_variation)))) | |
| vg = max(0, min(255, int(g * (1 + h_variation)))) | |
| vb = max(0, min(255, int(b * (1 + h_variation)))) | |
| img.putpixel((x, y), (vr, vg, vb)) | |
| return img | |
| def create_sunrise_sky(size): | |
| """Cria céu de nascer do sol""" | |
| width, height = size | |
| img = Image.new('RGB', size, (0, 0, 0)) | |
| draw = ImageDraw.Draw(img) | |
| for y in range(height): | |
| ratio = y / height | |
| if ratio < 0.4: | |
| r = 255 | |
| g = int(180 + 50 * (ratio / 0.4)) | |
| b = int(100 + 100 * (ratio / 0.4)) | |
| elif ratio < 0.7: | |
| sub_ratio = (ratio - 0.4) / 0.3 | |
| r = int(255 - 80 * sub_ratio) | |
| g = int(230 - 60 * sub_ratio) | |
| b = int(200 + 30 * sub_ratio) | |
| else: | |
| sub_ratio = (ratio - 0.7) / 0.3 | |
| r = int(175 - 100 * sub_ratio) | |
| g = int(170 - 70 * sub_ratio) | |
| b = int(230 + 25 * sub_ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| return img | |
| def create_golden_hour_sky(size): | |
| """Cria céu da hora dourada""" | |
| width, height = size | |
| img = Image.new('RGB', size, (0, 0, 0)) | |
| draw = ImageDraw.Draw(img) | |
| for y in range(height): | |
| ratio = y / height | |
| if ratio < 0.5: | |
| r = 255 | |
| g = int(200 + 55 * (ratio / 0.5)) | |
| b = int(100 + 100 * (ratio / 0.5)) | |
| else: | |
| sub_ratio = (ratio - 0.5) / 0.5 | |
| r = 255 | |
| g = int(255 - 100 * sub_ratio) | |
| b = int(200 - 150 * sub_ratio) | |
| brightness = 1.0 + 0.3 * math.sin(ratio * math.pi) | |
| r = min(255, int(r * brightness)) | |
| g = min(255, int(g * brightness)) | |
| b = min(255, int(b * brightness)) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| return img | |
| def create_night_sky(size): | |
| """Cria céu noturno estrelado""" | |
| width, height = size | |
| img = Image.new('RGB', size, (10, 10, 30)) | |
| draw = ImageDraw.Draw(img) | |
| # Gradiente do horizonte | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(10 + 20 * ratio) | |
| g = int(10 + 30 * ratio) | |
| b = int(30 + 50 * ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| # Estrelas | |
| for _ in range(200): | |
| x = random.randint(0, width) | |
| y = random.randint(0, height//1.5) | |
| brightness = random.choice([0.3, 0.5, 0.7, 0.9, 1.0]) | |
| size = 1 + int(brightness * 2) | |
| color = (int(255*brightness), int(255*brightness), int(255*brightness)) | |
| draw.ellipse([x-size, y-size, x+size, y+size], fill=color) | |
| return img | |
| def create_northern_lights_sky(size): | |
| """Cria céu com aurora boreal""" | |
| width, height = size | |
| img = Image.new('RGB', size, (10, 15, 40)) | |
| draw = ImageDraw.Draw(img) | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(10 + 15 * ratio) | |
| g = int(15 + 20 * ratio) | |
| b = int(40 + 30 * ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| # Efeito de aurora simplificado | |
| aurora_img = Image.new('RGBA', size, (0, 0, 0, 0)) | |
| aurora_draw = ImageDraw.Draw(aurora_img) | |
| for y in range(height//4, height//2, 5): | |
| for x in range(0, width, 10): | |
| wave = int(20 * math.sin(x * 0.02 + y * 0.01)) | |
| color = random.choice([(0, 255, 150), (100, 0, 255), (0, 200, 255)]) | |
| alpha = random.randint(30, 80) | |
| aurora_draw.ellipse([x-15, y+wave-5, x+15, y+wave+5], | |
| fill=(*color, alpha)) | |
| img = Image.alpha_composite(img.convert('RGBA'), aurora_img).convert('RGB') | |
| return img | |
| def create_space_sky(size): | |
| """Cria céu espacial""" | |
| width, height = size | |
| img = Image.new('RGB', size, (0, 0, 0)) | |
| draw = ImageDraw.Draw(img) | |
| # Estrelas | |
| for _ in range(300): | |
| x = random.randint(0, width) | |
| y = random.randint(0, height) | |
| brightness = random.uniform(0.3, 1.0) | |
| size_star = 1 + int(brightness * 2) | |
| color = (int(255*brightness), int(255*brightness), int(255*brightness)) | |
| draw.ellipse([x-size_star, y-size_star, x+size_star, y+size_star], fill=color) | |
| return img | |
| def create_foggy_sky(size): | |
| """Cria céu nebuloso""" | |
| width, height = size | |
| img = Image.new('RGB', size, (200, 210, 220)) | |
| draw = ImageDraw.Draw(img) | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(220 - 40 * ratio) | |
| g = int(225 - 30 * ratio) | |
| b = int(230 - 20 * ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| return img | |
| def create_rainbow_sky(size): | |
| """Cria céu com arco-íris""" | |
| width, height = size | |
| img = create_sunny_sky(size) | |
| draw = ImageDraw.Draw(img) | |
| # Arco-íris simplificado | |
| rainbow_colors = [(255,0,0), (255,127,0), (255,255,0), (0,255,0), | |
| (0,0,255), (75,0,130), (148,0,211)] | |
| center_x, center_y = width // 2, height * 1.5 | |
| for i, color in enumerate(rainbow_colors): | |
| radius = int(height * 0.8 - i * 10) | |
| for angle in np.linspace(-0.3, 0.3, 50): | |
| x = int(center_x + radius * math.sin(angle * math.pi)) | |
| y = int(center_y + radius * math.cos(angle * math.pi)) | |
| if 0 <= x < width and 0 <= y < height: | |
| draw.point((x, y), fill=color) | |
| return img | |
| def create_dramatic_sky(size): | |
| """Cria céu dramático""" | |
| width, height = size | |
| img = Image.new('RGB', size, (60, 80, 120)) | |
| draw = ImageDraw.Draw(img) | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(80 - 40 * ratio) | |
| g = int(60 + 40 * ratio) | |
| b = int(120 + 60 * ratio) | |
| variation = math.sin(y * 0.01) * 0.1 | |
| r = int(r * (1 + variation)) | |
| g = int(g * (1 + variation)) | |
| b = int(b * (1 + variation)) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| return img | |
| def create_pastel_sky(size): | |
| """Cria céu pastel suave""" | |
| width, height = size | |
| img = Image.new('RGB', size, (0, 0, 0)) | |
| draw = ImageDraw.Draw(img) | |
| pastel_colors = [(255, 200, 220), (220, 230, 255), | |
| (220, 255, 220), (255, 240, 200)] | |
| for y in range(height): | |
| ratio = y / height | |
| color_idx = ratio * (len(pastel_colors) - 1) | |
| idx1 = int(color_idx) | |
| idx2 = min(idx1 + 1, len(pastel_colors) - 1) | |
| blend = color_idx - idx1 | |
| r1, g1, b1 = pastel_colors[idx1] | |
| r2, g2, b2 = pastel_colors[idx2] | |
| r = int(r1 * (1 - blend) + r2 * blend) | |
| g = int(g1 * (1 - blend) + g2 * blend) | |
| b = int(b1 * (1 - blend) + b2 * blend) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| return img | |
| def create_gradient_sky(size): | |
| """Cria céu com gradiente""" | |
| width, height = size | |
| img = Image.new('RGB', size, (0, 0, 0)) | |
| draw = ImageDraw.Draw(img) | |
| # Gradiente azul para roxo | |
| for y in range(height): | |
| ratio = y / height | |
| r = int(100 + 155 * ratio) | |
| g = int(100 + 100 * ratio) | |
| b = int(255 - 100 * ratio) | |
| draw.line([(0, y), (width, y)], fill=(r, g, b)) | |
| return img | |
| def add_simple_clouds(base_image, count=5, cloud_type="cumulus"): | |
| """Adiciona nuvens simples à imagem""" | |
| img = base_image.copy() | |
| width, height = img.size | |
| if img.mode != 'RGBA': | |
| img = img.convert('RGBA') | |
| cloud_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) | |
| cloud_draw = ImageDraw.Draw(cloud_img) | |
| for _ in range(count): | |
| cloud_x = random.randint(0, width) | |
| cloud_y = random.randint(0, height // 2) | |
| cloud_size = random.randint(width // 8, width // 4) | |
| if cloud_type == "cumulus": | |
| # Nuvens redondas | |
| cloud_color = (255, 255, 255, 180) | |
| cloud_draw.ellipse([cloud_x-cloud_size, cloud_y-cloud_size//3, | |
| cloud_x+cloud_size, cloud_y+cloud_size//3], | |
| fill=cloud_color) | |
| else: | |
| # Nuvens alongadas | |
| cloud_color = (240, 240, 250, 150) | |
| for i in range(3): | |
| offset_x = random.randint(-cloud_size//3, cloud_size//3) | |
| offset_y = random.randint(-cloud_size//6, cloud_size//6) | |
| ellipse_size = cloud_size * random.uniform(0.3, 0.6) | |
| cloud_draw.ellipse([cloud_x+offset_x-ellipse_size, | |
| cloud_y+offset_y-ellipse_size//2, | |
| cloud_x+offset_x+ellipse_size, | |
| cloud_y+offset_y+ellipse_size//2], | |
| fill=cloud_color) | |
| # Aplicar suavização | |
| cloud_img = cloud_img.filter(ImageFilter.GaussianBlur(radius=3)) | |
| img = Image.alpha_composite(img, cloud_img) | |
| return img.convert('RGB') | |
| # ========== FUNÇÃO PRINCIPAL ========== | |
| def replace_sky(image, sky_type, intensity=0.8, auto_detect=True): | |
| """Substitui o céu na imagem""" | |
| try: | |
| log_message(f"Substituindo céu - Tipo: {sky_type}, Intensidade: {intensity}") | |
| # Validar imagem | |
| img, msg = validate_image(image) | |
| if img is None: | |
| return None, f"❌ {msg}", None | |
| # Redimensionar se necessário | |
| img = resize_image(img, MAX_IMAGE_SIZE) | |
| width, height = img.size | |
| # Detectar céu | |
| if auto_detect: | |
| sky_mask = detect_sky_region(img) | |
| log_message("✅ Detecção automática concluída") | |
| else: | |
| sky_mask = np.zeros((height, width), dtype=np.uint8) | |
| sky_mask[:int(height*0.4), :] = 255 | |
| log_message("⚠️ Usando máscara padrão") | |
| # Criar novo céu | |
| sky_generators = { | |
| "sunny_day": create_sunny_sky, | |
| "blue_sky": create_blue_sky, | |
| "cloudy": create_cloudy_sky, | |
| "stormy": create_stormy_sky, | |
| "sunset": create_sunset_sky, | |
| "sunrise": create_sunrise_sky, | |
| "golden_hour": create_golden_hour_sky, | |
| "night_sky": create_night_sky, | |
| "northern_lights": create_northern_lights_sky, | |
| "space": create_space_sky, | |
| "foggy": create_foggy_sky, | |
| "rainbow": create_rainbow_sky, | |
| "dramatic": create_dramatic_sky, | |
| "pastel": create_pastel_sky, | |
| "gradient": create_gradient_sky | |
| } | |
| generator = sky_generators.get(sky_type, create_sunny_sky) | |
| new_sky = generator((width, height)) | |
| # Converter máscara | |
| mask_pil = Image.fromarray(sky_mask).convert('L') | |
| mask_pil = mask_pil.filter(ImageFilter.GaussianBlur(radius=5)) | |
| # Ajustar intensidade | |
| if intensity != 1.0: | |
| mask_array = np.array(mask_pil).astype(float) | |
| mask_array = mask_array * intensity | |
| mask_pil = Image.fromarray(np.clip(mask_array, 0, 255).astype(np.uint8)) | |
| # Aplicar substituição | |
| result = img.copy() | |
| result.paste(new_sky, (0, 0), mask_pil) | |
| # Ajustar cores para integração | |
| if intensity > 0.5: | |
| # Suavizar transição | |
| edges = mask_pil.filter(ImageFilter.FIND_EDGES()) | |
| edges = edges.point(lambda x: 255 if x > 30 else 0) | |
| if np.any(np.array(edges) > 0): | |
| blurred_result = result.filter(ImageFilter.GaussianBlur(radius=2)) | |
| transition_mask = edges.filter(ImageFilter.GaussianBlur(radius=3)) | |
| transition_mask = transition_mask.point(lambda x: x // 2) | |
| result = Image.composite(blurred_result, result, transition_mask) | |
| log_message("✅ Céu substituído com sucesso") | |
| # Máscara para visualização | |
| mask_display = Image.fromarray(sky_mask).convert('RGB') | |
| mask_display = mask_display.resize((width//2, height//2)) | |
| status_msg = f"✅ Céu '{SKY_LIBRARY.get(sky_type, sky_type)}' aplicado!" | |
| return result, status_msg, mask_display | |
| except Exception as e: | |
| error_msg = f"❌ Erro ao substituir céu: {str(e)}" | |
| log_message(error_msg) | |
| import traceback | |
| traceback.print_exc() | |
| return None, error_msg, None | |
| def save_image(image, filename="sky_replaced.png"): | |
| """Salva imagem em arquivo temporário""" | |
| try: | |
| if image is None: | |
| return None | |
| temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png") | |
| image.save(temp_file.name, "PNG", optimize=True) | |
| file_size = os.path.getsize(temp_file.name) // 1024 | |
| log_message(f"Imagem salva: {temp_file.name} ({file_size}KB)") | |
| return temp_file.name | |
| except Exception as e: | |
| log_message(f"❌ Erro ao salvar: {str(e)}") | |
| return None | |
| # ========== INTERFACE GRADIO ========== | |
| print("🌤️ Criando interface Sky Replacer...") | |
| custom_css = """ | |
| .gradio-container { | |
| max-width: 1400px !important; | |
| margin: 0 auto !important; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| padding: 25px; | |
| background: linear-gradient(135deg, #36D1DC 0%, #5B86E5 100%); | |
| border-radius: 20px; | |
| color: white; | |
| } | |
| .header h1 { | |
| margin: 0; | |
| font-size: 2.8em; | |
| font-weight: 700; | |
| } | |
| .header p { | |
| margin: 10px 0 0 0; | |
| font-size: 1.2em; | |
| opacity: 0.9; | |
| } | |
| .sky-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | |
| gap: 15px; | |
| margin: 20px 0; | |
| } | |
| .sky-card { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border: 2px solid transparent; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .sky-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 8px 15px rgba(54, 209, 220, 0.3); | |
| border-color: #36D1DC; | |
| } | |
| .sky-card.active { | |
| border-color: #5B86E5; | |
| background: #f0f9ff; | |
| } | |
| .sky-icon { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| } | |
| .sky-name { | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .image-container { | |
| border-radius: 15px; | |
| overflow: hidden; | |
| border: 2px solid #e0e0e0; | |
| } | |
| .status-box { | |
| padding: 15px; | |
| border-radius: 10px; | |
| margin: 15px 0; | |
| font-weight: 500; | |
| } | |
| .status-success { | |
| background: #d4edda; | |
| color: #155724; | |
| border: 1px solid #c3e6cb; | |
| } | |
| .status-error { | |
| background: #f8d7da; | |
| color: #721c24; | |
| border: 1px solid #f5c6cb; | |
| } | |
| .download-btn { | |
| background: linear-gradient(135deg, #36D1DC 0%, #5B86E5 100%); | |
| color: white; | |
| border: none; | |
| padding: 12px 30px; | |
| border-radius: 25px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| width: 100%; | |
| } | |
| .download-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(54, 209, 220, 0.4); | |
| } | |
| .footer { | |
| text-align: center; | |
| margin-top: 30px; | |
| padding: 20px; | |
| color: #666; | |
| font-size: 0.9em; | |
| } | |
| """ | |
| # Interface principal | |
| def create_interface(): | |
| with gr.Blocks(title="🌤️ Sky Replacer - Photoshop AI App 10", | |
| theme=gr.themes.Soft(), | |
| css=custom_css) as demo: | |
| # Header | |
| gr.HTML(f""" | |
| <div class="header"> | |
| <h1>🌤️ Sky Replacer</h1> | |
| <p>App 10 do Photoshop AI Ecosystem - Substituição profissional de céus</p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| # Coluna esquerda - Upload e Controles | |
| with gr.Column(scale=1): | |
| with gr.Group(): | |
| gr.Markdown("### 📤 Upload da Imagem") | |
| image_input = gr.Image( | |
| label="Arraste uma foto com céu", | |
| type="pil", | |
| height=300, | |
| elem_classes="image-container" | |
| ) | |
| with gr.Group(): | |
| gr.Markdown("### ⚙️ Configurações") | |
| sky_select = gr.Dropdown( | |
| choices=list(SKY_LIBRARY.values()), | |
| value="☀️ Dia Ensolarado", | |
| label="Tipo de Céu", | |
| interactive=True | |
| ) | |
| intensity_slider = gr.Slider( | |
| minimum=0.1, | |
| maximum=1.0, | |
| value=0.8, | |
| step=0.1, | |
| label="Intensidade da Substituição", | |
| interactive=True | |
| ) | |
| auto_detect = gr.Checkbox( | |
| label="Detecção automática de céu", | |
| value=True, | |
| interactive=True | |
| ) | |
| apply_button = gr.Button( | |
| "🌤️ Aplicar Novo Céu", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| # Coluna central - Biblioteca de Céus | |
| with gr.Column(scale=2): | |
| with gr.Group(): | |
| gr.Markdown("### 🌈 Biblioteca de Céus") | |
| # Criar grid de céus | |
| sky_html = "<div class='sky-grid'>" | |
| for sky_key, sky_name in SKY_LIBRARY.items(): | |
| emoji = sky_name.split()[0] | |
| sky_html += f""" | |
| <div class='sky-card' onclick="selectSky('{sky_key}')"> | |
| <div class='sky-icon'>{emoji}</div> | |
| <div class='sky-name'>{sky_name}</div> | |
| </div> | |
| """ | |
| sky_html += "</div>" | |
| gr.HTML(sky_html) | |
| # Dropdown oculto para controle | |
| sky_select_hidden = gr.Dropdown( | |
| choices=list(SKY_LIBRARY.keys()), | |
| value="sunny_day", | |
| label="Céu Selecionado", | |
| visible=False | |
| ) | |
| # Coluna direita - Resultados | |
| with gr.Column(scale=1): | |
| with gr.Tabs(): | |
| with gr.TabItem("🎨 Resultado"): | |
| image_output = gr.Image( | |
| label="Imagem com Céu Substituído", | |
| type="pil", | |
| height=300, | |
| elem_classes="image-container" | |
| ) | |
| with gr.TabItem("🎯 Máscara"): | |
| mask_output = gr.Image( | |
| label="Área do Céu Detectada", | |
| type="pil", | |
| height=300, | |
| elem_classes="image-container", | |
| visible=False | |
| ) | |
| with gr.Group(): | |
| gr.Markdown("### 📊 Status") | |
| status_output = gr.HTML( | |
| value="<div class='status-box status-success'>Pronto para substituir céus!</div>", | |
| label="Status" | |
| ) | |
| with gr.Group(): | |
| gr.Markdown("### 💾 Download") | |
| download_button = gr.Button( | |
| "⬇️ Baixar Imagem", | |
| variant="secondary", | |
| size="lg", | |
| elem_classes="download-btn" | |
| ) | |
| download_file = gr.File( | |
| label="Arquivo para download", | |
| visible=False | |
| ) | |
| # Instruções | |
| gr.HTML(""" | |
| <div style="background: #f8f9fa; padding: 20px; border-radius: 15px; margin-top: 20px;"> | |
| <h3>📋 Como Usar:</h3> | |
| <ol style="margin: 10px 0; padding-left: 20px;"> | |
| <li><strong>Faça upload</strong> de uma imagem com céu visível</li> | |
| <li><strong>Escolha um céu</strong> da biblioteca clicando nos cards</li> | |
| <li><strong>Ajuste a intensidade</strong> para controlar a transição</li> | |
| <li><strong>Clique em "Aplicar Novo Céu"</strong> para processar</li> | |
| <li><strong>Baixe o resultado</strong> ou experimente outros céus</li> | |
| </ol> | |
| <p style="margin-top: 10px; color: #666;"> | |
| 💡 <strong>Dica:</strong> Use intensidade mais baixa (0.3-0.5) para transições mais sutis | |
| </p> | |
| </div> | |
| """) | |
| # Footer | |
| gr.HTML(""" | |
| <div class="footer"> | |
| <p>🌤️ Photoshop AI Ecosystem - App 10: Sky Replacer</p> | |
| <p>Substituição profissional de céus com detecção inteligente</p> | |
| </div> | |
| """) | |
| # JavaScript para interatividade | |
| gr.HTML(""" | |
| <script> | |
| function selectSky(skyKey) { | |
| // Atualizar dropdown visível | |
| const skyNames = { | |
| "sunny_day": "☀️ Dia Ensolarado", | |
| "blue_sky": "🔵 Céu Azul Limpo", | |
| "cloudy": "☁️ Céu Nublado", | |
| "stormy": "⛈️ Tempestade", | |
| "sunset": "🌅 Pôr do Sol", | |
| "sunrise": "🌇 Nascer do Sol", | |
| "golden_hour": "✨ Hora Dourada", | |
| "night_sky": "🌙 Noite Estrelada", | |
| "northern_lights": "🌈 Aurora Boreal", | |
| "space": "🚀 Espaço Sideral", | |
| "foggy": "🌫️ Nebuloso", | |
| "rainbow": "🌈 Arco-Íris", | |
| "dramatic": "🎭 Dramático", | |
| "pastel": "🎨 Pastel", | |
| "gradient": "🔄 Gradiente" | |
| }; | |
| document.querySelectorAll('[data-testid="dropdown"]')[0].value = skyNames[skyKey]; | |
| // Atualizar seleção visual | |
| document.querySelectorAll('.sky-card').forEach(card => { | |
| card.classList.remove('active'); | |
| }); | |
| // Destacar card selecionado | |
| const selectedCard = document.querySelector(`[onclick="selectSky('${skyKey}')"]`); | |
| if (selectedCard) { | |
| selectedCard.classList.add('active'); | |
| } | |
| // Disparar evento de mudança | |
| const event = new Event('change'); | |
| document.querySelectorAll('[data-testid="dropdown"]')[0].dispatchEvent(event); | |
| } | |
| // Inicializar com primeiro céu ativo | |
| document.addEventListener('DOMContentLoaded', function() { | |
| setTimeout(() => { | |
| selectSky('sunny_day'); | |
| }, 100); | |
| }); | |
| </script> | |
| """) | |
| # Funções de processamento | |
| def process_sky_replacement(image, sky_label, intensity, auto_detect_flag): | |
| """Processa a substituição do céu""" | |
| if image is None: | |
| return None, "<div class='status-box status-error'>❌ Por favor, faça upload de uma imagem.</div>", gr.update(visible=False) | |
| # Converter label para key | |
| sky_key = None | |
| for key, value in SKY_LIBRARY.items(): | |
| if value == sky_label: | |
| sky_key = key | |
| break | |
| if sky_key is None: | |
| return None, f"<div class='status-box status-error'>❌ Tipo de céu inválido: {sky_label}</div>", gr.update(visible=False) | |
| result, message, mask_display = replace_sky( | |
| image, sky_key, intensity, auto_detect_flag | |
| ) | |
| if result is None: | |
| return None, f"<div class='status-box status-error'>{message}</div>", gr.update(visible=False) | |
| status_html = f"<div class='status-box status-success'>{message}</div>" | |
| # Mostrar máscara | |
| mask_visible = gr.update(visible=mask_display is not None) | |
| return result, status_html, mask_display, mask_visible | |
| def trigger_download(image): | |
| """Dispara o download da imagem""" | |
| if image is None: | |
| return None | |
| temp_file = save_image(image) | |
| return temp_file | |
| # Conectar eventos | |
| apply_button.click( | |
| fn=process_sky_replacement, | |
| inputs=[image_input, sky_select, intensity_slider, auto_detect], | |
| outputs=[image_output, status_output, mask_output, mask_output] | |
| ) | |
| # Atualizar automaticamente quando parâmetros mudam | |
| sky_select.change( | |
| fn=process_sky_replacement, | |
| inputs=[image_input, sky_select, intensity_slider, auto_detect], | |
| outputs=[image_output, status_output, mask_output, mask_output] | |
| ) | |
| intensity_slider.change( | |
| fn=process_sky_replacement, | |
| inputs=[image_input, sky_select, intensity_slider, auto_detect], | |
| outputs=[image_output, status_output, mask_output, mask_output] | |
| ) | |
| auto_detect.change( | |
| fn=process_sky_replacement, | |
| inputs=[image_input, sky_select, intensity_slider, auto_detect], | |
| outputs=[image_output, status_output, mask_output, mask_output] | |
| ) | |
| # Botão de download | |
| download_button.click( | |
| fn=trigger_download, | |
| inputs=[image_output], | |
| outputs=[download_file] | |
| ) | |
| return demo | |
| # ========== EXECUÇÃO PRINCIPAL ========== | |
| if __name__ == "__main__": | |
| print("🚀 Inicializando Sky Replacer...") | |
| # Verificar se está no Hugging Face | |
| IS_HUGGINGFACE = os.getenv('SPACE_ID') is not None | |
| # Criar interface | |
| demo = create_interface() | |
| # Configurações | |
| server_name = "0.0.0.0" if IS_HUGGINGFACE else "127.0.0.1" | |
| server_port = int(os.getenv('PORT', 7860)) | |
| print(f"🌐 Servidor: {server_name}:{server_port}") | |
| print(f"🌤️ Céus disponíveis: {len(SKY_LIBRARY)}") | |
| print("🚀 Sky Replacer pronto para uso!") | |
| # Iniciar servidor | |
| try: | |
| demo.launch( | |
| server_name=server_name, | |
| server_port=server_port, | |
| share=False, | |
| debug=False, | |
| show_error=True, | |
| quiet=True | |
| ) | |
| except Exception as e: | |
| print(f"❌ Erro ao iniciar servidor: {str(e)}") | |
| print("⚠️ Tentando configuração alternativa...") | |
| # Configuração alternativa | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| quiet=True | |
| ) |