|
| 1 | +use std::path::PathBuf; |
| 2 | +use std::ops::{Range, Neg}; |
| 3 | + |
| 4 | +use anyhow::Result; |
| 5 | +use yuvxyb::{LinearRgb, TransferCharacteristic, ColorPrimaries, Rgb}; |
| 6 | + |
| 7 | +fn main() { |
| 8 | + compare("tank_srgb.png", "tank_rgb.png", "tank_comparison.png"); |
| 9 | + compare("chimera_srgb.png", "chimera_rgb.png", "chimera_comparison.png"); |
| 10 | + compare("chimera_srgb.png", "tank_rgb.png", "useless_comparison.png"); |
| 11 | +} |
| 12 | + |
| 13 | +fn compare(source_path: &str, expected_path: &str, graph_path: &str) { |
| 14 | + let source = struct_from_file(source_path, |d, w, h| Rgb::new(d, w, h, TransferCharacteristic::SRGB, ColorPrimaries::BT709)).unwrap(); |
| 15 | + let expected = struct_from_file(expected_path, LinearRgb::new).unwrap(); |
| 16 | + |
| 17 | + let actual = LinearRgb::try_from(source).unwrap(); |
| 18 | + |
| 19 | + let deltas = compare_data(expected.data(), actual.data()); |
| 20 | + draw_graph(deltas, ["R".to_string(), "G".to_string(), "B".to_string()], graph_path); |
| 21 | +} |
| 22 | + |
| 23 | +fn struct_from_file<T>(path: &str, create_fn: fn(Vec<[f32; 3]>, usize, usize) -> Result<T>) -> Result<T> { |
| 24 | + let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_data"); |
| 25 | + let img = image::open(base_path.join(path)).unwrap(); |
| 26 | + |
| 27 | + let w = img.width() as usize; |
| 28 | + let h = img.height() as usize; |
| 29 | + let data: Vec<[f32; 3]> = img |
| 30 | + .into_rgb32f() |
| 31 | + .chunks_exact(3) |
| 32 | + .map(|c| [c[0], c[1], c[2]]) |
| 33 | + .collect(); |
| 34 | + |
| 35 | + create_fn(data, w, h) |
| 36 | +} |
| 37 | + |
| 38 | +// return the delta for each channel/plane |
| 39 | +fn compare_data(expected: &[[f32; 3]], actual: &[[f32; 3]]) -> [Vec<f32>; 3] { |
| 40 | + let mut r = vec![0f32; expected.len()]; |
| 41 | + let mut g = vec![0f32; expected.len()]; |
| 42 | + let mut b = vec![0f32; expected.len()]; |
| 43 | + |
| 44 | + for (i, (pix_e, pix_a)) in expected.iter().zip(actual).enumerate() { |
| 45 | + r[i] = pix_a[0] - pix_e[0]; |
| 46 | + g[i] = pix_a[1] - pix_e[1]; |
| 47 | + b[i] = pix_a[2] - pix_e[2]; |
| 48 | + } |
| 49 | + |
| 50 | + [r, g, b] |
| 51 | +} |
| 52 | + |
| 53 | +fn draw_graph(deltas: [Vec<f32>; 3], plane_labels: [String; 3], path: &str) { |
| 54 | + use plotters::prelude::*; |
| 55 | + use plotters::data::fitting_range; |
| 56 | + |
| 57 | + let root = BitMapBackend::new(path, (640, 360)).into_drawing_area(); |
| 58 | + root.fill(&WHITE).unwrap(); |
| 59 | + let root = root.margin(5, 5, 5, 20); // leave some space on the right |
| 60 | + |
| 61 | + let dataset: Vec<(String, Quartiles)> = deltas.iter().zip(&plane_labels).map(|(v, l)| (l.clone(), Quartiles::new(&v))).collect(); |
| 62 | + let range = fitting_range(deltas.iter().flatten()); |
| 63 | + let range = refit_range(range); |
| 64 | + |
| 65 | + let mut cc = ChartBuilder::on(&root) |
| 66 | + .x_label_area_size(50) |
| 67 | + .y_label_area_size(25) |
| 68 | + .caption("Conversion errors per color plane", ("sans-serif", 20)) |
| 69 | + .build_cartesian_2d( |
| 70 | + // -5e-3f32..5e-3f32, |
| 71 | + range, |
| 72 | + plane_labels.into_segmented(), |
| 73 | + ) |
| 74 | + .unwrap(); |
| 75 | + |
| 76 | + cc.configure_mesh() |
| 77 | + .x_desc("Error") |
| 78 | + .x_label_style(("sans-serif", 20)) |
| 79 | + .y_labels(plane_labels.len()) |
| 80 | + .y_label_style(("sans-serif", 20)) |
| 81 | + .y_label_formatter(&|v| match v { |
| 82 | + SegmentValue::Exact(l) => l.to_string(), |
| 83 | + SegmentValue::CenterOf(l) => l.to_string(), |
| 84 | + SegmentValue::Last => String::new(), |
| 85 | + }) |
| 86 | + .light_line_style(&WHITE) |
| 87 | + .draw() |
| 88 | + .unwrap(); |
| 89 | + |
| 90 | + cc.draw_series(dataset.iter().map(|(l, q)| { |
| 91 | + Boxplot::new_horizontal(SegmentValue::CenterOf(l), q) |
| 92 | + .width(25) |
| 93 | + .whisker_width(0.5) |
| 94 | + })) |
| 95 | + .unwrap(); |
| 96 | + |
| 97 | + root.present().unwrap(); |
| 98 | +} |
| 99 | + |
| 100 | +// make "symmetric"/centered around zero |
| 101 | +fn refit_range<T: Neg<Output = T> + PartialOrd + Copy>(r: Range<T>) -> Range<T> { |
| 102 | + if r.end > -r.start { |
| 103 | + -r.end..r.end |
| 104 | + } else { |
| 105 | + r.start..-r.start |
| 106 | + } |
| 107 | +} |
0 commit comments