File size: 6,316 Bytes
cf27ba5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import cv2
import time
import numpy as np
from tqdm import tqdm
from scipy.spatial import Delaunay
from concurrent.futures import ProcessPoolExecutor

from src.process_images import get_images_and_landmarks

def morph(image_files, duration, frame_rate, output, guideline, is_dlib):
    # Get the list of images and landmarks
    images_list, landmarks_list = get_images_and_landmarks(image_files, is_dlib)

    video_frames = []   # List of frames for the video

    sequence_time = time.time()
    print("Generating morph sequence...", end="\n\n")

    # Use ProcessPoolExecutor to parallelize the generation of morph sequences
    with ProcessPoolExecutor() as executor:
        futures = []
        for i in range(1, len(images_list)):
            src_image, src_landmarks = images_list[i-1], landmarks_list[i-1]
            dst_image, dst_landmarks = images_list[i], landmarks_list[i]

            # Generate Delaunay Triangulation
            tri = Delaunay(dst_landmarks).simplices

            # Submit the task to the executor
            futures.append((i, executor.submit(generate_morph_sequence, duration, frame_rate, src_image, dst_image, src_landmarks, dst_landmarks, tri, guideline)))

        # Retrieve and store the results in the correct order
        results = [None] * (len(images_list) - 1)
        for idx, future in futures:
            results[idx - 1] = future.result()

        for sequence_frames in results:
            video_frames.extend(sequence_frames)


    print(f"Total time taken to generate morph sequence: {time.time() - sequence_time:.2f} seconds", end="\n\n")

    # Write the frames to a video file
    write_frames_to_video(video_frames, frame_rate, output)


def generate_morph_sequence(duration, frame_rate, image1, image2, landmarks1, landmarks2, tri, guideline):
    num_frames = int(duration * frame_rate)
    morphed_frames = []

    for frame in range(num_frames):
        alpha = frame / (num_frames - 1)

        # Working with floats for better precision
        image1_float = np.float32(image1)
        image2_float = np.float32(image2)

        # Compute the intermediate landmarks at time alpha
        landmarks = []
        for i in range(len(landmarks1)):
            x = (1 - alpha) * landmarks1[i][0] + alpha * landmarks2[i][0]
            y = (1 - alpha) * landmarks1[i][1] + alpha * landmarks2[i][1]
            landmarks.append((x, y))

        # Allocate space for final output
        morphed_frame = np.zeros_like(image1_float)

        for i in range(len(tri)):
            x = tri[i][0]
            y = tri[i][1]
            z = tri[i][2]

            t1 = [landmarks1[x], landmarks1[y], landmarks1[z]]
            t2 = [landmarks2[x], landmarks2[y], landmarks2[z]]
            t = [landmarks[x], landmarks[y], landmarks[z]]

            # Morph one triangle at a time.
            morph_triangle(image1_float, image2_float, morphed_frame, t1, t2, t, alpha)

            if guideline:
                # Draw lines for the face landmarks
                points = [(int(t[i][0]), int(t[i][1])) for i in range(3)]
                for i in range(3):
                    # image, (x1, y1), (x2, y2), color, thickness, lineType, shift
                    cv2.line(morphed_frame, points[i], points[(i + 1) % 3], (255, 255, 255), 1, 8, 0)
            
        # Convert the morphed image to RGB color space (from BGR)
        morphed_frame = cv2.cvtColor(np.uint8(morphed_frame), cv2.COLOR_BGR2RGB)

        morphed_frames.append(morphed_frame)

    return morphed_frames


def morph_triangle(image1, image2, morphed_image, t1, t2, t, alpha):
    # Calculate bounding rectangles and offset points together
    r, r1, r2 = [cv2.boundingRect(np.float32([tri])) for tri in [t, t1, t2]]

    # Offset the triangle points by the top-left corner of the corresponding bounding rectangle
    t_rect, t1_rect, t2_rect = [[(tri[i][0] - rect[0], tri[i][1] - rect[1]) for i in range(3)] 
                                    for tri, rect in zip([t, t1, t2], [r, r1, r2])]
    
    # Create a mask to keep only the pixels inside the triangle
    mask = np.zeros((r[3], r[2], 3), dtype=np.float32)
    # Fill the mask with white pixels inside the triangle
    cv2.fillConvexPoly(mask, np.int32(t_rect), (1.0, 1.0, 1.0), 16, 0)

    # Extract the triangle from the first and second image
    image1_rect = image1[r1[1]:r1[1]+r1[3], r1[0]:r1[0]+r1[2]]
    image2_rect = image2[r2[1]:r2[1]+r2[3], r2[0]:r2[0]+r2[2]]

    size = (r[2], r[3]) # Size of the bounding rectangle
    # Apply affine transformation to warp the triangles from the source image to the destination image
    warpImage1 = apply_affine_transform(image1_rect, t1_rect, t_rect, size)
    warpImage2 = apply_affine_transform(image2_rect, t2_rect, t_rect, size)

    # Perform alpha blending between the warped triangles and copy the result to the destination image
    morphed_image_rect = warpImage1 * (1 - alpha) + warpImage2 * alpha
    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

    return morphed_image


def apply_affine_transform(img, src, dst, size):
    """

    Apply an affine transformation to the image.

    """
    warp_matrix = cv2.getAffineTransform(np.float32(src), np.float32(dst))
    return cv2.warpAffine(img, warp_matrix, (size[0], size[1]), None, flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)


def write_frames_to_video(frames, frame_rate, output):
    # Get the height and width of the frames
    height, width, _ = frames[0].shape

    # Cut the outside pixels to remove the black border
    pad = 2
    new_height = height - pad * 2
    new_width = width - pad * 2


    # Initialize the video writer
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    out = cv2.VideoWriter(output, fourcc, frame_rate, (new_width, new_height))

    # Write the frames to the video
    print("Writing frames to video...")
    for frame in tqdm(frames):
        # Cut the outside pixels
        cut_frame = frame[pad:new_height+pad, pad:new_width+pad]

        out.write(cut_frame)

    out.release()
    print(f"Video saved at: {output}")