s2flow is a research codebase for:
- 4× super‑resolution/enhancement of 4‑band imagery (RGBN)
- Land cover semantic segmentation
It supports both tile-based workflows and large GeoTIFF sliding-window inference.
- s2flow — Flow Matching for Sentinel‑2 4× Super‑Resolution (and Land Cover)
- Table of contents
- Installation
- Repository map (what each folder does)
- Config-first workflow (how the CLI runs jobs)
- Job types (train/eval/inference) and the code they call
- Inference (deep dive; most important section)
- Config reference (every section and key that matters)
- Data formats and expected parquet schemas
- Models and sampling (how SR sampling works)
- Training and evaluation
- SLURM sweeps (batch experiments)
- Common gotchas and troubleshooting
This project requires Python 3.13+ (see pyproject.toml).
From the repository root:
pip install -e .This installs a console command:
s2flow→s2flow.cli:main(defined inpyproject.toml, implemented insrc/s2flow/cli.py)
- If CUDA is available, s2flow will use it by default (device selection is in
src/s2flow/utils.py). - Mixed precision (AMP) is enabled by default in several inference/training paths when
hyperparameters.use_amp: true.
Top-level folders you will interact with most:
configs/: ready-to-run YAML configs for training, evaluation, and inference.- Includes inference presets by solver and step count in
configs/inference_steps/.
- Includes inference presets by solver and step count in
data/: example data roots and parquet split files used by configs.- Your actual data may live elsewhere; configs must be updated accordingly.
runs/andlogs/: output and logging directories created automatically per job run.output/: example output locations used by some configs (not required; you can choose your own).
Core Python package:
src/s2flow/cli.py: CLI entry point; loads config; dispatches job types; sets up run directories; copies config to logs.models.py: constructs SR and LC models.engine/inference.py: “simple” directory inference for SR and LC.sliding_window.py: sliding-window inference for large rasters (SR only, or SR→LC).sampling.py: SR samplers/solvers (Euler/Heun/Midpoint/RK4/DDIM/DDPM + GAN mode).training.py: training loops and trainers.eval.py: SR and LC evaluation.
data/datasets.py: dataset classes + dataloader builders for training jobs.transforms.py: optional spatial augmentation.utils.py: scaling helper used across code.pca.py: PCA projection layer used for LPIPS/DISTS on multispectral images.
metrics.pyandloss.py: metrics and losses.slurm.py: sweep utilities for many-run experiments on SLURM clusters.
Everything runs from a YAML config file. You always call:
s2flow --config path/to/config.yamlWhat the CLI does (see src/s2flow/cli.py):
- Loads YAML into a Python dict.
- Requires
job.nameandjob.type. - Creates:
job.log_dir/job.name/job.out_dir/job.name/
- Copies your config into the log folder as
config.yaml(for provenance). - Adds a
pathssection to the in-memory config:paths.log_pathpaths.out_path
- Dispatches to the job handler based on
job.type. - Optionally writes a
COMPLETEfile to the run output directory.
The allowed job types are enforced in src/s2flow/cli.py.
sr_train- Model:
get_sr_modelinsrc/s2flow/models.py - Trainer chosen by config:
- If config contains
discriminator_model: Real-ESRGAN trainer (RealESRGANTrainer) insrc/s2flow/engine/training.py - Else if
sampling.solver: ddpm:DDPMSRTrainer - Else:
FlowMatchingSRTrainer
- If config contains
- Model:
sr_eval- Evaluation:
sr_model_evaluationinsrc/s2flow/engine/eval.py
- Evaluation:
sr_inference- Directory/tile inference:
simple_sr_model_inferenceinsrc/s2flow/engine/inference.py
- Directory/tile inference:
sr_sliding_window- Large raster inference:
SRSlidingWindowProcessorinsrc/s2flow/engine/sliding_window.py
- Large raster inference:
lc_train- Model:
get_lc_modelinsrc/s2flow/models.py - Trainer:
LandCoverTrainerinsrc/s2flow/engine/training.py
- Model:
lc_eval- Evaluation:
lc_model_evaluationinsrc/s2flow/engine/eval.py
- Evaluation:
lc_inference- Directory/tile inference:
simple_lc_model_inferenceinsrc/s2flow/engine/inference.py
- Directory/tile inference:
lc_sliding_window- Large raster SR→LC inference:
LCSlidingWindowProcessorinsrc/s2flow/engine/sliding_window.py
- Large raster SR→LC inference:
There are two fundamentally different inference styles:
- Simple (directory-based) inference
- Input: many GeoTIFF tiles in a folder
- Output: GeoTIFF tiles in an output folder (mirrors subfolders)
- Code:
src/s2flow/engine/inference.py
- Sliding-window inference
- Input: one large GeoTIFF (e.g., a full S2 composite tile)
- Output: one large GeoTIFF (SR output or LC predictions)
- Code:
src/s2flow/engine/sliding_window.py
Important SR concept: “SR inference” in this repo is sampling-based.
For Flow Matching / DDPM / DDIM modes, SR is not a single forward pass. The sampler iteratively refines a latent/state for sampling.num_steps steps using one of the solvers in src/s2flow/engine/sampling.py.
Use this if you have a directory of tiles you want to run SR on in batches.
Code path
- CLI: SR inference dispatch in
src/s2flow/cli.py - SR directory inference:
simple_sr_model_inferenceinsrc/s2flow/engine/inference.py - Sampler:
get_samplerinsrc/s2flow/engine/sampling.py
What it assumes about the input tiles
- Pixel values are in 0–10000 (S2-like scaling).
- The images are already at the “model-ready” spatial resolution.
- It does not automatically upsample the input; output tiles will have the same height/width as input tiles.
What it does (exactly)
- Finds input files:
data.data_root_path.glob(data.glob_pattern) - Reads each GeoTIFF:
rasterio→ NumPy array shaped[C, H, W] - Scales
0..10000 → -1..1usingscale(...)fromsrc/s2flow/data/utils.py - Batches tiles into a tensor shaped
[B, C, H, W]and moves to device - Calls
sampler.sample(input_batch) - Scales outputs
-1..1 → 0..10000 - Writes outputs to
data.out_root_path, preserving subpaths relative todata.data_root_path
Minimal SR inference config (template)
job:
name: my_sr_inference
type: sr_inference
log_dir: ./logs
out_dir: ./runs
data:
data_root_path: /path/to/input_tiles
glob_pattern: "**/*.tif"
out_root_path: /path/to/output_tiles
sr_model:
model_type: unet
pretrained_weights: /path/to/sr_weights.pt
sample_size: 256
in_channels: 8
out_channels: 4
hyperparameters:
micro_batch_size: 8
use_amp: true
sampling:
solver: euler
num_steps: 20
show_pbar: true
fixed_noise: falseReady-made examples
- Euler presets:
configs/inference_steps/euler/ - DDPM presets:
configs/inference_steps/ddpm/ - DDIM presets:
configs/inference_steps/ddim/ - GAN mode (RRDBNet):
configs/s2flow-srinf-real_esrgan.yaml
Run it:
s2flow --config configs/inference_steps/euler/s2flow-srinf_10.yamlUse this when you have one large raster (often too big for GPU memory) and want one large SR output GeoTIFF.
Code path
- CLI:
sr_sliding_window_inferenceinsrc/s2flow/cli.py - Processor:
SRSlidingWindowProcessorinsrc/s2flow/engine/sliding_window.py
What it assumes about the input GeoTIFF
- Values are in 0–10000.
- Band order: the processor’s
process_filedefaults tocorrect_band_order=Trueand reorders bands from BGRN → RGBN.- If your file is already RGBN, this reorder would be wrong (currently not exposed as a config switch; it is a function parameter).
How sliding window SR works (step-by-step, as implemented)
- Read entire raster into memory (
rasterio.open(...).read()). - Optional band reorder BGRN → RGBN.
- Pad the raster by reflection so tile coverage is complete at borders.
- Enumerate overlapping tiles:
- tile size:
inference.tile_size - stride:
inference.stride
- tile size:
- For each tile batch:
- Optional TTA:
- random horizontal flip
- random vertical flip
- random 0/90/180/270 rotation
- Upsample each tile by
inference.upscale_factorusing bicubic interpolation.- Note: the helper is named
_upsample_lanczos, but it uses PyTorch bicubic interpolation (seesrc/s2flow/engine/sliding_window.py).
- Note: the helper is named
- Scale to
-1..1. - Run sampler for
sampling.num_stepssteps withsampling.solver. - Reverse TTA transforms (if applied).
- Scale back to
0..10000. - Multiply each tile by a Gaussian weight mask and accumulate into a global output canvas.
- Optional TTA:
- Divide accumulated output by accumulated weights (this blends overlaps smoothly).
- Remove padding.
- Write output GeoTIFF:
- The geotransform pixel size is divided by
upscale_factor. - Output dtype is written as
int16(values clipped to0..10000).
- The geotransform pixel size is divided by
How Gaussian blending is configured
inference.gaussian_sigmais interpreted at output resolution.- If you do not provide it, the processor defaults to:
tile_size * upscale_factor / 2.
Important performance knobs
inference.batch_size: number of tiles processed at onceinference.tile_sizeandinference.stride: smaller tiles reduce memory; more overlap increases computesampling.num_steps: more steps = better quality (often) but slowerhyperparameters.use_amp: mixed precision can speed up inference on modern GPUs
Example config: configs/s2flow-sr_sliding_window.yaml
Run it:
s2flow --config configs/s2flow-sr_sliding_window.yamlTwo important gotchas for sliding window SR
- Tiles that are entirely zero (or NaN) are skipped by the tile generator.
- If your data contains legitimate large zero regions, you can unintentionally skip valid tiles.
- If you set
sampling.fixed_noise: true:- the sampler creates a fixed
x_0tensor hard-coded to shape(4, 256, 256)(seesrc/s2flow/engine/sampling.py). - This matches the common case: 4 channels, and output tile size =
tile_size * upscale_factor = 64 * 4 = 256. - If you change
tile_sizeorupscale_factor, fixed-noise may no longer match your tile shape.
- the sampler creates a fixed
Use this when you already have tile-sized images and want to create predicted land cover maps.
Code path
- CLI:
lc_model_inferenceinsrc/s2flow/cli.py - Directory inference:
simple_lc_model_inferenceinsrc/s2flow/engine/inference.py
What it expects
- Input tiles as GeoTIFFs, shaped
[C, H, W] - The value range depends on
data.source_data:s2ors2sr: input scaled from0..10000 → 0..1naip: input scaled from0..255 → 0..1
What it outputs
- A 1-band
uint8prediction map (class IDs). - Classes are written as 1-indexed (
prediction + 1). Internally, models typically use 0-indexed classes. - Optional color palette:
inference.save_colormap: trueinference.colormap: {class_id: [R, G, B, A], ...}
Minimal LC inference config (template)
job:
name: my_lc_inference
type: lc_inference
log_dir: ./logs
out_dir: ./runs
data:
data_root_path: /path/to/input_tiles
glob_pattern: "**/*.tif"
out_root_path: /path/to/output_preds
source_data: s2sr
lc_model:
model_type: segformer
in_channels: 4
num_classes: 7
pretrained_weights: /path/to/lc_weights.pt
hyperparameters:
micro_batch_size: 8
use_amp: false
inference:
save_colormap: true
colormap:
0: [0, 0, 0, 0]
1: [81, 108, 151, 255]Run it:
s2flow --config configs/s2flow-simple_lc_inf.yamlImportant known issue
In simple_lc_model_inference the code uses get_hp_dtype() but does not import it in inference.py. If AMP is enabled for LC inference, this can raise a NameError.
Workaround without code changes: set hyperparameters.use_amp: false for lc_inference.
Use this when you want land cover predictions for a large Sentinel‑2 raster, but your LC model expects SR-quality inputs. The pipeline is:
S2 raster → tiling → upsample (bicubic) → SR sampling → scale to 0..1 → LC model → stitch probabilities → write predictions
Code path
- CLI:
lc_sliding_window_inferenceinsrc/s2flow/cli.py - Processor:
LCSlidingWindowProcessorinsrc/s2flow/engine/sliding_window.py
What it outputs
- Primary output: prediction GeoTIFF (1 band,
uint8, 1-indexed classes) - Optional output: per-class probability GeoTIFF (
Cbands,float32) wheninference.save_probs: true
Example config: configs/s2flow-lc_sliding_window.yaml
Run it:
s2flow --config configs/s2flow-lc_sliding_window.yamlNote about the output-probabilities filename
The CLI currently constructs a probability path in a slightly confusing way (it uses a _preds.tif suffix). If you care about naming, set inference.save_probs: false or rename afterward.
This is the “what the code actually reads” map. If a key is not listed here, it is either unused or only used in a niche path.
Used by the CLI in src/s2flow/cli.py.
job.name(required): name of the run; also the folder name under logs and runs.job.type(required): selects which pipeline executes.job.log_dir(optional, defaultlogs): base log directory.job.out_dir(optional, defaultruns): base output directory.job.load_checkpoint(training jobs only): resume from checkpoint.job.checkpoint_filename(training jobs only): checkpoint file name, defaultcheckpoint.pt.job.cudnn_deterministic(optional): passed intotorch.backends.cudnn.deterministic.job.add_completed_file(optional, defaulttrue): if true, creates aCOMPLETEfile at the end of the run.
Inserted by the CLI before most jobs run:
paths.log_path: full per-run log directorypaths.out_path: full per-run output directory
Many modules assume these exist (especially training and eval).
Varies by job.
SR train/eval:
data.samples_par_path: parquet file describing splits and pathsdata.data_dir_path: root folder for paths in parquetdata.augmentations: if set tospatial, enables shared flip/rotate transforms during training (seesrc/s2flow/data/transforms.py)data.num_workers,data.pin_memory: dataloader settings
SR inference (directory tiles):
data.data_root_pathdata.glob_patterndata.out_root_path
Sliding window (SR or SR→LC):
data.input_pathdata.output_path(optional; if not provided, defaults underpaths.out_path)
LC train/eval:
data.samples_par_pathdata.data_dir_pathdata.source_data:s2,naip, ors2sr(controls which input column is used)data.fold: CV fold ID
LC inference (directory tiles):
data.data_root_pathdata.glob_patterndata.out_root_pathdata.source_data: affects scaling (0..10000vs0..255)
Built by get_sr_model in src/s2flow/models.py.
sr_model.model_type:unet: uses DiffusersUNet2DModelwrapped byUNetTensorWrapperrrdbnet: RRDBNet generator used in GAN mode
sr_model.pretrained_weights: required forsr_eval,sr_inference,sr_sliding_window,lc_sliding_windowsr_model.compile_model(optional): attemptstorch.compile()except when hostname is exactlygcer-a100
Architecture fields for UNet (common):
sample_size,in_channels,out_channels,block_out_channels,down_block_types,up_block_types,layers_per_block,norm_num_groups,time_embedding_type
Why in_channels is often 8 for UNet:
- In the samplers, SR uses
model_input = concat([x, cond], dim=1), where both are 4-channel tensors (so 8 total).
Built by get_lc_model in src/s2flow/models.py via segmentation-models-pytorch.
lc_model.model_type:unet,deeplabv3plus, orsegformerlc_model.encoder_name(model-dependent)lc_model.encoder_weights(oftenimagenet)lc_model.in_channels(usually 4)lc_model.num_classeslc_model.pretrained_weights: required forlc_eval,lc_inference,lc_sliding_window
Implemented in src/s2flow/engine/sampling.py.
sampling.gan: trueusesGANSampler(RRDBNet path)sampling.solver:euler,heun,midpoint,rk4,ddim,ddpmsampling.num_steps: number of steps / scheduler timestepssampling.fixed_noise: creates deterministic initial noise for ODE-like samplers (see fixed shape note above)sampling.show_pbar: progress bar inside sampling loops
Used by src/s2flow/engine/sliding_window.py.
inference.tile_size: tile size at input resolution (pixels)inference.stride: tile stride at input resolutioninference.batch_size: tiles per batchinference.upscale_factor: usually 4inference.gaussian_sigma: Gaussian blending sigma at output resolutioninference.tta: enable random flip/rotation test-time augmentationinference.tta_passes: number of passes; outputs are averagedinference.enable_pbar: show progress bar
LC-specific:
inference.save_probs: save probability raster (float32,Cbands) in addition to predictionsinference.colormap: palette for predictionsinference.save_colormap: whether to write colormap (directory LC inference uses this too)
Commonly used for inference:
hyperparameters.micro_batch_size: directory inference batch sizehyperparameters.use_amp: enables AMP contexts in samplers and sliding-window processors
The training code in this repo does not read shapefiles directly.
- In configs,
data.samples_par_pathpoints to a GeoParquet file (often namedsamples.par). - In code, datasets load it with
geopandas.read_parquet(...)(seesrc/s2flow/data/datasets.py).
If your labels start as a shapefile (polygons/lines/points), the expected pipeline is:
- Rasterize your shapefile into label rasters (GeoTIFFs) that align with the imagery tiles you will train on.
- Write a GeoParquet/Parquet “samples table” that lists the (relative) paths to imagery + label rasters and includes split/fold metadata.
For land-cover training (lc_train/lc_eval), each sample’s lc_path must point to a single-band label GeoTIFF where:
- Pixel values are 1-indexed class IDs (e.g.,
1..num_classes).- The loader converts them to 0-indexed internally via
target - 1.
- The loader converts them to 0-indexed internally via
- The raster grid (CRS, resolution, width/height, transform) should match the corresponding imagery tile.
- The code assumes per-pixel correspondence; it does not reproject/resample labels at load time.
Your GeoParquet may optionally include a geometry column (tile footprint polygons) and CRS metadata, but the loader only uses the path + split/fold columns.
SR GeoParquet (used by sr_train / sr_eval)
Required columns:
split: must includetrainandvalinput_path: conditioning image path (relative todata.data_dir_path)target_path: target image path (relative todata.data_dir_path)
Optional (recommended):
id: stable identifier used during evaluation outputs
LC GeoParquet (used by lc_train / lc_eval)
Required columns:
split: should includetrain/valrows, and may includetestrows for evaluation- Note: the LC dataloader excludes
split == "test"when creating train/val splits.
- Note: the LC dataloader excludes
fold: integer fold ID used for cross-validation (config usesdata.fold)lc_path: label raster path (relative todata.data_dir_path)- One imagery path column matching
data.source_data:s2_path(ifdata.source_data: s2)naip_path(ifdata.source_data: naip)s2sr_path(ifdata.source_data: s2sr)
Practical note: these path columns can be absolute, but the code always does data_dir_path / value, so relative paths rooted under data.data_dir_path are the simplest and most portable.
rasterioreads as[C, H, W].- SR paths assume S2-like scaling (
0..10000) and normalize to-1..1for sampling. - LC paths normalize to
0..1for segmentation models. - Scaling helper:
scale(...)insrc/s2flow/data/utils.py.
Dataset: S2NAIPDataset in src/s2flow/data/datasets.py.
Expected columns:
split:trainorvalinput_path: path to conditioning image (relative todata.data_dir_path)target_path: path to target image (relative todata.data_dir_path)id: used as an identifier during evaluation outputs/metrics (recommended)
Dataset classes in src/s2flow/data/datasets.py.
Expected columns:
split: includestestfor evaluationfold: integer (for cross-validation splitting)lc_path: label raster path (labels in files are expected to be 1-indexed; training shifts them to 0-indexed)- One of:
s2_path(ifdata.source_data: s2)naip_path(ifdata.source_data: naip)s2sr_path(ifdata.source_data: s2sr)
SR has multiple backends controlled by sampling.
Implemented in src/s2flow/engine/sampling.py.
Core idea:
- Initialize
xas noise. - For each time
tin0..1:- concatenate
xwith conditioning imagecond - predict a “velocity”
v = model([x, cond], t) - update
xaccording to the solver rule
- concatenate
Also in src/s2flow/engine/sampling.py, using Diffusers schedulers:
- Predict noise given
[x, cond]and timestept - Apply
scheduler.step(...)to updatex
If sampling.gan: true, sampling uses GANSampler:
- It optionally downsamples the conditioning image internally, then calls RRDBNet once.
- This is a feed-forward path (not iterative).
Training loop classes are in src/s2flow/engine/training.py.
Important points for novices:
- Gradient accumulation is used when
hyperparameters.batch_sizeis larger thanhyperparameters.micro_batch_size. - Checkpoints and models are written under
paths.out_path. - Metrics are written to CSV under
paths.log_path.
SR evaluation: src/s2flow/engine/eval.py
- Runs the sampler on validation samples and computes:
- L1, PSNR, SSIM, MS-SSIM
- LPIPS and DISTS computed after a PCA projection of 4-band → 3-band
LC evaluation: src/s2flow/engine/eval.py
- Runs on the
testsplit - Computes a confusion matrix and classification metrics (accuracy, precision, recall, F1, mIoU)
Because LPIPS/DISTS are RGB-based, the repo uses a PCA projection:
- PCA layer and joblib loading:
src/s2flow/data/pca.py - Packaged asset:
pca.joblib(included as package data) - Metric wrappers:
src/s2flow/metrics.py
If the PCA joblib is missing, code attempts to fit it from the SR training dataloader (which can take a long time).
Sweep helpers live in src/s2flow/slurm.py. They are not a CLI job type by default; they are utilities you import to create sweep drivers.
Key classes:
SlurmConfig: SLURM settings (partition, gres, time, max jobs, modules, env activation).BaseJob: writes per-job configs and ansbatchscript that runss2flow --config <config.yaml>.BaseSweep: manages job generation + queue throttling + Ctrl-C handling + optional cancellation.
Configs like configs/s2flow-sampling_sweep.yaml are typically used as “base configs” for sweep drivers.
That is expected for sr_inference. It assumes your tiles are already at the desired resolution.
Use sr_sliding_window for true 4× output size from a coarse GeoTIFF.
Sliding-window routines reorder BGRN → RGBN by default. If your GeoTIFF is already RGBN, results will look wrong.
Workaround: set hyperparameters.use_amp: false for lc_inference.
The CLI raises FileExistsError if data.output_path already exists for sliding-window jobs.
- Increasing
sampling.num_stepsincreases runtime roughly linearly. - Larger
tile_sizeincreases memory usage. - Smaller
strideincreases overlap, improving seam quality but increasing compute.