Created
December 6, 2024 02:57
-
-
Save potat-dev/087cf3f104bf64a815aea60cf553607f to your computer and use it in GitHub Desktop.
Image Comparison ELO Rater
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import random | |
import json | |
import tkinter as tk | |
from tkinter import messagebox, filedialog | |
from PIL import Image, ImageTk | |
from collections import deque | |
class ImageComparisonApp: | |
def __init__(self, master): | |
self.master = master | |
self.master.title("Image Comparison ELO Rater") | |
# Default ELO parameters | |
self.K_FACTOR = 32 # Standard K-factor for ELO calculation | |
self.INITIAL_RATING = 1500 # Starting rating for new images | |
# Data storage | |
self.ratings_file = 'image_ratings.json' | |
self.image_ratings = self.load_ratings() | |
# History management | |
self.history = deque(maxlen=10) # Store last 10 comparisons | |
# UI Setup | |
self.setup_ui() | |
# Image selection | |
self.image_folder = None | |
self.current_images = [] | |
# Bind keyboard shortcuts | |
self.master.bind('<Left>', lambda e: self.rate_image(0, 1)) | |
self.master.bind('<Right>', lambda e: self.rate_image(1, 0)) | |
self.master.bind('<Down>', lambda e: self.load_next_pair()) | |
self.master.bind('<Up>', lambda e: self.undo_last_vote()) | |
# self.master.bind('^', lambda e: self.undo_last_vote()) | |
def setup_ui(self): | |
# Folder selection button | |
self.select_folder_btn = tk.Button( | |
self.master, | |
text="Select Image Folder", | |
command=self.select_folder | |
) | |
self.select_folder_btn.pack(pady=10) | |
# Image display frames | |
self.image_frame = tk.Frame(self.master) | |
self.image_frame.pack(expand=True, fill=tk.BOTH, padx=20, pady=20) | |
self.image1_label = tk.Label(self.image_frame) | |
self.image1_label.pack(side=tk.LEFT, expand=True) | |
self.image2_label = tk.Label(self.image_frame) | |
self.image2_label.pack(side=tk.RIGHT, expand=True) | |
# Rating buttons | |
self.button_frame = tk.Frame(self.master) | |
self.button_frame.pack(pady=10) | |
self.image1_btn = tk.Button( | |
self.button_frame, | |
text="Choose Left Image (←)", | |
command=lambda: self.rate_image(0, 1) | |
) | |
self.image1_btn.pack(side=tk.LEFT, padx=10) | |
self.image2_btn = tk.Button( | |
self.button_frame, | |
text="Choose Right Image (→)", | |
command=lambda: self.rate_image(1, 0) | |
) | |
self.image2_btn.pack(side=tk.RIGHT, padx=10) | |
# Skip and Undo buttons | |
self.bottom_frame = tk.Frame(self.master) | |
self.bottom_frame.pack(pady=10) | |
self.skip_btn = tk.Button( | |
self.bottom_frame, | |
text="Skip This Pair (↓)", | |
command=self.load_next_pair | |
) | |
self.skip_btn.pack(side=tk.LEFT, padx=10) | |
self.undo_btn = tk.Button( | |
self.bottom_frame, | |
text="Undo Last Vote (↑)", | |
command=self.undo_last_vote | |
) | |
self.undo_btn.pack(side=tk.RIGHT, padx=10) | |
self.analysis_btn = tk.Button( | |
self.bottom_frame, | |
text="Run Analysis", | |
command=self.run_analysis | |
) | |
self.analysis_btn.pack(side=tk.BOTTOM, pady=10) | |
def select_folder(self): | |
"""Open folder selection dialog and prepare images""" | |
self.image_folder = filedialog.askdirectory() | |
if self.image_folder: | |
# Filter for image files | |
self.image_files = [ | |
f for f in os.listdir(self.image_folder) | |
if f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp')) | |
] | |
# Initialize ratings for new images if not existing | |
for img in self.image_files: | |
if img not in self.image_ratings: | |
self.image_ratings[img] = self.INITIAL_RATING | |
# Clear history | |
self.history.clear() | |
# Load first pair | |
self.load_next_pair() | |
def load_next_pair(self): | |
"""Select two random unique images for comparison""" | |
if len(self.image_files) < 2: | |
messagebox.showinfo("Error", "Not enough images in folder") | |
return | |
# Ensure two different images are selected | |
self.current_images = random.sample(self.image_files, 2) | |
# Display images | |
self.display_images() | |
def display_images(self): | |
"""Display the two selected images""" | |
for idx, (label, img_filename) in enumerate([ | |
(self.image1_label, self.current_images[0]), | |
(self.image2_label, self.current_images[1]) | |
]): | |
# Open and resize image | |
full_path = os.path.join(self.image_folder, img_filename) | |
img = Image.open(full_path) | |
img.thumbnail((400, 400)) # Resize while maintaining aspect ratio | |
photo = ImageTk.PhotoImage(img) | |
# Update label | |
label.config(image=photo) | |
label.image = photo # Keep a reference | |
def calculate_elo(self, rating1, rating2, score): | |
""" | |
Calculate new ELO ratings | |
score: 1 for winner, 0 for loser, 0.5 for draw | |
""" | |
expected_score1 = 1 / (1 + 10 ** ((rating2 - rating1) / 400)) | |
expected_score2 = 1 / (1 + 10 ** ((rating1 - rating2) / 400)) | |
new_rating1 = rating1 + self.K_FACTOR * (score - expected_score1) | |
new_rating2 = rating2 + self.K_FACTOR * ((1 - score) - expected_score2) | |
return new_rating1, new_rating2 | |
def rate_image(self, winner_idx, loser_idx): | |
"""Process image rating and update ELO scores""" | |
if not self.current_images: | |
messagebox.showinfo("Error", "Please select a folder first") | |
return | |
winner = self.current_images[winner_idx] | |
loser = self.current_images[loser_idx] | |
# Store current state for potential undo | |
self.history.append({ | |
'images': self.current_images.copy(), | |
'ratings': { | |
winner: self.image_ratings.get(winner, self.INITIAL_RATING), | |
loser: self.image_ratings.get(loser, self.INITIAL_RATING) | |
} | |
}) | |
# Get current ratings | |
winner_rating = self.image_ratings.get(winner, self.INITIAL_RATING) | |
loser_rating = self.image_ratings.get(loser, self.INITIAL_RATING) | |
# Calculate new ratings | |
new_winner_rating, new_loser_rating = self.calculate_elo( | |
winner_rating, loser_rating, 1 | |
) | |
# Update ratings | |
self.image_ratings[winner] = new_winner_rating | |
self.image_ratings[loser] = new_loser_rating | |
# Save ratings | |
self.save_ratings() | |
# Load next pair | |
self.load_next_pair() | |
def undo_last_vote(self): | |
"""Undo the last vote and restore previous ratings""" | |
if not self.history: | |
messagebox.showinfo("Info", "No previous vote to undo") | |
return | |
# Retrieve last vote | |
last_vote = self.history.pop() | |
# Restore previous images and ratings | |
self.current_images = last_vote['images'] | |
# Restore previous ratings | |
for image, rating in last_vote['ratings'].items(): | |
self.image_ratings[image] = rating | |
# Save updated ratings | |
self.save_ratings() | |
# Display previous images | |
self.display_images() | |
def load_ratings(self): | |
"""Load existing ratings from file""" | |
try: | |
with open(self.ratings_file, 'r') as f: | |
return json.load(f) | |
except (FileNotFoundError, json.JSONDecodeError): | |
return {} | |
def save_ratings(self): | |
"""Save current ratings to file""" | |
with open(self.ratings_file, 'w') as f: | |
json.dump(self.image_ratings, f, indent=4) | |
def run_analysis(self): | |
"""Generate a sorted list of images by their ELO rating""" | |
sorted_images = sorted( | |
self.image_ratings.items(), | |
key=lambda x: x[1], | |
reverse=True | |
) | |
print("Image Rankings:") | |
for rank, (image, rating) in enumerate(sorted_images, 1): | |
print(f"{rank}. {image}: {rating:.2f}") | |
def main(): | |
root = tk.Tk() | |
root.geometry("800x600") | |
app = ImageComparisonApp(root) | |
root.mainloop() | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment