From 6a5bafef50e1508cb199f55dab2e68f1aaca28d5 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Mon, 22 Sep 2025 13:57:43 -0400 Subject: [PATCH] Implement 'modifyidentified' feature for viewing and modifying identified faces in the PhotoTagger GUI. Enhance user experience with new controls for selecting and unselecting faces, and improve navigation and state management. Update README to reflect new functionality and installation requirements, including system dependencies. --- README.md | 902 ++++++++++++++++++++++++++++++++---------------- photo_tagger.py | 593 ++++++++++++++++++++++++++++++- setup.py | 39 +++ 3 files changed, 1217 insertions(+), 317 deletions(-) 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()