Pushing code to migrate git
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cleanup script for PunimTag project organization.
|
||||
|
||||
This script removes redundant test files and organizes the project structure.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
def cleanup_old_tests():
|
||||
"""Remove old test files that are now consolidated."""
|
||||
old_test_files = [
|
||||
"test_syntax_fix.html",
|
||||
"test_js_validator.html",
|
||||
"test_direct_error_check.html",
|
||||
"test_console_tracker.html",
|
||||
"test_syntax_check.html",
|
||||
"test_progressive.html",
|
||||
"test_simple_main.html",
|
||||
"test_diagnostic.html",
|
||||
"test_minimal.html",
|
||||
"debug_ui.html",
|
||||
"test_backend.py",
|
||||
"test_punimtag.py",
|
||||
"test_web_api.py"
|
||||
]
|
||||
|
||||
removed_count = 0
|
||||
for file_name in old_test_files:
|
||||
file_path = Path(file_name)
|
||||
if file_path.exists():
|
||||
try:
|
||||
file_path.unlink()
|
||||
print(f"✅ Removed: {file_name}")
|
||||
removed_count += 1
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to remove {file_name}: {e}")
|
||||
|
||||
print(f"\n📊 Cleanup complete: {removed_count} files removed")
|
||||
|
||||
def verify_structure():
|
||||
"""Verify that the new structure is properly organized."""
|
||||
required_structure = {
|
||||
"src/backend/app.py": "Main Flask application",
|
||||
"src/backend/db_manager.py": "Database manager",
|
||||
"src/backend/visual_identifier.py": "Face recognition",
|
||||
"src/utils/tag_manager.py": "Tag management",
|
||||
"config/settings.py": "Configuration settings",
|
||||
"data/punimtag_simple.db": "Main database",
|
||||
"tests/test_main.py": "Main test suite",
|
||||
"docs/product.md": "Product vision",
|
||||
"docs/structure.md": "Project structure",
|
||||
"docs/tech.md": "Technical architecture",
|
||||
"docs/api-standards.md": "API standards",
|
||||
"docs/testing-standards.md": "Testing standards",
|
||||
"docs/code-conventions.md": "Code conventions"
|
||||
}
|
||||
|
||||
print("\n🔍 Verifying project structure:")
|
||||
print("=" * 50)
|
||||
|
||||
all_good = True
|
||||
for file_path, description in required_structure.items():
|
||||
if Path(file_path).exists():
|
||||
print(f"✅ {file_path} - {description}")
|
||||
else:
|
||||
print(f"❌ {file_path} - {description} (MISSING)")
|
||||
all_good = False
|
||||
|
||||
return all_good
|
||||
|
||||
def create_gitignore():
|
||||
"""Create or update .gitignore file."""
|
||||
gitignore_content = """# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Database files (keep structure, ignore content)
|
||||
data/*.db
|
||||
data/*.sqlite
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
temp_face_crop_*.jpg
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
"""
|
||||
|
||||
with open(".gitignore", "w") as f:
|
||||
f.write(gitignore_content)
|
||||
|
||||
print("✅ Updated .gitignore file")
|
||||
|
||||
def main():
|
||||
"""Main cleanup function."""
|
||||
print("🧹 PunimTag Project Cleanup")
|
||||
print("=" * 50)
|
||||
|
||||
# Clean up old test files
|
||||
cleanup_old_tests()
|
||||
|
||||
# Verify structure
|
||||
structure_ok = verify_structure()
|
||||
|
||||
# Update gitignore
|
||||
create_gitignore()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
if structure_ok:
|
||||
print("🎉 Project cleanup completed successfully!")
|
||||
print("\n📋 Next steps:")
|
||||
print("1. Review the steering documents in docs/")
|
||||
print("2. Run tests: python tests/test_main.py")
|
||||
print("3. Start the app: python main.py")
|
||||
else:
|
||||
print("⚠️ Project cleanup completed with issues.")
|
||||
print("Please check the missing files above.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,436 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Face Clustering GUI for PunimTag
|
||||
Visual interface for viewing and identifying clustered unknown faces
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, simpledialog
|
||||
from PIL import Image, ImageTk
|
||||
import os
|
||||
import sqlite3
|
||||
from typing import List, Dict
|
||||
import pickle
|
||||
import numpy as np
|
||||
|
||||
|
||||
class FaceClusterGUI:
|
||||
def __init__(self, db_path: str = 'punimtag_simple.db'):
|
||||
self.db_path = db_path
|
||||
self.root = tk.Tk()
|
||||
self.root.title("PunimTag - Face Clustering")
|
||||
self.root.geometry("1200x800")
|
||||
|
||||
# Current cluster data
|
||||
self.clusters = []
|
||||
self.current_cluster_index = 0
|
||||
|
||||
# Image cache
|
||||
self.image_cache = {}
|
||||
|
||||
self.setup_ui()
|
||||
self.load_clusters()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the user interface"""
|
||||
# Main frame
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Configure grid weights
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(1, weight=1)
|
||||
main_frame.rowconfigure(1, weight=1)
|
||||
|
||||
# Title
|
||||
title_label = ttk.Label(main_frame, text="Unknown Face Clusters",
|
||||
font=('Arial', 16, 'bold'))
|
||||
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 10))
|
||||
|
||||
# Left panel - cluster list
|
||||
left_frame = ttk.LabelFrame(main_frame, text="Clusters", padding="5")
|
||||
left_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
|
||||
|
||||
# Cluster listbox
|
||||
self.cluster_listbox = tk.Listbox(left_frame, width=30)
|
||||
self.cluster_listbox.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
self.cluster_listbox.bind('<<ListboxSelect>>', self.on_cluster_select)
|
||||
|
||||
scrollbar = ttk.Scrollbar(left_frame, orient="vertical", command=self.cluster_listbox.yview)
|
||||
scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
|
||||
self.cluster_listbox.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
left_frame.columnconfigure(0, weight=1)
|
||||
left_frame.rowconfigure(0, weight=1)
|
||||
|
||||
# Center panel - face display
|
||||
center_frame = ttk.LabelFrame(main_frame, text="Faces in Cluster", padding="5")
|
||||
center_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Canvas for face thumbnails
|
||||
self.canvas = tk.Canvas(center_frame, bg='white')
|
||||
self.canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Scrollbars for canvas
|
||||
v_scrollbar = ttk.Scrollbar(center_frame, orient="vertical", command=self.canvas.yview)
|
||||
v_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
|
||||
h_scrollbar = ttk.Scrollbar(center_frame, orient="horizontal", command=self.canvas.xview)
|
||||
h_scrollbar.grid(row=1, column=0, sticky=(tk.W, tk.E))
|
||||
|
||||
self.canvas.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
|
||||
|
||||
center_frame.columnconfigure(0, weight=1)
|
||||
center_frame.rowconfigure(0, weight=1)
|
||||
|
||||
# Right panel - actions
|
||||
right_frame = ttk.LabelFrame(main_frame, text="Actions", padding="5")
|
||||
right_frame.grid(row=1, column=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(10, 0))
|
||||
|
||||
# Cluster info
|
||||
self.info_label = ttk.Label(right_frame, text="Select a cluster", wraplength=200)
|
||||
self.info_label.grid(row=0, column=0, pady=(0, 10), sticky=(tk.W, tk.E))
|
||||
|
||||
# Identify button
|
||||
self.identify_button = ttk.Button(right_frame, text="Identify as Person",
|
||||
command=self.identify_cluster, state='disabled')
|
||||
self.identify_button.grid(row=1, column=0, pady=5, sticky=(tk.W, tk.E))
|
||||
|
||||
# Skip button
|
||||
self.skip_button = ttk.Button(right_frame, text="Skip Cluster",
|
||||
command=self.skip_cluster, state='disabled')
|
||||
self.skip_button.grid(row=2, column=0, pady=5, sticky=(tk.W, tk.E))
|
||||
|
||||
# Refresh button
|
||||
self.refresh_button = ttk.Button(right_frame, text="Refresh Clusters",
|
||||
command=self.refresh_clusters)
|
||||
self.refresh_button.grid(row=3, column=0, pady=5, sticky=(tk.W, tk.E))
|
||||
|
||||
# Statistics
|
||||
self.stats_label = ttk.Label(right_frame, text="", wraplength=200)
|
||||
self.stats_label.grid(row=4, column=0, pady=(20, 0), sticky=(tk.W, tk.E))
|
||||
|
||||
right_frame.columnconfigure(0, weight=1)
|
||||
|
||||
# Status bar
|
||||
self.status_bar = ttk.Label(main_frame, text="Ready", relief=tk.SUNKEN)
|
||||
self.status_bar.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(10, 0))
|
||||
|
||||
def load_clusters(self):
|
||||
"""Load face clusters from database"""
|
||||
try:
|
||||
self.status_bar.config(text="Loading clusters...")
|
||||
self.root.update()
|
||||
|
||||
# Get clusters using simple clustering (without sklearn)
|
||||
clusters = self.get_simple_clusters()
|
||||
|
||||
self.clusters = clusters
|
||||
self.populate_cluster_list()
|
||||
self.update_statistics()
|
||||
|
||||
self.status_bar.config(text=f"Loaded {len(clusters)} clusters")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to load clusters: {e}")
|
||||
self.status_bar.config(text="Error loading clusters")
|
||||
|
||||
def get_simple_clusters(self) -> List[Dict]:
|
||||
"""Simple clustering without sklearn - group by face encoding similarity"""
|
||||
if not os.path.exists(self.db_path):
|
||||
return []
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get unidentified faces
|
||||
c.execute('''SELECT f.id, f.image_id, i.path, f.top, f.right, f.bottom, f.left, f.encoding
|
||||
FROM faces f
|
||||
JOIN images i ON f.image_id = i.id
|
||||
WHERE f.person_id IS NULL''')
|
||||
|
||||
faces = c.fetchall()
|
||||
|
||||
if len(faces) < 2:
|
||||
return []
|
||||
|
||||
# Simple clustering by face encoding similarity
|
||||
clusters = []
|
||||
used_faces = set()
|
||||
|
||||
for i, face1 in enumerate(faces):
|
||||
if face1[0] in used_faces:
|
||||
continue
|
||||
|
||||
cluster_faces = [face1]
|
||||
used_faces.add(face1[0])
|
||||
|
||||
encoding1 = pickle.loads(face1[7])
|
||||
|
||||
# Find similar faces
|
||||
for j, face2 in enumerate(faces[i+1:], i+1):
|
||||
if face2[0] in used_faces:
|
||||
continue
|
||||
|
||||
encoding2 = pickle.loads(face2[7])
|
||||
|
||||
# Calculate similarity (simple distance)
|
||||
try:
|
||||
import face_recognition
|
||||
distance = face_recognition.face_distance([encoding1], encoding2)[0]
|
||||
|
||||
if distance < 0.5: # Similar faces
|
||||
cluster_faces.append(face2)
|
||||
used_faces.add(face2[0])
|
||||
except:
|
||||
# Fallback to numpy distance if face_recognition not available
|
||||
distance = np.linalg.norm(encoding1 - encoding2)
|
||||
if distance < 0.8:
|
||||
cluster_faces.append(face2)
|
||||
used_faces.add(face2[0])
|
||||
|
||||
# Only create cluster if it has multiple faces
|
||||
if len(cluster_faces) >= 2:
|
||||
cluster_data = {
|
||||
'cluster_id': len(clusters),
|
||||
'face_count': len(cluster_faces),
|
||||
'faces': []
|
||||
}
|
||||
|
||||
for face in cluster_faces:
|
||||
cluster_data['faces'].append({
|
||||
'face_id': face[0],
|
||||
'image_id': face[1],
|
||||
'image_path': face[2],
|
||||
'location': (face[3], face[4], face[5], face[6])
|
||||
})
|
||||
|
||||
clusters.append(cluster_data)
|
||||
|
||||
# Sort by face count (largest clusters first)
|
||||
clusters.sort(key=lambda x: x['face_count'], reverse=True)
|
||||
|
||||
return clusters
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in simple clustering: {e}")
|
||||
return []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def populate_cluster_list(self):
|
||||
"""Populate the cluster list"""
|
||||
self.cluster_listbox.delete(0, tk.END)
|
||||
|
||||
for i, cluster in enumerate(self.clusters):
|
||||
label = f"Cluster {i+1} ({cluster['face_count']} faces)"
|
||||
self.cluster_listbox.insert(tk.END, label)
|
||||
|
||||
def on_cluster_select(self, event):
|
||||
"""Handle cluster selection"""
|
||||
selection = self.cluster_listbox.curselection()
|
||||
if not selection:
|
||||
return
|
||||
|
||||
self.current_cluster_index = selection[0]
|
||||
self.display_cluster_faces()
|
||||
self.identify_button.config(state='normal')
|
||||
self.skip_button.config(state='normal')
|
||||
|
||||
def display_cluster_faces(self):
|
||||
"""Display faces in the selected cluster"""
|
||||
if not self.clusters or self.current_cluster_index >= len(self.clusters):
|
||||
return
|
||||
|
||||
cluster = self.clusters[self.current_cluster_index]
|
||||
|
||||
# Update info
|
||||
info_text = f"Cluster {self.current_cluster_index + 1}\n"
|
||||
info_text += f"{cluster['face_count']} faces\n"
|
||||
info_text += f"Click 'Identify as Person' to name these faces"
|
||||
self.info_label.config(text=info_text)
|
||||
|
||||
# Clear canvas
|
||||
self.canvas.delete("all")
|
||||
|
||||
# Display face thumbnails
|
||||
x, y = 10, 10
|
||||
max_width = 0
|
||||
row_height = 0
|
||||
|
||||
for i, face in enumerate(cluster['faces']):
|
||||
try:
|
||||
thumbnail = self.get_face_thumbnail(face)
|
||||
if thumbnail:
|
||||
# Create image on canvas
|
||||
image_id = self.canvas.create_image(x, y, anchor=tk.NW, image=thumbnail)
|
||||
|
||||
# Add image path as text below
|
||||
filename = os.path.basename(face['image_path'])
|
||||
self.canvas.create_text(x + 50, y + 110, text=filename,
|
||||
width=100, font=('Arial', 8))
|
||||
|
||||
x += 120
|
||||
max_width = max(max_width, x)
|
||||
row_height = max(row_height, 130)
|
||||
|
||||
# New row after 4 images
|
||||
if (i + 1) % 4 == 0:
|
||||
x = 10
|
||||
y += row_height
|
||||
row_height = 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error displaying face thumbnail: {e}")
|
||||
|
||||
# Update canvas scroll region
|
||||
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
||||
|
||||
def get_face_thumbnail(self, face: Dict) -> ImageTk.PhotoImage:
|
||||
"""Get thumbnail image of a face"""
|
||||
cache_key = f"{face['face_id']}"
|
||||
|
||||
if cache_key in self.image_cache:
|
||||
return self.image_cache[cache_key]
|
||||
|
||||
try:
|
||||
# Load image
|
||||
image_path = face['image_path']
|
||||
if not os.path.exists(image_path):
|
||||
return None
|
||||
|
||||
img = Image.open(image_path)
|
||||
|
||||
# Crop face region
|
||||
top, right, bottom, left = face['location']
|
||||
# Add some padding
|
||||
padding = 20
|
||||
left = max(0, left - padding)
|
||||
top = max(0, top - padding)
|
||||
right = min(img.width, right + padding)
|
||||
bottom = min(img.height, bottom + padding)
|
||||
|
||||
face_img = img.crop((left, top, right, bottom))
|
||||
|
||||
# Resize to thumbnail
|
||||
face_img.thumbnail((100, 100), Image.Resampling.LANCZOS)
|
||||
|
||||
# Convert to PhotoImage
|
||||
photo = ImageTk.PhotoImage(face_img)
|
||||
|
||||
# Cache it
|
||||
self.image_cache[cache_key] = photo
|
||||
|
||||
return photo
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating thumbnail: {e}")
|
||||
return None
|
||||
|
||||
def identify_cluster(self):
|
||||
"""Identify all faces in cluster as a person"""
|
||||
if not self.clusters or self.current_cluster_index >= len(self.clusters):
|
||||
return
|
||||
|
||||
cluster = self.clusters[self.current_cluster_index]
|
||||
|
||||
# Get person name
|
||||
name = simpledialog.askstring("Identify Person",
|
||||
f"Enter name for {cluster['face_count']} faces:")
|
||||
|
||||
if not name or not name.strip():
|
||||
return
|
||||
|
||||
name = name.strip()
|
||||
|
||||
try:
|
||||
# Add person to database and assign faces
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# Add person
|
||||
c.execute('INSERT OR IGNORE INTO people (name) VALUES (?)', (name,))
|
||||
c.execute('SELECT id FROM people WHERE name = ?', (name,))
|
||||
person_id = c.fetchone()[0]
|
||||
|
||||
# Assign all faces in cluster
|
||||
for face in cluster['faces']:
|
||||
c.execute('''UPDATE faces
|
||||
SET person_id = ?, is_confirmed = 1
|
||||
WHERE id = ?''',
|
||||
(person_id, face['face_id']))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
messagebox.showinfo("Success", f"Identified {cluster['face_count']} faces as {name}")
|
||||
|
||||
# Refresh clusters
|
||||
self.refresh_clusters()
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to identify cluster: {e}")
|
||||
|
||||
def skip_cluster(self):
|
||||
"""Skip current cluster"""
|
||||
if self.current_cluster_index < len(self.clusters) - 1:
|
||||
self.cluster_listbox.selection_set(self.current_cluster_index + 1)
|
||||
self.on_cluster_select(None)
|
||||
else:
|
||||
messagebox.showinfo("Info", "This is the last cluster")
|
||||
|
||||
def refresh_clusters(self):
|
||||
"""Reload clusters from database"""
|
||||
# Clear cache
|
||||
self.image_cache.clear()
|
||||
|
||||
# Reload
|
||||
self.load_clusters()
|
||||
|
||||
# Reset selection
|
||||
self.identify_button.config(state='disabled')
|
||||
self.skip_button.config(state='disabled')
|
||||
self.canvas.delete("all")
|
||||
self.info_label.config(text="Select a cluster")
|
||||
|
||||
def update_statistics(self):
|
||||
"""Update statistics display"""
|
||||
if not os.path.exists(self.db_path):
|
||||
return
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM faces WHERE person_id IS NULL")
|
||||
unidentified = c.fetchone()[0]
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM faces WHERE person_id IS NOT NULL")
|
||||
identified = c.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
stats_text = f"Statistics:\n"
|
||||
stats_text += f"Unidentified: {unidentified}\n"
|
||||
stats_text += f"Identified: {identified}\n"
|
||||
stats_text += f"Clusters: {len(self.clusters)}"
|
||||
|
||||
self.stats_label.config(text=stats_text)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating statistics: {e}")
|
||||
|
||||
def run(self):
|
||||
"""Start the GUI"""
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
app = FaceClusterGUI()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Interactive Face Identifier for PunimTag
|
||||
Allows users to identify unknown faces in the database
|
||||
"""
|
||||
|
||||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
from punimtag import PunimTag
|
||||
from typing import Optional
|
||||
import sys
|
||||
|
||||
|
||||
class InteractiveFaceIdentifier:
|
||||
def __init__(self, db_path: str = 'punimtag.db'):
|
||||
self.tagger = PunimTag(db_path=db_path)
|
||||
self.window_name = 'Face Identifier'
|
||||
|
||||
def display_face(self, image_path: str, location: tuple) -> np.ndarray:
|
||||
"""Load and display image with face highlighted"""
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
print(f"Error: Could not load image {image_path}")
|
||||
return None
|
||||
|
||||
# Get face coordinates
|
||||
top, right, bottom, left = location
|
||||
|
||||
# Draw rectangle around face
|
||||
cv2.rectangle(img, (left, top), (right, bottom), (0, 255, 0), 3)
|
||||
|
||||
# Calculate display size (max 800x600)
|
||||
height, width = img.shape[:2]
|
||||
max_height, max_width = 600, 800
|
||||
|
||||
if height > max_height or width > max_width:
|
||||
scale = min(max_height/height, max_width/width)
|
||||
new_width = int(width * scale)
|
||||
new_height = int(height * scale)
|
||||
img = cv2.resize(img, (new_width, new_height))
|
||||
|
||||
return img
|
||||
|
||||
def get_user_input(self, face_info: dict) -> Optional[str]:
|
||||
"""Get user input for face identification"""
|
||||
print("\n" + "="*50)
|
||||
print(f"Image: {face_info['image_path']}")
|
||||
print(f"Face ID: {face_info['face_id']}")
|
||||
print("\nOptions:")
|
||||
print("1. Enter person's name")
|
||||
print("2. Skip this face (press Enter)")
|
||||
print("3. Quit (press 'q')")
|
||||
print("="*50)
|
||||
|
||||
user_input = input("\nEnter person's name (or press Enter to skip): ").strip()
|
||||
|
||||
if user_input.lower() == 'q':
|
||||
return 'QUIT'
|
||||
elif user_input == '':
|
||||
return None
|
||||
else:
|
||||
return user_input
|
||||
|
||||
def run(self):
|
||||
"""Run the interactive identification process"""
|
||||
print("PunimTag Interactive Face Identifier")
|
||||
print("=" * 50)
|
||||
|
||||
# Get unidentified faces
|
||||
unidentified = self.tagger.get_unidentified_faces()
|
||||
|
||||
if not unidentified:
|
||||
print("No unidentified faces found!")
|
||||
return
|
||||
|
||||
print(f"Found {len(unidentified)} unidentified faces")
|
||||
|
||||
# Create window
|
||||
cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL)
|
||||
|
||||
identified_count = 0
|
||||
|
||||
for i, face_info in enumerate(unidentified):
|
||||
print(f"\nProcessing face {i+1} of {len(unidentified)}")
|
||||
|
||||
# Display the face
|
||||
img = self.display_face(face_info['image_path'], face_info['location'])
|
||||
|
||||
if img is None:
|
||||
continue
|
||||
|
||||
cv2.imshow(self.window_name, img)
|
||||
cv2.waitKey(1) # Allow window to update
|
||||
|
||||
# Get user input
|
||||
name = self.get_user_input(face_info)
|
||||
|
||||
if name == 'QUIT':
|
||||
print("\nQuitting...")
|
||||
break
|
||||
elif name:
|
||||
# Add person and assign face
|
||||
person_id = self.tagger.add_person(name)
|
||||
self.tagger.assign_face_to_person(face_info['face_id'], person_id, is_confirmed=True)
|
||||
identified_count += 1
|
||||
print(f"✓ Identified as: {name}")
|
||||
else:
|
||||
print("⊘ Skipped")
|
||||
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Identification complete!")
|
||||
print(f"Identified {identified_count} faces")
|
||||
print(f"Skipped {len(unidentified) - identified_count} faces")
|
||||
|
||||
self.tagger.close()
|
||||
|
||||
|
||||
class CLIFaceIdentifier:
|
||||
"""Command-line only face identifier (no OpenCV required)"""
|
||||
|
||||
def __init__(self, db_path: str = 'punimtag.db'):
|
||||
self.tagger = PunimTag(db_path=db_path)
|
||||
|
||||
def run(self):
|
||||
"""Run CLI-based identification"""
|
||||
print("PunimTag CLI Face Identifier")
|
||||
print("=" * 50)
|
||||
|
||||
# Get unidentified faces
|
||||
unidentified = self.tagger.get_unidentified_faces()
|
||||
|
||||
if not unidentified:
|
||||
print("No unidentified faces found!")
|
||||
return
|
||||
|
||||
print(f"Found {len(unidentified)} unidentified faces\n")
|
||||
|
||||
identified_count = 0
|
||||
|
||||
for i, face_info in enumerate(unidentified):
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Face {i+1} of {len(unidentified)}")
|
||||
print(f"Image: {face_info['image_path']}")
|
||||
print(f"Location in image: top={face_info['location'][0]}, right={face_info['location'][1]}, "
|
||||
f"bottom={face_info['location'][2]}, left={face_info['location'][3]}")
|
||||
print(f"Face ID: {face_info['face_id']}")
|
||||
|
||||
name = input("\nEnter person's name (or press Enter to skip, 'q' to quit): ").strip()
|
||||
|
||||
if name.lower() == 'q':
|
||||
print("\nQuitting...")
|
||||
break
|
||||
elif name:
|
||||
# Add person and assign face
|
||||
person_id = self.tagger.add_person(name)
|
||||
self.tagger.assign_face_to_person(face_info['face_id'], person_id, is_confirmed=True)
|
||||
identified_count += 1
|
||||
print(f"✓ Identified as: {name}")
|
||||
else:
|
||||
print("⊘ Skipped")
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Identification complete!")
|
||||
print(f"Identified {identified_count} faces")
|
||||
print(f"Skipped {len(unidentified) - identified_count} faces")
|
||||
|
||||
self.tagger.close()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
# Check if OpenCV is available
|
||||
try:
|
||||
import cv2
|
||||
print("OpenCV available - using visual identifier")
|
||||
identifier = InteractiveFaceIdentifier()
|
||||
except ImportError:
|
||||
print("OpenCV not available - using CLI identifier")
|
||||
print("Install opencv-python for visual identification: pip install opencv-python")
|
||||
identifier = CLIFaceIdentifier()
|
||||
|
||||
try:
|
||||
identifier.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nInterrupted by user")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Face Identifier for PunimTag
|
||||
Works with the punimtag_simple.db database
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
from PIL import Image
|
||||
import pickle
|
||||
|
||||
class SimpleFaceIdentifier:
|
||||
def __init__(self, db_path='punimtag_simple.db'):
|
||||
self.db_path = db_path
|
||||
|
||||
def get_unidentified_faces(self, limit=10):
|
||||
"""Get a limited number of unidentified faces"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('''SELECT f.id, f.image_id, i.path, i.filename, f.top, f.right, f.bottom, f.left
|
||||
FROM faces f
|
||||
JOIN images i ON f.image_id = i.id
|
||||
WHERE f.person_id IS NULL
|
||||
LIMIT ?''', (limit,))
|
||||
|
||||
faces = c.fetchall()
|
||||
conn.close()
|
||||
return faces
|
||||
|
||||
def add_person(self, name):
|
||||
"""Add a new person"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
c = conn.cursor()
|
||||
c.execute('INSERT OR IGNORE INTO people (name) VALUES (?)', (name,))
|
||||
c.execute('SELECT id FROM people WHERE name = ?', (name,))
|
||||
person_id = c.fetchone()[0]
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return person_id
|
||||
|
||||
def assign_face(self, face_id, person_id):
|
||||
"""Assign a face to a person"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
c = conn.cursor()
|
||||
c.execute('UPDATE faces SET person_id = ?, is_confirmed = 1 WHERE id = ?',
|
||||
(person_id, face_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def run_cli_identifier(self):
|
||||
"""Run command line identifier"""
|
||||
print("\n🏷️ Simple Face Identifier")
|
||||
print("=" * 50)
|
||||
|
||||
faces = self.get_unidentified_faces(50) # Get first 50 faces
|
||||
|
||||
if not faces:
|
||||
print("No unidentified faces found!")
|
||||
return
|
||||
|
||||
print(f"Found {len(faces)} unidentified faces to process...")
|
||||
print("For each face, enter the person's name or 's' to skip\n")
|
||||
|
||||
for i, (face_id, image_id, path, filename, top, right, bottom, left) in enumerate(faces):
|
||||
print(f"\nFace {i+1}/{len(faces)}")
|
||||
print(f"📁 File: {filename}")
|
||||
print(f"📍 Location: top={top}, right={right}, bottom={bottom}, left={left}")
|
||||
|
||||
# Try to display basic info about the image
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
with Image.open(path) as img:
|
||||
print(f"🖼️ Image size: {img.size}")
|
||||
else:
|
||||
print("⚠️ Image file not found")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not read image: {e}")
|
||||
|
||||
while True:
|
||||
name = input(f"👤 Who is this person? (or 's' to skip): ").strip()
|
||||
|
||||
if name.lower() == 's':
|
||||
print("⏭️ Skipped")
|
||||
break
|
||||
elif name:
|
||||
try:
|
||||
person_id = self.add_person(name)
|
||||
self.assign_face(face_id, person_id)
|
||||
print(f"✅ Identified as '{name}'")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
else:
|
||||
print("Please enter a name or 's' to skip")
|
||||
|
||||
print(f"\n🎉 Completed processing {len(faces)} faces!")
|
||||
|
||||
# Show remaining count
|
||||
remaining = self.get_remaining_count()
|
||||
if remaining > 0:
|
||||
print(f"📊 {remaining} unidentified faces remaining")
|
||||
print("Run the script again to continue identifying faces")
|
||||
else:
|
||||
print("🏆 All faces have been identified!")
|
||||
|
||||
def get_remaining_count(self):
|
||||
"""Get count of remaining unidentified faces"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
c = conn.cursor()
|
||||
c.execute('SELECT COUNT(*) FROM faces WHERE person_id IS NULL')
|
||||
count = c.fetchone()[0]
|
||||
conn.close()
|
||||
return count
|
||||
|
||||
if __name__ == "__main__":
|
||||
identifier = SimpleFaceIdentifier()
|
||||
identifier.run_cli_identifier()
|
||||
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PunimTag GUI Starter
|
||||
Simple script to demonstrate the system and guide next steps
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def check_requirements():
|
||||
"""Check if all requirements are met"""
|
||||
print("🔍 Checking requirements...")
|
||||
|
||||
# Check if photos directory exists
|
||||
if not os.path.exists('photos'):
|
||||
print("❌ Photos directory not found")
|
||||
print(" Creating photos/ directory...")
|
||||
os.makedirs('photos', exist_ok=True)
|
||||
print(" ✅ Created photos/ directory")
|
||||
else:
|
||||
print("✅ Photos directory exists")
|
||||
|
||||
# Check if database exists
|
||||
if os.path.exists('punimtag_simple.db'):
|
||||
print("✅ Database exists")
|
||||
|
||||
# Get basic stats
|
||||
try:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('punimtag_simple.db')
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM images")
|
||||
image_count = c.fetchone()[0]
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM faces")
|
||||
face_count = c.fetchone()[0]
|
||||
|
||||
c.execute("SELECT COUNT(*) FROM faces WHERE person_id IS NULL")
|
||||
unidentified_count = c.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f" 📊 {image_count} images, {face_count} faces, {unidentified_count} unidentified")
|
||||
|
||||
return image_count, face_count, unidentified_count
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Error reading database: {e}")
|
||||
return 0, 0, 0
|
||||
else:
|
||||
print("❌ Database not found")
|
||||
print(" Run 'python punimtag_simple.py' first to process images")
|
||||
return 0, 0, 0
|
||||
|
||||
|
||||
def show_menu(image_count, face_count, unidentified_count):
|
||||
"""Show main menu"""
|
||||
print("\n" + "="*60)
|
||||
print("🏷️ PUNIMTAG - NEXT STEPS")
|
||||
print("="*60)
|
||||
|
||||
if image_count == 0:
|
||||
print("📝 GETTING STARTED:")
|
||||
print(" 1. Add photos to the 'photos/' directory")
|
||||
print(" 2. Run: python punimtag_simple.py")
|
||||
print(" 3. Come back here for face identification")
|
||||
print("\n💡 TIP: Start with 10-20 photos for testing")
|
||||
return
|
||||
|
||||
print(f"📊 CURRENT STATUS:")
|
||||
print(f" Images processed: {image_count}")
|
||||
print(f" Faces detected: {face_count}")
|
||||
print(f" Unidentified faces: {unidentified_count}")
|
||||
|
||||
print(f"\n🎯 AVAILABLE ACTIONS:")
|
||||
print(f" 1. Process more images")
|
||||
print(f" 2. Identify unknown faces (CLI)")
|
||||
print(f" 3. Manage database")
|
||||
print(f" 4. View statistics")
|
||||
if unidentified_count > 0:
|
||||
print(f" 5. Start simple web interface (coming soon)")
|
||||
print(f" 6. Exit")
|
||||
|
||||
|
||||
def process_images():
|
||||
"""Process images"""
|
||||
print("\n📷 Processing images...")
|
||||
try:
|
||||
result = subprocess.run([sys.executable, 'punimtag_simple.py'],
|
||||
capture_output=True, text=True)
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print("Errors:", result.stderr)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
def identify_faces():
|
||||
"""Identify faces using CLI tool"""
|
||||
print("\n👥 Starting face identification...")
|
||||
try:
|
||||
subprocess.run([sys.executable, 'interactive_identifier.py'])
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
def manage_database():
|
||||
"""Manage database"""
|
||||
print("\n🗄️ Starting database manager...")
|
||||
try:
|
||||
subprocess.run([sys.executable, 'db_manager.py'])
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
def show_statistics():
|
||||
"""Show detailed statistics"""
|
||||
print("\n📊 DETAILED STATISTICS")
|
||||
print("="*40)
|
||||
|
||||
try:
|
||||
from db_manager import DatabaseManager
|
||||
manager = DatabaseManager('punimtag_simple.db')
|
||||
manager.inspect_database()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
def show_next_gui_steps():
|
||||
"""Show what's coming next for GUI development"""
|
||||
print("\n🚀 NEXT GUI DEVELOPMENT STEPS")
|
||||
print("="*50)
|
||||
print("We have a working backend! Next steps for GUI:")
|
||||
print()
|
||||
print("✅ COMPLETED:")
|
||||
print(" • Face recognition and clustering")
|
||||
print(" • Database management")
|
||||
print(" • Jewish organization features")
|
||||
print(" • Search functionality")
|
||||
print(" • CLI tools for identification")
|
||||
print()
|
||||
print("🔄 IN PROGRESS:")
|
||||
print(" • Web-based GUI with Flask")
|
||||
print(" • Face clustering visualization")
|
||||
print(" • Interactive face identification")
|
||||
print()
|
||||
print("📋 PLANNED:")
|
||||
print(" • Advanced search interface")
|
||||
print(" • Tag management GUI")
|
||||
print(" • Statistics dashboard")
|
||||
print(" • Photo gallery with face highlights")
|
||||
print()
|
||||
print("💡 TO TEST THE WEB GUI:")
|
||||
print(" 1. Make sure you have processed some images")
|
||||
print(" 2. Run: python web_gui.py (when ready)")
|
||||
print(" 3. Open http://localhost:5000 in browser")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
print("🏷️ PunimTag GUI Starter")
|
||||
print("Welcome to the face recognition and photo tagging system!")
|
||||
|
||||
# Check requirements
|
||||
image_count, face_count, unidentified_count = check_requirements()
|
||||
|
||||
while True:
|
||||
show_menu(image_count, face_count, unidentified_count)
|
||||
|
||||
try:
|
||||
choice = input(f"\n➤ Select option: ").strip()
|
||||
|
||||
if choice == '1':
|
||||
process_images()
|
||||
# Refresh stats
|
||||
image_count, face_count, unidentified_count = check_requirements()
|
||||
|
||||
elif choice == '2':
|
||||
if face_count == 0:
|
||||
print("❌ No faces to identify. Process images first.")
|
||||
else:
|
||||
identify_faces()
|
||||
# Refresh stats
|
||||
image_count, face_count, unidentified_count = check_requirements()
|
||||
|
||||
elif choice == '3':
|
||||
manage_database()
|
||||
# Refresh stats
|
||||
image_count, face_count, unidentified_count = check_requirements()
|
||||
|
||||
elif choice == '4':
|
||||
show_statistics()
|
||||
|
||||
elif choice == '5':
|
||||
if unidentified_count > 0:
|
||||
print("🌐 Web interface is in development!")
|
||||
print("For now, use option 2 for CLI identification.")
|
||||
else:
|
||||
print("✅ All faces are identified!")
|
||||
|
||||
elif choice == '6':
|
||||
break
|
||||
|
||||
else:
|
||||
print("❌ Invalid option")
|
||||
|
||||
input("\nPress Enter to continue...")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 Goodbye!")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
input("Press Enter to continue...")
|
||||
|
||||
print("\n🎉 Thank you for using PunimTag!")
|
||||
show_next_gui_steps()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user