Add a progress bar
This commit is contained in:
parent
32ab253e6e
commit
46060579f1
3 changed files with 92 additions and 48 deletions
75
main.py
75
main.py
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
33
timelapse.py
33
timelapse.py
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue