diff --git a/README.md b/README.md index 9da396a..13904b0 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ pip install -e . Run it with: ``` -moviecolor input.mp4 [-l 30] [-o output_name] [--alt] [--help] +moviecolor input.mp4 [-e 30] [-o output_name] [--alt] [--help] ``` ->-l , --length: chosen part of the video from start (in Minutes) +>-e , --end: chosen part of the video from start (in Minutes) >-a , --alt: instead of using average color, use shrinked frames diff --git a/moviecolor/__main__.py b/moviecolor/__main__.py index 9d93a75..3b95695 100644 --- a/moviecolor/__main__.py +++ b/moviecolor/__main__.py @@ -1,69 +1,49 @@ -#from moviecolor import movcolor -from moviecolor.moviecolor import movcolor +#from moviecolor import Movcolor from pathlib import Path -import tkinter as tk import argparse -import threading -import time +import sys + +from moviecolor.moviecolor import Movcolor parser = argparse.ArgumentParser() parser.add_argument('in_file', type=Path, help='Input file path') -parser.add_argument('-o','--out_filename', type=Path, default='result', help='Output file path') -parser.add_argument('-l','--length', type=int, default=0 , help='Chosen part of the video from start in Minutes') -parser.add_argument('-a','--alt', action='store_true', help='Instead of gettig average color of frames, Each bar is the resized frame') +parser.add_argument('-o', '--out_filename', type=Path, + default='result', help='Output file path') +parser.add_argument('-s', '--start', type=int, default=0, + help='Start point of the chosen part of the video in Minutes') +parser.add_argument('-e', '--end', type=int, default=0, + help='End point of the chosen part of the video in Minutes') +parser.add_argument('-a', '--alt', action='store_true', + help='Instead of average color, Each bar is the resized frame') + def main(): + """Starting point of the program + to read the input args and create movcolor object""" args = parser.parse_args() input_file_path = args.in_file - + if not input_file_path.is_file(): print( - "\nEnter Valid input Path.\n" - "Example (on windows): \"c:\\video\\input with white space.mp4\"\n" - "Example (on linux): /home/video/file.mp4" + "\nEnter Valid input Path.\n" + "Example (on windows): \"c:\\video\\input with white space.mp4\"\n" + "Example (on linux): /home/video/file.mp4" ) - exit() + sys.exit() output_file_path = args.out_filename - video_length = args.length + start_point = args.start + end_point = args.end - obj1 = movcolor(1, input_file_path, output_file_path) - - if video_length != 0: - number_of_frames = video_length * 60 * 3 - else: - duration = obj1.get_video_duration() - number_of_frames = duration * 3 - video_length = int(duration/60) - if args.alt: - process_func = movcolor.process_frame_compress_width - refresh_image = obj1.refresh_image_alt - draw_func = obj1.draw_alt + mode = "alt" else: - process_func = movcolor.process_frame_average_color - refresh_image = obj1.refresh_image_normal - draw_func = obj1.draw_normal - - th = threading.Thread(target=obj1, args=(video_length ,process_func, draw_func, 0)) - - th.daemon = True # terminates whenever main thread does - th.start() - - while len(obj1.rgb_list) == 0: # rgb_list in refresh_image shouldnt be empty - time.sleep(.1) - - root = tk.Tk() - canvas = tk.Canvas(root, height=720, width=1500) - - root.title("MovieColor") - root.geometry("1500x720+0+10") - canvas.pack() + mode = "normal" - refresh_image(canvas, 1, number_of_frames) - root.mainloop() + obj1 = Movcolor(1, input_file_path, output_file_path, start_point, end_point, mode) + obj1.run() if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/moviecolor/moviecolor.py b/moviecolor/moviecolor.py index 1a421d9..8c47f2b 100644 --- a/moviecolor/moviecolor.py +++ b/moviecolor/moviecolor.py @@ -1,11 +1,36 @@ -from PIL import Image, ImageDraw, ImageTk -import ffmpeg +"""moviecolor module contains movcolor class +to generate the barcode image of video and show it +in real-time on a tkinter canvas. +""" + +import sys +import time import logging +import threading + +from PIL import Image, ImageDraw, ImageTk +import tkinter as tk import numpy as np -import subprocess -import sys +import ffmpeg + -class movcolor: +class Movcolor: + """Create an object to generate a barcode of a video file. + + Functions: + - get_video_duration() + - get_video_size() + - start_ffmpeg_process(start, end) + - read_frame(process, width, height) + - process_frame_average_color(frame) + - process_frame_compress_width(frame) + - draw_alt() + - draw_normal() + - refresh_image_alt(canvas, x_pixel, number_of_frames, *param) + - refresh_image_normal(canvas, x_pixel, number_of_frames) + - worker(process_frame, draw_func, start, end) + - run() + """ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -13,53 +38,120 @@ class movcolor: def __call__(self, *args): self.run(*args) - def __init__(self, id, in_path, out_path) -> None: - self.id = id + def __init__(self, instance_id, in_path, out_path, + start_point, end_point, draw_mode="normal"): + self.instance_id = instance_id self.in_path = in_path self.out_path = out_path + self.start_point = start_point + + if end_point != 0: + self.end_point = end_point + self.number_of_frames = end_point * 60 * 3 + else: + duration = self.get_video_duration() + self.number_of_frames = duration * 3 + self.end_point = int(duration/60) + + self.draw_mode = draw_mode + + if self.draw_mode == "alt": + self.process_func = self.process_frame_compress_width + self.refresh_image = self.refresh_image_alt + self.draw_func = self.draw_alt + else: + self.process_func = self.process_frame_average_color + self.refresh_image = self.refresh_image_normal + self.draw_func = self.draw_normal + + # used in refresh_image funcs to determine when to stop drawing bars self.bars_flag = 0 - self.rgb_list = [] + self.rgb_list = [] # list of the bars def get_video_duration(self): + """get duration of the video in seconds. + + Returns: + int: duration of the video in seconds + + Exceptions: + ffmpeg can't get some videos durations. + in these cases print an error and exit. + """ + probe = ffmpeg.probe(self.in_path) - video_info = next(s for s in probe['streams'] if s['codec_type'] == 'video') + video_info = next( + s for s in probe['streams'] if s['codec_type'] == 'video') try: duration = int(video_info['duration'].split('.')[0]) except: - print("ERROR: can't extract duration of the video, please specify it by '-l' option.") + print( + """ERROR: can't extract duration of the video, + please specify it by '-l' option.""") sys.exit() return duration def get_video_size(self): - self.logger.info('Getting video size for {!r}'.format(self.in_path)) + """get width and hight of the video. + + Returns: + tuple: width(int) and height(int) of video + """ + + self.logger.info(f'Getting video size for {self.in_path}') probe = ffmpeg.probe(self.in_path) - video_info = next(s for s in probe['streams'] if s['codec_type'] == 'video') + video_info = next( + s for s in probe['streams'] if s['codec_type'] == 'video') width = int(video_info['width']) height = int(video_info['height']) return width, height + def start_ffmpeg_process(self, start, end): + """starting and ending time of the video to get barcode. + each frame will piped in stream in bytes. + + Args: + start (int): start time of the video in minute + end (int): end time of the video in minute + + Returns: + process object: ffmpeg process object + which is called in read_frame to get frames in stream. + """ - def start_ffmpeg_process1(self, start, length): self.logger.info('Starting ffmpeg process1') - args = ( + process = ( ffmpeg .input(self.in_path) - .trim(start=start*60, end=length*60) - .filter_('fps',fps=3) # get 3 frames per second + .trim(start=start*60, end=end*60) + .filter_('fps', fps=3) # get 3 frames per second .output('pipe:', format='rawvideo', pix_fmt='rgb24') - .compile() + .run_async(pipe_stdout=True) ) - return subprocess.Popen(args, stdout=subprocess.PIPE) + return process + + def read_frame(self, process, width, height): + """get an array of each frame of the video. + + Args: + process (process object): ffmpeg object + which get the frames of video + + width (int): width of video + height (int): height of video + + Returns: + numpy frame object: numpy array of the frame + """ - def read_frame(self, process1, width, height): self.logger.debug('Reading frame') # Note: RGB24 == 3 bytes per pixel. frame_size = width * height * 3 - in_bytes = process1.stdout.read(frame_size) + in_bytes = process.stdout.read(frame_size) if len(in_bytes) == 0: frame = None else: @@ -72,22 +164,44 @@ def read_frame(self, process1, width, height): return frame @staticmethod - def process_frame_average_color(frame): - rgb_avg = int(np.average(frame[:,:,0])),int(np.average(frame[:,:,1])),int(np.average(frame[:,:,2])) + def process_frame_average_color(frame): + """get average color of a frame in tuple (rgb). + + Args: + frame (numpy frame object): numpy array of the frame + + Returns: + tuple: rgb color typle in ints + """ + + rgb_avg = int(np.average(frame[:, :, 0])), int( + np.average(frame[:, :, 1])), int(np.average(frame[:, :, 2])) return rgb_avg - + @staticmethod def process_frame_compress_width(frame): - img = Image.fromarray(frame, 'RGB').resize((1,720)) + """get shrinked image of a frame. + + Args: + frame (numpy frame object): numpy array of the frame + + Returns: + pillow.image: shrinked (resized) frame in img format + """ + + img = Image.fromarray(frame, 'RGB').resize((1, 720)) return img def draw_alt(self): + """draw and save the final barcode picture + (alt mode = shrinked frames).""" + len_rgb_list = len(self.rgb_list) - - image_height = int(len_rgb_list*9/16) # to make a 16:9 - new = Image.new('RGB',(len_rgb_list,image_height)) - for i in range(len_rgb_list-1): - new.paste(self.rgb_list[i].resize((1,image_height)), (i, 0)) + + image_height = int(len_rgb_list*9/16) # to make a 16:9 + new = Image.new('RGB', (len_rgb_list, image_height)) + for i in range(len_rgb_list-1): + new.paste(self.rgb_list[i].resize((1, image_height)), (i, 0)) if self.out_path.suffix.lower() == ".jpg": suff = "JPEG" @@ -100,16 +214,19 @@ def draw_alt(self): new.save(self.out_path, suff) def draw_normal(self): + """draw and save the final barcode picture + with average color for each frame.""" + len_rgb_list = len(self.rgb_list) - - image_height = int(len_rgb_list*9/16) # to make a 16:9 - new = Image.new('RGB',(int(len_rgb_list),image_height)) + + image_height = int(len_rgb_list*9/16) # to make a 16:9 + new = Image.new('RGB', (int(len_rgb_list), image_height)) draw = ImageDraw.Draw(new) - x_pixel = 1 # x axis of the next line to draw + x_pixel = 1 # x axis of the next line to draw for rgb_tuple in self.rgb_list: - draw.line((x_pixel,0,x_pixel,image_height), fill=rgb_tuple) + draw.line((x_pixel, 0, x_pixel, image_height), fill=rgb_tuple) x_pixel = x_pixel + 1 - + if self.out_path.suffix.lower() == ".jpg": suff = "JPEG" elif self.out_path.suffix.lower() == ".png": @@ -120,58 +237,109 @@ def draw_normal(self): new.save(self.out_path, suff) + def refresh_image_alt(self, canvas, x_pixel, number_of_frames, *param): + """draw each bar every 0.1 second by calling canvas.after. + (alt mode) + + Args: + canvas (tkinter.canvas): main tkinter canvas which show the bars + x_pixel (int): position of X axis to draw the next bar on canvas + number_of_frames (int): count of the frames to draw on canvas + """ + + dst = Image.new('RGB', (1500, 720)) + + if len(param) != 0: + dst = param[0] + + step = 1500 / number_of_frames + for rgb_tuple in self.rgb_list[int((x_pixel-1)*(1/step)):]: + dst.paste(rgb_tuple, (int(x_pixel), 0)) + x_pixel += 1 + global image + image = ImageTk.PhotoImage(dst) + canvas.create_image((750, 360), image=image) + + if len(self.rgb_list) != self.bars_flag: + canvas.after(100, self.refresh_image_alt, canvas, + x_pixel, number_of_frames, dst) + + def refresh_image_normal(self, canvas, x_pixel, number_of_frames): + """draw each bar every 0.1 second by calling canvas.after. + (normal mode) + + Args: + canvas (tkinter.canvas): main tkinter canvas which show the bars + x_pixel (int): position of X axis to draw the next bar on canvas + number_of_frames (int): count of the frames to draw on canvas + """ + + image_height = 720 + step = 1500 / number_of_frames + for rgb_tuple in self.rgb_list[int((x_pixel-1)*(1/step)):]: + canvas.create_line((x_pixel, 0, x_pixel, image_height), + fill='#%02x%02x%02x' % rgb_tuple, width=step) + x_pixel += 1 + + if len(self.rgb_list) != self.bars_flag: + canvas.after(100, self.refresh_image_normal, + canvas, x_pixel-step, number_of_frames) + + def worker(self, process_frame, draw_func, start, end): + """run the main functionality of program to save final image. + + Args: + process_frame (ffmpeg process object): ffmepg process object + which is called here and pass to read_frame. + + draw_func (function): one of two alt or noraml draw function + start (int): start time of video in minute + end (int): end time of video in minute + """ - def run(self, length, process_frame, draw_func, start): - width, height = self.get_video_size() - process1 = self.start_ffmpeg_process1(start, length) + process = self.start_ffmpeg_process(start, end) while True: - in_frame = self.read_frame(process1, width, height) + in_frame = self.read_frame(process, width, height) if in_frame is None: self.logger.info('End of input stream') break self.logger.debug('Processing frame') out_frame = process_frame(in_frame) - - self.rgb_list.append(out_frame) + self.rgb_list.append(out_frame) self.bars_flag = len(self.rgb_list) draw_func() - self.logger.info('Waiting for ffmpeg process1') - process1.wait() + process.wait() self.logger.info('Done') - def refresh_image_alt(self, canvas, x_pixel, number_of_frames, *param): - - dst = Image.new('RGB', (1500, 720)) + def run(self): + """run the worker thread and create tkinter canvas to + draw bars in real-time base on the --alt arg + with it's func or normal draw func. + """ + worker_args = (self.process_func, self.draw_func, self.start_point, self.end_point) + work_thread = threading.Thread(target=self.worker, args=worker_args) - if len(param) != 0 : - dst = param[0] + work_thread.daemon = True + work_thread.start() - step = 1500 / number_of_frames - for rgb_tuple in self.rgb_list[int((x_pixel-1)*(1/step)):]: - dst.paste(rgb_tuple, (int(x_pixel), 0)) - x_pixel += 1 - global image - image = ImageTk.PhotoImage(dst) - canvas.create_image((750, 360), image=image) + # rgb_list in refresh_image shouldnt be empty + while len(self.rgb_list) == 0: + time.sleep(0.1) - if len(self.rgb_list) != self.bars_flag: - canvas.after(100, self.refresh_image_alt, canvas, x_pixel, number_of_frames, dst) + root = tk.Tk() + canvas = tk.Canvas(root, height=720, width=1500) - def refresh_image_normal(self, canvas, x_pixel, number_of_frames): - - image_height = 720 - step = 1500 / number_of_frames - for rgb_tuple in self.rgb_list[int((x_pixel-1)*(1/step)):]: - canvas.create_line((x_pixel,0,x_pixel,image_height), fill='#%02x%02x%02x' % rgb_tuple, width=step) - x_pixel += 1 + root.title("MovieColor") + root.geometry("1500x720+0+10") + canvas.pack() - if len(self.rgb_list) != self.bars_flag: - canvas.after(100, self.refresh_image_normal, canvas, x_pixel-step, number_of_frames) \ No newline at end of file + self.refresh_image(canvas, 1, self.number_of_frames) + root.mainloop() \ No newline at end of file diff --git a/setup.py b/setup.py index 8e709a4..fac6a38 100644 --- a/setup.py +++ b/setup.py @@ -5,14 +5,14 @@ setuptools.setup( name="moviecolor", - version="1.1.0", + version="1.2.0", author="Mehran Asaad", author_email = 'mehran.asaad@gmail.com', license='MIT', url = 'https://github.com/AsaadMe/MovieColor', - download_url = 'https://github.com/AsaadMe/MovieColor/releases/tag/v1.1.0', + download_url = 'https://github.com/AsaadMe/MovieColor/releases/tag/v1.2.0', keywords = ['moviebarcode'], - description="Fast program to generate a 'Moviebarcode' of a video from average color of its frames with embedded ffmpeg and real-time progress interface.", + description="Generate a 'Moviebarcode' of a video from shrinked frames or average color of frames using ffmpeg with real-time progress interface.", long_description=long_description, long_description_content_type="text/markdown", packages=setuptools.find_packages(),