1lch2 commited on
Commit
cfd987c
·
0 Parent(s):

init repo

Browse files
Files changed (4) hide show
  1. .claude/CLAUDE.md +6 -0
  2. app.py +132 -0
  3. model_loader.py +215 -0
  4. requirements.txt +6 -0
.claude/CLAUDE.md ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ To use this application (linch97/UltraSharpV2: Upscale image with UltraSharpV2 model):
2
+ API schema: GET https://linch97-ultrasharpv2.hf.space/gradio_api/info
3
+ Call endpoint: POST https://linch97-ultrasharpv2.hf.space/gradio_api/call/v2/{endpoint} {"param_name": value, ...}
4
+ Poll result: GET https://linch97-ultrasharpv2.hf.space/gradio_api/call/{endpoint}/{event_id}
5
+ File inputs: POST https://linch97-ultrasharpv2.hf.space/gradio_api/upload -F "files=@file.ext", use as: {"path": "<returned-path>", "meta": {"\_type": "gradio.FileData"}, "orig_name": "file.ext"}
6
+ Auth: Bearer $HF_TOKEN (https://huggingface.co/settings/tokens)
app.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UltraSharp V2 — 图像超分辨率 Gradio 应用
3
+ ==========================================
4
+
5
+ ## 模型来源
6
+
7
+ 默认从 Kim2091/UltraSharpV2 公开仓库下载 4x-UltraSharpV2.pth,
8
+ 自动缓存到 ~/.cache/huggingface/hub/,无需手动上传。
9
+
10
+ ## 可选环境变量
11
+
12
+ MODEL_REPO_ID 覆盖默认仓库(默认 Kim2091/UltraSharpV2)
13
+ MODEL_FILENAME 覆盖默认文件名(默认 4x-UltraSharpV2.pth)
14
+ HF_ENDPOINT 镜像站,如 https://hf-mirror.com(国内加速)
15
+ HF_TOKEN 私有仓库的 token(公开仓库无需设置)
16
+
17
+ ## 本地运行
18
+
19
+ python app.py
20
+ # 国内镜像: HF_ENDPOINT=https://hf-mirror.com python app.py
21
+
22
+ ## 部署到 HuggingFace Space
23
+
24
+ 1. 在 Space 设置中将 Hardware 选为 ZeroGPU
25
+ 2. 无需设置 Secrets(模型来自公开仓库)
26
+ 3. 如需国内镜像,添加 Secret: HF_ENDPOINT = https://hf-mirror.com
27
+ """
28
+
29
+ import os
30
+ import gradio as gr
31
+ from model_loader import UltraSharpV2
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # ZeroGPU 兼容层
35
+ # ---------------------------------------------------------------------------
36
+ try:
37
+ import spaces
38
+
39
+ _zerogpu = spaces.GPU(duration=120) # 最长 GPU 占用 120s
40
+ IN_ZEROGPU = bool(os.environ.get("SPACES_ZERO_GPU"))
41
+ except ImportError:
42
+ spaces = None
43
+ _zerogpu = None
44
+ IN_ZEROGPU = False
45
+
46
+
47
+ def _gpu(fn):
48
+ """安全地应用 @spaces.GPU 装饰器(本地开发时退化为无操作)。"""
49
+ return _zerogpu(fn) if _zerogpu is not None else fn
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # 模型:始终在 CPU 上加载(ZeroGPU 启动时 GPU 不可用)
54
+ # ---------------------------------------------------------------------------
55
+ model = UltraSharpV2(device="cpu")
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # 推理函数(生成器模式 — ZeroGPU 硬性要求)
60
+ # ---------------------------------------------------------------------------
61
+ @_gpu
62
+ def on_upscale(image, tile_size, tile_overlap, target_scale):
63
+ if image is None:
64
+ yield None, "请先上传图片"
65
+ return
66
+
67
+ model.to_cuda()
68
+ try:
69
+ result, elapsed = model.upscale(
70
+ image, int(tile_size), int(tile_overlap), float(target_scale)
71
+ )
72
+ finally:
73
+ model.to_cpu()
74
+
75
+ yield result, f"耗时: {elapsed:.2f}s"
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Gradio UI
80
+ # ---------------------------------------------------------------------------
81
+ with gr.Blocks(title="UltraSharp V2") as demo:
82
+ device_display = "ZeroGPU" if IN_ZEROGPU else model.device.upper()
83
+ gr.Markdown("# UltraSharp V2 - 图像超分辨率")
84
+ gr.Markdown(f"**运行设备**: {device_display} | **模型原生倍率**: {model.scale}x")
85
+
86
+ with gr.Row():
87
+ with gr.Column(scale=1):
88
+ input_img = gr.Image(label="输入图片", type="pil", height=400)
89
+ target_scale = gr.Slider(
90
+ label="放大倍率",
91
+ minimum=1.0,
92
+ maximum=4.0,
93
+ value=4.0,
94
+ step=0.05,
95
+ info="> 模型原生倍率时, 输出先 4x 推理再 Lanczos 缩放",
96
+ )
97
+ tile_size = gr.Slider(
98
+ label="tile_size",
99
+ minimum=128,
100
+ maximum=1024,
101
+ value=512,
102
+ step=32,
103
+ info="分块大小,越小越省显存",
104
+ )
105
+ tile_overlap = gr.Slider(
106
+ label="tile_overlap",
107
+ minimum=0,
108
+ maximum=128,
109
+ value=32,
110
+ step=8,
111
+ info="块间重叠像素",
112
+ )
113
+
114
+ with gr.Column(scale=1):
115
+ run_btn = gr.Button("开始推理", variant="primary")
116
+ output_img = gr.Image(label="推理结果", height=400)
117
+ status = gr.Textbox(label="状态", interactive=False)
118
+
119
+ run_btn.click(
120
+ fn=on_upscale,
121
+ inputs=[input_img, tile_size, tile_overlap, target_scale],
122
+ outputs=[output_img, status],
123
+ )
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # ZeroGPU 必须启用 queue(默认并发 1,队列上限 10)
128
+ # ---------------------------------------------------------------------------
129
+ demo.queue(max_size=10, default_concurrency_limit=1)
130
+
131
+ if __name__ == "__main__":
132
+ demo.launch()
model_loader.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import torch
4
+ import numpy as np
5
+ from PIL import Image
6
+
7
+
8
+ def detect_device():
9
+ """Auto-detect device. Returns CPU on ZeroGPU (GPU not available at startup)."""
10
+ if torch.cuda.is_available():
11
+ return "cuda", torch.float16
12
+ return "cpu", torch.float32
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # HuggingFace Hub 模型下载
17
+ # ---------------------------------------------------------------------------
18
+ # 默认从 Kim2091/UltraSharpV2 公开仓库下载,可通过环境变量覆盖:
19
+ # MODEL_REPO_ID - 覆盖默认仓库 ID
20
+ # MODEL_FILENAME - 覆盖默认文件名
21
+ # HF_ENDPOINT - 镜像站地址,如 https://hf-mirror.com(国内加速)
22
+ # HF_TOKEN - 私有仓库的 token(公开仓库无需设置)
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _DEFAULT_REPO_ID = "Kim2091/UltraSharpV2"
26
+ _DEFAULT_FILENAME = "4x-UltraSharpV2.pth"
27
+
28
+ MODEL_CANDIDATES = [
29
+ "4x-UltraSharpV2.pth",
30
+ "4x-UltraSharpV2.safetensors",
31
+ "4x-UltraSharpV2.pt",
32
+ ]
33
+
34
+
35
+ def _download_from_hub(repo_id: str, filename: str) -> str:
36
+ """从 HuggingFace Hub 下载模型文件(自动缓存,重复调用不重新下载)。"""
37
+ from huggingface_hub import hf_hub_download
38
+
39
+ token = os.environ.get("HF_TOKEN")
40
+ endpoint = os.environ.get("HF_ENDPOINT")
41
+ if endpoint:
42
+ print(f"[UltraSharpV2] 使用镜像: {endpoint}")
43
+
44
+ print(f"[UltraSharpV2] 从 HF Hub 下载: {repo_id}/{filename}")
45
+ path = hf_hub_download(
46
+ repo_id=repo_id,
47
+ filename=filename,
48
+ token=token,
49
+ endpoint=endpoint,
50
+ )
51
+ print(f"[UltraSharpV2] 下载成功: {path}")
52
+ return path
53
+
54
+
55
+ def _resolve_model_path() -> str:
56
+ """按优先级解析模型路径: 本地文件 > HF Hub(默认 Kim2091/UltraSharpV2)。"""
57
+ # 1) 本地文件优先(存在则直接使用,跳过网络)
58
+ for name in MODEL_CANDIDATES:
59
+ if os.path.exists(name):
60
+ print(f"[UltraSharpV2] 使用本地模型: {name}")
61
+ return name
62
+
63
+ # 2) 从 HF Hub 下载
64
+ repo_id = os.environ.get("MODEL_REPO_ID", _DEFAULT_REPO_ID)
65
+ filename = os.environ.get("MODEL_FILENAME", _DEFAULT_FILENAME)
66
+ return _download_from_hub(repo_id, filename)
67
+
68
+
69
+ class UltraSharpV2:
70
+ def __init__(self, model_path=None, device=None):
71
+ """
72
+ Args:
73
+ model_path: path to model file (auto-resolve from HF Hub or local if None).
74
+ device: "cpu" (ZeroGPU default), "cuda", or None (auto-detect).
75
+ """
76
+ if model_path is None:
77
+ model_path = _resolve_model_path()
78
+
79
+ if device is not None:
80
+ self.device = device
81
+ self.dtype = torch.float16 if device == "cuda" else torch.float32
82
+ else:
83
+ self.device, self.dtype = detect_device()
84
+
85
+ self._model_path = model_path
86
+ self.model = self._load_model(model_path)
87
+ self.scale = self.model.scale
88
+ print(f"[UltraSharpV2] 设备: {self.device}, 精度: {self.dtype}")
89
+ print(f"[UltraSharpV2] 模型加载完毕, 放大倍率: {self.scale}x")
90
+
91
+ def _load_model(self, path):
92
+ from spandrel import ModelLoader
93
+
94
+ loader = ModelLoader()
95
+ model = loader.load_from_file(path)
96
+ model.model.to(self.device).to(self.dtype).eval()
97
+ return model
98
+
99
+ def to_cuda(self):
100
+ """Move model to CUDA (called inside @spaces.GPU decorated function)."""
101
+ if self.device == "cuda":
102
+ return
103
+ print("[UltraSharpV2] 正在将模型移至 GPU ...")
104
+ self.device = "cuda"
105
+ self.dtype = torch.float16
106
+ self.model.model.to(self.device).to(self.dtype)
107
+ torch.cuda.empty_cache()
108
+
109
+ def to_cpu(self):
110
+ """Move model back to CPU to release ZeroGPU memory."""
111
+ if self.device == "cpu":
112
+ return
113
+ print("[UltraSharpV2] 正在将模型移回 CPU ...")
114
+ self.model.model.to("cpu").to(torch.float32)
115
+ self.device = "cpu"
116
+ self.dtype = torch.float32
117
+ torch.cuda.empty_cache()
118
+
119
+ def upscale(
120
+ self,
121
+ image: Image.Image,
122
+ tile_size: int = 512,
123
+ tile_overlap: int = 32,
124
+ target_scale: float = 4.0,
125
+ ) -> tuple[Image.Image, float]:
126
+ start = time.time()
127
+
128
+ tensor = self._pil_to_tensor(image)
129
+ _, _, h, w = tensor.shape
130
+
131
+ if h <= tile_size and w <= tile_size:
132
+ with torch.no_grad():
133
+ output = self.model(tensor.to(self.dtype)).float()
134
+ else:
135
+ output = self._tiled_upscale(tensor, tile_size, tile_overlap)
136
+
137
+ result = self._tensor_to_pil(output)
138
+
139
+ if target_scale > 0 and abs(target_scale - self.scale) > 0.01:
140
+ dest_w = int(w * target_scale)
141
+ dest_h = int(h * target_scale)
142
+ result = result.resize((dest_w, dest_h), Image.LANCZOS)
143
+
144
+ elapsed = time.time() - start
145
+ print(
146
+ f"[UltraSharpV2] 推理完成, 尺寸: {h}x{w} -> {result.width}x{result.height}, 耗时: {elapsed:.2f}s"
147
+ )
148
+ return result, elapsed
149
+
150
+ def _pil_to_tensor(self, img: Image.Image) -> torch.Tensor:
151
+ if img.mode != "RGB":
152
+ img = img.convert("RGB")
153
+ arr = np.array(img).astype(np.float32) / 255.0
154
+ tensor = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0)
155
+ return tensor.to(self.device)
156
+
157
+ def _tensor_to_pil(self, tensor: torch.Tensor) -> Image.Image:
158
+ tensor = tensor.squeeze(0).float().clamp(0, 1)
159
+ arr = (tensor.permute(1, 2, 0).cpu().numpy() * 255).round().astype(np.uint8)
160
+ return Image.fromarray(arr)
161
+
162
+ def _tiled_upscale(
163
+ self, tensor: torch.Tensor, tile_size: int, tile_overlap: int
164
+ ) -> torch.Tensor:
165
+ _, c, h, w = tensor.shape
166
+ scale = self.scale
167
+ pad = min(tile_overlap, tile_size // 4)
168
+
169
+ padded = torch.nn.functional.pad(
170
+ tensor, (pad, pad, pad, pad), mode="reflect"
171
+ )
172
+ _, _, hp, wp = padded.shape
173
+
174
+ out_tile = tile_size * scale
175
+ out_hp = hp * scale
176
+ out_wp = wp * scale
177
+ stride = tile_size - pad * 2
178
+
179
+ output = torch.zeros(1, c, out_hp, out_wp, device=self.device, dtype=torch.float32)
180
+ weight = torch.zeros(1, 1, out_hp, out_wp, device=self.device, dtype=torch.float32)
181
+
182
+ wy = torch.ones(out_tile, device=self.device)
183
+ wx = torch.ones(out_tile, device=self.device)
184
+ if pad > 0:
185
+ ramp = torch.linspace(0, 1, pad * scale, device=self.device)
186
+ wy[: pad * scale] = ramp
187
+ wy[-pad * scale :] = ramp.flip(0)
188
+ wx[: pad * scale] = ramp
189
+ wx[-pad * scale :] = ramp.flip(0)
190
+ wmap = wy.view(1, 1, -1, 1) * wx.view(1, 1, 1, -1)
191
+
192
+ for y in range(0, hp, stride):
193
+ for x in range(0, wp, stride):
194
+ y1 = min(y + tile_size, hp)
195
+ x1 = min(x + tile_size, wp)
196
+ y0 = max(0, y1 - tile_size)
197
+ x0 = max(0, x1 - tile_size)
198
+
199
+ tile = padded[:, :, y0:y1, x0:x1]
200
+ with torch.no_grad():
201
+ out = self.model(tile.to(self.dtype)).float()
202
+
203
+ oh, ow = out.shape[2], out.shape[3]
204
+ oy0, ox0 = y0 * scale, x0 * scale
205
+ wc = wmap[:, :, :oh, :ow]
206
+
207
+ output[:, :, oy0 : oy0 + oh, ox0 : ox0 + ow] += out * wc
208
+ weight[:, :, oy0 : oy0 + oh, ox0 : ox0 + ow] += wc
209
+
210
+ output /= weight.clamp(min=1e-8)
211
+
212
+ crop_start = pad * scale
213
+ crop_end_h = crop_start + h * scale
214
+ crop_end_w = crop_start + w * scale
215
+ return output[:, :, crop_start:crop_end_h, crop_start:crop_end_w]
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch>=2.0
2
+ spandrel>=0.3
3
+ gradio>=4.0
4
+ Pillow
5
+ huggingface_hub
6
+ spaces>=0.3