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>
141 lines
4.5 KiB
Python
141 lines
4.5 KiB
Python
import io
|
|
import logging
|
|
import subprocess
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from backend.config import DOSSIER_PHOTOS, charger_config
|
|
|
|
log = logging.getLogger("photobooth.camera")
|
|
|
|
# Essayer d'importer gphoto2, sinon mode simulation
|
|
try:
|
|
import gphoto2 as gp
|
|
GPHOTO2_DISPONIBLE = True
|
|
except ImportError:
|
|
GPHOTO2_DISPONIBLE = False
|
|
log.warning("python-gphoto2 non installe, mode simulation active")
|
|
|
|
|
|
class Camera:
|
|
"""Controle de l'appareil DSLR via gphoto2."""
|
|
|
|
def __init__(self):
|
|
self.camera = None
|
|
self.contexte = None
|
|
self.connectee = False
|
|
|
|
def connecter(self) -> bool:
|
|
"""Connecte l'appareil photo."""
|
|
if not GPHOTO2_DISPONIBLE:
|
|
log.info("Mode simulation : camera virtuelle connectee")
|
|
self.connectee = True
|
|
return True
|
|
|
|
try:
|
|
self.contexte = gp.Context()
|
|
self.camera = gp.Camera()
|
|
self.camera.init(self.contexte)
|
|
self.connectee = True
|
|
log.info("Camera DSLR connectee")
|
|
return True
|
|
except gp.GPhoto2Error as e:
|
|
log.error(f"Erreur connexion camera : {e}")
|
|
self.connectee = False
|
|
return False
|
|
|
|
def deconnecter(self):
|
|
"""Deconnecte l'appareil photo."""
|
|
if self.camera and GPHOTO2_DISPONIBLE:
|
|
try:
|
|
self.camera.exit(self.contexte)
|
|
except gp.GPhoto2Error:
|
|
pass
|
|
self.camera = None
|
|
self.contexte = None
|
|
self.connectee = False
|
|
log.info("Camera deconnectee")
|
|
|
|
def capturer(self) -> Path | None:
|
|
"""Capture une photo et retourne le chemin du fichier."""
|
|
if not self.connectee:
|
|
log.error("Camera non connectee")
|
|
return None
|
|
|
|
horodatage = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
nom_fichier = f"photo_{horodatage}.jpg"
|
|
chemin_dest = DOSSIER_PHOTOS / nom_fichier
|
|
|
|
if not GPHOTO2_DISPONIBLE:
|
|
return self._capture_simulation(chemin_dest)
|
|
|
|
try:
|
|
chemin_camera = self.camera.capture(gp.GP_CAPTURE_IMAGE, self.contexte)
|
|
fichier_camera = self.camera.file_get(
|
|
chemin_camera.folder, chemin_camera.name, gp.GP_FILE_TYPE_NORMAL, self.contexte
|
|
)
|
|
fichier_camera.save(str(chemin_dest))
|
|
log.info(f"Photo capturee : {chemin_dest}")
|
|
return chemin_dest
|
|
except gp.GPhoto2Error as e:
|
|
log.error(f"Erreur capture : {e}")
|
|
return None
|
|
|
|
def preview(self) -> bytes | None:
|
|
"""Capture un apercu live (preview) en JPEG."""
|
|
if not self.connectee:
|
|
return None
|
|
|
|
if not GPHOTO2_DISPONIBLE:
|
|
return self._preview_simulation()
|
|
|
|
try:
|
|
fichier = self.camera.capture_preview(self.contexte)
|
|
donnees = fichier.get_data_and_size()
|
|
return bytes(donnees)
|
|
except gp.GPhoto2Error as e:
|
|
log.error(f"Erreur preview : {e}")
|
|
return None
|
|
|
|
def lister_appareils(self) -> list[str]:
|
|
"""Liste les appareils photo detectes."""
|
|
if not GPHOTO2_DISPONIBLE:
|
|
return ["[Simulation] Camera virtuelle"]
|
|
|
|
try:
|
|
appareils = gp.Camera.autodetect(self.contexte or gp.Context())
|
|
return [f"{nom} ({port})" for nom, port in appareils]
|
|
except gp.GPhoto2Error:
|
|
return []
|
|
|
|
def _capture_simulation(self, chemin: Path) -> Path:
|
|
"""Genere une image de test en mode simulation."""
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
img = Image.new("RGB", (1920, 1280), color=(40, 40, 40))
|
|
draw = ImageDraw.Draw(img)
|
|
texte = f"SIMULATION\n{datetime.now().strftime('%H:%M:%S')}"
|
|
draw.text((960, 640), texte, fill=(255, 255, 255), anchor="mm")
|
|
# Dessiner un cadre
|
|
draw.rectangle([50, 50, 1870, 1230], outline=(233, 30, 99), width=4)
|
|
img.save(chemin, "JPEG", quality=95)
|
|
log.info(f"Photo simulation : {chemin}")
|
|
return chemin
|
|
|
|
def _preview_simulation(self) -> bytes:
|
|
"""Genere un apercu de test en mode simulation."""
|
|
from PIL import Image, ImageDraw
|
|
|
|
img = Image.new("RGB", (640, 480), color=(30, 30, 30))
|
|
draw = ImageDraw.Draw(img)
|
|
draw.text((320, 240), "PREVIEW", fill=(200, 200, 200), anchor="mm")
|
|
draw.ellipse([300, 220, 340, 260], outline=(233, 30, 99), width=2)
|
|
buf = io.BytesIO()
|
|
img.save(buf, "JPEG", quality=70)
|
|
return buf.getvalue()
|
|
|
|
|
|
# Instance globale
|
|
camera = Camera()
|