Adds a calibration test driving the merged IcciPath with ground truth captured from the
ISO reference decoder running on the real test256.bin: the reference's own
process_dwt444_2 outputs (48-ch network input + the plane's 16 subbands), the selected
model's weights, and the reference's process_comp output.
Result: max|Δ| = 2.4e-07 (float epsilon). This is the first validation of the eICCI
implementation against real chain data rather than against my reading of the source, and
it confirms the whole plane path: conv_first + BN-fold + ReLU -> residual blocks (rect
[1,3]/[3,1] kernels, folded BN, scaled residual) -> conv_last -> out*scale + subbands
-> two-level inverse Haar.
Two things this pinned down, both of which had produced false confirmations earlier:
- the decoder never calls the model's `forward` (which is what post_filters/model_*.onnx
exports); it calls process_dwt444_2 once and process_comp per plane, so the exported
ONNX is the wrong artifact to drive. Weights must come from the reference state_dict,
whose tensors are unfolded and complete (the ONNX initializers are pre-folded and
lack BNY, scaleU/scaleV and the per-block scale).
- model_selection is 1-based (0 = plane not filtered), so [6,3,3] selects active[5] and
active[2] — confirmed by hooking the reference, not by re-reading the code.
Separately verified in the same run (numpy, same ground truth): the joint-UV path — one
shared trunk feeding conv_last_u/conv_last_v with distinct scaleU/scaleV — reproduces
U at 1.62e-07 and V at 1.65e-07, and the two-level Haar DWT reproduces the reference's
Y16/U16/V16 and the 48-channel concat exactly (0.0).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
66 lines
3 KiB
Rust
66 lines
3 KiB
Rust
//! Calibration for [`wai::jpegai_icci`] — the eICCI post-filter's plane path — against
|
||
//! GROUND TRUTH captured from the ISO reference decoder running on `test256.bin`.
|
||
//!
|
||
//! The fixture holds the reference's own `process_dwt444_2` outputs (the 48-channel
|
||
//! network input and the plane's 16 subbands) together with the selected model's
|
||
//! weights, and the reference's `process_comp` output. Weights arrive with BatchNorm
|
||
//! already folded (`w·γ/√(σ²+ε)`, `(b−μ)·γ/√(σ²+ε)+β`, ε=1e-5) — the same fold
|
||
//! [`Conv::fold_bn`] performs.
|
||
//!
|
||
//! NOTE: the weights must come from the reference's PyTorch `state_dict`, NOT from
|
||
//! `post_filters/model_*.onnx` — that graph wraps `forward`, which the decoder never
|
||
//! calls, and its initializers are pre-folded and incomplete (no BNY, no scaleU/scaleV,
|
||
//! no per-residual-block scale).
|
||
//!
|
||
//! env: WAI_JPEGAI_ICCI_FIXTURE = icci_y_fixture.json
|
||
#![cfg(feature = "neural_int")]
|
||
|
||
use wai::jpegai_icci::{Conv, IcciPath, ResBlock};
|
||
|
||
fn f32v(v: &serde_json::Value) -> Vec<f32> {
|
||
v.as_array().unwrap().iter().map(|x| x.as_f64().unwrap() as f32).collect()
|
||
}
|
||
fn conv_of(v: &serde_json::Value) -> Conv {
|
||
let g = |k: &str| v[k].as_u64().unwrap() as usize;
|
||
Conv { w: f32v(&v["w"]), b: f32v(&v["b"]), cin: g("cin"), cout: g("cout"),
|
||
kh: g("kh"), kw: g("kw"), ph: g("ph"), pw: g("pw") }
|
||
}
|
||
|
||
#[test]
|
||
fn icci_plane_path_matches_reference() {
|
||
let path = match std::env::var("WAI_JPEGAI_ICCI_FIXTURE") {
|
||
Ok(p) if std::path::Path::new(&p).exists() => p,
|
||
_ => { eprintln!("skip: set WAI_JPEGAI_ICCI_FIXTURE to icci_y_fixture.json"); return; }
|
||
};
|
||
let v: serde_json::Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
|
||
let (h, w) = (v["h"].as_u64().unwrap() as usize, v["w"].as_u64().unwrap() as usize);
|
||
|
||
let blocks: Vec<ResBlock> = v["blocks"].as_array().unwrap().iter().map(|b| ResBlock {
|
||
conv1: conv_of(&b["c1"]),
|
||
conv2: conv_of(&b["c2"]),
|
||
scale: b["scale"].as_f64().unwrap() as f32,
|
||
}).collect();
|
||
assert_eq!(blocks.len(), 2, "the Y path has two residual blocks");
|
||
|
||
let path_net = IcciPath {
|
||
conv_first: conv_of(&v["conv_first"]),
|
||
blocks,
|
||
conv_last: conv_of(&v["conv_last"]),
|
||
scale: v["scale"].as_f64().unwrap() as f32,
|
||
};
|
||
|
||
let yuv48 = f32v(&v["yuv48"]);
|
||
let sub16 = f32v(&v["sub16"]);
|
||
let expected = f32v(&v["expected"]);
|
||
assert_eq!(yuv48.len(), 48 * h * w, "network input is 48 channels at h×w");
|
||
assert_eq!(sub16.len(), 16 * h * w, "plane subbands are 16 channels");
|
||
|
||
let got = path_net.forward_plane(&yuv48, &sub16, h, w);
|
||
assert_eq!(got.len(), expected.len(), "output is the full-resolution plane");
|
||
|
||
let mut maxd = 0f32;
|
||
for i in 0..got.len() { maxd = maxd.max((got[i] - expected[i]).abs()); }
|
||
eprintln!("eICCI plane path vs reference process_comp: max|Δ| = {maxd:.3e}");
|
||
// float arithmetic on a ~100k-MAC-per-pixel path; ε-level agreement expected
|
||
assert!(maxd < 1e-4, "eICCI plane path diverged from the reference (max|Δ| = {maxd})");
|
||
}
|