diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44ec6ac..1f718da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,11 @@ # Contribution guidelines + Thank you for showing your interest in the repository and the idea. I am grateful to be working with fellow developers. ## Raising Issues -*Issues may be raised only upon problems raised while using the main and tc-version branches.* -Note that alpha branch code is highly unstable and is expected to break, kindly do not raise issues regarding it. When raising an issue, try your best to include the following information: + - Which branch were you using when things broke? - What was your input? - What was the throwback/error message received, if any? @@ -16,6 +16,7 @@ When raising an issue, try your best to include the following information: If some infromation is not available to you, you should mention that too. ## Responding to issues + *When contributing, you can contribute to any branch. The issues page will give you a list of issues you can resolve in the main branch.* diff --git a/README.md b/README.md index 91c4866..512269d 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,16 @@ A python program that creates ASCII graphics from images and videos. It can also play videos with subtitle support (given a .srt file)! :scream: # 🍎 Motivation + You have seen Music Players, Stackoverflow surfers, Hacker News portals etc. in the terminal, so it is the logical next step 😅. Besides, the terminal makes almost everything appear x10 times more cool. # ⚗️ Dependencies ## Language and Packages + The program runs using python3 The following python packages are used in the program: + - pysrt - opencv-python - Pillow @@ -26,6 +29,7 @@ All POSIX compliant terminals should work well. If you use windows, and the prog https://user-images.githubusercontent.com/55488899/161427699-1d606858-4a3d-4490-b21e-ecd4ac56a83b.mp4 # 🛠️ Usage + Navigate to the directory of the python script and run the following command ```shell python generate.py $VIDEO_FILENAME $SUBTITLE_FILENAME $OPTION @@ -37,6 +41,7 @@ python generate.py $VIDEO_FILENAME $OPTION Here `$VIDEO_FILENAME` and `$SUBTITLE_FILENAME` are the full path to the files and `$OPTION` takes values **0 for black and white output** and **1 for true color output** (see if your terminal supports true color before enabling) # 📝 TODO and Future Plans + - [x] Support 3-bit RGB (8-colors) - [x] Support true color (24-bit RGB) *visit tc-version branch* - [ ] Support automatic resizing diff --git a/generate.py b/generate.py index ac9e7f4..114b293 100644 --- a/generate.py +++ b/generate.py @@ -4,228 +4,179 @@ from cv2 import cv2 import pysrt import curses - -ASCII_CHAR_ARRAY = (" .:-=+*#%@", " .,:ilwW", " ▏▁░▂▖▃▍▐▒▀▞▚▌▅▆▊▓▇▉█", " `^|1aUBN", " .`!?xyWN") - -ASCII_CHARS = ASCII_CHAR_ARRAY[3] -MAX_PIXEL_VALUE = 255 - -def vid_render(st_matrix, st, ed, option): - media.addstr(0, 1, "Video Playback") - pixels = [st_matrix[i][:] for i in range (st, ed)] - # CONFIG OPTION - intensity measure - intensity_matrix = get_intensity_matrix(pixels) - intensity_matrix = normalize_intensity_matrix(intensity_matrix) - color_matrix = get_color_matrix(pixels) - - for i in range(len(intensity_matrix)): - offset = 1 - for j in range(len(intensity_matrix[0])): - intensity = intensity_matrix[i][j] - symbol_index = int(intensity / MAX_PIXEL_VALUE * len(ASCII_CHARS)) - 1 - symbol_index = symbol_index + 1 if symbol_index < 0 else symbol_index - asciiStr = ASCII_CHARS[symbol_index] + ASCII_CHARS[symbol_index] + ASCII_CHARS[symbol_index] - if option == 1: - color = color_matrix[i][j] - media.addstr(i + 1, offset, asciiStr, curses.color_pair(color)) - else: - media.addstr(i + 1, offset, asciiStr, curses.color_pair(0)) - offset += 3 - - media.refresh() - -def subtitle_show(subs, tstamp_ms): - """Function to get subtitles of current frame and display them""" - parts = subs.slice(starts_before={'milliseconds': int(tstamp_ms)}, ends_after={'milliseconds': int(tstamp_ms)}) - captions.addstr(0, 1, "Captions") - captions.move(1, 1) - for part in parts: - captions.addstr(part.text, curses.A_BOLD) - - captions.refresh() - -def get_pixel_matrix(image): - """Function to get image from the media file and change its dimensions to fit the terminal, then turning it into pixel matrix.""" - image = image.convert("HSV") - - # current row and column size definitions - ac_row, ac_col = image.size - # d1 and d2 are the width and height of image resp - size = media.getmaxyx() - d2 = min((size[0] - 2) - 3, int((ac_col * (size[1] - 2)) / ac_row)) - d1 = min(int((size[1] - 2) / 3), int((ac_row * d2) / ac_col)) - - # set image to determined d1 and column size - im = image.resize((d1, d2)) - pixels = list(im.getdata()) - return [pixels[i:i+im.width] for i in range(0, len(pixels), im.width)] - -def get_color_matrix(pixels): - """Function to get the colour codes (ANSI escape sequences) from RGB values of pixel.""" - color_matrix = [] - for row in pixels: - color_matrix_row = [] - for p in row: - # Convert values to percentages and normalize - hue = (p[0] / 255) * 179 - sat = (p[1] / 255) * 100 - cNum = 0 - if sat >= 30: - if (0 <= hue and hue < rUpperHue) or (hue <= 180 and hue > rLowerHue): - cNum = 4 - if hue <= gUpperHue and hue > gLowerHue: - cNum = 5 - if hue <= gLowerHue and hue > rUpperHue: - cNum = 1 - if hue <= bUpperHue and hue > bLowerHue: - cNum = 6 - if hue <= bLowerHue and hue > gUpperHue: - cNum = 3 - if hue <= rLowerHue and hue > bUpperHue: - cNum = 2 - color_matrix_row.append(cNum) - color_matrix.append(color_matrix_row) - return color_matrix - -def get_intensity_matrix(pixels): - """Function to set the measure of brightness to be used depending upon the - option, choose between three measures namely luminance, - lightness and average pixel values - """ - intensity_matrix = [] - for row in pixels: - intensity_matrix_row = [] - for p in row: - intensity = 0 - intensity = p[2] - # if option == 1: - # intensity = ((p[0] + p[1] + p[2]) / 3.0) - # elif option == 2: - # intensity = (max(p[0], p[1], p[2]) + min(p[0], p[1], p[2])) / 2 - # elif option == 3: - # intensity = (0.299 * p[0] * p[0] + 0.587 * p[1] * p[1] + 0.114 * p[2] * p[2]) ** 0.5 - # else: - # raise Exception("Unrecognised intensity option: %d" % option) - intensity_matrix_row.append(intensity) - intensity_matrix.append(intensity_matrix_row) - - return intensity_matrix - -def normalize_intensity_matrix(intensity_matrix): - """Function to normalize the intensity matrix so that values fall between acceptable limits.""" - normalized_intensity_matrix = [] - max_pixel = max(map(max, intensity_matrix)) - min_pixel = min(map(min, intensity_matrix)) - for row in intensity_matrix: - rescaled_row = [] - for p in row: - denm = float(max_pixel - min_pixel) - if denm == 0: - denm = 1 - r = MAX_PIXEL_VALUE * (p - min_pixel) / denm - rescaled_row.append(r) - normalized_intensity_matrix.append(rescaled_row) - - return normalized_intensity_matrix - -def print_from_image(filename, option): - """Function to take in an image & use its RGB values to decide upon an ASCII character - to represent it. This ASCII character will be based upon the brightness - measure calculated - - Manager function. - """ - try: - with Image.open(filename) as image: - pixels = get_pixel_matrix(image) - vid_render(pixels, 0, len(pixels), option) - except OSError: - print("Could not open image file!") - -def read_media_sub(vidfile, subfile, option): - """Function to read the media file and pass on data to rendering functions frame by frame.""" - vidcap = cv2.VideoCapture(vidfile) - subs = pysrt.open(subfile) - fps = vidcap.get(cv2.CAP_PROP_FPS) - while vidcap.isOpened(): - # read frames from the image - success, image = vidcap.read() - if not success: - break - dur = time.process_time() - cv2.imwrite("./data/frame.jpg", image) - print_from_image("./data/frame.jpg", option) - subtitle_show(subs, vidcap.get(cv2.CAP_PROP_POS_MSEC)) - dur = (time.process_time() - dur) - dur *= 1000 - if (round(1000/fps - dur) >= 0): - curses.napms(round(1000/fps - dur)) - vidcap.release() - cv2.destroyAllWindows() - -def read_media(vidfile, option): - """Function to read the media file and pass on data to rendering functions frame by frame.""" - vidcap = cv2.VideoCapture(vidfile) - fps = vidcap.get(cv2.CAP_PROP_FPS) - while vidcap.isOpened(): - # read frames from the image - success, image = vidcap.read() - if not success: - break - dur = time.process_time() - cv2.imwrite("./data/frame.jpg", image) - print_from_image("./data/frame.jpg", option) - dur = (time.process_time() - dur) - dur *= 1000 - if (round(1000/fps - dur) >= 0): - curses.napms(round(1000/fps - dur)) - vidcap.release() - cv2.destroyAllWindows() - -stdscr = curses.initscr() -curses.noecho() -curses.cbreak() -curses.start_color() -curses.init_pair(1, curses.COLOR_YELLOW, curses.COLOR_BLACK) -curses.init_pair(2, curses.COLOR_MAGENTA, curses.COLOR_BLACK) -curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLACK) -curses.init_pair(4, curses.COLOR_RED, curses.COLOR_BLACK) -curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) -curses.init_pair(6, curses.COLOR_BLUE, curses.COLOR_BLACK) - -# defining hsv color limits -# somewhat magic numbers, obtained by opening a color palette and observing the hue at which a color can be described as green -# same procedure for all numbers -gLowerHue = 36 -gUpperHue = 80 -rLowerHue = 159 -rUpperHue = 14 -bLowerHue = 104 -bUpperHue = 138 - -if len(sys.argv) == 3: - beginX = 0; beginY = 0 - height = curses.LINES; width = curses.COLS - media = curses.newwin(height - 1, width - 1, beginY, beginX) - media.border(0, 0, 0, 0, 0, 0, 0, 0) - vidfile = sys.argv[1] - colored_output = int(sys.argv[2]) - read_media(vidfile, colored_output) -else: - beginX = 0; beginY = 0 - height = curses.LINES - 5; width = curses.COLS - media = curses.newwin(height, width, beginY, beginX) - media.border(0, 0, 0, 0, 0, 0, 0, 0) - beginX = 0; beginY = curses.LINES - 5 - height = 5; width = curses.COLS - captions = curses.newwin(height, width, beginY, beginX) - captions.border(0, 0, 0, 0, 0, 0, 0, 0) - vidfile = sys.argv[1] - subfile = sys.argv[2] - colored_output = int(sys.argv[3]) - read_media_sub(vidfile, subfile, colored_output) - -media.getch() -curses.nocbreak() -curses.echo() -curses.endwin() \ No newline at end of file +import numpy as np + +class AMP(): + def __init__(self, chars_id=3, rLH=159, rUH=14, gLH=36, gUH=80, bLH=104, bUH=138): + ASCII_CHAR_ARRAY = (" .:-=+*#%@", " .,:ilwW", " ▏▁░▂▖▃▍▐▒▀▞▚▌▅▆▊▓▇▉█", " `^|1aUBN", " .`!?xyWN") + + self.ASCII_CHARS = ASCII_CHAR_ARRAY[chars_id] + self.MAX_PIXEL_VALUE = 255 + curses.init_pair(1, curses.COLOR_YELLOW, curses.COLOR_BLACK) + curses.init_pair(2, curses.COLOR_MAGENTA, curses.COLOR_BLACK) + curses.init_pair(3, curses.COLOR_CYAN, curses.COLOR_BLACK) + curses.init_pair(4, curses.COLOR_RED, curses.COLOR_BLACK) + curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK) + curses.init_pair(6, curses.COLOR_BLUE, curses.COLOR_BLACK) + + # defining hsv color limits + # somewhat magic numbers, obtained by opening a color palette and observing the hue at which a color can be described as green + # same procedure for all numbers + self.gLowerHue = gLH + self.gUpperHue = gUH + self.rLowerHue = rLH + self.rUpperHue = rUH + self.bLowerHue = bLH + self.bUpperHue = bUH + + + def vid_render(self, pixels, coloring = True): + """Function to merge and render the ASCII output to the terminal. + @param pixels - Pixel matrix of an image + @param coloring - Option to switch between color and bnw output. + """ + self.media.addstr(0, 1, "Video Playback") + intensity_matrix = pixels[:, :, 2] + intensity_matrix = self.normalize_intensity_matrix(intensity_matrix) + color_matrix = self.get_color_matrix(pixels) + + for i in range(len(intensity_matrix)): + offset = 1 + for j in range(len(intensity_matrix[0])): + symbol_index = int(intensity_matrix[i][j] / self.MAX_PIXEL_VALUE * len(self.ASCII_CHARS)) - 1 + symbol_index += int(symbol_index < 0) + asciiStr = self.ASCII_CHARS[symbol_index] * 3 + self.media.addstr(i + 1, offset, asciiStr, curses.color_pair(color_matrix[i][j] if coloring else 0)) + offset += 3 + + self.media.refresh() + + def subtitle_show(self, subs, tstamp_ms): + """Function to get subtitles of current frame and display them""" + self.captions.clear() + self.captions.border(' ', ' ', 0, 0, ' ', ' ', ' ', ' ') + parts = subs.slice(starts_before={'milliseconds': int(tstamp_ms)}, ends_after={'milliseconds': int(tstamp_ms)}) + self.captions.addstr(0, 1, "Captions") + self.captions.move(1, 0) + for part in parts: + self.captions.addstr(part.text, curses.A_BOLD) + + self.captions.refresh() + + + def get_pixel_matrix(self, image): + """Function to get image from the self.media file and change its dimensions to fit the terminal, then turning it into pixel matrix.""" + image = image.convert("HSV") + + # current row and column size definitions + ac_row, ac_col = image.size + # d1 and d2 are the width and height of image resp + size = self.media.getmaxyx() + d2 = min((size[0] - 2) - 3, int((ac_col * (size[1] - 2)) / ac_row)) + d1 = min(int((size[1] - 2) / 3), int((ac_row * d2) / ac_col)) + + # set image to determined d1 and column size + im = image.resize((d1, d2)) + pixels = np.reshape(im.getdata(), (d2, d1, 3)) + with open('error.txt', 'a') as f: + print(pixels.shape, file=f) + return pixels + + + def get_color_matrix(self, pixels): + """Function to get the colour codes (ANSI escape sequences) from RGB values of pixel.""" + color_matrix = [] + for row in pixels: + color_matrix_row = [] + for p in row: + # Convert values to percentages and normalize + hue = (p[0] / 255) * 179 + sat = (p[1] / 255) * 100 + cNum = 0 + if sat >= 30: + if (0 <= hue and hue < self.rUpperHue) or (hue <= 180 and hue > self.rLowerHue): + cNum = 4 + if hue <= self.gUpperHue and hue > self.gLowerHue: + cNum = 5 + if hue <= self.gLowerHue and hue > self.rUpperHue: + cNum = 1 + if hue <= self.bUpperHue and hue > self.bLowerHue: + cNum = 6 + if hue <= self.bLowerHue and hue > self.gUpperHue: + cNum = 3 + if hue <= self.rLowerHue and hue > self.bUpperHue: + cNum = 2 + color_matrix_row.append(cNum) + color_matrix.append(color_matrix_row) + return np.array(color_matrix) + + + def normalize_intensity_matrix(self, intensity_matrix): + """Function to normalize the intensity matrix so that values fall between acceptable limits.""" + maxval, minval = intensity_matrix.max(), intensity_matrix.min() + if (maxval != minval): intensity_matrix = (intensity_matrix - minval) * self.MAX_PIXEL_VALUE / maxval + return intensity_matrix + + + def print_from_image(self, filename, coloring): + """Function to take in an image & use its RGB values to decide upon an ASCII character + to represent it. This ASCII character will be based upon the brightness + measure calculated + + Manager function. + """ + try: + with Image.open(filename) as image: + pixels = self.get_pixel_matrix(image) + self.vid_render(pixels, coloring) + except OSError: + print("Could not open image file!") + + + def read_media_sub(self, vidfile, coloring, subfile=''): + """Function to read the self.media file and pass on data to rendering functions frame by frame.""" + vidcap = cv2.VideoCapture(vidfile) + if subfile: subs = pysrt.open(subfile,encoding='latin-1') + fps = vidcap.get(cv2.CAP_PROP_FPS) + while vidcap.isOpened(): + # read frames from the image + success, image = vidcap.read() + if not success: break + dur = time.process_time() + cv2.imwrite("./data/frame.jpg", image) + self.print_from_image("./data/frame.jpg", coloring) + if subfile: self.subtitle_show(subs, vidcap.get(cv2.CAP_PROP_POS_MSEC)) + dur = (time.process_time() - dur) * 1000 + if (round(1000/fps - dur) >= 0): curses.napms(round(1000/fps - dur)) + vidcap.release() + cv2.destroyAllWindows() + + + def main(self, argv): + beginX = 0; beginY = 0 + vidfile = argv[1] + colored_output = int(argv[-1]) + + if len(argv) == 3: height = curses.LINES; width = curses.COLS + else: height = curses.LINES - 5; width = curses.COLS + + self.media = curses.newwin(height, width, beginY, beginX) + self.media.border(0, 0, 0, 0, 0, 0, 0, 0) + + if len(argv) == 3: subfile = '' + else: + beginX = 0; beginY = curses.LINES - 5 + height = 5; width = curses.COLS + self.captions = curses.newwin(height, width, beginY, beginX) + self.captions.border(0, 0, 0, 0, 0, 0, 0, 0) + subfile = argv[2] + + self.read_media_sub(vidfile,colored_output,subfile) + + self.media.getch() + +def run(stdscr): + obj = AMP() + obj.main(sys.argv) + +curses.wrapper(run) \ No newline at end of file