From 46060579f135d712ba23b2b450116f1319e8077f Mon Sep 17 00:00:00 2001 From: Arnaud_Cayrol Date: Sun, 6 Apr 2025 21:19:20 +0200 Subject: [PATCH] Add a progress bar --- main.py | 75 +++++++++++++++++++++++++++++--------------- templates/index.html | 32 +++++++++++++++++++ timelapse.py | 33 ++++++------------- 3 files changed, 92 insertions(+), 48 deletions(-) diff --git a/main.py b/main.py index a4bb334..499bf80 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ import os import multiprocessing -from flask import Flask, request, render_template +import threading +from flask import Flask, request, render_template, jsonify from timelapse import process_faces app = Flask(__name__) @@ -17,43 +18,67 @@ LANDMARK_MODEL = "shape_predictor_68_face_landmarks.dat" LEFT_EYE_POS = (0.35, 0.45) AVAILABLE_CORES = multiprocessing.cpu_count() +# Global progress dictionary – only one job at a time is assumed here +progress_info = {"completed": 0, "total": 0, "status": "idle"} + +def update_progress(current, total): + progress_info["completed"] = current + progress_info["total"] = total + progress_info["status"] = "running" if current < total else "done" + +def background_process(person_id, padding_percent, resize_size, face_resolution_threshold, pose_threshold, max_workers): + try: + progress_info["status"] = "running" + process_faces( + api_key=API_KEY, + base_url=BASE_URL, + person_id=person_id, + output_folder=OUTPUT_FOLDER, + padding_percent=padding_percent, + resize_width=resize_size, + resize_height=resize_size, + min_face_width=face_resolution_threshold, + min_face_height=face_resolution_threshold, + pose_threshold=pose_threshold, + desired_left_eye=LEFT_EYE_POS, + max_workers=max_workers, + face_detect_model_path=FACE_DETECT_MODEL, + landmark_model_path=LANDMARK_MODEL, + progress_callback=update_progress + ) + except Exception as e: + progress_info["status"] = f"error: {e}" + else: + progress_info["status"] = "done" + +@app.route("/progress") +def progress(): + return jsonify(progress_info) + @app.route("/", methods=["GET", "POST"]) def index(): result = None error = None - # Create max_workers_options as a list of numbers from 1 to AVAILABLE_CORES + # Create max_workers_options as a list from 1 to AVAILABLE_CORES max_workers_options = list(range(1, AVAILABLE_CORES + 1)) if request.method == "POST": try: - api_key = API_KEY - base_url = BASE_URL - output_folder = OUTPUT_FOLDER - - # Get user input from the form person_id = request.form["person_id"] padding_percent = float(request.form.get("padding_percent", 30)) / 100 resize_size = int(request.form.get("resize_size", 512)) face_resolution_threshold = int(request.form.get("face_resolution_threshold", 128)) pose_threshold = float(request.form.get("pose_threshold", 25)) max_workers = int(request.form.get("max_workers", 1)) # default is 1 - - processed_files = process_faces( - api_key=api_key, - base_url=base_url, - person_id=person_id, - output_folder=output_folder, - padding_percent=padding_percent, - resize_width=resize_size, - resize_height=resize_size, - min_face_width=face_resolution_threshold, - min_face_height=face_resolution_threshold, - pose_threshold=pose_threshold, - desired_left_eye=LEFT_EYE_POS, - max_workers=max_workers, - face_detect_model_path=FACE_DETECT_MODEL, - landmark_model_path=LANDMARK_MODEL - ) - result = f"Finished processing. {len(processed_files)} images saved in '{output_folder}'." + # Reset progress info before starting + progress_info["completed"] = 0 + progress_info["total"] = 0 + progress_info["status"] = "idle" + # Start the processing in a background thread + threading.Thread( + target=background_process, + args=(person_id, padding_percent, resize_size, face_resolution_threshold, pose_threshold, max_workers) + ).start() + result = "Processing started. Please wait and watch the progress bar below." except Exception as e: error = f"Error processing request: {e}" return render_template("index.html", result=result, error=error, max_workers_options=max_workers_options) diff --git a/templates/index.html b/templates/index.html index 5b2710d..9e9d241 100644 --- a/templates/index.html +++ b/templates/index.html @@ -65,6 +65,14 @@ .result { color: #080; } + #progressContainer { + margin-top: 20px; + text-align: center; + } + progress { + width: 100%; + height: 25px; + } @@ -107,6 +115,30 @@ {% if result %}

{{ result }}

{% endif %} + +
+ +

0%

+
+ diff --git a/timelapse.py b/timelapse.py index 1d6b730..f7a4b13 100644 --- a/timelapse.py +++ b/timelapse.py @@ -1,5 +1,4 @@ # timelapse.py - import os import io import requests @@ -16,7 +15,6 @@ import logging class TqdmLoggingHandler(logging.Handler): def __init__(self, level=logging.NOTSET): super().__init__(level) - def emit(self, record): try: msg = self.format(record) @@ -31,7 +29,6 @@ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datef tqdm_handler.setFormatter(formatter) logger.addHandler(tqdm_handler) - def get_assets_with_person(api_key, base_url, person_id): headers = { 'Content-Type': 'application/json', @@ -63,19 +60,16 @@ def get_assets_with_person(api_key, base_url, person_id): payload["page"] = data['assets'].get('nextPage') return all_assets - def download_asset(api_key, base_url, asset_id): headers = {'x-api-key': api_key} response = requests.get(f'{base_url}/assets/{asset_id}/original', headers=headers) response.raise_for_status() return response.content - def format_timestamp(timestamp): dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) return dt.strftime("%Y%m%d_%H%M%S") - def crop_face_from_metadata(image, face_data, padding_percent): face_img_width = face_data.get("imageWidth") face_img_height = face_data.get("imageHeight") @@ -95,7 +89,6 @@ def crop_face_from_metadata(image, face_data, padding_percent): new_y2 = min(y2 + padding, img_height) return image.crop((new_x1, new_y1, new_x2, new_y2)) - def get_head_pose(shape, img_size): image_points = np.array([ (shape.part(30).x, shape.part(30).y), @@ -131,22 +124,18 @@ def get_head_pose(shape, img_size): pitch, yaw, roll = [float(angle) for angle in eulerAngles] return pitch, yaw, roll - def align_face(image, predictor, detector, desired_face_width, desired_face_height, desired_left_eye, pose_threshold): image_np = np.array(image) gray = cv2.cvtColor(image_np, cv2.COLOR_RGB2GRAY) - detections = detector(gray) if not detections: logger.info("No face detected in the crop. Discarding.") return None - if hasattr(detections[0], "rect"): rect = detections[0].rect else: rect = detections[0] - shape = predictor(gray, rect) img_size = (image_np.shape[1], image_np.shape[0]) pitch, yaw, roll = get_head_pose(shape, img_size) @@ -181,12 +170,10 @@ def align_face(image, predictor, detector, desired_face_width, desired_face_heig ) return Image.fromarray(aligned_face_np) - def process_asset_worker(asset, api_key, base_url, person_id, output_folder, padding_percent, min_face_width, min_face_height, resize_width, resize_height, pose_threshold, desired_left_eye, cnn_model_path, predictor_model_path): - detector = dlib.cnn_face_detection_model_v1(cnn_model_path) local_predictor = dlib.shape_predictor(predictor_model_path) try: @@ -224,11 +211,9 @@ def process_asset_worker(asset, api_key, base_url, person_id, output_folder, aligned_face.save(filename) return filename - def process_asset_wrapper(asset, process_args): return process_asset_worker(asset, *process_args) - def process_faces( api_key, base_url, @@ -243,25 +228,27 @@ def process_faces( desired_left_eye=(0.35, 0.45), max_workers=1, face_detect_model_path="mmod_human_face_detector.dat", - landmark_model_path="shape_predictor_68_face_landmarks.dat" + landmark_model_path="shape_predictor_68_face_landmarks.dat", + progress_callback=None # New optional parameter ): os.makedirs(output_folder, exist_ok=True) - assets = get_assets_with_person(api_key, base_url, person_id) logger.info(f"Found {len(assets)} assets containing the person.") - + total_assets = len(assets) + if progress_callback: + progress_callback(0, total_assets) process_args = ( api_key, base_url, person_id, output_folder, padding_percent, min_face_width, min_face_height, resize_width, resize_height, pose_threshold, desired_left_eye, face_detect_model_path, landmark_model_path ) - + results = [] with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as executor: - results = list(tqdm( - executor.map(process_asset_wrapper, assets, [process_args] * len(assets)), - total=len(assets) - )) + for result in tqdm(executor.map(process_asset_wrapper, assets, [process_args]*total_assets), total=total_assets): + results.append(result) + if progress_callback: + progress_callback(len(results), total_assets) processed_files = [r for r in results if r is not None] logger.info(f"Finished processing. {len(processed_files)} images saved.") return processed_files