MangaJPEGLQ as a 1× Core AI asset from the original weights
Browse files- .gitattributes +1 -0
- ESRGAN-MangaJPEGLQ.aimodel/main.hash +1 -0
- ESRGAN-MangaJPEGLQ.aimodel/main.mlirb +3 -0
- ESRGAN-MangaJPEGLQ.aimodel/metadata.json +8 -0
- README.md +11 -0
- export_esrgan.py +56 -0
- parity.py +51 -0
.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())
|