Add photo icon feature to PhotoTagger for easy access to original photos

This update introduces a new photo icon in the PhotoTagger interface, allowing users to click the camera icon on face images to open the original photo in their default image viewer. The feature includes cross-platform support for Windows, macOS, and Linux, ensuring proper window sizing and multiple viewer options. Tooltips are added for enhanced user guidance, and the README has been updated to reflect this new functionality and its usage.
This commit is contained in:
tanyar09 2025-10-02 14:16:41 -04:00
parent a14a8a4231
commit b910be9fe7
2 changed files with 168 additions and 7 deletions

View File

@ -536,6 +536,7 @@ 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
- 📷 **Photo Icon** - Click the camera icon in the top-right corner of the face to open the original photo in your default image viewer
- ✅ **Identification Status** - Shows if face is already identified and by whom
**Right Panel:**
@ -551,6 +552,7 @@ When you run `python3 photo_tagger.py identify --show-faces`, you'll see:
- ☑️ **Individual Checkboxes** - Select specific faces to identify together
- 📈 **Confidence Percentages** - Color-coded match quality
- 🖼️ **Face Images** - Thumbnail previews of similar faces
- 📷 **Photo Icons** - Click the camera icon on any similar face to view its original photo
- 🎮 **Control Buttons**:
- **✅ Identify** - Confirm the identification (saves immediately) - requires first name, last name, and date of birth
- **⬅️ Back** - Go back to previous face (shows image and status, repopulates fields)
@ -571,6 +573,7 @@ When you run `python3 photo_tagger.py auto-match --show-faces`, you'll see an im
**Left Panel:**
- 👤 **Matched Person** - The already identified person with complete information
- 🖼️ **Person Face Image** - Individual face crop of the matched person
- 📷 **Photo Icon** - Click the camera icon in the top-right corner to open the original photo
- 📁 **Detailed Person Info** - Shows:
- **Full Name** with middle and maiden names (if available)
- **Date of Birth** (if available)
@ -582,6 +585,7 @@ When you run `python3 photo_tagger.py auto-match --show-faces`, you'll see an im
- ☑️ **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
- 📷 **Photo Icons** - Click the camera icon on any face to view its original photo
- 📜 **Scrollable** - Handle many matches easily
- 🎯 **Smart Ordering** - Highest confidence matches appear first for easy selection
@ -829,11 +833,15 @@ This is now a minimal, focused tool. Key principles:
- ✅ **Auto-Selection** - Automatically selects first person in filtered results
- ✅ **Comprehensive Editing** - Edit all person fields: first, last, middle, maiden names, and date of birth
- ✅ **Visual Calendar Integration** - Professional date picker with month/year navigation
- ✅ **Smart Validation** - Save button only enabled when all required fields (first name, last name, date of birth) are filled
- ✅ **Real-time Feedback** - Button state updates instantly as you type or clear fields
- ✅ **Enhanced Layout** - Organized grid layout with labels directly under each input field
- ✅ **Immediate Database Saves** - Person information saved directly to database when clicking save
- ✅ **Visual Button States** - Disabled save button clearly grayed out when validation fails
### 📷 Photo Icon Feature (NEW!)
- ✅ **Source Photo Access** - Click the 📷 camera icon on any face to open the original photo
- ✅ **Smart Positioning** - Icons appear exactly in the top-right corner of each face image
- ✅ **Cross-Platform Support** - Opens photos in properly sized windows on Windows, macOS, and Linux
- ✅ **Helpful Tooltips** - "Show original photo" tooltip appears on hover
- ✅ **Available Everywhere** - Works on main faces (left panel) and similar faces (right panel)
- ✅ **Proper Window Sizing** - Photos open in reasonable window sizes, not fullscreen
- ✅ **Multiple Viewer Support** - Tries multiple image viewers for optimal experience
### Name Handling & Database
- ✅ **Fixed Comma Issues** - Names are now stored cleanly without commas in database

View File

@ -2283,6 +2283,12 @@ class PhotoTagger:
# Keep a reference to prevent garbage collection
canvas.image = photo
# Add photo icon using reusable function
self._create_photo_icon(canvas, photo_path,
face_x=x, face_y=y,
face_width=new_width, face_height=new_height,
canvas_width=canvas_width, canvas_height=canvas_height)
except Exception as e:
canvas.create_text(200, 200, text=f"❌ Could not load image: {e}", fill="red")
else:
@ -2747,6 +2753,12 @@ class PhotoTagger:
match_canvas.create_image(40, 40, image=photo)
match_canvas.image = photo # Keep reference
face_images.append(photo)
# Add photo icon to the similar face
self._create_photo_icon(match_canvas, photo_path, icon_size=15,
face_x=40, face_y=40,
face_width=80, face_height=80,
canvas_width=80, canvas_height=80)
else:
# No image available
match_canvas = tk.Canvas(match_frame, width=80, height=80, bg='white')
@ -2758,6 +2770,114 @@ class PhotoTagger:
match_canvas.pack(side=tk.LEFT, padx=(10, 0))
match_canvas.create_text(40, 40, text="", fill="red")
def _create_photo_icon(self, canvas, photo_path, icon_size=20, icon_x=None, icon_y=None,
canvas_width=None, canvas_height=None, face_x=None, face_y=None,
face_width=None, face_height=None):
"""Create a reusable photo icon with tooltip on a canvas"""
import tkinter as tk
import subprocess
import platform
import os
def open_source_photo(event):
"""Open the source photo in a properly sized window"""
try:
system = platform.system()
if system == "Windows":
# Try to open with a specific image viewer that supports window sizing
try:
subprocess.run(["mspaint", photo_path], check=False)
except:
os.startfile(photo_path)
elif system == "Darwin": # macOS
# Use Preview with specific window size
subprocess.run(["open", "-a", "Preview", photo_path])
else: # Linux and others
# Try common image viewers with window sizing options
viewers_to_try = [
["eog", "--new-window", photo_path], # Eye of GNOME
["gwenview", photo_path], # KDE image viewer
["feh", "--geometry", "800x600", photo_path], # feh with specific size
["gimp", photo_path], # GIMP
["xdg-open", photo_path] # Fallback to default
]
opened = False
for viewer_cmd in viewers_to_try:
try:
result = subprocess.run(viewer_cmd, check=False, capture_output=True)
if result.returncode == 0:
opened = True
break
except:
continue
if not opened:
# Final fallback
subprocess.run(["xdg-open", photo_path])
except Exception as e:
print(f"❌ Could not open photo: {e}")
# Create tooltip for the icon
tooltip = None
def show_tooltip(event):
nonlocal tooltip
if tooltip:
tooltip.destroy()
tooltip = tk.Toplevel()
tooltip.wm_overrideredirect(True)
tooltip.wm_geometry(f"+{event.x_root+10}+{event.y_root+10}")
label = tk.Label(tooltip, text="Show original photo",
background="lightyellow", relief="solid", borderwidth=1,
font=("Arial", 9))
label.pack()
def hide_tooltip(event):
nonlocal tooltip
if tooltip:
tooltip.destroy()
tooltip = None
# Calculate icon position
if icon_x is None or icon_y is None:
if face_x is not None and face_y is not None and face_width is not None and face_height is not None:
# Position relative to face image - exactly in the corner
face_right = face_x + face_width // 2
face_top = face_y - face_height // 2
icon_x = face_right - icon_size
icon_y = face_top
else:
# Position relative to canvas - exactly in the corner
if canvas_width is None:
canvas_width = canvas.winfo_width()
if canvas_height is None:
canvas_height = canvas.winfo_height()
icon_x = canvas_width - icon_size
icon_y = 0
# Ensure icon stays within canvas bounds
if canvas_width is None:
canvas_width = canvas.winfo_width()
if canvas_height is None:
canvas_height = canvas.winfo_height()
icon_x = min(icon_x, canvas_width - icon_size)
icon_y = max(icon_y, 0)
# Draw the photo icon
canvas.create_rectangle(icon_x, icon_y, icon_x + icon_size, icon_y + icon_size,
fill="white", outline="black", width=1, tags="photo_icon")
canvas.create_text(icon_x + icon_size//2, icon_y + icon_size//2,
text="📷", font=("Arial", 10), tags="photo_icon")
# Bind events
canvas.tag_bind("photo_icon", "<Button-1>", open_source_photo)
canvas.tag_bind("photo_icon", "<Enter>", lambda e: (canvas.config(cursor="hand2"), show_tooltip(e)))
canvas.tag_bind("photo_icon", "<Leave>", lambda e: (canvas.config(cursor=""), hide_tooltip(e)))
canvas.tag_bind("photo_icon", "<Motion>", lambda e: (show_tooltip(e) if tooltip else None))
return tooltip # Return tooltip reference for cleanup if needed
def _extract_face_crop(self, photo_path: str, location: tuple, face_id: int) -> str:
"""Extract and save individual face crop for identification with caching"""
try:
@ -3694,6 +3814,12 @@ class PhotoTagger:
# Configure row weights
main_frame.rowconfigure(0, weight=1)
# Check if there's only one person - if so, disable search functionality
# Use matched_ids instead of person_faces_list since we only show people with potential matches
matched_ids = [person_id for person_id, _, _ in person_faces_list if person_id in matches_by_matched and matches_by_matched[person_id]]
has_only_one_person = len(matched_ids) == 1
print(f"DEBUG: person_faces_list length: {len(person_faces_list)}, matched_ids length: {len(matched_ids)}, has_only_one_person: {has_only_one_person}")
# Search controls for filtering people by last name
last_name_search_var = tk.StringVar()
# Search field with label underneath (like modifyidentified edit section)
@ -3714,8 +3840,21 @@ class PhotoTagger:
clear_btn.pack(side=tk.LEFT)
# Helper label directly under the search input
last_name_label = ttk.Label(search_frame, text="Type Last Name", font=("Arial", 8), foreground="gray")
last_name_label.grid(row=1, column=0, sticky=tk.W, pady=(2, 0))
if has_only_one_person:
print("DEBUG: Disabling search functionality - only one person found")
# Disable search functionality if there's only one person
search_entry.config(state='disabled')
search_btn.config(state='disabled')
clear_btn.config(state='disabled')
# Add a label to explain why search is disabled
disabled_label = ttk.Label(search_frame, text="(Search disabled - only one person found)",
font=("Arial", 8), foreground="gray")
disabled_label.grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=(2, 0))
else:
print("DEBUG: Search functionality enabled - multiple people found")
# Normal helper label when search is enabled
last_name_label = ttk.Label(search_frame, text="Type Last Name", font=("Arial", 8), foreground="gray")
last_name_label.grid(row=1, column=0, sticky=tk.W, pady=(2, 0))
# Matched person info
matched_info_label = ttk.Label(left_frame, text="", font=("Arial", 10, "bold"))
@ -4168,6 +4307,14 @@ class PhotoTagger:
photo = ImageTk.PhotoImage(pil_image)
matched_canvas.create_image(150, 150, image=photo)
matched_canvas.image = photo
# Add photo icon to the matched person face - exactly in corner
# Use actual image dimensions instead of assuming 300x300
actual_width, actual_height = pil_image.size
self._create_photo_icon(matched_canvas, matched_photo_path, icon_size=20,
face_x=150, face_y=150,
face_width=actual_width, face_height=actual_height,
canvas_width=300, canvas_height=300)
except Exception as e:
matched_canvas.create_text(150, 150, text=f"❌ Could not load image: {e}", fill="red")
else:
@ -4275,6 +4422,12 @@ class PhotoTagger:
photo = ImageTk.PhotoImage(pil_image)
match_canvas.create_image(50, 50, image=photo)
match_canvas.image = photo
# Add photo icon to the unidentified face
self._create_photo_icon(match_canvas, unidentified_photo_path, icon_size=15,
face_x=50, face_y=50,
face_width=100, face_height=100,
canvas_width=100, canvas_height=100)
except Exception as e:
match_canvas.create_text(50, 50, text="", fill="red")
else: