aditya-rAj19 commited on
Commit
ddb0022
·
1 Parent(s): 27e2136

Super-resolve the head only, which makes real SR affordable on CPU

Browse files

RealESRGAN was GPU-only: on CPU a 4x pass over the full frame takes ~20 minutes
here, so the free tier fell back to a plain Lanczos resize and the "4K" download
was pure interpolation.

Now that everything outside the changed region is composited straight from the
full-resolution original, the background is already native and has nothing for a
super-resolution model to reconstruct. Only the head came through the swap's
128x128 bottleneck, so RealESRGAN now runs on that crop alone and Lanczos covers
the rest, feathered at the crop edge.

Measured on the Library target (CPU, 1086x1448 -> 4344x5792): head-only SR takes
370s against ~1200s for the full frame, and high-frequency detail in the face
region rises from 5.9 to 19.9. GPU behaviour is unchanged when no bbox is given.

Not yet safe to deploy synchronously — 370s exceeds the request timeout. The
async job endpoint lands next.

Files changed (2) hide show
  1. core/super_res.py +73 -23
  2. web_app.py +13 -2
core/super_res.py CHANGED
@@ -129,41 +129,91 @@ def restore_faces(image: np.ndarray) -> np.ndarray:
129
 
130
 
131
  def upscale_image(image: np.ndarray, scale: int = 4,
132
- realesrgan_weight: float = 0.5) -> np.ndarray:
 
133
  """
134
- Upscale the whole frame ~scale x for the high-resolution download.
135
 
136
  RealESRGAN adds resolution but, being a general-purpose model, it over-sharpens
137
  skin into a plastic/waxy texture on faces. So we BLEND it with a plain Lanczos
138
  upscale (realesrgan_weight controls the mix: 0 = pure natural Lanczos, 1 = full
139
  RealESRGAN) — keeping most of the detail while killing the artificial texture.
140
 
141
- RealESRGAN is only used on GPU — on CPU (e.g. the HuggingFace free tier) a 4x
142
- pass is too slow, so we fall back to pure Lanczos there.
 
 
 
 
 
 
143
  """
144
  h, w = image.shape[:2]
145
  lanczos = cv2.resize(image, (w * scale, h * scale),
146
  interpolation=cv2.INTER_LANCZOS4)
147
 
148
- if _device() == "cuda":
149
- upsampler = _load_realesrgan()
150
- if upsampler is not None:
151
- try:
152
- output, _ = upsampler.enhance(image, outscale=scale)
153
- if output is not None:
154
- if output.shape[:2] != lanczos.shape[:2]:
155
- output = cv2.resize(output, (lanczos.shape[1], lanczos.shape[0]),
156
- interpolation=cv2.INTER_LANCZOS4)
157
- blended = cv2.addWeighted(output, realesrgan_weight,
158
- lanczos, 1.0 - realesrgan_weight, 0)
159
- print(f"[super_res] RealESRGAN+Lanczos {scale}x -> "
160
- f"{blended.shape[1]}x{blended.shape[0]} (re={realesrgan_weight})")
161
- return blended
162
- except Exception as e:
163
- print(f"[super_res] RealESRGAN enhance failed: {e}")
164
-
165
- print(f"[super_res] Lanczos {scale}x -> {w*scale}x{h*scale}")
166
- return lanczos
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
 
169
  def enhance_resolution(image: np.ndarray, scale: int = 4) -> np.ndarray:
 
129
 
130
 
131
  def upscale_image(image: np.ndarray, scale: int = 4,
132
+ realesrgan_weight: float = 0.5,
133
+ focus_bbox=None) -> np.ndarray:
134
  """
135
+ Upscale ~scale x for the high-resolution download.
136
 
137
  RealESRGAN adds resolution but, being a general-purpose model, it over-sharpens
138
  skin into a plastic/waxy texture on faces. So we BLEND it with a plain Lanczos
139
  upscale (realesrgan_weight controls the mix: 0 = pure natural Lanczos, 1 = full
140
  RealESRGAN) — keeping most of the detail while killing the artificial texture.
141
 
142
+ focus_bbox restricts RealESRGAN to the head region. Everything outside it is
143
+ now composited from the untouched full-resolution target (see
144
+ utils.image_io.composite_onto_original), so only the head — which came
145
+ through the swap's 128x128 bottleneck — has detail worth reconstructing.
146
+ That matters most on CPU: measured on this pipeline a full 1.57MP frame
147
+ takes ~20 minutes, while the head crop alone is seconds, which is what makes
148
+ real super-resolution affordable on the free CPU tier instead of GPU-only.
149
+ Passing no bbox on CPU keeps the old Lanczos-only behaviour.
150
  """
151
  h, w = image.shape[:2]
152
  lanczos = cv2.resize(image, (w * scale, h * scale),
153
  interpolation=cv2.INTER_LANCZOS4)
154
 
155
+ on_gpu = _device() == "cuda"
156
+ if not on_gpu and focus_bbox is None:
157
+ print(f"[super_res] Lanczos {scale}x -> {w*scale}x{h*scale}")
158
+ return lanczos
159
+
160
+ upsampler = _load_realesrgan()
161
+ if upsampler is None:
162
+ print(f"[super_res] Lanczos {scale}x -> {w*scale}x{h*scale}")
163
+ return lanczos
164
+
165
+ try:
166
+ if focus_bbox is None:
167
+ output, _ = upsampler.enhance(image, outscale=scale)
168
+ if output is None:
169
+ return lanczos
170
+ if output.shape[:2] != lanczos.shape[:2]:
171
+ output = cv2.resize(output, (lanczos.shape[1], lanczos.shape[0]),
172
+ interpolation=cv2.INTER_LANCZOS4)
173
+ blended = cv2.addWeighted(output, realesrgan_weight,
174
+ lanczos, 1.0 - realesrgan_weight, 0)
175
+ print(f"[super_res] RealESRGAN+Lanczos {scale}x -> "
176
+ f"{blended.shape[1]}x{blended.shape[0]} (re={realesrgan_weight})")
177
+ return blended
178
+
179
+ # -- head-only super-resolution ------------------------------------
180
+ x1, y1, x2, y2 = [int(v) for v in focus_bbox]
181
+ bw, bh = x2 - x1, y2 - y1
182
+ # Generous margin so hair and jaw are reconstructed with the face, and
183
+ # the feathered seam lands on background rather than on skin.
184
+ cx1 = max(0, x1 - int(bw * 1.0)); cy1 = max(0, y1 - int(bh * 1.2))
185
+ cx2 = min(w, x2 + int(bw * 1.0)); cy2 = min(h, y2 + int(bh * 1.6))
186
+ crop = image[cy1:cy2, cx1:cx2]
187
+ if crop.size == 0:
188
+ return lanczos
189
+
190
+ out_crop, _ = upsampler.enhance(crop, outscale=scale)
191
+ if out_crop is None:
192
+ return lanczos
193
+ tw, th = (cx2 - cx1) * scale, (cy2 - cy1) * scale
194
+ if (out_crop.shape[1], out_crop.shape[0]) != (tw, th):
195
+ out_crop = cv2.resize(out_crop, (tw, th), interpolation=cv2.INTER_LANCZOS4)
196
+
197
+ region = lanczos[cy1 * scale:cy2 * scale, cx1 * scale:cx2 * scale]
198
+ mixed = cv2.addWeighted(out_crop, realesrgan_weight,
199
+ region, 1.0 - realesrgan_weight, 0)
200
+
201
+ # Feather the crop edge so the enhanced head doesn't end on a hard line.
202
+ m = np.zeros((th, tw), np.float32)
203
+ pad = max(2, int(min(th, tw) * 0.06))
204
+ m[pad:th - pad, pad:tw - pad] = 1.0
205
+ m = cv2.GaussianBlur(m, (0, 0), pad / 2.0)[..., None]
206
+
207
+ out = lanczos.copy()
208
+ out[cy1 * scale:cy2 * scale, cx1 * scale:cx2 * scale] = (
209
+ mixed.astype(np.float32) * m + region.astype(np.float32) * (1.0 - m)
210
+ ).astype(np.uint8)
211
+ print(f"[super_res] RealESRGAN on head {crop.shape[1]}x{crop.shape[0]} "
212
+ f"+ Lanczos {scale}x -> {out.shape[1]}x{out.shape[0]}")
213
+ return out
214
+ except Exception as e:
215
+ print(f"[super_res] RealESRGAN enhance failed: {e}")
216
+ return lanczos
217
 
218
 
219
  def enhance_resolution(image: np.ndarray, scale: int = 4) -> np.ndarray:
web_app.py CHANGED
@@ -864,13 +864,24 @@ def api_swap():
864
  # 9. Paste the changed region back onto the FULL-RESOLUTION target so the
865
  # background and body keep the original photo's detail rather than the
866
  # downscaled-then-interpolated version. No-op if nothing was resized.
 
867
  try:
868
  swapped = composite_onto_original(swapped, target, target_orig)
869
  except Exception as e:
870
  print(f"[swap] original-resolution composite skipped: {e}")
871
 
872
- # -- 4K upscale for download (RealESRGAN x4, Lanczos fallback) --------
873
- hi_res = upscale_image(swapped, scale=4)
 
 
 
 
 
 
 
 
 
 
874
 
875
  # The 4K result is returned inline as a base64 data-URI so the user can
876
  # download it client-side (works on the HF Space too). We do NOT store it
 
864
  # 9. Paste the changed region back onto the FULL-RESOLUTION target so the
865
  # background and body keep the original photo's detail rather than the
866
  # downscaled-then-interpolated version. No-op if nothing was resized.
867
+ _work_h, _work_w = swapped.shape[:2]
868
  try:
869
  swapped = composite_onto_original(swapped, target, target_orig)
870
  except Exception as e:
871
  print(f"[swap] original-resolution composite skipped: {e}")
872
 
873
+ # -- 4K upscale for download (RealESRGAN on the head, Lanczos elsewhere)
874
+ # The head is the only region that came through the swap's 128px
875
+ # bottleneck; the rest is already native resolution from step 9, so
876
+ # there's nothing there for a super-resolution model to reconstruct.
877
+ _sr_bbox = None
878
+ try:
879
+ _fs = swapped.shape[1] / float(_work_w) # working -> composited scale
880
+ x1, y1, x2, y2 = faces_tgt[0]
881
+ _sr_bbox = (x1 * _fs, y1 * _fs, x2 * _fs, y2 * _fs)
882
+ except Exception:
883
+ pass
884
+ hi_res = upscale_image(swapped, scale=4, focus_bbox=_sr_bbox)
885
 
886
  # The 4K result is returned inline as a base64 data-URI so the user can
887
  # download it client-side (works on the HF Space too). We do NOT store it