MarcinEU's picture
Add fp32 ONNX model, card, usage example, comparison samples, and conversion tooling
c07c0dc
Raw
History Blame Contribute Delete
3.74 kB
"""
Validate the ONNX export against the PyTorch reference.
Three checks:
1) Random-input parity: torch model logits vs ORT logits (the definitive graph check).
2) Cactus end-to-end parity: replicate refiners' MVANet eval path (resize 1024 bilinear ->
image_to_tensor -> ImageNet normalize -> model().sigmoid() -> resize back) for BOTH
torch and ORT, and compare each to the golden expected_cactus_mask.png.
3) Save the ORT and torch masks for visual inspection.
"""
import sys
from pathlib import Path
import numpy as np
import onnxruntime as ort
import torch
from PIL import Image
from refiners.solutions import BoxSegmenter
from refiners.fluxion.utils import image_to_tensor, normalize, tensor_to_image
ROOT = Path(__file__).resolve().parent.parent
MODEL = sys.argv[1] if len(sys.argv) > 1 else str(ROOT / "models" / "mvanet_box_segmenter.onnx")
OUT = ROOT / "assets" / "out"
OUT.mkdir(parents=True, exist_ok=True)
def main() -> None:
torch.manual_seed(0)
print(f"[load] torch model + ORT session ({MODEL})")
seg = BoxSegmenter(device="cpu")
model = seg.model.eval().float()
so = ort.SessionOptions()
sess = ort.InferenceSession(MODEL, sess_options=so, providers=["CPUExecutionProvider"])
iname = sess.get_inputs()[0].name
oname = sess.get_outputs()[0].name
print(f"[ort] input={sess.get_inputs()[0]} output={sess.get_outputs()[0]}")
def run_ort(x: torch.Tensor) -> np.ndarray:
return sess.run([oname], {iname: x.detach().cpu().numpy().astype(np.float32)})[0]
# 1) random parity
x = torch.randn(1, 3, 1024, 1024)
with torch.no_grad():
yt = model(x).numpy()
yo = run_ort(x)
d = np.abs(yt - yo)
st, so_ = 1 / (1 + np.exp(-yt)), 1 / (1 + np.exp(-yo))
print("\n[1] RANDOM input parity (logits):")
print(f" shapes torch={yt.shape} ort={yo.shape}")
print(f" logits max|d|={d.max():.5e} mean|d|={d.mean():.5e}")
print(f" sigmoid max|d|={np.abs(st - so_).max():.5e} mean|d|={np.abs(st - so_).mean():.5e}")
# 2) cactus end-to-end (matches refiners test_mvanet.py: MVANet direct, full image, no box)
img = Image.open(ROOT / "assets" / "cactus.png").convert("RGB")
in_t = image_to_tensor(img.resize((1024, 1024), Image.Resampling.BILINEAR)).squeeze()
in_t = normalize(in_t, [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]).unsqueeze(0)
with torch.no_grad():
pt = model(in_t).sigmoid()
po = torch.from_numpy(1 / (1 + np.exp(-run_ort(in_t)))).float()
print("\n[2] CACTUS end-to-end parity (probabilities):")
print(f" torch-vs-ort prob max|d|={float((pt - po).abs().max()):.5e} mean|d|={float((pt - po).abs().mean()):.5e}")
m_t = tensor_to_image(pt).resize(img.size, Image.Resampling.BILINEAR)
m_o = tensor_to_image(po).resize(img.size, Image.Resampling.BILINEAR)
exp = Image.open(ROOT / "assets" / "expected_cactus_mask.png").convert("L")
a_t = np.asarray(m_t).astype(np.int16)
a_o = np.asarray(m_o).astype(np.int16)
a_e = np.asarray(exp).astype(np.int16)
print(f" MAE(0-255) torch vs expected = {np.abs(a_t - a_e).mean():.3f}")
print(f" MAE(0-255) ort vs expected = {np.abs(a_o - a_e).mean():.3f}")
print(f" MAE(0-255) torch vs ort = {np.abs(a_t - a_o).mean():.3f}")
m_t.save(OUT / "cactus_torch_mask.png")
m_o.save(OUT / "cactus_ort_mask.png")
print(f" saved {OUT/'cactus_torch_mask.png'} and {OUT/'cactus_ort_mask.png'}")
# verdict
ok = float((pt - po).abs().max()) < 5e-2 and np.abs(a_o - a_e).mean() < 3.0
print("\n[verdict]", "PASS" if ok else "REVIEW",
"- ONNX matches PyTorch within tolerance" if ok else "- check tolerances above")
if __name__ == "__main__":
main()