init
This commit is contained in:
commit
eca5344c66
10 changed files with 445 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
*.bin
|
||||
*.elf
|
||||
*.hex
|
||||
*.lst
|
||||
*.map
|
||||
*.pdf
|
||||
target/
|
1
encoder/.gitignore
vendored
Normal file
1
encoder/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
124
encoder/Cargo.lock
generated
Normal file
124
encoder/Cargo.lock
generated
Normal file
|
@ -0,0 +1,124 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "compressor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"num-traits",
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
|
||||
dependencies = [
|
||||
"adler",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
9
encoder/Cargo.toml
Normal file
9
encoder/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "encoder"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
image = { version = "0.25.1", default-features = false, features = ["png"] }
|
1
encoder/rustfmt.toml
Normal file
1
encoder/rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
hard_tabs = true
|
35
encoder/src/dec.rs
Normal file
35
encoder/src/dec.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn rle_horizontal(_prev_frame: &Frame, encoded: &[u8]) -> Frame {
|
||||
let pixels = rle_255_decode(encoded);
|
||||
let mut frame = FRAME_0;
|
||||
let mut i = 0;
|
||||
for y in 0..HEIGHT {
|
||||
for x in 0..WIDTH {
|
||||
frame[x][y] = pixels[i];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn rle_vertical(_prev_frame: &Frame, encoded: &[u8]) -> Frame {
|
||||
let pixels = rle_255_decode(encoded);
|
||||
let mut frame = FRAME_0;
|
||||
let mut i = 0;
|
||||
for x in 0..WIDTH {
|
||||
for y in 0..HEIGHT {
|
||||
frame[x][y] = pixels[i];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn fill_white(_prev_frame: &Frame, _encoded: &[u8]) -> Frame {
|
||||
FRAME_1
|
||||
}
|
||||
|
||||
pub fn fill_black(_prev_frame: &Frame, _encoded: &[u8]) -> Frame {
|
||||
FRAME_0
|
||||
}
|
45
encoder/src/enc.rs
Normal file
45
encoder/src/enc.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use crate::*;
|
||||
|
||||
pub fn rle_horizontal(_prev_frame: &Frame, frame: &Frame) -> EncodedFrame {
|
||||
let mut pixels = Vec::new();
|
||||
for y in 0..HEIGHT {
|
||||
for x in 0..WIDTH {
|
||||
pixels.push(frame[x][y]);
|
||||
}
|
||||
}
|
||||
EncodedFrame {
|
||||
encoding: Encoding::RLEHorizontal,
|
||||
head_u4: 0,
|
||||
data: rle_255_encode(&pixels),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rle_vertical(_prev_frame: &Frame, frame: &Frame) -> EncodedFrame {
|
||||
let mut pixels = Vec::new();
|
||||
for x in 0..WIDTH {
|
||||
for y in 0..HEIGHT {
|
||||
pixels.push(frame[x][y]);
|
||||
}
|
||||
}
|
||||
EncodedFrame {
|
||||
encoding: Encoding::RLEVertical,
|
||||
head_u4: 0,
|
||||
data: rle_255_encode(&pixels),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_white(_prev_frame: &Frame, _frame: &Frame) -> EncodedFrame {
|
||||
EncodedFrame {
|
||||
encoding: Encoding::FillWhite,
|
||||
head_u4: 0,
|
||||
data: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_black(_prev_frame: &Frame, _frame: &Frame) -> EncodedFrame {
|
||||
EncodedFrame {
|
||||
encoding: Encoding::FillBlack,
|
||||
head_u4: 0,
|
||||
data: Vec::new(),
|
||||
}
|
||||
}
|
90
encoder/src/main.rs
Normal file
90
encoder/src/main.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
mod dec;
|
||||
mod enc;
|
||||
mod util;
|
||||
pub use util::*;
|
||||
|
||||
const INTERACTIVE: bool = false;
|
||||
|
||||
fn main() {
|
||||
let frames = get_all_frames("../video/frames/");
|
||||
let encoded = encode(&frames);
|
||||
println!("{} frames, total {} bytes", frames.len(), encoded.len());
|
||||
//
|
||||
}
|
||||
|
||||
fn encode(frames: &[Frame]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
let encodings: Vec<(FrameEncoder, FrameDecoder)> = vec![
|
||||
(enc::fill_white, dec::fill_white),
|
||||
(enc::fill_black, dec::fill_black),
|
||||
(enc::rle_horizontal, dec::rle_horizontal),
|
||||
(enc::rle_vertical, dec::rle_vertical),
|
||||
];
|
||||
let max_error = 0;
|
||||
|
||||
let mut last_frame = FRAME_0;
|
||||
for frame in frames {
|
||||
let mut options = Vec::new();
|
||||
for (encode, decode) in &encodings {
|
||||
let encoded = encode(&last_frame, frame);
|
||||
let decoded = decode(&last_frame, &encoded.data);
|
||||
let error = frame_error(frame, &decoded);
|
||||
if error <= max_error {
|
||||
options.push(encoded);
|
||||
}
|
||||
}
|
||||
options.sort_by_key(|b| b.data.len());
|
||||
let best_encoding = options.into_iter().next().unwrap();
|
||||
if INTERACTIVE {
|
||||
println!();
|
||||
println!(
|
||||
"{:?}, {} bytes",
|
||||
best_encoding.encoding,
|
||||
best_encoding.data.len() + 1
|
||||
);
|
||||
render_image(frame);
|
||||
let mut a = String::new();
|
||||
std::io::stdin().read_line(&mut a).unwrap();
|
||||
}
|
||||
let best_encoding = best_encoding.into_bytes();
|
||||
out.extend_from_slice(&best_encoding);
|
||||
last_frame = frame.clone();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(u8)]
|
||||
enum Encoding {
|
||||
FillWhite,
|
||||
FillBlack,
|
||||
RLEHorizontal,
|
||||
RLEVertical,
|
||||
BGStrips,
|
||||
CellDiff8Horizontal,
|
||||
CellDiff8Vertical,
|
||||
CellDiff4HH,
|
||||
CellDiff4HV,
|
||||
CellDiff4VH,
|
||||
CellDiff4VV,
|
||||
}
|
||||
type FrameEncoder = fn(previous_frame: &Frame, new_frame: &Frame) -> EncodedFrame;
|
||||
type FrameDecoder = fn(previous_frame: &Frame, encoded_bytes: &[u8]) -> Frame;
|
||||
|
||||
struct EncodedFrame {
|
||||
encoding: Encoding,
|
||||
head_u4: u8,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl EncodedFrame {
|
||||
fn into_bytes(self) -> Vec<u8> {
|
||||
let head = (self.encoding as u8) << 4;
|
||||
let head = head | (self.head_u4 & 15);
|
||||
|
||||
let mut out = Vec::with_capacity(self.data.len() + 1);
|
||||
out.push(head);
|
||||
out.extend(self.data.into_iter());
|
||||
out
|
||||
}
|
||||
}
|
115
encoder/src/util.rs
Normal file
115
encoder/src/util.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use std::{
|
||||
fs::{self, File},
|
||||
io::BufReader,
|
||||
};
|
||||
|
||||
use image::{self, DynamicImage, GenericImageView, ImageFormat, Rgba};
|
||||
|
||||
pub const WIDTH: usize = 40;
|
||||
pub const HEIGHT: usize = 32;
|
||||
pub const SIZE: usize = WIDTH * HEIGHT;
|
||||
|
||||
pub type Frame = [[u8; HEIGHT]; WIDTH];
|
||||
pub const FRAME_0: Frame = [[0; HEIGHT]; WIDTH];
|
||||
pub const FRAME_1: Frame = [[1; HEIGHT]; WIDTH];
|
||||
|
||||
fn convert_pixel(rgba: Rgba<u8>) -> u8 {
|
||||
(rgba.0[0] > 128) as u8
|
||||
}
|
||||
|
||||
pub fn convert_image(image: &DynamicImage) -> Frame {
|
||||
let mut frame = FRAME_0;
|
||||
for x in 0..WIDTH {
|
||||
for y in 0..HEIGHT {
|
||||
frame[x][y] = convert_pixel(image.get_pixel(x as u32, y as u32));
|
||||
}
|
||||
}
|
||||
frame
|
||||
}
|
||||
|
||||
pub fn get_all_frames(path: &str) -> Vec<Frame> {
|
||||
let frame_count = fs::read_dir(path).unwrap().count();
|
||||
let mut frames = Vec::new();
|
||||
for i in 0..frame_count {
|
||||
let path = format!("{}frame_{:04}.png", path, i + 1);
|
||||
let file = BufReader::new(File::open(path).unwrap());
|
||||
let image = image::load(file, ImageFormat::Png).unwrap();
|
||||
frames.push(convert_image(&image));
|
||||
}
|
||||
frames
|
||||
}
|
||||
|
||||
pub fn frame_error(real: &Frame, decoded: &Frame) -> usize {
|
||||
let mut error = 0;
|
||||
for x in 0..WIDTH {
|
||||
for y in 0..HEIGHT {
|
||||
if real[x][y] != decoded[x][y] {
|
||||
error += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
error
|
||||
}
|
||||
|
||||
fn render_pixel_pair(img: &Frame, x: usize, y: usize) {
|
||||
let char = match (img[x][y * 2], img[x][y * 2 + 1]) {
|
||||
(0, 0) => " ",
|
||||
(0, 1) => "▄",
|
||||
(1, 0) => "▀",
|
||||
(1, 1) => "█",
|
||||
_ => panic!("image contained nonbinary bytes"),
|
||||
};
|
||||
print!("{}", char);
|
||||
}
|
||||
|
||||
pub fn render_image(img: &Frame) {
|
||||
for y in 0..(HEIGHT / 2) {
|
||||
for x in 0..WIDTH {
|
||||
render_pixel_pair(img, x, y);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_images(left: &Frame, right: &Frame) {
|
||||
for y in 0..(HEIGHT / 2) {
|
||||
for x in 0..WIDTH {
|
||||
render_pixel_pair(left, x, y);
|
||||
print!(" ");
|
||||
render_pixel_pair(right, x, y);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rle_255_encode(raw: &[u8]) -> Vec<u8> {
|
||||
let mut encoded = Vec::new();
|
||||
let mut last_val = 0;
|
||||
let mut run = 0;
|
||||
for &val in raw {
|
||||
if val != last_val || run == 255 {
|
||||
encoded.push(run);
|
||||
if run == 255 {
|
||||
encoded.push(0);
|
||||
}
|
||||
run = 1;
|
||||
} else {
|
||||
run += 1;
|
||||
}
|
||||
last_val = val;
|
||||
}
|
||||
encoded.push(run);
|
||||
encoded
|
||||
}
|
||||
|
||||
pub fn rle_255_decode(encoded: &[u8]) -> Vec<u8> {
|
||||
let mut raw = Vec::new();
|
||||
let mut val = 0;
|
||||
for &run in encoded {
|
||||
for _ in 0..run {
|
||||
raw.push(val);
|
||||
}
|
||||
val = 1 - val;
|
||||
}
|
||||
raw
|
||||
}
|
18
video/convert.sh
Executable file
18
video/convert.sh
Executable file
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 480x360 base resolution
|
||||
# 42x32 is close enough
|
||||
# 40x32 is nicer to work with because divisible by 8
|
||||
|
||||
rm frames/*.png
|
||||
|
||||
ffmpeg -i *.webm \
|
||||
-r $1 \
|
||||
-ss 1 \
|
||||
-vf scale=40:32 \
|
||||
-sws_dither none \
|
||||
frames/frame_%04d.png 2> /dev/null
|
||||
# -pix_fmt monob \
|
||||
|
||||
echo converted with $1 fps
|
||||
echo $(ls -L1 frames | wc -l) frames
|
Loading…
Reference in a new issue