Refactor PhotoTagger GUI to enhance filtering capabilities. Introduce unique faces only and compare similar faces checkboxes, allowing users to filter displayed faces based on uniqueness and similarity. Update layout for better organization of date filters and controls, improving overall user experience. Adjust row configurations to minimize spacing and ensure proper expansion of panels.

This commit is contained in:
tanyar09 2025-09-30 13:06:25 -04:00
parent 4c0a1a3b38
commit da6f810b5b

View File

@ -642,7 +642,9 @@ class PhotoTagger:
root.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1) # Left panel
main_frame.columnconfigure(1, weight=1) # Right panel for similar faces
main_frame.rowconfigure(2, weight=1) # Main content row
# Configure row weights to minimize spacing around Unique checkbox
main_frame.rowconfigure(2, weight=0) # Unique checkbox row - no expansion
main_frame.rowconfigure(3, weight=1) # Main panels row - expandable
# Photo info
info_label = ttk.Label(main_frame, text="", font=("Arial", 10, "bold"))
@ -844,19 +846,140 @@ class PhotoTagger:
# Initial calendar display
update_calendar()
# Unique faces only checkbox variable (must be defined before widgets that use it)
unique_faces_var = tk.BooleanVar()
# Define update_similar_faces function first - reusing auto-match display logic
def update_similar_faces():
"""Update the similar faces panel when compare is enabled - reuses auto-match display logic"""
nonlocal similar_faces_data, similar_face_vars, similar_face_images, similar_face_crops, face_selection_states
# Note: Selection states are now saved automatically via callbacks (auto-match style)
# Clear existing similar faces
for widget in similar_scrollable_frame.winfo_children():
widget.destroy()
similar_face_vars.clear()
similar_face_images.clear()
# Clean up existing face crops
for crop_path in similar_face_crops:
try:
if os.path.exists(crop_path):
os.remove(crop_path)
except:
pass
similar_face_crops.clear()
if compare_var.get():
# Use the same filtering and sorting logic as auto-match (no unique faces filter for similar faces)
unidentified_similar_faces = self._get_filtered_similar_faces(face_id, tolerance, include_same_photo=False, face_status=face_status)
if unidentified_similar_faces:
# Get current face_id for selection state management
current_face_id = original_faces[i][0] # Get current face_id
# Reuse auto-match display logic for similar faces
self._display_similar_faces_in_panel(similar_scrollable_frame, unidentified_similar_faces,
similar_face_vars, similar_face_images, similar_face_crops,
current_face_id, face_selection_states, identify_data_cache)
# Note: Selection states are now restored automatically during checkbox creation (auto-match style)
else:
# No similar unidentified faces found
no_faces_label = ttk.Label(similar_scrollable_frame, text="No similar unidentified faces found",
foreground="gray", font=("Arial", 10))
no_faces_label.pack(pady=20)
else:
# Compare disabled - clear the panel
clear_label = ttk.Label(similar_scrollable_frame, text="Enable 'Compare with similar faces' to see matches",
foreground="gray", font=("Arial", 10))
clear_label.pack(pady=20)
# Update button states based on compare checkbox and list contents
update_select_clear_buttons_state()
# Unique faces change handler (must be defined before checkbox that uses it)
def on_unique_faces_change():
"""Handle unique faces checkbox change"""
nonlocal original_faces, i
if unique_faces_var.get():
# Show progress message
print("🔄 Applying unique faces filter...")
root.update() # Update UI to show the message
# Apply unique faces filtering to the main face list
try:
original_faces = self._filter_unique_faces_from_list(original_faces)
print(f"✅ Filter applied: {len(original_faces)} unique faces remaining")
except Exception as e:
print(f"⚠️ Error applying filter: {e}")
# Revert checkbox state
unique_faces_var.set(False)
return
else:
# Reload the original unfiltered face list
print("🔄 Reloading all faces...")
root.update() # Update UI to show the message
with self.get_db_connection() as conn:
cursor = conn.cursor()
query = '''
SELECT f.id, f.photo_id, p.path, p.filename, f.location
FROM faces f
JOIN photos p ON f.photo_id = p.id
WHERE f.person_id IS NULL
'''
params = []
# Add date taken filtering if specified
if date_from:
query += ' AND p.date_taken >= ?'
params.append(date_from)
if date_to:
query += ' AND p.date_taken <= ?'
params.append(date_to)
# Add date processed filtering if specified
if date_processed_from:
query += ' AND DATE(p.date_added) >= ?'
params.append(date_processed_from)
if date_processed_to:
query += ' AND DATE(p.date_added) <= ?'
params.append(date_processed_to)
query += ' ORDER BY f.id'
cursor.execute(query, params)
original_faces = list(cursor.fetchall())
print(f"✅ Reloaded: {len(original_faces)} faces")
# Reset to first face and update display
i = 0
update_similar_faces()
# Compare checkbox variable and handler (must be defined before widgets that use it)
compare_var = tk.BooleanVar()
def on_compare_change():
"""Handle compare checkbox change"""
update_similar_faces()
update_select_clear_buttons_state()
# Date filter controls
date_filter_frame = ttk.LabelFrame(main_frame, text="Date Filters", padding="5")
date_filter_frame.grid(row=1, column=0, columnspan=2, pady=(0, 10), sticky=(tk.W, tk.E))
date_filter_frame.columnconfigure(1, weight=1)
date_filter_frame.columnconfigure(4, weight=1)
date_filter_frame.columnconfigure(7, weight=1)
date_filter_frame.columnconfigure(10, weight=1)
date_filter_frame = ttk.LabelFrame(main_frame, text="Filter", padding="5")
date_filter_frame.grid(row=1, column=0, pady=(0, 0), sticky=tk.W)
date_filter_frame.columnconfigure(1, weight=0)
date_filter_frame.columnconfigure(4, weight=0)
# Date from
ttk.Label(date_filter_frame, text="From:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
ttk.Label(date_filter_frame, text="Taken date: from").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))
date_from_var = tk.StringVar(value=date_from or "")
date_from_entry = ttk.Entry(date_filter_frame, textvariable=date_from_var, width=12, state='readonly')
date_from_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 5))
date_from_entry = ttk.Entry(date_filter_frame, textvariable=date_from_var, width=10, state='readonly')
date_from_entry.grid(row=0, column=1, sticky=tk.W, padx=(0, 5))
# Calendar button for date from
def open_calendar_from():
@ -866,10 +989,10 @@ class PhotoTagger:
calendar_from_btn.grid(row=0, column=2, padx=(0, 10))
# Date to
ttk.Label(date_filter_frame, text="To:").grid(row=0, column=3, sticky=tk.W, padx=(0, 5))
ttk.Label(date_filter_frame, text="to").grid(row=0, column=3, sticky=tk.W, padx=(0, 5))
date_to_var = tk.StringVar(value=date_to or "")
date_to_entry = ttk.Entry(date_filter_frame, textvariable=date_to_var, width=12, state='readonly')
date_to_entry.grid(row=0, column=4, sticky=(tk.W, tk.E), padx=(0, 5))
date_to_entry = ttk.Entry(date_filter_frame, textvariable=date_to_var, width=10, state='readonly')
date_to_entry.grid(row=0, column=4, sticky=tk.W, padx=(0, 5))
# Calendar button for date to
def open_calendar_to():
@ -953,14 +1076,15 @@ class PhotoTagger:
print(f"👤 Found {len(unidentified)} unidentified faces with date filters")
print("💡 Navigate to refresh the display with filtered faces")
# Apply filter button (inside filter frame)
apply_filter_btn = ttk.Button(date_filter_frame, text="Apply Filter", command=apply_date_filter)
apply_filter_btn.grid(row=0, column=6, padx=(10, 0))
# Date processed filter (second row)
ttk.Label(date_filter_frame, text="Processed From:").grid(row=1, column=0, sticky=tk.W, padx=(0, 5), pady=(10, 0))
ttk.Label(date_filter_frame, text="Processed date: from").grid(row=1, column=0, sticky=tk.W, padx=(0, 5), pady=(10, 0))
date_processed_from_var = tk.StringVar()
date_processed_from_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_from_var, width=12, state='readonly')
date_processed_from_entry.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 5), pady=(10, 0))
date_processed_from_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_from_var, width=10, state='readonly')
date_processed_from_entry.grid(row=1, column=1, sticky=tk.W, padx=(0, 5), pady=(10, 0))
# Calendar button for date processed from
def open_calendar_processed_from():
@ -970,10 +1094,10 @@ class PhotoTagger:
calendar_processed_from_btn.grid(row=1, column=2, padx=(0, 10), pady=(10, 0))
# Date processed to
ttk.Label(date_filter_frame, text="Processed To:").grid(row=1, column=3, sticky=tk.W, padx=(0, 5), pady=(10, 0))
ttk.Label(date_filter_frame, text="to").grid(row=1, column=3, sticky=tk.W, padx=(0, 5), pady=(10, 0))
date_processed_to_var = tk.StringVar()
date_processed_to_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_to_var, width=12, state='readonly')
date_processed_to_entry.grid(row=1, column=4, sticky=(tk.W, tk.E), padx=(0, 5), pady=(10, 0))
date_processed_to_entry = ttk.Entry(date_filter_frame, textvariable=date_processed_to_var, width=10, state='readonly')
date_processed_to_entry.grid(row=1, column=4, sticky=tk.W, padx=(0, 5), pady=(10, 0))
# Calendar button for date processed to
def open_calendar_processed_to():
@ -982,14 +1106,24 @@ class PhotoTagger:
calendar_processed_to_btn = ttk.Button(date_filter_frame, text="📅", width=3, command=open_calendar_processed_to)
calendar_processed_to_btn.grid(row=1, column=5, padx=(0, 10), pady=(10, 0))
# Unique checkbox under the filter frame
unique_faces_checkbox = ttk.Checkbutton(main_frame, text="Unique faces only",
variable=unique_faces_var, command=on_unique_faces_change)
unique_faces_checkbox.grid(row=2, column=0, sticky=tk.W, padx=(0, 5), pady=0)
# Compare checkbox on the same row as Unique
compare_checkbox = ttk.Checkbutton(main_frame, text="Compare similar faces", variable=compare_var,
command=on_compare_change)
compare_checkbox.grid(row=2, column=1, sticky=tk.W, padx=(5, 10), pady=0)
# Left panel for main face
left_panel = ttk.Frame(main_frame)
left_panel.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
left_panel.grid(row=3, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5), pady=(0, 0))
left_panel.columnconfigure(0, weight=1)
# Right panel for similar faces
right_panel = ttk.LabelFrame(main_frame, text="Similar Faces", padding="5")
right_panel.grid(row=2, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
right_panel.grid(row=3, column=1, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
right_panel.columnconfigure(0, weight=1)
right_panel.rowconfigure(0, weight=1) # Make right panel expandable vertically
@ -1282,136 +1416,14 @@ class PhotoTagger:
# Initialize calendar
update_calendar()
# Unique faces only checkbox variable (defined before update_similar_faces function)
unique_faces_var = tk.BooleanVar()
# (moved) unique_faces_var is defined earlier before date filter widgets
# Define update_similar_faces function first - reusing auto-match display logic
def update_similar_faces():
"""Update the similar faces panel when compare is enabled - reuses auto-match display logic"""
nonlocal similar_faces_data, similar_face_vars, similar_face_images, similar_face_crops, face_selection_states
# Note: Selection states are now saved automatically via callbacks (auto-match style)
# Clear existing similar faces
for widget in similar_scrollable_frame.winfo_children():
widget.destroy()
similar_face_vars.clear()
similar_face_images.clear()
# Clean up existing face crops
for crop_path in similar_face_crops:
try:
if os.path.exists(crop_path):
os.remove(crop_path)
except:
pass
similar_face_crops.clear()
if compare_var.get():
# Use the same filtering and sorting logic as auto-match (no unique faces filter for similar faces)
unidentified_similar_faces = self._get_filtered_similar_faces(face_id, tolerance, include_same_photo=False, face_status=face_status)
if unidentified_similar_faces:
# Get current face_id for selection state management
current_face_id = original_faces[i][0] # Get current face_id
# Reuse auto-match display logic for similar faces
self._display_similar_faces_in_panel(similar_scrollable_frame, unidentified_similar_faces,
similar_face_vars, similar_face_images, similar_face_crops,
current_face_id, face_selection_states, identify_data_cache)
# Note: Selection states are now restored automatically during checkbox creation (auto-match style)
else:
# No similar unidentified faces found
no_faces_label = ttk.Label(similar_scrollable_frame, text="No similar unidentified faces found",
foreground="gray", font=("Arial", 10))
no_faces_label.pack(pady=20)
else:
# Compare disabled - clear the panel
clear_label = ttk.Label(similar_scrollable_frame, text="Enable 'Compare with similar faces' to see matches",
foreground="gray", font=("Arial", 10))
clear_label.pack(pady=20)
# Update button states based on compare checkbox and list contents
update_select_clear_buttons_state()
# (moved) update_similar_faces function is defined earlier before on_unique_faces_change
# Compare checkbox
compare_var = tk.BooleanVar()
# (moved) Compare checkbox is now inside date_filter_frame to the right of dates
def on_compare_change():
"""Handle compare checkbox change"""
update_similar_faces()
update_select_clear_buttons_state()
# (moved) on_unique_faces_change function is defined earlier before date filter widgets
compare_checkbox = ttk.Checkbutton(input_frame, text="Compare with similar faces", variable=compare_var,
command=on_compare_change)
compare_checkbox.grid(row=3, column=0, columnspan=4, sticky=tk.W, pady=(5, 0))
# Unique faces only checkbox widget
def on_unique_faces_change():
"""Handle unique faces checkbox change"""
nonlocal original_faces, i
if unique_faces_var.get():
# Show progress message
print("🔄 Applying unique faces filter...")
root.update() # Update UI to show the message
# Apply unique faces filtering to the main face list
try:
original_faces = self._filter_unique_faces_from_list(original_faces)
print(f"✅ Filter applied: {len(original_faces)} unique faces remaining")
except Exception as e:
print(f"⚠️ Error applying filter: {e}")
# Revert checkbox state
unique_faces_var.set(False)
return
else:
# Reload the original unfiltered face list
print("🔄 Reloading all faces...")
root.update() # Update UI to show the message
with self.get_db_connection() as conn:
cursor = conn.cursor()
query = '''
SELECT f.id, f.photo_id, p.path, p.filename, f.location
FROM faces f
JOIN photos p ON f.photo_id = p.id
WHERE f.person_id IS NULL
'''
params = []
# Add date taken filtering if specified
if date_from:
query += ' AND p.date_taken >= ?'
params.append(date_from)
if date_to:
query += ' AND p.date_taken <= ?'
params.append(date_to)
# Add date processed filtering if specified
if date_processed_from:
query += ' AND DATE(p.date_added) >= ?'
params.append(date_processed_from)
if date_processed_to:
query += ' AND DATE(p.date_added) <= ?'
params.append(date_processed_to)
query += ' ORDER BY f.id'
cursor.execute(query, params)
original_faces = list(cursor.fetchall())
print(f"✅ Reloaded: {len(original_faces)} faces")
# Reset to first face and update display
i = 0
update_similar_faces()
unique_faces_checkbox = ttk.Checkbutton(date_filter_frame, text="Unique faces only (hide duplicates with high/medium confidence)",
variable=unique_faces_var, command=on_unique_faces_change)
unique_faces_checkbox.grid(row=2, column=0, columnspan=6, sticky=tk.W, pady=(10, 0))
# Add callback to save person name when it changes
def on_name_change(*args):
@ -1757,9 +1769,9 @@ class PhotoTagger:
first_name_entry.bind('<Return>', on_enter)
last_name_entry.bind('<Return>', on_enter)
# Bottom control panel
# Bottom control panel (move to bottom below panels)
control_frame = ttk.Frame(main_frame)
control_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0), sticky=tk.E)
control_frame.grid(row=4, column=0, columnspan=3, pady=(10, 0), sticky=(tk.E, tk.S))
# Create button references for state management
back_btn = ttk.Button(control_frame, text="⬅️ Back", command=on_back)