Ein kleines Tinder für eure eigenen Fotos

Wahrscheinlich weil deutlich mehr über Python dokumentiert ist als über viele andere Programmiersprachen können wohl KI-Systeme Python deutlich fehlerfreier schreiben als alles andere.

Da ich eine gewissermaßen große Sammlung Bilder habe brauchte ich ein Triage Tool um zu entscheiden ob Bilder gut sind oder nicht. Auch weil ich damit Datensets für KI-Systeme schneller basteln kann.

Hier ist der Code:

import os
import shutil
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from PIL import Image, ImageTk
Image.MAX_IMAGE_PIXELS = None  # Remove decompression bomb protection

class ImageReviewTool:
    def __init__(self, master):
        self.master = master
        master.title("Image Triage Tool")
        master.geometry("800x600")  # Set initial window size
        master.minsize(400, 300)  # Set minimum window size

        # Create the main frame
        self.frame = tk.Frame(master)
        self.frame.pack(fill=tk.BOTH, expand=True)

        # Create the image display area
        self.image_label = tk.Label(self.frame)
        self.image_label.pack(fill=tk.BOTH, expand=True)

        # Create frame for file handling options
        options_frame = tk.Frame(self.frame)
        options_frame.pack(side=tk.BOTTOM, fill=tk.X)

        # Create dropdown for "Good" images handling
        good_options_frame = tk.Frame(options_frame)
        good_options_frame.pack(side=tk.LEFT, padx=10, pady=5)
        
        tk.Label(good_options_frame, text="Good images:").pack(side=tk.LEFT)
        self.good_handling = ttk.Combobox(good_options_frame, 
                                        values=["Copy", "Move"],
                                        state="readonly",
                                        width=10)
        self.good_handling.set("Copy")
        self.good_handling.pack(side=tk.LEFT, padx=5)

        # Create dropdown for "Bad" images handling
        bad_options_frame = tk.Frame(options_frame)
        bad_options_frame.pack(side=tk.LEFT, padx=10, pady=5)
        
        tk.Label(bad_options_frame, text="Bad images:").pack(side=tk.LEFT)
        self.bad_handling = ttk.Combobox(bad_options_frame,
                                       values=["Copy", "Move"],
                                       state="readonly",
                                       width=10)
        self.bad_handling.set("Move")
        self.bad_handling.pack(side=tk.LEFT, padx=5)

        # Create the "Good", "Bad", and "Skip" buttons
        button_frame = tk.Frame(self.frame)
        button_frame.pack(side=tk.BOTTOM, fill=tk.X)

        self.good_button = tk.Button(button_frame, text="Good (1)", command=self.handle_good)
        self.good_button.pack(side=tk.LEFT, padx=10, pady=10)

        self.bad_button = tk.Button(button_frame, text="Bad (0)", command=self.handle_bad)
        self.bad_button.pack(side=tk.LEFT, padx=10, pady=10)

        self.skip_button = tk.Button(button_frame, text="Skip (#)", command=self.handle_skip)
        self.skip_button.pack(side=tk.LEFT, padx=10, pady=10)

        # Create the directory selection buttons
        dir_button_frame = tk.Frame(self.frame)
        dir_button_frame.pack(side=tk.BOTTOM, fill=tk.X)

        self.select_directory_button = tk.Button(dir_button_frame, text="Select Directory", command=self.select_directory)
        self.select_directory_button.pack(side=tk.LEFT, padx=10, pady=10)

        self.select_good_directory_button = tk.Button(dir_button_frame, text="Select Good Directory", command=self.select_good_directory)
        self.select_good_directory_button.pack(side=tk.LEFT, padx=10, pady=10)

        self.select_bad_directory_button = tk.Button(dir_button_frame, text="Select Bad Directory", command=self.select_bad_directory)
        self.select_bad_directory_button.pack(side=tk.LEFT, padx=10, pady=10)

        # Add current file label
        self.file_label = tk.Label(self.frame, text="")
        self.file_label.pack(side=tk.BOTTOM, pady=5)

        self.current_directory = None
        self.good_folder = None
        self.bad_folder = None
        self.current_image_index = 0
        self.image_files = []
        self.reviewed_images = []
        self.log_file = "image_review_log.txt"
        self.current_image = None

        # Bind the window resize event to the resize_image method
        self.master.bind("<Configure>", self.resize_image)

        # Bind keyboard shortcuts
        self.master.bind("1", self.handle_good)
        self.master.bind("0", self.handle_bad)
        self.master.bind("#", self.handle_skip)

    def select_directory(self):
        self.current_directory = filedialog.askdirectory()
        if self.current_directory:
            self.image_files = [f for f in os.listdir(self.current_directory) if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif"))]
            self.current_image_index = 0
            self.reviewed_images = self.load_reviewed_images()
            self.display_image()

    def select_good_directory(self):
        self.good_folder = filedialog.askdirectory()

    def select_bad_directory(self):
        self.bad_folder = filedialog.askdirectory()

    def display_image(self):
        if self.current_directory and self.image_files:
            if self.current_image_index >= len(self.image_files):
                self.image_label.configure(image='')
                self.file_label.config(text="")
                messagebox.showinfo("Complete", "All images have been reviewed!")
                return

            try:
                image_path = os.path.join(self.current_directory, self.image_files[self.current_image_index])
                self.current_image = Image.open(image_path)
                
                # Update file info label
                img_size = os.path.getsize(image_path) / (1024 * 1024)  # Convert to MB
                self.file_label.config(
                    text=f"File: {self.image_files[self.current_image_index]} | "
                         f"Size: {img_size:.1f}MB | "
                         f"Dimensions: {self.current_image.width}x{self.current_image.height}"
                )
                
                self.resize_image(None)
            except Exception as e:
                messagebox.showerror("Error", f"Failed to load image: {str(e)}")
                self.current_image_index += 1
                self.display_image()

    def resize_image(self, event):
        if self.current_image:
            try:
                # Get the current window size
                window_width = self.master.winfo_width()
                window_height = self.master.winfo_height() - 150  # Adjust for buttons and options

                # Scale the image to fit the window while maintaining aspect ratio
                image_ratio = self.current_image.width / self.current_image.height
                if window_width / window_height > image_ratio:
                    new_height = window_height
                    new_width = int(new_height * image_ratio)
                else:
                    new_width = window_width
                    new_height = int(new_width / image_ratio)

                # Use thumbnail for memory-efficient resizing of large images
                img_copy = self.current_image.copy()
                img_copy.thumbnail((new_width, new_height), Image.Resampling.LANCZOS)
                self.photo = ImageTk.PhotoImage(img_copy)
                self.image_label.configure(image=self.photo)
            except Exception as e:
                messagebox.showerror("Error", f"Failed to resize image: {str(e)}")

    def handle_good(self, event=None):
        if self.current_directory and self.good_folder and self.image_files:
            src_path = os.path.join(self.current_directory, self.image_files[self.current_image_index])
            dst_path = os.path.join(self.good_folder, self.image_files[self.current_image_index])
            
            try:
                if self.good_handling.get() == "Copy":
                    shutil.copy2(src_path, dst_path)
                else:  # Move
                    shutil.move(src_path, dst_path)
                
                self.reviewed_images.append(self.image_files[self.current_image_index])
                self.save_reviewed_images()
                self.current_image_index += 1
                self.display_image()
            except Exception as e:
                messagebox.showerror("Error", f"Failed to process image: {str(e)}")

    def handle_bad(self, event=None):
        if self.current_directory and self.bad_folder and self.image_files:
            src_path = os.path.join(self.current_directory, self.image_files[self.current_image_index])
            dst_path = os.path.join(self.bad_folder, self.image_files[self.current_image_index])
            
            try:
                if self.bad_handling.get() == "Copy":
                    shutil.copy2(src_path, dst_path)
                else:  # Move
                    shutil.move(src_path, dst_path)
                
                self.reviewed_images.append(self.image_files[self.current_image_index])
                self.save_reviewed_images()
                self.current_image_index += 1
                self.display_image()
            except Exception as e:
                messagebox.showerror("Error", f"Failed to process image: {str(e)}")

    def handle_skip(self, event=None):
        if self.current_directory and self.image_files:
            self.current_image_index += 1
            self.display_image()

    def load_reviewed_images(self):
        reviewed_images = []
        if os.path.exists(self.log_file):
            with open(self.log_file, "r") as f:
                reviewed_images = [line.strip() for line in f.readlines()]
        return reviewed_images

    def save_reviewed_images(self):
        with open(self.log_file, "w") as f:
            for image in self.reviewed_images:
                f.write(image + "\n")

if __name__ == "__main__":
    root = tk.Tk()
    app = ImageReviewTool(root)
    root.mainloop()

Kopiert das Ding in eine .py Datei und führt das Ding dann mit python.exe aus… Natürlich solltet ihr verstehen (oder euch halt von der KI erklären lassen) was das Ding hier tut.

Erst die Good und Bad Ordner wählen, dann den Ordner mit Bildern die ihr sortieren müsst.
Keyboard-Bedienung geht auch 1 ist für gutes, 0 ist für schlechtes und # ist fürs Überspringen.

Sicher noch nicht das Beste, aber brauchbar. Der ganze Code kommt nahezu vollständig aus Claude.ai.
Viel Spaß damit, vielleicht könnt ihr es ja brauchen.

Cookie-Einwilligung mit Real Cookie Banner