arraypress commited on
Commit
57e8f83
·
verified ·
1 Parent(s): effebc8

MangaJPEGLQ as a 1× Core AI asset from the original weights

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ ESRGAN-MangaJPEGLQ.aimodel/main.mlirb filter=lfs diff=lfs merge=lfs -text
ESRGAN-MangaJPEGLQ.aimodel/main.hash ADDED
@@ -0,0 +1 @@
 
 
1
+ �RI\�JF8M}@~��Ѥ�vyqU��d(�n��q
ESRGAN-MangaJPEGLQ.aimodel/main.mlirb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9d52495cb34a46384d7d407ebeced1a4f576797155e4c5196428ee6ecf1bbe71
3
+ size 10148014
ESRGAN-MangaJPEGLQ.aimodel/metadata.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "description" : "1x-MangaJPEGLQ: x1 super-resolution, 256x256 -> 256x256 tile, float16, [0,1] RGB in\/out.",
3
+ "assetVersion" : "2.0",
4
+ "producer" : "coreai-core 1.0.0b3",
5
+ "license" : "CC-BY-NC-SA-4.0",
6
+ "author" : "bunzero (MangaJPEGLQ)",
7
+ "creationDate" : "20261012T085404Z"
8
+ }
README.md CHANGED
@@ -31,3 +31,14 @@ VincentGOURBIN/RealESRGAN-CoreML) labels them BSD-3; that is wrong for these two
31
  None to the models: each archive matched the SHA-256 Hugging Face listed upstream and is republished
32
  unzipped. Attribution: 4x-UltraSharp © Kim2091; MangaJPEGLQ © bunzero; Core ML conversion by
33
  VincentGOURBIN. This repository is shared under the same licence (CC BY-NC-SA 4.0).
 
 
 
 
 
 
 
 
 
 
 
 
31
  None to the models: each archive matched the SHA-256 Hugging Face listed upstream and is republished
32
  unzipped. Attribution: 4x-UltraSharp © Kim2091; MangaJPEGLQ © bunzero; Core ML conversion by
33
  VincentGOURBIN. This repository is shared under the same licence (CC BY-NC-SA 4.0).
34
+
35
+ ## MangaJPEGLQ on Core AI (`ESRGAN-MangaJPEGLQ.aimodel`, swift-upscale 2.1+)
36
+
37
+ `upscale --variant manga` crashed in swift-upscale 2.0 / upscale 0.3.0: **MangaJPEGLQ is a 1× model**
38
+ (it cleans JPEG blocks out of greyscale manga without enlarging — OpenModelDB lists `1x-MangaJPEGLQ`,
39
+ bunzero, CC BY-NC-SA 4.0), and the Core ML build was run as if it were 4×. This Core AI asset is
40
+ exported from bunzero's original weights (`1x-MangaJPEGLQ.pth`, OpenModelDB's file host) with
41
+ `export_esrgan.py`: a 256-px tile, float16, RGB in → Rec. 601 luma → the 1-channel model → copied to RGB.
42
+ Parity against the original PyTorch weights on six stock-photo tiles: **min 56.6 dB, mean 61.0 dB**
43
+ (`parity.py`). `upscale` now runs it as a clean-up pass before the default 4× model.
44
+ The Core ML `.mlpackage` builds above stay for older versions.
export_esrgan.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["coreai-core==1.0.0b3", "coreai-torch==0.4.3", "spandrel==0.4.1", "numpy", "pillow"]
4
+ # [tool.uv]
5
+ # index-url = "https://pypi.org/simple"
6
+ # prerelease = "allow"
7
+ # index-strategy = "unsafe-best-match"
8
+ # ///
9
+ """Export an ESRGAN-family upscaler (RRDBNet / SRVGGNetCompact, via spandrel) as a fixed-tile
10
+ Core AI asset with the same contract swift-upscale's CoreAIUpscaler runs for SwinIR-L:
11
+ input "x" [0,1] RGB, output "output" [0,1] RGB, clamped.
12
+ uv run export_esrgan.py <ckpt.pth> <out.aimodel> <tile> <float16|float32> <author> <licence>
13
+ """
14
+ import sys, time
15
+ from pathlib import Path
16
+ import torch
17
+ from spandrel import ModelLoader
18
+ from coreai.runtime import AIModelAssetMetadata
19
+ from coreai_torch import TorchConverter, get_decomp_table
20
+
21
+ ckpt, out, tile, dtype_name, author, licence = sys.argv[1], Path(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6]
22
+ dtype = {"float16": torch.float16, "float32": torch.float32}[dtype_name]
23
+ desc = ModelLoader().load_from_file(ckpt)
24
+ print("arch", type(desc.model).__name__, "scale", desc.scale, flush=True)
25
+
26
+ class Wrapped(torch.nn.Module):
27
+ """RGB in, RGB out. A greyscale model (one input channel — MangaJPEGLQ is built for
28
+ black-and-white manga) sees Rec. 601 luma, and its single output channel is copied to
29
+ all three, so the Swift side feeds and reads RGB either way."""
30
+ def __init__(self, m, in_ch, out_ch):
31
+ super().__init__(); self.m = m; self.in_ch = in_ch; self.out_ch = out_ch
32
+ self.register_buffer("luma", torch.tensor([0.299, 0.587, 0.114]).view(1, 3, 1, 1))
33
+ def forward(self, x):
34
+ if self.in_ch == 1:
35
+ x = (x * self.luma.to(x.dtype)).sum(dim=1, keepdim=True)
36
+ y = self.m(x)
37
+ if self.out_ch == 1:
38
+ y = y.repeat(1, 3, 1, 1)
39
+ return y.clamp(0, 1)
40
+
41
+ print("channels", desc.input_channels, "->", desc.output_channels, flush=True)
42
+ model = Wrapped(desc.model.eval(), desc.input_channels, desc.output_channels).to(dtype)
43
+ example = {"x": torch.rand(1, 3, tile, tile).to(dtype)}
44
+ t0 = time.time()
45
+ with torch.no_grad(), torch.autocast(device_type="cpu", dtype=dtype, enabled=dtype != torch.float32):
46
+ exported = torch.export.export(model, args=(), kwargs=example)
47
+ exported = exported.run_decompositions(get_decomp_table())
48
+ program = TorchConverter(mode=TorchConverter.Mode.RELEASE).add_exported_program(
49
+ exported_program=exported, input_names=["x"], output_names=["output"]).to_coreai()
50
+ md = AIModelAssetMetadata()
51
+ md.author = author
52
+ md.license = licence
53
+ md.model_description = (f"{Path(ckpt).stem}: x{desc.scale} super-resolution, {tile}x{tile} -> "
54
+ f"{tile*desc.scale}x{tile*desc.scale} tile, {dtype_name}, [0,1] RGB in/out.")
55
+ program.save_asset(out, metadata=md)
56
+ print(f"saved {out} in {time.time()-t0:.1f} s", flush=True)
parity.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["coreai-core==1.0.0b3", "coreai-torch==0.4.3", "spandrel==0.4.1", "numpy", "pillow"]
4
+ # [tool.uv]
5
+ # index-url = "https://pypi.org/simple"
6
+ # prerelease = "allow"
7
+ # index-strategy = "unsafe-best-match"
8
+ # ///
9
+ """Core AI export vs the original PyTorch weights (float32), tile by tile, on stock photos."""
10
+ import asyncio, sys, time
11
+ import numpy as np, torch
12
+ from PIL import Image
13
+ from spandrel import ModelLoader
14
+ import coreai.runtime as rt
15
+
16
+ ckpt, path, tile = sys.argv[1], sys.argv[2], int(sys.argv[3])
17
+ images = sys.argv[4:]
18
+ desc = ModelLoader().load_from_file(ckpt)
19
+ net = desc.model.eval().float()
20
+ luma = torch.tensor([0.299, 0.587, 0.114]).view(1, 3, 1, 1)
21
+ def reference(x):
22
+ if desc.input_channels == 1: x = (x * luma).sum(dim=1, keepdim=True)
23
+ y = net(x)
24
+ if desc.output_channels == 1: y = y.repeat(1, 3, 1, 1)
25
+ return y.clamp(0, 1)
26
+ def psnr(a, b):
27
+ mse = float(np.mean((a.astype(np.float64) - b.astype(np.float64)) ** 2))
28
+ return 99.0 if mse == 0 else 10 * np.log10(1.0 / mse)
29
+ async def main():
30
+ rng = np.random.default_rng(0); tiles = []
31
+ for p in images:
32
+ im = np.asarray(Image.open(p).convert("RGB")).astype(np.float32) / 255
33
+ h, w, _ = im.shape
34
+ for _ in range(3):
35
+ y = int(rng.integers(0, max(1, h - tile))); x = int(rng.integers(0, max(1, w - tile)))
36
+ c = im[y:y+tile, x:x+tile]
37
+ if c.shape[:2] == (tile, tile): tiles.append(c)
38
+ m = await rt.AIModel.load(path)
39
+ fn = m.load_function("main")
40
+ scores, times = [], []
41
+ for c in tiles:
42
+ x = torch.from_numpy(c).permute(2, 0, 1)[None].contiguous()
43
+ with torch.no_grad(): ref = reference(x)[0].permute(1, 2, 0).numpy()
44
+ t = time.time()
45
+ out = await fn({"x": rt.NDArray(data=x.half().contiguous())})
46
+ times.append(time.time() - t)
47
+ got = out["output"].numpy().astype(np.float32)[0].transpose(1, 2, 0)
48
+ scores.append(psnr(ref, got))
49
+ print(f"{path.split('/')[-1]:34} scale x{desc.scale} PSNR vs PyTorch fp32 over {len(scores)} tiles: "
50
+ f"min {min(scores):.1f} dB, mean {np.mean(scores):.1f} dB; {np.median(times)*1000:.0f} ms/tile")
51
+ asyncio.run(main())