diff --git a/.notes/phase1_quickstart.md b/.notes/phase1_quickstart.md index 9037f02..05a349c 100644 --- a/.notes/phase1_quickstart.md +++ b/.notes/phase1_quickstart.md @@ -85,3 +85,4 @@ Tests passed: 4/4 All systems ready for DeepFace implementation! + diff --git a/.notes/phase2_quickstart.md b/.notes/phase2_quickstart.md index 6cc4fa3..0b1fbf1 100644 --- a/.notes/phase2_quickstart.md +++ b/.notes/phase2_quickstart.md @@ -129,3 +129,4 @@ Model: Facenet All systems ready for Phase 3! + diff --git a/DEEPFACE_MIGRATION_COMPLETE.md b/DEEPFACE_MIGRATION_COMPLETE.md index 789f6b5..8d22078 100644 --- a/DEEPFACE_MIGRATION_COMPLETE.md +++ b/DEEPFACE_MIGRATION_COMPLETE.md @@ -403,3 +403,4 @@ Expected output: - `PHASE3_COMPLETE.md` - Core processing migration - `.notes/deepface_migration_plan.md` - Original migration plan + diff --git a/PHASE1_COMPLETE.md b/PHASE1_COMPLETE.md index 68602d8..d018752 100644 --- a/PHASE1_COMPLETE.md +++ b/PHASE1_COMPLETE.md @@ -261,3 +261,4 @@ python3 tests/test_phase1_schema.py All database schema updates are complete and tested. The foundation is ready for implementing DeepFace face processing in Phase 3. + diff --git a/PHASE2_COMPLETE.md b/PHASE2_COMPLETE.md index 31ce42a..a905c48 100644 --- a/PHASE2_COMPLETE.md +++ b/PHASE2_COMPLETE.md @@ -374,3 +374,4 @@ python3 tests/test_phase2_config.py All configuration updates complete and tested. The GUI now has DeepFace settings, and FaceProcessor is ready to receive them. Phase 3 will implement the actual DeepFace processing code. + diff --git a/PHASE3_COMPLETE.md b/PHASE3_COMPLETE.md index f8e6e20..1aae012 100644 --- a/PHASE3_COMPLETE.md +++ b/PHASE3_COMPLETE.md @@ -479,3 +479,4 @@ The system now uses state-of-the-art face detection and recognition. All core fu **🎉 Congratulations! The PunimTag system is now powered by DeepFace! 🎉** + diff --git a/src/gui/identify_panel.py b/src/gui/identify_panel.py index 9c2aca5..2f7074a 100644 --- a/src/gui/identify_panel.py +++ b/src/gui/identify_panel.py @@ -268,10 +268,14 @@ class IdentifyPanel: min_quality = self.components['quality_filter_var'].get() min_quality_score = min_quality / 100.0 - # Reload faces with current filters + # Get sort option + sort_display = self.components['sort_var'].get() + sort_by = self.sort_value_map.get(sort_display, "quality") + + # Reload faces with current filters and sort option self.current_faces = self._get_unidentified_faces(batch_size, date_from, date_to, date_processed_from, date_processed_to, - min_quality_score) + min_quality_score, sort_by) print(f"✅ Reloaded: {len(self.current_faces)} faces") @@ -332,6 +336,78 @@ class IdentifyPanel: batch_entry = ttk.Entry(batch_frame, textvariable=self.components['batch_var'], width=8) batch_entry.pack(side=tk.LEFT, padx=(0, 10)) + # Sort option + ttk.Label(batch_frame, text="Sort by:").pack(side=tk.LEFT, padx=(10, 5)) + self.components['sort_var'] = tk.StringVar(value="quality") + sort_options = [ + ("Quality (Best First)", "quality"), + ("Quality (Worst First)", "quality_asc"), + ("Date Taken (Newest First)", "date_taken"), + ("Date Taken (Oldest First)", "date_taken_asc"), + ("Date Added (Newest First)", "date_added"), + ("Date Added (Oldest First)", "date_added_asc"), + ("Filename (A-Z)", "filename"), + ("Filename (Z-A)", "filename_desc"), + ("Detection Confidence (High First)", "confidence"), + ("Detection Confidence (Low First)", "confidence_asc") + ] + sort_combo = ttk.Combobox(batch_frame, textvariable=self.components['sort_var'], + values=[opt[0] for opt in sort_options], state="readonly", width=25) + sort_combo.pack(side=tk.LEFT, padx=(0, 10)) + + # Map display names to sort values + self.sort_value_map = {opt[0]: opt[1] for opt in sort_options} + + # Add sort change handler + def on_sort_change(event=None): + """Handle sort option change - refresh face list if identification is active""" + if self.is_active and self.current_faces: + # Show progress message + print("🔄 Refreshing face list with new sort order...") + self.main_frame.update() + + # Get current filters + date_from = self.components['date_from_var'].get().strip() or None + date_to = self.components['date_to_var'].get().strip() or None + date_processed_from = self.components['date_processed_from_var'].get().strip() or None + date_processed_to = self.components['date_processed_to_var'].get().strip() or None + + # Get batch size + try: + batch_size = int(self.components['batch_var'].get().strip()) + except Exception: + batch_size = DEFAULT_BATCH_SIZE + + # Get quality filter + min_quality = self.components['quality_filter_var'].get() + min_quality_score = min_quality / 100.0 + + # Get new sort option + sort_display = self.components['sort_var'].get() + sort_by = self.sort_value_map.get(sort_display, "quality") + + # Reload faces with new sort order + self.current_faces = self._get_unidentified_faces(batch_size, date_from, date_to, + date_processed_from, date_processed_to, + min_quality_score, sort_by) + + # Reset to first face and update display + self.current_face_index = 0 + if self.current_faces: + self._update_current_face() + self._update_button_states() + + # Update similar faces if compare is enabled + if self.components['compare_var'].get(): + face_id, _, _, _, _, _, _, _, _ = self.current_faces[self.current_face_index] + self._update_similar_faces(face_id) + + print(f"✅ Refreshed: {len(self.current_faces)} faces with new sort order") + else: + print("⚠️ No faces found with current filters and sort order") + + sort_combo.bind('<>', on_sort_change) + # Start button start_btn = ttk.Button(batch_frame, text="🚀 Start Identification", command=self._start_identification) start_btn.pack(side=tk.LEFT, padx=(10, 0)) @@ -500,10 +576,14 @@ class IdentifyPanel: min_quality = self.components['quality_filter_var'].get() min_quality_score = min_quality / 100.0 - # Get unidentified faces with quality filter + # Get sort option + sort_display = self.components['sort_var'].get() + sort_by = self.sort_value_map.get(sort_display, "quality") + + # Get unidentified faces with quality filter and sort option self.current_faces = self._get_unidentified_faces(batch_size, date_from, date_to, date_processed_from, date_processed_to, - min_quality_score) + min_quality_score, sort_by) if not self.current_faces: messagebox.showinfo("No Faces", "🎉 All faces have been identified!") @@ -528,7 +608,7 @@ class IdentifyPanel: def _get_unidentified_faces(self, batch_size: int, date_from: str = None, date_to: str = None, date_processed_from: str = None, date_processed_to: str = None, - min_quality_score: float = 0.0) -> List[Tuple]: + min_quality_score: float = 0.0, sort_by: str = "quality") -> List[Tuple]: """Get unidentified faces from database with optional date and quality filtering""" with self.db.get_db_connection() as conn: cursor = conn.cursor() @@ -567,8 +647,9 @@ class IdentifyPanel: query += ' AND DATE(p.date_added) <= ?' params.append(date_processed_to) - # Order by quality score (highest first) to show best quality faces first - query += ' ORDER BY f.quality_score DESC' + # Order by selected sort option + sort_clause = self._get_sort_clause(sort_by) + query += f' ORDER BY {sort_clause}' query += ' LIMIT ?' params.append(batch_size) @@ -576,6 +657,22 @@ class IdentifyPanel: cursor.execute(query, params) return cursor.fetchall() + def _get_sort_clause(self, sort_by: str) -> str: + """Get SQL ORDER BY clause based on sort option""" + sort_clauses = { + "quality": "f.quality_score DESC", + "quality_asc": "f.quality_score ASC", + "date_taken": "p.date_taken DESC", + "date_taken_asc": "p.date_taken ASC", + "date_added": "p.date_added DESC", + "date_added_asc": "p.date_added ASC", + "filename": "p.filename ASC", + "filename_desc": "p.filename DESC", + "confidence": "f.face_confidence DESC", + "confidence_asc": "f.face_confidence ASC" + } + return sort_clauses.get(sort_by, "f.quality_score DESC") # Default to quality DESC + def _prefetch_identify_data(self, faces: List[Tuple]) -> Dict: """Pre-fetch all needed data to avoid repeated database queries""" cache = { @@ -1379,10 +1476,14 @@ class IdentifyPanel: min_quality = self.components['quality_filter_var'].get() min_quality_score = min_quality / 100.0 + # Get sort option + sort_display = self.components['sort_var'].get() + sort_by = self.sort_value_map.get(sort_display, "quality") + # Get more faces more_faces = self._get_unidentified_faces(DEFAULT_BATCH_SIZE, date_from, date_to, date_processed_from, date_processed_to, - min_quality_score) + min_quality_score, sort_by) if more_faces: # Add to current faces @@ -1575,10 +1676,14 @@ class IdentifyPanel: # Quality filter is already extracted above in min_quality min_quality_score = min_quality / 100.0 - # Reload faces with new filters + # Get sort option + sort_display = self.components['sort_var'].get() + sort_by = self.sort_value_map.get(sort_display, "quality") + + # Reload faces with new filters and sort option self.current_faces = self._get_unidentified_faces(batch_size, date_from, date_to, date_processed_from, date_processed_to, - min_quality_score) + min_quality_score, sort_by) if not self.current_faces: messagebox.showinfo("No Faces Found", "No unidentified faces found with the current filters.") diff --git a/tests/test_phase1_schema.py b/tests/test_phase1_schema.py index 4294cc0..474022d 100755 --- a/tests/test_phase1_schema.py +++ b/tests/test_phase1_schema.py @@ -326,3 +326,4 @@ if __name__ == "__main__": success = run_all_tests() sys.exit(0 if success else 1) + diff --git a/tests/test_phase3_deepface.py b/tests/test_phase3_deepface.py index 448b8c9..cacc97c 100755 --- a/tests/test_phase3_deepface.py +++ b/tests/test_phase3_deepface.py @@ -338,3 +338,4 @@ if __name__ == "__main__": success = run_all_tests() sys.exit(0 if success else 1) +