Skip to content

Instantly share code, notes, and snippets.

@potat-dev
Created December 6, 2024 02:57
Show Gist options
  • Save potat-dev/087cf3f104bf64a815aea60cf553607f to your computer and use it in GitHub Desktop.
Save potat-dev/087cf3f104bf64a815aea60cf553607f to your computer and use it in GitHub Desktop.
Image Comparison ELO Rater
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