# µScan V2.3.0 - Verbesserte Deutsche Version import tkinter as tk from tkinter import ttk, simpledialog, messagebox import cv2 from PIL import Image, ImageTk from pyzbar.pyzbar import decode from datetime import datetime, timedelta import threading import queue import json import os import time import numpy as np class PfandScanner: def __init__(self, window, window_title): self.window = window self.window.title(window_title) self.window.geometry("1600x900") self.window.minsize(1200, 700) self.window.configure(bg='#f0f0f0') # Configure main window grid self.window.columnconfigure(0, weight=1) self.window.rowconfigure(0, weight=1) # Style configuration self.setup_styles() self.data_file = "quantities.json" self.products_file = "products.json" self.load_data() self.barcode_times = {} self.prompted_barcodes = set() self.current_barcodes = [] # Store current frame's barcodes for outlining self.selected_device_index = tk.IntVar(value=0) self.last_process_time = time.time() # FPS Setting self.process_interval = 0.15 # Improved processing speed # Collapsible scan list state self.scan_list_collapsed = tk.BooleanVar(value=False) self.init_gui() self.init_camera() self.queue = queue.Queue() self.pfand_values = { "EINWEG": 0.25, "MEHRWEG": 0.15, "DOSE": 0.25, } self.update_preview() self.window.protocol("WM_DELETE_WINDOW", self.on_closing) self.process_queue() def setup_styles(self): """Setup custom styles for a modern look""" self.style = ttk.Style() self.style.theme_use('clam') # Configure colors self.colors = { 'primary': '#2c3e50', 'secondary': '#3498db', 'success': '#27ae60', 'warning': '#f39c12', 'danger': '#e74c3c', 'light': '#ecf0f1', 'dark': '#34495e' } # Configure custom styles self.style.configure('Title.TLabel', font=('Segoe UI', 16, 'bold'), foreground=self.colors['primary']) self.style.configure('Heading.TLabel', font=('Segoe UI', 12, 'bold'), foreground=self.colors['dark']) self.style.configure('Info.TLabel', font=('Segoe UI', 10), foreground=self.colors['dark']) self.style.configure('Success.TLabel', font=('Segoe UI', 10, 'bold'), foreground=self.colors['success']) self.style.configure('Custom.TLabelFrame', relief='solid', borderwidth=1) self.style.configure('Camera.TFrame', relief='solid', borderwidth=2) def load_data(self): """Load products from products.json and quantities from quantities.json""" # Load products if os.path.exists(self.products_file): try: with open(self.products_file, 'r', encoding='utf-8') as f: products_data = json.load(f) self.products = products_data.get("products", []) self.prices = products_data.get("prices", {}) except Exception as e: print(f"Fehler beim Laden der Produkte: {e}") self.products = [] self.prices = {} else: self.products = [] self.prices = {} # Load quantities if os.path.exists(self.data_file): try: with open(self.data_file, 'r', encoding='utf-8') as f: self.quantities = json.load(f) except Exception as e: print(f"Fehler beim Laden der Mengen: {e}") self.quantities = {} else: self.quantities = {} def save_json(self): """Save quantities to quantities.json""" try: with open(self.data_file, 'w', encoding='utf-8') as f: json.dump(self.quantities, f, indent=4, ensure_ascii=False) except Exception as e: print(f"Fehler beim Speichern der Mengen: {e}") def init_gui(self): # Main container self.main_frame = ttk.Frame(self.window, padding="10") self.main_frame.grid(sticky="nsew") self.main_frame.columnconfigure(1, weight=2) # Camera gets more space self.main_frame.columnconfigure(0, weight=1) # Controls self.main_frame.columnconfigure(2, weight=1) # Info self.main_frame.rowconfigure(1, weight=1) # Header header_frame = ttk.Frame(self.main_frame) header_frame.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0, 10)) title_label = ttk.Label(header_frame, text="µScan V2.3.0", style='Title.TLabel') title_label.pack(side="left") status_label = ttk.Label(header_frame, text="Erweiterte Barcode-Scanner", style='Info.TLabel') status_label.pack(side="right") # Control Panel (Left) self.control_frame = ttk.LabelFrame(self.main_frame, text="Steuerung", padding="10") self.control_frame.grid(row=1, column=0, padx=(0, 10), sticky="nsew") # Camera View (Center) self.camera_frame = ttk.LabelFrame(self.main_frame, text="Kameraansicht", padding="5") self.camera_frame.grid(row=1, column=1, padx=5, sticky="nsew") self.camera_frame.columnconfigure(0, weight=1) self.camera_frame.rowconfigure(0, weight=1) # Info Panel (Right) - now collapsible self.info_frame = ttk.LabelFrame(self.main_frame, text="Scan-Ergebnisse", padding="10") self.info_frame.grid(row=1, column=2, padx=(10, 0), sticky="nsew") # Camera display camera_container = ttk.Frame(self.camera_frame, relief='solid', borderwidth=2) camera_container.grid(sticky="nsew", padx=5, pady=5) camera_container.columnconfigure(0, weight=1) camera_container.rowconfigure(0, weight=1) self.camera_label = ttk.Label(camera_container, text="Kamera wird geladen...", anchor="center") self.camera_label.grid(sticky="nsew") self.init_device_selector() self.init_controls() self.init_treeview() self.init_statistics() def init_device_selector(self): device_frame = ttk.LabelFrame(self.control_frame, text="Kamera-Einstellungen", padding="10") device_frame.pack(fill="x", pady=(0, 10)) ttk.Label(device_frame, text="Kamera auswählen:", style='Heading.TLabel').pack(anchor="w") self.device_combo = ttk.Combobox(device_frame, state="readonly") self.device_combo.pack(fill="x", pady=(5, 10)) available_devices = self.list_video_devices() self.device_combo['values'] = [f"Kamera {i}" for i in available_devices] if available_devices: self.device_combo.current(0) self.device_combo.bind("<>", self.change_camera) # Camera status self.camera_status = ttk.Label(device_frame, text="Status: Initialisierung...", style='Info.TLabel') self.camera_status.pack(anchor="w") def init_controls(self): # Focus controls focus_frame = ttk.LabelFrame(self.control_frame, text="Fokus-Steuerung", padding="10") focus_frame.pack(fill="x", pady=(0, 10)) self.autofocus_var = tk.BooleanVar(value=True) self.autofocus_check = ttk.Checkbutton( focus_frame, text="Auto-Fokus", variable=self.autofocus_var, command=self.toggle_autofocus) self.autofocus_check.pack(anchor="w", pady=(0, 5)) ttk.Label(focus_frame, text="Manueller Fokus:").pack(anchor="w") self.focus_slider = ttk.Scale(focus_frame, from_=0, to=255, orient="horizontal") self.focus_slider.set(0) self.focus_slider.pack(fill="x", pady=(0, 5)) # Image processing controls process_frame = ttk.LabelFrame(self.control_frame, text="Bildverbesserung", padding="10") process_frame.pack(fill="x", pady=(0, 10)) ttk.Label(process_frame, text="Helligkeit:").pack(anchor="w") self.brightness_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal") self.brightness_slider.set(50) self.brightness_slider.pack(fill="x", pady=(0, 5)) ttk.Label(process_frame, text="Kontrast:").pack(anchor="w") self.contrast_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal") self.contrast_slider.set(50) self.contrast_slider.pack(fill="x", pady=(0, 10)) # Scan options scan_frame = ttk.LabelFrame(self.control_frame, text="Scan-Optionen", padding="10") scan_frame.pack(fill="x") self.outline_var = tk.BooleanVar(value=True) ttk.Checkbutton(scan_frame, text="Barcodes umranden", variable=self.outline_var).pack(anchor="w") self.beep_var = tk.BooleanVar(value=True) ttk.Checkbutton(scan_frame, text="Ton bei Scan", variable=self.beep_var).pack(anchor="w") def init_statistics(self): """Initialize statistics panel""" stats_frame = ttk.LabelFrame(self.info_frame, text="Statistiken", padding="10") stats_frame.pack(fill="x", pady=(0, 10)) self.total_scans_label = ttk.Label(stats_frame, text="Gesamte Scans: 0", style='Info.TLabel') self.total_scans_label.pack(anchor="w") self.total_value_label = ttk.Label(stats_frame, text="Gesamtwert: €0,00", style='Success.TLabel') self.total_value_label.pack(anchor="w") self.session_time_label = ttk.Label(stats_frame, text="Sitzungsdauer: 00:00", style='Info.TLabel') self.session_time_label.pack(anchor="w") self.session_start = datetime.now() self.total_scans = 0 self.total_value = 0.0 def init_treeview(self): # Header frame with collapse button tree_header_frame = ttk.Frame(self.info_frame) tree_header_frame.pack(fill="x", pady=(0, 5)) # Collapse button self.collapse_button = ttk.Button( tree_header_frame, text="▼ Scan-Liste", command=self.toggle_scan_list, width=15 ) self.collapse_button.pack(side="left") # Clear button clear_button = ttk.Button( tree_header_frame, text="Liste löschen", command=self.clear_scan_list ) clear_button.pack(side="right") # Treeview frame (collapsible) self.tree_container = ttk.Frame(self.info_frame) self.tree_container.pack(fill="both", expand=True) self.tree = ttk.Treeview( self.tree_container, columns=("Zeit", "Barcode", "Typ", "Pfand"), show="headings", height=15 ) # Configure columns self.tree.heading("Zeit", text="Zeit") self.tree.heading("Barcode", text="Barcode") self.tree.heading("Typ", text="Typ") self.tree.heading("Pfand", text="Pfand") self.tree.column("Zeit", width=100, minwidth=80) self.tree.column("Barcode", width=120, minwidth=100) self.tree.column("Typ", width=80, minwidth=60) self.tree.column("Pfand", width=70, minwidth=50) # Scrollbars v_scrollbar = ttk.Scrollbar(self.tree_container, orient="vertical", command=self.tree.yview) h_scrollbar = ttk.Scrollbar(self.tree_container, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) # Grid layout self.tree.grid(row=0, column=0, sticky="nsew") v_scrollbar.grid(row=0, column=1, sticky="ns") h_scrollbar.grid(row=1, column=0, sticky="ew") self.tree_container.columnconfigure(0, weight=1) self.tree_container.rowconfigure(0, weight=1) # Alternate row colors self.tree.tag_configure('oddrow', background='#f9f9f9') self.tree.tag_configure('evenrow', background='white') def toggle_scan_list(self): """Toggle the visibility of the scan list""" if self.scan_list_collapsed.get(): # Expand self.tree_container.pack(fill="both", expand=True) self.collapse_button.configure(text="▼ Scan-Liste") self.scan_list_collapsed.set(False) # Resize window columns self.main_frame.columnconfigure(2, weight=1) else: # Collapse self.tree_container.pack_forget() self.collapse_button.configure(text="▶ Scan-Liste") self.scan_list_collapsed.set(True) # Make info panel smaller self.main_frame.columnconfigure(2, weight=0, minsize=200) def clear_scan_list(self): """Clear all items from the scan list""" if messagebox.askyesno("Liste löschen", "Möchten Sie wirklich alle Einträge aus der Scan-Liste löschen?"): for item in self.tree.get_children(): self.tree.delete(item) def list_video_devices(self, max_devices=10): available = [] for i in range(max_devices): cap = cv2.VideoCapture(i) if cap.isOpened(): available.append(i) cap.release() return available def change_camera(self, event=None): index = self.device_combo.current() self.selected_device_index.set(index) self.init_camera() def init_camera(self): if hasattr(self, 'cap') and self.cap and self.cap.isOpened(): self.cap.release() device_index = self.selected_device_index.get() self.cap = cv2.VideoCapture(device_index, cv2.CAP_DSHOW if os.name == 'nt' else 0) if self.cap.isOpened(): self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) self.cap.set(cv2.CAP_PROP_FPS, 30) self.toggle_autofocus() self.camera_status.configure(text="Status: Verbunden ✓", foreground=self.colors['success']) else: self.camera_status.configure(text="Status: Fehler ✗", foreground=self.colors['danger']) def toggle_autofocus(self): if self.cap: if self.autofocus_var.get(): self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 1) self.focus_slider.state(['disabled']) else: self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 0) self.focus_slider.state(['!disabled']) self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.get()) def adjust_image(self, frame): brightness = self.brightness_slider.get() / 50.0 - 1.0 contrast = self.contrast_slider.get() / 50.0 adjusted = cv2.convertScaleAbs(frame, alpha=contrast, beta=brightness * 127) gray = cv2.cvtColor(adjusted, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) return cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) def draw_barcode_outline(self, frame, barcodes): if not self.outline_var.get(): return frame for barcode in barcodes: # Get barcode polygon points points = barcode.polygon if len(points) == 4: # Convert to numpy array pts = np.array([[point.x, point.y] for point in points], np.int32) pts = pts.reshape((-1, 1, 2)) # Draw colored outline based on barcode type barcode_data = barcode.data.decode('utf-8') if len(barcode_data) == 13: color = (0, 255, 0) # Green for EINWEG elif len(barcode_data) == 8: color = (255, 0, 0) # Blue for MEHRWEG else: color = (0, 165, 255) # Orange for DOSE # Draw outline cv2.polylines(frame, [pts], True, color, 3) # Add label rect = cv2.boundingRect(pts) label_pos = (rect[0], rect[1] - 10) pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE" deposit = self.pfand_values.get(pfand_type, 0.00) label = f"{pfand_type}: €{deposit:.2f}" # Draw label background (text_width, text_height), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2) cv2.rectangle(frame, (label_pos[0], label_pos[1] - text_height - 5), (label_pos[0] + text_width, label_pos[1] + 5), color, -1) # Draw label text cv2.putText(frame, label, label_pos, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) return frame def update_preview(self): try: if not hasattr(self, 'cap') or not self.cap.isOpened(): self.window.after(100, self.update_preview) return ret, frame = self.cap.read() if ret: if not self.autofocus_var.get(): self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.get()) current_time = time.time() if current_time - self.last_process_time >= self.process_interval: processed_frame = self.adjust_image(frame) barcodes = decode(processed_frame) or decode(frame) self.current_barcodes = barcodes for barcode in barcodes: barcode_data = barcode.data.decode('utf-8') self.queue.put(barcode_data) self.last_process_time = current_time # Draw barcode outlines frame_with_outlines = self.draw_barcode_outline(frame.copy(), self.current_barcodes) # Convert and display cv2image = cv2.cvtColor(frame_with_outlines, cv2.COLOR_BGR2RGBA) img = Image.fromarray(cv2image) # Resize to fit label while maintaining aspect ratio label_width = self.camera_label.winfo_width() label_height = self.camera_label.winfo_height() if label_width > 1 and label_height > 1: img.thumbnail((label_width, label_height), Image.Resampling.LANCZOS) imgtk = ImageTk.PhotoImage(image=img) self.camera_label.imgtk = imgtk self.camera_label.configure(image=imgtk) # Update session time self.update_statistics() except Exception as e: print(f"Fehler in der Video-Vorschau: {e}") self.window.after(10, self.update_preview) def update_statistics(self): session_time = datetime.now() - self.session_start hours, remainder = divmod(int(session_time.total_seconds()), 3600) minutes, _ = divmod(remainder, 60) time_str = f"{hours:02d}:{minutes:02d}" self.session_time_label.configure(text=f"Sitzungsdauer: {time_str}") self.total_scans_label.configure(text=f"Gesamte Scans: {self.total_scans}") self.total_value_label.configure(text=f"Gesamtwert: €{self.total_value:.2f}") def show_product_selection(self, barcode_data): if hasattr(self, 'product_win') and self.product_win.winfo_exists(): return self.product_win = tk.Toplevel(self.window) self.product_win.title("Produkt auswählen") self.product_win.geometry("500x400") self.product_win.resizable(True, True) # Center the window self.product_win.transient(self.window) self.product_win.grab_set() # Configure grid weights self.product_win.columnconfigure(0, weight=1) self.product_win.rowconfigure(1, weight=1) # Header frame header_frame = ttk.Frame(self.product_win, padding="20") header_frame.grid(row=0, column=0, sticky="ew") ttk.Label(header_frame, text="Produkt für Barcode auswählen:", style='Heading.TLabel').pack(pady=(0, 5)) ttk.Label(header_frame, text=f"'{barcode_data}'", style='Info.TLabel', font=('Courier', 10)).pack(pady=(0, 10)) # Main content frame content_frame = ttk.Frame(self.product_win, padding="20") content_frame.grid(row=1, column=0, sticky="nsew") content_frame.columnconfigure(0, weight=1) content_frame.rowconfigure(0, weight=1) selected_product = tk.StringVar() if self.products: # Create scrollable frame for products canvas = tk.Canvas(content_frame, bg='white') scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=canvas.yview) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) for i, product in enumerate(self.products): current_quantity = self.quantities.get(product, 0) price = self.prices.get(product, 0.00) product_frame = ttk.Frame(scrollable_frame, padding="5") product_frame.pack(fill="x", pady=2) ttk.Radiobutton( product_frame, text=f"{product}", variable=selected_product, value=product ).pack(side="left") info_text = f"(Aktuell: {current_quantity}, Preis: €{price:.2f})" ttk.Label(product_frame, text=info_text, style='Info.TLabel').pack(side="right") canvas.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") else: ttk.Label(content_frame, text="Noch keine Produkte definiert.", style='Info.TLabel').pack(pady=20) # Button frame button_frame = ttk.Frame(self.product_win, padding="20") button_frame.grid(row=2, column=0, sticky="ew") # Configure button frame columns button_frame.columnconfigure(0, weight=1) button_frame.columnconfigure(1, weight=0) button_frame.columnconfigure(2, weight=0) def confirm(): product = selected_product.get() if product: # Update quantity self.quantities[product] = self.quantities.get(product, 0) + 1 # Update total value with actual product price product_price = self.prices.get(product, 0.00) self.total_value += product_price self.save_json() self.update_statistics() self.product_win.destroy() # Show confirmation message messagebox.showinfo("Erfolgreich", f"Produkt '{product}' wurde hinzugefügt!") else: messagebox.showwarning("Keine Auswahl", "Bitte wählen Sie ein Produkt aus.") def cancel(): self.product_win.destroy() # Add new product button def add_new_product(): new_product = simpledialog.askstring("Neues Produkt", "Name des neuen Produkts:") if new_product and new_product.strip(): new_product = new_product.strip() if new_product not in self.products: # Ask for price price_str = simpledialog.askstring("Preis", f"Preis für '{new_product}' (€):") try: price = float(price_str.replace(',', '.')) if price_str else 0.00 except: price = 0.00 self.products.append(new_product) self.prices[new_product] = price # Save products try: products_data = {"products": self.products, "prices": self.prices} with open(self.products_file, 'w', encoding='utf-8') as f: json.dump(products_data, f, indent=4, ensure_ascii=False) except Exception as e: print(f"Fehler beim Speichern der Produkte: {e}") # Refresh the dialog self.product_win.destroy() self.window.after(0, self.show_product_selection, barcode_data) else: messagebox.showwarning("Produkt existiert", "Dieses Produkt ist bereits vorhanden.") # Buttons with better layout ttk.Button(button_frame, text="Neues Produkt", command=add_new_product).grid(row=0, column=0, sticky="w", padx=5) ttk.Button(button_frame, text="Abbrechen", command=cancel).grid(row=0, column=1, padx=5) ttk.Button(button_frame, text="Bestätigen", command=confirm).grid(row=0, column=2, padx=5) # Bind Enter key to confirm self.product_win.bind('', lambda e: confirm()) self.product_win.bind('', lambda e: cancel()) def process_queue(self): try: barcode_data = self.queue.get(timeout=0.1) now = datetime.now() # Rate limiting timestamps = self.barcode_times.get(barcode_data, []) timestamps = [t for t in timestamps if now - t <= timedelta(seconds=5)] if len(timestamps) >= 3: return timestamps.append(now) self.barcode_times[barcode_data] = timestamps current_time = now.strftime("%H:%M:%S") pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE" deposit = self.pfand_values.get(pfand_type, 0.00) # Determine row color row_count = len(self.tree.get_children()) tag = 'evenrow' if row_count % 2 == 0 else 'oddrow' self.tree.insert("", 0, values=(current_time, barcode_data, pfand_type, f"€{deposit:.2f}"), tags=(tag,)) # Update statistics self.total_scans += 1 # Note: Total value is updated in product selection dialog # Sound notification if self.beep_var.get(): self.window.bell() # Show product selection dialog if barcode_data not in self.prompted_barcodes: self.prompted_barcodes.add(barcode_data) self.window.after(0, self.show_product_selection, barcode_data) except queue.Empty: pass except Exception as e: print(f"Fehler in der Warteschlangenverarbeitung: {e}") finally: self.window.after(100, self.process_queue) def on_closing(self): """Clean shutdown of the application""" self.running = False # Wait for camera thread to finish if self.camera_thread and self.camera_thread.is_alive(): self.camera_thread.join(timeout=1.0) try: if hasattr(self, 'cap') and self.cap and self.cap.isOpened(): self.cap.release() cv2.destroyAllWindows() except: pass self.window.destroy() if __name__ != "__main__": def launch_pfand_scanner(): scanner_window = tk.Toplevel() PfandScanner(scanner_window, "µScan V2.3.0 - Verbesserte Deutsche Version") else: # For standalone testing root = tk.Tk() app = PfandScanner(root, "µScan V2.3.0 - Verbesserte Deutsche Version") root.mainloop()