| """Template-conditioned fingerprint RENDER generator (G_render). |
| |
| Goal (see design discussion): generate a NEW clean fingerprint image that is forensic and |
| hallucination-free WITHOUT relying on raw pixels. The trick: decouple *identity information* |
| (a TEMPLATE extracted once from the observation) from *image rendering* (a generator that only |
| sees the template, never the raw image). Forensic == the template is faithful; no-hallucination == |
| the generator can only render what the template specifies (extracted minutiae + deterministic |
| ridge flow), and blank/low-coverage regions stay blank. |
| |
| TEMPLATE conditioning channels (NO raw pixels here — this is the whole point): |
| 0 minutiae heatmap (Gaussian blobs at extracted minutiae; identity anchors) |
| 1-2 orientation field [cos 2θ, sin 2θ] * coherence (deterministic ridge flow) |
| 3 coherence / coverage (where the template HAS evidence; 0 => stay blank) |
| 4 ridge-frequency map (ridge spacing prior for realistic texture) |
| |
| Self-supervised training: target = a clean rolled print R; cond = template(R). The generator |
| learns to re-render R from R's own template. At inference, feed a template extracted from a LATENT |
| => a clean synthetic print of that identity, with gaps left blank (never invented). |
| """ |
| from __future__ import annotations |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
| from g_render.models.structure.heatmap import compute_orientation_field, render_gaussian_points |
| from g_render.models.frequency.ridge_freq import RidgeFrequencyNormalizer |
|
|
| TEMPLATE_CHANNELS = 5 |
|
|
|
|
| @torch.no_grad() |
| def build_template_conditioning( |
| image: torch.Tensor, |
| minutiae: list | None, |
| size: int, |
| rn: RidgeFrequencyNormalizer | None = None, |
| minu_sigma: float = 4.0, |
| ) -> torch.Tensor: |
| """image: [1,1,H,W] in [0,1] (used ONLY to extract the template, discarded after). |
| minutiae: list of (x,y[,theta]) in image pixels, or None. Returns cond [1, TEMPLATE_CHANNELS, size, size]. |
| NOTE: the returned tensor contains NO raw pixels — only extracted structure.""" |
| rn = rn or RidgeFrequencyNormalizer() |
| dev = image.device |
| H, W = image.shape[-2:] |
| of = compute_orientation_field(image) |
| coh = (of.pow(2).sum(1, keepdim=True) + 1e-12).sqrt().clamp(0, 1) |
| fmap, _ = rn.estimate_frequency_map(image) |
| fmap = F.interpolate(fmap, size=(H, W), mode="bilinear", align_corners=False) |
| if minutiae: |
| pts = [(float(m[0]), float(m[1])) for m in minutiae] |
| minu = render_gaussian_points(pts, out_size=size, src_size=(float(W), float(H)), |
| sigma=minu_sigma, device=dev)[None] |
| else: |
| minu = torch.zeros(1, 1, size, size, device=dev) |
| def rs(t): |
| return F.interpolate(t, size=(size, size), mode="bilinear", align_corners=False) |
| cond = torch.cat([minu, rs(of), rs(coh), rs(fmap.clamp(0, 1))], dim=1) |
| return cond.clamp(-1, 1) |
|
|
|
|
| class RenderGenerator(nn.Module): |
| """template conditioning -> clean fingerprint image, ATTENTION backbone. |
| |
| Wraps the in-repo UformerCondGenerator (window self-attention U-Net): attention enforces the |
| long-range ridge-flow / singular-point coherence that a plain conv U-Net misses, and propagates |
| the sparse minutiae/OF template globally (esp. across gap regions). No raw pixels in the input. |
| """ |
| def __init__(self, in_ch: int = TEMPLATE_CHANNELS, dims=(32, 64, 128, 256), |
| window: int = 8, embed_dim: int = 128, cover_tau: float = 0.15): |
| super().__init__() |
| from g_render.models.generators.uformer_gen import UformerCondGenerator |
| |
| self.body = UformerCondGenerator(in_channels=in_ch, dims=dims, embed_dim=embed_dim, |
| window=window, skip_mode="lowpass") |
| self.head = nn.Conv2d(2, 1, 1) |
| nn.init.zeros_(self.head.bias) |
| self.cover_tau = float(cover_tau) |
|
|
| def forward(self, cond: torch.Tensor) -> torch.Tensor: |
| y = self.body(cond) |
| img = torch.sigmoid(self.head(y)) |
| |
| |
| cover = (cond[:, 3:4] / self.cover_tau).clamp(0, 1) |
| return (img * cover + 1.0 * (1 - cover)).clamp(0, 1) |
|
|