Phips commited on
Commit
ad7233b
·
verified ·
1 Parent(s): 8343281

initial NERVE upscale Space

Browse files
README.md CHANGED
@@ -1,13 +1,20 @@
1
  ---
2
- title: Nerve
3
- emoji: ⚡
4
- colorFrom: red
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.28.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
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