# µ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) # Threading control self.running = True self.camera_thread = None self.process_thread = None self.init_gui() self.init_camera() self.queue = queue.Queue() self.frame_queue = queue.Queue(maxsize=2) # Limit queue size to prevent memory issues self.pfand_values = { "EINWEG": 0.25, "MEHRWEG": 0.15, "DOSE": 0.25, } # Start threads self.start_threads() self.window.protocol("WM_DELETE_WINDOW", self.on_closing) def setup_styles(self): 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 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", style='Title.TLabel') title_label.pack(side="left") status_label = ttk.Label(header_frame, text="v2.3.5", 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 start_threads(self): """Start camera and processing threads""" self.camera_thread = threading.Thread(target=self.camera_worker, daemon=True) self.camera_thread.start() self.process_thread = threading.Thread(target=self.process_worker, daemon=True) self.process_thread.start() # Start UI update loop self.update_preview() self.process_queue() def camera_worker(self): """Worker thread for camera capture and processing""" while self.running: try: if not hasattr(self, 'cap') or not self.cap.isOpened(): time.sleep(0.1) continue 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) # Put frame in queue for UI thread try: if self.frame_queue.full(): self.frame_queue.get_nowait() # Discard old frame self.frame_queue.put(frame_with_outlines, timeout=0.1) except queue.Full: pass # Skip this frame if queue is full except Exception as e: print(f"Fehler in der Kamera-Verarbeitung: {e}") time.sleep(0.1) def process_worker(self): """Worker thread for barcode processing""" while self.running: 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: continue 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) # Update UI in main thread self.window.after(0, self.add_to_treeview, current_time, barcode_data, pfand_type, deposit) # 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}") def add_to_treeview(self, current_time, barcode_data, pfand_type, deposit): """Add item to treeview (called from main thread)""" # 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 self.update_statistics() # Sound notification if self.beep_var.get(): self.window.bell() def update_preview(self): """Update camera preview (called from main thread)""" try: # Get frame from queue frame = self.frame_queue.get_nowait() # Convert and display cv2image = cv2.cvtColor(frame, 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) except queue.Empty: pass # No new frame available except Exception as e: print(f"Fehler in der Video-Vorschau: {e}") # Schedule next update if self.running: self.window.after(30, 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.minsize(450, 350) self.product_win.resizable(True, True) # Center the window self.product_win.transient(self.window) self.product_win.grab_set() # Configure grid weights for proper expansion self.product_win.columnconfigure(0, weight=1) self.product_win.rowconfigure(1, weight=1) # Header frame header_frame = ttk.Frame(self.product_win, padding="10") header_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 5)) header_frame.columnconfigure(0, weight=1) ttk.Label(header_frame, text="Produkt für Barcode auswählen:", style='Heading.TLabel').grid(row=0, column=0, sticky="w") ttk.Label(header_frame, text=f"'{barcode_data}'", style='Info.TLabel', font=('Courier', 10)).grid(row=1, column=0, sticky="w", pady=(0, 5)) # Main content frame with scrollable area content_frame = ttk.Frame(self.product_win) content_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=5) content_frame.columnconfigure(0, weight=1) content_frame.rowconfigure(0, weight=1) selected_product = tk.StringVar() # Create a canvas with scrollbar for the product list # Create a canvas with scrollbar for the product list canvas = tk.Canvas(content_frame, bg='white', highlightthickness=0) scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=canvas.yview) canvas.configure(yscrollcommand=scrollbar.set) # Frame inside canvas scrollable_frame = ttk.Frame(canvas) window_id = canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") # Update scrollregion when frame contents change scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) # Resize inner frame width when canvas resizes def on_canvas_configure(event): canvas.itemconfig(window_id, width=event.width) canvas.bind("", on_canvas_configure) # Place canvas + scrollbar canvas.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") # Add products to scrollable frame if self.products: 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) product_frame.columnconfigure(1, weight=1) ttk.Radiobutton( product_frame, text=product, variable=selected_product, value=product ).grid(row=0, column=0, sticky="w") info_text = f"Aktuell: {current_quantity}, Preis: €{price:.2f}" ttk.Label(product_frame, text=info_text, style='Info.TLabel').grid(row=0, column=1, sticky="e", padx=(10, 0)) else: no_products_label = ttk.Label(scrollable_frame, text="Noch keine Produkte definiert.", style='Info.TLabel', justify="center") no_products_label.pack(pady=20) # Button frame at the bottom button_frame = ttk.Frame(self.product_win, padding="10") button_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=(5, 10)) # Configure button frame columns for proper spacing button_frame.columnconfigure(0, weight=1) button_frame.columnconfigure(1, weight=0) button_frame.columnconfigure(2, weight=0) button_frame.columnconfigure(3, weight=0) # Helper functions for buttons 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() 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 proper 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=2, padx=5) ttk.Button(button_frame, text="Bestätigen", command=confirm).grid(row=0, column=3, padx=5) # Bind Enter key to confirm self.product_win.bind('', lambda e: confirm()) self.product_win.bind('', lambda e: cancel()) # Set focus to the window self.product_win.focus_set() def process_queue(self): try: # This method is now handled by the process_worker thread pass except Exception as e: print(f"Fehler in der Warteschlangenverarbeitung: {e}") # Schedule next check if self.running: self.window.after(100, self.process_queue) def on_closing(self): 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) if self.process_thread and self.process_thread.is_alive(): self.process_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.5") else: # For standalone testing root = tk.Tk() app = PfandScanner(root, "µScan V2.3.5") root.mainloop()