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.

This commit is contained in:
tanyar09 2025-09-22 13:57:43 -04:00
parent 2c67b2216d
commit 6a5bafef50
3 changed files with 1217 additions and 317 deletions

902
README.md
View File

@ -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 <your-repo>
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 <your-repo>
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 <your-repo>
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 <your-repo>
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
```

View File

@ -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("<Enter>", self.on_enter)
self.widget.bind("<Leave>", 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(
"<Configure>",
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(
"<Configure>",
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("<Configure>", 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('<Return>', lambda e: save_rename())
entry.bind('<Escape>', 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("<Button-1>", 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:

View File

@ -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()