Spaces:
Running
Running
Added Align and Order Options
Browse files- .gitignore +7 -0
- app.py +20 -7
- requirements.txt +2 -1
- src/landmark_detector.py +20 -0
- src/process_images.py +6 -0
- src/utils/align_images.py +47 -0
- src/utils/face_alignment.py +93 -0
- src/utils/sort_images.py +22 -0
.gitignore
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__
|
2 |
+
|
3 |
+
*.mp4
|
4 |
+
|
5 |
+
aligned_images/
|
6 |
+
|
7 |
+
flagged/
|
app.py
CHANGED
@@ -2,8 +2,10 @@ import gradio as gr
|
|
2 |
from datetime import datetime
|
3 |
|
4 |
from src.face_morp import morph
|
|
|
|
|
5 |
|
6 |
-
def transition(image_files, duration, fps, method, guideline):
|
7 |
time = datetime.now().strftime("%d.%m.%Y_%H.%M.%S")
|
8 |
output_name = f"output_{time}_{fps}fps.mp4"
|
9 |
|
@@ -12,31 +14,42 @@ def transition(image_files, duration, fps, method, guideline):
|
|
12 |
debug_messages = []
|
13 |
|
14 |
try:
|
15 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
morph(image_files, duration, fps, output_name, guideline, is_dlib)
|
17 |
debug_messages.append("Video generation successful")
|
18 |
return output_name, "\n".join(debug_messages)
|
19 |
|
20 |
except Exception as e:
|
21 |
error_message = f"Error: {str(e)}"
|
|
|
22 |
debug_messages.append(error_message)
|
23 |
return None, "\n".join(debug_messages)
|
24 |
|
25 |
if __name__ == "__main__":
|
26 |
-
|
27 |
gr.Interface(
|
28 |
fn=transition,
|
29 |
inputs=[
|
30 |
gr.File(file_count="multiple", type="filepath"),
|
31 |
gr.Slider(label="Duration (seconds) between images", minimum=1, maximum=10, step=1, value=3),
|
32 |
-
gr.Slider(label="Frames per second (fps)", minimum=
|
33 |
gr.Dropdown(label="Landmarks detection method", choices=["Dlib", "MediaPipe"], value="Dlib"),
|
|
|
|
|
34 |
gr.Checkbox(label="Guideline")
|
|
|
35 |
],
|
36 |
-
outputs=[gr.Video(), gr.Textbox(label="
|
37 |
examples=[
|
38 |
-
[["examples/1.png", "examples/2.png", "examples/3.png"], 3, 30, "Dlib", False]
|
39 |
],
|
40 |
title="Face Morphing",
|
41 |
description="Upload multiple images containing faces to create a transition video between them."
|
42 |
-
).launch()
|
|
|
2 |
from datetime import datetime
|
3 |
|
4 |
from src.face_morp import morph
|
5 |
+
from src.utils.align_images import align_images
|
6 |
+
from src.utils.sort_images import sort_images
|
7 |
|
8 |
+
def transition(image_files, duration, fps, method, align_resize, order_images, guideline):
|
9 |
time = datetime.now().strftime("%d.%m.%Y_%H.%M.%S")
|
10 |
output_name = f"output_{time}_{fps}fps.mp4"
|
11 |
|
|
|
14 |
debug_messages = []
|
15 |
|
16 |
try:
|
17 |
+
# Align and resize images
|
18 |
+
if align_resize:
|
19 |
+
aligned_dir = "aligned_images"
|
20 |
+
image_files = align_images(image_files, aligned_dir)
|
21 |
+
|
22 |
+
# Sort images by age
|
23 |
+
if order_images:
|
24 |
+
image_files = sort_images(image_files)
|
25 |
+
|
26 |
morph(image_files, duration, fps, output_name, guideline, is_dlib)
|
27 |
debug_messages.append("Video generation successful")
|
28 |
return output_name, "\n".join(debug_messages)
|
29 |
|
30 |
except Exception as e:
|
31 |
error_message = f"Error: {str(e)}"
|
32 |
+
print(error_message)
|
33 |
debug_messages.append(error_message)
|
34 |
return None, "\n".join(debug_messages)
|
35 |
|
36 |
if __name__ == "__main__":
|
|
|
37 |
gr.Interface(
|
38 |
fn=transition,
|
39 |
inputs=[
|
40 |
gr.File(file_count="multiple", type="filepath"),
|
41 |
gr.Slider(label="Duration (seconds) between images", minimum=1, maximum=10, step=1, value=3),
|
42 |
+
gr.Slider(label="Frames per second (fps)", minimum=2, maximum=60, step=1, value=30),
|
43 |
gr.Dropdown(label="Landmarks detection method", choices=["Dlib", "MediaPipe"], value="Dlib"),
|
44 |
+
gr.Checkbox(label="Align and Resize Images", value=True),
|
45 |
+
gr.Checkbox(label="Order Images by Age"),
|
46 |
gr.Checkbox(label="Guideline")
|
47 |
+
|
48 |
],
|
49 |
+
outputs=[gr.Video(), gr.Textbox(label="Output Message")],
|
50 |
examples=[
|
51 |
+
[["examples/1.png", "examples/2.png", "examples/3.png"], 3, 30, "Dlib", False, False, False]
|
52 |
],
|
53 |
title="Face Morphing",
|
54 |
description="Upload multiple images containing faces to create a transition video between them."
|
55 |
+
).launch(share=False)
|
requirements.txt
CHANGED
@@ -4,4 +4,5 @@ numpy==1.26.4
|
|
4 |
scipy==1.13.0
|
5 |
mediapipe==0.10.11
|
6 |
dlib==19.24.4
|
7 |
-
tqdm==4.66.4
|
|
|
|
4 |
scipy==1.13.0
|
5 |
mediapipe==0.10.11
|
6 |
dlib==19.24.4
|
7 |
+
tqdm==4.66.4
|
8 |
+
transformers==4.40.2
|
src/landmark_detector.py
CHANGED
@@ -13,6 +13,26 @@ def read_image(image_path):
|
|
13 |
|
14 |
return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
class DlibLandmarkDetector:
|
18 |
def __init__(self, predictor_model_path=f'{os.path.dirname(os.path.abspath(__file__))}/utils/shape_predictor_68_face_landmarks.dat'):
|
|
|
13 |
|
14 |
return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
15 |
|
16 |
+
class LandmarksDetector:
|
17 |
+
def __init__(self, predictor_model_path=f'{os.path.dirname(os.path.abspath(__file__))}/utils/shape_predictor_68_face_landmarks.dat'):
|
18 |
+
"""
|
19 |
+
:param predictor_model_path: path to shape_predictor_68_face_landmarks.dat file
|
20 |
+
"""
|
21 |
+
self.detector = dlib.get_frontal_face_detector() # cnn_face_detection_model_v1 also can be used
|
22 |
+
self.shape_predictor = dlib.shape_predictor(predictor_model_path)
|
23 |
+
|
24 |
+
def get_landmarks(self, image):
|
25 |
+
img = dlib.load_rgb_image(image)
|
26 |
+
dets = self.detector(img, 1)
|
27 |
+
dets = [dets[0]]
|
28 |
+
|
29 |
+
for detection in dets:
|
30 |
+
try:
|
31 |
+
face_landmarks = [(item.x, item.y) for item in self.shape_predictor(img, detection).parts()]
|
32 |
+
yield face_landmarks
|
33 |
+
except:
|
34 |
+
print("Exception in get_landmarks()!")
|
35 |
+
|
36 |
|
37 |
class DlibLandmarkDetector:
|
38 |
def __init__(self, predictor_model_path=f'{os.path.dirname(os.path.abspath(__file__))}/utils/shape_predictor_68_face_landmarks.dat'):
|
src/process_images.py
CHANGED
@@ -18,6 +18,7 @@ def get_images_and_landmarks(image_list, is_dlib):
|
|
18 |
raise ValueError("At least two images are required for morphing.")
|
19 |
# exit()
|
20 |
|
|
|
21 |
landmarks_list = [] # List of landmarks for each image
|
22 |
images_list = [] # List of images
|
23 |
|
@@ -40,4 +41,9 @@ def get_images_and_landmarks(image_list, is_dlib):
|
|
40 |
raise ValueError("At least two faces are required for morphing.")
|
41 |
# exit()
|
42 |
|
|
|
|
|
|
|
|
|
|
|
43 |
return images_list, landmarks_list
|
|
|
18 |
raise ValueError("At least two images are required for morphing.")
|
19 |
# exit()
|
20 |
|
21 |
+
|
22 |
landmarks_list = [] # List of landmarks for each image
|
23 |
images_list = [] # List of images
|
24 |
|
|
|
41 |
raise ValueError("At least two faces are required for morphing.")
|
42 |
# exit()
|
43 |
|
44 |
+
# if images dont have the same dimensions raise an error
|
45 |
+
if len(set([image.shape for image in images_list])) > 1:
|
46 |
+
raise ValueError("Images must have the same dimensions for morphing.")
|
47 |
+
# exit()
|
48 |
+
|
49 |
return images_list, landmarks_list
|
src/utils/align_images.py
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
import argparse
|
4 |
+
from src.utils.face_alignment import image_align
|
5 |
+
from src.landmark_detector import LandmarksDetector
|
6 |
+
|
7 |
+
|
8 |
+
def align_images(img_files, aligned_dir, output_size=1024, x_scale=1, y_scale=1, em_scale=0.1, use_alpha=False):
|
9 |
+
"""
|
10 |
+
Extracts and aligns all faces from images using DLib and a function from original FFHQ dataset preparation step
|
11 |
+
python align_images.py /raw_images /aligned_images
|
12 |
+
"""
|
13 |
+
|
14 |
+
ALIGNED_IMAGES_DIR = aligned_dir
|
15 |
+
|
16 |
+
# Create the directory if it doesn't exist
|
17 |
+
if not os.path.exists(ALIGNED_IMAGES_DIR):
|
18 |
+
os.makedirs(ALIGNED_IMAGES_DIR)
|
19 |
+
else: # Remove existing files in the directory
|
20 |
+
for file in os.listdir(ALIGNED_IMAGES_DIR):
|
21 |
+
os.remove(os.path.join(ALIGNED_IMAGES_DIR, file))
|
22 |
+
|
23 |
+
landmarks_detector = LandmarksDetector()
|
24 |
+
for img_path in img_files:
|
25 |
+
img_name = os.path.basename(img_path)
|
26 |
+
print('Aligning %s ...' % img_name)
|
27 |
+
try:
|
28 |
+
raw_img_path = img_path
|
29 |
+
fn = face_img_name = '%s_%02d.png' % (os.path.splitext(img_name)[0], 1)
|
30 |
+
if os.path.isfile(fn):
|
31 |
+
continue
|
32 |
+
print('Getting landmarks...')
|
33 |
+
|
34 |
+
for i, face_landmarks in enumerate(landmarks_detector.get_landmarks(raw_img_path), start=1):
|
35 |
+
try:
|
36 |
+
print('Starting face alignment...')
|
37 |
+
face_img_name = '%s_%02d.png' % (os.path.splitext(img_name)[0], i)
|
38 |
+
aligned_face_path = os.path.join(ALIGNED_IMAGES_DIR, face_img_name)
|
39 |
+
image_align(raw_img_path, aligned_face_path, face_landmarks, output_size=output_size, x_scale=x_scale, y_scale=y_scale, em_scale=em_scale, alpha=use_alpha)
|
40 |
+
print('Wrote result %s\n' % aligned_face_path)
|
41 |
+
except Exception as e:
|
42 |
+
raise Exception("Exception in face alignment!", e)
|
43 |
+
except:
|
44 |
+
raise Exception("Exception in landmark detection!")
|
45 |
+
|
46 |
+
# return absolute paths of aligned images
|
47 |
+
return [os.path.join(ALIGNED_IMAGES_DIR, img) for img in os.listdir(ALIGNED_IMAGES_DIR)]
|
src/utils/face_alignment.py
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import scipy.ndimage
|
3 |
+
import os
|
4 |
+
import PIL.Image
|
5 |
+
from PIL import Image
|
6 |
+
|
7 |
+
|
8 |
+
def image_align(src_file, dst_file, face_landmarks, output_size=1024, transform_size=4096, enable_padding=True, x_scale=1, y_scale=1, em_scale=0.1, alpha=False):
|
9 |
+
# Align function from FFHQ dataset pre-processing step
|
10 |
+
# https://github.com/NVlabs/ffhq-dataset/blob/master/download_ffhq.py
|
11 |
+
|
12 |
+
lm = np.array(face_landmarks)
|
13 |
+
lm_chin = lm[0 : 17] # left-right
|
14 |
+
lm_eyebrow_left = lm[17 : 22] # left-right
|
15 |
+
lm_eyebrow_right = lm[22 : 27] # left-right
|
16 |
+
lm_nose = lm[27 : 31] # top-down
|
17 |
+
lm_nostrils = lm[31 : 36] # top-down
|
18 |
+
lm_eye_left = lm[36 : 42] # left-clockwise
|
19 |
+
lm_eye_right = lm[42 : 48] # left-clockwise
|
20 |
+
lm_mouth_outer = lm[48 : 60] # left-clockwise
|
21 |
+
lm_mouth_inner = lm[60 : 68] # left-clockwise
|
22 |
+
|
23 |
+
# Calculate auxiliary vectors.
|
24 |
+
eye_left = np.mean(lm_eye_left, axis=0)
|
25 |
+
eye_right = np.mean(lm_eye_right, axis=0)
|
26 |
+
eye_avg = (eye_left + eye_right) * 0.5
|
27 |
+
eye_to_eye = eye_right - eye_left
|
28 |
+
mouth_left = lm_mouth_outer[0]
|
29 |
+
mouth_right = lm_mouth_outer[6]
|
30 |
+
mouth_avg = (mouth_left + mouth_right) * 0.5
|
31 |
+
eye_to_mouth = mouth_avg - eye_avg
|
32 |
+
|
33 |
+
# Choose oriented crop rectangle.
|
34 |
+
x = eye_to_eye - np.flipud(eye_to_mouth) * [-1, 1]
|
35 |
+
x /= np.hypot(*x)
|
36 |
+
x *= max(np.hypot(*eye_to_eye) * 2.0, np.hypot(*eye_to_mouth) * 1.8)
|
37 |
+
x *= x_scale
|
38 |
+
y = np.flipud(x) * [-y_scale, y_scale]
|
39 |
+
c = eye_avg + eye_to_mouth * em_scale
|
40 |
+
quad = np.stack([c - x - y, c - x + y, c + x + y, c + x - y])
|
41 |
+
qsize = np.hypot(*x) * 2
|
42 |
+
|
43 |
+
# Load in-the-wild image.
|
44 |
+
if not os.path.isfile(src_file):
|
45 |
+
print('\nCannot find source image. Please run "--wilds" before "--align".')
|
46 |
+
return
|
47 |
+
img = PIL.Image.open(src_file).convert('RGBA').convert('RGB')
|
48 |
+
|
49 |
+
# Shrink.
|
50 |
+
shrink = int(np.floor(qsize / output_size * 0.5))
|
51 |
+
if shrink > 1:
|
52 |
+
rsize = (int(np.rint(float(img.size[0]) / shrink)), int(np.rint(float(img.size[1]) / shrink)))
|
53 |
+
img = img.resize(rsize, Image.Resampling.LANCZOS)
|
54 |
+
quad /= shrink
|
55 |
+
qsize /= shrink
|
56 |
+
|
57 |
+
# Crop.
|
58 |
+
border = max(int(np.rint(qsize * 0.1)), 3)
|
59 |
+
crop = (int(np.floor(min(quad[:,0]))), int(np.floor(min(quad[:,1]))), int(np.ceil(max(quad[:,0]))), int(np.ceil(max(quad[:,1]))))
|
60 |
+
crop = (max(crop[0] - border, 0), max(crop[1] - border, 0), min(crop[2] + border, img.size[0]), min(crop[3] + border, img.size[1]))
|
61 |
+
if crop[2] - crop[0] < img.size[0] or crop[3] - crop[1] < img.size[1]:
|
62 |
+
img = img.crop(crop)
|
63 |
+
quad -= crop[0:2]
|
64 |
+
|
65 |
+
# Pad.
|
66 |
+
pad = (int(np.floor(min(quad[:,0]))), int(np.floor(min(quad[:,1]))), int(np.ceil(max(quad[:,0]))), int(np.ceil(max(quad[:,1]))))
|
67 |
+
pad = (max(-pad[0] + border, 0), max(-pad[1] + border, 0), max(pad[2] - img.size[0] + border, 0), max(pad[3] - img.size[1] + border, 0))
|
68 |
+
if enable_padding and max(pad) > border - 4:
|
69 |
+
pad = np.maximum(pad, int(np.rint(qsize * 0.3)))
|
70 |
+
img = np.pad(np.float32(img), ((pad[1], pad[3]), (pad[0], pad[2]), (0, 0)), 'reflect')
|
71 |
+
h, w, _ = img.shape
|
72 |
+
y, x, _ = np.ogrid[:h, :w, :1]
|
73 |
+
mask = np.maximum(1.0 - np.minimum(np.float32(x) / pad[0], np.float32(w-1-x) / pad[2]), 1.0 - np.minimum(np.float32(y) / pad[1], np.float32(h-1-y) / pad[3]))
|
74 |
+
blur = qsize * 0.02
|
75 |
+
img += (scipy.ndimage.gaussian_filter(img, [blur, blur, 0]) - img) * np.clip(mask * 3.0 + 1.0, 0.0, 1.0)
|
76 |
+
img += (np.median(img, axis=(0,1)) - img) * np.clip(mask, 0.0, 1.0)
|
77 |
+
img = np.uint8(np.clip(np.rint(img), 0, 255))
|
78 |
+
if alpha:
|
79 |
+
mask = 1-np.clip(3.0 * mask, 0.0, 1.0)
|
80 |
+
mask = np.uint8(np.clip(np.rint(mask*255), 0, 255))
|
81 |
+
img = np.concatenate((img, mask), axis=2)
|
82 |
+
img = PIL.Image.fromarray(img, 'RGBA')
|
83 |
+
else:
|
84 |
+
img = PIL.Image.fromarray(img, 'RGB')
|
85 |
+
quad += pad[:2]
|
86 |
+
|
87 |
+
# Transform.
|
88 |
+
img = img.transform((transform_size, transform_size), PIL.Image.QUAD, (quad + 0.5).flatten(), PIL.Image.BILINEAR)
|
89 |
+
if output_size < transform_size:
|
90 |
+
img = img.resize((output_size, output_size), Image.Resampling.LANCZOS)
|
91 |
+
|
92 |
+
# Save aligned image.
|
93 |
+
img.save(dst_file, 'PNG')
|
src/utils/sort_images.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from transformers import pipeline
|
2 |
+
|
3 |
+
|
4 |
+
def sort_images(image_files):
|
5 |
+
pipe = pipeline("image-classification", model="Robys01/facial_age_estimator")
|
6 |
+
|
7 |
+
def get_age(image):
|
8 |
+
result = pipe(image)
|
9 |
+
print(image, "age:", result[0]["label"])
|
10 |
+
return result[0]["label"]
|
11 |
+
|
12 |
+
|
13 |
+
image_files.sort(key=get_age)
|
14 |
+
|
15 |
+
return image_files
|
16 |
+
|
17 |
+
if __name__ == "__main__":
|
18 |
+
image_files = ["examples/3.png", "examples/1.png", "examples/2.png"]
|
19 |
+
sorted_images = sort_images(image_files)
|
20 |
+
|
21 |
+
|
22 |
+
print(sorted_images)
|