Spaces:
Sleeping
Sleeping
Download src/models/refiners.py from farrell236/OCT_Segmentation_a2: direct link, hf CLI and curl.
- Browser
- Download file 8.86 kB
-
https://huggingface.co/spaces/farrell236/OCT_Segmentation_a2/resolve/main/src/models/refiners.py
- Command line
-
hf download hf://spaces/farrell236/OCT_Segmentation_a2/src/models/refiners.py
-
curl -L -o refiners.py https://huggingface.co/spaces/farrell236/OCT_Segmentation_a2/resolve/main/src/models/refiners.py
8.86 kB
| """Local profile refinement and ordinary-coordinate-zero mixture models.""" | |
| import torch | |
| from torch import nn | |
| from torch.nn import functional as F | |
| class ProfileRefiner(nn.Module): | |
| def __init__(self, boundary_keys, radius=16, channels=16): | |
| super().__init__() | |
| if not 1 <= radius <= 64 or channels % 4: | |
| raise ValueError("Unsupported profile-refiner geometry") | |
| self.radius = radius | |
| self.boundary_keys = boundary_keys | |
| self.register_buffer( | |
| "offsets", torch.arange(-radius, radius + 1, dtype=torch.float32) | |
| ) | |
| self.features = nn.Sequential( | |
| nn.Conv1d(3, channels, 5, padding=2), | |
| nn.GroupNorm(4, channels), | |
| nn.SiLU(), | |
| nn.Conv1d(channels, channels, 5, padding=2), | |
| nn.GroupNorm(4, channels), | |
| nn.SiLU(), | |
| nn.Conv1d(channels, channels, 3, padding=1), | |
| nn.GroupNorm(4, channels), | |
| nn.SiLU(), | |
| ) | |
| self.heads = nn.ParameterDict( | |
| { | |
| d: nn.Parameter(torch.zeros(len(keys), channels)) | |
| for d, keys in boundary_keys.items() | |
| } | |
| ) | |
| self.register_buffer("prior", -0.5 * (self.offsets / max(radius / 3, 1.0)) ** 2) | |
| def profiles(self, image, center): | |
| n, one, h, a = image.shape | |
| K = center.shape[1] | |
| D = len(self.offsets) | |
| if one != 1 or center.shape != (n, K, a) or h < 2 or a < 2: | |
| raise ValueError("Profile extraction geometry mismatch") | |
| y = (center[:, :, None, :] + self.offsets[None, None, :, None]) * ( | |
| 2 / (h - 1) | |
| ) - 1 | |
| x = torch.arange(a, device=image.device, dtype=torch.float32)[ | |
| None, None, None, : | |
| ] | |
| parts = [] | |
| for shift in [-1, 0, 1]: | |
| gx = ((x + shift).clamp(0, a - 1) * (2 / (a - 1)) - 1).expand(n, K, D, a) | |
| grid = torch.stack([gx, y], dim=-1).reshape(n, K * D, a, 2) | |
| sampled = ( | |
| F.grid_sample( | |
| image, | |
| grid, | |
| mode="bilinear", | |
| padding_mode="border", | |
| align_corners=True, | |
| )[:, 0] | |
| .reshape(n, K, D, a) | |
| .permute(0, 1, 3, 2) | |
| ) | |
| parts.append(sampled[:, :, :, None, :]) | |
| return torch.cat(parts, dim=3).reshape(n * K * a, 3, D) | |
| def forward(self, image, center, device): | |
| n, K, a = center.shape | |
| if device not in self.boundary_keys or K != len(self.boundary_keys[device]): | |
| raise ValueError("Profile-refiner boundary mismatch") | |
| features = self.features(self.profiles(image, center)) | |
| features = features.reshape(n, K, a, features.shape[1], features.shape[2]) | |
| return ( | |
| torch.einsum("nkacd,kc->nkda", features, self.heads[device]) | |
| + self.prior[None, None, :, None] | |
| ) | |
| def residual(self, logits): | |
| return (logits.float().softmax(dim=2) * self.offsets[None, None, :, None]).sum( | |
| dim=2 | |
| ) | |
| class ProfileCascade(nn.Module): | |
| def __init__(self, config): | |
| super().__init__() | |
| from src.models import make_model | |
| self.coarse = make_model(config["coarse_config"]) | |
| self.refiner = ProfileRefiner( | |
| config["boundary_keys"], config["refine_radius"], config["refine_channels"] | |
| ) | |
| def forward(self, *args, **kwargs): | |
| raise ValueError("A profile cascade requires coarse and fine OCT grids") | |
| def refinement_loss( | |
| logits, target_residual, valid, offsets, sigma=0.8, coordinate_weight=0.1 | |
| ): | |
| """All valid targets contribute; CE projects to the representable interval. | |
| Expected absolute error uses the original, unprojected residual target, so | |
| out-of-window cases are still supervised and never discarded. | |
| """ | |
| from src.models.unet import boundary_loss | |
| radius = (len(offsets) - 1) // 2 | |
| ce = boundary_loss( | |
| logits, (target_residual + radius).clamp(0, 2 * radius), valid, sigma, 0.0 | |
| ) | |
| distance = ( | |
| logits.float().softmax(dim=2) | |
| * (offsets[None, None, :, None] - target_residual[:, :, None, :]).abs() | |
| ).sum(dim=2) | |
| counts = valid.sum(-1) | |
| terms = (distance * valid).sum(-1) / counts.clamp_min(1) | |
| risk = (terms * (counts > 0)).sum() / (counts > 0).sum().clamp_min(1) | |
| return ce + coordinate_weight * risk | |
| def fine_preparation_shape(config): | |
| if config.get("architecture") == "boundary_profile_refine_v1": | |
| return config["fine_height"], config["fine_width"] | |
| if config.get("architecture") == "boundary_ensemble_v1": | |
| shapes = {fine_preparation_shape(c["config"]) for c in config["components"]} | |
| shapes.discard(None) | |
| if len(shapes) > 1: | |
| raise ValueError("Embedded refiners need the same fine preprocessing grid") | |
| return next(iter(shapes)) if shapes else None | |
| return None | |
| class ZeroAtomNet(nn.Module): | |
| def __init__(self, config): | |
| super().__init__() | |
| from src.models import make_model | |
| child = config["coarse_config"] | |
| if child["architecture"] not in [ | |
| "boundary_unet_v1", | |
| "boundary_unet_axial_v1", | |
| "boundary_unet_global_v1", | |
| ]: | |
| raise ValueError("Zero atom needs an image U-Net backbone") | |
| self.coarse = make_model(child) | |
| self.boundary_keys = config["boundary_keys"] | |
| base = child["base"] | |
| hidden = config.get("zero_hidden_channels", 64) | |
| self.zero_heads = nn.ModuleDict( | |
| { | |
| d: nn.Sequential( | |
| nn.Conv1d(26 * base, hidden, 3, padding=1), | |
| nn.GroupNorm(8, hidden), | |
| nn.SiLU(), | |
| nn.Conv1d(hidden, hidden, 3, padding=1), | |
| nn.SiLU(), | |
| nn.Conv1d(hidden, len(keys), 1), | |
| ) | |
| for d, keys in self.boundary_keys.items() | |
| } | |
| ) | |
| for head in self.zero_heads.values(): | |
| nn.init.zeros_(head[-1].weight) | |
| nn.init.constant_(head[-1].bias, -8.0) | |
| def forward(self, x, device): | |
| x, skips = self.coarse.encode(x) | |
| for context in getattr(self.coarse, "axial", []): | |
| x = context(x) | |
| bottom = x | |
| x = self.coarse.decode(x, skips) | |
| logits = self.coarse.heads[device](x) | |
| if hasattr(self.coarse, "edge_heads"): | |
| edge = self.coarse.edge_heads[device]( | |
| torch.cat([bottom.mean(2), bottom.amax(2)], dim=1) | |
| ) | |
| edge = F.interpolate( | |
| edge, size=logits.shape[-1], mode="linear", align_corners=True | |
| ) | |
| logits = torch.cat( | |
| [logits[:, :, :1] + edge[:, :, None, :], logits[:, :, 1:]], dim=2 | |
| ) | |
| global_features = F.interpolate( | |
| torch.cat([bottom.mean(2), bottom.amax(2)], dim=1), | |
| size=x.shape[-1], | |
| mode="linear", | |
| align_corners=True, | |
| ) | |
| features = torch.cat([x.mean(2), x.amax(2), global_features], dim=1) | |
| return logits, self.zero_heads[device](features) | |
| def zero_atom_loss( | |
| logits, | |
| zero_logits, | |
| positive_positions, | |
| zero_fraction, | |
| valid, | |
| sigma=0.8, | |
| coordinate_weight=0.01, | |
| ): | |
| """All valid reference values contribute to a mixture likelihood and risk.""" | |
| h = logits.shape[-2] | |
| width = positive_positions.shape[-1] | |
| if logits.shape[-1] != width: | |
| logits = F.interpolate( | |
| logits.float(), size=(h, width), mode="bilinear", align_corners=True | |
| ) | |
| zero_logits = F.interpolate( | |
| zero_logits.float(), size=width, mode="linear", align_corners=True | |
| ) | |
| grid = torch.arange(h, device=logits.device, dtype=torch.float32)[ | |
| None, None, :, None | |
| ] | |
| zt = zero_fraction.float() | |
| yp = positive_positions.float() | |
| positive_weight = 1 - zt | |
| target = torch.exp(-0.5 * ((grid - yp[:, :, None, :]) / sigma) ** 2) | |
| target /= target.sum(2, keepdim=True).clamp_min(1e-8) | |
| logp = logits.float().log_softmax(2) | |
| probabilities = logp.exp() | |
| ce = -(target * logp).sum(2) | |
| likelihood = ( | |
| F.binary_cross_entropy_with_logits(zero_logits, zt, reduction="none") | |
| + positive_weight * ce | |
| ) | |
| pzero = zero_logits.sigmoid() | |
| positive_distance = (probabilities * (grid - yp[:, :, None, :]).abs()).sum(2) | |
| distance_from_zero = (probabilities * grid).sum(2) | |
| risk = pzero * positive_weight * yp + (1 - pzero) * ( | |
| zt * distance_from_zero + positive_weight * positive_distance | |
| ) | |
| point = likelihood + coordinate_weight * risk | |
| count = valid.sum(-1) | |
| term = (point * valid).sum(-1) / count.clamp_min(1) | |
| return (term * (count > 0)).sum() / (count > 0).sum().clamp_min(1) | |