open-standards/wai/wai-rs/tests/jpegai_uv_calibration.rs
Transaction Science 48becebcab wai-rs: the jpegai tests on ort rc.15, in the process's one environment
8a7492b23 moved ort to 2.0.0-rc.15 and gave ort_session.rs the environment
rc.15 asks every session builder for; five test files build sessions of their
own (jpegai_e2e and the four calibration tests) and still called
`Session::builder()` with none. They build only with `neural` (and
`legacy_int`), which no push-triggered job builds, and a checked list cut at
ten lines hid them.

rc.15 allows one environment per process, and these tests call into wai in the
same process, so they take wai's: `ort_session::environment` is public as
`wai::ort_environment` (a new path, compatible). All five build and run under
neural,semantic,legacy_int; without their JPEG AI fixtures they return early,
as before. jpegai_decode_path builds and passes under its features. clippy
reports nothing in the files this touches.

Changelog: none

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 00:15:14 -05:00

63 lines
3.2 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Calibration: my integer `jpegai_synthesis::decode_uv` vs the float reference —
//! run the SAME 256-channel latent through `synthesis16.onnx` (model_uv, ort) and
//! through `decode_uv`, compare the UV planes (max abs diff + PSNR). This is the
//! numerical-faithfulness pass for the chroma decoder. Needs `neural` (ort).
//!
//! env: WAI_JPEGAI_SYNTH16_UV = path to model_uv/synthesis16.onnx,
//! WAI_JPEGAI_UV_WEIGHTS = path to the exported 16-bit uv weights JSON.
#![cfg(all(feature = "neural", feature = "legacy_int"))]
use ort::session::{builder::GraphOptimizationLevel, Session};
use ort::value::Value;
use wai::jpegai_synthesis::{decode_uv, Weights};
fn ort_synth(path: &str, x: Vec<f32>) -> (Vec<f32>, Vec<usize>) {
let mut s = Session::builder(wai::ort_environment().unwrap()).unwrap()
.with_intra_threads(1).unwrap().with_inter_threads(1).unwrap()
.with_optimization_level(GraphOptimizationLevel::Level3).unwrap()
.commit_from_file(path).unwrap();
let name = s.inputs().first().unwrap().name().to_string();
let arr = ndarray::Array4::from_shape_vec((1, 256, 16, 16), x).unwrap();
let v = Value::from_array(arr).unwrap();
let out = s.run(ort::inputs![name.as_str() => v]).unwrap();
let view = out[0].try_extract_array::<f32>().unwrap();
(view.iter().copied().collect(), view.shape().to_vec())
}
#[test]
fn synthesis_uv_int_vs_float_psnr() {
let onnx = match std::env::var("WAI_JPEGAI_SYNTH16_UV") {
Ok(p) if std::path::Path::new(&p).exists() => p,
_ => { eprintln!("skip: set WAI_JPEGAI_SYNTH16_UV to model_uv/synthesis16.onnx"); return; }
};
let wjson = match std::env::var("WAI_JPEGAI_UV_WEIGHTS") {
Ok(p) if std::path::Path::new(&p).exists() => p,
_ => { eprintln!("skip: set WAI_JPEGAI_UV_WEIGHTS to the weights JSON"); return; }
};
// deterministic small latent [256,16,16] (typical post-LSBS magnitude)
let n = 256 * 16 * 16;
let lf: Vec<f32> = (0..n).map(|i| (((i % 97) as f32) - 48.0) / 96.0).collect();
let (uvf, shape) = ort_synth(&onnx, lf.clone());
let (fmn, fmx) = uvf.iter().fold((f32::MAX, f32::MIN), |(a, b), &v| (a.min(v), b.max(v)));
eprintln!("float uv-synth out shape = {:?}, len {}, range [{:.3}, {:.3}]", shape, uvf.len(), fmn, fmx);
eprintln!("float sample [0..6] = {:?}", &uvf[..6]);
let li: Vec<i64> = lf.iter().map(|&x| (x * 65536.0).round() as i64).collect();
let v: serde_json::Value = serde_json::from_slice(&std::fs::read(&wjson).unwrap()).unwrap();
let wt = Weights::from_json(&v).unwrap();
let uvi = decode_uv(&li, &wt);
assert_eq!(uvi.len(), uvf.len(), "output size mismatch (2×256×256 expected)");
let (mut maxd, mut mse) = (0f64, 0f64);
for i in 0..uvi.len() {
let f = uvf[i].clamp(0.0, 255.0) as f64;
let d = (uvi[i] as f64 - f).abs();
maxd = maxd.max(d);
mse += d * d;
}
mse /= uvi.len() as f64;
let psnr = if mse > 0.0 { 20.0 * (255.0f64).log10() - 10.0 * mse.log10() } else { f64::INFINITY };
eprintln!("INT vs FLOAT uv-synthesis: max|Δ| = {:.2} MSE = {:.2} PSNR = {:.2} dB", maxd, mse, psnr);
assert!(psnr > 45.0, "uv-synthesis PSNR {psnr:.2} dB too low (expected ~55-58)");
}