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:
tanyar09
2025-10-16 13:17:41 -04:00
parent d300eb1122
commit ef7a296a9b
28 changed files with 5665 additions and 124 deletions
+2 -1
View File
@@ -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
+47 -7
View File
@@ -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
View File
@@ -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()
+3 -2
View File
@@ -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
+4
View File
@@ -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()