""" 🎨 Object Replacer AI - Versão Funcional Substituição inteligente de objetos usando IA """ import gradio as gr from PIL import Image, ImageDraw, ImageFilter, ImageFont import numpy as np import torch from diffusers import StableDiffusionInpaintPipeline from scipy.ndimage import binary_dilation import tempfile import time print("🚀 Iniciando Object Replacer AI...") # Configurações DEVICE = "cuda" if torch.cuda.is_available() else "cpu" MAX_SIZE = 768 print(f"💻 Dispositivo: {DEVICE}") # Modelo global inpaint_pipe = None def log_msg(msg): print(f"[{time.strftime('%H:%M:%S')}] {msg}") def load_inpaint_model(): """Carrega modelo de inpainting""" global inpaint_pipe if inpaint_pipe is None: log_msg("📦 Carregando Stable Diffusion Inpainting...") try: inpaint_pipe = StableDiffusionInpaintPipeline.from_pretrained( "stabilityai/stable-diffusion-2-inpainting", torch_dtype=torch.float16 if DEVICE == "cuda" else torch.float32, safety_checker=None, requires_safety_checker=False ).to(DEVICE) if DEVICE == "cuda": inpaint_pipe.enable_attention_slicing() inpaint_pipe.enable_vae_slicing() log_msg("✅ Modelo carregado!") except Exception as e: log_msg(f"⚠️ Erro: {e}, tentando modelo alternativo...") inpaint_pipe = StableDiffusionInpaintPipeline.from_pretrained( "runwayml/stable-diffusion-inpainting", torch_dtype=torch.float16 if DEVICE == "cuda" else torch.float32, safety_checker=None ).to(DEVICE) return inpaint_pipe def resize_image(image, max_size=MAX_SIZE): """Redimensiona mantendo proporção""" if max(image.size) <= max_size: return image ratio = max_size / max(image.size) new_size = (int(image.width * ratio), int(image.height * ratio)) return image.resize(new_size, Image.Resampling.LANCZOS) def create_circular_mask(image_size, center_x, center_y, radius): """Cria máscara circular ao redor do ponto clicado""" mask = Image.new('L', image_size, 0) draw = ImageDraw.Draw(mask) # Desenhar círculo draw.ellipse( [center_x - radius, center_y - radius, center_x + radius, center_y + radius], fill=255 ) # Suavizar bordas mask = mask.filter(ImageFilter.GaussianBlur(radius=15)) return mask def create_mask_from_brush(image_size, brush_strokes): """Cria máscara a partir de traços de pincel""" mask = Image.new('L', image_size, 0) draw = ImageDraw.Draw(mask) if brush_strokes and len(brush_strokes) > 0: for stroke in brush_strokes: if len(stroke) >= 2: # Desenhar linha grossa draw.line(stroke, fill=255, width=30) # Suavizar mask = mask.filter(ImageFilter.GaussianBlur(radius=10)) return mask def smart_inpaint(image, mask, prompt, negative_prompt, steps, guidance): """Substitui objeto usando inpainting""" try: log_msg(f"🎨 Gerando: '{prompt}'") # Carregar modelo pipe = load_inpaint_model() # Ajustar tamanho para múltiplo de 8 w, h = image.size new_w = (w // 8) * 8 new_h = (h // 8) * 8 img_resized = image.resize((new_w, new_h), Image.Resampling.LANCZOS) mask_resized = mask.resize((new_w, new_h), Image.Resampling.LANCZOS) # Gerar start = time.time() with torch.no_grad(): result = pipe( prompt=prompt, negative_prompt=negative_prompt, image=img_resized, mask_image=mask_resized, num_inference_steps=steps, guidance_scale=guidance, strength=0.99 ) output = result.images[0] # Voltar ao tamanho original output = output.resize((w, h), Image.Resampling.LANCZOS) elapsed = time.time() - start log_msg(f"✅ Concluído em {elapsed:.1f}s") return output, f"✅ Objeto gerado em {elapsed:.1f}s!" except Exception as e: log_msg(f"❌ Erro: {e}") return None, f"❌ Erro: {str(e)}" def remove_object_smart(image, mask): """Remove objeto preenchendo com contexto""" try: log_msg("🗑️ Removendo objeto...") # Usar inpainting com prompt genérico prompt = "smooth background, natural surface, seamless, no objects" negative_prompt = "object, item, text, watermark, person, animal" result, msg = smart_inpaint(image, mask, prompt, negative_prompt, 40, 7.0) if result: return result, "✅ Objeto removido!" return None, msg except Exception as e: return None, f"❌ Erro ao remover: {str(e)}" # Interface Gradio with gr.Blocks(title="🎨 Object Replacer AI") as demo: gr.HTML("""

✨ Object Replacer AI

Substitua ou remova objetos com Inteligência Artificial

Powered by Stable Diffusion Inpainting
""") # Estado para armazenar dados mask_state = gr.State(None) original_image_state = gr.State(None) with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 📤 1. Carregue sua Imagem") input_image = gr.Image( label="", type="pil", interactive=True, height=350 ) gr.Markdown("### 🖌️ 2. Marque o Objeto") gr.Markdown("**Use o pincel para pintar sobre o objeto que deseja substituir/remover**") brush_image = gr.ImageMask( label="Pinte sobre o objeto", type="pil", height=350 ) gr.Markdown("**Dica:** Use o slider abaixo para ajustar o tamanho do traço") brush_size = gr.Slider( 10, 50, 20, step=5, label="Tamanho do Pincel", info="Ajuste o tamanho do pincel" ) clear_mask_btn = gr.Button("🔄 Limpar Máscara", size="sm") with gr.Column(scale=1): gr.Markdown("### ⚙️ 3. Configure a Ação") action_radio = gr.Radio( choices=[ ("🔄 Substituir por novo objeto", "replace"), ("🗑️ Remover objeto", "remove") ], value="replace", label="O que fazer?", interactive=True ) replacement_prompt = gr.Textbox( label="📝 Descreva o novo objeto (em inglês para melhor resultado)", placeholder="Ex: a red sports car / a beautiful flower / a modern laptop", lines=3, value="a beautiful red rose flower, photorealistic, high quality" ) negative_prompt = gr.Textbox( label="🚫 O que evitar (opcional)", placeholder="Ex: ugly, blurry, low quality", value="ugly, blurry, low quality, distorted, deformed", lines=2 ) with gr.Accordion("🎛️ Configurações Avançadas", open=False): steps_slider = gr.Slider( 10, 50, 25, step=5, label="Steps (Qualidade)", info="Mais steps = melhor qualidade (mas mais lento)" ) guidance_slider = gr.Slider( 1.0, 15.0, 7.5, step=0.5, label="Guidance Scale", info="7-9 recomendado. Maior = segue mais o prompt" ) process_btn = gr.Button( "✨ PROCESSAR IMAGEM", variant="primary", size="lg" ) gr.Markdown("### 🖼️ 4. Resultado") output_image = gr.Image( label="", type="pil", interactive=False, height=350 ) status_output = gr.Markdown("**Status:** Aguardando imagem...") with gr.Row(): download_btn = gr.Button("📥 Baixar Resultado", size="lg") reset_btn = gr.Button("🔄 Resetar Tudo", variant="secondary", size="lg") download_file = gr.File(label="Download", visible=False) # Exemplos with gr.Accordion("💡 Exemplos de Prompts", open=False): gr.Markdown(""" ### 🎯 Exemplos de prompts efetivos: **Para objetos realistas:** - `a red sports car, photorealistic, detailed, 4k` - `a beautiful yellow sunflower, vibrant colors, professional photo` - `a modern silver laptop, sleek design, high quality` - `a cute golden retriever puppy, fluffy, professional portrait` **Para estilo artístico:** - `a magical glowing crystal, fantasy art, ethereal lighting` - `a steampunk mechanical device, intricate gears, brass and copper` - `a neon cyberpunk sign, glowing letters, futuristic` **Dicas:** - ✅ Use inglês para melhores resultados - ✅ Seja específico: cores, estilo, qualidade - ✅ Adicione: "photorealistic, high quality, detailed" - ✅ Para remoção: deixe em branco ou use "smooth background" """) # Guia de uso gr.Markdown(""" --- ### 📖 Como usar: 1. **Carregue a imagem** - Clique ou arraste para a área de upload 2. **Marque o objeto** - Use o pincel para pintar sobre o que quer substituir/remover 3. **Escolha a ação**: - **Substituir**: Descreva o novo objeto em inglês - **Remover**: O objeto será removido e preenchido inteligentemente 4. **Ajuste configurações** (opcional) - Steps e Guidance Scale 5. **Clique em Processar** - Aguarde a IA gerar o resultado 6. **Baixe o resultado** - Se gostar, clique em Baixar **⚠️ Importante:** - Primeira execução pode demorar (carregando modelo ~2GB) - Marque bem a área do objeto com o pincel - Use prompts em inglês para melhor qualidade - GPU acelera significativamente o processo """) # Funções de callback def update_brush_image(image): """Atualiza a imagem no brush quando nova imagem é carregada""" if image is None: return None, None, "❌ Carregue uma imagem primeiro" img = resize_image(image, MAX_SIZE) return img, img, "✅ Imagem carregada! Use o pincel para marcar o objeto" input_image.change( fn=update_brush_image, inputs=[input_image], outputs=[brush_image, original_image_state, status_output] ) def toggle_prompt_visibility(action): """Mostra/esconde prompt baseado na ação""" return gr.update(visible=(action == "replace")) action_radio.change( fn=toggle_prompt_visibility, inputs=[action_radio], outputs=[replacement_prompt] ) def process_image(original_img, brush_data, action, prompt, neg_prompt, steps, guidance): """Processa a imagem com inpainting""" try: log_msg("=== INÍCIO DO PROCESSAMENTO ===") if original_img is None: return None, "❌ Carregue uma imagem primeiro!" if brush_data is None: return None, "❌ Use o pincel para marcar o objeto!" # ImageMask do Gradio retorna a imagem com composição da máscara # Precisamos extrair apenas a máscara das áreas pintadas img = original_img # Verificar o formato do brush_data log_msg(f"Tipo brush_data: {type(brush_data)}") if isinstance(brush_data, dict): log_msg(f"Keys: {list(brush_data.keys())}") # Obter imagem composta (background + camadas) composite = brush_data.get('composite', None) background = brush_data.get('background', None) layers = brush_data.get('layers', None) log_msg(f"Composite: {composite is not None}") log_msg(f"Background: {background is not None}") log_msg(f"Layers: {layers is not None}") if composite is not None: # Usar a imagem composta composite_img = composite if isinstance(composite, Image.Image) else Image.fromarray(composite) # A máscara é a diferença entre composite e background if background is not None: bg_img = background if isinstance(background, Image.Image) else Image.fromarray(background) # Converter para arrays numpy composite_arr = np.array(composite_img.convert('RGB')) bg_arr = np.array(bg_img.convert('RGB')) # Diferença absoluta diff = np.abs(composite_arr.astype(int) - bg_arr.astype(int)) diff_sum = np.sum(diff, axis=2) # Criar máscara onde há diferença mask_arr = (diff_sum > 30).astype(np.uint8) * 255 mask = Image.fromarray(mask_arr, mode='L') log_msg(f"Máscara criada por diferença: {mask.size}") else: # Se não tem background, tentar detectar as áreas pintadas # Assumir que as áreas pintadas são escuras/pretas composite_arr = np.array(composite_img.convert('L')) mask_arr = (composite_arr < 128).astype(np.uint8) * 255 mask = Image.fromarray(mask_arr, mode='L') log_msg(f"Máscara criada por threshold: {mask.size}") elif layers is not None and len(layers) > 0: # Processar camadas log_msg(f"Processando {len(layers)} camadas") # Começar com máscara vazia mask = Image.new('L', img.size, 0) for i, layer in enumerate(layers): if layer is None: continue log_msg(f"Layer {i}: {type(layer)}") if isinstance(layer, np.ndarray): # Se for array, converter if layer.ndim == 3: # Se RGB/RGBA, pegar canal alpha ou converter para cinza if layer.shape[2] == 4: layer_mask = Image.fromarray(layer[:, :, 3], mode='L') else: layer_img = Image.fromarray(layer) layer_mask = layer_img.convert('L') else: layer_mask = Image.fromarray(layer, mode='L') elif isinstance(layer, Image.Image): layer_mask = layer.convert('L') else: continue # Combinar com máscara existente mask_arr = np.array(mask) layer_arr = np.array(layer_mask) combined = np.maximum(mask_arr, layer_arr) mask = Image.fromarray(combined, mode='L') else: return None, "❌ Não foi possível extrair a máscara. Pinte sobre o objeto com o pincel!" elif isinstance(brush_data, Image.Image): # Se for uma imagem direta # A máscara é onde está pintado (normalmente em preto) brush_arr = np.array(brush_data.convert('L')) mask_arr = (brush_arr < 128).astype(np.uint8) * 255 mask = Image.fromarray(mask_arr, mode='L') log_msg(f"Máscara de Image direta: {mask.size}") else: return None, "❌ Formato de máscara não reconhecido!" # Verificar se a máscara tem conteúdo if mask is None: return None, "❌ Não foi possível criar a máscara. Pinte sobre o objeto!" mask_array = np.array(mask) white_pixels = np.sum(mask_array > 128) total_pixels = mask_array.size percentage = (white_pixels / total_pixels) * 100 log_msg(f"Pixels brancos: {white_pixels} ({percentage:.2f}% da imagem)") if white_pixels < 100: return None, f"❌ Área marcada muito pequena ({white_pixels} pixels). Pinte mais sobre o objeto!" # Dilatar a máscara levemente para cobrir melhor mask_array = np.array(mask) kernel_size = 5 kernel = np.ones((kernel_size, kernel_size), np.uint8) from scipy.ndimage import binary_dilation mask_dilated = binary_dilation(mask_array > 128, structure=kernel).astype(np.uint8) * 255 mask = Image.fromarray(mask_dilated, mode='L') # Suavizar bordas mask = mask.filter(ImageFilter.GaussianBlur(radius=5)) # Redimensionar img = resize_image(img, MAX_SIZE) mask = mask.resize(img.size, Image.Resampling.LANCZOS) log_msg(f"✅ Máscara válida! Processando - Ação: {action}") # Processar baseado na ação if action == "replace": if not prompt or len(prompt.strip()) < 3: return None, "❌ Descreva o novo objeto no campo de texto!" result, status = smart_inpaint(img, mask, prompt, neg_prompt, steps, guidance) else: # remove result, status = remove_object_smart(img, mask) return result, status except Exception as e: import traceback error = f"❌ Erro: {str(e)}\n\n{traceback.format_exc()}" log_msg(error) return None, error process_btn.click( fn=process_image, inputs=[original_image_state, brush_image, action_radio, replacement_prompt, negative_prompt, steps_slider, guidance_slider], outputs=[output_image, status_output] ) def save_result(image): """Salva resultado""" if image is None: return None temp = tempfile.NamedTemporaryFile(delete=False, suffix=".png") image.save(temp.name, "PNG", optimize=True) return temp.name download_btn.click( fn=save_result, inputs=[output_image], outputs=[download_file] ) def reset_all(): """Reseta tudo""" return None, None, None, None, None, "🔄 Tudo resetado! Comece novamente" reset_btn.click( fn=reset_all, outputs=[input_image, brush_image, output_image, mask_state, original_image_state, status_output] ) def clear_mask(img): """Limpa apenas a máscara""" if img: return img return None clear_mask_btn.click( fn=clear_mask, inputs=[original_image_state], outputs=[brush_image] ) gr.HTML("""

✨ Object Replacer AI ✨

🎨 Stable Diffusion Inpainting • 🖌️ Interface Intuitiva • ⚡ Resultados Profissionais

Dica: Para melhores resultados, marque bem o objeto e use prompts descritivos em inglês

""") if __name__ == "__main__": log_msg(f"✅ Sistema pronto! Dispositivo: {DEVICE}") log_msg("⚡ Modelo será carregado na primeira geração") demo.launch( server_name="0.0.0.0", share=True, show_error=True, theme=gr.themes.Soft() )