commit 75537258a72d14f2321419bff5a31c1feb4c7138 Author: Thies Mueller Date: Thu Apr 23 18:38:11 2026 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96d277d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.venv/ +config.json +assets/logo.png +photos/ \ No newline at end of file diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..c638a6e --- /dev/null +++ b/config.json.example @@ -0,0 +1,25 @@ +{ + "ftp": { + "host": "ftp.example.com", + "user": "username", + "password": "password", + "base_url": "https://photobox.example.com/images/" + }, + "printer_name": "DRUCKER", + "photo_path": "./photos/", + "buttons": { + "capture": "1", + "save": "2", + "print": "3" + }, + "logo": { + "enabled": true, + "path": "assets/logo.png", + "position": "top_right", + "scale": 0.25, + "margin": 30 + }, + "camera": { + "last_used": null + } +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..7ab1228 --- /dev/null +++ b/main.py @@ -0,0 +1,300 @@ +import sys +import cv2 +import json +import time +import os +from ftplib import FTP + +from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QVBoxLayout +from PyQt6.QtGui import QImage, QPixmap, QPainter, QFont, QColor +from PyQt6.QtCore import QTimer, Qt + +import qrcode + +CONFIG_PATH = "config.json" + +def load_config(): + with open(CONFIG_PATH) as f: + return json.load(f) + +def save_config(cfg): + with open(CONFIG_PATH, "w") as f: + json.dump(cfg, f, indent=2) + +CONFIG = load_config() + +def create_capture(index): + backends = [] + + if sys.platform == "darwin": + backends = [ + cv2.CAP_AVFOUNDATION, + cv2.CAP_ANY + ] + elif os.name == "nt": + backends = [ + cv2.CAP_DSHOW, + cv2.CAP_MSMF, + cv2.CAP_ANY + ] + else: + backends = [ + cv2.CAP_V4L2, + cv2.CAP_ANY + ] + + for backend in backends: + cap = cv2.VideoCapture(index, backend) + + if not cap.isOpened(): + continue + + for _ in range(10): + ret, frame = cap.read() + if ret and frame is not None: + return cap + + cap.release() + + return None + +def find_cameras(): + working = [] + + for i in range(2): + cap = create_capture(i) + + if cap is not None: + working.append(i) + cap.release() + + return working + +class CameraSelect(QWidget): + def __init__(self, cameras): + super().__init__() + self.setWindowTitle("Kamera auswählen") + self.setGeometry(200, 200, 400, 300) + + self.selected = None + + layout = QVBoxLayout() + + for cam in cameras: + btn = QPushButton(f"Kamera {cam}") + btn.clicked.connect(lambda _, c=cam: self.select(c)) + layout.addWidget(btn) + + self.setLayout(layout) + + def select(self, cam): + self.selected = cam + self.close() + +LIVE, COUNTDOWN, PHOTO, QR = range(4) + +class PhotoApp(QWidget): + def __init__(self): + super().__init__() + + self.setWindowTitle("Photo Booth") + self.showFullScreen() + + self.label = QLabel(self) + self.label.setGeometry(0, 0, 1920, 1080) + + self.cap = None + self.init_camera() + + self.state = LIVE + self.current_frame = None + self.photo = None + self.qr_img = None + + self.countdown = 0 + + self.timer = QTimer() + self.timer.timeout.connect(self.update_frame) + self.timer.start(30) + + self.countdown_timer = QTimer() + self.countdown_timer.timeout.connect(self.update_countdown) + + def init_camera(self): + cams = find_cameras() + + last = CONFIG.get("camera", {}).get("last_used") + + if last in cams: + cam_index = last + else: + selector = CameraSelect(cams) + selector.show() + while selector.isVisible(): + QApplication.processEvents() + cam_index = selector.selected + + CONFIG["camera"]["last_used"] = cam_index + save_config(CONFIG) + + self.cap = create_capture(cam_index) + + if self.cap is None: + sys.exit(1) + + for _ in range(10): + self.cap.read() + + if not self.cap.isOpened(): + sys.exit(1) + + def update_frame(self): + if not self.cap: + return + + ret, frame = self.cap.read() + + if not ret or frame is None: + return + + self.current_frame = frame + + if self.state == LIVE: + self.draw(frame, "Drücke 1 für Foto") + + elif self.state == COUNTDOWN: + self.draw(frame, str(self.countdown), big=True) + + elif self.state == PHOTO: + self.draw(self.photo, "2=Speichern | 3=Drucken") + + elif self.state == QR: + self.draw(self.qr_img, "QR Code scannen") + + def draw(self, frame, text="", big=False): + rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + h, w, ch = rgb.shape + + img = QImage(rgb.data, w, h, ch * w, QImage.Format.Format_RGB888) + pix = QPixmap.fromImage(img) + + painter = QPainter(pix) + + painter.fillRect(0, 900, 1920, 180, QColor(0, 0, 0, 120)) + painter.setPen(Qt.GlobalColor.white) + + if big: + painter.setFont(QFont("Arial", 200)) + painter.drawText(pix.rect(), Qt.AlignmentFlag.AlignCenter, text) + else: + painter.setFont(QFont("Arial", 40)) + painter.drawText(50, 1000, text) + + if self.countdown > 0 and not big: + painter.drawText(1700, 1000, str(self.countdown)) + + painter.end() + + self.label.setPixmap(pix) + + def keyPressEvent(self, event): + key = event.text() + + if key == CONFIG["buttons"]["capture"]: + self.start_countdown() + elif key == CONFIG["buttons"]["save"]: + self.save_photo() + elif key == CONFIG["buttons"]["print"]: + self.print_photo() + + def start_countdown(self): + if self.state != LIVE: + return + self.state = COUNTDOWN + self.countdown = 3 + self.countdown_timer.start(1000) + + def take_photo(self): + self.photo = self.current_frame.copy() + self.state = PHOTO + self.countdown = 60 + + def save_photo(self): + if self.state != PHOTO: + return + url = self.upload(self.photo) + self.make_qr(url) + self.state = QR + self.start_qr_timer() + + def print_photo(self): + if self.state != PHOTO: + return + url = self.upload(self.photo) + self.make_qr(url) + self.print_image() + self.state = QR + self.start_qr_timer() + + def update_countdown(self): + self.countdown -= 1 + + if self.state == COUNTDOWN and self.countdown == 0: + self.take_photo() + elif self.countdown <= 0: + self.reset() + + def start_qr_timer(self): + self.countdown = 30 + self.countdown_timer.start(1000) + + def reset(self): + self.state = LIVE + self.countdown = 0 + self.photo = None + self.qr_img = None + self.countdown_timer.stop() + + def upload(self, img): + filename = f"{int(time.time())}.jpg" + path = os.path.join(CONFIG["photo_path"], filename) + + os.makedirs(CONFIG["photo_path"], exist_ok=True) + cv2.imwrite(path, img) + + ftp = FTP(CONFIG["ftp"]["host"]) + ftp.login(CONFIG["ftp"]["user"], CONFIG["ftp"]["password"]) + + with open(path, "rb") as f: + ftp.storbinary(f"STOR {filename}", f) + + ftp.quit() + + return CONFIG["ftp"]["base_url"] + filename + + def make_qr(self, url): + img = qrcode.make(url) + img.save("qr.png") + self.qr_img = cv2.imread("qr.png") + + def print_image(self): + temp = "print.jpg" + cv2.imwrite(temp, self.photo) + + if os.name == "nt": + os.startfile(temp, "print") + else: + os.system(f"lp {temp}") + + def closeEvent(self, event): + if self.cap: + self.cap.release() + cv2.destroyAllWindows() + event.accept() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + win = PhotoApp() + win.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9b234d9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +opencv-python +pyqt6 +qrcode \ No newline at end of file