momoapps commited on
Commit
1004324
·
verified ·
1 Parent(s): a386360

Add DDColor paper tiny INT8 (default colouriser), DDColor build scripts, update model card

Browse files
README.md CHANGED
@@ -35,14 +35,25 @@ Pin a commit in your download URL (`/resolve/<commit>/<file>`) and verify the SH
35
  | `u2netp.onnx` | 4,574,861 | `309c8469258dda742793dce0ebea8e6dd393174f89934733ecc8b14c76f4ddd8` | U²-Netp (small) salient-object matte (default background model) | Unmodified copy of `u2netp.onnx` from [danielgatis/rembg release v0.0.0](https://github.com/danielgatis/rembg/releases/tag/v0.0.0) (rembg is MIT), converted from the weights of [xuebinqin/U-2-Net](https://github.com/xuebinqin/U-2-Net) | Apache-2.0, © Xuebin Qin et al. |
36
  | `u2net.onnx` | 175,997,641 | `8d10d2f3bb75ae3b6d527c77944fc5e7dcd94b29809d47a739a7a728a912b491` | U²-Net (full) salient-object matte (HD option) | Unmodified copy of `u2net.onnx` from [danielgatis/rembg release v0.0.0](https://github.com/danielgatis/rembg/releases/tag/v0.0.0) (rembg is MIT), converted from the weights of [xuebinqin/U-2-Net](https://github.com/xuebinqin/U-2-Net) | Apache-2.0, © Xuebin Qin et al. |
37
  | `ddcolor-paper-tiny.onnx` | 220,552,834 | `5479b1b00d8ba2b3c312e5378e75dc93eac6f80c8497e90169801896e1eeb68d` | DDColor-T (`ddcolor_paper_tiny`), ConvNeXt-T encoder + multi-scale colour decoder, fp32, opset 17 (photo colourisation) | Built by MomoApps from the official weights [piddnad/ddcolor_paper_tiny](https://huggingface.co/piddnad/ddcolor_paper_tiny) (commit `cf9fd99c1d7472689ec7413441c1b799a51866a3`, `pytorch_model.bin` sha256 `8a1277bc90a1bfbb6d2d83933a9a6bc821931879ca93e26e4fcec12165d41fce`) and the architecture in [piddnad/DDColor](https://github.com/piddnad/DDColor) (commit `2adb63f2656ac41cbdf7b894cddd94121a3faf13`). The weights load with no missing or unexpected keys. The 8 spectral-norm layers are folded into plain weights, which is exact in eval mode. Exported with `torch.onnx.export` (torch 2.15.0). | Apache-2.0, © Xiaoyang Kang et al. (DAMO Academy, Alibaba) |
 
38
 
39
  ## Input and output contracts
40
 
41
  - **GFPGAN (both files):** input `input`, float32 `[1,3,512,512]` RGB in −1..1 (one aligned face crop). Output `output`, the same shape and range.
42
  - **Real-ESRGAN general x4v3:** input `input`, float32 `[1,3,H,W]` RGB in 0..1, with dynamic H and W. Output `output`, `[1,3,4H,4W]`. Clamp the output to 0..1.
43
- - **DDColor paper tiny:** input `input`, float32 `[1,3,512,512]` RGB in 0..1. Build it as DDColor's `ColorizationPipeline` does: resize the photo to 512×512, convert to Lab (OpenCV float), keep L, set a = b = 0, and convert back to RGB. The result is the greyscale L channel replicated as RGB. Output `output`, float32 `[1,2,512,512]`: the predicted Lab a and b channels on OpenCV's float Lab scale (roughly −128..127; clamp them). Resize a and b to the original size, combine them with the original L, and convert Lab to RGB. Ops include `Einsum`, `Erf` and `LayerNormalization`, which the full ONNX Runtime Android package supports. A reduced-operator ORT Mobile build must include them.
44
  - **U²-Net / U²-Netp:** input `input.1`, float32 `[1,3,320,320]` RGB, with ImageNet normalisation applied after dividing by the peak pixel. The first output is the matte.
45
 
 
 
 
 
 
 
 
 
 
 
46
  ## Training data note: FFHQ
47
 
48
  GFPGAN was trained on the [FFHQ](https://github.com/NVlabs/ffhq-dataset) face dataset. FFHQ's images are published under a
 
35
  | `u2netp.onnx` | 4,574,861 | `309c8469258dda742793dce0ebea8e6dd393174f89934733ecc8b14c76f4ddd8` | U²-Netp (small) salient-object matte (default background model) | Unmodified copy of `u2netp.onnx` from [danielgatis/rembg release v0.0.0](https://github.com/danielgatis/rembg/releases/tag/v0.0.0) (rembg is MIT), converted from the weights of [xuebinqin/U-2-Net](https://github.com/xuebinqin/U-2-Net) | Apache-2.0, © Xuebin Qin et al. |
36
  | `u2net.onnx` | 175,997,641 | `8d10d2f3bb75ae3b6d527c77944fc5e7dcd94b29809d47a739a7a728a912b491` | U²-Net (full) salient-object matte (HD option) | Unmodified copy of `u2net.onnx` from [danielgatis/rembg release v0.0.0](https://github.com/danielgatis/rembg/releases/tag/v0.0.0) (rembg is MIT), converted from the weights of [xuebinqin/U-2-Net](https://github.com/xuebinqin/U-2-Net) | Apache-2.0, © Xuebin Qin et al. |
37
  | `ddcolor-paper-tiny.onnx` | 220,552,834 | `5479b1b00d8ba2b3c312e5378e75dc93eac6f80c8497e90169801896e1eeb68d` | DDColor-T (`ddcolor_paper_tiny`), ConvNeXt-T encoder + multi-scale colour decoder, fp32, opset 17 (photo colourisation) | Built by MomoApps from the official weights [piddnad/ddcolor_paper_tiny](https://huggingface.co/piddnad/ddcolor_paper_tiny) (commit `cf9fd99c1d7472689ec7413441c1b799a51866a3`, `pytorch_model.bin` sha256 `8a1277bc90a1bfbb6d2d83933a9a6bc821931879ca93e26e4fcec12165d41fce`) and the architecture in [piddnad/DDColor](https://github.com/piddnad/DDColor) (commit `2adb63f2656ac41cbdf7b894cddd94121a3faf13`). The weights load with no missing or unexpected keys. The 8 spectral-norm layers are folded into plain weights, which is exact in eval mode. Exported with `torch.onnx.export` (torch 2.15.0). | Apache-2.0, © Xiaoyang Kang et al. (DAMO Academy, Alibaba) |
38
+ | `ddcolor-paper-tiny-int8.onnx` | 56,619,279 | `29f3a5f5f794959faebaf034a142c34712833623f9ce672f6ef8f0a8ae8e5f5a` | DDColor-T, weight-only per-channel INT8 (default colouriser) | Built by MomoApps from `ddcolor-paper-tiny.onnx` above with `scripts/quantize_ddcolor_int8.py`. The 161 Conv/MatMul/Gemm weight tensors are stored as symmetric INT8 with one scale per output channel and expanded by `DequantizeLinear`. All compute stays float32, and Einsum, Erf, LayerNormalization, norms and biases stay in float. Against the float32 ONNX on two DDColor test photos, the mean ab difference is 0.41 / 0.32. Visually indistinguishable apart from edge artifacts the float model already has. | Apache-2.0, © Xiaoyang Kang et al. (DAMO Academy, Alibaba) |
39
 
40
  ## Input and output contracts
41
 
42
  - **GFPGAN (both files):** input `input`, float32 `[1,3,512,512]` RGB in −1..1 (one aligned face crop). Output `output`, the same shape and range.
43
  - **Real-ESRGAN general x4v3:** input `input`, float32 `[1,3,H,W]` RGB in 0..1, with dynamic H and W. Output `output`, `[1,3,4H,4W]`. Clamp the output to 0..1.
44
+ - **DDColor paper tiny (both files):** input `input`, float32 `[1,3,512,512]` RGB in 0..1. Build it as DDColor's `ColorizationPipeline` does: resize the photo to 512×512, convert to Lab (OpenCV float), keep L, set a = b = 0, and convert back to RGB. The result is the greyscale L channel replicated as RGB. Output `output`, float32 `[1,2,512,512]`: the predicted Lab a and b channels on OpenCV's float Lab scale (roughly −128..127; clamp them). Resize a and b to the original size, combine them with the original L, and convert Lab to RGB. Ops include `Einsum`, `Erf` and `LayerNormalization`, which the full ONNX Runtime Android package supports. A reduced-operator ORT Mobile build must include them. The INT8 file also uses `DequantizeLinear` (opset 13+, per-axis). ONNX Runtime's mobile checker recommends the CPU execution provider for both files, and both convert to `.ort` format.
45
  - **U²-Net / U²-Netp:** input `input.1`, float32 `[1,3,320,320]` RGB, with ImageNet normalisation applied after dividing by the peak pixel. The first output is the matte.
46
 
47
+ ## Build scripts (DDColor)
48
+
49
+ `scripts/` holds the scripts that produced the DDColor files. They are deterministic: re-running them on the box gave byte-identical files.
50
+
51
+ - `export_ddcolor_onnx.py DDCOLOR_REPO WEIGHTS_DIR OUT.onnx TEST_IMAGE [PREVIEW.jpg]` exports the float32 ONNX (opset 17) from the official weights. It folds spectral norm into plain weights and checks the result against PyTorch: max |Δab| was 0.0022 on the Audrey Hepburn test photo.
52
+ - `quantize_ddcolor_int8.py SRC.onnx DEST.onnx` produces the weight-only per-channel INT8 file.
53
+ - `compare_ddcolor.py REF.onnx TEST.onnx OUTDIR IMG...` reports the ab differences and writes side-by-side colourised JPGs.
54
+
55
+ The tools used were torch 2.15.0 (CPU), onnx 1.23.2, onnxruntime 1.31.1 and opencv-python-headless.
56
+
57
  ## Training data note: FFHQ
58
 
59
  GFPGAN was trained on the [FFHQ](https://github.com/NVlabs/ffhq-dataset) face dataset. FFHQ's images are published under a
ddcolor-paper-tiny-int8.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:29f3a5f5f794959faebaf034a142c34712833623f9ce672f6ef8f0a8ae8e5f5a
3
+ size 56619279
scripts/compare_ddcolor.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Compare two DDColor ONNX files on photos: ab max/mean difference plus side-by-side colourised JPGs.
3
+ Usage: compare_ddcolor.py REF.onnx TEST.onnx OUTDIR IMG [IMG ...]"""
4
+ import sys, time
5
+ from pathlib import Path
6
+ import cv2, numpy as np, onnxruntime as ort
7
+
8
+ def preprocess(img_bgr): # DDColor ColorizationPipeline preprocessing
9
+ img = (img_bgr / 255.0).astype(np.float32)
10
+ l = cv2.cvtColor(cv2.resize(img, (512, 512)), cv2.COLOR_BGR2Lab)[:, :, :1]
11
+ rgb = cv2.cvtColor(np.concatenate((l, 0 * l, 0 * l), -1), cv2.COLOR_LAB2RGB)
12
+ return rgb.transpose(2, 0, 1)[None].astype(np.float32)
13
+
14
+ def colourise(img_bgr, ab):
15
+ h, w = img_bgr.shape[:2]
16
+ l = cv2.cvtColor((img_bgr / 255.0).astype(np.float32), cv2.COLOR_BGR2Lab)[:, :, :1]
17
+ ab = cv2.resize(ab[0].transpose(1, 2, 0), (w, h), interpolation=cv2.INTER_NEAREST)
18
+ return (cv2.cvtColor(np.concatenate((l, ab), -1), cv2.COLOR_LAB2BGR) * 255).round().clip(0, 255).astype(np.uint8)
19
+
20
+ ref_s = ort.InferenceSession(sys.argv[1], providers=["CPUExecutionProvider"])
21
+ tst_s = ort.InferenceSession(sys.argv[2], providers=["CPUExecutionProvider"])
22
+ out = Path(sys.argv[3]); out.mkdir(parents=True, exist_ok=True)
23
+ for p in sys.argv[4:]:
24
+ img = cv2.imread(p); x = preprocess(img)
25
+ a = ref_s.run(["output"], {"input": x})[0]
26
+ t = time.time(); b = tst_s.run(["output"], {"input": x})[0]; dt = time.time() - t
27
+ d = np.abs(a - b)
28
+ ca, cb = colourise(img, a), colourise(img, b)
29
+ pix = np.abs(ca.astype(int) - cb.astype(int))
30
+ print(f"{Path(p).name}: ab max {d.max():.3f} mean {d.mean():.4f} p99 {np.percentile(d, 99):.3f} | "
31
+ f"8-bit RGB max {pix.max()} mean {pix.mean():.3f} | test run {dt:.2f}s")
32
+ stem = Path(p).stem.split()[0]
33
+ gap = np.full((ca.shape[0], 16, 3), 255, np.uint8)
34
+ side = np.concatenate((ca, gap, cb), 1)
35
+ s = 1400 / side.shape[1]
36
+ cv2.imwrite(str(out / f"{stem}_fp32_vs_test.jpg"), cv2.resize(side, None, fx=s, fy=s, interpolation=cv2.INTER_AREA), [cv2.IMWRITE_JPEG_QUALITY, 92])
scripts/export_ddcolor_onnx.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Export DDColor paper_tiny (Apache-2.0, piddnad/DDColor) to the OptiMix ONNX contract.
3
+
4
+ Weights: huggingface.co/piddnad/ddcolor_paper_tiny @ cf9fd99c1d7472689ec7413441c1b799a51866a3, pytorch_model.bin
5
+ sha256 8a1277bc90a1bfbb6d2d83933a9a6bc821931879ca93e26e4fcec12165d41fce
6
+ Code: github.com/piddnad/DDColor @ 2adb63f2656ac41cbdf7b894cddd94121a3faf13 (basicsr/archs/ddcolor_arch.py)
7
+ Contract: input `input` float32 [1,3,512,512] RGB 0..1, the L channel rendered as grey RGB
8
+ output `output` float32 [1,2,512,512] Lab a,b (OpenCV float Lab scale)
9
+ """
10
+ import hashlib, json, sys
11
+ from pathlib import Path
12
+ import cv2, numpy as np, onnx, onnxruntime as ort, torch
13
+
14
+ # Usage: export_ddcolor_onnx.py DDCOLOR_REPO_DIR WEIGHTS_DIR OUT.onnx TEST_IMAGE [PREVIEW.jpg]
15
+ # DDCOLOR_REPO_DIR: a checkout of github.com/piddnad/DDColor (commit above)
16
+ # WEIGHTS_DIR: holds config.json and pytorch_model.bin from piddnad/ddcolor_paper_tiny (commit above)
17
+ REPO = Path(sys.argv[1]).resolve(); sys.path.insert(0, str(REPO))
18
+ from basicsr.archs.ddcolor_arch import DDColor
19
+
20
+ WEIGHTS = Path(sys.argv[2])
21
+ OUT = Path(sys.argv[3])
22
+ TEST_IMAGE = sys.argv[4]
23
+ PREVIEW = sys.argv[5] if len(sys.argv) > 5 else None
24
+ OPSET = 17
25
+ cfg = json.loads((WEIGHTS / "config.json").read_text())
26
+ model = DDColor(**cfg)
27
+ state = torch.load(WEIGHTS / "pytorch_model.bin", map_location="cpu", weights_only=True)
28
+ state = state.get("params", state)
29
+ missing, unexpected = model.load_state_dict(state, strict=False)
30
+ print("missing", missing, "unexpected", unexpected)
31
+ assert not unexpected, unexpected
32
+ assert all(k.endswith("num_batches_tracked") for k in missing), missing
33
+ model.eval()
34
+
35
+ # Reference = the official model exactly as loaded (spectral-norm hooks intact).
36
+ img = cv2.imread(TEST_IMAGE)
37
+
38
+ def preprocess(img_bgr):
39
+ img = (img_bgr / 255.0).astype(np.float32)
40
+ r = cv2.resize(img, (512, 512))
41
+ l = cv2.cvtColor(r, cv2.COLOR_BGR2Lab)[:, :, :1]
42
+ lab = np.concatenate((l, np.zeros_like(l), np.zeros_like(l)), axis=-1)
43
+ rgb = cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)
44
+ return rgb.transpose(2, 0, 1)[None].astype(np.float32)
45
+
46
+ x = preprocess(img)
47
+ with torch.no_grad():
48
+ ref = model(torch.from_numpy(x)).numpy()
49
+
50
+ # Bake spectral norm into plain weights (W / sigma with the stored u, v; eval mode
51
+ # runs no power iteration), so the graph does not carry weight_orig plus a reshaped copy.
52
+ from torch.nn.utils import remove_spectral_norm
53
+ baked = 0
54
+ for mod in model.modules():
55
+ if hasattr(mod, "weight_orig"):
56
+ remove_spectral_norm(mod); baked += 1
57
+ print("spectral norms baked", baked)
58
+ with torch.no_grad():
59
+ torch.onnx.export(model, torch.rand(1, 3, 512, 512), str(OUT), opset_version=OPSET,
60
+ input_names=["input"], output_names=["output"], dynamo=False,
61
+ do_constant_folding=True)
62
+ m = onnx.load(str(OUT)); onnx.checker.check_model(m)
63
+ print("opset", [(o.domain, o.version) for o in m.opset_import])
64
+ print("inputs", [(i.name, [d.dim_value for d in i.type.tensor_type.shape.dim]) for i in m.graph.input])
65
+ print("outputs", [(o.name, [d.dim_value for d in o.type.tensor_type.shape.dim]) for o in m.graph.output])
66
+
67
+ with torch.no_grad():
68
+ baked_ref = model(torch.from_numpy(x)).numpy()
69
+ print("baked vs official torch max abs diff", float(np.abs(baked_ref - ref).max()))
70
+ sess = ort.InferenceSession(str(OUT), providers=["CPUExecutionProvider"])
71
+ got = sess.run(["output"], {"input": x})[0]
72
+ print("shape", got.shape, "ref range", float(ref.min()), float(ref.max()))
73
+ print("max abs diff", float(np.abs(got - ref).max()), "mean abs diff", float(np.abs(got - ref).mean()))
74
+ # Colourise the test image with the ONNX output, as DDColor's pipeline does.
75
+ h, w = img.shape[:2]
76
+ orig_l = cv2.cvtColor((img / 255.0).astype(np.float32), cv2.COLOR_BGR2Lab)[:, :, :1]
77
+ ab = cv2.resize(got[0].transpose(1, 2, 0), (w, h), interpolation=cv2.INTER_NEAREST)
78
+ out = cv2.cvtColor(np.concatenate((orig_l, ab), -1), cv2.COLOR_LAB2BGR)
79
+ if PREVIEW:
80
+ cv2.imwrite(PREVIEW, (out * 255).round().clip(0, 255).astype(np.uint8))
81
+ print("size", OUT.stat().st_size, "sha256", hashlib.sha256(OUT.read_bytes()).hexdigest())
scripts/quantize_ddcolor_int8.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Weight-only per-channel INT8 for ddcolor-paper-tiny.onnx (OptiMix).
3
+
4
+ Only weight initializers consumed by Conv, MatMul or Gemm are quantised (symmetric INT8, one scale per
5
+ output channel). Each becomes int8 data + float scale + DequantizeLinear(axis) feeding the original
6
+ tensor name, so compute stays float32. Einsum, Erf, LayerNormalization, norms, biases and small tensors
7
+ are left untouched in float32. DequantizeLinear with axis needs opset >= 13; the graph is opset 17.
8
+
9
+ Usage: quantize_ddcolor_int8.py SRC.onnx DEST.onnx
10
+ """
11
+ import hashlib, sys
12
+ import numpy as np, onnx
13
+ from onnx import helper, numpy_helper
14
+
15
+ src, dest = sys.argv[1], sys.argv[2]
16
+ m = onnx.load(src)
17
+ g = m.graph
18
+ inits = {i.name: i for i in g.initializer}
19
+ consumers = {}
20
+ for n in g.node:
21
+ for k, name in enumerate(n.input):
22
+ consumers.setdefault(name, []).append((n, k))
23
+
24
+ def axis_for(name, arr):
25
+ uses = consumers.get(name, [])
26
+ if not uses:
27
+ return None
28
+ axes = set()
29
+ for n, k in uses:
30
+ if n.op_type == "Conv" and k == 1 and arr.ndim == 4:
31
+ axes.add(0) # [out, in, kh, kw]
32
+ elif n.op_type == "MatMul" and k == 1 and arr.ndim == 2:
33
+ axes.add(1) # B is [K, N]; channel = N
34
+ elif n.op_type == "Gemm" and k == 1 and arr.ndim == 2:
35
+ tb = next((a.i for a in n.attribute if a.name == "transB"), 0)
36
+ axes.add(0 if tb else 1)
37
+ else:
38
+ return None # any other consumer: keep float
39
+ return axes.pop() if len(axes) == 1 else None
40
+
41
+ new_inits, new_nodes, done, kept = [], [], 0, 0
42
+ for init in list(g.initializer):
43
+ arr = numpy_helper.to_array(init)
44
+ ax = axis_for(init.name, arr) if arr.dtype == np.float32 and arr.size >= 4096 else None
45
+ if ax is None:
46
+ new_inits.append(init); kept += arr.nbytes; continue
47
+ red = tuple(i for i in range(arr.ndim) if i != ax)
48
+ scale = np.maximum(np.abs(arr).max(axis=red) / 127.0, 1e-12).astype(np.float32)
49
+ shp = [1] * arr.ndim; shp[ax] = -1
50
+ q = np.clip(np.round(arr / scale.reshape(shp)), -127, 127).astype(np.int8)
51
+ new_inits += [numpy_helper.from_array(q, init.name + "_q"),
52
+ numpy_helper.from_array(scale, init.name + "_scale"),
53
+ numpy_helper.from_array(np.zeros_like(scale, dtype=np.int8), init.name + "_zp")]
54
+ new_nodes.append(helper.make_node("DequantizeLinear",
55
+ [init.name + "_q", init.name + "_scale", init.name + "_zp"], [init.name],
56
+ name=init.name + "_dq", axis=ax))
57
+ done += 1
58
+
59
+ del g.initializer[:]; g.initializer.extend(new_inits)
60
+ nodes = new_nodes + list(g.node)
61
+ del g.node[:]; g.node.extend(nodes)
62
+ onnx.checker.check_model(m)
63
+ onnx.save(m, dest)
64
+ data = open(dest, "rb").read()
65
+ print(f"quantised {done} weights; float32 kept {kept/1e6:.1f} MB")
66
+ print(f"{hashlib.sha256(data).hexdigest()} {dest} ({len(data)} bytes)")