A bunch of changes
This commit is contained in:
135
bagheeraview.py
135
bagheeraview.py
@@ -579,8 +579,6 @@ class ThumbnailDelegate(QStyledItemDelegate):
|
||||
thumb_size = self.main_win.current_thumb_size
|
||||
path = index.data(PATH_ROLE)
|
||||
mtime = index.data(MTIME_ROLE)
|
||||
inode = index.data(INODE_ROLE)
|
||||
device_id = index.data(DEVICE_ROLE)
|
||||
|
||||
# Optimization: Use QPixmapCache to avoid expensive QImage->QPixmap
|
||||
# conversion on every paint event.
|
||||
@@ -589,6 +587,8 @@ class ThumbnailDelegate(QStyledItemDelegate):
|
||||
|
||||
if not source_pixmap or source_pixmap.isNull():
|
||||
# Not in UI cache, try to get from main thumbnail cache (Memory/LMDB)
|
||||
inode = index.data(INODE_ROLE)
|
||||
device_id = index.data(DEVICE_ROLE)
|
||||
img, _ = self.main_win.cache.get_thumbnail(
|
||||
path, requested_size=thumb_size, curr_mtime=mtime,
|
||||
inode=inode, device_id=device_id, async_load=True)
|
||||
@@ -863,21 +863,35 @@ class ThumbnailSortFilterProxyModel(QSortFilterProxyModel):
|
||||
def lessThan(self, left, right):
|
||||
"""Custom sorting logic for name and date."""
|
||||
sort_role = self.sortRole()
|
||||
left_data = self.sourceModel().data(left, sort_role)
|
||||
right_data = self.sourceModel().data(right, sort_role)
|
||||
|
||||
if sort_role == MTIME_ROLE:
|
||||
left = left_data if left_data is not None else 0
|
||||
right = right_data if right_data is not None else 0
|
||||
return left < right
|
||||
left_data = self.sourceModel().data(left, sort_role)
|
||||
right_data = self.sourceModel().data(right, sort_role)
|
||||
# Treat None as 0 for safe comparison
|
||||
left_val = left_data if left_data is not None else 0
|
||||
right_val = right_data if right_data is not None else 0
|
||||
return left_val < right_val
|
||||
|
||||
# Default (DisplayRole) is case-insensitive name sorting
|
||||
# Handle None values safely
|
||||
l_str = str(left_data) if left_data is not None else ""
|
||||
r_str = str(right_data) if right_data is not None else ""
|
||||
# Default (DisplayRole) is name sorting.
|
||||
# Optimization: Use the pre-calculated lowercase name from the cache
|
||||
# to avoid repeated string operations during sorting.
|
||||
left_path = self.sourceModel().data(left, PATH_ROLE)
|
||||
right_path = self.sourceModel().data(right, PATH_ROLE)
|
||||
|
||||
# Fallback for non-thumbnail items (like headers) or if cache is missing
|
||||
if not left_path or not right_path or not self._data_cache:
|
||||
l_str = str(self.sourceModel().data(left, Qt.DisplayRole) or "")
|
||||
r_str = str(self.sourceModel().data(right, Qt.DisplayRole) or "")
|
||||
return l_str.lower() < r_str.lower()
|
||||
|
||||
# Get from cache, with a fallback just in case
|
||||
_, left_name_lower = self._data_cache.get(
|
||||
left_path, (None, os.path.basename(left_path).lower()))
|
||||
_, right_name_lower = self._data_cache.get(
|
||||
right_path, (None, os.path.basename(right_path).lower()))
|
||||
|
||||
return left_name_lower < right_name_lower
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""
|
||||
@@ -908,6 +922,7 @@ class MainWindow(QMainWindow):
|
||||
self.current_thumb_size = THUMBNAILS_DEFAULT_SIZE
|
||||
self.face_names_history = []
|
||||
self.pet_names_history = []
|
||||
self.body_names_history = []
|
||||
self.object_names_history = []
|
||||
self.landmark_names_history = []
|
||||
self.mru_tags = deque(maxlen=APP_CONFIG.get(
|
||||
@@ -1466,6 +1481,10 @@ class MainWindow(QMainWindow):
|
||||
if "geometry" in mw_data:
|
||||
g = mw_data["geometry"]
|
||||
self.setGeometry(g["x"], g["y"], g["w"], g["h"])
|
||||
|
||||
selected_path = mw_data.get("selected_path")
|
||||
select_paths = [selected_path] if selected_path else None
|
||||
|
||||
if "window_state" in mw_data:
|
||||
self.restoreState(
|
||||
QByteArray.fromBase64(mw_data["window_state"].encode()))
|
||||
@@ -1521,7 +1540,7 @@ class MainWindow(QMainWindow):
|
||||
paths.append(d)
|
||||
self.start_scan([p.strip() for p in paths if p.strip()
|
||||
and os.path.exists(os.path.expanduser(p.strip()))],
|
||||
select_path=mw_data.get("selected_path"))
|
||||
select_paths=select_paths)
|
||||
if search_text:
|
||||
self.search_input.setEditText(search_text)
|
||||
|
||||
@@ -1643,6 +1662,11 @@ class MainWindow(QMainWindow):
|
||||
if len(self.face_names_history) > new_max_faces:
|
||||
self.face_names_history = self.face_names_history[:new_max_faces]
|
||||
|
||||
new_max_bodies = APP_CONFIG.get("body_menu_max_items",
|
||||
constants.FACES_MENU_MAX_ITEMS_DEFAULT)
|
||||
if len(self.body_names_history) > new_max_bodies:
|
||||
self.body_names_history = self.body_names_history[:new_max_bodies]
|
||||
|
||||
new_bg_color = APP_CONFIG.get("thumbnails_bg_color",
|
||||
constants.THUMBNAILS_BG_COLOR_DEFAULT)
|
||||
self.thumbnail_view.setStyleSheet(f"background-color: {new_bg_color};")
|
||||
@@ -1974,6 +1998,44 @@ class MainWindow(QMainWindow):
|
||||
|
||||
return False
|
||||
|
||||
def get_selected_paths(self):
|
||||
"""Returns a list of all selected file paths."""
|
||||
paths = []
|
||||
seen = set()
|
||||
for idx in self.thumbnail_view.selectedIndexes():
|
||||
path = self.proxy_model.data(idx, PATH_ROLE)
|
||||
if path and path not in seen:
|
||||
paths.append(path)
|
||||
seen.add(path)
|
||||
return paths
|
||||
|
||||
def restore_selection(self, paths):
|
||||
"""Restores selection for a list of paths."""
|
||||
if not paths:
|
||||
return
|
||||
|
||||
selection_model = self.thumbnail_view.selectionModel()
|
||||
selection = QItemSelection()
|
||||
first_valid_index = QModelIndex()
|
||||
|
||||
for path in paths:
|
||||
if path in self._path_to_model_index:
|
||||
persistent_index = self._path_to_model_index[path]
|
||||
if persistent_index.isValid():
|
||||
source_index = QModelIndex(persistent_index)
|
||||
proxy_index = self.proxy_model.mapFromSource(source_index)
|
||||
if proxy_index.isValid():
|
||||
selection.select(proxy_index, proxy_index)
|
||||
if not first_valid_index.isValid():
|
||||
first_valid_index = proxy_index
|
||||
|
||||
if not selection.isEmpty():
|
||||
selection_model.select(selection, QItemSelectionModel.ClearAndSelect)
|
||||
if first_valid_index.isValid():
|
||||
self.thumbnail_view.setCurrentIndex(first_valid_index)
|
||||
self.thumbnail_view.scrollTo(
|
||||
first_valid_index, QAbstractItemView.EnsureVisible)
|
||||
|
||||
def toggle_visibility(self):
|
||||
"""Toggles the visibility of the main window, opening a viewer if needed."""
|
||||
if self.isVisible():
|
||||
@@ -2247,7 +2309,7 @@ class MainWindow(QMainWindow):
|
||||
w.load_and_fit_image()
|
||||
|
||||
def start_scan(self, paths, sync_viewer=False, active_viewer=None,
|
||||
select_path=None):
|
||||
select_paths=None):
|
||||
"""
|
||||
Starts a new background scan for images.
|
||||
|
||||
@@ -2255,7 +2317,7 @@ class MainWindow(QMainWindow):
|
||||
paths (list): A list of file paths or directories to scan.
|
||||
sync_viewer (bool): If True, avoids clearing the grid.
|
||||
active_viewer (ImageViewer): A viewer to sync with the scan results.
|
||||
select_path (str): A path to select automatically after the scan finishes.
|
||||
select_paths (list): A list of paths to select automatically.
|
||||
"""
|
||||
self.is_cleaning = True
|
||||
self._suppress_updates = True
|
||||
@@ -2299,11 +2361,11 @@ class MainWindow(QMainWindow):
|
||||
self.scanner.progress_msg.connect(self.status_lbl.setText)
|
||||
self.scanner.more_files_available.connect(self.more_files_available)
|
||||
self.scanner.finished_scan.connect(
|
||||
lambda n: self._on_scan_finished(n, select_path))
|
||||
lambda n: self._on_scan_finished(n, select_paths))
|
||||
self.scanner.start()
|
||||
self._scan_all = False
|
||||
|
||||
def _on_scan_finished(self, n, select_path=None):
|
||||
def _on_scan_finished(self, n, select_paths=None):
|
||||
"""Slot for when the image scanner has finished."""
|
||||
self._suppress_updates = False
|
||||
self._scanner_last_index = self._scanner_total_files
|
||||
@@ -2331,8 +2393,8 @@ class MainWindow(QMainWindow):
|
||||
self.update_tag_edit_widget()
|
||||
|
||||
# Select a specific path if requested (e.g., after layout restore)
|
||||
if select_path:
|
||||
self.find_and_select_path(select_path)
|
||||
if select_paths:
|
||||
self.restore_selection(select_paths)
|
||||
|
||||
# Final rebuild to ensure all items are correctly placed
|
||||
if self.rebuild_timer.isActive():
|
||||
@@ -2573,7 +2635,7 @@ class MainWindow(QMainWindow):
|
||||
self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None))
|
||||
|
||||
# Preserve selection
|
||||
selected_path = self.get_current_selected_path()
|
||||
selected_paths = self.get_selected_paths()
|
||||
|
||||
mode = self.sort_combo.currentText()
|
||||
rev = "↓" in mode
|
||||
@@ -2628,7 +2690,7 @@ class MainWindow(QMainWindow):
|
||||
self._suppress_updates = False
|
||||
self.apply_filters()
|
||||
self.thumbnail_view.setUpdatesEnabled(True)
|
||||
self.find_and_select_path(selected_path)
|
||||
self.restore_selection(selected_paths)
|
||||
|
||||
if self.main_dock.isVisible() and \
|
||||
self.tags_tabs.currentWidget() == self.filter_widget:
|
||||
@@ -2782,7 +2844,7 @@ class MainWindow(QMainWindow):
|
||||
self._suppress_updates = False
|
||||
self.apply_filters()
|
||||
self.thumbnail_view.setUpdatesEnabled(True)
|
||||
self.find_and_select_path(selected_path)
|
||||
self.restore_selection(selected_paths)
|
||||
|
||||
if self.main_dock.isVisible() and \
|
||||
self.tags_tabs.currentWidget() == self.filter_widget:
|
||||
@@ -3064,7 +3126,7 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
|
||||
# Preserve selection
|
||||
selected_path = self.get_current_selected_path()
|
||||
selected_paths = self.get_selected_paths()
|
||||
|
||||
# Gather filter criteria from the UI
|
||||
include_tags = set()
|
||||
@@ -3112,8 +3174,8 @@ class MainWindow(QMainWindow):
|
||||
self.filtered_count_lbl.setText(UITexts.FILTERED_ZERO)
|
||||
|
||||
# Restore selection if it's still visible
|
||||
if selected_path:
|
||||
self.find_and_select_path(selected_path)
|
||||
if selected_paths:
|
||||
self.restore_selection(selected_paths)
|
||||
|
||||
# Sync open viewers with the new list of visible paths
|
||||
visible_paths = self.get_visible_image_paths()
|
||||
@@ -3163,13 +3225,18 @@ class MainWindow(QMainWindow):
|
||||
target_list.append(current_path)
|
||||
new_index = len(target_list) - 1
|
||||
|
||||
w.controller.update_list(
|
||||
target_list, new_index if new_index != -1 else None)
|
||||
# Check if we are preserving the image to pass correct metadata
|
||||
tags_to_pass = None
|
||||
rating_to_pass = 0
|
||||
|
||||
if new_index != -1 and new_index < len(target_list):
|
||||
if target_list[new_index] == current_path_in_viewer:
|
||||
tags_to_pass = viewer_tags
|
||||
rating_to_pass = viewer_rating
|
||||
|
||||
# Pass current image's tags and rating to the controller
|
||||
w.controller.update_list(
|
||||
target_list, new_index if new_index != -1 else None,
|
||||
viewer_tags, viewer_rating)
|
||||
tags_to_pass, rating_to_pass)
|
||||
if not w._is_persistent and not w.controller.image_list:
|
||||
w.close()
|
||||
continue
|
||||
@@ -3468,16 +3535,16 @@ class MainWindow(QMainWindow):
|
||||
if not self.history:
|
||||
return
|
||||
|
||||
current_selection = self.get_current_selected_path()
|
||||
current_selection = self.get_selected_paths()
|
||||
term = self.history[0]
|
||||
if term.startswith("file:/"):
|
||||
path = term[6:]
|
||||
if os.path.isfile(path):
|
||||
self.start_scan([os.path.dirname(path)], select_path=current_selection)
|
||||
self.start_scan([os.path.dirname(path)], select_paths=current_selection)
|
||||
return
|
||||
self.process_term(term, select_path=current_selection)
|
||||
self.process_term(term, select_paths=current_selection)
|
||||
|
||||
def process_term(self, term, select_path=None):
|
||||
def process_term(self, term, select_paths=None):
|
||||
"""Processes a search term, file path, or layout directive."""
|
||||
self.add_to_history(term)
|
||||
self.update_search_input()
|
||||
@@ -3529,7 +3596,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
else:
|
||||
# If a directory or search term, start a scan
|
||||
self.start_scan([path], select_path=select_path)
|
||||
self.start_scan([path], select_paths=select_paths)
|
||||
|
||||
def update_search_input(self):
|
||||
"""Updates the search input combo box with history items and icons."""
|
||||
@@ -3607,6 +3674,7 @@ class MainWindow(QMainWindow):
|
||||
self.tags_tabs.setCurrentIndex(d["active_dock_tab"])
|
||||
self.face_names_history = d.get("face_names_history", [])
|
||||
self.pet_names_history = d.get("pet_names_history", [])
|
||||
self.body_names_history = d.get("body_names_history", [])
|
||||
self.object_names_history = d.get("object_names_history", [])
|
||||
self.landmark_names_history = d.get("landmark_names_history", [])
|
||||
|
||||
@@ -3674,6 +3742,7 @@ class MainWindow(QMainWindow):
|
||||
APP_CONFIG["active_dock_tab"] = self.tags_tabs.currentIndex()
|
||||
APP_CONFIG["face_names_history"] = self.face_names_history
|
||||
APP_CONFIG["pet_names_history"] = self.pet_names_history
|
||||
APP_CONFIG["body_names_history"] = self.body_names_history
|
||||
APP_CONFIG["object_names_history"] = self.object_names_history
|
||||
APP_CONFIG["landmark_names_history"] = self.landmark_names_history
|
||||
APP_CONFIG["mru_tags"] = list(self.mru_tags)
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
v0.9.11 -
|
||||
· Hacer que el image viewer standalone admita múltiles sort
|
||||
· Filmstrip fixed
|
||||
|
||||
HAVE_BAGHEERASEARCH_LIB
|
||||
|
||||
Refactor `load_image` to check if `pixmap_original` is already valid before reloading to optimize performance.
|
||||
|
||||
Check if the `ImagePreloader` handles file deletion correctly if the file is deleted while being preloaded.
|
||||
|
||||
Me gustaría implementar un modo de "Comparación" para ver 2 o 4 imágenes lado a lado en el visor. ¿Cómo podría abordarlo?
|
||||
|
||||
· La instalación no debe usar Bagheera como motor a no ser que esté instalado.
|
||||
· Hacer que el image viewer standalone admita múltiples sort
|
||||
· Comprobar hotkeys y funcionamiento en general.
|
||||
· Inhibir el salvapantallas con el slideshow y añadir opción de menú para inhibirlo durante un tiempo determinado
|
||||
· Mejorar el menú Open, con nombres correctos e iconos adecuados
|
||||
@@ -12,12 +23,8 @@ v0.9.11 -
|
||||
· Si quisiera distribuir mi aplicación como un AppImage, ¿cómo empaquetaría estos plugins de KDE para que funcionen en otros sistemas?
|
||||
· Solucionar el problema de las ventanas de diálogo nativas, tan simple como usar PySide nativo.
|
||||
|
||||
Analiza si la estrategia LIFO (Last-In, First-Out) en `CacheLoader` es la ideal para una galería de imágenes o si debería ser mixta.
|
||||
|
||||
¿Cómo puedo añadir una opción para limitar el número de hilos que `ImageScanner` puede usar para la generación de miniaturas?
|
||||
|
||||
Verifica si el uso de `QPixmapCache` en `ThumbnailDelegate.paint_thumbnail` está optimizado para evitar la conversión repetitiva de QImage a QPixmap, lo que podría causar ralentizaciones al hacer scroll rápido.
|
||||
|
||||
Check if the `_suppress_updates` flag correctly prevents potential race conditions in `update_tag_list` when switching view modes rapidly.
|
||||
|
||||
Verify if `find_and_select_path` logic in `on_view_mode_changed` handles cases where the selected item is filtered out after the view mode change.
|
||||
|
||||
100
constants.py
100
constants.py
@@ -110,7 +110,7 @@ SCANNER_SETTINGS_DEFAULTS = {
|
||||
"scan_full_on_start": True,
|
||||
"person_tags": "",
|
||||
"generation_threads": 4,
|
||||
"search_engine": "Native"
|
||||
"search_engine": ""
|
||||
}
|
||||
|
||||
# --- IMAGE VIEWER DEFAULTS ---
|
||||
@@ -193,6 +193,10 @@ AVAILABLE_PET_ENGINES = []
|
||||
if HAVE_MEDIAPIPE:
|
||||
AVAILABLE_PET_ENGINES.append("mediapipe")
|
||||
|
||||
AVAILABLE_BODY_ENGINES = []
|
||||
if HAVE_MEDIAPIPE:
|
||||
AVAILABLE_BODY_ENGINES.append("mediapipe")
|
||||
|
||||
# Determine the default engine. This can be overridden by user config.
|
||||
DEFAULT_FACE_ENGINE = AVAILABLE_FACE_ENGINES[0] if AVAILABLE_FACE_ENGINES else None
|
||||
DEFAULT_PET_ENGINE = AVAILABLE_PET_ENGINES[0] if AVAILABLE_PET_ENGINES else None
|
||||
@@ -205,6 +209,7 @@ PET_DETECTION_ENGINE = APP_CONFIG.get("pet_detection_engine",
|
||||
DEFAULT_PET_ENGINE)
|
||||
|
||||
DEFAULT_PET_BOX_COLOR = "#98FB98" # PaleGreen
|
||||
DEFAULT_BODY_BOX_COLOR = "#FF4500" # OrangeRed
|
||||
DEFAULT_OBJECT_BOX_COLOR = "#FFD700" # Gold
|
||||
DEFAULT_LANDMARK_BOX_COLOR = "#00BFFF" # DeepSkyBlue
|
||||
# --- SHORTCUTS ---
|
||||
@@ -273,6 +278,7 @@ VIEWER_ACTIONS = {
|
||||
"detect_faces": ("Detect Faces", "Actions"),
|
||||
"detect_pets": ("Detect Pets", "Actions"),
|
||||
"fast_tag": ("Quick Tags", "Actions"),
|
||||
"detect_bodies": ("Detect Bodies", "Actions"),
|
||||
"rotate_right": ("Rotate Right", "Transform"),
|
||||
"rotate_left": ("Rotate Left", "Transform"),
|
||||
"zoom_in": ("Zoom In", "Transform"),
|
||||
@@ -299,6 +305,7 @@ DEFAULT_VIEWER_SHORTCUTS = {
|
||||
"fullscreen": (Qt.Key_F11, Qt.NoModifier),
|
||||
"detect_faces": (Qt.Key_F, Qt.NoModifier),
|
||||
"detect_pets": (Qt.Key_P, Qt.NoModifier),
|
||||
"detect_bodies": (Qt.Key_B, Qt.NoModifier),
|
||||
"fast_tag": (Qt.Key_T, Qt.NoModifier),
|
||||
"rotate_right": (Qt.Key_Plus, Qt.ControlModifier),
|
||||
"rotate_left": (Qt.Key_Minus, Qt.ControlModifier),
|
||||
@@ -395,13 +402,15 @@ _UI_TEXTS = {
|
||||
"RENAME_VIEWER_ERROR_TEXT": "Could not rename file: {}",
|
||||
"ADD_FACE_TITLE": "Add Face",
|
||||
"ADD_PET_TITLE": "Add Pet",
|
||||
"ADD_BODY_TITLE": "Add Body",
|
||||
"ADD_OBJECT_TITLE": "Add Object",
|
||||
"ADD_LANDMARK_TITLE": "Add Landmark",
|
||||
"ADD_FACE_LABEL": "Name:",
|
||||
"ADD_PET_LABEL": "Name:",
|
||||
"ADD_BODY_LABEL": "Name:",
|
||||
"ADD_OBJECT_LABEL": "Name:",
|
||||
"ADD_LANDMARK_LABEL": "Name:",
|
||||
"DELETE_FACE": "Delete Face or area",
|
||||
"DELETE_AREA_TITLE": "Delete area",
|
||||
"CREATE_TAG_TITLE": "Create Tag",
|
||||
"CREATE_TAG_TEXT": "The tag for '{}' does not exist. Do you want to create a "
|
||||
"new one?",
|
||||
@@ -409,6 +418,8 @@ _UI_TEXTS = {
|
||||
"NEW_PERSON_TAG_TEXT": "Enter the full path for the tag:",
|
||||
"NEW_PET_TAG_TITLE": "New Pet Tag",
|
||||
"NEW_PET_TAG_TEXT": "Enter the full path for the tag:",
|
||||
"NEW_BODY_TAG_TITLE": "New Body Tag",
|
||||
"NEW_BODY_TAG_TEXT": "Enter the full path for the tag:",
|
||||
"NEW_OBJECT_TAG_TITLE": "New Object Tag",
|
||||
"NEW_OBJECT_TAG_TEXT": "Enter the full path for the tag:",
|
||||
"NEW_LANDMARK_TAG_TITLE": "New Landmark Tag",
|
||||
@@ -418,10 +429,11 @@ _UI_TEXTS = {
|
||||
"one:",
|
||||
"FACE_NAME_TOOLTIP": "Type a name or select from history.",
|
||||
"CLEAR_TEXT_TOOLTIP": "Clear text field",
|
||||
"RENAME_FACE_TITLE": "Rename Face or area",
|
||||
"RENAME_AREA_TITLE": "Rename area",
|
||||
"SHOW_FACES": "Show Faces && other areas",
|
||||
"DETECT_FACES": "Detect Face",
|
||||
"DETECT_PETS": "Detect Pets",
|
||||
"DETECT_BODIES": "Detect Bodies",
|
||||
"NO_FACE_LIBS": "No face detection libraries found. Install 'mediapipe' or "
|
||||
"'face_recognition'.",
|
||||
"THUMBNAIL_NO_NAME": "No name",
|
||||
@@ -441,7 +453,7 @@ _UI_TEXTS = {
|
||||
"MENU_SHOW_HISTORY": "Show History",
|
||||
"MENU_SETTINGS": "Settings",
|
||||
"SETTINGS_GROUP_SCANNER": "Scanner",
|
||||
"SETTINGS_GROUP_FACES": "Faces && areas",
|
||||
"SETTINGS_GROUP_AREAS": "Areas",
|
||||
"SETTINGS_GROUP_THUMBNAILS": "Thumbnails",
|
||||
"SETTINGS_GROUP_VIEWER": "Image Viewer",
|
||||
"SETTINGS_PERSON_TAGS_LABEL": "Person tags:",
|
||||
@@ -460,8 +472,19 @@ _UI_TEXTS = {
|
||||
"to remember.",
|
||||
"TYPE_FACE": "Face",
|
||||
"TYPE_PET": "Pet",
|
||||
"TYPE_BODY": "Body",
|
||||
"TYPE_OBJECT": "Object",
|
||||
"TYPE_LANDMARK": "Landmark",
|
||||
"SETTINGS_BODY_TAGS_LABEL": "Body tags:",
|
||||
"SETTINGS_BODY_ENGINE_LABEL": "Body Detection Engine:",
|
||||
"SETTINGS_BODY_COLOR_LABEL": "Body box color:",
|
||||
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Max body history:",
|
||||
"SETTINGS_BODY_TAGS_TOOLTIP": "Default tags for bodies, separated by commas.",
|
||||
"SETTINGS_BODY_ENGINE_TOOLTIP": "Library used for body detection.",
|
||||
"SETTINGS_BODY_COLOR_TOOLTIP": "Color of the bounding box drawn around "
|
||||
"detected bodies.",
|
||||
"SETTINGS_BODY_HISTORY_TOOLTIP": "Maximum number of recently used body names "
|
||||
"to remember.",
|
||||
"SETTINGS_OBJECT_TAGS_LABEL": "Object tags:",
|
||||
"SETTINGS_OBJECT_ENGINE_LABEL": "Object Detection Engine:",
|
||||
"SETTINGS_OBJECT_COLOR_LABEL": "Object box color:",
|
||||
@@ -493,12 +516,15 @@ _UI_TEXTS = {
|
||||
"SETTINGS_THUMBS_RATING_COLOR_LABEL": "Thumbnails rating color:",
|
||||
"SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Thumbnails filename font size:",
|
||||
"SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Thumbnails tags font size:",
|
||||
"SETTINGS_SCAN_THREADS_LABEL": "Generation threads:",
|
||||
"SETTINGS_SCAN_THREADS_TOOLTIP": "Maximum number of simultaneous threads to"
|
||||
"generate thumbnails.",
|
||||
"SETTINGS_SCAN_MAX_LEVEL_LABEL": "Scan Max Level:",
|
||||
"SETTINGS_SCAN_BATCH_SIZE_LABEL": "Scan Batch Size:",
|
||||
"SETTINGS_SCAN_FULL_ON_START_LABEL": "Scan Full On Start:",
|
||||
"SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "File search engine:",
|
||||
"SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Engine to use for finding files. "
|
||||
"'Native' uses BagheeraSearch library. 'baloosearch' uses KDE Baloo command.",
|
||||
"'Bagheera' uses BagheeraSearch library. 'Baloo' uses 'baloosearch' command.",
|
||||
"SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Maximum directory depth to scan "
|
||||
"recursively.",
|
||||
"SETTINGS_SCAN_BATCH_SIZE_TOOLTIP": "Number of images to load in each batch.",
|
||||
@@ -524,8 +550,8 @@ _UI_TEXTS = {
|
||||
"SETTINGS_THUMBS_FILENAME_FONT_SIZE_TOOLTIP": "Font size for filenames in "
|
||||
"thumbnails.",
|
||||
"SETTINGS_THUMBS_TAGS_FONT_SIZE_TOOLTIP": "Font size for tags in thumbnails.",
|
||||
"SEARCH_ENGINE_NATIVE": "Native",
|
||||
"SEARCH_ENGINE_BALOO": "baloosearch",
|
||||
"SEARCH_ENGINE_NATIVE": "Bagheera",
|
||||
"SEARCH_ENGINE_BALOO": "Baloo",
|
||||
"SETTINGS_VIEWER_WHEEL_SPEED_LABEL": "Viewer mouse wheel speed:",
|
||||
"SETTINGS_THUMBS_FILENAME_LINES_LABEL": "Filename lines:",
|
||||
"SETTINGS_THUMBS_FILENAME_LINES_TOOLTIP": "Number of lines for the filename "
|
||||
@@ -801,19 +827,23 @@ _UI_TEXTS = {
|
||||
"RENAME_VIEWER_ERROR_TEXT": "No se pudo renombrar el archivo: {}",
|
||||
"ADD_FACE_TITLE": "Añadir Rostro",
|
||||
"ADD_PET_TITLE": "Añadir Mascota",
|
||||
"ADD_BODY_TITLE": "Añadir Cuerpo",
|
||||
"ADD_OBJECT_TITLE": "Añadir Objeto",
|
||||
"ADD_LANDMARK_TITLE": "Añadir Lugar",
|
||||
"ADD_FACE_LABEL": "Nombre:",
|
||||
"ADD_PET_LABEL": "Nombre:",
|
||||
"ADD_BODY_LABEL": "Nombre:",
|
||||
"ADD_OBJECT_LABEL": "Nombre:",
|
||||
"ADD_LANDMARK_LABEL": "Nombre:",
|
||||
"DELETE_FACE": "Eliminar Rostro o área",
|
||||
"DELETE_AREA_TITLE": "Eliminar área",
|
||||
"CREATE_TAG_TITLE": "Crear Etiqueta",
|
||||
"CREATE_TAG_TEXT": "La etiqueta para '{}' no existe. ¿Deseas crear una nueva?",
|
||||
"NEW_PERSON_TAG_TITLE": "Nueva Etiqueta de Persona",
|
||||
"NEW_PERSON_TAG_TEXT": "Introduce la ruta completa de la etiqueta:",
|
||||
"NEW_PET_TAG_TITLE": "Nueva Etiqueta de Mascota",
|
||||
"NEW_PET_TAG_TEXT": "Introduce la ruta completa de la etiqueta:",
|
||||
"NEW_BODY_TAG_TITLE": "Nueva Etiqueta de Cuerpo",
|
||||
"NEW_BODY_TAG_TEXT": "Introduce la ruta completa de la etiqueta:",
|
||||
"NEW_OBJECT_TAG_TITLE": "Nueva Etiqueta de Objeto",
|
||||
"NEW_OBJECT_TAG_TEXT": "Introduce la ruta completa de la etiqueta:",
|
||||
"NEW_LANDMARK_TAG_TITLE": "Nueva Etiqueta de Lugar",
|
||||
@@ -823,10 +853,11 @@ _UI_TEXTS = {
|
||||
"selecciona la correcta:",
|
||||
"FACE_NAME_TOOLTIP": "Escribe un nombre o selecciónalo del historial.",
|
||||
"CLEAR_TEXT_TOOLTIP": "Limpiar el campo de texto",
|
||||
"RENAME_FACE_TITLE": "Renombrar Rostro o área",
|
||||
"RENAME_AREA_TITLE": "Renombrar área",
|
||||
"SHOW_FACES": "Mostrar Rostros y otras áreas",
|
||||
"DETECT_FACES": "Detectar Rostros",
|
||||
"DETECT_PETS": "Detectar Mascotas",
|
||||
"DETECT_BODIES": "Detectar Cuerpos",
|
||||
"NO_FACE_LIBS": "No se encontraron librerías de detección de rostros. Instale "
|
||||
"'mediapipe' o 'face_recognition'.",
|
||||
"THUMBNAIL_NO_NAME": "Sin nombre",
|
||||
@@ -846,7 +877,7 @@ _UI_TEXTS = {
|
||||
"MENU_SHOW_HISTORY": "Mostrar Historial",
|
||||
"MENU_SETTINGS": "Opciones",
|
||||
"SETTINGS_GROUP_SCANNER": "Escáner",
|
||||
"SETTINGS_GROUP_FACES": "Rostros y áreas",
|
||||
"SETTINGS_GROUP_AREAS": "Áreas",
|
||||
"SETTINGS_GROUP_THUMBNAILS": "Miniaturas",
|
||||
"SETTINGS_GROUP_VIEWER": "Visor de Imágenes",
|
||||
"SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persona:",
|
||||
@@ -867,8 +898,21 @@ _UI_TEXTS = {
|
||||
"usados recientemente para recordar.",
|
||||
"TYPE_FACE": "Cara",
|
||||
"TYPE_PET": "Mascota",
|
||||
"TYPE_BODY": "Cuerpo",
|
||||
"TYPE_OBJECT": "Objeto",
|
||||
"TYPE_LANDMARK": "Lugar",
|
||||
"SETTINGS_BODY_TAGS_LABEL": "Etiquetas de cuerpo:",
|
||||
"SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de cuerpos:",
|
||||
"SETTINGS_BODY_COLOR_LABEL": "Color del recuadro de cuerpo:",
|
||||
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máx historial cuerpos:",
|
||||
"SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para cuerpos, "
|
||||
"separadas por comas.",
|
||||
"SETTINGS_BODY_ENGINE_TOOLTIP": "Librería utilizada para la detección de "
|
||||
"cuerpos.",
|
||||
"SETTINGS_BODY_COLOR_TOOLTIP": "Color del cuadro delimitador dibujado "
|
||||
"alrededor de los cuerpos detectados.",
|
||||
"SETTINGS_BODY_HISTORY_TOOLTIP": "Número máximo de nombres de cuerpos "
|
||||
"usados recientemente para recordar.",
|
||||
"SETTINGS_OBJECT_TAGS_LABEL": "Etiquetas de objeto:",
|
||||
"SETTINGS_OBJECT_ENGINE_LABEL": "Motor de detección de objetos:",
|
||||
"SETTINGS_OBJECT_COLOR_LABEL": "Color del recuadro de objeto:",
|
||||
@@ -906,8 +950,8 @@ _UI_TEXTS = {
|
||||
"SETTINGS_SCAN_BATCH_SIZE_LABEL": "Tamaño de Lote de Escaneo:",
|
||||
"SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "Motor de búsqueda de archivos:",
|
||||
"SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Motor a usar para buscar archivos. "
|
||||
"'Nativo' usa la librería de BagheeraSearch. 'baloosearch' usa el commando de"
|
||||
"KDE Baloo.",
|
||||
"'Bagheera' usa la librería de BagheeraSearch. 'Baloo0 usa el commando "
|
||||
"'baloosearch'",
|
||||
"SETTINGS_SCAN_FULL_ON_START_LABEL": "Escanear Todo al Inicio:",
|
||||
"SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Profundidad máxima de directorio para "
|
||||
"escanear recursivamente.",
|
||||
@@ -1213,19 +1257,23 @@ _UI_TEXTS = {
|
||||
"RENAME_VIEWER_ERROR_TEXT": "Non se puido renomear o ficheiro: {}",
|
||||
"ADD_FACE_TITLE": "Engadir Rostro",
|
||||
"ADD_PET_TITLE": "Engadir Mascota",
|
||||
"ADD_BODY_TITLE": "Engadir Corpo",
|
||||
"ADD_OBJECT_TITLE": "Engadir Obxecto",
|
||||
"ADD_LANDMARK_TITLE": "Engadir Lugar",
|
||||
"ADD_FACE_LABEL": "Nome:",
|
||||
"ADD_PET_LABEL": "Nome:",
|
||||
"ADD_BODY_LABEL": "Nome:",
|
||||
"ADD_OBJECT_LABEL": "Nome:",
|
||||
"ADD_LANDMARK_LABEL": "Nome:",
|
||||
"DELETE_FACE": "Eliminar Rostro ou área",
|
||||
"DELETE_AREA_TITLE": "Eliminar área",
|
||||
"CREATE_TAG_TITLE": "Crear Etiqueta",
|
||||
"CREATE_TAG_TEXT": "A etiqueta para '{}' non existe. Desexas crear unha nova?",
|
||||
"NEW_PERSON_TAG_TITLE": "Nova Etiqueta de Persoa",
|
||||
"NEW_PERSON_TAG_TEXT": "Introduce a ruta completa da etiqueta:",
|
||||
"NEW_PET_TAG_TITLE": "Nova Etiqueta de Mascota",
|
||||
"NEW_PET_TAG_TEXT": "Introduce a ruta completa da etiqueta:",
|
||||
"NEW_BODY_TAG_TITLE": "Nova Etiqueta de Corpo",
|
||||
"NEW_BODY_TAG_TEXT": "Introduce a ruta completa da etiqueta:",
|
||||
"NEW_OBJECT_TAG_TITLE": "Nova Etiqueta de Obxecto",
|
||||
"NEW_OBJECT_TAG_TEXT": "Introduce a ruta completa da etiqueta:",
|
||||
"NEW_LANDMARK_TAG_TITLE": "Nova Etiqueta de Lugar",
|
||||
@@ -1235,10 +1283,11 @@ _UI_TEXTS = {
|
||||
"selecciona a correcta:",
|
||||
"FACE_NAME_TOOLTIP": "Escribe un nome ou selecciónao do historial.",
|
||||
"CLEAR_TEXT_TOOLTIP": "Limpar o campo de texto",
|
||||
"RENAME_FACE_TITLE": "Renomear Rostro ou área",
|
||||
"RENAME_AREA_TITLE": "Renomear área",
|
||||
"SHOW_FACES": "Amosar Rostros e outras áreas",
|
||||
"DETECT_FACES": "Detectar Rostros",
|
||||
"DETECT_PETS": "Detectar Mascotas",
|
||||
"DETECT_BODIES": "Detectar Corpos",
|
||||
"NO_FACE_LIBS": "Non se atoparon librarías de detección de rostros. Instale "
|
||||
"'mediapipe' ou 'face_recognition'.",
|
||||
"THUMBNAIL_NO_NAME": "Sen nome",
|
||||
@@ -1259,7 +1308,7 @@ _UI_TEXTS = {
|
||||
"MENU_SHOW_HISTORY": "Amosar Historial",
|
||||
"MENU_SETTINGS": "Opcións",
|
||||
"SETTINGS_GROUP_SCANNER": "Escáner",
|
||||
"SETTINGS_GROUP_FACES": "Rostros e áreas",
|
||||
"SETTINGS_GROUP_AREAS": "´áreas",
|
||||
"SETTINGS_GROUP_THUMBNAILS": "Miniaturas",
|
||||
"SETTINGS_GROUP_VIEWER": "Visor de Imaxes",
|
||||
"SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persoa:",
|
||||
@@ -1280,8 +1329,21 @@ _UI_TEXTS = {
|
||||
"recentemente para lembrar.",
|
||||
"TYPE_FACE": "Cara",
|
||||
"TYPE_PET": "Mascota",
|
||||
"TYPE_BODY": "Corpo",
|
||||
"TYPE_OBJECT": "Obxecto",
|
||||
"TYPE_LANDMARK": "Lugar",
|
||||
"SETTINGS_BODY_TAGS_LABEL": "Etiquetas de corpo:",
|
||||
"SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de corpos:",
|
||||
"SETTINGS_BODY_COLOR_LABEL": "Cor do cadro de corpo:",
|
||||
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máx historial corpos:",
|
||||
"SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para corpos, "
|
||||
"separadas por comas.",
|
||||
"SETTINGS_BODY_ENGINE_TOOLTIP": "Libraría utilizada para a detección de "
|
||||
"corpos.",
|
||||
"SETTINGS_BODY_COLOR_TOOLTIP": "Cor do cadro delimitador debuxado arredor "
|
||||
"dos corpos detectados.",
|
||||
"SETTINGS_BODY_HISTORY_TOOLTIP": "Número máximo de nomes de corpos usados "
|
||||
"recentemente para lembrar.",
|
||||
"SETTINGS_OBJECT_TAGS_LABEL": "Etiquetas de obxecto:",
|
||||
"SETTINGS_OBJECT_ENGINE_LABEL": "Motor de detección de obxectos:",
|
||||
"SETTINGS_OBJECT_COLOR_LABEL": "Cor do cadro de obxecto:",
|
||||
@@ -1322,8 +1384,8 @@ _UI_TEXTS = {
|
||||
"SETTINGS_SCAN_BATCH_SIZE_LABEL": "Tamaño do Lote de Escaneo:",
|
||||
"SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "Motor de busca de ficheiros:",
|
||||
"SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Motor a usar para buscar ficheiros. "
|
||||
"'Nativo' usa la librería de BagheeraSearch. 'baloosearch' usa o comando de "
|
||||
"KDE Baloo.",
|
||||
"'Bagheera' usa a libraría de BagheeraSearch. 'Baloo' usa o comando de "
|
||||
"'baloosearch'.",
|
||||
"SETTINGS_SCAN_FULL_ON_START_LABEL": "Escanear Todo ao Inicio:",
|
||||
"SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Profundidade máxima de directorio para "
|
||||
"escanear recursivamente.",
|
||||
@@ -1354,8 +1416,8 @@ _UI_TEXTS = {
|
||||
"ficheiro en miniaturas.",
|
||||
"SETTINGS_THUMBS_TAGS_FONT_SIZE_TOOLTIP": "Tamaño de fonte para etiquetas en "
|
||||
"miniaturas.",
|
||||
"SEARCH_ENGINE_NATIVE": "Nativo",
|
||||
"SEARCH_ENGINE_BALOO": "baloosearch",
|
||||
"SEARCH_ENGINE_NATIVE": "Bagheera",
|
||||
"SEARCH_ENGINE_BALOO": "Baloo",
|
||||
"SETTINGS_THUMBS_FILENAME_LINES_LABEL": "Liñas para nome de ficheiro:",
|
||||
"SETTINGS_THUMBS_FILENAME_LINES_TOOLTIP": "Número de liñas para o nome do "
|
||||
"ficheiro debaixo da miniatura.",
|
||||
|
||||
@@ -16,11 +16,11 @@ from PySide6.QtCore import QThread, Signal, QMutex, QWaitCondition, QObject, Qt
|
||||
from PySide6.QtGui import QImage, QImageReader, QPixmap, QTransform
|
||||
from xmpmanager import XmpManager
|
||||
from constants import (
|
||||
APP_CONFIG, AVAILABLE_FACE_ENGINES, AVAILABLE_PET_ENGINES,
|
||||
APP_CONFIG, AVAILABLE_FACE_ENGINES, AVAILABLE_PET_ENGINES, AVAILABLE_BODY_ENGINES,
|
||||
MEDIAPIPE_FACE_MODEL_PATH, MEDIAPIPE_FACE_MODEL_URL, MEDIAPIPE_OBJECT_MODEL_PATH,
|
||||
MEDIAPIPE_OBJECT_MODEL_URL, RATING_XATTR_NAME, XATTR_NAME, UITexts
|
||||
)
|
||||
from metadatamanager import XattrManager
|
||||
from metadatamanager import XattrManager, load_common_metadata
|
||||
|
||||
|
||||
class ImagePreloader(QThread):
|
||||
@@ -78,21 +78,6 @@ class ImagePreloader(QThread):
|
||||
self.mutex.unlock()
|
||||
self.wait()
|
||||
|
||||
def _load_metadata(self, path):
|
||||
"""Loads tag and rating data for a path."""
|
||||
tags = []
|
||||
raw_tags = XattrManager.get_attribute(path, XATTR_NAME)
|
||||
if raw_tags:
|
||||
tags = sorted(list(set(t.strip()
|
||||
for t in raw_tags.split(',') if t.strip())))
|
||||
|
||||
raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0")
|
||||
try:
|
||||
rating = int(raw_rating)
|
||||
except ValueError:
|
||||
rating = 0
|
||||
return tags, rating
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
The main execution loop for the thread.
|
||||
@@ -124,7 +109,7 @@ class ImagePreloader(QThread):
|
||||
img = reader.read()
|
||||
if not img.isNull():
|
||||
# Load tags and rating here to avoid re-reading in main thread
|
||||
tags, rating = self._load_metadata(path)
|
||||
tags, rating = load_common_metadata(path)
|
||||
self.image_ready.emit(idx, path, img, tags, rating)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -157,6 +142,8 @@ class ImageController(QObject):
|
||||
self.faces = []
|
||||
self._current_tags = initial_tags if initial_tags is not None else []
|
||||
self._current_rating = initial_rating
|
||||
self._current_metadata_path = None
|
||||
self._loaded_path = None
|
||||
self.show_faces = False
|
||||
|
||||
# Preloading
|
||||
@@ -219,11 +206,27 @@ class ImageController(QObject):
|
||||
Loads the current image into the controller's main pixmap.
|
||||
"""
|
||||
path = self.get_current_path()
|
||||
self.pixmap_original = QPixmap()
|
||||
|
||||
# Optimization: Check if image is already loaded
|
||||
if path and self._loaded_path == path and not self.pixmap_original.isNull():
|
||||
self.rotation = 0
|
||||
self.flip_h = False
|
||||
self.flip_v = False
|
||||
self.faces = []
|
||||
|
||||
# Ensure metadata is consistent with current path
|
||||
if self._current_metadata_path != path:
|
||||
self._current_tags, self._current_rating = load_common_metadata(path)
|
||||
self._current_metadata_path = path
|
||||
|
||||
self.load_faces()
|
||||
self._trigger_preload()
|
||||
return True
|
||||
|
||||
self.pixmap_original = QPixmap()
|
||||
self._loaded_path = None
|
||||
self.rotation = 0
|
||||
self.flip_h = False
|
||||
self._current_tags = []
|
||||
self._current_rating = 0
|
||||
self.flip_v = False
|
||||
self.faces = []
|
||||
|
||||
@@ -236,6 +239,7 @@ class ImageController(QObject):
|
||||
# Clear cache to free memory as we have consumed the image
|
||||
self._current_tags = self._cached_next_tags
|
||||
self._current_rating = self._cached_next_rating
|
||||
self._current_metadata_path = path
|
||||
self._cached_next_image = None
|
||||
self._cached_next_index = -1
|
||||
self._cached_next_tags = None
|
||||
@@ -249,9 +253,12 @@ class ImageController(QObject):
|
||||
return False
|
||||
self.pixmap_original = QPixmap.fromImage(image)
|
||||
|
||||
# Load tags and rating if not from cache
|
||||
self._current_tags, self._current_rating = self._load_metadata(path)
|
||||
# Load tags and rating if not already set for this path
|
||||
if self._current_metadata_path != path:
|
||||
self._current_tags, self._current_rating = load_common_metadata(path)
|
||||
self._current_metadata_path = path
|
||||
|
||||
self._loaded_path = path
|
||||
self.load_faces()
|
||||
self._trigger_preload()
|
||||
return True
|
||||
@@ -422,6 +429,38 @@ class ImageController(QObject):
|
||||
face_data['h'] = h
|
||||
return face_data
|
||||
|
||||
def _create_region_from_pixels(self, x, y, w, h, img_w, img_h, region_type):
|
||||
"""
|
||||
Creates a normalized region dictionary from pixel coordinates.
|
||||
|
||||
Args:
|
||||
x (float): Top-left x coordinate in pixels.
|
||||
y (float): Top-left y coordinate in pixels.
|
||||
w (float): Width in pixels.
|
||||
h (float): Height in pixels.
|
||||
img_w (int): Image width in pixels.
|
||||
img_h (int): Image height in pixels.
|
||||
region_type (str): The type of region (Face, Pet, Body).
|
||||
|
||||
Returns:
|
||||
dict: Validated normalized region or None.
|
||||
"""
|
||||
if img_w <= 0 or img_h <= 0:
|
||||
return None
|
||||
|
||||
if w <= 0 or h <= 0:
|
||||
return None
|
||||
|
||||
new_region = {
|
||||
'name': '',
|
||||
'x': (x + w / 2) / img_w,
|
||||
'y': (y + h / 2) / img_h,
|
||||
'w': w / img_w,
|
||||
'h': h / img_h,
|
||||
'type': region_type
|
||||
}
|
||||
return self._clamp_and_validate_face(new_region)
|
||||
|
||||
def _detect_faces_face_recognition(self, path):
|
||||
"""Detects faces using the 'face_recognition' library."""
|
||||
import face_recognition
|
||||
@@ -433,12 +472,9 @@ class ImageController(QObject):
|
||||
for (top, right, bottom, left) in face_locations:
|
||||
box_w = right - left
|
||||
box_h = bottom - top
|
||||
new_face = {
|
||||
'name': '',
|
||||
'x': (left + box_w / 2) / w, 'y': (top + box_h / 2) / h,
|
||||
'w': box_w / w, 'h': box_h / h, 'type': 'Face'
|
||||
}
|
||||
validated_face = self._clamp_and_validate_face(new_face)
|
||||
validated_face = self._create_region_from_pixels(
|
||||
left, top, box_w, box_h, w, h, 'Face'
|
||||
)
|
||||
if validated_face:
|
||||
new_faces.append(validated_face)
|
||||
except Exception as e:
|
||||
@@ -484,15 +520,10 @@ class ImageController(QObject):
|
||||
img_h, img_w = mp_image.height, mp_image.width
|
||||
for detection in detection_result.detections:
|
||||
bbox = detection.bounding_box # This is in pixels
|
||||
new_face = {
|
||||
'name': '',
|
||||
'x': (bbox.origin_x + bbox.width / 2) / img_w,
|
||||
'y': (bbox.origin_y + bbox.height / 2) / img_h,
|
||||
'w': bbox.width / img_w,
|
||||
'h': bbox.height / img_h,
|
||||
'type': 'Face'
|
||||
}
|
||||
validated_face = self._clamp_and_validate_face(new_face)
|
||||
validated_face = self._create_region_from_pixels(
|
||||
bbox.origin_x, bbox.origin_y, bbox.width, bbox.height,
|
||||
img_w, img_h, 'Face'
|
||||
)
|
||||
if validated_face:
|
||||
new_faces.append(validated_face)
|
||||
|
||||
@@ -500,19 +531,27 @@ class ImageController(QObject):
|
||||
print(f"Error during MediaPipe detection: {e}")
|
||||
return new_faces
|
||||
|
||||
def _detect_pets_mediapipe(self, path):
|
||||
"""Detects pets using the 'mediapipe' library object detection."""
|
||||
def _detect_objects_mediapipe(self, path, allowlist, max_results, region_type):
|
||||
"""
|
||||
Generic method to detect objects using MediaPipe ObjectDetector.
|
||||
|
||||
Args:
|
||||
path (str): Path to image file.
|
||||
allowlist (list): List of category names to detect.
|
||||
max_results (int): Maximum number of results to return.
|
||||
region_type (str): The 'type' label for the detected regions.
|
||||
"""
|
||||
import mediapipe as mp
|
||||
from mediapipe.tasks import python
|
||||
from mediapipe.tasks.python import vision
|
||||
|
||||
new_pets = []
|
||||
new_regions = []
|
||||
|
||||
if not os.path.exists(MEDIAPIPE_OBJECT_MODEL_PATH):
|
||||
print(f"MediaPipe model not found at: {MEDIAPIPE_OBJECT_MODEL_PATH}")
|
||||
print("Please download 'efficientdet_lite0.tflite' and place it there.")
|
||||
print(f"URL: {MEDIAPIPE_OBJECT_MODEL_URL}")
|
||||
return new_pets
|
||||
return new_regions
|
||||
|
||||
try:
|
||||
base_options = python.BaseOptions(
|
||||
@@ -520,8 +559,8 @@ class ImageController(QObject):
|
||||
options = vision.ObjectDetectorOptions(
|
||||
base_options=base_options,
|
||||
score_threshold=0.5,
|
||||
max_results=5,
|
||||
category_allowlist=["cat", "dog"]) # Detect cats and dogs
|
||||
max_results=max_results,
|
||||
category_allowlist=allowlist)
|
||||
|
||||
# Silence MediaPipe warnings (stderr) during initialization
|
||||
stderr_fd = 2
|
||||
@@ -542,21 +581,24 @@ class ImageController(QObject):
|
||||
img_h, img_w = mp_image.height, mp_image.width
|
||||
for detection in detection_result.detections:
|
||||
bbox = detection.bounding_box
|
||||
new_pet = {
|
||||
'name': '',
|
||||
'x': (bbox.origin_x + bbox.width / 2) / img_w,
|
||||
'y': (bbox.origin_y + bbox.height / 2) / img_h,
|
||||
'w': bbox.width / img_w,
|
||||
'h': bbox.height / img_h,
|
||||
'type': 'Pet'
|
||||
}
|
||||
validated_pet = self._clamp_and_validate_face(new_pet)
|
||||
if validated_pet:
|
||||
new_pets.append(validated_pet)
|
||||
validated_region = self._create_region_from_pixels(
|
||||
bbox.origin_x, bbox.origin_y, bbox.width, bbox.height,
|
||||
img_w, img_h, region_type
|
||||
)
|
||||
if validated_region:
|
||||
new_regions.append(validated_region)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during MediaPipe pet detection: {e}")
|
||||
return new_pets
|
||||
print(f"Error during MediaPipe {region_type} detection: {e}")
|
||||
return new_regions
|
||||
|
||||
def _detect_pets_mediapipe(self, path):
|
||||
"""Detects pets using the 'mediapipe' library object detection."""
|
||||
return self._detect_objects_mediapipe(path, ["cat", "dog"], 5, "Pet")
|
||||
|
||||
def _detect_bodies_mediapipe(self, path):
|
||||
"""Detects bodies using the 'mediapipe' library object detection."""
|
||||
return self._detect_objects_mediapipe(path, ["person"], 10, "Body")
|
||||
|
||||
def detect_faces(self):
|
||||
"""
|
||||
@@ -615,6 +657,21 @@ class ImageController(QObject):
|
||||
|
||||
return []
|
||||
|
||||
def detect_bodies(self):
|
||||
"""
|
||||
Detects bodies using a configured or available detection engine.
|
||||
"""
|
||||
path = self.get_current_path()
|
||||
if not path:
|
||||
return []
|
||||
|
||||
engine = APP_CONFIG.get("body_detection_engine", "mediapipe")
|
||||
|
||||
if engine == "mediapipe" and "mediapipe" in AVAILABLE_BODY_ENGINES:
|
||||
return self._detect_bodies_mediapipe(path)
|
||||
|
||||
return []
|
||||
|
||||
def get_display_pixmap(self):
|
||||
"""
|
||||
Applies current transformations (rotation, zoom, flip) to the original
|
||||
@@ -709,30 +766,27 @@ class ImageController(QObject):
|
||||
elif self.index < 0:
|
||||
self.index = 0
|
||||
|
||||
# Update current image metadata if provided
|
||||
self._current_tags = current_image_tags \
|
||||
if current_image_tags is not None else []
|
||||
# Update current image metadata
|
||||
if current_image_tags is not None:
|
||||
self._current_tags = current_image_tags
|
||||
self._current_rating = current_image_rating
|
||||
self._current_metadata_path = self.get_current_path()
|
||||
else:
|
||||
# Reload from disk if not provided to ensure consistency
|
||||
path = self.get_current_path()
|
||||
if path:
|
||||
self._current_tags, self._current_rating = load_common_metadata(path)
|
||||
self._current_metadata_path = path
|
||||
else:
|
||||
self._current_tags = []
|
||||
self._current_rating = 0
|
||||
self._current_metadata_path = None
|
||||
|
||||
self._cached_next_image = None
|
||||
self._cached_next_index = -1
|
||||
self._trigger_preload()
|
||||
self.list_updated.emit(self.index)
|
||||
|
||||
def _load_metadata(self, path):
|
||||
"""Loads tag and rating data for a path."""
|
||||
tags = []
|
||||
raw_tags = XattrManager.get_attribute(path, XATTR_NAME)
|
||||
if raw_tags:
|
||||
tags = sorted(list(set(t.strip()
|
||||
for t in raw_tags.split(',') if t.strip())))
|
||||
|
||||
raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0")
|
||||
try:
|
||||
rating = int(raw_rating)
|
||||
except ValueError:
|
||||
rating = 0
|
||||
return tags, rating
|
||||
|
||||
def update_list_on_exists(self, new_list, new_index=None):
|
||||
"""
|
||||
Updates the list only if the old list is a subset of the new one.
|
||||
@@ -749,8 +803,17 @@ class ImageController(QObject):
|
||||
self.index = new_index
|
||||
if self.index >= len(self.image_list):
|
||||
self.index = max(0, len(self.image_list) - 1)
|
||||
self._current_tags = [] # Clear current tags/rating, will be reloaded
|
||||
|
||||
# Reload metadata for the current image to avoid stale/empty state
|
||||
path = self.get_current_path()
|
||||
if path:
|
||||
self._current_tags, self._current_rating = load_common_metadata(path)
|
||||
self._current_metadata_path = path
|
||||
else:
|
||||
self._current_tags = []
|
||||
self._current_rating = 0
|
||||
self._current_metadata_path = None
|
||||
|
||||
self._cached_next_image = None
|
||||
self._cached_next_index = -1
|
||||
self._trigger_preload()
|
||||
|
||||
@@ -1404,8 +1404,8 @@ class ImageScanner(QThread):
|
||||
return None, []
|
||||
|
||||
def _search(self, query):
|
||||
engine = APP_CONFIG.get("search_engine", "Native")
|
||||
if HAVE_BAGHEERASEARCH_LIB and (engine == "Native" or not SEARCH_CMD):
|
||||
engine = APP_CONFIG.get("search_engine", "Bagheera")
|
||||
if HAVE_BAGHEERASEARCH_LIB and (engine == "Bagheera" or not SEARCH_CMD):
|
||||
query_text, main_options, other_options = self._parse_query(query)
|
||||
try:
|
||||
searcher = BagheeraSearcher()
|
||||
|
||||
100
imageviewer.py
100
imageviewer.py
@@ -29,6 +29,7 @@ from PySide6.QtCore import (
|
||||
|
||||
from constants import (
|
||||
APP_CONFIG, DEFAULT_FACE_BOX_COLOR, DEFAULT_PET_BOX_COLOR, DEFAULT_VIEWER_SHORTCUTS,
|
||||
DEFAULT_BODY_BOX_COLOR,
|
||||
DEFAULT_OBJECT_BOX_COLOR, DEFAULT_LANDMARK_BOX_COLOR, FORCE_X11, ICON_THEME_VIEWER,
|
||||
ICON_THEME_VIEWER_FALLBACK, KSCREEN_DOCTOR_MARGIN, KWINOUTPUTCONFIG_PATH,
|
||||
VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_FORM_MARGIN, VIEWER_LABEL,
|
||||
@@ -47,6 +48,9 @@ class FaceNameDialog(QDialog):
|
||||
if region_type == "Pet":
|
||||
self.setWindowTitle(UITexts.ADD_PET_TITLE)
|
||||
layout_label = UITexts.ADD_PET_LABEL
|
||||
elif region_type == "Body":
|
||||
self.setWindowTitle(UITexts.ADD_BODY_TITLE)
|
||||
layout_label = UITexts.ADD_BODY_LABEL
|
||||
elif region_type == "Object":
|
||||
self.setWindowTitle(UITexts.ADD_OBJECT_TITLE)
|
||||
layout_label = UITexts.ADD_OBJECT_LABEL
|
||||
@@ -441,6 +445,8 @@ class FaceCanvas(QLabel):
|
||||
face_color = QColor(face_color_str)
|
||||
pet_color_str = APP_CONFIG.get("pet_box_color", DEFAULT_PET_BOX_COLOR)
|
||||
pet_color = QColor(pet_color_str)
|
||||
body_color_str = APP_CONFIG.get("body_box_color", DEFAULT_BODY_BOX_COLOR)
|
||||
body_color = QColor(body_color_str)
|
||||
object_color_str = APP_CONFIG.get("object_box_color", DEFAULT_OBJECT_BOX_COLOR)
|
||||
object_color = QColor(object_color_str)
|
||||
landmark_color_str = APP_CONFIG.get("landmark_box_color",
|
||||
@@ -452,11 +458,14 @@ class FaceCanvas(QLabel):
|
||||
rect = self.map_from_source(face)
|
||||
|
||||
is_pet = face.get('type') == 'Pet'
|
||||
is_body = face.get('type') == 'Body'
|
||||
is_object = face.get('type') == 'Object'
|
||||
is_landmark = face.get('type') == 'Landmark'
|
||||
|
||||
if is_pet:
|
||||
color = pet_color
|
||||
elif is_body:
|
||||
color = body_color
|
||||
elif is_object:
|
||||
color = object_color
|
||||
elif is_landmark:
|
||||
@@ -677,8 +686,10 @@ class FaceCanvas(QLabel):
|
||||
elif event.button() == Qt.LeftButton:
|
||||
self.dragging = True
|
||||
self.drag_start_pos = event.globalPosition().toPoint()
|
||||
self.drag_start_scroll_x = self.viewer.scroll_area.horizontalScrollBar().value()
|
||||
self.drag_start_scroll_y = self.viewer.scroll_area.verticalScrollBar().value()
|
||||
self.drag_start_scroll_x = \
|
||||
self.viewer.scroll_area.horizontalScrollBar().value()
|
||||
self.drag_start_scroll_y = \
|
||||
self.viewer.scroll_area.verticalScrollBar().value()
|
||||
self.setCursor(Qt.ClosedHandCursor)
|
||||
event.accept()
|
||||
else:
|
||||
@@ -863,12 +874,15 @@ class FaceCanvas(QLabel):
|
||||
menu = QMenu(self)
|
||||
action_face = menu.addAction(UITexts.TYPE_FACE)
|
||||
action_pet = menu.addAction(UITexts.TYPE_PET)
|
||||
action_body = menu.addAction(UITexts.TYPE_BODY)
|
||||
action_object = menu.addAction(UITexts.TYPE_OBJECT)
|
||||
action_landmark = menu.addAction(UITexts.TYPE_LANDMARK)
|
||||
# Show menu at mouse release position
|
||||
res = menu.exec(event.globalPosition().toPoint())
|
||||
if res == action_pet:
|
||||
region_type = "Pet"
|
||||
elif res == action_body:
|
||||
region_type = "Body"
|
||||
elif res == action_object:
|
||||
region_type = "Object"
|
||||
elif res == action_landmark:
|
||||
@@ -885,6 +899,8 @@ class FaceCanvas(QLabel):
|
||||
if self.viewer.main_win:
|
||||
if region_type == "Pet":
|
||||
history_list = self.viewer.main_win.pet_names_history
|
||||
elif region_type == "Body":
|
||||
history_list = self.viewer.main_win.body_names_history
|
||||
elif region_type == "Object":
|
||||
history_list = self.viewer.main_win.object_names_history
|
||||
elif region_type == "Landmark":
|
||||
@@ -903,6 +919,8 @@ class FaceCanvas(QLabel):
|
||||
if self.viewer.main_win:
|
||||
if region_type == "Pet":
|
||||
self.viewer.main_win.pet_names_history = updated_history
|
||||
elif region_type == "Body":
|
||||
self.viewer.main_win.body_names_history = updated_history
|
||||
elif region_type == "Object":
|
||||
self.viewer.main_win.object_names_history = updated_history
|
||||
elif region_type == "Landmark":
|
||||
@@ -1192,6 +1210,7 @@ class ImageViewer(QWidget):
|
||||
"flip_vertical": self.toggle_flip_vertical,
|
||||
"detect_faces": self.run_face_detection,
|
||||
"detect_pets": self.run_pet_detection,
|
||||
"detect_bodies": self.run_body_detection,
|
||||
"fast_tag": self.show_fast_tag_menu,
|
||||
"rotate_right": lambda: self.apply_rotation(90, True),
|
||||
"rotate_left": lambda: self.apply_rotation(-90, True),
|
||||
@@ -1217,7 +1236,7 @@ class ImageViewer(QWidget):
|
||||
Optimized to update the existing list if possible, rather than
|
||||
rebuilding it entirely.
|
||||
"""
|
||||
if not self.filmstrip.isVisible():
|
||||
if self.filmstrip.isHidden():
|
||||
return
|
||||
|
||||
# --- OPTIMIZATION ---
|
||||
@@ -1887,9 +1906,14 @@ class ImageViewer(QWidget):
|
||||
zoom = int(self.controller.zoom_factor * 100)
|
||||
self.sb_info_label.setText(f"{w} x {h} px | {zoom}%")
|
||||
|
||||
# Use tags from controller's internal state
|
||||
# Use tags from metadata if provided (priority to avoid race conditions),
|
||||
# otherwise fallback to controller's internal state.
|
||||
tags_source = self.controller._current_tags
|
||||
if metadata and 'tags' in metadata:
|
||||
tags_source = metadata['tags']
|
||||
|
||||
display_tags = [t.strip().split('/')[-1]
|
||||
for t in self.controller._current_tags if t.strip()]
|
||||
for t in tags_source if t.strip()]
|
||||
self.sb_tags_label.setText(", ".join(display_tags))
|
||||
|
||||
@Slot(str, dict)
|
||||
@@ -2080,8 +2104,8 @@ class ImageViewer(QWidget):
|
||||
return False
|
||||
|
||||
menu = QMenu(self)
|
||||
action_del = menu.addAction(UITexts.DELETE_FACE)
|
||||
action_ren = menu.addAction(UITexts.RENAME_FACE_TITLE)
|
||||
action_del = menu.addAction(UITexts.DELETE_AREA_TITLE)
|
||||
action_ren = menu.addAction(UITexts.RENAME_AREA_TITLE)
|
||||
res = menu.exec(event.globalPos())
|
||||
|
||||
if res == action_del:
|
||||
@@ -2107,6 +2131,8 @@ class ImageViewer(QWidget):
|
||||
if self.main_win:
|
||||
if region_type == "Pet":
|
||||
history_list = self.main_win.pet_names_history
|
||||
elif region_type == "Body":
|
||||
history_list = self.main_win.body_names_history
|
||||
elif region_type == "Object":
|
||||
history_list = self.main_win.object_names_history
|
||||
elif region_type == "Landmark":
|
||||
@@ -2135,6 +2161,8 @@ class ImageViewer(QWidget):
|
||||
if self.main_win:
|
||||
if region_type == "Pet":
|
||||
self.main_win.pet_names_history = updated_history
|
||||
elif region_type == "Body":
|
||||
self.main_win.body_names_history = updated_history
|
||||
elif region_type == "Object":
|
||||
self.main_win.object_names_history = updated_history
|
||||
elif region_type == "Landmark":
|
||||
@@ -2185,6 +2213,9 @@ class ImageViewer(QWidget):
|
||||
{"text": UITexts.DETECT_PETS, "action": "detect_pets",
|
||||
"icon": "edit-image-face-recognize"},
|
||||
"separator",
|
||||
{"text": UITexts.DETECT_BODIES, "action": "detect_bodies",
|
||||
"icon": "edit-image-face-recognize"},
|
||||
"separator",
|
||||
{"text": UITexts.VIEWER_MENU_ROTATE, "icon": "transform-rotate",
|
||||
"submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_ROTATE_LEFT,
|
||||
@@ -2491,6 +2522,61 @@ class ImageViewer(QWidget):
|
||||
if added_count > 0:
|
||||
self.controller.save_faces()
|
||||
|
||||
def run_body_detection(self):
|
||||
"""Runs body detection on the current image."""
|
||||
QApplication.setOverrideCursor(Qt.WaitCursor)
|
||||
try:
|
||||
new_bodies = self.controller.detect_bodies()
|
||||
finally:
|
||||
QApplication.restoreOverrideCursor()
|
||||
|
||||
if not new_bodies:
|
||||
return
|
||||
|
||||
IOU_THRESHOLD = 0.7
|
||||
added_count = 0
|
||||
for new_body in new_bodies:
|
||||
is_duplicate = False
|
||||
for existing_face in self.controller.faces:
|
||||
iou = self._calculate_iou(new_body, existing_face)
|
||||
if iou > IOU_THRESHOLD:
|
||||
is_duplicate = True
|
||||
break
|
||||
|
||||
if is_duplicate:
|
||||
continue
|
||||
|
||||
if not self.controller.show_faces:
|
||||
self.toggle_faces()
|
||||
|
||||
self.controller.faces.append(new_body)
|
||||
self.canvas.update()
|
||||
|
||||
w = self.canvas.width()
|
||||
h = self.canvas.height()
|
||||
self.scroll_area.ensureVisible(int(new_body.get('x', 0) * w),
|
||||
int(new_body.get('y', 0) * h), 50, 50)
|
||||
QApplication.processEvents()
|
||||
|
||||
# For bodies, we typically don't ask for a name immediately unless desired
|
||||
# Or we can treat it like pets/faces and ask. Let's ask.
|
||||
history = self.main_win.body_names_history if self.main_win else []
|
||||
full_tag, updated_history, ok = FaceNameDialog.get_name(
|
||||
self, history, main_win=self.main_win, region_type="Body")
|
||||
|
||||
if ok and full_tag:
|
||||
new_body['name'] = full_tag
|
||||
self.controller.toggle_tag(full_tag, True)
|
||||
if self.main_win:
|
||||
self.main_win.body_names_history = updated_history
|
||||
added_count += 1
|
||||
else:
|
||||
self.controller.faces.pop()
|
||||
self.canvas.update()
|
||||
|
||||
if added_count > 0:
|
||||
self.controller.save_faces()
|
||||
|
||||
def toggle_filmstrip(self):
|
||||
"""Shows or hides the filmstrip widget."""
|
||||
visible = not self.filmstrip.isVisible()
|
||||
|
||||
@@ -16,6 +16,7 @@ except ImportError:
|
||||
exiv2 = None
|
||||
HAVE_EXIV2 = False
|
||||
from utils import preserve_mtime
|
||||
from constants import RATING_XATTR_NAME, XATTR_NAME
|
||||
|
||||
|
||||
def notify_baloo(path):
|
||||
@@ -40,6 +41,24 @@ def notify_baloo(path):
|
||||
QDBusConnection.sessionBus().call(msg, QDBus.NoBlock)
|
||||
|
||||
|
||||
def load_common_metadata(path):
|
||||
"""
|
||||
Loads tag and rating data for a path using extended attributes.
|
||||
"""
|
||||
tags = []
|
||||
raw_tags = XattrManager.get_attribute(path, XATTR_NAME)
|
||||
if raw_tags:
|
||||
tags = sorted(list(set(t.strip()
|
||||
for t in raw_tags.split(',') if t.strip())))
|
||||
|
||||
raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0")
|
||||
try:
|
||||
rating = int(raw_rating)
|
||||
except ValueError:
|
||||
rating = 0
|
||||
return tags, rating
|
||||
|
||||
|
||||
class MetadataManager:
|
||||
"""Manages reading EXIF, IPTC, and XMP metadata."""
|
||||
|
||||
|
||||
85
settings.py
85
settings.py
@@ -25,7 +25,8 @@ from constants import (
|
||||
APP_CONFIG, AVAILABLE_FACE_ENGINES, DEFAULT_FACE_BOX_COLOR,
|
||||
DEFAULT_PET_BOX_COLOR, DEFAULT_OBJECT_BOX_COLOR, DEFAULT_LANDMARK_BOX_COLOR,
|
||||
FACES_MENU_MAX_ITEMS_DEFAULT, MEDIAPIPE_FACE_MODEL_PATH, MEDIAPIPE_FACE_MODEL_URL,
|
||||
AVAILABLE_PET_ENGINES, MEDIAPIPE_OBJECT_MODEL_PATH, MEDIAPIPE_OBJECT_MODEL_URL,
|
||||
AVAILABLE_PET_ENGINES, DEFAULT_BODY_BOX_COLOR,
|
||||
MEDIAPIPE_OBJECT_MODEL_PATH, MEDIAPIPE_OBJECT_MODEL_URL,
|
||||
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, TAGS_MENU_MAX_ITEMS_DEFAULT,
|
||||
THUMBNAILS_FILENAME_LINES_DEFAULT,
|
||||
THUMBNAILS_REFRESH_INTERVAL_DEFAULT, THUMBNAILS_BG_COLOR_DEFAULT,
|
||||
@@ -81,6 +82,7 @@ class SettingsDialog(QDialog):
|
||||
|
||||
self.current_face_color = DEFAULT_FACE_BOX_COLOR
|
||||
self.current_pet_color = DEFAULT_PET_BOX_COLOR
|
||||
self.current_body_color = DEFAULT_BODY_BOX_COLOR
|
||||
self.current_object_color = DEFAULT_OBJECT_BOX_COLOR
|
||||
self.current_landmark_color = DEFAULT_LANDMARK_BOX_COLOR
|
||||
self.current_thumbs_bg_color = THUMBNAILS_BG_COLOR_DEFAULT
|
||||
@@ -293,9 +295,9 @@ class SettingsDialog(QDialog):
|
||||
search_engine_layout = QHBoxLayout()
|
||||
search_engine_label = QLabel(UITexts.SETTINGS_SCANNER_SEARCH_ENGINE_LABEL)
|
||||
self.search_engine_combo = QComboBox()
|
||||
self.search_engine_combo.addItem(UITexts.SEARCH_ENGINE_NATIVE, "Native")
|
||||
self.search_engine_combo.addItem(UITexts.SEARCH_ENGINE_NATIVE, "Bagheera")
|
||||
if SEARCH_CMD:
|
||||
self.search_engine_combo.addItem(UITexts.SEARCH_ENGINE_BALOO, "baloosearch")
|
||||
self.search_engine_combo.addItem(UITexts.SEARCH_ENGINE_BALOO, "Baloo")
|
||||
|
||||
search_engine_layout.addWidget(search_engine_label)
|
||||
search_engine_layout.addWidget(self.search_engine_combo)
|
||||
@@ -462,6 +464,53 @@ class SettingsDialog(QDialog):
|
||||
self.pet_history_spin.setToolTip(UITexts.SETTINGS_PET_HISTORY_TOOLTIP)
|
||||
faces_layout.addLayout(pet_history_layout)
|
||||
|
||||
# --- Body Section ---
|
||||
faces_layout.addSpacing(10)
|
||||
body_header = QLabel("Body")
|
||||
body_header.setFont(QFont("Sans", 10, QFont.Bold))
|
||||
faces_layout.addWidget(body_header)
|
||||
|
||||
body_tags_layout = QHBoxLayout()
|
||||
body_tags_label = QLabel(UITexts.SETTINGS_BODY_TAGS_LABEL)
|
||||
self.body_tags_edit = QLineEdit()
|
||||
self.body_tags_edit.setPlaceholderText("tag1, tag2, tag3/subtag")
|
||||
self.body_tags_edit.setClearButtonEnabled(True)
|
||||
body_tags_layout.addWidget(body_tags_label)
|
||||
body_tags_layout.addWidget(self.body_tags_edit)
|
||||
body_tags_label.setToolTip(UITexts.SETTINGS_BODY_TAGS_TOOLTIP)
|
||||
self.body_tags_edit.setToolTip(UITexts.SETTINGS_BODY_TAGS_TOOLTIP)
|
||||
faces_layout.addLayout(body_tags_layout)
|
||||
|
||||
# body_engine_layout = QHBoxLayout()
|
||||
# body_engine_label = QLabel(UITexts.SETTINGS_BODY_ENGINE_LABEL)
|
||||
# self.body_engine_combo = QComboBox()
|
||||
# self.body_engine_combo.addItems(AVAILABLE_BODY_ENGINES)
|
||||
# body_engine_layout.addWidget(body_engine_label)
|
||||
# body_engine_layout.addWidget(self.body_engine_combo, 1)
|
||||
# body_engine_label.setToolTip(UITexts.SETTINGS_BODY_ENGINE_TOOLTIP)
|
||||
# self.body_engine_combo.setToolTip(UITexts.SETTINGS_BODY_ENGINE_TOOLTIP)
|
||||
# faces_layout.addLayout(body_engine_layout)
|
||||
|
||||
body_color_layout = QHBoxLayout()
|
||||
body_color_label = QLabel(UITexts.SETTINGS_BODY_COLOR_LABEL)
|
||||
self.body_color_btn = QPushButton()
|
||||
self.body_color_btn.clicked.connect(self.choose_body_color)
|
||||
body_color_layout.addWidget(body_color_label)
|
||||
body_color_layout.addWidget(self.body_color_btn)
|
||||
body_color_label.setToolTip(UITexts.SETTINGS_BODY_COLOR_TOOLTIP)
|
||||
self.body_color_btn.setToolTip(UITexts.SETTINGS_BODY_COLOR_TOOLTIP)
|
||||
faces_layout.addLayout(body_color_layout)
|
||||
|
||||
body_history_layout = QHBoxLayout()
|
||||
self.body_history_spin = QSpinBox()
|
||||
self.body_history_spin.setRange(5, 100)
|
||||
body_hist_label = QLabel(UITexts.SETTINGS_BODY_HISTORY_COUNT_LABEL)
|
||||
body_history_layout.addWidget(body_hist_label)
|
||||
body_history_layout.addWidget(self.body_history_spin)
|
||||
body_hist_label.setToolTip(UITexts.SETTINGS_BODY_HISTORY_TOOLTIP)
|
||||
self.body_history_spin.setToolTip(UITexts.SETTINGS_BODY_HISTORY_TOOLTIP)
|
||||
faces_layout.addLayout(body_history_layout)
|
||||
|
||||
# --- Object Section ---
|
||||
faces_layout.addSpacing(10)
|
||||
object_header = QLabel("Object")
|
||||
@@ -593,7 +642,7 @@ class SettingsDialog(QDialog):
|
||||
# Add tabs in the new order
|
||||
tabs.addTab(thumbs_tab, UITexts.SETTINGS_GROUP_THUMBNAILS)
|
||||
tabs.addTab(viewer_tab, UITexts.SETTINGS_GROUP_VIEWER)
|
||||
tabs.addTab(faces_tab, UITexts.SETTINGS_GROUP_FACES)
|
||||
tabs.addTab(faces_tab, UITexts.SETTINGS_GROUP_AREAS)
|
||||
tabs.addTab(scanner_tab, UITexts.SETTINGS_GROUP_SCANNER)
|
||||
|
||||
# --- Button Box ---
|
||||
@@ -625,16 +674,19 @@ class SettingsDialog(QDialog):
|
||||
person_tags = APP_CONFIG.get(
|
||||
"person_tags", SCANNER_SETTINGS_DEFAULTS["person_tags"])
|
||||
pet_tags = APP_CONFIG.get("pet_tags", "")
|
||||
body_tags = APP_CONFIG.get("body_tags", "")
|
||||
object_tags = APP_CONFIG.get("object_tags", "")
|
||||
landmark_tags = APP_CONFIG.get("landmark_tags", "")
|
||||
|
||||
face_detection_engine = APP_CONFIG.get("face_detection_engine")
|
||||
pet_detection_engine = APP_CONFIG.get("pet_detection_engine")
|
||||
body_detection_engine = APP_CONFIG.get("body_detection_engine")
|
||||
object_detection_engine = APP_CONFIG.get("object_detection_engine")
|
||||
landmark_detection_engine = APP_CONFIG.get("landmark_detection_engine")
|
||||
|
||||
face_color = APP_CONFIG.get("face_box_color", DEFAULT_FACE_BOX_COLOR)
|
||||
pet_color = APP_CONFIG.get("pet_box_color", DEFAULT_PET_BOX_COLOR)
|
||||
body_color = APP_CONFIG.get("body_box_color", DEFAULT_BODY_BOX_COLOR)
|
||||
object_color = APP_CONFIG.get("object_box_color", DEFAULT_OBJECT_BOX_COLOR)
|
||||
landmark_color = APP_CONFIG.get("landmark_box_color",
|
||||
DEFAULT_LANDMARK_BOX_COLOR)
|
||||
@@ -645,6 +697,8 @@ class SettingsDialog(QDialog):
|
||||
"faces_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT)
|
||||
pet_history_count = APP_CONFIG.get(
|
||||
"pets_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT)
|
||||
body_history_count = APP_CONFIG.get(
|
||||
"body_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT)
|
||||
object_history_count = APP_CONFIG.get(
|
||||
"object_menu_max_items", FACES_MENU_MAX_ITEMS_DEFAULT)
|
||||
landmark_history_count = APP_CONFIG.get(
|
||||
@@ -695,11 +749,13 @@ class SettingsDialog(QDialog):
|
||||
|
||||
self.person_tags_edit.setText(person_tags)
|
||||
self.pet_tags_edit.setText(pet_tags)
|
||||
self.body_tags_edit.setText(body_tags)
|
||||
self.object_tags_edit.setText(object_tags)
|
||||
self.landmark_tags_edit.setText(landmark_tags)
|
||||
|
||||
self.set_button_color(face_color)
|
||||
self.set_pet_button_color(pet_color)
|
||||
self.set_body_button_color(body_color)
|
||||
self.set_object_button_color(object_color)
|
||||
self.set_landmark_button_color(landmark_color)
|
||||
|
||||
@@ -709,6 +765,8 @@ class SettingsDialog(QDialog):
|
||||
if self.pet_engine_combo and pet_detection_engine in AVAILABLE_PET_ENGINES:
|
||||
self.pet_engine_combo.setCurrentText(pet_detection_engine)
|
||||
|
||||
if body_detection_engine and hasattr(self, "body_detection_engine_combo"):
|
||||
self.body_engine_combo.setCurrentText(body_detection_engine)
|
||||
if object_detection_engine and hasattr(self, "object_engine_combo"):
|
||||
self.object_engine_combo.setCurrentText(object_detection_engine)
|
||||
if landmark_detection_engine and hasattr(self, "landmark_engine_combo"):
|
||||
@@ -717,6 +775,7 @@ class SettingsDialog(QDialog):
|
||||
self.mru_tags_spin.setValue(mru_tags_count)
|
||||
self.face_history_spin.setValue(face_history_count)
|
||||
self.pet_history_spin.setValue(pet_history_count)
|
||||
self.body_history_spin.setValue(body_history_count)
|
||||
self.object_history_spin.setValue(object_history_count)
|
||||
self.landmark_history_spin.setValue(landmark_history_count)
|
||||
|
||||
@@ -771,6 +830,18 @@ class SettingsDialog(QDialog):
|
||||
if color.isValid():
|
||||
self.set_pet_button_color(color.name())
|
||||
|
||||
def set_body_button_color(self, color_str):
|
||||
"""Sets the background color of the body button and stores the value."""
|
||||
self.body_color_btn.setStyleSheet(
|
||||
f"background-color: {color_str}; border: 1px solid gray;")
|
||||
self.current_body_color = color_str
|
||||
|
||||
def choose_body_color(self):
|
||||
"""Opens a color picker dialog for body box."""
|
||||
color = QColorDialog.getColor(QColor(self.current_body_color), self)
|
||||
if color.isValid():
|
||||
self.set_body_button_color(color.name())
|
||||
|
||||
def set_object_button_color(self, color_str):
|
||||
"""Sets the background color of the object button."""
|
||||
self.object_color_btn.setStyleSheet(
|
||||
@@ -942,15 +1013,18 @@ class SettingsDialog(QDialog):
|
||||
APP_CONFIG["scan_full_on_start"] = self.scan_full_on_start_checkbox.isChecked()
|
||||
APP_CONFIG["person_tags"] = self.person_tags_edit.text()
|
||||
APP_CONFIG["pet_tags"] = self.pet_tags_edit.text()
|
||||
APP_CONFIG["body_tags"] = self.body_tags_edit.text()
|
||||
APP_CONFIG["object_tags"] = self.object_tags_edit.text()
|
||||
APP_CONFIG["landmark_tags"] = self.landmark_tags_edit.text()
|
||||
APP_CONFIG["face_box_color"] = self.current_face_color
|
||||
APP_CONFIG["pet_box_color"] = self.current_pet_color
|
||||
APP_CONFIG["body_box_color"] = self.current_body_color
|
||||
APP_CONFIG["object_box_color"] = self.current_object_color
|
||||
APP_CONFIG["landmark_box_color"] = self.current_landmark_color
|
||||
APP_CONFIG["tags_menu_max_items"] = self.mru_tags_spin.value()
|
||||
APP_CONFIG["faces_menu_max_items"] = self.face_history_spin.value()
|
||||
APP_CONFIG["pets_menu_max_items"] = self.pet_history_spin.value()
|
||||
APP_CONFIG["body_menu_max_items"] = self.body_history_spin.value()
|
||||
APP_CONFIG["object_menu_max_items"] = self.object_history_spin.value()
|
||||
APP_CONFIG["landmark_menu_max_items"] = self.landmark_history_spin.value()
|
||||
|
||||
@@ -975,9 +1049,10 @@ class SettingsDialog(QDialog):
|
||||
APP_CONFIG["viewer_wheel_speed"] = self.viewer_wheel_spin.value()
|
||||
APP_CONFIG["viewer_auto_resize_window"] = \
|
||||
self.viewer_auto_resize_check.isChecked()
|
||||
if self.face_engine_combo:
|
||||
APP_CONFIG["face_detection_engine"] = self.face_engine_combo.currentText()
|
||||
APP_CONFIG["pet_detection_engine"] = self.pet_engine_combo.currentText()
|
||||
if hasattr(self, "object_engine_combo"):
|
||||
APP_CONFIG["body_detection_engine"] = self.body_engine_combo.currentText()
|
||||
if hasattr(self, "object_engine_combo"):
|
||||
APP_CONFIG["object_detection_engine"] = \
|
||||
self.object_engine_combo.currentText()
|
||||
|
||||
13
widgets.py
13
widgets.py
@@ -1121,6 +1121,9 @@ class FaceNameInputWidget(QWidget):
|
||||
if self.region_type == "Pet":
|
||||
max_items = APP_CONFIG.get("pets_menu_max_items",
|
||||
FACES_MENU_MAX_ITEMS_DEFAULT)
|
||||
elif self.region_type == "Body":
|
||||
max_items = APP_CONFIG.get("body_menu_max_items",
|
||||
FACES_MENU_MAX_ITEMS_DEFAULT)
|
||||
elif self.region_type == "Object":
|
||||
max_items = APP_CONFIG.get("object_menu_max_items",
|
||||
FACES_MENU_MAX_ITEMS_DEFAULT)
|
||||
@@ -1188,6 +1191,12 @@ class FaceNameInputWidget(QWidget):
|
||||
parent_tags_str = "Pet"
|
||||
dialog_title = UITexts.NEW_PET_TAG_TITLE
|
||||
dialog_text = UITexts.NEW_PET_TAG_TEXT
|
||||
elif self.region_type == "Body":
|
||||
parent_tags_str = APP_CONFIG.get("body_tags", "Body")
|
||||
if not parent_tags_str or not parent_tags_str.strip():
|
||||
parent_tags_str = "Body"
|
||||
dialog_title = UITexts.NEW_BODY_TAG_TITLE
|
||||
dialog_text = UITexts.NEW_BODY_TAG_TEXT
|
||||
elif self.region_type == "Object":
|
||||
parent_tags_str = APP_CONFIG.get("object_tags", "Object")
|
||||
if not parent_tags_str or not parent_tags_str.strip():
|
||||
@@ -1273,6 +1282,10 @@ class FaceNameInputWidget(QWidget):
|
||||
parent_tags_str = APP_CONFIG.get("pet_tags", "Pet")
|
||||
if not parent_tags_str or not parent_tags_str.strip():
|
||||
parent_tags_str = "Pet"
|
||||
elif self.region_type == "Body":
|
||||
parent_tags_str = APP_CONFIG.get("body_tags", "Body")
|
||||
if not parent_tags_str or not parent_tags_str.strip():
|
||||
parent_tags_str = "Body"
|
||||
elif self.region_type == "Object":
|
||||
parent_tags_str = APP_CONFIG.get("object_tags", "Object")
|
||||
if not parent_tags_str or not parent_tags_str.strip():
|
||||
|
||||
Reference in New Issue
Block a user