Files
photobooth/backend/effects.py
Jules 9ce8fc0e55 Gestion evenements + cadres par event + fix cadre paysage + dossier non-imprimees
- CRUD evenements : creer, activer, editer, supprimer dans panneau admin
- Cadres specifiques par evenement (aucun/disponible/defaut/impose)
- Fix cadre paysage deformé : rotation auto si orientation ne correspond pas
- Photos non imprimees vont dans sous-dossier "non_imprimees" (USB/FTP)
  au lieu d'etre ignorees ; booth recoit toujours toutes les photos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-23 00:16:35 +02:00

170 lines
6.0 KiB
Python

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, DOSSIER_CADRES
log = logging.getLogger("photobooth.effects")
# Filtres disponibles
FILTRES = {
"original": "Couleur",
"nb": "Noir & Blanc",
"sepia": "Sepia",
}
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 appliquer_cadre_impression(img: Image.Image, format_papier: str, nom_cadre: str, event_id: str | None = None) -> Image.Image:
"""Composite un cadre PNG par-dessus l'image assemblée."""
chemin = None
if event_id:
from backend.evenements import DOSSIER_EVENEMENTS
chemin_event = DOSSIER_EVENEMENTS / event_id / "cadres" / format_papier / nom_cadre
if chemin_event.exists():
chemin = chemin_event
if chemin is None:
chemin = DOSSIER_CADRES / format_papier / nom_cadre
if not chemin.exists():
log.warning(f"Cadre introuvable : {chemin}")
return img
cadre = Image.open(chemin).convert("RGBA")
img_portrait = img.height > img.width
cadre_portrait = cadre.height > cadre.width
if img_portrait != cadre_portrait:
cadre = cadre.rotate(90, expand=True)
if cadre.size != img.size:
cadre = cadre.resize(img.size, Image.LANCZOS)
base = img.convert("RGBA")
composite = Image.alpha_composite(base, cadre)
return composite.convert("RGB")
def lister_cadres(format_papier: str) -> list[str]:
"""Liste les cadres disponibles pour un format donné."""
dossier = DOSSIER_CADRES / format_papier
if not dossier.exists():
return []
return sorted(f.name for f in dossier.iterdir() if f.suffix.lower() == ".png")
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]