"""Zero123++ v1.2 Gradio demo: single image -> 6 consistent multi-views.""" import spaces # MUST come before torch / diffusers 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)) # Composite onto gray background expected by Zero123++ VAE 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)