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:
parent
a14a8a4231
commit
b910be9fe7
18
README.md
18
README.md
@ -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
|
||||
|
||||
157
photo_tagger.py
157
photo_tagger.py
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user