initial NERVE upscale Space
Browse files- README.md +14 -7
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/nerve_arch.cpython-312.pyc +0 -0
- app.py +104 -0
- nerve_arch.py +97 -0
- requirements.txt +7 -0
README.md
CHANGED
|
@@ -1,13 +1,20 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
colorTo: pink
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: NERVE
|
| 3 |
+
colorFrom: indigo
|
| 4 |
+
colorTo: green
|
|
|
|
| 5 |
sdk: gradio
|
| 6 |
+
sdk_version: 5.25.2
|
|
|
|
| 7 |
app_file: app.py
|
| 8 |
pinned: false
|
| 9 |
+
suggested_hardware: zero-a10g
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# NERVE — image upscaling & restoration
|
| 13 |
+
|
| 14 |
+
A tiny demo for the released [NERVE](https://huggingface.co/Phips/NERVE) models.
|
| 15 |
+
|
| 16 |
+
- **2x / 4x release** — for clean images.
|
| 17 |
+
- **4x OTF GAN** — for degraded images (blurry, noisy, JPEG-compressed).
|
| 18 |
+
|
| 19 |
+
Input: an image. Output: an uncompressed PNG you can download.
|
| 20 |
+
Runs on ZeroGPU; large images are processed in tiles.
|
__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (6.26 kB). View file
|
|
|
__pycache__/nerve_arch.cpython-312.pyc
ADDED
|
Binary file (5.19 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""NERVE — upscale & restore images with the released NERVE models.
|
| 2 |
+
|
| 3 |
+
Simple Space: pick a model, drop an image, get an uncompressed PNG back.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import numpy as np
|
| 10 |
+
import spaces
|
| 11 |
+
import torch
|
| 12 |
+
from huggingface_hub import hf_hub_download
|
| 13 |
+
from PIL import Image
|
| 14 |
+
from safetensors.torch import load_file
|
| 15 |
+
|
| 16 |
+
from nerve_arch import nerve
|
| 17 |
+
|
| 18 |
+
REPO = "Phips/NERVE"
|
| 19 |
+
MODELS: dict[str, tuple[str, int]] = {
|
| 20 |
+
"NERVE 2x — release (clean input)": ("models/2x_NERVE_release.safetensors", 2),
|
| 21 |
+
"NERVE 4x — release (clean input)": ("models/4x_NERVE_release.safetensors", 4),
|
| 22 |
+
"NERVE 4x — OTF GAN (blurry / noisy / JPEG input)": (
|
| 23 |
+
"models/4x_NERVE_OTF_gan.safetensors",
|
| 24 |
+
4,
|
| 25 |
+
),
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
_cache: dict[str, torch.nn.Module] = {}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_model(key: str) -> torch.nn.Module:
|
| 32 |
+
if key not in _cache:
|
| 33 |
+
path, scale = MODELS[key]
|
| 34 |
+
model = nerve(scale=scale)
|
| 35 |
+
model.load_state_dict(
|
| 36 |
+
load_file(hf_hub_download(REPO, path), device="cpu"), strict=True
|
| 37 |
+
)
|
| 38 |
+
_cache[key] = model.eval()
|
| 39 |
+
return _cache[key]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@torch.inference_mode()
|
| 43 |
+
def _run(model: torch.nn.Module, img: np.ndarray, scale: int) -> np.ndarray:
|
| 44 |
+
"""img: float32 HWC RGB in [0,1]; returns float32 HWC RGB in [0,1]."""
|
| 45 |
+
h, w = img.shape[:2]
|
| 46 |
+
tile = 512 # LR tile size; keeps VRAM low on ZeroGPU
|
| 47 |
+
overlap = 32
|
| 48 |
+
out = np.zeros((h * scale, w * scale, 3), dtype=np.float32)
|
| 49 |
+
for y in range(0, h, tile - overlap):
|
| 50 |
+
for x in range(0, w, tile - overlap):
|
| 51 |
+
y1, x1 = min(y + tile, h), min(x + tile, w)
|
| 52 |
+
y0, x0 = max(0, y1 - tile), max(0, x1 - tile)
|
| 53 |
+
crop = img[y0:y1, x0:x1]
|
| 54 |
+
t = torch.from_numpy(crop.transpose(2, 0, 1)).unsqueeze(0).cuda()
|
| 55 |
+
y_hat = model(t).clamp(0, 1).squeeze(0).permute(1, 2, 0).cpu().numpy()
|
| 56 |
+
out[y0 * scale : y1 * scale, x0 * scale : x1 * scale] = y_hat
|
| 57 |
+
return out
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@spaces.GPU(duration=90)
|
| 61 |
+
def upscale(image: Image.Image, model_key: str) -> Image.Image:
|
| 62 |
+
if image is None:
|
| 63 |
+
raise gr.Error("Please upload an image first.")
|
| 64 |
+
model = get_model(model_key).cuda()
|
| 65 |
+
scale = MODELS[model_key][1]
|
| 66 |
+
|
| 67 |
+
rgb = np.asarray(image.convert("RGB"), dtype=np.float32) / 255.0
|
| 68 |
+
result = _run(model, rgb, scale)
|
| 69 |
+
return Image.fromarray((result * 255.0 + 0.5).astype(np.uint8), mode="RGB")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
with gr.Blocks(title="NERVE", theme=gr.themes.Soft()) as demo:
|
| 73 |
+
gr.Markdown(
|
| 74 |
+
"""
|
| 75 |
+
# NERVE
|
| 76 |
+
Lightweight super-resolution / restoration. Drop an image, pick a model,
|
| 77 |
+
press **Upscale**, download the full-quality PNG.
|
| 78 |
+
|
| 79 |
+
- **release** models are for **clean** images.
|
| 80 |
+
- the **OTF GAN** model is for **degraded** images (blurry, noisy, JPEG-y).
|
| 81 |
+
"""
|
| 82 |
+
)
|
| 83 |
+
with gr.Row():
|
| 84 |
+
with gr.Column():
|
| 85 |
+
inp = gr.Image(type="pil", label="Input image", height=320)
|
| 86 |
+
model = gr.Dropdown(
|
| 87 |
+
choices=list(MODELS),
|
| 88 |
+
value=list(MODELS)[1],
|
| 89 |
+
label="Model",
|
| 90 |
+
)
|
| 91 |
+
btn = gr.Button("Upscale", variant="primary")
|
| 92 |
+
with gr.Column():
|
| 93 |
+
out = gr.Image(type="pil", label="Output (uncompressed PNG)", height=320)
|
| 94 |
+
dl = gr.File(label="Download PNG")
|
| 95 |
+
|
| 96 |
+
def _wrap(image, model_key):
|
| 97 |
+
result = upscale(image, model_key)
|
| 98 |
+
result.save("/tmp/nerve_output.png")
|
| 99 |
+
return result, "/tmp/nerve_output.png"
|
| 100 |
+
|
| 101 |
+
btn.click(_wrap, inputs=[inp, model], outputs=[out, dl])
|
| 102 |
+
|
| 103 |
+
if __name__ == "__main__":
|
| 104 |
+
demo.queue().launch()
|
nerve_arch.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# NERVE — Norm-free Efficient Restoration for Various Edge devices.
|
| 2 |
+
#
|
| 3 |
+
# A lightweight super-resolution network designed for ease of use rather than
|
| 4 |
+
# benchmark chasing: pure convolution (Conv-ReLU-Conv residual blocks, no
|
| 5 |
+
# normalization, no attention, no gating), a single readable file, and standard
|
| 6 |
+
# ops only (Conv / Add / ReLU / PixelShuffle / bicubic Resize) so the checkpoint
|
| 7 |
+
# exports to dynamic-shape ONNX and converts to NCNN/CoreML without custom ops
|
| 8 |
+
# or fused/unfused checkpoint pairs.
|
| 9 |
+
#
|
| 10 |
+
# A 5x5 stem opens the receptive field for real-world degradations; a global
|
| 11 |
+
# residual and a bicubic input residual let the net learn only what bicubic
|
| 12 |
+
# upscaling misses. Default dim=64 / n_blocks=24 (~1.8M params at 4x).
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import math
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
from spandrel.util import store_hyperparameters
|
| 20 |
+
from torch import Tensor, nn
|
| 21 |
+
from torch.nn import functional as F # noqa: N812
|
| 22 |
+
|
| 23 |
+
def _icnr_init(conv: nn.Conv2d, upscale: int) -> None:
|
| 24 |
+
"""ICNR initialization for a Conv2d that feeds a PixelShuffle(upscale).
|
| 25 |
+
|
| 26 |
+
Every sub-pixel phase starts from the same base kernel, so the upsampling
|
| 27 |
+
head cannot begin with a checkerboard bias. Init-only: no graph or runtime
|
| 28 |
+
cost, and no effect once a checkpoint is loaded.
|
| 29 |
+
"""
|
| 30 |
+
out_c = conv.out_channels
|
| 31 |
+
sub = upscale * upscale
|
| 32 |
+
assert out_c % sub == 0
|
| 33 |
+
base_c = out_c // sub
|
| 34 |
+
base = nn.init.kaiming_uniform_(conv.weight[:base_c].clone(), a=math.sqrt(5))
|
| 35 |
+
with torch.no_grad():
|
| 36 |
+
conv.weight.data.zero_()
|
| 37 |
+
for i in range(sub):
|
| 38 |
+
conv.weight.data[i::sub] = base
|
| 39 |
+
if conv.bias is not None:
|
| 40 |
+
nn.init.zeros_(conv.bias)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class NerveBlock(nn.Module):
|
| 44 |
+
"""Conv-ReLU-Conv residual block. No normalization, no shortcuts."""
|
| 45 |
+
|
| 46 |
+
def __init__(self, dim: int) -> None:
|
| 47 |
+
super().__init__()
|
| 48 |
+
self.conv1 = nn.Conv2d(dim, dim, 3, padding=1, bias=False)
|
| 49 |
+
self.conv2 = nn.Conv2d(dim, dim, 3, padding=1, bias=False)
|
| 50 |
+
|
| 51 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 52 |
+
return x + self.conv2(F.relu(self.conv1(x)))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@store_hyperparameters()
|
| 56 |
+
class NERVE(nn.Module):
|
| 57 |
+
"""5x5 stem -> n_blocks residual blocks -> PixelShuffle head -> + bicubic(x)."""
|
| 58 |
+
|
| 59 |
+
hyperparameters = {} # noqa: RUF012
|
| 60 |
+
|
| 61 |
+
def __init__(
|
| 62 |
+
self,
|
| 63 |
+
*,
|
| 64 |
+
in_ch: int = 3,
|
| 65 |
+
out_ch: int = 3,
|
| 66 |
+
dim: int = 64,
|
| 67 |
+
n_blocks: int = 24,
|
| 68 |
+
upscale: int = 4,
|
| 69 |
+
) -> None:
|
| 70 |
+
super().__init__()
|
| 71 |
+
self.upscale = upscale
|
| 72 |
+
|
| 73 |
+
self.stem = nn.Conv2d(in_ch, dim, 5, padding=2, bias=False)
|
| 74 |
+
self.blocks = nn.Sequential(*[NerveBlock(dim) for _ in range(n_blocks)])
|
| 75 |
+
head = nn.Conv2d(dim, out_ch * upscale**2, 3, padding=1, bias=False)
|
| 76 |
+
self.upsampler = nn.Sequential(head, nn.PixelShuffle(upscale))
|
| 77 |
+
_icnr_init(head, upscale)
|
| 78 |
+
|
| 79 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 80 |
+
feat = self.stem(x)
|
| 81 |
+
out = feat + self.blocks(feat)
|
| 82 |
+
out = self.upsampler(out)
|
| 83 |
+
base = F.interpolate(
|
| 84 |
+
x, scale_factor=self.upscale, mode="bicubic", align_corners=False
|
| 85 |
+
)
|
| 86 |
+
return out + base
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def nerve(
|
| 90 |
+
scale: int = 4,
|
| 91 |
+
*,
|
| 92 |
+
in_ch: int = 3,
|
| 93 |
+
out_ch: int = 3,
|
| 94 |
+
dim: int = 64,
|
| 95 |
+
n_blocks: int = 24,
|
| 96 |
+
) -> NERVE:
|
| 97 |
+
return NERVE(in_ch=in_ch, out_ch=out_ch, dim=dim, n_blocks=n_blocks, upscale=scale)
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==5.25.2
|
| 2 |
+
spaces
|
| 3 |
+
torch
|
| 4 |
+
safetensors
|
| 5 |
+
huggingface_hub
|
| 6 |
+
Pillow
|
| 7 |
+
numpy
|