From 1d17784220b6b65f94de680465a1012d4bc77b91 Mon Sep 17 00:00:00 2001 From: chfour Date: Thu, 10 Mar 2022 21:21:19 +0100 Subject: [PATCH 01/17] whitespace corrections changed 2-space indents to the standard 4-spaces, added spaces after hashes in comments, there was also an unnecessary space after an equals sign in convert.py in processFrame() --- convert.py | 344 ++++++++++++++++++++++---------------------- imgToTextColor.py | 70 ++++----- videoToTextColor.py | 71 +++++---- 3 files changed, 241 insertions(+), 244 deletions(-) diff --git a/convert.py b/convert.py index e829004..e61116b 100644 --- a/convert.py +++ b/convert.py @@ -6,7 +6,7 @@ aspect_ratio = 16 / 9 -#Dimensions of the output in terminal characters +# Dimensions of the output in terminal characters width = 80 height = int(width / (2 * aspect_ratio)) @@ -21,7 +21,7 @@ frames = [] -#Our characters, and their approximate brightness values +# Our characters, and their approximate brightness values charSet = " ,(S#g@" levels = [0.000, 1.060, 2.167, 3.036, 3.977, 4.730, 6.000] numChrs = len(charSet) @@ -29,43 +29,43 @@ # Converts a greyscale video frame into a dithered 7-color frame def processFrame(scaled): - reduced = scaled * 6. / 255 - - out = np.zeros((height, width), dtype= np.int8) - - line = '' - for y in range(height): - for x in range(width): - level = min(6, max(0, int(reduced[y, x]))) - - error = reduced[y, x] - levels[level] - - err16 = error / 16 - - if (x + 1) < width: - reduced[y , x + 1] += 7 * err16 - if (y + 1) < height: - reduced[y + 1, x ] += 5 * err16 - - if (x + 1) < width: - reduced[y + 1, x + 1] += 1 * err16 - if (x - 1) > 0: - reduced[y + 1, x - 1] += 3 * err16 - - out[y, x] = level - - return out + reduced = scaled * 6. / 255 + + out = np.zeros((height, width), dtype=np.int8) + + line = '' + for y in range(height): + for x in range(width): + level = min(6, max(0, int(reduced[y, x]))) + + error = reduced[y, x] - levels[level] + + err16 = error / 16 + + if (x + 1) < width: + reduced[y , x + 1] += 7 * err16 + if (y + 1) < height: + reduced[y + 1, x ] += 5 * err16 + + if (x + 1) < width: + reduced[y + 1, x + 1] += 1 * err16 + if (x - 1) > 0: + reduced[y + 1, x - 1] += 3 * err16 + + out[y, x] = level + + return out # Prints out a frame in ASCII def toStr(frame): - line = '' - - for y in range(height): - for x in range(width): - line += charSet[frame[y, x]] - line += '\n' - - return line + line = '' + + for y in range(height): + for x in range(width): + line += charSet[frame[y, x]] + line += '\n' + + return line # Compute the prediction matrix for each character combination # Each row in this matrix corresponds with a character, and lists @@ -74,169 +74,169 @@ def toStr(frame): # We also convert the provided frame to this new markov encoding, and provide # the count of each prediction rank to be passed to the huffman encoding def computeMarkov(frame): - mat = np.zeros((numChrs, numChrs)).astype(np.uint16) + mat = np.zeros((numChrs, numChrs)).astype(np.uint16) - h, w = frame.shape + h, w = frame.shape - prevChar = 0 + prevChar = 0 - for y in range(h): - for x in range(w): - char = frame[y, x] + for y in range(h): + for x in range(w): + char = frame[y, x] - mat[prevChar, char] += 1 + mat[prevChar, char] += 1 - prevChar = char - - ranks = np.zeros((numChrs, numChrs)).astype(np.uint16) - for i in range(numChrs): - ranks[i][mat[i].argsort()] = 6 - np.arange(numChrs) + prevChar = char + + ranks = np.zeros((numChrs, numChrs)).astype(np.uint16) + for i in range(numChrs): + ranks[i][mat[i].argsort()] = 6 - np.arange(numChrs) - cnt = np.zeros(numChrs).astype(np.uint16) + cnt = np.zeros(numChrs).astype(np.uint16) - out = np.zeros_like(frame) - prevChar = 0 - for y in range(h): - for x in range(w): - char = frame[y, x] + out = np.zeros_like(frame) + prevChar = 0 + for y in range(h): + for x in range(w): + char = frame[y, x] - out[y, x] = ranks[prevChar, char] - cnt[out[y, x]] += 1 + out[y, x] = ranks[prevChar, char] + cnt[out[y, x]] += 1 - prevChar = char - - return out, ranks, cnt + prevChar = char + + return out, ranks, cnt # Computes Huffman encodings based on the counts of each number in the frame def computeHuffman(cnts): - codes = [] - sizes = [] - tree = [] - for i in range(len(cnts)): - codes.append('') - sizes.append((cnts[i], [i], i)) - tree.append((i, i)) - - sizes = sorted(sizes, reverse = True) - - while(len(sizes) > 1): - # Take the two least frequent entries - right = sizes.pop() - left = sizes.pop() - - (lnum, lchars, ltree) = left - (rnum, rchars, rtree) = right - - # Add a new tree node - tree.append((ltree, rtree)) - - # Update the encodings - for char in lchars: - codes[char] = '0' + codes[char] - for char in rchars: - codes[char] = '1' + codes[char] - - # Merge these entries - new = (lnum + rnum, lchars + rchars, len(tree) - 1) - - # Find the position in the list to inser these entries - for insertPos in range(len(sizes) + 1): - # Append if we hit the end of the list - if(insertPos == len(sizes)): - sizes.append(new) - break - - cnt, _, _ = sizes[insertPos] - - if(cnt <= lnum + rnum): - sizes.insert(insertPos, new) - break - - return codes, tree + codes = [] + sizes = [] + tree = [] + for i in range(len(cnts)): + codes.append('') + sizes.append((cnts[i], [i], i)) + tree.append((i, i)) + + sizes = sorted(sizes, reverse = True) + + while(len(sizes) > 1): + # Take the two least frequent entries + right = sizes.pop() + left = sizes.pop() + + (lnum, lchars, ltree) = left + (rnum, rchars, rtree) = right + + # Add a new tree node + tree.append((ltree, rtree)) + + # Update the encodings + for char in lchars: + codes[char] = '0' + codes[char] + for char in rchars: + codes[char] = '1' + codes[char] + + # Merge these entries + new = (lnum + rnum, lchars + rchars, len(tree) - 1) + + # Find the position in the list to inser these entries + for insertPos in range(len(sizes) + 1): + # Append if we hit the end of the list + if(insertPos == len(sizes)): + sizes.append(new) + break + + cnt, _, _ = sizes[insertPos] + + if(cnt <= lnum + rnum): + sizes.insert(insertPos, new) + break + + return codes, tree # Take a markov frame and an array of huffman encodings, and create an array of # bytes corresponding to the compressed frame def convertHuffman(markovFrame, codes): - out = '' + out = '' - h, w = frame.shape + h, w = frame.shape - for y in range(h): - for x in range(w): - out = out + codes[markovFrame[y, x]] - - # Pad this bit-string to be byte-aligned - padding = (8 - (len(out) % 8)) % 8 - out += ("0" * padding) + for y in range(h): + for x in range(w): + out = out + codes[markovFrame[y, x]] + + # Pad this bit-string to be byte-aligned + padding = (8 - (len(out) % 8)) % 8 + out += ("0" * padding) - # Convert each octet to a char - compressed = [] - for i in range(0, len(out), 8): - byte = out[i:i+8] - char = 0 - for bit in range(8): - char *= 2 - if byte[bit] == "1": - char += 1 + # Convert each octet to a char + compressed = [] + for i in range(0, len(out), 8): + byte = out[i:i+8] + char = 0 + for bit in range(8): + char *= 2 + if byte[bit] == "1": + char += 1 - compressed.append(char) + compressed.append(char) - return compressed + return compressed # Converts a rank matrix into a binary format to be stored in the output file def encodeMatrix(ranks): - out = [] + out = [] - for row in ranks: - encoding = 0 + for row in ranks: + encoding = 0 - fact = 1 - idxs = list(range(len(charSet))) + fact = 1 + idxs = list(range(len(charSet))) - for rank in range(len(charSet)): - rank = list(row).index(rank) - encoding += idxs.index(rank) * fact + for rank in range(len(charSet)): + rank = list(row).index(rank) + encoding += idxs.index(rank) * fact - fact *= len(idxs) - idxs.remove(rank) - - low_byte = int(encoding) % 256 - high_byte = (encoding - low_byte) // 256 - - out.append(high_byte) - out.append(low_byte) + fact *= len(idxs) + idxs.remove(rank) + + low_byte = int(encoding) % 256 + high_byte = (encoding - low_byte) // 256 + + out.append(high_byte) + out.append(low_byte) - return out + return out # Converts the huffman tree into a binary format to be stored in the output file def encodeTree(tree): - tree = tree[len(charSet):] + tree = tree[len(charSet):] - out = [] + out = [] - for (l, r) in tree: - out.append(l * 16 + r) + for (l, r) in tree: + out.append(l * 16 + r) - return out + return out # Load all frames into memory, then convert them to greyscale and resize them to # our terminal dimensions vidFrames = [] while(cap.isOpened()): - if (len(vidFrames) % 500) == 0: - print('Loading frame %i' % len(vidFrames)) - - # Skip frames to reach target framerate - for i in range(int(src_FPS / dest_FPS)): - ret, frame = cap.read() - - if frame is None: - break - - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - scaled = cv2.resize(gray, (width, height)) - - vidFrames.append(scaled) + if (len(vidFrames) % 500) == 0: + print('Loading frame %i' % len(vidFrames)) + + # Skip frames to reach target framerate + for i in range(int(src_FPS / dest_FPS)): + ret, frame = cap.read() + + if frame is None: + break + + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + scaled = cv2.resize(gray, (width, height)) + + vidFrames.append(scaled) # Compute dithering for all frames in parallel print('Dithering Frames') @@ -248,25 +248,25 @@ def encodeTree(tree): size = 0 with open('data', 'wb') as filehandle: - for frame in frames: - markovFrame, ranks, cnts = computeMarkov(frame) + for frame in frames: + markovFrame, ranks, cnts = computeMarkov(frame) - codes, tree = computeHuffman(cnts) - chars = convertHuffman(markovFrame, codes) + codes, tree = computeHuffman(cnts) + chars = convertHuffman(markovFrame, codes) - matrixData = encodeMatrix(ranks) - treeData = encodeTree(tree) + matrixData = encodeMatrix(ranks) + treeData = encodeTree(tree) - filehandle.write(bytearray(matrixData)) - filehandle.write(bytearray(treeData)) - filehandle.write(bytearray(chars)) + filehandle.write(bytearray(matrixData)) + filehandle.write(bytearray(treeData)) + filehandle.write(bytearray(chars)) - size += len(matrixData) + len(treeData) + len(chars) + size += len(matrixData) + len(treeData) + len(chars) # Print the size of the output file in human-readable form if size > 1048576: - print('%.1f MB' % (size / 1048576)) + print('%.1f MB' % (size / 1048576)) elif size > 1024: - print('%.1f KB' % (size / 1024)) + print('%.1f KB' % (size / 1024)) else: - print('%i B' % (size)) \ No newline at end of file + print('%i B' % (size)) \ No newline at end of file diff --git a/imgToTextColor.py b/imgToTextColor.py index 23167f5..0172a6d 100644 --- a/imgToTextColor.py +++ b/imgToTextColor.py @@ -3,60 +3,60 @@ import pickle import sys -#Width of the output in terminal characters +# Width of the output in terminal characters width = 80 height = 1 -#Our characters, and their approximate brightness values +# Our characters, and their approximate brightness values charSet = " ,(S#g@@g#S(, " # Generates a character sequence to set the foreground and background colors def setColor (bg, fg): - return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) + return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) black = setColor(16, 16) # Load in color lookup table data -lerped = pickle.load( open( "colors.pkl", "rb" ) ) +lerped = pickle.load(open("colors.pkl", "rb")) LUT = np.load("LUT.npy") # Convert an RGB image to a stream of text with ANSI color codes def convertImg(img): - line = "" - - for row in img: - for color in row: - color = np.round(color).astype(int) - - b, g, r = color[0], color[1], color[2] - - # Lookup the color index in the RGB lookup table - idx = LUT[b, g, r] - - # Get the ANSI color codes and lerp character - bg, fg, lerp, rgb = lerped[idx] - - char = charSet[lerp] - - line += "%s%c" % (setColor(bg, fg), char) - # End each line with a black background to avoid color fringe - line += "%s\n" % black - - # Move the cursor back to the top of the frame to prevent rolling - line += "\u001b[%iD\u001b[%iA" % (width, height + 1) - return line + line = "" + + for row in img: + for color in row: + color = np.round(color).astype(int) + + b, g, r = color[0], color[1], color[2] + + # Lookup the color index in the RGB lookup table + idx = LUT[b, g, r] + + # Get the ANSI color codes and lerp character + bg, fg, lerp, rgb = lerped[idx] + + char = charSet[lerp] + + line += "%s%c" % (setColor(bg, fg), char) + # End each line with a black background to avoid color fringe + line += "%s\n" % black + + # Move the cursor back to the top of the frame to prevent rolling + line += "\u001b[%iD\u001b[%iA" % (width, height + 1) + return line if len(sys.argv) == 2: - img = cv2.imread(sys.argv[1]) + img = cv2.imread(sys.argv[1]) - # Match the aspect ratio to that of the provided image - src_height, src_width, _ = img.shape + # Match the aspect ratio to that of the provided image + src_height, src_width, _ = img.shape - aspect_ratio = src_width / src_height - height = int(width / (2 * aspect_ratio)) + aspect_ratio = src_width / src_height + height = int(width / (2 * aspect_ratio)) - img = cv2.resize(img, (width, height)) - print(convertImg(img)) + img = cv2.resize(img, (width, height)) + print(convertImg(img)) else: - print("Expected image file as argument.") \ No newline at end of file + print("Expected image file as argument.") diff --git a/videoToTextColor.py b/videoToTextColor.py index c891ae3..e42367f 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -5,64 +5,61 @@ aspect_ratio = 16 / 9 -#Dimensions of the output in terminal characters +# Dimensions of the output in terminal characters width = 80 height = int(width / (2 * aspect_ratio)) - - -#Our characters, and their approximate brightness values +# Our characters, and their approximate brightness values charSet = " ,(S#g@@g#S(, " # Generates a character sequence to set the foreground and background colors def setColor (bg, fg): - return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) + return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) black = setColor(16, 16) # Load in color lookup table data -lerped = pickle.load( open( "colors.pkl", "rb" ) ) +lerped = pickle.load(open("colors.pkl", "rb")) LUT = np.load("LUT.npy") # Convert an RGB image to a stream of text with ANSI color codes def convertImg(img): - line = "" - - for row in img: - for color in row: - color = np.round(color).astype(int) - - b, g, r = color[0], color[1], color[2] - - # Lookup the color index in the RGB lookup table - idx = LUT[b, g, r] - - # Get the ANSI color codes and lerp character - bg, fg, lerp, rgb = lerped[idx] - - char = charSet[lerp] - - line += "%s%c" % (setColor(bg, fg), char) - # End each line with a black background to avoid color fringe - line += "%s\n" % black - - # Move the cursor back to the top of the frame to prevent rolling - line += "\u001b[%iD\u001b[%iA" % (width, height + 1) - return line + line = "" + for row in img: + for color in row: + color = np.round(color).astype(int) + + b, g, r = color[0], color[1], color[2] + + # Lookup the color index in the RGB lookup table + idx = LUT[b, g, r] + + # Get the ANSI color codes and lerp character + bg, fg, lerp, rgb = lerped[idx] + + char = charSet[lerp] + + line += "%s%c" % (setColor(bg, fg), char) + # End each line with a black background to avoid color fringe + line += "%s\n" % black + + # Move the cursor back to the top of the frame to prevent rolling + line += "\u001b[%iD\u001b[%iA" % (width, height + 1) + return line if len(sys.argv) == 2: - cap = cv2.VideoCapture(sys.argv[1]) + cap = cv2.VideoCapture(sys.argv[1]) - while(cap.isOpened()): - ret, frame = cap.read() + while(cap.isOpened()): + ret, frame = cap.read() - if frame is None: - break - - img = cv2.resize(frame, (width, height)) - print(convertImg(img)) + if frame is None: + break + + img = cv2.resize(frame, (width, height)) + print(convertImg(img)) else: print("Expected video file as argument.") From b67bee25e3fb82df49b7677b6cbdba1bbc01454e Mon Sep 17 00:00:00 2001 From: chfour Date: Thu, 10 Mar 2022 21:23:21 +0100 Subject: [PATCH 02/17] add a gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f5b8d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +/data +/data.h +/playback +/playback.exe +/vid.mp4 From ba5c201aec2a9f9879fc9252fbeebacc22d3033c Mon Sep 17 00:00:00 2001 From: chfour Date: Thu, 10 Mar 2022 21:25:14 +0100 Subject: [PATCH 03/17] add requirements.txt (should...? work everywhere) --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8e88864 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +joblib +opencv-python From a3c33114b8a330cdf321882bf0d2c1aed17f77e5 Mon Sep 17 00:00:00 2001 From: chfour Date: Thu, 10 Mar 2022 21:28:51 +0100 Subject: [PATCH 04/17] load colors.pkl from `with` close the file automatically after it's loaded --- imgToTextColor.py | 3 ++- videoToTextColor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/imgToTextColor.py b/imgToTextColor.py index 0172a6d..32f0ed4 100644 --- a/imgToTextColor.py +++ b/imgToTextColor.py @@ -18,7 +18,8 @@ def setColor (bg, fg): black = setColor(16, 16) # Load in color lookup table data -lerped = pickle.load(open("colors.pkl", "rb")) +with open("colors.pkl", "rb") as f: + lerped = pickle.load(f) LUT = np.load("LUT.npy") # Convert an RGB image to a stream of text with ANSI color codes diff --git a/videoToTextColor.py b/videoToTextColor.py index e42367f..4d51680 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -20,7 +20,8 @@ def setColor (bg, fg): black = setColor(16, 16) # Load in color lookup table data -lerped = pickle.load(open("colors.pkl", "rb")) +with open("colors.pkl", "rb") as f: + lerped = pickle.load(f) LUT = np.load("LUT.npy") # Convert an RGB image to a stream of text with ANSI color codes From ad0082b50f9cff167a4be4bfd115fdb64c75f47d Mon Sep 17 00:00:00 2001 From: chfour Date: Thu, 10 Mar 2022 21:30:34 +0100 Subject: [PATCH 05/17] rename readme.md to README.md --- readme.md => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename readme.md => README.md (100%) diff --git a/readme.md b/README.md similarity index 100% rename from readme.md rename to README.md From 79ee470df73cd621081c09add9d2e2d91f565365 Mon Sep 17 00:00:00 2001 From: chfour Date: Thu, 10 Mar 2022 21:32:38 +0100 Subject: [PATCH 06/17] Markdown formatting fixes --- README.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f4e9b38..f72ed60 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Color Video to Text Conversion -A few tools to convert video and images into ASCII art in an ANSI terminal. These tools support color output using the ANSI 256 color set, as well as the creation of a self-contained playback executable for video converted to text, with compression able to fit 4 minutes of 80 column 15 FPS video onto a single floppy disk! +A few tools to convert video and images into ASCII art in an ANSI terminal. These tools support color output using the ANSI 256 color set, +as well as the creation of a self-contained playback executable for video converted to text, with compression able to fit 4 minutes of +80 column 15 FPS video onto a single floppy disk! - ## Check out [this video](https://www.youtube.com/watch?v=uGoR3ZYZqjc) for more information and to see sample output for video to text conversion. +## Check out [this video](https://www.youtube.com/watch?v=uGoR3ZYZqjc) for more information and to see sample output for video to text conversion. ![Screenshot](screenshot.png) @@ -10,9 +12,10 @@ A sample image converted to text and printed to the terminal. --- -**Note:** To run these programs, you will need Python 3 installed, alongside NumPy and OpenCV (for image io). +**Note:** To run these programs, you will need Python 3 installed, alongside NumPy and OpenCV (for image I/O). ## Displaying Images as Text + The python script imageToTextColor.py will print an image file provided as an argument as text to the terminal. `python3 imageToTextColor.py your_image_here.jpg` @@ -20,22 +23,25 @@ The python script imageToTextColor.py will print an image file provided as an ar The width of the output can be configured in the header of the python file. ## Displaying Videos as Text + The python script videoToTextColor.py will play back a video provided as an argument as text to the terminal. `python3 videoToTextColor.py your_video_here.mp4` The width and aspect ratio of the output can be configured in the header of the python file. - ## Creating Video Playback Executables -The provided makefile allows building programs which will play the compressed text encoding of the video stored in the executable. The target video should be named `vid.mp4`, otherwise the path to the video can be changed in the header of convert.py. -To build for Linux targets (using GCC) run +The provided makefile allows building programs which will play the compressed text encoding of the video stored in the executable. +The target video should be named `vid.mp4`, otherwise the path to the video can be changed in the header of convert.py. + +To build for Linux targets (using GCC) run: `make playback` -Otherwise to build for Windows targets (using MinGW) run +Otherwise to build for Windows targets (using MinGW) run: `make playback.exe` -Other aspects of the video encoding, such as character width and framerate can be adjusted in both convert.py and playback.c. **Be sure to update these parameters in both files.** +Other aspects of the video encoding, such as character width and framerate can be adjusted in both convert.py and playback.c. +**Be sure to update these parameters in both files.** From 05f88eedbe89a6e8c18f90fb0bafbd4fb6d78ca1 Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 14:03:01 +0100 Subject: [PATCH 07/17] rename constants to CAPITALIZED_WITH_UNDERSCORES --- convert.py | 62 ++++++++++++++++++++++----------------------- imgToTextColor.py | 22 ++++++++-------- videoToTextColor.py | 24 +++++++++--------- 3 files changed, 53 insertions(+), 55 deletions(-) diff --git a/convert.py b/convert.py index e61116b..e26585a 100644 --- a/convert.py +++ b/convert.py @@ -4,50 +4,48 @@ import multiprocessing from joblib import Parallel, delayed -aspect_ratio = 16 / 9 +ASPECT_RATIO = 16 / 9 # Dimensions of the output in terminal characters -width = 80 -height = int(width / (2 * aspect_ratio)) +WIDTH = 80 +HEIGHT = int(WIDTH / (2 * ASPECT_RATIO)) # Framerate of the source and output video -src_FPS = 30 -dest_FPS = 15 +SRC_FPS = 30 +DEST_FPS = 15 -num_cores = multiprocessing.cpu_count() +NUM_CORES = multiprocessing.cpu_count() cap = cv2.VideoCapture('vid.mp4') frames = [] - # Our characters, and their approximate brightness values -charSet = " ,(S#g@" -levels = [0.000, 1.060, 2.167, 3.036, 3.977, 4.730, 6.000] -numChrs = len(charSet) +CHARSET = " ,(S#g@" +LEVELS = [0.000, 1.060, 2.167, 3.036, 3.977, 4.730, 6.000] +NUMCHARS = len(CHARSET) # Converts a greyscale video frame into a dithered 7-color frame def processFrame(scaled): reduced = scaled * 6. / 255 - out = np.zeros((height, width), dtype=np.int8) + out = np.zeros((HEIGHT, WIDTH), dtype=np.int8) - line = '' - for y in range(height): - for x in range(width): + for y in range(HEIGHT): + for x in range(WIDTH): level = min(6, max(0, int(reduced[y, x]))) - error = reduced[y, x] - levels[level] + error = reduced[y, x] - LEVELS[level] err16 = error / 16 - if (x + 1) < width: + if (x + 1) < WIDTH: reduced[y , x + 1] += 7 * err16 - if (y + 1) < height: + if (y + 1) < HEIGHT: reduced[y + 1, x ] += 5 * err16 - if (x + 1) < width: + if (x + 1) < WIDTH: reduced[y + 1, x + 1] += 1 * err16 if (x - 1) > 0: reduced[y + 1, x - 1] += 3 * err16 @@ -60,9 +58,9 @@ def processFrame(scaled): def toStr(frame): line = '' - for y in range(height): - for x in range(width): - line += charSet[frame[y, x]] + for y in range(HEIGHT): + for x in range(WIDTH): + line += CHARSET[frame[y, x]] line += '\n' return line @@ -74,7 +72,7 @@ def toStr(frame): # We also convert the provided frame to this new markov encoding, and provide # the count of each prediction rank to be passed to the huffman encoding def computeMarkov(frame): - mat = np.zeros((numChrs, numChrs)).astype(np.uint16) + mat = np.zeros((NUMCHARS, NUMCHARS)).astype(np.uint16) h, w = frame.shape @@ -88,11 +86,11 @@ def computeMarkov(frame): prevChar = char - ranks = np.zeros((numChrs, numChrs)).astype(np.uint16) - for i in range(numChrs): - ranks[i][mat[i].argsort()] = 6 - np.arange(numChrs) + ranks = np.zeros((NUMCHARS, NUMCHARS)).astype(np.uint16) + for i in range(NUMCHARS): + ranks[i][mat[i].argsort()] = 6 - np.arange(NUMCHARS) - cnt = np.zeros(numChrs).astype(np.uint16) + cnt = np.zeros(NUMCHARS).astype(np.uint16) out = np.zeros_like(frame) prevChar = 0 @@ -191,9 +189,9 @@ def encodeMatrix(ranks): encoding = 0 fact = 1 - idxs = list(range(len(charSet))) + idxs = list(range(len(CHARSET))) - for rank in range(len(charSet)): + for rank in range(len(CHARSET)): rank = list(row).index(rank) encoding += idxs.index(rank) * fact @@ -210,7 +208,7 @@ def encodeMatrix(ranks): # Converts the huffman tree into a binary format to be stored in the output file def encodeTree(tree): - tree = tree[len(charSet):] + tree = tree[len(CHARSET):] out = [] @@ -227,20 +225,20 @@ def encodeTree(tree): print('Loading frame %i' % len(vidFrames)) # Skip frames to reach target framerate - for i in range(int(src_FPS / dest_FPS)): + for i in range(int(SRC_FPS / DEST_FPS)): ret, frame = cap.read() if frame is None: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - scaled = cv2.resize(gray, (width, height)) + scaled = cv2.resize(gray, (WIDTH, HEIGHT)) vidFrames.append(scaled) # Compute dithering for all frames in parallel print('Dithering Frames') -frames = Parallel(n_jobs=num_cores)(delayed(processFrame)(i) for i in vidFrames) +frames = Parallel(n_jobs=NUM_CORES)(delayed(processFrame)(i) for i in vidFrames) # Compute markov and huffman encoding for all frames print('Encoding Frames') diff --git a/imgToTextColor.py b/imgToTextColor.py index 32f0ed4..ed0f0e5 100644 --- a/imgToTextColor.py +++ b/imgToTextColor.py @@ -4,22 +4,22 @@ import sys # Width of the output in terminal characters -width = 80 -height = 1 +WIDTH = 80 +HEIGHT = 1 # Our characters, and their approximate brightness values -charSet = " ,(S#g@@g#S(, " +CHARSET = " ,(S#g@@g#S(, " # Generates a character sequence to set the foreground and background colors def setColor (bg, fg): return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) -black = setColor(16, 16) +BLACK = setColor(16, 16) # Load in color lookup table data with open("colors.pkl", "rb") as f: - lerped = pickle.load(f) + LERPED = pickle.load(f) LUT = np.load("LUT.npy") # Convert an RGB image to a stream of text with ANSI color codes @@ -36,16 +36,16 @@ def convertImg(img): idx = LUT[b, g, r] # Get the ANSI color codes and lerp character - bg, fg, lerp, rgb = lerped[idx] + bg, fg, lerp, rgb = LERPED[idx] - char = charSet[lerp] + char = CHARSET[lerp] line += "%s%c" % (setColor(bg, fg), char) # End each line with a black background to avoid color fringe - line += "%s\n" % black + line += "%s\n" % BLACK # Move the cursor back to the top of the frame to prevent rolling - line += "\u001b[%iD\u001b[%iA" % (width, height + 1) + line += "\u001b[%iD\u001b[%iA" % (WIDTH, HEIGHT + 1) return line if len(sys.argv) == 2: @@ -55,9 +55,9 @@ def convertImg(img): src_height, src_width, _ = img.shape aspect_ratio = src_width / src_height - height = int(width / (2 * aspect_ratio)) + HEIGHT = int(WIDTH / (2 * aspect_ratio)) - img = cv2.resize(img, (width, height)) + img = cv2.resize(img, (WIDTH, HEIGHT)) print(convertImg(img)) else: print("Expected image file as argument.") diff --git a/videoToTextColor.py b/videoToTextColor.py index 4d51680..6123714 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -3,25 +3,25 @@ import pickle import sys -aspect_ratio = 16 / 9 +ASPECT_RATIO = 16 / 9 # Dimensions of the output in terminal characters -width = 80 -height = int(width / (2 * aspect_ratio)) +WIDTH = 80 +HEIGHT = int(WIDTH / (2 * ASPECT_RATIO)) # Our characters, and their approximate brightness values -charSet = " ,(S#g@@g#S(, " +CHARSET = " ,(S#g@@g#S(, " # Generates a character sequence to set the foreground and background colors -def setColor (bg, fg): +def setColor(bg, fg): return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) -black = setColor(16, 16) +BLACK = setColor(16, 16) # Load in color lookup table data with open("colors.pkl", "rb") as f: - lerped = pickle.load(f) + LERPED = pickle.load(f) LUT = np.load("LUT.npy") # Convert an RGB image to a stream of text with ANSI color codes @@ -37,16 +37,16 @@ def convertImg(img): idx = LUT[b, g, r] # Get the ANSI color codes and lerp character - bg, fg, lerp, rgb = lerped[idx] + bg, fg, lerp, rgb = LERPED[idx] - char = charSet[lerp] + char = CHARSET[lerp] line += "%s%c" % (setColor(bg, fg), char) # End each line with a black background to avoid color fringe - line += "%s\n" % black + line += "%s\n" % BLACK # Move the cursor back to the top of the frame to prevent rolling - line += "\u001b[%iD\u001b[%iA" % (width, height + 1) + line += "\u001b[%iD\u001b[%iA" % (WIDTH, HEIGHT + 1) return line @@ -59,7 +59,7 @@ def convertImg(img): if frame is None: break - img = cv2.resize(frame, (width, height)) + img = cv2.resize(frame, (WIDTH, HEIGHT)) print(convertImg(img)) else: print("Expected video file as argument.") From 197f87de5cc9b8fd2cf3aa78d501e1644d093bab Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 14:32:51 +0100 Subject: [PATCH 08/17] add mention of requirements.txt to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 242d341..c688275 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ A sample image converted to text and printed to the terminal. **Note:** To run these programs, you will need Python 3 installed, alongside NumPy and OpenCV (for image I/O). +Install these dependencies by running `pip install -r requirements.txt`. + ## Displaying Images as Text The python script imageToTextColor.py will print an image file provided as an argument as text to the terminal. From f5b0380cdebf90b9f669e8ff92bf8a34ccc622fa Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 14:37:40 +0100 Subject: [PATCH 09/17] remove unnecessary import from convert.py --- convert.py | 1 - 1 file changed, 1 deletion(-) diff --git a/convert.py b/convert.py index e26585a..ce3d8c6 100644 --- a/convert.py +++ b/convert.py @@ -1,6 +1,5 @@ import cv2 import numpy as np -import time import multiprocessing from joblib import Parallel, delayed From 9deef4f4ba5fd5d4dfc923ef5092d1dbf07706b0 Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 14:40:01 +0100 Subject: [PATCH 10/17] fix one indent in videoToTextColor.py --- videoToTextColor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/videoToTextColor.py b/videoToTextColor.py index 6123714..59f2c57 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -62,5 +62,5 @@ def convertImg(img): img = cv2.resize(frame, (WIDTH, HEIGHT)) print(convertImg(img)) else: - print("Expected video file as argument.") + print("Expected video file as argument.") From 8a24736e253aeb53f12167f326c95e9220b7911e Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 14:57:48 +0100 Subject: [PATCH 11/17] improved syntax and added function docstrings --- convert.py | 62 ++++++++++++++++++++++++++++++--------------- imgToTextColor.py | 9 +++++-- videoToTextColor.py | 11 +++++--- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/convert.py b/convert.py index ce3d8c6..310add1 100644 --- a/convert.py +++ b/convert.py @@ -25,8 +25,11 @@ NUMCHARS = len(CHARSET) -# Converts a greyscale video frame into a dithered 7-color frame def processFrame(scaled): + """ + Converts a greyscale video frame into a dithered 7-color frame + """ + reduced = scaled * 6. / 255 out = np.zeros((HEIGHT, WIDTH), dtype=np.int8) @@ -53,8 +56,11 @@ def processFrame(scaled): return out -# Prints out a frame in ASCII def toStr(frame): + """ + Prints out a frame in ASCII + """ + line = '' for y in range(HEIGHT): @@ -64,13 +70,16 @@ def toStr(frame): return line -# Compute the prediction matrix for each character combination -# Each row in this matrix corresponds with a character, and lists -# in decreasing order, the next most likely character to follow this one -# -# We also convert the provided frame to this new markov encoding, and provide -# the count of each prediction rank to be passed to the huffman encoding def computeMarkov(frame): + """ + Compute the prediction matrix for each character combination + Each row in this matrix corresponds with a character, and lists + in decreasing order, the next most likely character to follow this one + + We also convert the provided frame to this new markov encoding, and provide + the count of each prediction rank to be passed to the huffman encoding + """ + mat = np.zeros((NUMCHARS, NUMCHARS)).astype(np.uint16) h, w = frame.shape @@ -104,8 +113,11 @@ def computeMarkov(frame): return out, ranks, cnt -# Computes Huffman encodings based on the counts of each number in the frame def computeHuffman(cnts): + """ + Computes Huffman encodings based on the counts of each number in the frame + """ + codes = [] sizes = [] tree = [] @@ -114,9 +126,9 @@ def computeHuffman(cnts): sizes.append((cnts[i], [i], i)) tree.append((i, i)) - sizes = sorted(sizes, reverse = True) + sizes = sorted(sizes, reverse=True) - while(len(sizes) > 1): + while len(sizes) > 1: # Take the two least frequent entries right = sizes.pop() left = sizes.pop() @@ -139,21 +151,25 @@ def computeHuffman(cnts): # Find the position in the list to inser these entries for insertPos in range(len(sizes) + 1): # Append if we hit the end of the list - if(insertPos == len(sizes)): + if insertPos == len(sizes): sizes.append(new) break cnt, _, _ = sizes[insertPos] - if(cnt <= lnum + rnum): + if cnt <= lnum + rnum: sizes.insert(insertPos, new) break return codes, tree -# Take a markov frame and an array of huffman encodings, and create an array of -# bytes corresponding to the compressed frame + def convertHuffman(markovFrame, codes): + """ + Take a markov frame and an array of huffman encodings, and create an array of + bytes corresponding to the compressed frame + """ + out = '' h, w = frame.shape @@ -164,7 +180,7 @@ def convertHuffman(markovFrame, codes): # Pad this bit-string to be byte-aligned padding = (8 - (len(out) % 8)) % 8 - out += ("0" * padding) + out += "0" * padding # Convert each octet to a char compressed = [] @@ -180,8 +196,11 @@ def convertHuffman(markovFrame, codes): return compressed -# Converts a rank matrix into a binary format to be stored in the output file def encodeMatrix(ranks): + """ + Converts a rank matrix into a binary format to be stored in the output file + """ + out = [] for row in ranks: @@ -205,8 +224,11 @@ def encodeMatrix(ranks): return out -# Converts the huffman tree into a binary format to be stored in the output file def encodeTree(tree): + """ + Converts the huffman tree into a binary format to be stored in the output file + """ + tree = tree[len(CHARSET):] out = [] @@ -219,7 +241,7 @@ def encodeTree(tree): # Load all frames into memory, then convert them to greyscale and resize them to # our terminal dimensions vidFrames = [] -while(cap.isOpened()): +while cap.isOpened(): if (len(vidFrames) % 500) == 0: print('Loading frame %i' % len(vidFrames)) @@ -266,4 +288,4 @@ def encodeTree(tree): elif size > 1024: print('%.1f KB' % (size / 1024)) else: - print('%i B' % (size)) \ No newline at end of file + print('%i B' % (size)) diff --git a/imgToTextColor.py b/imgToTextColor.py index ed0f0e5..d4c582b 100644 --- a/imgToTextColor.py +++ b/imgToTextColor.py @@ -11,8 +11,10 @@ # Our characters, and their approximate brightness values CHARSET = " ,(S#g@@g#S(, " -# Generates a character sequence to set the foreground and background colors def setColor (bg, fg): + """ + Generates a character sequence to set the foreground and background colors + """ return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) BLACK = setColor(16, 16) @@ -22,8 +24,11 @@ def setColor (bg, fg): LERPED = pickle.load(f) LUT = np.load("LUT.npy") -# Convert an RGB image to a stream of text with ANSI color codes def convertImg(img): + """ + Convert an RGB image to a stream of text with ANSI color codes + """ + line = "" for row in img: diff --git a/videoToTextColor.py b/videoToTextColor.py index 59f2c57..c0ce12b 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -13,8 +13,10 @@ # Our characters, and their approximate brightness values CHARSET = " ,(S#g@@g#S(, " -# Generates a character sequence to set the foreground and background colors def setColor(bg, fg): + """ + Generates a character sequence to set the foreground and background colors + """ return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) BLACK = setColor(16, 16) @@ -24,8 +26,11 @@ def setColor(bg, fg): LERPED = pickle.load(f) LUT = np.load("LUT.npy") -# Convert an RGB image to a stream of text with ANSI color codes def convertImg(img): + """ + Generates a character sequence to set the foreground and background colors + """ + line = "" for row in img: for color in row: @@ -53,7 +58,7 @@ def convertImg(img): if len(sys.argv) == 2: cap = cv2.VideoCapture(sys.argv[1]) - while(cap.isOpened()): + while cap.isOpened(): ret, frame = cap.read() if frame is None: From 2dcc87d91b3da1098e8884c78af61fb81c668ad0 Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 15:04:47 +0100 Subject: [PATCH 12/17] import convertImg() from imgToText in vidToText in videoToTextColor.py, import setColor() and convertImg() from imgToTextColor.py instead of copying them --- imgToTextColor.py | 57 +++++++++++++++-------------- videoToTextColor.py | 89 ++++++++++++++------------------------------- 2 files changed, 57 insertions(+), 89 deletions(-) diff --git a/imgToTextColor.py b/imgToTextColor.py index d4c582b..76cc051 100644 --- a/imgToTextColor.py +++ b/imgToTextColor.py @@ -1,15 +1,4 @@ import numpy as np -import cv2 -import pickle -import sys - -# Width of the output in terminal characters -WIDTH = 80 -HEIGHT = 1 - - -# Our characters, and their approximate brightness values -CHARSET = " ,(S#g@@g#S(, " def setColor (bg, fg): """ @@ -17,13 +6,6 @@ def setColor (bg, fg): """ return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) -BLACK = setColor(16, 16) - -# Load in color lookup table data -with open("colors.pkl", "rb") as f: - LERPED = pickle.load(f) -LUT = np.load("LUT.npy") - def convertImg(img): """ Convert an RGB image to a stream of text with ANSI color codes @@ -53,16 +35,35 @@ def convertImg(img): line += "\u001b[%iD\u001b[%iA" % (WIDTH, HEIGHT + 1) return line -if len(sys.argv) == 2: - img = cv2.imread(sys.argv[1]) +if __name__ == "__main__": + import cv2 + import pickle + import sys + + # Width of the output in terminal characters + WIDTH = 80 + HEIGHT = 1 + + # Our characters, and their approximate brightness values + CHARSET = " ,(S#g@@g#S(, " + + BLACK = setColor(16, 16) + + # Load in color lookup table data + with open("colors.pkl", "rb") as f: + LERPED = pickle.load(f) + LUT = np.load("LUT.npy") + + if len(sys.argv) == 2: + img = cv2.imread(sys.argv[1]) - # Match the aspect ratio to that of the provided image - src_height, src_width, _ = img.shape + # Match the aspect ratio to that of the provided image + src_height, src_width, _ = img.shape - aspect_ratio = src_width / src_height - HEIGHT = int(WIDTH / (2 * aspect_ratio)) + aspect_ratio = src_width / src_height + HEIGHT = int(WIDTH / (2 * aspect_ratio)) - img = cv2.resize(img, (WIDTH, HEIGHT)) - print(convertImg(img)) -else: - print("Expected image file as argument.") + img = cv2.resize(img, (WIDTH, HEIGHT)) + print(convertImg(img)) + else: + print("Expected image file as argument.") diff --git a/videoToTextColor.py b/videoToTextColor.py index c0ce12b..2e76a3d 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -1,71 +1,38 @@ -import numpy as np -import cv2 -import pickle -import sys +from imgToTextColor import setColor, convertImg -ASPECT_RATIO = 16 / 9 +if __name__ == "__main__": + import numpy as np + import cv2 + import pickle + import sys -# Dimensions of the output in terminal characters -WIDTH = 80 -HEIGHT = int(WIDTH / (2 * ASPECT_RATIO)) + ASPECT_RATIO = 16 / 9 + # Dimensions of the output in terminal characters + WIDTH = 80 + HEIGHT = int(WIDTH / (2 * ASPECT_RATIO)) -# Our characters, and their approximate brightness values -CHARSET = " ,(S#g@@g#S(, " -def setColor(bg, fg): - """ - Generates a character sequence to set the foreground and background colors - """ - return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) + # Our characters, and their approximate brightness values + CHARSET = " ,(S#g@@g#S(, " -BLACK = setColor(16, 16) + BLACK = setColor(16, 16) -# Load in color lookup table data -with open("colors.pkl", "rb") as f: - LERPED = pickle.load(f) -LUT = np.load("LUT.npy") + # Load in color lookup table data + with open("colors.pkl", "rb") as f: + LERPED = pickle.load(f) + LUT = np.load("LUT.npy") -def convertImg(img): - """ - Generates a character sequence to set the foreground and background colors - """ - - line = "" - for row in img: - for color in row: - color = np.round(color).astype(int) + if len(sys.argv) == 2: + cap = cv2.VideoCapture(sys.argv[1]) - b, g, r = color[0], color[1], color[2] - - # Lookup the color index in the RGB lookup table - idx = LUT[b, g, r] - - # Get the ANSI color codes and lerp character - bg, fg, lerp, rgb = LERPED[idx] - - char = CHARSET[lerp] - - line += "%s%c" % (setColor(bg, fg), char) - # End each line with a black background to avoid color fringe - line += "%s\n" % BLACK - - # Move the cursor back to the top of the frame to prevent rolling - line += "\u001b[%iD\u001b[%iA" % (WIDTH, HEIGHT + 1) - return line - - -if len(sys.argv) == 2: - cap = cv2.VideoCapture(sys.argv[1]) - - while cap.isOpened(): - ret, frame = cap.read() - - if frame is None: - break - - img = cv2.resize(frame, (WIDTH, HEIGHT)) - print(convertImg(img)) -else: - print("Expected video file as argument.") + while cap.isOpened(): + ret, frame = cap.read() + if frame is None: + break + + img = cv2.resize(frame, (WIDTH, HEIGHT)) + print(convertImg(img)) + else: + print("Expected video file as argument.") From 531ef2e3c6883c1915796740604864dd6c1a3e63 Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 15:12:37 +0100 Subject: [PATCH 13/17] more variable and function renaming --- convert.py | 42 +++++++++++++++++++++--------------------- imgToTextColor.py | 10 +++++----- videoToTextColor.py | 6 +++--- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/convert.py b/convert.py index 310add1..f631423 100644 --- a/convert.py +++ b/convert.py @@ -25,7 +25,7 @@ NUMCHARS = len(CHARSET) -def processFrame(scaled): +def process_frame(scaled): """ Converts a greyscale video frame into a dithered 7-color frame """ @@ -56,7 +56,7 @@ def processFrame(scaled): return out -def toStr(frame): +def frame_to_str(frame): """ Prints out a frame in ASCII """ @@ -70,7 +70,7 @@ def toStr(frame): return line -def computeMarkov(frame): +def compute_markov(frame): """ Compute the prediction matrix for each character combination Each row in this matrix corresponds with a character, and lists @@ -113,7 +113,7 @@ def computeMarkov(frame): return out, ranks, cnt -def computeHuffman(cnts): +def compute_huffman(cnts): """ Computes Huffman encodings based on the counts of each number in the frame """ @@ -164,7 +164,7 @@ def computeHuffman(cnts): return codes, tree -def convertHuffman(markovFrame, codes): +def convert_huffman(markov_frame, codes): """ Take a markov frame and an array of huffman encodings, and create an array of bytes corresponding to the compressed frame @@ -176,7 +176,7 @@ def convertHuffman(markovFrame, codes): for y in range(h): for x in range(w): - out = out + codes[markovFrame[y, x]] + out = out + codes[markov_frame[y, x]] # Pad this bit-string to be byte-aligned padding = (8 - (len(out) % 8)) % 8 @@ -196,7 +196,7 @@ def convertHuffman(markovFrame, codes): return compressed -def encodeMatrix(ranks): +def encode_matrix(ranks): """ Converts a rank matrix into a binary format to be stored in the output file """ @@ -224,7 +224,7 @@ def encodeMatrix(ranks): return out -def encodeTree(tree): +def encode_tree(tree): """ Converts the huffman tree into a binary format to be stored in the output file """ @@ -240,10 +240,10 @@ def encodeTree(tree): # Load all frames into memory, then convert them to greyscale and resize them to # our terminal dimensions -vidFrames = [] +vid_frames = [] while cap.isOpened(): - if (len(vidFrames) % 500) == 0: - print('Loading frame %i' % len(vidFrames)) + if (len(vid_frames) % 500) == 0: + print('Loading frame %i' % len(vid_frames)) # Skip frames to reach target framerate for i in range(int(SRC_FPS / DEST_FPS)): @@ -255,11 +255,11 @@ def encodeTree(tree): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) scaled = cv2.resize(gray, (WIDTH, HEIGHT)) - vidFrames.append(scaled) + vid_frames.append(scaled) # Compute dithering for all frames in parallel print('Dithering Frames') -frames = Parallel(n_jobs=NUM_CORES)(delayed(processFrame)(i) for i in vidFrames) +frames = Parallel(n_jobs=NUM_CORES)(delayed(process_frame)(i) for i in vid_frames) # Compute markov and huffman encoding for all frames print('Encoding Frames') @@ -268,19 +268,19 @@ def encodeTree(tree): with open('data', 'wb') as filehandle: for frame in frames: - markovFrame, ranks, cnts = computeMarkov(frame) + markov_frame, ranks, cnts = compute_markov(frame) - codes, tree = computeHuffman(cnts) - chars = convertHuffman(markovFrame, codes) + codes, tree = compute_huffman(cnts) + chars = convert_huffman(markov_frame, codes) - matrixData = encodeMatrix(ranks) - treeData = encodeTree(tree) + matrix_data = encode_matrix(ranks) + tree_data = encode_tree(tree) - filehandle.write(bytearray(matrixData)) - filehandle.write(bytearray(treeData)) + filehandle.write(bytearray(matrix_data)) + filehandle.write(bytearray(tree_data)) filehandle.write(bytearray(chars)) - size += len(matrixData) + len(treeData) + len(chars) + size += len(matrix_data) + len(tree_data) + len(chars) # Print the size of the output file in human-readable form if size > 1048576: diff --git a/imgToTextColor.py b/imgToTextColor.py index 76cc051..7786368 100644 --- a/imgToTextColor.py +++ b/imgToTextColor.py @@ -1,12 +1,12 @@ import numpy as np -def setColor (bg, fg): +def set_color(bg, fg): """ Generates a character sequence to set the foreground and background colors """ return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) -def convertImg(img): +def convert_img(img): """ Convert an RGB image to a stream of text with ANSI color codes """ @@ -27,7 +27,7 @@ def convertImg(img): char = CHARSET[lerp] - line += "%s%c" % (setColor(bg, fg), char) + line += "%s%c" % (set_color(bg, fg), char) # End each line with a black background to avoid color fringe line += "%s\n" % BLACK @@ -47,7 +47,7 @@ def convertImg(img): # Our characters, and their approximate brightness values CHARSET = " ,(S#g@@g#S(, " - BLACK = setColor(16, 16) + BLACK = set_color(16, 16) # Load in color lookup table data with open("colors.pkl", "rb") as f: @@ -64,6 +64,6 @@ def convertImg(img): HEIGHT = int(WIDTH / (2 * aspect_ratio)) img = cv2.resize(img, (WIDTH, HEIGHT)) - print(convertImg(img)) + print(convert_img(img)) else: print("Expected image file as argument.") diff --git a/videoToTextColor.py b/videoToTextColor.py index 2e76a3d..4a3227a 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -1,4 +1,4 @@ -from imgToTextColor import setColor, convertImg +from imgToTextColor import set_color, convert_img if __name__ == "__main__": import numpy as np @@ -16,7 +16,7 @@ # Our characters, and their approximate brightness values CHARSET = " ,(S#g@@g#S(, " - BLACK = setColor(16, 16) + BLACK = set_color(16, 16) # Load in color lookup table data with open("colors.pkl", "rb") as f: @@ -33,6 +33,6 @@ break img = cv2.resize(frame, (WIDTH, HEIGHT)) - print(convertImg(img)) + print(convert_img(img)) else: print("Expected video file as argument.") From 28f48c16a21263c8d10a8500414dbe025693bc42 Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 15:27:45 +0100 Subject: [PATCH 14/17] fixes because I messed some things up --- imgToTextColor.py | 27 ++++++++++++--------------- videoToTextColor.py | 17 ++--------------- 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/imgToTextColor.py b/imgToTextColor.py index 7786368..84de219 100644 --- a/imgToTextColor.py +++ b/imgToTextColor.py @@ -1,4 +1,10 @@ import numpy as np +import pickle + +# Load in color lookup table data +with open("colors.pkl", "rb") as f: + LERPED = pickle.load(f) +LUT = np.load("LUT.npy") def set_color(bg, fg): """ @@ -6,7 +12,7 @@ def set_color(bg, fg): """ return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) -def convert_img(img): +def convert_img(img, charset=" ,(S#g@@g#S(, ", width=80, height=1): """ Convert an RGB image to a stream of text with ANSI color codes """ @@ -25,35 +31,26 @@ def convert_img(img): # Get the ANSI color codes and lerp character bg, fg, lerp, rgb = LERPED[idx] - char = CHARSET[lerp] + char = charset[lerp] line += "%s%c" % (set_color(bg, fg), char) # End each line with a black background to avoid color fringe - line += "%s\n" % BLACK + line += "\u001b[48;5;16;38;5;16m\n" # Move the cursor back to the top of the frame to prevent rolling - line += "\u001b[%iD\u001b[%iA" % (WIDTH, HEIGHT + 1) + line += "\u001b[%iD\u001b[%iA" % (width, height + 1) return line if __name__ == "__main__": import cv2 - import pickle import sys # Width of the output in terminal characters WIDTH = 80 - HEIGHT = 1 - - # Our characters, and their approximate brightness values - CHARSET = " ,(S#g@@g#S(, " + HEIGHT = 1 BLACK = set_color(16, 16) - # Load in color lookup table data - with open("colors.pkl", "rb") as f: - LERPED = pickle.load(f) - LUT = np.load("LUT.npy") - if len(sys.argv) == 2: img = cv2.imread(sys.argv[1]) @@ -64,6 +61,6 @@ def convert_img(img): HEIGHT = int(WIDTH / (2 * aspect_ratio)) img = cv2.resize(img, (WIDTH, HEIGHT)) - print(convert_img(img)) + print(convert_img(img, width=WIDTH, height=HEIGHT)) else: print("Expected image file as argument.") diff --git a/videoToTextColor.py b/videoToTextColor.py index 4a3227a..1f183d6 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -1,9 +1,7 @@ -from imgToTextColor import set_color, convert_img +from imgToTextColor import convert_img if __name__ == "__main__": - import numpy as np import cv2 - import pickle import sys ASPECT_RATIO = 16 / 9 @@ -12,17 +10,6 @@ WIDTH = 80 HEIGHT = int(WIDTH / (2 * ASPECT_RATIO)) - - # Our characters, and their approximate brightness values - CHARSET = " ,(S#g@@g#S(, " - - BLACK = set_color(16, 16) - - # Load in color lookup table data - with open("colors.pkl", "rb") as f: - LERPED = pickle.load(f) - LUT = np.load("LUT.npy") - if len(sys.argv) == 2: cap = cv2.VideoCapture(sys.argv[1]) @@ -33,6 +20,6 @@ break img = cv2.resize(frame, (WIDTH, HEIGHT)) - print(convert_img(img)) + print(convert_img(img, width=WIDTH, height=HEIGHT)) else: print("Expected video file as argument.") From c1e67d9bc81aee370f01cb692aaca41503c2b0b0 Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 15:45:33 +0100 Subject: [PATCH 15/17] changed %-formatting to f-strings --- convert.py | 8 ++++---- imgToTextColor.py | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/convert.py b/convert.py index f631423..2a5bc8d 100644 --- a/convert.py +++ b/convert.py @@ -243,7 +243,7 @@ def encode_tree(tree): vid_frames = [] while cap.isOpened(): if (len(vid_frames) % 500) == 0: - print('Loading frame %i' % len(vid_frames)) + print(f'Loading frame {len(vid_frames)}') # Skip frames to reach target framerate for i in range(int(SRC_FPS / DEST_FPS)): @@ -284,8 +284,8 @@ def encode_tree(tree): # Print the size of the output file in human-readable form if size > 1048576: - print('%.1f MB' % (size / 1048576)) + print(f'{size / 1048576:.1f} MB') elif size > 1024: - print('%.1f KB' % (size / 1024)) + print(f'{size / 1024:.1f} kB') else: - print('%i B' % (size)) + print(f'{size} B') diff --git a/imgToTextColor.py b/imgToTextColor.py index 84de219..0ee221d 100644 --- a/imgToTextColor.py +++ b/imgToTextColor.py @@ -10,7 +10,7 @@ def set_color(bg, fg): """ Generates a character sequence to set the foreground and background colors """ - return "\u001b[48;5;%s;38;5;%sm" % (bg, fg) + return f"\u001b[48;5;{bg};38;5;{fg}m" def convert_img(img, charset=" ,(S#g@@g#S(, ", width=80, height=1): """ @@ -33,12 +33,12 @@ def convert_img(img, charset=" ,(S#g@@g#S(, ", width=80, height=1): char = charset[lerp] - line += "%s%c" % (set_color(bg, fg), char) + line += set_color(bg, fg) + char # End each line with a black background to avoid color fringe line += "\u001b[48;5;16;38;5;16m\n" # Move the cursor back to the top of the frame to prevent rolling - line += "\u001b[%iD\u001b[%iA" % (width, height + 1) + line += f"\u001b[{width}D\u001b[{height + 1}A" return line if __name__ == "__main__": @@ -49,8 +49,6 @@ def convert_img(img, charset=" ,(S#g@@g#S(, ", width=80, height=1): WIDTH = 80 HEIGHT = 1 - BLACK = set_color(16, 16) - if len(sys.argv) == 2: img = cv2.imread(sys.argv[1]) From 165e5d8d2dd2ebeadde9407b3784b9147ab56d68 Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 15:57:36 +0100 Subject: [PATCH 16/17] changed double- to single-quotes in python files --- convert.py | 34 +++++++++++++++++----------------- imgToTextColor.py | 26 +++++++++++++------------- videoToTextColor.py | 4 ++-- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/convert.py b/convert.py index 2a5bc8d..9879b7e 100644 --- a/convert.py +++ b/convert.py @@ -20,15 +20,15 @@ frames = [] # Our characters, and their approximate brightness values -CHARSET = " ,(S#g@" +CHARSET = ' ,(S#g@' LEVELS = [0.000, 1.060, 2.167, 3.036, 3.977, 4.730, 6.000] NUMCHARS = len(CHARSET) def process_frame(scaled): - """ + ''' Converts a greyscale video frame into a dithered 7-color frame - """ + ''' reduced = scaled * 6. / 255 @@ -57,9 +57,9 @@ def process_frame(scaled): return out def frame_to_str(frame): - """ + ''' Prints out a frame in ASCII - """ + ''' line = '' @@ -71,14 +71,14 @@ def frame_to_str(frame): return line def compute_markov(frame): - """ + ''' Compute the prediction matrix for each character combination Each row in this matrix corresponds with a character, and lists in decreasing order, the next most likely character to follow this one We also convert the provided frame to this new markov encoding, and provide the count of each prediction rank to be passed to the huffman encoding - """ + ''' mat = np.zeros((NUMCHARS, NUMCHARS)).astype(np.uint16) @@ -114,9 +114,9 @@ def compute_markov(frame): return out, ranks, cnt def compute_huffman(cnts): - """ + ''' Computes Huffman encodings based on the counts of each number in the frame - """ + ''' codes = [] sizes = [] @@ -165,10 +165,10 @@ def compute_huffman(cnts): def convert_huffman(markov_frame, codes): - """ + ''' Take a markov frame and an array of huffman encodings, and create an array of bytes corresponding to the compressed frame - """ + ''' out = '' @@ -180,7 +180,7 @@ def convert_huffman(markov_frame, codes): # Pad this bit-string to be byte-aligned padding = (8 - (len(out) % 8)) % 8 - out += "0" * padding + out += '0' * padding # Convert each octet to a char compressed = [] @@ -189,7 +189,7 @@ def convert_huffman(markov_frame, codes): char = 0 for bit in range(8): char *= 2 - if byte[bit] == "1": + if byte[bit] == '1': char += 1 compressed.append(char) @@ -197,9 +197,9 @@ def convert_huffman(markov_frame, codes): return compressed def encode_matrix(ranks): - """ + ''' Converts a rank matrix into a binary format to be stored in the output file - """ + ''' out = [] @@ -225,9 +225,9 @@ def encode_matrix(ranks): return out def encode_tree(tree): - """ + ''' Converts the huffman tree into a binary format to be stored in the output file - """ + ''' tree = tree[len(CHARSET):] diff --git a/imgToTextColor.py b/imgToTextColor.py index 0ee221d..ac5eba8 100644 --- a/imgToTextColor.py +++ b/imgToTextColor.py @@ -2,22 +2,22 @@ import pickle # Load in color lookup table data -with open("colors.pkl", "rb") as f: +with open('colors.pkl', 'rb') as f: LERPED = pickle.load(f) -LUT = np.load("LUT.npy") +LUT = np.load('LUT.npy') def set_color(bg, fg): - """ + ''' Generates a character sequence to set the foreground and background colors - """ - return f"\u001b[48;5;{bg};38;5;{fg}m" + ''' + return f'\u001b[48;5;{bg};38;5;{fg}m' -def convert_img(img, charset=" ,(S#g@@g#S(, ", width=80, height=1): - """ +def convert_img(img, charset=' ,(S#g@@g#S(, ', width=80, height=1): + ''' Convert an RGB image to a stream of text with ANSI color codes - """ + ''' - line = "" + line = '' for row in img: for color in row: @@ -35,13 +35,13 @@ def convert_img(img, charset=" ,(S#g@@g#S(, ", width=80, height=1): line += set_color(bg, fg) + char # End each line with a black background to avoid color fringe - line += "\u001b[48;5;16;38;5;16m\n" + line += '\u001b[48;5;16;38;5;16m\n' # Move the cursor back to the top of the frame to prevent rolling - line += f"\u001b[{width}D\u001b[{height + 1}A" + line += f'\u001b[{width}D\u001b[{height + 1}A' return line -if __name__ == "__main__": +if __name__ == '__main__': import cv2 import sys @@ -61,4 +61,4 @@ def convert_img(img, charset=" ,(S#g@@g#S(, ", width=80, height=1): img = cv2.resize(img, (WIDTH, HEIGHT)) print(convert_img(img, width=WIDTH, height=HEIGHT)) else: - print("Expected image file as argument.") + print('Expected image file as argument.') diff --git a/videoToTextColor.py b/videoToTextColor.py index 1f183d6..8992a54 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -1,6 +1,6 @@ from imgToTextColor import convert_img -if __name__ == "__main__": +if __name__ == '__main__': import cv2 import sys @@ -22,4 +22,4 @@ img = cv2.resize(frame, (WIDTH, HEIGHT)) print(convert_img(img, width=WIDTH, height=HEIGHT)) else: - print("Expected video file as argument.") + print('Expected video file as argument.') From 699db051b3dec96e14f7495722ee24f091e03716 Mon Sep 17 00:00:00 2001 From: chfour Date: Fri, 11 Mar 2022 16:07:36 +0100 Subject: [PATCH 17/17] add webcam input to videoToTextColor.py! --- videoToTextColor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/videoToTextColor.py b/videoToTextColor.py index 8992a54..54f0f0b 100644 --- a/videoToTextColor.py +++ b/videoToTextColor.py @@ -11,7 +11,10 @@ HEIGHT = int(WIDTH / (2 * ASPECT_RATIO)) if len(sys.argv) == 2: - cap = cv2.VideoCapture(sys.argv[1]) + if sys.argv[1].startswith("cam:"): + cap = cv2.VideoCapture(int(sys.argv[1][4:])) + else: + cap = cv2.VideoCapture(sys.argv[1]) while cap.isOpened(): ret, frame = cap.read() @@ -22,4 +25,4 @@ img = cv2.resize(frame, (WIDTH, HEIGHT)) print(convert_img(img, width=WIDTH, height=HEIGHT)) else: - print('Expected video file as argument.') + print('Expected video file or webcam ID ("cam:n", where n is the camera index, starting with 0) as argument.')