""" Gradio web app — wallpaper simulator on top of wallpaper_sim.py. Deploy on Hugging Face Spaces by uploading this file + wallpaper_sim.py, requirements.txt, README.md (with HF metadata header), and the motifs/ folder. """ from __future__ import annotations from pathlib import Path import cv2 import numpy as np # Defensive monkey-patch for a known gradio_client bug where the API schema # inference treats a JSON `additionalProperties: true` (a bool) as if it were # a dict, throwing TypeError. Must be applied BEFORE `import gradio`. 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_js2pt = _gc_utils._json_schema_to_python_type def _safe_js2pt(schema, defs=None): if not isinstance(schema, dict): return "Any" return _orig_js2pt(schema, defs) _gc_utils._json_schema_to_python_type = _safe_js2pt import gradio as gr from wallpaper_sim import ( load_image, occlusion_mask, quad_mask, render_all, ) HERE = Path(__file__).parent MOTIFS_DIR = HERE / "motifs" PRESET_PATTERNS: list[Path] = sorted( [p for p in MOTIFS_DIR.glob("*.*") if p.suffix.lower() in {".jpg", ".jpeg", ".png"}] ) if MOTIFS_DIR.exists() else [] PRESET_LABELS = {p.stem: str(p) for p in PRESET_PATTERNS} CORNER_NAMES = ["TL", "TR", "BR", "BL"] def empty_state() -> dict: return { "photo_rgb": None, "photo_bgr": None, "current": [], "walls": [], } def draw_overlay(state: dict) -> np.ndarray | None: if state is None or state["photo_rgb"] is None: return None img = state["photo_rgb"].copy() if state["walls"]: overlay = img.copy() for j, w in enumerate(state["walls"]): poly = np.array(w["quad"], dtype=np.int32) cv2.fillPoly(overlay, [poly], (220, 50, 50)) cv2.polylines(img, [poly], True, (255, 0, 0), 4) c = poly.mean(axis=0).astype(int) cv2.putText(img, f"#{j+1}", tuple(c), cv2.FONT_HERSHEY_SIMPLEX, 1.8, (255, 0, 0), 5) img = cv2.addWeighted(overlay, 0.18, img, 0.82, 0) pts = state["current"] if pts: pts_arr = np.array(pts, dtype=np.int32) if len(pts) >= 3: overlay = img.copy() cv2.fillPoly(overlay, [pts_arr], (60, 220, 60)) img = cv2.addWeighted(overlay, 0.25, img, 0.75, 0) cv2.polylines(img, [pts_arr], True, (0, 200, 0), 4) elif len(pts) == 2: cv2.polylines(img, [pts_arr], False, (0, 200, 0), 3) for i, p in enumerate(pts): cv2.circle(img, (int(p[0]), int(p[1])), 12, (0, 200, 0), -1) cv2.circle(img, (int(p[0]), int(p[1])), 12, (0, 0, 0), 2) cv2.putText(img, CORNER_NAMES[i], (int(p[0]) + 15, int(p[1]) - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 0), 4) cv2.putText(img, CORNER_NAMES[i], (int(p[0]) + 15, int(p[1]) - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2) return img def status_msg(state: dict) -> str: if state is None or state["photo_rgb"] is None: return "**Étape 1.** Upload une photo de la pièce." n = len(state["current"]) n_walls = len(state["walls"]) walls_info = f" ({n_walls} mur(s) déjà validé(s))" if n_walls else "" if n == 0: return ("**Étape 2.** Clique 4 coins du mur dans l'ordre : " "haut-gauche → haut-droite → bas-droite → bas-gauche." + walls_info) if n < 4: next_name = CORNER_NAMES[n] return f"**{n}/4 points placés.** Prochain : `{next_name}`." + walls_info return ("**4 coins posés.** Saisis la largeur du mur (cm) puis " "« Ajouter ce mur ». Recommence pour un autre mur, ou passe " "au rendu.") + walls_info def on_upload(image_rgb, state): state = empty_state() if image_rgb is None: return state, None, status_msg(state), gr.update(visible=False) state["photo_rgb"] = image_rgb state["photo_bgr"] = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR) return state, draw_overlay(state), status_msg(state), gr.update(visible=True) def on_click(state, evt: gr.SelectData): if state is None or state["photo_rgb"] is None: return state, None, status_msg(state) if len(state["current"]) >= 4: return state, draw_overlay(state), status_msg(state) x, y = int(evt.index[0]), int(evt.index[1]) state["current"].append((x, y)) return state, draw_overlay(state), status_msg(state) def undo_point(state): if state and state["current"]: state["current"].pop() return state, draw_overlay(state), status_msg(state) def reset_current(state): if state: state["current"] = [] return state, draw_overlay(state), status_msg(state) def add_wall(state, width_cm): if state is None or state["photo_bgr"] is None: return state, None, "Upload d'abord une photo." if len(state["current"]) != 4: return state, draw_overlay(state), "Place exactement 4 coins." quad = np.array(state["current"], dtype=np.float32) q = quad_mask(state["photo_bgr"].shape[:2], quad) mask = occlusion_mask(state["photo_bgr"], q, chroma_threshold=14, use_semantic=True, use_sam=False, strictness=2) state["walls"].append({ "quad": quad, "mask": mask, "width_cm": float(width_cm), }) state["current"] = [] return state, draw_overlay(state), status_msg(state) def render_walls(state, pattern_label, mode, density): if state is None or state["photo_bgr"] is None: return None, "Upload une photo d'abord." if not state["walls"]: return None, "Ajoute au moins un mur (4 coins + Largeur + « Ajouter »)." pattern_path = PRESET_LABELS.get(pattern_label) if pattern_path is None: return None, "Choisis un motif." pattern_bgr = load_image(Path(pattern_path)) canvas_bgr, _ = render_all( state["photo_bgr"], state["walls"], pattern_bgr, mode=mode, density=density if mode == "tile" else None, shading_strength=0.85, feather=2, chroma_threshold=14, auto_mask=True, ) canvas_rgb = cv2.cvtColor(canvas_bgr, cv2.COLOR_BGR2RGB) walls_info = " | ".join( f"mur {i+1}: {int(w['width_cm'])} cm" for i, w in enumerate(state["walls"]) ) if mode == "tile": info = (f"✅ Rendu OK — **Densité = {density} cm** " f"(achetable sur wellpapers.com au même numéro). {walls_info}") else: info = f"✅ Rendu panoramique — {walls_info}" return canvas_rgb, info with gr.Blocks(title="Simulateur Papier Peint — Wellpapers") as demo: state = gr.State(empty_state()) gr.Markdown( "# 🪟 Simulateur Papier Peint\n" "Upload une photo, clique les coins du mur, saisis ses dimensions, " "choisis un motif et une densité — la même échelle que sur " "wellpapers.com.") with gr.Row(): with gr.Column(scale=3): gr.Markdown("## 1. Photo") photo_in = gr.Image(type="numpy", label="Upload la photo", sources=["upload", "clipboard"], height=300) gr.Markdown("## 2. Coins du mur") picker = gr.Image(type="numpy", label="Clique 4 coins (TL → TR → BR → BL)", interactive=False, height=520) status = gr.Markdown(status_msg(empty_state())) with gr.Row(): btn_undo = gr.Button("↩ Annuler dernier point") btn_reset = gr.Button("✕ Réinitialiser ce mur") with gr.Row(visible=False) as wall_controls: wall_width = gr.Number(value=300, label="Largeur du mur (cm)", precision=0, minimum=50, maximum=2000) btn_add_wall = gr.Button("✓ Ajouter ce mur", variant="primary") with gr.Column(scale=2): gr.Markdown("## 3. Motif & densité") pattern_choices = list(PRESET_LABELS.keys()) pattern_dd = gr.Dropdown( choices=pattern_choices, value=pattern_choices[0] if pattern_choices else None, label="Motif", ) mode_radio = gr.Radio( ["tile", "panoramic"], value="tile", label="Mode", info="tile = motif répété ; panoramic = image unique étirée", ) density_slider = gr.Slider( 10, 100, value=40, step=1, label="Densité (cm) — largeur d'une répétition du motif", info="Même échelle que le slider 'Taille des motifs' sur wellpapers.com", ) btn_render = gr.Button("🎨 Rendre", variant="primary", size="lg") output = gr.Image(label="Résultat", height=520, interactive=False) photo_in.upload(on_upload, [photo_in, state], [state, picker, status, wall_controls]) picker.select(on_click, [state], [state, picker, status]) btn_undo.click(undo_point, [state], [state, picker, status]) btn_reset.click(reset_current, [state], [state, picker, status]) btn_add_wall.click(add_wall, [state, wall_width], [state, picker, status]) btn_render.click(render_walls, [state, pattern_dd, mode_radio, density_slider], [output, status]) if __name__ == "__main__": demo.launch()