Spaces:
Running on Zero
Running on Zero
Download app.py from Matejstein/Photo2texture: direct link, hf CLI and curl.
- Browser
- Download file 15.5 kB
-
https://huggingface.co/spaces/Matejstein/Photo2texture/resolve/main/app.py
- Command line
-
hf download hf://spaces/Matejstein/Photo2texture/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Matejstein/Photo2texture/resolve/main/app.py
15.5 kB
| import spaces # MUST be line 1 for ZeroGPU | |
| # --------------------------------------------------------------------------- | |
| # GRADIO SCHEMA BUG PATCH (Prevents TypeError on boolean schema definitions) | |
| # --------------------------------------------------------------------------- | |
| import gradio_client.utils as gc_utils | |
| _orig_get_type = gc_utils.get_type | |
| def _safe_get_type(schema): | |
| if not isinstance(schema, dict): | |
| return "Any" | |
| return _orig_get_type(schema) | |
| gc_utils.get_type = _safe_get_type | |
| _orig_json_schema_to_python_type = gc_utils._json_schema_to_python_type | |
| def _safe_json_schema_to_python_type(schema, defs=None): | |
| if not isinstance(schema, dict): | |
| return "Any" | |
| return _orig_json_schema_to_python_type(schema, defs) | |
| gc_utils._json_schema_to_python_type = _safe_json_schema_to_python_type | |
| # --------------------------------------------------------------------------- | |
| import tempfile | |
| import os | |
| import random | |
| import torch | |
| import numpy as np | |
| from PIL import Image, ImageEnhance | |
| import gradio as gr | |
| from diffusers import StableDiffusionInpaintPipeline | |
| pipe_inpaint = None | |
| def get_inpaint_pipeline(): | |
| global pipe_inpaint | |
| if pipe_inpaint is None: | |
| pipe_inpaint = StableDiffusionInpaintPipeline.from_pretrained( | |
| "runwayml/stable-diffusion-inpainting", | |
| torch_dtype=torch.float16, | |
| safety_checker=None | |
| ) | |
| pipe_inpaint.enable_attention_slicing() | |
| return pipe_inpaint | |
| def shift_periodic_canvas(pil_img, shift_x_pct, shift_y_pct): | |
| """ | |
| Moves/rolls the periodic texture along X and Y axes by custom percentages. | |
| Allows dynamic positioning of any seam or feature across the canvas. | |
| """ | |
| if pil_img is None: | |
| return None | |
| arr = np.array(pil_img.convert("RGB")) | |
| h, w, _ = arr.shape | |
| shift_x = int(w * (shift_x_pct / 100.0)) | |
| shift_y = int(h * (shift_y_pct / 100.0)) | |
| rolled = np.roll(np.roll(arr, shift_x, axis=1), shift_y, axis=0) | |
| return Image.fromarray(rolled) | |
| def create_smooth_cosine_mask(width, height, seam_pct_x=0.25, seam_pct_y=0.35, mode="Full 2D (Both Axes)"): | |
| """C1-continuous smooth cosine crosshair mask.""" | |
| x = np.linspace(-0.5, 0.5, width) | |
| y = np.linspace(-0.5, 0.5, height) | |
| xx, yy = np.meshgrid(x, y) | |
| rx = seam_pct_x / 2.0 | |
| ry = seam_pct_y / 2.0 | |
| sx = np.zeros_like(xx) | |
| mask_x = np.abs(xx) <= rx | |
| sx[mask_x] = 0.5 * (1.0 + np.cos(np.pi * np.abs(xx[mask_x]) / rx)) | |
| if "Horizontal" in mode: | |
| mask_final = sx | |
| else: | |
| sy = np.zeros_like(yy) | |
| mask_y = np.abs(yy) <= ry | |
| sy[mask_y] = 0.5 * (1.0 + np.cos(np.pi * np.abs(yy[mask_y]) / ry)) | |
| mask_final = 1.0 - (1.0 - sx) * (1.0 - sy) | |
| mask_uint8 = (np.clip(mask_final, 0, 1) * 255).astype(np.uint8) | |
| return Image.fromarray(mask_uint8, mode="L") | |
| def extract_brush_mask(editor_data): | |
| """Extracts background image and painted red brush layer from Gradio ImageEditor.""" | |
| if editor_data is None: | |
| return None, None | |
| if isinstance(editor_data, dict): | |
| bg = editor_data.get("background") or editor_data.get("composite") | |
| if bg is None: | |
| return None, None | |
| bg = bg.convert("RGB") | |
| layers = editor_data.get("layers", []) | |
| if layers and len(layers) > 0: | |
| combined_alpha = np.zeros((bg.height, bg.width), dtype=np.uint8) | |
| for layer in layers: | |
| if layer is not None: | |
| layer_rgba = layer.convert("RGBA") | |
| if layer_rgba.size != (bg.width, bg.height): | |
| layer_rgba = layer_rgba.resize((bg.width, bg.height), Image.Resampling.NEAREST) | |
| alpha = np.array(layer_rgba.split()[-1]) | |
| combined_alpha = np.maximum(combined_alpha, alpha) | |
| if np.max(combined_alpha) > 0: | |
| mask_arr = np.where(combined_alpha > 10, 255, 0).astype(np.uint8) | |
| return bg, Image.fromarray(mask_arr, mode="L") | |
| return editor_data.convert("RGB"), None | |
| def create_tiled_preview(pil_img, mode="Horizontal Only (Landscapes/City Facades)"): | |
| preview_img = pil_img.copy() | |
| if preview_img.width > 1024: | |
| preview_img = preview_img.resize((1024, 1024), Image.Resampling.LANCZOS) | |
| w, h = preview_img.size | |
| if "Horizontal" in mode: | |
| tiled = Image.new("RGB", (w * 3, h)) | |
| for c in range(3): | |
| tiled.paste(preview_img, (c * w, 0)) | |
| else: | |
| tiled = Image.new("RGB", (w * 3, h * 3)) | |
| for r in range(3): | |
| for c in range(3): | |
| tiled.paste(preview_img, (c * w, r * h)) | |
| return tiled | |
| def export_files(scaled_result, resolution_str, ut99_quantize): | |
| res_val = int(resolution_str.split("x")[0]) | |
| temp_dir = tempfile.mkdtemp() | |
| pcx_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.pcx") | |
| bmp_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.bmp") | |
| if ut99_quantize: | |
| quantized = scaled_result.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) | |
| quantized.save(pcx_path, format="PCX") | |
| quantized.save(bmp_path, format="BMP") | |
| display_img = quantized.convert("RGB") | |
| else: | |
| scaled_result.save(pcx_path, format="PCX") | |
| scaled_result.save(bmp_path, format="BMP") | |
| display_img = scaled_result | |
| return display_img, pcx_path, bmp_path | |
| def auto_generate_seamless( | |
| input_image, prompt, negative_prompt, tiling_mode, | |
| shift_x_pct, shift_y_pct, seam_width_pct, seam_height_pct, steps, guidance_scale, seed_val, | |
| sharpness_boost, resolution_str, ut99_quantize | |
| ): | |
| if input_image is None: | |
| return None, None, None, None, None, "⚠️ Please upload a source photo first." | |
| actual_seed = random.randint(0, 2147483647) if (seed_val == -1 or seed_val is None) else int(seed_val) | |
| res_val = int(resolution_str.split("x")[0]) | |
| pipeline = get_inpaint_pipeline() | |
| pipeline.to("cuda") | |
| generator = torch.Generator("cuda").manual_seed(actual_seed) | |
| original_photo = input_image.convert("RGB") | |
| proc_photo = original_photo.resize((512, 512), Image.Resampling.LANCZOS) | |
| # Apply user-selected X and Y periodic movement/roll | |
| shifted_photo = shift_periodic_canvas(proc_photo, shift_x_pct, shift_y_pct) | |
| user_steps = max(10, int(steps)) | |
| user_guidance = float(guidance_scale) | |
| photo_prompt = prompt + ", raw photo, ultra realistic, sharp photographic detail, DSLR 8k, crisp texture, natural lighting" | |
| strict_negative = negative_prompt + ", oil painting, painting, illustration, drawing, 3d render, anime, smooth, blurry, artistic, cartoon, canvas texture, digital art, vector" | |
| seam_mask_cosine = create_smooth_cosine_mask( | |
| 512, 512, | |
| seam_pct_x=seam_width_pct / 100.0, | |
| seam_pct_y=seam_height_pct / 100.0, | |
| mode=tiling_mode | |
| ) | |
| inpainted_zone = pipeline( | |
| prompt=photo_prompt, | |
| negative_prompt=strict_negative, | |
| image=shifted_photo, | |
| mask_image=seam_mask_cosine, | |
| num_inference_steps=user_steps, | |
| guidance_scale=user_guidance, | |
| generator=generator | |
| ).images[0] | |
| seamless_result = Image.composite(inpainted_zone, shifted_photo, seam_mask_cosine) | |
| scaled_result = seamless_result.resize((res_val, res_val), Image.Resampling.LANCZOS) | |
| if sharpness_boost > 1.0: | |
| scaled_result = ImageEnhance.Sharpness(scaled_result).enhance(sharpness_boost) | |
| tiled_preview = create_tiled_preview(scaled_result, mode=tiling_mode) | |
| display_img, pcx_path, bmp_path = export_files(scaled_result, resolution_str, ut99_quantize) | |
| editor_update = {"background": scaled_result, "layers": [], "composite": scaled_result} | |
| status_msg = f"✅ Seamless Complete | Shift X: {shift_x_pct}%, Shift Y: {shift_y_pct}% | Seed: {actual_seed}" | |
| return editor_update, display_img, tiled_preview, pcx_path, bmp_path, status_msg | |
| def manual_edge_repair( | |
| editor_data, prompt, negative_prompt, tiling_mode, | |
| steps, guidance_scale, seed_val, sharpness_boost, resolution_str, ut99_quantize | |
| ): | |
| bg_img, mask_img = extract_brush_mask(editor_data) | |
| if bg_img is None or mask_img is None: | |
| return None, None, None, None, None, "⚠️ Paint red brush strokes over problem areas first." | |
| actual_seed = random.randint(0, 2147483647) if (seed_val == -1 or seed_val is None) else int(seed_val) | |
| res_val = int(resolution_str.split("x")[0]) | |
| pipeline = get_inpaint_pipeline() | |
| pipeline.to("cuda") | |
| generator = torch.Generator("cuda").manual_seed(actual_seed) | |
| init_512 = bg_img.resize((512, 512), Image.Resampling.LANCZOS) | |
| mask_512 = mask_img.resize((512, 512), Image.Resampling.NEAREST) | |
| user_steps = max(10, int(steps)) | |
| user_guidance = float(guidance_scale) | |
| photo_prompt = prompt + ", raw photo, ultra realistic, sharp photographic detail, DSLR 8k, seamless texture blending" | |
| strict_negative = negative_prompt + ", oil painting, painting, illustration, drawing, 3d render, anime, smooth, blurry, artistic, cartoon" | |
| repaired_512 = pipeline( | |
| prompt=photo_prompt, | |
| negative_prompt=strict_negative, | |
| image=init_512, | |
| mask_image=mask_512, | |
| num_inference_steps=user_steps, | |
| guidance_scale=user_guidance, | |
| generator=generator | |
| ).images[0] | |
| scaled_result = repaired_512.resize((res_val, res_val), Image.Resampling.LANCZOS) | |
| if sharpness_boost > 1.0: | |
| scaled_result = ImageEnhance.Sharpness(scaled_result).enhance(sharpness_boost) | |
| tiled_preview = create_tiled_preview(scaled_result, mode=tiling_mode) | |
| display_img, pcx_path, bmp_path = export_files(scaled_result, resolution_str, ut99_quantize) | |
| editor_update = {"background": scaled_result, "layers": [], "composite": scaled_result} | |
| status_msg = f"🩹 Edge Repair Complete | Seed: {actual_seed}" | |
| return editor_update, display_img, tiled_preview, pcx_path, bmp_path, status_msg | |
| def update_canvas_roll(editor_data, shift_x_pct, shift_y_pct): | |
| """Interactive button action to roll/move periodic texture tiles live on the editor canvas.""" | |
| if editor_data is None: | |
| return None | |
| if isinstance(editor_data, dict): | |
| bg = editor_data.get("background") or editor_data.get("composite") | |
| if bg is None: | |
| return None | |
| pil_img = bg.convert("RGB") | |
| else: | |
| pil_img = editor_data.convert("RGB") | |
| rolled = shift_periodic_canvas(pil_img, shift_x_pct, shift_y_pct) | |
| return {"background": rolled, "layers": [], "composite": rolled} | |
| # Mobile Clean Layout | |
| mobile_css = """ | |
| .gradio-container { padding: 6px !important; max-width: 100% !important; } | |
| .mobile-btn { width: 100% !important; font-size: 16px !important; padding: 12px !important; } | |
| """ | |
| with gr.Blocks(title="UT99 Mobile Texture Forge", css=mobile_css) as demo: | |
| gr.Markdown("### 📱 UT99 ZeroGPU Mobile Texture Forge") | |
| output_status = gr.Textbox(label="Status Log", value="Ready", interactive=False) | |
| with gr.Tabs(): | |
| # TAB 1: GENERATE & PERIODIC MOVEMENT | |
| with gr.Tab("1. Generate & Move Tiles"): | |
| input_img = gr.Image(type="pil", label="Upload Photo") | |
| tiling_mode = gr.Radio( | |
| choices=["Horizontal Only (Landscapes/City Facades)", "Full 2D (Both Axes)"], | |
| value="Full 2D (Both Axes)", | |
| label="Seamless Mode" | |
| ) | |
| gr.Markdown("#### ↔️ Periodic Tile Movement (Move seams/tiles on canvas)") | |
| with gr.Row(): | |
| shift_x_pct = gr.Slider(minimum=0, maximum=100, value=50, step=5, label="Move X-Axis (%)") | |
| shift_y_pct = gr.Slider(minimum=0, maximum=100, value=50, step=5, label="Move Y-Axis (%)") | |
| prompt = gr.Textbox( | |
| value="Detailed fabric curtain texture, sharp photographic pattern", | |
| label="AI Prompt" | |
| ) | |
| negative_prompt = gr.Textbox( | |
| value="seam line, border, frame, edge line, blur, watermark, repeating grid", | |
| label="Negative Prompt" | |
| ) | |
| with gr.Accordion("Advanced Options (Seam Mask Widths, CFG, Seed)", open=False): | |
| with gr.Row(): | |
| seam_width_pct = gr.Slider(minimum=10, maximum=50, value=30, step=5, label="Horiz Seam (%)") | |
| seam_height_pct = gr.Slider(minimum=10, maximum=60, value=35, step=5, label="Vert Seam (%)") | |
| with gr.Row(): | |
| steps = gr.Slider(minimum=10, maximum=50, value=25, step=1, label="Steps") | |
| guidance_scale = gr.Slider(minimum=1.0, maximum=15.0, value=7.5, step=0.5, label="CFG") | |
| with gr.Row(): | |
| sharpness_boost = gr.Slider(minimum=1.0, maximum=2.5, value=1.3, step=0.1, label="Sharpness") | |
| seed_val = gr.Number(value=-1, precision=0, label="Seed (-1 = Random)") | |
| resolution = gr.Dropdown( | |
| choices=["128x128", "256x256 (Default UT99)", "512x512 (HD UT99)", "1024x1024", "2048x2048 (2K)", "4096x4096 (4K)", "8192x8192 (8K)"], | |
| value="1024x1024", | |
| label="Resolution" | |
| ) | |
| ut99_quantize = gr.Checkbox(value=False, label="Convert to 8-Bit / 256 Colors (.PCX / .BMP)") | |
| btn_auto = gr.Button("🚀 Generate Seamless Texture", variant="primary", elem_classes=["mobile-btn"]) | |
| # TAB 2: MANUAL CANVAS TOUCH-UP | |
| with gr.Tab("2. Touch-Up & Move Canvas"): | |
| gr.Markdown("Draw red brush strokes over problem areas, or move the canvas tiles:") | |
| with gr.Row(): | |
| btn_roll_live = gr.Button("↔️ Roll / Move Canvas Tiles", variant="secondary") | |
| editor = gr.ImageEditor( | |
| type="pil", | |
| label="Touch-Up Canvas", | |
| brush=gr.Brush(colors=["#FF0000"], default_size=15), | |
| interactive=True | |
| ) | |
| btn_repair = gr.Button("🩹 Repair Red Painted Lines", variant="primary", elem_classes=["mobile-btn"]) | |
| # TAB 3: PREVIEW & DOWNLOAD | |
| with gr.Tab("3. Preview & Export"): | |
| output_texture = gr.Image(type="pil", label="Seamless Texture Output") | |
| output_preview = gr.Image(type="pil", label="Tiling Preview Test") | |
| with gr.Row(): | |
| pcx_file = gr.File(label="Download .PCX") | |
| bmp_file = gr.File(label="Download .BMP") | |
| # Action Connections | |
| btn_auto.click( | |
| fn=auto_generate_seamless, | |
| inputs=[ | |
| input_img, prompt, negative_prompt, tiling_mode, | |
| shift_x_pct, shift_y_pct, seam_width_pct, seam_height_pct, | |
| steps, guidance_scale, seed_val, sharpness_boost, resolution, ut99_quantize | |
| ], | |
| outputs=[editor, output_texture, output_preview, pcx_file, bmp_file, output_status] | |
| ) | |
| btn_roll_live.click( | |
| fn=update_canvas_roll, | |
| inputs=[editor, shift_x_pct, shift_y_pct], | |
| outputs=[editor] | |
| ) | |
| btn_repair.click( | |
| fn=manual_edge_repair, | |
| inputs=[ | |
| editor, prompt, negative_prompt, tiling_mode, | |
| steps, guidance_scale, seed_val, sharpness_boost, resolution, ut99_quantize | |
| ], | |
| outputs=[editor, output_texture, output_preview, pcx_file, bmp_file, output_status] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(show_api=False) | |