The cargo-class push deliberately excluded the neural/semantic work to avoid reverting origin; origin never independently changed neural, so this brings it forward cleanly and wires it up. - neural/mod.rs declares the full decoder family; rans.rs gains the categorical decoder it needs (glc/mlicpp/elic/hyperprior/int_synth/ cmic/dcvc/snac + the JPEG-AI entropy/codestream/σ/value/filter port). - runtime.rs uses the shared deterministic ort_session loader. - Restores the semantic/generative path (semantic.rs, min_sim/sim_metric) behind the `semantic` feature, with the bins/tests that construct ModelRequirement updated accordingly. - Re-adds byte-exact-conformance and the tools/ export/encode scripts. Green together: wai-rs all features (75 lib + integration tests) + clippy, byte-exact-conformance (8), wai-web wasm, wai-spz-gen. Co-Authored-By: Claude Opus 4.8 <[email protected]>
154 lines
6 KiB
Python
154 lines
6 KiB
Python
"""Export the mean-scale HYPERPRIOR (mbt2018-mean) sink artifacts for the
|
|
`wai.neural.mbt2018_mean` capability decoded by `wai-rs/src/neural/hyperprior.rs`.
|
|
|
|
Unlike bmshj2018 (factorized, one static CDF per channel), the hyperprior
|
|
conditions each main latent `y` on a side stream `z`: the sink decodes `z`,
|
|
runs `h_s(z)` to get per-element (scale, mean), and decodes `y` with a
|
|
Gaussian-conditional model. So the sink needs FOUR files, all written next to
|
|
the registered model path:
|
|
|
|
hyper_synthesis.onnx z_hat -> [1, 2*M, Sy, Sy] (scales ‖ means, CompressAI chunk order)
|
|
synthesis.onnx y_hat -> [1, 3, H, W] (in [0,1])
|
|
hyper_cdfs_q<q>.json factorized z CDF table (EntropyBottleneck)
|
|
gauss_cdfs_q<q>.json { scale_table, cdf[], cdf_length[], offset[] } (GaussianConditional)
|
|
|
|
The encoder side (wai_mbt2018_mean_encode.py) produces the WAI envelope whose
|
|
two rANS bitstreams these tables decode. CompressAI's rANS is the same coder
|
|
`wai-rs/src/neural/rans.rs` ports (precision=16, bypass=4), so the bytes match.
|
|
|
|
Run (needs torch + compressai + onnxruntime):
|
|
python tools/wai_mbt2018_mean_export.py --quality 3 --size 256 \
|
|
--out-dir wai-web/demo/models/mbt2018_mean
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
from compressai.zoo import models
|
|
|
|
|
|
class HyperSynthesis(torch.nn.Module):
|
|
"""z_hat -> gaussian params [1, 2*M, Sy, Sy] (scales ‖ means)."""
|
|
def __init__(self, net):
|
|
super().__init__()
|
|
self.h_s = net.h_s
|
|
|
|
def forward(self, z_hat: torch.Tensor) -> torch.Tensor:
|
|
return self.h_s(z_hat)
|
|
|
|
|
|
class Synthesis(torch.nn.Module):
|
|
"""y_hat -> image [1, 3, H, W] in [0,1]."""
|
|
def __init__(self, net):
|
|
super().__init__()
|
|
self.g_s = net.g_s
|
|
|
|
def forward(self, y_hat: torch.Tensor) -> torch.Tensor:
|
|
return self.g_s(y_hat).clamp(0.0, 1.0)
|
|
|
|
|
|
def factorized_cdf_blob(entropy_bottleneck, quality: int) -> dict:
|
|
cdf = entropy_bottleneck._quantized_cdf.cpu().int().tolist()
|
|
cdf_len = entropy_bottleneck._cdf_length.cpu().int().tolist()
|
|
offset = entropy_bottleneck._offset.cpu().int().tolist()
|
|
medians = entropy_bottleneck._get_medians().detach().reshape(-1).cpu().float().tolist()
|
|
return {
|
|
"model": "mbt2018-mean/hyper-z",
|
|
"quality": quality,
|
|
"precision": 16, "bypass_precision": 4,
|
|
"channels": len(cdf_len),
|
|
"cdf_length": cdf_len,
|
|
"offset": offset,
|
|
"medians": medians,
|
|
"cdf": [row[:cdf_len[i]] for i, row in enumerate(cdf)],
|
|
}
|
|
|
|
|
|
def gaussian_cdf_blob(gc, quality: int) -> dict:
|
|
cdf = gc._quantized_cdf.cpu().int().tolist()
|
|
cdf_len = gc._cdf_length.cpu().int().tolist()
|
|
offset = gc._offset.cpu().int().tolist()
|
|
scale_table = gc.scale_table.detach().reshape(-1).cpu().float().tolist()
|
|
return {
|
|
"model": "mbt2018-mean/gaussian-conditional",
|
|
"quality": quality,
|
|
"precision": 16, "bypass_precision": 4,
|
|
"scale_table": scale_table,
|
|
"cdf_length": cdf_len,
|
|
"offset": offset,
|
|
"cdf": [row[:cdf_len[i]] for i, row in enumerate(cdf)],
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--quality", type=int, default=3, help="CompressAI quality 1..8; must match encoder")
|
|
ap.add_argument("--size", type=int, default=256, help="square image side; probes latent shapes")
|
|
ap.add_argument("--out-dir", required=True)
|
|
ap.add_argument("--opset", type=int, default=17)
|
|
args = ap.parse_args()
|
|
out = Path(args.out_dir); out.mkdir(parents=True, exist_ok=True)
|
|
q = args.quality
|
|
|
|
print(f"loading mbt2018-mean (quality={q})…")
|
|
net = models["mbt2018-mean"](quality=q, pretrained=True).eval()
|
|
net.update(force=True)
|
|
|
|
# Probe latent shapes at the chosen image size.
|
|
with torch.no_grad():
|
|
y = net.g_a(torch.zeros(1, 3, args.size, args.size))
|
|
z = net.h_a(y)
|
|
_, M, Sy, _ = y.shape
|
|
_, N, Sz, _ = z.shape
|
|
print(f" main y: [1,{M},{Sy},{Sy}] hyper z: [1,{N},{Sz},{Sz}]")
|
|
|
|
z_dummy = torch.zeros(1, N, Sz, Sz)
|
|
y_dummy = torch.zeros(1, M, Sy, Sy)
|
|
hs, gs = HyperSynthesis(net).eval(), Synthesis(net).eval()
|
|
with torch.no_grad():
|
|
params = hs(z_dummy)
|
|
assert params.shape[1] == 2 * M, f"h_s gave {params.shape[1]} ch, want 2*M={2*M}"
|
|
img = gs(y_dummy)
|
|
print(f" h_s -> {tuple(params.shape)} (scales‖means) g_s -> {tuple(img.shape)}")
|
|
|
|
print("exporting hyper_synthesis.onnx + synthesis.onnx…")
|
|
torch.onnx.export(
|
|
hs, (z_dummy,), (out / "hyper_synthesis.onnx").as_posix(),
|
|
input_names=["z"], output_names=["params"],
|
|
dynamic_axes={"z": {2: "Sz", 3: "Sz"}, "params": {2: "Sy", 3: "Sy"}},
|
|
opset_version=args.opset, do_constant_folding=True, dynamo=False,
|
|
)
|
|
torch.onnx.export(
|
|
gs, (y_dummy,), (out / "synthesis.onnx").as_posix(),
|
|
input_names=["y"], output_names=["image"],
|
|
dynamic_axes={"y": {2: "Sy", 3: "Sy"}, "image": {2: "H", 3: "W"}},
|
|
opset_version=args.opset, do_constant_folding=True, dynamo=False,
|
|
)
|
|
|
|
(out / f"hyper_cdfs_q{q}.json").write_text(
|
|
json.dumps(factorized_cdf_blob(net.entropy_bottleneck, q), separators=(",", ":")))
|
|
(out / f"gauss_cdfs_q{q}.json").write_text(
|
|
json.dumps(gaussian_cdf_blob(net.gaussian_conditional, q), separators=(",", ":")))
|
|
print(f"wrote 2 ONNX + hyper_cdfs_q{q}.json + gauss_cdfs_q{q}.json → {out}")
|
|
|
|
# ONNX-vs-torch sanity.
|
|
try:
|
|
import numpy as np
|
|
import onnxruntime as ort
|
|
s_hs = ort.InferenceSession((out / "hyper_synthesis.onnx").as_posix(),
|
|
providers=["CPUExecutionProvider"])
|
|
d = float(np.max(np.abs(s_hs.run(None, {"z": z_dummy.numpy()})[0] - params.numpy())))
|
|
print(f"onnxruntime h_s max|onnx-torch| = {d:.3e}")
|
|
if d > 1e-3:
|
|
print("WARNING: h_s numerical mismatch > 1e-3", file=sys.stderr)
|
|
except Exception as e: # noqa: BLE001
|
|
print(f"(skipped onnxruntime check: {e})")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|