diff --git a/1280 x 800/Camera.py b/1280 x 800/Camera.py new file mode 100644 index 0000000..5ee3062 --- /dev/null +++ b/1280 x 800/Camera.py @@ -0,0 +1,55 @@ +from picamera2 import Picamera2, Preview +import PIL.Image, PIL.ImageTk + + +class Camera(): + def __init__(self, still_image_processing): + #Auflösung des Vorschaubildes + self.preview_width = 1280 + self.preview_height = 800 + #Auflösung des finalen Bildes + self.still_width = 4056 + self.still_height = 3040 + + self.camera = Picamera2() + #Low-Resolution-Config, für flüssigeres Vorschaubild + self.lowres_preview_config = self.camera.create_preview_configuration(main={"size": (self.preview_width, self.preview_height)}) + #Einstellung für Vorschaubild in höherer AUflsung (für Zoom) + self.highres_preview_config = self.camera.create_preview_configuration(main={"size": (2028, 1080)}) + #Einstellung für finales Bild, maximale Auflösung + self.capture_config = self.camera.create_still_configuration(main={"size": (self.still_width, self.still_height)}) + + + self.preview_config = self.lowres_preview_config + self.still_image_processing = still_image_processing + self.set_highres_preview_config = False + + def start_camera(self): + self.camera.configure(self.preview_config) + self.camera.start() + + def get_preview_image(self): + image = self.camera.capture_array() + return image + + #Kameravorschau auf höhere Auflösung umstellen + def get_highres_preview(self): + self.camera.stop() + if self.set_highres_preview_config == False: + self.preview_config = self.highres_preview_config + self.set_highres_preview_config = True + else: + self.preview_config = self.lowres_preview_config + self.set_highres_preview_config = False + + self.camera.configure(self.preview_config) + self.camera.start() + + def get_still_image(self): + image = self.camera.switch_mode_and_capture_array(self.capture_config) + self.still_image_processing.process_image(image) + + def get_aspect_ratio(self): + preview_ratio = self.preview_width / self.preview_height + return preview_ratio + diff --git a/1280 x 800/Gui_Grid.py b/1280 x 800/Gui_Grid.py new file mode 100644 index 0000000..416fed6 --- /dev/null +++ b/1280 x 800/Gui_Grid.py @@ -0,0 +1,638 @@ +import tkinter as tk +import PIL.Image, PIL.ImageTk +import numpy as np +from tkinter import ttk +from tkinter.font import Font +from tkinter import filedialog, messagebox +import subprocess +import os +import shutil + +class User_Interface(): + def __init__(self, camera, preview_processing, gpio_instance, still_image_processing): + #Instanzvariablen laden + self.still_image_processing = still_image_processing + self.preview_processing = preview_processing + self.gpio_instance = gpio_instance + self.camera = camera + self.root = tk.Tk() + + #Verzögerung des Vorschaubildes + self.video_feed_delay = 5 + + #Seitenverhältnis der Kamera und des Widgets auslesen, zur Platzierung des Vorschaubildes in diesem + self.camera_aspect_ratio = self.camera.get_aspect_ratio() + self.video_widget_aspect_ratio = None + self.new_width = 1280 + self.new_height = 800 + + #Flags für das derzeit aktive Menü + self.current_menu_right = None + self.current_menu_down = None + self.current_menu_down_right = None + self.submenu = False + + #Ein leerer Pixel, wird zur quadratischen Form der Buttons benötigt + self.pixel = tk.PhotoImage() + + + self.keyboard_process = None + + #Parameter des Lineals, mit Aktion verknüpft, sobald der Wert im Eingabefeld geändert wird + self.ruler_vertical_start = tk.StringVar() + self.ruler_vertical_end = tk.StringVar() + self.ruler_horizontal_start = tk.StringVar() + self.ruler_horizontal_end = tk.StringVar() + + self.ruler_vertical_start.set(0) + self.ruler_vertical_end.set(50) + self.ruler_horizontal_start.set(0) + self.ruler_horizontal_end.set(100) + + self.ruler_vertical_start.trace_add("write", lambda var, indx, mode: self.entry_fields_changed("ruler_vertical_start")) + self.ruler_vertical_end.trace_add("write", lambda var, indx, mode: self.entry_fields_changed("ruler_vertical_end")) + self.ruler_horizontal_start.trace_add("write", lambda var, indx, mode: self.entry_fields_changed("ruler_horizontal_start")) + self.ruler_horizontal_end.trace_add("write", lambda var, indx, mode: self.entry_fields_changed("ruler_horizontal_end")) + + + #Blitz / permanente Beleuchtung an / aus + self.permanent_lighting = False + self.activate_flash = False + + #Vollbild an / aus + self.fullscreen_active = True + + # Definiere Schriftarten + self.font_bold = Font(family="Helvetica", size=12, weight="bold") + self.font_normal = Font(family="Helvetica", size=12) + + # Checkbox Größe anpassen + self.style = ttk.Style() + self.style.configure("LargeCheckbutton.TCheckbutton", font=self.font_normal, size=25) + + #Variablen für den Dateinamen, ebenfalls mit Aktion verknüpft + self.storage_path_var = tk.StringVar(name="storage_path_var") + self.storage_path_var.set("/home/pi") + self.destination_path_var = tk.StringVar(name="destination_path_var") + self.filename_var = tk.StringVar(name="filename_var") + self.date_var = tk.BooleanVar(name="date_var") + self.time_var = tk.BooleanVar(name="time_var") + self.date_var.set(True) + self.time_var.set(True) + + self.storage_path_var.trace_add("write", lambda *args: self.entry_fields_changed("Speicherpfad", *args)) + self.destination_path_var.trace_add("write", lambda *args: self.entry_fields_changed("Zielpfad", *args)) + self.filename_var.trace_add("write", lambda *args: self.entry_fields_changed("Dateiname", *args)) + self.date_var.trace_add("write", lambda *args: self.entry_fields_changed("Datum", *args)) + self.time_var.trace_add("write", lambda *args: self.entry_fields_changed("Uhrzeit", *args)) + + + #GUI soll im Vollbild starten + def window_settings(self): + self.root.attributes('-fullscreen', True) + self.root.title('Titel') + + def start_interface(self): + self.root.mainloop() + + #Erstellen der Widgets aller Menüs + def build_widgets(self): + self.build_video_widget() + self.build_hide_menu_button() + self.main_menu_frame() + self.crop_menu_frame() + self.settings_menu_frame() + self.file_menu_frame() + self.main_menu_widgets() + self.crop_menu_widgets() + self.settings_menu_widgets() + self.file_menu_widgets() + + self.root.grid_rowconfigure(0, weight=1) + self.root.grid_columnconfigure(0, weight=1) + self.root.grid_columnconfigure(1, minsize=0) + + + def build_video_widget(self): + self.video_widget = tk.Canvas(self.root, width=1280, height=800, highlightthickness=0) + self.video_widget.grid(row=0, column=0, sticky="nsew") + + #Button "<", ">" zum Anzeigen / Verstecken des Menüs + def build_hide_menu_button(self): + self.hide_menu_button_container = tk.Frame(self.video_widget, background="white", bd=0, highlightthickness=0) + self.hide_menu_button_container.place(relx=1.0, rely=0.0, anchor="ne") + + self.hide_menu_button_settings = { + "master": self.hide_menu_button_container, + "image": self.pixel, + "compound": "c", + "height": 30, + "width": 30, + "command": self.toggle_menu + } + + self.hide_menu_button = tk.Button(**self.hide_menu_button_settings, text="<") + self.hide_menu_button.pack(side="right", padx=0, pady=0) + + + #Frames des Hauptmenüs, alle Menüs teilen sich in 3 Frames auf: Rechts vom Vorschaubild, Unter dem Vorschaubild sowie ein Frame unten rechts zwischen dem rechten und dem unteren Frame (2x2-Raster) + def main_menu_frame(self): + self.main_menu_frame_right = tk.Frame(self.root) + self.main_menu_frame_right.grid(row=0, column=1, sticky="nsew") + + self.main_menu_frame_down = tk.Frame(self.root) + self.main_menu_frame_down.grid(row=1, column=0, sticky="nsew") + + self.main_menu_frame_down_right = tk.Frame(self.root) + self.main_menu_frame_down_right.grid(row=1, column=1, sticky="nsew") + + self.main_menu_frame_right.grid_remove() + self.main_menu_frame_down.grid_remove() + self.main_menu_frame_down_right.grid_remove() + + + #Frames des Zuschnitt-Menüs + def crop_menu_frame(self): + self.crop_menu_frame_right = tk.Frame(self.root) + self.crop_menu_frame_right.grid(row=0, column=1, sticky="nsew") + + self.crop_menu_frame_down = tk.Frame(self.root) + self.crop_menu_frame_down.grid(row=1, column=0, sticky="nsew") + + self.crop_menu_frame_down_right = tk.Frame(self.root) + self.crop_menu_frame_down_right.grid(row=1, column=1, sticky="nsew") + + self.crop_menu_frame_right.grid_remove() + self.crop_menu_frame_down.grid_remove() + self.crop_menu_frame_down_right.grid_remove() + + #Frames des Bildeinstellungsmenüs + def settings_menu_frame(self): + self.settings_menu_frame_right = tk.Frame(self.root) + self.settings_menu_frame_right.grid(row=0, column=1, sticky="nsew") + + self.settings_menu_frame_down = tk.Frame(self.root) + self.settings_menu_frame_down.grid(row=1, column=0, sticky="nsew") + + self.settings_menu_frame_down_right = tk.Frame(self.root) + self.settings_menu_frame_down_right.grid(row=1, column=1, sticky="nsew") + + self.settings_menu_frame_right.grid_remove() + self.settings_menu_frame_down.grid_remove() + self.settings_menu_frame_down_right.grid_remove() + + #Frames des Dateimenüs + def file_menu_frame(self): + self.file_menu_frame_right = tk.Frame(self.root) + self.file_menu_frame_right.grid(row=0, column=1, sticky="nsew") + + self.file_menu_frame_down = tk.Frame(self.root) + self.file_menu_frame_down.grid(row=1, column=0, sticky="nsew") + + self.file_menu_frame_down_right = tk.Frame(self.root) + self.file_menu_frame_down_right.grid(row=1, column=1, sticky="nsew") + + self.file_menu_frame_right.grid_remove() + self.file_menu_frame_down.grid_remove() + self.file_menu_frame_down_right.grid_remove() + + + #Widgets für das Hauptmenü erstellen + def main_menu_widgets(self): + self.settings_menu_button = tk.Button(self.main_menu_frame_right, text="Bild-\neinstellungen", image=self.pixel, compound="c", height=80, width=80, command=self.show_settings_menu) + self.settings_menu_button.pack(pady=10) + + self.crop_menu_button = tk.Button(self.main_menu_frame_right, text="Zuschnitt", image=self.pixel, compound="c", height=80, width=80, command=self.show_crop_menu) + self.crop_menu_button.pack(pady=30) + + self.file_menu_button = tk.Button(self.main_menu_frame_right, text="Datei-\neinstellungen", image=self.pixel, compound="c", height=80, width=80, command=self.show_file_menu) + self.file_menu_button.pack(pady=30) + + buttons_frame_down = tk.Frame(self.main_menu_frame_down) + buttons_frame_down.pack(pady=10, fill='x', anchor='center') + + #Einen Frame erstellen, um die beiden unteren Buttons zentriert auszurichten + buttons_frame_down.grid_columnconfigure(0, weight=1) + buttons_frame_down.grid_columnconfigure(3, weight=1) + + # Vollbild beenden Button zentriert im Grid platzieren + self.fullscreen_button = tk.Button(buttons_frame_down, text="Vollbild beenden", command=self.toggle_fullscreen, width=20) + self.fullscreen_button.grid(row=0, column=1, padx=20) + + # Zoom Button zentriert im Grid und neben dem Vollbild beenden Button platzieren + self.zoom_button_state = tk.BooleanVar(value=False) + self.zoom_button = tk.Button(buttons_frame_down, text="Zoom", command=self.toggle_zoom, width=20) + self.zoom_button.grid(row=0, column=2, padx=20) + + + #Widgets des Zuschnittmenüs / Slider für ROI + def crop_menu_widgets(self): + back_button = tk.Button(self.crop_menu_frame_down_right, text="Zurück", image=self.pixel, compound="c", height=40, width=40, command=self.show_main_menu) + back_button.pack(pady=10) + + self.crop_width_value = tk.IntVar() + self.crop_width_slider = tk.Scale(self.crop_menu_frame_down, from_=1280, to=0, resolution= 2, orient='horizontal', variable=self.crop_width_value, command=self.update_value, width=30) + self.crop_width_slider.pack(fill='x', padx=50) + self.crop_width_value.set(1280) + + self.crop_height_value = tk.IntVar() + self.crop_height_slider = tk.Scale(self.crop_menu_frame_right, from_=800, to=0, resolution= 2, orient='vertical', variable=self.crop_height_value, command=self.update_value, width=30) + self.crop_height_slider.pack(fill='y', expand=True, pady=25, padx=10) + self.crop_height_value.set(800) + + + def settings_menu_widgets(self): + self.ext_lighting_state = tk.BooleanVar(value=False) + self.ext_lighting_button = tk.Button(self.settings_menu_frame_right, text="Beleuchtung \nPermanent", image=self.pixel, compound="c", height=80, width=80, command=self.toggle_ext_lighting) + self.ext_lighting_button.pack(pady=10) + + self.ext_flash_state = tk.BooleanVar(value=False) + self.ext_flash_button = tk.Button(self.settings_menu_frame_right, text="Blitz", image=self.pixel, compound="c", height=80, width=80, command=self.toggle_flash) + self.ext_flash_button.pack(pady=10) + + back_button = tk.Button(self.settings_menu_frame_down_right, text="Zurück", image=self.pixel, compound="c", height=40, width=40, command=self.show_main_menu) + back_button.pack(pady=10) + + self.show_preview_grid_state = tk.BooleanVar(value=False) + self.show_preview_grid_button = tk.Button(self.settings_menu_frame_right, text="Gitter", image=self.pixel, compound="c", height=80, width=80, command=self.toggle_preview_grid) + self.show_preview_grid_button.pack(pady=30) + + + buttons_frame = tk.Frame(self.settings_menu_frame_down) + buttons_frame.pack(pady=15, fill='x', expand=True) + + # Button für das Lineal + self.show_preview_ruler_state = tk.BooleanVar(value=False) + self.show_preview_ruler_button = tk.Button(buttons_frame, text="Lineal", image=self.pixel, compound="c", height=30, width=80, command=self.toggle_preview_ruler) + self.show_preview_ruler_button.pack(side='left', padx=20) + + # Container für Lineal horizontal mit zentrierten Eingabefeldern + horizontal_ruler_frame = tk.Frame(buttons_frame, height=60) + horizontal_ruler_frame.pack(side='left', padx=10) + tk.Label(horizontal_ruler_frame, text="Lineal horizontal", height=1).pack() + + # Frame für die Eingabefelder, um sie zentriert unter dem Text zu positionieren + horizontal_ruler_entries_frame = tk.Frame(horizontal_ruler_frame) + horizontal_ruler_entries_frame.pack() + tk.Entry(horizontal_ruler_entries_frame, width=5, textvariable=self.ruler_horizontal_start).pack(side='left', padx=(0, 5)) # Ein wenig Platz zwischen den Feldern + tk.Entry(horizontal_ruler_entries_frame, width=5, textvariable=self.ruler_horizontal_end).pack(side='left') + + # Container für Lineal vertikal mit zentrierten Eingabefeldern + vertical_ruler_frame = tk.Frame(buttons_frame, height=60) + vertical_ruler_frame.pack(side='left', padx=10) + tk.Label(vertical_ruler_frame, text="Lineal vertikal", height=1).pack() + + # Frame für die Eingabefelder, um sie zentriert unter dem Text zu positionieren + vertical_ruler_entries_frame = tk.Frame(vertical_ruler_frame) + vertical_ruler_entries_frame.pack() + tk.Entry(vertical_ruler_entries_frame, width=5, textvariable=self.ruler_vertical_start).pack(side='left', padx=(0, 5)) # Ein wenig Platz zwischen den Feldern + tk.Entry(vertical_ruler_entries_frame, width=5, textvariable=self.ruler_vertical_end).pack(side='left') + + # Button für die Uhrzeit + self.show_preview_clock_state = tk.BooleanVar(value=False) + self.show_preview_clock_button = tk.Button(buttons_frame, text="Uhrzeit", image=self.pixel, compound="c", height=30, width=80, command=self.toggle_preview_clock) + self.show_preview_clock_button.pack(side='left', padx=20) + + # Button für die Tastatur + keyboard_button = tk.Button(buttons_frame, text="Tastatur", image=self.pixel, compound="c", height=30, width=80, command=self.toggle_keyboard) + keyboard_button.pack(side='left', padx=20) + + + + + def file_menu_widgets(self): + back_button_frame = tk.Frame(self.file_menu_frame_down_right) + back_button_frame.pack(side='right', padx=10, pady=10) + back_button = tk.Button(back_button_frame, text="Zurück", image=self.pixel, compound="c", height=40, width=40, command=self.show_main_menu) + back_button.pack() + + text_font = Font(family="Helvetica", size=14) + self.style.configure("LargeCheckbutton.TCheckbutton", font=('Helvetica', 14)) + + # "Kopiermenü"-Label + label_copy_menu = tk.Label(self.file_menu_frame_right, text="Kopieren - Zielordner", font=self.font_bold) + label_copy_menu.pack(pady=(15, 5)) + + # "Zielpfad"-Bereich + destination_path_frame = tk.Frame(self.file_menu_frame_right) + destination_path_frame.pack(pady=5, fill='x', padx=10) + + self.entry_destination_path = tk.Entry(destination_path_frame, textvariable=self.destination_path_var, font=text_font, width=40) + self.entry_destination_path.pack(side='left') + + browse_button_destination = tk.Button(destination_path_frame, text="...", command=lambda: self.choose_directory(self.entry_destination_path)) + browse_button_destination.pack(side='left', padx=(5, 0)) + + # "Kopieren"-Button + button_copy = tk.Button(self.file_menu_frame_right, text="Kopieren", command=self.copy, font=self.font_normal) + button_copy.pack(pady=15) + + keyboard_button = tk.Button(self.file_menu_frame_down, text="Tastatur", font=self.font_normal, command=self.toggle_keyboard) + keyboard_button.pack(pady=10) + + # Horizontaler Trennstrich + separator = ttk.Separator(self.file_menu_frame_right, orient='horizontal') + separator.pack(fill='x', pady=5) + + label_file_menu = tk.Label(self.file_menu_frame_right, text="Dateimenü", font=self.font_bold) + label_file_menu.pack(pady=(5, 5)) + + # "Speicherpfad"-Bereich + label_storage_path = tk.Label(self.file_menu_frame_right, text="Speicherpfad", font=self.font_normal) + label_storage_path.pack(pady=5) + storage_path_frame = tk.Frame(self.file_menu_frame_right) + storage_path_frame.pack(pady=5, fill='x', padx=10) # padx=10 fügt Abstand zum linken Rand hinzu + + self.entry_storage_path = tk.Entry(storage_path_frame, textvariable=self.storage_path_var, font=text_font, width=40) # Reduzierte Breite + self.entry_storage_path.pack(side='left') + + browse_button_storage = tk.Button(storage_path_frame, text="...", command=lambda: self.choose_directory(self.entry_storage_path)) + browse_button_storage.pack(side='left', padx=(5, 0)) + + # "Dateiname"-Label und Eingabefeld + label_filename = tk.Label(self.file_menu_frame_right, text="Dateiname", font=self.font_normal) + label_filename.pack(pady=5) + self.entry_filename = tk.Entry(self.file_menu_frame_right, textvariable=self.filename_var, font=text_font, width=40) + self.entry_filename.pack(pady=5) + + # Gemeinsamer Rahmen für Datum- und Uhrzeit-Checkboxen + datetime_frame = tk.Frame(self.file_menu_frame_right) + datetime_frame.pack(pady=5, fill='x') + + # Datum-Checkbox und Label + date_frame = tk.Frame(datetime_frame) + date_frame.pack(side='left', expand=True, padx=5) + checkbox_date = ttk.Checkbutton(date_frame, text="Datum", style="LargeCheckbutton.TCheckbutton", variable=self.date_var) + checkbox_date.grid(row=0, column=0, sticky="w") + date_label = tk.Label(date_frame, text="", font=self.font_normal) + date_label.grid(row=0, column=1, sticky="w") + + # Uhrzeit-Checkbox und Label + time_frame = tk.Frame(datetime_frame) + time_frame.pack(side='left', expand=True, padx=5) + checkbox_time = ttk.Checkbutton(time_frame, text="Uhrzeit", style="LargeCheckbutton.TCheckbutton", variable=self.time_var) + checkbox_time.grid(row=0, column=0, sticky="w") + time_label = tk.Label(time_frame, text="", font=self.font_normal) + time_label.grid(row=0, column=1, sticky="w") + + + #Methode, um die Daten vom Arbeitspfad in den Zielordner zu kopieren + def copy(self): + try: + if not os.path.exists(self.destination_path_var.get()): + os.makedirs(self.destination_path_var.get()) + + for item in os.listdir(self.storage_path_var.get()): + source_item = os.path.join(self.storage_path_var.get(), item) + destination_item = os.path.join(self.destination_path_var.get(), item) + + if os.path.isdir(source_item): + shutil.copytree(source_item, destination_item, dirs_exist_ok=True) + else: + shutil.copy2(source_item, destination_item) + + self.show_success_message("Kopieren erfolgreich", "Alle Inhalte wurden erfolgreich kopiert.") + + except Exception as e: + messagebox.showerror("Fehler", f"Ein Fehler ist aufgetreten: {e}") + + + #Methode, um ein Nachrichtenfenster anzuzeigen + def show_success_message(self, title, message): + msg_window = tk.Toplevel(self.root) + msg_window.title(title) + msg_window.geometry("300x100") + tk.Label(msg_window, text=message).pack(pady=10) + + # OK-Button zum Schließen des Fensters + ok_button = tk.Button(msg_window, text="OK", command=msg_window.destroy) + ok_button.pack(pady=5) + + # Fenster automatisch nach 5 Sekunden schließen + msg_window.after(5000, msg_window.destroy) + + + #Aktuell aktives Menü schließen + def close_menu(self): + self.current_menu_right.grid_remove() + self.current_menu_down.grid_remove() + self.current_menu_down_right.grid_remove() + + #Aktuell aktives Menü öffnen (z.B. nachdem der Button ">", "<" gedrückt wurde) + def open_menu(self): + self.current_menu_right.grid() + self.current_menu_down.grid() + self.current_menu_down_right.grid() + self.update_aspect_ratio() + + def show_main_menu(self): + self.close_menu() + self.current_menu_right = self.main_menu_frame_right + self.current_menu_down = self.main_menu_frame_down + self.current_menu_down_right = self.main_menu_frame_down_right + self.open_menu() + + def show_crop_menu(self): + self.close_menu() + self.current_menu_right = self.crop_menu_frame_right + self.current_menu_down = self.crop_menu_frame_down + self.current_menu_down_right = self.crop_menu_frame_down_right + self.open_menu() + + def show_settings_menu(self): + self.close_menu() + self.current_menu_right = self.settings_menu_frame_right + self.current_menu_down = self.settings_menu_frame_down + self.current_menu_down_right = self.settings_menu_frame_down_right + self.open_menu() + + def show_file_menu(self): + self.close_menu() + self.current_menu_right = self.file_menu_frame_right + self.current_menu_down = self.file_menu_frame_down + self.current_menu_down_right = self.file_menu_frame_down_right + self.open_menu() + + #Methode, um den File-Browser zu öffnen + def choose_directory(self, entry_field): + folder_selected = filedialog.askdirectory() + if folder_selected: + entry_field.delete(0, tk.END) + entry_field.insert(0, folder_selected) + + + #Methode wird ausgeführt, sobald sich der Wert in einem der Eingabefelder geändert hat + def entry_fields_changed(self, var_name, *args): + self.still_image_processing.update_file_parameters(self.filename_var.get(), self.storage_path_var.get(), + self.destination_path_var.get(), self.date_var.get(), + self.time_var.get()) + + self.ip.update_ruler_values(int(self.ruler_vertical_start.get()), int(self.ruler_vertical_end.get()), + int(self.ruler_horizontal_start.get()), int(self.ruler_horizontal_end.get())) + + self.still_image_processing.get_ruler_settings(int(self.ruler_vertical_start.get()), int(self.ruler_vertical_end.get()), + int(self.ruler_horizontal_start.get()), int(self.ruler_horizontal_end.get())) + + + #Blendet das Menü ein bzw. aus + def toggle_menu(self): + if self.current_menu_right and self.current_menu_right.grid_info(): + self.root.grid_columnconfigure(1, minsize=0) + self.close_menu() + self.update_aspect_ratio() + self.video_widget.grid(row=0, column=0, sticky="nsew") + self.hide_menu_button.config(text="<") + + else: + if not self.current_menu_right: + self.current_menu_right = self.main_menu_frame_right + self.current_menu_down = self.main_menu_frame_down + self.current_menu_down_right = self.main_menu_frame_down_right + self.root.grid_columnconfigure(1, minsize=30) + self.open_menu() + self.menu_width = self.current_menu_right.winfo_width() + self.video_widget.grid(row=0, column=0, sticky="nsew") + self.hide_menu_button.config(text=">") + + + #Methode um die ROI-Werte an die Bildbearbeitung zu übergeben + def update_value(self, value): + self.preview_processing.update_roi(self.crop_width_value.get(), self.crop_height_value.get()) + self.still_image_processing.update_roi(self.crop_width_value.get(), self.crop_height_value.get()) + + + + + #Externe Beleuchtung permanent ein- / ausschalten + def toggle_ext_lighting(self): + self.ext_lighting_state.set(not self.ext_lighting_state.get()) + self.update_button_appearance(self.ext_lighting_button, self.ext_lighting_state) + + if self.ext_lighting_state.get(): + self.permanent_lighting = True + else: + self.permanent_lighting = False + self.gpio_instance.get_gui_settings(self.activate_flash, self.permanent_lighting) + + #Blitz ein- / ausschalten + def toggle_flash(self): + self.ext_flash_state.set(not self.ext_flash_state.get()) + self.update_button_appearance(self.ext_flash_button, self.ext_flash_state) + + if self.ext_flash_state.get(): + self.activate_flash = True + else: + self.activate_flash = False + self.gpio_instance.get_gui_settings(self.activate_flash, self.permanent_lighting) + + + #Button Gitter ein- aus + def toggle_preview_grid(self): + self.show_preview_grid_state.set(not self.show_preview_grid_state.get()) + self.update_button_appearance(self.show_preview_grid_button, self.show_preview_grid_state) + self.send_settings() + + #Button Lineal ein- aus + def toggle_preview_ruler(self): + self.show_preview_ruler_state.set(not self.show_preview_ruler_state.get()) + self.update_button_appearance(self.show_preview_ruler_button, self.show_preview_ruler_state) + self.send_settings() + + #Button Uhr ein - aus + def toggle_preview_clock(self): + self.show_preview_clock_state.set(not self.show_preview_clock_state.get()) + self.update_button_appearance(self.show_preview_clock_button, self.show_preview_clock_state) + self.send_settings() + + #Button Zoom ein- aus + def toggle_zoom(self): + self.zoom_button_state.set(not self.zoom_button_state.get()) + self.update_button_appearance(self.zoom_button, self.zoom_button_state) + self.change_preview_resolution() + self.send_settings() + + + #Aussehen der Toggle-Buttons variieren + def update_button_appearance(self, button, state_var): + if state_var.get(): + button.config(relief="sunken", bg="dimgray") # Eingeschalteter Zustand + + else: + button.config(relief="raised", bg="lightgray") # Ausgeschalteter Zustand + + + #Einstellungen der Toggle-Buttons an die bildverarbeitenden Instanzen übergeben + def send_settings(self): + self.preview_processing.get_settings(self.show_preview_grid_state.get(), self.show_preview_ruler_state.get(), + self.show_preview_clock_state.get(), self.zoom_button_state.get()) + + self.still_image_processing.update_picture_parameters(self.show_preview_clock_state.get(), + self.show_preview_ruler_state.get()) + + + #Seitenverhältnis anpassen, sodass das video_widget sich entsprechend der ROI anpasst + def update_aspect_ratio(self): + self.root.update_idletasks() + self.video_widget_width = self.video_widget.winfo_width() + self.video_widget_height = self.video_widget.winfo_height() + self.video_widget_aspect_ratio = self.video_widget_width / self.video_widget_height + + if self.video_widget_aspect_ratio is None: + self.new_width = 1280 + self.new_height = 800 + else: + self.new_width = self.video_widget_width + self.new_height = int(self.new_width / self.camera_aspect_ratio) + + #Tastatur ein- aus + def toggle_keyboard(self): + if self.keyboard_process: + self.keyboard_process.terminate() + self.keyboard_process = None + else: + self.keyboard_process = subprocess.Popen(['matchbox-keyboard']) + + + #Vorschaubild im video_widget aktualisieren + def update_video_widget(self): + image = self.camera.get_preview_image() #Bild aus der Kamera holen + image = self.preview_processing.get_preview_image(image) # Weitere Bildverarbeitung + + #Abfragen, falls das Bild in einem unerwarteten Format auftritt + if image.shape[2] not in [3, 4]: + raise ValueError("Das Bild muss 3 (RGB) oder 4 (RGBA) Farbkanäle haben") + + if image.dtype != np.uint8: + image = (image - image.min()) / (image.max() - image.min()) * 255.0 + image = image.astype(np.uint8) + + # Erzeugen des PIL-Bildes aus dem Array und Skalieren auf die neue Größe + resized_image = PIL.Image.fromarray(image).resize((self.new_width, self.new_height), PIL.Image.ANTIALIAS) + + self.preview_image = PIL.ImageTk.PhotoImage(image=resized_image) + self.video_widget.create_image(0, 0, image=self.preview_image, anchor=tk.NW) + self.root.after(self.video_feed_delay, self.update_video_widget) + + + #Erhöhen der Vorschauauflösung, um den gezoomten Bereich klarer darzustellen + def change_preview_resolution(self): + self.camera.get_highres_preview() + + + #Vollbild ein- ausschalten + def toggle_fullscreen(self): + if self.fullscreen_active == True: + self.root.attributes('-fullscreen', False) + self.fullscreen_button.config(text="Vollbild") + self.fullscreen_active = False + + else: + self.root.attributes('-fullscreen', True) + self.fullscreen_button.config(text="Vollbild beenden") + self.fullscreen_active = True + + + + + + diff --git a/1280 x 800/Image_Processing.py b/1280 x 800/Image_Processing.py new file mode 100644 index 0000000..420d748 --- /dev/null +++ b/1280 x 800/Image_Processing.py @@ -0,0 +1,335 @@ +import cv2 +import time +import numpy as np +import math +import datetime + +class Image_Processing(): + def __init__(self): + + #Parameter des Vorschaubildes + self.roi_width = 1280 + self.roi_height = 800 + self.preview_roi_height_offset = 0 + self.preview_roi_width_offset = 0 + self.preview_width = 1280 + self.preview_height = 800 + self.center_y = self.preview_height // 2 + self.center_x = self.preview_width // 2 + + #Einstellungen Gitter + self.grid_line_color = (255, 0, 0, 255) + self.grid_line_thickness = 2 + self.grid_line_spacing = 100 + self.show_grid = False + self.show_zoom = False + + #Zoom-Faktor für das Vorschaubild + self.zoom_factor = 1.5 + + + #Einstellungen Lineal + self.show_ruler = False + self.show_clock = False + self.horizontal_ruler_start = 0 + self.horizontal_ruler_end = 100 + self.vertical_ruler_start = 0 + self.vertical_ruler_end = 50 + self.ruler_thickness = 30 + self.ruler_font = cv2.FONT_HERSHEY_PLAIN + self.ruler_color = (255, 255, 255, 255) # Weiß + self.ruler_text_color = (0, 0, 0, 255) # Schwarz + self.ruler_line_color = (0, 0, 0, 255) # Schwarz + self.ruler_initial_width = self.preview_width + self.ruler_initial_height = self.preview_height + self.update_ruler_settings() + + #Einstellungen Uhr + self.font = cv2.FONT_HERSHEY_SIMPLEX + self.font_size = 0.5 + self.font_color = (0, 0, 0, 255) + self.background_color = (255, 255, 255, 255) + self.text_thickness = 1 + self.line_type = cv2.LINE_AA + + + self.right_border = self.preview_width + self.left_border = 0 + self.upper_border = self.preview_height + self.bottom_border = 0 + + + #Bild wird aus dem GUI-Code an Image_Processing übergeben (sorgt für flüssigeres Vorschaubild) + def get_preview_image(self, image): + self.preview_image = image + self.preview_image_processing() + return self.preview_image + + #Einstellungen werden aus der Benutzeroberfläche geladen + def get_settings(self, show_grid, show_ruler, show_clock, show_zoom): + self.show_grid = show_grid + self.show_ruler = show_ruler + self.show_clock = show_clock + self.show_zoom = show_zoom + + #Durchgeführte Schritte zur Bildbearbeitung des Vorschaubilds + def preview_image_processing(self): + self.crop_preview_image() + self.zoom_picture() + self.add_grid_lines() + self.add_ruler() + self.add_clock() + + + #Aktualisieren der Parameter zum Zuschneiden des ROI (symmetrischer Zuschnitt) + def update_roi(self, roi_width, roi_height): + self.roi_width = roi_width + self.roi_height = roi_height + + self.preview_roi_width_offset = round(((self.preview_width - self.roi_width) / 2)) + self.preview_roi_height_offset = round(((self.preview_height - self.roi_height) / 2)) + + self.right_border = self.center_x + (self.roi_width // 2) + self.left_border = self.center_x - (self.roi_width // 2) + self.upper_border = self.center_y + (self.roi_height // 2) + self.bottom_border = self.center_y - (self.roi_height // 2) + + self.update_ruler_settings() #Position des Lineals soll mit dem ROI mitwandern + + + + def crop_preview_image(self): + if self.roi_width != self.preview_width or self.roi_height != self.preview_height: + + cropped_image = self.preview_image[self.preview_roi_height_offset:(self.preview_height - self.preview_roi_height_offset), + self.preview_roi_width_offset:(self.preview_width - self.preview_roi_width_offset), :] + + # Erstellen eines neuen, schwarzen Bildes mit der gewünschten Größe (800x480) + final_image = np.zeros((self.preview_height, self.preview_width, 4), dtype=np.uint8) + + # Berechnen der Startkoordinaten für das Einfügen des zugeschnittenen Bildes in das schwarze Bild + start_y = (self.preview_height - (cropped_image.shape[0])) // 2 + start_x = (self.preview_width - (cropped_image.shape[1])) // 2 + + # Einfügen des zugeschnittenen Bildes in das schwarze Bild + final_image[start_y:start_y + cropped_image.shape[0], start_x:start_x + cropped_image.shape[1]] = cropped_image + + # Aktualisieren von self.preview_image mit dem neuen Bild + self.preview_image = final_image + + else: + return + + + #Gitterlinien zur Vorschau hinzufügen + def add_grid_lines(self): + if self.show_grid: + # Horizontale Linien + for y in range(self.center_y, self.bottom_border, -self.grid_line_spacing): # Nach oben + cv2.line(self.preview_image, (self.left_border, y), (self.right_border, y), self.grid_line_color, self.grid_line_thickness) + for y in range(self.center_y, self.upper_border, self.grid_line_spacing): # Nach unten + cv2.line(self.preview_image, (self.left_border, y), (self.right_border, y), self.grid_line_color, self.grid_line_thickness) + + # Vertikale Linien + for x in range(self.center_x, self.left_border, -self.grid_line_spacing): # Nach links + cv2.line(self.preview_image, (x, self.bottom_border), (x, self.upper_border), self.grid_line_color, self.grid_line_thickness) + for x in range(self.center_x, self.right_border, self.grid_line_spacing): # Nach rechts + cv2.line(self.preview_image, (x, self.bottom_border), (x, self.upper_border), self.grid_line_color, self.grid_line_thickness) + + + #Einstellungen für das Lineal aktualisieren + def update_ruler_settings(self): + #Prüfen, ob der Bereich des Lineals gestreckt/gestaucht werden muss + self.ruler_scaling_width = self.roi_width / self.ruler_initial_width + self.ruler_scaling_height = self.roi_height / self.ruler_initial_height + + #Endwert des Lineals berechnen + self.ruler_horizontal_end = (self.horizontal_ruler_end - self.horizontal_ruler_start) * self.ruler_scaling_width + self.ruler_vertical_end = (self.vertical_ruler_end - self.vertical_ruler_start) * self.ruler_scaling_height + + #Ausgehend vom Bildbereich und des Endwerts wird die räumliche Auflösung in Pixel pro mm berechnet, um das Lineal anschließend flexibel zu skalieren + self.px_mm_horizontal = math.ceil(self.roi_width / self.ruler_horizontal_end) + self.px_mm_vertical = math.ceil(self.roi_height / self.ruler_vertical_end) + + + #Lineal im Vorschaubild anzeigen + def add_ruler(self): + if self.show_ruler: + # ROI Position und Größe in der Benutzeroberfläche berechnen + roi_x_start = max(0, self.center_x - self.roi_width // 2) + roi_y_start = max(0, self.center_y - self.roi_height // 2) + + # Erstellen des vertikalen Lineals + adjusted_height = self.roi_height + self.ruler_vertical = np.zeros((adjusted_height, self.ruler_thickness, 4), dtype=np.uint8) + self.ruler_vertical[:, :] = self.ruler_color + + # Erstellen des horizontalen Lineals + self.ruler_horizontal = np.zeros((self.ruler_thickness, self.roi_width, 4), dtype=np.uint8) + self.ruler_horizontal[:, :] = self.ruler_color + + # Markierungen und Beschriftungen zum vertikalen Lineal hinzufügen + for pos in range(self.ruler_thickness, self.roi_height, self.px_mm_vertical * 5): + is_thick_line = (pos - self.ruler_thickness) % (self.px_mm_vertical * 10) == 0 #Alle 10 mm eine dicke Linie + line_thickness = 2 if is_thick_line else 1 + line_length = self.ruler_thickness if is_thick_line else self.ruler_thickness // 2 + line_start = 0 if is_thick_line else int(self.ruler_thickness * 0.5) # Dünne Linien sind halb so lang + cv2.line(self.ruler_vertical, (line_start, pos), (self.ruler_thickness, pos), self.ruler_line_color, line_thickness) + + if is_thick_line: + text = f"{int((pos - self.ruler_thickness) / self.px_mm_vertical)}" + cv2.putText(self.ruler_vertical, text, (2, pos - 5), self.ruler_font, 0.8, self.ruler_text_color, 1) + + # Markierungen und Beschriftungen zum horizontalen Lineal hinzufügen + for pos in range(0, self.roi_width, self.px_mm_horizontal * 5): + is_thick_line = pos % (self.px_mm_horizontal * 10) == 0 + line_thickness = 2 if is_thick_line else 1 + line_start = 0 if is_thick_line else int(self.ruler_thickness * 0.5) + cv2.line(self.ruler_horizontal, (pos + self.ruler_thickness, line_start), (pos + self.ruler_thickness, self.ruler_thickness), self.ruler_line_color, line_thickness) + + if is_thick_line: + text = f"{int((pos / self.px_mm_horizontal))}" + text_x = pos +25- cv2.getTextSize(text, self.ruler_font, 0.8, 1)[0][0] + text_y = self.ruler_thickness - 5 #Abstand zum oberen Rand + cv2.putText(self.ruler_horizontal, text, (text_x, text_y), self.ruler_font, 0.8, self.ruler_text_color, 1) + + # Platzieren des vertikalen Lineals im Vorschaubild + self.preview_image[roi_y_start:roi_y_start + self.roi_height, roi_x_start:roi_x_start + self.ruler_thickness] = self.ruler_vertical + + # Platzieren des horizontalen Lineals im Vorschaubild + self.preview_image[roi_y_start:roi_y_start + self.ruler_thickness, roi_x_start:roi_x_start + self.roi_width] = self.ruler_horizontal + + + + #Uhrzeit unten rechts hinzufügen + def add_clock(self): + if self.show_clock: + timestamp = datetime.datetime.now().strftime("%H:%M") + image_height, image_width = self.preview_image.shape[:2] + + # Abstand vom linken Rand abhängig vom Lineal + margin_x = self.ruler_thickness + 5 if self.show_ruler else 5 + # Konstanter Abstand vom unteren Rand + margin_y = 30 + + # Padding erhöhen, um die Box größer zu machen + padding = 5 + + # Dynamische Schriftgröße basierend auf Bildhöhe + font_scale = image_height / 800 + + # Textgröße und -position berechnen + (text_width, text_height), base_line = cv2.getTextSize(timestamp, self.font, font_scale, self.text_thickness) + + # Die y-Position des Textes unter Berücksichtigung des unteren Randes + text_position_y = image_height - margin_y - base_line - padding + text_position = (margin_x + padding, text_position_y) + + # Berechnung der Hintergrundpositionen unter Berücksichtigung des zusätzlichen Padding + background_l = (text_position[0] - padding, text_position[1] - text_height - padding) + background_r = (text_position[0] + text_width + padding, text_position[1] + base_line + padding) + + # Hintergrund und Text zeichnen + cv2.rectangle(self.preview_image, background_l, background_r, self.background_color, -1) + cv2.putText(self.preview_image, timestamp, text_position, self.font, font_scale, self.font_color, self.text_thickness, self.line_type) + + + #Start- und Endwerte des Lineals werden durch die Benutzeroberfläche definiert + def update_ruler_values(self, ruler_vertical_start, ruler_vertical_end, + ruler_horizontal_start, ruler_horizontal_end): + self.vertical_ruler_start = ruler_vertical_start + self.vertical_ruler_end = ruler_vertical_end + self.horizontal_ruler_start = ruler_horizontal_start + self.horizontal_ruler_end = ruler_horizontal_end + + self.update_ruler_settings() + + + #Vorschaubild an einen Bereich gezoomt + def zoom_picture(self): + if self.show_zoom: + image_height, image_width = self.preview_image.shape[:2] + + # Größe des zentralen Bereichs berechnen, der mit doppeltem Zoom dargestellt werden soll + # Die Größe des zentralen Bereichs ist halb so groß wie die Originalgröße geteilt durch den Zoomfaktor + central_width = round(image_width // (2 * self.zoom_factor)) + central_height = round(image_height // (2 * self.zoom_factor)) + + # Mittelpunkt des Bildes berechnen + center_x = image_width // 2 + center_y = image_height // 2 + + # Zentralen Bereich definieren + crop_x_start = max(center_x - central_width // 2, 0) + crop_x_end = min(center_x + central_width // 2, image_width) + crop_y_start = max(center_y - central_height // 2, 0) + crop_y_end = min(center_y + central_height // 2, image_height) + + # Bildausschnitt extrahieren + cropped_image = self.preview_image[crop_y_start:crop_y_end, crop_x_start:crop_x_end] + + # Bildausschnitt auf die ursprüngliche Größe des `preview_image` skalieren + scaled_image = cv2.resize(cropped_image, (self.preview_width, self.preview_height)) + + # Das skalierte Bild als neues Vorschaubild festlegen + self.preview_image = scaled_image + + + + + + + + + + + + + + + + + + + + + #Methode um das Bild verdunkelt darzustellen, ist aktuell nicht implementiert, da die Performance etwas schlechter ist... + #Sieht aber besser aus, hierbei wird der Bereich außerhalb der ROI verdunkelt dargestellt + def crop_preview_image_Darkened(self): + + start_time = time.time() + + if self.roi_width != self.preview_width or self.roi_height != self.preview_height: + # Erstellen einer Kopie des Bildes für die Verdunkelung + darkened_image = self.preview_image.copy() + + # Definition des Verdunkelungsfaktors + darken_factor = 0.7 + + # Erstellen einer Maske für die Bereiche, die verdunkelt werden sollen + mask = np.zeros_like(self.preview_image, dtype=bool) + + # Setzen der Bereiche außerhalb des ROI in der Maske auf True + top = self.preview_roi_height_offset + bottom = self.preview_height - self.preview_roi_height_offset + left = self.preview_roi_width_offset + right = self.preview_width - self.preview_roi_width_offset + + mask[:top] = True + mask[bottom:] = True + mask[top:bottom, :left] = True + mask[top:bottom, right:] = True + + # Anwenden der Verdunkelung nur auf die durch die Maske ausgewählten Bereiche + darkened_image[mask] = (darkened_image[mask].astype(np.float32) * darken_factor).astype(np.uint8) + + # Aktualisieren des Bildes mit der verdunkelten Version + self.preview_image = darkened_image + + print(f"Zeit: {time.time() - start_time}") + else: + return + + + + diff --git a/1280 x 800/Main.py b/1280 x 800/Main.py new file mode 100644 index 0000000..8c3f5a0 --- /dev/null +++ b/1280 x 800/Main.py @@ -0,0 +1,28 @@ +import threading +import Camera +import Gui_Grid +import Image_Processing +import gpio_control +import Still_Image_Processing + +#Instanz zur Endbild-Bearbeitung starten +still_image_processing = Still_Image_Processing.Still_Image_Processing() + +#Kamerainstanz starten +camera = Camera.Camera(still_image_processing) +camera.start_camera() + +#Instanz zur Vorschaubild-Bearbeitung starten +preview_processing = Image_Processing.Image_Processing() + +#GPIO-Überwachung starten +gpio_instance = gpio_control.GPIOControl(camera) +gpio_thread = threading.Thread(target=gpio_instance.start) +gpio_thread.start() + +#Benutzeroberfläche starten +gui = Gui_Grid.User_Interface(camera, preview_processing, gpio_instance, still_image_processing) +gui.window_settings() +gui.build_widgets() +gui.update_video_widget() +gui.start_interface() diff --git a/1280 x 800/Still_Image_Processing.py b/1280 x 800/Still_Image_Processing.py new file mode 100644 index 0000000..f51bfb6 --- /dev/null +++ b/1280 x 800/Still_Image_Processing.py @@ -0,0 +1,266 @@ +import cv2 +import numpy as np +import math +import datetime +import os + + +class Still_Image_Processing(): + def __init__(self): + + #Einstellungen für den Dateinamen / Kopiermenü + self.file_name_gui = "" + self.storage_path = "/home/pi" + self.copy_path = "" + self.filename_add_date = False + self.filename_add_time = False + self.filename = "" + + #Flags, ob die Zeit bzw. das Lineal im finalen Bild zu sehen sind + self.add_time = False + self.add_ruler = False + + #Parameter des Vorschaubilds und des finalen Bilds, werden zur Skalierung benötigt + self.preview_width = 1280 + self.preview_height = 800 + self.preview_width_roi = 1280 + self.preview_height_roi = 800 + self.still_width_roi = 4056 + self.still_height_roi = 3040 + self.still_width = 4056 + self.still_height = 3040 + + #Parameter für ROI + self.still_width_offset = 0 + self.still_height_offset = 0 + + #Einstellungen für die Uhr + self.font = cv2.FONT_HERSHEY_SIMPLEX + self.font_size = 1 + self.font_color = (0, 0, 0, 255) + self.background_color = (255, 255, 255, 255) + self.text_thickness = 2 + self.line_type = cv2.LINE_AA + + #Einstellungen für das Lineal + self.horizontal_ruler_start = 0 + self.horizontal_ruler_end = 100 + self.vertical_ruler_start = 0 + self.vertical_ruler_end = 50 + self.ruler_thickness = 80 + self.ruler_font_size = 2 + self.ruler_font = cv2.FONT_HERSHEY_PLAIN + self.ruler_color = (255, 255, 255, 255) # Weiß + self.ruler_text_color = (0, 0, 0, 255) # Schwarz + self.ruler_line_color = (0, 0, 0, 255) # Schwarz + self.ruler_initial_width = self.preview_width + self.ruler_initial_height = self.preview_height + + self.still_image = None + + + #Einstellungen für Dateinamen werden durch die Benutzeroberfläche definiert + def update_file_parameters(self, file_name, storage_path, copy_path, filename_add_date, filename_add_time): + self.file_name_gui = file_name + self.storage_path = storage_path + self.copy_path = copy_path + self.filename_add_date = filename_add_date + self.filename_add_time = filename_add_time + + + #Bildung des Dateinamens. Standard ist "Bild-{Nummerierung}". Ggf. kann das Bild mit Datum / Zeit versehen werden oder einen individuellen Namen erhalten + def update_filename(self): + base_name = self.file_name_gui.rstrip(".png") if self.file_name_gui else "Bild" + + time_date_now = datetime.datetime.now() + if self.filename_add_date: + base_name += time_date_now.strftime("_%Y-%m-%d") + if self.filename_add_time: + base_name += time_date_now.strftime("_%H-%M-%S") + + final_name = base_name + counter = 1 + while os.path.exists(os.path.join(self.storage_path, final_name + ".png")): #Wenn Bild mit dem Namen bereits existiert, wird der Zähler erhöht, bis kein Bild gefunden wurde + final_name = f"{base_name}-{counter}" + counter += 1 + + self.file_name = os.path.join(self.storage_path, final_name + ".png") + + + #Methode zum Speichern des Bildes + def save_picture(self): + self.update_filename() + rgb_image = cv2.cvtColor(self.still_image, cv2.COLOR_BGR2RGB) #Konvertierung von BGR zu RGB erforderlich + cv2.imwrite(self.file_name, rgb_image) + + + #Es wird aus der GUI übergeben, ob die Uhr bzw. das Lineal angezeigt werden sollen + def update_picture_parameters(self, add_time_to_image, add_ruler_to_image): + self.add_time = add_time_to_image + self.add_ruler = add_ruler_to_image + + #Aktualisieren der ROI und der erforderlichen Parameter + def update_roi(self, roi_width, roi_height): + self.preview_width_roi = roi_width + self.preview_height_roi = roi_height + + + self.roi_width_factor = self.preview_width_roi / self.preview_width + self.roi_height_factor = self.preview_height_roi / self.preview_height + + self.still_width_roi = round(self.roi_width_factor * self.still_width) + self.still_height_roi = round(self.roi_height_factor * self.still_height) + + self.still_width_offset = round(((self.still_width - self.still_width_roi) / 2)) + self.still_height_offset = round(((self.still_height - self.still_height_roi) / 2)) + + + #Uhrzeit im Bild einblenden + def add_time_to_image(self): + if self.add_time == True: + timestamp = datetime.datetime.now().strftime("%H:%M") + image_height, image_width = self.still_image.shape[:2] + + # Abstand vom linken Rand abhängig vom Lineal + margin_x = self.ruler_thickness + 5 if self.add_ruler else 5 + # Konstanter Abstand vom unteren Rand + margin_y = 30 + + # Padding erhöhen, um die Box größer zu machen + padding = 5 + + # Dynamische Schriftgröße basierend auf Bildhöhe + font_scale = image_height / 800 + + # Textgröße und -position berechnen + (text_width, text_height), base_line = cv2.getTextSize(timestamp, self.font, font_scale, self.text_thickness) + + # Die y-Position des Textes unter Berücksichtigung des unteren Randes + text_position_y = image_height - margin_y - base_line - padding + text_position = (margin_x + padding, text_position_y) + + # Berechnung der Hintergrundpositionen + background_l = (text_position[0] - padding, text_position[1] - text_height - padding) + background_r = (text_position[0] + text_width + padding, text_position[1] + base_line + padding) + + cv2.rectangle(self.still_image, background_l, background_r, self.background_color, -1) + cv2.putText(self.still_image, timestamp, text_position, self.font, font_scale, self.font_color, self.text_thickness, self.line_type) + + + + #Einstellungen des Lineals aus der Benutzeroberfläche laden + def get_ruler_settings(self, ruler_vertical_start, ruler_vertical_end, + ruler_horizontal_start, ruler_horizontal_end): + + self.vertical_ruler_start = ruler_vertical_start + self.vertical_ruler_end = ruler_vertical_end + self.horizontal_ruler_start = ruler_horizontal_start + self.horizontal_ruler_end = ruler_horizontal_end + + + #Erforderliche Einstellungen des Lineals bearbeiten, dieses passt sich an den linken bzw. oberen Bildrand an + def update_ruler_settings(self): + self.ruler_scaling_width = self.still_image.shape[1] / self.still_width + self.ruler_scaling_height = self.still_image.shape[0] / self.still_height + + self.ruler_horizontal_end = (self.horizontal_ruler_end - self.horizontal_ruler_start) * self.ruler_scaling_width + self.ruler_vertical_end = (self.vertical_ruler_end - self.vertical_ruler_start) * self.ruler_scaling_height + + self.px_mm_horizontal = math.ceil(self.still_image.shape[1] / self.ruler_horizontal_end) + self.px_mm_vertical = math.ceil(self.still_image.shape[0] / self.ruler_vertical_end) + + + def add_ruler_to_image(self): + if self.add_ruler: + self.update_ruler_settings() + # Initialisierung des horizontalen Lineals am oberen Rand als RGB-Bild + self.ruler_horizontal = np.zeros((self.ruler_thickness, self.still_image.shape[1], 3), dtype=np.uint8) + self.ruler_horizontal[:, :] = self.ruler_color[:3] + + self.ruler_vertical = np.zeros((self.still_image.shape[0], self.ruler_thickness, 3), dtype=np.uint8) + self.ruler_vertical[:, :] = self.ruler_color[:3] + + # Markierungen und Beschriftungen zum horizontalen Lineal hinzufügen + for pos in range(self.ruler_thickness, self.still_image.shape[1], self.px_mm_horizontal * 5): + is_thick_line = (pos-self.ruler_thickness) % (self.px_mm_horizontal * 10) == 0 + line_thickness = 2 if is_thick_line else 1 + cv2.line(self.ruler_horizontal, (pos, 0), (pos, self.ruler_thickness), self.ruler_line_color, line_thickness) + if is_thick_line: + text = f"{int((pos-self.ruler_thickness) / self.px_mm_horizontal)}" + text_x = pos + self.ruler_thickness // 2 - cv2.getTextSize(text, self.ruler_font, self.ruler_font_size, 1)[0][0] // 2 + text_y = self.ruler_thickness - 5 + cv2.putText(self.ruler_horizontal, text, (text_x, text_y), self.ruler_font, self.ruler_font_size, self.ruler_text_color, 1) + + # Markierungen und Beschriftungen zum vertikalen Lineal hinzufügen + for pos in range(self.ruler_thickness, self.still_image.shape[0], self.px_mm_vertical * 5): + is_thick_line = (pos-self.ruler_thickness) % (self.px_mm_vertical * 10) == 0 + line_thickness = 2 if is_thick_line else 1 + cv2.line(self.ruler_vertical, (0, pos), (self.ruler_thickness, pos), self.ruler_line_color, line_thickness) + if is_thick_line: + text = f"{int((pos-self.ruler_thickness) / self.px_mm_vertical)}" + cv2.putText(self.ruler_vertical, text, (2, pos - 5), self.ruler_font, self.ruler_font_size, self.ruler_text_color, 1) + + # Anbringen des horizontalen Lineals am oberen Rand des Bildes + self.still_image[:self.ruler_thickness, :] = self.ruler_horizontal + + # Anbringen des vertikalen Lineals am linken Rand des Bildes + self.still_image[:, :self.ruler_thickness] = self.ruler_vertical + + + #Alle Schritte zur Bearbeitung des finalen Bildes + def process_image(self, image): + self.still_image = image + self.crop_image() + self.add_time_to_image() + self.add_ruler_to_image() + self.save_picture() + + + #Der in der ROI definierten Bildbereich extrahieren + def crop_image(self): + if self.still_width_roi != self.still_width or self.still_height_roi != self.still_height: + + self.still_image = self.still_image[self.still_height_offset:(self.still_height - self.still_height_offset), + self.still_width_offset:(self.still_width - self.still_width_offset), :] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/1280 x 800/gpio_control.py b/1280 x 800/gpio_control.py new file mode 100644 index 0000000..0cffae1 --- /dev/null +++ b/1280 x 800/gpio_control.py @@ -0,0 +1,74 @@ +import RPi.GPIO as GPIO +import time +import threading + +class GPIOControl: + def __init__(self, camera): + GPIO.setmode(GPIO.BCM) + self.camera = camera + + #Verzögerung im Ein- und Ausschalten der Beleuchtung + self.lighting_delay = 100 / 1000 + + # Pin-Definitionen + self.lighting_trigger_gpio = 17 #Schalter für Beleuchtung Ein/Aus + self.camera_trigger_gpio = 27 #Schalter für Kamera auslösen + self.lighting_output_gpio = 23 #Ausgang für Beleuchtung + + self.activate_flash = False + self.permanent_lighting = False + + # Pin-Setup + GPIO.setup(self.lighting_trigger_gpio, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(self.camera_trigger_gpio, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(self.lighting_output_gpio, GPIO.OUT, initial=GPIO.LOW) + + self.camera_trigger_time = None + self.running = True + + def start(self): + # Startet die Überwachung in einem separaten Thread, ist wegen der blockierenden Ausführung der Benutzeroberfläche erforderlich + self.thread = threading.Thread(target=self.monitor_gpio) + self.thread.start() + + def stop(self): + self.running = False + self.thread.join() + + #Blitz / Dauerbeleuchtung an/aus wird durch GUI eingestellt + def get_gui_settings(self, activate_flash, permanent_lighting): + self.activate_flash = activate_flash + self.permanent_lighting = permanent_lighting + + self.activate_lighting() + self.deactivate_lighting() + + + + def activate_lighting(self): + if self.activate_flash == True or self.permanent_lighting == True: + GPIO.output(self.lighting_output_gpio, GPIO.HIGH) + + def deactivate_lighting(self): + if self.permanent_lighting == False: + GPIO.output(self.lighting_output_gpio, GPIO.LOW) + + def monitor_gpio(self): + try: + while self.running: + if GPIO.input(self.lighting_trigger_gpio) == GPIO.LOW: + self.activate_lighting() + + if GPIO.input(self.camera_trigger_gpio) == GPIO.LOW: + self.activate_lighting() + time.sleep(self.lighting_delay) + self.camera.get_still_image() + time.sleep(self.lighting_delay) + self.deactivate_lighting() + + else: + if GPIO.input(self.camera_trigger_gpio) == GPIO.HIGH and GPIO.input(self.lighting_trigger_gpio) == GPIO.HIGH: + self.deactivate_lighting() + time.sleep(0.1) + finally: + GPIO.cleanup() diff --git a/800x480/Camera.py b/800x480/Camera.py new file mode 100644 index 0000000..e2129eb --- /dev/null +++ b/800x480/Camera.py @@ -0,0 +1,55 @@ +from picamera2 import Picamera2, Preview +import PIL.Image, PIL.ImageTk + + +class Camera(): + def __init__(self, still_image_processing): + #Auflösung des Vorschaubildes + self.preview_width = 800 + self.preview_height = 480 + #Auflösung des finalen Bildes + self.still_width = 4056 + self.still_height = 3040 + + self.camera = Picamera2() + #Low-Resolution-Config, für flüssigeres Vorschaubild + self.lowres_preview_config = self.camera.create_preview_configuration(main={"size": (self.preview_width, self.preview_height)}) + #Einstellung für Vorschaubild in höherer AUflsung (für Zoom) + self.highres_preview_config = self.camera.create_preview_configuration(main={"size": (2028, 1080)}) + #Einstellung für finales Bild, maximale Auflösung + self.capture_config = self.camera.create_still_configuration(main={"size": (self.still_width, self.still_height)}) + + + self.preview_config = self.lowres_preview_config + self.still_image_processing = still_image_processing + self.set_highres_preview_config = False + + def start_camera(self): + self.camera.configure(self.preview_config) + self.camera.start() + + def get_preview_image(self): + image = self.camera.capture_array() + return image + + #Kameravorschau auf höhere Auflösung umstellen + def get_highres_preview(self): + self.camera.stop() + if self.set_highres_preview_config == False: + self.preview_config = self.highres_preview_config + self.set_highres_preview_config = True + else: + self.preview_config = self.lowres_preview_config + self.set_highres_preview_config = False + + self.camera.configure(self.preview_config) + self.camera.start() + + def get_still_image(self): + image = self.camera.switch_mode_and_capture_array(self.capture_config) + self.still_image_processing.process_image(image) + + def get_aspect_ratio(self): + preview_ratio = self.preview_width / self.preview_height + return preview_ratio + diff --git a/800x480/Gui_Grid.py b/800x480/Gui_Grid.py new file mode 100644 index 0000000..2b3b456 --- /dev/null +++ b/800x480/Gui_Grid.py @@ -0,0 +1,638 @@ +import tkinter as tk +import PIL.Image, PIL.ImageTk +import numpy as np +from tkinter import ttk +from tkinter.font import Font +from tkinter import filedialog, messagebox +import subprocess +import os +import shutil + +class User_Interface(): + def __init__(self, camera, preview_processing, gpio_instance, still_image_processing): + #Instanzvariablen laden + self.still_image_processing = still_image_processing + self.preview_processing = preview_processing + self.gpio_instance = gpio_instance + self.camera = camera + self.root = tk.Tk() + + #Verzögerung des Vorschaubildes + self.video_feed_delay = 5 + + #Seitenverhältnis der Kamera und des Widgets auslesen, zur Platzierung des Vorschaubildes in diesem + self.camera_aspect_ratio = self.camera.get_aspect_ratio() + self.video_widget_aspect_ratio = None + self.new_width = 800 + self.new_height = 480 + + #Flags für das derzeit aktive Menü + self.current_menu_right = None + self.current_menu_down = None + self.current_menu_down_right = None + self.submenu = False + + #Ein leerer Pixel, wird zur quadratischen Form der Buttons benötigt + self.pixel = tk.PhotoImage() + + + self.keyboard_process = None + + #Parameter des Lineals, mit Aktion verknüpft, sobald der Wert im Eingabefeld geändert wird + self.ruler_vertical_start = tk.StringVar() + self.ruler_vertical_end = tk.StringVar() + self.ruler_horizontal_start = tk.StringVar() + self.ruler_horizontal_end = tk.StringVar() + + self.ruler_vertical_start.set(0) + self.ruler_vertical_end.set(50) + self.ruler_horizontal_start.set(0) + self.ruler_horizontal_end.set(100) + + self.ruler_vertical_start.trace_add("write", lambda var, indx, mode: self.entry_fields_changed("ruler_vertical_start")) + self.ruler_vertical_end.trace_add("write", lambda var, indx, mode: self.entry_fields_changed("ruler_vertical_end")) + self.ruler_horizontal_start.trace_add("write", lambda var, indx, mode: self.entry_fields_changed("ruler_horizontal_start")) + self.ruler_horizontal_end.trace_add("write", lambda var, indx, mode: self.entry_fields_changed("ruler_horizontal_end")) + + + #Blitz / permanente Beleuchtung an / aus + self.permanent_lighting = False + self.activate_flash = False + + #Vollbild an / aus + self.fullscreen_active = True + + # Definiere Schriftarten + self.font_bold = Font(family="Helvetica", size=12, weight="bold") + self.font_normal = Font(family="Helvetica", size=12) + + # Checkbox Größe anpassen + self.style = ttk.Style() + self.style.configure("LargeCheckbutton.TCheckbutton", font=self.font_normal, size=25) + + #Variablen für den Dateinamen, ebenfalls mit Aktion verknüpft + self.storage_path_var = tk.StringVar(name="storage_path_var") + self.storage_path_var.set("/home/pi") + self.destination_path_var = tk.StringVar(name="destination_path_var") + self.filename_var = tk.StringVar(name="filename_var") + self.date_var = tk.BooleanVar(name="date_var") + self.time_var = tk.BooleanVar(name="time_var") + self.date_var.set(True) + self.time_var.set(True) + + self.storage_path_var.trace_add("write", lambda *args: self.entry_fields_changed("Speicherpfad", *args)) + self.destination_path_var.trace_add("write", lambda *args: self.entry_fields_changed("Zielpfad", *args)) + self.filename_var.trace_add("write", lambda *args: self.entry_fields_changed("Dateiname", *args)) + self.date_var.trace_add("write", lambda *args: self.entry_fields_changed("Datum", *args)) + self.time_var.trace_add("write", lambda *args: self.entry_fields_changed("Uhrzeit", *args)) + + + #GUI soll im Vollbild starten + def window_settings(self): + self.root.attributes('-fullscreen', True) + self.root.title('Titel') + + def start_interface(self): + self.root.mainloop() + + #Erstellen der Widgets aller Menüs + def build_widgets(self): + self.build_video_widget() + self.build_hide_menu_button() + self.main_menu_frame() + self.crop_menu_frame() + self.settings_menu_frame() + self.file_menu_frame() + self.main_menu_widgets() + self.crop_menu_widgets() + self.settings_menu_widgets() + self.file_menu_widgets() + + self.root.grid_rowconfigure(0, weight=1) + self.root.grid_columnconfigure(0, weight=1) + self.root.grid_columnconfigure(1, minsize=0) + + + def build_video_widget(self): + self.video_widget = tk.Canvas(self.root, width=800, height=480, highlightthickness=0) + self.video_widget.grid(row=0, column=0, sticky="nsew") + + #Button "<", ">" zum Anzeigen / Verstecken des Menüs + def build_hide_menu_button(self): + self.hide_menu_button_container = tk.Frame(self.video_widget, background="white", bd=0, highlightthickness=0) + self.hide_menu_button_container.place(relx=1.0, rely=0.0, anchor="ne") + + self.hide_menu_button_settings = { + "master": self.hide_menu_button_container, + "image": self.pixel, + "compound": "c", + "height": 30, + "width": 30, + "command": self.toggle_menu + } + + self.hide_menu_button = tk.Button(**self.hide_menu_button_settings, text="<") + self.hide_menu_button.pack(side="right", padx=0, pady=0) + + + #Frames des Hauptmenüs, alle Menüs teilen sich in 3 Frames auf: Rechts vom Vorschaubild, Unter dem Vorschaubild sowie ein Frame unten rechts zwischen dem rechten und dem unteren Frame (2x2-Raster) + def main_menu_frame(self): + self.main_menu_frame_right = tk.Frame(self.root) + self.main_menu_frame_right.grid(row=0, column=1, sticky="nsew") + + self.main_menu_frame_down = tk.Frame(self.root) + self.main_menu_frame_down.grid(row=1, column=0, sticky="nsew") + + self.main_menu_frame_down_right = tk.Frame(self.root) + self.main_menu_frame_down_right.grid(row=1, column=1, sticky="nsew") + + self.main_menu_frame_right.grid_remove() + self.main_menu_frame_down.grid_remove() + self.main_menu_frame_down_right.grid_remove() + + + #Frames des Zuschnitt-Menüs + def crop_menu_frame(self): + self.crop_menu_frame_right = tk.Frame(self.root) + self.crop_menu_frame_right.grid(row=0, column=1, sticky="nsew") + + self.crop_menu_frame_down = tk.Frame(self.root) + self.crop_menu_frame_down.grid(row=1, column=0, sticky="nsew") + + self.crop_menu_frame_down_right = tk.Frame(self.root) + self.crop_menu_frame_down_right.grid(row=1, column=1, sticky="nsew") + + self.crop_menu_frame_right.grid_remove() + self.crop_menu_frame_down.grid_remove() + self.crop_menu_frame_down_right.grid_remove() + + #Frames des Bildeinstellungsmenüs + def settings_menu_frame(self): + self.settings_menu_frame_right = tk.Frame(self.root) + self.settings_menu_frame_right.grid(row=0, column=1, sticky="nsew") + + self.settings_menu_frame_down = tk.Frame(self.root) + self.settings_menu_frame_down.grid(row=1, column=0, sticky="nsew") + + self.settings_menu_frame_down_right = tk.Frame(self.root) + self.settings_menu_frame_down_right.grid(row=1, column=1, sticky="nsew") + + self.settings_menu_frame_right.grid_remove() + self.settings_menu_frame_down.grid_remove() + self.settings_menu_frame_down_right.grid_remove() + + #Frames des Dateimenüs + def file_menu_frame(self): + self.file_menu_frame_right = tk.Frame(self.root) + self.file_menu_frame_right.grid(row=0, column=1, sticky="nsew") + + self.file_menu_frame_down = tk.Frame(self.root) + self.file_menu_frame_down.grid(row=1, column=0, sticky="nsew") + + self.file_menu_frame_down_right = tk.Frame(self.root) + self.file_menu_frame_down_right.grid(row=1, column=1, sticky="nsew") + + self.file_menu_frame_right.grid_remove() + self.file_menu_frame_down.grid_remove() + self.file_menu_frame_down_right.grid_remove() + + + #Widgets für das Hauptmenü erstellen + def main_menu_widgets(self): + self.settings_menu_button = tk.Button(self.main_menu_frame_right, text="Bild-\neinstellungen", image=self.pixel, compound="c", height=80, width=80, command=self.show_settings_menu) + self.settings_menu_button.pack(pady=10) + + self.crop_menu_button = tk.Button(self.main_menu_frame_right, text="Zuschnitt", image=self.pixel, compound="c", height=80, width=80, command=self.show_crop_menu) + self.crop_menu_button.pack(pady=30) + + self.file_menu_button = tk.Button(self.main_menu_frame_right, text="Datei-\neinstellungen", image=self.pixel, compound="c", height=80, width=80, command=self.show_file_menu) + self.file_menu_button.pack(pady=30) + + buttons_frame_down = tk.Frame(self.main_menu_frame_down) + buttons_frame_down.pack(pady=10, fill='x', anchor='center') + + #Einen Frame erstellen, um die beiden unteren Buttons zentriert auszurichten + buttons_frame_down.grid_columnconfigure(0, weight=1) + buttons_frame_down.grid_columnconfigure(3, weight=1) + + # Vollbild beenden Button zentriert im Grid platzieren + self.fullscreen_button = tk.Button(buttons_frame_down, text="Vollbild beenden", command=self.toggle_fullscreen, width=20) + self.fullscreen_button.grid(row=0, column=1, padx=20) + + # Zoom Button zentriert im Grid und neben dem Vollbild beenden Button platzieren + self.zoom_button_state = tk.BooleanVar(value=False) + self.zoom_button = tk.Button(buttons_frame_down, text="Zoom", command=self.toggle_zoom, width=20) + self.zoom_button.grid(row=0, column=2, padx=20) + + + #Widgets des Zuschnittmenüs / Slider für ROI + def crop_menu_widgets(self): + back_button = tk.Button(self.crop_menu_frame_down_right, text="Zurück", image=self.pixel, compound="c", height=40, width=40, command=self.show_main_menu) + back_button.pack(pady=10) + + self.crop_width_value = tk.IntVar() + self.crop_width_slider = tk.Scale(self.crop_menu_frame_down, from_=800, to=0, resolution= 2, orient='horizontal', variable=self.crop_width_value, command=self.update_value, width=30) + self.crop_width_slider.pack(fill='x', padx=50) + self.crop_width_value.set(800) + + self.crop_height_value = tk.IntVar() + self.crop_height_slider = tk.Scale(self.crop_menu_frame_right, from_=480, to=0, resolution= 2, orient='vertical', variable=self.crop_height_value, command=self.update_value, width=30) + self.crop_height_slider.pack(fill='y', expand=True, pady=25, padx=10) + self.crop_height_value.set(480) + + + def settings_menu_widgets(self): + self.ext_lighting_state = tk.BooleanVar(value=False) + self.ext_lighting_button = tk.Button(self.settings_menu_frame_right, text="Beleuchtung \nPermanent", image=self.pixel, compound="c", height=80, width=80, command=self.toggle_ext_lighting) + self.ext_lighting_button.pack(pady=10) + + self.ext_flash_state = tk.BooleanVar(value=False) + self.ext_flash_button = tk.Button(self.settings_menu_frame_right, text="Blitz", image=self.pixel, compound="c", height=80, width=80, command=self.toggle_flash) + self.ext_flash_button.pack(pady=10) + + back_button = tk.Button(self.settings_menu_frame_down_right, text="Zurück", image=self.pixel, compound="c", height=40, width=40, command=self.show_main_menu) + back_button.pack(pady=10) + + self.show_preview_grid_state = tk.BooleanVar(value=False) + self.show_preview_grid_button = tk.Button(self.settings_menu_frame_right, text="Gitter", image=self.pixel, compound="c", height=80, width=80, command=self.toggle_preview_grid) + self.show_preview_grid_button.pack(pady=30) + + + buttons_frame = tk.Frame(self.settings_menu_frame_down) + buttons_frame.pack(pady=15, fill='x', expand=True) + + # Button für das Lineal + self.show_preview_ruler_state = tk.BooleanVar(value=False) + self.show_preview_ruler_button = tk.Button(buttons_frame, text="Lineal", image=self.pixel, compound="c", height=30, width=80, command=self.toggle_preview_ruler) + self.show_preview_ruler_button.pack(side='left', padx=20) + + # Container für Lineal horizontal mit zentrierten Eingabefeldern + horizontal_ruler_frame = tk.Frame(buttons_frame, height=60) + horizontal_ruler_frame.pack(side='left', padx=10) + tk.Label(horizontal_ruler_frame, text="Lineal horizontal", height=1).pack() + + # Frame für die Eingabefelder, um sie zentriert unter dem Text zu positionieren + horizontal_ruler_entries_frame = tk.Frame(horizontal_ruler_frame) + horizontal_ruler_entries_frame.pack() + tk.Entry(horizontal_ruler_entries_frame, width=5, textvariable=self.ruler_horizontal_start).pack(side='left', padx=(0, 5)) # Ein wenig Platz zwischen den Feldern + tk.Entry(horizontal_ruler_entries_frame, width=5, textvariable=self.ruler_horizontal_end).pack(side='left') + + # Container für Lineal vertikal mit zentrierten Eingabefeldern + vertical_ruler_frame = tk.Frame(buttons_frame, height=60) + vertical_ruler_frame.pack(side='left', padx=10) + tk.Label(vertical_ruler_frame, text="Lineal vertikal", height=1).pack() + + # Frame für die Eingabefelder, um sie zentriert unter dem Text zu positionieren + vertical_ruler_entries_frame = tk.Frame(vertical_ruler_frame) + vertical_ruler_entries_frame.pack() + tk.Entry(vertical_ruler_entries_frame, width=5, textvariable=self.ruler_vertical_start).pack(side='left', padx=(0, 5)) # Ein wenig Platz zwischen den Feldern + tk.Entry(vertical_ruler_entries_frame, width=5, textvariable=self.ruler_vertical_end).pack(side='left') + + # Button für die Uhrzeit + self.show_preview_clock_state = tk.BooleanVar(value=False) + self.show_preview_clock_button = tk.Button(buttons_frame, text="Uhrzeit", image=self.pixel, compound="c", height=30, width=80, command=self.toggle_preview_clock) + self.show_preview_clock_button.pack(side='left', padx=20) + + # Button für die Tastatur + keyboard_button = tk.Button(buttons_frame, text="Tastatur", image=self.pixel, compound="c", height=30, width=80, command=self.toggle_keyboard) + keyboard_button.pack(side='left', padx=20) + + + + + def file_menu_widgets(self): + back_button_frame = tk.Frame(self.file_menu_frame_down_right) + back_button_frame.pack(side='right', padx=10, pady=10) + back_button = tk.Button(back_button_frame, text="Zurück", image=self.pixel, compound="c", height=40, width=40, command=self.show_main_menu) + back_button.pack() + + text_font = Font(family="Helvetica", size=14) + self.style.configure("LargeCheckbutton.TCheckbutton", font=('Helvetica', 14)) + + # "Kopiermenü"-Label + label_copy_menu = tk.Label(self.file_menu_frame_right, text="Kopieren - Zielordner", font=self.font_bold) + label_copy_menu.pack(pady=(15, 5)) + + # "Zielpfad"-Bereich + destination_path_frame = tk.Frame(self.file_menu_frame_right) + destination_path_frame.pack(pady=5, fill='x', padx=10) + + self.entry_destination_path = tk.Entry(destination_path_frame, textvariable=self.destination_path_var, font=text_font, width=40) + self.entry_destination_path.pack(side='left') + + browse_button_destination = tk.Button(destination_path_frame, text="...", command=lambda: self.choose_directory(self.entry_destination_path)) + browse_button_destination.pack(side='left', padx=(5, 0)) + + # "Kopieren"-Button + button_copy = tk.Button(self.file_menu_frame_right, text="Kopieren", command=self.copy, font=self.font_normal) + button_copy.pack(pady=15) + + keyboard_button = tk.Button(self.file_menu_frame_down, text="Tastatur", font=self.font_normal, command=self.toggle_keyboard) + keyboard_button.pack(pady=10) + + # Horizontaler Trennstrich + separator = ttk.Separator(self.file_menu_frame_right, orient='horizontal') + separator.pack(fill='x', pady=5) + + label_file_menu = tk.Label(self.file_menu_frame_right, text="Dateimenü", font=self.font_bold) + label_file_menu.pack(pady=(5, 5)) + + # "Speicherpfad"-Bereich + label_storage_path = tk.Label(self.file_menu_frame_right, text="Speicherpfad", font=self.font_normal) + label_storage_path.pack(pady=5) + storage_path_frame = tk.Frame(self.file_menu_frame_right) + storage_path_frame.pack(pady=5, fill='x', padx=10) # padx=10 fügt Abstand zum linken Rand hinzu + + self.entry_storage_path = tk.Entry(storage_path_frame, textvariable=self.storage_path_var, font=text_font, width=40) # Reduzierte Breite + self.entry_storage_path.pack(side='left') + + browse_button_storage = tk.Button(storage_path_frame, text="...", command=lambda: self.choose_directory(self.entry_storage_path)) + browse_button_storage.pack(side='left', padx=(5, 0)) + + # "Dateiname"-Label und Eingabefeld + label_filename = tk.Label(self.file_menu_frame_right, text="Dateiname", font=self.font_normal) + label_filename.pack(pady=5) + self.entry_filename = tk.Entry(self.file_menu_frame_right, textvariable=self.filename_var, font=text_font, width=40) + self.entry_filename.pack(pady=5) + + # Gemeinsamer Rahmen für Datum- und Uhrzeit-Checkboxen + datetime_frame = tk.Frame(self.file_menu_frame_right) + datetime_frame.pack(pady=5, fill='x') + + # Datum-Checkbox und Label + date_frame = tk.Frame(datetime_frame) + date_frame.pack(side='left', expand=True, padx=5) + checkbox_date = ttk.Checkbutton(date_frame, text="Datum", style="LargeCheckbutton.TCheckbutton", variable=self.date_var) + checkbox_date.grid(row=0, column=0, sticky="w") + date_label = tk.Label(date_frame, text="", font=self.font_normal) + date_label.grid(row=0, column=1, sticky="w") + + # Uhrzeit-Checkbox und Label + time_frame = tk.Frame(datetime_frame) + time_frame.pack(side='left', expand=True, padx=5) + checkbox_time = ttk.Checkbutton(time_frame, text="Uhrzeit", style="LargeCheckbutton.TCheckbutton", variable=self.time_var) + checkbox_time.grid(row=0, column=0, sticky="w") + time_label = tk.Label(time_frame, text="", font=self.font_normal) + time_label.grid(row=0, column=1, sticky="w") + + + #Methode, um die Daten vom Arbeitspfad in den Zielordner zu kopieren + def copy(self): + try: + if not os.path.exists(self.destination_path_var.get()): + os.makedirs(self.destination_path_var.get()) + + for item in os.listdir(self.storage_path_var.get()): + source_item = os.path.join(self.storage_path_var.get(), item) + destination_item = os.path.join(self.destination_path_var.get(), item) + + if os.path.isdir(source_item): + shutil.copytree(source_item, destination_item, dirs_exist_ok=True) + else: + shutil.copy2(source_item, destination_item) + + self.show_success_message("Kopieren erfolgreich", "Alle Inhalte wurden erfolgreich kopiert.") + + except Exception as e: + messagebox.showerror("Fehler", f"Ein Fehler ist aufgetreten: {e}") + + + #Methode, um ein Nachrichtenfenster anzuzeigen + def show_success_message(self, title, message): + msg_window = tk.Toplevel(self.root) + msg_window.title(title) + msg_window.geometry("300x100") + tk.Label(msg_window, text=message).pack(pady=10) + + # OK-Button zum Schließen des Fensters + ok_button = tk.Button(msg_window, text="OK", command=msg_window.destroy) + ok_button.pack(pady=5) + + # Fenster automatisch nach 5 Sekunden schließen + msg_window.after(5000, msg_window.destroy) + + + #Aktuell aktives Menü schließen + def close_menu(self): + self.current_menu_right.grid_remove() + self.current_menu_down.grid_remove() + self.current_menu_down_right.grid_remove() + + #Aktuell aktives Menü öffnen (z.B. nachdem der Button ">", "<" gedrückt wurde) + def open_menu(self): + self.current_menu_right.grid() + self.current_menu_down.grid() + self.current_menu_down_right.grid() + self.update_aspect_ratio() + + def show_main_menu(self): + self.close_menu() + self.current_menu_right = self.main_menu_frame_right + self.current_menu_down = self.main_menu_frame_down + self.current_menu_down_right = self.main_menu_frame_down_right + self.open_menu() + + def show_crop_menu(self): + self.close_menu() + self.current_menu_right = self.crop_menu_frame_right + self.current_menu_down = self.crop_menu_frame_down + self.current_menu_down_right = self.crop_menu_frame_down_right + self.open_menu() + + def show_settings_menu(self): + self.close_menu() + self.current_menu_right = self.settings_menu_frame_right + self.current_menu_down = self.settings_menu_frame_down + self.current_menu_down_right = self.settings_menu_frame_down_right + self.open_menu() + + def show_file_menu(self): + self.close_menu() + self.current_menu_right = self.file_menu_frame_right + self.current_menu_down = self.file_menu_frame_down + self.current_menu_down_right = self.file_menu_frame_down_right + self.open_menu() + + #Methode, um den File-Browser zu öffnen + def choose_directory(self, entry_field): + folder_selected = filedialog.askdirectory() + if folder_selected: + entry_field.delete(0, tk.END) + entry_field.insert(0, folder_selected) + + + #Methode wird ausgeführt, sobald sich der Wert in einem der Eingabefelder geändert hat + def entry_fields_changed(self, var_name, *args): + self.still_image_processing.update_file_parameters(self.filename_var.get(), self.storage_path_var.get(), + self.destination_path_var.get(), self.date_var.get(), + self.time_var.get()) + + self.ip.update_ruler_values(int(self.ruler_vertical_start.get()), int(self.ruler_vertical_end.get()), + int(self.ruler_horizontal_start.get()), int(self.ruler_horizontal_end.get())) + + self.still_image_processing.get_ruler_settings(int(self.ruler_vertical_start.get()), int(self.ruler_vertical_end.get()), + int(self.ruler_horizontal_start.get()), int(self.ruler_horizontal_end.get())) + + + #Blendet das Menü ein bzw. aus + def toggle_menu(self): + if self.current_menu_right and self.current_menu_right.grid_info(): + self.root.grid_columnconfigure(1, minsize=0) + self.close_menu() + self.update_aspect_ratio() + self.video_widget.grid(row=0, column=0, sticky="nsew") + self.hide_menu_button.config(text="<") + + else: + if not self.current_menu_right: + self.current_menu_right = self.main_menu_frame_right + self.current_menu_down = self.main_menu_frame_down + self.current_menu_down_right = self.main_menu_frame_down_right + self.root.grid_columnconfigure(1, minsize=30) + self.open_menu() + self.menu_width = self.current_menu_right.winfo_width() + self.video_widget.grid(row=0, column=0, sticky="nsew") + self.hide_menu_button.config(text=">") + + + #Methode um die ROI-Werte an die Bildbearbeitung zu übergeben + def update_value(self, value): + self.preview_processing.update_roi(self.crop_width_value.get(), self.crop_height_value.get()) + self.still_image_processing.update_roi(self.crop_width_value.get(), self.crop_height_value.get()) + + + + + #Externe Beleuchtung permanent ein- / ausschalten + def toggle_ext_lighting(self): + self.ext_lighting_state.set(not self.ext_lighting_state.get()) + self.update_button_appearance(self.ext_lighting_button, self.ext_lighting_state) + + if self.ext_lighting_state.get(): + self.permanent_lighting = True + else: + self.permanent_lighting = False + self.gpio_instance.get_gui_settings(self.activate_flash, self.permanent_lighting) + + #Blitz ein- / ausschalten + def toggle_flash(self): + self.ext_flash_state.set(not self.ext_flash_state.get()) + self.update_button_appearance(self.ext_flash_button, self.ext_flash_state) + + if self.ext_flash_state.get(): + self.activate_flash = True + else: + self.activate_flash = False + self.gpio_instance.get_gui_settings(self.activate_flash, self.permanent_lighting) + + + #Button Gitter ein- aus + def toggle_preview_grid(self): + self.show_preview_grid_state.set(not self.show_preview_grid_state.get()) + self.update_button_appearance(self.show_preview_grid_button, self.show_preview_grid_state) + self.send_settings() + + #Button Lineal ein- aus + def toggle_preview_ruler(self): + self.show_preview_ruler_state.set(not self.show_preview_ruler_state.get()) + self.update_button_appearance(self.show_preview_ruler_button, self.show_preview_ruler_state) + self.send_settings() + + #Button Uhr ein - aus + def toggle_preview_clock(self): + self.show_preview_clock_state.set(not self.show_preview_clock_state.get()) + self.update_button_appearance(self.show_preview_clock_button, self.show_preview_clock_state) + self.send_settings() + + #Button Zoom ein- aus + def toggle_zoom(self): + self.zoom_button_state.set(not self.zoom_button_state.get()) + self.update_button_appearance(self.zoom_button, self.zoom_button_state) + self.change_preview_resolution() + self.send_settings() + + + #Aussehen der Toggle-Buttons variieren + def update_button_appearance(self, button, state_var): + if state_var.get(): + button.config(relief="sunken", bg="dimgray") # Eingeschalteter Zustand + + else: + button.config(relief="raised", bg="lightgray") # Ausgeschalteter Zustand + + + #Einstellungen der Toggle-Buttons an die bildverarbeitenden Instanzen übergeben + def send_settings(self): + self.preview_processing.get_settings(self.show_preview_grid_state.get(), self.show_preview_ruler_state.get(), + self.show_preview_clock_state.get(), self.zoom_button_state.get()) + + self.still_image_processing.update_picture_parameters(self.show_preview_clock_state.get(), + self.show_preview_ruler_state.get()) + + + #Seitenverhältnis anpassen, sodass das video_widget sich entsprechend der ROI anpasst + def update_aspect_ratio(self): + self.root.update_idletasks() + self.video_widget_width = self.video_widget.winfo_width() + self.video_widget_height = self.video_widget.winfo_height() + self.video_widget_aspect_ratio = self.video_widget_width / self.video_widget_height + + if self.video_widget_aspect_ratio is None: + self.new_width = 800 + self.new_height = 480 + else: + self.new_width = self.video_widget_width + self.new_height = int(self.new_width / self.camera_aspect_ratio) + + #Tastatur ein- aus + def toggle_keyboard(self): + if self.keyboard_process: + self.keyboard_process.terminate() + self.keyboard_process = None + else: + self.keyboard_process = subprocess.Popen(['matchbox-keyboard']) + + + #Vorschaubild im video_widget aktualisieren + def update_video_widget(self): + image = self.camera.get_preview_image() #Bild aus der Kamera holen + image = self.preview_processing.get_preview_image(image) # Weitere Bildverarbeitung + + #Abfragen, falls das Bild in einem unerwarteten Format auftritt + if image.shape[2] not in [3, 4]: + raise ValueError("Das Bild muss 3 (RGB) oder 4 (RGBA) Farbkanäle haben") + + if image.dtype != np.uint8: + image = (image - image.min()) / (image.max() - image.min()) * 255.0 + image = image.astype(np.uint8) + + # Erzeugen des PIL-Bildes aus dem Array und Skalieren auf die neue Größe + resized_image = PIL.Image.fromarray(image).resize((self.new_width, self.new_height), PIL.Image.ANTIALIAS) + + self.preview_image = PIL.ImageTk.PhotoImage(image=resized_image) + self.video_widget.create_image(0, 0, image=self.preview_image, anchor=tk.NW) + self.root.after(self.video_feed_delay, self.update_video_widget) + + + #Erhöhen der Vorschauauflösung, um den gezoomten Bereich klarer darzustellen + def change_preview_resolution(self): + self.camera.get_highres_preview() + + + #Vollbild ein- ausschalten + def toggle_fullscreen(self): + if self.fullscreen_active == True: + self.root.attributes('-fullscreen', False) + self.fullscreen_button.config(text="Vollbild") + self.fullscreen_active = False + + else: + self.root.attributes('-fullscreen', True) + self.fullscreen_button.config(text="Vollbild beenden") + self.fullscreen_active = True + + + + + + diff --git a/800x480/Image_Processing.py b/800x480/Image_Processing.py new file mode 100644 index 0000000..3c4cc1c --- /dev/null +++ b/800x480/Image_Processing.py @@ -0,0 +1,335 @@ +import cv2 +import time +import numpy as np +import math +import datetime + +class Image_Processing(): + def __init__(self): + + #Parameter des Vorschaubildes + self.roi_width = 800 + self.roi_height = 480 + self.preview_roi_height_offset = 0 + self.preview_roi_width_offset = 0 + self.preview_width = 800 + self.preview_height = 480 + self.center_y = self.preview_height // 2 + self.center_x = self.preview_width // 2 + + #Einstellungen Gitter + self.grid_line_color = (255, 0, 0, 255) + self.grid_line_thickness = 2 + self.grid_line_spacing = 100 + self.show_grid = False + self.show_zoom = False + + #Zoom-Faktor für das Vorschaubild + self.zoom_factor = 1.5 + + + #Einstellungen Lineal + self.show_ruler = False + self.show_clock = False + self.horizontal_ruler_start = 0 + self.horizontal_ruler_end = 100 + self.vertical_ruler_start = 0 + self.vertical_ruler_end = 50 + self.ruler_thickness = 30 + self.ruler_font = cv2.FONT_HERSHEY_PLAIN + self.ruler_color = (255, 255, 255, 255) # Weiß + self.ruler_text_color = (0, 0, 0, 255) # Schwarz + self.ruler_line_color = (0, 0, 0, 255) # Schwarz + self.ruler_initial_width = self.preview_width + self.ruler_initial_height = self.preview_height + self.update_ruler_settings() + + #Einstellungen Uhr + self.font = cv2.FONT_HERSHEY_SIMPLEX + self.font_size = 0.5 + self.font_color = (0, 0, 0, 255) + self.background_color = (255, 255, 255, 255) + self.text_thickness = 1 + self.line_type = cv2.LINE_AA + + + self.right_border = self.preview_width + self.left_border = 0 + self.upper_border = self.preview_height + self.bottom_border = 0 + + + #Bild wird aus dem GUI-Code an Image_Processing übergeben (sorgt für flüssigeres Vorschaubild) + def get_preview_image(self, image): + self.preview_image = image + self.preview_image_processing() + return self.preview_image + + #Einstellungen werden aus der Benutzeroberfläche geladen + def get_settings(self, show_grid, show_ruler, show_clock, show_zoom): + self.show_grid = show_grid + self.show_ruler = show_ruler + self.show_clock = show_clock + self.show_zoom = show_zoom + + #Durchgeführte Schritte zur Bildbearbeitung des Vorschaubilds + def preview_image_processing(self): + self.crop_preview_image() + self.zoom_picture() + self.add_grid_lines() + self.add_ruler() + self.add_clock() + + + #Aktualisieren der Parameter zum Zuschneiden des ROI (symmetrischer Zuschnitt) + def update_roi(self, roi_width, roi_height): + self.roi_width = roi_width + self.roi_height = roi_height + + self.preview_roi_width_offset = round(((self.preview_width - self.roi_width) / 2)) + self.preview_roi_height_offset = round(((self.preview_height - self.roi_height) / 2)) + + self.right_border = self.center_x + (self.roi_width // 2) + self.left_border = self.center_x - (self.roi_width // 2) + self.upper_border = self.center_y + (self.roi_height // 2) + self.bottom_border = self.center_y - (self.roi_height // 2) + + self.update_ruler_settings() #Position des Lineals soll mit dem ROI mitwandern + + + + def crop_preview_image(self): + if self.roi_width != self.preview_width or self.roi_height != self.preview_height: + + cropped_image = self.preview_image[self.preview_roi_height_offset:(self.preview_height - self.preview_roi_height_offset), + self.preview_roi_width_offset:(self.preview_width - self.preview_roi_width_offset), :] + + # Erstellen eines neuen, schwarzen Bildes mit der gewünschten Größe (800x480) + final_image = np.zeros((self.preview_height, self.preview_width, 4), dtype=np.uint8) + + # Berechnen der Startkoordinaten für das Einfügen des zugeschnittenen Bildes in das schwarze Bild + start_y = (self.preview_height - (cropped_image.shape[0])) // 2 + start_x = (self.preview_width - (cropped_image.shape[1])) // 2 + + # Einfügen des zugeschnittenen Bildes in das schwarze Bild + final_image[start_y:start_y + cropped_image.shape[0], start_x:start_x + cropped_image.shape[1]] = cropped_image + + # Aktualisieren von self.preview_image mit dem neuen Bild + self.preview_image = final_image + + else: + return + + + #Gitterlinien zur Vorschau hinzufügen + def add_grid_lines(self): + if self.show_grid: + # Horizontale Linien + for y in range(self.center_y, self.bottom_border, -self.grid_line_spacing): # Nach oben + cv2.line(self.preview_image, (self.left_border, y), (self.right_border, y), self.grid_line_color, self.grid_line_thickness) + for y in range(self.center_y, self.upper_border, self.grid_line_spacing): # Nach unten + cv2.line(self.preview_image, (self.left_border, y), (self.right_border, y), self.grid_line_color, self.grid_line_thickness) + + # Vertikale Linien + for x in range(self.center_x, self.left_border, -self.grid_line_spacing): # Nach links + cv2.line(self.preview_image, (x, self.bottom_border), (x, self.upper_border), self.grid_line_color, self.grid_line_thickness) + for x in range(self.center_x, self.right_border, self.grid_line_spacing): # Nach rechts + cv2.line(self.preview_image, (x, self.bottom_border), (x, self.upper_border), self.grid_line_color, self.grid_line_thickness) + + + #Einstellungen für das Lineal aktualisieren + def update_ruler_settings(self): + #Prüfen, ob der Bereich des Lineals gestreckt/gestaucht werden muss + self.ruler_scaling_width = self.roi_width / self.ruler_initial_width + self.ruler_scaling_height = self.roi_height / self.ruler_initial_height + + #Endwert des Lineals berechnen + self.ruler_horizontal_end = (self.horizontal_ruler_end - self.horizontal_ruler_start) * self.ruler_scaling_width + self.ruler_vertical_end = (self.vertical_ruler_end - self.vertical_ruler_start) * self.ruler_scaling_height + + #Ausgehend vom Bildbereich und des Endwerts wird die räumliche Auflösung in Pixel pro mm berechnet, um das Lineal anschließend flexibel zu skalieren + self.px_mm_horizontal = math.ceil(self.roi_width / self.ruler_horizontal_end) + self.px_mm_vertical = math.ceil(self.roi_height / self.ruler_vertical_end) + + + #Lineal im Vorschaubild anzeigen + def add_ruler(self): + if self.show_ruler: + # ROI Position und Größe in der Benutzeroberfläche berechnen + roi_x_start = max(0, self.center_x - self.roi_width // 2) + roi_y_start = max(0, self.center_y - self.roi_height // 2) + + # Erstellen des vertikalen Lineals + adjusted_height = self.roi_height + self.ruler_vertical = np.zeros((adjusted_height, self.ruler_thickness, 4), dtype=np.uint8) + self.ruler_vertical[:, :] = self.ruler_color + + # Erstellen des horizontalen Lineals + self.ruler_horizontal = np.zeros((self.ruler_thickness, self.roi_width, 4), dtype=np.uint8) + self.ruler_horizontal[:, :] = self.ruler_color + + # Markierungen und Beschriftungen zum vertikalen Lineal hinzufügen + for pos in range(self.ruler_thickness, self.roi_height, self.px_mm_vertical * 5): + is_thick_line = (pos - self.ruler_thickness) % (self.px_mm_vertical * 10) == 0 #Alle 10 mm eine dicke Linie + line_thickness = 2 if is_thick_line else 1 + line_length = self.ruler_thickness if is_thick_line else self.ruler_thickness // 2 + line_start = 0 if is_thick_line else int(self.ruler_thickness * 0.5) # Dünne Linien sind halb so lang + cv2.line(self.ruler_vertical, (line_start, pos), (self.ruler_thickness, pos), self.ruler_line_color, line_thickness) + + if is_thick_line: + text = f"{int((pos - self.ruler_thickness) / self.px_mm_vertical)}" + cv2.putText(self.ruler_vertical, text, (2, pos - 5), self.ruler_font, 0.8, self.ruler_text_color, 1) + + # Markierungen und Beschriftungen zum horizontalen Lineal hinzufügen + for pos in range(0, self.roi_width, self.px_mm_horizontal * 5): + is_thick_line = pos % (self.px_mm_horizontal * 10) == 0 + line_thickness = 2 if is_thick_line else 1 + line_start = 0 if is_thick_line else int(self.ruler_thickness * 0.5) + cv2.line(self.ruler_horizontal, (pos + self.ruler_thickness, line_start), (pos + self.ruler_thickness, self.ruler_thickness), self.ruler_line_color, line_thickness) + + if is_thick_line: + text = f"{int((pos / self.px_mm_horizontal))}" + text_x = pos +25- cv2.getTextSize(text, self.ruler_font, 0.8, 1)[0][0] + text_y = self.ruler_thickness - 5 #Abstand zum oberen Rand + cv2.putText(self.ruler_horizontal, text, (text_x, text_y), self.ruler_font, 0.8, self.ruler_text_color, 1) + + # Platzieren des vertikalen Lineals im Vorschaubild + self.preview_image[roi_y_start:roi_y_start + self.roi_height, roi_x_start:roi_x_start + self.ruler_thickness] = self.ruler_vertical + + # Platzieren des horizontalen Lineals im Vorschaubild + self.preview_image[roi_y_start:roi_y_start + self.ruler_thickness, roi_x_start:roi_x_start + self.roi_width] = self.ruler_horizontal + + + + #Uhrzeit unten rechts hinzufügen + def add_clock(self): + if self.show_clock: + timestamp = datetime.datetime.now().strftime("%H:%M") + image_height, image_width = self.preview_image.shape[:2] + + # Abstand vom linken Rand abhängig vom Lineal + margin_x = self.ruler_thickness + 5 if self.show_ruler else 5 + # Konstanter Abstand vom unteren Rand + margin_y = 30 + + # Padding erhöhen, um die Box größer zu machen + padding = 5 + + # Dynamische Schriftgröße basierend auf Bildhöhe + font_scale = image_height / 800 + + # Textgröße und -position berechnen + (text_width, text_height), base_line = cv2.getTextSize(timestamp, self.font, font_scale, self.text_thickness) + + # Die y-Position des Textes unter Berücksichtigung des unteren Randes + text_position_y = image_height - margin_y - base_line - padding + text_position = (margin_x + padding, text_position_y) + + # Berechnung der Hintergrundpositionen unter Berücksichtigung des zusätzlichen Padding + background_l = (text_position[0] - padding, text_position[1] - text_height - padding) + background_r = (text_position[0] + text_width + padding, text_position[1] + base_line + padding) + + # Hintergrund und Text zeichnen + cv2.rectangle(self.preview_image, background_l, background_r, self.background_color, -1) + cv2.putText(self.preview_image, timestamp, text_position, self.font, font_scale, self.font_color, self.text_thickness, self.line_type) + + + #Start- und Endwerte des Lineals werden durch die Benutzeroberfläche definiert + def update_ruler_values(self, ruler_vertical_start, ruler_vertical_end, + ruler_horizontal_start, ruler_horizontal_end): + self.vertical_ruler_start = ruler_vertical_start + self.vertical_ruler_end = ruler_vertical_end + self.horizontal_ruler_start = ruler_horizontal_start + self.horizontal_ruler_end = ruler_horizontal_end + + self.update_ruler_settings() + + + #Vorschaubild an einen Bereich gezoomt + def zoom_picture(self): + if self.show_zoom: + image_height, image_width = self.preview_image.shape[:2] + + # Größe des zentralen Bereichs berechnen, der mit doppeltem Zoom dargestellt werden soll + # Die Größe des zentralen Bereichs ist halb so groß wie die Originalgröße geteilt durch den Zoomfaktor + central_width = round(image_width // (2 * self.zoom_factor)) + central_height = round(image_height // (2 * self.zoom_factor)) + + # Mittelpunkt des Bildes berechnen + center_x = image_width // 2 + center_y = image_height // 2 + + # Zentralen Bereich definieren + crop_x_start = max(center_x - central_width // 2, 0) + crop_x_end = min(center_x + central_width // 2, image_width) + crop_y_start = max(center_y - central_height // 2, 0) + crop_y_end = min(center_y + central_height // 2, image_height) + + # Bildausschnitt extrahieren + cropped_image = self.preview_image[crop_y_start:crop_y_end, crop_x_start:crop_x_end] + + # Bildausschnitt auf die ursprüngliche Größe des `preview_image` skalieren + scaled_image = cv2.resize(cropped_image, (self.preview_width, self.preview_height)) + + # Das skalierte Bild als neues Vorschaubild festlegen + self.preview_image = scaled_image + + + + + + + + + + + + + + + + + + + + + #Methode um das Bild verdunkelt darzustellen, ist aktuell nicht implementiert, da die Performance etwas schlechter ist... + #Sieht aber besser aus, hierbei wird der Bereich außerhalb der ROI verdunkelt dargestellt + def crop_preview_image_Darkened(self): + + start_time = time.time() + + if self.roi_width != self.preview_width or self.roi_height != self.preview_height: + # Erstellen einer Kopie des Bildes für die Verdunkelung + darkened_image = self.preview_image.copy() + + # Definition des Verdunkelungsfaktors + darken_factor = 0.7 + + # Erstellen einer Maske für die Bereiche, die verdunkelt werden sollen + mask = np.zeros_like(self.preview_image, dtype=bool) + + # Setzen der Bereiche außerhalb des ROI in der Maske auf True + top = self.preview_roi_height_offset + bottom = self.preview_height - self.preview_roi_height_offset + left = self.preview_roi_width_offset + right = self.preview_width - self.preview_roi_width_offset + + mask[:top] = True + mask[bottom:] = True + mask[top:bottom, :left] = True + mask[top:bottom, right:] = True + + # Anwenden der Verdunkelung nur auf die durch die Maske ausgewählten Bereiche + darkened_image[mask] = (darkened_image[mask].astype(np.float32) * darken_factor).astype(np.uint8) + + # Aktualisieren des Bildes mit der verdunkelten Version + self.preview_image = darkened_image + + print(f"Zeit: {time.time() - start_time}") + else: + return + + + + diff --git a/800x480/Main.py b/800x480/Main.py new file mode 100644 index 0000000..8c3f5a0 --- /dev/null +++ b/800x480/Main.py @@ -0,0 +1,28 @@ +import threading +import Camera +import Gui_Grid +import Image_Processing +import gpio_control +import Still_Image_Processing + +#Instanz zur Endbild-Bearbeitung starten +still_image_processing = Still_Image_Processing.Still_Image_Processing() + +#Kamerainstanz starten +camera = Camera.Camera(still_image_processing) +camera.start_camera() + +#Instanz zur Vorschaubild-Bearbeitung starten +preview_processing = Image_Processing.Image_Processing() + +#GPIO-Überwachung starten +gpio_instance = gpio_control.GPIOControl(camera) +gpio_thread = threading.Thread(target=gpio_instance.start) +gpio_thread.start() + +#Benutzeroberfläche starten +gui = Gui_Grid.User_Interface(camera, preview_processing, gpio_instance, still_image_processing) +gui.window_settings() +gui.build_widgets() +gui.update_video_widget() +gui.start_interface() diff --git a/800x480/Still_Image_Processing.py b/800x480/Still_Image_Processing.py new file mode 100644 index 0000000..c9603be --- /dev/null +++ b/800x480/Still_Image_Processing.py @@ -0,0 +1,266 @@ +import cv2 +import numpy as np +import math +import datetime +import os + + +class Still_Image_Processing(): + def __init__(self): + + #Einstellungen für den Dateinamen / Kopiermenü + self.file_name_gui = "" + self.storage_path = "/home/pi" + self.copy_path = "" + self.filename_add_date = False + self.filename_add_time = False + self.filename = "" + + #Flags, ob die Zeit bzw. das Lineal im finalen Bild zu sehen sind + self.add_time = False + self.add_ruler = False + + #Parameter des Vorschaubilds und des finalen Bilds, werden zur Skalierung benötigt + self.preview_width = 800 + self.preview_height = 480 + self.preview_width_roi = 800 + self.preview_height_roi = 480 + self.still_width_roi = 4056 + self.still_height_roi = 3040 + self.still_width = 4056 + self.still_height = 3040 + + #Parameter für ROI + self.still_width_offset = 0 + self.still_height_offset = 0 + + #Einstellungen für die Uhr + self.font = cv2.FONT_HERSHEY_SIMPLEX + self.font_size = 1 + self.font_color = (0, 0, 0, 255) + self.background_color = (255, 255, 255, 255) + self.text_thickness = 2 + self.line_type = cv2.LINE_AA + + #Einstellungen für das Lineal + self.horizontal_ruler_start = 0 + self.horizontal_ruler_end = 100 + self.vertical_ruler_start = 0 + self.vertical_ruler_end = 50 + self.ruler_thickness = 80 + self.ruler_font_size = 2 + self.ruler_font = cv2.FONT_HERSHEY_PLAIN + self.ruler_color = (255, 255, 255, 255) # Weiß + self.ruler_text_color = (0, 0, 0, 255) # Schwarz + self.ruler_line_color = (0, 0, 0, 255) # Schwarz + self.ruler_initial_width = self.preview_width + self.ruler_initial_height = self.preview_height + + self.still_image = None + + + #Einstellungen für Dateinamen werden durch die Benutzeroberfläche definiert + def update_file_parameters(self, file_name, storage_path, copy_path, filename_add_date, filename_add_time): + self.file_name_gui = file_name + self.storage_path = storage_path + self.copy_path = copy_path + self.filename_add_date = filename_add_date + self.filename_add_time = filename_add_time + + + #Bildung des Dateinamens. Standard ist "Bild-{Nummerierung}". Ggf. kann das Bild mit Datum / Zeit versehen werden oder einen individuellen Namen erhalten + def update_filename(self): + base_name = self.file_name_gui.rstrip(".png") if self.file_name_gui else "Bild" + + time_date_now = datetime.datetime.now() + if self.filename_add_date: + base_name += time_date_now.strftime("_%Y-%m-%d") + if self.filename_add_time: + base_name += time_date_now.strftime("_%H-%M-%S") + + final_name = base_name + counter = 1 + while os.path.exists(os.path.join(self.storage_path, final_name + ".png")): #Wenn Bild mit dem Namen bereits existiert, wird der Zähler erhöht, bis kein Bild gefunden wurde + final_name = f"{base_name}-{counter}" + counter += 1 + + self.file_name = os.path.join(self.storage_path, final_name + ".png") + + + #Methode zum Speichern des Bildes + def save_picture(self): + self.update_filename() + rgb_image = cv2.cvtColor(self.still_image, cv2.COLOR_BGR2RGB) #Konvertierung von BGR zu RGB erforderlich + cv2.imwrite(self.file_name, rgb_image) + + + #Es wird aus der GUI übergeben, ob die Uhr bzw. das Lineal angezeigt werden sollen + def update_picture_parameters(self, add_time_to_image, add_ruler_to_image): + self.add_time = add_time_to_image + self.add_ruler = add_ruler_to_image + + #Aktualisieren der ROI und der erforderlichen Parameter + def update_roi(self, roi_width, roi_height): + self.preview_width_roi = roi_width + self.preview_height_roi = roi_height + + + self.roi_width_factor = self.preview_width_roi / self.preview_width + self.roi_height_factor = self.preview_height_roi / self.preview_height + + self.still_width_roi = round(self.roi_width_factor * self.still_width) + self.still_height_roi = round(self.roi_height_factor * self.still_height) + + self.still_width_offset = round(((self.still_width - self.still_width_roi) / 2)) + self.still_height_offset = round(((self.still_height - self.still_height_roi) / 2)) + + + #Uhrzeit im Bild einblenden + def add_time_to_image(self): + if self.add_time == True: + timestamp = datetime.datetime.now().strftime("%H:%M") + image_height, image_width = self.still_image.shape[:2] + + # Abstand vom linken Rand abhängig vom Lineal + margin_x = self.ruler_thickness + 5 if self.add_ruler else 5 + # Konstanter Abstand vom unteren Rand + margin_y = 30 + + # Padding erhöhen, um die Box größer zu machen + padding = 5 + + # Dynamische Schriftgröße basierend auf Bildhöhe + font_scale = image_height / 800 + + # Textgröße und -position berechnen + (text_width, text_height), base_line = cv2.getTextSize(timestamp, self.font, font_scale, self.text_thickness) + + # Die y-Position des Textes unter Berücksichtigung des unteren Randes + text_position_y = image_height - margin_y - base_line - padding + text_position = (margin_x + padding, text_position_y) + + # Berechnung der Hintergrundpositionen + background_l = (text_position[0] - padding, text_position[1] - text_height - padding) + background_r = (text_position[0] + text_width + padding, text_position[1] + base_line + padding) + + cv2.rectangle(self.still_image, background_l, background_r, self.background_color, -1) + cv2.putText(self.still_image, timestamp, text_position, self.font, font_scale, self.font_color, self.text_thickness, self.line_type) + + + + #Einstellungen des Lineals aus der Benutzeroberfläche laden + def get_ruler_settings(self, ruler_vertical_start, ruler_vertical_end, + ruler_horizontal_start, ruler_horizontal_end): + + self.vertical_ruler_start = ruler_vertical_start + self.vertical_ruler_end = ruler_vertical_end + self.horizontal_ruler_start = ruler_horizontal_start + self.horizontal_ruler_end = ruler_horizontal_end + + + #Erforderliche Einstellungen des Lineals bearbeiten, dieses passt sich an den linken bzw. oberen Bildrand an + def update_ruler_settings(self): + self.ruler_scaling_width = self.still_image.shape[1] / self.still_width + self.ruler_scaling_height = self.still_image.shape[0] / self.still_height + + self.ruler_horizontal_end = (self.horizontal_ruler_end - self.horizontal_ruler_start) * self.ruler_scaling_width + self.ruler_vertical_end = (self.vertical_ruler_end - self.vertical_ruler_start) * self.ruler_scaling_height + + self.px_mm_horizontal = math.ceil(self.still_image.shape[1] / self.ruler_horizontal_end) + self.px_mm_vertical = math.ceil(self.still_image.shape[0] / self.ruler_vertical_end) + + + def add_ruler_to_image(self): + if self.add_ruler: + self.update_ruler_settings() + # Initialisierung des horizontalen Lineals am oberen Rand als RGB-Bild + self.ruler_horizontal = np.zeros((self.ruler_thickness, self.still_image.shape[1], 3), dtype=np.uint8) + self.ruler_horizontal[:, :] = self.ruler_color[:3] + + self.ruler_vertical = np.zeros((self.still_image.shape[0], self.ruler_thickness, 3), dtype=np.uint8) + self.ruler_vertical[:, :] = self.ruler_color[:3] + + # Markierungen und Beschriftungen zum horizontalen Lineal hinzufügen + for pos in range(self.ruler_thickness, self.still_image.shape[1], self.px_mm_horizontal * 5): + is_thick_line = (pos-self.ruler_thickness) % (self.px_mm_horizontal * 10) == 0 + line_thickness = 2 if is_thick_line else 1 + cv2.line(self.ruler_horizontal, (pos, 0), (pos, self.ruler_thickness), self.ruler_line_color, line_thickness) + if is_thick_line: + text = f"{int((pos-self.ruler_thickness) / self.px_mm_horizontal)}" + text_x = pos + self.ruler_thickness // 2 - cv2.getTextSize(text, self.ruler_font, self.ruler_font_size, 1)[0][0] // 2 + text_y = self.ruler_thickness - 5 + cv2.putText(self.ruler_horizontal, text, (text_x, text_y), self.ruler_font, self.ruler_font_size, self.ruler_text_color, 1) + + # Markierungen und Beschriftungen zum vertikalen Lineal hinzufügen + for pos in range(self.ruler_thickness, self.still_image.shape[0], self.px_mm_vertical * 5): + is_thick_line = (pos-self.ruler_thickness) % (self.px_mm_vertical * 10) == 0 + line_thickness = 2 if is_thick_line else 1 + cv2.line(self.ruler_vertical, (0, pos), (self.ruler_thickness, pos), self.ruler_line_color, line_thickness) + if is_thick_line: + text = f"{int((pos-self.ruler_thickness) / self.px_mm_vertical)}" + cv2.putText(self.ruler_vertical, text, (2, pos - 5), self.ruler_font, self.ruler_font_size, self.ruler_text_color, 1) + + # Anbringen des horizontalen Lineals am oberen Rand des Bildes + self.still_image[:self.ruler_thickness, :] = self.ruler_horizontal + + # Anbringen des vertikalen Lineals am linken Rand des Bildes + self.still_image[:, :self.ruler_thickness] = self.ruler_vertical + + + #Alle Schritte zur Bearbeitung des finalen Bildes + def process_image(self, image): + self.still_image = image + self.crop_image() + self.add_time_to_image() + self.add_ruler_to_image() + self.save_picture() + + + #Der in der ROI definierten Bildbereich extrahieren + def crop_image(self): + if self.still_width_roi != self.still_width or self.still_height_roi != self.still_height: + + self.still_image = self.still_image[self.still_height_offset:(self.still_height - self.still_height_offset), + self.still_width_offset:(self.still_width - self.still_width_offset), :] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/800x480/gpio_control.py b/800x480/gpio_control.py new file mode 100644 index 0000000..0cffae1 --- /dev/null +++ b/800x480/gpio_control.py @@ -0,0 +1,74 @@ +import RPi.GPIO as GPIO +import time +import threading + +class GPIOControl: + def __init__(self, camera): + GPIO.setmode(GPIO.BCM) + self.camera = camera + + #Verzögerung im Ein- und Ausschalten der Beleuchtung + self.lighting_delay = 100 / 1000 + + # Pin-Definitionen + self.lighting_trigger_gpio = 17 #Schalter für Beleuchtung Ein/Aus + self.camera_trigger_gpio = 27 #Schalter für Kamera auslösen + self.lighting_output_gpio = 23 #Ausgang für Beleuchtung + + self.activate_flash = False + self.permanent_lighting = False + + # Pin-Setup + GPIO.setup(self.lighting_trigger_gpio, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(self.camera_trigger_gpio, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(self.lighting_output_gpio, GPIO.OUT, initial=GPIO.LOW) + + self.camera_trigger_time = None + self.running = True + + def start(self): + # Startet die Überwachung in einem separaten Thread, ist wegen der blockierenden Ausführung der Benutzeroberfläche erforderlich + self.thread = threading.Thread(target=self.monitor_gpio) + self.thread.start() + + def stop(self): + self.running = False + self.thread.join() + + #Blitz / Dauerbeleuchtung an/aus wird durch GUI eingestellt + def get_gui_settings(self, activate_flash, permanent_lighting): + self.activate_flash = activate_flash + self.permanent_lighting = permanent_lighting + + self.activate_lighting() + self.deactivate_lighting() + + + + def activate_lighting(self): + if self.activate_flash == True or self.permanent_lighting == True: + GPIO.output(self.lighting_output_gpio, GPIO.HIGH) + + def deactivate_lighting(self): + if self.permanent_lighting == False: + GPIO.output(self.lighting_output_gpio, GPIO.LOW) + + def monitor_gpio(self): + try: + while self.running: + if GPIO.input(self.lighting_trigger_gpio) == GPIO.LOW: + self.activate_lighting() + + if GPIO.input(self.camera_trigger_gpio) == GPIO.LOW: + self.activate_lighting() + time.sleep(self.lighting_delay) + self.camera.get_still_image() + time.sleep(self.lighting_delay) + self.deactivate_lighting() + + else: + if GPIO.input(self.camera_trigger_gpio) == GPIO.HIGH and GPIO.input(self.lighting_trigger_gpio) == GPIO.HIGH: + self.deactivate_lighting() + time.sleep(0.1) + finally: + GPIO.cleanup()