Pineapple (CH5)
+ µScan better
+ Versioning
This commit is contained in:
@@ -385,8 +385,8 @@ class PfandCalculator:
|
|||||||
label = tk.Label(
|
label = tk.Label(
|
||||||
about_window,
|
about_window,
|
||||||
text=(
|
text=(
|
||||||
"PfandApp V.8.15.100 - 'Pineapple'\n"
|
"PfandApp V.8.15.200 - 'Pineapple'\n"
|
||||||
"PaketVersion 1.1 ( PKG1.1 )\n\n"
|
"PaketVersion 1.2 ( PKG1.2 )\n\n"
|
||||||
"Erstellt mit TKinter, CV2, Numpy, PyZbar, TGTG-API, TKCalendar, Datetime\n"
|
"Erstellt mit TKinter, CV2, Numpy, PyZbar, TGTG-API, TKCalendar, Datetime\n"
|
||||||
),
|
),
|
||||||
padx=10,
|
padx=10,
|
||||||
@@ -562,9 +562,10 @@ class PfandCalculator:
|
|||||||
about_uscan,
|
about_uscan,
|
||||||
text=(
|
text=(
|
||||||
"µScan - Der bessere Barcode Scanner\n"
|
"µScan - Der bessere Barcode Scanner\n"
|
||||||
"Version 2.1.0\n"
|
"Version 2.2.2\n"
|
||||||
"µScan erfordert einen UI Reload (Strg+R) in der Root Anwendung\n"
|
"µScan erfordert einen UI Reload (Strg+R) in der Root Anwendung\n"
|
||||||
"µScan ist für mehrere Barcodes gemacht, die in einer kurzen Zeit gescannt werden sollten\n"
|
"µScan ist für mehrere Barcodes gemacht, die in einer kurzen Zeit gescannt werden sollten\n"
|
||||||
|
"Beachte das µScan eine Kamera benötigt die mindestens 30FPS aufnehmen kann (Process-FPS können eingestellt werden!)"
|
||||||
),
|
),
|
||||||
justify="left",
|
justify="left",
|
||||||
anchor="center"
|
anchor="center"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# µScan V2.1.0
|
# µScan V2.2.2
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, simpledialog, messagebox
|
from tkinter import ttk, simpledialog, messagebox
|
||||||
import cv2
|
import cv2
|
||||||
@@ -9,11 +9,16 @@ import threading
|
|||||||
import queue
|
import queue
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
class PfandScanner:
|
class PfandScanner:
|
||||||
def __init__(self, window, window_title):
|
def __init__(self, window, window_title):
|
||||||
self.window = window
|
self.window = window
|
||||||
self.window.title(window_title)
|
self.window.title(window_title)
|
||||||
|
self.window.geometry("1920x1080")
|
||||||
|
self.window.minsize(960, 540)
|
||||||
|
self.window.columnconfigure(0, weight=1)
|
||||||
|
self.window.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
self.data_file = "quantities.json"
|
self.data_file = "quantities.json"
|
||||||
self.load_json()
|
self.load_json()
|
||||||
@@ -21,18 +26,80 @@ class PfandScanner:
|
|||||||
self.barcode_times = {}
|
self.barcode_times = {}
|
||||||
self.prompted_barcodes = set()
|
self.prompted_barcodes = set()
|
||||||
|
|
||||||
self.camera_frame = ttk.Frame(window)
|
self.selected_device_index = tk.IntVar(value=0)
|
||||||
self.camera_frame.pack(side="left", padx=10, pady=5)
|
self.last_process_time = time.time()
|
||||||
|
|
||||||
self.control_frame = ttk.Frame(window)
|
# FPS Einstellung ist hier!
|
||||||
self.control_frame.pack(side="left", padx=10, pady=5, fill="y")
|
# FPS Setting is here!
|
||||||
|
self.process_interval = 0.30 # 30 FPS
|
||||||
|
|
||||||
self.info_frame = ttk.Frame(window)
|
self.init_gui()
|
||||||
self.info_frame.pack(side="right", padx=10, pady=5, fill="both", expand=True)
|
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 init_gui(self):
|
||||||
|
self.main_frame = ttk.Frame(self.window)
|
||||||
|
self.main_frame.grid(sticky="nsew")
|
||||||
|
self.main_frame.columnconfigure(0, weight=3)
|
||||||
|
self.main_frame.columnconfigure(1, weight=1)
|
||||||
|
self.main_frame.columnconfigure(2, weight=3)
|
||||||
|
self.main_frame.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self.camera_frame = ttk.Frame(self.main_frame)
|
||||||
|
self.camera_frame.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")
|
||||||
|
|
||||||
|
self.control_frame = ttk.Frame(self.main_frame)
|
||||||
|
self.control_frame.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")
|
||||||
|
|
||||||
|
self.info_frame = ttk.Frame(self.main_frame)
|
||||||
|
self.info_frame.grid(row=0, column=2, padx=5, pady=5, sticky="nsew")
|
||||||
|
|
||||||
self.camera_label = ttk.Label(self.camera_frame)
|
self.camera_label = ttk.Label(self.camera_frame)
|
||||||
self.camera_label.pack()
|
self.camera_label.pack(expand=True, fill="both")
|
||||||
|
|
||||||
|
self.init_device_selector()
|
||||||
|
self.init_controls()
|
||||||
|
self.init_treeview()
|
||||||
|
|
||||||
|
def init_device_selector(self):
|
||||||
|
device_frame = ttk.LabelFrame(self.control_frame, text="Video Device")
|
||||||
|
device_frame.pack(pady=5, padx=5, fill="x")
|
||||||
|
|
||||||
|
ttk.Label(device_frame, text="Choose Camera:").pack(anchor="w", padx=5)
|
||||||
|
|
||||||
|
self.device_combo = ttk.Combobox(device_frame, state="readonly")
|
||||||
|
self.device_combo.pack(fill="x", padx=5)
|
||||||
|
|
||||||
|
available_devices = self.list_video_devices()
|
||||||
|
self.device_combo['values'] = [f"Camera {i}" for i in available_devices]
|
||||||
|
self.device_combo.current(0)
|
||||||
|
self.device_combo.bind("<<ComboboxSelected>>", self.change_camera)
|
||||||
|
|
||||||
|
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_controls(self):
|
||||||
focus_frame = ttk.LabelFrame(self.control_frame, text="Camera Controls")
|
focus_frame = ttk.LabelFrame(self.control_frame, text="Camera Controls")
|
||||||
focus_frame.pack(pady=5, padx=5, fill="x")
|
focus_frame.pack(pady=5, padx=5, fill="x")
|
||||||
|
|
||||||
@@ -59,40 +126,27 @@ class PfandScanner:
|
|||||||
self.contrast_slider.set(50)
|
self.contrast_slider.set(50)
|
||||||
self.contrast_slider.pack(pady=2, padx=5, fill="x")
|
self.contrast_slider.pack(pady=2, padx=5, fill="x")
|
||||||
|
|
||||||
|
def init_treeview(self):
|
||||||
self.tree = ttk.Treeview(self.info_frame, columns=("Time", "Barcode", "Type", "Deposit"), show="headings")
|
self.tree = ttk.Treeview(self.info_frame, columns=("Time", "Barcode", "Type", "Deposit"), show="headings")
|
||||||
self.tree.heading("Time", text="Time")
|
for col in ["Time", "Barcode", "Type", "Deposit"]:
|
||||||
self.tree.heading("Barcode", text="Barcode")
|
self.tree.heading(col, text=col)
|
||||||
self.tree.heading("Type", text="Type")
|
|
||||||
self.tree.heading("Deposit", text="Deposit (€)")
|
|
||||||
|
|
||||||
self.tree.column("Time", width=150)
|
self.tree.column("Time", width=150)
|
||||||
self.tree.column("Barcode", width=200)
|
self.tree.column("Barcode", width=200)
|
||||||
self.tree.column("Type", width=100)
|
self.tree.column("Type", width=100)
|
||||||
self.tree.column("Deposit", width=100)
|
self.tree.column("Deposit", width=100)
|
||||||
|
|
||||||
self.tree.pack(fill="both", expand=True)
|
self.tree.pack(fill="both", expand=True)
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(self.info_frame, orient="vertical", command=self.tree.yview)
|
scrollbar = ttk.Scrollbar(self.info_frame, orient="vertical", command=self.tree.yview)
|
||||||
scrollbar.pack(side="right", fill="y")
|
scrollbar.pack(side="right", fill="y")
|
||||||
self.tree.configure(yscrollcommand=scrollbar.set)
|
self.tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
def init_camera(self):
|
||||||
self.cap = cv2.VideoCapture(0)
|
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)
|
||||||
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
|
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
|
||||||
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
||||||
self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 0)
|
self.toggle_autofocus()
|
||||||
self.cap.set(cv2.CAP_PROP_FOCUS, 0)
|
|
||||||
|
|
||||||
self.queue = queue.Queue()
|
|
||||||
|
|
||||||
self.pfand_values = {
|
|
||||||
"EINWEG": 0.25,
|
|
||||||
"MEHRWEG": 0.15,
|
|
||||||
"DOSE": 0.25,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.process_video()
|
|
||||||
self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
||||||
self.process_queue()
|
|
||||||
|
|
||||||
def load_json(self):
|
def load_json(self):
|
||||||
if os.path.exists(self.data_file):
|
if os.path.exists(self.data_file):
|
||||||
@@ -106,6 +160,7 @@ class PfandScanner:
|
|||||||
json.dump(self.quantities, f, indent=4)
|
json.dump(self.quantities, f, indent=4)
|
||||||
|
|
||||||
def toggle_autofocus(self):
|
def toggle_autofocus(self):
|
||||||
|
if self.cap:
|
||||||
if self.autofocus_var.get():
|
if self.autofocus_var.get():
|
||||||
self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 1)
|
self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 1)
|
||||||
self.focus_slider.state(['disabled'])
|
self.focus_slider.state(['disabled'])
|
||||||
@@ -120,27 +175,24 @@ class PfandScanner:
|
|||||||
adjusted = cv2.convertScaleAbs(frame, alpha=contrast, beta=brightness * 127)
|
adjusted = cv2.convertScaleAbs(frame, alpha=contrast, beta=brightness * 127)
|
||||||
gray = cv2.cvtColor(adjusted, cv2.COLOR_BGR2GRAY)
|
gray = cv2.cvtColor(adjusted, cv2.COLOR_BGR2GRAY)
|
||||||
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
|
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
|
||||||
binary = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
return cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
cv2.THRESH_BINARY, 11, 2)
|
cv2.THRESH_BINARY, 11, 2)
|
||||||
return binary
|
|
||||||
|
|
||||||
def process_video(self):
|
def update_preview(self):
|
||||||
try:
|
try:
|
||||||
ret, frame = self.cap.read()
|
ret, frame = self.cap.read()
|
||||||
if ret:
|
if ret:
|
||||||
if not self.autofocus_var.get():
|
if not self.autofocus_var.get():
|
||||||
self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.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)
|
processed_frame = self.adjust_image(frame)
|
||||||
barcodes = decode(processed_frame) or decode(frame)
|
barcodes = decode(processed_frame) or decode(frame)
|
||||||
|
|
||||||
for barcode in barcodes:
|
for barcode in barcodes:
|
||||||
points = barcode.polygon
|
|
||||||
if len(points) == 4:
|
|
||||||
pts = [(p.x, p.y) for p in points]
|
|
||||||
cv2.polylines(frame, [cv2.convexHull(cv2.UMat(cv2.Mat(pts))).get()], True, (0, 0, 255), 2)
|
|
||||||
barcode_data = barcode.data.decode('utf-8')
|
barcode_data = barcode.data.decode('utf-8')
|
||||||
self.queue.put(barcode_data)
|
self.queue.put(barcode_data)
|
||||||
|
self.last_process_time = current_time
|
||||||
|
|
||||||
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
|
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
|
||||||
img = Image.fromarray(cv2image)
|
img = Image.fromarray(cv2image)
|
||||||
@@ -148,9 +200,9 @@ class PfandScanner:
|
|||||||
self.camera_label.imgtk = imgtk
|
self.camera_label.imgtk = imgtk
|
||||||
self.camera_label.configure(image=imgtk)
|
self.camera_label.configure(image=imgtk)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in processing video: {e}")
|
print(f"Error in video preview: {e}")
|
||||||
|
|
||||||
self.window.after(100, self.process_video)
|
self.window.after(10, self.update_preview) # ~100 FPS preview
|
||||||
|
|
||||||
def show_product_selection(self, barcode_data):
|
def show_product_selection(self, barcode_data):
|
||||||
if hasattr(self, 'product_win') and self.product_win.winfo_exists():
|
if hasattr(self, 'product_win') and self.product_win.winfo_exists():
|
||||||
@@ -168,7 +220,7 @@ class PfandScanner:
|
|||||||
def confirm():
|
def confirm():
|
||||||
prod = selected_product.get()
|
prod = selected_product.get()
|
||||||
if prod:
|
if prod:
|
||||||
self.quantities[prod] += 1
|
self.quantities[prod] = self.quantities.get(prod, 0) + 1
|
||||||
self.save_json()
|
self.save_json()
|
||||||
self.product_win.destroy()
|
self.product_win.destroy()
|
||||||
else:
|
else:
|
||||||
@@ -179,27 +231,19 @@ class PfandScanner:
|
|||||||
def process_queue(self):
|
def process_queue(self):
|
||||||
try:
|
try:
|
||||||
barcode_data = self.queue.get(timeout=0.1)
|
barcode_data = self.queue.get(timeout=0.1)
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
if barcode_data in self.barcode_times:
|
timestamps = self.barcode_times.get(barcode_data, [])
|
||||||
timestamps = self.barcode_times[barcode_data]
|
|
||||||
timestamps = [t for t in timestamps if now - t <= timedelta(seconds=5)]
|
timestamps = [t for t in timestamps if now - t <= timedelta(seconds=5)]
|
||||||
if len(timestamps) >= 3:
|
if len(timestamps) >= 3:
|
||||||
return
|
return
|
||||||
timestamps.append(now)
|
timestamps.append(now)
|
||||||
self.barcode_times[barcode_data] = timestamps
|
self.barcode_times[barcode_data] = timestamps
|
||||||
else:
|
|
||||||
self.barcode_times[barcode_data] = [now]
|
|
||||||
|
|
||||||
current_time = now.strftime("%Y-%m-%d %H:%M:%S")
|
current_time = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
if len(barcode_data) == 13:
|
pfand_type = "EINWEG" if len(barcode_data) == 13 else "MEHRWEG" if len(barcode_data) == 8 else "DOSE"
|
||||||
pfand_type = "EINWEG"
|
|
||||||
elif len(barcode_data) == 8:
|
|
||||||
pfand_type = "MEHRWEG"
|
|
||||||
else:
|
|
||||||
pfand_type = "DOSE"
|
|
||||||
deposit = self.pfand_values.get(pfand_type, 0.00)
|
deposit = self.pfand_values.get(pfand_type, 0.00)
|
||||||
|
|
||||||
self.tree.insert("", 0, values=(current_time, barcode_data, pfand_type, f"{deposit:.2f}"))
|
self.tree.insert("", 0, values=(current_time, barcode_data, pfand_type, f"{deposit:.2f}"))
|
||||||
|
|
||||||
if barcode_data not in self.prompted_barcodes:
|
if barcode_data not in self.prompted_barcodes:
|
||||||
@@ -211,14 +255,14 @@ class PfandScanner:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in queue processing: {e}")
|
print(f"Error in queue processing: {e}")
|
||||||
finally:
|
finally:
|
||||||
self.window.after(100, self.process_queue) # freez delay
|
self.window.after(100, self.process_queue)
|
||||||
|
|
||||||
def on_closing(self):
|
def on_closing(self):
|
||||||
if self.cap.isOpened():
|
if self.cap and self.cap.isOpened():
|
||||||
self.cap.release()
|
self.cap.release()
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
|
|
||||||
if __name__ != "__main__":
|
if __name__ != "__main__":
|
||||||
def launch_pfand_scanner():
|
def launch_pfand_scanner():
|
||||||
scanner_window = tk.Toplevel()
|
scanner_window = tk.Toplevel()
|
||||||
PfandScanner(scanner_window, "µScan V2.1.0")
|
PfandScanner(scanner_window, "µScan V2.2.2")
|
||||||
|
|||||||
Reference in New Issue
Block a user