| """Zero123++ v1.2 Gradio demo: single image -> 6 consistent multi-views.""" |
|
|
| import spaces |
|
|
| import torch |
| import gradio as gr |
| from PIL import Image |
| from diffusers import DiffusionPipeline, EulerAncestralDiscreteScheduler |
|
|
| MODEL_ID = "sudo-ai/zero123plus-v1.2" |
| CUSTOM_PIPELINE = "sudo-ai/zero123plus-pipeline" |
| VIEW_LABELS = [ |
| "Azimuth 30° / Elev 20°", |
| "Azimuth 90° / Elev -10°", |
| "Azimuth 150° / Elev 20°", |
| "Azimuth 210° / Elev -10°", |
| "Azimuth 270° / Elev 20°", |
| "Azimuth 330° / Elev -10°", |
| ] |
|
|
| pipe = DiffusionPipeline.from_pretrained( |
| MODEL_ID, |
| custom_pipeline=CUSTOM_PIPELINE, |
| torch_dtype=torch.float16, |
| ) |
| pipe.scheduler = EulerAncestralDiscreteScheduler.from_config( |
| pipe.scheduler.config, |
| timestep_spacing="trailing", |
| ) |
| pipe.to("cuda") |
|
|
|
|
| def expand2square(pil_img: Image.Image, background_color: tuple[int, int, int, int]) -> Image.Image: |
| """Pad a non-square image to a square canvas.""" |
| width, height = pil_img.size |
| if width == height: |
| return pil_img |
| if width > height: |
| result = Image.new(pil_img.mode, (width, width), background_color) |
| result.paste(pil_img, (0, (width - height) // 2)) |
| return result |
| result = Image.new(pil_img.mode, (height, height), background_color) |
| result.paste(pil_img, ((height - width) // 2, 0)) |
| return result |
|
|
|
|
| def prepare_input(image: Image.Image, remove_bg: bool) -> Image.Image: |
| """Square-pad and optionally rembg the input image.""" |
| if image is None: |
| raise gr.Error("Please upload an input image.") |
| img = image.convert("RGBA") |
| if max(img.size) > 1280: |
| scale = 1280 / max(img.size) |
| img = img.resize( |
| (round(img.width * scale), round(img.height * scale)), |
| Image.Resampling.LANCZOS, |
| ) |
| if remove_bg: |
| from rembg import remove |
|
|
| img = remove(img) |
| img = expand2square(img, (127, 127, 127, 0)) |
| |
| rgb = Image.new("RGB", img.size, (127, 127, 127)) |
| rgb.paste(img, mask=img.split()[-1] if img.mode == "RGBA" else None) |
| return rgb |
|
|
|
|
| def split_views(sheet: Image.Image) -> list[Image.Image]: |
| """Split the 2x3 Zero123++ sheet into six view images.""" |
| side = sheet.width // 2 |
| views: list[Image.Image] = [] |
| for y in range(0, sheet.height, side): |
| for x in range(0, sheet.width, side): |
| views.append(sheet.crop((x, y, x + side, y + side))) |
| return views[:6] |
|
|
|
|
| def maybe_remove_bg_views(views: list[Image.Image], enabled: bool) -> list[Image.Image]: |
| """Optionally remove gray background from each generated view.""" |
| if not enabled: |
| return views |
| from rembg import remove |
|
|
| cleaned: list[Image.Image] = [] |
| for view in views: |
| out = remove(view.convert("RGBA")) |
| cleaned.append(out) |
| return cleaned |
|
|
|
|
| @spaces.GPU(duration=120) |
| def generate_multiview( |
| image: Image.Image, |
| remove_input_bg: bool, |
| remove_output_bg: bool, |
| guidance_scale: float, |
| num_inference_steps: int, |
| seed: int, |
| ) -> tuple[Image.Image, Image.Image, Image.Image, Image.Image, Image.Image, Image.Image, Image.Image]: |
| """Generate six consistent novel views from a single input image with Zero123++ v1.2.""" |
| cond = prepare_input(image, remove_input_bg) |
| generator = torch.Generator(device=pipe.device).manual_seed(int(seed)) |
| sheet = pipe( |
| cond, |
| num_inference_steps=int(num_inference_steps), |
| guidance_scale=float(guidance_scale), |
| generator=generator, |
| ).images[0] |
| views = maybe_remove_bg_views(split_views(sheet), remove_output_bg) |
| while len(views) < 6: |
| views.append(Image.new("RGB", (320, 320), (127, 127, 127))) |
| return (sheet, views[0], views[1], views[2], views[3], views[4], views[5]) |
|
|
|
|
| TITLE = "Zero123++ v1.2 Multi-View Demo" |
| DESCRIPTION = """ |
| Upload one image → get **6 consistent multi-views** via |
| [`sudo-ai/zero123plus-v1.2`](https://huggingface.co/sudo-ai/zero123plus-v1.2). |
| |
| Fixed cameras (relative azimuth / absolute elevation): |
| `30°/20°, 90°/-10°, 150°/20°, 210°/-10°, 270°/20°, 330°/-10°` · FOV `30°`. |
| |
| **License note:** model weights are **CC-BY-NC 4.0** (non-commercial). Code is Apache-2.0. |
| """ |
|
|
| with gr.Blocks(title=TITLE, theme=gr.themes.Soft(primary_hue="blue")) as demo: |
| gr.Markdown(f"# {TITLE}") |
| gr.Markdown(DESCRIPTION) |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| input_image = gr.Image( |
| type="pil", |
| image_mode="RGBA", |
| label="Input image", |
| height=360, |
| ) |
| remove_input_bg = gr.Checkbox(value=True, label="Remove input background (rembg)") |
| remove_output_bg = gr.Checkbox(value=False, label="Remove output backgrounds (rembg)") |
| guidance_scale = gr.Slider(1.0, 10.0, value=4.0, step=0.5, label="CFG scale") |
| num_inference_steps = gr.Slider( |
| 15, |
| 100, |
| value=36, |
| step=1, |
| label="Inference steps", |
| info="~28 for general objects; 75+ for faces / fine detail", |
| ) |
| seed = gr.Number(value=42, precision=0, label="Seed") |
| run_btn = gr.Button("Generate 6 views", variant="primary") |
|
|
| gr.Examples( |
| examples=[ |
| ["https://d.skis.ltd/nrp/sample-data/lysol.png"], |
| ], |
| inputs=[input_image], |
| label="Examples", |
| cache_examples=False, |
| ) |
|
|
| with gr.Column(scale=1): |
| sheet_out = gr.Image(type="pil", label="Full 2×3 sheet", height=360) |
| with gr.Row(): |
| v0 = gr.Image(type="pil", label=VIEW_LABELS[0], height=200) |
| v1 = gr.Image(type="pil", label=VIEW_LABELS[1], height=200) |
| v2 = gr.Image(type="pil", label=VIEW_LABELS[2], height=200) |
| with gr.Row(): |
| v3 = gr.Image(type="pil", label=VIEW_LABELS[3], height=200) |
| v4 = gr.Image(type="pil", label=VIEW_LABELS[4], height=200) |
| v5 = gr.Image(type="pil", label=VIEW_LABELS[5], height=200) |
|
|
| run_btn.click( |
| fn=generate_multiview, |
| inputs=[ |
| input_image, |
| remove_input_bg, |
| remove_output_bg, |
| guidance_scale, |
| num_inference_steps, |
| seed, |
| ], |
| outputs=[sheet_out, v0, v1, v2, v3, v4, v5], |
| ) |
|
|
| if __name__ == "__main__": |
| demo.queue(max_size=8).launch(mcp_server=True) |
|
|