From e9fece97b85c2f590f16b64e870dca8262fef094 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 6 Sep 2026 18:43:56 +0100 Subject: [PATCH] vtracer-bench: codec-free library, codecs behind a cli feature - The library now speaks the image crate's buffer types only: fidelity(&RgbImage, &RgbImage, thresh) -> (FidelityReport, GrayImage). A tracer embedding the metric pulls no image codec at all. - New `cli` feature (default on) brings png/jpeg/webp decoding and gates the vtracer-bench bin (required-features). - dssim-core with default features off: drops the rayon thread pool. - Scores are unchanged: the CLI prints the same value before and after. --- Cargo.lock | 48 +------------------------------- crates/vtracer-bench/Cargo.toml | 18 +++++++++--- crates/vtracer-bench/src/lib.rs | 41 ++++++++++++++------------- crates/vtracer-bench/src/main.rs | 17 +++-------- 4 files changed, 40 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d99ba9..c6e4379 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,31 +194,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - [[package]] name = "crunchy" version = "0.2.4" @@ -239,7 +214,6 @@ checksum = "e3c601412450ff29a9258b2f85b18b38f658caf70fad1692f40ca863d86cb753" dependencies = [ "imgref", "itertools 0.14.0", - "rayon", "rgb", ] @@ -645,26 +619,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "resvg" version = "0.45.1" @@ -994,7 +948,7 @@ dependencies = [ [[package]] name = "vtracer-bench" -version = "1.0.0-alpha.4" +version = "1.0.0-alpha.5" dependencies = [ "dssim-core", "image", diff --git a/crates/vtracer-bench/Cargo.toml b/crates/vtracer-bench/Cargo.toml index 9bd4a2d..f6d6433 100644 --- a/crates/vtracer-bench/Cargo.toml +++ b/crates/vtracer-bench/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "vtracer-bench" description = "Blind fidelity benchmark for raster-to-vector tracers: compare the original raster with a rendered reconstruction and get one 0..1 fidelity score built from PSNR, SSIM and a clustered-diff patch metric." -version.workspace = true +version = "1.0.0-alpha.5" authors.workspace = true edition.workspace = true license.workspace = true @@ -10,9 +10,19 @@ repository.workspace = true categories = ["graphics", "development-tools::testing"] keywords = ["vectorization", "benchmark", "fidelity", "ssim", "psnr"] +[features] +default = ["cli"] +# The CLI decodes originals and rendered candidates from files; the +# library itself only speaks the image crate's buffer types, so a +# tracer embedding the metric pulls no codec at all. +cli = ["image/png", "image/jpeg", "image/webp"] + +[[bin]] +name = "vtracer-bench" +required-features = ["cli"] + [dependencies] visioncortex.workspace = true -dssim-core = "3" +dssim-core = { version = "3", default-features = false } rgb = "0.8" -# Decode-only: trimmed to real input formats (drops the AV1 encoder + OpenEXR). -image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } +image = { version = "0.25", default-features = false } diff --git a/crates/vtracer-bench/src/lib.rs b/crates/vtracer-bench/src/lib.rs index 98e37e4..75223f3 100644 --- a/crates/vtracer-bench/src/lib.rs +++ b/crates/vtracer-bench/src/lib.rs @@ -26,6 +26,7 @@ //! weight: it tracks visual accuracy best and is the axis most robust to a //! mildly compressed or blurred source. +use image::{GrayImage, RgbImage}; use visioncortex::BinaryImage; /// Patch mass fraction that halves the patch subscore. @@ -77,18 +78,14 @@ fn dssim_score(a_rgb: &[u8], b_rgb: &[u8], w: usize, h: usize) -> f64 { val.into() } -/// Compare an original against a candidate reconstruction, both RGB8, w×h. -/// `thresh` is the RGB Euclidean bad-pixel gate (use [`DEFAULT_THRESH`]). -/// Returns the report plus the bad-pixel mask (255/0, one byte per pixel). -pub fn fidelity( - orig_rgb: &[u8], - cand_rgb: &[u8], - w: usize, - h: usize, - thresh: f64, -) -> (FidelityReport, Vec) { - assert_eq!(orig_rgb.len(), w * h * 3); - assert_eq!(cand_rgb.len(), w * h * 3); +/// Compare an original against a candidate reconstruction (same +/// dimensions). `thresh` is the RGB Euclidean bad-pixel gate (use +/// [`DEFAULT_THRESH`]). Returns the report plus the raw bad-pixel mask +/// (255 = bad). +pub fn fidelity(orig: &RgbImage, cand: &RgbImage, thresh: f64) -> (FidelityReport, GrayImage) { + assert_eq!(orig.dimensions(), cand.dimensions(), "rasters must match in size"); + let (w, h) = (orig.width() as usize, orig.height() as usize); + let (orig_rgb, cand_rgb) = (orig.as_raw().as_slice(), cand.as_raw().as_slice()); // PSNR + RMSE + bad-pixel binarization in one pass let mut sse = 0f64; @@ -185,7 +182,7 @@ pub fn fidelity( s_patch, fidelity, }, - mask, + GrayImage::from_raw(w as u32, h as u32, mask).expect("mask dims"), ) } @@ -197,12 +194,16 @@ mod tests { (0..w * h).flat_map(|_| c).collect() } + fn img(w: usize, h: usize, px: &[u8]) -> RgbImage { + RgbImage::from_raw(w as u32, h as u32, px.to_vec()).unwrap() + } + #[test] fn identical_is_one() { let a = flat(64, 64, [120, 90, 200]); - let (r, mask) = fidelity(&a, &a, 64, 64, DEFAULT_THRESH); + let (r, mask) = fidelity(&img(64, 64, &a), &img(64, 64, &a), DEFAULT_THRESH); assert_eq!(r.bad_px, 0); - assert!(mask.iter().all(|&m| m == 0)); + assert!(mask.as_raw().iter().all(|&m| m == 0)); assert!((r.fidelity - 1.0).abs() < 1e-9, "fidelity {}", r.fidelity); } @@ -221,8 +222,8 @@ mod tests { let (x, y) = ((k % 16) * 4, (k / 16) * 4); // 4px spacing: 256 singleton clusters dust[(y * 64 + x) * 3..(y * 64 + x) * 3 + 3].fill(0); } - let (rb, _) = fidelity(&clean, &blob, 64, 64, DEFAULT_THRESH); - let (rd, _) = fidelity(&clean, &dust, 64, 64, DEFAULT_THRESH); + let (rb, _) = fidelity(&img(64, 64, &clean), &img(64, 64, &blob), DEFAULT_THRESH); + let (rd, _) = fidelity(&img(64, 64, &clean), &img(64, 64, &dust), DEFAULT_THRESH); assert_eq!(rb.bad_px, 256); assert_eq!(rd.bad_px, 256); // dust vanishes under the opening entirely; the blob survives @@ -252,7 +253,7 @@ mod tests { fil[(y * 64 + x) * 3..(y * 64 + x) * 3 + 3].fill(0); } } - let (rf, _) = fidelity(&clean, &fil, 64, 64, DEFAULT_THRESH); + let (rf, _) = fidelity(&img(64, 64, &clean), &img(64, 64, &fil), DEFAULT_THRESH); assert_eq!(rf.bad_px, 128); assert_eq!(rf.clusters, 0); assert!( @@ -266,8 +267,8 @@ mod tests { let a = flat(32, 32, [100, 100, 100]); let mild: Vec = a.iter().map(|&v| v + 4).collect(); let harsh: Vec = a.iter().map(|&v| v + 60).collect(); - let (rm, _) = fidelity(&a, &mild, 32, 32, DEFAULT_THRESH); - let (rh, _) = fidelity(&a, &harsh, 32, 32, DEFAULT_THRESH); + let (rm, _) = fidelity(&img(32, 32, &a), &img(32, 32, &mild), DEFAULT_THRESH); + let (rh, _) = fidelity(&img(32, 32, &a), &img(32, 32, &harsh), DEFAULT_THRESH); assert!(rm.fidelity > rh.fidelity); assert!(rh.fidelity < 0.4, "harsh {}", rh.fidelity); } diff --git a/crates/vtracer-bench/src/main.rs b/crates/vtracer-bench/src/main.rs index 391afbf..61aaf48 100644 --- a/crates/vtracer-bench/src/main.rs +++ b/crates/vtracer-bench/src/main.rs @@ -37,22 +37,13 @@ fn main() { } let orig = image::open(&args[1]).expect("open original").to_rgb8(); - let (w, h) = (orig.width() as usize, orig.height() as usize); - let img = image::open(&args[2]).expect("open candidate").to_rgb8(); - assert_eq!( - (img.width() as usize, img.height() as usize), - (w, h), - "candidate raster must match original dimensions" - ); - let cand: Vec = img.into_raw(); + let cand = image::open(&args[2]).expect("open candidate").to_rgb8(); + assert_eq!(cand.dimensions(), orig.dimensions(), "candidate raster must match original dimensions"); - let (r, mask) = fidelity(orig.as_raw(), &cand, w, h, thresh); + let (r, mask) = fidelity(&orig, &cand, thresh); if let Some(out) = mask_out { - image::GrayImage::from_raw(w as u32, h as u32, mask) - .unwrap() - .save(&out) - .expect("save mask"); + mask.save(&out).expect("save mask"); } println!(