- 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>
159 lines
5.5 KiB
Python
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]
|