ascarlettvfx's picture
Update app.py
e8488d9 verified
Raw
History Blame Contribute Delete
13.7 kB
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)