Init photobooth - borne photo evenementielle RPi4
Backend FastAPI complet : camera gphoto2, filtres/overlays Pillow, chroma key OpenCV, multi-shot/collage, GIF, impression CUPS, email SMTP, QR code. Frontend web vanilla (HTML/CSS/JS) pour Chromium kiosk. Menu admin cache (appui long coin ecran). Toutes les fonctionnalites activables/desactivables. Scripts install + systemd pour RPi4. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
142
backend/effects.py
Normal file
142
backend/effects.py
Normal file
@@ -0,0 +1,142 @@
|
||||
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]
|
||||
Reference in New Issue
Block a user