initial commit
This commit is contained in:
commit
0b2189683b
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.direnv/
|
||||
*.mp4
|
||||
*.bin
|
||||
__pycache__
|
||||
result
|
||||
result-*
|
5
Makefile
Normal file
5
Makefile
Normal file
@ -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
|
171
decode.cpp
Normal file
171
decode.cpp
Normal file
@ -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() {
|
||||
|
||||
}
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
av
|
11
shell.nix
Normal file
11
shell.nix
Normal file
@ -0,0 +1,11 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
pkgs.mkShellNoCC {
|
||||
packages = with pkgs; [
|
||||
gnumake
|
||||
yt-dlp
|
||||
(python3.withPackages (ps: with ps; [
|
||||
av
|
||||
]))
|
||||
];
|
||||
}
|
186
test.py
Executable file
186
test.py
Executable file
@ -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
Block a user