A bunch of changes
This commit is contained in:
@@ -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()
|
||||
|
||||
# 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 []
|
||||
self._current_rating = current_image_rating
|
||||
# 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
|
||||
self._current_rating = 0
|
||||
|
||||
# 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()
|
||||
|
||||
Reference in New Issue
Block a user