Add Auto-Match Panel to Dashboard GUI for enhanced face matching functionality
This commit introduces the AutoMatchPanel class into the Dashboard GUI, providing a fully integrated interface for automatic face matching. The new panel allows users to start the auto-match process, configure tolerance settings, and visually confirm matches between identified and unidentified faces. It includes features for bulk selection of matches, smart navigation through matched individuals, and a search filter for large databases. The README has been updated to reflect the new functionality and improvements in the auto-match workflow, enhancing the overall user experience in managing photo identifications.
This commit is contained in:
parent
e5ec0e4aea
commit
8ce538c508
@ -51,13 +51,13 @@ python3 setup.py # Installs system deps + Python packages
|
||||
python3 photo_tagger.py dashboard
|
||||
|
||||
# 3. Use the menu bar to access all features:
|
||||
# 📁 Scan - Add photos to your collection
|
||||
# 🔍 Process - Detect faces in photos
|
||||
# 👤 Identify - Identify people in photos
|
||||
# 🔗 Auto-Match - Find matching faces automatically
|
||||
# 🔎 Search - Find photos by person name
|
||||
# ✏️ Modify - Edit face identifications
|
||||
# 🏷️ Tags - Manage photo tags
|
||||
# 📁 Scan - Add photos to your collection (✅ Fully Functional)
|
||||
# 🔍 Process - Detect faces in photos (✅ Fully Functional)
|
||||
# 👤 Identify - Identify people in photos (✅ Fully Functional)
|
||||
# 🔗 Auto-Match - Find matching faces automatically (✅ Fully Functional)
|
||||
# 🔎 Search - Find photos by person name (✅ Fully Functional)
|
||||
# ✏️ Modify - Edit face identifications (🚧 Coming Soon)
|
||||
# 🏷️ Tags - Manage photo tags (🚧 Coming Soon)
|
||||
```
|
||||
|
||||
## 📦 Installation
|
||||
@ -149,17 +149,47 @@ The dashboard automatically opens in full screen mode and provides a fully respo
|
||||
- **Responsive Layout**: Adapts to window resizing with dynamic updates
|
||||
- **Enhanced Navigation**: Back/Next buttons with unsaved changes protection
|
||||
|
||||
#### **🔗 Auto-Match Panel** *(Coming Soon)*
|
||||
- **Person-Centric View**: Show matched person with potential matches
|
||||
- **Confidence Scoring**: Color-coded match confidence levels
|
||||
- **Bulk Selection**: Select multiple faces for identification
|
||||
- **Smart Navigation**: Efficient browsing through matches
|
||||
#### **🔗 Auto-Match Panel** *(Fully Functional)*
|
||||
- **Person-Centric Workflow**: Groups faces by already identified people
|
||||
- **Visual Confirmation**: Left panel shows identified person, right panel shows potential matches
|
||||
- **Confidence Scoring**: Color-coded match confidence levels with detailed descriptions
|
||||
- **Bulk Selection**: Select multiple faces for identification with Select All/Clear All
|
||||
- **Smart Navigation**: Back/Next buttons to move between different people
|
||||
- **Search Functionality**: Filter people by last name for large databases
|
||||
- **Pre-selection**: Previously identified faces are automatically checked
|
||||
- **Unsaved Changes Protection**: Warns before losing unsaved work
|
||||
- **Database Integration**: Proper transactions and face encoding updates
|
||||
|
||||
#### **🔎 Search Panel** *(Coming Soon)*
|
||||
- **Advanced Search**: Find photos by person name
|
||||
- **Multiple Search Types**: Name-based, tag-based, date-based searches
|
||||
- **Results Display**: Grid view with face thumbnails
|
||||
- **Export Options**: Save search results
|
||||
##### **Auto-Match Workflow**
|
||||
The auto-match feature works in a **person-centric** way:
|
||||
|
||||
1. **Group by Person**: Faces are grouped by already identified people (not unidentified faces)
|
||||
2. **Show Matched Person**: Left side shows the identified person and their face
|
||||
3. **Show Unidentified Faces**: Right side shows all unidentified faces that match this person
|
||||
4. **Select and Save**: Check the faces you want to identify with this person, then click "Save Changes"
|
||||
5. **Navigate**: Use Back/Next to move between different people
|
||||
6. **Correct Mistakes**: Go back and uncheck faces to unidentify them
|
||||
7. **Pre-selected Checkboxes**: Previously identified faces are automatically checked when you go back
|
||||
|
||||
**Key Benefits:**
|
||||
- **1-to-Many**: One person can have multiple unidentified faces matched to them
|
||||
- **Visual Confirmation**: See exactly what you're identifying before saving
|
||||
- **Easy Corrections**: Go back and fix mistakes by unchecking faces
|
||||
- **Smart Tracking**: Previously identified faces are pre-selected for easy review
|
||||
|
||||
##### **Auto-Match Configuration**
|
||||
- **Tolerance Setting**: Adjust matching sensitivity (lower = stricter matching)
|
||||
- **Start Button**: Prominently positioned on the left for easy access
|
||||
- **Search Functionality**: Filter people by last name for large databases
|
||||
- **Exit Button**: "Exit Auto-Match" with unsaved changes protection
|
||||
|
||||
#### **🔎 Search Panel** *(Fully Functional)*
|
||||
- **Multiple Search Types**: Search photos by name, date, tags, and special categories
|
||||
- **Advanced Filtering**: Filter by folder location with browse functionality
|
||||
- **Results Display**: Sortable table with person names, tags, processed status, and dates
|
||||
- **Interactive Results**: Click to open photos, browse folders, and view people
|
||||
- **Tag Management**: Add and remove tags from selected photos
|
||||
- **Responsive Layout**: Adapts to window resizing with proper scrolling
|
||||
|
||||
#### **✏️ Modify Panel** *(Coming Soon)*
|
||||
- **Review Identifications**: View all identified people
|
||||
@ -319,13 +349,13 @@ source venv/bin/activate
|
||||
python3 photo_tagger.py dashboard
|
||||
|
||||
# Then use the menu bar in the dashboard:
|
||||
# 📁 Scan - Add photos
|
||||
# 🔍 Process - Detect faces
|
||||
# 👤 Identify - Identify people
|
||||
# 🔗 Auto-Match - Find matches
|
||||
# 🔎 Search - Find photos
|
||||
# ✏️ Modify - Edit identifications
|
||||
# 🏷️ Tags - Manage tags
|
||||
# 📁 Scan - Add photos (✅ Fully Functional)
|
||||
# 🔍 Process - Detect faces (✅ Fully Functional)
|
||||
# 👤 Identify - Identify people (✅ Fully Functional)
|
||||
# 🔗 Auto-Match - Find matches (✅ Fully Functional)
|
||||
# 🔎 Search - Find photos (✅ Fully Functional)
|
||||
# ✏️ Modify - Edit identifications (🚧 Coming Soon)
|
||||
# 🏷️ Tags - Manage tags (🚧 Coming Soon)
|
||||
|
||||
# Legacy command line usage
|
||||
python3 photo_tagger.py scan ~/Pictures --recursive
|
||||
@ -353,8 +383,8 @@ python3 photo_tagger.py stats
|
||||
|
||||
### Phase 2: GUI Panel Integration (In Progress)
|
||||
- [x] Identify panel integration (fully functional)
|
||||
- [ ] Auto-Match panel integration
|
||||
- [ ] Search panel integration
|
||||
- [x] Auto-Match panel integration (fully functional)
|
||||
- [x] Search panel integration (fully functional)
|
||||
- [ ] Modify panel integration
|
||||
- [ ] Tags panel integration
|
||||
|
||||
|
||||
868
auto_match_panel.py
Normal file
868
auto_match_panel.py
Normal file
@ -0,0 +1,868 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Auto-Match Panel for PunimTag Dashboard
|
||||
Embeds the full auto-match GUI functionality into the dashboard frame
|
||||
"""
|
||||
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from PIL import Image, ImageTk
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
|
||||
from config import DEFAULT_FACE_TOLERANCE
|
||||
from database import DatabaseManager
|
||||
from face_processing import FaceProcessor
|
||||
from gui_core import GUICore
|
||||
|
||||
|
||||
class AutoMatchPanel:
|
||||
"""Integrated auto-match panel that embeds the full auto-match GUI functionality into the dashboard"""
|
||||
|
||||
def __init__(self, parent_frame: ttk.Frame, db_manager: DatabaseManager,
|
||||
face_processor: FaceProcessor, gui_core: GUICore, verbose: int = 0):
|
||||
"""Initialize the auto-match panel"""
|
||||
self.parent_frame = parent_frame
|
||||
self.db = db_manager
|
||||
self.face_processor = face_processor
|
||||
self.gui_core = gui_core
|
||||
self.verbose = verbose
|
||||
|
||||
# Panel state
|
||||
self.is_active = False
|
||||
self.matches_by_matched = {}
|
||||
self.data_cache = {}
|
||||
self.current_matched_index = 0
|
||||
self.matched_ids = []
|
||||
self.filtered_matched_ids = None
|
||||
self.identified_faces_per_person = {}
|
||||
self.checkbox_states_per_person = {}
|
||||
self.original_checkbox_states_per_person = {}
|
||||
self.identified_count = 0
|
||||
|
||||
# GUI components
|
||||
self.components = {}
|
||||
self.main_frame = None
|
||||
|
||||
def create_panel(self) -> ttk.Frame:
|
||||
"""Create the auto-match panel with all GUI components"""
|
||||
self.main_frame = ttk.Frame(self.parent_frame)
|
||||
|
||||
# Configure grid weights for full screen responsiveness
|
||||
self.main_frame.columnconfigure(0, weight=1) # Left panel
|
||||
self.main_frame.columnconfigure(1, weight=1) # Right panel
|
||||
self.main_frame.rowconfigure(0, weight=0) # Configuration row - fixed height
|
||||
self.main_frame.rowconfigure(1, weight=1) # Main panels row - expandable
|
||||
self.main_frame.rowconfigure(2, weight=0) # Control buttons row - fixed height
|
||||
|
||||
# Create all GUI components
|
||||
self._create_gui_components()
|
||||
|
||||
# Create main content panels
|
||||
self._create_main_panels()
|
||||
|
||||
return self.main_frame
|
||||
|
||||
def _create_gui_components(self):
|
||||
"""Create all GUI components for the auto-match interface"""
|
||||
# Configuration frame
|
||||
config_frame = ttk.LabelFrame(self.main_frame, text="Configuration", padding="10")
|
||||
config_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||
# Don't give weight to any column to prevent stretching
|
||||
|
||||
# Start button (moved to the left)
|
||||
start_btn = ttk.Button(config_frame, text="🚀 Start Auto-Match", command=self._start_auto_match)
|
||||
start_btn.grid(row=0, column=0, padx=(0, 20))
|
||||
|
||||
# Tolerance setting
|
||||
ttk.Label(config_frame, text="Tolerance:").grid(row=0, column=1, sticky=tk.W, padx=(0, 2))
|
||||
self.components['tolerance_var'] = tk.StringVar(value=str(DEFAULT_FACE_TOLERANCE))
|
||||
tolerance_entry = ttk.Entry(config_frame, textvariable=self.components['tolerance_var'], width=8)
|
||||
tolerance_entry.grid(row=0, column=2, sticky=tk.W, padx=(0, 10))
|
||||
ttk.Label(config_frame, text="(lower = stricter matching)").grid(row=0, column=3, sticky=tk.W)
|
||||
|
||||
def _create_main_panels(self):
|
||||
"""Create the main left and right panels"""
|
||||
# Left panel for identified person
|
||||
self.components['left_panel'] = ttk.LabelFrame(self.main_frame, text="Identified Person", padding="10")
|
||||
self.components['left_panel'].grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
|
||||
|
||||
# Right panel for unidentified faces
|
||||
self.components['right_panel'] = ttk.LabelFrame(self.main_frame, text="Unidentified Faces to Match", padding="10")
|
||||
self.components['right_panel'].grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
|
||||
|
||||
# Create left panel content
|
||||
self._create_left_panel_content()
|
||||
|
||||
# Create right panel content
|
||||
self._create_right_panel_content()
|
||||
|
||||
# Create control buttons
|
||||
self._create_control_buttons()
|
||||
|
||||
def _create_left_panel_content(self):
|
||||
"""Create the left panel content for identified person"""
|
||||
left_panel = self.components['left_panel']
|
||||
|
||||
# Search controls for filtering people by last name
|
||||
search_frame = ttk.Frame(left_panel)
|
||||
search_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||
search_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# Search input
|
||||
self.components['search_var'] = tk.StringVar()
|
||||
search_entry = ttk.Entry(search_frame, textvariable=self.components['search_var'], width=20)
|
||||
search_entry.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=(0, 5))
|
||||
|
||||
# Search buttons
|
||||
search_btn = ttk.Button(search_frame, text="Search", width=8, command=self._apply_search_filter)
|
||||
search_btn.grid(row=0, column=1, padx=(0, 5))
|
||||
|
||||
clear_btn = ttk.Button(search_frame, text="Clear", width=6, command=self._clear_search_filter)
|
||||
clear_btn.grid(row=0, column=2)
|
||||
|
||||
# Search help label
|
||||
self.components['search_help_label'] = ttk.Label(search_frame, text="Type Last Name",
|
||||
font=("Arial", 8), foreground="gray")
|
||||
self.components['search_help_label'].grid(row=1, column=0, columnspan=3, sticky=tk.W, pady=(2, 0))
|
||||
|
||||
# Person info label
|
||||
self.components['person_info_label'] = ttk.Label(left_panel, text="", font=("Arial", 10, "bold"))
|
||||
self.components['person_info_label'].grid(row=1, column=0, pady=(0, 10), sticky=tk.W)
|
||||
|
||||
# Person image canvas
|
||||
style = ttk.Style()
|
||||
canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
|
||||
self.components['person_canvas'] = tk.Canvas(left_panel, width=300, height=300,
|
||||
bg=canvas_bg_color, highlightthickness=0)
|
||||
self.components['person_canvas'].grid(row=2, column=0, pady=(0, 10))
|
||||
|
||||
# Save button
|
||||
self.components['save_btn'] = ttk.Button(left_panel, text="💾 Save Changes",
|
||||
command=self._save_changes, state='disabled')
|
||||
self.components['save_btn'].grid(row=3, column=0, pady=(0, 10), sticky=(tk.W, tk.E))
|
||||
|
||||
def _create_right_panel_content(self):
|
||||
"""Create the right panel content for unidentified faces"""
|
||||
right_panel = self.components['right_panel']
|
||||
|
||||
# Control buttons for matches (Select All / Clear All)
|
||||
matches_controls_frame = ttk.Frame(right_panel)
|
||||
matches_controls_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||
|
||||
self.components['select_all_btn'] = ttk.Button(matches_controls_frame, text="☑️ Select All",
|
||||
command=self._select_all_matches, state='disabled')
|
||||
self.components['select_all_btn'].pack(side=tk.LEFT, padx=(0, 5))
|
||||
|
||||
self.components['clear_all_btn'] = ttk.Button(matches_controls_frame, text="☐ Clear All",
|
||||
command=self._clear_all_matches, state='disabled')
|
||||
self.components['clear_all_btn'].pack(side=tk.LEFT)
|
||||
|
||||
# Create scrollable frame for matches
|
||||
matches_frame = ttk.Frame(right_panel)
|
||||
matches_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
matches_frame.columnconfigure(0, weight=1)
|
||||
matches_frame.rowconfigure(0, weight=1)
|
||||
|
||||
# Create canvas and scrollbar for matches
|
||||
style = ttk.Style()
|
||||
canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
|
||||
self.components['matches_canvas'] = tk.Canvas(matches_frame, bg=canvas_bg_color, highlightthickness=0)
|
||||
self.components['matches_canvas'].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
scrollbar = ttk.Scrollbar(matches_frame, orient="vertical", command=self.components['matches_canvas'].yview)
|
||||
scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
|
||||
self.components['matches_canvas'].configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
# Configure right panel grid weights
|
||||
right_panel.columnconfigure(0, weight=1)
|
||||
right_panel.rowconfigure(1, weight=1)
|
||||
|
||||
def _create_control_buttons(self):
|
||||
"""Create the control buttons for navigation"""
|
||||
control_frame = ttk.Frame(self.main_frame)
|
||||
control_frame.grid(row=2, column=0, columnspan=2, pady=(10, 0))
|
||||
|
||||
self.components['back_btn'] = ttk.Button(control_frame, text="⏮️ Back",
|
||||
command=self._go_back, state='disabled')
|
||||
self.components['back_btn'].grid(row=0, column=0, padx=(0, 5))
|
||||
|
||||
self.components['next_btn'] = ttk.Button(control_frame, text="⏭️ Next",
|
||||
command=self._go_next, state='disabled')
|
||||
self.components['next_btn'].grid(row=0, column=1, padx=5)
|
||||
|
||||
self.components['quit_btn'] = ttk.Button(control_frame, text="❌ Exit Auto-Match",
|
||||
command=self._quit_auto_match)
|
||||
self.components['quit_btn'].grid(row=0, column=2, padx=(5, 0))
|
||||
|
||||
def _start_auto_match(self):
|
||||
"""Start the auto-match process"""
|
||||
try:
|
||||
tolerance = float(self.components['tolerance_var'].get().strip())
|
||||
if tolerance < 0 or tolerance > 1:
|
||||
raise ValueError
|
||||
except Exception:
|
||||
messagebox.showerror("Error", "Please enter a valid tolerance value between 0.0 and 1.0.")
|
||||
return
|
||||
|
||||
include_same_photo = False # Always exclude same photo matching
|
||||
|
||||
# Get all identified faces (one per person) to use as reference faces
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT f.id, f.person_id, f.photo_id, f.location, p.filename, f.quality_score
|
||||
FROM faces f
|
||||
JOIN photos p ON f.photo_id = p.id
|
||||
WHERE f.person_id IS NOT NULL AND f.quality_score >= 0.3
|
||||
ORDER BY f.person_id, f.quality_score DESC
|
||||
''')
|
||||
identified_faces = cursor.fetchall()
|
||||
|
||||
if not identified_faces:
|
||||
messagebox.showinfo("No Identified Faces", "🔍 No identified faces found for auto-matching")
|
||||
return
|
||||
|
||||
# Group by person and get the best quality face per person
|
||||
person_faces = {}
|
||||
for face in identified_faces:
|
||||
person_id = face[1]
|
||||
if person_id not in person_faces:
|
||||
person_faces[person_id] = face
|
||||
|
||||
# Convert to ordered list to ensure consistent ordering
|
||||
person_faces_list = []
|
||||
for person_id, face in person_faces.items():
|
||||
# Get person name for ordering
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT first_name, last_name FROM people WHERE id = ?', (person_id,))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
first_name, last_name = result
|
||||
if last_name and first_name:
|
||||
person_name = f"{last_name}, {first_name}"
|
||||
elif last_name:
|
||||
person_name = last_name
|
||||
elif first_name:
|
||||
person_name = first_name
|
||||
else:
|
||||
person_name = "Unknown"
|
||||
else:
|
||||
person_name = "Unknown"
|
||||
person_faces_list.append((person_id, face, person_name))
|
||||
|
||||
# Sort by person name for consistent, user-friendly ordering
|
||||
person_faces_list.sort(key=lambda x: x[2]) # Sort by person name (index 2)
|
||||
|
||||
# Find similar faces for each identified person
|
||||
self.matches_by_matched = {}
|
||||
for person_id, reference_face, person_name in person_faces_list:
|
||||
reference_face_id = reference_face[0]
|
||||
|
||||
# Use the same filtering and sorting logic as identify
|
||||
similar_faces = self.face_processor._get_filtered_similar_faces(
|
||||
reference_face_id, tolerance, include_same_photo, face_status=None)
|
||||
|
||||
# Convert to auto-match format
|
||||
person_matches = []
|
||||
for similar_face in similar_faces:
|
||||
match = {
|
||||
'unidentified_id': similar_face['face_id'],
|
||||
'unidentified_photo_id': similar_face['photo_id'],
|
||||
'unidentified_filename': similar_face['filename'],
|
||||
'unidentified_location': similar_face['location'],
|
||||
'matched_id': reference_face_id,
|
||||
'matched_photo_id': reference_face[2],
|
||||
'matched_filename': reference_face[4],
|
||||
'matched_location': reference_face[3],
|
||||
'person_id': person_id,
|
||||
'distance': similar_face['distance'],
|
||||
'quality_score': similar_face['quality_score'],
|
||||
'adaptive_tolerance': similar_face.get('adaptive_tolerance', tolerance)
|
||||
}
|
||||
person_matches.append(match)
|
||||
|
||||
self.matches_by_matched[person_id] = person_matches
|
||||
|
||||
# Flatten all matches for counting
|
||||
all_matches = []
|
||||
for person_matches in self.matches_by_matched.values():
|
||||
all_matches.extend(person_matches)
|
||||
|
||||
if not all_matches:
|
||||
messagebox.showinfo("No Matches", "🔍 No similar faces found for auto-identification")
|
||||
return
|
||||
|
||||
# Pre-fetch all needed data
|
||||
self.data_cache = self._prefetch_auto_match_data(self.matches_by_matched)
|
||||
|
||||
# Initialize state
|
||||
self.matched_ids = [person_id for person_id, _, _ in person_faces_list
|
||||
if person_id in self.matches_by_matched and self.matches_by_matched[person_id]]
|
||||
self.filtered_matched_ids = None
|
||||
self.current_matched_index = 0
|
||||
self.identified_faces_per_person = {}
|
||||
self.checkbox_states_per_person = {}
|
||||
self.original_checkbox_states_per_person = {}
|
||||
self.identified_count = 0
|
||||
|
||||
# Check if there's only one person - disable search if so
|
||||
has_only_one_person = len(self.matched_ids) == 1
|
||||
if has_only_one_person:
|
||||
self.components['search_var'].set("")
|
||||
search_entry = None
|
||||
for widget in self.components['left_panel'].winfo_children():
|
||||
if isinstance(widget, ttk.Frame) and len(widget.winfo_children()) > 0:
|
||||
for child in widget.winfo_children():
|
||||
if isinstance(child, ttk.Entry):
|
||||
search_entry = child
|
||||
break
|
||||
if search_entry:
|
||||
search_entry.config(state='disabled')
|
||||
self.components['search_help_label'].config(text="(Search disabled - only one person found)")
|
||||
|
||||
# Enable controls
|
||||
self._update_control_states()
|
||||
|
||||
# Show the first person
|
||||
self._update_display()
|
||||
|
||||
self.is_active = True
|
||||
|
||||
def _prefetch_auto_match_data(self, matches_by_matched: Dict) -> Dict:
|
||||
"""Pre-fetch all needed data to avoid repeated database queries"""
|
||||
data_cache = {}
|
||||
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Pre-fetch all person names and details
|
||||
person_ids = list(matches_by_matched.keys())
|
||||
if person_ids:
|
||||
placeholders = ','.join('?' * len(person_ids))
|
||||
cursor.execute(f'SELECT id, first_name, last_name, middle_name, maiden_name, date_of_birth FROM people WHERE id IN ({placeholders})', person_ids)
|
||||
data_cache['person_details'] = {}
|
||||
for row in cursor.fetchall():
|
||||
person_id = row[0]
|
||||
first_name = row[1] or ''
|
||||
last_name = row[2] or ''
|
||||
middle_name = row[3] or ''
|
||||
maiden_name = row[4] or ''
|
||||
date_of_birth = row[5] or ''
|
||||
|
||||
# Create full name display
|
||||
name_parts = []
|
||||
if first_name:
|
||||
name_parts.append(first_name)
|
||||
if middle_name:
|
||||
name_parts.append(middle_name)
|
||||
if last_name:
|
||||
name_parts.append(last_name)
|
||||
if maiden_name:
|
||||
name_parts.append(f"({maiden_name})")
|
||||
|
||||
full_name = ' '.join(name_parts)
|
||||
data_cache['person_details'][person_id] = {
|
||||
'full_name': full_name,
|
||||
'first_name': first_name,
|
||||
'last_name': last_name,
|
||||
'middle_name': middle_name,
|
||||
'maiden_name': maiden_name,
|
||||
'date_of_birth': date_of_birth
|
||||
}
|
||||
|
||||
# Pre-fetch all photo paths (both matched and unidentified)
|
||||
all_photo_ids = set()
|
||||
for person_matches in matches_by_matched.values():
|
||||
for match in person_matches:
|
||||
all_photo_ids.add(match['matched_photo_id'])
|
||||
all_photo_ids.add(match['unidentified_photo_id'])
|
||||
|
||||
if all_photo_ids:
|
||||
photo_ids_list = list(all_photo_ids)
|
||||
placeholders = ','.join('?' * len(photo_ids_list))
|
||||
cursor.execute(f'SELECT id, path FROM photos WHERE id IN ({placeholders})', photo_ids_list)
|
||||
data_cache['photo_paths'] = {row[0]: row[1] for row in cursor.fetchall()}
|
||||
|
||||
return data_cache
|
||||
|
||||
def _update_display(self):
|
||||
"""Update the display for the current person"""
|
||||
active_ids = self.filtered_matched_ids if self.filtered_matched_ids is not None else self.matched_ids
|
||||
|
||||
if self.current_matched_index >= len(active_ids):
|
||||
self._finish_auto_match()
|
||||
return
|
||||
|
||||
matched_id = active_ids[self.current_matched_index]
|
||||
matches_for_this_person = self.matches_by_matched[matched_id]
|
||||
|
||||
# Update button states
|
||||
self._update_control_states()
|
||||
|
||||
# Update save button text with person name
|
||||
self._update_save_button_text()
|
||||
|
||||
# Get the first match to get matched person info
|
||||
if not matches_for_this_person:
|
||||
print(f"❌ Error: No matches found for current person {matched_id}")
|
||||
# Skip to next person if available
|
||||
if self.current_matched_index < len(active_ids) - 1:
|
||||
self.current_matched_index += 1
|
||||
self._update_display()
|
||||
else:
|
||||
self._finish_auto_match()
|
||||
return
|
||||
|
||||
first_match = matches_for_this_person[0]
|
||||
|
||||
# Use cached data instead of database queries
|
||||
person_details = self.data_cache['person_details'].get(first_match['person_id'], {})
|
||||
person_name = person_details.get('full_name', "Unknown")
|
||||
date_of_birth = person_details.get('date_of_birth', '')
|
||||
matched_photo_path = self.data_cache['photo_paths'].get(first_match['matched_photo_id'], None)
|
||||
|
||||
# Create detailed person info display
|
||||
person_info_lines = [f"👤 Person: {person_name}"]
|
||||
if date_of_birth:
|
||||
person_info_lines.append(f"📅 Born: {date_of_birth}")
|
||||
person_info_lines.extend([
|
||||
f"📁 Photo: {first_match['matched_filename']}",
|
||||
f"📍 Face location: {first_match['matched_location']}"
|
||||
])
|
||||
|
||||
# Update matched person info
|
||||
self.components['person_info_label'].config(text="\n".join(person_info_lines))
|
||||
|
||||
# Display matched person face
|
||||
self.components['person_canvas'].delete("all")
|
||||
if matched_photo_path:
|
||||
matched_crop_path = self.face_processor._extract_face_crop(
|
||||
matched_photo_path,
|
||||
first_match['matched_location'],
|
||||
f"matched_{first_match['person_id']}"
|
||||
)
|
||||
|
||||
if matched_crop_path and os.path.exists(matched_crop_path):
|
||||
try:
|
||||
pil_image = Image.open(matched_crop_path)
|
||||
pil_image.thumbnail((300, 300), Image.Resampling.LANCZOS)
|
||||
photo = ImageTk.PhotoImage(pil_image)
|
||||
self.components['person_canvas'].create_image(150, 150, image=photo)
|
||||
self.components['person_canvas'].image = photo
|
||||
|
||||
# Add photo icon to the matched person face
|
||||
actual_width, actual_height = pil_image.size
|
||||
top_left_x = 150 - (actual_width // 2)
|
||||
top_left_y = 150 - (actual_height // 2)
|
||||
self.gui_core.create_photo_icon(self.components['person_canvas'], matched_photo_path, icon_size=20,
|
||||
face_x=top_left_x, face_y=top_left_y,
|
||||
face_width=actual_width, face_height=actual_height,
|
||||
canvas_width=300, canvas_height=300)
|
||||
except Exception as e:
|
||||
self.components['person_canvas'].create_text(150, 150, text=f"❌ Could not load image: {e}", fill="red")
|
||||
else:
|
||||
self.components['person_canvas'].create_text(150, 150, text="🖼️ No face crop available", fill="gray")
|
||||
|
||||
# Clear and populate unidentified faces
|
||||
self._update_matches_display(matches_for_this_person, matched_id)
|
||||
|
||||
def _update_matches_display(self, matches_for_this_person, matched_id):
|
||||
"""Update the matches display for the current person"""
|
||||
# Clear existing matches
|
||||
self.components['matches_canvas'].delete("all")
|
||||
self.match_checkboxes = []
|
||||
self.match_vars = []
|
||||
|
||||
# Create frame for unidentified faces inside canvas
|
||||
matches_inner_frame = ttk.Frame(self.components['matches_canvas'])
|
||||
self.components['matches_canvas'].create_window((0, 0), window=matches_inner_frame, anchor="nw")
|
||||
|
||||
# Use cached photo paths
|
||||
photo_paths = self.data_cache['photo_paths']
|
||||
|
||||
# Create all checkboxes
|
||||
for i, match in enumerate(matches_for_this_person):
|
||||
# Get unidentified face info from cached data
|
||||
unidentified_photo_path = photo_paths.get(match['unidentified_photo_id'], '')
|
||||
|
||||
# Calculate confidence
|
||||
confidence_pct = (1 - match['distance']) * 100
|
||||
confidence_desc = self.face_processor._get_confidence_description(confidence_pct)
|
||||
|
||||
# Create match frame
|
||||
match_frame = ttk.Frame(matches_inner_frame)
|
||||
match_frame.grid(row=i, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
# Checkbox for this match
|
||||
match_var = tk.BooleanVar()
|
||||
|
||||
# Restore previous checkbox state if available
|
||||
unique_key = f"{matched_id}_{match['unidentified_id']}"
|
||||
if matched_id in self.checkbox_states_per_person and unique_key in self.checkbox_states_per_person[matched_id]:
|
||||
saved_state = self.checkbox_states_per_person[matched_id][unique_key]
|
||||
match_var.set(saved_state)
|
||||
# Otherwise, pre-select if this face was previously identified for this person
|
||||
elif matched_id in self.identified_faces_per_person and match['unidentified_id'] in self.identified_faces_per_person[matched_id]:
|
||||
match_var.set(True)
|
||||
|
||||
self.match_vars.append(match_var)
|
||||
|
||||
# Capture original state at render time
|
||||
if matched_id not in self.original_checkbox_states_per_person:
|
||||
self.original_checkbox_states_per_person[matched_id] = {}
|
||||
if unique_key not in self.original_checkbox_states_per_person[matched_id]:
|
||||
self.original_checkbox_states_per_person[matched_id][unique_key] = match_var.get()
|
||||
|
||||
# Add callback to save state immediately when checkbox changes
|
||||
def on_checkbox_change(var, person_id, face_id):
|
||||
unique_key = f"{person_id}_{face_id}"
|
||||
if person_id not in self.checkbox_states_per_person:
|
||||
self.checkbox_states_per_person[person_id] = {}
|
||||
|
||||
current_value = var.get()
|
||||
self.checkbox_states_per_person[person_id][unique_key] = current_value
|
||||
|
||||
# Bind the callback to the variable
|
||||
current_person_id = matched_id
|
||||
current_face_id = match['unidentified_id']
|
||||
match_var.trace('w', lambda *args, var=match_var, person_id=current_person_id, face_id=current_face_id: on_checkbox_change(var, person_id, face_id))
|
||||
|
||||
# Configure match frame for grid layout
|
||||
match_frame.columnconfigure(0, weight=0) # Checkbox column - fixed width
|
||||
match_frame.columnconfigure(1, weight=0) # Image column - fixed width
|
||||
match_frame.columnconfigure(2, weight=1) # Text column - expandable
|
||||
|
||||
# Checkbox
|
||||
checkbox = ttk.Checkbutton(match_frame, variable=match_var)
|
||||
checkbox.grid(row=0, column=0, rowspan=2, sticky=(tk.W, tk.N), padx=(0, 5))
|
||||
self.match_checkboxes.append(checkbox)
|
||||
|
||||
# Unidentified face image
|
||||
match_canvas = None
|
||||
if unidentified_photo_path:
|
||||
style = ttk.Style()
|
||||
canvas_bg_color = style.lookup('TFrame', 'background') or '#d9d9d9'
|
||||
match_canvas = tk.Canvas(match_frame, width=100, height=100, bg=canvas_bg_color, highlightthickness=0)
|
||||
match_canvas.grid(row=0, column=1, rowspan=2, sticky=(tk.W, tk.N), padx=(5, 10))
|
||||
|
||||
unidentified_crop_path = self.face_processor._extract_face_crop(
|
||||
unidentified_photo_path,
|
||||
match['unidentified_location'],
|
||||
f"unid_{match['unidentified_id']}"
|
||||
)
|
||||
|
||||
if unidentified_crop_path and os.path.exists(unidentified_crop_path):
|
||||
try:
|
||||
pil_image = Image.open(unidentified_crop_path)
|
||||
pil_image.thumbnail((100, 100), Image.Resampling.LANCZOS)
|
||||
photo = ImageTk.PhotoImage(pil_image)
|
||||
match_canvas.create_image(50, 50, image=photo)
|
||||
match_canvas.image = photo
|
||||
|
||||
# Add photo icon
|
||||
self.gui_core.create_photo_icon(match_canvas, unidentified_photo_path, icon_size=15,
|
||||
face_x=0, face_y=0,
|
||||
face_width=100, face_height=100,
|
||||
canvas_width=100, canvas_height=100)
|
||||
except Exception:
|
||||
match_canvas.create_text(50, 50, text="❌", fill="red")
|
||||
else:
|
||||
match_canvas.create_text(50, 50, text="🖼️", fill="gray")
|
||||
|
||||
# Confidence badge and filename
|
||||
info_container = ttk.Frame(match_frame)
|
||||
info_container.grid(row=0, column=2, rowspan=2, sticky=(tk.W, tk.E))
|
||||
|
||||
badge = self.gui_core.create_confidence_badge(info_container, confidence_pct)
|
||||
badge.pack(anchor=tk.W)
|
||||
|
||||
filename_label = ttk.Label(info_container, text=f"📁 {match['unidentified_filename']}",
|
||||
font=("Arial", 8), foreground="gray")
|
||||
filename_label.pack(anchor=tk.W, pady=(2, 0))
|
||||
|
||||
# Update Select All / Clear All button states
|
||||
self._update_match_control_buttons_state()
|
||||
|
||||
# Update scroll region
|
||||
self.components['matches_canvas'].update_idletasks()
|
||||
self.components['matches_canvas'].configure(scrollregion=self.components['matches_canvas'].bbox("all"))
|
||||
|
||||
def _update_control_states(self):
|
||||
"""Update control button states based on current position"""
|
||||
active_ids = self.filtered_matched_ids if self.filtered_matched_ids is not None else self.matched_ids
|
||||
|
||||
# Enable/disable Back button
|
||||
if self.current_matched_index > 0:
|
||||
self.components['back_btn'].config(state='normal')
|
||||
else:
|
||||
self.components['back_btn'].config(state='disabled')
|
||||
|
||||
# Enable/disable Next button
|
||||
if self.current_matched_index < len(active_ids) - 1:
|
||||
self.components['next_btn'].config(state='normal')
|
||||
else:
|
||||
self.components['next_btn'].config(state='disabled')
|
||||
|
||||
# Enable save button if we have matches
|
||||
if active_ids and self.current_matched_index < len(active_ids):
|
||||
self.components['save_btn'].config(state='normal')
|
||||
else:
|
||||
self.components['save_btn'].config(state='disabled')
|
||||
|
||||
def _update_save_button_text(self):
|
||||
"""Update save button text with current person name"""
|
||||
active_ids = self.filtered_matched_ids if self.filtered_matched_ids is not None else self.matched_ids
|
||||
if self.current_matched_index < len(active_ids):
|
||||
matched_id = active_ids[self.current_matched_index]
|
||||
matches_for_current_person = self.matches_by_matched[matched_id]
|
||||
if matches_for_current_person:
|
||||
person_id = matches_for_current_person[0]['person_id']
|
||||
person_details = self.data_cache['person_details'].get(person_id, {})
|
||||
person_name = person_details.get('full_name', "Unknown")
|
||||
self.components['save_btn'].config(text=f"💾 Save changes for {person_name}")
|
||||
else:
|
||||
self.components['save_btn'].config(text="💾 Save Changes")
|
||||
else:
|
||||
self.components['save_btn'].config(text="💾 Save Changes")
|
||||
|
||||
def _update_match_control_buttons_state(self):
|
||||
"""Enable/disable Select All / Clear All based on matches presence"""
|
||||
if hasattr(self, 'match_vars') and self.match_vars:
|
||||
self.components['select_all_btn'].config(state='normal')
|
||||
self.components['clear_all_btn'].config(state='normal')
|
||||
else:
|
||||
self.components['select_all_btn'].config(state='disabled')
|
||||
self.components['clear_all_btn'].config(state='disabled')
|
||||
|
||||
def _select_all_matches(self):
|
||||
"""Select all match checkboxes"""
|
||||
if hasattr(self, 'match_vars'):
|
||||
for var in self.match_vars:
|
||||
var.set(True)
|
||||
|
||||
def _clear_all_matches(self):
|
||||
"""Clear all match checkboxes"""
|
||||
if hasattr(self, 'match_vars'):
|
||||
for var in self.match_vars:
|
||||
var.set(False)
|
||||
|
||||
def _save_changes(self):
|
||||
"""Save changes for the current person"""
|
||||
active_ids = self.filtered_matched_ids if self.filtered_matched_ids is not None else self.matched_ids
|
||||
if self.current_matched_index < len(active_ids):
|
||||
matched_id = active_ids[self.current_matched_index]
|
||||
matches_for_this_person = self.matches_by_matched[matched_id]
|
||||
|
||||
# Initialize identified faces for this person if not exists
|
||||
if matched_id not in self.identified_faces_per_person:
|
||||
self.identified_faces_per_person[matched_id] = set()
|
||||
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Process all matches (both checked and unchecked)
|
||||
for i, (match, var) in enumerate(zip(matches_for_this_person, self.match_vars)):
|
||||
if var.get():
|
||||
# Face is checked - assign to person
|
||||
cursor.execute(
|
||||
'UPDATE faces SET person_id = ? WHERE id = ?',
|
||||
(match['person_id'], match['unidentified_id'])
|
||||
)
|
||||
|
||||
# Use cached person name
|
||||
person_details = self.data_cache['person_details'].get(match['person_id'], {})
|
||||
person_name = person_details.get('full_name', "Unknown")
|
||||
|
||||
# Track this face as identified for this person
|
||||
self.identified_faces_per_person[matched_id].add(match['unidentified_id'])
|
||||
|
||||
print(f"✅ Identified as: {person_name}")
|
||||
self.identified_count += 1
|
||||
else:
|
||||
# Face is unchecked - check if it was previously identified for this person
|
||||
if match['unidentified_id'] in self.identified_faces_per_person[matched_id]:
|
||||
# This face was previously identified for this person, now unchecking it
|
||||
cursor.execute(
|
||||
'UPDATE faces SET person_id = NULL WHERE id = ?',
|
||||
(match['unidentified_id'],)
|
||||
)
|
||||
|
||||
# Remove from identified faces for this person
|
||||
self.identified_faces_per_person[matched_id].discard(match['unidentified_id'])
|
||||
|
||||
print(f"❌ Unidentified: {match['unidentified_filename']}")
|
||||
|
||||
# Update person encodings for all affected persons
|
||||
for person_id in set(match['person_id'] for match in matches_for_this_person if match['person_id']):
|
||||
self.face_processor.update_person_encodings(person_id)
|
||||
|
||||
conn.commit()
|
||||
|
||||
# After saving, set original states to the current UI states
|
||||
current_snapshot = {}
|
||||
for match, var in zip(matches_for_this_person, self.match_vars):
|
||||
unique_key = f"{matched_id}_{match['unidentified_id']}"
|
||||
current_snapshot[unique_key] = var.get()
|
||||
self.checkbox_states_per_person[matched_id] = dict(current_snapshot)
|
||||
self.original_checkbox_states_per_person[matched_id] = dict(current_snapshot)
|
||||
|
||||
def _go_back(self):
|
||||
"""Go back to the previous person"""
|
||||
if self.current_matched_index > 0:
|
||||
self.current_matched_index -= 1
|
||||
self._update_display()
|
||||
|
||||
def _go_next(self):
|
||||
"""Go to the next person"""
|
||||
active_ids = self.filtered_matched_ids if self.filtered_matched_ids is not None else self.matched_ids
|
||||
if self.current_matched_index < len(active_ids) - 1:
|
||||
self.current_matched_index += 1
|
||||
self._update_display()
|
||||
else:
|
||||
self._finish_auto_match()
|
||||
|
||||
def _apply_search_filter(self):
|
||||
"""Filter people by last name and update navigation"""
|
||||
query = self.components['search_var'].get().strip().lower()
|
||||
if query:
|
||||
# Filter person_faces_list by last name
|
||||
filtered_people = []
|
||||
for person_id in self.matched_ids:
|
||||
# Get person name from cache
|
||||
person_details = self.data_cache['person_details'].get(person_id, {})
|
||||
person_name = person_details.get('full_name', '')
|
||||
|
||||
# Extract last name from person_name
|
||||
if ',' in person_name:
|
||||
last_name = person_name.split(',')[0].strip().lower()
|
||||
else:
|
||||
# Try to extract last name from full name
|
||||
name_parts = person_name.strip().split()
|
||||
if name_parts:
|
||||
last_name = name_parts[-1].lower()
|
||||
else:
|
||||
last_name = ''
|
||||
|
||||
if query in last_name:
|
||||
filtered_people.append(person_id)
|
||||
|
||||
self.filtered_matched_ids = filtered_people
|
||||
else:
|
||||
self.filtered_matched_ids = None
|
||||
|
||||
# Reset to first person in filtered list
|
||||
self.current_matched_index = 0
|
||||
if self.filtered_matched_ids:
|
||||
self._update_display()
|
||||
else:
|
||||
# No matches - clear display
|
||||
self.components['person_info_label'].config(text="No people match filter")
|
||||
self.components['person_canvas'].delete("all")
|
||||
self.components['person_canvas'].create_text(150, 150, text="No matches found", fill="gray")
|
||||
self.components['matches_canvas'].delete("all")
|
||||
self._update_control_states()
|
||||
|
||||
def _clear_search_filter(self):
|
||||
"""Clear filter and show all people"""
|
||||
self.components['search_var'].set("")
|
||||
self.filtered_matched_ids = None
|
||||
self.current_matched_index = 0
|
||||
self._update_display()
|
||||
|
||||
def _finish_auto_match(self):
|
||||
"""Finish the auto-match process"""
|
||||
print(f"\n✅ Auto-identified {self.identified_count} faces")
|
||||
messagebox.showinfo("Auto-Match Complete", f"Auto-identified {self.identified_count} faces")
|
||||
self._cleanup()
|
||||
|
||||
def _quit_auto_match(self):
|
||||
"""Quit the auto-match process"""
|
||||
# Check for unsaved changes before quitting
|
||||
if self._has_unsaved_changes():
|
||||
result = messagebox.askyesnocancel(
|
||||
"Unsaved Changes",
|
||||
"You have unsaved changes that will be lost if you quit.\n\n"
|
||||
"Yes: Save current changes and quit\n"
|
||||
"No: Quit without saving\n"
|
||||
"Cancel: Return to auto-match"
|
||||
)
|
||||
if result is None:
|
||||
# Cancel
|
||||
return
|
||||
if result:
|
||||
# Save current person's changes, then quit
|
||||
self._save_changes()
|
||||
|
||||
self._cleanup()
|
||||
|
||||
def _has_unsaved_changes(self):
|
||||
"""Check if there are any unsaved changes"""
|
||||
for person_id, current_states in self.checkbox_states_per_person.items():
|
||||
if person_id in self.original_checkbox_states_per_person:
|
||||
original_states = self.original_checkbox_states_per_person[person_id]
|
||||
# Check if any checkbox state differs from its original state
|
||||
for key, current_value in current_states.items():
|
||||
if key not in original_states or original_states[key] != current_value:
|
||||
return True
|
||||
else:
|
||||
# If person has current states but no original states, there are changes
|
||||
if any(current_states.values()):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up resources and reset state"""
|
||||
# Clean up face crops
|
||||
self.face_processor.cleanup_face_crops()
|
||||
|
||||
# Reset state
|
||||
self.matches_by_matched = {}
|
||||
self.data_cache = {}
|
||||
self.current_matched_index = 0
|
||||
self.matched_ids = []
|
||||
self.filtered_matched_ids = None
|
||||
self.identified_faces_per_person = {}
|
||||
self.checkbox_states_per_person = {}
|
||||
self.original_checkbox_states_per_person = {}
|
||||
self.identified_count = 0
|
||||
|
||||
# Clear displays
|
||||
self.components['person_info_label'].config(text="")
|
||||
self.components['person_canvas'].delete("all")
|
||||
self.components['matches_canvas'].delete("all")
|
||||
|
||||
# Disable controls
|
||||
self.components['back_btn'].config(state='disabled')
|
||||
self.components['next_btn'].config(state='disabled')
|
||||
self.components['save_btn'].config(state='disabled')
|
||||
self.components['select_all_btn'].config(state='disabled')
|
||||
self.components['clear_all_btn'].config(state='disabled')
|
||||
|
||||
# Clear search
|
||||
self.components['search_var'].set("")
|
||||
self.components['search_help_label'].config(text="Type Last Name")
|
||||
|
||||
# Re-enable search entry
|
||||
search_entry = None
|
||||
for widget in self.components['left_panel'].winfo_children():
|
||||
if isinstance(widget, ttk.Frame) and len(widget.winfo_children()) > 0:
|
||||
for child in widget.winfo_children():
|
||||
if isinstance(child, ttk.Entry):
|
||||
search_entry = child
|
||||
break
|
||||
if search_entry:
|
||||
search_entry.config(state='normal')
|
||||
|
||||
self.is_active = False
|
||||
|
||||
def activate(self):
|
||||
"""Activate the panel"""
|
||||
self.is_active = True
|
||||
|
||||
def deactivate(self):
|
||||
"""Deactivate the panel"""
|
||||
if self.is_active:
|
||||
self._cleanup()
|
||||
self.is_active = False
|
||||
1448
dashboard_gui.py
1448
dashboard_gui.py
File diff suppressed because it is too large
Load Diff
@ -308,7 +308,7 @@ class FaceProcessor:
|
||||
elif confidence_pct >= 50:
|
||||
return "🔴 (Low - Questionable)"
|
||||
else:
|
||||
return "⚫ (Very Low - Unlikely)"
|
||||
return "⚫ (Very Low)"
|
||||
|
||||
def _calculate_adaptive_tolerance(self, base_tolerance: float, face_quality: float, match_confidence: float = None) -> float:
|
||||
"""Calculate adaptive tolerance based on face quality and match confidence"""
|
||||
@ -637,7 +637,7 @@ class FaceProcessor:
|
||||
elif confidence_pct >= 50:
|
||||
return "🔴 (Low - Questionable)"
|
||||
else:
|
||||
return "⚫ (Very Low - Unlikely)"
|
||||
return "⚫ (Very Low)"
|
||||
|
||||
def _display_similar_faces_in_panel(self, parent_frame, similar_faces_data, face_vars, face_images, face_crops, current_face_id=None, face_selection_states=None, data_cache=None):
|
||||
"""Display similar faces in a panel - reuses auto-match display logic"""
|
||||
|
||||
@ -328,7 +328,7 @@ class GUICore:
|
||||
elif confidence_pct >= 50:
|
||||
return "🔴 (Low - Questionable)"
|
||||
else:
|
||||
return "⚫ (Very Low - Unlikely)"
|
||||
return "⚫ (Very Low)"
|
||||
|
||||
def center_window(self, root, width: int = None, height: int = None):
|
||||
"""Center a window on the screen"""
|
||||
|
||||
@ -268,12 +268,16 @@ class IdentifyPanel:
|
||||
"""Create the left panel content for face identification"""
|
||||
left_panel = self.components['left_panel']
|
||||
|
||||
# Face image display - larger for full screen
|
||||
self.components['face_canvas'] = tk.Canvas(left_panel, width=400, height=400, bg='white', relief='sunken', bd=2)
|
||||
# Create a main content frame that can expand
|
||||
main_content_frame = ttk.Frame(left_panel)
|
||||
main_content_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||
|
||||
# Face image display - flexible height for better layout
|
||||
self.components['face_canvas'] = tk.Canvas(main_content_frame, width=400, height=400, bg='white', relief='sunken', bd=2)
|
||||
self.components['face_canvas'].pack(pady=(0, 15))
|
||||
|
||||
# Person name fields
|
||||
name_frame = ttk.LabelFrame(left_panel, text="Person Information", padding="5")
|
||||
name_frame = ttk.LabelFrame(main_content_frame, text="Person Information", padding="5")
|
||||
name_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# First name
|
||||
@ -324,7 +328,7 @@ class IdentifyPanel:
|
||||
self._setup_last_name_autocomplete(last_name_entry)
|
||||
|
||||
# Control buttons
|
||||
button_frame = ttk.Frame(left_panel)
|
||||
button_frame = ttk.Frame(main_content_frame)
|
||||
button_frame.pack(fill=tk.X, pady=(10, 0))
|
||||
|
||||
self.components['identify_btn'] = ttk.Button(button_frame, text="✅ Identify", command=self._identify_face, state='disabled')
|
||||
@ -336,7 +340,7 @@ class IdentifyPanel:
|
||||
self.components['next_btn'] = ttk.Button(button_frame, text="➡️ Next", command=self._go_next)
|
||||
self.components['next_btn'].pack(side=tk.LEFT, padx=(0, 5))
|
||||
|
||||
self.components['quit_btn'] = ttk.Button(button_frame, text="❌ Quit", command=self._quit_identification)
|
||||
self.components['quit_btn'] = ttk.Button(button_frame, text="❌ Exit Identify Faces", command=self._quit_identification)
|
||||
self.components['quit_btn'].pack(side=tk.RIGHT)
|
||||
|
||||
def _create_right_panel_content(self):
|
||||
@ -374,6 +378,12 @@ class IdentifyPanel:
|
||||
self.components['similar_scrollable_frame'] = ttk.Frame(similar_canvas)
|
||||
similar_canvas.create_window((0, 0), window=self.components['similar_scrollable_frame'], anchor='nw')
|
||||
|
||||
# Configure scrollable frame to expand with canvas
|
||||
def configure_scroll_region(event):
|
||||
similar_canvas.configure(scrollregion=similar_canvas.bbox("all"))
|
||||
|
||||
self.components['similar_scrollable_frame'].bind('<Configure>', configure_scroll_region)
|
||||
|
||||
# Store canvas reference for scrolling
|
||||
self.components['similar_canvas'] = similar_canvas
|
||||
|
||||
@ -1042,7 +1052,7 @@ class IdentifyPanel:
|
||||
elif confidence_pct >= 50:
|
||||
return "(Low)"
|
||||
else:
|
||||
return "(Very Low - Unlikely)"
|
||||
return "(Very Low)"
|
||||
|
||||
def _identify_face(self):
|
||||
"""Identify the current face"""
|
||||
|
||||
@ -50,7 +50,7 @@ class PhotoTagger:
|
||||
self.modify_identified_gui = ModifyIdentifiedGUI(self.db, self.face_processor, verbose)
|
||||
self.tag_manager_gui = TagManagerGUI(self.db, self.gui_core, self.tag_manager, self.face_processor, verbose)
|
||||
self.search_gui = SearchGUI(self.db, self.search_stats, self.gui_core, self.tag_manager, verbose)
|
||||
self.dashboard_gui = DashboardGUI(self.gui_core, self.db, self.face_processor, on_scan=self._dashboard_scan, on_process=self._dashboard_process, on_identify=self._dashboard_identify)
|
||||
self.dashboard_gui = DashboardGUI(self.gui_core, self.db, self.face_processor, on_scan=self._dashboard_scan, on_process=self._dashboard_process, on_identify=self._dashboard_identify, search_stats=self.search_stats, tag_manager=self.tag_manager)
|
||||
|
||||
# Legacy compatibility - expose some methods directly
|
||||
self._db_connection = None
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user