Download corridorkey_refiner.py from mantelmoritz/CorridorKey-Realtime-EdgeRefiner: direct link, hf CLI and curl.
- Browser
- Download file 6.93 kB
-
https://huggingface.co/mantelmoritz/CorridorKey-Realtime-EdgeRefiner/resolve/main/corridorkey_refiner.py
- Command line
-
hf download hf://mantelmoritz/CorridorKey-Realtime-EdgeRefiner/corridorkey_refiner.py
-
curl -L -o corridorkey_refiner.py https://huggingface.co/mantelmoritz/CorridorKey-Realtime-EdgeRefiner/resolve/main/corridorkey_refiner.py
6.93 kB
| """CorridorKey Real-Time Edge Refiner: standalone reference implementation. | |
| Licensed under the Corridor Key Licence (CC BY-NC-SA 4.0 + additional terms), see LICENSE. | |
| Built for use with CorridorKey 2.0 by Corridor Digital LLC. | |
| The refiner restores native-resolution edge detail (hair strands, motion blur, soft edges) on top of a | |
| CorridorKey 2.0 matte that was computed at a reduced resolution. Inputs per native pixel: | |
| I native RGB frame (sRGB, [0, 1]) | |
| B smooth background estimate (sRGB), from pixels CorridorKey calls background | |
| F CorridorKey foreground transferred onto the native frame (I + upsampled (F_low - I_low)) | |
| a_up CorridorKey alpha, bilinear upsampled | |
| a_gf alpha of a colour guided filter (He et al.) driven by CorridorKey's low-res alpha | |
| It predicts residuals for alpha (on top of a_gf) and foreground (on top of F). The result is only used | |
| inside an uncertainty band around CorridorKey's edges; elsewhere the guided-filter result is kept. | |
| `refine()` below processes a whole frame at once (simple, a few ms on a modern GPU at 1080p). The real-time | |
| engine evaluates exactly the same maths on 32x32 band tiles only. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| IN_CHANNELS = 11 | |
| class EdgeRefiner(nn.Module): | |
| def __init__(self, width: int = 24, dilations=(1, 2, 4, 1)): | |
| super().__init__() | |
| self.dilations = tuple(int(d) for d in dilations) | |
| self.inp = nn.Conv2d(IN_CHANNELS, width, 3, padding=1) | |
| self.blocks = nn.ModuleList(nn.Conv2d(width, width, 3, padding=d, dilation=d) for d in self.dilations) | |
| self.out = nn.Conv2d(width, 4, 1) | |
| def receptive_radius(self) -> int: | |
| return 1 + sum(self.dilations) | |
| def forward(self, x: torch.Tensor): | |
| """x: (B, 11, H, W) = [I(3), B(3), F(3), a_up(1), a_gf(1)]. Returns (alpha, fg_srgb) in [0, 1].""" | |
| h = F.gelu(self.inp(x)) | |
| for blk in self.blocks: | |
| h = h + F.gelu(blk(h)) | |
| d = self.out(h) | |
| alpha = (x[:, 10:11] + d[:, 0:1]).clamp(0.0, 1.0) | |
| fg = (x[:, 6:9] + d[:, 1:4]).clamp(0.0, 1.0) | |
| return alpha, fg | |
| def load_refiner(path: str, device="cuda", dtype=torch.float16) -> EdgeRefiner: | |
| """Load a .safetensors checkpoint of this repository.""" | |
| from safetensors import safe_open | |
| from safetensors.torch import load_file | |
| with safe_open(path, framework="pt") as f: | |
| cfg = json.loads(f.metadata()["config"]) | |
| net = EdgeRefiner(width=cfg["width"], dilations=tuple(cfg["dilations"])) | |
| net.load_state_dict(load_file(path)) | |
| return net.to(device=device, dtype=dtype).eval() | |
| # --------------------------------------------------------------------------------------------------------- | |
| # Input construction (identical to the real-time engine) | |
| # --------------------------------------------------------------------------------------------------------- | |
| def srgb_to_linear(x): | |
| return torch.where(x <= 0.04045, x / 12.92, ((x + 0.055) / 1.055) ** 2.4) | |
| def linear_to_srgb(x): | |
| x = x.clamp(min=0.0) | |
| return torch.where(x <= 0.0031308, x * 12.92, 1.055 * x.clamp(min=0.0031308) ** (1 / 2.4) - 0.055) | |
| def guided_coef(rgb_low, alpha_low, radius: int = 2, eps: float = 5e-4): | |
| """Colour guided filter (He et al.) at low resolution: smoothed (a_r, a_g, a_b, b) coefficients.""" | |
| k = 2 * radius + 1 | |
| box = lambda z: F.avg_pool2d(z, k, stride=1, padding=radius, count_include_pad=False) | |
| I, p = rgb_low, alpha_low | |
| _, _, h, w = p.shape | |
| mean_I, mean_p = box(I), box(p) | |
| cov_Ip = box(I * p) - mean_I * mean_p | |
| mean_II = box((I.unsqueeze(1) * I.unsqueeze(2)).reshape(1, 9, h, w)).reshape(1, 3, 3, h, w) | |
| s = mean_II - mean_I.unsqueeze(1) * mean_I.unsqueeze(2) | |
| s = s + eps * torch.eye(3, device=s.device, dtype=s.dtype).view(1, 3, 3, 1, 1) | |
| a = torch.linalg.solve(s.permute(0, 3, 4, 1, 2), cov_Ip.permute(0, 2, 3, 1).unsqueeze(-1)) | |
| a = a.squeeze(-1).permute(0, 3, 1, 2) | |
| b = mean_p - (a * mean_I).sum(1, keepdim=True) | |
| return torch.cat([box(a), box(b)], 1) | |
| def push_pull(num, den): | |
| """Hole-filling interpolation: weighted average num/den, holes (den ~ 0) filled from coarser levels.""" | |
| levels = [(num, den)] | |
| while min(levels[-1][0].shape[-2:]) > 4: | |
| n, d = levels[-1] | |
| levels.append((F.avg_pool2d(n, 2, ceil_mode=True), F.avg_pool2d(d, 2, ceil_mode=True))) | |
| n, d = levels[-1] | |
| est = (n.sum((-2, -1), keepdim=True) / d.sum((-2, -1), keepdim=True).clamp(min=1e-6)).expand_as(n) | |
| for n, d in reversed(levels): | |
| est = F.interpolate(est, size=n.shape[-2:], mode="bilinear", align_corners=False) | |
| d = d.clamp(0.0, 1.0) | |
| est = (n + (1.0 - d) * est) / (d + (1.0 - d)).clamp(min=1e-6) | |
| return est | |
| def background_low(rgb_low, alpha_low): | |
| w = (1.0 - alpha_low).clamp(0.0, 1.0) ** 4 | |
| return linear_to_srgb(push_pull(srgb_to_linear(rgb_low.clamp(0.0, 1.0)) * w, w).clamp(0.0, 1.0)) | |
| def refiner_band(alpha_low, gain: float = 10.0, lo: float = 0.004): | |
| """Where the refiner may change the guided result: uncertain alpha, dilated, weighted by nearby | |
| foreground (gain > 0 keeps it out of noisy/dark screen areas) and feathered.""" | |
| unc = ((alpha_low > lo) & (alpha_low < 0.996)).float() | |
| unc = F.max_pool2d(unc, 5, stride=1, padding=2) | |
| if gain > 0: | |
| near_fg = (F.max_pool2d(alpha_low, 5, stride=1, padding=2) * gain).clamp(0.0, 1.0) | |
| unc = F.avg_pool2d(unc * near_fg, 3, stride=1, padding=1, count_include_pad=False) | |
| return unc | |
| def refine(net: EdgeRefiner, rgb, rgb_low, alpha_low, fg_low, band_gain: float = 10.0): | |
| """ | |
| rgb: (1, 3, H, W) native frame, sRGB in [0, 1] | |
| rgb_low: (1, 3, h, w) the frame as CorridorKey saw it (downscaled), sRGB | |
| alpha_low: (1, 1, h', w') CorridorKey alpha (any low resolution, e.g. 256x256 from a 512x512 pass) | |
| fg_low: (1, 3, h', w') CorridorKey straight foreground, sRGB | |
| Returns (alpha, fg) at native resolution, float32. Apply despill / alpha contrast afterwards as needed. | |
| """ | |
| rgb, alpha_low, fg_low = rgb.float(), alpha_low.float(), fg_low.float() | |
| rgb_low = F.interpolate(rgb_low.float(), size=alpha_low.shape[-2:], mode="bilinear", align_corners=False) | |
| H, W = rgb.shape[-2:] | |
| up = lambda z: F.interpolate(z, size=(H, W), mode="bilinear", align_corners=False) | |
| coef = up(guided_coef(rgb_low, alpha_low)) | |
| a_gf = ((coef[:, :3] * rgb).sum(1, keepdim=True) + coef[:, 3:4]).clamp(0.0, 1.0) | |
| fg_in = (rgb + up(fg_low - rgb_low)).clamp(0.0, 1.0) | |
| x = torch.cat([rgb, up(background_low(rgb_low, alpha_low)), fg_in, up(alpha_low), a_gf], 1) | |
| p = next(net.parameters()) | |
| a_r, f_r = net(x.to(p.dtype)) | |
| band = up(refiner_band(alpha_low, band_gain)) | |
| alpha = band * a_r.float() + (1.0 - band) * a_gf | |
| fg = band * f_r.float() + (1.0 - band) * fg_in | |
| return alpha, fg | |