Updated
This commit is contained in:
parent
cb61d24bc3
commit
85d5c120d4
162
DEMO.md
Normal file
162
DEMO.md
Normal 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.
|
||||
55
README.md
55
README.md
@ -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
|
||||
```
|
||||
@ -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
193
demo.sh
Executable 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)"
|
||||
92
demo_photos/DEMO_INSTRUCTIONS.md
Normal file
92
demo_photos/DEMO_INSTRUCTIONS.md
Normal 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
23
demo_photos/README.md
Normal 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`
|
||||
1
demo_photos/events/demo_events.txt
Normal file
1
demo_photos/events/demo_events.txt
Normal file
@ -0,0 +1 @@
|
||||
Demo placeholder - replace with actual photos
|
||||
488
photo_tagger.py
488
photo_tagger.py
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user