Download StarDenoise/model.py from MilanMagik/AstroStarDetect: direct link, hf CLI and curl.
- Browser
- Download file 4.3 kB
-
https://huggingface.co/MilanMagik/AstroStarDetect/resolve/main/StarDenoise/model.py
- Command line
-
hf download hf://MilanMagik/AstroStarDetect/StarDenoise/model.py
-
curl -L -o model.py https://huggingface.co/MilanMagik/AstroStarDetect/resolve/main/StarDenoise/model.py
4.3 kB
| """PyTorch reproduction of the SetiAstro Cosmic Clarity AI 3.6 denoiser. | |
| The architecture follows SetiAstroCosmicClarity_denoise.py from the upstream | |
| MIT-licensed project. It is intentionally kept independent of the original | |
| GUI so it can be embedded in a larger image-processing pipeline. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import Any | |
| import torch | |
| from torch import nn | |
| class ResidualBlock(nn.Module): | |
| def __init__(self, channels: int) -> None: | |
| super().__init__() | |
| self.conv1 = nn.Conv2d(channels, channels, 3, padding=1) | |
| self.relu = nn.ReLU(inplace=False) | |
| self.conv2 = nn.Conv2d(channels, channels, 3, padding=1) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.relu(self.conv2(self.relu(self.conv1(x))) + x) | |
| class CosmicClarityDenoiseCNN(nn.Module): | |
| """Full-resolution residual encoder/decoder used by AI 3.6. | |
| Despite the encoder/decoder names, the network does not spatially | |
| downsample. Dilated convolutions and skip concatenations preserve faint | |
| stars and small-scale structure while increasing the receptive field. | |
| """ | |
| def __init__(self) -> None: | |
| super().__init__() | |
| self.encoder1 = self._encoder(3, 16) | |
| self.encoder2 = self._encoder(16, 32) | |
| self.encoder3 = self._encoder(32, 64, dilation=2) | |
| self.encoder4 = self._encoder(64, 128) | |
| self.encoder5 = self._encoder(128, 256, dilation=2) | |
| self.decoder5 = self._decoder(256 + 128, 128) | |
| self.decoder4 = self._decoder(128 + 64, 64) | |
| self.decoder3 = self._decoder(64 + 32, 32) | |
| self.decoder2 = self._decoder(32 + 16, 16) | |
| self.decoder1 = nn.Sequential(nn.Conv2d(16, 3, 3, padding=1), nn.Sigmoid()) | |
| def _encoder(in_channels: int, out_channels: int, dilation: int = 1) -> nn.Sequential: | |
| return nn.Sequential( | |
| nn.Conv2d( | |
| in_channels, | |
| out_channels, | |
| 3, | |
| padding=dilation, | |
| dilation=dilation, | |
| ), | |
| nn.ReLU(inplace=False), | |
| ResidualBlock(out_channels), | |
| ) | |
| def _decoder(in_channels: int, out_channels: int) -> nn.Sequential: | |
| return nn.Sequential( | |
| nn.Conv2d(in_channels, out_channels, 3, padding=1), | |
| nn.ReLU(inplace=False), | |
| ResidualBlock(out_channels), | |
| ) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| e1 = self.encoder1(x) | |
| e2 = self.encoder2(e1) | |
| e3 = self.encoder3(e2) | |
| e4 = self.encoder4(e3) | |
| e5 = self.encoder5(e4) | |
| d5 = self.decoder5(torch.cat((e5, e4), dim=1)) | |
| d4 = self.decoder4(torch.cat((d5, e3), dim=1)) | |
| d3 = self.decoder3(torch.cat((d4, e2), dim=1)) | |
| d2 = self.decoder2(torch.cat((d3, e1), dim=1)) | |
| return self.decoder1(d2) | |
| def _safe_torch_load(path: Path, device: torch.device) -> Any: | |
| """Load a trusted upstream checkpoint while preferring restricted unpickling.""" | |
| try: | |
| return torch.load(path, map_location=device, weights_only=True) | |
| except TypeError: # torch < 2.0 compatibility | |
| return torch.load(path, map_location=device) | |
| def load_model(weights: str | Path, device: torch.device) -> CosmicClarityDenoiseCNN: | |
| weights_path = Path(weights) | |
| if not weights_path.is_file(): | |
| raise FileNotFoundError( | |
| f"Model weights not found: {weights_path}. Run download_assets.py first." | |
| ) | |
| checkpoint = _safe_torch_load(weights_path, device) | |
| if not isinstance(checkpoint, dict): | |
| raise ValueError("Unexpected checkpoint format: expected a mapping") | |
| state_dict = checkpoint.get("model_state_dict", checkpoint) | |
| model = CosmicClarityDenoiseCNN().to(device) | |
| model.load_state_dict(state_dict, strict=True) | |
| model.eval() | |
| return model | |
| def model_statistics(model: nn.Module) -> dict[str, float | int]: | |
| parameters = sum(parameter.numel() for parameter in model.parameters()) | |
| trainable = sum(parameter.numel() for parameter in model.parameters() if parameter.requires_grad) | |
| return { | |
| "parameters": parameters, | |
| "trainable_parameters": trainable, | |
| "fp32_size_mib": parameters * 4 / 1024**2, | |
| } | |