It's finally doing it but the output video is very 8bit looking, it's a ranging in colors but it making it appear to be in lines. I want it to look smooth, very cool is what I'd like.. Here's the script talk to me before u rewrite it. import numpy as np
import os
import time
from PIL import Image
from moviepy.editor import VideoClip, ImageSequenceClip
from moviepy.video.fx.all import fadein, fadeout
from tqdm import tqdm
from multiprocessing import Pool
from functools import partial


# Paths
image_one_path = '/home/daddy/morph/one.jpeg'
image_two_path = '/home/daddy/morph/two.jpeg'
output_path = '/home/daddy/morph/output'
output_filename = f'morph{int(time.time())}.mp4'


# Function to create each frame of the video
def make_frame(t, img1_array, img2_array, particle_positions, explosion_offsets,
               particle_to_target_position, duration1, duration2, duration3,
               width, height, particle_size):
    if t < duration1:
        # Display the first image
        frame_array = img1_array.copy()
    elif t < duration1 + duration2:
        # Morphing effect with particle explosion and implosion
        progress = (t - duration1) / duration2  # Normalize progress to [0, 1]

        # Create an empty frame with transparency
        frame_array = np.zeros_like(img1_array)

        for idx, (x, y) in enumerate(particle_positions):
            # Extract the patch from the first image
            patch = img1_array[y:y + particle_size, x:x + particle_size].copy()

            # Explosion offsets
            offset_x, offset_y = explosion_offsets[idx]

            if progress < 0.5:
                # Explosion phase
                explosion_progress = progress * 2  # Scale progress to [0, 1]
                current_x = x + explosion_progress * offset_x
                current_y = y + explosion_progress * offset_y
            else:
                # Implosion phase
                implosion_progress = (progress - 0.5) * 2  # Scale progress to [0, 1]

                # Target positions in the second image
                target_x, target_y = particle_to_target_position.get((x, y), (x, y))

                # Current position moves from exploded position to target position
                current_x = (x + offset_x) + implosion_progress * (target_x - (x + offset_x))
                current_y = (y + offset_y) + implosion_progress * (target_y - (y + offset_y))

            # Convert positions to integers and ensure they stay within frame boundaries
            current_x = int(np.clip(current_x, 0, width - particle_size))
            current_y = int(np.clip(current_y, 0, height - particle_size))

            # Place the current patch onto the frame
            frame_array[current_y:current_y + particle_size, current_x:current_x + particle_size] = patch

    else:
        # Hold the last frame where particles have formed the letters
        frame_array = np.zeros_like(img1_array)
        for idx, (x, y) in enumerate(particle_positions):
            # Extract the patch from the first image
            patch = img1_array[y:y + particle_size, x:x + particle_size].copy()
            # Target positions in the second image
            target_x, target_y = particle_to_target_position.get((x, y), (x, y))
            # Ensure positions are within frame boundaries
            current_x = int(np.clip(target_x, 0, width - particle_size))
            current_y = int(np.clip(target_y, 0, height - particle_size))
            # Place the patch at the target position
            frame_array[current_y:current_y + particle_size, current_x:current_x + particle_size] = patch

    # Convert the NumPy array back to an image
    frame_image = Image.fromarray(frame_array, 'RGBA')
    # Return frame as an RGB image (MoviePy expects RGB format)
    return np.array(frame_image.convert('RGB'))


# Function for processing a range of frames (used in multiprocessing)
def process_frame_range(frame_numbers, img1_array, img2_array, particle_positions, explosion_offsets,
                        particle_to_target_position, duration1, duration2, duration3,
                        width, height, particle_size, fps, temp_folder):
    for frame_number in frame_numbers:
        t = frame_number / fps
        frame = make_frame(t, img1_array, img2_array, particle_positions, explosion_offsets,
                           particle_to_target_position, duration1, duration2, duration3,
                           width, height, particle_size)

        # Save frame to disk
        frame_image = Image.fromarray(frame)
        frame_path = os.path.join(temp_folder, f"frame_{frame_number:05d}.jpg")
        frame_image.save(frame_path, format='JPEG', quality=95)  # Adjust quality as needed


def generate_video_from_frames(total_frames, fps, temp_folder, output_video_path):
    frame_paths = [os.path.join(temp_folder, f"frame_{i:05d}.jpg") for i in range(total_frames)]

    # Use MoviePy to create the video from frames
    clip = ImageSequenceClip(frame_paths, fps=fps)

    # Apply fade-in and fade-out effects
    clip = fadein(clip, 1.5).fx(fadeout, 1.5)

    # Write the video file
    clip.write_videofile(output_video_path, codec='libx264', audio=False)


# Main program
if __name__ == "__main__":
    # Input parameters with default values
    try:
        duration1 = int(input("Enter duration for first image display (seconds, default 5): ") or 5)
        duration2 = int(input("Enter duration for morphing effect (seconds, default 5): ") or 5)
        duration3 = int(input("Enter duration for final image hold (seconds, default 5): ") or 5)
        particle_size = int(input("Enter particle size (pixels, default 4): ") or 4)
        target_threshold = int(input("Enter threshold for target pixel detection (0-765, default 100): ") or 100)
    except ValueError:
        print("Invalid input. Please enter integer values.")
        exit(1)

    # Load images and ensure they are in RGBA format
    img1 = Image.open(image_one_path).convert('RGBA').resize((1024, 1024))
    img2 = Image.open(image_two_path).convert('RGBA').resize((1024, 1024))

    # Convert images to NumPy arrays
    img1_array = np.array(img1)
    img2_array = np.array(img2)

    # Get image dimensions
    width, height = img1.size

    # Generate particle positions
    particle_positions = [(x, y) for y in range(0, height - particle_size + 1, particle_size)
                          for x in range(0, width - particle_size + 1, particle_size)]

    # Prepare explosion offsets
    explosion_offsets = [(np.random.uniform(-width, width), np.random.uniform(-height, height))
                         for _ in particle_positions]

    # Detect target pixels in the second image (e.g., dark pixels)
    target_pixel_positions = []
    for y in range(0, height - particle_size + 1, particle_size):
        for x in range(0, width - particle_size + 1, particle_size):
            patch = img2_array[y:y + particle_size, x:x + particle_size, :3]
            patch_sum = patch.sum()
            if patch_sum < target_threshold * particle_size * particle_size * 3:
                target_pixel_positions.append((x, y))

    # Map particles to target pixels
    particle_to_target_position = {}
    num_particles = len(particle_positions)
    num_targets = len(target_pixel_positions)

    if num_targets > 0:
        if num_particles <= num_targets:
            selected_targets = np.random.choice(num_targets, num_particles, replace=False)
            for idx, particle_pos in enumerate(particle_positions):
                target_pos = target_pixel_positions[selected_targets[idx]]
                particle_to_target_position[particle_pos] = target_pos
        else:
            repeats = num_particles // num_targets
            remainder = num_particles % num_targets
            target_positions_extended = target_pixel_positions * repeats + target_pixel_positions[:remainder]
            for idx, particle_pos in enumerate(particle_positions):
                target_pos = target_positions_extended[idx]
                particle_to_target_position[particle_pos] = target_pos
    else:
        for particle_pos in particle_positions:
            particle_to_target_position[particle_pos] = particle_pos

    total_duration = duration1 + duration2 + duration3
    fps = 24  # Frames per second
    total_frames = int(total_duration * fps)

    # Create a temporary folder for frames
    temp_folder = os.path.join(output_path, "temp_frames")
    os.makedirs(temp_folder, exist_ok=True)

    # Prepare frame numbers
    frame_numbers = list(range(total_frames))

    # Divide frame numbers into chunks for multiprocessing
    num_processes = 2  # Adjust based on your device's capabilities
    chunk_size = len(frame_numbers) // num_processes
    frame_chunks = [frame_numbers[i:i + chunk_size] for i in range(0, len(frame_numbers), chunk_size)]

    # Partial function with fixed arguments
    process_frame_range_partial = partial(
        process_frame_range,
        img1_array=img1_array,
        img2_array=img2_array,
        particle_positions=particle_positions,
        explosion_offsets=explosion_offsets,
        particle_to_target_position=particle_to_target_position,
        duration1=duration1,
        duration2=duration2,
        duration3=duration3,
        width=width,
        height=height,
        particle_size=particle_size,
        fps=fps,
        temp_folder=temp_folder
    )

    # Process frames in parallel
    with Pool(num_processes) as pool:
        list(tqdm(pool.imap(process_frame_range_partial, frame_chunks), total=len(frame_chunks)))

    # Generate the final video from the processed frames
    generate_video_from_frames(total_frames, fps, temp_folder, os.path.join(output_path, output_filename))

    # Clean up temporary frames
    for frame_file in os.listdir(temp_folder):
        os.remove(os.path.join(temp_folder, frame_file))
    os.rmdir(temp_folder)

    print(f"Video saved to {os.path.join(output_path, output_filename)}")