initial commit

This commit is contained in:
Michael Francis 2024-07-25 22:26:49 -04:00
commit e25067842d
4 changed files with 1494 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1174
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
name = "recall-challenge"
version = "0.1.0"
edition = "2021"
[dependencies]
byteorder = "1.5.0"
clap = { version = "4.5.11", features = ["derive"] }
futures = "0.3.30"
minifb = "0.27.0"
ndarray = "0.15.6"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
thiserror = "1.0.63"
tokio = { version = "1.39.1", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1.4.0"

301
src/main.rs Normal file
View File

@ -0,0 +1,301 @@
use clap::Parser;
use minifb::{Window, WindowOptions};
use serde::Deserialize;
use std::{
io::{Read, Write},
path::PathBuf,
};
const NUM_CHANNELS: u32 = 3;
use ndarray::Array3;
#[derive(Parser, Debug)]
struct Config {
#[clap(short, long)]
input: PathBuf,
#[clap(short, long)]
output: PathBuf,
#[clap(short, long)]
rules: PathBuf,
}
#[derive(Deserialize, Debug)]
struct Rules {
size: [u32; 2],
rects: Vec<Rects>,
}
#[derive(Deserialize, Debug)]
struct Source {
x: u32,
y: u32,
width: u32,
height: u32,
}
#[derive(Deserialize, Debug)]
struct Destination {
x: u32,
y: u32,
}
#[derive(Deserialize, Debug)]
struct Rects {
#[serde(rename = "src")]
source: Source,
#[serde(rename = "dest")]
destination: Destination,
// TODO: What's the range of values for alpha?
alpha: f32,
// TODO: What's the range of values for z?
z: i32,
}
impl PartialEq for Rects {
fn eq(&self, other: &Self) -> bool {
self.z == other.z
}
}
impl PartialOrd for Rects {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.z.cmp(&other.z))
}
}
struct Video<'a> {
width: u32,
height: u32,
num_frames: u32,
reader: &'a mut dyn Read,
}
impl<'a> Video<'a> {
fn try_from_reader(reader: &'a mut dyn Read) -> Result<Self, Error> {
let width = {
let mut buf = [0u8; 4];
reader
.read_exact(&mut buf)
.map_err(|_| Error::InvalidVideoHeader)?;
u32::from_be_bytes(buf)
};
let height = {
let mut buf = [0u8; 4];
reader
.read_exact(&mut buf)
.map_err(|_| Error::InvalidVideoHeader)?;
u32::from_be_bytes(buf)
};
let num_frames = {
let mut buf = [0u8; 4];
reader
.read_exact(&mut buf)
.map_err(|_| Error::InvalidVideoHeader)?;
u32::from_be_bytes(buf)
};
Ok(Self {
width,
height,
num_frames,
reader,
})
}
fn next_frame(&mut self) -> Result<Frame, Error> {
let frame_size = (self.width * self.height * NUM_CHANNELS) as usize;
let mut frame_data = vec![0; frame_size];
// Because we're reading from a stream, each time we call next we'll be moved
// to the next frame
self.reader.read_exact(&mut frame_data).unwrap();
Frame::try_from_data(self.width, self.height, frame_data)
}
}
struct Frame {
width: u32,
height: u32,
data: Array3<u8>,
}
// struct Pixel {
// r: u8,
// g: u8,
// b: u8,
// }
impl Frame {
fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
data: Array3::<u8>::zeros((width as usize, height as usize, NUM_CHANNELS as usize)),
}
}
fn try_from_data(width: u32, height: u32, data: Vec<u8>) -> Result<Self, Error> {
let data = Array3::<u8>::from_shape_vec(
(width as usize, height as usize, NUM_CHANNELS as usize),
data,
)?;
Ok(Self {
width,
height,
data,
})
}
fn as_32bit(&self) -> Vec<u32> {
self.data
.iter()
.map(|&x| {
let r = x as u32;
let g = x as u32;
let b = x as u32;
(r << 16) | (g << 8) | b
})
.collect()
}
}
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("Invalid video header")]
InvalidVideoHeader,
#[error("Invalid frame shape")]
InvalidFrameSize(#[from] ndarray::ShapeError),
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::parse();
let rules: Rules = serde_json::from_reader(std::fs::File::open(config.rules)?)?;
let reader = &mut std::io::BufReader::new(std::fs::File::open(config.input)?);
let mut input_video = Video::try_from_reader(reader)?;
let mut window = Window::new(
"Test - ESC to exit",
input_video.width as usize,
input_video.height as usize,
WindowOptions::default(),
)
.unwrap_or_else(|e| {
panic!("{}", e);
});
while window.is_open() && !window.is_key_down(minifb::Key::Escape) {
let input_frame = input_video.next_frame()?;
let new_frame = apply_rules(&input_frame, &rules);
window
.update_with_buffer(
&new_frame.as_32bit(),
new_frame.width as usize,
new_frame.height as usize,
)
.unwrap();
}
Ok(())
}
fn apply_rules(frame: &Frame, rules: &Rules) -> Frame {
rules.rects.iter().fold(
Frame::new(rules.size[0], rules.size[1]),
|mutate_frame, rule| apply_rule(&frame, mutate_frame, rule),
)
}
fn apply_rule(input_frame: &Frame, mut mutate_frame: Frame, rule: &Rects) -> Frame {
// println!(
// "[Z index {}] Copy ({} + {}, {} + {}) to ({}, {})",
// rule.z,
// rule.source.x,
// rule.source.width,
// rule.source.y,
// rule.source.height,
// rule.destination.x,
// rule.destination.y,
// );
for (x_count, x) in (rule.source.x..(rule.source.x + rule.source.width)).enumerate() {
for (y_count, y) in (rule.source.y..(rule.source.y + rule.source.height)).enumerate() {
for z in 0..NUM_CHANNELS as usize {
let pixel = input_frame.data[[x as usize, y as usize, z]];
// println!("Pixel at ({}, {}) = {}", x, y, pixel);
mutate_frame.data[[
(rule.destination.x + x_count as u32) as usize,
(rule.destination.y + y_count as u32) as usize,
z,
]] = apply_alpha(
pixel,
mutate_frame.data[[x as usize, y as usize, z]],
rule.alpha,
);
}
}
}
mutate_frame
}
fn apply_alpha(foreground: u8, background: u8, alpha: f32) -> u8 {
(foreground as f32 * alpha + background as f32 * (1.0 - alpha)) as u8
}
#[cfg(test)]
// Pretty assert is too slow for this amount of data
// use pretty_assertions::assert_eq;
#[test]
fn basic() {
let input_video_path = PathBuf::from(format!(
"{}/fixtures/test/input.rvid",
env!("CARGO_MANIFEST_DIR")
));
let output_video_path = PathBuf::from(format!(
"{}/fixtures/test/expected.rvid",
env!("CARGO_MANIFEST_DIR")
));
let rules_path = PathBuf::from(format!(
"{}/fixtures/test/rules.json",
env!("CARGO_MANIFEST_DIR")
));
println!("{:?}", input_video_path);
let mut rules: Rules =
serde_json::from_reader(std::fs::File::open(rules_path).unwrap()).unwrap();
rules.rects.sort_by(|a, b| a.partial_cmp(b).unwrap());
let in_reader = &mut std::io::BufReader::new(std::fs::File::open(input_video_path).unwrap());
let mut input_video = Video::try_from_reader(in_reader).unwrap();
let out_reader = &mut std::io::BufReader::new(std::fs::File::open(output_video_path).unwrap());
let mut output_video = Video::try_from_reader(out_reader).unwrap();
let input_frame = input_video.next_frame().unwrap();
let expected_frame = output_video.next_frame().unwrap();
let new_frame = apply_rules(&input_frame, &rules);
// write the new frame to a file
let out = std::fs::File::create("./generated.frame").unwrap();
let mut writer = std::io::BufWriter::new(out);
writer.write_all(&new_frame.data.into_raw_vec()).unwrap();
let out = std::fs::File::create("./expected.frame").unwrap();
let mut writer = std::io::BufWriter::new(out);
writer
.write_all(&expected_frame.data.into_raw_vec())
.unwrap();
// dbg!(&new_frame.data);
// assert_eq!(new_frame.data, expected_frame.data);
}