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
|