Spaces:
Running
Running
| import io | |
| import os | |
| import gradio as gr | |
| from google import genai | |
| from PIL import Image | |
| # Initialize the Gemini client | |
| client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY")) | |
| GREEN_SCREEN_COLOR = (0, 255, 0) # chroma key green for padding regions | |
| def add_padding(image, side, padding_size): | |
| """Add green-screen padding to specified side of image""" | |
| width, height = image.size | |
| if side == "left": | |
| new_image = Image.new( | |
| "RGB", (width + padding_size, height), color=GREEN_SCREEN_COLOR | |
| ) | |
| new_image.paste(image, (padding_size, 0)) | |
| elif side == "right": | |
| new_image = Image.new( | |
| "RGB", (width + padding_size, height), color=GREEN_SCREEN_COLOR | |
| ) | |
| new_image.paste(image, (0, 0)) | |
| elif side == "top": | |
| new_image = Image.new( | |
| "RGB", (width, height + padding_size), color=GREEN_SCREEN_COLOR | |
| ) | |
| new_image.paste(image, (0, padding_size)) | |
| elif side == "bottom": | |
| new_image = Image.new( | |
| "RGB", (width, height + padding_size), color=GREEN_SCREEN_COLOR | |
| ) | |
| new_image.paste(image, (0, 0)) | |
| return new_image | |
| def preview_padding(image, side, padding_size): | |
| """Add green-screen padding to image and show preview""" | |
| if image is None: | |
| return None, None, "No image to pad", "" | |
| if not isinstance(image, Image.Image): | |
| image = Image.fromarray(image) | |
| padded_image = add_padding(image, side, padding_size) | |
| width, height = padded_image.size | |
| info_text = ( | |
| f"Padded size: {width} x {height} pixels ({padding_size}px added to {side})" | |
| ) | |
| direction_text = { | |
| "left": "left side", | |
| "right": "right side", | |
| "top": "top", | |
| "bottom": "bottom", | |
| } | |
| default_prompt = ( | |
| f"This image has a green-screen region on the {direction_text[side]}. " | |
| f"Please naturally extend and outpaint the image to fill in the green-screen area on the {direction_text[side]}. " | |
| f"Make it look seamless and consistent with the existing image content, matching the style, colors, and content naturally. " | |
| f"Do not leave any green regions - completely fill in the {direction_text[side]} area." | |
| ) | |
| return padded_image, side, info_text, default_prompt | |
| def outpaint_with_nano_banana(padded_image, side, custom_prompt): | |
| """Use Nano Banana to outpaint the padded image""" | |
| if padded_image is None: | |
| return None, "No padded image to outpaint", None | |
| if side is None: | |
| return None, "No side information available", None | |
| if custom_prompt and custom_prompt.strip(): | |
| prompt = custom_prompt | |
| else: | |
| direction_text = { | |
| "left": "left side", | |
| "right": "right side", | |
| "top": "top", | |
| "bottom": "bottom", | |
| } | |
| prompt = ( | |
| f"This image has a green-screen region on the {direction_text[side]}. " | |
| f"Please naturally extend and outpaint the image to fill in the green-screen area on the {direction_text[side]}." | |
| ) | |
| try: | |
| response = client.models.generate_content( | |
| model="gemini-2.5-flash-image", contents=[padded_image, prompt] | |
| ) | |
| for part in response.candidates[0].content.parts: | |
| if getattr(part, "inline_data", None): | |
| result_image = Image.open(io.BytesIO(part.inline_data.data)) | |
| w, h = result_image.size | |
| return result_image, f"Output size: {w} x {h} pixels", result_image | |
| w, h = padded_image.size | |
| return padded_image, f"Output size: {w} x {h} pixels (no result from API)", padded_image | |
| except Exception as e: | |
| return padded_image, f"Error: {str(e)}", padded_image | |
| def reuse_outpainted_image(outpainted_image): | |
| """Set the most recent outpainted image as the new input and reset padding state.""" | |
| if outpainted_image is None: | |
| return ( | |
| None, | |
| "No outpainted image available. Run an outpaint first.", | |
| None, | |
| "No padding applied", | |
| None, | |
| None, | |
| ) | |
| if not isinstance(outpainted_image, Image.Image): | |
| outpainted_image = Image.fromarray(outpainted_image) | |
| w, h = outpainted_image.size | |
| return ( | |
| outpainted_image, | |
| f"Image size: {w} x {h} pixels", | |
| None, | |
| "No padding applied", | |
| None, | |
| None, | |
| ) | |
| def get_image_info(image): | |
| """Get image size information""" | |
| if image is None: | |
| return "No image uploaded" | |
| if not isinstance(image, Image.Image): | |
| image = Image.fromarray(image) | |
| w, h = image.size | |
| return f"Image size: {w} x {h} pixels" | |
| # Create Gradio interface | |
| with gr.Blocks(title="Nano Banana Outpainting") as demo: | |
| gr.Markdown("# π Nano Banana Outpainting App") | |
| gr.Markdown( | |
| "Upload an image and click a direction to add green-screen padding. Review the padding, then click 'Outpaint' to fill it in with AI." | |
| ) | |
| # State variables | |
| padded_state = gr.State(None) | |
| side_state = gr.State(None) | |
| outpainted_state = gr.State(None) | |
| # βββββββββββββββββββββββββββββ | |
| # ROW 1: The three image panes | |
| # βββββββββββββββββββββββββββββ | |
| with gr.Row(): | |
| # LEFT: Upload + info | |
| with gr.Column(): | |
| input_image = gr.Image(label="Upload Image", type="pil") | |
| image_info = gr.Textbox( | |
| label="Input Image Info", interactive=False, value="No image uploaded" | |
| ) | |
| # MIDDLE: Padded preview + info | |
| with gr.Column(): | |
| padded_preview = gr.Image( | |
| label="Padded Image (with green-screen pixels)", | |
| type="pil", | |
| interactive=False, | |
| ) | |
| padded_info = gr.Textbox( | |
| label="Padded Image Info", interactive=False, value="No padding applied" | |
| ) | |
| # RIGHT: Outpainted result + info | |
| with gr.Column(): | |
| output_image = gr.Image( | |
| label="Outpainted Result", type="pil", interactive=False | |
| ) | |
| output_info = gr.Textbox( | |
| label="Output Image Info", interactive=False, value="No output yet" | |
| ) | |
| # βββββββββββββββββββββββββββββ | |
| # ROW 2: Step 1 and Step 2 side-by-side | |
| # βββββββββββββββββββββββββββββ | |
| with gr.Row(): | |
| # STEP 1 PANEL (left) | |
| with gr.Column(scale=1, min_width=360): | |
| gr.Markdown("### Step 1: Choose direction to add green-screen padding") | |
| # Padding size belongs to Step 1 | |
| gr.Markdown("**Padding Size:**") | |
| padding_slider = gr.Slider( | |
| minimum=50, | |
| maximum=500, | |
| value=100, | |
| step=10, | |
| label="Green-Screen Padding Size (pixels)", | |
| info="Adjust how many pixels to add", | |
| ) | |
| with gr.Row(): | |
| btn_top = gr.Button("β¬οΈ Top", size="lg") | |
| with gr.Row(): | |
| btn_left = gr.Button("β¬ οΈ Left", size="lg") | |
| btn_right = gr.Button("β‘οΈ Right", size="lg") | |
| with gr.Row(): | |
| btn_bottom = gr.Button("β¬οΈ Bottom", size="lg") | |
| gr.Markdown("### Step 3: Outpaint the green-screen region") | |
| btn_outpaint = gr.Button( | |
| "π¨ Outpaint with Nano Banana", size="lg", variant="primary" | |
| ) | |
| gr.Markdown("### Optional: Reuse the outpainted result") | |
| btn_reuse = gr.Button("β»οΈ Reuse Outpainted Image") | |
| # STEP 2 PANEL (right) | |
| with gr.Column(scale=1, min_width=360): | |
| gr.Markdown("### Step 2: Customize Prompt (Optional)") | |
| prompt_textbox = gr.Textbox( | |
| label="Outpainting Prompt", | |
| placeholder="Click a direction button to see the default prompt, then edit if desired", | |
| value="", | |
| lines=6, | |
| info="Edit this prompt to control what Nano Banana generates.", | |
| show_label=True, | |
| ) | |
| # Wire events | |
| input_image.change(fn=get_image_info, inputs=input_image, outputs=image_info) | |
| btn_top.click( | |
| fn=lambda img, pad_size: preview_padding(img, "top", pad_size), | |
| inputs=[input_image, padding_slider], | |
| outputs=[padded_preview, side_state, padded_info, prompt_textbox], | |
| ).then(fn=lambda img: img, inputs=padded_preview, outputs=padded_state) | |
| btn_bottom.click( | |
| fn=lambda img, pad_size: preview_padding(img, "bottom", pad_size), | |
| inputs=[input_image, padding_slider], | |
| outputs=[padded_preview, side_state, padded_info, prompt_textbox], | |
| ).then(fn=lambda img: img, inputs=padded_preview, outputs=padded_state) | |
| btn_left.click( | |
| fn=lambda img, pad_size: preview_padding(img, "left", pad_size), | |
| inputs=[input_image, padding_slider], | |
| outputs=[padded_preview, side_state, padded_info, prompt_textbox], | |
| ).then(fn=lambda img: img, inputs=padded_preview, outputs=padded_state) | |
| btn_right.click( | |
| fn=lambda img, pad_size: preview_padding(img, "right", pad_size), | |
| inputs=[input_image, padding_slider], | |
| outputs=[padded_preview, side_state, padded_info, prompt_textbox], | |
| ).then(fn=lambda img: img, inputs=padded_preview, outputs=padded_state) | |
| btn_outpaint.click( | |
| fn=outpaint_with_nano_banana, | |
| inputs=[padded_state, side_state, prompt_textbox], | |
| outputs=[output_image, output_info, outpainted_state], | |
| ) | |
| btn_reuse.click( | |
| fn=reuse_outpainted_image, | |
| inputs=outpainted_state, | |
| outputs=[input_image, image_info, padded_preview, padded_info, padded_state, side_state], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(debug=True) | |