wellpapers / app.py
thibproduct's picture
Update app.py
0169d95 verified
Raw
History Blame Contribute Delete
9.76 kB
"""
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()