Spaces:
Running on Zero
Running on Zero
GPU postprocessing pipeline + TF32 + conditional torch.compile
Browse files
app.py
CHANGED
|
@@ -36,6 +36,17 @@ try:
|
|
| 36 |
except ImportError:
|
| 37 |
HAS_SPACES = False
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
# Workaround: Gradio cache_examples bug with None outputs.
|
| 40 |
_original_read_from_flag = gr.components.Component.read_from_flag
|
| 41 |
def _patched_read_from_flag(self, payload):
|
|
@@ -116,7 +127,7 @@ def clean_matte(alpha_np, area_threshold=300, dilation=15, blur_size=5):
|
|
| 116 |
is_3d = alpha_np.ndim == 3
|
| 117 |
if is_3d:
|
| 118 |
alpha_np = alpha_np[:, :, 0]
|
| 119 |
-
mask_8u = (alpha_np > 0.
|
| 120 |
num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask_8u, connectivity=8)
|
| 121 |
valid = np.zeros(num_labels, dtype=bool)
|
| 122 |
valid[1:] = stats[1:, cv2.CC_STAT_AREA] >= area_threshold
|
|
@@ -138,6 +149,60 @@ def create_checkerboard(w, h, checker_size=64, color1=0.15, color2=0.55):
|
|
| 138 |
def premultiply(fg, alpha):
|
| 139 |
return fg * alpha
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
# ---------------------------------------------------------------------------
|
| 142 |
# Fast classical green-screen mask
|
| 143 |
# ---------------------------------------------------------------------------
|
|
@@ -288,8 +353,14 @@ def _load_greenformer(img_size):
|
|
| 288 |
torch.backends.cuda.mem_efficient_sdp_enabled(),
|
| 289 |
torch.backends.cuda.math_sdp_enabled())
|
| 290 |
|
| 291 |
-
|
| 292 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
try:
|
| 294 |
compiled = torch.compile(model)
|
| 295 |
dummy = torch.zeros(1, 4, img_size, img_size, dtype=torch.float16, device="cuda")
|
|
@@ -301,7 +372,7 @@ def _load_greenformer(img_size):
|
|
| 301 |
logger.warning("torch.compile() failed, using eager mode: %s", e)
|
| 302 |
torch.cuda.empty_cache()
|
| 303 |
else:
|
| 304 |
-
logger.info("Skipping torch.compile() (
|
| 305 |
|
| 306 |
logger.info("GreenFormer loaded on CUDA (img_size=%d)", img_size)
|
| 307 |
return model
|
|
@@ -359,61 +430,58 @@ def corridorkey_frame_onnx(session, image_f32, mask_f32, img_size,
|
|
| 359 |
# ---------------------------------------------------------------------------
|
| 360 |
def corridorkey_batch_pytorch(model, images_f32, masks_f32, img_size,
|
| 361 |
despill_strength=0.5, auto_despeckle=True, despeckle_size=400):
|
| 362 |
-
"""PyTorch batched inference
|
| 363 |
-
|
| 364 |
-
Args:
|
| 365 |
-
model: GreenFormer model on CUDA
|
| 366 |
-
images_f32: list of [H, W, 3] float32 numpy arrays (0-1, sRGB)
|
| 367 |
-
masks_f32: list of [H, W] float32 numpy arrays (0-1)
|
| 368 |
-
img_size: model input resolution (1024 or 2048)
|
| 369 |
-
|
| 370 |
-
Returns:
|
| 371 |
-
list of dicts with 'alpha' [H,W,1] and 'fg' [H,W,3]
|
| 372 |
-
"""
|
| 373 |
import torch
|
|
|
|
| 374 |
|
| 375 |
batch_size = len(images_f32)
|
| 376 |
if batch_size == 0:
|
| 377 |
return []
|
| 378 |
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
img_r = cv2.resize(img, (img_size, img_size))
|
| 386 |
-
mask_r = cv2.resize(mask, (img_size, img_size))[:, :, np.newaxis]
|
| 387 |
-
inp = np.concatenate([(img_r - IMAGENET_MEAN) / IMAGENET_STD, mask_r], axis=-1)
|
| 388 |
-
batch_inputs.append(inp.transpose(2, 0, 1)) # [4, H, W]
|
| 389 |
|
| 390 |
-
|
| 391 |
-
|
| 392 |
|
| 393 |
-
# Forward
|
| 394 |
with torch.inference_mode():
|
| 395 |
-
out = model(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
fgs_gpu = out["fg"].float().cpu().numpy() # [B, 3, H, W]
|
| 400 |
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
-
# Postprocess each frame
|
| 405 |
results = []
|
| 406 |
for i in range(batch_size):
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
alpha = alpha[:, :, np.newaxis]
|
| 412 |
-
if auto_despeckle:
|
| 413 |
-
alpha = clean_matte(alpha, area_threshold=despeckle_size, dilation=25, blur_size=5)
|
| 414 |
-
fg = despill(fg, green_limit_mode="average", strength=despill_strength)
|
| 415 |
-
results.append({"alpha": alpha, "fg": fg})
|
| 416 |
-
|
| 417 |
return results
|
| 418 |
|
| 419 |
|
|
|
|
| 36 |
except ImportError:
|
| 37 |
HAS_SPACES = False
|
| 38 |
|
| 39 |
+
# GPU perf: TF32 tensor cores for FP32 postprocessing ops
|
| 40 |
+
try:
|
| 41 |
+
import torch as _torch
|
| 42 |
+
_torch.set_float32_matmul_precision('high')
|
| 43 |
+
del _torch
|
| 44 |
+
except ImportError:
|
| 45 |
+
pass
|
| 46 |
+
# Persist torch.compile inductor cache across ZeroGPU sessions
|
| 47 |
+
_inductor_cache = os.path.join(os.path.expanduser("~"), ".cache", "corridorkey", "inductor")
|
| 48 |
+
os.environ.setdefault("TORCHINDUCTOR_CACHE_DIR", _inductor_cache)
|
| 49 |
+
|
| 50 |
# Workaround: Gradio cache_examples bug with None outputs.
|
| 51 |
_original_read_from_flag = gr.components.Component.read_from_flag
|
| 52 |
def _patched_read_from_flag(self, payload):
|
|
|
|
| 127 |
is_3d = alpha_np.ndim == 3
|
| 128 |
if is_3d:
|
| 129 |
alpha_np = alpha_np[:, :, 0]
|
| 130 |
+
mask_8u = (alpha_np > 0.25).astype(np.uint8) * 255
|
| 131 |
num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask_8u, connectivity=8)
|
| 132 |
valid = np.zeros(num_labels, dtype=bool)
|
| 133 |
valid[1:] = stats[1:, cv2.CC_STAT_AREA] >= area_threshold
|
|
|
|
| 149 |
def premultiply(fg, alpha):
|
| 150 |
return fg * alpha
|
| 151 |
|
| 152 |
+
# ---------------------------------------------------------------------------
|
| 153 |
+
# GPU postprocessing utilities (keep data on device, avoid CPU↔GPU transfers)
|
| 154 |
+
# ---------------------------------------------------------------------------
|
| 155 |
+
def despill_torch(image, strength, screen_channel=1):
|
| 156 |
+
"""GPU despill on [B, 3, H, W] tensor."""
|
| 157 |
+
import torch
|
| 158 |
+
if strength <= 0.0:
|
| 159 |
+
return image
|
| 160 |
+
other_a, other_b = [i for i in (0, 1, 2) if i != screen_channel]
|
| 161 |
+
screen, a, b = image[:, screen_channel], image[:, other_a], image[:, other_b]
|
| 162 |
+
spill = torch.clamp(screen - (a + b) / 2.0, min=0.0)
|
| 163 |
+
out = [None, None, None]
|
| 164 |
+
out[screen_channel] = screen - spill
|
| 165 |
+
out[other_a] = a + spill * 0.5
|
| 166 |
+
out[other_b] = b + spill * 0.5
|
| 167 |
+
despilled = torch.stack(out, dim=1)
|
| 168 |
+
return image * (1.0 - strength) + despilled * strength if strength < 1.0 else despilled
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _connected_components_gpu(mask, min_dist=4, max_iter=100):
|
| 172 |
+
"""GPU flood-fill connected components on [B, 1, H, W] binary mask."""
|
| 173 |
+
import torch
|
| 174 |
+
import torch.nn.functional as F
|
| 175 |
+
bs, _, H, W = mask.shape
|
| 176 |
+
comp = (torch.randperm(bs * W * H, device=mask.device, dtype=torch.float32) + 1.1).view(mask.shape)
|
| 177 |
+
comp[mask != 1] = 0
|
| 178 |
+
k = 2 * min_dist + 1
|
| 179 |
+
for _ in range(max_iter):
|
| 180 |
+
comp[mask == 1] = F.max_pool2d(comp, k, stride=1, padding=min_dist)[mask == 1]
|
| 181 |
+
_, comp = torch.unique(comp, return_inverse=True)
|
| 182 |
+
return comp.view(mask.shape)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def clean_matte_torch(alpha, area_threshold, dilation=25, blur_size=5):
|
| 186 |
+
"""GPU clean matte on [B, 1, H, W] tensor. Removes small disconnected blobs."""
|
| 187 |
+
import torch
|
| 188 |
+
import torch.nn.functional as F
|
| 189 |
+
import torchvision.transforms.functional as TF
|
| 190 |
+
mask = (alpha > 0.25).float()
|
| 191 |
+
comp = _connected_components_gpu(mask, max_iter=max(area_threshold // 20, 5))
|
| 192 |
+
sizes = torch.bincount(comp.flatten())
|
| 193 |
+
big = torch.nonzero(sizes >= area_threshold).squeeze(-1)
|
| 194 |
+
big = big[big > 0]
|
| 195 |
+
cleaned = torch.zeros_like(mask)
|
| 196 |
+
if big.numel() > 0:
|
| 197 |
+
cleaned[torch.isin(comp, big)] = 1.0
|
| 198 |
+
if dilation > 0:
|
| 199 |
+
for _ in range(dilation // 2):
|
| 200 |
+
cleaned = F.max_pool2d(cleaned, 5, stride=1, padding=2)
|
| 201 |
+
if blur_size > 0:
|
| 202 |
+
cleaned = TF.gaussian_blur(cleaned, [blur_size * 2 + 1, blur_size * 2 + 1])
|
| 203 |
+
return alpha * cleaned
|
| 204 |
+
|
| 205 |
+
|
| 206 |
# ---------------------------------------------------------------------------
|
| 207 |
# Fast classical green-screen mask
|
| 208 |
# ---------------------------------------------------------------------------
|
|
|
|
| 353 |
torch.backends.cuda.mem_efficient_sdp_enabled(),
|
| 354 |
torch.backends.cuda.math_sdp_enabled())
|
| 355 |
|
| 356 |
+
should_compile = False
|
| 357 |
+
if sys.platform in ("linux", "win32"):
|
| 358 |
+
if not HAS_SPACES:
|
| 359 |
+
should_compile = True
|
| 360 |
+
elif os.path.isdir(_inductor_cache) and os.listdir(_inductor_cache):
|
| 361 |
+
should_compile = True
|
| 362 |
+
logger.info("Warm ZeroGPU: using cached inductor kernels")
|
| 363 |
+
if should_compile:
|
| 364 |
try:
|
| 365 |
compiled = torch.compile(model)
|
| 366 |
dummy = torch.zeros(1, 4, img_size, img_size, dtype=torch.float16, device="cuda")
|
|
|
|
| 372 |
logger.warning("torch.compile() failed, using eager mode: %s", e)
|
| 373 |
torch.cuda.empty_cache()
|
| 374 |
else:
|
| 375 |
+
logger.info("Skipping torch.compile() (cold start, no cached kernels)")
|
| 376 |
|
| 377 |
logger.info("GreenFormer loaded on CUDA (img_size=%d)", img_size)
|
| 378 |
return model
|
|
|
|
| 430 |
# ---------------------------------------------------------------------------
|
| 431 |
def corridorkey_batch_pytorch(model, images_f32, masks_f32, img_size,
|
| 432 |
despill_strength=0.5, auto_despeckle=True, despeckle_size=400):
|
| 433 |
+
"""PyTorch batched inference with full GPU pipeline (preprocess + inference + postprocess on device)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
import torch
|
| 435 |
+
import torchvision.transforms.functional as TF
|
| 436 |
|
| 437 |
batch_size = len(images_f32)
|
| 438 |
if batch_size == 0:
|
| 439 |
return []
|
| 440 |
|
| 441 |
+
w, h = images_f32[0].shape[1], images_f32[0].shape[0]
|
| 442 |
+
|
| 443 |
+
# --- GPU Preprocessing in FP32 (avoids FP16 precision loss in normalize/resize) ---
|
| 444 |
+
batch_imgs = torch.stack([
|
| 445 |
+
torch.from_numpy(img.transpose(2, 0, 1)) for img in images_f32
|
| 446 |
+
]).cuda()
|
| 447 |
+
batch_masks = torch.stack([
|
| 448 |
+
torch.from_numpy(m if m.ndim == 2 else m[:, :, 0]).unsqueeze(0) for m in masks_f32
|
| 449 |
+
]).cuda()
|
| 450 |
|
| 451 |
+
batch_imgs = TF.resize(batch_imgs, [img_size, img_size], antialias=False)
|
| 452 |
+
batch_masks = TF.resize(batch_masks, [img_size, img_size], antialias=False)
|
| 453 |
+
batch_imgs = TF.normalize(batch_imgs, [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
|
| 455 |
+
inp = torch.cat([batch_imgs, batch_masks], dim=1).half()
|
| 456 |
+
del batch_imgs, batch_masks
|
| 457 |
|
| 458 |
+
# --- Forward Pass ---
|
| 459 |
with torch.inference_mode():
|
| 460 |
+
out = model(inp)
|
| 461 |
+
del inp
|
| 462 |
+
|
| 463 |
+
# --- GPU Postprocessing (despill + clean_matte + resize stay on device) ---
|
| 464 |
+
alpha = out["alpha"].float()
|
| 465 |
+
fg = out["fg"].float()
|
| 466 |
|
| 467 |
+
alpha = TF.resize(alpha, [h, w])
|
| 468 |
+
fg = TF.resize(fg, [h, w])
|
|
|
|
| 469 |
|
| 470 |
+
if auto_despeckle:
|
| 471 |
+
alpha = clean_matte_torch(alpha, area_threshold=int(despeckle_size), dilation=25, blur_size=5)
|
| 472 |
+
fg = despill_torch(fg, despill_strength)
|
| 473 |
+
|
| 474 |
+
# --- Single CPU transfer at the end ---
|
| 475 |
+
alpha_np = alpha.cpu().numpy()
|
| 476 |
+
fg_np = fg.cpu().numpy()
|
| 477 |
+
del alpha, fg
|
| 478 |
|
|
|
|
| 479 |
results = []
|
| 480 |
for i in range(batch_size):
|
| 481 |
+
results.append({
|
| 482 |
+
"alpha": alpha_np[i].transpose(1, 2, 0),
|
| 483 |
+
"fg": fg_np[i].transpose(1, 2, 0),
|
| 484 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
return results
|
| 486 |
|
| 487 |
|