filippawlicki commited on
Commit
6870c03
·
1 Parent(s): 4445f96

Add Gradio demo app for NanoVSR

Browse files
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ checkpoints/
4
+ .venv/
5
+ *.mp4_nanovsr_x*.mp4
6
+ gradio_cached_examples/
7
+ flagged/
8
+ .DS_Store
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
- title: Nanovsr Demo
3
- emoji: 🏃
4
  colorFrom: yellow
5
  colorTo: green
6
  sdk: gradio
@@ -12,4 +12,50 @@ license: mit
12
  short_description: Real-time 4x video super-resolution on edge devices
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: 'NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices'
3
+ emoji: 🔍
4
  colorFrom: yellow
5
  colorTo: green
6
  sdk: gradio
 
12
  short_description: Real-time 4x video super-resolution on edge devices
13
  ---
14
 
15
+ <div align="center">
16
+
17
+ # NanoVSR — Real-Time 4× Video Super-Resolution on Edge Devices
18
+
19
+ [![arXiv](https://img.shields.io/badge/arXiv-2607.10495-b31b1b)](https://arxiv.org/abs/2607.10495) [![GitHub](https://img.shields.io/badge/GitHub-nanovsr-181717?logo=github)](https://github.com/filippawlicki/nanovsr) [![License](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/filippawlicki/nanovsr/blob/main/LICENSE)
20
+
21
+ </div>
22
+
23
+ Interactive demo for **NanoVSR** (ECCV 2026), a fully convolutional, bidirectional-recurrent video
24
+ super-resolution model built for edge devices. It reaches **28.64 dB on REDS4** at **27 FPS on a
25
+ Jetson Orin NX (25 W)**, with no custom CUDA ops — just plain 3×3 convolutions after
26
+ reparameterization.
27
+
28
+ Upload a low-resolution clip (or try the built-in REDS4 example) and pick one of four model sizes
29
+ to see NanoVSR upscale it 4× in real time, with a side-by-side comparison and a before/after slider.
30
+ Inference runs on Hugging Face **ZeroGPU**.
31
+
32
+ ## Model zoo
33
+
34
+ | Model | Params | REDS4 PSNR/SSIM | Orin NX (FPS) |
35
+ | :--- | ---: | :---: | ---: |
36
+ | NanoVSR-226k | 226k | 28.23 / 0.8057 | 43.86 |
37
+ | NanoVSR-644k (baseline) | 644k | 28.64 / 0.8215 | 27.20 |
38
+ | NanoVSR-1.7M | 1.7M | 29.15 / 0.8364 | 19.58 |
39
+ | NanoVSR-5.4M | 5.4M | 29.73 / 0.8526 | 8.66 |
40
+
41
+ Checkpoints are downloaded on demand from the [GitHub releases](https://github.com/filippawlicki/nanovsr/releases)
42
+ of the main repo and cached for the lifetime of the Space.
43
+
44
+ ## Links
45
+
46
+ - Paper: [arXiv:2607.10495](https://arxiv.org/abs/2607.10495)
47
+ - Code, training and TensorRT deployment: [github.com/filippawlicki/nanovsr](https://github.com/filippawlicki/nanovsr)
48
+
49
+ ## Citation
50
+
51
+ ```bibtex
52
+ @misc{pawlicki2026nanovsr,
53
+ title={NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices},
54
+ author={Filip Pawlicki and Marcel Kańduła and Marcin Pucek and Kamil Dobies},
55
+ year={2026},
56
+ eprint={2607.10495},
57
+ archivePrefix={arXiv},
58
+ primaryClass={cs.CV},
59
+ url={https://arxiv.org/abs/2607.10495},
60
+ }
61
+ ```
app.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """NanoVSR — real-time video super-resolution, interactive Gradio demo.
2
+
3
+ Paper: "NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices" (ECCV 2026)
4
+ Code: https://github.com/filippawlicki/nanovsr
5
+ """
6
+ import os
7
+ import tempfile
8
+ import time
9
+
10
+ import cv2
11
+ import gradio as gr
12
+ import imageio.v2 as imageio
13
+ import numpy as np
14
+ import torch
15
+
16
+ try:
17
+ import spaces
18
+ except ImportError:
19
+ class _NoOpGPU:
20
+ def __call__(self, fn=None, **kwargs):
21
+ if fn is not None:
22
+ return fn
23
+ return lambda f: f
24
+
25
+ class spaces:
26
+ GPU = _NoOpGPU()
27
+
28
+ from inference import (
29
+ DEFAULT_MODEL,
30
+ MODEL_INFO,
31
+ SCALE,
32
+ get_model,
33
+ run_chunked_inference,
34
+ )
35
+
36
+ ASSETS_DIR = os.path.join(os.path.dirname(__file__), "assets")
37
+ EXAMPLE_VIDEO = os.path.join(ASSETS_DIR, "examples", "reds4_000_lr.mp4")
38
+ TEASER_VIDEO = os.path.join(ASSETS_DIR, "teaser.mp4")
39
+
40
+ gr.set_static_paths([ASSETS_DIR])
41
+
42
+ MAX_SIDE_CHOICES = [180, 240, 320, 480]
43
+ DIVIDER_PX = 8
44
+
45
+
46
+ def _even(n):
47
+ return n - (n % 2)
48
+
49
+
50
+ def _read_video(path, max_frames):
51
+ reader = imageio.get_reader(path)
52
+ meta = reader.get_meta_data()
53
+ fps = meta.get("fps", 25.0) or 25.0
54
+
55
+ frames = []
56
+ for i, frame in enumerate(reader):
57
+ if max_frames is not None and i >= max_frames:
58
+ break
59
+ if frame.shape[-1] == 4:
60
+ frame = frame[..., :3]
61
+ frames.append(frame)
62
+ reader.close()
63
+
64
+ if not frames:
65
+ raise gr.Error("Could not read any frames from the input video.")
66
+ return frames, float(fps)
67
+
68
+
69
+ def _resize_to_cap(frames, max_side):
70
+ h, w = frames[0].shape[:2]
71
+ longest = max(h, w)
72
+ if longest <= max_side:
73
+ new_h, new_w = _even(h), _even(w)
74
+ else:
75
+ scale = max_side / longest
76
+ new_h, new_w = _even(int(round(h * scale))), _even(int(round(w * scale)))
77
+ new_h, new_w = max(new_h, 8), max(new_w, 8)
78
+
79
+ if (new_h, new_w) == (h, w):
80
+ return frames, (h, w)
81
+ resized = [cv2.resize(f, (new_w, new_h), interpolation=cv2.INTER_AREA) for f in frames]
82
+ return resized, (h, w)
83
+
84
+
85
+ def _label(img, text):
86
+ img = np.ascontiguousarray(img)
87
+ scale = max(0.5, img.shape[0] / 720.0)
88
+ thickness = max(1, int(round(2 * scale)))
89
+ origin = (int(16 * scale), int(34 * scale))
90
+ cv2.putText(img, text, origin, cv2.FONT_HERSHEY_SIMPLEX, scale, (0, 0, 0), thickness * 3, cv2.LINE_AA)
91
+ cv2.putText(img, text, origin, cv2.FONT_HERSHEY_SIMPLEX, scale, (255, 255, 255), thickness, cv2.LINE_AA)
92
+ return img
93
+
94
+
95
+ def _make_comparison_frame(lq_rgb, sr_rgb):
96
+ h, w = sr_rgb.shape[:2]
97
+ base = cv2.resize(lq_rgb, (w, h), interpolation=cv2.INTER_NEAREST)
98
+ base = _label(base, "Input (LR, nearest-upscaled)")
99
+ sr = _label(np.ascontiguousarray(sr_rgb), "NanoVSR")
100
+ divider = np.full((h, DIVIDER_PX, 3), 255, dtype=np.uint8)
101
+ return np.hstack([base, divider, sr])
102
+
103
+
104
+ def _estimate_duration(model_name, frames, chunk_size, use_fp16, progress=None):
105
+ del use_fp16, progress
106
+ n = len(frames)
107
+ factor = {"NanoVSR-226k (fastest)": 0.15, "NanoVSR-644k (baseline)": 0.25,
108
+ "NanoVSR-1.7M": 0.4, "NanoVSR-5.4M (best quality)": 0.8}.get(model_name, 0.3)
109
+ return int(min(180, max(25, 20 + n * factor)))
110
+
111
+
112
+ @spaces.GPU(duration=_estimate_duration)
113
+ def _gpu_infer(model_name, frames, chunk_size, use_fp16, progress=gr.Progress()):
114
+ model = get_model(model_name)
115
+ device = "cuda" if torch.cuda.is_available() else "cpu"
116
+
117
+ def cb(done, total):
118
+ progress(done / total, desc=f"Upscaling frames ({done}/{total}) on {device.upper()}")
119
+
120
+ progress(0, desc="Starting inference...")
121
+ return run_chunked_inference(model, frames, chunk_size, device, use_fp16, progress_cb=cb)
122
+
123
+
124
+ def upscale(video_path, model_name, chunk_size, max_frames, max_side, use_fp16,
125
+ progress=gr.Progress()):
126
+ if not video_path:
127
+ raise gr.Error("Please upload or select a low-resolution video first.")
128
+
129
+ progress(0, desc="Reading video...")
130
+ frames, fps = _read_video(video_path, max_frames)
131
+ frames, orig_size = _resize_to_cap(frames, max_side)
132
+ in_h, in_w = frames[0].shape[:2]
133
+
134
+ t0 = time.time()
135
+ sr_frames = _gpu_infer(model_name, frames, int(chunk_size), bool(use_fp16), progress)
136
+ elapsed = time.time() - t0
137
+
138
+ progress(0.95, desc="Encoding output video...")
139
+ sr_h, sr_w = sr_frames.shape[1:3]
140
+
141
+ out_frames = [_make_comparison_frame(frames[i], sr_frames[i]) for i in range(len(frames))]
142
+ preview_before = cv2.resize(frames[0], (sr_w, sr_h), interpolation=cv2.INTER_NEAREST)
143
+
144
+ out_fd, out_path = tempfile.mkstemp(suffix=f"_nanovsr_x{SCALE}.mp4")
145
+ os.close(out_fd)
146
+ out_fps = max(1.0, min(fps, 60.0))
147
+ writer = imageio.get_writer(out_path, fps=out_fps, codec="libx264",
148
+ ffmpeg_params=["-crf", "18"], macro_block_size=1)
149
+ for f in out_frames:
150
+ writer.append_data(f)
151
+ writer.close()
152
+
153
+ resize_note = (f" (downscaled from {orig_size[1]}x{orig_size[0]} to fit the "
154
+ f"{max_side}px cap)" if (orig_size[1], orig_size[0]) != (in_w, in_h) else "")
155
+ status = (
156
+ f"**Model:** {model_name} · **{MODEL_INFO[model_name]['psnr']}**\n\n"
157
+ f"**Resolution:** {in_w}x{in_h}{resize_note} → {sr_w}x{sr_h} (x{SCALE})\n\n"
158
+ f"**Frames processed:** {len(frames)} · **Inference time:** {elapsed:.2f}s "
159
+ f"({len(frames) / max(elapsed, 1e-6):.1f} FPS)"
160
+ )
161
+ preview = (preview_before, np.ascontiguousarray(sr_frames[0]))
162
+ return out_path, preview, status
163
+
164
+
165
+ THEME = gr.themes.Soft(primary_hue="blue", secondary_hue="cyan")
166
+
167
+ CSS = """
168
+ .status-box { font-size: 0.95em; }
169
+ .badge-row { display: flex !important; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 0; }
170
+ .badge-row a, .badge-row img { display: inline-block !important; margin: 0 !important; }
171
+ """
172
+
173
+ with gr.Blocks(title="NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices") as demo:
174
+ gr.Markdown(
175
+ """
176
+ <div align="center">
177
+
178
+ # NanoVSR — Real-Time 4× Video Super-Resolution on Edge Devices
179
+
180
+ <p class="badge-row" style="display:flex; gap:8px; justify-content:center; flex-wrap:wrap; margin:0;">
181
+ <a href="https://arxiv.org/abs/2607.10495"><img src="https://img.shields.io/badge/arXiv-2607.10495-b31b1b" alt="arXiv"></a>
182
+ <a href="https://github.com/filippawlicki/nanovsr"><img src="https://img.shields.io/badge/GitHub-nanovsr-181717?logo=github" alt="GitHub"></a>
183
+ <a href="https://github.com/filippawlicki/nanovsr/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License"></a>
184
+ </p>
185
+
186
+ </div>
187
+
188
+ A fully convolutional, bidirectional-recurrent video super-resolution model that reaches
189
+ **27–44 FPS on a Jetson Orin NX** while upscaling 4×. Upload a low-resolution clip below and
190
+ watch NanoVSR sharpen it in real time — or press one of the examples to try it instantly.
191
+ """
192
+ )
193
+ gr.HTML(
194
+ f'<video id="header-video" autoplay muted loop playsinline '
195
+ f'style="width:100%; border-radius:12px; display:block;">'
196
+ f'<source src="/gradio_api/file={TEASER_VIDEO}" type="video/mp4"></video>'
197
+ )
198
+
199
+ with gr.Row():
200
+ with gr.Column(scale=1):
201
+ video_in = gr.Video(label="Low-resolution input video", sources=["upload"])
202
+ gr.Examples(
203
+ examples=[[EXAMPLE_VIDEO]],
204
+ inputs=[video_in],
205
+ label="Example (REDS4 clip '000', the paper's own benchmark set)",
206
+ )
207
+ model_dd = gr.Dropdown(
208
+ choices=list(MODEL_INFO.keys()), value=DEFAULT_MODEL, label="Model",
209
+ info="Bigger models = higher quality, more compute.",
210
+ )
211
+ with gr.Accordion("Advanced options", open=False):
212
+ chunk_size = gr.Slider(4, 30, value=15, step=1, label="Temporal chunk size (T)",
213
+ info="Frames processed per forward pass. The paper's edge setting is 15.")
214
+ max_frames = gr.Slider(8, 150, value=60, step=1, label="Max frames to process",
215
+ info="Caps runtime; trim long videos to the first N frames.")
216
+ max_side = gr.Dropdown(MAX_SIDE_CHOICES, value=320, label="Max input side (px)",
217
+ info="Input is downscaled (never upscaled) to this cap before "
218
+ "running NanoVSR, which expects genuinely low-res input.")
219
+ fp16 = gr.Checkbox(value=True, label="FP16 inference (GPU only, faster)")
220
+ run_btn = gr.Button("Upscale ▶", variant="primary")
221
+
222
+ with gr.Column(scale=1):
223
+ video_out = gr.Video(label="NanoVSR result", buttons=["download"])
224
+ slider_out = gr.ImageSlider(label="Before / after (first frame)", buttons=[])
225
+ status_out = gr.Markdown(elem_classes=["status-box"])
226
+
227
+ run_btn.click(
228
+ fn=upscale,
229
+ inputs=[video_in, model_dd, chunk_size, max_frames, max_side, fp16],
230
+ outputs=[video_out, slider_out, status_out],
231
+ )
232
+
233
+ with gr.Accordion("Model zoo & citation", open=False):
234
+ model_list_md = "\n".join(f"- **{name}** — {info['psnr']}" for name, info in MODEL_INFO.items())
235
+ citation_md = model_list_md + "\n\n" + (
236
+ """All models perform 4× upscaling; larger models trade speed for quality. Full details,
237
+ training code and TensorRT deployment scripts are in the
238
+ [GitHub repo](https://github.com/filippawlicki/nanovsr).
239
+
240
+ ```bibtex
241
+ @misc{pawlicki2026nanovsr,
242
+ title={NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices},
243
+ author={Filip Pawlicki and Marcel Kańduła and Marcin Pucek and Kamil Dobies},
244
+ year={2026},
245
+ eprint={2607.10495},
246
+ archivePrefix={arXiv},
247
+ primaryClass={cs.CV},
248
+ url={https://arxiv.org/abs/2607.10495},
249
+ }
250
+ ```"""
251
+ )
252
+ gr.Markdown(citation_md)
253
+
254
+ if __name__ == "__main__":
255
+ demo.queue().launch(theme=THEME, css=CSS)
assets/examples/reds4_000_lr.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa60410340d9c8d9f93c8ec74d89b29bb7315644ad4468131df4a736fbd891b0
3
+ size 568786
assets/teaser.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d4ba8cdb816963a41689599f8299ce31eb1bdc956965acacb907b37836fde8bd
3
+ size 748387
inference.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Checkpoint management and inference helpers for the NanoVSR Gradio demo.
2
+
3
+ Vendored/adapted from the main nanovsr repo's utils.py + demo.py so this Space
4
+ has no dependency on the training repo at runtime.
5
+ """
6
+ import os
7
+
8
+ import numpy as np
9
+ import requests
10
+ import torch
11
+
12
+ from models.nanovsr import NanoVSR
13
+
14
+ SCALE = 4
15
+ CKPT_DIR = os.path.join(os.path.dirname(__file__), "checkpoints")
16
+ RELEASE_BASE = "https://github.com/filippawlicki/nanovsr/releases/download/v1.0"
17
+
18
+ MODEL_INFO = {
19
+ "NanoVSR-226k (fastest)": dict(file="nanovsr_226k.pth", num_feat=32, num_blocks=8,
20
+ psnr="28.23 dB REDS4"),
21
+ "NanoVSR-644k (baseline)": dict(file="nanovsr_644k.pth", num_feat=48, num_blocks=12,
22
+ psnr="28.64 dB REDS4"),
23
+ "NanoVSR-1.7M": dict(file="nanovsr_1.7m.pth", num_feat=64, num_blocks=20,
24
+ psnr="29.15 dB REDS4"),
25
+ "NanoVSR-5.4M (best quality)": dict(file="nanovsr_5.4m.pth", num_feat=96, num_blocks=30,
26
+ psnr="29.73 dB REDS4"),
27
+ }
28
+ DEFAULT_MODEL = "NanoVSR-644k (baseline)"
29
+
30
+ _model_cache = {}
31
+
32
+
33
+ def ensure_checkpoint(model_name):
34
+ info = MODEL_INFO[model_name]
35
+ os.makedirs(CKPT_DIR, exist_ok=True)
36
+ path = os.path.join(CKPT_DIR, info["file"])
37
+ if not os.path.exists(path):
38
+ url = f"{RELEASE_BASE}/{info['file']}"
39
+ tmp_path = path + ".part"
40
+ with requests.get(url, stream=True, timeout=60) as resp:
41
+ resp.raise_for_status()
42
+ with open(tmp_path, "wb") as f:
43
+ for chunk in resp.iter_content(chunk_size=1 << 20):
44
+ f.write(chunk)
45
+ os.replace(tmp_path, path)
46
+ return path
47
+
48
+
49
+ def get_model(model_name):
50
+ """Load (and cache on CPU) the requested NanoVSR variant."""
51
+ if model_name in _model_cache:
52
+ return _model_cache[model_name]
53
+
54
+ info = MODEL_INFO[model_name]
55
+ checkpoint_path = ensure_checkpoint(model_name)
56
+ state_dict = torch.load(checkpoint_path, map_location="cpu")
57
+ if isinstance(state_dict, dict):
58
+ for key in ["params_ema", "params", "model_state_dict"]:
59
+ if key in state_dict:
60
+ state_dict = state_dict[key]
61
+ break
62
+
63
+ model = NanoVSR(num_feat=info["num_feat"], num_blocks=info["num_blocks"])
64
+ model.load_state_dict(state_dict, strict=False)
65
+ model.switch_to_deploy()
66
+ model.eval()
67
+
68
+ _model_cache[model_name] = model
69
+ return model
70
+
71
+
72
+ def frames_to_tensor(frames_rgb):
73
+ """List/array of [H, W, 3] uint8 RGB frames -> [T, 3, H, W] float tensor in [0, 1]."""
74
+ batch = np.stack(frames_rgb, axis=0).transpose(0, 3, 1, 2)
75
+ batch = np.ascontiguousarray(batch)
76
+ return torch.from_numpy(batch).float() / 255.0
77
+
78
+
79
+ def tensor_to_frames(tensor):
80
+ """[T, 3, H, W] float tensor in [0, 1] -> [T, H, W, 3] uint8 RGB numpy array."""
81
+ arr = (tensor.clamp(0, 1).mul(255.0).round().to(torch.uint8)
82
+ .permute(0, 2, 3, 1).contiguous().cpu().numpy())
83
+ return arr
84
+
85
+
86
+ @torch.inference_mode()
87
+ def run_chunked_inference(model, frames_rgb, chunk_size, device, use_fp16, progress_cb=None):
88
+ """Run NanoVSR over a list of LR RGB frames in temporal chunks.
89
+
90
+ Returns a [T, H*4, W*4, 3] uint8 RGB numpy array.
91
+ """
92
+ model = model.to(device)
93
+ autocast_ctx = (torch.autocast(device_type="cuda", dtype=torch.float16)
94
+ if use_fp16 and device == "cuda" else torch.autocast(device_type="cpu", enabled=False))
95
+
96
+ outputs = []
97
+ n = len(frames_rgb)
98
+ for start in range(0, n, chunk_size):
99
+ chunk = frames_rgb[start:start + chunk_size]
100
+ lq = frames_to_tensor(chunk).unsqueeze(0).to(device)
101
+ with autocast_ctx:
102
+ sr = model(lq)
103
+ outputs.append(tensor_to_frames(sr.float().squeeze(0)))
104
+ if progress_cb is not None:
105
+ progress_cb(min(start + chunk_size, n), n)
106
+
107
+ model.cpu()
108
+ if device == "cuda":
109
+ torch.cuda.empty_cache()
110
+
111
+ return np.concatenate(outputs, axis=0)
models/__init__.py ADDED
File without changes
models/nanovsr.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ import numpy as np
7
+
8
+ class RepVGGBlock(nn.Module):
9
+ def __init__(self, in_channels, out_channels, stride=1, deploy=False):
10
+ super(RepVGGBlock, self).__init__()
11
+ self.deploy = deploy
12
+ self.stride = stride
13
+ self.in_channels = in_channels
14
+ self.out_channels = out_channels
15
+ self.activation = nn.LeakyReLU(0.1, inplace=True)
16
+
17
+ if deploy:
18
+ self.rbr_reparam = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=True)
19
+ else:
20
+ self.rbr_identity = nn.BatchNorm2d(in_channels) if out_channels == in_channels and stride == 1 else None
21
+ self.rbr_dense = nn.Sequential(
22
+ nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False),
23
+ nn.BatchNorm2d(out_channels)
24
+ )
25
+ self.rbr_1x1 = nn.Sequential(
26
+ nn.Conv2d(in_channels, out_channels, kernel_size=1, padding=0, bias=False),
27
+ nn.BatchNorm2d(out_channels)
28
+ )
29
+
30
+ def forward(self, x):
31
+ if self.deploy:
32
+ return self.activation(self.rbr_reparam(x))
33
+
34
+ if self.rbr_identity is None:
35
+ id_out = 0
36
+ else:
37
+ id_out = self.rbr_identity(x)
38
+
39
+ return self.activation(self.rbr_dense(x) + self.rbr_1x1(x) + id_out)
40
+
41
+ def get_equivalent_kernel_bias(self):
42
+ kernel3x3, bias3x3 = self._fuse_bn_tensor(self.rbr_dense)
43
+
44
+ kernel1x1, bias1x1 = self._fuse_bn_tensor(self.rbr_1x1)
45
+
46
+ kernelid, biasid = self._fuse_bn_tensor(self.rbr_identity)
47
+
48
+ return (
49
+ kernel3x3 + self._pad_1x1_to_3x3_tensor(kernel1x1) + kernelid,
50
+ bias3x3 + bias1x1 + biasid
51
+ )
52
+
53
+ def _pad_1x1_to_3x3_tensor(self, kernel1x1):
54
+ if kernel1x1 is None:
55
+ return 0
56
+ else:
57
+ return F.pad(kernel1x1, [1, 1, 1, 1])
58
+
59
+ def _fuse_bn_tensor(self, branch):
60
+ if branch is None:
61
+ return 0, 0
62
+ if isinstance(branch, nn.Sequential):
63
+ kernel = branch[0].weight
64
+ running_mean = branch[1].running_mean
65
+ running_var = branch[1].running_var
66
+ gamma = branch[1].weight
67
+ beta = branch[1].bias
68
+ eps = branch[1].eps
69
+ else:
70
+ assert isinstance(branch, nn.BatchNorm2d)
71
+ if not hasattr(self, 'id_tensor'):
72
+ input_dim = self.in_channels
73
+ kernel_value = np.zeros((self.in_channels, input_dim, 3, 3), dtype=np.float32)
74
+ for i in range(self.in_channels):
75
+ kernel_value[i, i % input_dim, 1, 1] = 1
76
+ self.id_tensor = torch.from_numpy(kernel_value).to(branch.weight.device)
77
+ kernel = self.id_tensor
78
+ running_mean = branch.running_mean
79
+ running_var = branch.running_var
80
+ gamma = branch.weight
81
+ beta = branch.bias
82
+ eps = branch.eps
83
+
84
+ std = (running_var + eps).sqrt()
85
+ t = (gamma / std).reshape(-1, 1, 1, 1)
86
+ return kernel * t, beta - running_mean * gamma / std
87
+
88
+ def switch_to_deploy(self):
89
+ if self.deploy:
90
+ return
91
+ kernel, bias = self.get_equivalent_kernel_bias()
92
+ self.rbr_reparam = nn.Conv2d(
93
+ in_channels=self.rbr_dense[0].in_channels,
94
+ out_channels=self.rbr_dense[0].out_channels,
95
+ kernel_size=self.rbr_dense[0].kernel_size,
96
+ stride=self.rbr_dense[0].stride,
97
+ padding=self.rbr_dense[0].padding,
98
+ bias=True)
99
+ self.rbr_reparam.weight.data = kernel
100
+ self.rbr_reparam.bias.data = bias
101
+
102
+ self.__delattr__('rbr_dense')
103
+ self.__delattr__('rbr_1x1')
104
+ if hasattr(self, 'rbr_identity'):
105
+ self.__delattr__('rbr_identity')
106
+ self.deploy = True
107
+
108
+
109
+ class PixelShuffleBlock(nn.Module):
110
+ def __init__(self, in_channels, out_channels, upscale_factor=2):
111
+ super(PixelShuffleBlock, self).__init__()
112
+ self.conv = nn.Conv2d(in_channels, out_channels * (upscale_factor ** 2), 3, 1, 1)
113
+ self.pixel_shuffle = nn.PixelShuffle(upscale_factor)
114
+ self.prelu = nn.PReLU()
115
+
116
+ def forward(self, x):
117
+ x = self.conv(x)
118
+ x = self.pixel_shuffle(x)
119
+ return self.prelu(x)
120
+
121
+
122
+ class NanoVSR(nn.Module):
123
+ def __init__(self, num_feat=32, num_blocks=7, deploy=False):
124
+ super(NanoVSR, self).__init__()
125
+ self.num_feat = num_feat
126
+ self.deploy = deploy
127
+
128
+ self.feat_extract = RepVGGBlock(3, num_feat, deploy=deploy)
129
+
130
+ self.forward_net = nn.Sequential(*[RepVGGBlock(num_feat, num_feat, deploy=deploy) for _ in range(num_blocks)])
131
+ self.backward_net = nn.Sequential(*[RepVGGBlock(num_feat, num_feat, deploy=deploy) for _ in range(num_blocks)])
132
+
133
+ self.fusion = nn.Conv2d(num_feat * 2, num_feat, 1, 1, 0, bias=True)
134
+
135
+ self.upsample1 = PixelShuffleBlock(num_feat, num_feat, upscale_factor=2)
136
+ self.upsample2 = PixelShuffleBlock(num_feat, 32, upscale_factor=2)
137
+
138
+ self.conv_last = nn.Conv2d(32, 3, 3, 1, 1, bias=True)
139
+
140
+ def forward(self, x):
141
+ b, t, c, h, w = x.size()
142
+
143
+ x_flat = x.view(-1, c, h, w)
144
+ feats = self.feat_extract(x_flat)
145
+ feats = feats.view(b, t, -1, h, w)
146
+
147
+ forward_feats = []
148
+ feat_prop = torch.zeros_like(feats[:, 0, ...])
149
+ for i in range(t):
150
+ feat_prop = self.forward_net(feats[:, i, ...] + feat_prop)
151
+ forward_feats.append(feat_prop)
152
+
153
+ backward_feats = []
154
+ feat_prop = torch.zeros_like(feats[:, 0, ...])
155
+ for i in range(t - 1, -1, -1):
156
+ feat_prop = self.backward_net(feats[:, i, ...] + feat_prop)
157
+ backward_feats.insert(0, feat_prop)
158
+
159
+ outputs = []
160
+
161
+ for i in range(t):
162
+ f_fused = torch.cat([forward_feats[i], backward_feats[i]], dim=1)
163
+ f_fused = self.fusion(f_fused)
164
+
165
+ out = self.upsample1(f_fused)
166
+ out = self.upsample2(out)
167
+ out = self.conv_last(out)
168
+
169
+ base = F.interpolate(x[:, i, ...], scale_factor=4, mode='bilinear', align_corners=False)
170
+ out += base
171
+ outputs.append(out)
172
+
173
+ final_video = torch.stack(outputs, dim=1)
174
+
175
+ return final_video
176
+
177
+ def switch_to_deploy(self):
178
+ if self.deploy:
179
+ return
180
+
181
+ for module in self.modules():
182
+ if module is self:
183
+ continue
184
+ if hasattr(module, 'switch_to_deploy'):
185
+ module.switch_to_deploy()
186
+
187
+ self.deploy = True
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio==6.20.0
2
+ spaces
3
+ torch>=2.1
4
+ numpy>=1.24
5
+ requests>=2.31
6
+ opencv-python-headless>=4.8
7
+ imageio>=2.34
8
+ imageio-ffmpeg>=0.5