| """ |
| sen2sr.model.architecture |
| ========================= |
| Official SEN2SR model loader (ESAOpenSR / tacofoundation). |
| |
| This module replaces the custom RRDB implementation with the actual pretrained |
| SEN2SR model downloaded from HuggingFace via the mlstac library. |
| |
| Model variants |
| -------------- |
| Three variants are available on HuggingFace (tacofoundation/sen2sr): |
| |
| βββββββββββββββββββββββββββββββββ¬βββββββββββββ¬βββββββββββββ¬ββββββββββ |
| β Variant β In bands β In / Out β Scale β |
| βββββββββββββββββββββββββββββββββΌβββββββββββββΌβββββββββββββΌββββββββββ€ |
| β SEN2SRLite/main β default β 10 bands β (10,H,W) β Γ4 β |
| β SEN2SRLite/NonReference_RGBN β 4 bands β (4,H,W) β Γ4 β |
| β SEN2SRLite/Reference_RSWIR_x2 β 10 bands β (10,H,W) β Γ2 β |
| βββββββββββββββββββββββββββββββββ΄βββββββββββββ΄βββββββββββββ΄ββββββββββ |
| |
| Default (SEN2SRLite/main): |
| - Input : (B, 10, H, W) float32 [0, 1], bands in order |
| B02 B03 B04 B05 B06 B07 B08 B8A B11 B12 |
| - Output : (B, 10, HΓ4, WΓ4) float32 |
| - Pixel size : 10 m β 2.5 m |
| - Weights : auto-downloaded from HuggingFace on first call (~100 MB) |
| |
| The download is idempotent: if the model directory already exists it is reused. |
| No HuggingFace account or API token is required (public model). |
| """ |
| from __future__ import annotations |
|
|
| import os |
| from pathlib import Path |
| from typing import Literal |
|
|
| import torch |
| import torch.nn as nn |
|
|
| from s2sr_pipe.utils.logging_utils import get_logger |
|
|
| logger = get_logger("architecture") |
|
|
| |
| _HF_BASE = "https://huggingface.co/tacofoundation/sen2sr/resolve/main" |
|
|
| VARIANT_URLS: dict[str, str] = { |
| "main": f"{_HF_BASE}/SEN2SRLite/main/mlm.json", |
| "rgbn_x4": f"{_HF_BASE}/SEN2SRLite/NonReference_RGBN_x4/mlm.json", |
| "rswir_x2": f"{_HF_BASE}/SEN2SRLite/Reference_RSWIR_x2/mlm.json", |
| } |
|
|
| VARIANT_SCALE: dict[str, int] = { |
| "main": 4, |
| "rgbn_x4": 4, |
| "rswir_x2": 2, |
| } |
|
|
| VARIANT_IN_CHANNELS: dict[str, int] = { |
| "main": 10, |
| "rgbn_x4": 4, |
| "rswir_x2": 10, |
| } |
|
|
| ModelVariant = Literal["main", "rgbn_x4", "rswir_x2"] |
|
|
|
|
| |
|
|
| def build_model( |
| variant: ModelVariant = "main", |
| model_dir: str | Path = "sen2sr_model", |
| device: torch.device | str = "cpu", |
| force_download: bool = False, |
| ) -> nn.Module: |
| """ |
| Download (once) and load the official SEN2SR pretrained model. |
| |
| Parameters |
| ---------- |
| variant : Which SEN2SR variant to use (see module docstring). |
| model_dir : Local directory where the model files are stored. |
| The directory is created if it doesn't exist. |
| Re-using the same directory avoids re-downloading. |
| device : Target device for inference. |
| force_download: Re-download even if model_dir already exists. |
| |
| Returns |
| ------- |
| torch.nn.Module in eval mode, on *device*. |
| |
| Raises |
| ------ |
| ImportError : if `mlstac` is not installed. |
| ValueError : if an unknown variant name is given. |
| RuntimeError : if the download or model compilation fails. |
| |
| Notes |
| ----- |
| * mlstac.download() is idempotent: it skips the download if the target |
| directory already contains a valid model. |
| * compiled_model() returns a standard torch.nn.Module (likely TorchScript |
| or a wrapped nn.Module), fully compatible with torch.no_grad() and AMP. |
| * The model expects patches of exactly 128Γ128 pixels at the LR side. |
| Larger inputs are handled by sen2sr.predict_large() in inference.py. |
| """ |
| try: |
| import mlstac |
| except ImportError as exc: |
| raise ImportError( |
| "mlstac is required to load the official SEN2SR model.\n" |
| "Install it with: pip install mlstac sen2sr" |
| ) from exc |
|
|
| if variant not in VARIANT_URLS: |
| raise ValueError( |
| f"Unknown variant '{variant}'. " |
| f"Choose from: {list(VARIANT_URLS.keys())}" |
| ) |
|
|
| model_dir = Path(model_dir).resolve() |
| mlm_json = model_dir / "mlm.json" |
|
|
| |
| if force_download or not mlm_json.exists(): |
| logger.info( |
| "Downloading SEN2SR weights (%s) from HuggingFace -> %s", |
| variant, |
| model_dir, |
| ) |
| logger.info("URL: %s", VARIANT_URLS[variant]) |
| mlstac.download( |
| file=VARIANT_URLS[variant], |
| output_dir=str(model_dir), |
| ) |
| logger.info("Download complete.") |
| else: |
| logger.info( |
| "Model already present at %s - skipping download. " |
| "Use force_download=True to re-download.", |
| model_dir, |
| ) |
|
|
| |
| logger.info("Loading compiled model from %s ...", model_dir) |
| container = mlstac.load(str(model_dir)) |
| model: nn.Module = container.compiled_model(device=device) |
| model = model.to(device) |
| model.eval() |
|
|
| scale = VARIANT_SCALE[variant] |
| in_ch = VARIANT_IN_CHANNELS[variant] |
| logger.info( |
| "SEN2SR model ready | variant=%s | in_channels=%d | scale=x%d | device=%s", |
| variant, |
| in_ch, |
| scale, |
| device, |
| ) |
| return model |
|
|
|
|
| def get_scale(variant: ModelVariant = "main") -> int: |
| """Return the upscale factor for a given variant.""" |
| return VARIANT_SCALE[variant] |
|
|
|
|
| def get_in_channels(variant: ModelVariant = "main") -> int: |
| """Return the number of input channels expected by a given variant.""" |
| return VARIANT_IN_CHANNELS[variant] |
|
|