diff --git a/Cargo.toml b/Cargo.toml index b470ec3..d29913d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ exclude = ["test_data", "yuvxyb-dump"] [features] default = ["fastmath"] fastmath = [] +build-acctest = ["image", "plotters"] [dependencies] av-data = "0.4.1" @@ -23,12 +24,20 @@ paste = "1.0.9" thiserror = "1.0.40" v_frame = "0.3.0" +# acctest dependencies +image = { version = "0.24.5", optional = true } +plotters = { version = "0.3.1", optional = true } + [dev-dependencies] criterion = "0.4.0" -image = "0.24.4" +image = "0.24.5" interpolate_name = "0.2.3" rand = "0.8.5" [[bench]] name = "benches" harness = false + +[[bin]] +name = "acctest" +required-features = ["build-acctest"] diff --git a/src/bin/acctest.rs b/src/bin/acctest.rs new file mode 100644 index 0000000..3bff4f1 --- /dev/null +++ b/src/bin/acctest.rs @@ -0,0 +1,107 @@ +use std::path::PathBuf; +use std::ops::{Range, Neg}; + +use anyhow::Result; +use yuvxyb::{LinearRgb, TransferCharacteristic, ColorPrimaries, Rgb}; + +fn main() { + compare("tank_srgb.png", "tank_rgb.png", "tank_comparison.png"); + compare("chimera_srgb.png", "chimera_rgb.png", "chimera_comparison.png"); + compare("chimera_srgb.png", "tank_rgb.png", "useless_comparison.png"); +} + +fn compare(source_path: &str, expected_path: &str, graph_path: &str) { + let source = struct_from_file(source_path, |d, w, h| Rgb::new(d, w, h, TransferCharacteristic::SRGB, ColorPrimaries::BT709)).unwrap(); + let expected = struct_from_file(expected_path, LinearRgb::new).unwrap(); + + let actual = LinearRgb::try_from(source).unwrap(); + + let deltas = compare_data(expected.data(), actual.data()); + draw_graph(deltas, ["R".to_string(), "G".to_string(), "B".to_string()], graph_path); +} + +fn struct_from_file(path: &str, create_fn: fn(Vec<[f32; 3]>, usize, usize) -> Result) -> Result { + let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_data"); + let img = image::open(base_path.join(path)).unwrap(); + + let w = img.width() as usize; + let h = img.height() as usize; + let data: Vec<[f32; 3]> = img + .into_rgb32f() + .chunks_exact(3) + .map(|c| [c[0], c[1], c[2]]) + .collect(); + + create_fn(data, w, h) +} + +// return the delta for each channel/plane +fn compare_data(expected: &[[f32; 3]], actual: &[[f32; 3]]) -> [Vec; 3] { + let mut r = vec![0f32; expected.len()]; + let mut g = vec![0f32; expected.len()]; + let mut b = vec![0f32; expected.len()]; + + for (i, (pix_e, pix_a)) in expected.iter().zip(actual).enumerate() { + r[i] = pix_a[0] - pix_e[0]; + g[i] = pix_a[1] - pix_e[1]; + b[i] = pix_a[2] - pix_e[2]; + } + + [r, g, b] +} + +fn draw_graph(deltas: [Vec; 3], plane_labels: [String; 3], path: &str) { + use plotters::prelude::*; + use plotters::data::fitting_range; + + let root = BitMapBackend::new(path, (640, 360)).into_drawing_area(); + root.fill(&WHITE).unwrap(); + let root = root.margin(5, 5, 5, 20); // leave some space on the right + + let dataset: Vec<(String, Quartiles)> = deltas.iter().zip(&plane_labels).map(|(v, l)| (l.clone(), Quartiles::new(&v))).collect(); + let range = fitting_range(deltas.iter().flatten()); + let range = refit_range(range); + + let mut cc = ChartBuilder::on(&root) + .x_label_area_size(50) + .y_label_area_size(25) + .caption("Conversion errors per color plane", ("sans-serif", 20)) + .build_cartesian_2d( + // -5e-3f32..5e-3f32, + range, + plane_labels.into_segmented(), + ) + .unwrap(); + + cc.configure_mesh() + .x_desc("Error") + .x_label_style(("sans-serif", 20)) + .y_labels(plane_labels.len()) + .y_label_style(("sans-serif", 20)) + .y_label_formatter(&|v| match v { + SegmentValue::Exact(l) => l.to_string(), + SegmentValue::CenterOf(l) => l.to_string(), + SegmentValue::Last => String::new(), + }) + .light_line_style(&WHITE) + .draw() + .unwrap(); + + cc.draw_series(dataset.iter().map(|(l, q)| { + Boxplot::new_horizontal(SegmentValue::CenterOf(l), q) + .width(25) + .whisker_width(0.5) + })) + .unwrap(); + + root.present().unwrap(); +} + +// make "symmetric"/centered around zero +fn refit_range + PartialOrd + Copy>(r: Range) -> Range { + if r.end > -r.start { + -r.end..r.end + } else { + r.start..-r.start + } +} diff --git a/test_data/chimera_rgb.png b/test_data/chimera_rgb.png new file mode 100644 index 0000000..67bf849 Binary files /dev/null and b/test_data/chimera_rgb.png differ diff --git a/test_data/chimera_srgb.png b/test_data/chimera_srgb.png new file mode 100644 index 0000000..1ba3037 Binary files /dev/null and b/test_data/chimera_srgb.png differ diff --git a/test_data/tank_rgb.png b/test_data/tank_rgb.png new file mode 100644 index 0000000..b262621 Binary files /dev/null and b/test_data/tank_rgb.png differ