Refactor PhotoTagger to enhance data handling by storing person information in separate fields for first name, last name, middle name, maiden name, and date of birth. Improve identification logic to ensure data integrity and streamline user experience. Update README to reflect changes in data storage and new features.
This commit is contained in:
parent
9f11a1a647
commit
8c9da7362b
12
README.md
12
README.md
@ -129,7 +129,7 @@ python3 photo_tagger.py auto-match --auto --show-faces
|
||||
**🎯 New GUI-Based Identification Features:**
|
||||
- 🖼️ **Visual Face Display** - See individual face crops in the GUI
|
||||
- 📝 **Separate Name Fields** - Dedicated text input fields for first name, last name, middle name, and maiden name
|
||||
- 🎯 **Smart Name Parsing** - Supports "Last, First" format that gets properly parsed and stored
|
||||
- 🎯 **Direct Field Storage** - Names are stored directly in separate fields for maximum reliability
|
||||
- ☑️ **Compare with Similar Faces** - Compare current face with similar unidentified faces
|
||||
- 🎨 **Modern Interface** - Clean, intuitive GUI with buttons and input fields
|
||||
- 💾 **Window Size Memory** - Remembers your preferred window size
|
||||
@ -142,6 +142,7 @@ python3 photo_tagger.py auto-match --auto --show-faces
|
||||
- 💾 **Quit Confirmation** - Saves pending identifications when closing the application
|
||||
- ⚡ **Performance Optimized** - Pre-fetched data for faster similar faces display
|
||||
- 🎯 **Clean Database Storage** - Names are stored as separate first_name and last_name fields without commas
|
||||
- 🔧 **Improved Data Handling** - Fixed field restoration and quit confirmation logic for better reliability
|
||||
|
||||
**🎯 New Auto-Match GUI Features:**
|
||||
- 📊 **Person-Centric View** - Shows matched person on left, all their unidentified faces on right
|
||||
@ -626,7 +627,14 @@ This is now a minimal, focused tool. Key principles:
|
||||
|
||||
## 🆕 Recent Improvements (Latest Version)
|
||||
|
||||
### 🔄 Field Navigation & Preservation Fixes (NEW!)
|
||||
### 🔧 Data Storage & Reliability Improvements (NEW!)
|
||||
- ✅ **Eliminated Redundant Storage** - Removed unnecessary combined name field for cleaner data structure
|
||||
- ✅ **Direct Field Access** - Names stored and accessed directly without parsing/combining logic
|
||||
- ✅ **Fixed Quit Confirmation** - Proper detection of pending identifications when quitting
|
||||
- ✅ **Improved Error Handling** - Better type consistency prevents runtime errors
|
||||
- ✅ **Enhanced Performance** - Eliminated string manipulation overhead for faster operations
|
||||
|
||||
### 🔄 Field Navigation & Preservation Fixes
|
||||
- ✅ **Fixed Name Field Confusion** - First and last names now stay in correct fields during navigation
|
||||
- ✅ **Enhanced Data Storage** - Individual field tracking prevents name swapping issues
|
||||
- ✅ **Date of Birth Preservation** - Date of birth now preserved even when entered alone (without names)
|
||||
|
||||
222
photo_tagger.py
222
photo_tagger.py
@ -430,47 +430,48 @@ class PhotoTagger:
|
||||
|
||||
for face_id, person_data in face_person_names.items():
|
||||
# Handle person data dict format
|
||||
person_name = person_data.get('name', '').strip()
|
||||
date_of_birth = person_data.get('date_of_birth', '').strip()
|
||||
middle_name = person_data.get('middle_name', '').strip()
|
||||
maiden_name = person_data.get('maiden_name', '').strip()
|
||||
|
||||
if person_name:
|
||||
try:
|
||||
with self.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
# Add person if doesn't exist
|
||||
# Parse person_name in "Last, First" or single-token format
|
||||
parts = [p.strip() for p in person_name.split(',', 1)]
|
||||
if len(parts) == 2:
|
||||
last_name, first_name = parts[0], parts[1]
|
||||
else:
|
||||
first_name = parts[0] if parts else ''
|
||||
last_name = ''
|
||||
if isinstance(person_data, dict):
|
||||
first_name = person_data.get('first_name', '').strip()
|
||||
last_name = person_data.get('last_name', '').strip()
|
||||
date_of_birth = person_data.get('date_of_birth', '').strip()
|
||||
middle_name = person_data.get('middle_name', '').strip()
|
||||
maiden_name = person_data.get('maiden_name', '').strip()
|
||||
|
||||
# Only save if we have at least a first or last name
|
||||
if first_name or last_name:
|
||||
try:
|
||||
with self.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
# Add person if doesn't exist
|
||||
cursor.execute('INSERT OR IGNORE INTO people (first_name, last_name, middle_name, maiden_name, date_of_birth) VALUES (?, ?, ?, ?, ?)', (first_name, last_name, middle_name, maiden_name, date_of_birth))
|
||||
cursor.execute('SELECT id FROM people WHERE first_name = ? AND last_name = ? AND middle_name = ? AND maiden_name = ? AND date_of_birth = ?', (first_name, last_name, middle_name, maiden_name, date_of_birth))
|
||||
result = cursor.fetchone()
|
||||
person_id = result[0] if result else None
|
||||
|
||||
cursor.execute('INSERT OR IGNORE INTO people (first_name, last_name, middle_name, maiden_name, date_of_birth) VALUES (?, ?, ?, ?, ?)', (first_name, last_name, middle_name, maiden_name, date_of_birth))
|
||||
cursor.execute('SELECT id FROM people WHERE first_name = ? AND last_name = ? AND middle_name = ? AND maiden_name = ? AND date_of_birth = ?', (first_name, last_name, middle_name, maiden_name, date_of_birth))
|
||||
result = cursor.fetchone()
|
||||
person_id = result[0] if result else None
|
||||
# Update people cache if new person was added
|
||||
display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
|
||||
if display_name not in identify_data_cache['people_names']:
|
||||
identify_data_cache['people_names'].append(display_name)
|
||||
identify_data_cache['people_names'].sort() # Keep sorted
|
||||
|
||||
# Assign face to person
|
||||
cursor.execute(
|
||||
'UPDATE faces SET person_id = ? WHERE id = ?',
|
||||
(person_id, face_id)
|
||||
)
|
||||
|
||||
# Update people cache if new person was added
|
||||
if person_name not in identify_data_cache['people_names']:
|
||||
identify_data_cache['people_names'].append(person_name)
|
||||
identify_data_cache['people_names'].sort() # Keep sorted
|
||||
# Update person encodings
|
||||
self._update_person_encodings(person_id)
|
||||
saved_count += 1
|
||||
display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
|
||||
print(f"✅ Saved identification: {display_name}")
|
||||
|
||||
# Assign face to person
|
||||
cursor.execute(
|
||||
'UPDATE faces SET person_id = ? WHERE id = ?',
|
||||
(person_id, face_id)
|
||||
)
|
||||
|
||||
# Update person encodings
|
||||
self._update_person_encodings(person_id)
|
||||
saved_count += 1
|
||||
print(f"✅ Saved identification: {person_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error saving identification for {person_name}: {e}")
|
||||
except Exception as e:
|
||||
display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
|
||||
print(f"❌ Error saving identification for {display_name}: {e}")
|
||||
else:
|
||||
# Handle legacy string format - skip for now as it doesn't have complete data
|
||||
pass
|
||||
|
||||
if saved_count > 0:
|
||||
identified_count += saved_count
|
||||
@ -491,18 +492,17 @@ class PhotoTagger:
|
||||
for k, v in face_person_names.items():
|
||||
if k not in face_status or face_status[k] != 'identified':
|
||||
# Handle person data dict format
|
||||
person_name = v.get('name', '').strip()
|
||||
date_of_birth = v.get('date_of_birth', '').strip()
|
||||
# Check if name has both first and last name
|
||||
if person_name and date_of_birth:
|
||||
# Parse name to check for both first and last name
|
||||
if ',' in person_name:
|
||||
last_name, first_name = person_name.split(',', 1)
|
||||
if last_name.strip() and first_name.strip():
|
||||
pending_identifications[k] = v
|
||||
else:
|
||||
# Single name format - not complete
|
||||
pass
|
||||
if isinstance(v, dict):
|
||||
first_name = v.get('first_name', '').strip()
|
||||
last_name = v.get('last_name', '').strip()
|
||||
date_of_birth = v.get('date_of_birth', '').strip()
|
||||
|
||||
# Check if we have complete data (both first and last name, plus date of birth)
|
||||
if first_name and last_name and date_of_birth:
|
||||
pending_identifications[k] = v
|
||||
else:
|
||||
# Handle legacy string format - not considered complete without date of birth
|
||||
pass
|
||||
|
||||
if pending_identifications:
|
||||
# Ask user if they want to save pending identifications
|
||||
@ -640,7 +640,7 @@ class PhotoTagger:
|
||||
calendar_window.grab_set()
|
||||
|
||||
# Calculate center position before showing the window
|
||||
window_width = 350
|
||||
window_width = 400
|
||||
window_height = 400
|
||||
screen_width = calendar_window.winfo_screenwidth()
|
||||
screen_height = calendar_window.winfo_screenheight()
|
||||
@ -928,16 +928,19 @@ class PhotoTagger:
|
||||
current_face_id = original_faces[i][0]
|
||||
first_name = first_name_var.get().strip()
|
||||
last_name = last_name_var.get().strip()
|
||||
middle_name = middle_name_var.get().strip()
|
||||
maiden_name = maiden_name_var.get().strip()
|
||||
date_of_birth = date_of_birth_var.get().strip()
|
||||
|
||||
if first_name or last_name:
|
||||
if last_name and first_name:
|
||||
current_name = f"{last_name}, {first_name}"
|
||||
elif last_name:
|
||||
current_name = last_name
|
||||
elif first_name:
|
||||
current_name = first_name
|
||||
else:
|
||||
current_name = ""
|
||||
face_person_names[current_face_id] = current_name
|
||||
# Store as dictionary to maintain consistency
|
||||
face_person_names[current_face_id] = {
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'middle_name': middle_name,
|
||||
'maiden_name': maiden_name,
|
||||
'date_of_birth': date_of_birth
|
||||
}
|
||||
elif current_face_id in face_person_names:
|
||||
# Remove empty names from storage
|
||||
del face_person_names[current_face_id]
|
||||
@ -947,9 +950,6 @@ class PhotoTagger:
|
||||
|
||||
# Buttons moved to bottom of window
|
||||
|
||||
# Instructions
|
||||
instructions = ttk.Label(input_frame, text="Select from dropdown or type new name", foreground="gray")
|
||||
instructions.grid(row=2, column=0, columnspan=4, pady=(10, 0))
|
||||
|
||||
# Right panel for similar faces
|
||||
similar_faces_frame = ttk.Frame(right_panel)
|
||||
@ -1039,17 +1039,10 @@ class PhotoTagger:
|
||||
date_of_birth = date_of_birth_var.get().strip()
|
||||
|
||||
if first_name or last_name:
|
||||
if last_name and first_name:
|
||||
current_name = f"{last_name}, {first_name}"
|
||||
elif last_name:
|
||||
current_name = last_name
|
||||
elif first_name:
|
||||
current_name = first_name
|
||||
else:
|
||||
current_name = ""
|
||||
# Store all fields
|
||||
face_person_names[current_face_id] = {
|
||||
'name': current_name,
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'middle_name': middle_name,
|
||||
'maiden_name': maiden_name,
|
||||
'date_of_birth': date_of_birth
|
||||
@ -1168,18 +1161,20 @@ class PhotoTagger:
|
||||
for k, v in face_person_names.items():
|
||||
if k not in face_status or face_status[k] != 'identified':
|
||||
# Handle person data dict format
|
||||
person_name = v.get('name', '').strip()
|
||||
date_of_birth = v.get('date_of_birth', '').strip()
|
||||
# Check if name has both first and last name
|
||||
if person_name and date_of_birth:
|
||||
# Parse name to check for both first and last name
|
||||
if ',' in person_name:
|
||||
last_name, first_name = person_name.split(',', 1)
|
||||
if last_name.strip() and first_name.strip():
|
||||
pending_identifications[k] = v
|
||||
else:
|
||||
# Single name format - not complete
|
||||
pass
|
||||
if isinstance(v, dict):
|
||||
first_name = v.get('first_name', '').strip()
|
||||
last_name = v.get('last_name', '').strip()
|
||||
date_of_birth = v.get('date_of_birth', '').strip()
|
||||
|
||||
# Check if we have complete data (both first and last name, plus date of birth)
|
||||
if first_name and last_name and date_of_birth:
|
||||
pending_identifications[k] = v
|
||||
else:
|
||||
# Handle legacy string format
|
||||
person_name = v.strip()
|
||||
date_of_birth = '' # Legacy format doesn't have date_of_birth
|
||||
# Legacy format is not considered complete without date of birth
|
||||
pass
|
||||
|
||||
if pending_identifications:
|
||||
# Ask user if they want to save pending identifications
|
||||
@ -1464,16 +1459,33 @@ class PhotoTagger:
|
||||
# Set person name input - restore saved name or use database/empty value
|
||||
if face_id in face_person_names:
|
||||
# Restore previously entered name for this face
|
||||
full_name = face_person_names[face_id]
|
||||
# Parse "Last, First" format back to separate fields
|
||||
if ', ' in full_name:
|
||||
parts = full_name.split(', ', 1)
|
||||
last_name_var.set(parts[0].strip())
|
||||
first_name_var.set(parts[1].strip())
|
||||
person_data = face_person_names[face_id]
|
||||
if isinstance(person_data, dict):
|
||||
# Handle dictionary format - use individual field values for proper restoration
|
||||
first_name = person_data.get('first_name', '').strip()
|
||||
last_name = person_data.get('last_name', '').strip()
|
||||
middle_name = person_data.get('middle_name', '').strip()
|
||||
maiden_name = person_data.get('maiden_name', '').strip()
|
||||
date_of_birth = person_data.get('date_of_birth', '').strip()
|
||||
|
||||
# Restore all fields directly
|
||||
first_name_var.set(first_name)
|
||||
last_name_var.set(last_name)
|
||||
middle_name_var.set(middle_name)
|
||||
maiden_name_var.set(maiden_name)
|
||||
date_of_birth_var.set(date_of_birth)
|
||||
else:
|
||||
# Single name format
|
||||
first_name_var.set(full_name)
|
||||
last_name_var.set("")
|
||||
# Handle legacy string format (for backward compatibility)
|
||||
full_name = person_data
|
||||
# Parse "Last, First" format back to separate fields
|
||||
if ', ' in full_name:
|
||||
parts = full_name.split(', ', 1)
|
||||
last_name_var.set(parts[0].strip())
|
||||
first_name_var.set(parts[1].strip())
|
||||
else:
|
||||
# Single name format
|
||||
first_name_var.set(full_name)
|
||||
last_name_var.set("")
|
||||
elif is_already_identified:
|
||||
# Pre-populate with the current person name from database
|
||||
with self.get_db_connection() as conn:
|
||||
@ -1614,25 +1626,19 @@ class PhotoTagger:
|
||||
if current_face_id in face_person_names:
|
||||
person_data = face_person_names[current_face_id]
|
||||
if isinstance(person_data, dict):
|
||||
person_name = person_data.get('name', '').strip()
|
||||
date_of_birth = person_data.get('date_of_birth', '').strip()
|
||||
# Use individual field values for proper restoration
|
||||
first_name = person_data.get('first_name', '').strip()
|
||||
last_name = person_data.get('last_name', '').strip()
|
||||
middle_name = person_data.get('middle_name', '').strip()
|
||||
maiden_name = person_data.get('maiden_name', '').strip()
|
||||
date_of_birth = person_data.get('date_of_birth', '').strip()
|
||||
|
||||
# Parse "Last, First" format back to separate fields
|
||||
if ', ' in person_name:
|
||||
parts = person_name.split(', ', 1)
|
||||
last_name_var.set(parts[0].strip())
|
||||
first_name_var.set(parts[1].strip())
|
||||
else:
|
||||
# Single name format
|
||||
first_name_var.set(person_name)
|
||||
last_name_var.set("")
|
||||
|
||||
# Repopulate all fields
|
||||
date_of_birth_var.set(date_of_birth)
|
||||
# Restore all fields directly
|
||||
first_name_var.set(first_name)
|
||||
last_name_var.set(last_name)
|
||||
middle_name_var.set(middle_name)
|
||||
maiden_name_var.set(maiden_name)
|
||||
date_of_birth_var.set(date_of_birth)
|
||||
else:
|
||||
# Clear fields
|
||||
first_name_var.set("")
|
||||
@ -3700,7 +3706,7 @@ class PhotoTagger:
|
||||
calendar_window.grab_set()
|
||||
|
||||
# Calculate center position before showing the window
|
||||
window_width = 350
|
||||
window_width = 400
|
||||
window_height = 400
|
||||
screen_width = calendar_window.winfo_screenwidth()
|
||||
screen_height = calendar_window.winfo_screenheight()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user