Files
pfand_PKG/PfandApplication/pfand_scanner.py

682 lines
28 KiB
Python

# µ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("<<ComboboxSelected>>", 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(
"<Configure>",
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('<Return>', lambda e: confirm())
self.product_win.bind('<Escape>', 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()