Robys01 commited on
Commit
cf27ba5
·
verified ·
1 Parent(s): ad14814

Upload code

Browse files
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