diff --git a/README.md b/README.md index 2cebc3f..0d9f376 100644 --- a/README.md +++ b/README.md @@ -1,307 +1,597 @@ -# PunimTag CLI - Minimal Photo Face Tagger - -A simple command-line tool for automatic face recognition and photo tagging. No web interface, no complex dependencies - just the essentials. - -## ๐Ÿš€ Quick Start - -```bash -# 1. Setup (one time only) -git clone -cd PunimTag -python3 -m venv venv -source venv/bin/activate # On Windows: venv\Scripts\activate -python3 setup.py - -# 2. Scan photos -python3 photo_tagger.py scan /path/to/your/photos - -# 3. Process faces -python3 photo_tagger.py process - -# 4. Identify faces with visual display -python3 photo_tagger.py identify --show-faces - -# 5. Auto-match faces across photos -python3 photo_tagger.py auto-match --show-faces - -# 6. View statistics -python3 photo_tagger.py stats -``` - -## ๐Ÿ“ฆ Installation - -### Automatic Setup (Recommended) -```bash -# Clone and setup -git clone -cd PunimTag - -# Create virtual environment (IMPORTANT!) -python3 -m venv venv -source venv/bin/activate # On Windows: venv\Scripts\activate - -# Run setup script -python3 setup.py -``` - -**โš ๏ธ IMPORTANT**: Always activate the virtual environment before running any commands: -```bash -source venv/bin/activate # Run this every time you open a new terminal -``` - -### Manual Setup (Alternative) -```bash -python3 -m venv venv -source venv/bin/activate -pip install -r requirements.txt -python3 photo_tagger.py stats # Creates database -``` - -## ๐ŸŽฏ Commands - -### Scan for Photos -```bash -# Scan a folder -python3 photo_tagger.py scan /path/to/photos - -# Scan recursively (recommended) -python3 photo_tagger.py scan /path/to/photos --recursive -``` - -### Process Photos for Faces -```bash -# Process 50 photos (default) -python3 photo_tagger.py process - -# Process 20 photos with CNN model (more accurate) -python3 photo_tagger.py process --limit 20 --model cnn - -# Process with HOG model (faster) -python3 photo_tagger.py process --limit 100 --model hog -``` - -### Identify Faces (Enhanced!) -```bash -# Identify with individual face display (RECOMMENDED) -python3 photo_tagger.py identify --show-faces --batch 10 - -# 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 -``` - -**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 -- `list` = show known people - -### Add Tags -```bash -# Tag photos matching pattern -python3 photo_tagger.py tag --pattern "vacation" - -# Tag any photos -python3 photo_tagger.py tag -``` - -### Search -```bash -# Find photos with a person -python3 photo_tagger.py search "John" - -# Find photos with partial name match -python3 photo_tagger.py search "Joh" -``` - -### Statistics -```bash -# View database statistics -python3 photo_tagger.py stats -``` - -## ๐Ÿ“Š Enhanced Example Workflow - -```bash -# ALWAYS activate virtual environment first! -source venv/bin/activate - -# 1. Scan your photo collection -python3 photo_tagger.py scan ~/Pictures --recursive - -# 2. Process photos for faces (start with small batch) -python3 photo_tagger.py process --limit 20 - -# 3. Check what we found -python3 photo_tagger.py stats - -# 4. Identify faces with visual display (ENHANCED!) -python3 photo_tagger.py identify --show-faces --batch 10 - -# 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" - -# 7. Add some tags -python3 photo_tagger.py tag --pattern "birthday" -``` - -## ๐Ÿ—ƒ๏ธ Database - -The tool uses SQLite database (`photos.db` by default) with these tables: -- **photos** - Photo file paths and processing status -- **people** - Known people names -- **faces** - Face encodings and locations -- **tags** - Custom tags for photos - -## โš™๏ธ Configuration - -### Face Detection Models -- **hog** - Faster, good for CPU-only systems -- **cnn** - More accurate, requires more processing power - -### Database Location -```bash -# Use custom database file -python3 photo_tagger.py scan /photos --db /path/to/my.db -``` - -## ๐Ÿ”ง System Requirements - -### Required System Packages (Ubuntu/Debian) -```bash -sudo apt update -sudo apt install -y cmake build-essential libopenblas-dev liblapack-dev libx11-dev libgtk-3-dev python3-dev python3-venv -``` - -### Python Dependencies -- `face-recognition` - Face detection and recognition -- `dlib` - Machine learning library -- `pillow` - Image processing -- `numpy` - Numerical operations -- `click` - Command line interface -- `setuptools` - Package management - -## ๐Ÿ“ File Structure - -``` -PunimTag/ -โ”œโ”€โ”€ photo_tagger.py # Main CLI tool -โ”œโ”€โ”€ setup.py # Setup script -โ”œโ”€โ”€ run.sh # Convenience script (auto-activates venv) -โ”œโ”€โ”€ requirements.txt # Python dependencies -โ”œโ”€โ”€ README.md # This file -โ”œโ”€โ”€ venv/ # Virtual environment (created by setup) -โ”œโ”€โ”€ photos.db # Database (created automatically) -โ”œโ”€โ”€ data/ # Additional data files -โ””โ”€โ”€ logs/ # Log files -``` - -## ๐Ÿšจ Troubleshooting - -### "externally-managed-environment" Error -**Solution**: Always use a virtual environment! -```bash -python3 -m venv venv -source venv/bin/activate -python3 setup.py -``` - -### Virtual Environment Not Active -**Problem**: Commands fail or use wrong Python -**Solution**: Always activate the virtual environment: -```bash -source venv/bin/activate -# You should see (venv) in your prompt -``` - -### dlib Installation Issues -```bash -# Ubuntu/Debian - install system dependencies first -sudo apt-get install build-essential cmake libopenblas-dev - -# Then retry setup -source venv/bin/activate -python3 setup.py -``` - -### "Please install face_recognition_models" Warning -This warning is harmless - the application still works correctly. It's a known issue with Python 3.13. - -### Memory Issues -- Use `--model hog` for faster processing -- Process in smaller batches with `--limit 10` -- Close other applications to free memory - -### No Faces Found -- Check image quality and lighting -- Ensure faces are clearly visible -- Try `--model cnn` for better detection - -## ๐ŸŽฏ What This Tool Does - -โœ… **Simple**: Single Python file, minimal dependencies -โœ… **Fast**: Efficient face detection and recognition -โœ… **Private**: Everything runs locally, no cloud services -โœ… **Flexible**: Batch processing, interactive identification -โœ… **Lightweight**: No web interface overhead - -## ๐Ÿ“ˆ Performance Tips - -- **Always use virtual environment** to avoid conflicts -- Start with small batches (`--limit 20`) to test -- Use `hog` model for speed, `cnn` for accuracy -- Process photos in smaller folders first -- Identify faces in batches to avoid fatigue - -## ๐Ÿค Contributing - -This is now a minimal, focused tool. Key principles: -- Keep it simple and fast -- CLI-only interface -- Minimal dependencies -- Clear, readable code -- **Always use python3** commands - ---- - -**Total project size**: ~300 lines of Python code -**Dependencies**: 6 essential packages -**Setup time**: ~5 minutes -**Perfect for**: Batch processing personal photo collections - -## ๐Ÿ”„ Common Commands Cheat Sheet - -```bash -# Setup (one time) -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 --show-faces --batch 10 -./run.sh auto-match --show-faces -./run.sh stats - -# 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 --show-faces --batch 10 -python3 photo_tagger.py auto-match --show-faces -python3 photo_tagger.py stats +# PunimTag CLI - Minimal Photo Face Tagger + +A simple command-line tool for automatic face recognition and photo tagging. No web interface, no complex dependencies - just the essentials. + +## ๐Ÿ“‹ System Requirements + +### Minimum Requirements +- **Python**: 3.7 or higher +- **Operating System**: Linux, macOS, or Windows +- **RAM**: 2GB+ (4GB+ recommended for large photo collections) +- **Storage**: 100MB for application + space for photos and database +- **Display**: X11 display server (Linux) or equivalent for image viewing + +### Supported Platforms +- โœ… **Ubuntu/Debian** (fully supported with automatic dependency installation) +- โœ… **macOS** (manual dependency installation required) +- โœ… **Windows** (with WSL or manual setup) +- โš ๏ธ **Other Linux distributions** (manual dependency installation required) + +### What Gets Installed Automatically (Ubuntu/Debian) +The setup script automatically installs these system packages: +- **Build tools**: `cmake`, `build-essential` +- **Math libraries**: `libopenblas-dev`, `liblapack-dev` (for face recognition) +- **GUI libraries**: `libx11-dev`, `libgtk-3-dev`, `libboost-python-dev` +- **Image viewer**: `feh` (for face identification interface) + +## ๐Ÿš€ Quick Start + +```bash +# 1. Setup (one time only) - installs all dependencies including image viewer +git clone +cd PunimTag +python3 -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +python3 setup.py # Installs system deps + Python packages + +# 2. Scan photos +python3 photo_tagger.py scan /path/to/your/photos + +# 3. Process faces +python3 photo_tagger.py process + +# 4. Identify faces with visual display +python3 photo_tagger.py identify --show-faces + +# 5. Auto-match faces across photos (with improved algorithm) +python3 photo_tagger.py auto-match --show-faces + +# 6. View and modify identified faces (NEW!) +python3 photo_tagger.py modifyidentified + +# 7. View statistics +python3 photo_tagger.py stats +``` + +## ๐Ÿ“ฆ Installation + +### Automatic Setup (Recommended) +```bash +# Clone and setup +git clone +cd PunimTag + +# Create virtual environment (IMPORTANT!) +python3 -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Run setup script +python3 setup.py +``` + +**โš ๏ธ IMPORTANT**: Always activate the virtual environment before running any commands: +```bash +source venv/bin/activate # Run this every time you open a new terminal +``` + +### Manual Setup (Alternative) +```bash +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +python3 photo_tagger.py stats # Creates database +``` + +## ๐ŸŽฏ Commands + +### Scan for Photos +```bash +# Scan a folder +python3 photo_tagger.py scan /path/to/photos + +# Scan recursively (recommended) +python3 photo_tagger.py scan /path/to/photos --recursive +``` + +### Process Photos for Faces (with Quality Scoring) +```bash +# Process 50 photos (default) - now includes face quality scoring +python3 photo_tagger.py process + +# Process 20 photos with CNN model (more accurate) +python3 photo_tagger.py process --limit 20 --model cnn + +# Process with HOG model (faster) +python3 photo_tagger.py process --limit 100 --model hog +``` + +**๐Ÿ”ฌ Quality Scoring Features:** +- **Automatic Assessment** - Each face gets a quality score (0.0-1.0) based on multiple factors +- **Smart Filtering** - Only faces above quality threshold (โ‰ฅ0.2) are used for matching +- **Quality Metrics** - Evaluates sharpness, brightness, contrast, size, aspect ratio, and position +- **Verbose Output** - Use `--verbose` to see quality scores during processing + +### Identify Faces (GUI-Enhanced!) +```bash +# Identify with GUI interface and face display (RECOMMENDED) +python3 photo_tagger.py identify --show-faces --batch 10 + +# GUI mode without face crops (coordinates only) +python3 photo_tagger.py identify --batch 10 + +# Auto-match faces across photos with GUI (NEW!) +python3 photo_tagger.py auto-match --show-faces + +# Auto-identify high-confidence matches +python3 photo_tagger.py auto-match --auto --show-faces +``` + +**๐ŸŽฏ New GUI-Based Identification Features:** +- ๐Ÿ–ผ๏ธ **Visual Face Display** - See individual face crops in the GUI +- ๐Ÿ“ **Dropdown Name Selection** - Choose from known people or type new names +- โ˜‘๏ธ **Compare with Similar Faces** - Compare current face with similar unidentified faces +- ๐ŸŽจ **Modern Interface** - Clean, intuitive GUI with buttons and input fields +- ๐Ÿ’พ **Window Size Memory** - Remembers your preferred window size +- ๐Ÿšซ **No Terminal Input** - All interaction through the GUI interface +- โฌ…๏ธ **Back Navigation** - Go back to previous faces (shows images and identification status) +- ๐Ÿ”„ **Re-identification** - Change identifications by going back and re-identifying +- ๐Ÿ’พ **Auto-Save** - All identifications are saved immediately (no need to save manually) +- โ˜‘๏ธ **Select All/Clear All** - Bulk selection buttons for similar faces (enabled only when Compare is active) +- โš ๏ธ **Smart Navigation Warnings** - Prevents accidental loss of selected similar faces +- ๐Ÿ’พ **Quit Confirmation** - Saves pending identifications when closing the application +- โšก **Performance Optimized** - Pre-fetched data for faster similar faces display + +**๐ŸŽฏ New Auto-Match GUI Features:** +- ๐Ÿ“Š **Person-Centric View** - Shows matched person on left, all their unidentified faces on right +- โ˜‘๏ธ **Checkbox Selection** - Select which unidentified faces to identify with this person +- ๐Ÿ“ˆ **Confidence Percentages** - Color-coded match confidence levels +- ๐Ÿ–ผ๏ธ **Side-by-Side Layout** - Matched person on left, unidentified faces on right +- ๐Ÿ“œ **Scrollable Matches** - Handle many potential matches easily +- ๐ŸŽฎ **Enhanced Controls** - Back, Next, or Quit buttons (navigation only) +- ๐Ÿ’พ **Smart Save Button** - "Save changes for [Person Name]" button in left panel +- ๐Ÿ”„ **State Persistence** - Checkbox selections preserved when navigating between people +- ๐Ÿšซ **Smart Navigation** - Next button disabled on last person, Back button disabled on first +- ๐Ÿ’พ **Bidirectional Changes** - Can both identify and unidentify faces in the same session +- โšก **Optimized Performance** - Efficient database queries and streamlined interface + +### View & Modify Identified Faces (NEW) +```bash +# Open the Modify Identified Faces interface +python3 photo_tagger.py modifyidentified +``` + +This GUI lets you quickly review all identified people, rename them, and temporarily unmatch faces before committing. + +**Left Panel (People):** +- ๐Ÿ‘ฅ **People List** - Shows all identified people with face counts +- ๐Ÿ–ฑ๏ธ **Clickable Names** - Click to select a person (selected name is bold) +- โœ๏ธ **Edit Name Icon** - Rename a person; tooltip shows "Update name" + +**Right Panel (Faces):** +- ๐Ÿงฉ **Person Faces** - Thumbnails of all faces identified as the selected person +- โŒ **X on Each Face** - Temporarily unmatch a face (does not save yet) +- โ†ถ **Undo Changes** - Restores unmatched faces for the current person only +- ๐Ÿ”„ **Responsive Grid** - Faces wrap to the next line when the panel is narrow + +**Bottom Controls:** +- ๐Ÿ’พ **Save changes** - Commits all pending unmatched faces across all people to the database +- โŒ **Quit** - Closes the window (unsaved temporary changes are discarded) + +Notes: +- Changes are temporary until you click "Save changes" at the bottom. +- Undo restores only the currently viewed person's faces. +- Saving updates the database and refreshes counts. + +## ๐Ÿง  Advanced Algorithm Features + +**๐ŸŽฏ Intelligent Face Matching Engine:** +- ๐Ÿ” **Face Quality Scoring** - Automatically evaluates face quality based on sharpness, brightness, contrast, size, and position +- ๐Ÿ“Š **Adaptive Tolerance** - Adjusts matching strictness based on face quality (higher quality = stricter matching) +- ๐Ÿšซ **Quality Filtering** - Only processes faces above minimum quality threshold (โ‰ฅ0.2) for better accuracy +- ๐ŸŽฏ **Smart Matching** - Uses multiple quality factors to determine the best matches +- โšก **Performance Optimized** - Efficient database queries with quality-based indexing + +**๐Ÿ”ฌ Quality Assessment Metrics:** +- **Sharpness Detection** - Uses Laplacian variance to detect blurry faces +- **Brightness Analysis** - Prefers faces with optimal lighting conditions +- **Contrast Evaluation** - Higher contrast faces score better for recognition +- **Size Optimization** - Larger, clearer faces get higher quality scores +- **Aspect Ratio** - Prefers square face crops for better recognition +- **Position Scoring** - Centered faces in photos score higher + +**๐Ÿ“ˆ Confidence Levels:** +- ๐ŸŸข **Very High (80%+)** - Almost Certain match +- ๐ŸŸก **High (70%+)** - Likely Match +- ๐ŸŸ  **Medium (60%+)** - Possible Match +- ๐Ÿ”ด **Low (50%+)** - Questionable +- โšซ **Very Low (<50%)** - Unlikely + +**GUI Interactive Elements:** +- **Person Name Dropdown** - Select from known people or type new names +- **Compare Checkbox** - Compare with similar unidentified faces (persistent setting) +- **Identify Button** - Confirm the identification (saves immediately) +- **Back Button** - Go back to previous face (shows image and identification status) +- **Next Button** - Move to next face +- **Quit Button** - Exit application (all changes already saved) + +### Add Tags +```bash +# Tag photos matching pattern +python3 photo_tagger.py tag --pattern "vacation" + +# Tag any photos +python3 photo_tagger.py tag +``` + +### Search +```bash +# Find photos with a person +python3 photo_tagger.py search "John" + +# Find photos with partial name match +python3 photo_tagger.py search "Joh" +``` + +### Statistics +```bash +# View database statistics +python3 photo_tagger.py stats +``` + +## ๐Ÿ“Š Enhanced Example Workflow + +```bash +# ALWAYS activate virtual environment first! +source venv/bin/activate + +# 1. Scan your photo collection +python3 photo_tagger.py scan ~/Pictures --recursive + +# 2. Process photos for faces (start with small batch) +python3 photo_tagger.py process --limit 20 + +# 3. Check what we found +python3 photo_tagger.py stats + +# 4. Identify faces with GUI interface (ENHANCED!) +python3 photo_tagger.py identify --show-faces --batch 10 + +# 5. Auto-match faces across photos with GUI (NEW!) +python3 photo_tagger.py auto-match --show-faces + +# 6. Search for photos of someone +python3 photo_tagger.py search "Alice" + +# 7. Add some tags +python3 photo_tagger.py tag --pattern "birthday" +``` + +## ๐Ÿ—ƒ๏ธ Database + +The tool uses SQLite database (`data/photos.db` by default) with these tables: +- **photos** - Photo file paths and processing status +- **people** - Known people names +- **faces** - Face encodings and locations +- **tags** - Custom tags for photos + +## โš™๏ธ Configuration + +### Face Detection Models +- **hog** - Faster, good for CPU-only systems +- **cnn** - More accurate, requires more processing power + +### Database Location +```bash +# Use custom database file +python3 photo_tagger.py scan /photos --db /path/to/my.db +``` + +## ๐Ÿ”ง System Requirements + +### Required System Packages (Ubuntu/Debian) +```bash +sudo apt update +sudo apt install -y cmake build-essential libopenblas-dev liblapack-dev libx11-dev libgtk-3-dev python3-dev python3-venv +``` + +### Python Dependencies +- `face-recognition` - Face detection and recognition +- `dlib` - Machine learning library +- `pillow` - Image processing +- `numpy` - Numerical operations +- `click` - Command line interface +- `setuptools` - Package management + +## ๐Ÿ“ File Structure + +``` +PunimTag/ +โ”œโ”€โ”€ photo_tagger.py # Main CLI tool +โ”œโ”€โ”€ setup.py # Setup script +โ”œโ”€โ”€ run.sh # Convenience script (auto-activates venv) +โ”œโ”€โ”€ requirements.txt # Python dependencies +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ gui_config.json # GUI window size preferences (created automatically) +โ”œโ”€โ”€ venv/ # Virtual environment (created by setup) +โ”œโ”€โ”€ data/ +โ”‚ โ””โ”€โ”€ photos.db # Database (created automatically) +โ”œโ”€โ”€ data/ # Additional data files +โ””โ”€โ”€ logs/ # Log files +``` + +## ๐Ÿšจ Troubleshooting + +### "externally-managed-environment" Error +**Solution**: Always use a virtual environment! +```bash +python3 -m venv venv +source venv/bin/activate +python3 setup.py +``` + +### Virtual Environment Not Active +**Problem**: Commands fail or use wrong Python +**Solution**: Always activate the virtual environment: +```bash +source venv/bin/activate +# You should see (venv) in your prompt +``` + +### Image Viewer Not Opening During Identify +**Problem**: Face crops are saved but don't open automatically +**Solution**: The setup script installs `feh` (image viewer) automatically on Ubuntu/Debian. For other systems: +- **Ubuntu/Debian**: `sudo apt install feh` +- **macOS**: `brew install feh` +- **Windows**: Install a Linux subsystem or use WSL +- **Alternative**: Use `--show-faces` flag without auto-opening - face crops will be saved to `/tmp/` for manual viewing + +### GUI Interface Issues +**Problem**: GUI doesn't appear or has issues +**Solution**: The tool now uses tkinter for all identification interfaces: +- **Ubuntu/Debian**: `sudo apt install python3-tk` (usually pre-installed) +- **macOS**: tkinter is included with Python +- **Windows**: tkinter is included with Python +- **Fallback**: If GUI fails, the tool will show error messages and continue + +**Common GUI Issues:** +- **Window appears in corner**: The GUI centers itself automatically on first run +- **Window size not remembered**: Check that `gui_config.json` is writable +- **"destroy" command error**: Fixed in latest version - window cleanup is now safe +- **GUI freezes**: Use Ctrl+C to interrupt, then restart the command + +### dlib Installation Issues +```bash +# Ubuntu/Debian - install system dependencies first +sudo apt-get install build-essential cmake libopenblas-dev + +# Then retry setup +source venv/bin/activate +python3 setup.py +``` + +### "Please install face_recognition_models" Warning +This warning is harmless - the application still works correctly. It's a known issue with Python 3.13. + +### Memory Issues +- Use `--model hog` for faster processing +- Process in smaller batches with `--limit 10` +- Close other applications to free memory + +### No Faces Found +- Check image quality and lighting +- Ensure faces are clearly visible +- Try `--model cnn` for better detection + +## ๐ŸŽจ GUI Interface Guide + +### Face Identification GUI +When you run `python3 photo_tagger.py identify --show-faces`, you'll see: + +**Left Panel:** +- ๐Ÿ“ **Photo Info** - Shows filename and face location +- ๐Ÿ–ผ๏ธ **Face Image** - Individual face crop for easy identification +- โœ… **Identification Status** - Shows if face is already identified and by whom + +**Right Panel:** +- ๐Ÿ“ **Person Name Dropdown** - Select from known people or type new names (pre-filled for re-identification) +- โ˜‘๏ธ **Compare Checkbox** - Compare with similar unidentified faces (persistent across navigation) +- โ˜‘๏ธ **Select All/Clear All Buttons** - Bulk selection controls (enabled only when Compare is active) +- ๐Ÿ“œ **Similar Faces List** - Scrollable list of similar unidentified faces with: + - โ˜‘๏ธ **Individual Checkboxes** - Select specific faces to identify together + - ๐Ÿ“ˆ **Confidence Percentages** - Color-coded match quality + - ๐Ÿ–ผ๏ธ **Face Images** - Thumbnail previews of similar faces +- ๐ŸŽฎ **Control Buttons**: + - **โœ… Identify** - Confirm the identification (saves immediately) + - **โฌ…๏ธ Back** - Go back to previous face (shows image and status) + - **โžก๏ธ Next** - Move to next face + - **โŒ Quit** - Exit application (all changes already saved) + +### Auto-Match GUI (Enhanced with Smart Algorithm) +When you run `python3 photo_tagger.py auto-match --show-faces`, you'll see an improved interface with: + +**๐Ÿง  Smart Algorithm Features:** +- **Quality-Based Matching** - Only high-quality faces are processed for better accuracy +- **Adaptive Tolerance** - Matching strictness adjusts based on face quality +- **Confidence Scoring** - Color-coded confidence levels (๐ŸŸข Very High, ๐ŸŸก High, ๐ŸŸ  Medium, ๐Ÿ”ด Low, โšซ Very Low) +- **Performance Optimized** - Faster processing with quality-based filtering + +**Interface Layout:** + +**Left Panel:** +- ๐Ÿ‘ค **Matched Person** - The already identified person +- ๐Ÿ–ผ๏ธ **Person Face Image** - Individual face crop of the matched person +- ๐Ÿ“ **Photo Info** - Shows person name, photo filename, and face location +- ๐Ÿ’พ **Save Button** - "Save changes for [Person Name]" - saves all checkbox selections + +**Right Panel:** +- โ˜‘๏ธ **Unidentified Faces** - All unidentified faces that match this person (sorted by confidence): + - โ˜‘๏ธ **Checkboxes** - Select which faces to identify with this person (pre-selected if previously identified) + - ๐Ÿ“ˆ **Confidence Percentages** - Color-coded match quality (highest confidence at top) + - ๐Ÿ–ผ๏ธ **Face Images** - Face crops of unidentified faces +- ๐Ÿ“œ **Scrollable** - Handle many matches easily +- ๐ŸŽฏ **Smart Ordering** - Highest confidence matches appear first for easy selection + +**Bottom Controls (Navigation Only):** +- **โฎ๏ธ Back** - Go back to previous person (disabled on first person) +- **โญ๏ธ Next** - Move to next person (disabled on last person) +- **โŒ Quit** - Exit auto-match process + +### Compare with Similar Faces Workflow +The Compare feature in the Identify GUI works seamlessly with the main identification process: + +1. **Enable Compare**: Check "Compare with similar faces" to see similar unidentified faces +2. **View Similar Faces**: Right panel shows all similar faces with confidence percentages and thumbnails +3. **Select Faces**: Use individual checkboxes or Select All/Clear All buttons to choose faces +4. **Enter Person Name**: Type or select the person's name in the dropdown +5. **Identify Together**: Click Identify to identify the current face and all selected similar faces at once +6. **Smart Navigation**: System warns if you try to navigate away with selected faces but no name +7. **Quit Protection**: When closing, system offers to save any pending identifications + +**Key Benefits:** +- **Bulk Identification**: Identify multiple similar faces with one action +- **Visual Confirmation**: See exactly which faces you're identifying together +- **Smart Warnings**: Prevents accidental loss of work +- **Performance Optimized**: Instant loading of similar faces + +### Auto-Match Workflow +The auto-match feature now works in a **person-centric** way: + +1. **Group by Person**: Faces are grouped by already identified people (not unidentified faces) +2. **Show Matched Person**: Left side shows the identified person and their face +3. **Show Unidentified Faces**: Right side shows all unidentified faces that match this person +4. **Select and Save**: Check the faces you want to identify with this person, then click "Save Changes" +5. **Navigate**: Use Back/Next to move between different people +6. **Correct Mistakes**: Go back and uncheck faces to unidentify them +7. **Pre-selected Checkboxes**: Previously identified faces are automatically checked when you go back + +**Key Benefits:** +- **1-to-Many**: One person can have multiple unidentified faces matched to them +- **Visual Confirmation**: See exactly what you're identifying before saving +- **Easy Corrections**: Go back and fix mistakes by unchecking faces +- **Smart Tracking**: Previously identified faces are pre-selected for easy review +- **Fast Performance**: Optimized database queries and streamlined interface + +### GUI Tips +- **Window Resizing**: Resize the window - it remembers your size preference +- **Keyboard Shortcuts**: Press Enter in the name field to identify +- **Back Navigation**: Use Back button to return to previous faces - images and identification status are preserved +- **Re-identification**: Go back to any face and change the identification - the name field is pre-filled +- **Auto-Save**: All identifications are saved immediately - no need to manually save +- **Compare Mode**: Enable Compare checkbox to see similar unidentified faces - setting persists across navigation +- **Bulk Selection**: Use Select All/Clear All buttons to quickly select or clear all similar faces +- **Smart Buttons**: Select All/Clear All buttons are only enabled when Compare mode is active +- **Navigation Warnings**: System warns if you try to navigate away with selected faces but no person name +- **Quit Confirmation**: When closing, system asks if you want to save pending identifications +- **Consistent Results**: Compare mode shows the same faces as auto-match with identical confidence scoring +- **Multiple Matches**: In auto-match, you can select multiple faces to identify with one person +- **Smart Navigation**: Back/Next buttons are disabled appropriately (Back disabled on first, Next disabled on last) +- **State Persistence**: Checkbox selections are preserved when navigating between people +- **Per-Person States**: Each person's selections are completely independent +- **Save Button Location**: Save button is in the left panel with the person's name for clarity +- **Performance**: Similar faces load instantly thanks to pre-fetched data optimization +- **Bidirectional Changes**: You can both identify and unidentify faces in the same session +- **Confidence Colors**: + - ๐ŸŸข 80%+ = Very High (Almost Certain) + - ๐ŸŸก 70%+ = High (Likely Match) + - ๐ŸŸ  60%+ = Medium (Possible Match) + - ๐Ÿ”ด 50%+ = Low (Questionable) + - โšซ <50% = Very Low (Unlikely) + +## ๐Ÿ†• Recent Improvements + +### Auto-Match UX Enhancements (Latest) +- **๐Ÿ’พ Smart Save Button**: "Save changes for [Person Name]" button moved to left panel for better UX +- **๐Ÿ”„ State Persistence**: Checkbox selections now preserved when navigating between people +- **๐Ÿšซ Smart Navigation**: Next button disabled on last person, Back button disabled on first +- **๐ŸŽฏ Per-Person States**: Each person's checkbox selections are completely independent +- **โšก Real-time Saving**: Checkbox states saved immediately when changed + +### Consistent Face-to-Face Comparison System +- **๐Ÿ”„ Unified Logic**: Both auto-match and identify now use the same face comparison algorithm +- **๐Ÿ“Š Consistent Results**: Identical confidence scoring and face matching across both modes +- **๐ŸŽฏ Same Tolerance**: Both functionalities respect the same tolerance settings +- **โšก Performance**: Eliminated code duplication for better maintainability +- **๐Ÿ”ง Refactored**: Single reusable function for face filtering and sorting + +### Compare Checkbox Enhancements +- **๐ŸŒ Global Setting**: Compare checkbox state persists when navigating between faces +- **๐Ÿ”„ Auto-Update**: Similar faces automatically refresh when using Back/Next buttons +- **๐Ÿ‘ฅ Consistent Display**: Compare mode shows the same faces as auto-match +- **๐Ÿ“ˆ Smart Filtering**: Only shows faces with 40%+ confidence (same as auto-match) +- **๐ŸŽฏ Proper Sorting**: Faces sorted by confidence (highest first) + +### Back Navigation & Re-identification +- **โฌ…๏ธ Back Button**: Navigate back to previous faces with full image display +- **๐Ÿ”„ Re-identification**: Change any identification by going back and re-identifying +- **๐Ÿ“ Pre-filled Names**: Name field shows current identification for easy changes +- **โœ… Status Display**: Shows who each face is identified as when going back + +### Improved Cleanup & Performance +- **๐Ÿงน Better Cleanup**: Proper cleanup of temporary files and resources +- **๐Ÿ’พ Auto-Save**: All identifications save immediately (removed redundant Save & Quit) +- **๐Ÿ”„ Code Reuse**: Eliminated duplicate functions for better maintainability +- **โšก Optimized**: Faster navigation and better memory management + +### Enhanced User Experience +- **๐Ÿ–ผ๏ธ Image Preservation**: Face images show correctly when navigating back +- **๐ŸŽฏ Smart Caching**: Face crops are properly cached and cleaned up +- **๐Ÿ”„ Bidirectional Changes**: Can both identify and unidentify faces in same session +- **๐Ÿ’พ Window Memory**: Remembers window size and position preferences + +## ๐ŸŽฏ What This Tool Does + +โœ… **Simple**: Single Python file, minimal dependencies +โœ… **Fast**: Efficient face detection and recognition +โœ… **Private**: Everything runs locally, no cloud services +โœ… **Flexible**: Batch processing, interactive identification +โœ… **Lightweight**: No web interface overhead +โœ… **GUI-Enhanced**: Modern interface for face identification +โœ… **User-Friendly**: Back navigation, re-identification, and auto-save + +## ๐Ÿ“ˆ Performance Tips + +- **Always use virtual environment** to avoid conflicts +- Start with small batches (`--limit 20`) to test +- Use `hog` model for speed, `cnn` for accuracy +- Process photos in smaller folders first +- Identify faces in batches to avoid fatigue + +## ๐Ÿค Contributing + +This is now a minimal, focused tool. Key principles: +- Keep it simple and fast +- CLI-only interface +- Minimal dependencies +- Clear, readable code +- **Always use python3** commands + +--- + +**Total project size**: ~300 lines of Python code +**Dependencies**: 6 essential packages +**Setup time**: ~5 minutes +**Perfect for**: Batch processing personal photo collections + +## ๐Ÿ”„ Common Commands Cheat Sheet + +```bash +# Setup (one time) +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 --show-faces --batch 10 +./run.sh auto-match --show-faces +./run.sh modifyidentified +./run.sh stats + +# Daily usage - Option 2: Manual venv activation (GUI-ENHANCED) +source venv/bin/activate +python3 photo_tagger.py scan ~/Pictures --recursive +python3 photo_tagger.py process --limit 50 +python3 photo_tagger.py identify --show-faces --batch 10 # Opens GUI +python3 photo_tagger.py auto-match --show-faces # Opens GUI +python3 photo_tagger.py modifyidentified # Opens GUI to view/modify +python3 photo_tagger.py stats ``` \ No newline at end of file diff --git a/photo_tagger.py b/photo_tagger.py index 00fc60b..61a1f9e 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -408,6 +408,7 @@ class PhotoTagger: # Track window state to prevent multiple destroy calls window_destroyed = False + selected_person_id = None force_exit = False # Track current face crop path for cleanup @@ -465,8 +466,11 @@ class PhotoTagger: if not validate_navigation(): return # Cancel close - # Check if there are pending identifications - pending_identifications = {k: v for k, v in face_person_names.items() if v.strip()} + # Check if there are pending identifications (faces with names but not yet saved) + pending_identifications = { + k: v for k, v in face_person_names.items() + if v.strip() and (k not in face_status or face_status[k] != 'identified') + } if pending_identifications: # Ask user if they want to save pending identifications @@ -600,7 +604,7 @@ class PhotoTagger: foreground="gray", font=("Arial", 10)) clear_label.pack(pady=20) - # Update button states based on compare checkbox + # Update button states based on compare checkbox and list contents update_select_clear_buttons_state() # Compare checkbox @@ -664,8 +668,8 @@ class PhotoTagger: clear_all_btn.pack(side=tk.LEFT) def update_select_clear_buttons_state(): - """Enable/disable Select All and Clear All buttons based on compare checkbox state""" - if compare_var.get(): + """Enable/disable Select All and Clear All based on compare state and presence of items""" + if compare_var.get() and similar_face_vars: select_all_btn.config(state='normal') clear_all_btn.config(state='normal') else: @@ -786,8 +790,11 @@ class PhotoTagger: if not validate_navigation(): return # Cancel quit - # Check if there are pending identifications - pending_identifications = {k: v for k, v in face_person_names.items() if v.strip()} + # Check if there are pending identifications (faces with names but not yet saved) + pending_identifications = { + k: v for k, v in face_person_names.items() + if v.strip() and (k not in face_status or face_status[k] != 'identified') + } if pending_identifications: @@ -2175,20 +2182,50 @@ class PhotoTagger: matches_frame = ttk.Frame(right_frame) matches_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + # Control buttons for matches (Select All / Clear All) + matches_controls_frame = ttk.Frame(matches_frame) + matches_controls_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), padx=5, pady=(5, 0)) + + def select_all_matches(): + """Select all match checkboxes""" + for var in match_vars: + var.set(True) + + def clear_all_matches(): + """Clear all match checkboxes""" + for var in match_vars: + var.set(False) + + select_all_matches_btn = ttk.Button(matches_controls_frame, text="โ˜‘๏ธ Select All", command=select_all_matches) + select_all_matches_btn.pack(side=tk.LEFT, padx=(0, 5)) + + clear_all_matches_btn = ttk.Button(matches_controls_frame, text="โ˜ Clear All", command=clear_all_matches) + clear_all_matches_btn.pack(side=tk.LEFT) + + def update_match_control_buttons_state(): + """Enable/disable Select All / Clear All based on matches presence""" + if match_vars: + select_all_matches_btn.config(state='normal') + clear_all_matches_btn.config(state='normal') + else: + select_all_matches_btn.config(state='disabled') + clear_all_matches_btn.config(state='disabled') + # Create scrollbar for matches scrollbar = ttk.Scrollbar(right_frame, orient="vertical", command=None) - scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + scrollbar.grid(row=0, column=1, rowspan=1, sticky=(tk.N, tk.S)) # Create canvas for matches with scrollbar matches_canvas = tk.Canvas(matches_frame, yscrollcommand=scrollbar.set) - matches_canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + matches_canvas.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) scrollbar.config(command=matches_canvas.yview) # Configure grid weights right_frame.columnconfigure(0, weight=1) right_frame.rowconfigure(0, weight=1) matches_frame.columnconfigure(0, weight=1) - matches_frame.rowconfigure(0, weight=1) + matches_frame.rowconfigure(0, weight=0) # Controls row takes minimal space + matches_frame.rowconfigure(1, weight=1) # Canvas row expandable # Control buttons (navigation only) control_frame = ttk.Frame(main_frame) @@ -2373,6 +2410,10 @@ class PhotoTagger: # Get the first match to get matched person info if not matches_for_this_person: print(f"โŒ Error: No matches found for current person {matched_id}") + # No items on the right panel โ€“ disable Select All / Clear All + match_checkboxes.clear() + match_vars.clear() + update_match_control_buttons_state() # Skip to next person if available if current_matched_index < len(matched_ids) - 1: current_matched_index += 1 @@ -2415,6 +2456,7 @@ class PhotoTagger: matches_canvas.delete("all") match_checkboxes.clear() match_vars.clear() + update_match_control_buttons_state() # Create frame for unidentified faces inside canvas matches_inner_frame = ttk.Frame(matches_canvas) @@ -2505,6 +2547,9 @@ class PhotoTagger: else: match_canvas.create_text(50, 50, text="๐Ÿ–ผ๏ธ", fill="gray") + # Update Select All / Clear All button states after populating + update_match_control_buttons_state() + # Update scroll region matches_canvas.update_idletasks() matches_canvas.configure(scrollregion=matches_canvas.bbox("all")) @@ -2529,6 +2574,528 @@ class PhotoTagger: return identified_count + def modifyidentified(self) -> int: + """Modify identified faces interface - empty window with Quit button for now""" + import tkinter as tk + from tkinter import ttk, messagebox + from PIL import Image, ImageTk + import os + + # Simple tooltip implementation + class ToolTip: + def __init__(self, widget, text): + self.widget = widget + self.text = text + self.tooltip_window = None + self.widget.bind("", self.on_enter) + self.widget.bind("", self.on_leave) + + def on_enter(self, event=None): + if self.tooltip_window or not self.text: + return + x, y, _, _ = self.widget.bbox("insert") if hasattr(self.widget, 'bbox') else (0, 0, 0, 0) + x += self.widget.winfo_rootx() + 25 + y += self.widget.winfo_rooty() + 25 + + self.tooltip_window = tw = tk.Toplevel(self.widget) + tw.wm_overrideredirect(True) + tw.wm_geometry(f"+{x}+{y}") + + label = tk.Label(tw, text=self.text, justify=tk.LEFT, + background="#ffffe0", relief=tk.SOLID, borderwidth=1, + font=("tahoma", "8", "normal")) + label.pack(ipadx=1) + + def on_leave(self, event=None): + if self.tooltip_window: + self.tooltip_window.destroy() + self.tooltip_window = None + + # Create the main window + root = tk.Tk() + root.title("View and Modify Identified Faces") + root.resizable(True, True) + + # Track window state to prevent multiple destroy calls + window_destroyed = False + temp_crops = [] + right_panel_images = [] # Keep PhotoImage refs alive + selected_person_id = None + + # Hide window initially to prevent flash at corner + root.withdraw() + + # Set up protocol handler for window close button (X) + def on_closing(): + nonlocal window_destroyed + # Cleanup temp crops + for crop in list(temp_crops): + try: + if os.path.exists(crop): + os.remove(crop) + except: + pass + temp_crops.clear() + if not window_destroyed: + window_destroyed = True + try: + root.destroy() + except tk.TclError: + pass # Window already destroyed + + root.protocol("WM_DELETE_WINDOW", on_closing) + + # Set up window size saving + saved_size = self._setup_window_size_saving(root) + + # Create main frame + main_frame = ttk.Frame(root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Configure grid weights + root.columnconfigure(0, weight=1) + root.rowconfigure(0, weight=1) + main_frame.columnconfigure(0, weight=1) + main_frame.columnconfigure(1, weight=2) + main_frame.rowconfigure(1, weight=1) + + # Title label + title_label = ttk.Label(main_frame, text="View and Modify Identified Faces", font=("Arial", 16, "bold")) + title_label.grid(row=0, column=0, columnspan=2, pady=(0, 10), sticky=tk.W) + + # Left panel: People list + people_frame = ttk.LabelFrame(main_frame, text="People", padding="10") + people_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 8)) + people_frame.columnconfigure(0, weight=1) + people_canvas = tk.Canvas(people_frame, bg='white') + people_scrollbar = ttk.Scrollbar(people_frame, orient="vertical", command=people_canvas.yview) + people_list_inner = ttk.Frame(people_canvas) + people_canvas.create_window((0, 0), window=people_list_inner, anchor="nw") + people_canvas.configure(yscrollcommand=people_scrollbar.set) + + people_list_inner.bind( + "", + lambda e: people_canvas.configure(scrollregion=people_canvas.bbox("all")) + ) + + people_canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + people_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + people_frame.rowconfigure(0, weight=1) + + # Right panel: Faces for selected person + faces_frame = ttk.LabelFrame(main_frame, text="Faces", padding="10") + faces_frame.grid(row=1, column=1, sticky=(tk.W, tk.E, tk.N, tk.S)) + faces_frame.columnconfigure(0, weight=1) + faces_frame.rowconfigure(0, weight=1) + + faces_canvas = tk.Canvas(faces_frame, bg='white') + faces_scrollbar = ttk.Scrollbar(faces_frame, orient="vertical", command=faces_canvas.yview) + faces_inner = ttk.Frame(faces_canvas) + faces_canvas.create_window((0, 0), window=faces_inner, anchor="nw") + faces_canvas.configure(yscrollcommand=faces_scrollbar.set) + + faces_inner.bind( + "", + lambda e: faces_canvas.configure(scrollregion=faces_canvas.bbox("all")) + ) + + # Track current person for responsive face grid + current_person_id = None + current_person_name = "" + resize_job = None + + # Track unmatched faces (temporary changes) + unmatched_faces = set() # All face IDs unmatched across people (for global save) + unmatched_by_person = {} # person_id -> set(face_id) for per-person undo + original_faces_data = [] # store original faces data for potential future use + + def on_faces_canvas_resize(event): + nonlocal resize_job + if current_person_id is None: + return + # Debounce re-render on resize + try: + if resize_job is not None: + root.after_cancel(resize_job) + except Exception: + pass + resize_job = root.after(150, lambda: show_person_faces(current_person_id, current_person_name)) + + faces_canvas.bind("", on_faces_canvas_resize) + + faces_canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + faces_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + + # Load people from DB with counts + people_data = [] # list of dicts: {id, name, count} + + def load_people(): + nonlocal people_data + with self.get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT p.id, p.name, COUNT(f.id) as face_count + FROM people p + JOIN faces f ON f.person_id = p.id + GROUP BY p.id, p.name + HAVING face_count > 0 + ORDER BY p.name COLLATE NOCASE + """ + ) + people_data = [{ + 'id': pid, + 'name': name, + 'count': count + } for (pid, name, count) in cursor.fetchall()] + + def clear_faces_panel(): + for w in faces_inner.winfo_children(): + w.destroy() + # Cleanup temp crops + for crop in list(temp_crops): + try: + if os.path.exists(crop): + os.remove(crop) + except: + pass + temp_crops.clear() + right_panel_images.clear() + + def unmatch_face(face_id: int): + """Temporarily unmatch a face from the current person""" + nonlocal unmatched_faces, unmatched_by_person + unmatched_faces.add(face_id) + # Track per-person for Undo + person_set = unmatched_by_person.get(current_person_id) + if person_set is None: + person_set = set() + unmatched_by_person[current_person_id] = person_set + person_set.add(face_id) + # Refresh the display + show_person_faces(current_person_id, current_person_name) + + def undo_changes(): + """Undo all temporary changes""" + nonlocal unmatched_faces, unmatched_by_person + if current_person_id in unmatched_by_person: + for fid in list(unmatched_by_person[current_person_id]): + unmatched_faces.discard(fid) + unmatched_by_person[current_person_id].clear() + # Refresh the display + show_person_faces(current_person_id, current_person_name) + + def save_changes(): + """Save unmatched faces to database""" + if not unmatched_faces: + return + + # Confirm with user + result = messagebox.askyesno( + "Confirm Changes", + f"Are you sure you want to unlink {len(unmatched_faces)} face(s) from '{current_person_name}'?\n\n" + "This will make these faces unidentified again." + ) + + if not result: + return + + # Update database + with self.get_db_connection() as conn: + cursor = conn.cursor() + for face_id in unmatched_faces: + cursor.execute('UPDATE faces SET person_id = NULL WHERE id = ?', (face_id,)) + conn.commit() + + # Store count for message before clearing + unlinked_count = len(unmatched_faces) + + # Clear unmatched faces and refresh + unmatched_faces.clear() + original_faces_data.clear() + + # Refresh people list to update counts + load_people() + populate_people_list() + + # Refresh faces display + show_person_faces(current_person_id, current_person_name) + + messagebox.showinfo("Changes Saved", f"Successfully unlinked {unlinked_count} face(s) from '{current_person_name}'.") + + def show_person_faces(person_id: int, person_name: str): + nonlocal current_person_id, current_person_name, unmatched_faces, original_faces_data + current_person_id = person_id + current_person_name = person_name + clear_faces_panel() + + # Determine how many columns fit the available width + available_width = faces_canvas.winfo_width() + if available_width <= 1: + available_width = faces_frame.winfo_width() + tile_width = 150 # approx tile + padding + cols = max(1, available_width // tile_width) + + # Header row + header = ttk.Label(faces_inner, text=f"Faces for: {person_name}", font=("Arial", 12, "bold")) + header.grid(row=0, column=0, columnspan=cols, sticky=tk.W, pady=(0, 5)) + + # Control buttons row + button_frame = ttk.Frame(faces_inner) + button_frame.grid(row=1, column=0, columnspan=cols, sticky=tk.W, pady=(0, 10)) + + # Enable Undo only if current person has unmatched faces + current_has_unmatched = bool(unmatched_by_person.get(current_person_id)) + undo_btn = ttk.Button(button_frame, text="โ†ถ Undo changes", + command=lambda: undo_changes(), + state="disabled" if not current_has_unmatched else "normal") + undo_btn.pack(side=tk.LEFT, padx=(0, 10)) + + # Note: Save button moved to bottom control bar + + # Query faces for this person + with self.get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT f.id, f.location, ph.path, ph.filename + FROM faces f + JOIN photos ph ON ph.id = f.photo_id + WHERE f.person_id = ? + ORDER BY f.id DESC + """, + (person_id,) + ) + rows = cursor.fetchall() + + # Filter out unmatched faces + visible_rows = [row for row in rows if row[0] not in unmatched_faces] + + if not visible_rows: + ttk.Label(faces_inner, text="No faces found." if not rows else "All faces unmatched.", foreground="gray").grid(row=2, column=0, sticky=tk.W) + return + + # Grid thumbnails with responsive column count + row_index = 2 # Start after header and buttons + col_index = 0 + for face_id, location, photo_path, filename in visible_rows: + crop_path = self._extract_face_crop(photo_path, location, face_id) + thumb = None + if crop_path and os.path.exists(crop_path): + try: + img = Image.open(crop_path) + img.thumbnail((130, 130), Image.Resampling.LANCZOS) + photo_img = ImageTk.PhotoImage(img) + temp_crops.append(crop_path) + right_panel_images.append(photo_img) + thumb = photo_img + except Exception: + thumb = None + + tile = ttk.Frame(faces_inner, padding="5") + tile.grid(row=row_index, column=col_index, padx=5, pady=5, sticky=tk.N) + + # Create a frame for the face image with X button overlay + face_frame = ttk.Frame(tile) + face_frame.grid(row=0, column=0) + + canvas = tk.Canvas(face_frame, width=130, height=130, bg='white') + canvas.grid(row=0, column=0) + if thumb is not None: + canvas.create_image(65, 65, image=thumb) + else: + canvas.create_text(65, 65, text="๐Ÿ–ผ๏ธ", fill="gray") + + # X button to unmatch face + x_btn = ttk.Button(face_frame, text="โœ–", width=2, + command=lambda fid=face_id: unmatch_face(fid)) + x_btn.place(x=110, y=5) # Position in top-right corner + + ttk.Label(tile, text=f"ID {face_id}", foreground="gray").grid(row=1, column=0) + + col_index += 1 + if col_index >= cols: + col_index = 0 + row_index += 1 + + def populate_people_list(): + for w in people_list_inner.winfo_children(): + w.destroy() + for idx, person in enumerate(people_data): + row = ttk.Frame(people_list_inner) + row.grid(row=idx, column=0, sticky=(tk.W, tk.E), pady=4) + # Freeze per-row values to avoid late-binding issues + row_person = person + row_idx = idx + + # Make person name clickable + def make_click_handler(p_id, p_name, p_idx): + def on_click(event): + nonlocal selected_person_id + # Reset all labels to normal font + for child in people_list_inner.winfo_children(): + for widget in child.winfo_children(): + if isinstance(widget, ttk.Label): + widget.config(font=("Arial", 10)) + # Set clicked label to bold + event.widget.config(font=("Arial", 10, "bold")) + selected_person_id = p_id + # Show faces for this person + show_person_faces(p_id, p_name) + return on_click + + + # Edit (rename) button + def start_edit_person(row_frame, person_record, row_index): + for w in row_frame.winfo_children(): + w.destroy() + + entry_var = tk.StringVar(value=person_record['name']) + entry = ttk.Entry(row_frame, textvariable=entry_var, width=24) + entry.pack(side=tk.LEFT) + entry.focus_set() + + def save_rename(): + new_name = entry_var.get().strip() + if not new_name: + messagebox.showwarning("Invalid name", "Person name cannot be empty.") + return + # Prevent duplicate names + with self.get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT id FROM people WHERE name = ? AND id != ?', (new_name, person_record['id'])) + dup = cursor.fetchone() + if dup: + messagebox.showwarning("Duplicate name", f"A person named '{new_name}' already exists.") + return + cursor.execute('UPDATE people SET name = ? WHERE id = ?', (new_name, person_record['id'])) + conn.commit() + # Refresh list + current_selected_id = person_record['id'] + load_people() + populate_people_list() + # Reselect and refresh right panel header if needed + if selected_person_id == current_selected_id or selected_person_id is None: + # Find updated name + updated = next((p for p in people_data if p['id'] == current_selected_id), None) + if updated: + # Bold corresponding label + for child in people_list_inner.winfo_children(): + # child is row frame: contains label and button + widgets = child.winfo_children() + if not widgets: + continue + lbl = widgets[0] + if isinstance(lbl, ttk.Label) and lbl.cget('text').startswith(updated['name'] + " ("): + lbl.config(font=("Arial", 10, "bold")) + break + # Update right panel header by re-showing faces + show_person_faces(updated['id'], updated['name']) + + def cancel_edit(): + # Rebuild the row back to label + edit + for w in row_frame.winfo_children(): + w.destroy() + rebuild_row(row_frame, person_record, row_index) + + save_btn = ttk.Button(row_frame, text="๐Ÿ’พ Save", command=save_rename) + save_btn.pack(side=tk.LEFT, padx=(5, 0)) + cancel_btn = ttk.Button(row_frame, text="โœ– Cancel", command=cancel_edit) + cancel_btn.pack(side=tk.LEFT, padx=(5, 0)) + + # Keyboard shortcuts + entry.bind('', lambda e: save_rename()) + entry.bind('', lambda e: cancel_edit()) + + def rebuild_row(row_frame, p, i): + # Label (clickable) + name_lbl = ttk.Label(row_frame, text=f"{p['name']} ({p['count']})", font=("Arial", 10)) + name_lbl.pack(side=tk.LEFT) + name_lbl.bind("", make_click_handler(p['id'], p['name'], i)) + name_lbl.config(cursor="hand2") + # Edit button + edit_btn = ttk.Button(row_frame, text="โœ๏ธ", width=3, command=lambda r=row_frame, pr=p, ii=i: start_edit_person(r, pr, ii)) + edit_btn.pack(side=tk.RIGHT) + # Add tooltip to edit button + ToolTip(edit_btn, "Update name") + # Bold if selected + if (selected_person_id is None and i == 0) or (selected_person_id == p['id']): + name_lbl.config(font=("Arial", 10, "bold")) + + # Build row contents with edit button + rebuild_row(row, row_person, row_idx) + + # Initial load + load_people() + populate_people_list() + + # Show first person's faces by default and mark selected + if people_data: + selected_person_id = people_data[0]['id'] + show_person_faces(people_data[0]['id'], people_data[0]['name']) + + # Control buttons + control_frame = ttk.Frame(main_frame) + control_frame.grid(row=2, column=0, columnspan=2, pady=(10, 0), sticky=tk.E) + + def on_quit(): + nonlocal window_destroyed + on_closing() + if not window_destroyed: + window_destroyed = True + try: + root.destroy() + except tk.TclError: + pass # Window already destroyed + + def on_save_all_changes(): + # Use global unmatched_faces set; commit all across people + nonlocal unmatched_faces + if not unmatched_faces: + messagebox.showinfo("Nothing to Save", "There are no pending changes to save.") + return + result = messagebox.askyesno( + "Confirm Save", + f"Unlink {len(unmatched_faces)} face(s) across all people?\n\nThis will make them unidentified." + ) + if not result: + return + with self.get_db_connection() as conn: + cursor = conn.cursor() + for face_id in unmatched_faces: + cursor.execute('UPDATE faces SET person_id = NULL WHERE id = ?', (face_id,)) + conn.commit() + count = len(unmatched_faces) + unmatched_faces.clear() + # Refresh people list and right panel for current selection + load_people() + populate_people_list() + if current_person_id is not None and current_person_name: + show_person_faces(current_person_id, current_person_name) + messagebox.showinfo("Changes Saved", f"Successfully unlinked {count} face(s).") + + save_btn_bottom = ttk.Button(control_frame, text="๐Ÿ’พ Save changes", command=on_save_all_changes) + save_btn_bottom.pack(side=tk.RIGHT, padx=(0, 10)) + quit_btn = ttk.Button(control_frame, text="โŒ Quit", command=on_quit) + quit_btn.pack(side=tk.RIGHT) + + # Show the window + try: + root.deiconify() + root.lift() + root.focus_force() + except tk.TclError: + # Window was destroyed before we could show it + return 0 + + # Main event loop + try: + root.mainloop() + except tk.TclError: + pass # Window was destroyed + + return 0 + def main(): """Main CLI interface""" @@ -2541,6 +3108,7 @@ Examples: 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 modifyidentified # Show and Modify identified 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 @@ -2549,7 +3117,7 @@ Examples: ) parser.add_argument('command', - choices=['scan', 'process', 'identify', 'tag', 'search', 'stats', 'match', 'auto-match'], + choices=['scan', 'process', 'identify', 'tag', 'search', 'stats', 'match', 'auto-match', 'modifyidentified'], help='Command to execute') parser.add_argument('target', nargs='?', @@ -2641,6 +3209,9 @@ Examples: include_twins = getattr(args, 'include_twins', False) tagger.auto_identify_matches(args.tolerance, not args.auto, show_faces, include_twins) + elif args.command == 'modifyidentified': + tagger.modifyidentified() + return 0 except KeyboardInterrupt: diff --git a/setup.py b/setup.py index 71d2cbc..ae31e24 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,40 @@ def check_python_version(): return True +def install_system_dependencies(): + """Install system-level packages required for compilation and runtime""" + print("๐Ÿ”ง Installing system dependencies...") + print(" (Build tools, libraries, and image viewer)") + + # Check if we're on a Debian/Ubuntu system + if Path("/usr/bin/apt").exists(): + try: + # Install required system packages for building Python packages and running tools + packages = [ + "cmake", "build-essential", "libopenblas-dev", "liblapack-dev", + "libx11-dev", "libgtk-3-dev", "libboost-python-dev", "feh" + ] + + print(f"๐Ÿ“ฆ Installing packages: {', '.join(packages)}") + subprocess.run([ + "sudo", "apt", "install", "-y" + ] + packages, check=True) + print("โœ… System dependencies installed successfully") + return True + except subprocess.CalledProcessError as e: + print(f"โŒ Failed to install system dependencies: {e}") + print(" You may need to run: sudo apt update") + return False + else: + print("โš ๏ธ System dependency installation not supported on this platform") + print(" Please install manually:") + print(" - cmake, build-essential") + print(" - libopenblas-dev, liblapack-dev") + print(" - libx11-dev, libgtk-3-dev, libboost-python-dev") + print(" - feh (image viewer)") + return True + + def install_requirements(): """Install Python requirements""" requirements_file = Path("requirements.txt") @@ -84,6 +118,11 @@ def main(): print() + # Install system dependencies + if not install_system_dependencies(): + return 1 + print() + # Create directories print("๐Ÿ“ Creating directories...") create_directories()