Spaces:
Running
Running
Upload code
Browse files- examples/1.png +0 -0
- examples/2.png +0 -0
- examples/3.png +0 -0
- src/README.md +9 -0
- src/__init__.py +4 -0
- src/face_morp.py +155 -0
- src/landmark_detector.py +109 -0
- src/process_images.py +43 -0
examples/1.png
ADDED
![]() |
examples/2.png
ADDED
![]() |
examples/3.png
ADDED
![]() |
src/README.md
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Requirements
|
2 |
+
```
|
3 |
+
python=3.11.9
|
4 |
+
mediapipe
|
5 |
+
|
6 |
+
```
|
7 |
+
```
|
8 |
+
python mycode/main.py mycode/input_aligned --frames 30 --duration 3 --verbose
|
9 |
+
```
|
src/__init__.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
|
3 |
+
if __name__ == '__main__':
|
4 |
+
print("This is a placeholder for the main function")
|
src/face_morp.py
ADDED
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import time
|
3 |
+
import numpy as np
|
4 |
+
from tqdm import tqdm
|
5 |
+
from scipy.spatial import Delaunay
|
6 |
+
from concurrent.futures import ProcessPoolExecutor
|
7 |
+
|
8 |
+
from src.process_images import get_images_and_landmarks
|
9 |
+
|
10 |
+
def morph(image_files, duration, frame_rate, output, guideline, is_dlib):
|
11 |
+
# Get the list of images and landmarks
|
12 |
+
images_list, landmarks_list = get_images_and_landmarks(image_files, is_dlib)
|
13 |
+
|
14 |
+
video_frames = [] # List of frames for the video
|
15 |
+
|
16 |
+
sequence_time = time.time()
|
17 |
+
print("Generating morph sequence...", end="\n\n")
|
18 |
+
|
19 |
+
# Use ProcessPoolExecutor to parallelize the generation of morph sequences
|
20 |
+
with ProcessPoolExecutor() as executor:
|
21 |
+
futures = []
|
22 |
+
for i in range(1, len(images_list)):
|
23 |
+
src_image, src_landmarks = images_list[i-1], landmarks_list[i-1]
|
24 |
+
dst_image, dst_landmarks = images_list[i], landmarks_list[i]
|
25 |
+
|
26 |
+
# Generate Delaunay Triangulation
|
27 |
+
tri = Delaunay(dst_landmarks).simplices
|
28 |
+
|
29 |
+
# Submit the task to the executor
|
30 |
+
futures.append((i, executor.submit(generate_morph_sequence, duration, frame_rate, src_image, dst_image, src_landmarks, dst_landmarks, tri, guideline)))
|
31 |
+
|
32 |
+
# Retrieve and store the results in the correct order
|
33 |
+
results = [None] * (len(images_list) - 1)
|
34 |
+
for idx, future in futures:
|
35 |
+
results[idx - 1] = future.result()
|
36 |
+
|
37 |
+
for sequence_frames in results:
|
38 |
+
video_frames.extend(sequence_frames)
|
39 |
+
|
40 |
+
|
41 |
+
print(f"Total time taken to generate morph sequence: {time.time() - sequence_time:.2f} seconds", end="\n\n")
|
42 |
+
|
43 |
+
# Write the frames to a video file
|
44 |
+
write_frames_to_video(video_frames, frame_rate, output)
|
45 |
+
|
46 |
+
|
47 |
+
def generate_morph_sequence(duration, frame_rate, image1, image2, landmarks1, landmarks2, tri, guideline):
|
48 |
+
num_frames = int(duration * frame_rate)
|
49 |
+
morphed_frames = []
|
50 |
+
|
51 |
+
for frame in range(num_frames):
|
52 |
+
alpha = frame / (num_frames - 1)
|
53 |
+
|
54 |
+
# Working with floats for better precision
|
55 |
+
image1_float = np.float32(image1)
|
56 |
+
image2_float = np.float32(image2)
|
57 |
+
|
58 |
+
# Compute the intermediate landmarks at time alpha
|
59 |
+
landmarks = []
|
60 |
+
for i in range(len(landmarks1)):
|
61 |
+
x = (1 - alpha) * landmarks1[i][0] + alpha * landmarks2[i][0]
|
62 |
+
y = (1 - alpha) * landmarks1[i][1] + alpha * landmarks2[i][1]
|
63 |
+
landmarks.append((x, y))
|
64 |
+
|
65 |
+
# Allocate space for final output
|
66 |
+
morphed_frame = np.zeros_like(image1_float)
|
67 |
+
|
68 |
+
for i in range(len(tri)):
|
69 |
+
x = tri[i][0]
|
70 |
+
y = tri[i][1]
|
71 |
+
z = tri[i][2]
|
72 |
+
|
73 |
+
t1 = [landmarks1[x], landmarks1[y], landmarks1[z]]
|
74 |
+
t2 = [landmarks2[x], landmarks2[y], landmarks2[z]]
|
75 |
+
t = [landmarks[x], landmarks[y], landmarks[z]]
|
76 |
+
|
77 |
+
# Morph one triangle at a time.
|
78 |
+
morph_triangle(image1_float, image2_float, morphed_frame, t1, t2, t, alpha)
|
79 |
+
|
80 |
+
if guideline:
|
81 |
+
# Draw lines for the face landmarks
|
82 |
+
points = [(int(t[i][0]), int(t[i][1])) for i in range(3)]
|
83 |
+
for i in range(3):
|
84 |
+
# image, (x1, y1), (x2, y2), color, thickness, lineType, shift
|
85 |
+
cv2.line(morphed_frame, points[i], points[(i + 1) % 3], (255, 255, 255), 1, 8, 0)
|
86 |
+
|
87 |
+
# Convert the morphed image to RGB color space (from BGR)
|
88 |
+
morphed_frame = cv2.cvtColor(np.uint8(morphed_frame), cv2.COLOR_BGR2RGB)
|
89 |
+
|
90 |
+
morphed_frames.append(morphed_frame)
|
91 |
+
|
92 |
+
return morphed_frames
|
93 |
+
|
94 |
+
|
95 |
+
def morph_triangle(image1, image2, morphed_image, t1, t2, t, alpha):
|
96 |
+
# Calculate bounding rectangles and offset points together
|
97 |
+
r, r1, r2 = [cv2.boundingRect(np.float32([tri])) for tri in [t, t1, t2]]
|
98 |
+
|
99 |
+
# Offset the triangle points by the top-left corner of the corresponding bounding rectangle
|
100 |
+
t_rect, t1_rect, t2_rect = [[(tri[i][0] - rect[0], tri[i][1] - rect[1]) for i in range(3)]
|
101 |
+
for tri, rect in zip([t, t1, t2], [r, r1, r2])]
|
102 |
+
|
103 |
+
# Create a mask to keep only the pixels inside the triangle
|
104 |
+
mask = np.zeros((r[3], r[2], 3), dtype=np.float32)
|
105 |
+
# Fill the mask with white pixels inside the triangle
|
106 |
+
cv2.fillConvexPoly(mask, np.int32(t_rect), (1.0, 1.0, 1.0), 16, 0)
|
107 |
+
|
108 |
+
# Extract the triangle from the first and second image
|
109 |
+
image1_rect = image1[r1[1]:r1[1]+r1[3], r1[0]:r1[0]+r1[2]]
|
110 |
+
image2_rect = image2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]]
|
111 |
+
|
112 |
+
size = (r[2], r[3]) # Size of the bounding rectangle
|
113 |
+
# Apply affine transformation to warp the triangles from the source image to the destination image
|
114 |
+
warpImage1 = apply_affine_transform(image1_rect, t1_rect, t_rect, size)
|
115 |
+
warpImage2 = apply_affine_transform(image2_rect, t2_rect, t_rect, size)
|
116 |
+
|
117 |
+
# Perform alpha blending between the warped triangles and copy the result to the destination image
|
118 |
+
morphed_image_rect = warpImage1 * (1 - alpha) + warpImage2 * alpha
|
119 |
+
morphed_image[r[1]:r[1]+r[3], r[0]:r[0]+r[2]] = morphed_image[r[1]:r[1]+r[3], r[0]:r[0]+r[2]] * (1 - mask) + morphed_image_rect * mask
|
120 |
+
|
121 |
+
return morphed_image
|
122 |
+
|
123 |
+
|
124 |
+
def apply_affine_transform(img, src, dst, size):
|
125 |
+
"""
|
126 |
+
Apply an affine transformation to the image.
|
127 |
+
"""
|
128 |
+
warp_matrix = cv2.getAffineTransform(np.float32(src), np.float32(dst))
|
129 |
+
return cv2.warpAffine(img, warp_matrix, (size[0], size[1]), None, flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
130 |
+
|
131 |
+
|
132 |
+
def write_frames_to_video(frames, frame_rate, output):
|
133 |
+
# Get the height and width of the frames
|
134 |
+
height, width, _ = frames[0].shape
|
135 |
+
|
136 |
+
# Cut the outside pixels to remove the black border
|
137 |
+
pad = 2
|
138 |
+
new_height = height - pad * 2
|
139 |
+
new_width = width - pad * 2
|
140 |
+
|
141 |
+
|
142 |
+
# Initialize the video writer
|
143 |
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
144 |
+
out = cv2.VideoWriter(output, fourcc, frame_rate, (new_width, new_height))
|
145 |
+
|
146 |
+
# Write the frames to the video
|
147 |
+
print("Writing frames to video...")
|
148 |
+
for frame in tqdm(frames):
|
149 |
+
# Cut the outside pixels
|
150 |
+
cut_frame = frame[pad:new_height+pad, pad:new_width+pad]
|
151 |
+
|
152 |
+
out.write(cut_frame)
|
153 |
+
|
154 |
+
out.release()
|
155 |
+
print(f"Video saved at: {output}")
|
src/landmark_detector.py
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import cv2
|
3 |
+
import dlib
|
4 |
+
import mediapipe as mp
|
5 |
+
|
6 |
+
def read_image(image_path):
|
7 |
+
"""
|
8 |
+
Read an image from the given path and convert it to RGB.
|
9 |
+
"""
|
10 |
+
image = cv2.imread(image_path)
|
11 |
+
if image is None:
|
12 |
+
raise FileNotFoundError(f"Image not found at path: {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'):
|
19 |
+
"""
|
20 |
+
:param predictor_model_path: path to shape_predictor_68_face_landmarks.dat file
|
21 |
+
"""
|
22 |
+
self.detector = dlib.get_frontal_face_detector() # cnn_face_detection_model_v1 also can be used
|
23 |
+
self.predictor = dlib.shape_predictor(predictor_model_path)
|
24 |
+
|
25 |
+
def get_landmarks(self, image_path):
|
26 |
+
# image = dlib.load_rgb_image(image_path)
|
27 |
+
image = read_image(image_path)
|
28 |
+
height, width, _ = image.shape
|
29 |
+
|
30 |
+
# Detect the faces in the image
|
31 |
+
dets = self.detector(image, 1) # 1 indicates to upsample the image 1 time. Higher values may give better results
|
32 |
+
|
33 |
+
# Raise an exception if no face is detected
|
34 |
+
if len(dets) == 0:
|
35 |
+
raise Exception("No face detected in the image at path: ", image_path)
|
36 |
+
|
37 |
+
# Get the landmarks of the first face detected
|
38 |
+
face_landmarks = [(item.x, item.y) for item in self.predictor(image, dets[0]).parts()]
|
39 |
+
|
40 |
+
# Add corner and edge midpoints as landmarks to include the background
|
41 |
+
corner_landmarks = [(1, 1), (1, height - 1), (width - 1, 1), (width - 1, height - 1)]
|
42 |
+
edge_landmarks = [(1, (height - 1)//2), ((width - 1)//2, 1), ((width - 1)//2, height - 1), (width - 1, (height - 1)//2)]
|
43 |
+
|
44 |
+
# Concatenate the landmarks
|
45 |
+
landmarks = face_landmarks + corner_landmarks + edge_landmarks
|
46 |
+
|
47 |
+
return landmarks, image
|
48 |
+
|
49 |
+
def show_landmarked_image(self, image_path, landmarks):
|
50 |
+
image = read_image(image_path)
|
51 |
+
|
52 |
+
for landmark in landmarks:
|
53 |
+
x, y = landmark
|
54 |
+
cv2.circle(image, (x, y), 1, (255, 255, 0), -1) # image, (x, y), radius, color, thickness (-1 to fill)
|
55 |
+
|
56 |
+
cv2.imshow('image', image)
|
57 |
+
cv2.waitKey(0)
|
58 |
+
cv2.destroyAllWindows()
|
59 |
+
|
60 |
+
|
61 |
+
class MediaPipeLandmarkDetector:
|
62 |
+
def __init__(self):
|
63 |
+
|
64 |
+
self.face_mesh = mp.solutions.face_mesh.FaceMesh(
|
65 |
+
static_image_mode=True,
|
66 |
+
max_num_faces=1,
|
67 |
+
min_detection_confidence=0.5)
|
68 |
+
|
69 |
+
|
70 |
+
def get_landmarks(self, image_path):
|
71 |
+
|
72 |
+
image = read_image(image_path)
|
73 |
+
height, width, _ = image.shape
|
74 |
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
75 |
+
|
76 |
+
# Process the image
|
77 |
+
results = self.face_mesh.process(image_rgb)
|
78 |
+
|
79 |
+
# Raise an exception if no face is detected
|
80 |
+
if results.multi_face_landmarks is None:
|
81 |
+
raise Exception("No face detected in the image at path: ", image_path)
|
82 |
+
|
83 |
+
# Extract the face landmarks
|
84 |
+
face_landmarks = results.multi_face_landmarks[0]
|
85 |
+
face_landmarks_normalized = [[landmark.x , landmark.y] for landmark in face_landmarks.landmark]
|
86 |
+
|
87 |
+
# Add corner and edge midpoints as landmarks to include the background
|
88 |
+
corner_landmarks = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
89 |
+
edge_landmarks = [(0, 0.5), (0.5, 0), (0.5, 1), (1, 0.5)]
|
90 |
+
|
91 |
+
# Concatenate the corner and edge landmarks
|
92 |
+
landmarks = corner_landmarks + edge_landmarks + face_landmarks_normalized
|
93 |
+
|
94 |
+
# Multiply the landmarks with the image dimensions
|
95 |
+
landmarks = [(int(x * width) - 1, int(y * height) - 1) for x, y in landmarks]
|
96 |
+
landmarks = [(max(1, x), max(1, y)) for x, y in landmarks]
|
97 |
+
|
98 |
+
return landmarks, image
|
99 |
+
|
100 |
+
def show_landmarked_image(self, image_path, landmarks):
|
101 |
+
image = cv2.imread(image_path)
|
102 |
+
|
103 |
+
for landmark in landmarks:
|
104 |
+
x, y = landmark
|
105 |
+
cv2.circle(image, (x, y), 1, (255, 255, 0), -1)
|
106 |
+
|
107 |
+
cv2.imshow('image', image)
|
108 |
+
cv2.waitKey(0)
|
109 |
+
cv2.destroyAllWindows()
|
src/process_images.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from tqdm import tqdm
|
3 |
+
|
4 |
+
from src.landmark_detector import MediaPipeLandmarkDetector, DlibLandmarkDetector
|
5 |
+
|
6 |
+
|
7 |
+
def get_images_and_landmarks(image_list, is_dlib):
|
8 |
+
|
9 |
+
# Get the list of images in the directory
|
10 |
+
image_paths = []
|
11 |
+
for file in image_list:
|
12 |
+
if file.endswith(".jpg") or file.endswith(".png"):
|
13 |
+
image_paths.append(file)
|
14 |
+
else:
|
15 |
+
print(f"Skipping file: {file}. Not a supported image format. (jpg or png)")
|
16 |
+
|
17 |
+
if len(image_paths) < 2:
|
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 |
+
|
24 |
+
# Initialize the landmark detector
|
25 |
+
landmark_detector = DlibLandmarkDetector() if is_dlib else MediaPipeLandmarkDetector()
|
26 |
+
|
27 |
+
print("Generating landmarks for the images...")
|
28 |
+
|
29 |
+
# Detect landmarks for each image
|
30 |
+
for image_path in tqdm(image_paths):
|
31 |
+
try:
|
32 |
+
landmarks, image = landmark_detector.get_landmarks(image_path)
|
33 |
+
landmarks_list.append(landmarks)
|
34 |
+
images_list.append(image)
|
35 |
+
except Exception as e:
|
36 |
+
print(f"{e} \nSkipping image: {image_path}\n")
|
37 |
+
continue
|
38 |
+
|
39 |
+
if len(landmarks_list) < 2:
|
40 |
+
raise ValueError("At least two faces are required for morphing.")
|
41 |
+
# exit()
|
42 |
+
|
43 |
+
return images_list, landmarks_list
|