gdubrasquetd's picture
deploy: bundle s2sr_pipe, fix requirements
e854df7 verified
Raw
History Blame Contribute Delete
6.66 kB
"""
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")
# ── HuggingFace URLs ──────────────────────────────────────────────────────────
_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"]
# ── Model loader ──────────────────────────────────────────────────────────────
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"
# ── Download ──────────────────────────────────────────────────────────────
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,
)
# ── Load & compile ────────────────────────────────────────────────────────
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]