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}")