diff --git a/DEMO.md b/DEMO.md new file mode 100644 index 0000000..d61ff21 --- /dev/null +++ b/DEMO.md @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index 30fc53b..2cebc3f 100644 --- a/README.md +++ b/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 ``` \ No newline at end of file diff --git a/REBUILD_SUMMARY.md b/REBUILD_SUMMARY.md deleted file mode 100644 index cd91f9d..0000000 --- a/REBUILD_SUMMARY.md +++ /dev/null @@ -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. diff --git a/demo.sh b/demo.sh new file mode 100755 index 0000000..60a456f --- /dev/null +++ b/demo.sh @@ -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)" \ No newline at end of file diff --git a/demo_photos/DEMO_INSTRUCTIONS.md b/demo_photos/DEMO_INSTRUCTIONS.md new file mode 100644 index 0000000..85aaf37 --- /dev/null +++ b/demo_photos/DEMO_INSTRUCTIONS.md @@ -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 \ No newline at end of file diff --git a/demo_photos/README.md b/demo_photos/README.md new file mode 100644 index 0000000..1ab590a --- /dev/null +++ b/demo_photos/README.md @@ -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` \ No newline at end of file diff --git a/demo_photos/events/demo_events.txt b/demo_photos/events/demo_events.txt new file mode 100644 index 0000000..16f7d4e --- /dev/null +++ b/demo_photos/events/demo_events.txt @@ -0,0 +1 @@ +Demo placeholder - replace with actual photos diff --git a/photo_tagger.py b/photo_tagger.py index 64b2906..6b7d4d4 100644 --- a/photo_tagger.py +++ b/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: