Spaces:
Running on Zero
Running on Zero
File size: 6,542 Bytes
217ad7a 5433669 217ad7a 5433669 217ad7a 5433669 217ad7a 5433669 217ad7a 5433669 217ad7a 5433669 217ad7a 5433669 217ad7a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | """Optional CUDA for the two heavy numeric steps (colour clustering, per-label smoothing), and the Hugging Face ZeroGPU hook.
Nothing here is required: with no torch, no CUDA device, or HATCHLING_GPU=0, ling_hatch uses scikit-learn and OpenCV as before.
The GPU path gives the same kind of result, not bit-identical labels, so a sheet made on the GPU can differ in a few edge pixels
from the same sheet made on the CPU.
"""
import functools
import math
import os
try:
import spaces # present on Hugging Face Spaces; on ZeroGPU the GPU is only attached inside @spaces.GPU calls
except Exception:
spaces = None
try:
import torch
import torch.nn.functional as F
except Exception:
torch = None
MIN_PIXELS = 20000 # smaller label maps are faster on the CPU than the trip to the device
_active = 0 # how many gpu_task calls are running in this process
_failed = False # CUDA raised once: stay on the CPU from then on
# On a Hugging Face Space with ZeroGPU, torch pretends to have CUDA at all times but only really has it inside a @spaces.GPU call: outside one, the
# first CUDA operation ends in "Low-level CUDA init reached". So on a Space the GPU is used only inside a gpu_task.
ON_SPACE = spaces is not None and bool(os.environ.get('SPACE_ID') or os.environ.get('SPACES_ZERO_GPU'))
def device():
"""'cuda' when CUDA is usable and not switched off, else None. On a Space this is only so inside a gpu_task call."""
if torch is None or _failed or os.environ.get('HATCHLING_GPU', '1').lower() in ('0', 'false', 'no', 'off'):
return None
if ON_SPACE and not _active: return None
try:
return 'cuda' if torch.cuda.is_available() else None
except Exception:
return None
def fail(e):
"""CUDA refused: stay on the CPU from here on."""
global _failed
_failed = True; print('GPU path failed (%s: %s): using the CPU' % (type(e).__name__, str(e)[:120]))
def attempt(on_gpu, on_cpu):
"""Run the GPU version; if CUDA refuses for any reason, remember that and run the CPU version instead."""
if device():
try: return on_gpu()
except Exception as e: fail(e)
return on_cpu()
def gpu_task(duration=120):
"""Decorator for the functions that reach the heavy steps: ZeroGPU's @spaces.GPU where it exists, otherwise nothing. It marks the call as one in
which the GPU may be used."""
def wrap(fn):
@functools.wraps(fn)
def marked(*a, **k):
global _active
_active += 1
try: return fn(*a, **k)
finally: _active -= 1
return spaces.GPU(duration=duration)(marked) if spaces is not None else marked
return wrap
class KMeans:
"""k-means++ with n_init restarts on the GPU; the part of sklearn's KMeans that ling_hatch uses."""
def __init__(self, k, n_init=4, random_state=0, iters=60):
self.k, self.n_init, self.seed, self.iters = k, n_init, int(random_state), iters
self.cluster_centers_ = None
def fit(self, X):
dev = device()
x = torch.as_tensor(X, dtype=torch.float32, device=dev)
best = None
for r in range(self.n_init):
g = torch.Generator(device=dev).manual_seed(self.seed * 1009 + r)
c = self._init(x, g)
for _ in range(self.iters):
a = torch.cdist(x, c).argmin(1)
nc = torch.zeros_like(c).index_add_(0, a, x)
cnt = torch.bincount(a, minlength=self.k).clamp_min(1).unsqueeze(1)
nc = torch.where(torch.bincount(a, minlength=self.k).unsqueeze(1) > 0, nc / cnt, c)
moved = (nc - c).abs().max().item(); c = nc
if moved < 1e-4: break
inertia = torch.cdist(x, c).min(1).values.pow(2).sum().item()
if best is None or inertia < best[0]: best = (inertia, c)
self._c = best[1]; self.cluster_centers_ = best[1].cpu().numpy().astype('float64')
return self
def _init(self, x, g):
n = x.shape[0]
c = x[torch.randint(n, (1,), generator=g, device=x.device)]
for _ in range(1, self.k):
d2 = torch.cdist(x, c).min(1).values.pow(2)
tot = d2.sum()
i = torch.multinomial(d2 / tot, 1, generator=g) if tot > 0 else torch.randint(n, (1,), generator=g, device=x.device)
c = torch.cat([c, x[i]])
return c
def predict(self, X):
x = torch.as_tensor(X, dtype=torch.float32, device=self._c.device)
out = [torch.cdist(x[i:i + (1 << 20)], self._c).argmin(1) for i in range(0, x.shape[0], 1 << 20)]
return torch.cat(out).cpu().numpy().astype('int32')
def _kernel(sigma, dev):
"""OpenCV's GaussianBlur kernel for float32 with ksize 0: round(8 sigma + 1) | 1 taps."""
n = int(round(sigma * 8 + 1)) | 1; r = n // 2
t = torch.arange(-r, r + 1, device=dev, dtype=torch.float32)
k = torch.exp(-t * t / (2 * sigma * sigma)); return k / k.sum(), r
def _blur(x, sigma):
"""x: (1, 1, H, W) float32. Separable Gaussian with reflect-101 borders, as cv2.GaussianBlur."""
k, r = _kernel(sigma, x.device)
r = min(r, x.shape[2] - 1, x.shape[3] - 1)
if r < 1: return x
k = k[len(k) // 2 - r: len(k) // 2 + r + 1]; k = k / k.sum()
x = F.conv2d(F.pad(x, (r, r, 0, 0), mode='reflect'), k.view(1, 1, 1, -1))
return F.conv2d(F.pad(x, (0, 0, r, r), mode='reflect'), k.view(1, 1, -1, 1))
def mode_clean(lbl, sigma, size=None):
"""Each pixel takes the label with the strongest Gaussian-smoothed presence (cv2 loop of _mode_clean / the upscale in posterize).
size=(w, h): the result is bicubically upscaled to that size first, as posterize does for the working mask."""
import numpy as np
dev = device()
t = torch.as_tensor(lbl, device=dev)
best = torch.full(size[::-1] if size else tuple(t.shape), -1.0 if size else 0.0, device=dev)
out = torch.zeros(tuple(best.shape), dtype=torch.uint8, device=dev) if size else t.clone()
for v in torch.unique(t).tolist():
sc = _blur((t == v).float()[None, None], sigma)
if size: sc = F.interpolate(sc, size=(size[1], size[0]), mode='bicubic', align_corners=False)
sc = sc[0, 0]; m = sc > best
out[m] = v; best = torch.where(m, sc, best)
return out.cpu().numpy().astype(np.uint8)
|