ParamDev commited on
Commit
3e1fb90
·
verified ·
1 Parent(s): 8ab66f8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -136
app.py CHANGED
@@ -22,31 +22,32 @@ DRIVE_IDS = {
22
  "hresnet_x4": "15xmXXZNH2wMyeQv4ie5hagT7eWK9MgP6", # placeholder = ESRGAN x2
23
  }
24
 
25
- # SRCNN .pth lives in the same HF Space repo (uploaded by user)
26
  SRCNN_PTH = os.path.join(os.path.dirname(__file__), "srcnn_x4.pth")
27
 
28
  MODEL_LABELS = {
29
  "esrgan_x4": "Real-ESRGAN ×4",
30
  "srcnn_x4": "SRCNN ×4",
31
- "hresnet_x4": "HResNet ×4", # placeholder until real weights available
32
  }
33
 
34
  MODEL_SCALES = {
35
  "esrgan_x4": 4,
36
  "srcnn_x4": 4,
37
- "hresnet_x4": 2, # underlying model is ESRGAN x2
38
  }
39
 
40
 
41
  # ===========================================================================
42
- # SRCNN architecture (3-layer, matches the .pth weights)
 
43
  # ===========================================================================
44
  class SRCNN(nn.Module):
45
- def __init__(self, num_channels: int = 3):
46
  super().__init__()
47
- self.conv1 = nn.Conv2d(num_channels, 64, kernel_size=9, padding=9 // 2)
48
- self.conv2 = nn.Conv2d(64, 32, kernel_size=5, padding=5 // 2)
49
- self.conv3 = nn.Conv2d(32, num_channels, kernel_size=5, padding=5 // 2)
50
  self.relu = nn.ReLU(inplace=True)
51
 
52
  def forward(self, x: torch.Tensor) -> torch.Tensor:
@@ -66,53 +67,56 @@ SRCNN_MODEL = None
66
 
67
 
68
  def _load_esrgan_onnx(key: str):
69
- """Download ESRGAN ONNX from Drive (gdown handles confirmation pages)."""
70
- filename = f"{key}.onnx"
71
- dest = os.path.join(CACHE_DIR, filename)
72
  if not os.path.exists(dest):
73
  print(f"Downloading {MODEL_LABELS[key]} from Drive …")
74
  gdown.download(id=DRIVE_IDS[key], output=dest, quiet=False, fuzzy=True)
75
  if os.path.exists(dest):
76
  sess = ort.InferenceSession(dest, sess_options=sess_opts,
77
  providers=["CPUExecutionProvider"])
78
- meta = sess.get_inputs()[0]
79
- ONNX_SESSIONS[key] = (sess, meta)
80
  print(f"Loaded {MODEL_LABELS[key]} ✓")
81
  else:
82
- print(f"[ERROR] Could not load {key} — file missing after download.")
83
 
84
 
85
  def _load_srcnn_pth():
86
- """Load SRCNN directly from the .pth file in the Space repo."""
 
 
 
 
 
 
87
  global SRCNN_MODEL
88
  if not os.path.exists(SRCNN_PTH):
89
- print(f"[WARN] srcnn_x4.pth not found at {SRCNN_PTH} — SRCNN will be skipped.")
90
  return
91
- model = SRCNN(num_channels=3)
92
  state = torch.load(SRCNN_PTH, map_location="cpu")
93
  # Unwrap common checkpoint wrappers
94
- for key in ("model", "state_dict", "params"):
95
- if isinstance(state, dict) and key in state:
96
- state = state[key]
97
  state = {k.replace("module.", ""): v for k, v in state.items()}
98
- try:
99
- model.load_state_dict(state, strict=True)
100
- except RuntimeError:
101
- model.load_state_dict(state, strict=False)
102
- print("[WARN] SRCNN loaded with strict=False (minor key mismatch).")
103
  model.eval()
104
  SRCNN_MODEL = model
105
- print("Loaded SRCNN ×4 from .pth ✓")
106
 
107
 
108
  # Boot-time loading
109
- for k in ("esrgan_x4", "hresnet_x4"):
110
  try:
111
- _load_esrgan_onnx(k)
112
- except Exception as e:
113
- print(f"[ERROR] {k}: {e}")
114
 
115
- _load_srcnn_pth()
 
 
 
116
 
117
 
118
  # ===========================================================================
@@ -120,41 +124,58 @@ _load_srcnn_pth()
120
  # ===========================================================================
121
 
122
  def _onnx_tile(sess, meta, tile: np.ndarray) -> np.ndarray:
123
- """HWC float32 [0,1] → HWC float32 [0,1]."""
124
  patch = tile.transpose(2, 0, 1)[None, ...]
125
  out = sess.run(None, {meta.name: patch})[0]
126
  return out.squeeze(0).transpose(1, 2, 0)
127
 
128
 
129
  def _srcnn_tile(tile: np.ndarray, scale: int = 4) -> np.ndarray:
130
- """Bicubic upsample → SRCNN refine. HWC float32 [0,1] → HWC float32."""
131
- t = torch.from_numpy(tile.transpose(2, 0, 1)).unsqueeze(0) # NCHW
132
- h, w = t.shape[2], t.shape[3]
133
- up = F.interpolate(t, size=(h * scale, w * scale),
134
- mode="bicubic", align_corners=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  with torch.no_grad():
136
- out = SRCNN_MODEL(up)
137
- return out.squeeze(0).permute(1, 2, 0).numpy()
 
 
 
 
 
 
138
 
139
 
140
  def upscale(input_img: Image.Image, model_key: str, max_dim: int = 1024) -> Image.Image:
141
- """
142
- Tile-based upscale dispatcher.
143
- Works for both ONNX sessions (ESRGAN) and the torch SRCNN model.
144
- """
145
- # Guard
146
  if model_key == "srcnn_x4" and SRCNN_MODEL is None:
147
- raise RuntimeError("SRCNN model not loaded.")
148
  if model_key in ("esrgan_x4", "hresnet_x4") and model_key not in ONNX_SESSIONS:
149
- raise RuntimeError(f"{MODEL_LABELS[model_key]} not loaded.")
150
 
151
  scale = MODEL_SCALES[model_key]
 
152
 
153
- # Tile size: ESRGAN uses fixed 128×128 LR tiles;
154
- # SRCNN works on any size (we tile at 128 for consistency).
155
- TILE = 128
156
-
157
- # Cap input size
158
  w, h = input_img.size
159
  if w > max_dim or h > max_dim:
160
  factor = max_dim / float(max(w, h))
@@ -174,8 +195,8 @@ def upscale(input_img: Image.Image, model_key: str, max_dim: int = 1024) -> Imag
174
 
175
  for i in range(tiles_h):
176
  for j in range(tiles_w):
177
- y0, x0 = i * TILE, j * TILE
178
- tile = arr_pad[y0:y0 + TILE, x0:x0 + TILE]
179
 
180
  if model_key == "srcnn_x4":
181
  up_tile = _srcnn_tile(tile, scale=scale)
@@ -190,25 +211,6 @@ def upscale(input_img: Image.Image, model_key: str, max_dim: int = 1024) -> Imag
190
  return Image.fromarray((final * 255.0).round().astype(np.uint8))
191
 
192
 
193
- def make_comparison_png(original: Image.Image, upscaled: Image.Image) -> str:
194
- """
195
- Save a side-by-side PNG (original | upscaled, same display height)
196
- that the ImageSlider widget will use.
197
- """
198
- up_w, up_h = upscaled.size
199
- orig_resized = original.resize((up_w, up_h), Image.LANCZOS)
200
-
201
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
202
- orig_resized.save(tmp.name) # left image for slider
203
- tmp.close()
204
-
205
- tmp2 = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
206
- upscaled.save(tmp2.name) # right image for slider
207
- tmp2.close()
208
-
209
- return tmp.name, tmp2.name
210
-
211
-
212
  # ===========================================================================
213
  # Gradio callback
214
  # ===========================================================================
@@ -217,18 +219,28 @@ def run_upscale(input_img: Image.Image, model_name: str):
217
  if input_img is None:
218
  return None, None, None
219
 
220
- # Map display label key
221
- key = next(k for k, v in MODEL_LABELS.items() if v == model_name)
 
 
 
 
222
 
223
- result = upscale(input_img, key)
224
- orig_path, up_path = make_comparison_png(input_img, result)
 
 
225
 
226
- # Also save download copy
227
- dl_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
228
- result.save(dl_tmp.name, format="PNG")
229
- dl_tmp.close()
230
 
231
- return (orig_path, up_path), result, dl_tmp.name
 
 
 
 
 
232
 
233
 
234
  # ===========================================================================
@@ -237,59 +249,28 @@ def run_upscale(input_img: Image.Image, model_name: str):
237
 
238
  css = """
239
  @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700&display=swap');
240
-
241
  body, .gradio-container { font-family: 'DM Sans', sans-serif !important; }
242
-
243
- #title {
244
- text-align: center;
245
- padding: 24px 0 8px;
246
- }
247
- #title h1 {
248
- font-size: 2rem;
249
- font-weight: 700;
250
- letter-spacing: -0.5px;
251
- margin: 0;
252
- }
253
  #title p { color: #666; margin: 4px 0 0; }
254
-
255
  #run-btn {
256
  background: linear-gradient(135deg, #0f0c29, #302b63, #24243e) !important;
257
- color: #fff !important;
258
- font-weight: 700 !important;
259
- font-size: 1rem !important;
260
- border-radius: 10px !important;
261
- padding: 14px 0 !important;
262
- width: 100%;
263
- letter-spacing: 0.03em;
264
- transition: opacity 0.2s;
265
  }
266
  #run-btn:hover { opacity: 0.85; }
267
-
268
  #dl-btn button {
269
- background: #f4f4f4 !important;
270
- border: 1px solid #ddd !important;
271
- color: #333 !important;
272
- border-radius: 8px !important;
273
- width: 100%;
274
- font-size: 0.85rem !important;
275
  }
276
-
277
  .section-label {
278
- font-size: 0.75rem;
279
- font-weight: 700;
280
- letter-spacing: 0.1em;
281
- text-transform: uppercase;
282
- color: #999;
283
- margin-bottom: 6px;
284
  }
285
  """
286
 
287
- available_models = [v for k, v in MODEL_LABELS.items()
288
- if k == "srcnn_x4" and SRCNN_MODEL is not None
289
- or k in ONNX_SESSIONS]
290
-
291
- # Always show all three in dropdown regardless of load state
292
- # (shows error in output if model failed to load)
293
  dropdown_choices = list(MODEL_LABELS.values())
294
 
295
  with gr.Blocks(css=css, title="SpectraGAN Upscaler") as demo:
@@ -303,7 +284,6 @@ with gr.Blocks(css=css, title="SpectraGAN Upscaler") as demo:
303
 
304
  with gr.Row(equal_height=True):
305
 
306
- # ── Left panel: controls ───────────────────────────────────────────
307
  with gr.Column(scale=1, min_width=260):
308
  gr.HTML('<div class="section-label">Source Image</div>')
309
  inp_image = gr.Image(type="pil", show_label=False, height=260)
@@ -314,32 +294,22 @@ with gr.Blocks(css=css, title="SpectraGAN Upscaler") as demo:
314
  value=dropdown_choices[0],
315
  show_label=False,
316
  )
317
-
318
  run_btn = gr.Button("⚡ Upscale", elem_id="run-btn")
319
-
320
- dl_btn = gr.DownloadButton(
321
  label="⬇ Download upscaled PNG",
322
  elem_id="dl-btn",
323
  visible=True,
324
  )
325
 
326
- # ── Right panel: results ───────────────────────────────────────────
327
  with gr.Column(scale=2):
328
-
329
- gr.HTML('<div class="section-label">Before / After</div>')
330
  slider = gr.ImageSlider(
331
- label="Drag to compare",
332
  show_label=False,
333
  height=420,
334
  type="filepath",
335
  )
336
-
337
  gr.HTML('<div class="section-label" style="margin-top:16px">Upscaled Preview</div>')
338
- out_preview = gr.Image(
339
- type="pil",
340
- show_label=False,
341
- height=200,
342
- )
343
 
344
  run_btn.click(
345
  fn=run_upscale,
 
22
  "hresnet_x4": "15xmXXZNH2wMyeQv4ie5hagT7eWK9MgP6", # placeholder = ESRGAN x2
23
  }
24
 
25
+ # srcnn_x4.pth must be in the Space repo root (same folder as app.py)
26
  SRCNN_PTH = os.path.join(os.path.dirname(__file__), "srcnn_x4.pth")
27
 
28
  MODEL_LABELS = {
29
  "esrgan_x4": "Real-ESRGAN ×4",
30
  "srcnn_x4": "SRCNN ×4",
31
+ "hresnet_x4": "HResNet ×4",
32
  }
33
 
34
  MODEL_SCALES = {
35
  "esrgan_x4": 4,
36
  "srcnn_x4": 4,
37
+ "hresnet_x4": 2, # underlying model is ESRGAN x2 (placeholder)
38
  }
39
 
40
 
41
  # ===========================================================================
42
+ # SRCNN architecture3 conv layers, 1-channel (Y / grayscale) input
43
+ # Your .pth was trained on grayscale, so num_channels=1 here.
44
  # ===========================================================================
45
  class SRCNN(nn.Module):
46
+ def __init__(self, num_channels: int = 1):
47
  super().__init__()
48
+ self.conv1 = nn.Conv2d(num_channels, 64, kernel_size=9, padding=4)
49
+ self.conv2 = nn.Conv2d(64, 32, kernel_size=5, padding=2)
50
+ self.conv3 = nn.Conv2d(32, num_channels, kernel_size=5, padding=2)
51
  self.relu = nn.ReLU(inplace=True)
52
 
53
  def forward(self, x: torch.Tensor) -> torch.Tensor:
 
67
 
68
 
69
  def _load_esrgan_onnx(key: str):
70
+ """Download ESRGAN ONNX from Drive via gdown (handles confirmation pages)."""
71
+ dest = os.path.join(CACHE_DIR, f"{key}.onnx")
 
72
  if not os.path.exists(dest):
73
  print(f"Downloading {MODEL_LABELS[key]} from Drive …")
74
  gdown.download(id=DRIVE_IDS[key], output=dest, quiet=False, fuzzy=True)
75
  if os.path.exists(dest):
76
  sess = ort.InferenceSession(dest, sess_options=sess_opts,
77
  providers=["CPUExecutionProvider"])
78
+ ONNX_SESSIONS[key] = (sess, sess.get_inputs()[0])
 
79
  print(f"Loaded {MODEL_LABELS[key]} ✓")
80
  else:
81
+ print(f"[ERROR] {key} — file missing after download attempt.")
82
 
83
 
84
  def _load_srcnn_pth():
85
+ """
86
+ Load SRCNN from .pth in the Space repo root.
87
+ The weights use 1-channel (grayscale / Y) input — confirmed by the
88
+ conv1.weight shape torch.Size([64, 1, 9, 9]) in the checkpoint.
89
+ Inference will convert RGB → YCbCr, enhance Y with SRCNN,
90
+ bicubic-upsample CbCr, then recompose back to RGB.
91
+ """
92
  global SRCNN_MODEL
93
  if not os.path.exists(SRCNN_PTH):
94
+ print(f"[WARN] srcnn_x4.pth not found at {SRCNN_PTH} — SRCNN skipped.")
95
  return
96
+ model = SRCNN(num_channels=1)
97
  state = torch.load(SRCNN_PTH, map_location="cpu")
98
  # Unwrap common checkpoint wrappers
99
+ for wrap_key in ("model", "state_dict", "params"):
100
+ if isinstance(state, dict) and wrap_key in state:
101
+ state = state[wrap_key]
102
  state = {k.replace("module.", ""): v for k, v in state.items()}
103
+ model.load_state_dict(state, strict=True)
 
 
 
 
104
  model.eval()
105
  SRCNN_MODEL = model
106
+ print("Loaded SRCNN ×4 from .pth ✓ (grayscale/Y-channel model)")
107
 
108
 
109
  # Boot-time loading
110
+ for _k in ("esrgan_x4", "hresnet_x4"):
111
  try:
112
+ _load_esrgan_onnx(_k)
113
+ except Exception as _e:
114
+ print(f"[ERROR] {_k}: {_e}")
115
 
116
+ try:
117
+ _load_srcnn_pth()
118
+ except Exception as _e:
119
+ print(f"[ERROR] SRCNN: {_e}")
120
 
121
 
122
  # ===========================================================================
 
124
  # ===========================================================================
125
 
126
  def _onnx_tile(sess, meta, tile: np.ndarray) -> np.ndarray:
127
+ """HWC float32 [0,1] in → HWC float32 out."""
128
  patch = tile.transpose(2, 0, 1)[None, ...]
129
  out = sess.run(None, {meta.name: patch})[0]
130
  return out.squeeze(0).transpose(1, 2, 0)
131
 
132
 
133
  def _srcnn_tile(tile: np.ndarray, scale: int = 4) -> np.ndarray:
134
+ """
135
+ Enhance a single RGB tile using the grayscale SRCNN model.
136
+ Strategy: split into YCbCr → SRCNN on Y → bicubic CbCr → recompose RGB.
137
+ tile: HWC float32 [0, 1]
138
+ returns: HWC float32 [0, 1] at scale× resolution
139
+ """
140
+ tile_uint8 = (np.clip(tile, 0, 1) * 255).round().astype(np.uint8)
141
+ tile_pil = Image.fromarray(tile_uint8)
142
+ tile_ycbcr = tile_pil.convert("YCbCr")
143
+ y_pil, cb_pil, cr_pil = tile_ycbcr.split()
144
+
145
+ orig_w, orig_h = tile_pil.size
146
+ up_w, up_h = orig_w * scale, orig_h * scale
147
+
148
+ # Upsample CbCr channels with bicubic (no SRCNN needed there)
149
+ cb_up = cb_pil.resize((up_w, up_h), Image.BICUBIC)
150
+ cr_up = cr_pil.resize((up_w, up_h), Image.BICUBIC)
151
+
152
+ # Bicubic upsample Y, then refine with SRCNN
153
+ y_arr = np.array(y_pil).astype(np.float32) / 255.0 # (H, W)
154
+ y_t = torch.from_numpy(y_arr).unsqueeze(0).unsqueeze(0) # (1, 1, H, W)
155
+ y_up = F.interpolate(y_t, size=(up_h, up_w), mode="bicubic", align_corners=False)
156
+
157
  with torch.no_grad():
158
+ y_refined = SRCNN_MODEL(y_up) # (1, 1, H*s, W*s)
159
+
160
+ y_out = (y_refined.squeeze().numpy() * 255.0).clip(0, 255).round().astype(np.uint8)
161
+ y_up_pil = Image.fromarray(y_out, mode="L")
162
+
163
+ # Recompose YCbCr → RGB
164
+ out_rgb = Image.merge("YCbCr", [y_up_pil, cb_up, cr_up]).convert("RGB")
165
+ return np.array(out_rgb).astype(np.float32) / 255.0
166
 
167
 
168
  def upscale(input_img: Image.Image, model_key: str, max_dim: int = 1024) -> Image.Image:
169
+ """Tile-based upscale dispatcher for ONNX (ESRGAN) and torch (SRCNN)."""
 
 
 
 
170
  if model_key == "srcnn_x4" and SRCNN_MODEL is None:
171
+ raise RuntimeError("SRCNN model not loaded — check that srcnn_x4.pth is in the repo root.")
172
  if model_key in ("esrgan_x4", "hresnet_x4") and model_key not in ONNX_SESSIONS:
173
+ raise RuntimeError(f"{MODEL_LABELS[model_key]} failed to load at startup.")
174
 
175
  scale = MODEL_SCALES[model_key]
176
+ TILE = 128 # LR tile size (consistent across all models)
177
 
178
+ # Cap input size to avoid OOM
 
 
 
 
179
  w, h = input_img.size
180
  if w > max_dim or h > max_dim:
181
  factor = max_dim / float(max(w, h))
 
195
 
196
  for i in range(tiles_h):
197
  for j in range(tiles_w):
198
+ y0, x0 = i * TILE, j * TILE
199
+ tile = arr_pad[y0:y0 + TILE, x0:x0 + TILE]
200
 
201
  if model_key == "srcnn_x4":
202
  up_tile = _srcnn_tile(tile, scale=scale)
 
211
  return Image.fromarray((final * 255.0).round().astype(np.uint8))
212
 
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  # ===========================================================================
215
  # Gradio callback
216
  # ===========================================================================
 
219
  if input_img is None:
220
  return None, None, None
221
 
222
+ key = next(k for k, v in MODEL_LABELS.items() if v == model_name)
223
+ result = upscale(input_img, key)
224
+
225
+ # Resize original to same dimensions as output for the slider
226
+ up_w, up_h = result.size
227
+ orig_resized = input_img.resize((up_w, up_h), Image.LANCZOS).convert("RGB")
228
 
229
+ # Save both as temp files for ImageSlider
230
+ tmp_orig = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
231
+ orig_resized.save(tmp_orig.name)
232
+ tmp_orig.close()
233
 
234
+ tmp_up = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
235
+ result.save(tmp_up.name)
236
+ tmp_up.close()
 
237
 
238
+ # Separate download copy
239
+ tmp_dl = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
240
+ result.save(tmp_dl.name, format="PNG")
241
+ tmp_dl.close()
242
+
243
+ return (tmp_orig.name, tmp_up.name), result, tmp_dl.name
244
 
245
 
246
  # ===========================================================================
 
249
 
250
  css = """
251
  @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700&display=swap');
 
252
  body, .gradio-container { font-family: 'DM Sans', sans-serif !important; }
253
+ #title { text-align: center; padding: 24px 0 8px; }
254
+ #title h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.5px; margin: 0; }
 
 
 
 
 
 
 
 
 
255
  #title p { color: #666; margin: 4px 0 0; }
 
256
  #run-btn {
257
  background: linear-gradient(135deg, #0f0c29, #302b63, #24243e) !important;
258
+ color: #fff !important; font-weight: 700 !important;
259
+ font-size: 1rem !important; border-radius: 10px !important;
260
+ padding: 14px 0 !important; width: 100%; letter-spacing: 0.03em;
 
 
 
 
 
261
  }
262
  #run-btn:hover { opacity: 0.85; }
 
263
  #dl-btn button {
264
+ background: #f4f4f4 !important; border: 1px solid #ddd !important;
265
+ color: #333 !important; border-radius: 8px !important;
266
+ width: 100%; font-size: 0.85rem !important;
 
 
 
267
  }
 
268
  .section-label {
269
+ font-size: 0.75rem; font-weight: 700; letter-spacing: 0.1em;
270
+ text-transform: uppercase; color: #999; margin-bottom: 6px;
 
 
 
 
271
  }
272
  """
273
 
 
 
 
 
 
 
274
  dropdown_choices = list(MODEL_LABELS.values())
275
 
276
  with gr.Blocks(css=css, title="SpectraGAN Upscaler") as demo:
 
284
 
285
  with gr.Row(equal_height=True):
286
 
 
287
  with gr.Column(scale=1, min_width=260):
288
  gr.HTML('<div class="section-label">Source Image</div>')
289
  inp_image = gr.Image(type="pil", show_label=False, height=260)
 
294
  value=dropdown_choices[0],
295
  show_label=False,
296
  )
 
297
  run_btn = gr.Button("⚡ Upscale", elem_id="run-btn")
298
+ dl_btn = gr.DownloadButton(
 
299
  label="⬇ Download upscaled PNG",
300
  elem_id="dl-btn",
301
  visible=True,
302
  )
303
 
 
304
  with gr.Column(scale=2):
305
+ gr.HTML('<div class="section-label">Before / After — drag to compare</div>')
 
306
  slider = gr.ImageSlider(
 
307
  show_label=False,
308
  height=420,
309
  type="filepath",
310
  )
 
311
  gr.HTML('<div class="section-label" style="margin-top:16px">Upscaled Preview</div>')
312
+ out_preview = gr.Image(type="pil", show_label=False, height=200)
 
 
 
 
313
 
314
  run_btn.click(
315
  fn=run_upscale,