initial commit
This commit is contained in:
commit
0b2189683b
|
@ -0,0 +1,6 @@
|
||||||
|
.direnv/
|
||||||
|
*.mp4
|
||||||
|
*.bin
|
||||||
|
__pycache__
|
||||||
|
result
|
||||||
|
result-*
|
|
@ -0,0 +1,5 @@
|
||||||
|
bad_apple.bin: bad_apple.mp4
|
||||||
|
python test.py
|
||||||
|
|
||||||
|
bad_apple.mp4:
|
||||||
|
yt-dlp "https://www.youtube.com/watch?v=FtutLA63Cp8" -o bad_apple.mp4 -f mp4
|
|
@ -0,0 +1,171 @@
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
|
#define SSD1306_MEMORYMODE 0x20
|
||||||
|
#define SSD1306_COLUMNADDR 0x21
|
||||||
|
#define SSD1306_PAGEADDR 0x22
|
||||||
|
#define SSD1306_SETCONTRAST 0x81
|
||||||
|
#define SSD1306_CHARGEPUMP 0x8D
|
||||||
|
#define SSD1306_SEGREMAP 0xA0
|
||||||
|
#define SSD1306_DISPLAYALLON_RESUME 0xA4
|
||||||
|
#define SSD1306_DISPLAYALLON 0xA5
|
||||||
|
#define SSD1306_NORMALDISPLAY 0xA6
|
||||||
|
#define SSD1306_INVERTDISPLAY 0xA7
|
||||||
|
#define SSD1306_SETMULTIPLEX 0xA8
|
||||||
|
#define SSD1306_DISPLAYOFF 0xAE
|
||||||
|
#define SSD1306_DISPLAYON 0xAF
|
||||||
|
#define SSD1306_COMSCANINC 0xC0
|
||||||
|
#define SSD1306_COMSCANDEC 0xC8
|
||||||
|
#define SSD1306_SETDISPLAYOFFSET 0xD3
|
||||||
|
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5
|
||||||
|
#define SSD1306_SETPRECHARGE 0xD9
|
||||||
|
#define SSD1306_SETCOMPINS 0xDA
|
||||||
|
#define SSD1306_SETVCOMDETECT 0xDB
|
||||||
|
|
||||||
|
#define SSD1306_SETLOWCOLUMN 0x00
|
||||||
|
#define SSD1306_SETHIGHCOLUMN 0x10
|
||||||
|
#define SSD1306_SETSTARTLINE 0x40
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
|
#define SSD1306_EXTERNALVCC 0x01 // External display voltage source
|
||||||
|
#define SSD1306_SWITCHCAPVCC 0x02 // Gen. display voltage from 3.3V
|
||||||
|
|
||||||
|
#define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26 // Init rt scroll
|
||||||
|
#define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27 // Init left scroll
|
||||||
|
#define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29 // Init diag scroll
|
||||||
|
#define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A // Init diag scroll
|
||||||
|
#define SSD1306_DEACTIVATE_SCROLL 0x2E // Stop scroll
|
||||||
|
#define SSD1306_ACTIVATE_SCROLL 0x2F // Start scroll
|
||||||
|
#define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3 // Set scroll range
|
||||||
|
|
||||||
|
#define WIDTH 85
|
||||||
|
#define HEIGHT 64
|
||||||
|
#define BUFFERSIZE (WIDTH * HEIGHT / 8)
|
||||||
|
#define I2C_ADDR 0x3c
|
||||||
|
#define OLED_RESET 4
|
||||||
|
|
||||||
|
uint8_t framebuffer[BUFFERSIZE] = {};
|
||||||
|
|
||||||
|
void pixel_set(int x, int y, bool value) {
|
||||||
|
uint8_t bitmask = (1 << (y & 0x7));
|
||||||
|
uint8_t* offset = &buffer[x + (y / 8) * WIDTH];
|
||||||
|
if (value) *offset |= bitmask;
|
||||||
|
else *offset &= ~bitmask;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pixel_flip(int x, int y) {
|
||||||
|
uint8_t bitmask = (1 << (y & 0x7));
|
||||||
|
uint8_t* offset = &buffer[x + (y / 8) * WIDTH];
|
||||||
|
*offset ^= bitmask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* decode_frame(uint8_t* data) {
|
||||||
|
int pixel_x = 0;
|
||||||
|
int pixel_y = 0;
|
||||||
|
while (pixel_x != WIDHT-1 && pixel_y != HEIGHT-1) {
|
||||||
|
uint8_t byte = *data++;
|
||||||
|
|
||||||
|
int do_flip = byte & 0x80;
|
||||||
|
int n_affected = byte & 0x3f;
|
||||||
|
if (byte & 0x40) {
|
||||||
|
n_affected = (n_affected << 8) | *(data++);
|
||||||
|
}
|
||||||
|
|
||||||
|
// idea: traverse along y instead, to optimize this logic
|
||||||
|
while (n_affected--) {
|
||||||
|
if (do_flip) pixel_flip(pixel_x, pixel_y);
|
||||||
|
if (pixel_x == WIDTH-1) {
|
||||||
|
pixel_x = 0;
|
||||||
|
pixel_y++;
|
||||||
|
} else {
|
||||||
|
pixel_x++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data; // returns the next frame offset
|
||||||
|
}
|
||||||
|
|
||||||
|
void ssd1306_command(uint8_t c) {
|
||||||
|
Wire->beginTransmission(I2C_ADDR);
|
||||||
|
Wire.write(0x00); // Co = 0, D/C = 0
|
||||||
|
Wire.write(c);
|
||||||
|
Wire->endTransmission();
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_display(int reset_pin, bool vcc_is_5v_external) {
|
||||||
|
Wire.begin();
|
||||||
|
pinMode(OLED_RESET, OUTPUT);
|
||||||
|
|
||||||
|
// reset
|
||||||
|
digitalWrite(OLED_RESET, HIGH);
|
||||||
|
delay(1);
|
||||||
|
digitalWrite(OLED_RESET, LOW);
|
||||||
|
delay(10);
|
||||||
|
digitalWrite(OLED_RESET, HIGH);
|
||||||
|
|
||||||
|
// init sequence
|
||||||
|
ssd1306_command(SSD1306_DISPLAYOFF);
|
||||||
|
ssd1306_command(SSD1306_SETDISPLAYCLOCKDIV);
|
||||||
|
ssd1306_command(0x80); // the suggested ratio
|
||||||
|
ssd1306_command(SSD1306_SETMULTIPLEX);
|
||||||
|
ssd1306_command(HEIGHT - 1);
|
||||||
|
|
||||||
|
ssd1306_command(SSD1306_SETDISPLAYOFFSET);
|
||||||
|
ssd1306_command(0x00); // no offset
|
||||||
|
ssd1306_command(SSD1306_SETSTARTLINE | 0x0); // (line #0)
|
||||||
|
ssd1306_command(SSD1306_CHARGEPUMP);
|
||||||
|
|
||||||
|
ssd1306_command((vcc_is_5v_external) ? 0x10 : 0x14);
|
||||||
|
|
||||||
|
ssd1306_command(SSD1306_MEMORYMODE); // 0x20
|
||||||
|
ssd1306_command(0x00); // 0x0 act like ks0108
|
||||||
|
ssd1306_command(SSD1306_SEGREMAP | 0x1);
|
||||||
|
ssd1306_command(SSD1306_COMSCANDEC);
|
||||||
|
|
||||||
|
uint8_t comPins, contrast;
|
||||||
|
if ((WIDTH == 128) && (HEIGHT == 64)) { comPins = 0x12; contrast = (vcc_is_5v_external) ? 0x9F : 0xCF; }
|
||||||
|
//else if ((WIDTH == 128) && (HEIGHT == 32)) { comPins = 0x02; contrast = 0x8F; }
|
||||||
|
//else if ((WIDTH == 96) && (HEIGHT == 16)) { comPins = 0x02; contrast = (vcc_is_5v_external) ? 0x10 : 0xAF; }
|
||||||
|
//else { /* Other screen varieties -- TBD */ }
|
||||||
|
|
||||||
|
ssd1306_command(SSD1306_SETCOMPINS);
|
||||||
|
ssd1306_command(comPins);
|
||||||
|
ssd1306_command(SSD1306_SETCONTRAST);
|
||||||
|
ssd1306_command(contrast);
|
||||||
|
|
||||||
|
ssd1306_command(SSD1306_SETPRECHARGE); // 0xd9
|
||||||
|
ssd1306_command((vcc_is_5v_external) ? 0x22 : 0xF1);
|
||||||
|
|
||||||
|
ssd1306_command(SSD1306_SETVCOMDETECT);
|
||||||
|
ssd1306_command(0x40);
|
||||||
|
ssd1306_command(SSD1306_DISPLAYALLON_RESUME);
|
||||||
|
ssd1306_command(SSD1306_NORMALDISPLAY);
|
||||||
|
ssd1306_command(SSD1306_DEACTIVATE_SCROLL);
|
||||||
|
ssd1306_command(SSD1306_DISPLAYON); // Main screen turn on
|
||||||
|
|
||||||
|
ssd1306_command();
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_framebuffer() {
|
||||||
|
int buffersize = 32;
|
||||||
|
Wire.beginTransmission(I2C_ADDR);
|
||||||
|
Wire.write(0x40);
|
||||||
|
for (int i = 0; i < BUFFERSIZE; i++) {
|
||||||
|
if (!--buffersize) {
|
||||||
|
Wire.endTransmission();
|
||||||
|
Wire.beginTransmission(I2C_ADDR);
|
||||||
|
buffersize = 32;
|
||||||
|
}
|
||||||
|
Wire.write(framebuffer[i]);
|
||||||
|
}
|
||||||
|
Wire.endTransmission();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Setup() {
|
||||||
|
init_display();
|
||||||
|
send_framebuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
Void Loop() {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
av
|
|
@ -0,0 +1,11 @@
|
||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
|
pkgs.mkShellNoCC {
|
||||||
|
packages = with pkgs; [
|
||||||
|
gnumake
|
||||||
|
yt-dlp
|
||||||
|
(python3.withPackages (ps: with ps; [
|
||||||
|
av
|
||||||
|
]))
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from glm import ivec2
|
||||||
|
from PIL import Image
|
||||||
|
from typing import Iterable
|
||||||
|
import av
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
SIZE = ivec2(4, 3) * 64 / 4
|
||||||
|
SIZE = ivec2(4, 3) * 64 / 3
|
||||||
|
LENGTH = SIZE.x * SIZE.y
|
||||||
|
RENDER_SCALE = 13
|
||||||
|
|
||||||
|
|
||||||
|
def read_mp4() -> Iterable[np.array]:
|
||||||
|
container = av.open("bad_apple.mp4")
|
||||||
|
for frame_handler in container.decode(video=0):
|
||||||
|
# convert frame to bool array
|
||||||
|
image = frame_handler.to_image()
|
||||||
|
image = image.resize(SIZE) # downscale
|
||||||
|
frame = np.array(image)[:,:,0].T > 128
|
||||||
|
yield frame
|
||||||
|
|
||||||
|
def encode_frames(frames) -> Iterable[bytes]:
|
||||||
|
no_compression_cumulative, compression_cumulative = 0, 0
|
||||||
|
|
||||||
|
frame_prev = np.zeros((SIZE), dtype=bool) # assume black frame at start
|
||||||
|
for n, frame in enumerate(frames):
|
||||||
|
data = encode_frame(frame, frame_prev)
|
||||||
|
yield data
|
||||||
|
frame_prev = frame
|
||||||
|
|
||||||
|
no_compression_cumulative += LENGTH // 8
|
||||||
|
compression_cumulative += len(data)
|
||||||
|
print("frame",
|
||||||
|
f"{n:>5}| len:",
|
||||||
|
f"{len(data):>5} =",
|
||||||
|
f"{compression_cumulative:>8} /",
|
||||||
|
f"{no_compression_cumulative} =>",
|
||||||
|
f"{compression_cumulative / no_compression_cumulative * 100:>10.3}% ratio",
|
||||||
|
end="\r")
|
||||||
|
print()
|
||||||
|
|
||||||
|
def encode_frame(frame, frame_prev) -> bytes:
|
||||||
|
def group(frame_data):
|
||||||
|
mask = frame_data[:-1] != frame_data[1:]
|
||||||
|
mask = np.concatenate((mask, [True]))
|
||||||
|
sizes = np.arange(LENGTH)
|
||||||
|
sizes = sizes[mask]
|
||||||
|
sizes[1:] -= sizes[:-1]
|
||||||
|
sizes[0] += 1
|
||||||
|
return zip(sizes, frame_data[mask])
|
||||||
|
def group_(frame_data): # slow, but readable
|
||||||
|
n = 1
|
||||||
|
for a, b in zip(frame_data[:-1], frame_data[1:]):
|
||||||
|
if a != b:
|
||||||
|
yield n, a
|
||||||
|
n = 1
|
||||||
|
else:
|
||||||
|
n += 1
|
||||||
|
yield n, b
|
||||||
|
|
||||||
|
out = [] # bytearray() ?
|
||||||
|
write_byte = out.append
|
||||||
|
|
||||||
|
prev_skipped = True # don't skip the first one
|
||||||
|
n_pixels_left = LENGTH
|
||||||
|
|
||||||
|
#for n_values, set_value in group((frame != frame_prev).reshape(-1).T):
|
||||||
|
for n_values, set_value in group((frame != frame_prev).reshape(-1)):
|
||||||
|
assert n_values <= 0x3fff
|
||||||
|
|
||||||
|
## skip n_values == 1, they can be inferred
|
||||||
|
n_pixels_left -= n_values # avoid skipping the last one
|
||||||
|
if n_values == 1 and not prev_skipped and n_pixels_left:
|
||||||
|
prev_skipped = True # avoid skippin multiple times in a row
|
||||||
|
continue
|
||||||
|
prev_skipped = False
|
||||||
|
|
||||||
|
if n_values > 0x3f:
|
||||||
|
write_byte( (set_value << 7) | (1 << 6) | (n_values >> 8) )
|
||||||
|
write_byte( n_values & 0xff )
|
||||||
|
else:
|
||||||
|
write_byte( (set_value << 7) | (n_values & 0x3f) )
|
||||||
|
|
||||||
|
#print(bytes(out))
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
def decode_frame(target_buffer, data, data_offset=0) -> int:
|
||||||
|
def read_byte():
|
||||||
|
nonlocal data_offset
|
||||||
|
try:
|
||||||
|
out = data[data_offset]
|
||||||
|
except:
|
||||||
|
print(f"\n\n{data_offset=} {len(data)=}")
|
||||||
|
raise
|
||||||
|
data_offset += 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
buffer = np.zeros(LENGTH, dtype=bool)
|
||||||
|
buffer_offset = 0
|
||||||
|
|
||||||
|
prev_value = None
|
||||||
|
while buffer_offset != LENGTH:
|
||||||
|
|
||||||
|
byte = read_byte()
|
||||||
|
value = byte >> 7
|
||||||
|
if byte & 0x40:
|
||||||
|
n_values = ((byte & 0x3f) << 8) | read_byte()
|
||||||
|
else:
|
||||||
|
n_values = (byte & 0x3f)
|
||||||
|
|
||||||
|
if prev_value == value: # there was an omitted n_values=1 in between
|
||||||
|
buffer[buffer_offset] = not value
|
||||||
|
buffer_offset += 1
|
||||||
|
|
||||||
|
buffer[buffer_offset:buffer_offset + n_values] = value
|
||||||
|
buffer_offset += n_values
|
||||||
|
prev_value = value
|
||||||
|
|
||||||
|
#target_buffer ^= buffer.reshape((SIZE.y, SIZE.x)).T
|
||||||
|
target_buffer ^= buffer.reshape(SIZE)
|
||||||
|
|
||||||
|
return data_offset
|
||||||
|
|
||||||
|
def render_frames(frames, fps = None):
|
||||||
|
import pygame
|
||||||
|
pygame.init()
|
||||||
|
display = pygame.display.set_mode(SIZE * RENDER_SCALE)
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
|
||||||
|
ignored_rows = 0
|
||||||
|
for frame in frames:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.KEYDOWN and event.key == pygame.K_q:
|
||||||
|
pygame.quit(); return
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit(); return
|
||||||
|
|
||||||
|
# render frame
|
||||||
|
draw_buffer = np.zeros((*SIZE, 3), dtype=np.uint8)
|
||||||
|
draw_buffer[frame,:] = 0xff
|
||||||
|
|
||||||
|
if 0: # updated line vizualisation
|
||||||
|
for y in range(SIZE.y):
|
||||||
|
if not (frame[:, y] != frame_prev[:, y]).any():
|
||||||
|
draw_buffer[:, y, 1] = 0
|
||||||
|
draw_buffer[:, y, 2] = 0xff
|
||||||
|
ignored_rows += 1
|
||||||
|
|
||||||
|
surf = pygame.surfarray.make_surface(draw_buffer)
|
||||||
|
display.blit(pygame.transform.scale(surf, SIZE*RENDER_SCALE), (0, 0))
|
||||||
|
|
||||||
|
pygame.display.update()
|
||||||
|
if fps: clock.tick(fps)
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
def encode_then_decode(frames):
|
||||||
|
decode_buffer = np.zeros(SIZE, dtype=bool)
|
||||||
|
for data in encode_frames(frames):
|
||||||
|
offset = decode_frame(decode_buffer, data)
|
||||||
|
assert len(data) == offset
|
||||||
|
yield decode_buffer
|
||||||
|
|
||||||
|
def decoder(encoded_data, reuse=True):
|
||||||
|
data_offset = 0
|
||||||
|
decode_buffer = np.zeros(SIZE, dtype=bool)
|
||||||
|
while data_offset < len(encoded_data):
|
||||||
|
data_offset = decode_frame(decode_buffer, encoded_data, data_offset)
|
||||||
|
yield decode_buffer if reuse else decode_buffer.copy()
|
||||||
|
|
||||||
|
print(f"{SIZE = }")
|
||||||
|
print(f"{LENGTH = }")
|
||||||
|
print(f"{RENDER_SCALE = }")
|
||||||
|
|
||||||
|
#render_frames(read_mp4())
|
||||||
|
#render_frames(encode_then_decode(read_mp4()))
|
||||||
|
with open("bad_apple.bin", "wb") as f: f.write(b''.join(encode_frames(read_mp4())))
|
||||||
|
|
||||||
|
#with open("bad_apple.bak.bin", "rb") as f: render_frames(decoder(f.read()) )#, fps = 30)
|
||||||
|
#with open("bad_apple.bak.bin", "rb") as f: list(encode_frames(decoder(f.read(), reuse=0)))
|
||||||
|
#with open("bad_apple.bak.bin", "rb") as f: list(encode_then_decode(decoder(f.read(), reuse=0)))
|
||||||
|
|
||||||
|
main()
|
Loading…
Reference in New Issue