import streamlit as st import numpy as np from PIL import Image, ImageDraw import cv2 import io from backend.utilities import pil_to_cv, cv_to_pil, create_checkerboard from backend.bg_remover.shadow_generator import blend_rgba def generate_gradient_background(w: int, h: int, style: str) -> np.ndarray: """Generate a custom studio gradient background (RGBA).""" bg = np.zeros((h, w, 4), dtype=np.uint8) bg[:, :, 3] = 255 # fully opaque if style == "Midnight Glow": # Radial gradient from dark indigo (#1A1B35) to black (#0B0C10) cx, cy = w // 2, h // 2 max_dist = np.sqrt(cx**2 + cy**2) y, x = np.indices((h, w)) dist = np.sqrt((x - cx)**2 + (y - cy)**2) factor = np.clip(dist / max_dist, 0.0, 1.0) # Color 1: 26, 27, 53 | Color 2: 11, 12, 16 for c in range(3): c1, c2 = [26, 27, 53][c], [11, 12, 16][c] bg[:, :, c] = (c1 * (1.0 - factor) + c2 * factor).astype(np.uint8) elif style == "Sunset Studio": # Linear vertical gradient from soft orange (#FF7E5F) to deep purple (#FEB47B) y, _ = np.indices((h, w)) factor = (y / h).astype(float) # Color 1: 255, 126, 95 | Color 2: 254, 180, 123 for c in range(3): c1, c2 = [255, 126, 95][c], [254, 180, 123][c] bg[:, :, c] = (c1 * (1.0 - factor) + c2 * factor).astype(np.uint8) elif style == "Clean Studio": # Vertical gradient from light gray (#F5F7FA) to slate gray (#B3C0CD) y, _ = np.indices((h, w)) factor = (y / h).astype(float) # Color 1: 245, 247, 250 | Color 2: 179, 192, 205 for c in range(3): c1, c2 = [245, 247, 250][c], [179, 192, 205][c] bg[:, :, c] = (c1 * (1.0 - factor) + c2 * factor).astype(np.uint8) elif style == "Neon Cyber": # Diagonal gradient from hot magenta (#FF007F) to cyber cyan (#00F0FF) y, x = np.indices((h, w)) # Normalized coordinates summing for diagonal factor = np.clip((x / w + y / h) / 2.0, 0.0, 1.0) # Color 1: 255, 0, 127 | Color 2: 0, 240, 255 for c in range(3): c1, c2 = [255, 0, 127][c], [0, 240, 255][c] bg[:, :, c] = (c1 * (1.0 - factor) + c2 * factor).astype(np.uint8) return bg def composite_cutout_on_bg(cutout_pil: Image.Image, bg_type: str, custom_color: str = "#FFFFFF", custom_bg_file = None) -> Image.Image: """Composite the transparent cutout over a selected background style.""" w, h = cutout_pil.size cutout_cv = pil_to_cv(cutout_pil) # RGBA BGR/BGRA # Ensure BG is shape (H, W, 4) bg_rgba = np.zeros((h, w, 4), dtype=np.uint8) bg_rgba[:, :, 3] = 255 # solid background by default if bg_type == "Transparent (Checkerboard)": bg_rgba = create_checkerboard(w, h, square_size=16) elif bg_type == "Solid White": bg_rgba[:, :, :3] = 255 elif bg_type == "Solid Black": bg_rgba[:, :, :3] = 0 elif bg_type == "Custom Solid Color": # Convert hex custom_color "#RRGGBB" to BGR hex_color = custom_color.lstrip('#') r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) bg_rgba[:, :, 0] = b bg_rgba[:, :, 1] = g bg_rgba[:, :, 2] = r elif bg_type in ["Midnight Glow", "Sunset Studio", "Clean Studio", "Neon Cyber"]: bg_rgba = generate_gradient_background(w, h, bg_type) elif bg_type == "Custom Upload Image" and custom_bg_file is not None: try: # Load and resize custom background to match the cutout dimensions custom_bg_pil = Image.open(custom_bg_file).convert("RGBA") custom_bg_resized = custom_bg_pil.resize((w, h), Image.Resampling.LANCZOS) bg_rgba = pil_to_cv(custom_bg_resized) except Exception as e: # Fallback to white if error bg_rgba[:, :, :3] = 255 st.error(f"Error loading custom background: {str(e)}") # Perform alpha blending blended_cv = blend_rgba(top=cutout_cv, bottom=bg_rgba) return cv_to_pil(blended_cv) def render_preview_ui( original_pil: Image.Image, processed_pil: Image.Image, bounding_box: tuple = None, show_bounding_box: bool = False ): """ Renders the beautiful side-by-side image previews with interactive controls. """ st.markdown( """

🖼️ VISUAL COMPARISON

""", unsafe_allow_html=True ) # 1. Bounding Box visual overlay helper display_original = original_pil.copy() if show_bounding_box and bounding_box is not None: x, y, w, h = bounding_box draw = ImageDraw.Draw(display_original) # Draw elegant bounding box with thick line and glowing color (#00F0FF) draw.rectangle([x, y, x + w, y + h], outline="#00F0FF", width=3) # Optional: Add small label draw.text((x + 5, y + 5), "GrabCut Bounding Box", fill="#00F0FF") # 2. Render background selector in preview options st.markdown('
', unsafe_allow_html=True) col_bg, col_color, col_custom_upload = st.columns([2, 1, 2]) with col_bg: bg_preview_type = st.selectbox( "Background Preview Mode", [ "Transparent (Checkerboard)", "Solid White", "Solid Black", "Custom Solid Color", "Clean Studio", "Midnight Glow", "Sunset Studio", "Neon Cyber", "Custom Upload Image" ], index=0, key="bg_preview_select" ) with col_color: custom_color = "#FFFFFF" if bg_preview_type == "Custom Solid Color": custom_color = st.color_picker("Pick Color", "#6C63FF", key="custom_color_picker") else: st.write("") # placeholder with col_custom_upload: custom_bg_file = None if bg_preview_type == "Custom Upload Image": custom_bg_file = st.file_uploader("Upload Backdrop Image", type=["jpg", "jpeg", "png", "webp"], key="custom_bg_uploader") else: st.write("") # placeholder st.markdown('
', unsafe_allow_html=True) # 3. Composite output cutout on selected background composited_processed = composite_cutout_on_bg( processed_pil, bg_preview_type, custom_color=custom_color, custom_bg_file=custom_bg_file ) # 4. Display the columns col_left, col_right = st.columns(2) with col_left: st.markdown('

ORIGINAL SOURCE IMAGE

', unsafe_allow_html=True) st.image(display_original, use_container_width=True) with col_right: st.markdown('

EXTRACTED SUBJECT PREVIEW

', unsafe_allow_html=True) st.image(composited_processed, use_container_width=True)