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
+24 -3
View File
@@ -3,14 +3,35 @@
Configuration constants and settings for PunimTag
"""
import os
import warnings
# Suppress TensorFlow warnings (must be before DeepFace import)
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
warnings.filterwarnings('ignore')
# Default file paths
DEFAULT_DB_PATH = "data/photos.db"
DEFAULT_CONFIG_FILE = "gui_config.json"
DEFAULT_WINDOW_SIZE = "600x500"
# Face detection settings
DEFAULT_FACE_DETECTION_MODEL = "hog"
DEFAULT_FACE_TOLERANCE = 0.6
# DeepFace Settings
DEEPFACE_DETECTOR_BACKEND = "retinaface" # Options: retinaface, mtcnn, opencv, ssd
DEEPFACE_MODEL_NAME = "ArcFace" # Best accuracy model
DEEPFACE_DISTANCE_METRIC = "cosine" # For similarity calculation
DEEPFACE_ENFORCE_DETECTION = False # Don't fail if no faces found
DEEPFACE_ALIGN_FACES = True # Face alignment for better accuracy
# DeepFace Options for GUI
DEEPFACE_DETECTOR_OPTIONS = ["retinaface", "mtcnn", "opencv", "ssd"]
DEEPFACE_MODEL_OPTIONS = ["ArcFace", "Facenet", "Facenet512", "VGG-Face"]
# Face tolerance/threshold settings (adjusted for DeepFace)
DEFAULT_FACE_TOLERANCE = 0.4 # Lower for DeepFace (was 0.6 for face_recognition)
DEEPFACE_SIMILARITY_THRESHOLD = 60 # Minimum similarity percentage (0-100)
# Legacy settings (kept for compatibility until Phase 3 migration)
DEFAULT_FACE_DETECTION_MODEL = "hog" # Legacy - will be replaced by DEEPFACE_DETECTOR_BACKEND
DEFAULT_BATCH_SIZE = 20
DEFAULT_PROCESSING_LIMIT = 50
+50 -12
View File
@@ -74,7 +74,7 @@ class DatabaseManager:
)
''')
# Faces table
# Faces table (updated for DeepFace)
cursor.execute('''
CREATE TABLE IF NOT EXISTS faces (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -85,12 +85,15 @@ class DatabaseManager:
confidence REAL DEFAULT 0.0,
quality_score REAL DEFAULT 0.0,
is_primary_encoding BOOLEAN DEFAULT 0,
detector_backend TEXT DEFAULT 'retinaface',
model_name TEXT DEFAULT 'ArcFace',
face_confidence REAL DEFAULT 0.0,
FOREIGN KEY (photo_id) REFERENCES photos (id),
FOREIGN KEY (person_id) REFERENCES people (id)
)
''')
# Person encodings table for multiple encodings per person
# Person encodings table for multiple encodings per person (updated for DeepFace)
cursor.execute('''
CREATE TABLE IF NOT EXISTS person_encodings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -98,6 +101,8 @@ class DatabaseManager:
face_id INTEGER NOT NULL,
encoding BLOB NOT NULL,
quality_score REAL DEFAULT 0.0,
detector_backend TEXT DEFAULT 'retinaface',
model_name TEXT DEFAULT 'ArcFace',
created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (person_id) REFERENCES people (id),
FOREIGN KEY (face_id) REFERENCES faces (id)
@@ -223,14 +228,34 @@ class DatabaseManager:
cursor.execute('UPDATE photos SET processed = 1 WHERE id = ?', (photo_id,))
def add_face(self, photo_id: int, encoding: bytes, location: str, confidence: float = 0.0,
quality_score: float = 0.0, person_id: Optional[int] = None) -> int:
"""Add a face to the database and return its ID"""
quality_score: float = 0.0, person_id: Optional[int] = None,
detector_backend: str = 'retinaface',
model_name: str = 'ArcFace',
face_confidence: float = 0.0) -> int:
"""Add a face to the database and return its ID
Args:
photo_id: ID of the photo containing the face
encoding: Face encoding as bytes (512 floats for ArcFace = 4096 bytes)
location: Face location as string (DeepFace format: "{'x': x, 'y': y, 'w': w, 'h': h}")
confidence: Legacy confidence value (kept for compatibility)
quality_score: Quality score 0.0-1.0
person_id: ID of identified person (None if unidentified)
detector_backend: DeepFace detector used (retinaface, mtcnn, opencv, ssd)
model_name: DeepFace model used (ArcFace, Facenet, etc.)
face_confidence: Confidence from DeepFace detector
Returns:
Face ID
"""
with self.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO faces (photo_id, person_id, encoding, location, confidence, quality_score)
VALUES (?, ?, ?, ?, ?, ?)
''', (photo_id, person_id, encoding, location, confidence, quality_score))
INSERT INTO faces (photo_id, person_id, encoding, location, confidence,
quality_score, detector_backend, model_name, face_confidence)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (photo_id, person_id, encoding, location, confidence, quality_score,
detector_backend, model_name, face_confidence))
return cursor.lastrowid
def update_face_person(self, face_id: int, person_id: Optional[int]):
@@ -394,14 +419,27 @@ class DatabaseManager:
''', (person_id, min_quality))
return cursor.fetchall()
def add_person_encoding(self, person_id: int, face_id: int, encoding: bytes, quality_score: float):
"""Add a person encoding"""
def add_person_encoding(self, person_id: int, face_id: int, encoding: bytes,
quality_score: float,
detector_backend: str = 'retinaface',
model_name: str = 'ArcFace'):
"""Add a person encoding
Args:
person_id: ID of the person
face_id: ID of the face this encoding came from
encoding: Face encoding as bytes
quality_score: Quality score 0.0-1.0
detector_backend: DeepFace detector used
model_name: DeepFace model used
"""
with self.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO person_encodings (person_id, face_id, encoding, quality_score)
VALUES (?, ?, ?, ?)
''', (person_id, face_id, encoding, quality_score))
INSERT INTO person_encodings (person_id, face_id, encoding, quality_score,
detector_backend, model_name)
VALUES (?, ?, ?, ?, ?, ?)
''', (person_id, face_id, encoding, quality_score, detector_backend, model_name))
def update_person_encodings(self, person_id: int):
"""Update person encodings by removing old ones and adding current face encodings"""
+181 -47
View File
@@ -6,24 +6,56 @@ Face detection, encoding, and matching functionality for PunimTag
import os
import tempfile
import numpy as np
import face_recognition
from PIL import Image, ImageDraw, ImageFont
from typing import List, Dict, Tuple, Optional
from functools import lru_cache
from src.core.config import DEFAULT_FACE_DETECTION_MODEL, DEFAULT_FACE_TOLERANCE, MIN_FACE_QUALITY
# DeepFace library for face detection and recognition
try:
from deepface import DeepFace
DEEPFACE_AVAILABLE = True
except ImportError:
DEEPFACE_AVAILABLE = False
print("⚠️ Warning: DeepFace not available, some features may not work")
from src.core.config import (
DEFAULT_FACE_DETECTION_MODEL,
DEFAULT_FACE_TOLERANCE,
MIN_FACE_QUALITY,
DEEPFACE_DETECTOR_BACKEND,
DEEPFACE_MODEL_NAME,
DEEPFACE_ENFORCE_DETECTION,
DEEPFACE_ALIGN_FACES
)
from src.core.database import DatabaseManager
class FaceProcessor:
"""Handles face detection, encoding, and matching operations"""
def __init__(self, db_manager: DatabaseManager, verbose: int = 0):
"""Initialize face processor"""
def __init__(self, db_manager: DatabaseManager, verbose: int = 0,
detector_backend: str = None, model_name: str = None):
"""Initialize face processor with DeepFace settings
Args:
db_manager: Database manager instance
verbose: Verbosity level (0-3)
detector_backend: DeepFace detector backend (retinaface, mtcnn, opencv, ssd)
If None, uses DEEPFACE_DETECTOR_BACKEND from config
model_name: DeepFace model name (ArcFace, Facenet, Facenet512, VGG-Face)
If None, uses DEEPFACE_MODEL_NAME from config
"""
self.db = db_manager
self.verbose = verbose
self.detector_backend = detector_backend or DEEPFACE_DETECTOR_BACKEND
self.model_name = model_name or DEEPFACE_MODEL_NAME
self._face_encoding_cache = {}
self._image_cache = {}
if self.verbose >= 2:
print(f"🔧 FaceProcessor initialized:")
print(f" Detector: {self.detector_backend}")
print(f" Model: {self.model_name}")
@lru_cache(maxsize=1000)
def _get_cached_face_encoding(self, face_id: int, encoding_bytes: bytes) -> np.ndarray:
@@ -90,45 +122,84 @@ class FaceProcessor:
continue
try:
# Load image and find faces
# Process with DeepFace
if self.verbose >= 1:
print(f"📸 Processing: {filename}")
elif self.verbose == 0:
print(".", end="", flush=True)
if self.verbose >= 2:
print(f" 🔍 Loading image: {photo_path}")
print(f" 🔍 Using DeepFace: detector={self.detector_backend}, model={self.model_name}")
image = face_recognition.load_image_file(photo_path)
face_locations = face_recognition.face_locations(image, model=model)
# Use DeepFace.represent() to get face detection and encodings
results = DeepFace.represent(
img_path=photo_path,
model_name=self.model_name,
detector_backend=self.detector_backend,
enforce_detection=DEEPFACE_ENFORCE_DETECTION,
align=DEEPFACE_ALIGN_FACES
)
if face_locations:
face_encodings = face_recognition.face_encodings(image, face_locations)
if self.verbose >= 1:
print(f" 👤 Found {len(face_locations)} faces")
# Save faces to database with quality scores
for i, (encoding, location) in enumerate(zip(face_encodings, face_locations)):
# Check cancellation within inner loop as well
if stop_event is not None and getattr(stop_event, 'is_set', None) and stop_event.is_set():
print("⏹️ Processing cancelled by user")
break
# Calculate face quality score
quality_score = self._calculate_face_quality_score(image, location)
self.db.add_face(
photo_id=photo_id,
encoding=encoding.tobytes(),
location=str(location),
quality_score=quality_score
)
if self.verbose >= 3:
print(f" Face {i+1}: {location} (quality: {quality_score:.2f})")
else:
if not results:
if self.verbose >= 1:
print(f" 👤 No faces found")
elif self.verbose >= 2:
print(f" 👤 {filename}: No faces found")
# Mark as processed even with no faces
self.db.mark_photo_processed(photo_id)
processed_count += 1
continue
if self.verbose >= 1:
print(f" 👤 Found {len(results)} faces")
# Process each detected face
for i, result in enumerate(results):
# Check cancellation within inner loop
if stop_event is not None and getattr(stop_event, 'is_set', None) and stop_event.is_set():
print("⏹️ Processing cancelled by user")
break
# Extract face region info from DeepFace result
facial_area = result.get('facial_area', {})
face_confidence = result.get('face_confidence', 0.0)
embedding = np.array(result['embedding'])
# Convert DeepFace facial_area {x, y, w, h} to our location format
location = {
'x': facial_area.get('x', 0),
'y': facial_area.get('y', 0),
'w': facial_area.get('w', 0),
'h': facial_area.get('h', 0)
}
# Calculate face quality score
# Convert facial_area to (top, right, bottom, left) for quality calculation
face_location_tuple = (
facial_area.get('y', 0), # top
facial_area.get('x', 0) + facial_area.get('w', 0), # right
facial_area.get('y', 0) + facial_area.get('h', 0), # bottom
facial_area.get('x', 0) # left
)
# Load image for quality calculation
image = Image.open(photo_path)
image_np = np.array(image)
quality_score = self._calculate_face_quality_score(image_np, face_location_tuple)
# Store in database with DeepFace format
self.db.add_face(
photo_id=photo_id,
encoding=embedding.tobytes(),
location=str(location), # Store as string representation of dict
confidence=0.0, # Legacy field
quality_score=quality_score,
person_id=None,
detector_backend=self.detector_backend,
model_name=self.model_name,
face_confidence=face_confidence
)
if self.verbose >= 3:
print(f" Face {i+1}: {location} (quality: {quality_score:.2f}, confidence: {face_confidence:.2f})")
# Mark as processed
self.db.mark_photo_processed(photo_id)
@@ -223,11 +294,23 @@ class FaceProcessor:
# Remove from cache if file doesn't exist
del self._image_cache[cache_key]
# Parse location tuple from string format
# Parse location from string format and handle both DeepFace and legacy formats
if isinstance(location, str):
location = eval(location)
import ast
location = ast.literal_eval(location)
top, right, bottom, left = location
# Handle both DeepFace dict format and legacy tuple format
if isinstance(location, dict):
# DeepFace format: {x, y, w, h}
left = location.get('x', 0)
top = location.get('y', 0)
width = location.get('w', 0)
height = location.get('h', 0)
right = left + width
bottom = top + height
else:
# Legacy face_recognition format: (top, right, bottom, left)
top, right, bottom, left = location
# Load the image
image = Image.open(photo_path)
@@ -330,25 +413,64 @@ class FaceProcessor:
else:
return "⚫ (Very Low)"
def _calculate_cosine_similarity(self, encoding1: np.ndarray, encoding2: np.ndarray) -> float:
"""Calculate cosine similarity distance between two face encodings
Returns distance value (0 = identical, 2 = opposite) for compatibility with face_recognition API.
Uses cosine similarity internally which is better for DeepFace embeddings.
"""
try:
# Ensure encodings are numpy arrays
enc1 = np.array(encoding1).flatten()
enc2 = np.array(encoding2).flatten()
# Check if encodings have the same length
if len(enc1) != len(enc2):
if self.verbose >= 2:
print(f"⚠️ Encoding length mismatch: {len(enc1)} vs {len(enc2)}")
return 2.0 # Maximum distance on mismatch
# Normalize encodings
enc1_norm = enc1 / (np.linalg.norm(enc1) + 1e-8)
enc2_norm = enc2 / (np.linalg.norm(enc2) + 1e-8)
# Calculate cosine similarity
cosine_sim = np.dot(enc1_norm, enc2_norm)
# Clamp to valid range [-1, 1]
cosine_sim = np.clip(cosine_sim, -1.0, 1.0)
# Convert to distance (0 = identical, 2 = opposite)
# For consistency with face_recognition's distance metric
distance = 1.0 - cosine_sim # Range [0, 2], where 0 is perfect match
return distance
except Exception as e:
if self.verbose >= 1:
print(f"⚠️ Error calculating similarity: {e}")
return 2.0 # Maximum distance on error
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"""
# Start with base tolerance
"""Calculate adaptive tolerance based on face quality and match confidence
Note: For DeepFace, tolerance values are generally lower than face_recognition
"""
# Start with base tolerance (e.g., 0.4 instead of 0.6 for DeepFace)
tolerance = base_tolerance
# Adjust based on face quality (higher quality = stricter tolerance)
# More conservative: range 0.9 to 1.1 instead of 0.8 to 1.2
quality_factor = 0.9 + (face_quality * 0.2) # Range: 0.9 to 1.1
tolerance *= quality_factor
# If we have match confidence, adjust further
if match_confidence is not None:
# Higher confidence matches can use stricter tolerance
# More conservative: range 0.95 to 1.05 instead of 0.9 to 1.1
confidence_factor = 0.95 + (match_confidence * 0.1) # Range: 0.95 to 1.05
confidence_factor = 0.95 + (match_confidence * 0.1)
tolerance *= confidence_factor
# Ensure tolerance stays within reasonable bounds
return max(0.3, min(0.8, tolerance)) # Reduced max from 0.9 to 0.8
# Ensure tolerance stays within reasonable bounds for DeepFace
return max(0.2, min(0.6, tolerance)) # Lower range for DeepFace
def _get_filtered_similar_faces(self, face_id: int, tolerance: float, include_same_photo: bool = False, face_status: dict = None) -> List[Dict]:
"""Get similar faces with consistent filtering and sorting logic used by both auto-match and identify"""
@@ -454,7 +576,7 @@ class FaceProcessor:
avg_quality = (target_quality + other_quality) / 2
adaptive_tolerance = self._calculate_adaptive_tolerance(tolerance, avg_quality)
distance = face_recognition.face_distance([target_encoding], other_enc)[0]
distance = self._calculate_cosine_similarity(target_encoding, other_enc)
if distance <= adaptive_tolerance:
# Get photo info for this face
photo_info = self.db.get_face_photo_info(other_id)
@@ -552,11 +674,23 @@ class FaceProcessor:
# Remove from cache if file doesn't exist
del self._image_cache[cache_key]
# Parse location tuple from string format
# Parse location from string format and handle both DeepFace and legacy formats
if isinstance(location, str):
location = eval(location)
import ast
location = ast.literal_eval(location)
top, right, bottom, left = location
# Handle both DeepFace dict format and legacy tuple format
if isinstance(location, dict):
# DeepFace format: {x, y, w, h}
left = location.get('x', 0)
top = location.get('y', 0)
width = location.get('w', 0)
height = location.get('h', 0)
right = left + width
bottom = top + height
else:
# Legacy face_recognition format: (top, right, bottom, left)
top, right, bottom, left = location
# Load the image
image = Image.open(photo_path)
+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()
+5
View File
@@ -6,10 +6,15 @@ Simple command-line tool for face recognition and photo tagging
import os
import sys
import warnings
import argparse
import threading
from typing import List, Dict, Tuple, Optional
# Suppress TensorFlow warnings (must be before DeepFace import)
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
warnings.filterwarnings('ignore')
# Import our new modules
from src.core.config import (
DEFAULT_DB_PATH, DEFAULT_FACE_DETECTION_MODEL, DEFAULT_FACE_TOLERANCE,