Add a progress bar

This commit is contained in:
Arnaud_Cayrol 2025-04-06 21:19:20 +02:00
parent 32ab253e6e
commit 46060579f1
3 changed files with 92 additions and 48 deletions

75
main.py
View file

@ -1,6 +1,7 @@
import os import os
import multiprocessing import multiprocessing
from flask import Flask, request, render_template import threading
from flask import Flask, request, render_template, jsonify
from timelapse import process_faces from timelapse import process_faces
app = Flask(__name__) app = Flask(__name__)
@ -17,43 +18,67 @@ LANDMARK_MODEL = "shape_predictor_68_face_landmarks.dat"
LEFT_EYE_POS = (0.35, 0.45) LEFT_EYE_POS = (0.35, 0.45)
AVAILABLE_CORES = multiprocessing.cpu_count() 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"]) @app.route("/", methods=["GET", "POST"])
def index(): def index():
result = None result = None
error = 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)) max_workers_options = list(range(1, AVAILABLE_CORES + 1))
if request.method == "POST": if request.method == "POST":
try: 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"] person_id = request.form["person_id"]
padding_percent = float(request.form.get("padding_percent", 30)) / 100 padding_percent = float(request.form.get("padding_percent", 30)) / 100
resize_size = int(request.form.get("resize_size", 512)) resize_size = int(request.form.get("resize_size", 512))
face_resolution_threshold = int(request.form.get("face_resolution_threshold", 128)) face_resolution_threshold = int(request.form.get("face_resolution_threshold", 128))
pose_threshold = float(request.form.get("pose_threshold", 25)) pose_threshold = float(request.form.get("pose_threshold", 25))
max_workers = int(request.form.get("max_workers", 1)) # default is 1 max_workers = int(request.form.get("max_workers", 1)) # default is 1
# Reset progress info before starting
processed_files = process_faces( progress_info["completed"] = 0
api_key=api_key, progress_info["total"] = 0
base_url=base_url, progress_info["status"] = "idle"
person_id=person_id, # Start the processing in a background thread
output_folder=output_folder, threading.Thread(
padding_percent=padding_percent, target=background_process,
resize_width=resize_size, args=(person_id, padding_percent, resize_size, face_resolution_threshold, pose_threshold, max_workers)
resize_height=resize_size, ).start()
min_face_width=face_resolution_threshold, result = "Processing started. Please wait and watch the progress bar below."
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}'."
except Exception as e: except Exception as e:
error = f"Error processing request: {e}" error = f"Error processing request: {e}"
return render_template("index.html", result=result, error=error, max_workers_options=max_workers_options) return render_template("index.html", result=result, error=error, max_workers_options=max_workers_options)

View file

@ -65,6 +65,14 @@
.result { .result {
color: #080; color: #080;
} }
#progressContainer {
margin-top: 20px;
text-align: center;
}
progress {
width: 100%;
height: 25px;
}
</style> </style>
</head> </head>
<body> <body>
@ -107,6 +115,30 @@
{% if result %} {% if result %}
<p class="result">{{ result }}</p> <p class="result">{{ result }}</p>
{% endif %} {% endif %}
<!-- Progress Bar -->
<div id="progressContainer">
<progress id="progressBar" value="0" max="100"></progress>
<p id="progressText">0%</p>
</div>
</div> </div>
<script>
function fetchProgress() {
fetch('/progress')
.then(response => response.json())
.then(data => {
const progressBar = document.getElementById("progressBar");
const progressText = document.getElementById("progressText");
if (data.total > 0) {
progressBar.max = data.total;
progressBar.value = data.completed;
const percent = Math.floor((data.completed / data.total) * 100);
progressText.textContent = percent + "%";
}
})
.catch(err => console.error('Error fetching progress:', err));
}
// Poll every 1 second
setInterval(fetchProgress, 1000);
</script>
</body> </body>
</html> </html>

View file

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