Spaces:
Sleeping
Sleeping
| """ | |
| 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() |