Files
photobooth/backend/effects.py
Jules e3d1d9cc07 Impression Mitsubishi K60 + cadres + interface admin distante
- Intégration selphy_print (backend CUPS dyesub Mitsubishi CP-K60DW-S)
- Formats impression : 15x20, 10x15, 10x15 2-strips avec cadre et orientation portrait/paysage
- Cadres d'impression par format (strip/10x15/15x20) : upload, sélection, aperçu live
- Cadres démo générés : pellicule noir/vintage/couleurs + bordures classique/doré/rose gold
- Écran de choix de cadre avant capture (clic direct, aperçu grand format)
- Filtres réduits à 3 (Couleur, N&B, Sépia) et appliqués sur tous les strips
- Port 80 via authbind, route /admin avec redirection auto depuis poste distant
- Autologin LightDM corrigé (pam-autologin-service)
- Bouton évacuer bourrage papier dans admin
- Fix : _gp_lock manquant dans Camera.__init__

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 21:38:31 +02:00

159 lines
5.5 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) -> Image.Image:
"""Composite un cadre PNG par-dessus l'image assemblée."""
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")
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]