Add a basic parameters webpage

This commit is contained in:
Arnaud_Cayrol 2025-04-06 19:08:27 +02:00
parent 4e0c0ca748
commit 188658d6fa
4 changed files with 112 additions and 46 deletions

6
.gitignore vendored
View file

@ -1 +1,5 @@
.idea
.idea
dlib-19.24.99-cp312-cp312-win_amd64.whl
mmod_human_face_detector.dat
shape_predictor_68_face_landmarks.dat
output/

View file

@ -1,14 +1,4 @@
#!/usr/bin/env python3
"""
This tool helps create selfie timelapses from your Immich instance.
It uses the powerful machine learning features of Immich to gather all the photographs where a particular individual
appears, retrieves the bounding box metadata, and automatically crops and aligns the photos.
Some manual sorting is still required to achieve the best effect in the video.
I personally found that a video frame rate of 15 fps looks pretty good.
Script by Arnaud Cayrol
"""
# timelapse.py
import os
import io
@ -36,7 +26,6 @@ class TqdmLoggingHandler(logging.Handler):
except Exception:
self.handleError(record)
# Configure logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
tqdm_handler = TqdmLoggingHandler()
@ -150,13 +139,11 @@ def align_face(image, predictor, detector, desired_face_width, desired_face_heig
image_np = np.array(image)
gray = cv2.cvtColor(image_np, cv2.COLOR_RGB2GRAY)
# Detect faces using the provided detector.
detections = detector(gray)
if not detections:
logger.info("No face detected in the crop. Discarding.")
return None
# If using CNN detector, extract the rectangle from the detection.
if hasattr(detections[0], "rect"):
rect = detections[0].rect
else:
@ -203,7 +190,6 @@ def process_asset_worker(asset, api_key, base_url, person_id, output_folder,
cnn_model_path, predictor_model_path):
detector = dlib.cnn_face_detection_model_v1(cnn_model_path)
# Use predictor_model_path from command line instead of hard-coded path.
local_predictor = dlib.shape_predictor(predictor_model_path)
try:
asset_id = asset['id']
@ -245,45 +231,39 @@ def process_asset_wrapper(asset, process_args):
return process_asset_worker(asset, *process_args)
def main():
parser = argparse.ArgumentParser(description="Process and align faces from assets.")
parser.add_argument("--api-key", required=True, help="API key for authentication")
parser.add_argument("--base-url", required=True, help="Base URL for the API")
parser.add_argument("--person-id", required=True, help="ID of the person to search for")
parser.add_argument("--output-folder", default="output", help="Folder to save output images")
parser.add_argument("--padding-percent", type=float, default=0.3, help="Padding percentage for face crop")
parser.add_argument("--resize-width", type=int, default=512, help="Output image width")
parser.add_argument("--resize-height", type=int, default=512, help="Output image height")
parser.add_argument("--min-face-width", type=int, default=128, help="Minimum face width")
parser.add_argument("--min-face-height", type=int, default=128, help="Minimum face height")
parser.add_argument("--pose-threshold", type=float, default=25, help="Threshold for acceptable head orientation towards camera")
parser.add_argument("--desired-left-eye", type=float, nargs=2, default=[0.35, 0.45],
help="Desired left eye position as fraction (x y) in the output image")
parser.add_argument("--max-workers", type=int, default=4, help="Maximum number of parallel workers")
parser.add_argument("--face-detect-model-path", default="mmod_human_face_detector.dat", help="Path to the CNN face detector model file")
parser.add_argument("--landmark-model-path", default="shape_predictor_68_face_landmarks.dat", help="Path to the face landmark predictor model file")
args = parser.parse_args()
def process_faces(
api_key,
base_url,
person_id,
output_folder="output",
padding_percent=0.3,
resize_width=512,
resize_height=512,
min_face_width=128,
min_face_height=128,
pose_threshold=25,
desired_left_eye=(0.35, 0.45),
max_workers=4,
face_detect_model_path="mmod_human_face_detector.dat",
landmark_model_path="shape_predictor_68_face_landmarks.dat"
):
os.makedirs(output_folder, exist_ok=True)
os.makedirs(args.output_folder, exist_ok=True)
assets = get_assets_with_person(args.api_key, args.base_url, args.person_id)
assets = get_assets_with_person(api_key, base_url, person_id)
logger.info(f"Found {len(assets)} assets containing the person.")
process_args = (
args.api_key, args.base_url, args.person_id, args.output_folder,
args.padding_percent, args.min_face_width, args.min_face_height,
args.resize_width, args.resize_height, args.pose_threshold, tuple(args.desired_left_eye),
args.face_detect_model_path, args.landmark_model_path
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
)
with concurrent.futures.ProcessPoolExecutor(max_workers=args.max_workers) as executor:
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)
))
processed_files = [r for r in results if r is not None]
logger.info(f"Finished processing. {len(processed_files)} images saved.")
if __name__ == "__main__":
main()
return processed_files

52
main.py Normal file
View file

@ -0,0 +1,52 @@
# main.py
from flask import Flask, request, render_template
from immich_selfie_timelapse import process_faces
app = Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def index():
result = None
if request.method == "POST":
# Read form fields, converting as necessary.
api_key = request.form["api_key"]
base_url = request.form["base_url"]
person_id = request.form["person_id"]
output_folder = request.form.get("output_folder", "output")
padding_percent = float(request.form.get("padding_percent", 0.3))
resize_width = int(request.form.get("resize_width", 512))
resize_height = int(request.form.get("resize_height", 512))
min_face_width = int(request.form.get("min_face_width", 128))
min_face_height = int(request.form.get("min_face_height", 128))
pose_threshold = float(request.form.get("pose_threshold", 25))
desired_left_eye_x = float(request.form.get("desired_left_eye_x", 0.35))
desired_left_eye_y = float(request.form.get("desired_left_eye_y", 0.45))
max_workers = int(request.form.get("max_workers", 4))
face_detect_model_path = request.form.get("face_detect_model_path", "mmod_human_face_detector.dat")
landmark_model_path = request.form.get("landmark_model_path", "shape_predictor_68_face_landmarks.dat")
# Run the process_faces function with provided parameters
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_width,
resize_height=resize_height,
min_face_width=min_face_width,
min_face_height=min_face_height,
pose_threshold=pose_threshold,
desired_left_eye=(desired_left_eye_x, desired_left_eye_y),
max_workers=max_workers,
face_detect_model_path=face_detect_model_path,
landmark_model_path=landmark_model_path
)
result = f"Finished processing. {len(processed_files)} images saved in {output_folder}"
return render_template("index.html", result=result)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)

30
templates/index.html Normal file
View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>Immich Selfie Timelapse Tool</title>
</head>
<body>
<h1>Create a Selfie Timelapse</h1>
<form method="post">
<label>API Key: <input type="text" name="api_key" required></label><br><br>
<label>Base URL: <input type="text" name="base_url" required></label><br><br>
<label>Person ID: <input type="text" name="person_id" required></label><br><br>
<label>Output Folder: <input type="text" name="output_folder" value="output"></label><br><br>
<label>Padding Percent: <input type="number" step="0.01" name="padding_percent" value="0.3"></label><br><br>
<label>Resize Width: <input type="number" name="resize_width" value="512"></label><br><br>
<label>Resize Height: <input type="number" name="resize_height" value="512"></label><br><br>
<label>Min Face Width: <input type="number" name="min_face_width" value="128"></label><br><br>
<label>Min Face Height: <input type="number" name="min_face_height" value="128"></label><br><br>
<label>Pose Threshold: <input type="number" step="0.1" name="pose_threshold" value="25"></label><br><br>
<label>Desired Left Eye X: <input type="number" step="0.01" name="desired_left_eye_x" value="0.35"></label><br><br>
<label>Desired Left Eye Y: <input type="number" step="0.01" name="desired_left_eye_y" value="0.45"></label><br><br>
<label>Max Workers: <input type="number" name="max_workers" value="4"></label><br><br>
<label>Face Detect Model Path: <input type="text" name="face_detect_model_path" value="mmod_human_face_detector.dat"></label><br><br>
<label>Landmark Model Path: <input type="text" name="landmark_model_path" value="shape_predictor_68_face_landmarks.dat"></label><br><br>
<button type="submit">Generate Timelapse</button>
</form>
{% if result %}
<h2>{{ result }}</h2>
{% endif %}
</body>
</html>