docforensics / model /architecture.py
Suryakarthik-1
Deploy DocForensics to Hugging Face Spaces
70520f0
Raw
History Blame Contribute Delete
3.82 kB
import torch
import torch.nn as nn
import torch.nn.functional as F
def srm_filter(x: torch.Tensor) -> torch.Tensor:
"""Apply fixed high-pass filters to expose noise residuals."""
kernels = torch.tensor([
[[ 0, 0, 0], [ 0, -1, 0], [ 0, 1, 0]],
[[ 0, 0, 0], [ 0, -1, 1], [ 0, 0, 0]],
[[ 0, -1, 0], [-1, 4, -1], [ 0, -1, 0]],
], dtype=torch.float32).unsqueeze(1).to(x.device) # (3,1,3,3)
B, C, H, W = x.shape
residuals = []
for c in range(C):
ch = x[:, c:c+1, :, :]
filtered = F.conv2d(ch, kernels, padding=1)
residuals.append(filtered[:, :1, :, :])
return torch.cat(residuals, dim=1) # (B, 3, H, W)
class ConvBlock(nn.Module):
def __init__(self, in_ch: int, out_ch: int):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
)
def forward(self, x):
return self.block(x)
class TamperNet(nn.Module):
def __init__(self, base_ch: int = 32):
super().__init__()
# Encoder — RGB stream
self.e1_rgb = ConvBlock(3, base_ch)
self.e2_rgb = ConvBlock(base_ch, base_ch * 2)
self.e3_rgb = ConvBlock(base_ch * 2, base_ch * 4)
# Encoder — SRM noise stream
self.e1_srm = ConvBlock(3, base_ch)
self.e2_srm = ConvBlock(base_ch, base_ch * 2)
self.e3_srm = ConvBlock(base_ch * 2, base_ch * 4)
self.pool = nn.MaxPool2d(2)
# Bottleneck (both streams fused)
self.bottleneck = ConvBlock(base_ch * 8, base_ch * 8)
# Decoder
self.up3 = nn.ConvTranspose2d(base_ch * 8, base_ch * 4, 2, stride=2)
self.dec3 = ConvBlock(base_ch * 12, base_ch * 4)
self.up2 = nn.ConvTranspose2d(base_ch * 4, base_ch * 2, 2, stride=2)
self.dec2 = ConvBlock(base_ch * 6, base_ch * 2)
self.up1 = nn.ConvTranspose2d(base_ch * 2, base_ch, 2, stride=2)
self.dec1 = ConvBlock(base_ch * 3, base_ch)
# Output heads
self.mask_head = nn.Conv2d(base_ch, 1, 1)
self.cls_head = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(base_ch * 8, 1),
)
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
srm = srm_filter(x)
# Encode both streams
e1r = self.e1_rgb(x); e1s = self.e1_srm(srm)
e2r = self.e2_rgb(self.pool(e1r)); e2s = self.e2_srm(self.pool(e1s))
e3r = self.e3_rgb(self.pool(e2r)); e3s = self.e3_srm(self.pool(e2s))
# Fuse at bottleneck (pool a third time so decoder's 3 upsamplings cancel)
fused = self.bottleneck(torch.cat([self.pool(e3r), self.pool(e3s)], dim=1))
cls_logit = self.cls_head(fused)
# Decode with skip connections (align spatial size before cat)
up3_out = self.up3(fused)
e3r_ = F.interpolate(e3r, size=up3_out.shape[2:])
e3s_ = F.interpolate(e3s, size=up3_out.shape[2:])
d3 = self.dec3(torch.cat([up3_out, e3r_, e3s_], dim=1))
up2_out = self.up2(d3)
e2r_ = F.interpolate(e2r, size=up2_out.shape[2:])
e2s_ = F.interpolate(e2s, size=up2_out.shape[2:])
d2 = self.dec2(torch.cat([up2_out, e2r_, e2s_], dim=1))
up1_out = self.up1(d2)
e1r_ = F.interpolate(e1r, size=up1_out.shape[2:])
e1s_ = F.interpolate(e1s, size=up1_out.shape[2:])
d1 = self.dec1(torch.cat([up1_out, e1r_, e1s_], dim=1))
mask = torch.sigmoid(self.mask_head(d1))
return mask, cls_logit