From eca5344c660cb155f40d003caa8c5ab4083f804c Mon Sep 17 00:00:00 2001 From: CrispyPin Date: Wed, 10 Apr 2024 19:04:29 +0200 Subject: [PATCH] init --- .gitignore | 7 +++ encoder/.gitignore | 1 + encoder/Cargo.lock | 124 +++++++++++++++++++++++++++++++++++++++++++ encoder/Cargo.toml | 9 ++++ encoder/rustfmt.toml | 1 + encoder/src/dec.rs | 35 ++++++++++++ encoder/src/enc.rs | 45 ++++++++++++++++ encoder/src/main.rs | 90 +++++++++++++++++++++++++++++++ encoder/src/util.rs | 115 +++++++++++++++++++++++++++++++++++++++ video/convert.sh | 18 +++++++ 10 files changed, 445 insertions(+) create mode 100644 .gitignore create mode 100644 encoder/.gitignore create mode 100644 encoder/Cargo.lock create mode 100644 encoder/Cargo.toml create mode 100644 encoder/rustfmt.toml create mode 100644 encoder/src/dec.rs create mode 100644 encoder/src/enc.rs create mode 100644 encoder/src/main.rs create mode 100644 encoder/src/util.rs create mode 100755 video/convert.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93108d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.bin +*.elf +*.hex +*.lst +*.map +*.pdf +target/ diff --git a/encoder/.gitignore b/encoder/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/encoder/.gitignore @@ -0,0 +1 @@ +/target diff --git a/encoder/Cargo.lock b/encoder/Cargo.lock new file mode 100644 index 0000000..49cda42 --- /dev/null +++ b/encoder/Cargo.lock @@ -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" diff --git a/encoder/Cargo.toml b/encoder/Cargo.toml new file mode 100644 index 0000000..1bb3b8b --- /dev/null +++ b/encoder/Cargo.toml @@ -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"] } diff --git a/encoder/rustfmt.toml b/encoder/rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/encoder/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/encoder/src/dec.rs b/encoder/src/dec.rs new file mode 100644 index 0000000..51acb5f --- /dev/null +++ b/encoder/src/dec.rs @@ -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 +} diff --git a/encoder/src/enc.rs b/encoder/src/enc.rs new file mode 100644 index 0000000..0a5126c --- /dev/null +++ b/encoder/src/enc.rs @@ -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(), + } +} diff --git a/encoder/src/main.rs b/encoder/src/main.rs new file mode 100644 index 0000000..8b3916f --- /dev/null +++ b/encoder/src/main.rs @@ -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 { + 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, +} + +impl EncodedFrame { + fn into_bytes(self) -> Vec { + 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 + } +} diff --git a/encoder/src/util.rs b/encoder/src/util.rs new file mode 100644 index 0000000..51c40e8 --- /dev/null +++ b/encoder/src/util.rs @@ -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 { + (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 { + 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 { + 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 { + let mut raw = Vec::new(); + let mut val = 0; + for &run in encoded { + for _ in 0..run { + raw.push(val); + } + val = 1 - val; + } + raw +} diff --git a/video/convert.sh b/video/convert.sh new file mode 100755 index 0000000..e91d818 --- /dev/null +++ b/video/convert.sh @@ -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