import gc import os import random import gradio as gr import numpy as np import torch from PIL import Image, ImageDraw, ImageFilter from diffusers import AutoPipelineForImage2Image, LCMScheduler DEVICE = "cpu" SIZE = 512 BASE_MODEL = os.environ.get("BASE_MODEL", "Lykon/dreamshaper-7") LCM_LORA = os.environ.get("LCM_LORA", "latent-consistency/lcm-lora-sdv1-5") IP_ADAPTER_REPO = os.environ.get("IP_ADAPTER_REPO", "h94/IP-Adapter") IP_ADAPTER_WEIGHT = os.environ.get("IP_ADAPTER_WEIGHT", "ip-adapter_sd15.bin") print("Loading base model...") pipe = AutoPipelineForImage2Image.from_pretrained( BASE_MODEL, torch_dtype=torch.float32, ) print("Loading LCM scheduler...") pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config) print("Loading LCM-LoRA...") pipe.load_lora_weights(LCM_LORA) try: pipe.fuse_lora() except Exception as exc: print(f"Could not fuse LoRA, continuing without fuse: {exc}") print("Loading IP-Adapter...") pipe.load_ip_adapter( IP_ADAPTER_REPO, subfolder="models", weight_name=IP_ADAPTER_WEIGHT, low_cpu_mem_usage=True, ) pipe = pipe.to(DEVICE) # IMPORTANT: # Do not enable attention slicing with IP-Adapter. # It can cause: # AttributeError: 'tuple' object has no attribute 'shape' try: pipe.vae.enable_slicing() except Exception as exc: print(f"Could not enable VAE slicing: {exc}") SIDE_PROMPTS = { "left": ( "orthographic left side elevation of the same storefront building, " "flat lighting, square texture tile, plausible side wall details, " "brick or concrete, side windows, grime, utility pipes, urban wear, " "no people, no cars, no perspective distortion" ), "right": ( "orthographic right side elevation of the same storefront building, " "flat lighting, square texture tile, plausible side wall details, " "brick or concrete, side windows, grime, utility pipes, urban wear, " "no people, no cars, no perspective distortion" ), "back": ( "orthographic rear elevation of the same storefront building, " "flat lighting, square texture tile, plausible rear wall details, " "service door, vents, pipes, brick or concrete, grime, urban wear, " "no people, no cars, no perspective distortion" ), "top": ( "orthographic roof view of the same storefront building, " "flat top-down square texture tile, tar roof or concrete roof, " "vents, HVAC units, roof seams, stains, grime, urban wear, " "no perspective distortion" ), } def center_crop_resize(img: Image.Image, size: int = SIZE) -> Image.Image: img = img.convert("RGB") width, height = img.size side = min(width, height) left = (width - side) // 2 top = (height - side) // 2 img = img.crop((left, top, left + side, top + side)) img = img.resize((size, size), Image.LANCZOS) return img def average_color(img: Image.Image) -> tuple: arr = np.array(img.convert("RGB")) avg = np.mean(arr.reshape(-1, 3), axis=0).astype(np.uint8) return tuple(avg.tolist()) def sample_region(front_img: Image.Image, side: str, size: int = SIZE) -> Image.Image: img = center_crop_resize(front_img, size) arr = np.array(img) strip = max(32, size // 7) if side == "left": patch = arr[:, :strip, :] elif side == "right": patch = arr[:, size - strip:, :] elif side == "top": patch = arr[:strip, :, :] else: # For back view, sample the center third to avoid edge artifacts. x1 = size // 3 x2 = size - size // 3 patch = arr[:, x1:x2, :] return Image.fromarray(patch).convert("RGB") def add_noise_texture(img: Image.Image, amount: int = 16, seed: int = 0) -> Image.Image: rng = np.random.default_rng(seed) arr = np.array(img).astype(np.int16) noise = rng.normal(0, amount, arr.shape).astype(np.int16) arr = np.clip(arr + noise, 0, 255).astype(np.uint8) return Image.fromarray(arr, "RGB") def blend_texture( base: Image.Image, texture_source: Image.Image, opacity: float = 0.35, ) -> Image.Image: texture = texture_source.resize(base.size, Image.BICUBIC) texture = texture.filter(ImageFilter.GaussianBlur(radius=7)) return Image.blend(base.convert("RGB"), texture.convert("RGB"), opacity) def draw_rect(draw: ImageDraw.ImageDraw, xy, outline, width=3, fill=None): x1, y1, x2, y2 = xy draw.rectangle( (int(x1), int(y1), int(x2), int(y2)), outline=outline, width=width, fill=fill, ) def draw_line(draw: ImageDraw.ImageDraw, xy, fill, width=2): draw.line(tuple(int(v) for v in xy), fill=fill, width=width) def darken(color: tuple, factor: float = 0.65) -> tuple: return tuple(max(0, min(255, int(c * factor))) for c in color) def lighten(color: tuple, factor: float = 1.25) -> tuple: return tuple(max(0, min(255, int(c * factor))) for c in color) def hybrid_side_guide( front_img: Image.Image, side: str, seed: int, size: int = SIZE, ) -> Image.Image: """ Hybrid guide image. Purpose: - The front image is used by IP-Adapter as visual/style reference. - This guide image gives img2img a rough structural canvas. - It avoids directly warping the front facade into a side view. """ rng = random.Random(seed) patch = sample_region(front_img, side, size) base_color = average_color(patch) if side == "top": base_color = darken(base_color, 0.55) guide = Image.new("RGB", (size, size), base_color) guide = blend_texture(guide, patch, opacity=0.28) guide = add_noise_texture(guide, amount=10, seed=seed) draw = ImageDraw.Draw(guide) line_color = darken(base_color, 0.55) light_line = lighten(base_color, 1.35) if side in ["left", "right"]: # Wall divisions. for x in [size * 0.22, size * 0.48, size * 0.74]: draw_line(draw, (x, 0, x, size), fill=line_color, width=2) for y in [size * 0.18, size * 0.42, size * 0.68, size * 0.86]: draw_line(draw, (0, y, size, y), fill=darken(base_color, 0.75), width=1) # Generic side windows. window_fill = darken(base_color, 0.45) for i in range(2): x = size * (0.18 + i * 0.38) + rng.randint(-12, 12) y = size * 0.22 + rng.randint(-20, 20) draw_rect( draw, (x, y, x + size * 0.18, y + size * 0.18), outline=line_color, fill=window_fill, width=3, ) # Pipe / utility line. pipe_x = size * (0.82 if side == "left" else 0.14) draw_line( draw, (pipe_x, size * 0.08, pipe_x, size * 0.92), fill=light_line, width=4, ) draw_line( draw, (pipe_x + 8, size * 0.08, pipe_x + 8, size * 0.92), fill=line_color, width=2, ) # Bottom grime band. grime = Image.new("RGBA", (size, size), (35, 30, 25, 0)) grime_draw = ImageDraw.Draw(grime) grime_draw.rectangle( (0, int(size * 0.78), size, size), fill=(35, 30, 25, 55), ) guide = Image.alpha_composite(guide.convert("RGBA"), grime).convert("RGB") elif side == "back": # Rear wall frame. draw_rect(draw, (0, 0, size - 1, size - 1), outline=line_color, width=4) # Service door. door_fill = darken(base_color, 0.5) draw_rect( draw, (size * 0.38, size * 0.45, size * 0.62, size * 0.95), outline=line_color, fill=door_fill, width=4, ) # Vents / utility panels. vent_fill = darken(base_color, 0.62) draw_rect( draw, (size * 0.12, size * 0.18, size * 0.26, size * 0.34), outline=line_color, fill=vent_fill, width=3, ) draw_rect( draw, (size * 0.74, size * 0.18, size * 0.88, size * 0.34), outline=line_color, fill=vent_fill, width=3, ) # Rear wall horizontal material / grime lines. for y in [size * 0.22, size * 0.44, size * 0.66, size * 0.84]: draw_line( draw, (0, y, size, y), fill=darken(base_color, 0.75), width=2, ) # Pipes. draw_line( draw, (size * 0.08, size * 0.15, size * 0.08, size * 0.95), fill=light_line, width=4, ) draw_line( draw, (size * 0.92, size * 0.10, size * 0.92, size * 0.72), fill=light_line, width=3, ) elif side == "top": roof_overlay = Image.new("RGBA", (size, size), (35, 35, 35, 70)) guide = Image.alpha_composite(guide.convert("RGBA"), roof_overlay).convert("RGB") draw = ImageDraw.Draw(guide) roof_line = (130, 130, 130) # Roof seams. for y in [size * 0.18, size * 0.34, size * 0.52, size * 0.70, size * 0.86]: draw_line(draw, (0, y, size, y), fill=roof_line, width=2) # HVAC / roof units. draw_rect( draw, (size * 0.16, size * 0.20, size * 0.36, size * 0.40), outline=(210, 210, 210), fill=(80, 80, 80), width=3, ) draw_rect( draw, (size * 0.58, size * 0.30, size * 0.80, size * 0.52), outline=(210, 210, 210), fill=(75, 75, 75), width=3, ) # Vents / drains. for cx, cy in [(0.25, 0.68), (0.70, 0.74), (0.48, 0.18)]: r = size * 0.035 draw.ellipse( ( int(size * cx - r), int(size * cy - r), int(size * cx + r), int(size * cy + r), ), outline=(210, 210, 210), width=3, ) return guide.filter(ImageFilter.GaussianBlur(radius=0.35)) @torch.inference_mode() def generate_side(front_image, side, prompt_suffix, strength, steps, seed, ip_scale): if front_image is None: raise gr.Error("Upload a front image first.") seed = int(seed) ref_image = center_crop_resize(front_image) guide_image = hybrid_side_guide(front_image, side, seed=seed) base_prompt = SIDE_PROMPTS[side] if prompt_suffix and prompt_suffix.strip(): prompt = f"{base_prompt}, {prompt_suffix.strip()}" else: prompt = base_prompt pipe.set_ip_adapter_scale(float(ip_scale)) generator = torch.Generator(device=DEVICE).manual_seed(seed) result = pipe( prompt=prompt, image=guide_image, ip_adapter_image=ref_image, strength=float(strength), num_inference_steps=int(steps), guidance_scale=1.5, generator=generator, ).images[0] gc.collect() status = ( "Done. Used DreamShaper-7 + LCM-LoRA + IP-Adapter on CPU. " "The guide image is hybrid: sampled material color/texture plus procedural side/back/roof layout. " "The uploaded front image is used as the IP-Adapter reference." ) return result, guide_image, status with gr.Blocks(title="Building Side View Generator - IPAdapter CPU") as demo: gr.Markdown( """ # Building Side View Generator - IPAdapter CPU Upload a front-facing cropped building image and generate **one plausible side view**. This CPU prototype uses: - `Lykon/dreamshaper-7` - `latent-consistency/lcm-lora-sdv1-5` - `h94/IP-Adapter` The uploaded front image is used as the **IP-Adapter reference image**. The guide image uses a **hybrid approach**: sampled material color/texture from the front image plus procedural side/back/roof layout. """ ) with gr.Row(): front = gr.Image(label="Front Image", type="pil") output = gr.Image(label="Generated Side View", type="pil") with gr.Row(): guide_preview = gr.Image( label="Hybrid Guide Image Used For Img2Img", type="pil", ) status = gr.Textbox(label="Status") with gr.Row(): side = gr.Dropdown( choices=["left", "right", "back", "top"], value="left", label="View to Generate", ) prompt_suffix = gr.Textbox( label="Optional Prompt Add-on", placeholder="e.g. weathered brick, old windows, grime, utility pipes", ) with gr.Row(): strength = gr.Slider( minimum=0.35, maximum=0.85, value=0.62, step=0.05, label="Img2Img Strength", ) steps = gr.Slider( minimum=2, maximum=6, value=4, step=1, label="Inference Steps", ) with gr.Row(): ip_scale = gr.Slider( minimum=0.2, maximum=1.0, value=0.60, step=0.05, label="IPAdapter Scale", ) seed = gr.Number( value=1234, precision=0, label="Seed", ) run = gr.Button("Generate Side View") run.click( fn=generate_side, inputs=[front, side, prompt_suffix, strength, steps, seed, ip_scale], outputs=[output, guide_preview, status], api_name="generate_side", ) if __name__ == "__main__": demo.launch(show_error=True)