From 232242bfdf3ec7f0b2b938f76854703b29cf3915 Mon Sep 17 00:00:00 2001 From: Phinehas Beresford <78656905+Cracko298@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:33:16 -0400 Subject: [PATCH 1/6] Delete python/cli.py --- python/cli.py | 218 -------------------------------------------------- 1 file changed, 218 deletions(-) delete mode 100644 python/cli.py diff --git a/python/cli.py b/python/cli.py deleted file mode 100644 index 3902a89..0000000 --- a/python/cli.py +++ /dev/null @@ -1,218 +0,0 @@ -import time -import warnings -import sys -import os -import cv2 - -warnings.simplefilter("ignore") -os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" -import pygame - -import min_video - -RECOMMENDED_WIDTH = 128 -RECOMMENDED_HEIGHT = 96 - -def get_min_video_from_mp4(input_path: str, width: int = -1, height: int = -1) -> min_video.Video: - cap = cv2.VideoCapture(input_path) - - assert cap.isOpened(), "Failed to open: " + input_path - - w = int(cap.get(3)) - h = int(cap.get(4)) - - if width != -1: - w = width - - if height != -1: - h = height - - vid = min_video.Video(w, h) - - total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - progress_width = 50 - - print("Processing frames:") - - start_time = time.time() # Start time for calculating elapsed time - - for i in range(total_frames): - ret, frame = cap.read() - - if not ret: - break - - if width != -1 and height != -1: - frame = cv2.resize(frame, (width, height)) - - m_frame = min_video.Frame(w, h) - - for y in range(h): - for x in range(w): - r = frame[y, x, 2] - g = frame[y, x, 1] - b = frame[y, x, 0] - - m_frame.set_color(x, y, r, g, b) - - vid.add_frame(m_frame) - - progress = int((i / total_frames) * progress_width) - percent = int((i / total_frames) * 100) - if percent == 99: - percent = 100 - - elapsed_time = time.time() - start_time - estimated_time = (elapsed_time / (i + 1)) * (total_frames - i - 1) - - print('\r', '[' + '#' * progress + '-' * (progress_width - progress) + f'] {percent}% ' - f'| Estimated: {estimated_time:.2f}s', end='') - - print('\n') - return vid - - - -def show_help(): - print("MinVideo command line tool (Python version)") - print("Source: https://github.com/Wolfyxon/MinVideo") - print() - print("Available options:") - print(" help : Shows this text") - print(" play : Plays video in the MinVideo") - print(" parse : Shows video data") - print(" convert [output file] [width] [height]: Converts a MP4 video to the MinVideo format.") - print() - print("NOTE: Videos are resized to 128x96 as it's a optimal size. Use -1 -1 to use the original size.") - -def parse_option(): - if len(sys.argv) < 3: - print("Video path is required") - exit(1) - - path = sys.argv[2] - - print("Reading: " + path + "...") - - with open(path, "rb") as file: - data = file.read() - - w = min_video.Video.get_width_from_data(data) - h = min_video.Video.get_height_from_data(data) - frames = min_video.Video.get_frame_amount_from_data(data) - - print("Size: " + str(w) + "x" + str(h)) - print("Frames: " + str(frames)) - -def convert_option(): - if len(sys.argv) < 3: - print("At least 1 argument is required: convert [output file] [width] [height]") - exit(1) - - w = RECOMMENDED_WIDTH - h = RECOMMENDED_HEIGHT - - if len(sys.argv) >= 5: - w = int(sys.argv[4]) - - if len(sys.argv) >= 6: - h = int(sys.argv[5]) - - in_path = sys.argv[2] - out_path = "" - - if len(sys.argv) > 3: - out_path = sys.argv[3] - else: - file, ext = os.path.splitext(in_path) - out_path = file + ".minv" - - print("Converting standard video: " + in_path) - print("To MinVideo: " + out_path) - print("Using size: " + str(w) + "x" + str(h) + "\n") - - tm = time.time() - - vid = get_min_video_from_mp4(in_path, w, h) - total_frames = len(vid.frames) - - vid.save_file(out_path) - - print("\nDone") - print(str(w) + "x" + str(h) + " " + str(total_frames) + " frames") - print("Conversion took " + str(time.time() - tm) + " seconds") - -def play_option(): - if len(sys.argv) < 3: - print("Video path is required") - exit(1) - - path = sys.argv[2] - - if not os.path.isfile(path): - print("File not found: " + path) - exit(-1) - - print("Reading file: " + path + "...") - - tm = time.time() - - data = open(path, "rb").read() - - width = min_video.Video.get_width_from_data(data) - height = min_video.Video.get_height_from_data(data) - frames = min_video.Video.get_frame_amount_from_data(data) - - print("File read") - print("Reading took " + str(time.time() - tm) + " seconds") - print(str(width) + "x" + str(height) + " " + str( frames ) + " frames") - - - pygame.init() - screen = pygame.display.set_mode((width, height),pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE) - pygame.display.set_caption("MinVideo renderer") - - surface = pygame.Surface((width, height)) - - while True: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - print("Playback stopped, quitting") - pygame.quit() - sys.exit() - - - def render(frame): - for y in range(height): - for x in range(width): - rgb = frame.get_color(x, y) - surface.set_at((x, y), rgb) - - screen.blit(pygame.transform.scale(surface, (screen.get_width(), screen.get_height())), (0, 0)) - pygame.display.flip() - - min_video.Video.foreach_frame(data, render) - -if __name__ == "__main__": - if len(sys.argv) <= 1: - show_help() - exit() - - match sys.argv[1]: - case "help": - show_help() - - case "convert": - convert_option() - - case "play": - play_option() - - case "parse": - parse_option() - - case _: - print("Unknown option: " + sys.argv[1]) - print("") - show_help() - \ No newline at end of file From ea2bfe98d749431633a745f949f5400544839511 Mon Sep 17 00:00:00 2001 From: Phinehas Beresford <78656905+Cracko298@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:33:26 -0400 Subject: [PATCH 2/6] Add files via upload --- python/renderer.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 python/renderer.py diff --git a/python/renderer.py b/python/renderer.py new file mode 100644 index 0000000..237fb7a --- /dev/null +++ b/python/renderer.py @@ -0,0 +1,83 @@ +import os, sys, time, shutil, glob +import threading +import queue +from pygame.locals import QUIT +os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = 'hide' +import pygame + +# Initialize Pygame +pygame.init() + +# Read initial file info +with open("race.minv", 'rb') as outf: + width = int.from_bytes(outf.read(0x08), byteorder='little') + outf.seek(0x08) + height = int.from_bytes(outf.read(0x08), byteorder='little') + outf.seek(0x10) + dataClus = outf.read() # Read all frame data + +# Each frame has width*height*3 bytes (RGB) +frame_size = width * height * 3 +num_frames = len(dataClus) // frame_size + +# Create Pygame window +screen = pygame.display.set_mode((width, height)) +pygame.display.set_caption("Frame Viewer") + +# Set frame rate to 30 FPS +fps = 30 +clock = pygame.time.Clock() + +# Function to convert raw RGB data to a Pygame Surface +def rgb_to_surface(rgb_data, width, height): + surface = pygame.image.fromstring(rgb_data, (width, height), 'RGB') + return surface + +# Queue to hold frames for rendering +frame_queue = queue.Queue(maxsize=1000) # Limits the number of frames in queue + +# Thread to read frames from the file +def frame_reader(): + current_frame = 0 + while current_frame < num_frames: + start = current_frame * frame_size + end = start + frame_size + frame_data = dataClus[start:end] + + # Put frame data in the queue + frame_queue.put(frame_data) + + # Move to the next frame + current_frame = (current_frame + 1) % num_frames + +# Thread to render frames +def frame_renderer(): + running = True + while running: + for event in pygame.event.get(): + if event.type == QUIT: + running = False + + if not frame_queue.empty(): + frame_data = frame_queue.get() + surface = rgb_to_surface(frame_data, width, height) + + # Display frame on the screen + screen.blit(surface, (0, 0)) + pygame.display.flip() + + # Control frame rate (30 FPS) + clock.tick(fps) + + pygame.quit() + +# Create and start the threads +reader_thread = threading.Thread(target=frame_reader) +renderer_thread = threading.Thread(target=frame_renderer) + +reader_thread.start() +renderer_thread.start() + +# Wait for both threads to complete +reader_thread.join() +renderer_thread.join() From 9c8159623057c71976311b7551fbdb56bad30fd1 Mon Sep 17 00:00:00 2001 From: Phinehas Beresford <78656905+Cracko298@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:33:43 -0400 Subject: [PATCH 3/6] Delete python/min_video.py --- python/min_video.py | 189 -------------------------------------------- 1 file changed, 189 deletions(-) delete mode 100644 python/min_video.py diff --git a/python/min_video.py b/python/min_video.py deleted file mode 100644 index 2520364..0000000 --- a/python/min_video.py +++ /dev/null @@ -1,189 +0,0 @@ -# DEPRECATED -# This library is going to be partially remade for better performance - -import math - -VIDEO_SIZE_BYTE_LENGTH = 8 -VIDEO_MAX_DIMENSION = VIDEO_SIZE_BYTE_LENGTH * 255 -BYTES_BEFORE_FRAMES = VIDEO_SIZE_BYTE_LENGTH * 2 - -def dimension_split(dimension: int) -> list[int]: - res = [] - - if dimension != 0: - count = math.ceil(dimension / 255) - res = [dimension // count] * count - - for i in range(dimension % count): - res[i] += 1 - - while len(res) < VIDEO_SIZE_BYTE_LENGTH: - res.append(0) - - return res - -def get_coords_at_idx(frame_index: int, width: int, height: int) -> tuple[int, int]: - x = frame_index % width - y = (frame_index // width) % height - - return x, y - - -class Frame: - def __init__(self, width, height) -> None: - assert width <= VIDEO_MAX_DIMENSION, "Width cannot be greater than " + str(VIDEO_MAX_DIMENSION) - assert height <= VIDEO_MAX_DIMENSION, "Height cannot be greater than " + str(VIDEO_MAX_DIMENSION) - assert width >= 1, "Width must be at least 1" - assert height >= 1, "Height must be at least 1" - - self.pixels: list[ list[int] ] = [] - self.width = width - self.height = height - - self.pixels = [None for _ in range(width * height)] - - def get_index(self, x: int, y: int) -> int: - return y * self.width + x - - def set_color(self, x: int, y: int, r: int, g: int, b: int): - idx = self.get_index(x, y) - - assert x <= self.width, "X out of range" - assert y <= self.height, "Y out of range" - - px = [r, g, b] - self.pixels[idx] = px - - def get_color(self, x: int, y: int) -> list[int]: - idx = self.get_index(x, y) - - if idx >= len(self.pixels): - return - - col = self.pixels[idx] - - if not col: - return [0, 0, 0] - - return col - - def get_data(self) -> list[int]: - res = [] - - for i in self.pixels: - for c in i: - res.append(c) - - return res - -class Video: - def __init__(self, width: int, height: int) -> None: - assert width <= VIDEO_MAX_DIMENSION, "Width cannot be greater than " + str(VIDEO_MAX_DIMENSION) - assert height <= VIDEO_MAX_DIMENSION, "Height cannot be greater than " + str(VIDEO_MAX_DIMENSION) - assert width >= 1, "Width must be at least 1" - assert height >= 1, "Height must be at least 1" - - self.frames: list[Frame] = [] - self.width = width - self.height = height - - def add_frame(self, frame: Frame): - self.frames.append(frame) - - def get_data(self) -> list[int]: - data = [] - - data.extend(dimension_split(self.width)) - data.extend(dimension_split(self.height)) - - for frame in self.frames: - for color in frame.get_data(): - data.append(color) - - return data - - def get_bytes(self) -> bytes: - return bytes(self.get_data()) - - def save_file(self, path: str): - with open(path, "wb") as file: - file.write(self.get_bytes()) - - @staticmethod - def get_width_from_data(data: list[int]) -> int: - return sum(data[:VIDEO_SIZE_BYTE_LENGTH]) - - @staticmethod - def get_height_from_data(data: list[int]) -> int: - return sum(data[VIDEO_SIZE_BYTE_LENGTH:VIDEO_SIZE_BYTE_LENGTH * 2]) - - @staticmethod - def get_frame_amount_from_data(data: list[int]) -> int: - w = Video.get_width_from_data(data) - h = Video.get_height_from_data(data) - - return (len(data) - VIDEO_SIZE_BYTE_LENGTH * 2) // 3 // (w * h) - - @staticmethod - def from_data(data: list[int]): - data_len = len(data) - w = Video.get_width_from_data(data) - h = Video.get_height_from_data(data) - pixel_amt = w * h * 3 - vid = Video(w, h) - frames = (data_len - BYTES_BEFORE_FRAMES) // pixel_amt - - for frame_i in range(frames): - frame = Frame(w, h) - - color_start_index = BYTES_BEFORE_FRAMES + frame_i * pixel_amt - colors = data[color_start_index:color_start_index + pixel_amt] - - for i in range(pixel_amt // 3): - x, y = get_coords_at_idx(i, w, h) - - b = colors[i * 3] - g = colors[i * 3 + 1] - r = colors[i * 3 + 2] - - frame.set_color(x, y, r, g, b) - - vid.add_frame(frame) - - return vid - - @staticmethod - def foreach_frame(data: list[int], callback): - data_len = len(data) - w = Video.get_width_from_data(data) - h = Video.get_height_from_data(data) - pixel_amt = w * h * 3 - - frames = (data_len - BYTES_BEFORE_FRAMES) // pixel_amt - - for frame_i in range(frames): - frame = Frame(w, h) - - color_start_index = BYTES_BEFORE_FRAMES + frame_i * pixel_amt - colors = data[color_start_index:color_start_index + pixel_amt] - - for i in range(pixel_amt // 3): - x, y = get_coords_at_idx(i, w, h) - - b = colors[i * 3] - g = colors[i * 3 + 1] - r = colors[i * 3 + 2] - - frame.set_color(x, y, r, g, b) - - callback(frame) - - @staticmethod - def from_file(path: str): - with open(path, "rb") as file: - return Video.from_data(file.read()) - - -if __name__ == "__main__": - print("This script is to be used a library! Refer to 'cli.py'") - print("Use `import min_video` to access its classes and functions") \ No newline at end of file From 35c8b70641247e29adea3a7da465210c9b08aa9b Mon Sep 17 00:00:00 2001 From: Phinehas Beresford <78656905+Cracko298@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:35:35 -0400 Subject: [PATCH 4/6] Add files via upload --- python/MinVideoGenerator.py | 133 ++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 python/MinVideoGenerator.py diff --git a/python/MinVideoGenerator.py b/python/MinVideoGenerator.py new file mode 100644 index 0000000..e3e68b8 --- /dev/null +++ b/python/MinVideoGenerator.py @@ -0,0 +1,133 @@ +import cv2, os, threading +from PIL import Image +from tkinter import filedialog +from concurrent.futures import ThreadPoolExecutor +from tqdm import tqdm + +def save_frame(frame, count, output_folder): + pil_image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + frame_filename = f"{output_folder}\\frame_{count}.png" + pil_image.save(frame_filename) + +def extract_frames(video_path, output_folder, every_n_frame=1): + video = cv2.VideoCapture(video_path) + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + success, frame = video.read() + count = 0 + frame_count = 0 + if not os.path.exists(output_folder): + os.makedirs(output_folder) + + with ThreadPoolExecutor() as executor: + futures = [] + with tqdm(total=total_frames // every_n_frame, desc="Extracting Frames") as pbar: + while success: + if frame_count % every_n_frame == 0: + futures.append(executor.submit(save_frame, frame, count, output_folder)) + count += 1 + pbar.update(1) + + success, frame = video.read() + frame_count += 1 + + for future in futures: + future.result() + + video.release() + +def getFrameList(outdir): + fileList0 = os.listdir(outdir) + list0 = [] + for file in fileList0: + if "frame_" in file and ".png" in file: + list0.append(file) + + list0.sort(key=lambda x: int(x.split('_')[1].split('.')[0])) + return list0 + +def process_single_image(image_path, output_file): + try: + with Image.open(f".\\extracted_frames\\{image_path}") as img: + img = img.convert('RGB') + width, height = img.size + for y in range(height): + for x in range(width): + r, g, b = img.getpixel((x, y)) + output_file.write(bytes([r, g, b])) + except Exception as e: + print(f"Error processing {image_path}: {e}") + +# Function to process images with threading and progress bar +def process_images(image_files, output_file): + with open(output_file, 'ab+') as out: + with ThreadPoolExecutor(max_workers=8) as executor: + # Initialize a thread lock for writing to the file + lock = threading.Lock() + + # Function that will handle threading and writing + def threaded_process(image_path): + # Process each image and write output in a thread-safe way + with lock: + process_single_image(image_path, out) + + # Create a progress bar + with tqdm(total=len(image_files), desc="Processing Images") as progress_bar: + # Submit tasks to the ThreadPoolExecutor + futures = [executor.submit(threaded_process, img) for img in image_files] + + # Wait for each thread to complete and update the progress bar + for _ in futures: + _.result() # To catch any exception in threads + progress_bar.update(1) + +def getVideoDemensions(vf): + video = cv2.VideoCapture(vf) + x = video.get(cv2.CAP_PROP_FRAME_WIDTH) + y = video.get(cv2.CAP_PROP_FRAME_HEIGHT) + video.release() + return x, y + +video_file = filedialog.askopenfilename( + defaultextension=".mp4", + filetypes=[("Video files", "*.mp4;*.avi;*.mov;*.wmv")], + initialdir=os.getcwd(), + title="Load Video File" +) + +minvFile = f"{video_file.replace('.mp4', '.minv')}" +number1, number2 = getVideoDemensions(video_file) +bytes_list1 = [] +bytes_list2 = [] + +# Convert the first integer to an 8-byte array +n = number1 +while n > 0: + byte = min(n, 255) + bytes_list1.append(byte) + n -= byte +while len(bytes_list1) < 8: + bytes_list1.append(0) + +# Convert the second integer to an 8-byte array +n = number2 +while n > 0: + byte = min(n, 255) + bytes_list2.append(byte) + n -= byte +while len(bytes_list2) < 8: + bytes_list2.append(0) + +# Write the byte arrays to a file +with open(minvFile, 'wb') as f: + for byte1 in bytes_list1: + byte1 = int(byte1).to_bytes(1, 'little') + f.write(byte1) + for byte2 in bytes_list2: + byte2 = int(byte2).to_bytes(1, 'little') + f.write(byte2) + + +output_directory = ".\\extracted_frames" +extract_frames(video_file, output_directory, every_n_frame=1) +list0 = getFrameList(output_directory) +process_images(list0, f"{minvFile}") From d52dcff4c2551bb14d34afba1c4e77897c145658 Mon Sep 17 00:00:00 2001 From: Phinehas Beresford <78656905+Cracko298@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:59:29 -0400 Subject: [PATCH 5/6] Delete python/MinVideoGenerator.py --- python/MinVideoGenerator.py | 133 ------------------------------------ 1 file changed, 133 deletions(-) delete mode 100644 python/MinVideoGenerator.py diff --git a/python/MinVideoGenerator.py b/python/MinVideoGenerator.py deleted file mode 100644 index e3e68b8..0000000 --- a/python/MinVideoGenerator.py +++ /dev/null @@ -1,133 +0,0 @@ -import cv2, os, threading -from PIL import Image -from tkinter import filedialog -from concurrent.futures import ThreadPoolExecutor -from tqdm import tqdm - -def save_frame(frame, count, output_folder): - pil_image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) - frame_filename = f"{output_folder}\\frame_{count}.png" - pil_image.save(frame_filename) - -def extract_frames(video_path, output_folder, every_n_frame=1): - video = cv2.VideoCapture(video_path) - total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) - success, frame = video.read() - count = 0 - frame_count = 0 - if not os.path.exists(output_folder): - os.makedirs(output_folder) - - with ThreadPoolExecutor() as executor: - futures = [] - with tqdm(total=total_frames // every_n_frame, desc="Extracting Frames") as pbar: - while success: - if frame_count % every_n_frame == 0: - futures.append(executor.submit(save_frame, frame, count, output_folder)) - count += 1 - pbar.update(1) - - success, frame = video.read() - frame_count += 1 - - for future in futures: - future.result() - - video.release() - -def getFrameList(outdir): - fileList0 = os.listdir(outdir) - list0 = [] - for file in fileList0: - if "frame_" in file and ".png" in file: - list0.append(file) - - list0.sort(key=lambda x: int(x.split('_')[1].split('.')[0])) - return list0 - -def process_single_image(image_path, output_file): - try: - with Image.open(f".\\extracted_frames\\{image_path}") as img: - img = img.convert('RGB') - width, height = img.size - for y in range(height): - for x in range(width): - r, g, b = img.getpixel((x, y)) - output_file.write(bytes([r, g, b])) - except Exception as e: - print(f"Error processing {image_path}: {e}") - -# Function to process images with threading and progress bar -def process_images(image_files, output_file): - with open(output_file, 'ab+') as out: - with ThreadPoolExecutor(max_workers=8) as executor: - # Initialize a thread lock for writing to the file - lock = threading.Lock() - - # Function that will handle threading and writing - def threaded_process(image_path): - # Process each image and write output in a thread-safe way - with lock: - process_single_image(image_path, out) - - # Create a progress bar - with tqdm(total=len(image_files), desc="Processing Images") as progress_bar: - # Submit tasks to the ThreadPoolExecutor - futures = [executor.submit(threaded_process, img) for img in image_files] - - # Wait for each thread to complete and update the progress bar - for _ in futures: - _.result() # To catch any exception in threads - progress_bar.update(1) - -def getVideoDemensions(vf): - video = cv2.VideoCapture(vf) - x = video.get(cv2.CAP_PROP_FRAME_WIDTH) - y = video.get(cv2.CAP_PROP_FRAME_HEIGHT) - video.release() - return x, y - -video_file = filedialog.askopenfilename( - defaultextension=".mp4", - filetypes=[("Video files", "*.mp4;*.avi;*.mov;*.wmv")], - initialdir=os.getcwd(), - title="Load Video File" -) - -minvFile = f"{video_file.replace('.mp4', '.minv')}" -number1, number2 = getVideoDemensions(video_file) -bytes_list1 = [] -bytes_list2 = [] - -# Convert the first integer to an 8-byte array -n = number1 -while n > 0: - byte = min(n, 255) - bytes_list1.append(byte) - n -= byte -while len(bytes_list1) < 8: - bytes_list1.append(0) - -# Convert the second integer to an 8-byte array -n = number2 -while n > 0: - byte = min(n, 255) - bytes_list2.append(byte) - n -= byte -while len(bytes_list2) < 8: - bytes_list2.append(0) - -# Write the byte arrays to a file -with open(minvFile, 'wb') as f: - for byte1 in bytes_list1: - byte1 = int(byte1).to_bytes(1, 'little') - f.write(byte1) - for byte2 in bytes_list2: - byte2 = int(byte2).to_bytes(1, 'little') - f.write(byte2) - - -output_directory = ".\\extracted_frames" -extract_frames(video_file, output_directory, every_n_frame=1) -list0 = getFrameList(output_directory) -process_images(list0, f"{minvFile}") From 10eeae6ad832f3a1a84a88034229b9643af0f003 Mon Sep 17 00:00:00 2001 From: Phinehas Beresford <78656905+Cracko298@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:00:34 -0400 Subject: [PATCH 6/6] Added better renderer, and generator --- python/generator.py | 114 ++++++++++++++++++++++++++++++++++++++++++++ python/renderer.py | 53 +++++++++----------- 2 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 python/generator.py diff --git a/python/generator.py b/python/generator.py new file mode 100644 index 0000000..c703a66 --- /dev/null +++ b/python/generator.py @@ -0,0 +1,114 @@ +import cv2, os, threading +from PIL import Image +from tkinter import filedialog +from concurrent.futures import ThreadPoolExecutor +from tqdm import tqdm + +def save_frame(frame, count, output_folder): + pil_image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + frame_filename = f"{output_folder}\\frame_{count}.png" + pil_image.save(frame_filename) + +def extract_frames(video_path, output_folder, every_n_frame=1): + video = cv2.VideoCapture(video_path) + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + success, frame = video.read() + count = 0 + frame_count = 0 + if not os.path.exists(output_folder): + os.makedirs(output_folder) + + with ThreadPoolExecutor() as executor: + futures = [] + with tqdm(total=total_frames // every_n_frame, desc="Extracting Frames") as pbar: + while success: + if frame_count % every_n_frame == 0: + futures.append(executor.submit(save_frame, frame, count, output_folder)) + count += 1 + pbar.update(1) + + success, frame = video.read() + frame_count += 1 + + for future in futures: + future.result() + + video.release() + +def getFrameList(outdir): + fileList0 = os.listdir(outdir) + list0 = [] + for file in fileList0: + if "frame_" in file and ".png" in file: + list0.append(file) + + list0.sort(key=lambda x: int(x.split('_')[1].split('.')[0])) + return list0 + +def process_single_image(image_path, output_file): + try: + with Image.open(f".\\extracted_frames\\{image_path}") as img: + img = img.convert('RGB') + width, height = img.size + for y in range(height): + for x in range(width): + r, g, b = img.getpixel((x, y)) + output_file.write(bytes([r, g, b])) + except Exception as e: + print(f"Error processing {image_path}: {e}") + +# Function to process images with threading and progress bar +def process_images(image_files, output_file): + with open(output_file, 'ab+') as out: + with ThreadPoolExecutor(max_workers=8) as executor: + # Initialize a thread lock for writing to the file + lock = threading.Lock() + + # Function that will handle threading and writing + def threaded_process(image_path): + # Process each image and write output in a thread-safe way + with lock: + process_single_image(image_path, out) + + # Create a progress bar + with tqdm(total=len(image_files), desc="Processing Images") as progress_bar: + # Submit tasks to the ThreadPoolExecutor + futures = [executor.submit(threaded_process, img) for img in image_files] + + # Wait for each thread to complete and update the progress bar + for _ in futures: + _.result() # To catch any exception in threads + progress_bar.update(1) + +def getVideoDemensions(vf): + video = cv2.VideoCapture(vf) + x = video.get(cv2.CAP_PROP_FRAME_WIDTH) + y = video.get(cv2.CAP_PROP_FRAME_HEIGHT) + fps = video.get(cv2.CAP_PROP_FPS) + video.release() + return x, y, fps + +video_file = filedialog.askopenfilename( + defaultextension=".mp4", + filetypes=[("Video files", "*.mp4;*.avi;*.mov;*.wmv")], + initialdir=os.getcwd(), + title="Load Video File" +) + +minvFile = f"{video_file.replace('.mp4', '.minv')}" +number1, number2, framespersecond = getVideoDemensions(video_file) + +with open(minvFile, 'wb') as f: + byte1 = int(number1).to_bytes(6, 'little') + f.write(byte1) + byte2 = int(number2).to_bytes(6, 'little') + f.write(byte2) + print(framespersecond) + byte3 = int(framespersecond).to_bytes(4, 'little') + f.write(byte3) + + +output_directory = ".\\extracted_frames" +extract_frames(video_file, output_directory, every_n_frame=1) +list0 = getFrameList(output_directory) +process_images(list0, f"{minvFile}") diff --git a/python/renderer.py b/python/renderer.py index 237fb7a..cd24770 100644 --- a/python/renderer.py +++ b/python/renderer.py @@ -1,56 +1,54 @@ import os, sys, time, shutil, glob +from tkinter import filedialog import threading import queue from pygame.locals import QUIT os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = 'hide' import pygame - -# Initialize Pygame pygame.init() -# Read initial file info -with open("race.minv", 'rb') as outf: - width = int.from_bytes(outf.read(0x08), byteorder='little') - outf.seek(0x08) - height = int.from_bytes(outf.read(0x08), byteorder='little') +video_file = filedialog.askopenfilename( + defaultextension=".minv", + filetypes=[("MinVideo files", "*.minv;*.miv;*.minvideo;*.mvid")], + initialdir=os.getcwd(), + title="Load MinVideo File" +) + +with open(video_file, 'rb') as outf: + width = int.from_bytes(outf.read(0x06), byteorder='little') + outf.seek(0x06) + height = int.from_bytes(outf.read(0x06), byteorder='little') + outf.seek(0x0C) + fps = int.from_bytes(outf.read(0x04), byteorder='little') outf.seek(0x10) - dataClus = outf.read() # Read all frame data + dataClus = outf.read() -# Each frame has width*height*3 bytes (RGB) frame_size = width * height * 3 num_frames = len(dataClus) // frame_size - -# Create Pygame window screen = pygame.display.set_mode((width, height)) -pygame.display.set_caption("Frame Viewer") +pygame.display.set_caption("MinVideo Player") -# Set frame rate to 30 FPS -fps = 30 clock = pygame.time.Clock() - -# Function to convert raw RGB data to a Pygame Surface def rgb_to_surface(rgb_data, width, height): surface = pygame.image.fromstring(rgb_data, (width, height), 'RGB') return surface -# Queue to hold frames for rendering -frame_queue = queue.Queue(maxsize=1000) # Limits the number of frames in queue +if fps >= 40: + frame_queue = queue.Queue(maxsize=20) +elif fps >= 20 and fps < 40: + frame_queue = queue.Queue(maxsize=10) +else: + frame_queue = queue.Queue(maxsize=5) -# Thread to read frames from the file def frame_reader(): current_frame = 0 while current_frame < num_frames: start = current_frame * frame_size end = start + frame_size frame_data = dataClus[start:end] - - # Put frame data in the queue frame_queue.put(frame_data) - - # Move to the next frame current_frame = (current_frame + 1) % num_frames -# Thread to render frames def frame_renderer(): running = True while running: @@ -62,22 +60,17 @@ def frame_renderer(): frame_data = frame_queue.get() surface = rgb_to_surface(frame_data, width, height) - # Display frame on the screen screen.blit(surface, (0, 0)) pygame.display.flip() - # Control frame rate (30 FPS) clock.tick(fps) pygame.quit() -# Create and start the threads reader_thread = threading.Thread(target=frame_reader) renderer_thread = threading.Thread(target=frame_renderer) reader_thread.start() renderer_thread.start() - -# Wait for both threads to complete reader_thread.join() -renderer_thread.join() +renderer_thread.join() \ No newline at end of file