Last active
October 17, 2024 17:15
-
-
Save vxgmichel/b278220d4a39799d4ca30e709c3fcb62 to your computer and use it in GitHub Desktop.
Video conversion for Turing Complete 80x48 display.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
Convert an mp4 video file to uncompressed 80x48 video frames. | |
The output data is meant to be used as direct memory for the 80x24 Turing complete | |
console. In particular, pixels are grouped vertically so 2 pixels can fit into a | |
single `▀` (`\xdf`) character. | |
For instance, the following frame: | |
p00 p01 p02 ... | |
p10 p11 p12 ... | |
... | |
Would be represented with the bytes below: | |
\xdf p00_r p00_g p00_b p10_r p10_g p10_b \x00 | |
\xdf p01_r p01_g p01_b p11_r p11_g p11_b \x00 | |
\xdf p02_r p02_g p02_b p12_r p12_g p12_b \x00 | |
... | |
This means that an entire frame is 15360 (`80*24*8`) bytes. Frames can be advanced | |
using the console offset with increments of 1920 (`80*24`). | |
Example usage: | |
$ pip install youtube-dl numpy opencv-python | |
$ youtube-dl https://youtu.be/UkgK8eUdpAo -f mp4 -o badapple.mp4 | |
$ ./convert_cp437.py badapple.mp4 badapple.bin | |
""" | |
import argparse | |
from pathlib import Path | |
from typing import Optional | |
import cv2 | |
import numpy as np | |
def mp4_to_np(path: Path, nb_frames: Optional[int] = None): | |
capture = cv2.VideoCapture(str(path)) | |
count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) | |
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
count = count if nb_frames is None else min(count, nb_frames) | |
array = np.empty((count, height, width, 3), np.uint8) | |
for i in range(count): | |
_, array[i] = capture.read() | |
capture.release() | |
return array | |
def main(source: Path, destination: Path, nb_frames: Optional[int] = None): | |
buf = mp4_to_np(source, nb_frames) | |
count, width, height, depth = buf.shape | |
with destination.open("wb") as f: | |
for i in range(count): | |
frame = cv2.resize(buf[i], dsize=(80, 48), interpolation=cv2.INTER_CUBIC) | |
data = bytearray(24 * 80 * 8) | |
data[0::8] = b"\xdf" * 24 * 80 | |
data[1::8] = frame[0::2, :, 0].tobytes() | |
data[2::8] = frame[0::2, :, 1].tobytes() | |
data[3::8] = frame[0::2, :, 2].tobytes() | |
data[4::8] = frame[1::2, :, 0].tobytes() | |
data[5::8] = frame[1::2, :, 1].tobytes() | |
data[6::8] = frame[1::2, :, 2].tobytes() | |
f.write(data) | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser() | |
parser.add_argument("source", type=Path) | |
parser.add_argument("destination", type=Path) | |
parser.add_argument("--nb-frames", "-n", type=int, default=None) | |
namespace = parser.parse_args() | |
main(namespace.source, namespace.destination, namespace.nb_frames) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
Convert an mp4 video file to 80x48 QOI compressed video frames. | |
The QOI images are simply concatenated into a single file. | |
Use the `--diff` flag in order to produce difference frames instead of full frames. | |
This might produce smaller files, although it is not always the case. | |
Example usage: | |
$ pip install youtube-dl numpy opencv-python qoi | |
$ youtube-dl https://youtu.be/UkgK8eUdpAo -f mp4 -o badapple.mp4 | |
$ ./convert_qoi.py badapple.mp4 qoi.bin | |
""" | |
import argparse | |
from pathlib import Path | |
from typing import Optional | |
import qoi | |
import cv2 | |
import numpy as np | |
def mp4_to_np(path: Path, nb_frames: Optional[int] = None): | |
capture = cv2.VideoCapture(str(path)) | |
count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) | |
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
count = count if nb_frames is None else min(count, nb_frames) | |
array = np.empty((count, height, width, 3), np.uint8) | |
for i in range(count): | |
_, array[i] = capture.read() | |
capture.release() | |
return array | |
def main( | |
source: Path, destination: Path, nb_frames: Optional[int] = None, diff: bool = False | |
): | |
buf = mp4_to_np(source, nb_frames) | |
count, width, height, depth = buf.shape | |
previous_frame = np.zeros((48, 80, 3), np.uint8) | |
with destination.open("wb") as f: | |
for i in range(count): | |
frame = cv2.resize(buf[i], dsize=(80, 48), interpolation=cv2.INTER_CUBIC) | |
if diff: | |
difference = frame - previous_frame | |
f.write(qoi.encode(difference)) | |
else: | |
f.write(qoi.encode(frame)) | |
previous_frame = frame | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser() | |
parser.add_argument("source", type=Path) | |
parser.add_argument("destination", type=Path) | |
parser.add_argument("--nb-frames", "-n", type=int, default=None) | |
parser.add_argument("--diff", "-d", action="store_true") | |
namespace = parser.parse_args() | |
main(namespace.source, namespace.destination, namespace.nb_frames, namespace.diff) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
Convert an mp4 video file to uncompressed 80x24 video frames. | |
Frames are represented as 80 x 24 pixels (3 bytes per pixel representing RGB colors). | |
For instance, data between offset 57600 (`10 * 80 * 24 * 3`) and `63360` (`11 * 80 * 24 * 3`) | |
represents the 11th frame. | |
Example usage: | |
$ pip install youtube-dl numpy opencv-python | |
$ youtube-dl https://youtu.be/UkgK8eUdpAo -f mp4 -o badapple.mp4 | |
$ ./convert_raw.py badapple.mp4 badapple.bin | |
""" | |
import argparse | |
from pathlib import Path | |
from typing import Optional | |
import cv2 | |
import numpy as np | |
def mp4_to_np(path: Path, nb_frames: Optional[int] = None): | |
capture = cv2.VideoCapture(str(path)) | |
count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) | |
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
count = count if nb_frames is None else min(count, nb_frames) | |
array = np.empty((count, height, width, 3), np.uint8) | |
for i in range(count): | |
_, array[i] = capture.read() | |
capture.release() | |
return array | |
def main(source: Path, destination: Path, nb_frames: Optional[int] = None): | |
buf = mp4_to_np(source, nb_frames) | |
count, width, height, depth = buf.shape | |
target = np.empty((count, 24, 80, 3), np.uint8) | |
for i in range(count): | |
target[i] = cv2.resize(buf[i], dsize=(80, 24), interpolation=cv2.INTER_CUBIC) | |
destination.write_bytes(target.tobytes()) | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser() | |
parser.add_argument("source", type=Path) | |
parser.add_argument("destination", type=Path) | |
parser.add_argument("--nb-frames", "-n", type=int, default=None) | |
namespace = parser.parse_args() | |
main(namespace.source, namespace.destination, namespace.nb_frames) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
coool