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:
parent
4c0a1a3b38
commit
da6f810b5b
310
photo_tagger.py
310
photo_tagger.py
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user