feat: Complete migration to DeepFace with full integration and testing
This commit finalizes the migration from face_recognition to DeepFace across all phases. It includes updates to the database schema, core processing, GUI integration, and comprehensive testing. All features are now powered by DeepFace technology, providing superior accuracy and enhanced metadata handling. The README and documentation have been updated to reflect these changes, ensuring clarity on the new capabilities and production readiness of the PunimTag system. All tests are passing, confirming the successful integration.
This commit is contained in:
@@ -212,7 +212,8 @@ class AutoMatchPanel:
|
||||
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
|
||||
SELECT f.id, f.person_id, f.photo_id, f.location, p.filename, f.quality_score,
|
||||
f.face_confidence, f.detector_backend, f.model_name
|
||||
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
|
||||
|
||||
@@ -5,12 +5,17 @@ Designed with web migration in mind - single window with menu bar and content ar
|
||||
"""
|
||||
|
||||
import os
|
||||
import warnings
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from typing import Dict, Optional, Callable
|
||||
|
||||
# Suppress TensorFlow warnings (must be before DeepFace import)
|
||||
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
from src.gui.gui_core import GUICore
|
||||
from src.gui.identify_panel import IdentifyPanel
|
||||
from src.gui.modify_panel import ModifyPanel
|
||||
@@ -1669,6 +1674,9 @@ class DashboardGUI:
|
||||
|
||||
def _create_process_panel(self) -> ttk.Frame:
|
||||
"""Create the process panel (migrated from original dashboard)"""
|
||||
from src.core.config import DEEPFACE_DETECTOR_OPTIONS, DEEPFACE_MODEL_OPTIONS
|
||||
from src.core.config import DEEPFACE_DETECTOR_BACKEND, DEEPFACE_MODEL_NAME
|
||||
|
||||
panel = ttk.Frame(self.content_frame)
|
||||
|
||||
# Configure panel grid for responsiveness
|
||||
@@ -1684,9 +1692,34 @@ class DashboardGUI:
|
||||
form_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 20))
|
||||
form_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# DeepFace Settings Section
|
||||
deepface_frame = ttk.LabelFrame(form_frame, text="DeepFace Settings", padding="15")
|
||||
deepface_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15))
|
||||
deepface_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Detector Backend Selection
|
||||
tk.Label(deepface_frame, text="Face Detector:", font=("Arial", 11)).grid(row=0, column=0, sticky=tk.W, pady=(0, 10))
|
||||
self.detector_var = tk.StringVar(value=DEEPFACE_DETECTOR_BACKEND)
|
||||
detector_combo = ttk.Combobox(deepface_frame, textvariable=self.detector_var,
|
||||
values=DEEPFACE_DETECTOR_OPTIONS,
|
||||
state="readonly", width=12, font=("Arial", 10))
|
||||
detector_combo.grid(row=0, column=1, sticky=tk.W, padx=(10, 0), pady=(0, 10))
|
||||
tk.Label(deepface_frame, text="(RetinaFace recommended for accuracy)",
|
||||
font=("Arial", 9), fg="gray").grid(row=0, column=2, sticky=tk.W, padx=(10, 0), pady=(0, 10))
|
||||
|
||||
# Model Selection
|
||||
tk.Label(deepface_frame, text="Recognition Model:", font=("Arial", 11)).grid(row=1, column=0, sticky=tk.W)
|
||||
self.model_var = tk.StringVar(value=DEEPFACE_MODEL_NAME)
|
||||
model_combo = ttk.Combobox(deepface_frame, textvariable=self.model_var,
|
||||
values=DEEPFACE_MODEL_OPTIONS,
|
||||
state="readonly", width=12, font=("Arial", 10))
|
||||
model_combo.grid(row=1, column=1, sticky=tk.W, padx=(10, 0))
|
||||
tk.Label(deepface_frame, text="(ArcFace provides best accuracy)",
|
||||
font=("Arial", 9), fg="gray").grid(row=1, column=2, sticky=tk.W, padx=(10, 0))
|
||||
|
||||
# Limit option
|
||||
limit_frame = ttk.Frame(form_frame)
|
||||
limit_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 15))
|
||||
limit_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 15))
|
||||
|
||||
self.limit_enabled = tk.BooleanVar(value=False)
|
||||
limit_check = tk.Checkbutton(limit_frame, text="Limit processing to", variable=self.limit_enabled, font=("Arial", 11))
|
||||
@@ -1700,23 +1733,23 @@ class DashboardGUI:
|
||||
|
||||
# Action button
|
||||
self.process_btn = ttk.Button(form_frame, text="🚀 Start Processing", command=self._run_process)
|
||||
self.process_btn.grid(row=1, column=0, sticky=tk.W, pady=(20, 0))
|
||||
self.process_btn.grid(row=2, column=0, sticky=tk.W, pady=(20, 0))
|
||||
|
||||
# Cancel button (initially hidden/disabled)
|
||||
self.cancel_btn = tk.Button(form_frame, text="✖ Cancel", command=self._cancel_process, state="disabled")
|
||||
self.cancel_btn.grid(row=1, column=0, sticky=tk.E, pady=(20, 0))
|
||||
self.cancel_btn.grid(row=2, column=0, sticky=tk.E, pady=(20, 0))
|
||||
|
||||
# Progress bar
|
||||
self.progress_var = tk.DoubleVar()
|
||||
self.progress_bar = ttk.Progressbar(form_frame, variable=self.progress_var,
|
||||
maximum=100, length=400, mode='determinate')
|
||||
self.progress_bar.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(15, 0))
|
||||
self.progress_bar.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(15, 0))
|
||||
|
||||
# Progress status label
|
||||
self.progress_status_var = tk.StringVar(value="Ready to process")
|
||||
progress_status_label = tk.Label(form_frame, textvariable=self.progress_status_var,
|
||||
font=("Arial", 11), fg="gray")
|
||||
progress_status_label.grid(row=3, column=0, sticky=tk.W, pady=(5, 0))
|
||||
progress_status_label.grid(row=4, column=0, sticky=tk.W, pady=(5, 0))
|
||||
|
||||
return panel
|
||||
|
||||
@@ -2011,8 +2044,15 @@ class DashboardGUI:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Run the actual processing with real progress updates and stop event
|
||||
result = self.on_process(limit_value, progress_callback, self._process_stop_event)
|
||||
# Get selected detector and model settings
|
||||
detector = getattr(self, 'detector_var', None)
|
||||
model = getattr(self, 'model_var', None)
|
||||
detector_backend = detector.get() if detector else None
|
||||
model_name = model.get() if model else None
|
||||
|
||||
# Run the actual processing with real progress updates, stop event, and DeepFace settings
|
||||
result = self.on_process(limit_value, self._process_stop_event, progress_callback,
|
||||
detector_backend, model_name)
|
||||
|
||||
# Ensure progress reaches 100% at the end
|
||||
self.progress_var.set(100)
|
||||
|
||||
+19
-10
@@ -196,7 +196,7 @@ class IdentifyPanel:
|
||||
|
||||
# Update similar faces if compare is enabled
|
||||
if self.components['compare_var'].get():
|
||||
face_id, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
face_id, _, _, _, _, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
self._update_similar_faces(face_id)
|
||||
|
||||
self.components['unique_check'] = ttk.Checkbutton(self.main_frame, text="Unique faces only",
|
||||
@@ -213,7 +213,7 @@ class IdentifyPanel:
|
||||
self.components['clear_all_btn'].config(state='normal')
|
||||
# Update similar faces if we have a current face
|
||||
if self.current_faces and self.current_face_index < len(self.current_faces):
|
||||
face_id, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
face_id, _, _, _, _, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
self._update_similar_faces(face_id)
|
||||
else:
|
||||
# Disable select all/clear all buttons
|
||||
@@ -441,8 +441,10 @@ class IdentifyPanel:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build the SQL query with optional date filtering
|
||||
# Include DeepFace metadata: face_confidence, quality_score, detector_backend, model_name
|
||||
query = '''
|
||||
SELECT f.id, f.photo_id, p.path, p.filename, f.location
|
||||
SELECT f.id, f.photo_id, p.path, p.filename, f.location,
|
||||
f.face_confidence, f.quality_score, f.detector_backend, f.model_name
|
||||
FROM faces f
|
||||
JOIN photos p ON f.photo_id = p.id
|
||||
WHERE f.person_id IS NULL
|
||||
@@ -599,10 +601,17 @@ class IdentifyPanel:
|
||||
if not self.current_faces or self.current_face_index >= len(self.current_faces):
|
||||
return
|
||||
|
||||
face_id, photo_id, photo_path, filename, location = self.current_faces[self.current_face_index]
|
||||
face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model = self.current_faces[self.current_face_index]
|
||||
|
||||
# Update info label
|
||||
self.components['info_label'].config(text=f"Face {self.current_face_index + 1} of {len(self.current_faces)} - {filename}")
|
||||
# Update info label with DeepFace metadata
|
||||
info_text = f"Face {self.current_face_index + 1} of {len(self.current_faces)} - {filename}"
|
||||
if face_conf is not None and face_conf > 0:
|
||||
info_text += f" | Detection: {face_conf*100:.1f}%"
|
||||
if quality is not None:
|
||||
info_text += f" | Quality: {quality*100:.0f}%"
|
||||
if detector:
|
||||
info_text += f" | {detector}/{model}" if model else f" | {detector}"
|
||||
self.components['info_label'].config(text=info_text)
|
||||
|
||||
# Extract and display face crop (show_faces is always True)
|
||||
face_crop_path = self.face_processor._extract_face_crop(photo_path, location, face_id)
|
||||
@@ -1068,7 +1077,7 @@ class IdentifyPanel:
|
||||
if not self.current_faces or self.current_face_index >= len(self.current_faces):
|
||||
return
|
||||
|
||||
face_id, photo_id, photo_path, filename, location = self.current_faces[self.current_face_index]
|
||||
face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model = self.current_faces[self.current_face_index]
|
||||
|
||||
# Get person data
|
||||
person_data = {
|
||||
@@ -1158,7 +1167,7 @@ class IdentifyPanel:
|
||||
elif validation_result == 'save_and_continue':
|
||||
# Save the current identification before proceeding
|
||||
if self.current_faces and self.current_face_index < len(self.current_faces):
|
||||
face_id, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
face_id, _, _, _, _, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
first_name = self.components['first_name_var'].get().strip()
|
||||
last_name = self.components['last_name_var'].get().strip()
|
||||
date_of_birth = self.components['date_of_birth_var'].get().strip()
|
||||
@@ -1190,7 +1199,7 @@ class IdentifyPanel:
|
||||
elif validation_result == 'save_and_continue':
|
||||
# Save the current identification before proceeding
|
||||
if self.current_faces and self.current_face_index < len(self.current_faces):
|
||||
face_id, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
face_id, _, _, _, _, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
first_name = self.components['first_name_var'].get().strip()
|
||||
last_name = self.components['last_name_var'].get().strip()
|
||||
date_of_birth = self.components['date_of_birth_var'].get().strip()
|
||||
@@ -1264,7 +1273,7 @@ class IdentifyPanel:
|
||||
elif validation_result == 'save_and_continue':
|
||||
# Save the current identification before proceeding
|
||||
if self.current_faces and self.current_face_index < len(self.current_faces):
|
||||
face_id, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
face_id, _, _, _, _, _, _, _, _ = self.current_faces[self.current_face_index]
|
||||
first_name = self.components['first_name_var'].get().strip()
|
||||
last_name = self.components['last_name_var'].get().strip()
|
||||
date_of_birth = self.components['date_of_birth_var'].get().strip()
|
||||
|
||||
@@ -479,7 +479,8 @@ class ModifyPanel:
|
||||
with self.db.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT f.id, f.photo_id, p.path, p.filename, f.location
|
||||
SELECT f.id, f.photo_id, p.path, p.filename, f.location,
|
||||
f.face_confidence, f.quality_score, f.detector_backend, f.model_name
|
||||
FROM faces f
|
||||
JOIN photos p ON f.photo_id = p.id
|
||||
WHERE f.person_id = ?
|
||||
@@ -527,7 +528,7 @@ class ModifyPanel:
|
||||
# Clear existing images
|
||||
self.right_panel_images.clear()
|
||||
|
||||
for i, (face_id, photo_id, photo_path, filename, location) in enumerate(faces):
|
||||
for i, (face_id, photo_id, photo_path, filename, location, face_conf, quality, detector, model) in enumerate(faces):
|
||||
row = i // faces_per_row
|
||||
col = i % faces_per_row
|
||||
|
||||
|
||||
@@ -1483,6 +1483,10 @@ class TagManagerPanel:
|
||||
def activate(self):
|
||||
"""Activate the panel"""
|
||||
self.is_active = True
|
||||
# Reload photos data when activating the panel
|
||||
self._load_existing_tags()
|
||||
self._load_photos()
|
||||
self._switch_view_mode(self.view_mode_var.get())
|
||||
# Rebind mousewheel scrolling when activated
|
||||
self._bind_mousewheel_scrolling()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user