Refactor IdentifyGUI for improved face identification and management
This commit enhances the IdentifyGUI class by updating the face identification process to handle similar faces more effectively. The logic for updating the current face index has been streamlined, allowing for better flow when identifying faces. Additionally, new methods have been introduced to manage selected similar faces, ensuring that all identified faces are properly marked and displayed. The form clearing functionality has also been updated to include similar face selections, improving user experience during the identification process.
This commit is contained in:
parent
69150b2025
commit
1972a69685
@ -277,7 +277,8 @@ class IdentifyGUI:
|
||||
# Main processing loop
|
||||
while not window_destroyed:
|
||||
# Check if current face is identified and update index if needed
|
||||
if not self._update_current_face_index(original_faces, i, face_status):
|
||||
has_faces, i = self._update_current_face_index(original_faces, i, face_status)
|
||||
if not has_faces:
|
||||
# All faces have been identified
|
||||
print("\n🎉 All faces have been identified!")
|
||||
break
|
||||
@ -547,19 +548,17 @@ class IdentifyGUI:
|
||||
# Clear form after successful identification
|
||||
self._clear_form(gui_components)
|
||||
|
||||
# Update face index to move to next unidentified face
|
||||
has_faces, i = self._update_current_face_index(original_faces, i, face_status)
|
||||
if not has_faces:
|
||||
# All faces have been identified
|
||||
print("\n🎉 All faces have been identified!")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
messagebox.showerror("Error", f"Error processing identification: {e}")
|
||||
|
||||
# Increment index for normal flow (identification or error) - but not if we're at the last item
|
||||
if i < len(original_faces) - 1:
|
||||
i += 1
|
||||
self._update_button_states(gui_components, original_faces, i, face_status)
|
||||
# Only update similar faces if compare is enabled
|
||||
if gui_components['compare_var'].get():
|
||||
self._update_similar_faces(gui_components, face_id, tolerance, face_status,
|
||||
face_selection_states, identify_data_cache, original_faces, i)
|
||||
|
||||
# Clean up current face crop when moving forward after identification
|
||||
if face_crop_path and os.path.exists(face_crop_path):
|
||||
try:
|
||||
@ -1370,7 +1369,7 @@ class IdentifyGUI:
|
||||
unidentified_faces = [face for face in original_faces if face[0] not in face_status or face_status[face[0]] != 'identified']
|
||||
if not unidentified_faces:
|
||||
# All faces identified, we're done
|
||||
return False
|
||||
return False, i
|
||||
|
||||
# Find the current face in the unidentified list
|
||||
current_face_id = original_faces[i][0] if i < len(original_faces) else None
|
||||
@ -1401,7 +1400,7 @@ class IdentifyGUI:
|
||||
if i < 0:
|
||||
i = 0
|
||||
|
||||
return True
|
||||
return True, i
|
||||
|
||||
def _get_current_face_position(self, original_faces, i, face_status):
|
||||
"""Get current face position among unidentified faces"""
|
||||
@ -1933,7 +1932,10 @@ class IdentifyGUI:
|
||||
identify_data_cache['last_names'].append(last_name)
|
||||
identify_data_cache['last_names'].sort()
|
||||
|
||||
# Assign face to person
|
||||
# Get selected similar faces
|
||||
selected_similar_faces = self._get_selected_similar_faces()
|
||||
|
||||
# Assign main face to person
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
@ -1941,16 +1943,35 @@ class IdentifyGUI:
|
||||
(person_id, face_id)
|
||||
)
|
||||
|
||||
# Assign selected similar faces to the same person
|
||||
similar_faces_identified = 0
|
||||
if selected_similar_faces:
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for similar_face_id in selected_similar_faces:
|
||||
cursor.execute(
|
||||
'UPDATE faces SET person_id = ? WHERE id = ?',
|
||||
(person_id, similar_face_id)
|
||||
)
|
||||
similar_faces_identified += 1
|
||||
|
||||
# Update person encodings
|
||||
self.face_processor.update_person_encodings(person_id)
|
||||
|
||||
# Mark face as identified
|
||||
# Mark main face as identified
|
||||
face_status[face_id] = 'identified'
|
||||
|
||||
display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
|
||||
print(f"✅ Identified as: {display_name}")
|
||||
# Mark selected similar faces as identified
|
||||
for similar_face_id in selected_similar_faces:
|
||||
face_status[similar_face_id] = 'identified'
|
||||
|
||||
return 1
|
||||
display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
|
||||
if similar_faces_identified > 0:
|
||||
print(f"✅ Identified as: {display_name} (main face + {similar_faces_identified} similar faces)")
|
||||
else:
|
||||
print(f"✅ Identified as: {display_name}")
|
||||
|
||||
return 1 + similar_faces_identified
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing identification: {e}")
|
||||
@ -2026,12 +2047,17 @@ class IdentifyGUI:
|
||||
return 'continue' # No changes, safe to continue
|
||||
|
||||
def _clear_form(self, gui_components):
|
||||
"""Clear all form fields"""
|
||||
"""Clear all form fields and similar face selections"""
|
||||
gui_components['first_name_var'].set("")
|
||||
gui_components['last_name_var'].set("")
|
||||
gui_components['middle_name_var'].set("")
|
||||
gui_components['maiden_name_var'].set("")
|
||||
gui_components['date_of_birth_var'].set("")
|
||||
|
||||
# Clear selected similar faces
|
||||
if hasattr(self, '_similar_face_vars'):
|
||||
for face_id, var in self._similar_face_vars:
|
||||
var.set(False)
|
||||
|
||||
def _get_form_data(self, gui_components):
|
||||
"""Get current form data as a dictionary"""
|
||||
@ -2043,6 +2069,15 @@ class IdentifyGUI:
|
||||
'date_of_birth': gui_components['date_of_birth_var'].get().strip()
|
||||
}
|
||||
|
||||
def _get_selected_similar_faces(self):
|
||||
"""Get list of selected similar face IDs"""
|
||||
selected_faces = []
|
||||
if hasattr(self, '_similar_face_vars'):
|
||||
for face_id, var in self._similar_face_vars:
|
||||
if var.get(): # If checkbox is checked
|
||||
selected_faces.append(face_id)
|
||||
return selected_faces
|
||||
|
||||
def _set_form_data(self, gui_components, form_data):
|
||||
"""Set form data from a dictionary"""
|
||||
gui_components['first_name_var'].set(form_data.get('first_name', ''))
|
||||
|
||||
@ -441,6 +441,22 @@ class SearchGUI:
|
||||
combo.grid(row=1, column=1, padx=(0, 8), sticky=(tk.W, tk.E))
|
||||
combo.focus_set()
|
||||
|
||||
def get_saved_tag_types_for_photo(photo_id: int):
|
||||
"""Get saved linkage types for a photo {tag_id: type_int}"""
|
||||
types = {}
|
||||
try:
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT tag_id, linkage_type FROM phototaglinkage WHERE photo_id = ?', (photo_id,))
|
||||
for row in cursor.fetchall():
|
||||
try:
|
||||
types[row[0]] = int(row[1]) if row[1] is not None else 0
|
||||
except Exception:
|
||||
types[row[0]] = 0
|
||||
except Exception:
|
||||
pass
|
||||
return types
|
||||
|
||||
def add_selected_tag():
|
||||
tag_name = tag_var.get().strip()
|
||||
if not tag_name:
|
||||
@ -469,7 +485,7 @@ class SearchGUI:
|
||||
messagebox.showerror("Error", f"Failed to create tag '{tag_name}'", parent=popup)
|
||||
return
|
||||
|
||||
# Add tag to all selected photos
|
||||
# Add tag to all selected photos with single linkage type (0)
|
||||
affected = 0
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@ -507,28 +523,76 @@ class SearchGUI:
|
||||
widget.destroy()
|
||||
selected_tag_vars.clear()
|
||||
|
||||
# Get all unique tags from selected photos
|
||||
all_tag_ids = set()
|
||||
# Get tags that exist in ALL selected photos
|
||||
# First, get all tags for each photo
|
||||
photo_tags = {} # photo_id -> set of tag_ids
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for photo_id in photo_ids:
|
||||
cursor.execute('SELECT tag_id FROM phototaglinkage WHERE photo_id = ?', (photo_id,))
|
||||
photo_tags[photo_id] = set()
|
||||
cursor.execute('SELECT tag_id, linkage_type FROM phototaglinkage WHERE photo_id = ?', (photo_id,))
|
||||
for row in cursor.fetchall():
|
||||
all_tag_ids.add(row[0])
|
||||
photo_tags[photo_id].add(row[0])
|
||||
|
||||
if not all_tag_ids:
|
||||
# Find intersection - tags that exist in ALL selected photos
|
||||
if not photo_tags:
|
||||
ttk.Label(scrollable_frame, text="No tags linked to selected photos", foreground="gray").pack(anchor=tk.W, pady=5)
|
||||
return
|
||||
|
||||
for tag_id in sorted(all_tag_ids):
|
||||
# Start with tags from first photo, then intersect with others
|
||||
common_tag_ids = set(photo_tags[photo_ids[0]])
|
||||
for photo_id in photo_ids[1:]:
|
||||
common_tag_ids = common_tag_ids.intersection(photo_tags[photo_id])
|
||||
|
||||
if not common_tag_ids:
|
||||
ttk.Label(scrollable_frame, text="No common tags found across all selected photos", foreground="gray").pack(anchor=tk.W, pady=5)
|
||||
return
|
||||
|
||||
# Get linkage type information for common tags
|
||||
# For tags that exist in all photos, we need to determine the linkage type
|
||||
# If a tag has different linkage types across photos, we'll show the most restrictive
|
||||
common_tag_data = {} # tag_id -> {linkage_type, photo_count}
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for photo_id in photo_ids:
|
||||
cursor.execute('SELECT tag_id, linkage_type FROM phototaglinkage WHERE photo_id = ? AND tag_id IN ({})'.format(','.join('?' * len(common_tag_ids))), [photo_id] + list(common_tag_ids))
|
||||
for row in cursor.fetchall():
|
||||
tag_id = row[0]
|
||||
linkage_type = int(row[1]) if row[1] is not None else 0
|
||||
if tag_id not in common_tag_data:
|
||||
common_tag_data[tag_id] = {'linkage_type': linkage_type, 'photo_count': 0}
|
||||
common_tag_data[tag_id]['photo_count'] += 1
|
||||
# If we find a bulk linkage type (1), use that as it's more restrictive
|
||||
if linkage_type == 1:
|
||||
common_tag_data[tag_id]['linkage_type'] = 1
|
||||
|
||||
# Sort tags by name for consistent display
|
||||
for tag_id in sorted(common_tag_data.keys()):
|
||||
tag_name = tag_id_to_name.get(tag_id, f"Unknown {tag_id}")
|
||||
var = tk.BooleanVar()
|
||||
selected_tag_vars[tag_name] = var
|
||||
frame = ttk.Frame(scrollable_frame)
|
||||
frame.pack(fill=tk.X, pady=1)
|
||||
|
||||
# Determine if this tag can be selected for deletion
|
||||
# In single linkage dialog, only allow deleting single linkage type (0) tags
|
||||
linkage_type = common_tag_data[tag_id]['linkage_type']
|
||||
can_select = (linkage_type == 0) # Only single linkage type can be deleted
|
||||
|
||||
cb = ttk.Checkbutton(frame, variable=var)
|
||||
if not can_select:
|
||||
try:
|
||||
cb.state(["disabled"]) # disable selection for bulk tags
|
||||
except Exception:
|
||||
pass
|
||||
cb.pack(side=tk.LEFT, padx=(0, 5))
|
||||
ttk.Label(frame, text=tag_name).pack(side=tk.LEFT)
|
||||
|
||||
# Display tag name with status information
|
||||
type_label = 'single' if linkage_type == 0 else 'bulk'
|
||||
photo_count = common_tag_data[tag_id]['photo_count']
|
||||
status_text = f" (saved {type_label}, on {photo_count}/{len(photo_ids)} photos)"
|
||||
status_color = "black" if can_select else "gray"
|
||||
ttk.Label(frame, text=tag_name + status_text, foreground=status_color).pack(side=tk.LEFT)
|
||||
|
||||
def remove_selected_tags():
|
||||
tag_ids_to_remove = []
|
||||
@ -539,12 +603,16 @@ class SearchGUI:
|
||||
if not tag_ids_to_remove:
|
||||
return
|
||||
|
||||
# Remove tags from all selected photos
|
||||
# Only remove single linkage type tags (bulk tags should be disabled anyway)
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for photo_id in photo_ids:
|
||||
for tag_id in tag_ids_to_remove:
|
||||
cursor.execute('DELETE FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?', (photo_id, tag_id))
|
||||
# Double-check that this is a single linkage type before deleting
|
||||
cursor.execute('SELECT linkage_type FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?', (photo_id, tag_id))
|
||||
result = cursor.fetchone()
|
||||
if result and int(result[0]) == 0: # Only delete single linkage type
|
||||
cursor.execute('DELETE FROM phototaglinkage WHERE photo_id = ? AND tag_id = ?', (photo_id, tag_id))
|
||||
|
||||
refresh_tag_list()
|
||||
|
||||
|
||||
@ -556,13 +556,12 @@ class TagManagerGUI:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT p.id, p.filename, p.path, p.processed, p.date_taken, p.date_added,
|
||||
COUNT(f.id) as face_count,
|
||||
GROUP_CONCAT(DISTINCT t.tag_name) as tags
|
||||
(SELECT COUNT(*) FROM faces f WHERE f.photo_id = p.id) as face_count,
|
||||
(SELECT GROUP_CONCAT(DISTINCT t.tag_name)
|
||||
FROM phototaglinkage ptl
|
||||
JOIN tags t ON t.id = ptl.tag_id
|
||||
WHERE ptl.photo_id = p.id) as tags
|
||||
FROM photos p
|
||||
LEFT JOIN faces f ON f.photo_id = p.id
|
||||
LEFT JOIN phototaglinkage ptl ON ptl.photo_id = p.id
|
||||
LEFT JOIN tags t ON t.id = ptl.tag_id
|
||||
GROUP BY p.id, p.filename, p.path, p.processed, p.date_taken, p.date_added
|
||||
ORDER BY p.date_taken DESC, p.filename
|
||||
''')
|
||||
photos_data = []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user