Simplified process parameters
This commit is contained in:
parent
2bfc68c520
commit
9df9d7519c
2 changed files with 43 additions and 91 deletions
86
main.py
86
main.py
|
|
@ -2,13 +2,11 @@ import logging
|
|||
import multiprocessing
|
||||
import os
|
||||
import threading
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Callable, Dict, List, Optional, Tuple
|
||||
from typing import Callable, Dict, List, Tuple
|
||||
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
from timelapse import ProcessConfig, process_faces, validate_immich_connection
|
||||
from timelapse import process_faces, validate_immich_connection
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -27,16 +25,15 @@ class AppConfig:
|
|||
"""Configuration for the application."""
|
||||
api_key: str = os.environ.get("IMMICH_API_KEY", "")
|
||||
base_url: str = os.environ.get("IMMICH_BASE_URL", "")
|
||||
person_id: str = None
|
||||
output_folder: str = "output"
|
||||
landmark_model: str ="shape_predictor_68_face_landmarks.dat"
|
||||
default_resize_size: int = 512
|
||||
default_face_resolution_threshold: int = 128
|
||||
default_pose_threshold: float = 25.0
|
||||
default_left_eye_pos: Tuple[float, float] = (0.35, 0.4)
|
||||
default_framerate: int = 24
|
||||
default_date_format: str = "%Y-%m-%d"
|
||||
|
||||
|
||||
landmark_model: str = "shape_predictor_68_face_landmarks.dat"
|
||||
resize_size: int = 512
|
||||
face_resolution_threshold: int = 128
|
||||
pose_threshold: float = 25.0
|
||||
left_eye_pos: Tuple[float, float] = (0.35, 0.4)
|
||||
date_from: str = None
|
||||
date_to: str = None
|
||||
|
||||
# Initialize Flask app
|
||||
app = Flask(__name__)
|
||||
|
|
@ -44,7 +41,7 @@ app = Flask(__name__)
|
|||
# Global state
|
||||
AVAILABLE_CORES = multiprocessing.cpu_count()
|
||||
progress_info: Dict[str, any] = {"completed": 0, "total": 0, "status": "idle"}
|
||||
processing_thread: Optional[threading.Thread] = None
|
||||
processing_thread: threading.Thread = None
|
||||
cancel_requested: bool = False
|
||||
config = AppConfig()
|
||||
|
||||
|
|
@ -74,26 +71,12 @@ def check_output_folder() -> Tuple[bool, int]:
|
|||
return len(files) == 0, len(files)
|
||||
|
||||
def background_process(
|
||||
person_id: str,
|
||||
resize_size: int,
|
||||
face_resolution_threshold: int,
|
||||
pose_threshold: float,
|
||||
left_eye_pos: Tuple[float, float],
|
||||
date_from: Optional[str] = None,
|
||||
date_to: Optional[str] = None,
|
||||
progress_callback: Optional[Callable] = None,
|
||||
cancel_flag: Optional[Callable] = None
|
||||
progress_callback: Callable = None,
|
||||
cancel_flag: Callable = None
|
||||
) -> List[str]:
|
||||
"""Process faces in the background.
|
||||
|
||||
Args:
|
||||
person_id: ID of the person to process
|
||||
resize_size: Size to resize output images to
|
||||
face_resolution_threshold: Minimum face resolution threshold
|
||||
pose_threshold: Maximum allowed head pose deviation
|
||||
left_eye_pos: Desired position of the left eye in output
|
||||
date_from: Optional start date in YYYY-MM-DD format
|
||||
date_to: Optional end date in YYYY-MM-DD format
|
||||
progress_callback: Optional callback for progress updates
|
||||
cancel_flag: Optional function to check for cancellation
|
||||
|
||||
|
|
@ -101,24 +84,8 @@ def background_process(
|
|||
List of processed file paths
|
||||
"""
|
||||
try:
|
||||
process_config = ProcessConfig(
|
||||
api_key=config.api_key,
|
||||
base_url=config.base_url,
|
||||
person_id=person_id,
|
||||
output_folder=config.output_folder,
|
||||
resize_width=resize_size,
|
||||
resize_height=resize_size,
|
||||
min_face_width=face_resolution_threshold,
|
||||
min_face_height=face_resolution_threshold,
|
||||
pose_threshold=pose_threshold,
|
||||
left_eye_pos=left_eye_pos,
|
||||
landmark_model_path=config.landmark_model,
|
||||
date_from=date_from,
|
||||
date_to=date_to
|
||||
)
|
||||
|
||||
return process_faces(
|
||||
config=process_config,
|
||||
config=config,
|
||||
max_workers=1,
|
||||
progress_callback=progress_callback,
|
||||
cancel_flag=cancel_flag
|
||||
|
|
@ -178,20 +145,17 @@ def index() -> str:
|
|||
try:
|
||||
cancel_requested = False
|
||||
|
||||
# Get form data with defaults
|
||||
person_id = request.form["person_id"]
|
||||
resize_size = int(request.form.get("resize_size", config.default_resize_size))
|
||||
face_resolution_threshold = int(request.form.get("face_resolution_threshold",
|
||||
config.default_face_resolution_threshold))
|
||||
pose_threshold = float(request.form.get("pose_threshold", config.default_pose_threshold))
|
||||
|
||||
# Optional date ranges
|
||||
date_from = request.form.get("date_from") or None
|
||||
date_to = request.form.get("date_to") or None
|
||||
# Get form data
|
||||
config.person_id = request.form["person_id"]
|
||||
config.resize_size = int(request.form.get("resize_size"))
|
||||
config.face_resolution_threshold = int(request.form.get("face_resolution_threshold"))
|
||||
config.pose_threshold = float(request.form.get("pose_threshold"))
|
||||
config.date_from = request.form.get("date_from")
|
||||
config.date_to = request.form.get("date_to")
|
||||
|
||||
# Video compilation options
|
||||
compile_video = request.form.get("compile_video") == "on"
|
||||
framerate = int(request.form.get("framerate", config.default_framerate))
|
||||
framerate = int(request.form.get("framerate", 15))
|
||||
|
||||
# Reset progress info
|
||||
progress_info.update({
|
||||
|
|
@ -204,10 +168,8 @@ def index() -> str:
|
|||
# Start processing
|
||||
processing_thread = threading.Thread(
|
||||
target=background_process,
|
||||
args=(person_id, resize_size, face_resolution_threshold, pose_threshold,
|
||||
config.default_left_eye_pos, config.output_folder, date_from, date_to,
|
||||
update_progress, lambda: cancel_requested)
|
||||
)
|
||||
args=(update_progress, lambda: cancel_requested)
|
||||
)
|
||||
processing_thread.start()
|
||||
result = "Processing started. Please wait and watch the progress bar below."
|
||||
|
||||
|
|
|
|||
48
timelapse.py
48
timelapse.py
|
|
@ -11,7 +11,7 @@ import cv2
|
|||
import dlib
|
||||
from tqdm import tqdm
|
||||
import logging
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
class TqdmLoggingHandler(logging.Handler):
|
||||
def __init__(self, level=logging.NOTSET):
|
||||
|
|
@ -36,21 +36,19 @@ face_predictor = None
|
|||
|
||||
|
||||
@dataclass
|
||||
class ProcessConfig:
|
||||
"""
|
||||
Dataclass to hold configuration parameters for processing assets.
|
||||
"""
|
||||
class AppConfig:
|
||||
"""Configuration for the application."""
|
||||
api_key: str
|
||||
base_url: str
|
||||
person_id: str
|
||||
output_folder: str = "output"
|
||||
resize_width: int = 512
|
||||
resize_height: int = 512
|
||||
min_face_width: int = 128
|
||||
min_face_height: int = 128
|
||||
pose_threshold: float = 25
|
||||
left_eye_pos: tuple = (0.35, 0.4)
|
||||
landmark_model_path: str = "shape_predictor_68_face_landmarks.dat"
|
||||
output_folder: str
|
||||
landmark_model: str
|
||||
resize_size: int
|
||||
face_resolution_threshold: int
|
||||
pose_threshold: float
|
||||
left_eye_pos: Tuple[float, float]
|
||||
date_from: str
|
||||
date_to: str
|
||||
|
||||
def draw_landmarks(image, landmarks, face_rect, output_path):
|
||||
"""
|
||||
|
|
@ -486,7 +484,7 @@ def crop_and_align_face(image, face_data, resize_size, face_resolution_threshold
|
|||
return None
|
||||
|
||||
|
||||
def process_asset_worker(asset, config: ProcessConfig):
|
||||
def process_asset_worker(asset, config: AppConfig):
|
||||
"""
|
||||
Worker function to process a single asset.
|
||||
|
||||
|
|
@ -495,14 +493,13 @@ def process_asset_worker(asset, config: ProcessConfig):
|
|||
|
||||
Args:
|
||||
asset (dict): The asset metadata.
|
||||
config (ProcessConfig): Configuration parameters.
|
||||
config (AppConfig): Configuration parameters.
|
||||
|
||||
Returns:
|
||||
str or None: The file path of the saved image if processing is successful; otherwise None.
|
||||
"""
|
||||
try:
|
||||
asset_id = asset['id']
|
||||
|
||||
image_bytes = download_asset(config.api_key, config.base_url, asset_id)
|
||||
image = Image.open(io.BytesIO(image_bytes))
|
||||
image = ImageOps.exif_transpose(image)
|
||||
|
|
@ -514,12 +511,11 @@ def process_asset_worker(asset, config: ProcessConfig):
|
|||
matching_person = next((p for p in asset.get('people', []) if p.get('id') == config.person_id), None)
|
||||
face_data = matching_person.get('faces', [])[0]
|
||||
|
||||
|
||||
aligned_face = crop_and_align_face(
|
||||
image,
|
||||
face_data,
|
||||
resize_size=config.resize_width,
|
||||
face_resolution_threshold=config.min_face_width,
|
||||
resize_size=config.resize_size,
|
||||
face_resolution_threshold=config.face_resolution_threshold,
|
||||
pose_threshold=config.pose_threshold,
|
||||
left_eye_pos=config.left_eye_pos
|
||||
)
|
||||
|
|
@ -527,15 +523,13 @@ def process_asset_worker(asset, config: ProcessConfig):
|
|||
if aligned_face is None:
|
||||
return None
|
||||
|
||||
os.makedirs(config.output_folder, exist_ok=True)
|
||||
dt = datetime.fromisoformat(asset['fileCreatedAt'].replace("Z", "+00:00"))
|
||||
timestamp = dt.strftime("%Y%m%d_%H%M%S")
|
||||
filename = os.path.join(config.output_folder, f"{timestamp}.jpg")
|
||||
aligned_face.save(filename)
|
||||
return filename
|
||||
|
||||
def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None, date_from=None, date_to=None,
|
||||
cancel_flag=None):
|
||||
def process_faces(config: AppConfig, max_workers=1, progress_callback=None, cancel_flag=None):
|
||||
"""
|
||||
Processes assets containing the person and saves aligned face images.
|
||||
|
||||
|
|
@ -543,23 +537,19 @@ def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None,
|
|||
concurrently download, crop, and align faces.
|
||||
|
||||
Args:
|
||||
config (ProcessConfig): Configuration parameters.
|
||||
config (AppConfig): Configuration parameters.
|
||||
max_workers (int): Number of worker processes.
|
||||
progress_callback (callable, optional): A callback function for progress updates.
|
||||
date_from (str, optional): Start date for filtering assets.
|
||||
date_to (str, optional): End date for filtering assets.
|
||||
cancel_flag (callable, optional): A function that returns True if processing should be cancelled.
|
||||
|
||||
Returns:
|
||||
list: A list of file paths of the saved images.
|
||||
"""
|
||||
os.makedirs(config.output_folder, exist_ok=True)
|
||||
|
||||
if cancel_flag and cancel_flag():
|
||||
logger.info("Processing was cancelled.")
|
||||
return []
|
||||
|
||||
assets = get_assets_with_person(config.api_key, config.base_url, config.person_id, date_from, date_to)
|
||||
assets = get_assets_with_person(config.api_key, config.base_url, config.person_id, config.date_from, config.date_to)
|
||||
logger.info(f"Found {len(assets)} assets containing the person.")
|
||||
|
||||
total_assets = len(assets)
|
||||
|
|
@ -568,7 +558,7 @@ def process_faces(config: ProcessConfig, max_workers=1, progress_callback=None,
|
|||
processed_files = []
|
||||
completed_count = 0
|
||||
|
||||
initializer_args = (config.landmark_model_path,)
|
||||
initializer_args = (config.landmark_model)
|
||||
with concurrent.futures.ProcessPoolExecutor(
|
||||
max_workers=max_workers,
|
||||
initializer=initialize_worker,
|
||||
|
|
|
|||
Loading…
Reference in a new issue