File size: 4,138 Bytes
1004324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/usr/bin/env python3
"""Export DDColor paper_tiny (Apache-2.0, piddnad/DDColor) to the OptiMix ONNX contract.

Weights: huggingface.co/piddnad/ddcolor_paper_tiny @ cf9fd99c1d7472689ec7413441c1b799a51866a3, pytorch_model.bin
         sha256 8a1277bc90a1bfbb6d2d83933a9a6bc821931879ca93e26e4fcec12165d41fce
Code:    github.com/piddnad/DDColor @ 2adb63f2656ac41cbdf7b894cddd94121a3faf13 (basicsr/archs/ddcolor_arch.py)
Contract: input  `input`  float32 [1,3,512,512] RGB 0..1, the L channel rendered as grey RGB
          output `output` float32 [1,2,512,512] Lab a,b (OpenCV float Lab scale)
"""
import hashlib, json, sys
from pathlib import Path
import cv2, numpy as np, onnx, onnxruntime as ort, torch

# Usage: export_ddcolor_onnx.py DDCOLOR_REPO_DIR WEIGHTS_DIR OUT.onnx TEST_IMAGE [PREVIEW.jpg]
#   DDCOLOR_REPO_DIR: a checkout of github.com/piddnad/DDColor (commit above)
#   WEIGHTS_DIR: holds config.json and pytorch_model.bin from piddnad/ddcolor_paper_tiny (commit above)
REPO = Path(sys.argv[1]).resolve(); sys.path.insert(0, str(REPO))
from basicsr.archs.ddcolor_arch import DDColor

WEIGHTS = Path(sys.argv[2])
OUT = Path(sys.argv[3])
TEST_IMAGE = sys.argv[4]
PREVIEW = sys.argv[5] if len(sys.argv) > 5 else None
OPSET = 17
cfg = json.loads((WEIGHTS / "config.json").read_text())
model = DDColor(**cfg)
state = torch.load(WEIGHTS / "pytorch_model.bin", map_location="cpu", weights_only=True)
state = state.get("params", state)
missing, unexpected = model.load_state_dict(state, strict=False)
print("missing", missing, "unexpected", unexpected)
assert not unexpected, unexpected
assert all(k.endswith("num_batches_tracked") for k in missing), missing
model.eval()

# Reference = the official model exactly as loaded (spectral-norm hooks intact).
img = cv2.imread(TEST_IMAGE)

def preprocess(img_bgr):
    img = (img_bgr / 255.0).astype(np.float32)
    r = cv2.resize(img, (512, 512))
    l = cv2.cvtColor(r, cv2.COLOR_BGR2Lab)[:, :, :1]
    lab = np.concatenate((l, np.zeros_like(l), np.zeros_like(l)), axis=-1)
    rgb = cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)
    return rgb.transpose(2, 0, 1)[None].astype(np.float32)

x = preprocess(img)
with torch.no_grad():
    ref = model(torch.from_numpy(x)).numpy()

# Bake spectral norm into plain weights (W / sigma with the stored u, v; eval mode
# runs no power iteration), so the graph does not carry weight_orig plus a reshaped copy.
from torch.nn.utils import remove_spectral_norm
baked = 0
for mod in model.modules():
    if hasattr(mod, "weight_orig"):
        remove_spectral_norm(mod); baked += 1
print("spectral norms baked", baked)
with torch.no_grad():
    torch.onnx.export(model, torch.rand(1, 3, 512, 512), str(OUT), opset_version=OPSET,
                      input_names=["input"], output_names=["output"], dynamo=False,
                      do_constant_folding=True)
m = onnx.load(str(OUT)); onnx.checker.check_model(m)
print("opset", [(o.domain, o.version) for o in m.opset_import])
print("inputs", [(i.name, [d.dim_value for d in i.type.tensor_type.shape.dim]) for i in m.graph.input])
print("outputs", [(o.name, [d.dim_value for d in o.type.tensor_type.shape.dim]) for o in m.graph.output])

with torch.no_grad():
    baked_ref = model(torch.from_numpy(x)).numpy()
print("baked vs official torch max abs diff", float(np.abs(baked_ref - ref).max()))
sess = ort.InferenceSession(str(OUT), providers=["CPUExecutionProvider"])
got = sess.run(["output"], {"input": x})[0]
print("shape", got.shape, "ref range", float(ref.min()), float(ref.max()))
print("max abs diff", float(np.abs(got - ref).max()), "mean abs diff", float(np.abs(got - ref).mean()))
# Colourise the test image with the ONNX output, as DDColor's pipeline does.
h, w = img.shape[:2]
orig_l = cv2.cvtColor((img / 255.0).astype(np.float32), cv2.COLOR_BGR2Lab)[:, :, :1]
ab = cv2.resize(got[0].transpose(1, 2, 0), (w, h), interpolation=cv2.INTER_NEAREST)
out = cv2.cvtColor(np.concatenate((orig_l, ab), -1), cv2.COLOR_LAB2BGR)
if PREVIEW:
    cv2.imwrite(PREVIEW, (out * 255).round().clip(0, 255).astype(np.uint8))
print("size", OUT.stat().st_size, "sha256", hashlib.sha256(OUT.read_bytes()).hexdigest())