Skip to content

Instantly share code, notes, and snippets.

@vxgmichel
Last active October 17, 2024 17:15
Show Gist options
  • Save vxgmichel/b278220d4a39799d4ca30e709c3fcb62 to your computer and use it in GitHub Desktop.
Save vxgmichel/b278220d4a39799d4ca30e709c3fcb62 to your computer and use it in GitHub Desktop.
Video conversion for Turing Complete 80x48 display.
#!/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)
#!/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)
#!/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)
@Eltend
Copy link

Eltend commented Jul 14, 2022

coool

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment