Fotobox

Wir wollen heiraten. Im kleinen Kreis - und möglichst ohne allzu viel Tam-Tam. Aber eins, fanden wir, darf auf keinen Fall fehlen:

Eine Fotobox.

Und wenn du die horrenden Preise auf diversen Onlineportalen ebenso schmerzhaft findest wie wir, dann darfst du dich gern ein bisschen umsehen. Vielleicht findest du sogar wie ich den Mut, dir so ein Ding einfach selbst zu bauen.

Ganz ehrlich: Das Teil ist so individuell gewachsen, dass ich beim Schreiben dieses Blogs selbst mehrfach nachschauen musste, was ich da eigentlich zusammengedönert habe - und warum es überhaupt funktioniert

Daher ein kleiner Disclaimer vorweg: Manche der hier verwendeten Skripte bieten beste Voraussetzungen, um sich großkalibrig in den Fuß zu schießen, besonders, wenn das Teil nicht autark läuft.
Deshalb mein Rat: Auch wenn ich viele Vorlagen direkt auf dem Silbertablett serviere, kopiere bitte nicht alles stumpf 1:1 und erwarte sofortige Funktionalität.

Weiterhin verzichte ich in diesem Blog auf Erklärungen von ein paar Grundbefehlen, da diese eigentlich zu den Voraussetzungen gehören sollten, damit man sich sowas auch bauen kann.

So, los geht's.

Fotobox Ansicht 1 Fotobox Ansicht 2

Die Idee

Das Konzept lässt sich in wenigen Worten beschreiben: Die Fotobox soll tragbar sein und über ein kompaktes Design verfügen. Ein Touchdisplay sorgt für eine einfache Bedienung. Es gibt ein Vorschaubild vor der Aufnahme sowie die Möglichkeit, zwischen Einzelfotos und Collagen zu wählen. Bereits getätigte Aufnahmen können in einer Galerie angesehen werden. Außerdem lässt sich jedes Foto entweder direkt drucken oder per QR-Code bequem an ein Smartphone übertragen.

Die Hardware

Als Gehäuse dient ein ausrangierter Macintosh Classic - nicht nur, weil er hübsch ist, sondern weil ich zufällig günstig einen fand. Gerade groß genug, dass ein 8 Zoll Touchdisplay von Waveshare hineinpasst.

Das Display bietet eine praktische Halterung, an der ich einen Raspberry Pi 5 befestigt habe. Die Grundinstallation hierfür ist in diesem Blog nicht beschrieben, sollte aber in jedem Fall mit aktiviertem SSH-Zugriff erfolgen.
Für die Druckfunktion kommt ein Canon SELPHY CP1500 zum Einsatz - handlich, zuverlässig und per USB ansprechbar. Schön wäre es natürlich gewesen, hätte der auch im Gehäuse des Macintosh Platz gefunden. Hätte die Sache aber wahrscheinlich unnötig kompliziert gemacht, vor allem beim Wechseln des Farbbandes oder des Fotopapiers.

Damit der großzügige Innenraum des Gehäuses nicht ungenutzt bleibt (und die originalen Öffnungen für serielle Ports nicht traurig leer bleiben), habe ich ihn zusätzlich mit einer USB-Verlängerung samt passender Buchse sowie einem RJ45-Kabel mit Einbaubuchse ausgestattet.

Konfiguration des Displays

Der Einbau des Displays ist eigentlich nahezu selbsterklärend, die Kabel sind alle mitgeliefert und können direkt am Pi angeschlossen werden. Eine Schwierigkeit war allerdings, dass das Bild hochkant, also im Portraitmodus dargestellt wurde. Ich brauchte es aber im Landscape-Modus, also um 90 Grad gedreht. Und das möglichst bei jedem Boot ganz automatisch. Der Hersteller kam mit folgendem Code, der in der /boot/firmware/cmdline.txt ergänzt werden musste:

video=HDMI-A-1:768x1024@60,rotate=90 console=serial0,115200
console=tty1 root=PARTUUID=34834c29-02
rootfstype=ext4 fsck.repair=yes rootwait

Problem dabei ist, dass der Screen dabei beim Startup gedreht ist, aber sobald der User sich am System einloggt, der Desktop wieder im Portraitmodus, also wieder „hochkant“ dargestellt wird.

Das liegt daran, dass die Rotationseinstellungen in der /boot/firmware/cmdline.txt zwar den Kernel-Framebuffer (KMS) betreffen, diese jedoch nicht automatisch auf die Desktop-Umgebung (X11 oder Wayland) angewendet werden. GDM3 (GNOME Display Manager) und die Desktop-Umgebung haben separate Konfigurationsmechanismen für die Anzeige.

Um das Problem zu lösen, musste ich zunächst sicherstellen, dass GDM3 nicht Wayland verwendet, sondern X11 (wird auch später nochmal wichtig). Dazu wird in der Datei /etc/gdm3/daemon.conf der Eintrag WaylandEnable=false aktiviert:

[daemon]
WaylandEnable=false

Danach war ein Neustart nötig, damit diese Änderung greift. Allerdings reicht das allein nicht aus - man muss zusätzlich die Drehung für die Desktop-Session konfigurieren. Das geht am einfachsten mit xrandr, einem Werkzeug zum Einstellen von Bildschirmparametern unter X11.

Per SSH kann man dann testen, ob die Rotation wie gewünscht funktioniert. Dazu muss DISPLAY=:0 exportiert und anschließend xrandr aufgerufen werden:

export DISPLAY=:0
xrandr --output HDMI-1 --rotate right

Wenn das klappt, kann man die Rotation dauerhaft einrichten. Dazu habe ich ein kleines Skript erstellt:

#!/bin/bash
xrandr --output HDMI-1 --rotate right

Das Skript wird am besten als /usr/local/bin/rotate_display.sh abgelegt und ausführbar gemacht:

sudo chmod +x /usr/local/bin/rotate_display.sh

Anschließend richtet man einen Autostart-Eintrag ein, damit das Skript bei jedem Login automatisch ausgeführt wird. Dazu wird eine neue Datei unter /etc/xdg/autostart/rotate_display.desktop erstellt:

[Desktop Entry]
Type=Application
Exec=/usr/local/bin/rotate_display.sh
Hidden=false
X-GNOME-Autostart-enabled=true
Name=Rotate Display
Comment=Rotate the HDMI Display

Mit dieser Lösung dreht sich das Display nun zuverlässig bei jedem Login automatisch ins Querformat und die Darstellung passt endlich so, wie ich sie brauche.

Bitte fragt nicht, wie viele Stunden meines Lebens mich die Recherche und Konfiguration dafür gekostet hat. Völlig geisteskrank.

Kamera

Für die Bildquelle gibt’s zwei bequeme Wege: eine USB-Webcam (UVC) oder die Pi-Kamera. USB-Webcams sind plug-and-play und funktionieren in OpenCV sofort. Die Pi-Kamera bietet dafür oft bessere Latenz und Lichtausbeute, braucht aber den modernen libcamera/picamera2-Stack. Nimm das, was dir mechanisch und qualitativ am besten passt.

Tipps für gute Ergebnisse: vernünftige Ausleuchtung (ja, wirklich!), Belichtungszeit nicht zu lang, bei Webcams mit Fixfokus auf Abstand achten und Auto-WB/AE notfalls per API oder GStreamer/OpenCV justieren.

Variante A - USB-Webcam (am einfachsten):

import cv2
cap = cv2.VideoCapture(0)  # ggf. 1, wenn mehrere Geräte
ok, frame = cap.read()
if ok:
    # weiterverarbeiten/speichern
    pass

Variante B - Pi-Kamera (picamera2):

sudo apt install libcamera-apps
pip install picamera2
from picamera2 import Picamera2
picam2 = Picamera2()
picam2.configure(picam2.create_still_configuration())
picam2.start()
frame = picam2.capture_array()
# weiterverarbeiten/speichern

Die Software

Jetzt kommt der Punkt, an dem es auch eigentlich richtig spannend wird. Also mehr oder weniger.
Ich bin ehrlich: Die hier dargestellten Programmierungskünste (Python, HTML und JavaScript) übersteigen meine Qualifikationen als Systemintegrator. Die künstliche Intelligenz meines Vertrauens hat mich hierbei sehr unterstützt. Heißt natürlich nicht, dass immer alles auf Anhieb funktioniert hat und meine eigenen Gehirnzellen nicht auch strapaziert worden sind.

Zunächst einmal brauchen wir neben dem bereits installierten Raspberry Pi OS noch ein paar Werkzeuge: Apache als Webserver, Python 3 mit pip sowie ein paar wichtige Bibliotheken wie Flask, OpenCV und Pillow. Und jetzt kommt der Teil, bei dem PEP 668 ins Spiel kommt:

PEP 668 sorgt in neueren Python-Installationen dafür, dass pip keine Systempakete mehr direkt verändern darf. Das ist gut gemeint (Schutz vor kaputten Systembibliotheken), sorgt aber dafür, dass der klassische pip install …-Ansatz direkt ins Leere läuft. Die Lösung: Wir arbeiten in einer virtuellen Umgebung (venv), die komplett isoliert ist. Das schützt das System - und macht das ganze Setup sauberer.

Und all das machen wir in einem eigenen Ordner im Home-Verzeichnis, damit wir es schnell wiederfinden.

sudo apt update
cd ~
mkdir fotobox
cd fotobox
sudo apt install python3 python3-pip python3-venv apache2
python3 -m venv .venv
source .venv/bin/activate
pip install flask opencv-python-headless pillow qrcode[pil] gunicorn

Jetzt können wir fleißig drauf los programmieren und in unserem fotobox-Ordner die eigentliche Webanwendung app.py schreiben, das HTML-Frontend unter templates/index.html, sowie das passende Stylesheet unter static/styles.css.

Als Zusammenfassung: Damit Flask auf alle Dateien zugreifen kann, sollte die Verzeichnisstruktur wie folgt aussehen:

fotobox/
├── app.py
├── static/
│   └── styles.css
└── templates/
    └── index.html

Und damit wäre der erste Schritt auch schon getan und wir können entsprechende Rechte setzen, damit unsere Fotos auch gespeichert werden können - ohne „777“:

sudo mkdir -p /var/www/html/fotobox/fotos
sudo chown -R fb:www-data /var/www/html/fotobox
sudo chmod -R 750 /var/www/html/fotobox
sudo chmod 770 /var/www/html/fotobox/fotos

Dependencies

Was wir vor allem im Betrieb wollen: Anschalten und los. Ohne SSH, ohne Terminal, ohne Maus und Tastatur. Dazu richten wir drei Dinge ein: Systemdienst, Kiosk-Modus im Browser und „Always On“ (kein Screensaver, Cursor aus).

1) Systemdienst (Gunicorn)

Statt den Flask-Dev-Server zu nutzen, starten wir die App stabil über gunicorn und lassen sie von systemd beaufsichtigen:

sudo nano /etc/systemd/system/fotobox.service
[Unit]
Description=Fotobox (Gunicorn)
After=network-online.target
Wants=network-online.target

[Service]
User=fb
WorkingDirectory=/home/fb/fotobox
Environment=PATH=/home/fb/fotobox/.venv/bin
ExecStart=/home/fb/fotobox/.venv/bin/gunicorn -w 2 -b 127.0.0.1:5000 app:app
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable fotobox.service
sudo systemctl start fotobox.service
sudo systemctl status fotobox.service

2) Apache Konfiguration

Damit wir unsere Fotobox-Webapp nicht immer über http://localhost:5000 starten müssen, richten wir Apache als Reverse Proxy ein. Apache nimmt die Anfragen auf Port 80 entgegen und leitet sie an Gunicorn weiter. Zusätzlich erlauben wir den Zugriff auf den Foto-Ordner, damit die generierten Bilder über QR-Codes ausgeliefert werden können.

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo systemctl restart apache2
sudo nano /etc/apache2/sites-available/000-default.conf
<VirtualHost *:80>
    ServerName fotobox.local

    ProxyPass / http://127.0.0.1:5000/
    ProxyPassReverse / http://127.0.0.1:5000/

    <Directory "/var/www/html/fotobox/fotos">
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/fotobox_error.log
    CustomLog ${APACHE_LOG_DIR}/fotobox_access.log combined
</VirtualHost>
sudo systemctl reload apache2

Ab jetzt erreichst du die Fotobox bequem über http://fotobox.local oder die IP des Raspberry Pi.

3) Kiosk-Tools & Session-Start-Skript

Wir installieren Tools und starten Chromium im Kiosk-Modus. Cursor weg, Display bleibt wach.

sudo apt update
sudo apt install chromium-browser unclutter x11-xserver-utils avahi-daemon
mkdir -p ~/.local/bin
nano ~/.local/bin/fotobox-session.sh
#!/usr/bin/env bash
export DISPLAY=:0

# Bildschirm wach halten
xset s off
xset -dpms
xset s noblank

# Mauszeiger ausblenden
unclutter -idle 0.2 -root &

# Warten bis App lauscht
sleep 2

# Chromium im Kiosk
chromium-browser --noerrdialogs --disable-session-crashed-bubble \
  --disable-infobars --incognito --kiosk --app=http://localhost:5000
chmod +x ~/.local/bin/fotobox-session.sh

4) Autostart im Benutzer-Login (X11/GDM)

Auto-Login auf Desktop aktivieren (raspi-config) und dann Autostart-Eintrag setzen:

mkdir -p ~/.config/autostart
nano ~/.config/autostart/fotobox-kiosk.desktop
[Desktop Entry]
Type=Application
Name=Fotobox Kiosk
Comment=Startet Chromium im Kiosk-Modus
Exec=/home/fb/.local/bin/fotobox-session.sh
X-GNOME-Autostart-enabled=true

Drucken (CUPS)

Wenn ein Foto entsteht, soll es am Ende nicht nur auf dem Display hübsch aussehen, sondern am besten auch als kleines Andenken in der Hand landen. Dafür binden wir den Canon SELPHY CP1500 über CUPS ein - das ist der Druckdienst unter Linux. Idealerweise spricht der Drucker IPP bzw. IPP-over-USB, dann taucht er automatisch auf. Ansonsten helfen die Gutenprint-Treiber.

Ziel: Der Drucker wird als Systemdrucker registriert, und wir können aus Python schlicht mit lp drucken. Achte in CUPS auf das passende Medienformat (z. B. Postcard), randlosen Druck und einen sinnvollen Standard.

sudo apt install cups cups-client printer-driver-gutenprint ipp-usb
sudo usermod -aG lpadmin $USER
sudo systemctl enable --now cups

Danach im Browser http://localhost:631 öffnen, Drucker hinzufügen und als Standard setzen. Aus Python druckst du z. B. so:

import subprocess
# Namen in CUPS mit: lpstat -p -d
subprocess.run(["lp", "-d", "SELPHY_CP1500", "/var/www/html/fotobox/fotos/shot.jpg"])

QR-Codes

Nicht jeder will sofort drucken - viele möchten das Bild direkt aufs Handy. Dafür bekommt jedes gespeicherte Foto eine eigene URL (z. B. /fotos/DATEINAME.jpg), und wir generieren dazu einen QR-Code. Dieser Code verweist auf die lokale Adresse deiner Box (z. B. http://fotobox.local/fotos/… oder die IP).

Wichtig: Handy und Fotobox müssen im selben Netzwerk sein. Wenn du die Box als eigenen Hotspot betreibst, ist das am zuverlässigsten. Für die QR-Erzeugung nutzen wir das Python-Paket qrcode[pil].

pip install qrcode[pil]

Beispiel-Route in Flask: erzeugt on-the-fly ein PNG mit dem Link auf das gewünschte Foto.

from io import BytesIO
import qrcode
from flask import send_file, request

@app.route("/qr/<filename>")
def qr(filename):
    # zeigt auf den statisch auszuliefernden Foto-Pfad
    url = f"http://{request.host}/fotos/{filename}"
    img = qrcode.make(url)
    buf = BytesIO()
    img.save(buf, format="PNG")
    buf.seek(0)
    return send_file(buf, mimetype="image/png")

Alles zusammenführen: app.py & index.html

Jetzt bringen wir die Bausteine zusammen: Routen in app.py, ein schlankes Frontend in templates/index.html und ein paar JS-Helferlein. Ziel: Ein Klick → Foto → Vorschau → optional drucken oder per QR aufs Handy.

1) Minimaler Kern für app.py

Das Backend rendert die Startseite, macht ein Foto, liefert Dateien/QR-Codes aus und stößt den Druck an. Die Dateinamen sind Zeitstempel-basiert, und wir sperren die Kamera kurz mit einem Lock, damit nichts kollidiert. (Kamera-Implementierung: USB-Cam über OpenCV; PiCam via picamera2 - siehe Kamera-Abschnitt.)

from flask import Flask, render_template, request, jsonify, send_from_directory, abort
from datetime import datetime
from pathlib import Path
import subprocess, threading

app = Flask(__name__)

FOTO_DIR = Path("/var/www/html/fotobox/fotos")
FOTO_DIR.mkdir(parents=True, exist_ok=True)

camera_lock = threading.Lock()

def capture_one():
    """Nimmt ein Einzelbild auf und speichert es als JPG. 
    Ersetze den OpenCV-Teil durch picamera2, wenn du die Pi-Kamera nutzt."""
    import cv2
    with camera_lock:
        cap = cv2.VideoCapture(0)  # ggf. 1, wenn mehrere Kameras
        ok, frame = cap.read()
        cap.release()
    if not ok:
        raise RuntimeError("Kamera liefert kein Bild")
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"shot_{ts}.jpg"
    out = FOTO_DIR / filename
    cv2.imwrite(str(out), frame)
    return filename

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/capture", methods=["POST"])
def capture():
    mode = (request.json or {}).get("mode", "single")
    # TODO: Collage-Modus später mit Pillow zusammenbauen
    try:
        fname = capture_one()
        return jsonify({"ok": True, "filename": fname})
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)}), 500

@app.route("/print", methods=["POST"])
def do_print():
    data = request.json or {}
    fname = data.get("filename")
    if not fname or "/" in fname or ".." in fname:
        abort(400)
    path = FOTO_DIR / fname
    if not path.exists():
        abort(404)
    # Druck asynchron starten, damit das UI nicht blockiert
    subprocess.Popen(["lp", "-d", "SELPHY_CP1500", str(path)])
    return jsonify({"ok": True})

@app.route("/fotos/<path:filename>")
def fotos(filename):
    if "/" in filename or ".." in filename:
        abort(400)
    return send_from_directory(str(FOTO_DIR), filename)

@app.route("/healthz")
def health():
    return "ok", 200

# Wichtig: 'app' ist das WSGI-Objekt für gunicorn (ExecStart ... app:app)

Hinweis zu Apache: Wenn du willst, dass Apache die Fotos direkt (ohne Flask) ausliefert, ergänze in deiner vHost-Config eine Ausnahme und ein Alias:

# In <VirtualHost> vor ProxyPass / ...
ProxyPass /fotos !
Alias /fotos /var/www/html/fotobox/fotos
<Directory "/var/www/html/fotobox/fotos">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

2) Schlanke Startseite templates/index.html

Minimal-UI: Vorschau-Bild, zwei Aufnahme-Buttons (Einzel/Collage als Platzhalter), ein Druck-Button, ein Download-Link und der QR-Code zum Scannen. Das JS spricht die /capture- und /print-Routen an.

<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Fotobox</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>

  <main class="box">
    <div class="preview">
      <img id="preview" src="{{ url_for('static', filename='placeholder.jpg') }}" alt="Vorschau" />
    </div>

    <div class="controls">
      <button id="btnSingle" onclick="takePhoto('single')">Einzelfoto</button>
      <button id="btnCollage" onclick="takePhoto('collage')" disabled>Collage (bald)</button>
      <button id="btnPrint" onclick="printLast()">Drucken</button>
      <a id="dl" href="#" download>Download</a>
    </div>

    <div class="qrwrap">
      <img id="qr" alt="QR-Code erscheint nach Aufnahme">
    </div>
  </main>

  <script>
    let lastFilename = null;

    function toggle(disabled) {
      document.querySelectorAll('.controls button').forEach(b => b.disabled = disabled);
    }

    async function takePhoto(mode = 'single') {
      toggle(true);
      try {
        const res = await fetch('/capture', {
          method: 'POST',
          headers: {'Content-Type': 'application/json'},
          body: JSON.stringify({mode})
        });
        const data = await res.json();
        if (!data.ok) throw new Error(data.error || 'Unbekannter Fehler');
        lastFilename = data.filename;
        const url = `/fotos/${data.filename}`;
        document.getElementById('preview').src = url + '?t=' + Date.now();
        document.getElementById('dl').href = url;
        document.getElementById('qr').src = `/qr/${data.filename}`;
      } catch (e) {
        alert('Fehler bei der Aufnahme: ' + e.message);
      } finally {
        toggle(false);
      }
    }

    async function printLast() {
      if (!lastFilename) return alert('Noch kein Foto aufgenommen.');
      try {
        await fetch('/print', {
          method: 'POST',
          headers: {'Content-Type': 'application/json'},
          body: JSON.stringify({filename: lastFilename})
        });
        // Optional: kleines Toast/Feedback einbauen
      } catch (e) {
        alert('Druck fehlgeschlagen: ' + e.message);
      }
    }
  </script>

</body>
</html>

Wenn man es nun noch hübsch macht, kommt am Ende vielleicht ein UI raus, das ungefähr so wie meins aussieht:

Fotobox UI

3) Praxis-Tipps (die gern vergessen werden)

Fazit & Zukunftsperspektive

Wer bis hierhin durchgehalten hat: Respekt. Du hast jetzt den groben Bauplan, um eine eigene Fotobox ins Leben zu rufen - von der Hardware über die Software bis hin zu Browser-Kiosk und QR-Code-Trickserei. Klingt nach „einfach zusammenstecken“, fühlt sich in der Praxis aber eher an wie ein kleiner Hindernislauf mit Stolpersteinen aus Python-Umgebungen, Apache-Configs und nicht ganz kooperativen Druckertreibern.

Wichtig ist: Dieser Blog liefert Inspiration und Beispiele, keine Komplettlösung. Nicht, weil ich gemein bin - sondern weil jedes Setup eigene Macken hat: andere Kamera, andere Pi-Version, andere Rechte, andere Ordner.

Mein Rat: Nimm die Schnipsel, probier sie aus, lerne an den Fehlern (die werden kommen) und bau dir die Fotobox so, wie sie zu deinem Event passt. Am Ende zählt nicht, dass es „out of the box“ lief, sondern dass du irgendwann vor deinem DIY-Kasten stehst, den Auslöser drückst und die Leute lachen, wenn ihr Foto erscheint.

Und ob du das Ding jetzt erweitern willst, steht dir völlig frei.

Ich persönlich habe die Fotobox noch erweitert mit einer kleinen LED Anzeige, die darstellt, ob alle Systemjobs laufen und einem kleinen physischen Resetknopf, der diese zurücksetzt, wenn irgendwas gegen die Wand gefahren ist.
Des Weiteren ziert eine magnetische Halterung die Rückseite des Macs, auf welcher wiederum die Kamera gemountet ist.

Das Schöne an so einem DIY Projekt ist ja immer, das "nach oben hin" keine Grenzen gesetzt sind.

Also: Vielleicht stolperst du, vielleicht fluchst du, vielleicht lernst du dabei mehr über Linux und Webserver als dir lieb ist. Aber wenn es dann läuft… dann lohnt es sich. Versprochen :)