diff --git a/README.md b/README.md index 830c2a9..e6dfd73 100644 --- a/README.md +++ b/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 diff --git a/photo_tagger.py b/photo_tagger.py index 80a8be5..340c2f5 100644 --- a/photo_tagger.py +++ b/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", "", open_source_photo) + canvas.tag_bind("photo_icon", "", lambda e: (canvas.config(cursor="hand2"), show_tooltip(e))) + canvas.tag_bind("photo_icon", "", lambda e: (canvas.config(cursor=""), hide_tooltip(e))) + canvas.tag_bind("photo_icon", "", 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: