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:
140
backend/camera.py
Normal file
140
backend/camera.py
Normal file
@@ -0,0 +1,140 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user