translator-ink / model.py
davidventura's picture
Upload folder using huggingface_hub
a321ee4 verified
Raw
History Blame Contribute Delete
4.29 kB
"""Mini U-Net for per-strip ink coverage: RGB strip in, soft alpha matte out.
Fully convolutional; height is 48 in training. `levels=N` needs (H, W) divisible by
2**N (so levels<=4 at H=48: 48/16=3). Each extra level enlarges the receptive field
so the interiors of thick/superbold strokes (edge-starved at 2 levels) get filled, not
just outlined; depth buys reach far more cheaply (in FLOPs) than wider channels do.
Layer names are kept fixed per level so older 2-/3-level checkpoints still load.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
def conv_block(cin: int, cout: int) -> nn.Sequential:
return nn.Sequential(
nn.Conv2d(cin, cout, 3, padding=1),
nn.BatchNorm2d(cout),
nn.ReLU(inplace=True),
nn.Conv2d(cout, cout, 3, padding=1),
nn.BatchNorm2d(cout),
nn.ReLU(inplace=True),
)
class InkUNet(nn.Module):
def __init__(self, base: int = 16, levels: int = 2, bold_from: int = 1, detach_bold: bool = False,
bold_head: str = "dilated"):
super().__init__()
self.levels = levels
self.bold_from = bold_from
self.detach_bold = detach_bold
self.bold_head_kind = bold_head
self.enc1 = conv_block(3, base)
self.enc2 = conv_block(base, base * 2)
self.enc3 = conv_block(base * 2, base * 4)
self.pool = nn.MaxPool2d(2)
if levels >= 3:
self.enc4 = conv_block(base * 4, base * 8)
self.up3 = nn.ConvTranspose2d(base * 8, base * 4, 2, stride=2)
self.dec3 = conv_block(base * 8, base * 4)
if levels >= 4:
self.enc5 = conv_block(base * 8, base * 16)
self.up4 = nn.ConvTranspose2d(base * 16, base * 8, 2, stride=2)
self.dec4 = conv_block(base * 16, base * 8)
self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, stride=2)
self.dec2 = conv_block(base * 4, base * 2)
self.up1 = nn.ConvTranspose2d(base * 2, base, 2, stride=2)
self.dec1 = conv_block(base * 2, base)
self.matte_head = nn.Conv2d(base, 1, 1)
# Bold reads from a deeper decoder stage (bold_from: 1=dec1 full-res/base ch,
# 2=dec2 陆-res/base路2, 3=dec3 录-res/base路4). Deeper = more channels and more
# spatial pooling to denoise the per-pixel stroke-width estimate; the bold logit
# is upsampled back to full res (per-region pooling downstream wants no crispness).
bc = base * (2 ** (bold_from - 1))
if bold_head == "1x1":
self.bold_head = nn.Conv2d(bc, 1, 1)
else:
self.bold_head = nn.Sequential(
nn.Conv2d(bc, bc, 3, padding=4, dilation=4),
nn.ReLU(inplace=True),
nn.Conv2d(bc, bc, 3, padding=8, dilation=8),
nn.ReLU(inplace=True),
nn.Conv2d(bc, 1, 1),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
e1 = self.enc1(x)
e2 = self.enc2(self.pool(e1))
e3 = self.enc3(self.pool(e2))
if self.levels >= 3:
e4 = self.enc4(self.pool(e3))
if self.levels >= 4:
e5 = self.enc5(self.pool(e4))
e4 = self.dec4(torch.cat([self.up4(e5), e4], dim=1))
e3 = self.dec3(torch.cat([self.up3(e4), e3], dim=1))
d2 = self.dec2(torch.cat([self.up2(e3), e2], dim=1))
d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=1))
bold_src = {1: d1, 2: d2, 3: e3}[self.bold_from]
# detach_bold: bold gradient does not flow into the shared trunk, so the trunk
# optimises purely for the matte (no negative transfer) and the bold head is a
# readout on the matte-optimal features.
if self.detach_bold:
bold_src = bold_src.detach()
bold = self.bold_head(bold_src)
if self.bold_from > 1:
bold = F.interpolate(bold, size=d1.shape[-2:], mode="bilinear", align_corners=False)
return torch.cat([self.matte_head(d1), bold], dim=1)
def param_count(model: nn.Module) -> int:
return sum(p.numel() for p in model.parameters())
if __name__ == "__main__":
m = InkUNet()
print(f"params: {param_count(m):,}")
y = m(torch.zeros(1, 3, 48, 320))
print("out:", tuple(y.shape))