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