This commit is contained in:
ilia 2025-09-15 12:16:01 -04:00
parent cb61d24bc3
commit 85d5c120d4
8 changed files with 985 additions and 239 deletions

162
DEMO.md Normal file
View File

@ -0,0 +1,162 @@
# 🎬 PunimTag Complete Demo Guide
## 🎯 Quick Client Demo (10 minutes)
**Perfect for:** Client presentations, showcasing enhanced face recognition features
---
## 🚀 Setup (2 minutes)
### 1. Prerequisites
```bash
cd /home/beast/Code/punimtag
source venv/bin/activate # Always activate first!
sudo apt install feh # Image viewer (one-time setup)
```
### 2. Prepare Demo
```bash
# Clean start
rm -f demo.db
# Check demo photos (should have 6+ photos with faces)
find demo_photos/ -name "*.jpg" -o -name "*.png" | wc -l
```
---
## 🎭 Client Demo Script (8 minutes)
### **Opening (30 seconds)**
*"I'll show you PunimTag - an enhanced face recognition tool that runs entirely on your local machine. It features visual face identification and intelligent cross-photo matching."*
### **Step 1: Scan & Process (2 minutes)**
```bash
# Scan photos
python3 photo_tagger.py scan demo_photos --recursive --db demo.db -v
# Process for faces
python3 photo_tagger.py process --db demo.db -v
# Show results
python3 photo_tagger.py stats --db demo.db
```
**Say:** *"Perfect! It found X photos and detected Y faces automatically."*
### **Step 2: Visual Face Identification (3 minutes)**
```bash
python3 photo_tagger.py identify --show-faces --batch 3 --db demo.db
```
**Key points to mention:**s
- *"Notice how it shows individual face crops - no guessing!"*
- *"Each face opens automatically in the image viewer"*
- *"You see exactly which person you're identifying"*
### **Step 3: Smart Auto-Matching (3 minutes)**
```bash
python3 photo_tagger.py auto-match --show-faces --db demo.db
```
**Key points to mention:**
- *"Watch how it finds the same people across different photos"*
- *"Side-by-side comparison with confidence scoring"*
- *"Only suggests logical cross-photo matches"*
- *"Color-coded confidence: Green=High, Yellow=Medium, Red=Low"*
### **Step 4: Search & Results (1 minute)**
```bash
# Search for identified person
python3 photo_tagger.py search "Alice" --db demo.db
# Final statistics
python3 photo_tagger.py stats --db demo.db
```
**Say:** *"Now you can instantly find all photos containing any person."*
---
## 🎯 Key Demo Points for Clients
**Privacy-First**: Everything runs locally, no cloud services
**Visual Interface**: See actual faces, not coordinates
**Intelligent Matching**: Cross-photo recognition with confidence scores
**Professional Quality**: Color-coded confidence, automatic cleanup
**Easy to Use**: Simple commands, clear visual feedback
**Fast & Efficient**: Batch processing, smart suggestions
---
## 🔧 Advanced Features (Optional)
### Confidence Control
```bash
# Strict matching (high confidence only)
python3 photo_tagger.py auto-match --tolerance 0.3 --show-faces --db demo.db
# Automatic high-confidence identification
python3 photo_tagger.py auto-match --auto --show-faces --db demo.db
```
### Twins Detection
```bash
# Include same-photo matching (for twins)
python3 photo_tagger.py auto-match --include-twins --show-faces --db demo.db
```
---
## 📊 Confidence Guide
| Level | Color | Description | Recommendation |
|-------|-------|-------------|----------------|
| 80%+ | 🟢 | Very High - Almost Certain | Accept confidently |
| 70%+ | 🟡 | High - Likely Match | Probably correct |
| 60%+ | 🟠 | Medium - Possible | Review carefully |
| 50%+ | 🔴 | Low - Questionable | Likely incorrect |
| <50% | | Very Low - Unlikely | Filtered out |
---
## 🚨 Demo Troubleshooting
**If no faces display:**
- Check feh installation: `sudo apt install feh`
- Manually open: `feh /tmp/face_*_crop.jpg`
**If no auto-matches:**
- Ensure same people appear in multiple photos
- Lower tolerance: `--tolerance 0.7`
**If confidence seems low:**
- 60-70% is normal for different lighting/angles
- 80%+ indicates excellent matches
---
## 🎪 Complete Demo Commands
```bash
# Full demo workflow
source venv/bin/activate
rm -f demo.db
python3 photo_tagger.py scan demo_photos --recursive --db demo.db -v
python3 photo_tagger.py process --db demo.db -v
python3 photo_tagger.py stats --db demo.db
python3 photo_tagger.py identify --show-faces --batch 3 --db demo.db
python3 photo_tagger.py auto-match --show-faces --db demo.db
python3 photo_tagger.py search "Alice" --db demo.db
python3 photo_tagger.py stats --db demo.db
```
**Or use the interactive script:**
```bash
./demo.sh
```
---
**🎉 Demo Complete!** Clients will see a professional-grade face recognition system with visual interfaces and intelligent matching capabilities.

View File

@ -18,10 +18,13 @@ python3 photo_tagger.py scan /path/to/your/photos
# 3. Process faces
python3 photo_tagger.py process
# 4. Identify faces interactively
python3 photo_tagger.py identify
# 4. Identify faces with visual display
python3 photo_tagger.py identify --show-faces
# 5. View statistics
# 5. Auto-match faces across photos
python3 photo_tagger.py auto-match --show-faces
# 6. View statistics
python3 photo_tagger.py stats
```
@ -77,19 +80,32 @@ python3 photo_tagger.py process --limit 20 --model cnn
python3 photo_tagger.py process --limit 100 --model hog
```
### Identify Faces
### Identify Faces (Enhanced!)
```bash
# Identify 20 faces interactively
python3 photo_tagger.py identify
# Identify with individual face display (RECOMMENDED)
python3 photo_tagger.py identify --show-faces --batch 10
# Identify 10 faces at a time
# Traditional mode (coordinates only)
python3 photo_tagger.py identify --batch 10
# Auto-match faces across photos (NEW!)
python3 photo_tagger.py auto-match --show-faces
# Auto-identify high-confidence matches
python3 photo_tagger.py auto-match --auto --show-faces
```
**Interactive commands during identification:**
**Enhanced identification features:**
- 🖼️ **Individual face crops** - See exactly which face you're identifying
- 🔍 **Side-by-side comparisons** - Visual matching across photos
- 📊 **Confidence scoring** - Know how similar faces are
- 🎯 **Smart cross-photo matching** - Find same people in different photos
- 🧹 **Auto cleanup** - No temporary files left behind
**Interactive commands:**
- Type person's name to identify
- `s` = skip this face
- `q` = quit
- `q` = quit
- `list` = show known people
### Add Tags
@ -116,7 +132,7 @@ python3 photo_tagger.py search "Joh"
python3 photo_tagger.py stats
```
## 📊 Example Workflow
## 📊 Enhanced Example Workflow
```bash
# ALWAYS activate virtual environment first!
@ -131,13 +147,16 @@ python3 photo_tagger.py process --limit 20
# 3. Check what we found
python3 photo_tagger.py stats
# 4. Identify some faces
python3 photo_tagger.py identify --batch 10
# 4. Identify faces with visual display (ENHANCED!)
python3 photo_tagger.py identify --show-faces --batch 10
# 5. Search for photos of someone
# 5. Auto-match faces across photos (NEW!)
python3 photo_tagger.py auto-match --show-faces
# 6. Search for photos of someone
python3 photo_tagger.py search "Alice"
# 6. Add some tags
# 7. Add some tags
python3 photo_tagger.py tag --pattern "birthday"
```
@ -274,13 +293,15 @@ python3 -m venv venv && source venv/bin/activate && python3 setup.py
# Daily usage - Option 1: Use run script (automatic venv activation)
./run.sh scan ~/Pictures --recursive
./run.sh process --limit 50
./run.sh identify --batch 10
./run.sh identify --show-faces --batch 10
./run.sh auto-match --show-faces
./run.sh stats
# Daily usage - Option 2: Manual venv activation
# Daily usage - Option 2: Manual venv activation (ENHANCED)
source venv/bin/activate
python3 photo_tagger.py scan ~/Pictures --recursive
python3 photo_tagger.py process --limit 50
python3 photo_tagger.py identify --batch 10
python3 photo_tagger.py identify --show-faces --batch 10
python3 photo_tagger.py auto-match --show-faces
python3 photo_tagger.py stats
```

View File

@ -1,210 +0,0 @@
# PunimTag Complete Rebuild - Summary
## 🎯 What We Did
Completely rebuilt PunimTag from a complex web application into a **simple, focused CLI tool** for photo face tagging.
## 📊 Before vs After
### Before (Complex)
```
- 182KB Flask web app (4,365+ lines)
- Complex web interface with embedded HTML/CSS/JS
- Multiple legacy files and dependencies
- Web framework overhead
- Difficult to understand and modify
- Large repository size
```
### After (Simple)
```
- 17KB CLI tool (~400 lines)
- Clean command-line interface
- Minimal dependencies (6 packages)
- No web framework overhead
- Easy to understand and modify
- Small repository size
```
## 🗂️ New Project Structure
```
PunimTag/
├── photo_tagger.py # Main CLI tool (17KB)
├── setup.py # Setup script (3KB)
├── requirements.txt # 6 minimal dependencies
├── README.md # Clear documentation
├── test_basic.py # Basic functionality tests
├── data/ # Database files (not in git)
├── photos/ # User photos (not in git)
└── .gitignore # Excludes large files
```
## 🧹 What We Removed
### Files Deleted
- `src/backend/app.py` (182KB web interface)
- `src/backend/web_gui.py`
- `src/backend/punimtag.py`
- `src/backend/punimtag_simple.py`
- All web frontend files
- Complex documentation
- Test files for web interface
- Configuration files
- Scripts directory
### Dependencies Removed
- `flask` - Web framework
- `opencv-python` - Computer vision (optional)
- `scikit-learn` - Machine learning extras
- All web-related dependencies
## ✅ What We Kept
### Core Functionality
- Face detection and recognition
- Database schema for photos, faces, people, tags
- Batch processing capabilities
- Interactive identification
- Search and statistics
### Essential Dependencies
- `face-recognition` - Core face recognition
- `dlib` - Machine learning backend
- `pillow` - Image processing
- `numpy` - Numerical operations
- `click` - CLI interface
## 🚀 New CLI Commands
```bash
# Scan photos
python photo_tagger.py scan /path/to/photos
# Process faces
python photo_tagger.py process --limit 50
# Identify faces interactively
python photo_tagger.py identify --batch 20
# Add tags
python photo_tagger.py tag --pattern "vacation"
# Search for person
python photo_tagger.py search "John"
# View statistics
python photo_tagger.py stats
```
## 💡 Key Improvements
### Simplicity
- **90% size reduction** - From 182KB to 17KB
- **Single file** - Everything in `photo_tagger.py`
- **Clear workflow** - Scan → Process → Identify → Search
### Performance
- **Faster startup** - No web framework overhead
- **Efficient processing** - Direct face recognition calls
- **Batch operations** - Process photos in manageable chunks
### Usability
- **Better CLI** - Clear commands with help text
- **Interactive identification** - Easy face tagging
- **Progress feedback** - Clear status messages
### Maintainability
- **Readable code** - Well-structured, documented
- **Minimal dependencies** - Easy to install and maintain
- **Focused purpose** - Does one thing well
## 🧪 Testing
### Basic Tests Pass ✅
```
📋 Testing: Database Schema ✅
📋 Testing: CLI Structure ✅
📊 Results: 2/2 tests passed
```
### Ready for Use
- Database schema works correctly
- CLI argument parsing functional
- Code structure is sound
- Dependencies are minimal
## 📦 Installation
### Quick Start
```bash
# 1. Setup
python setup.py
# 2. Use
python photo_tagger.py scan /photos
python photo_tagger.py process
python photo_tagger.py identify
```
### Manual Install
```bash
pip install -r requirements.txt
python photo_tagger.py stats
```
## 🎯 Benefits Achieved
### For Development
- **Easier to understand** - Single focused file
- **Faster to modify** - No complex web interface
- **Simpler testing** - CLI is easier to test
- **Better git workflow** - Small, focused commits
### For Users
- **Faster execution** - No web server overhead
- **Better for batch processing** - CLI is perfect for automation
- **Lower resource usage** - Minimal memory footprint
- **More reliable** - Fewer dependencies, fewer failure points
### For Deployment
- **Smaller repository** - Only essential files
- **Easier installation** - Fewer dependencies
- **Better portability** - Runs anywhere Python runs
- **No security concerns** - No web server to secure
## 🔮 Future Possibilities
The new minimal structure makes it easy to add features:
### Easy Additions
- Export functionality
- Different face detection models
- Batch tagging operations
- Integration with other tools
### Optional Features
- Web interface (if needed later)
- GUI wrapper (tkinter/Qt)
- API endpoints (Flask add-on)
- Cloud sync (separate module)
## 📈 Success Metrics
- **Code size**: 182KB → 17KB (90% reduction)
- **Dependencies**: 15+ → 6 (60% reduction)
- **Complexity**: High → Low
- **Setup time**: ~30min → ~5min
- **Learning curve**: Steep → Gentle
## 🎉 Conclusion
Successfully transformed PunimTag from a complex web application into a **focused, efficient CLI tool** that does exactly what's needed:
**Simple** - Easy to understand and use
**Fast** - Efficient face recognition processing
**Reliable** - Minimal dependencies, fewer failure points
**Maintainable** - Clean code, clear structure
**Portable** - Runs anywhere Python runs
The project is now **ready for development** and **easy to extend** while maintaining its core simplicity and focus.

193
demo.sh Executable file
View File

@ -0,0 +1,193 @@
#!/bin/bash
# PunimTag Demo Helper Script
# Makes running demo commands easier
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}🎬 PunimTag Demo Helper${NC}"
echo "=================================="
# Check if virtual environment exists
if [ ! -d "venv" ]; then
echo -e "${RED}❌ Virtual environment not found!${NC}"
echo "Run: python3 -m venv venv && source venv/bin/activate && python3 setup.py"
exit 1
fi
# Activate virtual environment
source venv/bin/activate
# Check if demo photos exist
photo_count=$(find demo_photos/ -name "*.jpg" -o -name "*.png" -o -name "*.jpeg" 2>/dev/null | wc -l)
if [ "$photo_count" -eq 0 ]; then
echo -e "${YELLOW}⚠️ No demo photos found!${NC}"
echo "Please add photos to demo_photos/ folders first."
echo "See: demo_photos/DEMO_INSTRUCTIONS.md"
echo ""
echo "Demo folder structure:"
ls -la demo_photos/
exit 1
fi
echo -e "${GREEN}✅ Found $photo_count demo photos${NC}"
# Function to run demo commands with explanations
demo_command() {
echo ""
echo -e "${YELLOW}📋 $1${NC}"
echo "Command: $2"
echo "Press Enter to run..."
read
echo -e "${BLUE}Running: $2${NC}"
eval $2
echo ""
}
# Demo workflow
echo ""
echo "🎯 Enhanced Demo Workflow:"
echo "1. Clean demo database"
echo "2. Scan photos"
echo "3. Process for faces"
echo "4. Show statistics"
echo "5. Identify faces with visual display (ENHANCED!)"
echo "6. Auto-match faces across photos (NEW!)"
echo "7. Search for people"
echo "8. Show verbose modes"
echo ""
# Clean demo database
demo_command "Clean previous demo data" "rm -f demo.db"
# Scan photos
demo_command "Scan photos with verbose output" "python3 photo_tagger.py scan demo_photos --recursive --db demo.db -v"
# Process faces
demo_command "Process photos to find faces" "python3 photo_tagger.py process --db demo.db -v"
# Show stats
demo_command "Show database statistics" "python3 photo_tagger.py stats --db demo.db"
# Enhanced visual identification
echo -e "${YELLOW}📋 Enhanced face identification with visual display${NC}"
echo "Command: python3 photo_tagger.py identify --show-faces --batch 3 --db demo.db"
echo "Press Enter to run (you'll see individual face crops!)..."
read
echo -e "${BLUE}Running: python3 photo_tagger.py identify --show-faces --batch 3 --db demo.db${NC}"
python3 photo_tagger.py identify --show-faces --batch 3 --db demo.db
# Smart auto-matching
echo ""
echo -e "${YELLOW}📋 Smart auto-matching across photos${NC}"
echo "Command: python3 photo_tagger.py auto-match --show-faces --db demo.db"
echo "Press Enter to run (you'll see side-by-side face comparisons!)..."
read
echo -e "${BLUE}Running: python3 photo_tagger.py auto-match --show-faces --db demo.db${NC}"
python3 photo_tagger.py auto-match --show-faces --db demo.db
# Search for people
echo ""
echo -e "${YELLOW}📋 Search for identified people${NC}"
echo "Available people in database:"
python3 -c "
import sqlite3
conn = sqlite3.connect('demo.db')
cursor = conn.cursor()
cursor.execute('SELECT DISTINCT name FROM people')
people = cursor.fetchall()
for person in people:
print(f' - {person[0]}')
conn.close()
"
if [ $? -eq 0 ]; then
echo ""
echo "Enter a person's name to search for:"
read person_name
if [ ! -z "$person_name" ]; then
echo -e "${BLUE}Running: python3 photo_tagger.py search \"$person_name\" --db demo.db${NC}"
python3 photo_tagger.py search "$person_name" --db demo.db
fi
fi
# Verbose modes demo
echo ""
echo -e "${YELLOW}📋 Verbose modes demonstration${NC}"
echo "Let's see different verbosity levels..."
# Reset a photo for verbose demo
python3 -c "
import sqlite3
conn = sqlite3.connect('demo.db')
conn.execute('UPDATE photos SET processed = 0 WHERE id = 1')
conn.commit()
conn.close()
"
demo_command "Quiet mode (default)" "python3 photo_tagger.py process --limit 1 --db demo.db"
# Reset photo
python3 -c "
import sqlite3
conn = sqlite3.connect('demo.db')
conn.execute('UPDATE photos SET processed = 0 WHERE id = 1')
conn.commit()
conn.close()
"
demo_command "Verbose mode (-v)" "python3 photo_tagger.py process --limit 1 --db demo.db -v"
# Reset photo
python3 -c "
import sqlite3
conn = sqlite3.connect('demo.db')
conn.execute('UPDATE photos SET processed = 0 WHERE id = 1')
conn.commit()
conn.close()
"
demo_command "Very verbose mode (-vv)" "python3 photo_tagger.py process --limit 1 --db demo.db -vv"
# Reset photo
python3 -c "
import sqlite3
conn = sqlite3.connect('demo.db')
conn.execute('UPDATE photos SET processed = 0 WHERE id = 1')
conn.commit()
conn.close()
"
demo_command "Maximum verbose (-vvv)" "python3 photo_tagger.py process --limit 1 --db demo.db -vvv"
# Final stats
demo_command "Final statistics" "python3 photo_tagger.py stats --db demo.db"
echo ""
echo -e "${GREEN}🎉 Demo complete!${NC}"
echo ""
echo "📋 Summary of what we demonstrated:"
echo " ✅ Photo scanning and indexing"
echo " ✅ Automatic face detection"
echo " ✅ Interactive face identification"
echo " ✅ Person-based photo search"
echo " ✅ Verbose output modes (-v, -vv, -vvv)"
echo " ✅ Database statistics and reporting"
echo ""
echo "🔧 Enhanced features demonstrated:"
echo " ✅ Visual face identification with --show-faces"
echo " ✅ Smart cross-photo auto-matching"
echo " ✅ Confidence-based match suggestions"
echo " ✅ Side-by-side face comparisons"
echo " ✅ Non-blocking image display"
echo ""
echo "🔧 Additional features to explore:"
echo " - Custom tagging: python3 photo_tagger.py tag"
echo " - Different models: --model cnn (more accurate)"
echo " - Tolerance adjustment: --tolerance 0.3 (stricter) or 0.7 (lenient)"
echo " - Twins detection: --include-twins"
echo " - Auto mode: --auto (high-confidence auto-identification)"

View File

@ -0,0 +1,92 @@
# 🎯 Enhanced Demo Photo Setup Instructions
To run the enhanced demo with visual face recognition, you need sample photos in this folder structure.
## 📸 Quick Setup for Enhanced Demo
1. **Find 6-12 photos** with clear faces from your collection
2. **Copy them** into the subfolders below:
```
demo_photos/
├── family/ ← 3-5 photos with family members (SOME PEOPLE IN MULTIPLE PHOTOS)
├── friends/ ← 2-3 photos with friends
└── events/ ← 2-4 photos from events/gatherings
```
## 🎭 Ideal Demo Photos for Enhanced Features:
- **Clear faces**: Well-lit, not too small (face recognition works better)
- **Multiple people**: 2-5 people per photo works best
- **⭐ REPEAT appearances**: Same people in multiple photos (for auto-matching demo!)
- **Mix of scenarios**: Group photos + individual portraits
- **Different lighting/angles**: Shows robustness of cross-photo matching
## 🚀 Enhanced Demo Test Commands:
### Basic Setup Test:
```bash
# Test the scan
source venv/bin/activate
python3 photo_tagger.py scan demo_photos --recursive --db demo.db -v
# Should output something like:
# 📁 Found 8 photos, added 8 new photos
```
### Face Detection Test:
```bash
python3 photo_tagger.py process --db demo.db -v
# Should output:
# 🔍 Processing 8 photos for faces...
# 📸 Processing: family_photo1.jpg
# 👤 Found 4 faces
```
### Enhanced Identification Test:
```bash
# Test individual face display
python3 photo_tagger.py identify --show-faces --batch 2 --db demo.db
# Should show individual face crops automatically
```
## 🎪 Enhanced Demo Success Criteria:
After adding photos, you should be able to demonstrate:
1. ✅ **Scan** finds your photos
2. ✅ **Process** detects faces in all photos
3. ✅ **Individual face display** shows cropped faces during identification
4. ✅ **Cross-photo matching** finds same people in different photos
5. ✅ **Confidence scoring** with color-coded quality levels
6. ✅ **Visual comparison** with side-by-side face images
7. ✅ **Search** finds photos by person name
8. ✅ **Smart filtering** only shows logical matches
## 📊 Expected Demo Results:
With good demo photos, you should see:
- **15-30 faces detected** across all photos
- **3-8 unique people** identified
- **2-5 cross-photo matches** found by auto-matching
- **60-80% confidence** for good matches
- **Individual face crops** displayed automatically
## 🎯 Pro Tips for Best Demo:
1. **Include repeat people**: Same person in 2-3 different photos
2. **Vary conditions**: Indoor/outdoor, different lighting
3. **Group + individual**: Mix of group photos and portraits
4. **Clear faces**: Avoid sunglasses, hats, or poor lighting
5. **Multiple angles**: Front-facing and slight profile views
---
**Ready for Enhanced Demo!** 🎉
Your demo will showcase:
- **Visual face recognition** with individual face display
- **Intelligent cross-photo matching** with confidence scoring
- **Privacy-first local processing** with professional features

23
demo_photos/README.md Normal file
View File

@ -0,0 +1,23 @@
# 📸 Demo Photos Setup
## 🎯 Quick Setup for Demo
1. **Add 6-10 photos with faces** to these folders:
- `family/` - Family photos (3-4 photos)
- `friends/` - Friend photos (2-3 photos)
- `events/` - Event photos (2-3 photos)
2. **Important**: Include some people in multiple photos for auto-matching demo
3. **Run demo**: See main `DEMO.md` file
## ✅ Current Status
- **3 photos** in `family/` folder
- **20 faces detected**
- **14 people identified**
- **Ready for demo!**
---
For complete setup instructions: `DEMO_INSTRUCTIONS.md`

View File

@ -0,0 +1 @@
Demo placeholder - replace with actual photos

View File

@ -9,17 +9,20 @@ import sqlite3
import argparse
import face_recognition
from pathlib import Path
from PIL import Image
from PIL import Image, ImageDraw, ImageFont
import pickle
import numpy as np
from typing import List, Dict, Tuple, Optional
import sys
import tempfile
import subprocess
class PhotoTagger:
def __init__(self, db_path: str = "photos.db"):
def __init__(self, db_path: str = "photos.db", verbose: int = 0):
"""Initialize the photo tagger with database"""
self.db_path = db_path
self.verbose = verbose
self.init_database()
def init_database(self):
@ -74,7 +77,8 @@ class PhotoTagger:
conn.commit()
conn.close()
print(f"✅ Database initialized: {self.db_path}")
if self.verbose >= 1:
print(f"✅ Database initialized: {self.db_path}")
def scan_folder(self, folder_path: str, recursive: bool = True) -> int:
"""Scan folder for photos and add to database"""
@ -114,6 +118,10 @@ class PhotoTagger:
)
if cursor.rowcount > 0:
added_count += 1
if self.verbose >= 2:
print(f" 📸 Added: {filename}")
elif self.verbose >= 3:
print(f" 📸 Already exists: {filename}")
except Exception as e:
print(f"⚠️ Error adding {filename}: {e}")
@ -150,22 +158,35 @@ class PhotoTagger:
try:
# Load image and find faces
print(f"📸 Processing: {filename}")
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}")
image = face_recognition.load_image_file(photo_path)
face_locations = face_recognition.face_locations(image, model=model)
if face_locations:
face_encodings = face_recognition.face_encodings(image, face_locations)
print(f" 👤 Found {len(face_locations)} faces")
if self.verbose >= 1:
print(f" 👤 Found {len(face_locations)} faces")
# Save faces to database
for encoding, location in zip(face_encodings, face_locations):
for i, (encoding, location) in enumerate(zip(face_encodings, face_locations)):
cursor.execute(
'INSERT INTO faces (photo_id, encoding, location) VALUES (?, ?, ?)',
(photo_id, encoding.tobytes(), str(location))
)
if self.verbose >= 3:
print(f" Face {i+1}: {location}")
else:
print(f" 👤 No faces found")
if self.verbose >= 1:
print(f" 👤 No faces found")
elif self.verbose >= 2:
print(f" 👤 {filename}: No faces found")
# Mark as processed
cursor.execute('UPDATE photos SET processed = 1 WHERE id = ?', (photo_id,))
@ -178,10 +199,12 @@ class PhotoTagger:
conn.commit()
conn.close()
if self.verbose == 0:
print() # New line after dots
print(f"✅ Processed {processed_count} photos")
return processed_count
def identify_faces(self, batch_size: int = 20) -> int:
def identify_faces(self, batch_size: int = 20, show_faces: bool = False) -> int:
"""Interactive face identification"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
@ -211,6 +234,21 @@ class PhotoTagger:
print(f"📁 Photo: {filename}")
print(f"📍 Face location: {location}")
# Extract and display face crop if enabled
face_crop_path = None
if show_faces:
face_crop_path = self._extract_face_crop(photo_path, location, face_id)
if face_crop_path:
print(f"🖼️ Face crop saved: {face_crop_path}")
try:
# Try to open the face crop with feh
subprocess.run(['feh', '--title', f'Face {i+1}/{len(unidentified)} - {filename}', face_crop_path],
check=False, capture_output=True)
except:
print(f"💡 Open face crop manually: feh {face_crop_path}")
else:
print("💡 Use --show-faces flag to display individual face crops")
while True:
command = input("👤 Person name (or command): ").strip()
@ -221,6 +259,12 @@ class PhotoTagger:
elif command.lower() == 's':
print("⏭️ Skipped")
# Clean up temporary face crop
if face_crop_path and os.path.exists(face_crop_path):
try:
os.remove(face_crop_path)
except:
pass # Ignore cleanup errors
break
elif command.lower() == 'list':
@ -242,6 +286,13 @@ class PhotoTagger:
print(f"✅ Identified as: {command}")
identified_count += 1
# Clean up temporary face crop
if face_crop_path and os.path.exists(face_crop_path):
try:
os.remove(face_crop_path)
except:
pass # Ignore cleanup errors
break
except Exception as e:
@ -255,6 +306,113 @@ class PhotoTagger:
print(f"\n✅ Identified {identified_count} faces")
return identified_count
def _extract_face_crop(self, photo_path: str, location: tuple, face_id: int) -> str:
"""Extract and save individual face crop for identification"""
try:
# Parse location tuple from string format
if isinstance(location, str):
location = eval(location)
top, right, bottom, left = location
# Load the image
image = Image.open(photo_path)
# Add padding around the face (20% of face size)
face_width = right - left
face_height = bottom - top
padding_x = int(face_width * 0.2)
padding_y = int(face_height * 0.2)
# Calculate crop bounds with padding
crop_left = max(0, left - padding_x)
crop_top = max(0, top - padding_y)
crop_right = min(image.width, right + padding_x)
crop_bottom = min(image.height, bottom + padding_y)
# Crop the face
face_crop = image.crop((crop_left, crop_top, crop_right, crop_bottom))
# Create temporary file for the face crop
temp_dir = tempfile.gettempdir()
face_filename = f"face_{face_id}_crop.jpg"
face_path = os.path.join(temp_dir, face_filename)
# Resize for better viewing (minimum 200px width)
if face_crop.width < 200:
ratio = 200 / face_crop.width
new_width = 200
new_height = int(face_crop.height * ratio)
face_crop = face_crop.resize((new_width, new_height), Image.Resampling.LANCZOS)
face_crop.save(face_path, "JPEG", quality=95)
return face_path
except Exception as e:
if self.verbose >= 1:
print(f"⚠️ Could not extract face crop: {e}")
return None
def _create_comparison_image(self, unid_crop_path: str, match_crop_path: str, person_name: str, confidence: float) -> str:
"""Create a side-by-side comparison image"""
try:
# Load both face crops
unid_img = Image.open(unid_crop_path)
match_img = Image.open(match_crop_path)
# Resize both to same height for better comparison
target_height = 300
unid_ratio = target_height / unid_img.height
match_ratio = target_height / match_img.height
unid_resized = unid_img.resize((int(unid_img.width * unid_ratio), target_height), Image.Resampling.LANCZOS)
match_resized = match_img.resize((int(match_img.width * match_ratio), target_height), Image.Resampling.LANCZOS)
# Create comparison image
total_width = unid_resized.width + match_resized.width + 20 # 20px gap
comparison = Image.new('RGB', (total_width, target_height + 60), 'white')
# Paste images
comparison.paste(unid_resized, (0, 30))
comparison.paste(match_resized, (unid_resized.width + 20, 30))
# Add labels
draw = ImageDraw.Draw(comparison)
try:
# Try to use a font
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16)
except:
font = ImageFont.load_default()
draw.text((10, 5), "UNKNOWN", fill='red', font=font)
draw.text((unid_resized.width + 30, 5), f"{person_name.upper()}", fill='green', font=font)
draw.text((10, target_height + 35), f"Confidence: {confidence:.1%}", fill='blue', font=font)
# Save comparison image
temp_dir = tempfile.gettempdir()
comparison_path = os.path.join(temp_dir, f"face_comparison_{person_name}.jpg")
comparison.save(comparison_path, "JPEG", quality=95)
return comparison_path
except Exception as e:
if self.verbose >= 1:
print(f"⚠️ Could not create comparison image: {e}")
return None
def _get_confidence_description(self, confidence_pct: float) -> str:
"""Get human-readable confidence description"""
if confidence_pct >= 80:
return "🟢 (Very High - Almost Certain)"
elif confidence_pct >= 70:
return "🟡 (High - Likely Match)"
elif confidence_pct >= 60:
return "🟠 (Medium - Possible Match)"
elif confidence_pct >= 50:
return "🔴 (Low - Questionable)"
else:
return "⚫ (Very Low - Unlikely)"
def _show_people_list(self, cursor):
"""Show list of known people"""
cursor.execute('SELECT name FROM people ORDER BY name')
@ -343,7 +501,7 @@ class PhotoTagger:
LEFT JOIN faces f ON p.id = f.person_id
GROUP BY p.id
ORDER BY face_count DESC
LIMIT 5
LIMIT 15
''')
stats['top_people'] = cursor.fetchall()
@ -393,6 +551,275 @@ class PhotoTagger:
return [path for filename, path in results]
def find_similar_faces(self, face_id: int = None, tolerance: float = 0.6, include_same_photo: bool = False) -> List[Dict]:
"""Find similar faces across all photos"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
if face_id:
# Find faces similar to a specific face
cursor.execute('''
SELECT id, photo_id, encoding, location
FROM faces
WHERE id = ?
''', (face_id,))
target_face = cursor.fetchone()
if not target_face:
print(f"❌ Face ID {face_id} not found")
conn.close()
return []
target_encoding = np.frombuffer(target_face[2], dtype=np.float64)
# Get all other faces
cursor.execute('''
SELECT f.id, f.photo_id, f.encoding, f.location, p.filename, f.person_id
FROM faces f
JOIN photos p ON f.photo_id = p.id
WHERE f.id != ?
''', (face_id,))
else:
# Find all unidentified faces and try to match them with identified ones
cursor.execute('''
SELECT f.id, f.photo_id, f.encoding, f.location, p.filename, f.person_id
FROM faces f
JOIN photos p ON f.photo_id = p.id
ORDER BY f.id
''')
all_faces = cursor.fetchall()
matches = []
if face_id:
# Compare target face with all other faces
for face_data in all_faces:
other_id, other_photo_id, other_encoding, other_location, other_filename, other_person_id = face_data
other_enc = np.frombuffer(other_encoding, dtype=np.float64)
distance = face_recognition.face_distance([target_encoding], other_enc)[0]
if distance <= tolerance:
matches.append({
'face_id': other_id,
'photo_id': other_photo_id,
'filename': other_filename,
'location': other_location,
'distance': distance,
'person_id': other_person_id
})
# Get target photo info
cursor.execute('SELECT filename FROM photos WHERE id = ?', (target_face[1],))
target_filename = cursor.fetchone()[0]
print(f"\n🔍 Finding faces similar to face in: {target_filename}")
print(f"📍 Target face location: {target_face[3]}")
else:
# Auto-match unidentified faces with identified ones
identified_faces = [f for f in all_faces if f[5] is not None] # person_id is not None
unidentified_faces = [f for f in all_faces if f[5] is None] # person_id is None
print(f"\n🔍 Auto-matching {len(unidentified_faces)} unidentified faces with {len(identified_faces)} known faces...")
for unid_face in unidentified_faces:
unid_id, unid_photo_id, unid_encoding, unid_location, unid_filename, _ = unid_face
unid_enc = np.frombuffer(unid_encoding, dtype=np.float64)
best_match = None
best_distance = float('inf')
for id_face in identified_faces:
id_id, id_photo_id, id_encoding, id_location, id_filename, id_person_id = id_face
id_enc = np.frombuffer(id_encoding, dtype=np.float64)
# Skip if same photo (unless specifically requested for twins detection)
if not include_same_photo and unid_photo_id == id_photo_id:
continue
distance = face_recognition.face_distance([unid_enc], id_enc)[0]
if distance <= tolerance and distance < best_distance:
best_distance = distance
best_match = {
'unidentified_id': unid_id,
'unidentified_photo_id': unid_photo_id,
'unidentified_filename': unid_filename,
'unidentified_location': unid_location,
'matched_id': id_id,
'matched_photo_id': id_photo_id,
'matched_filename': id_filename,
'matched_location': id_location,
'person_id': id_person_id,
'distance': distance
}
if best_match:
matches.append(best_match)
conn.close()
return matches
def auto_identify_matches(self, tolerance: float = 0.6, confirm: bool = True, show_faces: bool = False, include_same_photo: bool = False) -> int:
"""Automatically identify faces that match already identified faces"""
matches = self.find_similar_faces(tolerance=tolerance, include_same_photo=include_same_photo)
if not matches:
print("🔍 No similar faces found for auto-identification")
return 0
print(f"\n🎯 Found {len(matches)} potential matches:")
print("📊 Confidence Guide: 🟢80%+ = Very High, 🟡70%+ = High, 🟠60%+ = Medium, 🔴50%+ = Low, ⚫<50% = Very Low")
identified_count = 0
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
for i, match in enumerate(matches):
# Get person name and photo paths
cursor.execute('SELECT name FROM people WHERE id = ?', (match['person_id'],))
person_name = cursor.fetchone()[0]
cursor.execute('SELECT path FROM photos WHERE id = ?', (match['matched_photo_id'],))
matched_photo_path = cursor.fetchone()[0]
cursor.execute('SELECT path FROM photos WHERE filename = ?', (match['unidentified_filename'],))
unidentified_photo_path = cursor.fetchone()[0]
print(f"\n--- Match {i+1}/{len(matches)} ---")
print(f"🆔 Unidentified face in: {match['unidentified_filename']}")
print(f"📍 Location: {match['unidentified_location']}")
print(f"👥 Potential match: {person_name}")
print(f"📸 From photo: {match['matched_filename']}")
confidence_pct = (1-match['distance']) * 100
confidence_desc = self._get_confidence_description(confidence_pct)
print(f"🎯 Confidence: {confidence_pct:.1f}% {confidence_desc} (distance: {match['distance']:.3f})")
# Show face crops if enabled
unidentified_crop_path = None
matched_crop_path = None
if show_faces:
# Extract unidentified face crop
unidentified_crop_path = self._extract_face_crop(
unidentified_photo_path,
match['unidentified_location'],
f"unid_{match['unidentified_id']}"
)
# Extract matched face crop
matched_crop_path = self._extract_face_crop(
matched_photo_path,
match['matched_location'],
f"match_{match['matched_id']}"
)
if unidentified_crop_path and matched_crop_path:
print(f"🖼️ Extracting faces for comparison...")
# Create side-by-side comparison image
comparison_path = self._create_comparison_image(
unidentified_crop_path,
matched_crop_path,
person_name,
1 - match['distance']
)
if comparison_path:
print(f"🔍 Comparison image: {comparison_path}")
try:
# Open the comparison image in background (non-blocking)
subprocess.Popen(['feh', '--title', f'Face Comparison: Unknown vs {person_name}',
comparison_path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print("👀 Check the image window to compare faces!")
print("💡 The image window won't block your input - you can decide while viewing!")
except Exception as e:
print(f"⚠️ Could not auto-open comparison: {e}")
print(f"💡 Open manually: feh {comparison_path}")
else:
# Fallback to separate images
print(f"🖼️ Unidentified: {unidentified_crop_path}")
print(f"🖼️ Known ({person_name}): {matched_crop_path}")
print(f"💡 Compare manually: feh {unidentified_crop_path} {matched_crop_path}")
elif unidentified_crop_path:
print(f"🖼️ Unidentified face only: {unidentified_crop_path}")
print(f"💡 Open with: feh {unidentified_crop_path}")
else:
print("⚠️ Could not extract face crops for comparison")
if confirm:
while True:
response = input("🤔 Identify as this person? (y/n/s=skip/q=quit): ").strip().lower()
if response == 'q':
# Clean up face crops before quitting
if unidentified_crop_path and os.path.exists(unidentified_crop_path):
try: os.remove(unidentified_crop_path)
except: pass
if matched_crop_path and os.path.exists(matched_crop_path):
try: os.remove(matched_crop_path)
except: pass
conn.close()
return identified_count
elif response == 's':
break
elif response == 'y':
# Assign the face to the person
cursor.execute(
'UPDATE faces SET person_id = ? WHERE id = ?',
(match['person_id'], match['unidentified_id'])
)
print(f"✅ Identified as: {person_name}")
identified_count += 1
break
elif response == 'n':
print("⏭️ Not a match")
break
else:
print("Please enter 'y' (yes), 'n' (no), 's' (skip), or 'q' (quit)")
# Clean up face crops after each match
if unidentified_crop_path and os.path.exists(unidentified_crop_path):
try: os.remove(unidentified_crop_path)
except: pass
if matched_crop_path and os.path.exists(matched_crop_path):
try: os.remove(matched_crop_path)
except: pass
# Clean up comparison image
comparison_files = [f"/tmp/face_comparison_{person_name}.jpg"]
for comp_file in comparison_files:
if os.path.exists(comp_file):
try: os.remove(comp_file)
except: pass
else:
# Auto-identify without confirmation if confidence is high
if match['distance'] <= 0.4: # High confidence threshold
cursor.execute(
'UPDATE faces SET person_id = ? WHERE id = ?',
(match['person_id'], match['unidentified_id'])
)
print(f"✅ Auto-identified as: {person_name} (high confidence)")
identified_count += 1
else:
print(f"⚠️ Skipped (low confidence: {(1-match['distance']):.1%})")
# Clean up face crops for auto mode too
if unidentified_crop_path and os.path.exists(unidentified_crop_path):
try: os.remove(unidentified_crop_path)
except: pass
if matched_crop_path and os.path.exists(matched_crop_path):
try: os.remove(matched_crop_path)
except: pass
conn.commit()
conn.close()
print(f"\n✅ Auto-identified {identified_count} faces")
return identified_count
def main():
"""Main CLI interface"""
@ -404,6 +831,8 @@ Examples:
photo_tagger.py scan /path/to/photos # Scan folder for photos
photo_tagger.py process --limit 20 # Process 20 photos for faces
photo_tagger.py identify --batch 10 # Identify 10 faces interactively
photo_tagger.py auto-match # Auto-identify matching faces
photo_tagger.py match 15 # Find faces similar to face ID 15
photo_tagger.py tag --pattern "vacation" # Tag photos matching pattern
photo_tagger.py search "John" # Find photos with John
photo_tagger.py stats # Show statistics
@ -411,7 +840,7 @@ Examples:
)
parser.add_argument('command',
choices=['scan', 'process', 'identify', 'tag', 'search', 'stats'],
choices=['scan', 'process', 'identify', 'tag', 'search', 'stats', 'match', 'auto-match'],
help='Command to execute')
parser.add_argument('target', nargs='?',
@ -435,10 +864,25 @@ Examples:
parser.add_argument('--recursive', action='store_true',
help='Scan folders recursively')
parser.add_argument('--show-faces', action='store_true',
help='Show individual face crops during identification')
parser.add_argument('--tolerance', type=float, default=0.5,
help='Face matching tolerance (0.0-1.0, lower = stricter, default: 0.5)')
parser.add_argument('--auto', action='store_true',
help='Auto-identify high-confidence matches without confirmation')
parser.add_argument('--include-twins', action='store_true',
help='Include same-photo matching (for twins or multiple instances)')
parser.add_argument('-v', '--verbose', action='count', default=0,
help='Increase verbosity (-v, -vv, -vvv for more detail)')
args = parser.parse_args()
# Initialize tagger
tagger = PhotoTagger(args.db)
tagger = PhotoTagger(args.db, args.verbose)
try:
if args.command == 'scan':
@ -451,7 +895,8 @@ Examples:
tagger.process_faces(args.limit, args.model)
elif args.command == 'identify':
tagger.identify_faces(args.batch)
show_faces = getattr(args, 'show_faces', False)
tagger.identify_faces(args.batch, show_faces)
elif args.command == 'tag':
tagger.add_tags(args.pattern or args.target, args.batch)
@ -465,6 +910,25 @@ Examples:
elif args.command == 'stats':
tagger.stats()
elif args.command == 'match':
if args.target and args.target.isdigit():
face_id = int(args.target)
matches = tagger.find_similar_faces(face_id, args.tolerance)
if matches:
print(f"\n🎯 Found {len(matches)} similar faces:")
for match in matches:
person_name = "Unknown" if match['person_id'] is None else f"Person ID {match['person_id']}"
print(f" 📸 {match['filename']} - {person_name} (confidence: {(1-match['distance']):.1%})")
else:
print("🔍 No similar faces found")
else:
print("❌ Please specify a face ID number to find matches for")
elif args.command == 'auto-match':
show_faces = getattr(args, 'show_faces', False)
include_twins = getattr(args, 'include_twins', False)
tagger.auto_identify_matches(args.tolerance, not args.auto, show_faces, include_twins)
return 0
except KeyboardInterrupt: