Update app.py
Browse files
app.py
CHANGED
|
@@ -11,6 +11,8 @@ import time
|
|
| 11 |
import types
|
| 12 |
import cv2
|
| 13 |
import urllib.request
|
|
|
|
|
|
|
| 14 |
import warnings
|
| 15 |
import torch
|
| 16 |
import numpy as np
|
|
@@ -95,28 +97,51 @@ print("Setting up GFPGAN & Real-ESRGAN...")
|
|
| 95 |
weights_dir = "weights"
|
| 96 |
os.makedirs(weights_dir, exist_ok=True)
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
# 1. Real-ESRGAN Weights Setup
|
| 99 |
esrgan_path = f"{weights_dir}/RealESRGAN_x4plus.pth"
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
| 103 |
|
| 104 |
esrgan_model = RRDBNet(
|
| 105 |
num_in_ch=3, num_out_ch=3, num_feat=64,
|
| 106 |
num_block=23, num_grow_ch=32, scale=4
|
| 107 |
)
|
| 108 |
|
|
|
|
| 109 |
enhancer = RealESRGANer(
|
| 110 |
scale=4, model_path=esrgan_path, model=esrgan_model,
|
| 111 |
-
tile=512, tile_pad=10, pre_pad=0,
|
| 112 |
-
|
|
|
|
| 113 |
)
|
| 114 |
|
| 115 |
# 2. GFPGAN Weights Setup
|
| 116 |
gfpgan_path = f"{weights_dir}/GFPGANv1.3.pth"
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
| 120 |
|
| 121 |
gfpgan_restorer = GFPGANer(
|
| 122 |
model_path=gfpgan_path,
|
|
@@ -124,7 +149,7 @@ gfpgan_restorer = GFPGANer(
|
|
| 124 |
arch="clean",
|
| 125 |
channel_multiplier=2,
|
| 126 |
bg_upsampler=enhancer,
|
| 127 |
-
device=
|
| 128 |
)
|
| 129 |
|
| 130 |
print("GFPGAN & Real-ESRGAN ready.")
|
|
@@ -158,10 +183,21 @@ print("All models loaded successfully.")
|
|
| 158 |
# 6. HELPER FUNCTIONS
|
| 159 |
# ================================================================
|
| 160 |
def is_greyscale_or_sepia(image):
|
| 161 |
-
rgb = np.
|
| 162 |
hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
|
| 167 |
def calculate_padding(ow, oh, direction, pct):
|
|
@@ -174,7 +210,9 @@ def calculate_padding(ow, oh, direction, pct):
|
|
| 174 |
elif direction == "Bottom": bottom = v_ext
|
| 175 |
elif direction == "Horizontal": left = right = h_ext
|
| 176 |
elif direction == "Vertical": top = bottom = v_ext
|
| 177 |
-
elif direction in ["Both", "All Sides"]:
|
|
|
|
|
|
|
| 178 |
return left, top, right, bottom
|
| 179 |
|
| 180 |
|
|
@@ -340,6 +378,9 @@ def fast_single_pass_outpaint(
|
|
| 340 |
raise gr.Error("Please upload an image first.")
|
| 341 |
|
| 342 |
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
image = cap_image(image, 1024)
|
| 345 |
ow, oh = image.size
|
|
@@ -370,6 +411,20 @@ def fast_single_pass_outpaint(
|
|
| 370 |
target_w = int(16 * math.ceil(target_w / 16))
|
| 371 |
target_h = int(16 * math.ceil(target_h / 16))
|
| 372 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
canvas = Image.new("RGB", (target_w, target_h), (127, 127, 127))
|
| 374 |
canvas.paste(image, (left, top))
|
| 375 |
|
|
@@ -429,15 +484,19 @@ def fast_single_pass_outpaint(
|
|
| 429 |
bs = brisque(t, data_range=1.0).item()
|
| 430 |
final_info += f"\n📉 BRISQUE: {bs:.2f} (lower better)"
|
| 431 |
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
except Exception as e:
|
| 442 |
final_info += f"\n⚠️ Metrics error: {e}"
|
| 443 |
|
|
|
|
| 11 |
import types
|
| 12 |
import cv2
|
| 13 |
import urllib.request
|
| 14 |
+
import shutil
|
| 15 |
+
import tempfile
|
| 16 |
import warnings
|
| 17 |
import torch
|
| 18 |
import numpy as np
|
|
|
|
| 97 |
weights_dir = "weights"
|
| 98 |
os.makedirs(weights_dir, exist_ok=True)
|
| 99 |
|
| 100 |
+
# Download into a temporary file and atomically publish only a complete weight file.
|
| 101 |
+
def ensure_weight_file(path, url, min_bytes=1_000_000):
|
| 102 |
+
if os.path.isfile(path) and os.path.getsize(path) >= min_bytes:
|
| 103 |
+
return
|
| 104 |
+
|
| 105 |
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
| 106 |
+
fd, temp_path = tempfile.mkstemp(prefix=".download-", dir=os.path.dirname(path) or ".")
|
| 107 |
+
os.close(fd)
|
| 108 |
+
try:
|
| 109 |
+
with urllib.request.urlopen(url, timeout=60) as response, open(temp_path, "wb") as out:
|
| 110 |
+
shutil.copyfileobj(response, out)
|
| 111 |
+
if os.path.getsize(temp_path) < min_bytes:
|
| 112 |
+
raise RuntimeError(f"Downloaded weight file is unexpectedly small: {path}")
|
| 113 |
+
os.replace(temp_path, path)
|
| 114 |
+
finally:
|
| 115 |
+
if os.path.exists(temp_path):
|
| 116 |
+
os.remove(temp_path)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
# 1. Real-ESRGAN Weights Setup
|
| 120 |
esrgan_path = f"{weights_dir}/RealESRGAN_x4plus.pth"
|
| 121 |
+
ensure_weight_file(
|
| 122 |
+
esrgan_path,
|
| 123 |
+
"https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth",
|
| 124 |
+
)
|
| 125 |
|
| 126 |
esrgan_model = RRDBNet(
|
| 127 |
num_in_ch=3, num_out_ch=3, num_feat=64,
|
| 128 |
num_block=23, num_grow_ch=32, scale=4
|
| 129 |
)
|
| 130 |
|
| 131 |
+
restoration_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 132 |
enhancer = RealESRGANer(
|
| 133 |
scale=4, model_path=esrgan_path, model=esrgan_model,
|
| 134 |
+
tile=512, tile_pad=10, pre_pad=0,
|
| 135 |
+
half=(restoration_device.type == "cuda"),
|
| 136 |
+
device=restoration_device,
|
| 137 |
)
|
| 138 |
|
| 139 |
# 2. GFPGAN Weights Setup
|
| 140 |
gfpgan_path = f"{weights_dir}/GFPGANv1.3.pth"
|
| 141 |
+
ensure_weight_file(
|
| 142 |
+
gfpgan_path,
|
| 143 |
+
"https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth",
|
| 144 |
+
)
|
| 145 |
|
| 146 |
gfpgan_restorer = GFPGANer(
|
| 147 |
model_path=gfpgan_path,
|
|
|
|
| 149 |
arch="clean",
|
| 150 |
channel_multiplier=2,
|
| 151 |
bg_upsampler=enhancer,
|
| 152 |
+
device=restoration_device
|
| 153 |
)
|
| 154 |
|
| 155 |
print("GFPGAN & Real-ESRGAN ready.")
|
|
|
|
| 183 |
# 6. HELPER FUNCTIONS
|
| 184 |
# ================================================================
|
| 185 |
def is_greyscale_or_sepia(image):
|
| 186 |
+
rgb = np.asarray(image.convert("RGB"), dtype=np.uint8)
|
| 187 |
hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
|
| 188 |
+
mean_sat = float(np.mean(hsv[:, :, 1]))
|
| 189 |
+
is_greyscale = mean_sat < 25
|
| 190 |
+
|
| 191 |
+
# A simple warm-channel heuristic; intentionally avoid classifying grayscale
|
| 192 |
+
# images as sepia. This is a prompt hint, not a strict image classifier.
|
| 193 |
+
channel_means = rgb.mean(axis=(0, 1))
|
| 194 |
+
red_mean, green_mean, blue_mean = channel_means
|
| 195 |
+
is_sepia = (
|
| 196 |
+
not is_greyscale
|
| 197 |
+
and red_mean > green_mean + 8
|
| 198 |
+
and green_mean > blue_mean + 5
|
| 199 |
+
)
|
| 200 |
+
return is_greyscale, bool(is_sepia)
|
| 201 |
|
| 202 |
|
| 203 |
def calculate_padding(ow, oh, direction, pct):
|
|
|
|
| 210 |
elif direction == "Bottom": bottom = v_ext
|
| 211 |
elif direction == "Horizontal": left = right = h_ext
|
| 212 |
elif direction == "Vertical": top = bottom = v_ext
|
| 213 |
+
elif direction in ["Both", "All Sides"]:
|
| 214 |
+
left = right = h_ext
|
| 215 |
+
top = bottom = v_ext
|
| 216 |
return left, top, right, bottom
|
| 217 |
|
| 218 |
|
|
|
|
| 378 |
raise gr.Error("Please upload an image first.")
|
| 379 |
|
| 380 |
start_time = time.time()
|
| 381 |
+
steps = int(steps)
|
| 382 |
+
if not 10 <= steps <= 50:
|
| 383 |
+
raise gr.Error("Inference steps must be between 10 and 50.")
|
| 384 |
|
| 385 |
image = cap_image(image, 1024)
|
| 386 |
ow, oh = image.size
|
|
|
|
| 411 |
target_w = int(16 * math.ceil(target_w / 16))
|
| 412 |
target_h = int(16 * math.ceil(target_h / 16))
|
| 413 |
|
| 414 |
+
# Bound worst-case FLUX memory use. Raise this only after validating the
|
| 415 |
+
# selected ZeroGPU allocation can handle larger canvases reliably.
|
| 416 |
+
max_outpaint_side = 2048
|
| 417 |
+
max_outpaint_pixels = 4_194_304
|
| 418 |
+
if (
|
| 419 |
+
target_w > max_outpaint_side
|
| 420 |
+
or target_h > max_outpaint_side
|
| 421 |
+
or target_w * target_h > max_outpaint_pixels
|
| 422 |
+
):
|
| 423 |
+
raise gr.Error(
|
| 424 |
+
f"Requested canvas is too large ({target_w}x{target_h}). "
|
| 425 |
+
f"Reduce the extension percentage or use a smaller input image."
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
canvas = Image.new("RGB", (target_w, target_h), (127, 127, 127))
|
| 429 |
canvas.paste(image, (left, top))
|
| 430 |
|
|
|
|
| 484 |
bs = brisque(t, data_range=1.0).item()
|
| 485 |
final_info += f"\n📉 BRISQUE: {bs:.2f} (lower better)"
|
| 486 |
|
| 487 |
+
try:
|
| 488 |
+
clip_model.to("cuda")
|
| 489 |
+
with torch.no_grad():
|
| 490 |
+
inputs = clip_processor(
|
| 491 |
+
text=[prompt[:77]], images=current_img,
|
| 492 |
+
return_tensors="pt", padding=True, truncation=True
|
| 493 |
+
).to("cuda")
|
| 494 |
+
clip_score = clip_model(**inputs).logits_per_image.item()
|
| 495 |
+
final_info += f"\n🤖 CLIP: {clip_score:.4f} (higher better)"
|
| 496 |
+
finally:
|
| 497 |
+
# Keep this large optional metrics model off the GPU even
|
| 498 |
+
# when preprocessing or scoring fails.
|
| 499 |
+
clip_model.to("cpu")
|
| 500 |
except Exception as e:
|
| 501 |
final_info += f"\n⚠️ Metrics error: {e}"
|
| 502 |
|