Photo2texture / app.py
Matejstein's picture
Create app.py
c4402ae verified
Raw History Blame Contribute Delete
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
@spaces.GPU
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
@spaces.GPU
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)