import logging from io import BytesIO from pathlib import Path from PIL import Image, ImageEnhance, ImageFilter, ImageOps from backend.config import DOSSIER_OVERLAYS, DOSSIER_EXPORTS, DOSSIER_FONDS log = logging.getLogger("photobooth.effects") # Filtres disponibles FILTRES = { "original": "Original", "nb": "Noir & Blanc", "sepia": "Sepia", "vintage": "Vintage", "contraste": "Contraste fort", "lumineux": "Lumineux", "chaud": "Tons chauds", "froid": "Tons froids", "flou_artistique": "Flou artistique", } def appliquer_filtre(chemin_photo: Path, filtre: str) -> Path: """Applique un filtre a une photo et sauvegarde dans exports.""" img = Image.open(chemin_photo) if filtre == "nb": img = ImageOps.grayscale(img).convert("RGB") elif filtre == "sepia": img = ImageOps.grayscale(img) img = ImageOps.colorize(img, black="#704214", white="#f0e0c0") elif filtre == "vintage": img = ImageOps.grayscale(img) img = ImageOps.colorize(img, black="#2b1700", white="#f5deb3") enhancer = ImageEnhance.Contrast(img) img = enhancer.enhance(0.85) elif filtre == "contraste": enhancer = ImageEnhance.Contrast(img) img = enhancer.enhance(1.5) elif filtre == "lumineux": enhancer = ImageEnhance.Brightness(img) img = enhancer.enhance(1.3) elif filtre == "chaud": r, g, b = img.split() r = r.point(lambda x: min(255, x + 20)) b = b.point(lambda x: max(0, x - 20)) img = Image.merge("RGB", (r, g, b)) elif filtre == "froid": r, g, b = img.split() r = r.point(lambda x: max(0, x - 20)) b = b.point(lambda x: min(255, x + 20)) img = Image.merge("RGB", (r, g, b)) elif filtre == "flou_artistique": img = img.filter(ImageFilter.GaussianBlur(radius=2)) nom_export = f"{chemin_photo.stem}_{filtre}.jpg" chemin_export = DOSSIER_EXPORTS / nom_export img.save(chemin_export, "JPEG", quality=95) log.info(f"Filtre '{filtre}' applique : {chemin_export}") return chemin_export def appliquer_overlay(chemin_photo: Path, nom_overlay: str) -> Path: """Applique un overlay PNG par-dessus la photo.""" chemin_overlay = DOSSIER_OVERLAYS / nom_overlay if not chemin_overlay.exists(): log.error(f"Overlay introuvable : {chemin_overlay}") return chemin_photo photo = Image.open(chemin_photo).convert("RGBA") overlay = Image.open(chemin_overlay).convert("RGBA") overlay = overlay.resize(photo.size, Image.LANCZOS) composite = Image.alpha_composite(photo, overlay) resultat = composite.convert("RGB") nom_export = f"{chemin_photo.stem}_overlay.jpg" chemin_export = DOSSIER_EXPORTS / nom_export resultat.save(chemin_export, "JPEG", quality=95) log.info(f"Overlay '{nom_overlay}' applique : {chemin_export}") return chemin_export def chroma_key(chemin_photo: Path, nom_fond: str | None = None, couleur_cle: str = "#00ff00", tolerance: int = 40) -> Path: """Remplace le fond vert par un fond choisi.""" try: import cv2 import numpy as np except ImportError: log.error("OpenCV non installe, chroma key indisponible") return chemin_photo img = cv2.imread(str(chemin_photo)) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # Plage de vert pour le chroma key vert_bas = np.array([35 - tolerance // 2, 50, 50]) vert_haut = np.array([85 + tolerance // 2, 255, 255]) masque = cv2.inRange(hsv, vert_bas, vert_haut) # Adoucir le masque masque = cv2.GaussianBlur(masque, (5, 5), 0) # Charger le fond if nom_fond: chemin_fond = DOSSIER_FONDS / nom_fond if chemin_fond.exists(): fond = cv2.imread(str(chemin_fond)) fond = cv2.resize(fond, (img.shape[1], img.shape[0])) else: fond = np.full_like(img, (200, 200, 200)) else: fond = np.full_like(img, (200, 200, 200)) # Combiner masque_inv = cv2.bitwise_not(masque) premier_plan = cv2.bitwise_and(img, img, mask=masque_inv) arriere_plan = cv2.bitwise_and(fond, fond, mask=masque) resultat = cv2.add(premier_plan, arriere_plan) nom_export = f"{chemin_photo.stem}_chroma.jpg" chemin_export = DOSSIER_EXPORTS / nom_export cv2.imwrite(str(chemin_export), resultat) log.info(f"Chroma key applique : {chemin_export}") return chemin_export def lister_overlays() -> list[str]: """Liste les overlays disponibles.""" if not DOSSIER_OVERLAYS.exists(): return [] return [f.name for f in DOSSIER_OVERLAYS.iterdir() if f.suffix.lower() == ".png"] def lister_fonds() -> list[str]: """Liste les fonds disponibles pour le chroma key.""" if not DOSSIER_FONDS.exists(): return [] extensions = {".jpg", ".jpeg", ".png"} return [f.name for f in DOSSIER_FONDS.iterdir() if f.suffix.lower() in extensions]